Skip to content

feat: Offline AI Plant/Wildlife Identification#1906

Merged
andrew-bierman merged 14 commits into
developmentfrom
copilot/offline-ai-plant-identification
Apr 11, 2026
Merged

feat: Offline AI Plant/Wildlife Identification#1906
andrew-bierman merged 14 commits into
developmentfrom
copilot/offline-ai-plant-identification

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 9, 2026

  • Fix searchSpecies to include imageDescription and habitat in search
  • Fix hardcoded "Unknown species" → t('wildlife.unknownSpecies')
  • Remove unused wildlifeHistoryCountAtom
  • Remove redundant as SelectedImage cast
  • Refactor WildlifeScreen to use useWildlifeHistory hook
  • Fix double JSON encoding in wildlifeAtoms.ts
  • Fix blank query fallback in searchSpecies
  • Fix empty-string ?? bug in useWildlifeIdentification offline path
  • Sanitize error log in useWildlifeIdentification
  • Use permanentlyPersistImageLocally in IdentificationScreen
  • Fix SpeciesDetailScreen history lookup + loading state
  • Add hasError branch in WildlifeScreen
  • Move R2 delete to finally in API route
  • Make 500 API response generic
  • Add i18n for source, dangerLevel, category in SpeciesCard and SpeciesDetailScreen
  • Add JSDoc for confidence in types.ts
  • Add translation keys to en.json
  • Set enableWildlifeIdentification: false (ship dark by default)
  • Delete persisted image files in deleteIdentification / clearHistory
  • Tighten isNetworkError (remove over-broad return true fallback)
  • Add scientificName fallback in API species.id slug
  • Wrap permanentlyPersistImageLocally in try/catch in IdentificationScreen
  • Add hasError branch in SpeciesDetailScreen (surfaces history load error instead of "not found")
  • Add { getOnInit: true } to atomWithStorage so loadable correctly enters loading state on cold start
  • Guard stale completions in IdentificationScreen onSuccess (ignore results if user changed image mid-flight)
  • Use t() for wildlife tile title and category keywords in home/index.tsx
  • Add error handling for generateObject in wildlifeIdentificationService.ts
  • Disable press/chevron on empty-result history rows in WildlifeScreen
  • Fix offlineIdentifier.ts: remove early-return, pass trimmed query to searchSpecies
  • Fix IdentificationScreen.tsx: use useRef to track current image URI for accurate stale-request guard
  • Fix API route: await R2 delete before sending response (remove fire-and-forget finally block)
  • Fix useWildlifeHistory.ts: async updaters for AsyncStorage-backed atom to handle hydration
  • Fix home/index.tsx: move wildlife tile title/keywords inside DashboardScreen with useMemo for locale reactivity
  • Fix speciesDatabase.ts: type getSpeciesByCategory parameter as SpeciesCategory
  • Fix useWildlifeIdentification.ts: improve upload error message with filename context
Original prompt

This section details on the original issue you should resolve

<issue_title>PackRat #1809: Offline AI Plant/Wildlife Identification</issue_title>
<issue_description>On-device species recognition with ExecuTorch/TFLite. Camera integration, pre-downloaded species database. Works offline.

Status: In Progress (via subagent)
SwarmBoard: https://swarmboard-api.abbierman101.workers.dev</issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Summary by CodeRabbit

  • New Features
    • Wildlife identification: capture or upload photos (with optional description) to identify species.
    • Online + offline identification with automatic fallback.
    • Species detail pages with rich info (names, danger level, habitat, conservation, facts).
    • Identification history persisted and browsable; quick access from a new Wildlife section and dashboard tile; search includes wildlife entries when enabled.
  • Documentation
    • Added localization strings for the wildlife workflows.

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@copilot

1 similar comment
@andrew-bierman
Copy link
Copy Markdown
Collaborator

@copilot

Copilot AI changed the title [WIP] Add offline AI plant and wildlife identification functionality feat: Offline AI Plant/Wildlife Identification Mar 9, 2026
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot

The Biome CI shows action_required (pending manual workflow approval), not a code failure. All new wildlife feature files pass biome check with zero errors or warnings. The 4 warnings in the CI output are pre-existing in unrelated files (GuideCard.tsx, EXAMPLES.tsx, api/test/setup.ts) and were present before this PR.

