app: extract AppContent from MainActivity to a separate file in ui package

This commit is contained in:
Han Yin 2025-09-20 11:37:17 -07:00
parent 42e3972b30
commit f833c3a7ac
4 changed files with 574 additions and 556 deletions

View File

@ -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
)
}
}
}
}
}

View File

@ -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"

View File

@ -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
)
}
}
}
}
}

View File

@ -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()
}