app: extract AppContent from MainActivity to a separate file in ui package
This commit is contained in:
parent
42e3972b30
commit
f833c3a7ac
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue