# Project export: GrocerView

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: Think before you bite
- Devpost: https://devpost.com/software/nutriview-tw1uvi
- GitHub: https://github.com/NguyenEvan/GrocerView
- Video: https://www.youtube.com/embed/wJah_uS45uY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Evan Bao Nguyen (15 commits), wizwilzo (1 commits), austinlien (1 commits)

## Devpost submission (written by the team)

### Inspiration

As consumers in a market flooded with competing products and unfamiliar ingredient lists, we often find ourselves staring at labels full of names we've never seen before. Who is this really for? Our inspiration behind GrocerView was to cut through the noise and give people access to real, evidence-based information about what they're putting in their bodies, not influencer opinions, but actual scientific research.

### What it does

GrocerView is a mobile app that lets users scan the barcode of any grocery item and instantly receive a breakdown of its ingredients backed by scientific literature. The app queries research databases like PubMed and OpenAlex, retrieves relevant papers on each ingredient, and presents AI-generated summaries along with direct quotes from peer-reviewed studies. Users can also flag specific ingredients they want to avoid and see parent company ownership information for products.

### How we built it

Frontend: React Native with Expo for cross-platform iOS and Android support. We used Zustand for state management and MMKV for fast local caching. Backend: FastAPI (Python) serving REST endpoints for ingredient resolution and research retrieval. RAG Pipeline: We built a Retrieval-Augmented Generation system with multiple stages: Paper Discovery: Search PubMed and OpenAlex in parallel using resolved ingredient synonyms Full-Text Retrieval: Fetch open-access papers from PubMed Central (with license verification to ensure we can legally quote) Chunking & Embedding: Split papers into passages and store vectors in ChromaDB for semantic search Regulatory Data: Query authoritative sources (FDA GRAS status, WHO JECFA evaluations, NIH Office of Dietary Supplements) for official safety assessments Comprehensive Retrieval: Query three focus areas in parallel (safety, health, general) and deduplicate results Quote Verification: Ensure every quote in the LLM output actually exists in the retrieved evidence, ensuring no hallucinated citations Summary Generation: Claude (via OpenRouter) generates summaries with inline citations linking back to source papers Paper Discovery: Search PubMed and OpenAlex in parallel using resolved ingredient synonyms Full-Text Retrieval: Fetch open-access papers from PubMed Central (with license verification to ensure we can legally quote) Chunking & Embedding: Split papers into passages and store vectors in ChromaDB for semantic search Regulatory Data: Query authoritative sources (FDA GRAS status, WHO JECFA evaluations, NIH Office of Dietary Supplements) for official safety assessments Comprehensive Retrieval: Query three focus areas in parallel (safety, health, general) and deduplicate results Quote Verification: Ensure every quote in the LLM output actually exists in the retrieved evidence, ensuring no hallucinated citations Summary Generation: Claude (via OpenRouter) generates summaries with inline citations linking back to source papers Ingredient Resolution: Multi-source resolver querying PubChem, FoodOn, and Open Food Facts taxonomy in parallel to normalize ingredient names and find synonyms for better paper discovery. LLM Integration: OpenRouter API for generating summaries with citations, using Claude as the underlying model.

### Challenges we ran into

Our biggest challenge was that the RAG system was technically demanding on its own: fetching full-text papers, parsing them into clean text, chunking appropriately, and ensuring the LLM citations actually matched source material required significant iteration. This complexity made it difficult to test and integrate with the rest of the system. We couldn't easily verify if the frontend was displaying data correctly when the backend pipeline itself was still being debugged. A user scanning a barcode triggers a chain reaction through five different systems, and if any link breaks, the whole experience falls apart. We also had to handle the reality that not every ingredient has extensive research, so we built fallback paths and caching to provide useful information even when papers are sparse.

### Accomplishments we're proud of

We're most proud of building a working RAG system that cites real research papers. In an era where health information on social media often comes from unqualified influencers, GrocerView provides users with information they can actually verify. Every quote links back to its source paper on PubMed. We also built a robust multi-source ingredient resolver that can handle everything from "Red 40" to "E300" to "ascorbic acid" and understand they're related.

### What we learned

