feat: HuggingFace GGUF Explorer & Downloader integration#82
Conversation
Implements Issue Siddhesh2377#81 - HuggingFace GGUF repository explorer in Model Store. Features: - HuggingFace Hub API integration for searching GGUF model repositories - New HuggingFaceExplorerRepository with searchGgufRepositories() - Explorer UI card in Model Store Settings tab with search, results display - Animated expand/collapse, staggered result entry, status transitions - One-tap add of discovered repos to the model store Additional fixes: - Fix UnusedMaterial3ScaffoldPaddingParameter lint error in IntroScreen - Add missing test dependencies (junit, androidx-junit, espresso-core) to all modules Closes Siddhesh2377#81
There was a problem hiding this comment.
Pull request overview
Adds a HuggingFace GGUF Explorer to the Model Store Settings tab, enabling users to search HuggingFace for GGUF repositories and add them to the app’s repository list.
Changes:
- Introduces HuggingFace Hub search API support and a new
HuggingFaceExplorerRepositoryfor GGUF repo discovery. - Adds Explorer state + actions to
ModelStoreViewModeland a new Explorer UI card (search, animated results, add button) to the Settings tab. - Applies a Scaffold padding lint fix in
IntroScreenand adds missing test dependencies across modules.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| neuron-packet/build.gradle.kts | Adds unit + instrumentation test dependencies. |
| memory-vault/build.gradle.kts | Adds unit + instrumentation test dependencies. |
| app/build.gradle.kts | Adds unit + instrumentation test dependencies for the app module. |
| app/src/main/java/com/dark/tool_neuron/viewmodel/ModelStoreViewModel.kt | Adds explorer query/results/loading/error state plus search/add actions. |
| app/src/main/java/com/dark/tool_neuron/ui/screen/ModelStoreScreen.kt | Adds Explorer card UI in Settings tab with animations and add-to-store action. |
| app/src/main/java/com/dark/tool_neuron/ui/screen/IntroScreen.kt | Fixes Scaffold inner padding usage. |
| app/src/main/java/com/dark/tool_neuron/repo/HuggingFaceExplorerRepository.kt | New repository wrapper for searching GGUF repos via HuggingFace API. |
| app/src/main/java/com/dark/tool_neuron/network/HuggingFaceApi.kt | Adds searchModels() endpoint + response model. |
| README.md | Updates product description text (contains a new typo). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| results.take(8).forEachIndexed { index, repo -> | ||
| val isAdded = existingRepoPaths.contains(repo.id.lowercase()) | ||
|
|
||
| var visible by remember(repo.id) { mutableStateOf(false) } | ||
| LaunchedEffect(repo.id) { | ||
| delay(index * 60L) | ||
| visible = true | ||
| } | ||
|
|
||
| AnimatedVisibility( | ||
| visible = visible, | ||
| enter = slideInVertically( | ||
| initialOffsetY = { it / 2 }, | ||
| animationSpec = spring( | ||
| dampingRatio = 0.75f, | ||
| stiffness = 300f | ||
| ) | ||
| ) + fadeIn(spring(stiffness = 300f)) | ||
| ) { | ||
| Column { | ||
| ExplorerResultRow( | ||
| repo = repo, | ||
| isAdded = isAdded, | ||
| onAdd = { onAdd(repo) } | ||
| ) | ||
| if (index < results.take(8).lastIndex) { | ||
| HorizontalDivider( | ||
| color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f) | ||
| ) |
There was a problem hiding this comment.
results.take(8) is recomputed multiple times inside the results loop (including inside the divider condition), which creates unnecessary list allocations on recomposition. Consider computing a single val displayedResults = results.take(8) before the loop and reuse it.
| </p> | ||
|
|
||
| ToolNeuron is the most advanced offline-first AI assistant for Android, featuring complete on-device processing with enterprise-grade encryption, intelligent document understanding through RAG (Retrieval-Augmented Generation), text-to-speech, an extensible plugin system, AI character cards (TavernAI v2 compatible), persistent AI memory, and sophisticated memory management. Your data never leaves your device. No cloud dependencies. No subscriptions. True digital sovereignty. | ||
| ToolNeuron is the most advanced offline-first AI assistant for Android, featuring complete on-device processing with enterprise-grade encryption, intelligent document understanding through RAG (Retrieval-Augmented Generation), text-to-speech, an extensible plugin system, AI character cards (TavernAI v2 compatible), persistent AImemory, and sophisticated memory management. Your data never leaves your device. No cloud dependencies. No subscriptions. True digital sovereignty. |
There was a problem hiding this comment.
README sentence now reads "persistent AImemory" (missing space), which looks like a typo in the project description. Please change it back to "AI memory" (or the preferred wording used elsewhere).
| ToolNeuron is the most advanced offline-first AI assistant for Android, featuring complete on-device processing with enterprise-grade encryption, intelligent document understanding through RAG (Retrieval-Augmented Generation), text-to-speech, an extensible plugin system, AI character cards (TavernAI v2 compatible), persistent AImemory, and sophisticated memory management. Your data never leaves your device. No cloud dependencies. No subscriptions. True digital sovereignty. | |
| ToolNeuron is the most advanced offline-first AI assistant for Android, featuring complete on-device processing with enterprise-grade encryption, intelligent document understanding through RAG (Retrieval-Augmented Generation), text-to-speech, an extensible plugin system, AI character cards (TavernAI v2 compatible), persistent AI memory, and sophisticated memory management. Your data never leaves your device. No cloud dependencies. No subscriptions. True digital sovereignty. |
| fun searchExplorerRepositories() { | ||
| viewModelScope.launch { | ||
| val query = _explorerQuery.value.trim() | ||
| if (query.isBlank()) { | ||
| _explorerError.value = "Enter a search term" | ||
| _explorerResults.value = emptyList() | ||
| return@launch | ||
| } | ||
|
|
||
| _isExplorerLoading.value = true | ||
| _explorerError.value = null | ||
|
|
||
| explorerRepository.searchGgufRepositories(query).onSuccess { repos -> | ||
| _explorerResults.value = repos | ||
| if (repos.isEmpty()) { | ||
| _explorerError.value = "No repositories found" | ||
| } | ||
| }.onFailure { exception -> | ||
| _explorerResults.value = emptyList() | ||
| _explorerError.value = exception.message ?: "Search failed" | ||
| } | ||
|
|
||
| _isExplorerLoading.value = false | ||
| } |
There was a problem hiding this comment.
searchExplorerRepositories() performs a network request and response mapping inside viewModelScope.launch without switching to an IO dispatcher. This work can run on the Main thread and cause UI jank; consider wrapping the search call (and mapping) in withContext(Dispatchers.IO) (similar to RepositoryValidator.validateRepository).
| fun searchExplorerRepositories() { | ||
| viewModelScope.launch { | ||
| val query = _explorerQuery.value.trim() | ||
| if (query.isBlank()) { | ||
| _explorerError.value = "Enter a search term" | ||
| _explorerResults.value = emptyList() | ||
| return@launch | ||
| } | ||
|
|
||
| _isExplorerLoading.value = true | ||
| _explorerError.value = null | ||
|
|
||
| explorerRepository.searchGgufRepositories(query).onSuccess { repos -> | ||
| _explorerResults.value = repos | ||
| if (repos.isEmpty()) { | ||
| _explorerError.value = "No repositories found" | ||
| } | ||
| }.onFailure { exception -> | ||
| _explorerResults.value = emptyList() | ||
| _explorerError.value = exception.message ?: "Search failed" | ||
| } | ||
|
|
||
| _isExplorerLoading.value = false | ||
| } |
There was a problem hiding this comment.
searchExplorerRepositories() can be triggered multiple times, launching concurrent coroutines. This can lead to out-of-order results and incorrect _isExplorerLoading toggling (an earlier request finishing last can overwrite newer state). Consider tracking a search Job and cancelling previous searches, and set _isExplorerLoading back to false in a finally block tied to the latest request.
| suspend fun searchGgufRepositories(query: String, limit: Int = 20): Result<List<HuggingFaceExplorerRepo>> { | ||
| return try { | ||
| val response = HuggingFaceClient.api.searchModels( | ||
| filter = "gguf", | ||
| search = query.trim(), | ||
| sort = "downloads", | ||
| direction = -1, | ||
| limit = limit.coerceIn(1, 50) | ||
| ) | ||
|
|
||
| if (!response.isSuccessful) { | ||
| return Result.failure(Exception("Search failed (${response.code()})")) | ||
| } | ||
|
|
||
| val repositories = response.body().orEmpty() | ||
| .mapNotNull { repo -> | ||
| val repoId = repo.id | ||
| if (repoId.isBlank() || !repoId.contains("/")) return@mapNotNull null | ||
| HuggingFaceExplorerRepo( | ||
| id = repoId, | ||
| author = repo.author ?: repoId.substringBefore("/"), | ||
| downloads = repo.downloads ?: 0L, | ||
| likes = repo.likes ?: 0L, | ||
| gated = repo.gated ?: false, | ||
| tags = repo.tags.orEmpty().filter { it.isNotBlank() }.take(6) | ||
| ) | ||
| } | ||
| .distinctBy { it.id } | ||
|
|
||
| Result.success(repositories) | ||
| } catch (e: Exception) { | ||
| Result.failure(e) | ||
| } |
There was a problem hiding this comment.
searchGgufRepositories() performs the Retrofit call and maps the full response without enforcing an IO dispatcher. Since callers may invoke this from the Main thread, consider wrapping the body of this method with withContext(Dispatchers.IO) (and keep only lightweight state updates on Main) to avoid accidental Main-thread work.
| ActionButton( | ||
| onClickListener = {}, | ||
| icon = Icons.Default.CheckCircle, |
There was a problem hiding this comment.
When isAdded is true, the UI renders an ActionButton with an empty onClickListener. This remains focusable/clickable but does nothing, which is confusing and hurts accessibility. Consider rendering a non-clickable icon (or add an enabled parameter to ActionButton and set it to false, with disabled semantics/colors).
| ActionButton( | |
| onClickListener = {}, | |
| icon = Icons.Default.CheckCircle, | |
| Icon( | |
| imageVector = Icons.Default.CheckCircle, |
- Extract displayedResults to avoid repeated results.take(8) allocations - Wrap searchGgufRepositories() in withContext(Dispatchers.IO) - Track search Job and cancel previous searches to prevent race conditions - Use finally block for _isExplorerLoading to ensure proper cleanup - Replace clickable ActionButton with non-clickable Icon for added state - Fix 'AImemory' typo in README
|
@Siddhesh2377 can you review the pr? |
Hey working on some stuff, the pr review will take some time |
|
Hey @Godzilla675 I have made some major changes in the code base so i think this pr is not a good option, maybe update the fork or re-fork it and create a pr, as it will be very veyr easy now that most of the use less code has been deleted |
Resolve remaining Model Store merge conflicts after syncing with the latest base branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@Siddhesh2377 I fixed the merge conflicts. |
Summary
Implements Issue #81 — adds a HuggingFace GGUF Explorer to the Model Store Settings tab.
Features
Additional Fixes
Build Verification
✅ assembleDebug ✅ assembleRelease ✅ lintDebug ✅ testDebugUnitTest (all modules)
Closes #81