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 snackbarHostState = remember { SnackbarHostState() }
// Inference engine state
// App core states
val engineState by mainViewModel.engineState.collectAsState()
val showUserOnboarding by mainViewModel.showUserOnboarding.collectAsState()
// Model state
val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState()
@ -323,7 +324,7 @@ fun AppContent(
toggleMenu = modelsViewModel::toggleFilterMenu
),
importing = BottomBarConfig.Models.Managing.ImportConfig(
showTooltip = true,
showTooltip = showUserOnboarding,
isMenuVisible = showImportModelMenu,
toggleMenu = { show -> modelsManagementViewModel.toggleImportMenu(show) },
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.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
@ -23,11 +25,11 @@ class UserPreferences @Inject constructor (
) {
companion object {
// Performance monitoring preferences
private const val DATASTORE_SETTINGS = "settings"
private val Context.settingsDataStore: DataStore<Preferences>
by preferencesDataStore(name = DATASTORE_SETTINGS)
// Preferences keys
private val PERFORMANCE_MONITORING_ENABLED = booleanPreferencesKey("performance_monitoring_enabled")
private val USE_FAHRENHEIT_TEMPERATURE = booleanPreferencesKey("use_fahrenheit_temperature")
private val MONITORING_INTERVAL_MS = longPreferencesKey("monitoring_interval_ms")
@ -45,16 +47,15 @@ class UserPreferences @Inject constructor (
/**
* Gets whether performance monitoring is enabled.
*/
fun isPerformanceMonitoringEnabled(): Flow<Boolean> {
return context.settingsDataStore.data.map { preferences ->
fun isPerformanceMonitoringEnabled(): Flow<Boolean> =
context.settingsDataStore.data.map { preferences ->
preferences[PERFORMANCE_MONITORING_ENABLED] != false
}
}
/**
* Sets whether performance monitoring is enabled.
*/
suspend fun setPerformanceMonitoringEnabled(enabled: Boolean) {
suspend fun setPerformanceMonitoringEnabled(enabled: Boolean) = withContext(Dispatchers.IO) {
context.settingsDataStore.edit { preferences ->
preferences[PERFORMANCE_MONITORING_ENABLED] = enabled
}
@ -63,16 +64,15 @@ class UserPreferences @Inject constructor (
/**
* Gets whether temperature should be displayed in Fahrenheit.
*/
fun usesFahrenheitTemperature(): Flow<Boolean> {
return context.settingsDataStore.data.map { preferences ->
fun usesFahrenheitTemperature(): Flow<Boolean> =
context.settingsDataStore.data.map { preferences ->
preferences[USE_FAHRENHEIT_TEMPERATURE] == true
}
}
/**
* 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 ->
preferences[USE_FAHRENHEIT_TEMPERATURE] = useFahrenheit
}
@ -83,16 +83,15 @@ class UserPreferences @Inject constructor (
*
* TODO-han.yin: replace with Enum value instead of millisecond value
*/
fun getMonitoringInterval(): Flow<Long> {
return context.settingsDataStore.data.map { preferences ->
fun getMonitoringInterval(): Flow<Long> =
context.settingsDataStore.data.map { preferences ->
preferences[MONITORING_INTERVAL_MS] ?: DEFAULT_MONITORING_INTERVAL_MS
}
}
/**
* Sets the monitoring interval in milliseconds.
*/
suspend fun setMonitoringInterval(intervalMs: Long) {
suspend fun setMonitoringInterval(intervalMs: Long) = withContext(Dispatchers.IO) {
context.settingsDataStore.edit { preferences ->
preferences[MONITORING_INTERVAL_MS] = intervalMs
}
@ -101,16 +100,15 @@ class UserPreferences @Inject constructor (
/**
* Gets the current theme mode.
*/
fun getThemeMode(): Flow<Int> {
return context.settingsDataStore.data.map { preferences ->
fun getThemeMode(): Flow<Int> =
context.settingsDataStore.data.map { preferences ->
preferences[THEME_MODE] ?: THEME_MODE_AUTO
}
}
/**
* Sets the theme mode.
*/
suspend fun setThemeMode(mode: Int) {
suspend fun setThemeMode(mode: Int) = withContext(Dispatchers.IO) {
context.settingsDataStore.edit { preferences ->
preferences[THEME_MODE] = mode
}

View File

@ -1,23 +1,65 @@
package com.example.llama.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.llama.data.source.prefs.AppPreferences
import com.example.llama.engine.InferenceService
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
@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 (
private val appPreferences: AppPreferences,
private val inferenceService: InferenceService,
) : ViewModel() {
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
*/
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 androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.llama.data.source.prefs.UserPreferences
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.MemoryMetrics
import com.example.llama.monitoring.PerformanceMonitor