We learned how to integrate AI as a core part of application functionality rather than just a feature. Building the RAG pipeline taught us about embeddings, vector databases, chunking strategies, and prompt engineering for accurate citations. Beyond the technical skills, we learned to work together under pressure, navigating the stress of a hackathon while keeping momentum going through sleepless nights.

### What's next

Planned Features: Environmental metrics (plastic pollution, business practices, pesticides) Country of origin tracking (e.g., olive oil from multiple countries, products processed/packaged in different locations) Nutritional goal tracking (calorie/protein ratios, macro targets) Health agency guidance integration (FDA, HHS recommendations) Alternative product suggestions (e.g., glass vs plastic packaging) Scan history and favorites Social sentiment analysis from Reddit discussions about products Future Possibilities: Location-based grocery store finder with sale alerts Recipe recommendations based on scanned products Crowdsourced product data for items not in existing databases Smart grocery lists organized by store

## README (from the GitHub repository)

# GrocerView

A mobile app that scans food barcodes to analyze ingredients and provide evidence-based safety information using a RAG (Retrieval-Augmented Generation) pipeline powered by scientific literature.

## Features

- **Barcode Scanning**: Scan product barcodes using your phone's camera
- **Ingredient Analysis**: Automatically parse and identify ingredients from product labels
- **Safety Research**: Query PubMed and OpenAlex for scientific papers on ingredient safety
- **AI Summaries**: Get LLM-generated summaries of research findings
- **Evidence Citations**: View direct quotes from scientific papers with source links
- **Ingredient Flagging**: Set custom unwanted ingredients to flag in scanned products
- **Company Ownership**: See parent company information for products

## Tech Stack

### Frontend (my-app/)
- React Native with Expo
- TypeScript
- Zustand for state management
- MMKV for local caching
- expo-camera for barcode scanning

### Backend (backend/)
- FastAPI (Python)
- ChromaDB for vector storage
- PubMed & OpenAlex APIs for paper discovery
- OpenRouter for LLM summarization
- Multi-source ingredient resolution (PubChem, FoodOn, OFF Taxonomy)

## Getting Started

### Prerequisites
- Node.js 18+
- Python 3.10+
- Expo Go app on your mobile device

### Frontend Setup
```bash
cd my-app
npm install
npx expo start
```

### Backend Setup
```bash
cd backend
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload --host 0.0.0.0
```

### Environment Variables
Copy the example env file and fill in your keys:
```bash
cd backend
cp .env.example .env
```

