AI-powered job matching for Switzerland. Upload your LinkedIn profile, and JobLens scores hundreds of real job postings against your skills using the LLM of your choice.
Everything runs in your browser. Your API keys and profile data never touch a server -- you bring your own key (BYOK), and all processing happens client-side.
- LinkedIn PDF parsing -- drag-and-drop your LinkedIn export, and an LLM extracts a structured profile (skills, experience, languages, education)
- jobs.ch integration -- searches Switzerland's largest job board via its semantic search API and scrapes full job descriptions
- LLM-powered scoring -- each job is scored 0-100 based on skill match (40%), experience level (25%), domain fit (20%), and language fit (15%)
- Multi-provider support -- works with OpenAI (GPT-4o), Anthropic (Claude Sonnet), or Google Gemini
- Filtering & sorting -- filter by minimum score, sort by match score, date, distance, or company
- Distance calculation -- shows how far each job is from your city (Haversine formula, 24 Swiss cities)
- Privacy-first -- no backend, no database, no tracking. API keys are stored only in your browser's localStorage
- Node.js 18+
- An API key from OpenAI, Anthropic, or Google AI Studio
# 1. Start the CORS proxy
cd worker
npm install
npm run dev # runs on http://localhost:8787
# 2. Start the webapp (in a second terminal)
cd webapp
npm install
npm run dev # runs on http://localhost:5173Open http://localhost:5173 and follow the 4-step wizard.
- Webapp: deploy the
webapp/directory to Vercel, Netlify, or GitHub Pages (npm run buildproduces a staticdist/folder) - Proxy: deploy the worker to Cloudflare (
cd worker && npm run deploy), then setVITE_PROXY_URLto your worker URL before building the webapp
- Settings -- pick your LLM provider and enter your API key
- Profile -- upload your LinkedIn PDF; text is extracted client-side with pdf.js, then an LLM structures it into JSON
- Search -- enter keywords (pre-filled from your profile title); JobLens fetches up to 2000 jobs from jobs.ch, enriches the top 50 with full descriptions, and batch-scores them with your LLM
- Results -- browse scored jobs with color-coded match scores, matching/missing skills, and direct links to job postings
+-----------------------+
| User's Browser |
| |
| React SPA (Vite) |
| - PDF parsing |
| - LLM calls |
| - Job scoring |
| - Zustand state |
+----------+------------+
|
All requests via
/proxy/<host>/...
|
+----------v------------+
| Cloudflare Worker |
| (CORS Proxy) |
| |
| - Stateless |
| - No storage/logging |
| - Host whitelist |
+----+------+-------+---+
| | |
+-------------+ +---+---+ +------------+
| | | |
+------v------+ +-----v-----+ | +------------v---+
| LLM APIs | | jobs.ch | | | jobs.ch |
| | | Search API| | | Detail Pages |
| - OpenAI | +-----------+ | | (JSON-LD) |
| - Anthropic | | +----------------+
| - Gemini | |
+-------------+ |
|
+--------v--------+
| review-api |
| .jobs.ch |
+-----------------+
| Directory | Purpose |
|---|---|
src/components/ |
4 step components (Settings, ProfileUpload, JobSearch, JobResults) + JobCard |
src/services/ |
Core logic: llm-client (multi-provider LLM wrapper), job-scraper (jobs.ch API + HTML scraping), job-scorer (batch scoring), pdf-parser (pdf.js text extraction), geo (distance calc) |
src/prompts/ |
LLM prompt templates for profile extraction and job scoring |
src/store/ |
Zustand store with localStorage persistence (API key, profile, search params) |
src/types/ |
TypeScript interfaces for Job, ScoredJob, UserProfile, etc. |
A ~70-line Cloudflare Worker that transparently forwards requests to whitelisted hosts. It exists solely to bypass browser CORS restrictions. It does not store, log, or transform any data. Allowed hosts:
api.openai.com,api.anthropic.com,generativelanguage.googleapis.com(LLM providers)job-search-api.jobs.ch,www.jobs.ch,review-api.jobs.ch(job data)
- PDF upload --
pdf-parser.tsextracts raw text using pdf.js (entirely client-side) - Profile extraction --
llm-client.tssends text to chosen LLM with a structured extraction prompt; returnsUserProfileJSON - Job search --
job-scraper.tscalls jobs.ch semantic search API (paginated, up to 20 pages of 100 jobs). For the top 50 results, it fetches detail pages and extracts descriptions from JSON-LD markup - Distance enrichment --
geo.tscalculates Haversine distance from the user's city to each job's coordinates - Scoring --
job-scorer.tsbatches 20 jobs per LLM call (3 concurrent batches), scoring each job 0-100 with matching/missing skills - Display --
JobResults.tsxrenders sorted/filtered results withJobCardcomponents
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite |
| Styling | Tailwind CSS |
| State | Zustand (with localStorage persistence) |
| PDF parsing | pdfjs-dist (client-side) |
| CORS proxy | Cloudflare Workers |
| LLM providers | OpenAI, Anthropic, Google Gemini |
| Job data | jobs.ch (search API + HTML scraping) |
| Testing | Vitest |
npm run dev # start dev server (webapp or worker)
npm run build # type-check + production build (webapp)
npm run test # run tests (webapp)
npm run test:watch # run tests in watch mode (webapp)
npm run lint # ESLint (webapp)
npm run deploy # deploy to Cloudflare (worker)