Skip to content

Latest commit

 

History

History
273 lines (209 loc) · 11 KB

File metadata and controls

273 lines (209 loc) · 11 KB

Native project setup

This repository contains the JavaScript/TypeScript app plus the JS-side native configuration (metro.config.js, react-native.config.js, Gemfile, babel.config.js, app.json, index.js). The platform projects (ios/, android/) are generated by the React Native CLI (RN 0.76.5 template), then patched with the app-specific changes below.

Status: the android/ project has been generated, patched, built and verified running on an Android emulator (API 35 arm64) and a physical Pixel device, including model download → llama.rn load → on-device generation. The dependency pins in "Version compatibility" below are required for the build to succeed against RN 0.76.5's toolchain. The ios/ project is generated, patched (incl. the Share Extension), and builds and runs on the iOS simulator; on-device iOS QA (large-model memory entitlement) is the remaining item.

The app loads a ~1–2 GB GGUF model into memory via llama.rn, so the patches below mostly exist to raise memory limits and allow model downloads.

Prerequisites

  • Node.js ≥ 18, Watchman
  • Ruby + Bundler (gem install bundler), then bundle install (uses Gemfile)
  • iOS: Xcode 15+, CocoaPods (via Bundler), a real device recommended (the simulator cannot use the Metal GPU backend and large models are slow)
  • Android: JDK 17, Android Studio, NDK, an arm64 device (llama.cpp needs 64-bit; many emulators are slow for inference)

1. Generate the native projects

The project name is AiNoteOfflineAiMemo (see app.json). Generate a throwaway template with the same name and copy its platform folders in:

# From a scratch directory, NOT this repo:
npx @react-native-community/cli@0.76.5 init AiNoteOfflineAiMemo --version 0.76.5 --skip-install

# Then copy the platform folders into this repo:
cp -R AiNoteOfflineAiMemo/ios   <this-repo>/ios
cp -R AiNoteOfflineAiMemo/android <this-repo>/android

Keep this repo's existing index.js, app.json, babel.config.js, metro.config.js and package.json — do not overwrite them with the template's copies.

Then install pods:

bundle install
cd ios && bundle exec pod install && cd ..

2. iOS patches

Entitlements (large-model memory)

A 1–2 GB model exceeds the default per-app memory limit on many devices. Add an entitlements file (e.g. ios/AiNoteOfflineAiMemo/AiNoteOfflineAiMemo.entitlements) and reference it in the target's Signing & Capabilities:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.developer.kernel.increased-memory-limit</key>
  <true/>
  <key>com.apple.developer.kernel.extended-virtual-addressing</key>
  <true/>
</dict>
</plist>

Status: the shipped AiNoteOfflineAiMemo.entitlements currently contains only the App Group (used by the Share Extension). The memory entitlement above is not yet applied — add it when targeting real devices with large models (the simulator uses host RAM and does not need it). See "Remaining iOS work" at the end.

Info.plist

Model downloads use HTTPS (see src/core/models/catalog.ts), so App Transport Security needs no exceptions. If you later add an HTTP mirror, add a scoped ATS exception rather than disabling ATS globally.

3. Android patches

android/app/src/main/AndroidManifest.xml

Add the INTERNET permission (model download) and largeHeap to the application tag:

<uses-permission android:name="android.permission.INTERNET" />

<application
    android:largeHeap="true"
    ... >

android/app/build.gradle

Restrict to 64-bit ABIs (llama.cpp is arm64/x86_64 only) to keep the APK small:

android {
    defaultConfig {
        ndk {
            abiFilters "arm64-v8a", "x86_64"
        }
    }
}

4. Secrets / configuration

The app has no build-time secrets. .env / .env.example are placeholders for any future configuration; nothing in src/ reads them today. Never commit a populated .env (it is git-ignored).

5. Run

npm start                 # Metro bundler
npm run ios               # or: npx react-native run-ios --device
npm run android

On first launch no model is downloaded yet — open Settings → AI model and download one (Wi-Fi recommended; ~1.1 GB for the default Qwen2.5 1.5B). Inference is unavailable until a model finishes downloading and loading.

Note: npm run ios / npm run android scripts are added by the CLI template in step 1. This repo's package.json already defines start / ios / android.

Running without Metro (standalone QA)

To verify the UI on an emulator without a Metro dev server (e.g. when port 8081 is taken, or for a quick smoke test), build the release variant — it embeds the JS bundle into the APK:

cd android && ./gradlew :app:installRelease
adb shell am start -n com.ainoteofflineaimemo/.MainActivity

The release buildType is signed with the debug keystore (template default), so it installs without extra signing setup. AI features still require a downloaded model; notes, navigation and settings work fully offline.

6. Version compatibility (required pins)

RN 0.76.5 ships Kotlin 1.9.24 and a codegen that several newer community packages have outgrown. These pins are required for the Android build to succeed (verified on the emulator):

Package Pin Why
react-native-screens ~4.4.0 4.25+ declares a prop type RN 0.76 codegen can't parse (generateCodegenSchemaFromJavaScript fails on accessibilityContainerViewIsModal).
@react-native-async-storage/async-storage ^2.x 3.x requires Kotlin ≥ 2.1.0; the template uses 1.9.24, so KSP fails.

