UI: address Rojin's UX feedbacks

This commit is contained in:
Han Yin 2025-09-01 20:27:49 -07:00
parent 6fb4a94cc3
commit e067f7051b
9 changed files with 78 additions and 10 deletions

View File

@ -99,6 +99,7 @@ fun AppContent(
val engineState by mainViewModel.engineState.collectAsState() val engineState by mainViewModel.engineState.collectAsState()
val showModelImportTooltip by mainViewModel.showModelImportTooltip.collectAsState() val showModelImportTooltip by mainViewModel.showModelImportTooltip.collectAsState()
val showChatTooltip by mainViewModel.showChatTooltip.collectAsState() val showChatTooltip by mainViewModel.showChatTooltip.collectAsState()
val showManagementTooltip by mainViewModel.showModelManagementTooltip.collectAsState()
// Model state // Model state
val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState() val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState()
@ -223,9 +224,11 @@ fun AppContent(
modelsViewModel.resetPreselection() modelsViewModel.resetPreselection()
openDrawer() openDrawer()
}, },
showTooltip = showManagementTooltip && !showChatTooltip && hasModelsInstalled,
showManagingToggle = !showChatTooltip && hasModelsInstalled, showManagingToggle = !showChatTooltip && hasModelsInstalled,
onToggleManaging = { onToggleManaging = {
if (hasModelsInstalled) { if (hasModelsInstalled) {
mainViewModel.waiveModelManagementTooltip()
modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING)
} }
}, },

View File

@ -29,6 +29,7 @@ class AppPreferences @Inject constructor (
// Preference keys // Preference keys
private val USER_HAS_IMPORTED_FIRST_MODEL = booleanPreferencesKey("user_has_imported_first_model") private val USER_HAS_IMPORTED_FIRST_MODEL = booleanPreferencesKey("user_has_imported_first_model")
private val USER_HAS_CHATTED_WITH_MODEL = booleanPreferencesKey("user_has_chatted_with_model") private val USER_HAS_CHATTED_WITH_MODEL = booleanPreferencesKey("user_has_chatted_with_model")
private val USER_HAS_NAVIGATED_TO_MANAGEMENT = booleanPreferencesKey("user_has_navigated_to_management")
} }
/** /**
@ -64,4 +65,21 @@ class AppPreferences @Inject constructor (
preferences[USER_HAS_CHATTED_WITH_MODEL] = done preferences[USER_HAS_CHATTED_WITH_MODEL] = done
} }
} }
/**
* Gets whether the user has navigated to model management screen.
*/
fun userHasNavigatedToManagement(): Flow<Boolean> =
context.appDataStore.data.map { preferences ->
preferences[USER_HAS_NAVIGATED_TO_MANAGEMENT] == true
}
/**
* Sets whether the user has navigated to model management screen.
*/
suspend fun setUserHasNavigatedToManagement(done: Boolean) = withContext(Dispatchers.IO) {
context.appDataStore.edit { preferences ->
preferences[USER_HAS_NAVIGATED_TO_MANAGEMENT] = done
}
}
} }

View File

