Skip to content

Commit 8b21e7a

Browse files
Merge pull request #14 from alichherawalla/feat/bring-your-own-model
Feat/bring your own model
2 parents 7f36ee6 + 6885d08 commit 8b21e7a

28 files changed

Lines changed: 4733 additions & 728 deletions

.maestro/flows/p0/00-setup-model.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,10 @@ tags:
155155
timeout: 15000
156156
- takeScreenshot: 10-files
157157

158-
# Download the model
158+
# Download the model (tap the download icon on the first file card)
159159
- evalScript: "${console.log('SETUP - Downloading')}"
160160
- tapOn:
161-
text: "Download"
161+
id: "file-card-0-download"
162162
- extendedWaitUntil:
163163
visible:
164164
text: "Success"

.maestro/flows/p0/04-image-generation.yaml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,10 @@ tags:
142142
text: "Absolute Reality (CPU)"
143143
timeout: 5000
144144

145-
# Tap download button (1st card = index 0)
145+
# Tap download icon on 1st image model card
146146
- evalScript: "${console.log('IMAGE_GEN - Downloading CPU model')}"
147147
- tapOn:
148-
text: "Download"
149-
index: 0
148+
id: "image-model-card-0-download"
150149

151150
# Wait for download to complete
152151
- evalScript: "${console.log('IMAGE_GEN - Waiting for download...')}"
@@ -195,6 +194,14 @@ tags:
195194
- tapOn:
196195
id: "chat-settings-icon"
197196

197+
# Expand the IMAGE GENERATION section (collapsed by default)
198+
- extendedWaitUntil:
199+
visible:
200+
text: "IMAGE GENERATION"
201+
timeout: 5000
202+
- tapOn:
203+
text: "IMAGE GENERATION"
204+
198205
- extendedWaitUntil:
199206
visible:
200207
text: "Auto-detect image requests"

README.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ OffgridMobile is a React Native application that brings large language models, v
3333
## Key Features at a Glance
3434

3535
- **Text Generation** - Multi-model GGUF support, streaming inference, custom system prompts
36+
- **Bring Your Own Model** - Import local .gguf files from device storage alongside HuggingFace downloads
37+
- **Smart Model Discovery** - Curated recommendations by device RAM, advanced filters (org, size, quantization, type)
3638
- **Vision AI** - Multimodal understanding with automatic mmproj handling
3739
- **Image Generation** - On-device Stable Diffusion (CPU/NPU), real-time preview, background generation
3840
- **AI Prompt Enhancement** - Use text LLM to expand simple prompts into detailed descriptions for better image quality
@@ -56,7 +58,7 @@ OffgridMobile is a React Native application that brings large language models, v
5658

5759
### Text Generation
5860

59-
Multi-model LLM inference using llama.cpp compiled for ARM64 Android via llama.rn native bindings. Supports any GGUF-format model compatible with llama.cpp:
61+
Multi-model LLM inference using llama.cpp compiled for ARM64 Android via llama.rn native bindings. Supports any GGUF-format model compatible with llama.cpp — downloaded from HuggingFace or imported directly from device storage:
6062

6163
- **Streaming inference** with real-time token callbacks
6264
- **OpenCL GPU offloading** on Qualcomm Adreno GPUs (experimental, optional)
@@ -92,7 +94,8 @@ Multimodal understanding via vision-language models (VLMs) with automatic mmproj
9294

9395
**Supported Vision Models:**
9496
- SmolVLM (500M, 2.2B) - Fast, compact, 7-10s inference on flagship devices
95-
- Qwen2-VL, Qwen3-VL - Excellent multilingual vision understanding
97+
- Qwen3-VL (2B, 8B) - Vision-language with thinking mode, excellent multilingual understanding
98+
- Gemma 3n E4B - Vision + audio, built for mobile with selective activation
9699
- LLaVA - Large Language and Vision Assistant
97100
- MiniCPM-V - Efficient multimodal
98101

@@ -731,10 +734,19 @@ Subscribers are weakly held, services never leak references.
731734