Is there something specific you'd like me to address or change?

@copilot

The wildlife identification feature is fully implemented and all new files pass lint checks. Let me know if you have any specific requests or feedback!

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@coderabbitai Please review this PR for code quality, best practices, and potential issues.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

@andrew-bierman, I'll perform a full review of this PR, examining code quality, best practices, and potential issues across all the changes — including the bundled species database, offline identifier, Jotai/AsyncStorage persistence, the wildlife screens, and the API route implementation.

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d08a3bb8-d950-407d-9804-543212cdf4d3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a wildlife identification feature: UI screens and tiles, persisted history atoms and hooks, offline species database and identifier, online identification service and API route using OpenAI, route wiring and i18n strings, plus feature flag gating.

Changes

Cohort / File(s) Summary
Dashboard & Config
apps/expo/app/(app)/(tabs)/(home)/index.tsx, apps/expo/config.ts
Adds wildlife tile to dashboard (conditional on featureFlags.enableWildlifeIdentification) and exposes new feature flag defaulting to false.
App Routes
apps/expo/app/(app)/wildlife/index.tsx, apps/expo/app/(app)/wildlife/identify.tsx, apps/expo/app/(app)/wildlife/[id].tsx
New route components that render WildlifeScreen, IdentificationScreen, and SpeciesDetailScreen respectively.
Feature Barrel & Exports
apps/expo/features/wildlife/index.ts, apps/expo/features/wildlife/components/index.ts, apps/expo/features/wildlife/hooks/index.ts, apps/expo/features/wildlife/screens/index.ts
Added index barrels re-exporting components, hooks, screens, and types for the wildlife feature.
UI Components
apps/expo/features/wildlife/components/WildlifeTile.tsx, apps/expo/features/wildlife/components/SpeciesCard.tsx
New UI: dashboard tile navigation and pressable species card with metadata, theming and optional callbacks.
Screens
apps/expo/features/wildlife/screens/WildlifeScreen.tsx, apps/expo/features/wildlife/screens/IdentificationScreen.tsx, apps/expo/features/wildlife/screens/SpeciesDetailScreen.tsx
New screens for listing history, performing identification (image pick, offline/online flow, persist), and species detail with fallback lookup.
State & Hooks
apps/expo/features/wildlife/atoms/wildlifeAtoms.ts, apps/expo/features/wildlife/hooks/useWildlifeHistory.ts, apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts
Persistent Jotai atoms for history (AsyncStorage), hooks to manage history, and a React Query mutation hook that attempts online identification with offline fallback.
Data & Offline Logic
apps/expo/features/wildlife/data/speciesDatabase.ts, apps/expo/features/wildlife/lib/offlineIdentifier.ts
Adds large static species database with search utilities and offline identification helpers (confidence scoring, category/danger queries).
Types
apps/expo/features/wildlife/types.ts
Domain types: SpeciesCategory, SpeciesEntry, IdentificationResult, WildlifeIdentification.
API: Routes & Services
packages/api/src/routes/wildlife/index.ts, packages/api/src/routes/index.ts, packages/api/src/services/wildlifeIdentificationService.ts, packages/api/src/services/index.ts
New POST /api/wildlife/identify route wired into protected routes; WildlifeIdentificationService uses OpenAI schema-based generation and is re-exported.
i18n
apps/expo/lib/i18n/locales/en.json
Adds wildlife localization strings for UI, categories, danger levels, prompts, and messages.

Sequence Diagrams

sequenceDiagram
    participant User
    participant ExpoClient as Expo Client
    participant OfflineID as Offline Identifier
    participant SpeciesDB as Species Database
    participant APIServer as API Server
    participant OpenAI as OpenAI Service

    User->>ExpoClient: Select image & optional description, request identify
    ExpoClient->>OfflineID: identifyFromDescription(description)
    OfflineID->>SpeciesDB: searchSpecies(query)
    SpeciesDB-->>OfflineID: Matching species (offline results)
    alt Network Available
        ExpoClient->>APIServer: POST /api/wildlife/identify (image key)
        APIServer->>OpenAI: generateObject(prompt + imageUrl, schema)
        OpenAI-->>APIServer: Identification results
        APIServer-->>ExpoClient: Online results
    else Network Error
        ExpoClient-->>OfflineID: Use offline results as fallback
    end
    ExpoClient->>SpeciesDB: Save persisted image + add to history (wildlifeHistoryAtom)
    ExpoClient->>User: Display results
