A local-first web app for reviewing PDFs (often exported from PowerPoint): it extracts highlighted regions and annotations, shows each page as a slide image next to editable notes, and stores metadata in SQLite by default or in Postgres (Supabase) when DATABASE_URL is set.
This repository is a monorepo (single Git repo):
| Package | Path | Role |
|---|---|---|
| API | apps/api/ |
FastAPI, PDF processing, SQLite or Postgres (data/ at repo root for files) |
| Web | frontend/ |
React + Vite (npm workspace) |
Shared at repo root: data/ (runtime), static/ (production UI build output), pyproject.toml (Ruff), package.json (npm workspaces).
| Layer | Technology |
|---|---|
| API | FastAPI, Uvicorn |
| PDF / slides | PyMuPDF, pdfplumber, pdf2image (Poppler), Pillow |
| DB | SQLite (data/db.sqlite, WAL) or Postgres via DATABASE_URL (e.g. Supabase) |
| UI | React 19, Vite, Ant Design, TipTap (WYSIWYG notes), dnd-kit (sidebar order) |
- Import PDFs — Upload one or more PDFs; pages are rendered to PNGs under
data/slides/. - Highlight extraction — Standard annotations, yellow vector fills (common in PowerPoint PDFs), and pixmap fallback when needed.
- Sidebar — Drag to reorder documents; rename titles; delete documents; export selected docs to a plain-text study file.
- Slides — Filter by “no highlights”, “starred only”; hide slides; rescan from the stored PDF copy (
data/originals/). - Notes — Each highlight or Add note entry uses a rich editor (bold, lists, etc.); text is stored as HTML; star a note to mark it; delete per note.
- Single-server deploy —
npm run buildwrites the SPA intostatic/; FastAPI serves the UI and API on one port.
-
Python 3.11+ (3.13 works with the pinned stack)
-
Node.js 20+ (for the frontend)
-
Poppler — required by
pdf2imageto rasterize pages:brew install poppler
On Linux, install the
poppler-utils(or equivalent) package for your distribution.
-
Clone the repository and enter the project directory.
-
Python virtual environment (recommended at repo root)
python3 -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install -r apps/api/requirements.txt
-
Install frontend dependencies (npm workspaces — run once from repo root)
npm install
-
Production UI build (writes to
static/)npm run build
-
Run the API (serves API +
static/SPA)source venv/bin/activate cd apps/api python main.py
-
Open http://localhost:8000 in your browser.
With DATABASE_URL set (and empty/unset for plain SQLite), the API uses psycopg against your Postgres database. The app still stores PDF originals and slide PNGs on disk under data/; only documents / slides / highlights metadata live in Postgres.
-
Create a Supabase project and run the SQL in
supabase/migrations/20260403180000_bella_note_initial.sql(or apply migrations from the Supabase dashboard / CLI). -
In Supabase: Project Settings → Database → Connection string → URI. Prefer Session pooler (Session mode) if you deploy anywhere without IPv6 (e.g. Render); Direct can fail with “network is unreachable” there. Include
?sslmode=requireif not already in the string. -
Add the URI to a
.envfile at the repo root (copy from.env.example), for example:cp .env.example .env # Edit .env and set DATABASE_URL=postgresql://... -
Start the API as usual; variables from
.envload automatically:cd apps/api && python main.py
Optional: put overrides in
apps/api/.env(loaded after the root.env). -
Slide images on a host without persistent disk (e.g. Render): set
SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEY(see.env.example). Slides are stored in thebella-note-slidesStorage bucket; the UI still uses/slides/...and the API redirects to the public object URL.
With the server running, interactive docs are at http://localhost:8000/docs (OpenAPI).
Terminal 1 — API with auto-reload (working directory must be apps/api so imports resolve):
source venv/bin/activate
cd apps/api
uvicorn main:app --reload --host 127.0.0.1 --port 8000Terminal 2 — Vite dev server (from repo root; proxies API routes to port 8000):
npm run devUse http://localhost:5173 during UI work.
Install Python dev tools:
source venv/bin/activate
pip install -r apps/api/requirements-dev.txtPython — from the repository root:
ruff check .
ruff format --check . # or `ruff format .` to applyFrontend — from repo root:
npm run lint
npm run lint:fixnpm run build runs tsc -b in the workspace.
GitHub Actions (.github/workflows/lint.yml) runs Ruff, ESLint, and the production build on push and pull requests.
Vite 8 uses Rolldown, which depends on platform-specific native bindings. The lockfile reflects the machine where you last ran npm install; GitHub Actions uses Linux x64. Without an explicit Linux binding entry, npm ci on CI can succeed locally on macOS but fail in Actions with “Cannot find native binding” / missing @rolldown/binding-linux-x64-gnu.
This repo pins @rolldown/binding-linux-x64-gnu under optionalDependencies in frontend/package.json (same version as the rolldown required by Vite) so Linux CI installs that package while macOS skips it.
When upgrading Vite: align that optional dependency with the Rolldown version Vite pulls in (check npm ls rolldown or package-lock.json), run npm install at the repo root, and commit the updated lockfile. If you prefer to avoid native bindings altogether, you can instead standardize on Vite 6 (Rollup-based) and drop the Rolldown binding workaround.
.
├── package.json # npm workspaces (includes frontend)
├── package-lock.json # after npm install at root
├── pyproject.toml # Ruff (repo-wide)
├── apps/
│ └── api/ # FastAPI backend
│ ├── main.py
│ ├── database.py
│ ├── pdf_processor.py
│ ├── requirements.txt
│ └── requirements-dev.txt
├── frontend/ # React + Vite (workspace package)
│ ├── src/
│ └── package.json
├── data/ # Runtime (gitignored except .gitkeep)
│ ├── db.sqlite
│ ├── slides/
│ └── originals/
└── static/ # Built SPA (gitignored; from npm run build)
- Export — Use the export control in the sidebar to include checked documents in
study_master.txt(plain text; HTML in notes is stripped for export). - Rescan — Only works when a stored original exists. Older uploads without a copy must be re-imported once.
- Flattened PDFs — If highlights are baked into a flat image with no vectors or annotations, extraction may find nothing; the app will warn after upload or rescan.
static/is ignored so the repo stays free of build artifacts; CI or deploy steps should runnpm run build.data/(database, slides, originals) is ignored except optional.gitkeepplaceholders—do not commit personal PDFs or databases unless you intend to.
No license file is included in this repository; add one if you distribute the project.