UI: add a new MainActivity; update manifest

This commit is contained in:
Han Yin 2025-04-11 14:40:11 -07:00
parent 7e5c80cee9
commit ca2b7772ce
3 changed files with 445 additions and 0 deletions

View File

@ -20,6 +20,17 @@
android:exported="true"
android:theme="@style/Theme.LlamaAndroid">
<intent-filter>
<!-- <action android:name="android.intent.action.MAIN" />-->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".revamp.MainActivity"
android:exported="true"
android:theme="@style/Theme.LlamaAndroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@ -0,0 +1,229 @@
package com.example.llama.revamp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.rememberDrawerState
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.llama.revamp.engine.InferenceEngine
import com.example.llama.revamp.navigation.AppDestinations
import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.UnloadModelConfirmationDialog
import com.example.llama.revamp.ui.screens.BenchmarkScreen
import com.example.llama.revamp.ui.screens.ConversationScreen
import com.example.llama.revamp.ui.screens.ModelSelectionScreen
import com.example.llama.revamp.ui.screens.ModeSelectionScreen
import com.example.llama.revamp.ui.screens.SettingsScreen
import com.example.llama.revamp.ui.screens.SettingsTab
import com.example.llama.revamp.ui.theme.LlamaTheme
import com.example.llama.revamp.util.ViewModelFactoryProvider
import com.example.llama.revamp.viewmodel.MainViewModel
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LlamaTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppContent()
}
}
}
}
}
@Composable
fun AppContent() {
val navController = rememberNavController()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
// Create inference engine
val inferenceEngine = remember { InferenceEngine() }
// Create factory for MainViewModel
val factory = remember { ViewModelFactoryProvider.getMainViewModelFactory(inferenceEngine) }
// Get ViewModel instance with factory
val viewModel: MainViewModel = viewModel(factory = factory)
val engineState by viewModel.engineState.collectAsState()
val navigationActions = remember(navController) {
NavigationActions(navController)
}
// Model unloading confirmation
var showUnloadDialog by remember { mutableStateOf(false) }
var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) }
// Observe back button
LaunchedEffect(navController) {
navController.addOnDestinationChangedListener { _, destination, _ ->
// Log navigation for debugging
println("Navigation: ${destination.route}")
}
}
// Handle drawer state
val openDrawer: () -> Unit = {
coroutineScope.launch {
drawerState.open()
}
}
// Main Content
NavHost(
navController = navController,
startDestination = AppDestinations.MODEL_SELECTION_ROUTE
) {
// Model Selection Screen
composable(AppDestinations.MODEL_SELECTION_ROUTE) {
ModelSelectionScreen(
onModelSelected = { modelInfo ->
viewModel.selectModel(modelInfo)
navigationActions.navigateToModeSelection()
},
onManageModelsClicked = {
navigationActions.navigateToSettings(SettingsTab.MODEL_MANAGEMENT.name)
},
onMenuClicked = openDrawer,
drawerState = drawerState,
navigationActions = navigationActions
)
}
// Mode Selection Screen
composable(AppDestinations.MODE_SELECTION_ROUTE) {
ModeSelectionScreen(
engineState = engineState,
onBenchmarkSelected = {
viewModel.prepareForBenchmark()
navigationActions.navigateToBenchmark()
},
onConversationSelected = { systemPrompt ->
viewModel.prepareForConversation(systemPrompt)
navigationActions.navigateToConversation()
},
onBackPressed = {
// Need to unload model before going back
if (viewModel.isModelLoaded()) {
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
},
drawerState = drawerState,
navigationActions = navigationActions
)
}
// Conversation Screen
composable(AppDestinations.CONVERSATION_ROUTE) {
ConversationScreen(
onBackPressed = {
// Need to unload model before going back
if (viewModel.isModelLoaded()) {
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
},
drawerState = drawerState,
navigationActions = navigationActions,
viewModel = viewModel
)
}
// Benchmark Screen
composable(AppDestinations.BENCHMARK_ROUTE) {
BenchmarkScreen(
onBackPressed = {
// Need to unload model before going back
if (viewModel.isModelLoaded()) {
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
},
onRerunPressed = {
viewModel.rerunBenchmark()
},
onSharePressed = {
// Stub for sharing functionality
},
drawerState = drawerState,
navigationActions = navigationActions,
viewModel = viewModel
)
}
// Settings Screen
composable(
route = "${AppDestinations.SETTINGS_ROUTE}/{tab}",
arguments = listOf(
navArgument("tab") {
type = NavType.StringType
defaultValue = SettingsTab.GENERAL.name
}
)
) { backStackEntry ->
val tabName = backStackEntry.arguments?.getString("tab") ?: SettingsTab.GENERAL.name
val tab = try {
SettingsTab.valueOf(tabName)
} catch (e: IllegalArgumentException) {
SettingsTab.GENERAL
}
SettingsScreen(
selectedTab = tab,
onBackPressed = { navController.popBackStack() },
drawerState = drawerState,
navigationActions = navigationActions
)
}
}
// Model unload confirmation dialog
if (showUnloadDialog) {
UnloadModelConfirmationDialog(
onConfirm = {
showUnloadDialog = false
coroutineScope.launch {
viewModel.unloadModel()
pendingNavigation?.invoke()
pendingNavigation = null
}
},
onDismiss = {
showUnloadDialog = false
pendingNavigation = null
}
)
}
}

View File

@ -0,0 +1,205 @@
package com.example.llama.revamp.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.engine.InferenceEngine
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Main ViewModel that handles the LLM engine state and operations.
*/
class MainViewModel(
private val inferenceEngine: InferenceEngine = InferenceEngine()
) : ViewModel() {
// Expose the engine state
val engineState: StateFlow<InferenceEngine.State> = inferenceEngine.state
// Benchmark results
val benchmarkResults: StateFlow<String?> = inferenceEngine.benchmarkResults
// Selected model information
private val _selectedModel = MutableStateFlow<ModelInfo?>(null)
val selectedModel: StateFlow<ModelInfo?> = _selectedModel.asStateFlow()
// Benchmark parameters
private var pp: Int = 32
private var tg: Int = 32
private var pl: Int = 512
// Messages in the conversation
private val _messages = MutableStateFlow<List<Message>>(emptyList())
val messages: StateFlow<List<Message>> = _messages.asStateFlow()
// System prompt for the conversation
private val _systemPrompt = MutableStateFlow<String?>(null)
val systemPrompt: StateFlow<String?> = _systemPrompt.asStateFlow()
/**
* Selects a model for use.
*/
fun selectModel(modelInfo: ModelInfo) {
_selectedModel.value = modelInfo
}
/**
* Prepares the engine for benchmark mode.
*/
fun prepareForBenchmark() {
viewModelScope.launch {
_selectedModel.value?.let { model ->
inferenceEngine.loadModel(model.path)
runBenchmark()
}
}
}
/**
* Runs the benchmark with current parameters.
*/
private suspend fun runBenchmark() {
inferenceEngine.bench(pp, tg, pl)
}
/**
* Reruns the benchmark.
*/
fun rerunBenchmark() {
viewModelScope.launch {
runBenchmark()
}
}
/**
* Prepares the engine for conversation mode.
*/
fun prepareForConversation(systemPrompt: String? = null) {
_systemPrompt.value = systemPrompt
viewModelScope.launch {
_selectedModel.value?.let { model ->
inferenceEngine.loadModel(model.path, systemPrompt)
}
}
}
/**
* Sends a user message and collects the response.
*/
fun sendMessage(content: String) {
if (content.isBlank()) return
// Add user message
val userMessage = Message.User(
content = content,
timestamp = System.currentTimeMillis()
)
_messages.value = _messages.value + userMessage
// Create placeholder for assistant message
val assistantMessage = Message.Assistant(
content = "",
timestamp = System.currentTimeMillis(),
isComplete = false
)
_messages.value = _messages.value + assistantMessage
// Get response from engine
val messageIndex = _messages.value.size - 1
viewModelScope.launch {
val response = StringBuilder()
inferenceEngine.sendUserPrompt(content).collect { token ->
response.append(token)
// Update the assistant message with the generated text
val currentMessages = _messages.value.toMutableList()
val currentAssistantMessage = currentMessages[messageIndex] as Message.Assistant
currentMessages[messageIndex] = currentAssistantMessage.copy(
content = response.toString(),
isComplete = false
)
_messages.value = currentMessages
}
// Mark message as complete when generation finishes
val finalMessages = _messages.value.toMutableList()
val finalAssistantMessage = finalMessages[messageIndex] as Message.Assistant
finalMessages[messageIndex] = finalAssistantMessage.copy(
isComplete = true
)
_messages.value = finalMessages
}
}
/**
* Unloads the currently loaded model.
*/
suspend fun unloadModel() {
inferenceEngine.unloadModel()
_messages.value = emptyList()
}
/**
* Checks if a model is currently loaded.
*/
fun isModelLoaded(): Boolean {
return engineState.value !is InferenceEngine.State.Uninitialized &&
engineState.value !is InferenceEngine.State.LibraryLoaded
}
/**
* Clean up resources when ViewModel is cleared.
*/
override fun onCleared() {
inferenceEngine.destroy()
super.onCleared()
}
/**
* Factory for creating MainViewModel instances.
*/
class Factory(private val inferenceEngine: InferenceEngine) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(inferenceEngine) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
/**
* Sealed class representing messages in a conversation.
*/
sealed class Message {
abstract val content: String
abstract val timestamp: Long
val formattedTime: String
get() {
val formatter = SimpleDateFormat("h:mm a", Locale.getDefault())
return formatter.format(Date(timestamp))
}
data class User(
override val content: String,
override val timestamp: Long
) : Message()
data class Assistant(
override val content: String,
override val timestamp: Long,
val isComplete: Boolean = true
) : Message()
}