Loading
sequenceDiagram
    participant User
    participant ExpoClient as Expo Client
    participant History as Wildlife History (Jotai)
    participant Detail as Species Detail Screen
    participant SpeciesDB as Species Database

    User->>ExpoClient: Open Wildlife screen
    ExpoClient->>History: load history (wildlifeHistoryAtom)
    History-->>ExpoClient: WildlifeIdentification[]
    ExpoClient->>User: Render history list

    User->>ExpoClient: Tap history item -> navigate /wildlife/[id]
    Detail->>SpeciesDB: getSpeciesById(id)
    alt Species found
        SpeciesDB-->>Detail: SpeciesEntry
    else Not found
        Detail->>History: load matching history entry
        History-->>Detail: result from saved history
    end
    Detail->>User: Render species details
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

🐇 I hopped through code with curious paws,
New tiles and screens and data drawers.
Offline searches, OpenAI light—
Nature named by day or night.
A rabbit cheers: wild things in sight! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Offline AI Plant/Wildlife Identification' clearly and concisely summarizes the main feature addition across all changeset files, matching the primary objective of implementing offline species identification.
Linked Issues check ✅ Passed The PR comprehensively implements all coding objectives from issue #1816: offline identification UI (screens, components), local persistence (atoms, hooks), species database, API route with error handling, and feature flag controls.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing offline AI plant/wildlife identification: new routes, screens, components, data layer, state management, API integration, internationalization support, and configuration are all within the stated PR scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch copilot/offline-ai-plant-identification

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

@andrew-bierman, I'll perform a full review of this PR, examining code quality, best practices, and potential issues across all the changes — including the bundled species database, offline identifier, Jotai/AsyncStorage persistence, the wildlife screens, and the API route implementation.

✅ Actions performed

Full review triggered.

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@copilot CodeRabbit has provided review feedback on this PR. Please address the comments and suggestions. Once you've made the necessary changes, I'll ask CodeRabbit to re-review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (1)
apps/expo/features/wildlife/types.ts (1)

29-32: Make the confidence contract explicit.

SpeciesCard multiplies this value by 100, so a plain number here leaves every producer guessing whether the API returns 0..1 or 0..100. Please document or narrow the range at the type boundary.

