diff --git a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt index da77c5d23e..0759b2047b 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt @@ -97,7 +97,7 @@ fun AppContent( // App core states val engineState by mainViewModel.engineState.collectAsState() val showModelImportTooltip by mainViewModel.showModelImportTooltip.collectAsState() - val showChatTooltip by mainViewModel.showModelImportTooltip.collectAsState() + val showChatTooltip by mainViewModel.showChatTooltip.collectAsState() // Model state val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState() @@ -222,9 +222,12 @@ fun AppContent( modelsViewModel.resetPreselection() openDrawer() }, - onToggleManaging = if (hasModelsInstalled) { - { modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) } - } else null, + showManagingToggle = !showChatTooltip && hasModelsInstalled, + onToggleManaging = { + if (hasModelsInstalled) { + modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) + } + }, ) ModelScreenUiMode.SEARCHING -> TopBarConfig.None() @@ -234,7 +237,9 @@ fun AppContent( navigationIcon = NavigationIcon.Back { modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) }, - storageMetrics = if (isMonitoringEnabled) storageMetrics else null, + storageMetrics = + if (isMonitoringEnabled && !showModelImportTooltip) + storageMetrics else null, ) ModelScreenUiMode.DELETING -> TopBarConfig.ModelsDeleting( @@ -272,6 +277,7 @@ fun AppContent( toggleMenu = modelsViewModel::toggleFilterMenu ), runAction = BottomBarConfig.Models.RunActionConfig( + showTooltip = showChatTooltip, preselectedModelToRun = preselection, onClickRun = { if (modelsViewModel.selectModel(it)) { @@ -290,6 +296,7 @@ fun AppContent( }, onSearch = { /* No-op for now */ }, runAction = BottomBarConfig.Models.RunActionConfig( + showTooltip = false, preselectedModelToRun = preselection, onClickRun = { if (modelsViewModel.selectModel(it)) { @@ -477,15 +484,20 @@ fun AppContent( // 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() } }, - onFirstModelImportSuccess = - if (showModelImportTooltip) { - { mainViewModel.waiveModelImportTooltip() } - } else null, onScaffoldEvent = handleScaffoldEvent, modelsViewModel = modelsViewModel, managementViewModel = modelsManagementViewModel, @@ -498,7 +510,12 @@ fun AppContent( onScaffoldEvent = handleScaffoldEvent, onNavigateBack = { navigationActions.navigateUp() }, onNavigateToBenchmark = { navigationActions.navigateToBenchmark(it) }, - onNavigateToConversation = { navigationActions.navigateToConversation(it) }, + onNavigateToConversation = { + navigationActions.navigateToConversation(it) + if (showChatTooltip) { + mainViewModel.waiveChatTooltip() + } + }, viewModel = modelLoadingViewModel ) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/AppScaffold.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/AppScaffold.kt index 3f24e2ad12..77e2eb1d30 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/AppScaffold.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/AppScaffold.kt @@ -19,7 +19,7 @@ import com.example.llama.ui.scaffold.topbar.DefaultTopBar import com.example.llama.ui.scaffold.topbar.ModelsBrowsingTopBar import com.example.llama.ui.scaffold.topbar.NavigationIcon import com.example.llama.ui.scaffold.topbar.PerformanceTopBar -import com.example.llama.ui.scaffold.topbar.StorageTopBar +import com.example.llama.ui.scaffold.topbar.ModelsManagementTopBar import com.example.llama.ui.scaffold.topbar.TopBarConfig /** @@ -71,6 +71,7 @@ fun AppScaffold( is TopBarConfig.ModelsBrowsing -> ModelsBrowsingTopBar( title = topBarconfig.title, + showManagingToggle = topBarconfig.showManagingToggle, onToggleManaging = topBarconfig.onToggleManaging, onNavigateBack = topBarconfig.navigationIcon.backAction, onMenuOpen = topBarconfig.navigationIcon.menuAction @@ -83,7 +84,7 @@ fun AppScaffold( onQuit = topBarconfig.navigationIcon.quitAction ) - is TopBarConfig.ModelsManagement -> StorageTopBar( + is TopBarConfig.ModelsManagement -> ModelsManagementTopBar( title = topBarconfig.title, storageMetrics = topBarconfig.storageMetrics, onScaffoldEvent = onScaffoldEvent, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BottomBarConfig.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BottomBarConfig.kt index a1eb050805..535dfacf46 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BottomBarConfig.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BottomBarConfig.kt @@ -89,6 +89,7 @@ sealed class BottomBarConfig { ) : BottomBarConfig() data class RunActionConfig( + val showTooltip: Boolean, val preselectedModelToRun: PreselectedModelToRun?, val onClickRun: (PreselectedModelToRun) -> Unit, ) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt index 949c9ab766..90fc78a77c 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt @@ -18,19 +18,26 @@ import androidx.compose.material3.BottomAppBar import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text +import androidx.compose.material3.TooltipAnchorPosition +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.test.isEnabled import androidx.compose.ui.unit.dp import com.example.llama.data.model.ModelSortOrder +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ModelsBrowsingBottomBar( isSearchingEnabled: Boolean, @@ -39,6 +46,17 @@ fun ModelsBrowsingBottomBar( filteringConfig: BottomBarConfig.Models.Browsing.FilteringConfig, runActionConfig: BottomBarConfig.Models.RunActionConfig, ) { + val tooltipState = rememberTooltipState( + initialIsVisible = runActionConfig.showTooltip, + isPersistent = runActionConfig.showTooltip + ) + + LaunchedEffect(runActionConfig.preselectedModelToRun) { + if (runActionConfig.showTooltip && runActionConfig.preselectedModelToRun != null) { + tooltipState.show() + } + } + BottomAppBar( actions = { // Enter search action @@ -167,23 +185,35 @@ fun ModelsBrowsingBottomBar( } }, floatingActionButton = { - // Only show FAB if a model is selected - AnimatedVisibility( - visible = runActionConfig.preselectedModelToRun != null, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() + TooltipBox( + positionProvider = TooltipDefaults.rememberTooltipPositionProvider( + TooltipAnchorPosition.Above), + state = tooltipState, + tooltip = { + PlainTooltip { + Text("Tap this button to run your first model!") + } + }, + onDismissRequest = {} ) { - FloatingActionButton( - onClick = { - runActionConfig.preselectedModelToRun?.let { - runActionConfig.onClickRun(it) - } - }, + // Only show FAB if a model is selected + AnimatedVisibility( + visible = runActionConfig.preselectedModelToRun != null, + enter = scaleIn() + fadeIn(), + exit = scaleOut() + fadeOut() ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = "Run with selected model" - ) + FloatingActionButton( + onClick = { + runActionConfig.preselectedModelToRun?.let { + runActionConfig.onClickRun(it) + } + }, + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = "Run with selected model" + ) + } } } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsManagementBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsManagementBottomBar.kt index 2e1003381f..cf2b972275 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsManagementBottomBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsManagementBottomBar.kt @@ -46,7 +46,9 @@ fun ModelsManagementBottomBar( filteringConfig: BottomBarConfig.Models.Managing.FilteringConfig, importingConfig: BottomBarConfig.Models.Managing.ImportConfig, ) { - val tooltipState = rememberTooltipState() + val tooltipState = rememberTooltipState( + initialIsVisible = false, + isPersistent = importingConfig.showTooltip) LaunchedEffect(importingConfig) { if (importingConfig.showTooltip && !importingConfig.isMenuVisible) { @@ -181,12 +183,13 @@ fun ModelsManagementBottomBar( TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider( TooltipAnchorPosition.Above), + state = tooltipState, tooltip = { PlainTooltip { Text("Tap this button to install your first model!") } }, - state = tooltipState + onDismissRequest = {} ) { FloatingActionButton( onClick = { importingConfig.toggleMenu(true) }, @@ -204,7 +207,7 @@ fun ModelsManagementBottomBar( onDismissRequest = { importingConfig.toggleMenu(false) } ) { DropdownMenuItem( - text = { Text("Import a local model") }, + text = { Text("Import a local GGUF model") }, leadingIcon = { Icon( imageVector = Icons.Default.FolderOpen, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsBrowsingTopBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsBrowsingTopBar.kt index 53c56412a1..5b5b4bfeff 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsBrowsingTopBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsBrowsingTopBar.kt @@ -24,7 +24,8 @@ import androidx.compose.ui.unit.dp @Composable fun ModelsBrowsingTopBar( title: String, - onToggleManaging: (() -> Unit)? = null, + showManagingToggle: Boolean, + onToggleManaging: () -> Unit, onNavigateBack: (() -> Unit)? = null, onMenuOpen: (() -> Unit)? = null, ) { @@ -52,9 +53,9 @@ fun ModelsBrowsingTopBar( } }, actions = { - onToggleManaging?.let { - ModelManageActionToggle(it) - } + if (showManagingToggle) { + ModelManageActionToggle(onToggleManaging) + } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/StorageTopBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsManagementTopBar.kt similarity index 99% rename from examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/StorageTopBar.kt rename to examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsManagementTopBar.kt index cf0a565756..0ddc3837e0 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/StorageTopBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsManagementTopBar.kt @@ -24,7 +24,7 @@ import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable -fun StorageTopBar( +fun ModelsManagementTopBar( title: String, storageMetrics: StorageMetrics?, onScaffoldEvent: (ScaffoldEvent) -> Unit, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/TopBarConfig.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/TopBarConfig.kt index 2f07fc5773..a27ebf4365 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/TopBarConfig.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/TopBarConfig.kt @@ -28,7 +28,8 @@ sealed class TopBarConfig { data class ModelsBrowsing( override val title: String, override val navigationIcon: NavigationIcon, - val onToggleManaging: (() -> Unit)?, + val showManagingToggle: Boolean, + val onToggleManaging: () -> Unit, ) : TopBarConfig() // Model batch-deletion top bar with a toggle to turn on/off manage mode diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ConversationScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ConversationScreen.kt index 3fb58cf4cb..e5e8d7c4a4 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ConversationScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ConversationScreen.kt @@ -90,7 +90,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import java.util.Locale -private const val AUTO_REPOSITION_INTERVAL = 150L /** * Screen for LLM conversation with user. diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsBrowsingScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsBrowsingScreen.kt index 55cd1ef4e7..1a52a35310 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsBrowsingScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsBrowsingScreen.kt @@ -22,6 +22,7 @@ import com.example.llama.viewmodel.PreselectedModelToRun @Composable fun ModelsBrowsingScreen( + showChatTooltip: Boolean, filteredModels: List?, activeFiltersCount: Int, preselection: PreselectedModelToRun?, @@ -58,7 +59,7 @@ fun ModelsBrowsingScreen( } else { // Model cards LazyColumn( - Modifier.fillMaxSize(), // .padding(horizontal = 16.dp), + Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp), contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp), ) { diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt index 38c882cfc2..c4cdbe1e03 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt @@ -19,15 +19,19 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.Help import androidx.compose.material.icons.automirrored.outlined.ContactSupport +import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Attribution +import androidx.compose.material.icons.filled.Celebration import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Today import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox @@ -80,10 +84,11 @@ import java.util.Locale */ @Composable fun ModelsManagementAndDeletingScreen( + showModelImportTooltip: Boolean, + onFirstModelImportSuccess: (ModelInfo) -> Unit, filteredModels: List?, activeFiltersCount: Int, isDeleting: Boolean, - onFirstModelImportSuccess: (() -> Unit)?, onScaffoldEvent: (ScaffoldEvent) -> Unit, modelsViewModel: ModelsViewModel, managementViewModel: ModelsManagementViewModel, @@ -118,7 +123,7 @@ fun ModelsManagementAndDeletingScreen( } val message = "If you already have GGUF models on your computer, " + - "please transfer it onto your device, and then select \"Import a local model\".\n\n" + + "please transfer it onto your device, and then select \"Import a local GGUF model\".\n\n" + "Otherwise, select \"Download from HuggingFace\" and pick one you like." InfoView( @@ -208,14 +213,20 @@ fun ModelsManagementAndDeletingScreen( } is Importation.Success -> { - LaunchedEffect(state) { - onScaffoldEvent( - ScaffoldEvent.ShowSnackbar( - message = "Imported model: ${state.model.name}" + if (showModelImportTooltip) { + FirstModelImportSuccessDialog { + onFirstModelImportSuccess(state.model) + managementViewModel.resetManagementState() + } + } else { + LaunchedEffect(state) { + onScaffoldEvent( + ScaffoldEvent.ShowSnackbar( + message = "Imported model: ${state.model.name}" + ) ) - ) - onFirstModelImportSuccess?.invoke() - managementViewModel.resetManagementState() + managementViewModel.resetManagementState() + } } } @@ -616,6 +627,34 @@ fun HuggingFaceModelListItem( } } +@Composable +private fun FirstModelImportSuccessDialog( + onConfirm: () -> Unit, +) { + AlertDialog( + // Prevent dismissal via back button during deletion + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ), + onDismissRequest = {}, + text = { + InfoView( + title = "Congratulations", + icon = Icons.Default.Celebration, + message = "You have just installed your first Large Language Model!\n\n" + + "Tap \"Continue\" to check it out!", + action = InfoAction( + label = "Continue", + icon = Icons.AutoMirrored.Default.ArrowForward, + onAction = onConfirm + ) + ) + }, + confirmButton = {} + ) +} + @Composable private fun BatchDeleteConfirmationDialog( count: Int, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsScreen.kt index b2a89cb733..7018b33c11 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsScreen.kt @@ -33,8 +33,10 @@ import com.example.llama.viewmodel.PreselectedModelToRun.RamWarning @OptIn(ExperimentalMaterial3Api::class) @Composable fun ModelsScreen( + showModelImportTooltip: Boolean, + onFirstModelImportSuccess: (ModelInfo) -> Unit, + showChatTooltip: Boolean, onConfirmSelection: (ModelInfo, RamWarning) -> Unit, - onFirstModelImportSuccess: (() -> Unit)?, onScaffoldEvent: (ScaffoldEvent) -> Unit, modelsViewModel: ModelsViewModel, managementViewModel: ModelsManagementViewModel, @@ -79,6 +81,7 @@ fun ModelsScreen( when (currentMode) { ModelScreenUiMode.BROWSING -> ModelsBrowsingScreen( + showChatTooltip = showChatTooltip, filteredModels = filteredModels, preselection = preselection, onManageModelsClicked = { @@ -95,11 +98,12 @@ fun ModelsScreen( ) ModelScreenUiMode.MANAGING, ModelScreenUiMode.DELETING -> ModelsManagementAndDeletingScreen( + showModelImportTooltip = showModelImportTooltip, + onFirstModelImportSuccess = onFirstModelImportSuccess, filteredModels = filteredModels, isDeleting = currentMode == ModelScreenUiMode.DELETING, onScaffoldEvent = onScaffoldEvent, activeFiltersCount = activeFiltersCount, - onFirstModelImportSuccess = onFirstModelImportSuccess, modelsViewModel = modelsViewModel, managementViewModel = managementViewModel, )