732735
**Text Models:**
733736
- Hugging Face API integration (`src/services/modelManager.ts`)
734-
- Filters: LM Studio compatible, Official/Verified/Community
737+
- Curated recommended models filtered by device RAM (Qwen 3, Llama 3.2, Gemma 3, SmolLM3, Phi-4)
738+
- Advanced filtering: organization (Qwen, Meta, Google, Microsoft, Mistral, DeepSeek, HuggingFace, NVIDIA), size category (tiny/small/medium/large), quantization level, model type (text/vision/code)
739+
- Filters: LM Studio compatible, Official/Verified/Community credibility badges
735740
- Automatic GGUF quantization detection
736741
- RAM compatibility checks based on device memory
737742

743+
**Local Model Import (Bring Your Own Model):**
744+
- Import `.gguf` files directly from device storage via native file picker
745+
- Validates file format, parses model name and quantization from filename
746+
- Handles Android `content://` URIs by copying to app storage
747+
- Progress tracking during file import
748+
- Imported models appear alongside downloaded models in the model selector
749+
738750
**Image Models:**
739751
- xororz HuggingFace repos (pre-converted MNN/QNN)
740752
- Dynamic model list fetch
@@ -795,7 +807,7 @@ Prevents OOM crashes by blocking loads that would exceed safe RAM limits.
795807
**Scenario:** User travels with no internet access, needs AI assistance for writing, research, or problem-solving.
796808

797809
**Implementation:**
798-
- Download Qwen3-2B-Instruct (Q4_K_M, ~2.5GB) once
810+
- Download Qwen 3 0.6B or Llama 3.2 3B (Q4_K_M) from curated recommendations, or import your own .gguf model from device storage
799811
- Create project with custom system prompt: "You are a helpful writing assistant..."
800812
- Generate responses entirely on-device
801813
- All conversations persist locally
@@ -831,7 +843,7 @@ Prevents OOM crashes by blocking loads that would exceed safe RAM limits.
831843
**Scenario:** Developer needs code assistance without sharing proprietary code with cloud services.
832844

833845
**Implementation:**
834-
- Download Qwen3-Coder or Phi-3-Mini (Q4_K_M)
846+
- Download Qwen 3 Coder A3B or Phi-4 Mini (Q4_K_M)
835847
- Create "Code Review" project with system prompt
836848
- Paste code snippets, receive suggestions
837849
- All code stays on device
@@ -889,7 +901,7 @@ Prevents OOM crashes by blocking loads that would exceed safe RAM limits.
889901
- **React Navigation 7.x** - Native navigation
890902
- **React Native Reanimated 4.x** - Performant native-thread animations
891903
- **React Native Haptic Feedback** - Haptic responses on interactions
892-
- **@react-native-documents/picker** - Native document picker for file attachments
904+
- **@react-native-documents/picker** - Native document picker for file attachments and local model import
893905
- **@react-native-documents/viewer** - Native document viewer (QuickLook / Intent.ACTION_VIEW)
894906

895907
### Native Modules
@@ -1055,10 +1067,10 @@ OffgridMobile/
10551067
│ │ ├── AnimatedListItem.tsx # Entry animation + press feedback combo
10561068
│ │ ├── AnimatedPressable.tsx # Spring scale + haptic press wrapper
10571069
│ │ ├── AppSheet.tsx # Custom swipe-to-dismiss bottom sheet
1058-
│ │ ├── ChatInput.tsx # Message input with attachments
1070+
│ │ ├── ChatInput.tsx # Message input with attachments, vision/image-mode badges
10591071
│ │ ├── ChatMessage.tsx # Message bubbles with metadata
10601072
│ │ ├── DebugSheet.tsx # Developer debug bottom sheet
1061-
│ │ ├── ModelCard.tsx # Model display card
1073+
│ │ ├── ModelCard.tsx # Model display card (compact/full modes, icon actions)
10621074
│ │ ├── ModelSelectorModal.tsx # Quick model switcher
10631075
│ │ ├── GenerationSettingsModal.tsx # Image generation settings
10641076
│ │ ├── ProjectSelectorSheet.tsx # Project picker bottom sheet