💡 Suggested tightening
 export interface IdentificationResult {
   species: SpeciesEntry;
+  /** Normalized confidence score in the range [0, 1]. */
   confidence: number;
   source: 'online' | 'offline';
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/expo/features/wildlife/types.ts` around lines 29 - 32, The
IdentificationResult.confidence field is ambiguous (0..1 vs 0..100); update the
contract to explicitly represent a 0..1 probability by replacing the plain
number with a constrained/Branded type (e.g., Confidence0To1 or type alias with
a descriptive JSDoc) and update usages like SpeciesCard to rely on that 0..1
contract (it currently multiplies by 100). Change the IdentificationResult
interface (symbol: IdentificationResult) to use the new Confidence type and add
a brief JSDoc comment clarifying the range so producers and consumers know the
expected 0..1 probability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/expo/features/wildlife/atoms/wildlifeAtoms.ts`:
- Around line 6-17: The current wildlifeStorage wrapper manually calls
JSON.parse/JSON.stringify around AsyncStorage causing double-encoding and
hydration failures; replace the custom adapter by passing AsyncStorage directly
into createJSONStorage (i.e., use createJSONStorage with AsyncStorage as the
storage backend) and remove the getItem/setItem/removeItem JSON serialization
logic so createJSONStorage can handle parsing/stringifying itself; update the
wildlifeStorage declaration (symbol: wildlifeStorage) to use createJSONStorage
with AsyncStorage.

In `@apps/expo/features/wildlife/components/SpeciesCard.tsx`:
- Around line 1-3: SpeciesCard is rendering enum-backed values (source,
dangerLevel, category) raw; replace those direct uses with translated labels by
mapping each enum to an i18n key or using the app's t()/translate utility inside
SpeciesCard (or a helper map) to render user-facing strings (e.g., map
IdentificationResult.source values like "offline" to translation keys such as
"species.source.offline"); update all usages of source, dangerLevel, and
category in the SpeciesCard render (lines previously flagged ~38-59) to call the
translator or lookup map so non-English locales show localized labels.

In `@apps/expo/features/wildlife/data/speciesDatabase.ts`:
- Around line 494-502: searchSpecies currently treats blank/whitespace queries
as matches because it lowercases without checking for emptiness; modify
searchSpecies to trim the incoming query and immediately return an empty array
if the trimmed string is empty, otherwise proceed with the existing lowercase
match logic against SPECIES_DATABASE (references: searchSpecies,
SPECIES_DATABASE, commonName, scientificName, description, characteristics).

In `@apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts`:
- Line 57: The offline query selection currently uses the nullish coalescing
operator so an empty description string ("" ) wins; update the logic in
useWildlifeIdentification where identifyFromDescription is called so
blank/whitespace descriptions are treated as absent—e.g., evaluate offlineQuery
with a trim check (use offlineQuery && offlineQuery.trim().length > 0) and pass
offlineQuery only when non-empty, otherwise pass selectedImage.fileName to
identifyFromDescription.
- Line 56: In useWildlifeIdentification, replace the current raw error logging
in the catch/fallback that calls console.warn('Online identification
unavailable, using offline database:', error) with a sanitized log that only
includes non-sensitive fields (e.g., error?.code and error?.message) or remove
the error payload entirely; update the console.warn invocation in that block so
it doesn't print the full Axios error object.

In `@apps/expo/features/wildlife/screens/IdentificationScreen.tsx`:
- Line 20: The selectedImage URI is being stored directly instead of a persisted
local filename; update the useImagePicker destructure to include
permanentlyPersistImageLocally, call
permanentlyPersistImageLocally(selectedImage) just before invoking
addIdentification, and pass the returned persisted filename (not
selectedImage.uri) into addIdentification; reference the useImagePicker hook,
the permanentlyPersistImageLocally function, the selectedImage object, and the
addIdentification call to locate where to change the code.

In `@apps/expo/features/wildlife/screens/SpeciesDetailScreen.tsx`:
- Around line 30-42: The fallback lookup currently finds a history entry by
history entry id (historyLoadable.data.find(h => h.id === id)) which fails when
the route id is a species id; change the fallback to search historyLoadable.data
for any entry whose results array contains a result with result.species.id ===
id (update the logic that sets historyEntry/displaySpecies accordingly), and
ensure you do not render the not-found UI until wildlife history has finished
loading (check historyLoadable.state === 'hasData' before showing the
speciesNotFound view and render a loading state while it is still pending).

In `@apps/expo/features/wildlife/screens/WildlifeScreen.tsx`:
- Line 34: The hardcoded fallback label "Unknown species" in WildlifeScreen
should be localized; replace the literal Text node with the i18n lookup used
elsewhere (call t with an appropriate key like t('wildlife.unknownSpecies')),
add that key to the translations files under the wildlife namespace in all
locales, and update the component to use t('wildlife.unknownSpecies') instead of
the hardcoded string so the fallback label is localized across languages.
- Around line 48-52: The code collapses non-'hasData' loadable states into an
empty array—update handling of wildlifeHistoryAtom/historyLoadable so you check
for historyLoadable.state === 'hasError' and surface an error UI (or message)
before falling back to empty array and ensure the same explicit error branch is
used in the rendering logic around the 84-114 conditional block; additionally
remove the hardcoded "Unknown species" string by either moving useTranslation()
into the HistoryItem component or passing the t function into HistoryItem and
replacing the literal with a translation key (e.g.,
t('wildlife.unknownSpecies')) so the fallback is localized.

In `@packages/api/src/routes/wildlife/index.ts`:
- Around line 110-114: The temp-upload deletion currently runs only after a
successful call to WildlifeIdentificationService.identifySpecies and can leak or
convert a success into a 500; refactor the handler so the model call
(service.identifySpecies(imageUrl)) runs inside a try block, move
PACKRAT_BUCKET.delete(image) into a finally block and make the delete
best-effort (catch/log any errors from PACKRAT_BUCKET.delete so they don't
change the response), ensuring the identification result (identification) is
returned or the model error is handled independently of the upload cleanup.
- Around line 142-149: The 500-response currently echoes internal error details
via c.json({ error: `Failed to identify species: ${error.message}` }, 500);
update the 500 branch in the error handler (the conditional that checks error
instanceof Error and the current return c.json(...) for 500) to return a generic
message (e.g., { error: 'Internal server error' } or { error: 'Failed to
identify species' }) without including error.message, while leaving the 400
branch (Invalid image / Unsupported image format) and existing logging/Sentry
reporting unchanged.

---

Nitpick comments:
In `@apps/expo/features/wildlife/types.ts`:
- Around line 29-32: The IdentificationResult.confidence field is ambiguous
(0..1 vs 0..100); update the contract to explicitly represent a 0..1 probability
by replacing the plain number with a constrained/Branded type (e.g.,
Confidence0To1 or type alias with a descriptive JSDoc) and update usages like
SpeciesCard to rely on that 0..1 contract (it currently multiplies by 100).
Change the IdentificationResult interface (symbol: IdentificationResult) to use
the new Confidence type and add a brief JSDoc comment clarifying the range so
producers and consumers know the expected 0..1 probability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4590d1c7-0d37-45bf-9591-7478eafc8075

📥 Commits

Reviewing files that changed from the base of the PR and between 3f94763 and f7ba379.

📒 Files selected for processing (25)
  • apps/expo/app/(app)/(tabs)/(home)/index.tsx
  • apps/expo/app/(app)/wildlife/[id].tsx
  • apps/expo/app/(app)/wildlife/identify.tsx
  • apps/expo/app/(app)/wildlife/index.tsx
  • apps/expo/config.ts
  • apps/expo/features/wildlife/atoms/wildlifeAtoms.ts
  • apps/expo/features/wildlife/components/SpeciesCard.tsx
  • apps/expo/features/wildlife/components/WildlifeTile.tsx
  • apps/expo/features/wildlife/components/index.ts
  • apps/expo/features/wildlife/data/speciesDatabase.ts
  • apps/expo/features/wildlife/hooks/index.ts
  • apps/expo/features/wildlife/hooks/useWildlifeHistory.ts
  • apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts
  • apps/expo/features/wildlife/index.ts
  • apps/expo/features/wildlife/lib/offlineIdentifier.ts
  • apps/expo/features/wildlife/screens/IdentificationScreen.tsx
  • apps/expo/features/wildlife/screens/SpeciesDetailScreen.tsx
  • apps/expo/features/wildlife/screens/WildlifeScreen.tsx
  • apps/expo/features/wildlife/screens/index.ts
  • apps/expo/features/wildlife/types.ts
  • apps/expo/lib/i18n/locales/en.json
  • packages/api/src/routes/index.ts
  • packages/api/src/routes/wildlife/index.ts
  • packages/api/src/services/index.ts
  • packages/api/src/services/wildlifeIdentificationService.ts

Comment thread apps/expo/features/wildlife/atoms/wildlifeAtoms.ts Outdated
Comment thread apps/expo/features/wildlife/components/SpeciesCard.tsx
Comment thread apps/expo/features/wildlife/data/speciesDatabase.ts
Comment thread apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts Outdated
Comment thread apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts Outdated
Comment thread apps/expo/features/wildlife/screens/SpeciesDetailScreen.tsx
Comment thread apps/expo/features/wildlife/screens/WildlifeScreen.tsx Outdated
Comment thread apps/expo/features/wildlife/screens/WildlifeScreen.tsx Outdated
Comment thread packages/api/src/routes/wildlife/index.ts Outdated
Comment thread packages/api/src/routes/wildlife/index.ts Outdated
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot CodeRabbit has provided review feedback on this PR. Please address the comments and suggestions. Once you've made the necessary changes, I'll ask CodeRabbit to re-review.

Addressed the issues in commit 057905d:

  • Offline search quality fixsearchSpecies() now also searches imageDescription and habitat fields, which were populated for every species entry but never included in the keyword search, significantly improving offline identification accuracy
  • i18n — hardcoded "Unknown species" string replaced with t('wildlife.unknownSpecies') (key added to en.json)
  • Dead code removal — removed unused wildlifeHistoryCountAtom export
  • Type cast cleanup — removed redundant as SelectedImage cast and the now-unused SelectedImage import in IdentificationScreen.tsx
  • ConsistencyWildlifeScreen now uses useWildlifeHistory hook instead of reading the atom directly

All files pass biome check with zero errors/warnings.

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@copilot CodeRabbit has reviewed this PR and provided feedback. Please address the review comments and push updates. Once you've made the changes, the monitor will ask CodeRabbit to re-review.

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@copilot CodeRabbit has provided review feedback on this PR. Please review and address the comments.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot CodeRabbit has provided review feedback on this PR. Please review and address the comments.

All CodeRabbit comments addressed in commit f7a43bc:

Critical:

  • wildlifeAtoms.ts — fixed double JSON encoding (was manually JSON.parse/stringify inside createJSONStorage, which already handles that)
  • useWildlifeIdentification.ts — fixed empty-string ?? bug (blank description now falls back to filename); sanitized error log (no more raw Axios object)
  • SpeciesDetailScreen.tsx — fixed history lookup to also search by result.species.id (was only matching history-entry id, so tapping online results landed on "not found"); added loading spinner before showing the not-found state
  • packages/api/src/routes/wildlife/index.ts — moved R2 delete to finally with best-effort .catch() so temp uploads are always cleaned up; removed error.message from 500 responses

Major:

  • speciesDatabase.tssearchSpecies now trims and returns [] for blank queries
  • IdentificationScreen.tsx — calls permanentlyPersistImageLocally() before addIdentification (matches pattern in CreatePackItemForm)
  • WildlifeScreen.tsx — added explicit hasError branch instead of silently collapsing to empty state

Minor:

  • SpeciesCard.tsxsource, dangerLevel, category now use t() for i18n
  • types.ts — added JSDoc documenting confidence is [0, 1]
  • en.json — added historyLoadError, source.*, dangerLevel.*, category.* keys

@copilot CodeRabbit has reviewed this PR and provided feedback. Please address the review comments and push updates. Once you've made the changes, the monitor will ask CodeRabbit to re-review.

All changes pushed in commit f7a43bc. See the reply to the latest comment for the full breakdown.

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@copilot CodeRabbit has reviewed this PR. Please address the feedback.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot CodeRabbit has reviewed this PR. Please address the feedback.

All CodeRabbit feedback was already addressed in commit f7a43bc. The 3 threads still showing as "open" in the UI are from the original review on commit f7ba3795 and the fixes are present in the current code:

  • SpeciesCard.tsxsource, dangerLevel, category now rendered via t() (lines 43, 57, 62)
  • speciesDatabase.ts — blank query returns [] with trim() early exit (lines 495-496)
  • SpeciesDetailScreen.tsx — fallback searches h.results.some(r => r.species.id === id) and shows a spinner while history loads (lines 34, 43-48)

The CI action_required status is a workflow approval gate (not a code failure) — the run has 0 failed jobs.

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@copilot CodeRabbit has reviewed this PR. Please address the feedback.

@andrew-bierman andrew-bierman changed the base branch from main to development March 12, 2026 05:19
@andrew-bierman andrew-bierman force-pushed the copilot/offline-ai-plant-identification branch from cdb9951 to 419153d Compare April 11, 2026 05:36
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 11, 2026

Coverage Report for Expo Unit Tests Coverage (./apps/expo)

Status Category Percentage Covered / Total
🔵 Lines 74.32% 495 / 666
🔵 Statements 74.32% (🎯 75%) 495 / 666
🔵 Functions 91.07% 51 / 56
🔵 Branches 91.38% 191 / 209
File CoverageNo changed files found.
Generated in workflow #71 for commit 1f71f0b by the Vitest Coverage Report Action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 11, 2026

Coverage Report for API Unit Tests Coverage (./packages/api)

Status Category Percentage Covered / Total
🔵 Lines 89.66% 902 / 1006
🔵 Statements 89.66% (🎯 80%) 902 / 1006
🔵 Functions 97.95% 48 / 49
🔵 Branches 89.74% 280 / 312
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/api/src/services/wildlifeIdentificationService.ts 0% 0% 0% 0% 1-100
Generated in workflow #71 for commit 1f71f0b by the Vitest Coverage Report Action

Copilot AI and others added 14 commits April 11, 2026 00:06
Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
- Fix searchSpecies() to also search imageDescription and habitat fields
  (offline identification was skipping these purpose-built matching fields)
- Replace hardcoded 'Unknown species' with t('wildlife.unknownSpecies')
  and add the key to en.json
- Remove unused wildlifeHistoryCountAtom (dead code)
- Remove redundant `as SelectedImage` cast and unused SelectedImage import
  in IdentificationScreen (TypeScript narrows type in the conditional branch)
- Refactor WildlifeScreen to use useWildlifeHistory hook instead of
  reading wildlifeHistoryAtom directly for consistent abstraction

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Critical:
- wildlifeAtoms: fix double JSON encoding - pass AsyncStorage directly to
  createJSONStorage instead of manually calling JSON.parse/stringify
- useWildlifeIdentification: fix empty-string ?? bug (offlineQuery='' won
  over filename); sanitize error log (no more raw Axios object in console)
- SpeciesDetailScreen: fix history lookup to also search by species.id in
  h.results (was only checking history entry id, broke navigation from
  online identification results); add loading spinner before 'not found'
- API route: move R2 delete to finally with best-effort catch so temp
  uploads are always cleaned up; make 500 response generic (no error.message)

Major:
- speciesDatabase: trim + early return in searchSpecies for blank queries
- IdentificationScreen: call permanentlyPersistImageLocally() before
  addIdentification to persist image across cache evictions
- WildlifeScreen: add explicit hasError branch (was silently showing
  empty state when AsyncStorage fails)
- SpeciesCard: translate source/dangerLevel/category via t()

Minor:
- types.ts: add JSDoc documenting confidence is [0,1]
- en.json: add historyLoadError, source.*, dangerLevel.*, category.* keys

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
…i18n, error handling

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Critical:
- Fix AI SDK generateObject call: use `messages` with ImagePart instead of
  `prompt` (which is text-only) for image-based species identification
- Wrap imageUrl in `new URL()` for proper ImagePart type

Major:
- Add feature flag gate to all wildlife route pages (index, identify, [id])
  redirecting to home when enableWildlifeIdentification is disabled
- Pass undefined instead of empty string for offlineQuery so blank
  descriptions don't bypass the fileName fallback
- Don't wipe identification results when user cancels image picker;
  only clear on successful new image selection
- Improve isNetworkError: check Axios error codes (ERR_NETWORK, ECONNABORTED,
  etc.) and tighten message matching to avoid misclassifying server-side
  timeouts as network errors
…, R2 cleanup, i18n, types

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
… lookup

Add missing Stack layout for the wildlife/ route group which would crash
iOS navigation without it. SpeciesDetailScreen already handles deep-link
lookups via getSpeciesById() so no changes needed there.
@andrew-bierman andrew-bierman force-pushed the copilot/offline-ai-plant-identification branch from 1c64b71 to 1f71f0b Compare April 11, 2026 06:06
@andrew-bierman andrew-bierman merged commit abfcbe3 into development Apr 11, 2026
5 of 10 checks passed
@andrew-bierman andrew-bierman deleted the copilot/offline-ai-plant-identification branch April 11, 2026 06:11
andrew-bierman added a commit that referenced this pull request Apr 11, 2026
…nents

Replace invalid icon names (like 'delete') that weren't in the nwui Icon
type allowlist. These were causing check-types failures on development
after #1882, #1885, #1906 merged.
andrew-bierman added a commit that referenced this pull request May 14, 2026
andrew-bierman added a commit that referenced this pull request May 14, 2026
…nents

Replace invalid icon names (like 'delete') that weren't in the nwui Icon
type allowlist. These were causing check-types failures on development
after #1882, #1885, #1906 merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PackRat #1809: Offline AI Plant/Wildlife Identification

3 participants