@ -71,6 +71,7 @@ fun AppScaffold(
is TopBarConfig.ModelsBrowsing -> ModelsBrowsingTopBar( is TopBarConfig.ModelsBrowsing -> ModelsBrowsingTopBar(
title = topBarconfig.title, title = topBarconfig.title,
showTooltip = topBarconfig.showTooltip,
showManagingToggle = topBarconfig.showManagingToggle, showManagingToggle = topBarconfig.showManagingToggle,
onToggleManaging = topBarconfig.onToggleManaging, onToggleManaging = topBarconfig.onToggleManaging,
onNavigateBack = topBarconfig.navigationIcon.backAction, onNavigateBack = topBarconfig.navigationIcon.backAction,

View File

@ -207,7 +207,7 @@ fun ModelsManagementBottomBar(
onDismissRequest = { importingConfig.toggleMenu(false) } onDismissRequest = { importingConfig.toggleMenu(false) }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Import a local GGUF model") }, text = { Text("Import a local model") },
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = Icons.Default.FolderOpen, imageVector = Icons.Default.FolderOpen,
@ -217,7 +217,7 @@ fun ModelsManagementBottomBar(
onClick = importingConfig.importFromLocal onClick = importingConfig.importFromLocal
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Download from HuggingFace") }, text = { Text("Download a model from Hugging Face") },
leadingIcon = { leadingIcon = {
Icon( Icon(
painter = painterResource(id = R.drawable.logo_huggingface), painter = painterResource(id = R.drawable.logo_huggingface),

View File

@ -12,10 +12,16 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -24,11 +30,23 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun ModelsBrowsingTopBar( fun ModelsBrowsingTopBar(
title: String, title: String,
showTooltip: Boolean,
showManagingToggle: Boolean, showManagingToggle: Boolean,
onToggleManaging: () -> Unit, onToggleManaging: () -> Unit,
onNavigateBack: (() -> Unit)? = null, onNavigateBack: (() -> Unit)? = null,
onMenuOpen: (() -> Unit)? = null, onMenuOpen: (() -> Unit)? = null,
) { ) {
val tooltipState = rememberTooltipState(
initialIsVisible = showTooltip,
isPersistent = showTooltip
)
LaunchedEffect(showTooltip, showManagingToggle) {
if (showTooltip && showManagingToggle) {
tooltipState.show()
}
}
TopAppBar( TopAppBar(
title = { Text(title) }, title = { Text(title) },
navigationIcon = { navigationIcon = {
@ -54,8 +72,20 @@ fun ModelsBrowsingTopBar(
}, },
actions = { actions = {
if (showManagingToggle) { if (showManagingToggle) {
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
TooltipAnchorPosition.Below),
state = tooltipState,
tooltip = {
PlainTooltip {
Text("Tap this button to install another model or manage your models!")
}
},
onDismissRequest = {}
) {
ModelManageActionToggle(onToggleManaging) ModelManageActionToggle(onToggleManaging)
} }
}
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,

View File

@ -28,6 +28,7 @@ sealed class TopBarConfig {
data class ModelsBrowsing( data class ModelsBrowsing(
override val title: String, override val title: String,
override val navigationIcon: NavigationIcon, override val navigationIcon: NavigationIcon,
val showTooltip: Boolean,
val showManagingToggle: Boolean, val showManagingToggle: Boolean,
val onToggleManaging: () -> Unit, val onToggleManaging: () -> Unit,
) : TopBarConfig() ) : TopBarConfig()

View File

@ -43,6 +43,7 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -250,6 +251,7 @@ fun ModelLoadingScreen(
if (!showedSystemPromptWarning) { if (!showedSystemPromptWarning) {
onScaffoldEvent(ScaffoldEvent.ShowSnackbar( onScaffoldEvent(ScaffoldEvent.ShowSnackbar(
message = "Model may not support system prompt!\nProceed with caution.", message = "Model may not support system prompt!\nProceed with caution.",
duration = SnackbarDuration.Long,
)) ))
showedSystemPromptWarning = true showedSystemPromptWarning = true
} }

View File

@ -117,7 +117,7 @@ fun ModelsManagementAndDeletingScreen(
} else if (filteredModels.isEmpty()) { } else if (filteredModels.isEmpty()) {
// Prompt the user to import a model // Prompt the user to import a model
val title = when (activeFiltersCount) { val title = when (activeFiltersCount) {
0 -> "Import or download,\n have it your way" 0 -> "Choose a model:\nImport locally or download online"
1 -> "No models match\n the selected filter" 1 -> "No models match\n the selected filter"
else -> "No models match\n the selected filters" else -> "No models match\n the selected filters"
} }

View File

@ -29,6 +29,9 @@ class MainViewModel @Inject constructor (
private val _showChatTooltip = MutableStateFlow(true) private val _showChatTooltip = MutableStateFlow(true)
val showChatTooltip: StateFlow<Boolean> = _showChatTooltip.asStateFlow() val showChatTooltip: StateFlow<Boolean> = _showChatTooltip.asStateFlow()
private val _showModelManagementTooltip = MutableStateFlow(true)
val showModelManagementTooltip: StateFlow<Boolean> = _showModelManagementTooltip.asStateFlow()
/** /**
* Unload the current model and release the resources * Unload the current model and release the resources
@ -47,12 +50,11 @@ class MainViewModel @Inject constructor (
_showChatTooltip.value = !it _showChatTooltip.value = !it
} }
} }
launch {
appPreferences.userHasNavigatedToManagement().collect {
_showModelManagementTooltip.value = !it
} }
} }
fun waiveModelImportTooltip() {
viewModelScope.launch {
appPreferences.setUserHasImportedFirstModel(true)
} }
} }
@ -61,4 +63,15 @@ class MainViewModel @Inject constructor (
appPreferences.setUserHasChattedWithModel(true) appPreferences.setUserHasChattedWithModel(true)
} }
} }
fun waiveModelImportTooltip() {
viewModelScope.launch {
appPreferences.setUserHasImportedFirstModel(true)
}
}
fun waiveModelManagementTooltip() {
viewModelScope.launch {
appPreferences.setUserHasNavigatedToManagement(true)
}
}
} }