Skip to content

Commit b820c8b

Browse files
committed
Add HF model picker and cache manager
1 parent 307e709 commit b820c8b

5 files changed

Lines changed: 753 additions & 23 deletions

File tree

app/src/main/java/tridefender/llama/snapdragon/ui/MainScreen.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.navigation.compose.rememberNavController
1717
import tridefender.llama.snapdragon.R
1818
import tridefender.llama.snapdragon.ui.kernel.KernelScreen
1919
import tridefender.llama.snapdragon.ui.launcher.AllConfigScreen
20+
import tridefender.llama.snapdragon.ui.launcher.ModelManagerScreen
2021
import tridefender.llama.snapdragon.ui.runtime.RuntimeScreen
2122

2223
@OptIn(ExperimentalMaterial3Api::class)
@@ -35,7 +36,7 @@ fun MainScreen(
3536
NavigationBarItem(
3637
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
3738
label = { Text(stringResource(R.string.nav_config)) },
38-
selected = currentRoute == "config",
39+
selected = currentRoute == "config" || currentRoute == "model_manager",
3940
onClick = {
4041
if (currentRoute != "config") {
4142
navController.navigate("config") {
@@ -77,7 +78,10 @@ fun MainScreen(
7778
modifier = Modifier.padding(paddingValues)
7879
) {
7980
composable("config") {
80-
AllConfigScreen()
81+
AllConfigScreen(onManageModels = { navController.navigate("model_manager") })
82+
}
83+
composable("model_manager") {
84+
ModelManagerScreen(onBack = { navController.popBackStack() })
8185
}
8286
composable("kernels") {
8387
KernelScreen()

app/src/main/java/tridefender/llama/snapdragon/ui/launcher/AllConfigScreen.kt

Lines changed: 174 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ import androidx.compose.animation.fadeIn
1111
import androidx.compose.animation.fadeOut
1212
import androidx.compose.animation.shrinkVertically
1313
import androidx.compose.foundation.layout.*
14+
import androidx.compose.foundation.lazy.LazyColumn
15+
import androidx.compose.foundation.lazy.items
1416
import androidx.compose.foundation.rememberScrollState
1517
import androidx.compose.foundation.shape.RoundedCornerShape
1618
import androidx.compose.foundation.verticalScroll
1719
import androidx.compose.material.icons.Icons
1820
import androidx.compose.material.icons.filled.Android
1921
import androidx.compose.material.icons.filled.Cloud
22+
import androidx.compose.material.icons.filled.CloudDownload
2023
import androidx.compose.material.icons.filled.Folder
2124
import androidx.compose.material.icons.filled.Memory
2225
import androidx.compose.material.icons.filled.Settings
2326
import androidx.compose.material.icons.filled.Storage
2427
import androidx.compose.material.icons.outlined.Check
25-
import androidx.compose.material.icons.outlined.Refresh
2628
import androidx.compose.material3.*
2729
import androidx.compose.runtime.*
2830
import androidx.compose.ui.Alignment
@@ -41,6 +43,8 @@ import tridefender.llama.snapdragon.model.DeviceType
4143
import tridefender.llama.snapdragon.model.FlashAttentionMode
4244
import tridefender.llama.snapdragon.model.MemoryFitState
4345
import tridefender.llama.snapdragon.model.PoolingType
46+
import tridefender.llama.snapdragon.viewmodel.HuggingFaceFileEntry
47+
import tridefender.llama.snapdragon.viewmodel.HuggingFacePickerState
4448
import tridefender.llama.snapdragon.viewmodel.ModelCatalogEntry
4549
import tridefender.llama.snapdragon.viewmodel.ModelConfigViewModel
4650

@@ -84,11 +88,14 @@ fun NumberField(
8488
@OptIn(ExperimentalMaterial3Api::class)
8589
@Composable
8690
fun AllConfigScreen(
91+
onManageModels: () -> Unit = {},
8792
viewModel: ModelConfigViewModel = hiltViewModel()
8893
) {
8994
val config by viewModel.config.collectAsState()
9095
val analysis by viewModel.analysis.collectAsState()
96+
val hfPicker by viewModel.hfPicker.collectAsState()
9197
val context = LocalContext.current
98+
var showHfPicker by remember { mutableStateOf(false) }
9299

93100
val filePickerLauncher = rememberLauncherForActivityResult(
94101
contract = ActivityResultContracts.StartActivityForResult()
@@ -124,7 +131,8 @@ fun AllConfigScreen(
124131
onModelSelect = { viewModel.updateModelPath(it) },
125132
onIsEmbeddingChange = { viewModel.updateIsEmbedding(it) },
126133
onPoolingTypeChange = { viewModel.updatePoolingType(it) },
127-
onRefreshModels = { viewModel.refreshModelCatalog() },
134+
onHfPickerClick = { showHfPicker = true },
135+
onManageModels = onManageModels,
128136
onBrowseClick = {
129137
filePickerLauncher.launch(
130138
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
@@ -136,6 +144,18 @@ fun AllConfigScreen(
136144
)
137145
}
138146
)
147+
148+
if (showHfPicker) {
149+
HuggingFaceModelPickerDialog(
150+
state = hfPicker,
151+
onDismiss = { showHfPicker = false },
152+
onQueryChange = viewModel::updateHfQuery,
153+
onSearch = viewModel::searchHuggingFaceModels,
154+
onRepoSelect = viewModel::loadHuggingFaceFiles,
155+
onDownload = viewModel::downloadHuggingFaceFile,
156+
onMessageShown = viewModel::clearHfMessage
157+
)
158+
}
139159

140160
DeviceSection(
141161
deviceType = config.deviceType,
@@ -194,7 +214,8 @@ fun ModelSection(
194214
onModelSelect: (String) -> Unit,
195215
onIsEmbeddingChange: (Boolean) -> Unit,
196216
onPoolingTypeChange: (PoolingType) -> Unit,
197-
onRefreshModels: () -> Unit,
217+
onHfPickerClick: () -> Unit,
218+
onManageModels: () -> Unit,
198219
onBrowseClick: () -> Unit
199220
) {
200221
ConfigSectionCard(
@@ -281,29 +302,40 @@ fun ModelSection(
281302

282303
Row(
283304
modifier = Modifier.fillMaxWidth(),
284-
horizontalArrangement = Arrangement.End,
305+
horizontalArrangement = Arrangement.SpaceBetween,
285306
verticalAlignment = Alignment.CenterVertically
286307
) {
287-
TextButton(onClick = onRefreshModels) {
308+
TextButton(onClick = onHfPickerClick) {
288309
Icon(
289-
Icons.Outlined.Refresh,
310+
Icons.Default.CloudDownload,
290311
contentDescription = null,
291312
modifier = Modifier.size(18.dp)
292313
)
293314
Spacer(modifier = Modifier.width(6.dp))
294-
Text(stringResource(R.string.refresh_models))
315+
Text(stringResource(R.string.hf_model_picker))
295316
}
296-
TextButton(onClick = onBrowseClick) {
297-
Icon(
298-
Icons.Default.Folder,
299-
contentDescription = null,
300-
modifier = Modifier.size(18.dp)
301-
)
302-
Spacer(modifier = Modifier.width(6.dp))
303-
Text(stringResource(R.string.browse))
317+
Row(verticalAlignment = Alignment.CenterVertically) {
318+
TextButton(onClick = onManageModels) {
319+
Icon(
320+
Icons.Default.Settings,
321+
contentDescription = null,
322+
modifier = Modifier.size(18.dp)
323+
)
324+
Spacer(modifier = Modifier.width(6.dp))
325+
Text(stringResource(R.string.manage_models))
326+
}
327+
TextButton(onClick = onBrowseClick) {
328+
Icon(
329+
Icons.Default.Folder,
330+
contentDescription = null,
331+
modifier = Modifier.size(18.dp)
332+
)
333+
Spacer(modifier = Modifier.width(6.dp))
334+
Text(stringResource(R.string.browse))
335+
}
304336
}
305337
}
306-
338+
307339
Spacer(modifier = Modifier.height(16.dp))
308340

309341
Surface(
@@ -762,7 +794,7 @@ private fun MemoryFitState.fitColor(): Color = when (this) {
762794
MemoryFitState.GOOD_FIT -> Color(0xFF2E7D32)
763795
}
764796

765-
private fun formatBytes(bytes: Long): String {
797+
internal fun formatBytes(bytes: Long): String {
766798
val absBytes = kotlin.math.abs(bytes.toDouble())
767799
val gib = 1024.0 * 1024.0 * 1024.0
768800
val mib = 1024.0 * 1024.0
@@ -773,6 +805,131 @@ private fun formatBytes(bytes: Long): String {
773805
}
774806
}
775807

808+
@OptIn(ExperimentalMaterial3Api::class)
809+
@Composable
810+
private fun HuggingFaceModelPickerDialog(
811+
state: HuggingFacePickerState,
812+
onDismiss: () -> Unit,
813+
onQueryChange: (String) -> Unit,
814+
onSearch: () -> Unit,
815+
onRepoSelect: (String) -> Unit,
816+
onDownload: (HuggingFaceFileEntry) -> Unit,
817+
onMessageShown: () -> Unit
818+
) {
819+
AlertDialog(
820+
onDismissRequest = onDismiss,
821+
confirmButton = {
822+
TextButton(onClick = onDismiss) {
823+
Text(stringResource(R.string.close))
824+
}
825+
},
826+
title = { Text(stringResource(R.string.hf_model_picker)) },
827+
text = {
828+
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
829+
Row(
830+
modifier = Modifier.fillMaxWidth(),
831+
horizontalArrangement = Arrangement.spacedBy(8.dp),
832+
verticalAlignment = Alignment.CenterVertically
833+
) {
834+
OutlinedTextField(
835+
value = state.query,
836+
onValueChange = onQueryChange,
837+
label = { Text(stringResource(R.string.hf_search_hint)) },
838+
singleLine = true,
839+
modifier = Modifier.weight(1f)
840+
)
841+
Button(
842+
onClick = onSearch,
843+
enabled = !state.isSearching && state.downloadFileName == null
844+
) {
845+
Text(stringResource(R.string.search))
846+
}
847+
}
848+
849+
state.message?.let { message ->
850+
AssistChip(
851+
onClick = onMessageShown,
852+
label = { Text(message, maxLines = 2, overflow = TextOverflow.Ellipsis) }
853+
)
854+
}
855+
856+
if (state.isSearching || state.isLoadingFiles) {
857+
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
858+
}
859+
860+
LazyColumn(
861+
modifier = Modifier
862+
.fillMaxWidth()
863+
.heightIn(max = 360.dp),
864+
verticalArrangement = Arrangement.spacedBy(6.dp)
865+
) {
866+
if (state.selectedRepoId == null) {
867+
items(state.searchResults) { repo ->
868+
ListItem(
869+
headlineContent = {
870+
Text(repo.modelId, maxLines = 1, overflow = TextOverflow.Ellipsis)
871+
},
872+
supportingContent = {
873+
Text(
874+
stringResource(
875+
R.string.hf_model_stats,
876+
repo.downloads ?: 0,
877+
repo.likes ?: 0
878+
)
879+
)
880+
},
881+
trailingContent = {
882+
TextButton(onClick = { onRepoSelect(repo.modelId) }) {
883+
Text(stringResource(R.string.select))
884+
}
885+
}
886+
)
887+
}
888+
} else {
889+
item {
890+
TextButton(onClick = { onQueryChange(state.query); onSearch() }) {
891+
Text(stringResource(R.string.hf_back_to_results))
892+
}
893+
}
894+
items(state.files) { file ->
895+
ListItem(
896+
headlineContent = {
897+
Text(file.fileName, maxLines = 1, overflow = TextOverflow.Ellipsis)
898+
},
899+
supportingContent = {
900+
Text(file.sizeBytes?.let { formatBytes(it) } ?: stringResource(R.string.unknown_size))
901+
},
902+
trailingContent = {
903+
TextButton(
904+
onClick = { onDownload(file) },
905+
enabled = state.downloadFileName == null
906+
) {
907+
Text(stringResource(R.string.download))
908+
}
909+
}
910+
)
911+
}
912+
}
913+
}
914+
915+
state.downloadFileName?.let { fileName ->
916+
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
917+
Text(
918+
stringResource(R.string.downloading_model, fileName),
919+
maxLines = 1,
920+
overflow = TextOverflow.Ellipsis
921+
)
922+
LinearProgressIndicator(
923+
progress = { state.downloadProgress ?: 0f },
924+
modifier = Modifier.fillMaxWidth()
925+
)
926+
}
927+
}
928+
}
929+
}
930+
)
931+
}
932+
776933
@Composable
777934
fun ConfigSectionCard(
778935
title: String,

0 commit comments

Comments
 (0)