__tests__/integration/generation/generationFlow.test.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
setupWithActiveModel,
1919
setupWithConversation,
2020
flushPromises,
21+
wait,
2122
getChatState,
2223
collectSubscriptionValues,
2324
} from '../../utils/testHelpers';
@@ -225,13 +226,13 @@ describe('Generation Flow Integration', () => {
225226

226227
await flushPromises();
227228

228-
// Stream tokens
229+
// Stream tokens (need wait(60) to allow 50ms token buffer flush)
229230
streamCallback?.('Hello');
230-
await flushPromises();
231+
await wait(60);
231232
expect(getChatState().streamingMessage).toBe('Hello');
232233

233234
streamCallback?.(' world');
234-
await flushPromises();
235+
await wait(60);
235236
expect(getChatState().streamingMessage).toBe('Hello world');
236237

237238
completeCallback?.('');
@@ -555,7 +556,7 @@ describe('Generation Flow Integration', () => {
555556

556557
await flushPromises();
557558
streamCallback?.('Token');
558-
await flushPromises();
559+
await wait(60);
559560
completeCallback?.('');
560561
await generatePromise;
561562

@@ -568,9 +569,11 @@ describe('Generation Flow Integration', () => {
568569
const generatingState = values.find((v: any) => v.isGenerating);
569570
expect(generatingState).toBeDefined();
570571

571-
// Should have a state with content
572-
const contentState = values.find((v: any) => v.streamingContent === 'Token');
573-
expect(contentState).toBeDefined();
572+
// Tokens are accumulated internally without notifying subscribers
573+
// (by design, to avoid flooding the JS thread). Verify that
574+
// the thinking→streaming transition was notified instead.
575+
const streamingState = values.find((v: any) => v.isGenerating && !v.isThinking);
576+
expect(streamingState).toBeDefined();
574577

575578
// Last state should be idle
576579
const lastState: any = values[values.length - 1];

__tests__/rntl/components/ChatInput.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,35 @@ describe('ChatInput', () => {
413413
// Toggle is hidden when no model loaded
414414
expect(queryByTestId('image-mode-toggle')).toBeNull();
415415
});
416+
417+
it('ON badge disappears when toggling back to auto', () => {
418+
const { getByTestId, queryByTestId } = render(
419+
<ChatInput {...defaultProps} imageModelLoaded={true} />
420+
);
421+
422+
const toggle = getByTestId('image-mode-toggle');
423+
// Toggle to force
424+
fireEvent.press(toggle);
425+
expect(queryByTestId('image-mode-on-badge')).toBeTruthy();
426+
427+
// Toggle back to auto
428+
fireEvent.press(toggle);
429+
expect(queryByTestId('image-mode-on-badge')).toBeNull();
430+
});
431+
432+
it('hides image mode toggle when settings.imageGenerationMode is not manual', () => {
433+
mockUseAppStore.mockReturnValue({
434+
settings: {
435+
imageGenerationMode: 'auto',
436+
},
437+
});
438+
439+
const { queryByTestId } = render(
440+
<ChatInput {...defaultProps} imageModelLoaded={true} />
441+
);
442+
443+
expect(queryByTestId('image-mode-toggle')).toBeNull();
444+
});
416445
});
417446

418447
// ============================================================================
@@ -445,6 +474,22 @@ describe('ChatInput', () => {
445474
// Should show "Vision" badge
446475
expect(getByTestId('vision-indicator')).toBeTruthy();
447476
});
477+
478+
it('hides Vision indicator when vision is not supported', () => {
479+
const { queryByTestId } = render(
480+
<ChatInput {...defaultProps} supportsVision={false} />
481+
);
482+
483+
expect(queryByTestId('vision-indicator')).toBeNull();
484+
});
485+
486+
it('Vision badge contains correct text', () => {
487+
const { getByText } = render(
488+
<ChatInput {...defaultProps} supportsVision={true} />
489+
);
490+
491+
expect(getByText('Vision')).toBeTruthy();
492+
});
448493
});
449494

450495
// ============================================================================

0 commit comments

Comments
 (0)