diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/MainActivity.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/MainActivity.kt index 03d339cac4..3d36a776a3 100644 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/MainActivity.kt +++ b/examples/llama.android/app/src/main/java/com/arm/aiplayground/MainActivity.kt @@ -1,70 +1,25 @@ package com.arm.aiplayground -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.DrawerValue import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface -import androidx.compose.material3.rememberDrawerState -import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import com.arm.aichat.InferenceEngine.State -import com.arm.aichat.isUninterruptible -import com.arm.aiplayground.engine.ModelLoadingMetrics -import com.arm.aiplayground.navigation.AppDestinations -import com.arm.aiplayground.navigation.NavigationActions -import com.arm.aiplayground.ui.scaffold.AnimatedNavHost -import com.arm.aiplayground.ui.scaffold.AppNavigationDrawer -import com.arm.aiplayground.ui.scaffold.AppScaffold -import com.arm.aiplayground.ui.scaffold.ScaffoldConfig -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import com.arm.aiplayground.ui.scaffold.bottombar.BottomBarConfig -import com.arm.aiplayground.ui.scaffold.topbar.NavigationIcon -import com.arm.aiplayground.ui.scaffold.topbar.TopBarConfig -import com.arm.aiplayground.ui.screens.BenchmarkScreen -import com.arm.aiplayground.ui.screens.ConversationScreen -import com.arm.aiplayground.ui.screens.ModelLoadingScreen -import com.arm.aiplayground.ui.screens.ModelsScreen -import com.arm.aiplayground.ui.screens.SettingsGeneralScreen +import com.arm.aiplayground.ui.AppContent import com.arm.aiplayground.ui.theme.LlamaTheme import com.arm.aiplayground.ui.theme.isDarkTheme import com.arm.aiplayground.ui.theme.md_theme_dark_scrim import com.arm.aiplayground.ui.theme.md_theme_light_scrim -import com.arm.aiplayground.viewmodel.BenchmarkViewModel -import com.arm.aiplayground.viewmodel.ConversationViewModel -import com.arm.aiplayground.viewmodel.MainViewModel -import com.arm.aiplayground.viewmodel.ModelLoadingViewModel -import com.arm.aiplayground.viewmodel.ModelScreenUiMode -import com.arm.aiplayground.viewmodel.ModelsManagementViewModel -import com.arm.aiplayground.viewmodel.ModelsViewModel import com.arm.aiplayground.viewmodel.SettingsViewModel import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -102,509 +57,3 @@ class MainActivity : ComponentActivity() { } } } - -@Composable -fun AppContent( - settingsViewModel: SettingsViewModel, - mainViewModel: MainViewModel = hiltViewModel(), - modelsViewModel: ModelsViewModel = hiltViewModel(), - modelsManagementViewModel: ModelsManagementViewModel = hiltViewModel(), - modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(), - benchmarkViewModel: BenchmarkViewModel = hiltViewModel(), - conversationViewModel: ConversationViewModel = hiltViewModel(), -) { - val coroutineScope = rememberCoroutineScope() - val snackbarHostState = remember { SnackbarHostState() } - - // App core states - val engineState by mainViewModel.engineState.collectAsState() - val showModelImportTooltip by mainViewModel.showModelImportTooltip.collectAsState() - val showChatTooltip by mainViewModel.showChatTooltip.collectAsState() - val showManagementTooltip by mainViewModel.showModelManagementTooltip.collectAsState() - - // Model state - val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState() - - // Metric states for scaffolds - val isMonitoringEnabled by settingsViewModel.isMonitoringEnabled.collectAsState() - val memoryUsage by settingsViewModel.memoryUsage.collectAsState() - val temperatureInfo by settingsViewModel.temperatureMetrics.collectAsState() - val useFahrenheit by settingsViewModel.useFahrenheitUnit.collectAsState() - val storageMetrics by settingsViewModel.storageMetrics.collectAsState() - - // Navigation - val navController = rememberNavController() - val navigationActions = remember(navController) { NavigationActions(navController) } - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute by remember(navBackStackEntry) { - derivedStateOf { navBackStackEntry?.destination?.route ?: "" } - } - - // Determine if drawer gestures should be enabled based on route - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val drawerGesturesEnabled by remember(currentRoute, drawerState.currentValue) { - derivedStateOf { - // Always allow gesture dismissal when drawer is open - if (drawerState.currentValue == DrawerValue.Open) true else false - } - } - val openDrawer: () -> Unit = { coroutineScope.launch { drawerState.open() } } - - // Handle child screens' scaffold events - val handleScaffoldEvent: (ScaffoldEvent) -> Unit = { event -> - when (event) { - is ScaffoldEvent.ShowSnackbar -> { - coroutineScope.launch { - if (event.actionLabel != null && event.onAction != null) { - val result = snackbarHostState.showSnackbar( - message = event.message, - actionLabel = event.actionLabel, - withDismissAction = event.withDismissAction, - duration = event.duration - ) - if (result == SnackbarResult.ActionPerformed) { - event.onAction() - } - } else { - snackbarHostState.showSnackbar( - message = event.message, - withDismissAction = event.withDismissAction, - duration = event.duration - ) - } - } - } - is ScaffoldEvent.ChangeTitle -> { - // TODO-han.yin: TBD - } - is ScaffoldEvent.ShareText -> { - val shareIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, event.text) - event.title?.let { putExtra(Intent.EXTRA_SUBJECT, it) } - type = event.mimeType - } - - val shareChooser = Intent.createChooser(shareIntent, event.title ?: "Share via") - - // Use the current activity for context - val context = (navController.context as? Activity) - ?: throw IllegalStateException("Activity context required for sharing") - - try { - context.startActivity(shareChooser) - } catch (_: ActivityNotFoundException) { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "No app found to share content", - duration = SnackbarDuration.Short - ) - } - } catch (e: Exception) { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "Share failed due to ${e.message}", - duration = SnackbarDuration.Short - ) - } - } - } - } - } - - // Create scaffold's top & bottom bar configs based on current route - val scaffoldConfig = when { - // Model selection screen - currentRoute == AppDestinations.MODELS_ROUTE -> { - // Collect states for bottom bar - val allModels by modelsViewModel.allModels.collectAsState() - val filteredModels by modelsViewModel.filteredModels.collectAsState() - val sortOrder by modelsViewModel.sortOrder.collectAsState() - val showSortMenu by modelsViewModel.showSortMenu.collectAsState() - val activeFilters by modelsViewModel.activeFilters.collectAsState() - val showFilterMenu by modelsViewModel.showFilterMenu.collectAsState() - val preselection by modelsViewModel.preselectedModelToRun.collectAsState() - - val selectedModelsToDelete by modelsManagementViewModel.selectedModelsToDelete.collectAsState() - val showImportModelMenu by modelsManagementViewModel.showImportModelMenu.collectAsState() - - val hasModelsInstalled = allModels?.isNotEmpty() == true - - // Create file launcher for importing local models - val fileLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument() - ) { uri -> uri?.let { modelsManagementViewModel.importLocalModelFileSelected(it) } } - - ScaffoldConfig( - topBarConfig = - when (modelScreenUiMode) { - ModelScreenUiMode.BROWSING -> - TopBarConfig.ModelsBrowsing( - title = "Installed models", - navigationIcon = NavigationIcon.Menu { - modelsViewModel.resetPreselection() - openDrawer() - }, - showTooltip = showManagementTooltip && !showChatTooltip && hasModelsInstalled, - showManagingToggle = !showChatTooltip && hasModelsInstalled, - onToggleManaging = { - if (hasModelsInstalled) { - mainViewModel.waiveModelManagementTooltip() - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - } - }, - ) - ModelScreenUiMode.SEARCHING -> - TopBarConfig.None() - ModelScreenUiMode.MANAGING -> - TopBarConfig.ModelsManagement( - title = "Managing models", - navigationIcon = NavigationIcon.Back { - modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) - }, - storageMetrics = - if (isMonitoringEnabled && !showModelImportTooltip) - storageMetrics else null, - ) - ModelScreenUiMode.DELETING -> - TopBarConfig.ModelsDeleting( - title = "Deleting models", - navigationIcon = NavigationIcon.Quit { - modelsManagementViewModel.resetManagementState() - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - }, - ) - }, - bottomBarConfig = - when (modelScreenUiMode) { - ModelScreenUiMode.BROWSING -> - BottomBarConfig.Models.Browsing( - isSearchingEnabled = hasModelsInstalled, - onToggleSearching = { - modelsViewModel.toggleMode(ModelScreenUiMode.SEARCHING) - }, - sorting = BottomBarConfig.Models.Browsing.SortingConfig( - isEnabled = hasModelsInstalled, - currentOrder = sortOrder, - isMenuVisible = showSortMenu, - toggleMenu = modelsViewModel::toggleSortMenu, - selectOrder = { - modelsViewModel.setSortOrder(it) - modelsViewModel.toggleSortMenu(false) - } - ), - filtering = BottomBarConfig.Models.Browsing.FilteringConfig( - isEnabled = hasModelsInstalled, - filters = activeFilters, - onToggleFilter = modelsViewModel::toggleFilter, - onClearFilters = modelsViewModel::clearFilters, - isMenuVisible = showFilterMenu, - toggleMenu = modelsViewModel::toggleFilterMenu - ), - runAction = BottomBarConfig.Models.RunActionConfig( - showTooltip = showChatTooltip, - preselectedModelToRun = preselection, - onClickRun = { - if (modelsViewModel.selectModel(it)) { - modelsViewModel.resetPreselection() - navigationActions.navigateToModelLoading() - } - } - ), - ) - - ModelScreenUiMode.SEARCHING -> - BottomBarConfig.Models.Searching( - textFieldState = modelsViewModel.searchFieldState, - onQuitSearching = { - modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) - }, - onSearch = { /* No-op for now */ }, - runAction = BottomBarConfig.Models.RunActionConfig( - showTooltip = false, - preselectedModelToRun = preselection, - onClickRun = { - if (modelsViewModel.selectModel(it)) { - modelsViewModel.resetPreselection() - navigationActions.navigateToModelLoading() - } - } - ), - ) - - ModelScreenUiMode.MANAGING -> - BottomBarConfig.Models.Managing( - isDeletionEnabled = filteredModels?.isNotEmpty() == true, - onToggleDeleting = { - modelsViewModel.toggleMode(ModelScreenUiMode.DELETING) - }, - sorting = BottomBarConfig.Models.Managing.SortingConfig( - isEnabled = hasModelsInstalled, - currentOrder = sortOrder, - isMenuVisible = showSortMenu, - toggleMenu = { modelsViewModel.toggleSortMenu(it) }, - selectOrder = { - modelsViewModel.setSortOrder(it) - modelsViewModel.toggleSortMenu(false) - } - ), - filtering = BottomBarConfig.Models.Managing.FilteringConfig( - isEnabled = hasModelsInstalled, - filters = activeFilters, - onToggleFilter = modelsViewModel::toggleFilter, - onClearFilters = modelsViewModel::clearFilters, - isMenuVisible = showFilterMenu, - toggleMenu = modelsViewModel::toggleFilterMenu - ), - importing = BottomBarConfig.Models.Managing.ImportConfig( - showTooltip = showModelImportTooltip, - isMenuVisible = showImportModelMenu, - toggleMenu = { show -> modelsManagementViewModel.toggleImportMenu(show) }, - importFromLocal = { - fileLauncher.launch(arrayOf("application/octet-stream", "*/*")) - modelsManagementViewModel.toggleImportMenu(false) - }, - importFromHuggingFace = { - modelsManagementViewModel.queryModelsFromHuggingFace(memoryUsage) - modelsManagementViewModel.toggleImportMenu(false) - } - ), - ) - - ModelScreenUiMode.DELETING -> - BottomBarConfig.Models.Deleting( - onQuitDeleting = { - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - }, - selectedModels = selectedModelsToDelete, - selectAllFilteredModels = { - filteredModels?.let { - modelsManagementViewModel.selectModelsToDelete(it) - } - }, - clearAllSelectedModels = { - modelsManagementViewModel.clearSelectedModelsToDelete() - }, - deleteSelected = { - selectedModelsToDelete.let { - if (it.isNotEmpty()) { - modelsManagementViewModel.batchDeletionClicked(it) - } - } - }, - ) - } - ) - } - - // Model loading screen - currentRoute == AppDestinations.MODEL_LOADING_ROUTE -> - ScaffoldConfig( - topBarConfig = TopBarConfig.Performance( - title = "Select a mode", - navigationIcon = NavigationIcon.Back { - modelLoadingViewModel.onBackPressed { navigationActions.navigateUp() } - }, - memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, - temperatureInfo = null - ) - ) - - // Benchmark screen - currentRoute.startsWith(AppDestinations.BENCHMARK_ROUTE) -> { - val showShareFab by benchmarkViewModel.showShareFab.collectAsState() - val showModelCard by benchmarkViewModel.showModelCard.collectAsState() - - ScaffoldConfig( - topBarConfig = TopBarConfig.Performance( - title = "Benchmark", - navigationIcon = NavigationIcon.Back { - benchmarkViewModel.onBackPressed { navigationActions.navigateUp() } - }, - memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, - temperatureInfo = if (isMonitoringEnabled) Pair(temperatureInfo, useFahrenheit) else null - ), - bottomBarConfig = BottomBarConfig.Benchmark( - engineIdle = !engineState.isUninterruptible, - showShareFab = showShareFab, - onShare = { benchmarkViewModel.shareResult(handleScaffoldEvent) }, - onRerun = { benchmarkViewModel.rerunBenchmark(handleScaffoldEvent) }, - onClear = { benchmarkViewModel.clearResults(handleScaffoldEvent) }, - showModelCard = showModelCard, - onToggleModelCard = benchmarkViewModel::toggleModelCard, - ) - ) - } - - // Conversation screen - currentRoute.startsWith(AppDestinations.CONVERSATION_ROUTE) -> { - val showModelCard by conversationViewModel.showModelCard.collectAsState() - - val modelThinkingOrSpeaking = - engineState is State.ProcessingUserPrompt || engineState is State.Generating - - val showStubMessage = null // { -// handleScaffoldEvent(ScaffoldEvent.ShowSnackbar( -// message = "Stub for now, let me know if you want it done :)" -// )) -// } - - ScaffoldConfig( - topBarConfig = TopBarConfig.Performance( - title = "Chat", - navigationIcon = NavigationIcon.Back { - conversationViewModel.onBackPressed { navigationActions.navigateUp() } - }, - memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, - temperatureInfo = if (isMonitoringEnabled) Pair(temperatureInfo, useFahrenheit) else null, - ), - bottomBarConfig = BottomBarConfig.Conversation( - isEnabled = !modelThinkingOrSpeaking, - textFieldState = conversationViewModel.inputFieldState, - onSendClick = conversationViewModel::sendMessage, - showModelCard = showModelCard, - onToggleModelCard = conversationViewModel::toggleModelCard, - onAttachPhotoClick = showStubMessage, - onAttachFileClick = showStubMessage, - onAudioInputClick = showStubMessage, - ) - ) - } - - // Settings screen - currentRoute == AppDestinations.SETTINGS_GENERAL_ROUTE -> - ScaffoldConfig( - topBarConfig = TopBarConfig.Default( - title = "Settings", - navigationIcon = NavigationIcon.Back { navigationActions.navigateUp() } - ) - ) - - // Fallback for empty screen or unknown routes - else -> ScaffoldConfig( - topBarConfig = TopBarConfig.Default(title = "", navigationIcon = NavigationIcon.None) - ) - } - - // Main UI hierarchy - AppNavigationDrawer( - drawerState = drawerState, - navigationActions = navigationActions, - gesturesEnabled = drawerGesturesEnabled, - currentRoute = currentRoute - ) { - // The AppScaffold now uses the config we created - AppScaffold( - topBarconfig = scaffoldConfig.topBarConfig, - bottomBarConfig = scaffoldConfig.bottomBarConfig, - onScaffoldEvent = handleScaffoldEvent, - snackbarHostState = snackbarHostState, - ) { paddingValues -> - // AnimatedNavHost inside the scaffold content - AnimatedNavHost( - navController = navController, - startDestination = AppDestinations.MODELS_ROUTE, - modifier = Modifier.padding(paddingValues) - ) { - // Model Selection Screen - composable(AppDestinations.MODELS_ROUTE) { - ModelsScreen( - showModelImportTooltip = showModelImportTooltip, - onFirstModelImportSuccess = { model -> - if (showModelImportTooltip) { - mainViewModel.waiveModelImportTooltip() - modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) - modelsViewModel.preselectModel(model, true) - } - }, - showChatTooltip = showChatTooltip, - onConfirmSelection = { modelInfo, ramWarning -> - if (modelsViewModel.confirmSelectedModel(modelInfo, ramWarning)) { - navigationActions.navigateToModelLoading() - } - }, - onScaffoldEvent = handleScaffoldEvent, - modelsViewModel = modelsViewModel, - managementViewModel = modelsManagementViewModel, - ) - } - - // Mode Selection Screen - composable(AppDestinations.MODEL_LOADING_ROUTE) { - ModelLoadingScreen( - onScaffoldEvent = handleScaffoldEvent, - onNavigateBack = { navigationActions.navigateUp() }, - onNavigateToBenchmark = { navigationActions.navigateToBenchmark(it) }, - onNavigateToConversation = { - navigationActions.navigateToConversation(it) - if (showChatTooltip) { - mainViewModel.waiveChatTooltip() - } - }, - viewModel = modelLoadingViewModel - ) - } - - // Benchmark Screen - composable( - route = AppDestinations.BENCHMARK_ROUTE_WITH_PARAMS, - arguments = listOf( - navArgument("modelLoadTimeMs") { - type = NavType.LongType - defaultValue = 0L - } - ) - ) { backStackEntry -> - val modelLoadTimeMs = backStackEntry.arguments?.getLong("modelLoadTimeMs") ?: 0L - val metrics = if (modelLoadTimeMs > 0) { - ModelLoadingMetrics(modelLoadTimeMs) - } else throw IllegalArgumentException("Expecting a valid ModelLoadingMetrics!") - - BenchmarkScreen( - loadingMetrics = metrics, - onScaffoldEvent = handleScaffoldEvent, - onNavigateBack = { navigationActions.navigateUp() }, - viewModel = benchmarkViewModel - ) - } - - // Conversation Screen - composable( - route = AppDestinations.CONVERSATION_ROUTE_WITH_PARAMS, - arguments = listOf( - navArgument("modelLoadTimeMs") { - type = NavType.LongType - defaultValue = 0L - }, - navArgument("promptTimeMs") { - type = NavType.LongType - defaultValue = 0L - } - ) - ) { backStackEntry -> - val modelLoadTimeMs = backStackEntry.arguments?.getLong("modelLoadTimeMs") ?: 0L - val promptTimeMs = backStackEntry.arguments?.getLong("promptTimeMs") ?: 0L - val metrics = if (modelLoadTimeMs > 0) { - ModelLoadingMetrics( - modelLoadingTimeMs = modelLoadTimeMs, - systemPromptProcessingTimeMs = if (promptTimeMs > 0) promptTimeMs else null - ) - } else throw IllegalArgumentException("Expecting a valid ModelLoadingMetrics!") - - ConversationScreen( - loadingMetrics = metrics, - onNavigateBack = { navigationActions.navigateUp() }, - viewModel = conversationViewModel - ) - } - - // Settings Screen - composable(AppDestinations.SETTINGS_GENERAL_ROUTE) { - SettingsGeneralScreen( - viewModel = settingsViewModel - ) - } - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/navigation/AppDestinations.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/navigation/AppDestinations.kt index 5a2220dc98..f4641dc6f6 100644 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/navigation/AppDestinations.kt +++ b/examples/llama.android/app/src/main/java/com/arm/aiplayground/navigation/AppDestinations.kt @@ -12,10 +12,13 @@ object AppDestinations { const val MODEL_LOADING_ROUTE = "model_loading" const val CONVERSATION_ROUTE = "conversation" - const val CONVERSATION_ROUTE_WITH_PARAMS = "conversation/{modelLoadTimeMs}/{promptTimeMs}" + const val CONVERSATION_ROUTE_WITH_PARAMS = "conversation/{modelLoadTimeMs}/{promptProcessTimeMs}" + const val CONVERSATION_ROUTE_PARAM_MODEL_LOAD_TIME = "modelLoadTimeMs" + const val CONVERSATION_ROUTE_PARAM_PROMPT_PROCESS_TIME = "promptProcessTimeMs" const val BENCHMARK_ROUTE = "benchmark" const val BENCHMARK_ROUTE_WITH_PARAMS = "benchmark/{modelLoadTimeMs}" + const val BENCHMARK_ROUTE_PARAM_MODEL_LOAD_TIME = "modelLoadTimeMs" // Settings destinations const val SETTINGS_GENERAL_ROUTE = "settings_general" diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/AppContent.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/AppContent.kt new file mode 100644 index 0000000000..5c15cd166c --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/AppContent.kt @@ -0,0 +1,566 @@ +package com.arm.aiplayground.ui + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.arm.aichat.InferenceEngine +import com.arm.aichat.isUninterruptible +import com.arm.aiplayground.engine.ModelLoadingMetrics +import com.arm.aiplayground.navigation.AppDestinations +import com.arm.aiplayground.navigation.AppDestinations.BENCHMARK_ROUTE_PARAM_MODEL_LOAD_TIME +import com.arm.aiplayground.navigation.AppDestinations.CONVERSATION_ROUTE_PARAM_MODEL_LOAD_TIME +import com.arm.aiplayground.navigation.AppDestinations.CONVERSATION_ROUTE_PARAM_PROMPT_PROCESS_TIME +import com.arm.aiplayground.navigation.NavigationActions +import com.arm.aiplayground.ui.scaffold.AnimatedNavHost +import com.arm.aiplayground.ui.scaffold.AppNavigationDrawer +import com.arm.aiplayground.ui.scaffold.AppScaffold +import com.arm.aiplayground.ui.scaffold.ScaffoldConfig +import com.arm.aiplayground.ui.scaffold.ScaffoldEvent +import com.arm.aiplayground.ui.scaffold.bottombar.BottomBarConfig +import com.arm.aiplayground.ui.scaffold.topbar.NavigationIcon +import com.arm.aiplayground.ui.scaffold.topbar.TopBarConfig +import com.arm.aiplayground.ui.screens.BenchmarkScreen +import com.arm.aiplayground.ui.screens.ConversationScreen +import com.arm.aiplayground.ui.screens.ModelLoadingScreen +import com.arm.aiplayground.ui.screens.ModelsScreen +import com.arm.aiplayground.ui.screens.SettingsGeneralScreen +import com.arm.aiplayground.viewmodel.BenchmarkViewModel +import com.arm.aiplayground.viewmodel.ConversationViewModel +import com.arm.aiplayground.viewmodel.MainViewModel +import com.arm.aiplayground.viewmodel.ModelLoadingViewModel +import com.arm.aiplayground.viewmodel.ModelScreenUiMode +import com.arm.aiplayground.viewmodel.ModelsManagementViewModel +import com.arm.aiplayground.viewmodel.ModelsViewModel +import com.arm.aiplayground.viewmodel.SettingsViewModel +import kotlinx.coroutines.launch + +@Composable +fun AppContent( + settingsViewModel: SettingsViewModel, + mainViewModel: MainViewModel = hiltViewModel(), + modelsViewModel: ModelsViewModel = hiltViewModel(), + modelsManagementViewModel: ModelsManagementViewModel = hiltViewModel(), + modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(), + benchmarkViewModel: BenchmarkViewModel = hiltViewModel(), + conversationViewModel: ConversationViewModel = hiltViewModel(), +) { + val coroutineScope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + // App core states + val engineState by mainViewModel.engineState.collectAsState() + val showModelImportTooltip by mainViewModel.showModelImportTooltip.collectAsState() + val showChatTooltip by mainViewModel.showChatTooltip.collectAsState() + val showManagementTooltip by mainViewModel.showModelManagementTooltip.collectAsState() + + // Model state + val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState() + + // Metric states for scaffolds + val isMonitoringEnabled by settingsViewModel.isMonitoringEnabled.collectAsState() + val memoryUsage by settingsViewModel.memoryUsage.collectAsState() + val temperatureInfo by settingsViewModel.temperatureMetrics.collectAsState() + val useFahrenheit by settingsViewModel.useFahrenheitUnit.collectAsState() + val storageMetrics by settingsViewModel.storageMetrics.collectAsState() + + // Navigation + val navController = rememberNavController() + val navigationActions = remember(navController) { NavigationActions(navController) } + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute by remember(navBackStackEntry) { + derivedStateOf { navBackStackEntry?.destination?.route ?: "" } + } + + // Determine if drawer gestures should be enabled based on route + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val drawerGesturesEnabled by remember(currentRoute, drawerState.currentValue) { + derivedStateOf { + // Always allow gesture dismissal when drawer is open + if (drawerState.currentValue == DrawerValue.Open) true else false + } + } + val openDrawer: () -> Unit = { coroutineScope.launch { drawerState.open() } } + + // Handle child screens' scaffold events + val handleScaffoldEvent: (ScaffoldEvent) -> Unit = { event -> + when (event) { + is ScaffoldEvent.ShowSnackbar -> { + coroutineScope.launch { + if (event.actionLabel != null && event.onAction != null) { + val result = snackbarHostState.showSnackbar( + message = event.message, + actionLabel = event.actionLabel, + withDismissAction = event.withDismissAction, + duration = event.duration + ) + if (result == SnackbarResult.ActionPerformed) { + event.onAction() + } + } else { + snackbarHostState.showSnackbar( + message = event.message, + withDismissAction = event.withDismissAction, + duration = event.duration + ) + } + } + } + is ScaffoldEvent.ChangeTitle -> { + // TODO-han.yin: TBD + } + is ScaffoldEvent.ShareText -> { + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, event.text) + event.title?.let { putExtra(Intent.EXTRA_SUBJECT, it) } + type = event.mimeType + } + + val shareChooser = Intent.createChooser(shareIntent, event.title ?: "Share via") + + // Use the current activity for context + val context = (navController.context as? Activity) + ?: throw IllegalStateException("Activity context required for sharing") + + try { + context.startActivity(shareChooser) + } catch (_: ActivityNotFoundException) { + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = "No app found to share content", + duration = SnackbarDuration.Short + ) + } + } catch (e: Exception) { + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = "Share failed due to ${e.message}", + duration = SnackbarDuration.Short + ) + } + } + } + } + } + + // Create scaffold's top & bottom bar configs based on current route + val scaffoldConfig = when { + // Model selection screen + currentRoute == AppDestinations.MODELS_ROUTE -> { + // Collect states for bottom bar + val allModels by modelsViewModel.allModels.collectAsState() + val filteredModels by modelsViewModel.filteredModels.collectAsState() + val sortOrder by modelsViewModel.sortOrder.collectAsState() + val showSortMenu by modelsViewModel.showSortMenu.collectAsState() + val activeFilters by modelsViewModel.activeFilters.collectAsState() + val showFilterMenu by modelsViewModel.showFilterMenu.collectAsState() + val preselection by modelsViewModel.preselectedModelToRun.collectAsState() + + val selectedModelsToDelete by modelsManagementViewModel.selectedModelsToDelete.collectAsState() + val showImportModelMenu by modelsManagementViewModel.showImportModelMenu.collectAsState() + + val hasModelsInstalled = allModels?.isNotEmpty() == true + + // Create file launcher for importing local models + val fileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> uri?.let { modelsManagementViewModel.importLocalModelFileSelected(it) } } + + ScaffoldConfig( + topBarConfig = + when (modelScreenUiMode) { + ModelScreenUiMode.BROWSING -> + TopBarConfig.ModelsBrowsing( + title = "Installed models", + navigationIcon = NavigationIcon.Menu { + modelsViewModel.resetPreselection() + openDrawer() + }, + showTooltip = showManagementTooltip && !showChatTooltip && hasModelsInstalled, + showManagingToggle = !showChatTooltip && hasModelsInstalled, + onToggleManaging = { + if (hasModelsInstalled) { + mainViewModel.waiveModelManagementTooltip() + modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) + } + }, + ) + + ModelScreenUiMode.SEARCHING -> + TopBarConfig.None() + + ModelScreenUiMode.MANAGING -> + TopBarConfig.ModelsManagement( + title = "Managing models", + navigationIcon = NavigationIcon.Back { + modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) + }, + storageMetrics = + if (isMonitoringEnabled && !showModelImportTooltip) + storageMetrics else null, + ) + + ModelScreenUiMode.DELETING -> + TopBarConfig.ModelsDeleting( + title = "Deleting models", + navigationIcon = NavigationIcon.Quit { + modelsManagementViewModel.resetManagementState() + modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) + }, + ) + }, + bottomBarConfig = + when (modelScreenUiMode) { + ModelScreenUiMode.BROWSING -> + BottomBarConfig.Models.Browsing( + isSearchingEnabled = hasModelsInstalled, + onToggleSearching = { + modelsViewModel.toggleMode(ModelScreenUiMode.SEARCHING) + }, + sorting = BottomBarConfig.Models.Browsing.SortingConfig( + isEnabled = hasModelsInstalled, + currentOrder = sortOrder, + isMenuVisible = showSortMenu, + toggleMenu = modelsViewModel::toggleSortMenu, + selectOrder = { + modelsViewModel.setSortOrder(it) + modelsViewModel.toggleSortMenu(false) + } + ), + filtering = BottomBarConfig.Models.Browsing.FilteringConfig( + isEnabled = hasModelsInstalled, + filters = activeFilters, + onToggleFilter = modelsViewModel::toggleFilter, + onClearFilters = modelsViewModel::clearFilters, + isMenuVisible = showFilterMenu, + toggleMenu = modelsViewModel::toggleFilterMenu + ), + runAction = BottomBarConfig.Models.RunActionConfig( + showTooltip = showChatTooltip, + preselectedModelToRun = preselection, + onClickRun = { + if (modelsViewModel.selectModel(it)) { + modelsViewModel.resetPreselection() + navigationActions.navigateToModelLoading() + } + } + ), + ) + + ModelScreenUiMode.SEARCHING -> + BottomBarConfig.Models.Searching( + textFieldState = modelsViewModel.searchFieldState, + onQuitSearching = { + modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) + }, + onSearch = { /* No-op for now */ }, + runAction = BottomBarConfig.Models.RunActionConfig( + showTooltip = false, + preselectedModelToRun = preselection, + onClickRun = { + if (modelsViewModel.selectModel(it)) { + modelsViewModel.resetPreselection() + navigationActions.navigateToModelLoading() + } + } + ), + ) + + ModelScreenUiMode.MANAGING -> + BottomBarConfig.Models.Managing( + isDeletionEnabled = filteredModels?.isNotEmpty() == true, + onToggleDeleting = { + modelsViewModel.toggleMode(ModelScreenUiMode.DELETING) + }, + sorting = BottomBarConfig.Models.Managing.SortingConfig( + isEnabled = hasModelsInstalled, + currentOrder = sortOrder, + isMenuVisible = showSortMenu, + toggleMenu = { modelsViewModel.toggleSortMenu(it) }, + selectOrder = { + modelsViewModel.setSortOrder(it) + modelsViewModel.toggleSortMenu(false) + } + ), + filtering = BottomBarConfig.Models.Managing.FilteringConfig( + isEnabled = hasModelsInstalled, + filters = activeFilters, + onToggleFilter = modelsViewModel::toggleFilter, + onClearFilters = modelsViewModel::clearFilters, + isMenuVisible = showFilterMenu, + toggleMenu = modelsViewModel::toggleFilterMenu + ), + importing = BottomBarConfig.Models.Managing.ImportConfig( + showTooltip = showModelImportTooltip, + isMenuVisible = showImportModelMenu, + toggleMenu = { show -> + modelsManagementViewModel.toggleImportMenu(show) + }, + importFromLocal = { + fileLauncher.launch(arrayOf("application/octet-stream", "*/*")) + modelsManagementViewModel.toggleImportMenu(false) + }, + importFromHuggingFace = { + modelsManagementViewModel.queryModelsFromHuggingFace(memoryUsage) + modelsManagementViewModel.toggleImportMenu(false) + } + ), + ) + + ModelScreenUiMode.DELETING -> + BottomBarConfig.Models.Deleting( + onQuitDeleting = { + modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) + }, + selectedModels = selectedModelsToDelete, + selectAllFilteredModels = { + filteredModels?.let { + modelsManagementViewModel.selectModelsToDelete(it) + } + }, + clearAllSelectedModels = { + modelsManagementViewModel.clearSelectedModelsToDelete() + }, + deleteSelected = { + selectedModelsToDelete.let { + if (it.isNotEmpty()) { + modelsManagementViewModel.batchDeletionClicked(it) + } + } + }, + ) + } + ) + } + + // Model loading screen + currentRoute == AppDestinations.MODEL_LOADING_ROUTE -> + ScaffoldConfig( + topBarConfig = TopBarConfig.Performance( + title = "Select a mode", + navigationIcon = NavigationIcon.Back { + modelLoadingViewModel.onBackPressed { navigationActions.navigateUp() } + }, + memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, + temperatureInfo = null + ) + ) + + // Benchmark screen + currentRoute.startsWith(AppDestinations.BENCHMARK_ROUTE) -> { + val showShareFab by benchmarkViewModel.showShareFab.collectAsState() + val showModelCard by benchmarkViewModel.showModelCard.collectAsState() + + ScaffoldConfig( + topBarConfig = TopBarConfig.Performance( + title = "Benchmark", + navigationIcon = NavigationIcon.Back { + benchmarkViewModel.onBackPressed { navigationActions.navigateUp() } + }, + memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, + temperatureInfo = if (isMonitoringEnabled) Pair( + temperatureInfo, + useFahrenheit + ) else null + ), + bottomBarConfig = BottomBarConfig.Benchmark( + engineIdle = !engineState.isUninterruptible, + showShareFab = showShareFab, + onShare = { benchmarkViewModel.shareResult(handleScaffoldEvent) }, + onRerun = { benchmarkViewModel.rerunBenchmark(handleScaffoldEvent) }, + onClear = { benchmarkViewModel.clearResults(handleScaffoldEvent) }, + showModelCard = showModelCard, + onToggleModelCard = benchmarkViewModel::toggleModelCard, + ) + ) + } + + // Conversation screen + currentRoute.startsWith(AppDestinations.CONVERSATION_ROUTE) -> { + val showModelCard by conversationViewModel.showModelCard.collectAsState() + + val modelThinkingOrSpeaking = + engineState is InferenceEngine.State.ProcessingUserPrompt || engineState is InferenceEngine.State.Generating + + ScaffoldConfig( + topBarConfig = TopBarConfig.Performance( + title = "Chat", + navigationIcon = NavigationIcon.Back { + conversationViewModel.onBackPressed { navigationActions.navigateUp() } + }, + memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, + temperatureInfo = if (isMonitoringEnabled) Pair( + temperatureInfo, + useFahrenheit + ) else null, + ), + bottomBarConfig = BottomBarConfig.Conversation( + isEnabled = !modelThinkingOrSpeaking, + textFieldState = conversationViewModel.inputFieldState, + onSendClick = conversationViewModel::sendMessage, + showModelCard = showModelCard, + onToggleModelCard = conversationViewModel::toggleModelCard, + ) + ) + } + + // Settings screen + currentRoute == AppDestinations.SETTINGS_GENERAL_ROUTE -> + ScaffoldConfig( + topBarConfig = TopBarConfig.Default( + title = "Settings", + navigationIcon = NavigationIcon.Back { navigationActions.navigateUp() } + ) + ) + + // Fallback for empty screen or unknown routes + else -> ScaffoldConfig( + topBarConfig = TopBarConfig.Default(title = "", navigationIcon = NavigationIcon.None) + ) + } + + // Main UI hierarchy + AppNavigationDrawer( + drawerState = drawerState, + navigationActions = navigationActions, + gesturesEnabled = drawerGesturesEnabled, + currentRoute = currentRoute + ) { + // The AppScaffold now uses the config we created + AppScaffold( + topBarconfig = scaffoldConfig.topBarConfig, + bottomBarConfig = scaffoldConfig.bottomBarConfig, + onScaffoldEvent = handleScaffoldEvent, + snackbarHostState = snackbarHostState, + ) { paddingValues -> + // AnimatedNavHost inside the scaffold content + AnimatedNavHost( + navController = navController, + startDestination = AppDestinations.MODELS_ROUTE, + modifier = Modifier.Companion.padding(paddingValues) + ) { + // Model Selection Screen + composable(AppDestinations.MODELS_ROUTE) { + ModelsScreen( + showModelImportTooltip = showModelImportTooltip, + onFirstModelImportSuccess = { model -> + if (showModelImportTooltip) { + mainViewModel.waiveModelImportTooltip() + modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) + modelsViewModel.preselectModel(model, true) + } + }, + showChatTooltip = showChatTooltip, + onConfirmSelection = { modelInfo, ramWarning -> + if (modelsViewModel.confirmSelectedModel(modelInfo, ramWarning)) { + navigationActions.navigateToModelLoading() + } + }, + onScaffoldEvent = handleScaffoldEvent, + modelsViewModel = modelsViewModel, + managementViewModel = modelsManagementViewModel, + ) + } + + // Mode Selection Screen + composable(AppDestinations.MODEL_LOADING_ROUTE) { + ModelLoadingScreen( + onScaffoldEvent = handleScaffoldEvent, + onNavigateBack = { navigationActions.navigateUp() }, + onNavigateToBenchmark = { navigationActions.navigateToBenchmark(it) }, + onNavigateToConversation = { + navigationActions.navigateToConversation(it) + if (showChatTooltip) { + mainViewModel.waiveChatTooltip() + } + }, + viewModel = modelLoadingViewModel + ) + } + + // Benchmark Screen + composable( + route = AppDestinations.BENCHMARK_ROUTE_WITH_PARAMS, + arguments = listOf( + navArgument(BENCHMARK_ROUTE_PARAM_MODEL_LOAD_TIME) { + type = NavType.Companion.LongType + defaultValue = 0L + } + ) + ) { backStackEntry -> + val modelLoadTimeMs = backStackEntry.arguments?.getLong(BENCHMARK_ROUTE_PARAM_MODEL_LOAD_TIME) ?: 0L + val metrics = if (modelLoadTimeMs > 0) { + ModelLoadingMetrics(modelLoadTimeMs) + } else throw IllegalArgumentException("Expecting a valid ModelLoadingMetrics!") + + BenchmarkScreen( + loadingMetrics = metrics, + onScaffoldEvent = handleScaffoldEvent, + onNavigateBack = { navigationActions.navigateUp() }, + viewModel = benchmarkViewModel + ) + } + + // Conversation Screen + composable( + route = AppDestinations.CONVERSATION_ROUTE_WITH_PARAMS, + arguments = listOf( + navArgument(CONVERSATION_ROUTE_PARAM_MODEL_LOAD_TIME) { + type = NavType.Companion.LongType + defaultValue = 0L + }, + navArgument(CONVERSATION_ROUTE_PARAM_PROMPT_PROCESS_TIME) { + type = NavType.Companion.LongType + defaultValue = 0L + } + ) + ) { backStackEntry -> + val modelLoadTimeMs = backStackEntry.arguments?.getLong( + CONVERSATION_ROUTE_PARAM_MODEL_LOAD_TIME) ?: 0L + val promptProcessTimeMs = backStackEntry.arguments?.getLong( + CONVERSATION_ROUTE_PARAM_PROMPT_PROCESS_TIME) ?: 0L + val metrics = if (modelLoadTimeMs > 0) { + ModelLoadingMetrics( + modelLoadingTimeMs = modelLoadTimeMs, + systemPromptProcessingTimeMs = if (promptProcessTimeMs > 0) promptProcessTimeMs else null + ) + } else throw IllegalArgumentException("Expecting a valid ModelLoadingMetrics!") + + ConversationScreen( + loadingMetrics = metrics, + onNavigateBack = { navigationActions.navigateUp() }, + viewModel = conversationViewModel + ) + } + + // Settings Screen + composable(AppDestinations.SETTINGS_GENERAL_ROUTE) { + SettingsGeneralScreen( + viewModel = settingsViewModel + ) + } + } + } + } +} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BottomBarConfig.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BottomBarConfig.kt index cd090a021c..bca11e3150 100644 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BottomBarConfig.kt +++ b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BottomBarConfig.kt @@ -111,8 +111,8 @@ sealed class BottomBarConfig { val onSendClick: () -> Unit, val showModelCard: Boolean, val onToggleModelCard: (Boolean) -> Unit, - val onAttachPhotoClick: (() -> Unit)?, - val onAttachFileClick: (() -> Unit)?, - val onAudioInputClick: (() -> Unit)?, + val onAttachPhotoClick: (() -> Unit)? = null, + val onAttachFileClick: (() -> Unit)? = null, + val onAudioInputClick: (() -> Unit)? = null, ) : BottomBarConfig() }