Architecture & model download

  • newArchEnabled=true is required. llama.rn@0.12.4's Android module only compiles against the new architecture (Fabric/TurboModules). With newArchEnabled=false it fails (RNLlamaModule cannot be converted to NativeModule). The app therefore runs in bridgeless/new-arch mode.
  • Model download uses react-native-fs (RNFS.downloadFile), not a background-download library. react-native-background-downloader@2.3.4 doesn't compile against RN 0.76, and the maintained @kesha-antonov/... fork's TurboModule does not register under bridgeless (its JS throws "doesn't seem to be linked"). RNFS works under the new architecture via the legacy-module interop layer and follows the Hugging Face redirect, so the download path lives in src/app/services/modelFiles.ts (downloadModel).
  • reactNativeArchitectures=arm64-v8a in android/gradle.properties keeps build times down (one ABI). Add x86_64 if you need Intel emulators; arm64 covers all modern phones and Apple-silicon emulators.

Android SDK components used: NDK 26.1.10909125, CMake 3.22.1, platform android-35, build-tools 35.0.0 (match android/build.gradle ext). react-native-vector-icons is a real dependency (Paper's icons); its fonts are copied into the APK via apply from: ".../react-native-vector-icons/fonts.gradle" in android/app/build.gradle.

7. iOS (verified on simulator)

cd ios && RCT_NEW_ARCH_ENABLED=1 pod install && cd ..   # new arch, matches llama.rn
# Build + run a release build on the simulator (embeds the JS bundle, no Metro):
xcodebuild -workspace ios/AiNoteOfflineAiMemo.xcworkspace -scheme AiNoteOfflineAiMemo \
  -configuration Release -sdk iphonesimulator -derivedDataPath ios/build \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' CODE_SIGNING_ALLOWED=NO build
xcrun simctl install booted ios/build/Build/Products/Release-iphonesimulator/AiNoteOfflineAiMemo.app
xcrun simctl launch booted org.reactjs.native.example.AiNoteOfflineAiMemo

iOS-specific wiring already applied to ios/:

  • UIAppFonts (Info.plist) lists the vector-icons .ttfs. The RNVectorIcons pod copies the fonts into the bundle, but without UIAppFonts iOS won't register them and every icon renders as a "?" box.

  • ainote:// URL scheme (Info.plist CFBundleURLTypes) + RCTLinkingManager in AppDelegate.mm openURL — the deep-link half of the share flow.

  • Share Extension (implemented) — receiving text from the system share sheet on iOS uses a separate Share Extension target (the Android ACTION_SEND intent-filter has no direct iOS equivalent). It writes the shared text to a shared App Group container and dispatches into the app via the ainote:// scheme + Linking handler.

Remaining iOS work:

  • Large-model memory entitlement — add com.apple.developer.kernel.increased-memory-limit (+ extended VA) in an .entitlements file for real devices loading 1–2 GB models. Not needed for the simulator (it uses host RAM).

Local development: faster model downloads

The default model is ~1.1 GB, so re-downloading it from Hugging Face on every fresh install is slow. Two ways to avoid that:

1. Serve the model from a LAN mirror

Cache the GGUF on your machine once and serve it over the LAN, then point the app at it (dev builds only — release always uses the real catalog):

make fetch-model     # caches the default GGUF into .model-cache/ (once)
make serve-model     # serves .model-cache/ over HTTP (set MODEL_PORT to taste)

fetch-model uses the Hugging Face client (hf) because HF serves large files via Xet (chunked) — plain curl/wget cannot reconstruct them (a full GET truncates). Install it once and, for un-throttled speed, authenticate:

pipx install 'huggingface_hub[hf_xet]'   # provides the `hf` command
hf auth login                            # optional; anonymous DLs are rate-limited

Then set DEV_MODEL_BASE_URL in src/app/devConfig.ts (pick a free port — the default 8000 is commonly taken; make serve-model MODEL_PORT=8123 etc.):

  • Android emulator: http://10.0.2.2:<port>
  • iOS simulator: http://localhost:<port>
  • physical device: http://<your-mac-LAN-ip>:<port> (same Wi-Fi)

The app downloads ${DEV_MODEL_BASE_URL}/<original-gguf-filename> — a LAN transfer instead of the public CDN. Keep your local URL out of git with git update-index --skip-worktree src/app/devConfig.ts. This also makes an interrupted download cheap to retry, so byte-range resume isn't needed in dev.

2. Reinstall (don't uninstall) — the model persists

The downloaded model lives in the app's data container, and a reinstall keeps that container, so you rarely need to download twice:

  • Android: adb install -r app-release.apk (or npx react-native run-android) preserves /data/data/<pkg>/files/models/. A full uninstall wipes it.
  • iOS simulator: xcrun simctl install / npx react-native run-ios over the same bundle id preserves the container. Erasing the simulator or deleting the app wipes it.

So during iteration, prefer reinstalling over uninstalling, and the ~1.1 GB download (plus the persisted ModelManager "downloaded" state) carries over.