UI: centralize the AppScaffold and modularize its configs
This commit is contained in:
parent
72e97b93c5
commit
63fc56d603
|
|
@ -4,17 +4,22 @@ import android.llama.cpp.InferenceEngine.State
|
|||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -24,6 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
|
|
@ -32,6 +38,10 @@ import com.example.llama.revamp.navigation.AppDestinations
|
|||
import com.example.llama.revamp.navigation.NavigationActions
|
||||
import com.example.llama.revamp.ui.components.AnimatedNavHost
|
||||
import com.example.llama.revamp.ui.components.AppNavigationDrawer
|
||||
import com.example.llama.revamp.ui.components.AppScaffold
|
||||
import com.example.llama.revamp.ui.components.BottomBarConfig
|
||||
import com.example.llama.revamp.ui.components.ScaffoldEvent
|
||||
import com.example.llama.revamp.ui.components.TopBarConfig
|
||||
import com.example.llama.revamp.ui.components.UnloadModelConfirmationDialog
|
||||
import com.example.llama.revamp.ui.screens.BenchmarkScreen
|
||||
import com.example.llama.revamp.ui.screens.ConversationScreen
|
||||
|
|
@ -42,6 +52,8 @@ import com.example.llama.revamp.ui.screens.SettingsGeneralScreen
|
|||
import com.example.llama.revamp.ui.theme.LlamaTheme
|
||||
import com.example.llama.revamp.viewmodel.ConversationViewModel
|
||||
import com.example.llama.revamp.viewmodel.MainViewModel
|
||||
import com.example.llama.revamp.viewmodel.ModelsManagementViewModel
|
||||
import com.example.llama.revamp.viewmodel.PerformanceViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -66,10 +78,13 @@ class MainActivity : ComponentActivity() {
|
|||
@Composable
|
||||
fun AppContent(
|
||||
mainViewModel: MainViewModel = hiltViewModel(),
|
||||
performanceViewModel: PerformanceViewModel = hiltViewModel(),
|
||||
modelsManagementViewModel: ModelsManagementViewModel = hiltViewModel(),
|
||||
conversationViewModel: ConversationViewModel = hiltViewModel(),
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// Inference engine state
|
||||
val engineState by mainViewModel.engineState.collectAsState()
|
||||
|
|
@ -87,45 +102,34 @@ fun AppContent(
|
|||
}
|
||||
}
|
||||
|
||||
// Metric states for scaffolds
|
||||
val memoryUsage by performanceViewModel.memoryUsage.collectAsState()
|
||||
val temperatureInfo by performanceViewModel.temperatureMetrics.collectAsState()
|
||||
val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState()
|
||||
val storageMetrics by performanceViewModel.storageMetrics.collectAsState()
|
||||
|
||||
// Navigation
|
||||
val navController = rememberNavController()
|
||||
val navigationActions = remember(navController) { NavigationActions(navController) }
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute by remember {
|
||||
val currentRoute by remember(navBackStackEntry) {
|
||||
derivedStateOf { navBackStackEntry?.destination?.route ?: "" }
|
||||
}
|
||||
var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||
LaunchedEffect(navController) {
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
// Log navigation for debugging
|
||||
println("Navigation: ${destination.route}")
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if current route requires model unloading
|
||||
val routeNeedsModelUnloading by remember(currentRoute) {
|
||||
derivedStateOf {
|
||||
currentRoute == AppDestinations.CONVERSATION_ROUTE
|
||||
|| currentRoute == AppDestinations.BENCHMARK_ROUTE
|
||||
|| currentRoute == AppDestinations.MODEL_LOADING_ROUTE
|
||||
}
|
||||
}
|
||||
// Model unloading confirmation
|
||||
var showUnloadDialog by remember { mutableStateOf(false) }
|
||||
val handleBackWithModelCheck = {
|
||||
when {
|
||||
isModelUninterruptible -> {
|
||||
// If model is non-interruptible at all, ignore the request
|
||||
true // Mark as handled
|
||||
}
|
||||
isModelLoaded -> {
|
||||
showUnloadDialog = true
|
||||
pendingNavigation = { navigationActions.navigateUp() }
|
||||
true // Mark as handled
|
||||
}
|
||||
else -> {
|
||||
navigationActions.navigateUp()
|
||||
true // Mark as handled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,128 +146,260 @@ fun AppContent(
|
|||
}
|
||||
val openDrawer: () -> Unit = { coroutineScope.launch { drawerState.open() } }
|
||||
|
||||
// Register a system back handler for screens that need unload confirmation
|
||||
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
DisposableEffect(lifecycleOwner, backDispatcher, currentRoute, isModelLoaded) {
|
||||
val callback = object : OnBackPressedCallback(
|
||||
// Only enable for screens that need model unloading confirmation
|
||||
routeNeedsModelUnloading && isModelLoaded
|
||||
) {
|
||||
override fun handleOnBackPressed() {
|
||||
handleBackWithModelCheck()
|
||||
// Create scaffold's top & bottom bar configs based on current route
|
||||
val topBarConfig = when (currentRoute) {
|
||||
// (Home) Model selection screen
|
||||
AppDestinations.MODEL_SELECTION_ROUTE -> {
|
||||
TopBarConfig.Default(
|
||||
title = "Models",
|
||||
navigationIcon = TopBarConfig.NavigationIcon.Menu(openDrawer)
|
||||
)
|
||||
}
|
||||
|
||||
// Settings screen
|
||||
AppDestinations.SETTINGS_GENERAL_ROUTE -> {
|
||||
TopBarConfig.Default(
|
||||
title = "Settings",
|
||||
navigationIcon = TopBarConfig.NavigationIcon.Back { navigationActions.navigateUp() }
|
||||
)
|
||||
}
|
||||
|
||||
// Storage management screen
|
||||
AppDestinations.MODELS_MANAGEMENT_ROUTE -> {
|
||||
TopBarConfig.Storage(
|
||||
title = "Models Management",
|
||||
navigationIcon = TopBarConfig.NavigationIcon.Back { navigationActions.navigateUp() },
|
||||
storageMetrics = storageMetrics
|
||||
)
|
||||
}
|
||||
|
||||
// Model loading screen
|
||||
AppDestinations.MODEL_LOADING_ROUTE -> {
|
||||
TopBarConfig.Performance(
|
||||
title = "Load Model",
|
||||
navigationIcon = TopBarConfig.NavigationIcon.Back(handleBackWithModelCheck),
|
||||
memoryMetrics = memoryUsage,
|
||||
temperatureInfo = null
|
||||
)
|
||||
}
|
||||
|
||||
// Benchmark and Conversation screens
|
||||
AppDestinations.BENCHMARK_ROUTE, AppDestinations.CONVERSATION_ROUTE -> {
|
||||
TopBarConfig.Performance(
|
||||
title = when(currentRoute) {
|
||||
AppDestinations.CONVERSATION_ROUTE -> "Chat"
|
||||
AppDestinations.BENCHMARK_ROUTE -> "Benchmark"
|
||||
else -> "LlamaAndroid"
|
||||
},
|
||||
navigationIcon = TopBarConfig.NavigationIcon.Back(handleBackWithModelCheck),
|
||||
memoryMetrics = memoryUsage,
|
||||
temperatureInfo = Pair(temperatureInfo, useFahrenheit)
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback for unknown routes
|
||||
else -> {
|
||||
TopBarConfig.Default(
|
||||
title = "LlamaAndroid",
|
||||
navigationIcon = TopBarConfig.NavigationIcon.None
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val bottomBarConfig = when (currentRoute) {
|
||||
AppDestinations.MODELS_MANAGEMENT_ROUTE -> {
|
||||
// Collect the needed states
|
||||
val sortOrder by modelsManagementViewModel.sortOrder.collectAsState()
|
||||
val isMultiSelectionMode by modelsManagementViewModel.isMultiSelectionMode.collectAsState()
|
||||
val selectedModels by modelsManagementViewModel.selectedModels.collectAsState()
|
||||
val showSortMenu by modelsManagementViewModel.showSortMenu.collectAsState()
|
||||
val showImportModelMenu by modelsManagementViewModel.showImportModelMenu.collectAsState()
|
||||
|
||||
// Create file launcher for importing local models
|
||||
val fileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri -> uri?.let { modelsManagementViewModel.localModelFileSelected(it) } }
|
||||
|
||||
BottomBarConfig.ModelsManagement(
|
||||
isMultiSelectionMode = isMultiSelectionMode,
|
||||
selectedModels = selectedModels,
|
||||
onSelectAll = { modelsManagementViewModel.selectAllModels() },
|
||||
onDeselectAll = { modelsManagementViewModel.clearSelectedModels() },
|
||||
onDeleteSelected = {
|
||||
if (selectedModels.isNotEmpty()) {
|
||||
modelsManagementViewModel.batchDeletionClicked(selectedModels.toMap())
|
||||
}
|
||||
},
|
||||
onSortClicked = { modelsManagementViewModel.toggleSortMenu(true) },
|
||||
onFilterClicked = { /* TODO: implement filtering */ },
|
||||
onDeleteModeClicked = { modelsManagementViewModel.setMultiSelectionMode(true) },
|
||||
onAddModelClicked = { modelsManagementViewModel.toggleImportMenu(true) },
|
||||
onExitSelectionMode = { modelsManagementViewModel.setMultiSelectionMode(false) },
|
||||
showSortMenu = showSortMenu,
|
||||
onSortMenuDismissed = { modelsManagementViewModel.toggleSortMenu(false) },
|
||||
currentSortOrder = sortOrder,
|
||||
onSortOptionSelected = {
|
||||
modelsManagementViewModel.setSortOrder(it)
|
||||
modelsManagementViewModel.toggleSortMenu(false)
|
||||
},
|
||||
showImportModelMenu = showImportModelMenu,
|
||||
onImportMenuDismissed = { modelsManagementViewModel.toggleImportMenu(false) },
|
||||
onImportLocalModelClicked = {
|
||||
fileLauncher.launch(arrayOf("application/octet-stream", "*/*"))
|
||||
modelsManagementViewModel.toggleImportMenu(false)
|
||||
},
|
||||
onImportHuggingFaceClicked = {
|
||||
modelsManagementViewModel.importFromHuggingFace()
|
||||
modelsManagementViewModel.toggleImportMenu(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> BottomBarConfig.None
|
||||
}
|
||||
|
||||
// Handle child screens' scaffold events
|
||||
val handleScaffoldEvent: (ScaffoldEvent) -> Unit = { event ->
|
||||
when (event) {
|
||||
is ScaffoldEvent.ShowSnackbar -> {
|
||||
coroutineScope.launch {
|
||||
if (event.actionLabel != null && event.onAction != null) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = event.message,
|
||||
actionLabel = event.actionLabel,
|
||||
withDismissAction = event.withDismissAction,
|
||||
duration = event.duration
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
event.onAction()
|
||||
}
|
||||
} else {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = event.message,
|
||||
withDismissAction = event.withDismissAction,
|
||||
duration = event.duration
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ScaffoldEvent.ChangeTitle -> {
|
||||
// TODO-han.yin: TBD
|
||||
}
|
||||
}
|
||||
|
||||
backDispatcher?.addCallback(lifecycleOwner, callback)
|
||||
|
||||
// Remove the callback when the effect leaves the composition
|
||||
onDispose {
|
||||
callback.remove()
|
||||
}
|
||||
}
|
||||
// Added protection to handle Compose-based back navigation
|
||||
BackHandler(
|
||||
enabled = routeNeedsModelUnloading && isModelLoaded
|
||||
&& drawerState.currentValue == DrawerValue.Closed
|
||||
) {
|
||||
handleBackWithModelCheck()
|
||||
}
|
||||
|
||||
// Main Content with navigation drawer wrapper
|
||||
// Register system back handler
|
||||
BackHandlerSetup(
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher,
|
||||
currentRoute = currentRoute,
|
||||
isModelLoaded = isModelLoaded,
|
||||
handleBackWithModelCheck = handleBackWithModelCheck
|
||||
)
|
||||
|
||||
// Main UI hierarchy
|
||||
AppNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
navigationActions = navigationActions,
|
||||
gesturesEnabled = drawerGesturesEnabled,
|
||||
currentRoute = currentRoute
|
||||
) {
|
||||
AnimatedNavHost(
|
||||
navController = navController,
|
||||
startDestination = AppDestinations.MODEL_SELECTION_ROUTE
|
||||
) {
|
||||
// Model Selection Screen
|
||||
composable(AppDestinations.MODEL_SELECTION_ROUTE) {
|
||||
ModelSelectionScreen(
|
||||
onModelSelected = { modelInfo ->
|
||||
navigationActions.navigateToModelLoading()
|
||||
},
|
||||
onManageModelsClicked = {
|
||||
navigationActions.navigateToModelsManagement()
|
||||
},
|
||||
onMenuClicked = openDrawer,
|
||||
)
|
||||
}
|
||||
// The AppScaffold now uses the config we created
|
||||
AppScaffold(
|
||||
topBarconfig = topBarConfig,
|
||||
bottomBarConfig = bottomBarConfig,
|
||||
snackbarHostState = snackbarHostState,
|
||||
) { paddingValues ->
|
||||
// AnimatedNavHost inside the scaffold content
|
||||
AnimatedNavHost(
|
||||
navController = navController,
|
||||
startDestination = AppDestinations.MODEL_SELECTION_ROUTE,
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
// Model Selection Screen
|
||||
composable(AppDestinations.MODEL_SELECTION_ROUTE) {
|
||||
ModelSelectionScreen(
|
||||
onModelSelected = { modelInfo ->
|
||||
navigationActions.navigateToModelLoading()
|
||||
},
|
||||
onManageModelsClicked = {
|
||||
navigationActions.navigateToModelsManagement()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Mode Selection Screen
|
||||
composable(AppDestinations.MODEL_LOADING_ROUTE) {
|
||||
ModelLoadingScreen(
|
||||
engineState = engineState,
|
||||
onBenchmarkSelected = { prepareJob ->
|
||||
// Wait for preparation to complete, then navigate if still active
|
||||
val loadingJob = coroutineScope.launch {
|
||||
prepareJob.join()
|
||||
if (isActive) { navigationActions.navigateToBenchmark() }
|
||||
// Mode Selection Screen
|
||||
composable(AppDestinations.MODEL_LOADING_ROUTE) {
|
||||
ModelLoadingScreen(
|
||||
engineState = engineState,
|
||||
onBenchmarkSelected = { prepareJob ->
|
||||
// Wait for preparation to complete, then navigate if still active
|
||||
val loadingJob = coroutineScope.launch {
|
||||
prepareJob.join()
|
||||
if (isActive) { navigationActions.navigateToBenchmark() }
|
||||
}
|
||||
|
||||
pendingNavigation = {
|
||||
prepareJob.cancel()
|
||||
loadingJob.cancel()
|
||||
navigationActions.navigateUp()
|
||||
}
|
||||
},
|
||||
onConversationSelected = { systemPrompt, prepareJob ->
|
||||
// Wait for preparation to complete, then navigate if still active
|
||||
val loadingJob = coroutineScope.launch {
|
||||
prepareJob.join()
|
||||
if (isActive) { navigationActions.navigateToConversation() }
|
||||
}
|
||||
|
||||
pendingNavigation = {
|
||||
prepareJob.cancel()
|
||||
loadingJob.cancel()
|
||||
navigationActions.navigateUp()
|
||||
}
|
||||
},
|
||||
onBackPressed = {
|
||||
// Need to unload model before going back
|
||||
handleBackWithModelCheck()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Benchmark Screen
|
||||
composable(AppDestinations.BENCHMARK_ROUTE) {
|
||||
BenchmarkScreen(
|
||||
onBackPressed = {
|
||||
// Need to unload model before going back
|
||||
handleBackWithModelCheck()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pendingNavigation = {
|
||||
prepareJob.cancel()
|
||||
loadingJob.cancel()
|
||||
navigationActions.navigateUp()
|
||||
}
|
||||
},
|
||||
onConversationSelected = { systemPrompt, prepareJob ->
|
||||
// Wait for preparation to complete, then navigate if still active
|
||||
val loadingJob = coroutineScope.launch {
|
||||
prepareJob.join()
|
||||
if (isActive) { navigationActions.navigateToConversation() }
|
||||
}
|
||||
// Conversation Screen
|
||||
composable(AppDestinations.CONVERSATION_ROUTE) {
|
||||
ConversationScreen(
|
||||
onBackPressed = {
|
||||
// Need to unload model before going back
|
||||
handleBackWithModelCheck()
|
||||
},
|
||||
viewModel = conversationViewModel
|
||||
)
|
||||
}
|
||||
|
||||
pendingNavigation = {
|
||||
prepareJob.cancel()
|
||||
loadingJob.cancel()
|
||||
navigationActions.navigateUp()
|
||||
}
|
||||
},
|
||||
onBackPressed = {
|
||||
// Need to unload model before going back
|
||||
handleBackWithModelCheck()
|
||||
},
|
||||
)
|
||||
}
|
||||
// Settings General Screen
|
||||
composable(AppDestinations.SETTINGS_GENERAL_ROUTE) {
|
||||
SettingsGeneralScreen(
|
||||
onBackPressed = { navigationActions.navigateUp() },
|
||||
)
|
||||
}
|
||||
|
||||
// Benchmark Screen
|
||||
composable(AppDestinations.BENCHMARK_ROUTE) {
|
||||
BenchmarkScreen(
|
||||
onBackPressed = {
|
||||
// Need to unload model before going back
|
||||
handleBackWithModelCheck()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Conversation Screen
|
||||
composable(AppDestinations.CONVERSATION_ROUTE) {
|
||||
ConversationScreen(
|
||||
onBackPressed = {
|
||||
// Need to unload model before going back
|
||||
handleBackWithModelCheck()
|
||||
},
|
||||
viewModel = conversationViewModel
|
||||
)
|
||||
}
|
||||
|
||||
// Settings General Screen
|
||||
composable(AppDestinations.SETTINGS_GENERAL_ROUTE) {
|
||||
SettingsGeneralScreen(
|
||||
onBackPressed = { navigationActions.navigateUp() },
|
||||
onMenuClicked = openDrawer
|
||||
)
|
||||
}
|
||||
|
||||
// Models Management Screen
|
||||
composable(AppDestinations.MODELS_MANAGEMENT_ROUTE) {
|
||||
ModelsManagementScreen(
|
||||
onBackPressed = { navigationActions.navigateUp() },
|
||||
)
|
||||
// Models Management Screen
|
||||
composable(AppDestinations.MODELS_MANAGEMENT_ROUTE) {
|
||||
ModelsManagementScreen(
|
||||
onBackPressed = { navigationActions.navigateUp() },
|
||||
onScaffoldEvent = handleScaffoldEvent,
|
||||
viewModel = modelsManagementViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -276,6 +412,7 @@ fun AppContent(
|
|||
onConfirm = {
|
||||
isUnloading = true
|
||||
coroutineScope.launch {
|
||||
// TODO-han.yin: Clear conversation upon normal exiting
|
||||
// Handle screen specific cleanups
|
||||
when(engineState) {
|
||||
is State.Benchmarking -> {}
|
||||
|
|
@ -301,3 +438,39 @@ fun AppContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackHandlerSetup(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
backDispatcher: OnBackPressedDispatcher?,
|
||||
currentRoute: String,
|
||||
isModelLoaded: Boolean,
|
||||
handleBackWithModelCheck: () -> Unit
|
||||
) {
|
||||
val routeNeedsModelUnloading = currentRoute in listOf(
|
||||
AppDestinations.CONVERSATION_ROUTE,
|
||||
AppDestinations.BENCHMARK_ROUTE,
|
||||
AppDestinations.MODEL_LOADING_ROUTE
|
||||
)
|
||||
|
||||
DisposableEffect(lifecycleOwner, backDispatcher, currentRoute, isModelLoaded) {
|
||||
android.util.Log.w("JOJO", "BackHandlerSetup: currentRoute: $currentRoute")
|
||||
|
||||
val callback = object : OnBackPressedCallback(
|
||||
routeNeedsModelUnloading && isModelLoaded
|
||||
) {
|
||||
override fun handleOnBackPressed() {
|
||||
handleBackWithModelCheck()
|
||||
}
|
||||
}
|
||||
|
||||
backDispatcher?.addCallback(lifecycleOwner, callback)
|
||||
onDispose { callback.remove() }
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = routeNeedsModelUnloading && isModelLoaded
|
||||
) {
|
||||
handleBackWithModelCheck()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ class UserPreferences @Inject constructor (
|
|||
|
||||
/**
|
||||
* Gets the monitoring interval in milliseconds.
|
||||
*
|
||||
* TODO-han.yin: replace with Enum value instead of millisecond value
|
||||
*/
|
||||
fun getMonitoringInterval(): Flow<Long> {
|
||||
return context.dataStore.data.map { preferences ->
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ class ModelRepositoryImpl @Inject constructor(
|
|||
|
||||
private const val INTERNAL_STORAGE_PATH = "models"
|
||||
|
||||
private const val STORAGE_METRICS_UPDATE_INTERVAL = 5_000L
|
||||
private const val STORAGE_METRICS_UPDATE_INTERVAL = 10_000L
|
||||
private const val BYTES_IN_GB = 1024f * 1024f * 1024f
|
||||
|
||||
private const val MODEL_IMPORT_SPACE_BUFFER_SCALE = 1.2f
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
package com.example.llama.revamp.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
|
||||
/**
|
||||
* Events called back from child screens
|
||||
*/
|
||||
sealed class ScaffoldEvent {
|
||||
data class ShowSnackbar(
|
||||
val message: String,
|
||||
val duration: SnackbarDuration = SnackbarDuration.Short,
|
||||
val withDismissAction: Boolean = false,
|
||||
val actionLabel: String? = null,
|
||||
val onAction: (() -> Unit)? = null
|
||||
) : ScaffoldEvent()
|
||||
|
||||
data class ChangeTitle(val newTitle: String) : ScaffoldEvent()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppScaffold(
|
||||
topBarconfig: TopBarConfig,
|
||||
bottomBarConfig: BottomBarConfig = BottomBarConfig.None,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
val topBar: @Composable () -> Unit = {
|
||||
when (topBarconfig) {
|
||||
is TopBarConfig.Performance -> {
|
||||
PerformanceTopBar(
|
||||
title = topBarconfig.title,
|
||||
memoryMetrics = topBarconfig.memoryMetrics,
|
||||
temperatureDisplay = topBarconfig.temperatureInfo,
|
||||
onNavigateBack = (topBarconfig.navigationIcon as? TopBarConfig.NavigationIcon.Back)?.onNavigateBack,
|
||||
onMenuOpen = (topBarconfig.navigationIcon as? TopBarConfig.NavigationIcon.Menu)?.onMenuOpen
|
||||
)
|
||||
}
|
||||
|
||||
is TopBarConfig.Storage -> {
|
||||
StorageTopBar(
|
||||
title = topBarconfig.title,
|
||||
storageMetrics = topBarconfig.storageMetrics,
|
||||
onNavigateBack = (topBarconfig.navigationIcon as? TopBarConfig.NavigationIcon.Back)?.onNavigateBack
|
||||
)
|
||||
}
|
||||
|
||||
is TopBarConfig.Default -> {
|
||||
DefaultTopBar(
|
||||
title = topBarconfig.title,
|
||||
onNavigateBack = (topBarconfig.navigationIcon as? TopBarConfig.NavigationIcon.Back)?.onNavigateBack,
|
||||
onMenuOpen = (topBarconfig.navigationIcon as? TopBarConfig.NavigationIcon.Menu)?.onMenuOpen
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bottomBar: @Composable () -> Unit = {
|
||||
when (bottomBarConfig) {
|
||||
is BottomBarConfig.None -> {
|
||||
/* No bottom bar */
|
||||
}
|
||||
is BottomBarConfig.ModelsManagement -> {
|
||||
ModelsManagementBottomBar(
|
||||
isMultiSelectionMode = bottomBarConfig.isMultiSelectionMode,
|
||||
selectedModels = bottomBarConfig.selectedModels,
|
||||
onSelectAll = bottomBarConfig.onSelectAll,
|
||||
onDeselectAll = bottomBarConfig.onDeselectAll,
|
||||
onDeleteSelected = bottomBarConfig.onDeleteSelected,
|
||||
onSortClicked = bottomBarConfig.onSortClicked,
|
||||
onFilterClicked = bottomBarConfig.onFilterClicked,
|
||||
onDeleteModeClicked = bottomBarConfig.onDeleteModeClicked,
|
||||
onAddModelClicked = bottomBarConfig.onAddModelClicked,
|
||||
onExitSelectionMode = bottomBarConfig.onExitSelectionMode,
|
||||
showSortMenu = bottomBarConfig.showSortMenu,
|
||||
onSortMenuDismissed = bottomBarConfig.onSortMenuDismissed,
|
||||
currentSortOrder = bottomBarConfig.currentSortOrder,
|
||||
onSortOptionSelected = bottomBarConfig.onSortOptionSelected,
|
||||
showImportModelMenu = bottomBarConfig.showImportModelMenu,
|
||||
onImportMenuDismissed = bottomBarConfig.onImportMenuDismissed,
|
||||
onImportLocalModelClicked = bottomBarConfig.onImportLocalModelClicked,
|
||||
onImportHuggingFaceClicked = bottomBarConfig.onImportHuggingFaceClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = topBar,
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
package com.example.llama.revamp.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.example.llama.revamp.viewmodel.PerformanceViewModel
|
||||
|
||||
@Composable
|
||||
fun DefaultAppScaffold(
|
||||
title: String,
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
onMenuOpen: (() -> Unit)? = null,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
DefaultTopBar(
|
||||
title = title,
|
||||
onNavigateBack = onNavigateBack,
|
||||
onMenuOpen = onMenuOpen
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PerformanceAppScaffold(
|
||||
performanceViewModel: PerformanceViewModel = hiltViewModel(),
|
||||
title: String,
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
onMenuOpen: (() -> Unit)? = null,
|
||||
showTemperature: Boolean = false,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
// Collect performance metrics
|
||||
val memoryUsage by performanceViewModel.memoryUsage.collectAsState()
|
||||
val temperatureInfo by performanceViewModel.temperatureMetrics.collectAsState()
|
||||
val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
PerformanceTopBar(
|
||||
title = title,
|
||||
memoryMetrics = memoryUsage,
|
||||
temperatureDisplay = if (showTemperature) Pair(temperatureInfo, useFahrenheit) else null,
|
||||
onNavigateBack = onNavigateBack,
|
||||
onMenuOpen = onMenuOpen,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StorageAppScaffold(
|
||||
title: String,
|
||||
storageUsed: Float,
|
||||
storageTotal: Float,
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
bottomBar: @Composable () -> Unit = {},
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
StorageTopBar(
|
||||
title = title,
|
||||
storageUsed = storageUsed,
|
||||
storageTotal = storageTotal,
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
bottomBar = bottomBar,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
package com.example.llama.revamp.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ClearAll
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
import androidx.compose.material.icons.filled.FilterAlt
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.llama.R
|
||||
import com.example.llama.revamp.data.model.ModelInfo
|
||||
import com.example.llama.revamp.viewmodel.ModelSortOrder
|
||||
|
||||
/**
|
||||
* [BottomAppBar] configurations
|
||||
*/
|
||||
sealed class BottomBarConfig {
|
||||
|
||||
object None : BottomBarConfig()
|
||||
|
||||
data class ModelsManagement(
|
||||
val isMultiSelectionMode: Boolean,
|
||||
val selectedModels: Map<String, ModelInfo>,
|
||||
val onSelectAll: () -> Unit,
|
||||
val onDeselectAll: () -> Unit,
|
||||
val onDeleteSelected: () -> Unit,
|
||||
val onSortClicked: () -> Unit,
|
||||
val onFilterClicked: () -> Unit,
|
||||
val onDeleteModeClicked: () -> Unit,
|
||||
val onAddModelClicked: () -> Unit,
|
||||
val onExitSelectionMode: () -> Unit,
|
||||
val showSortMenu: Boolean,
|
||||
val onSortMenuDismissed: () -> Unit,
|
||||
val currentSortOrder: ModelSortOrder,
|
||||
val onSortOptionSelected: (ModelSortOrder) -> Unit,
|
||||
val showImportModelMenu: Boolean,
|
||||
val onImportMenuDismissed: () -> Unit,
|
||||
val onImportLocalModelClicked: () -> Unit,
|
||||
val onImportHuggingFaceClicked: () -> Unit
|
||||
) : BottomBarConfig()
|
||||
|
||||
// TODO-han.yin: add more bottom bar types here
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModelsManagementBottomBar(
|
||||
isMultiSelectionMode: Boolean,
|
||||
selectedModels: Map<String, ModelInfo>,
|
||||
onSelectAll: () -> Unit,
|
||||
onDeselectAll: () -> Unit,
|
||||
onDeleteSelected: () -> Unit,
|
||||
onSortClicked: () -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
onDeleteModeClicked: () -> Unit,
|
||||
onAddModelClicked: () -> Unit,
|
||||
onExitSelectionMode: () -> Unit,
|
||||
showSortMenu: Boolean,
|
||||
onSortMenuDismissed: () -> Unit,
|
||||
currentSortOrder: ModelSortOrder,
|
||||
onSortOptionSelected: (ModelSortOrder) -> Unit,
|
||||
showImportModelMenu: Boolean,
|
||||
onImportMenuDismissed: () -> Unit,
|
||||
onImportLocalModelClicked: () -> Unit,
|
||||
onImportHuggingFaceClicked: () -> Unit
|
||||
) {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
if (isMultiSelectionMode) {
|
||||
// Multi-selection mode actions
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = "Select all"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onDeselectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ClearAll,
|
||||
contentDescription = "Deselect all"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onDeleteSelected,
|
||||
enabled = selectedModels.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete selected",
|
||||
tint = if (selectedModels.isNotEmpty())
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Default mode actions
|
||||
IconButton(onClick = onSortClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = "Sort models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sort dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = showSortMenu,
|
||||
onDismissRequest = onSortMenuDismissed
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Name (A-Z)") },
|
||||
trailingIcon = {
|
||||
if (currentSortOrder == ModelSortOrder.NAME_ASC)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by name in ascending order, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSortOptionSelected(ModelSortOrder.NAME_ASC)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Name (Z-A)") },
|
||||
trailingIcon = {
|
||||
if (currentSortOrder == ModelSortOrder.NAME_DESC)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by name in descending order, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSortOptionSelected(ModelSortOrder.NAME_DESC)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Size (Smallest first)") },
|
||||
trailingIcon = {
|
||||
if (currentSortOrder == ModelSortOrder.SIZE_ASC)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by size in ascending order, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSortOptionSelected(ModelSortOrder.SIZE_ASC)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Size (Largest first)") },
|
||||
trailingIcon = {
|
||||
if (currentSortOrder == ModelSortOrder.SIZE_DESC)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by size in descending order, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSortOptionSelected(ModelSortOrder.SIZE_DESC)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Last used") },
|
||||
trailingIcon = {
|
||||
if (currentSortOrder == ModelSortOrder.LAST_USED)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by last used, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSortOptionSelected(ModelSortOrder.LAST_USED)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onFilterClicked
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FilterAlt,
|
||||
contentDescription = "Filter models"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onDeleteModeClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DeleteSweep,
|
||||
contentDescription = "Delete models"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = if (isMultiSelectionMode) onExitSelectionMode else onAddModelClicked,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isMultiSelectionMode) Icons.Default.Close else Icons.Default.Add,
|
||||
contentDescription = if (isMultiSelectionMode) "Exit selection mode" else "Add model"
|
||||
)
|
||||
}
|
||||
|
||||
// Add model dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = showImportModelMenu,
|
||||
onDismissRequest = onImportMenuDismissed
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Import local model") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FolderOpen,
|
||||
contentDescription = "Import a local model on the device"
|
||||
)
|
||||
},
|
||||
onClick = onImportLocalModelClicked
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Download from HuggingFace") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.logo_huggingface),
|
||||
contentDescription = "Browse and download a model from HuggingFace",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
},
|
||||
onClick = onImportHuggingFaceClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -22,9 +22,47 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.llama.revamp.data.repository.StorageMetrics
|
||||
import com.example.llama.revamp.monitoring.MemoryMetrics
|
||||
import com.example.llama.revamp.monitoring.TemperatureMetrics
|
||||
import com.example.llama.revamp.monitoring.TemperatureWarningLevel
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* [TopAppBar] configurations
|
||||
*/
|
||||
sealed class TopBarConfig {
|
||||
abstract val title: String
|
||||
abstract val navigationIcon: NavigationIcon
|
||||
|
||||
// Data class for performance monitoring scaffolds
|
||||
data class Performance(
|
||||
override val title: String,
|
||||
override val navigationIcon: NavigationIcon,
|
||||
val memoryMetrics: MemoryMetrics,
|
||||
val temperatureInfo: Pair<TemperatureMetrics, Boolean>?,
|
||||
) : TopBarConfig()
|
||||
|
||||
// Data class for storage management scaffolds
|
||||
data class Storage(
|
||||
override val title: String,
|
||||
override val navigationIcon: NavigationIcon,
|
||||
val storageMetrics: StorageMetrics?
|
||||
) : TopBarConfig()
|
||||
|
||||
// Data class for default/simple scaffolds
|
||||
data class Default(
|
||||
override val title: String,
|
||||
override val navigationIcon: NavigationIcon
|
||||
) : TopBarConfig()
|
||||
|
||||
// Helper class for navigation icon configuration
|
||||
sealed class NavigationIcon {
|
||||
data class Menu(val onMenuOpen: () -> Unit) : NavigationIcon()
|
||||
data class Back(val onNavigateBack: () -> Unit) : NavigationIcon()
|
||||
object None : NavigationIcon()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -118,8 +156,7 @@ fun PerformanceTopBar(
|
|||
@Composable
|
||||
fun StorageTopBar(
|
||||
title: String,
|
||||
storageUsed: Float,
|
||||
storageTotal: Float,
|
||||
storageMetrics: StorageMetrics?,
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
) {
|
||||
TopAppBar(
|
||||
|
|
@ -135,7 +172,7 @@ fun StorageTopBar(
|
|||
}
|
||||
},
|
||||
actions = {
|
||||
StorageIndicator(usedGB = storageUsed, totalGB = storageTotal)
|
||||
StorageIndicator(storageMetrics = storageMetrics)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
|
|
@ -159,10 +196,10 @@ fun MemoryIndicator(memoryUsage: MemoryMetrics) {
|
|||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
val memoryText = String.format("%.1f / %.1f GB", memoryUsage.availableGb, memoryUsage.totalGb)
|
||||
|
||||
Text(
|
||||
text = memoryText,
|
||||
text = String.format(
|
||||
Locale.getDefault(), "%.1f / %.1f GB", memoryUsage.availableGb, memoryUsage.totalGb
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
|
@ -199,14 +236,23 @@ fun TemperatureIndicator(temperatureMetrics: TemperatureMetrics, useFahrenheit:
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun StorageIndicator(usedGB: Float, totalGB: Float) {
|
||||
fun StorageIndicator(storageMetrics: StorageMetrics?) {
|
||||
val usedGb = storageMetrics?.usedGB
|
||||
val totalGb = storageMetrics?.totalGB
|
||||
val usedRatio = if (usedGb != null && totalGb != null && totalGb > 0.0f) {
|
||||
usedGb / totalGb
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(end = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SdStorage,
|
||||
contentDescription = "Storage",
|
||||
tint = when {
|
||||
usedGB / totalGB > 0.9f -> MaterialTheme.colorScheme.error
|
||||
usedGB / totalGB > 0.7f -> MaterialTheme.colorScheme.tertiary
|
||||
usedRatio == null -> MaterialTheme.colorScheme.onSurface
|
||||
usedRatio > 0.9f -> MaterialTheme.colorScheme.error
|
||||
usedRatio > 0.7f -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
)
|
||||
|
|
@ -214,7 +260,9 @@ fun StorageIndicator(usedGB: Float, totalGB: Float) {
|
|||
Spacer(modifier = Modifier.width(2.dp))
|
||||
|
||||
Text(
|
||||
text = String.format("%.1f / %.1f GB", usedGB, totalGB),
|
||||
text = storageMetrics?.let {
|
||||
String.format(Locale.getDefault(), "%.1f / %.1f GB", it.usedGB, it.totalGB)
|
||||
} ?: String.format(Locale.getDefault(), " - / - GB"),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.example.llama.revamp.ui.components.ModelCard
|
||||
import com.example.llama.revamp.ui.components.PerformanceAppScaffold
|
||||
import com.example.llama.revamp.ui.theme.MonospacedTextStyle
|
||||
import com.example.llama.revamp.viewmodel.BenchmarkViewModel
|
||||
|
||||
|
|
@ -42,85 +41,78 @@ fun BenchmarkScreen(
|
|||
viewModel.runBenchmark()
|
||||
}
|
||||
|
||||
PerformanceAppScaffold(
|
||||
title = "Chat",
|
||||
onNavigateBack = onBackPressed,
|
||||
showTemperature = true
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Selected model card
|
||||
selectedModel?.let { model ->
|
||||
ModelCard(
|
||||
model = model,
|
||||
onClick = { /* No action on click */ },
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
isSelected = null
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Selected model card
|
||||
selectedModel?.let { model ->
|
||||
ModelCard(
|
||||
model = model,
|
||||
onClick = { /* No action on click */ },
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
isSelected = null
|
||||
)
|
||||
}
|
||||
|
||||
// Benchmark results or loading indicator
|
||||
when {
|
||||
engineState is State.Benchmarking -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Running benchmark...",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark results or loading indicator
|
||||
when {
|
||||
engineState is State.Benchmarking -> {
|
||||
benchmarkResults != null -> {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Running benchmark...",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
benchmarkResults != null -> {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = benchmarkResults ?: "",
|
||||
style = MonospacedTextStyle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Benchmark results will appear here",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = benchmarkResults ?: "",
|
||||
style = MonospacedTextStyle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Benchmark results will appear here",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,12 +52,10 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import com.example.llama.revamp.ui.components.ModelCardWithSystemPrompt
|
||||
import com.example.llama.revamp.ui.components.PerformanceAppScaffold
|
||||
import com.example.llama.revamp.viewmodel.ConversationViewModel
|
||||
import com.example.llama.revamp.viewmodel.Message
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -113,46 +111,39 @@ fun ConversationScreen(
|
|||
}
|
||||
}
|
||||
|
||||
PerformanceAppScaffold(
|
||||
title = "Chat",
|
||||
onNavigateBack = onBackPressed,
|
||||
showTemperature = true
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
// System prompt display (collapsible)
|
||||
selectedModel?.let {
|
||||
ModelCardWithSystemPrompt(it, systemPrompt)
|
||||
}
|
||||
|
||||
// Messages list
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
// System prompt display (collapsible)
|
||||
selectedModel?.let {
|
||||
ModelCardWithSystemPrompt(it, systemPrompt)
|
||||
}
|
||||
|
||||
// Messages list
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ConversationMessageList(
|
||||
messages = messages,
|
||||
listState = listState,
|
||||
)
|
||||
}
|
||||
|
||||
// Input area
|
||||
ConversationInputField(
|
||||
value = inputText,
|
||||
onValueChange = { inputText = it },
|
||||
onSendClick = {
|
||||
if (inputText.isNotBlank()) {
|
||||
viewModel.sendMessage(inputText)
|
||||
inputText = ""
|
||||
}
|
||||
},
|
||||
isEnabled = !isProcessing && !isGenerating
|
||||
ConversationMessageList(
|
||||
messages = messages,
|
||||
listState = listState,
|
||||
)
|
||||
}
|
||||
|
||||
// Input area
|
||||
ConversationInputField(
|
||||
value = inputText,
|
||||
onValueChange = { inputText = it },
|
||||
onSendClick = {
|
||||
if (inputText.isNotBlank()) {
|
||||
viewModel.sendMessage(inputText)
|
||||
inputText = ""
|
||||
}
|
||||
},
|
||||
isEnabled = !isProcessing && !isGenerating
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +178,7 @@ fun MessageBubble(message: Message) {
|
|||
formattedTime = message.formattedTime,
|
||||
content = message.content
|
||||
)
|
||||
|
||||
is Message.Assistant.Ongoing -> AssistantMessageBubble(
|
||||
formattedTime = message.formattedTime,
|
||||
content = message.content,
|
||||
|
|
@ -194,6 +186,7 @@ fun MessageBubble(message: Message) {
|
|||
isComplete = false,
|
||||
metrics = null
|
||||
)
|
||||
|
||||
is Message.Assistant.Completed -> AssistantMessageBubble(
|
||||
formattedTime = message.formattedTime,
|
||||
content = message.content,
|
||||
|
|
@ -301,7 +294,9 @@ fun AssistantMessageBubble(
|
|||
|
||||
// Show metrics or generation status below the bubble
|
||||
Row(
|
||||
modifier = Modifier.height(20.dp).padding(top = 4.dp),
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.padding(top = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (!isComplete) {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.example.llama.revamp.data.model.SystemPrompt
|
||||
import com.example.llama.revamp.ui.components.ModelCard
|
||||
import com.example.llama.revamp.ui.components.PerformanceAppScaffold
|
||||
import com.example.llama.revamp.viewmodel.ModelLoadingViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -118,327 +117,323 @@ fun ModelLoadingScreen(
|
|||
onConversationSelected(systemPrompt, prepareJob)
|
||||
}
|
||||
|
||||
PerformanceAppScaffold(
|
||||
title = "Load Model",
|
||||
onNavigateBack = onBackPressed,
|
||||
showTemperature = false
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Selected model card
|
||||
selectedModel?.let { model ->
|
||||
ModelCard(
|
||||
model = model,
|
||||
onClick = { /* No action on click */ },
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
isSelected = null
|
||||
)
|
||||
}
|
||||
|
||||
// Benchmark card
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
.selectable(
|
||||
selected = selectedMode == Mode.BENCHMARK,
|
||||
onClick = {
|
||||
selectedMode = Mode.BENCHMARK
|
||||
useSystemPrompt = false
|
||||
},
|
||||
enabled = !isLoading,
|
||||
role = Role.RadioButton
|
||||
)
|
||||
) {
|
||||
// Selected model card
|
||||
selectedModel?.let { model ->
|
||||
ModelCard(
|
||||
model = model,
|
||||
onClick = { /* No action on click */ },
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
isSelected = null
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedMode == Mode.BENCHMARK,
|
||||
onClick = null // handled by parent selectable
|
||||
)
|
||||
Text(
|
||||
text = "Benchmark",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark card
|
||||
Card(
|
||||
// Conversation card with integrated system prompt picker & editor
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 4.dp)
|
||||
// Only fill height if system prompt is active
|
||||
.then(if (useSystemPrompt) Modifier.weight(1f) else Modifier)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
.selectable(
|
||||
selected = selectedMode == Mode.BENCHMARK,
|
||||
onClick = {
|
||||
selectedMode = Mode.BENCHMARK
|
||||
useSystemPrompt = false
|
||||
},
|
||||
enabled = !isLoading,
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.padding(bottom = 12.dp)
|
||||
// Only fill height if system prompt is active
|
||||
.then(if (useSystemPrompt) Modifier.fillMaxSize() else Modifier)
|
||||
) {
|
||||
// Conversation option
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = selectedMode == Mode.CONVERSATION,
|
||||
onClick = { selectedMode = Mode.CONVERSATION },
|
||||
enabled = !isLoading,
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.padding(top = 16.dp, start = 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedMode == Mode.BENCHMARK,
|
||||
selected = selectedMode == Mode.CONVERSATION,
|
||||
onClick = null // handled by parent selectable
|
||||
)
|
||||
Text(
|
||||
text = "Benchmark",
|
||||
text = "Conversation",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Conversation card with integrated system prompt picker & editor
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 4.dp)
|
||||
// Only fill height if system prompt is active
|
||||
.then(if (useSystemPrompt) Modifier.weight(1f) else Modifier)
|
||||
) {
|
||||
Column(
|
||||
// System prompt row with switch
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
// Only fill height if system prompt is active
|
||||
.then(if (useSystemPrompt) Modifier.fillMaxSize() else Modifier)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Conversation option
|
||||
Row(
|
||||
Text(
|
||||
text = "Use system prompt",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = selectedMode == Mode.CONVERSATION,
|
||||
onClick = { selectedMode = Mode.CONVERSATION },
|
||||
enabled = !isLoading,
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.padding(top = 16.dp, start = 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedMode == Mode.CONVERSATION,
|
||||
onClick = null // handled by parent selectable
|
||||
)
|
||||
Text(
|
||||
text = "Conversation",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
.padding(start = 32.dp) // Align with radio text
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
// System prompt row with switch
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Use system prompt",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.padding(start = 32.dp) // Align with radio text
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Switch(
|
||||
checked = useSystemPrompt,
|
||||
onCheckedChange = {
|
||||
useSystemPrompt = it
|
||||
if (it && selectedMode != Mode.CONVERSATION) {
|
||||
selectedMode = Mode.CONVERSATION
|
||||
}
|
||||
},
|
||||
enabled = !isLoading
|
||||
)
|
||||
}
|
||||
|
||||
// System prompt content (visible when switch is on)
|
||||
AnimatedVisibility(
|
||||
visible = useSystemPrompt && selectedMode == Mode.CONVERSATION,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize()
|
||||
.padding(start = 48.dp, end = 16.dp)
|
||||
) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, bottom = 8.dp)
|
||||
)
|
||||
|
||||
// Tab selector using SegmentedButton
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SegmentedButton(
|
||||
selected = selectedTab == SystemPromptTab.PRESETS,
|
||||
onClick = { selectedTab = SystemPromptTab.PRESETS },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 3),
|
||||
icon = {
|
||||
if (selectedTab == SystemPromptTab.PRESETS) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text("Presets") }
|
||||
)
|
||||
|
||||
SegmentedButton(
|
||||
selected = selectedTab == SystemPromptTab.CUSTOM,
|
||||
onClick = { selectedTab = SystemPromptTab.CUSTOM },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 3),
|
||||
icon = {
|
||||
if (selectedTab == SystemPromptTab.CUSTOM) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text("Custom") }
|
||||
)
|
||||
|
||||
SegmentedButton(
|
||||
selected = selectedTab == SystemPromptTab.RECENTS,
|
||||
onClick = { selectedTab = SystemPromptTab.RECENTS },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 2, count = 3),
|
||||
icon = {
|
||||
if (selectedTab == SystemPromptTab.RECENTS) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text("Recents") }
|
||||
)
|
||||
Switch(
|
||||
checked = useSystemPrompt,
|
||||
onCheckedChange = {
|
||||
useSystemPrompt = it
|
||||
if (it && selectedMode != Mode.CONVERSATION) {
|
||||
selectedMode = Mode.CONVERSATION
|
||||
}
|
||||
},
|
||||
enabled = !isLoading
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// System prompt content (visible when switch is on)
|
||||
AnimatedVisibility(
|
||||
visible = useSystemPrompt && selectedMode == Mode.CONVERSATION,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize()
|
||||
.padding(start = 48.dp, end = 16.dp)
|
||||
) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, bottom = 8.dp)
|
||||
)
|
||||
|
||||
// Content based on selected tab
|
||||
when (selectedTab) {
|
||||
SystemPromptTab.PRESETS -> {
|
||||
if (presetPrompts.isEmpty()) {
|
||||
Text(
|
||||
text = "No preset prompts available.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
} else {
|
||||
PromptList(
|
||||
prompts = presetPrompts,
|
||||
selectedPromptId = selectedPrompt?.id,
|
||||
expandedPromptId = expandedPromptId,
|
||||
onPromptSelected = {
|
||||
selectedPrompt = it
|
||||
expandedPromptId = it.id
|
||||
},
|
||||
onExpandPrompt = { expandedPromptId = it }
|
||||
// Tab selector using SegmentedButton
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SegmentedButton(
|
||||
selected = selectedTab == SystemPromptTab.PRESETS,
|
||||
onClick = { selectedTab = SystemPromptTab.PRESETS },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 3),
|
||||
icon = {
|
||||
if (selectedTab == SystemPromptTab.PRESETS) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text("Presets") }
|
||||
)
|
||||
|
||||
SystemPromptTab.CUSTOM -> {
|
||||
// Custom prompt editor (fill remaining space)
|
||||
OutlinedTextField(
|
||||
value = customPromptText,
|
||||
onValueChange = {
|
||||
customPromptText = it
|
||||
// Deselect any preset prompt if typing custom
|
||||
if (it.isNotBlank()) {
|
||||
selectedPrompt = null
|
||||
}
|
||||
SegmentedButton(
|
||||
selected = selectedTab == SystemPromptTab.CUSTOM,
|
||||
onClick = { selectedTab = SystemPromptTab.CUSTOM },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 3),
|
||||
icon = {
|
||||
if (selectedTab == SystemPromptTab.CUSTOM) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text("Custom") }
|
||||
)
|
||||
|
||||
SegmentedButton(
|
||||
selected = selectedTab == SystemPromptTab.RECENTS,
|
||||
onClick = { selectedTab = SystemPromptTab.RECENTS },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 2, count = 3),
|
||||
icon = {
|
||||
if (selectedTab == SystemPromptTab.RECENTS) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text("Recents") }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Content based on selected tab
|
||||
when (selectedTab) {
|
||||
SystemPromptTab.PRESETS -> {
|
||||
if (presetPrompts.isEmpty()) {
|
||||
Text(
|
||||
text = "No preset prompts available.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
} else {
|
||||
PromptList(
|
||||
prompts = presetPrompts,
|
||||
selectedPromptId = selectedPrompt?.id,
|
||||
expandedPromptId = expandedPromptId,
|
||||
onPromptSelected = {
|
||||
selectedPrompt = it
|
||||
expandedPromptId = it.id
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize(), // Fill available space
|
||||
label = { Text("Enter system prompt") },
|
||||
placeholder = { Text("You are a helpful assistant...") },
|
||||
minLines = 5
|
||||
onExpandPrompt = { expandedPromptId = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SystemPromptTab.RECENTS -> {
|
||||
if (recentPrompts.isEmpty()) {
|
||||
Text(
|
||||
text = "No recent prompts found.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
} else {
|
||||
PromptList(
|
||||
prompts = recentPrompts,
|
||||
selectedPromptId = selectedPrompt?.id,
|
||||
expandedPromptId = expandedPromptId,
|
||||
onPromptSelected = {
|
||||
selectedPrompt = it
|
||||
expandedPromptId = it.id
|
||||
},
|
||||
onExpandPrompt = { expandedPromptId = it }
|
||||
)
|
||||
}
|
||||
SystemPromptTab.CUSTOM -> {
|
||||
// Custom prompt editor (fill remaining space)
|
||||
OutlinedTextField(
|
||||
value = customPromptText,
|
||||
onValueChange = {
|
||||
customPromptText = it
|
||||
// Deselect any preset prompt if typing custom
|
||||
if (it.isNotBlank()) {
|
||||
selectedPrompt = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize(), // Fill available space
|
||||
label = { Text("Enter system prompt") },
|
||||
placeholder = { Text("You are a helpful assistant...") },
|
||||
minLines = 5
|
||||
)
|
||||
}
|
||||
|
||||
SystemPromptTab.RECENTS -> {
|
||||
if (recentPrompts.isEmpty()) {
|
||||
Text(
|
||||
text = "No recent prompts found.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
} else {
|
||||
PromptList(
|
||||
prompts = recentPrompts,
|
||||
selectedPromptId = selectedPrompt?.id,
|
||||
expandedPromptId = expandedPromptId,
|
||||
onPromptSelected = {
|
||||
selectedPrompt = it
|
||||
expandedPromptId = it.id
|
||||
},
|
||||
onExpandPrompt = { expandedPromptId = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flexible spacer when system prompt is not active
|
||||
if (!useSystemPrompt) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
// Flexible spacer when system prompt is not active
|
||||
if (!useSystemPrompt) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
// Start button
|
||||
Button(
|
||||
onClick = {
|
||||
when (selectedMode) {
|
||||
Mode.BENCHMARK -> handleBenchmarkSelected()
|
||||
// Start button
|
||||
Button(
|
||||
onClick = {
|
||||
when (selectedMode) {
|
||||
Mode.BENCHMARK -> handleBenchmarkSelected()
|
||||
|
||||
Mode.CONVERSATION -> {
|
||||
val systemPrompt = if (useSystemPrompt) {
|
||||
when (selectedTab) {
|
||||
SystemPromptTab.PRESETS, SystemPromptTab.RECENTS ->
|
||||
selectedPrompt?.let { prompt ->
|
||||
// Save the prompt to recent prompts database
|
||||
modelLoadingViewModel.savePromptToRecents(prompt)
|
||||
prompt.content
|
||||
Mode.CONVERSATION -> {
|
||||
val systemPrompt = if (useSystemPrompt) {
|
||||
when (selectedTab) {
|
||||
SystemPromptTab.PRESETS, SystemPromptTab.RECENTS ->
|
||||
selectedPrompt?.let { prompt ->
|
||||
// Save the prompt to recent prompts database
|
||||
modelLoadingViewModel.savePromptToRecents(prompt)
|
||||
prompt.content
|
||||
}
|
||||
|
||||
SystemPromptTab.CUSTOM ->
|
||||
customPromptText.takeIf { it.isNotBlank() }
|
||||
?.also { promptText ->
|
||||
// Save custom prompt to database
|
||||
modelLoadingViewModel.saveCustomPromptToRecents(
|
||||
promptText
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
SystemPromptTab.CUSTOM ->
|
||||
customPromptText.takeIf { it.isNotBlank() }
|
||||
?.also { promptText ->
|
||||
// Save custom prompt to database
|
||||
modelLoadingViewModel.saveCustomPromptToRecents(promptText)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
handleConversationSelected(systemPrompt)
|
||||
}
|
||||
|
||||
null -> { /* No mode selected */ }
|
||||
handleConversationSelected(systemPrompt)
|
||||
}
|
||||
|
||||
null -> { /* No mode selected */
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = selectedMode != null && !isLoading &&
|
||||
(!useSystemPrompt || hasActiveSystemPrompt)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.width(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = when (engineState) {
|
||||
is State.LoadingModel -> "Loading model..."
|
||||
is State.ProcessingSystemPrompt -> "Processing system prompt..."
|
||||
is State.ModelReady -> "Preparing conversation..."
|
||||
else -> "Processing..."
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
} else {
|
||||
Text(text = "Start", style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = selectedMode != null && !isLoading &&
|
||||
(!useSystemPrompt || hasActiveSystemPrompt)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.width(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = when (engineState) {
|
||||
is State.LoadingModel -> "Loading model..."
|
||||
is State.ProcessingSystemPrompt -> "Processing system prompt..."
|
||||
is State.ModelReady -> "Preparing conversation..."
|
||||
else -> "Processing..."
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
} else {
|
||||
Text(text = "Start", style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import com.example.llama.revamp.data.model.ModelInfo
|
||||
import com.example.llama.revamp.ui.components.ModelCard
|
||||
import com.example.llama.revamp.ui.components.ModelCardActions
|
||||
import com.example.llama.revamp.ui.components.PerformanceAppScaffold
|
||||
import com.example.llama.revamp.viewmodel.ModelSelectionViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -37,7 +36,6 @@ import com.example.llama.revamp.viewmodel.ModelSelectionViewModel
|
|||
fun ModelSelectionScreen(
|
||||
onModelSelected: (ModelInfo) -> Unit,
|
||||
onManageModelsClicked: () -> Unit,
|
||||
onMenuClicked: () -> Unit,
|
||||
viewModel: ModelSelectionViewModel = hiltViewModel(),
|
||||
) {
|
||||
val models by viewModel.availableModels.collectAsState()
|
||||
|
|
@ -47,35 +45,28 @@ fun ModelSelectionScreen(
|
|||
onModelSelected(model)
|
||||
}
|
||||
|
||||
PerformanceAppScaffold(
|
||||
title = "Models",
|
||||
onMenuOpen = onMenuClicked,
|
||||
showTemperature = false
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
if (models.isEmpty()) {
|
||||
EmptyModelsView(onManageModelsClicked)
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(models) { model ->
|
||||
ModelCard(
|
||||
model = model,
|
||||
onClick = { handleModelSelection(model) },
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
isSelected = null, // Not in selection mode
|
||||
actionButton = {
|
||||
ModelCardActions.PlayButton {
|
||||
handleModelSelection(model)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
if (models.isEmpty()) {
|
||||
EmptyModelsView(onManageModelsClicked)
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(models) { model ->
|
||||
ModelCard(
|
||||
model = model,
|
||||
onClick = { handleModelSelection(model) },
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
isSelected = null, // Not in selection mode
|
||||
actionButton = {
|
||||
ModelCardActions.PlayButton {
|
||||
handleModelSelection(model)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.example.llama.revamp.ui.screens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -16,63 +14,33 @@ import androidx.compose.foundation.layout.size
|
|||
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.Sort
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ClearAll
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
import androidx.compose.material.icons.filled.FilterAlt
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.example.llama.R
|
||||
import com.example.llama.revamp.data.model.ModelInfo
|
||||
import com.example.llama.revamp.ui.components.ModelCard
|
||||
import com.example.llama.revamp.ui.components.ModelCardActions
|
||||
import com.example.llama.revamp.ui.components.StorageAppScaffold
|
||||
import com.example.llama.revamp.ui.components.ScaffoldEvent
|
||||
import com.example.llama.revamp.util.formatSize
|
||||
import com.example.llama.revamp.viewmodel.ModelManagementState
|
||||
import com.example.llama.revamp.viewmodel.ModelManagementState.Deletion
|
||||
import com.example.llama.revamp.viewmodel.ModelManagementState.Importation
|
||||
import com.example.llama.revamp.viewmodel.ModelSortOrder
|
||||
import com.example.llama.revamp.viewmodel.ModelsManagementViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Screen for managing LLM models (view, download, delete)
|
||||
|
|
@ -80,381 +48,150 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
fun ModelsManagementScreen(
|
||||
onBackPressed: () -> Unit,
|
||||
viewModel: ModelsManagementViewModel = hiltViewModel()
|
||||
onScaffoldEvent: (ScaffoldEvent) -> Unit,
|
||||
viewModel: ModelsManagementViewModel,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// ViewModel states
|
||||
val storageMetrics by viewModel.storageMetrics.collectAsState()
|
||||
val sortOrder by viewModel.sortOrder.collectAsState()
|
||||
val sortedModels by viewModel.sortedModels.collectAsState()
|
||||
val managementState by viewModel.managementState.collectAsState()
|
||||
|
||||
// UI state: sorting
|
||||
var showSortMenu by remember { mutableStateOf(false) }
|
||||
|
||||
// UI state: importing
|
||||
var showImportModelMenu by remember { mutableStateOf(false) }
|
||||
val fileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri -> uri?.let { viewModel.localModelFileSelected(it) } }
|
||||
|
||||
// UI state: multi-selecting
|
||||
var isMultiSelectionMode by remember { mutableStateOf(false) }
|
||||
val selectedModels = remember { mutableStateMapOf<String, ModelInfo>() }
|
||||
val exitSelectionMode = {
|
||||
isMultiSelectionMode = false
|
||||
selectedModels.clear()
|
||||
}
|
||||
// Selection state from ViewModel
|
||||
val isMultiSelectionMode by viewModel.isMultiSelectionMode.collectAsState()
|
||||
val selectedModels by viewModel.selectedModels.collectAsState()
|
||||
|
||||
BackHandler(
|
||||
enabled = managementState is Importation.Importing || isMultiSelectionMode
|
||||
enabled = isMultiSelectionMode
|
||||
|| managementState is Importation.Importing
|
||||
|| managementState is Deletion.Deleting
|
||||
) {
|
||||
if (isMultiSelectionMode) {
|
||||
// Exit selection mode if in selection mode
|
||||
exitSelectionMode()
|
||||
viewModel.setMultiSelectionMode(false)
|
||||
} else {
|
||||
/* Ignore back press while processing model management requests */
|
||||
}
|
||||
}
|
||||
|
||||
StorageAppScaffold(
|
||||
title = "Models Management",
|
||||
storageUsed = storageMetrics?.usedGB ?: 0f,
|
||||
storageTotal = storageMetrics?.totalGB ?: 0f,
|
||||
onNavigateBack = onBackPressed,
|
||||
snackbarHostState = snackbarHostState,
|
||||
bottomBar = {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
if (isMultiSelectionMode) {
|
||||
// Multi-selection mode actions
|
||||
IconButton(onClick = {
|
||||
// Select all
|
||||
selectedModels.putAll(sortedModels.map { it.id to it })
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = "Select all"
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Model cards
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
items(items = sortedModels, key = { it.id }) { model ->
|
||||
ModelCard(
|
||||
model = model,
|
||||
onClick = {
|
||||
if (isMultiSelectionMode) {
|
||||
viewModel.toggleModelSelection(model.id)
|
||||
} else {
|
||||
viewModel.viewModelDetails(model.id)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
// Deselect all
|
||||
selectedModels.clear()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ClearAll,
|
||||
contentDescription = "Deselect all"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (selectedModels.isNotEmpty()) {
|
||||
viewModel.batchDeletionClicked(selectedModels.toMap())
|
||||
}
|
||||
},
|
||||
enabled = selectedModels.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete selected",
|
||||
tint = if (selectedModels.isNotEmpty())
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Default mode actions
|
||||
IconButton(onClick = { showSortMenu = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = "Sort models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sort dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = showSortMenu,
|
||||
onDismissRequest = { showSortMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Name (A-Z)") },
|
||||
trailingIcon = {
|
||||
if (sortOrder == ModelSortOrder.NAME_ASC)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by name in ascending order, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setSortOrder(ModelSortOrder.NAME_ASC)
|
||||
showSortMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Name (Z-A)") },
|
||||
trailingIcon = {
|
||||
if (sortOrder == ModelSortOrder.NAME_DESC)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by name in descending order, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setSortOrder(ModelSortOrder.NAME_DESC)
|
||||
showSortMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Size (Smallest first)") },
|
||||
trailingIcon = {
|
||||
if (sortOrder == ModelSortOrder.SIZE_ASC)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by size in ascending order, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setSortOrder(ModelSortOrder.SIZE_ASC)
|
||||
showSortMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Size (Largest first)") },
|
||||
trailingIcon = {
|
||||
if (sortOrder == ModelSortOrder.SIZE_DESC)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by size in descending order, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setSortOrder(ModelSortOrder.SIZE_DESC)
|
||||
showSortMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Last used") },
|
||||
trailingIcon = {
|
||||
if (sortOrder == ModelSortOrder.LAST_USED)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Sort by last used, selected"
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setSortOrder(ModelSortOrder.LAST_USED)
|
||||
showSortMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {/* TODO-han.yin: implement filtering once metadata ready */ }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FilterAlt,
|
||||
contentDescription = "Filter models"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
isMultiSelectionMode = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DeleteSweep,
|
||||
contentDescription = "Delete models"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (isMultiSelectionMode) {
|
||||
exitSelectionMode()
|
||||
} else {
|
||||
showImportModelMenu = true
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isMultiSelectionMode) Icons.Default.Close else Icons.Default.Add,
|
||||
contentDescription = if (isMultiSelectionMode) "Exit selection mode" else "Add model"
|
||||
)
|
||||
}
|
||||
|
||||
// Add model dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = showImportModelMenu,
|
||||
onDismissRequest = { showImportModelMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Import local model") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FolderOpen,
|
||||
contentDescription = "Import a local model on the device"
|
||||
},
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
isSelected =
|
||||
if (isMultiSelectionMode) selectedModels.contains(model.id) else null,
|
||||
actionButton =
|
||||
if (!isMultiSelectionMode) {
|
||||
{
|
||||
ModelCardActions.InfoButton(
|
||||
onClick = { viewModel.viewModelDetails(model.id) }
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
fileLauncher.launch(arrayOf("application/octet-stream", "*/*"))
|
||||
showImportModelMenu = false
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Model import progress overlay
|
||||
when (val state = managementState) {
|
||||
is Importation.Confirming -> {
|
||||
ImportProgressDialog(
|
||||
fileName = state.fileName,
|
||||
fileSize = state.fileSize,
|
||||
isImporting = false,
|
||||
progress = 0.0f,
|
||||
onConfirm = {
|
||||
viewModel.importLocalModelFile(state.uri, state.fileName, state.fileSize)
|
||||
},
|
||||
onCancel = { viewModel.resetManagementState() }
|
||||
)
|
||||
}
|
||||
|
||||
is Importation.Importing -> {
|
||||
ImportProgressDialog(
|
||||
fileName = state.fileName,
|
||||
fileSize = state.fileSize,
|
||||
isImporting = true,
|
||||
isCancelling = state.isCancelling,
|
||||
progress = state.progress,
|
||||
onConfirm = {},
|
||||
onCancel = { viewModel.cancelOngoingLocalModelImport() },
|
||||
)
|
||||
}
|
||||
|
||||
is Importation.Error -> {
|
||||
ErrorDialog(
|
||||
title = "Import Failed",
|
||||
message = state.message,
|
||||
onDismiss = { viewModel.resetManagementState() }
|
||||
)
|
||||
}
|
||||
|
||||
is Importation.Success -> {
|
||||
LaunchedEffect(state) {
|
||||
onScaffoldEvent(
|
||||
ScaffoldEvent.ShowSnackbar(
|
||||
message = "Imported model: ${state.model.name}"
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Download from HuggingFace") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.logo_huggingface),
|
||||
contentDescription = "Browse and download a model from HuggingFace",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.importFromHuggingFace()
|
||||
showImportModelMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
viewModel.resetManagementState()
|
||||
}
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Model cards
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues).fillMaxSize().padding(16.dp)
|
||||
) {
|
||||
items(items = sortedModels, key = { it.id }) { model ->
|
||||
ModelCard(
|
||||
model = model,
|
||||
onClick = {
|
||||
if (isMultiSelectionMode) {
|
||||
// Toggle selection
|
||||
if (selectedModels.contains(model.id)) {
|
||||
selectedModels.remove(model.id)
|
||||
} else {
|
||||
selectedModels.put(model.id, sortedModels.first { it.id == model.id } )
|
||||
}
|
||||
} else {
|
||||
// View model details
|
||||
viewModel.viewModelDetails(model.id)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
isSelected =
|
||||
if (isMultiSelectionMode) selectedModels.contains(model.id) else null,
|
||||
actionButton =
|
||||
if (!isMultiSelectionMode) {
|
||||
{
|
||||
ModelCardActions.InfoButton(
|
||||
onClick = { viewModel.viewModelDetails(model.id) }
|
||||
)
|
||||
}
|
||||
} else null
|
||||
}
|
||||
|
||||
is Deletion.Confirming -> {
|
||||
BatchDeleteConfirmationDialog(
|
||||
count = state.models.size,
|
||||
onConfirm = { viewModel.deleteModels(state.models) },
|
||||
onDismiss = { viewModel.resetManagementState() },
|
||||
isDeleting = false
|
||||
)
|
||||
}
|
||||
|
||||
is Deletion.Deleting -> {
|
||||
BatchDeleteConfirmationDialog(
|
||||
count = state.models.size,
|
||||
onConfirm = { /* No-op during processing */ },
|
||||
onDismiss = { /* No-op during processing */ },
|
||||
isDeleting = true
|
||||
)
|
||||
}
|
||||
|
||||
is Deletion.Error -> {
|
||||
ErrorDialog(
|
||||
title = "Deletion Failed",
|
||||
message = state.message,
|
||||
onDismiss = { viewModel.resetManagementState() }
|
||||
)
|
||||
}
|
||||
|
||||
is Deletion.Success -> {
|
||||
LaunchedEffect(state) {
|
||||
viewModel.setMultiSelectionMode(false)
|
||||
|
||||
val count = state.models.size
|
||||
onScaffoldEvent(
|
||||
ScaffoldEvent.ShowSnackbar(
|
||||
message = "Deleted $count ${if (count > 1) "models" else "model"}.",
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Model import progress overlay
|
||||
when (val state = managementState) {
|
||||
is Importation.Confirming -> {
|
||||
ImportProgressDialog(
|
||||
fileName = state.fileName,
|
||||
fileSize = state.fileSize,
|
||||
isImporting = false,
|
||||
progress = 0.0f,
|
||||
onConfirm = {
|
||||
viewModel.importLocalModelFile(
|
||||
state.uri, state.fileName, state.fileSize
|
||||
)
|
||||
},
|
||||
onCancel = { viewModel.resetManagementState() }
|
||||
)
|
||||
}
|
||||
|
||||
is Importation.Importing -> {
|
||||
ImportProgressDialog(
|
||||
fileName = state.fileName,
|
||||
fileSize = state.fileSize,
|
||||
isImporting = true,
|
||||
isCancelling = state.isCancelling,
|
||||
progress = state.progress,
|
||||
onConfirm = {},
|
||||
onCancel = { viewModel.cancelOngoingLocalModelImport() },
|
||||
)
|
||||
}
|
||||
|
||||
is Importation.Error -> {
|
||||
ErrorDialog(
|
||||
title = "Import Failed",
|
||||
message = state.message,
|
||||
onDismiss = { viewModel.resetManagementState() }
|
||||
)
|
||||
}
|
||||
|
||||
is Importation.Success -> {
|
||||
LaunchedEffect(state) {
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = "Imported model: ${state.model.name}",
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
viewModel.resetManagementState()
|
||||
}
|
||||
}
|
||||
|
||||
is Deletion.Confirming -> {
|
||||
BatchDeleteConfirmationDialog(
|
||||
count = state.models.size,
|
||||
onConfirm = { viewModel.deleteModels(state.models) },
|
||||
onDismiss = { viewModel.resetManagementState() },
|
||||
isDeleting = false
|
||||
)
|
||||
}
|
||||
|
||||
is Deletion.Deleting -> {
|
||||
BatchDeleteConfirmationDialog(
|
||||
count = state.models.size,
|
||||
onConfirm = { /* No-op during processing */ },
|
||||
onDismiss = { /* No-op during processing */ },
|
||||
isDeleting = true
|
||||
)
|
||||
}
|
||||
|
||||
is Deletion.Error -> {
|
||||
ErrorDialog(
|
||||
title = "Deletion Failed",
|
||||
message = state.message,
|
||||
onDismiss = { viewModel.resetManagementState() }
|
||||
)
|
||||
}
|
||||
|
||||
is Deletion.Success -> {
|
||||
LaunchedEffect(state) {
|
||||
exitSelectionMode()
|
||||
coroutineScope.launch {
|
||||
val count = state.models.size
|
||||
snackbarHostState.showSnackbar(
|
||||
message = "Deleted $count ${if (count > 1) "models" else "model"}.",
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
is ModelManagementState.Idle -> { /* Idle state, nothing to show */ }
|
||||
}
|
||||
is ModelManagementState.Idle -> { /* Idle state, nothing to show */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.example.llama.revamp.ui.components.DefaultAppScaffold
|
||||
import com.example.llama.revamp.viewmodel.PerformanceViewModel
|
||||
|
||||
/**
|
||||
|
|
@ -31,76 +30,68 @@ import com.example.llama.revamp.viewmodel.PerformanceViewModel
|
|||
fun SettingsGeneralScreen(
|
||||
performanceViewModel: PerformanceViewModel = hiltViewModel(),
|
||||
onBackPressed: () -> Unit,
|
||||
onMenuClicked: () -> Unit
|
||||
) {
|
||||
// Collect state from ViewModel
|
||||
val isMonitoringEnabled by performanceViewModel.isMonitoringEnabled.collectAsState()
|
||||
val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState()
|
||||
|
||||
DefaultAppScaffold(
|
||||
title = "Settings",
|
||||
onNavigateBack = onBackPressed,
|
||||
onMenuOpen = onMenuClicked
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SettingsCategory(title = "Performance Monitoring") {
|
||||
SettingsSwitch(
|
||||
title = "Enable Monitoring",
|
||||
description = "Display memory, battery and temperature information",
|
||||
checked = isMonitoringEnabled,
|
||||
onCheckedChange = { performanceViewModel.setMonitoringEnabled(it) }
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SettingsCategory(title = "Performance Monitoring") {
|
||||
SettingsSwitch(
|
||||
title = "Enable Monitoring",
|
||||
description = "Display memory, battery and temperature information",
|
||||
checked = isMonitoringEnabled,
|
||||
onCheckedChange = { performanceViewModel.setMonitoringEnabled(it) }
|
||||
)
|
||||
|
||||
SettingsSwitch(
|
||||
title = "Use Fahrenheit",
|
||||
description = "Display temperature in Fahrenheit instead of Celsius",
|
||||
checked = useFahrenheit,
|
||||
onCheckedChange = { performanceViewModel.setUseFahrenheitUnit(it) }
|
||||
)
|
||||
}
|
||||
SettingsSwitch(
|
||||
title = "Use Fahrenheit",
|
||||
description = "Display temperature in Fahrenheit instead of Celsius",
|
||||
checked = useFahrenheit,
|
||||
onCheckedChange = { performanceViewModel.setUseFahrenheitUnit(it) }
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCategory(title = "Theme") {
|
||||
SettingsSwitch(
|
||||
title = "Dark Theme",
|
||||
description = "Use dark theme throughout the app",
|
||||
checked = true, // This would be connected to theme state in a real app
|
||||
onCheckedChange = {
|
||||
/* TODO-hyin: Implement theme switching between Auto, Light and Dark */
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingsCategory(title = "Theme") {
|
||||
SettingsSwitch(
|
||||
title = "Dark Theme",
|
||||
description = "Use dark theme throughout the app",
|
||||
checked = true, // This would be connected to theme state in a real app
|
||||
onCheckedChange = {
|
||||
/* TODO-hyin: Implement theme switching between Auto, Light and Dark */
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCategory(title = "About") {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
SettingsCategory(title = "About") {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Kleidi LLaMA",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
text = "Kleidi LLaMA",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Version 1.0.0",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "Version 1.0.0",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Local inference for LLM models on your device.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Local inference for LLM models on your device.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.example.llama.revamp.data.model.ModelInfo
|
||||
import com.example.llama.revamp.data.repository.InsufficientStorageException
|
||||
import com.example.llama.revamp.data.repository.ModelRepository
|
||||
import com.example.llama.revamp.data.repository.StorageMetrics
|
||||
import com.example.llama.revamp.util.getFileNameFromUri
|
||||
import com.example.llama.revamp.util.getFileSizeFromUri
|
||||
import com.example.llama.revamp.viewmodel.ModelManagementState.Deletion
|
||||
|
|
@ -32,13 +31,7 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
private val modelRepository: ModelRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val storageMetrics: StateFlow<StorageMetrics?> = modelRepository.getStorageMetrics()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
// Data: available models
|
||||
private val _availableModels: StateFlow<List<ModelInfo>> = modelRepository.getModels()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
|
|
@ -46,11 +39,68 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
private val _sortedModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
||||
val sortedModels: StateFlow<List<ModelInfo>> = _sortedModels.asStateFlow()
|
||||
|
||||
// UI state: multi-selection mode
|
||||
private val _isMultiSelectionMode = MutableStateFlow(false)
|
||||
val isMultiSelectionMode: StateFlow<Boolean> = _isMultiSelectionMode.asStateFlow()
|
||||
|
||||
fun setMultiSelectionMode(enabled: Boolean) {
|
||||
_isMultiSelectionMode.value = enabled
|
||||
if (!enabled) {
|
||||
clearSelectedModels()
|
||||
}
|
||||
}
|
||||
|
||||
// UI state: models selected in multi-selection
|
||||
private val _selectedModels = MutableStateFlow<Map<String, ModelInfo>>(emptyMap())
|
||||
val selectedModels: StateFlow<Map<String, ModelInfo>> = _selectedModels.asStateFlow()
|
||||
|
||||
fun toggleModelSelection(modelId: String) {
|
||||
val current = _selectedModels.value.toMutableMap()
|
||||
val model = _sortedModels.value.find { it.id == modelId }
|
||||
|
||||
if (model != null) {
|
||||
if (current.containsKey(modelId)) {
|
||||
current.remove(modelId)
|
||||
} else {
|
||||
current[modelId] = model
|
||||
}
|
||||
_selectedModels.value = current
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAllModels() {
|
||||
_selectedModels.value = _sortedModels.value.associateBy { it.id }
|
||||
}
|
||||
|
||||
fun clearSelectedModels() {
|
||||
_selectedModels.value = emptyMap()
|
||||
}
|
||||
|
||||
// UI state: sort menu
|
||||
private val _sortOrder = MutableStateFlow(ModelSortOrder.NAME_ASC)
|
||||
val sortOrder: StateFlow<ModelSortOrder> = _sortOrder.asStateFlow()
|
||||
|
||||
private val _sortedModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
||||
val sortedModels: StateFlow<List<ModelInfo>> = _sortedModels.asStateFlow()
|
||||
fun setSortOrder(order: ModelSortOrder) {
|
||||
_sortOrder.value = order
|
||||
}
|
||||
|
||||
private val _showSortMenu = MutableStateFlow(false)
|
||||
val showSortMenu: StateFlow<Boolean> = _showSortMenu.asStateFlow()
|
||||
|
||||
fun toggleSortMenu(show: Boolean) {
|
||||
_showSortMenu.value = show
|
||||
}
|
||||
|
||||
// UI state: import menu
|
||||
private val _showImportModelMenu = MutableStateFlow(false)
|
||||
val showImportModelMenu: StateFlow<Boolean> = _showImportModelMenu.asStateFlow()
|
||||
|
||||
fun toggleImportMenu(show: Boolean) {
|
||||
_showImportModelMenu.value = show
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -68,14 +118,7 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
ModelSortOrder.LAST_USED -> models.sortedByDescending { it.lastUsed ?: 0 }
|
||||
}
|
||||
|
||||
fun setSortOrder(order: ModelSortOrder) {
|
||||
_sortOrder.value = order
|
||||
}
|
||||
|
||||
fun viewModelDetails(modelId: String) {
|
||||
// TODO-han.yin: Stub for now. Would navigate to model details screen or show dialog
|
||||
}
|
||||
|
||||
// Internal state
|
||||
private val _managementState = MutableStateFlow<ModelManagementState>(ModelManagementState.Idle)
|
||||
val managementState: StateFlow<ModelManagementState> = _managementState.asStateFlow()
|
||||
|
||||
|
|
@ -83,6 +126,10 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
_managementState.value = ModelManagementState.Idle
|
||||
}
|
||||
|
||||
fun viewModelDetails(modelId: String) {
|
||||
// TODO-han.yin: Stub for now. Would navigate to model details screen or show dialog
|
||||
}
|
||||
|
||||
/**
|
||||
* First show confirmation instead of starting import immediately
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package com.example.llama.revamp.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.llama.revamp.data.preferences.UserPreferences
|
||||
import com.example.llama.revamp.data.repository.ModelRepository
|
||||
import com.example.llama.revamp.data.repository.StorageMetrics
|
||||
import com.example.llama.revamp.monitoring.BatteryMetrics
|
||||
import com.example.llama.revamp.monitoring.MemoryMetrics
|
||||
import com.example.llama.revamp.monitoring.PerformanceMonitor
|
||||
|
|
@ -22,10 +23,15 @@ import javax.inject.Inject
|
|||
*/
|
||||
@HiltViewModel
|
||||
class PerformanceViewModel @Inject constructor(
|
||||
private val userPreferences: UserPreferences,
|
||||
private val performanceMonitor: PerformanceMonitor,
|
||||
private val userPreferences: UserPreferences
|
||||
private val modelRepository: ModelRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
// Storage usage metrics
|
||||
private val _storageMetrics = MutableStateFlow<StorageMetrics?>(null)
|
||||
val storageMetrics: StateFlow<StorageMetrics?> = _storageMetrics.asStateFlow()
|
||||
|
||||
// Memory usage metrics
|
||||
private val _memoryUsage = MutableStateFlow(MemoryMetrics(0, 0, 0, 0f, 0f))
|
||||
val memoryUsage: StateFlow<MemoryMetrics> = _memoryUsage.asStateFlow()
|
||||
|
|
@ -68,6 +74,12 @@ class PerformanceViewModel @Inject constructor(
|
|||
private fun startMonitoring() {
|
||||
val interval = _monitoringInterval.value
|
||||
|
||||
viewModelScope.launch {
|
||||
modelRepository.getStorageMetrics().collect { metrics ->
|
||||
_storageMetrics.value = metrics
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
performanceMonitor.monitorMemoryUsage(interval).collect { metrics ->
|
||||
_memoryUsage.value = metrics
|
||||
|
|
@ -132,20 +144,4 @@ class PerformanceViewModel @Inject constructor(
|
|||
private fun isMonitoringActive(): Boolean {
|
||||
return _isMonitoringEnabled.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating PerformanceViewModel instances.
|
||||
*/
|
||||
class Factory(
|
||||
private val performanceMonitor: PerformanceMonitor,
|
||||
private val userPreferences: UserPreferences
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(PerformanceViewModel::class.java)) {
|
||||
return PerformanceViewModel(performanceMonitor, userPreferences) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue