A BI dashboard for NovaBite Consumer Goods with an LLM-powered conversational interface. Built with FastAPI + React + React Native (Expo), seeded from 1,000 rows of transactional sales data.
| Layer | Tech |
|---|---|
| Backend | Python 3.11+, FastAPI, Uvicorn |
| Database | SQLite (file: backend/novabite.db) |
| LLM | Groq API (llama-3.1-8b-instant / llama-3.3-70b-versatile) |
| Web Frontend | React 19, TypeScript, Vite 8 |
| Mobile | React Native (Expo SDK 54) |
| Styling (web) | Tailwind CSS v4, Inter font, Material Symbols |
| Styling (mobile) | react-native-size-matters, MaterialIcons |
| Charts | Custom SVG (no chart library) |
| Package mgmt | uv (backend), bun (frontend), npm (mobile) |
- Python ≥ 3.11, uv
- bun (or npm/pnpm)
- Docker (optional — for containerized run)
- A Groq API key — free tier at console.groq.com
- Expo Go app (v54) on your phone for mobile testing
cp backend/.env.example backend/.env.local
# Edit backend/.env.local and set GROQ_API_KEY
docker compose up --build- Web app: http://localhost
- Backend API: http://localhost:8000
- API docs: http://localhost:8000/docs
- Mobile: Still needs manual setup (see below)
# Terminal 1
cd backend
cp .env.example .env.local
# Edit .env.local and set GROQ_API_KEY
uv sync
uv run python seed.py
uv run devStarts on http://0.0.0.0:8000 with hot reload. The seed script creates novabite.db and is idempotent.
# Terminal 2 (backend must be running)
cd frontend
bun install
bun run devStarts on http://localhost:5173. Proxies /api requests to the backend.
# Terminal 3 (backend must be running)
cd mobile
cp .env.example .env
# Edit .env — set EXPO_PUBLIC_API_URL to your machine's LAN IP
# e.g. EXPO_PUBLIC_API_URL=http://192.168.0.168:8000
# Find it with: hostname -I
npm install
npx expo startScan the QR code with Expo Go app (v54) on your phone. Your phone must be on the same Wi-Fi network as the dev machine.
- Create a Docker Space at https://huggingface.co/new-space
- Clone the Space repo and copy the backend files in:
cp -r backend/* /path/to/space/ cp -r data/ /path/to/space/ - Adjust the Dockerfile's
COPYpaths (files are at root in the Space):COPY . . COPY data/ /app/data/
- Push to the Space — HF auto-builds
- Set
GROQ_API_KEYin Space → Settings → Repository secrets
The backend seeds the database on every container start (idempotent — skips if already seeded).
- Push the
frontend/directory to a GitHub repo - Import it in Vercel (Framework preset: Vite)
- Add
vercel.jsonat the frontend root:{ "rewrites": [ { "source": "/api/(.*)", "destination": "https://YOUR_USERNAME-novabite-bi-backend.hf.space/api/$1" } ] } - Deploy — Vercel proxies
/api/*calls to the HF backend
See backend/.env.example:
| Variable | Default | Notes |
|---|---|---|
GROQ_API_KEY |
— | Required |
GROQ_MODEL |
llama-3.1-8b-instant |
Any Groq-hosted model |
LLM_PROVIDER |
groq |
groq or google |
GOOGLE_API_KEY |
— | Fallback |
GOOGLE_MODEL |
gemma-4-26b-it |
— |
EXPO_PUBLIC_API_URL |
(mobile) | Phone-accessible backend URL |
1,000 rows of NovaBite sales transactions (data/novabite_sales_data.csv) with 18 columns:
transaction_id, date, month, quarter, sku, product_name, category, subcategory, region, channel, sales_rep, units_sold, unit_price_usd, gross_revenue_usd, discount_pct, net_revenue_usd, cogs_usd, gross_profit_usd
Covering 2024–2025 across 5 regions (North/South/East/West/Central), 4 categories, 4 channels, and 12 products.
Run uv run python backend/seed.py to populate the database (idempotent — skips if already seeded).
| Method | Path | Returns |
|---|---|---|
GET |
/api/products |
All products with total units & revenue |
GET |
/api/summary |
KPI card data + YoY trends |
GET |
/api/trends |
Monthly revenue time series |
POST |
/api/chat |
Streaming SSE response from the LLM |
Send { "question": "your question", "history": [...] } to /api/chat. The backend builds a compact text context from the database (~500 tokens) containing all KPIs, per-region and per-quarter revenue, category margins, top reps, and channel comparisons. This context is injected into the system prompt.
Previous conversation turns can be sent in the optional history array as {role, content} pairs (OpenAI message format). These are inserted between the system prompt and the current question, giving the LLM awareness of prior context. History is scoped to the page/session — refreshing clears it.
The response streams as SSE via the Groq SDK (groq Python package). Temperature is set to 0.1 for factual consistency. If the primary model fails, it falls back through llama-3.3-70b-versatile → llama-3.1-8b-instant. Fallback models that are decommissioned on Groq (gemma2-9b-it, llama3-70b-8192) have been removed from the chain.
Test questions that should work:
- "Which region had the highest net revenue in Q1 2024?"
- "What is the gross profit margin for the Snacks category?"
- "Which sales rep closed the most units in 2025?"
- "Compare E-Commerce vs Modern Trade net revenue."
- "What was the best performing product in the West region?"
Web Browser (React SPA) ──nginx /api──▶ FastAPI ──▶ SQLite
:80 :8000
│
┌────────────────────┤
▼ ▼
Expo Go (mobile) Groq API
(React Native) (LLM inference)
- The dashboard fetches summary + trends on mount for KPI cards and charts.
- Chat questions go through the backend, which pre-computes data slices (cached in memory) and sends them as prompt context — no dynamic SQL generation, no SQL injection risk.
- The typewriter effect on the web frontend runs at 30ms per tick (1 char/tick) for a natural ChatGPT-like streaming feel.
- Mobile uses
expo/fetchfor properReadableStreamsupport (React Native's globalfetchlacks it).
| Component | Purpose |
|---|---|
Dashboard |
Fetches /api/summary + /api/trends, renders KPIs + charts |
Chat |
Streaming chat with typewriter, suggestion buttons |
Sidebar |
Collapsible nav (overlay on mobile, persistent on desktop) |
Header |
Tab switcher, sidebar toggle, centered nav |
KpiCard |
Metric display with trend indicator |
TrendChart |
SVG line chart — monthly revenue |
CategoryChart |
SVG bar chart — category breakdown |
| Component | Purpose |
|---|---|
app/(tabs)/dashboard.tsx |
Dashboard screen with KPI cards + charts |
app/(tabs)/chat.tsx |
Chat screen with streaming, avatars, pull-to-refresh |
components/KpiCard |
Metric card with trend + MaterialIcons icon |
components/TrendChart |
SVG line chart |
components/CategoryChart |
SVG bar chart (safe-area aware) |
api.ts |
API client using expo/fetch for stream support |
| Script | Command |
|---|---|
| Dev server | dev (alias for uvicorn app.main:app --reload) |
| Seed DB | uv run python backend/seed.py |
| Script | Command |
|---|---|
| Dev | dev |
| Build | build (tsc -b && vite build) |
| Preview | preview |
| Lint | lint |
- FastAPI over Express: Native async, auto OpenAPI docs, pandas/httpx ecosystem.
- Groq over Google/OpenAI: 30 RPM free tier vs 3 RPM Google, no credit card needed, sub-100ms token latency.
llama-3.1-8b-instant: Fast, reliable, generous free-tier TPM limits (6K).openai/gpt-oss-120bandgemma2-9b-itwere tried but decommissioned or too restrictive on free tier (8K TPM).- Pre-computed context over dynamic SQL: Inject all aggregates into the prompt — simpler, no SQL injection risk, covers all test questions.
- Compact context format: The context is formatted as plain text (~500 tokens) to stay within Groq free-tier TPM limits. Verbose dict reprs were replaced with inline
key(value)format. - Custom SVG over Recharts: Removed recharts dependency; ~6KB vs ~200KB, exact design control.
- Separate
.envper directory: Backend needs API keys, frontend doesn't.backend/.env.localis gitignored. - No JOINs, no indexes: 1,000 rows doesn't need them. Multiple SELECTs preferred for simplicity.
- Docker: Single
docker compose upbuilds both services. Frontend served via nginx, backend via uvicorn. Nginx proxies/apito the backend container. expo/fetchfor mobile streaming: React Native's globalfetchdoesn't supportReadableStream. The Expo-providedexpo/fetchmodule provides a WinterCG-compliant implementation that does.- Groq SDK over raw httpx: The
groqPython SDK handles SSE parsing, retries, and error types. The old httpx-based manual SSE parser was replaced for maintainability.
- Persistent SQLite volume in Docker: The DB is seeded on every container start (idempotent). A Docker volume would persist data across restarts and avoid the ~1s seed overhead on cold boot.
- Better error handling on frontend: Network failures, rate limits, and malformed responses could show friendlier messages instead of "Error: failed to get response."
- Pagination on products: Only 12 products so it's fine, but the endpoint should support
?limitand?offsetfor larger datasets. - Mobile keyboard handling: The chat input should avoid being hidden by the on-screen keyboard (KeyboardAvoidingView).
- Request cancellation: The abort controller was removed during the typewriter refactor. Long responses should be cancellable mid-stream.
- CI/CD: A GitHub Actions workflow that runs tests on push would catch regressions before review.
- Prompt versioning: The context template in
context.pyis hardcoded. A prompt registry with version tracking would make iteration safer. - Dynamic context selection: Currently all data slices are injected for every question. For very large datasets, a routing layer could select only relevant context.
- Mobile push notifications: Alerts for key metric changes would make the mobile app more useful as a monitoring tool.
2788923 project initialization
da25b8d CSV → SQLite seeding (pandas, 1000 rows)
a2fe273 GET /api/products, /api/summary, /api/trends
ff91393 POST /api/chat with Groq LLM streaming
43a4082 Backend MVP — data context + routing
b7709ba Tailwind CSS + hardcoded KPI dashboard
5ff0e7a KPI dashboard UI fixes
0139850 Unit tests (seed + routes), strict TypeScript
926b80c Dockerfiles + docker-compose.yml
11d2fa9 Spec-accurate project structure
01e464c Mobile app — Expo SDK 54
e69ff87 Groq model fallback, expo/fetch for streams
24659c6 README update, context size reduction
280dc0e Merge mobile branch into main
b6420fa Root .gitignore
60c7d90 README + gitignore cleanup
b9d4fbb Remove fallback LLMs failing on HF Spaces
- Pre-computed context over dynamic SQL: Injecting all aggregates into the prompt is simpler and avoids SQL injection risk, but won't scale to millions of rows. For 1,000 rows it's the right call.
- No JOINs, no indexes: Multiple simple SELECTs are easier to read and debug. With 1,000 rows, query time is sub-ms anyway.
- SQLite over PostgreSQL: No server to manage, file-based, good enough for the data size. Would hit concurrency limits under real load.
- Custom SVGs over Recharts: Saved ~200KB bundle size and gave exact design control, but took longer to implement and has no built-in interactivity (tooltips, zoom).
- Flat config module over class-based: 5 environment variables didn't warrant a config class. Less ceremony, more readable.
llama-3.1-8b-instantover larger models: Smaller model means lower latency and higher free-tier TPM (6K vs 8K for gpt-oss-120b), but the 8B model may miss nuance on complex questions.- Context trimmed to ~500 tokens: Had to reduce verbose dict reprs to fit within Groq free-tier TPM limits. Some detail lost (e.g. exact margin values truncated to 2 decimal places only).
- Groq SDK over raw httpx: SDK is cleaner but adds a dependency. The old httpx SSE parser was brittle with edge cases (partial chunks, DONE marker spacing).
expo/fetchfor mobile: Only works in Expo environments (not bare React Native). If the app were ejected, we'd need a polyfill likereact-native-fetch-api.backend/.env.localinstead of root.env: Backend and frontend have different env needs. Keeping per-directory avoids confusion even though the spec shows root.env.example.- Single Docker compose profile: No dev/prod separation. Fast feedback, but the production image includes dev tooling (bun install, full build chain).
- No request timeout on LLM calls: If Groq hangs, the streaming response hangs indefinitely. A timeout wrapper would be safer.
- Magic numbers in chart SVGs: The TrendChart viewBox (800×280) and gridline positions are hardcoded. Responsive sizing could be more robust.
revmind/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI app + CORS + router mounts
│ │ ├── config.py # Env vars (flat module constants)
│ │ ├── database.py # SQLite connection
│ │ ├── context.py # Pre-computes data context for LLM
│ │ ├── llm.py # Groq client + streaming + fallback
│ │ └── routes/
│ │ ├── chat.py # POST /api/chat (streaming SSE)
│ │ ├── products.py # GET /api/products
│ │ ├── summary.py # GET /api/summary
│ │ └── trends.py # GET /api/trends
│ ├── tests/
│ │ ├── conftest.py # FastAPI TestClient fixture
│ │ ├── test_seed.py # 6 seed tests
│ │ └── test_routes.py # 13 route tests
│ ├── seed.py # Idempotent CSV→SQLite loader
│ ├── Dockerfile
│ ├── .env.example
│ ├── pyproject.toml
│ └── novabite.db # SQLite DB (gitignored)
├── frontend/
│ ├── src/
│ │ ├── App.tsx # Root: tab routing + sidebar
│ │ ├── types.ts # TypeScript interfaces
│ │ ├── index.css # Tailwind v4 + custom theme
│ │ └── components/
│ │ ├── Dashboard.tsx
│ │ ├── Chat.tsx
│ │ ├── Header.tsx
│ │ ├── Sidebar.tsx
│ │ ├── KpiCard.tsx
│ │ ├── TrendChart.tsx
│ │ └── CategoryChart.tsx
│ ├── Dockerfile
│ ├── nginx.conf
│ └── package.json
├── data/
│ └── novabite_sales_data.csv # 1,000-row sales dataset
└── docker-compose.yml