Required variables:
| Variable | Description |
|----------|-------------|
| `OPENROUTER_API_KEY` | API key from [OpenRouter](https://openrouter.ai/) |
| `OPENROUTER_DEFAULT_MODEL` | LLM model to use (default: `anthropic/claude-sonnet-4`) |
| `OPEN_FOOD_FACTS_USER_AGENT` | User agent for OFF API (default: `GrocerView/1.0`) |
| `NCBI_API_KEY` | API key from [NCBI](https://www.ncbi.nlm.nih.gov/account/settings/) for higher PubMed rate limits |
| `NCBI_CONTACT_EMAIL` | Your email for NCBI API requests |

## Project Structure

```
GrocerView/
├── my-app/                 # React Native frontend
│   ├── app/                # Expo Router pages
│   ├── components/         # Reusable UI components
│   ├── store/              # Zustand state stores
│   └── data/               # Static data files
├── backend/                # FastAPI backend
│   ├── app/
│   │   ├── api/            # API routes
│   │   ├── services/       # External API integrations
│   │   └── rag/            # RAG pipeline components
│   ├── data/               # ChromaDB and cached data
│   └── cache/              # API response cache
└── README.md
```

## API Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/ingredient/{name}/summary` | GET | Get AI summary for an ingredient |
| `/api/v1/ingredient/{name}/evidence` | GET | Get research quotes with citations |
| `/api/v1/ingredient/resolve` | GET | Resolve ingredient to canonical name |
| `/api/v1/ingredient/resolve/batch` | POST | Batch resolve multiple ingredients |

## Data Sources

- **Open Food Facts**: Product and ingredient data
- **USDA FoodData Central**: Nutritional information
- **PubMed/PMC**: Scientific literature
- **OpenAlex**: Academic paper metadata
- **PubChem**: Chemical compound data
- **FoodOn**: Food ontology

## License

MIT License - see [LICENSE](LICENSE) for details.


## Detected evidence (automated analysis)

Indexed codebase: 66 recognized source files, 656 KB.
- FastAPI (technology) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (120 of 283)

```
.gitignore
backend/.env.example
backend/.gitignore
backend/app/__init__.py
backend/app/api/__init__.py
backend/app/api/router.py
backend/app/api/routes/__init__.py
backend/app/api/routes/analysis.py
backend/app/api/routes/companies.py
backend/app/api/routes/ingredients.py
backend/app/api/routes/products.py
backend/app/cache/__init__.py
backend/app/cache/summary_cache.py
backend/app/core/__init__.py
backend/app/core/config.py
backend/app/main.py
backend/app/models/__init__.py
backend/app/models/company.py
backend/app/models/product.py
backend/app/rag/__init__.py
backend/app/rag/analyzer.py
backend/app/rag/chunker.py
backend/app/rag/document_processor.py
backend/app/rag/evidence_framing.py
backend/app/rag/license_checker.py
backend/app/rag/llm_integration.py
backend/app/rag/openrouter.py
backend/app/rag/prompts.py
backend/app/rag/quote_verification.py
backend/app/rag/vector_store.py
backend/app/services/__init__.py
backend/app/services/codex_gsfa.py
backend/app/services/crossref.py
backend/app/services/europe_pmc.py
backend/app/services/fda_gras.py
backend/app/services/foodon.py
backend/app/services/ingredient_resolver.py
backend/app/services/jecfa.py
backend/app/services/nih_ods.py
backend/app/services/off_taxonomy.py
backend/app/services/open_food_facts.py
backend/app/services/openalex.py
backend/app/services/paper_discovery.py
backend/app/services/pmc_id_converter.py
backend/app/services/pmc.py
backend/app/services/pubchem.py
backend/app/services/pubmed.py
backend/app/services/semantic_scholar.py
backend/app/services/unpaywall.py
backend/app/services/usda.py
backend/app/services/wikidata.py
backend/build_corpus.py
backend/cache/fda_gras/094815785c0934fc.json
backend/cache/fda_gras/25c8a14a6688afcc.json
backend/cache/fda_gras/2df133fbd0536805.json
backend/cache/fda_gras/358c2f145599fec7.json
backend/cache/fda_gras/486287217b3e2830.json
backend/cache/fda_gras/6d8b748145c6f1dd.json
backend/cache/fda_gras/71b6854bf7e77172.json
backend/cache/fda_gras/953ddc1d0ccf2b22.json
backend/cache/fda_gras/96d2b87e67d3840d.json
backend/cache/fda_gras/9ca6753e0104f4cc.json
backend/cache/fda_gras/b2184916649cff45.json
backend/cache/fda_gras/b340afbe543fd13f.json
backend/cache/fda_gras/c015dc9b9b20b660.json
backend/cache/fda_gras/d1693fa6d7dc59eb.json
backend/cache/fda_gras/d210ffeb3c4f7669.json
backend/cache/fda_gras/e051892c8d2d88fd.json
backend/cache/fda_gras/e5cd8a162d5b826e.json
backend/cache/fda_gras/f2771d3f9fab74e6.json
backend/cache/fda_gras/fcc658f406c92406.json
backend/cache/gsfa/1853e5f420bcbd30.json
backend/cache/gsfa/9988d762f2f14e00.json
backend/cache/gsfa/b314d949d8f47167.json
backend/cache/jecfa/094815785c0934fc.json
backend/cache/jecfa/0fa0d8342e32647b.json
backend/cache/jecfa/101cc385f8e7564c.json
backend/cache/jecfa/193a1d439f358f20.json
backend/cache/jecfa/1ae5d1a63cf3b1eb.json
backend/cache/jecfa/2014626e5baa6756.json
backend/cache/jecfa/25c8a14a6688afcc.json
backend/cache/jecfa/2bc71def2c925403.json
backend/cache/jecfa/2c8bf4890d7783f7.json
backend/cache/jecfa/375aa57c95367ae7.json
backend/cache/jecfa/3aee5d72344809e7.json
backend/cache/jecfa/3bdd3eb67e781445.json
backend/cache/jecfa/41eac0945030a011.json
backend/cache/jecfa/431fc30fdbf0638b.json
backend/cache/jecfa/44ba73a49297c4f4.json
backend/cache/jecfa/44f1c65f53da9e5d.json
backend/cache/jecfa/45007fa35804fcf9.json
backend/cache/jecfa/470d1e9c9877e6ae.json
backend/cache/jecfa/486287217b3e2830.json
backend/cache/jecfa/4eb5c3baea4e6ccd.json
backend/cache/jecfa/5768478610e79f33.json
backend/cache/jecfa/59e8f367f68ba80c.json
backend/cache/jecfa/5a405d2eed09d797.json
backend/cache/jecfa/60d2ed1481ef9e91.json
backend/cache/jecfa/683ed14d1621f242.json
backend/cache/jecfa/71b6854bf7e77172.json
backend/cache/jecfa/72f02dd9c71e0599.json
backend/cache/jecfa/80c2412bb78f21f4.json
backend/cache/jecfa/81c9bcc541716a3f.json
backend/cache/jecfa/845607fabc7e6f2c.json
backend/cache/jecfa/88cc1e1f305b5c57.json
backend/cache/jecfa/89820898bcb635db.json
backend/cache/jecfa/922bee589b576c25.json
backend/cache/jecfa/954aef542ea72936.json
backend/cache/jecfa/9690f3a195b939b9.json
backend/cache/jecfa/96d2b87e67d3840d.json
backend/cache/jecfa/99bf7c8272c59565.json
backend/cache/jecfa/9adbfc2811750a16.json
backend/cache/jecfa/9ca6753e0104f4cc.json
backend/cache/jecfa/9cd3222360ce5332.json
backend/cache/jecfa/9d0b12c70c5f394b.json
backend/cache/jecfa/a79ce44d2fa2628f.json
backend/cache/jecfa/a8f9e8cb25c403f2.json
backend/cache/jecfa/aed3e7beb2940ff9.json
backend/cache/jecfa/bba790b4d0e44317.json
backend/cache/jecfa/bdaf472fff5f96e4.json
[163 more files omitted for size]
```

### Dependencies

- backend/requirements.txt: fastapi@==0.109.0, httpx@==0.26.0, pydantic@==2.5.0, pydantic-settings@==2.1.0, python-dotenv@==1.0.0, uvicorn[standard]@==0.27.0
- my-app/package.json: @expo-google-fonts/space-grotesk@^0.4.1, @expo/vector-icons@^15.0.3, @react-native-async-storage/async-storage@^2.2.0, @react-navigation/bottom-tabs@^7.10.0, @react-navigation/elements@^2.6.3, @react-navigation/native@^7.1.28, @react-navigation/native-stack@^7.10.0, @types/react@~19.1.0, eslint@^9.25.0, eslint-config-expo@~10.0.0, expo@~54.0.31, expo-barcode-scanner@^13.0.1, expo-camera@~17.0.10, expo-constants@~18.0.13, expo-font@~14.0.10, expo-haptics@~15.0.8, expo-image@~3.0.11, expo-linking@~8.0.11, expo-router@~6.0.21, expo-splash-screen@~31.0.13, expo-status-bar@~3.0.9, expo-symbols@~1.0.8, expo-system-ui@~6.0.9, expo-web-browser@~15.0.10, react@19.1.0, react-dom@19.1.0, react-native@0.81.5, react-native-gesture-handler@~2.28.0, react-native-markdown-display@^7.0.2, react-native-mmkv@^4.1.1, react-native-reanimated@~4.1.1, react-native-safe-area-context@~5.6.0, react-native-screens@~4.16.0, react-native-web@~0.21.0, react-native-worklets@0.5.1, typescript@~5.9.2, zustand@^5.0.10

### Recent commits (newest first)

- Changing format of .env.example to match .env
- Remove planning docs from tracking
- Adding README
- Remove chromadb from tracking, add to gitignore
- reverting back
- Merge pull request #8 from NguyenEvan/feature/ingredients-llm
- details for ingredients
- Merge pull request #7 from NguyenEvan/feature/rag-pipeline
- Remove .claude from tracking and add to .gitignore
- Merge pull request #6 from NguyenEvan/feature/ingredients-llm
- Merge pull request #5 from NguyenEvan/feature/rag-pipeline
- Merge branch 'main' of github.com:NguyenEvan/GrocerView into feature/rag-pipeline
- llm add bs
- RAG pipeline implementation
- Merge pull request #4 from NguyenEvan/my-feature
- eeee
- Merge pull request #3 from NguyenEvan/unwanted-flagging
- Adding unwanted ingredients and scan flagging
- Merge pull request #2 from NguyenEvan/off-api-integration
- Front to Back

## Key source files (fetched from GitHub, selected and truncated for size)

### backend/requirements.txt

```
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.0
pydantic-settings==2.1.0
httpx==0.26.0
python-dotenv==1.0.0

```

### my-app/package.json

```
{
  "name": "my-app",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@expo-google-fonts/space-grotesk": "^0.4.1",
    "@expo/vector-icons": "^15.0.3",
    "@react-native-async-storage/async-storage": "^2.2.0",
    "@react-navigation/bottom-tabs": "^7.10.0",
    "@react-navigation/elements": "^2.6.3",
    "@react-navigation/native": "^7.1.28",
    "@react-navigation/native-stack": "^7.10.0",
    "expo": "~54.0.31",
    "expo-barcode-scanner": "^13.0.1",
    "expo-camera": "~17.0.10",
    "expo-constants": "~18.0.13",
    "expo-font": "~14.0.10",
    "expo-haptics": "~15.0.8",
    "expo-image": "~3.0.11",
    "expo-linking": "~8.0.11",
    "expo-router": "~6.0.21",
    "expo-splash-screen": "~31.0.13",
    "expo-status-bar": "~3.0.9",
    "expo-symbols": "~1.0.8",
    "expo-system-ui": "~6.0.9",
    "expo-web-browser": "~15.0.10",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-native": "0.81.5",
    "react-native-gesture-handler": "~2.28.0",
    "react-native-mmkv": "^4.1.1",
    "react-native-reanimated": "~4.1.1",
    "react-native-safe-area-context": "~5.6.0",
    "react-native-screens": "~4.16.0",
    "react-native-web": "~0.21.0",
    "react-native-worklets": "0.5.1",
    "zustand": "^5.0.10",
    "react-native-markdown-display": "^7.0.2"
  },
  "devDependencies": {
    "@types/react": "~19.1.0",
    "eslint": "^9.25.0",
    "eslint-config-expo": "~10.0.0",
    "typescript": "~5.9.2"
  },
  "private": true
}

```

### my-app/app/index.tsx

```typescript
import { Redirect } from "expo-router";

export default function Index() {
  return <Redirect href="/(tabs)" />;
}

```

### my-app/app/_layout.tsx

```typescript
import { Stack } from "expo-router";

export default function RootLayout() {
  return <Stack screenOptions={{ headerShown: false }} />;
}

```

### backend/app/main.py

```python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.api.router import api_router

app = FastAPI(
    title="GrocerView API",
    description="API for grocery product analysis with AI-powered insights",
    version="1.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(api_router, prefix="/api/v1")


@app.get("/health")
async def health():
    return {"status": "ok"}

```

### my-app/app/(tabs)/_layout.tsx

```typescript
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";

const TAB_BAR_BG = "#F8F5F0";
const TAB_BAR_BORDER = "#E1DACB";
const TAB_ACTIVE = "#6B8E23";
const TAB_INACTIVE = "#8B978D";

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        headerShown: false,
        tabBarStyle: {
          backgroundColor: TAB_BAR_BG,
          borderTopColor: TAB_BAR_BORDER,
          borderTopWidth: 1,
          height: 64,
          paddingBottom: 10,
          paddingTop: 8,
        },
        tabBarActiveTintColor: TAB_ACTIVE,
        tabBarInactiveTintColor: TAB_INACTIVE,
        tabBarLabelStyle: {
          fontSize: 12,
          fontWeight: "600",
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Scan",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="scan-outline" size={size ?? 22} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="unwanted"
        options={{
          title: "Unwanted",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="ban-outline" size={size ?? 22} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

```

### my-app/app/(tabs)/index.tsx

```typescript
import { StatusBar } from "expo-status-bar";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  ActivityIndicator,
  type LayoutChangeEvent,
  Platform,
  Pressable,
  ScrollView,
  StyleSheet,
  Text,
  View,
  Vibration,
} from "react-native";
import { Linking } from "react-native";
import {
  CameraView,
  type BarcodeScanningResult,
  useCameraPermissions,
} from "expo-camera";
import { SafeAreaView } from "react-native-safe-area-context";
import { useFonts, SpaceGrotesk_400Regular, SpaceGrotesk_500Medium, SpaceGrotesk_600SemiBold, SpaceGrotesk_700Bold } from "@expo-google-fonts/space-grotesk";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import Markdown from "react-native-markdown-display";
import { ManualEntrySheet } from "../../components/ManualEntrySheet";
import { ScannerOverlay } from "../../components/ScannerOverlay";
import { useUnwantedIngredientsStore } from "../../store/useUnwantedIngredientsStore";
import companyOwnershipData from "../../data/companyOwnership.json";

type ScanRecord = {
  value: string;
  source: "camera" | "manual";
  format?: string;
  timestamp: number;
};

type CompanyChainEntry = {
  name: string;
  level: number;
};

type CompanyOwnershipEntry = {
  brand: string;
  parent: string;
  parent_chain: CompanyChainEntry[];
  ultimate_parent: string;
  subsidiaries: string[];
};

type CompanyOwnershipMap = Record<string, CompanyOwnershipEntry>;

type EvidencePassage = {
  id?: string;
  text: string;
  section?: string;
  section_heading?: string;
  relevance_score?: number;
  paper?: {
    pmid?: string;
    title?: string;
    year?: number;
    journal?: string;
  };
  urls?: {
    pubmed?: string;
    pmc?: string;
    doi?: string;
  };
};

type EvidenceResponse = {
  ingredient: string;
  passages: EvidencePassage[];
  total_passages?: number;
};

const ACCENT = "#6B8E23";
const ACCENT_DARK = "#E6E0D2";
const BARCODE_TYPES = ["ean13", "ean8", "upc_a", "upc_e", "qr", "code39", "code128"];
const OFF_BASE_URL = "https://world.openfoodfacts.org/api/v2/product";
const USDA_SEARCH_URL = "https://api.nal.usda.gov/fdc/v1/foods/search";
const USDA_FOOD_URL = "https://api.nal.usda.gov/fdc/v1/food";
const USDA_API_KEY = process.env.EXPO_PUBLIC_USDA_API_KEY;
const COMPANY_OWNERSHIP_DATA = companyOwnershipData as CompanyOwnershipMap;
const COMPANY_SEPARATOR = /[,;/]/;
const INGREDIENTS_LINE_HEIGHT = 20;
const INGREDIENTS_MAX_LINES = 4;
const INGREDIENTS_MAX_HEIGHT = INGREDIENTS_LINE_HEIGHT * INGREDIENTS_MAX_LINES;
// Use 10.0.2.2 for Android emulator, localhost for iOS sim, or your IP for physical device
// TODO: Replace with your computer's local IP for physical device testing
const DEV_SERVER_IP = "172.20.10.7"; // Your computer's Wi-Fi IP
const RAG_BASE_URL = __DEV__
  ? Platform.OS === "android"
    ? "http://10.0.2.2:8000"
    : `http://${DEV_SERVER_IP}:8000`
  : "https://your-production-api.com";
const EVIDENCE_MAX_PASSAGES = 10;

const normalizeIngredientItem = (item: string) =>
  item.replace(/\s+/g, " ").trim();

const splitTopLevelItems = (value: string) => {
  const items: string[] = [];
  let current = "";
  let depth = 0;
  for (const char of value) {
    if (char === "(") {
      depth += 1;
    } else if (char === ")" && depth > 0) {
      depth -= 1;
    }
    if ((char === "," || char === ";") && depth === 0) {
      const trimmed = current.trim();
      if (trimmed) items.push(trimmed);
      current = "";
      continue;
    }
    current += char;
  }
  const trimmed = current.trim();
  if (trimmed) items.push(trimmed);
  return items;
};

const splitIngredientItems = (value?: string) => {
  if (!value) return [];
  const results: string[] = [];
  splitTopLevelItems(value).forEach((item) => {
    const innerItems: string[] = [];
    const matches = item.match(/\(([^()]*)\)/g);
    if (matches) {
      matches.forEach((match) => {
        const inner = match.slice(1, -1);
        splitTopLevelItems(inner).forEach((innerItem) => {
          const cleanedInner = normalizeIngredientItem(innerItem);
          if (cleanedInner) innerItems.push(cleanedInner);
        });
      });
    }
    if (innerItems.length) {
      innerItems.forEach((innerItem) => results.push(innerItem));
      return;
    }
    const cleaned = normalizeIngredientItem(item.replace(/\([^)]*\)/g, " "));
    if (cleaned) results.push(cleaned);
  });
  return results;
};

const normalizeCompanyKey = (value: string) => value.trim().toLowerCase();
const normalizeCompanyLookupKey = (value: string) =>
  value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();

const companyOwnershipIndex = new Map<string, CompanyOwnershipEntry>();
const companyOwnershipNormalizedIndex = new Map<string, CompanyOwnershipEntry>();

for (const [key, entry] of Object.entries(COMPANY_OWNERSHIP_DATA)) {
  companyOwnershipIndex.set(normalizeCompanyKey(key), entry);
  const normalizedKey = normalizeCompanyLookupKey(key);
  if (normalizedKey && !companyOwnershipNormalizedIndex.has(normalizedKey)) {
    companyOwnershipNormalizedIndex.set(normalizedKey, entry);
  }
}

const splitCompanyCandidates = (value?: string) => {
  if (!value) return [];
  return value
    .split(COMPANY_SEPARATOR)
    .map((item) => item.trim())
    .filter(Boolean);
};

const findCompanyOwnership = (candidates: string[]) => {
  for (const candidate of candidates) {
    const directMatch = COMPANY_OWNERSHIP_DATA[candidate];
    if (directMatch) return directMatch;
    const lowered = companyOwnershipIndex.get(normalizeCompanyKey(candidate));
    if (lowered) return lowered;
  }

  for (const candidate of candidates) {
    const normalized = normalizeCompanyLookupKey(candidate);
    if (!normalized) continue;
    const normalizedMatch = companyOwnershipNormalizedIndex.get(normalized);
    if (normalizedMatch) return normalizedMatch;
  }

  return null;
};

export default function Index() {
  const [permission, requestPermission] = useCameraPermissions();
  const [isTorchOn, setT
[truncated — 39840 more characters]
```

### my-app/eslint.config.js

```javascript
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');

module.exports = defineConfig([
  expoConfig,
  {
    ignores: ['dist/*'],
  },
]);

```

### my-app/store/useUnwantedIngredientsStore.ts

```typescript
import { create } from "zustand";
import AsyncStorage from "@react-native-async-storage/async-storage";

export type UnwantedIngredient = {
  name: string;
  addedAt: number;
};

const STORAGE_KEY = "unwanted_ingredients";

const loadItems = async (): Promise<UnwantedIngredient[]> => {
  try {
    const raw = await AsyncStorage.getItem(STORAGE_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
};

const persistItems = async (items: UnwantedIngredient[]) => {
  try {
    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(items));
  } catch {
    // Ignore storage errors to avoid blocking UI updates.
  }
};

type UnwantedIngredientsStore = {
  items: UnwantedIngredient[];
  hasHydrated: boolean;
  hydrate: () => Promise<void>;
  addItem: (name: string) => void;
  removeItem: (name: string) => void;
  clear: () => void;
};

export const useUnwantedIngredientsStore = create<UnwantedIngredientsStore>((set) => ({
  items: [],
  hasHydrated: false,
  hydrate: async () => {
    const items = await loadItems();
    set({ items, hasHydrated: true });
  },
  addItem: (name) =>
    set((state) => {
      const trimmed = name.trim();
      if (!trimmed) return state;
      const exists = state.items.some(
        (item) => item.name.toLowerCase() === trimmed.toLowerCase()
      );
      if (exists) return state;
      const next = [{ name: trimmed, addedAt: Date.now() }, ...state.items];
      void persistItems(next);
      return { items: next };
    }),
  removeItem: (name) =>
    set((state) => {
      const next = state.items.filter(
        (item) => item.name.toLowerCase() !== name.toLowerCase()
      );
      void persistItems(next);
      return { items: next };
    }),
  clear: () =>
    set(() => {
      void persistItems([]);
      return { items: [] };
    }),
}));

```

### my-app/components/ScannerOverlay.tsx

```typescript
import { useEffect, useRef } from "react";
import { Animated, StyleSheet, View, Text } from "react-native";

type ScannerOverlayProps = {
  accentColor: string;
  label?: string;
};

const FRAME_SIZE = 260;

export function ScannerOverlay({ accentColor, label }: ScannerOverlayProps) {
  const scanLine = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.loop(
      Animated.sequence([
        Animated.timing(scanLine, {
          toValue: FRAME_SIZE - 12,
          duration: 1800,
          useNativeDriver: true,
        }),
        Animated.timing(scanLine, {
          toValue: 0,
          duration: 1800,
          useNativeDriver: true,
        }),
      ])
    ).start();
  }, [scanLine]);

  return (
    <View style={StyleSheet.absoluteFill} pointerEvents="none">
      <View style={styles.maskRow}>
        <View style={styles.mask} />
      </View>
      <View style={styles.centerRow}>
        <View style={styles.mask} />
        <View style={[styles.frame, { borderColor: accentColor }]}>
          <View style={[styles.corner, styles.cornerTopLeft, { borderColor: accentColor }]} />
          <View style={[styles.corner, styles.cornerTopRight, { borderColor: accentColor }]} />
          <View
            style={[styles.corner, styles.cornerBottomLeft, { borderColor: accentColor }]}
          />
          <View
            style={[styles.corner, styles.cornerBottomRight, { borderColor: accentColor }]}
          />
          <Animated.View
            style={[
              styles.scanLine,
              { backgroundColor: accentColor, transform: [{ translateY: scanLine }] },
            ]}
          />
          {label ? <Text style={styles.label}>{label}</Text> : null}
        </View>
        <View style={styles.mask} />
      </View>
      <View style={styles.maskRow}>
        <View style={styles.mask} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  maskRow: {
    flex: 1,
    flexDirection: "row",
  },
  mask: {
    flex: 1,
    backgroundColor: "rgba(5, 12, 12, 0.6)",
  },
  centerRow: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "center",
  },
  frame: {
    width: FRAME_SIZE,
    height: FRAME_SIZE,
    borderRadius: 24,
    borderWidth: 1,
    overflow: "hidden",
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(2, 9, 9, 0.15)",
  },
  corner: {
    position: "absolute",
    width: 26,
    height: 26,
  },
  cornerTopLeft: {
    top: 0,
    left: 0,
    borderLeftWidth: 3,
    borderTopWidth: 3,
    borderRadius: 12,
  },
  cornerTopRight: {
    top: 0,
    right: 0,
    borderRightWidth: 3,
    borderTopWidth: 3,
    borderRadius: 12,
  },
  cornerBottomLeft: {
    bottom: 0,
    left: 0,
    borderLeftWidth: 3,
    borderBottomWidth: 3,
    borderRadius: 12,
  },
  cornerBottomRight: {
    bottom: 0,
    right: 0,
    borderRightWidth: 3,
    borderBottomWidth: 3,
    borderRadius: 12,
  },
  scanLine: {
    position: "absolute",
    height: 2,
    width: "100%",
    opacity: 0.8,
  },
  label: {
    position: "absolute",
    bottom: 18,
    color: "#e9f7f7",
    fontSize: 14,
    letterSpacing: 0.3,
  },
});

```

[48 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]