UX: implement onboarding tooltips for model import and onboarding
This commit is contained in:
parent
1c73f6215f
commit
5471635c9d
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ sealed class BottomBarConfig {
|
|||
) : BottomBarConfig()
|
||||
|
||||
data class RunActionConfig(
|
||||
val showTooltip: Boolean,
|
||||
val preselectedModelToRun: PreselectedModelToRun?,
|
||||
val onClickRun: (PreselectedModelToRun) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import java.util.Locale
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StorageTopBar(
|
||||
fun ModelsManagementTopBar(
|
||||
title: String,
|
||||
storageMetrics: StorageMetrics?,
|
||||
onScaffoldEvent: (ScaffoldEvent) -> Unit,
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.example.llama.viewmodel.PreselectedModelToRun
|
|||
|
||||
@Composable
|
||||
fun ModelsBrowsingScreen(
|
||||
showChatTooltip: Boolean,
|
||||
filteredModels: List<ModelInfo>?,
|
||||
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),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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<ModelInfo>?,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue