@@ -11,18 +11,20 @@ import androidx.compose.animation.fadeIn
1111import androidx.compose.animation.fadeOut
1212import androidx.compose.animation.shrinkVertically
1313import androidx.compose.foundation.layout.*
14+ import androidx.compose.foundation.lazy.LazyColumn
15+ import androidx.compose.foundation.lazy.items
1416import androidx.compose.foundation.rememberScrollState
1517import androidx.compose.foundation.shape.RoundedCornerShape
1618import androidx.compose.foundation.verticalScroll
1719import androidx.compose.material.icons.Icons
1820import androidx.compose.material.icons.filled.Android
1921import androidx.compose.material.icons.filled.Cloud
22+ import androidx.compose.material.icons.filled.CloudDownload
2023import androidx.compose.material.icons.filled.Folder
2124import androidx.compose.material.icons.filled.Memory
2225import androidx.compose.material.icons.filled.Settings
2326import androidx.compose.material.icons.filled.Storage
2427import androidx.compose.material.icons.outlined.Check
25- import androidx.compose.material.icons.outlined.Refresh
2628import androidx.compose.material3.*
2729import androidx.compose.runtime.*
2830import androidx.compose.ui.Alignment
@@ -41,6 +43,8 @@ import tridefender.llama.snapdragon.model.DeviceType
4143import tridefender.llama.snapdragon.model.FlashAttentionMode
4244import tridefender.llama.snapdragon.model.MemoryFitState
4345import tridefender.llama.snapdragon.model.PoolingType
46+ import tridefender.llama.snapdragon.viewmodel.HuggingFaceFileEntry
47+ import tridefender.llama.snapdragon.viewmodel.HuggingFacePickerState
4448import tridefender.llama.snapdragon.viewmodel.ModelCatalogEntry
4549import tridefender.llama.snapdragon.viewmodel.ModelConfigViewModel
4650
@@ -84,11 +88,14 @@ fun NumberField(
8488@OptIn(ExperimentalMaterial3Api ::class )
8589@Composable
8690fun 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
777934fun ConfigSectionCard (
778935 title : String ,
0 commit comments