UI: adds AppPreferences to track user onboarding status

This commit is contained in:
Han Yin 2025-08-30 22:34:30 -07:00
parent a9b84b9db3
commit c87ff9c1b3
5 changed files with 130 additions and 22 deletions

View File

@ -94,8 +94,9 @@ fun AppContent(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
// Inference engine state // App core states
val engineState by mainViewModel.engineState.collectAsState() val engineState by mainViewModel.engineState.collectAsState()
val showUserOnboarding by mainViewModel.showUserOnboarding.collectAsState()
// Model state // Model state
val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState() val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState()
@ -323,7 +324,7 @@ fun AppContent(
toggleMenu = modelsViewModel::toggleFilterMenu toggleMenu = modelsViewModel::toggleFilterMenu
), ),
importing = BottomBarConfig.Models.Managing.ImportConfig( importing = BottomBarConfig.Models.Managing.ImportConfig(
showTooltip = true, showTooltip = showUserOnboarding,
isMenuVisible = showImportModelMenu, isMenuVisible = showImportModelMenu,
toggleMenu = { show -> modelsManagementViewModel.toggleImportMenu(show) }, toggleMenu = { show -> modelsManagementViewModel.toggleImportMenu(show) },
importFromLocal = { importFromLocal = {

View File

@ -0,0 +1,67 @@
package com.example.llama.data.source.prefs
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* Manages internal preferences for the application.
*/
@Singleton
class AppPreferences @Inject constructor (
@ApplicationContext private val context: Context
) {
companion object {
private const val DATASTORE_APP = "app"
private val Context.appDataStore: DataStore<Preferences>
by preferencesDataStore(name = DATASTORE_APP)
// Preference keys
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")
}
/**
* Gets whether the user has imported his first model
*/
fun userHasImportedFirstModel(): Flow<Boolean> =
context.appDataStore.data.map { preferences ->
preferences[USER_HAS_IMPORTED_FIRST_MODEL] == true
}
/**
* Sets whether the user has completed importing the first model.
*/
suspend fun setUserHasImportedFirstModel(done: Boolean) = withContext(Dispatchers.IO) {
context.appDataStore.edit { preferences ->
preferences[USER_HAS_IMPORTED_FIRST_MODEL] = done
}
}
/**
* Gets whether the user has chatted with a model
*/
fun userHasChattedWithModel(): Flow<Boolean> =
context.appDataStore.data.map { preferences ->
preferences[USER_HAS_CHATTED_WITH_MODEL] == true
}
/**
* Sets whether the user has completed chatting with a model.
*/
suspend fun setUserHasChattedWithModel(done: Boolean) = withContext(Dispatchers.IO) {
context.appDataStore.edit { preferences ->
preferences[USER_HAS_CHATTED_WITH_MODEL] = done
}
}
}

View File

@ -9,8 +9,10 @@ import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -23,11 +25,11 @@ class UserPreferences @Inject constructor (
) { ) {
companion object { companion object {
// Performance monitoring preferences
private const val DATASTORE_SETTINGS = "settings" private const val DATASTORE_SETTINGS = "settings"
private val Context.settingsDataStore: DataStore<Preferences> private val Context.settingsDataStore: DataStore<Preferences>
by preferencesDataStore(name = DATASTORE_SETTINGS) by preferencesDataStore(name = DATASTORE_SETTINGS)
// Preferences keys
private val PERFORMANCE_MONITORING_ENABLED = booleanPreferencesKey("performance_monitoring_enabled") private val PERFORMANCE_MONITORING_ENABLED = booleanPreferencesKey("performance_monitoring_enabled")
private val USE_FAHRENHEIT_TEMPERATURE = booleanPreferencesKey("use_fahrenheit_temperature") private val USE_FAHRENHEIT_TEMPERATURE = booleanPreferencesKey("use_fahrenheit_temperature")
private val MONITORING_INTERVAL_MS = longPreferencesKey("monitoring_interval_ms") private val MONITORING_INTERVAL_MS = longPreferencesKey("monitoring_interval_ms")
@ -45,16 +47,15 @@ class UserPreferences @Inject constructor (
/** /**
* Gets whether performance monitoring is enabled. * Gets whether performance monitoring is enabled.
*/ */
fun isPerformanceMonitoringEnabled(): Flow<Boolean> { fun isPerformanceMonitoringEnabled(): Flow<Boolean> =
return context.settingsDataStore.data.map { preferences -> context.settingsDataStore.data.map { preferences ->
preferences[PERFORMANCE_MONITORING_ENABLED] != false preferences[PERFORMANCE_MONITORING_ENABLED] != false
} }
}
/** /**
* Sets whether performance monitoring is enabled. * Sets whether performance monitoring is enabled.
*/ */
suspend fun setPerformanceMonitoringEnabled(enabled: Boolean) { suspend fun setPerformanceMonitoringEnabled(enabled: Boolean) = withContext(Dispatchers.IO) {
context.settingsDataStore.edit { preferences -> context.settingsDataStore.edit { preferences ->
preferences[PERFORMANCE_MONITORING_ENABLED] = enabled preferences[PERFORMANCE_MONITORING_ENABLED] = enabled
} }
@ -63,16 +64,15 @@ class UserPreferences @Inject constructor (
/** /**
* Gets whether temperature should be displayed in Fahrenheit. * Gets whether temperature should be displayed in Fahrenheit.
*/ */
fun usesFahrenheitTemperature(): Flow<Boolean> { fun usesFahrenheitTemperature(): Flow<Boolean> =
return context.settingsDataStore.data.map { preferences -> context.settingsDataStore.data.map { preferences ->
preferences[USE_FAHRENHEIT_TEMPERATURE] == true preferences[USE_FAHRENHEIT_TEMPERATURE] == true
} }
}
/** /**
* Sets whether temperature should be displayed in Fahrenheit. * Sets whether temperature should be displayed in Fahrenheit.
*/ */
suspend fun setUseFahrenheitTemperature(useFahrenheit: Boolean) { suspend fun setUseFahrenheitTemperature(useFahrenheit: Boolean) = withContext(Dispatchers.IO) {
context.settingsDataStore.edit { preferences -> context.settingsDataStore.edit { preferences ->
preferences[USE_FAHRENHEIT_TEMPERATURE] = useFahrenheit preferences[USE_FAHRENHEIT_TEMPERATURE] = useFahrenheit
} }
@ -83,16 +83,15 @@ class UserPreferences @Inject constructor (
* *
* TODO-han.yin: replace with Enum value instead of millisecond value * TODO-han.yin: replace with Enum value instead of millisecond value
*/ */
fun getMonitoringInterval(): Flow<Long> { fun getMonitoringInterval(): Flow<Long> =
return context.settingsDataStore.data.map { preferences -> context.settingsDataStore.data.map { preferences ->
preferences[MONITORING_INTERVAL_MS] ?: DEFAULT_MONITORING_INTERVAL_MS preferences[MONITORING_INTERVAL_MS] ?: DEFAULT_MONITORING_INTERVAL_MS
} }
}
/** /**
* Sets the monitoring interval in milliseconds. * Sets the monitoring interval in milliseconds.
*/ */
suspend fun setMonitoringInterval(intervalMs: Long) { suspend fun setMonitoringInterval(intervalMs: Long) = withContext(Dispatchers.IO) {
context.settingsDataStore.edit { preferences -> context.settingsDataStore.edit { preferences ->
preferences[MONITORING_INTERVAL_MS] = intervalMs preferences[MONITORING_INTERVAL_MS] = intervalMs
} }
@ -101,16 +100,15 @@ class UserPreferences @Inject constructor (
/** /**
* Gets the current theme mode. * Gets the current theme mode.
*/ */
fun getThemeMode(): Flow<Int> { fun getThemeMode(): Flow<Int> =
return context.settingsDataStore.data.map { preferences -> context.settingsDataStore.data.map { preferences ->
preferences[THEME_MODE] ?: THEME_MODE_AUTO preferences[THEME_MODE] ?: THEME_MODE_AUTO
} }
}
/** /**
* Sets the theme mode. * Sets the theme mode.
*/ */
suspend fun setThemeMode(mode: Int) { suspend fun setThemeMode(mode: Int) = withContext(Dispatchers.IO) {
context.settingsDataStore.edit { preferences -> context.settingsDataStore.edit { preferences ->
preferences[THEME_MODE] = mode preferences[THEME_MODE] = mode
} }

View File

@ -1,23 +1,65 @@
package com.example.llama.viewmodel package com.example.llama.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.llama.data.source.prefs.AppPreferences
import com.example.llama.engine.InferenceService import com.example.llama.engine.InferenceService
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
/** /**
* Main ViewModel that expose the core states of [InferenceEngine] * Main ViewModel that expose the core states of [InferenceService] and [AppPreferences]
*/ */
class MainViewModel @Inject constructor ( class MainViewModel @Inject constructor (
private val appPreferences: AppPreferences,
private val inferenceService: InferenceService, private val inferenceService: InferenceService,
) : ViewModel() { ) : ViewModel() {
val engineState = inferenceService.engineState val engineState = inferenceService.engineState
// App preferences
private val _showModelImportTooltip = MutableStateFlow(true)
val showModelImportTooltip: StateFlow<Boolean> = _showModelImportTooltip.asStateFlow()
private val _showChatTooltip = MutableStateFlow(true)
val showChatTooltip: StateFlow<Boolean> = _showChatTooltip.asStateFlow()
/** /**
* Unload the current model and release the resources * Unload the current model and release the resources
*/ */
suspend fun unloadModel() = inferenceService.unloadModel() suspend fun unloadModel() = inferenceService.unloadModel()
init {
viewModelScope.launch {
launch {
appPreferences.userHasImportedFirstModel().collect {
_showModelImportTooltip.value = !it
}
}
launch {
appPreferences.userHasChattedWithModel().collect {
_showChatTooltip.value = !it
}
}
}
} }
fun waiveModelImportTooltip() {
android.util.Log.w("JOJO", "WAIVE IMPORT TOOLTIP!")
viewModelScope.launch {
appPreferences.setUserHasImportedFirstModel(true)
}
}
fun waiveChatTooltip() {
viewModelScope.launch {
appPreferences.setUserHasChattedWithModel(true)
}
}
}

View File

@ -4,8 +4,8 @@ import android.llama.cpp.LLamaTier
import android.llama.cpp.TierDetection import android.llama.cpp.TierDetection
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.llama.data.source.prefs.UserPreferences
import com.example.llama.data.repo.ModelRepository import com.example.llama.data.repo.ModelRepository
import com.example.llama.data.source.prefs.UserPreferences
import com.example.llama.monitoring.BatteryMetrics import com.example.llama.monitoring.BatteryMetrics
import com.example.llama.monitoring.MemoryMetrics import com.example.llama.monitoring.MemoryMetrics
import com.example.llama.monitoring.PerformanceMonitor import com.example.llama.monitoring.PerformanceMonitor