Skip to content

Commit 45c707e

Browse files
committed
better
1 parent b525a29 commit 45c707e

2 files changed

Lines changed: 284 additions & 67 deletions

File tree

app/src/main/java/com/flockyou/ui/screens/AiSettingsScreen.kt

Lines changed: 283 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -518,81 +518,130 @@ private fun ModelSelectorDialog(
518518
onDownloadModel: (AiModel) -> Unit,
519519
onDismiss: () -> Unit
520520
) {
521+
// Categorize models
522+
val builtInModels = availableModels.filter { it == AiModel.RULE_BASED }
523+
val googleAiModels = availableModels.filter { it == AiModel.GEMINI_NANO }
524+
val downloadableModels = availableModels.filter {
525+
it != AiModel.RULE_BASED && it != AiModel.GEMINI_NANO
526+
}
527+
521528
AlertDialog(
522529
onDismissRequest = onDismiss,
523-
title = { Text("Select AI Model") },
530+
title = {
531+
Column {
532+
Text("Select AI Engine")
533+
Text(
534+
text = "Choose how detection analysis is performed",
535+
style = MaterialTheme.typography.bodySmall,
536+
color = MaterialTheme.colorScheme.onSurfaceVariant
537+
)
538+
}
539+
},
524540
text = {
525541
LazyColumn(
526542
verticalArrangement = Arrangement.spacedBy(8.dp)
527543
) {
528-
items(availableModels) { model ->
529-
val isSelected = model.id == currentModelId
530-
val isDownloaded = model == AiModel.RULE_BASED || model == AiModel.GEMINI_NANO // Simplified check
544+
// Built-in section
545+
if (builtInModels.isNotEmpty()) {
546+
item {
547+
EngineCategoryHeader(
548+
title = "Built-in",
549+
subtitle = "No download required",
550+
icon = Icons.Default.CheckCircle
551+
)
552+
}
553+
items(builtInModels) { model ->
554+
EngineOptionCard(
555+
model = model,
556+
isSelected = model.id == currentModelId,
557+
isRecommended = true,
558+
recommendedReason = "Works everywhere",
559+
onSelect = { onSelectModel(model) }
560+
)
561+
}
562+
}
531563

532-
Card(
533-
modifier = Modifier
534-
.fillMaxWidth()
535-
.clickable {
536-
if (isDownloaded || model == AiModel.RULE_BASED) {
537-
onSelectModel(model)
538-
} else {
539-
onDownloadModel(model)
540-
}
541-
},
542-
colors = CardDefaults.cardColors(
543-
containerColor = if (isSelected)
544-
MaterialTheme.colorScheme.primaryContainer
545-
else
546-
MaterialTheme.colorScheme.surfaceVariant
547-
),
548-
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
549-
) {
550-
Column(modifier = Modifier.padding(12.dp)) {
551-
Row(
552-
verticalAlignment = Alignment.CenterVertically
553-
) {
554-
Column(modifier = Modifier.weight(1f)) {
555-
Row(verticalAlignment = Alignment.CenterVertically) {
556-
Text(
557-
text = model.displayName,
558-
style = MaterialTheme.typography.titleSmall,
559-
fontWeight = FontWeight.Medium
560-
)
561-
if (model.requiresNpu) {
562-
Spacer(modifier = Modifier.width(4.dp))
563-
SuggestionChip(
564-
onClick = { },
565-
label = { Text("NPU", style = MaterialTheme.typography.labelSmall) }
566-
)
567-
}
568-
}
569-
if (model.sizeMb > 0) {
570-
Text(
571-
text = "${model.sizeMb} MB • ${model.quantization}",
572-
style = MaterialTheme.typography.labelSmall,
573-
color = MaterialTheme.colorScheme.onSurfaceVariant
574-
)
575-
}
576-
}
577-
if (isSelected) {
578-
Icon(
579-
imageVector = Icons.Default.CheckCircle,
580-
contentDescription = "Selected",
581-
tint = MaterialTheme.colorScheme.primary
582-
)
583-
} else if (model.sizeMb > 0 && model != AiModel.GEMINI_NANO) {
584-
Icon(
585-
imageVector = Icons.Default.Download,
586-
contentDescription = "Download required",
587-
tint = MaterialTheme.colorScheme.onSurfaceVariant
588-
)
589-
}
590-
}
564+
// Google AI section (Gemini Nano)
565+
if (googleAiModels.isNotEmpty()) {
566+
item {
567+
Spacer(modifier = Modifier.height(8.dp))
568+
EngineCategoryHeader(
569+
title = "Google AI",
570+
subtitle = "Powered by Gemini Nano via AICore",
571+
icon = Icons.Default.AutoAwesome
572+
)
573+
}
574+
items(googleAiModels) { model ->
575+
val hasAiCore = deviceCapabilities?.hasAiCore == true
576+
EngineOptionCard(
577+
model = model,
578+
isSelected = model.id == currentModelId,
579+
isRecommended = hasAiCore,
580+
recommendedReason = if (hasAiCore) "Best for Pixel 8+" else null,
581+
isAvailable = hasAiCore,
582+
unavailableReason = if (!hasAiCore) "Requires Pixel 8+ with AICore" else null,
583+
onSelect = { if (hasAiCore) onSelectModel(model) }
584+
)
585+
}
586+
}
587+
588+
// Downloadable GGUF Models section
589+
if (downloadableModels.isNotEmpty()) {
590+
item {
591+
Spacer(modifier = Modifier.height(8.dp))
592+
EngineCategoryHeader(
593+
title = "Downloadable Models",
594+
subtitle = "GGUF format • Runs on any device",
595+
icon = Icons.Default.Download
596+
)
597+
}
598+
599+
// Small models (< 500MB)
600+
val smallModels = downloadableModels.filter { it.sizeMb < 500 }
601+
val largeModels = downloadableModels.filter { it.sizeMb >= 500 }
602+
603+
if (smallModels.isNotEmpty()) {
604+
item {
605+
Text(
606+
text = "Lightweight (< 500 MB)",
607+
style = MaterialTheme.typography.labelSmall,
608+
color = MaterialTheme.colorScheme.primary,
609+
modifier = Modifier.padding(start = 4.dp, top = 4.dp)
610+
)
611+
}
612+
items(smallModels) { model ->
613+
EngineOptionCard(
614+
model = model,
615+
isSelected = model.id == currentModelId,
616+
isRecommended = model == AiModel.SMOLLM_360M,
617+
recommendedReason = if (model == AiModel.SMOLLM_360M) "Best balance" else null,
618+
showDownloadIcon = true,
619+
onSelect = { onDownloadModel(model) }
620+
)
621+
}
622+
}
623+
624+
if (largeModels.isNotEmpty()) {
625+
item {
591626
Spacer(modifier = Modifier.height(4.dp))
592627
Text(
593-
text = model.description,
594-
style = MaterialTheme.typography.bodySmall,
595-
color = MaterialTheme.colorScheme.onSurfaceVariant
628+
text = "Full-Size (500+ MB)",
629+
style = MaterialTheme.typography.labelSmall,
630+
color = MaterialTheme.colorScheme.primary,
631+
modifier = Modifier.padding(start = 4.dp, top = 4.dp)
632+
)
633+
}
634+
items(largeModels) { model ->
635+
val hasEnoughRam = (deviceCapabilities?.availableRamMb ?: 0) > model.sizeMb * 1.5
636+
EngineOptionCard(
637+
model = model,
638+
isSelected = model.id == currentModelId,
639+
isRecommended = model == AiModel.GEMMA2_2B && hasEnoughRam,
640+
recommendedReason = if (model == AiModel.GEMMA2_2B && hasEnoughRam) "Best quality" else null,
641+
isAvailable = hasEnoughRam,
642+
unavailableReason = if (!hasEnoughRam) "Needs ${(model.sizeMb * 1.5).toInt()} MB RAM" else null,
643+
showDownloadIcon = true,
644+
onSelect = { if (hasEnoughRam) onDownloadModel(model) }
596645
)
597646
}
598647
}
@@ -607,6 +656,174 @@ private fun ModelSelectorDialog(
607656
)
608657
}
609658

659+
@Composable
660+
private fun EngineCategoryHeader(
661+
title: String,
662+
subtitle: String,
663+
icon: androidx.compose.ui.graphics.vector.ImageVector
664+
) {
665+
Row(
666+
modifier = Modifier
667+
.fillMaxWidth()
668+
.padding(vertical = 4.dp),
669+
verticalAlignment = Alignment.CenterVertically
670+
) {
671+
Icon(
672+
imageVector = icon,
673+
contentDescription = null,
674+
tint = MaterialTheme.colorScheme.primary,
675+
modifier = Modifier.size(20.dp)
676+
)
677+
Spacer(modifier = Modifier.width(8.dp))
678+
Column {
679+
Text(
680+
text = title,
681+
style = MaterialTheme.typography.titleSmall,
682+
fontWeight = FontWeight.Bold,
683+
color = MaterialTheme.colorScheme.primary
684+
)
685+
Text(
686+
text = subtitle,
687+
style = MaterialTheme.typography.labelSmall,
688+
color = MaterialTheme.colorScheme.onSurfaceVariant
689+
)
690+
}
691+
}
692+
}
693+
694+
@OptIn(ExperimentalMaterial3Api::class)
695+
@Composable
696+
private fun EngineOptionCard(
697+
model: AiModel,
698+
isSelected: Boolean,
699+
isRecommended: Boolean = false,
700+
recommendedReason: String? = null,
701+
isAvailable: Boolean = true,
702+
unavailableReason: String? = null,
703+
showDownloadIcon: Boolean = false,
704+
onSelect: () -> Unit
705+
) {
706+
val alpha = if (isAvailable) 1f else 0.5f
707+
708+
Card(
709+
modifier = Modifier
710+
.fillMaxWidth()
711+
.clickable(enabled = isAvailable) { onSelect() },
712+
colors = CardDefaults.cardColors(
713+
containerColor = when {
714+
isSelected -> MaterialTheme.colorScheme.primaryContainer
715+
isRecommended && isAvailable -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f)
716+
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha)
717+
}
718+
),
719+
border = when {
720+
isSelected -> BorderStroke(2.dp, MaterialTheme.colorScheme.primary)
721+
isRecommended && isAvailable -> BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f))
722+
else -> null
723+
}
724+
) {
725+
Column(modifier = Modifier.padding(12.dp)) {
726+
Row(
727+
verticalAlignment = Alignment.CenterVertically
728+
) {
729+
Column(modifier = Modifier.weight(1f)) {
730+
Row(verticalAlignment = Alignment.CenterVertically) {
731+
Text(
732+
text = model.displayName,
733+
style = MaterialTheme.typography.titleSmall,
734+
fontWeight = FontWeight.Medium,
735+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha)
736+
)
737+
if (model.requiresNpu) {
738+
Spacer(modifier = Modifier.width(6.dp))
739+
Surface(
740+
shape = MaterialTheme.shapes.small,
741+
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.2f)
742+
) {
743+
Text(
744+
text = "NPU",
745+
style = MaterialTheme.typography.labelSmall,
746+
color = MaterialTheme.colorScheme.tertiary,
747+
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
748+
)
749+
}
750+
}
751+
if (isRecommended && recommendedReason != null) {
752+
Spacer(modifier = Modifier.width(6.dp))
753+
Surface(
754+
shape = MaterialTheme.shapes.small,
755+
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
756+
) {
757+
Text(
758+
text = recommendedReason,
759+
style = MaterialTheme.typography.labelSmall,
760+
color = MaterialTheme.colorScheme.primary,
761+
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
762+
)
763+
}
764+
}
765+
}
766+
if (model.sizeMb > 0) {
767+
Text(
768+
text = "${model.sizeMb} MB • ${model.quantization}",
769+
style = MaterialTheme.typography.labelSmall,
770+
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha)
771+
)
772+
}
773+
}
774+
when {
775+
isSelected -> Icon(
776+
imageVector = Icons.Default.CheckCircle,
777+
contentDescription = "Selected",
778+
tint = MaterialTheme.colorScheme.primary
779+
)
780+
!isAvailable -> Icon(
781+
imageVector = Icons.Default.Block,
782+
contentDescription = "Unavailable",
783+
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
784+
)
785+
showDownloadIcon -> Icon(
786+
imageVector = Icons.Default.Download,
787+
contentDescription = "Download required",
788+
tint = MaterialTheme.colorScheme.primary
789+
)
790+
}
791+
}
792+
793+
Spacer(modifier = Modifier.height(4.dp))
794+
795+
Text(
796+
text = if (!isAvailable && unavailableReason != null) unavailableReason else model.description,
797+
style = MaterialTheme.typography.bodySmall,
798+
color = if (!isAvailable) MaterialTheme.colorScheme.error.copy(alpha = 0.7f)
799+
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha)
800+
)
801+
802+
// Show capabilities for non-rule-based models
803+
if (model != AiModel.RULE_BASED && isAvailable) {
804+
Spacer(modifier = Modifier.height(6.dp))
805+
Row(
806+
horizontalArrangement = Arrangement.spacedBy(4.dp)
807+
) {
808+
model.capabilities.take(3).forEach { cap ->
809+
Surface(
810+
shape = MaterialTheme.shapes.extraSmall,
811+
color = MaterialTheme.colorScheme.surfaceVariant
812+
) {
813+
Text(
814+
text = cap,
815+
style = MaterialTheme.typography.labelSmall,
816+
color = MaterialTheme.colorScheme.onSurfaceVariant,
817+
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
818+
)
819+
}
820+
}
821+
}
822+
}
823+
}
824+
}
825+
}
826+
610827
@Composable
611828
private fun CapabilitiesCard(
612829
settings: AiSettings,

app/src/main/java/com/flockyou/ui/screens/AllDetectionsScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ private fun UnifiedPatternCard(
720720
val uriHandler = LocalUriHandler.current
721721
Column {
722722
Spacer(modifier = Modifier.height(8.dp))
723-
HorizontalDivider()
723+
Divider()
724724
Spacer(modifier = Modifier.height(8.dp))
725725

726726
Text(

0 commit comments

Comments
 (0)