# Project export: HireUp

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: TreeHacks 2026
- Tagline: Match with the best applicants, powered by AI agents and TikTok's recommendation algorithm.
- Devpost: https://devpost.com/software/hireright
- GitHub: https://github.com/pahu2353/HireUp
- Video: https://www.youtube.com/embed/a03VWeouYsk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Y Combinator] Build an Iconic YC Company with AI (1st Place: Guaranteed YC interview 2nd Place: Guaranteed YC Office Hours 3rd Place: Guaranteed YC Office Hours))
- Team: 4 GitHub contributor(s) — pahu2353 (25 commits), Richard (13 commits), Harry Jiang (11 commits), Cursor (1 commits)

## Devpost submission (written by the team)

### Overview

Motivation Imagine you’re a recruiter at a startup trying to hire a great engineer. You open applications expecting ~100 relevant candidates, but instead you see over 2,000. How do you pick out who to interview, and how can you be sure you didn't miss the best candidates? The modern job application pipeline is a lose-lose for everyone involved. Job seekers have to constantly spam apply just to have a shot, while recruiting teams have to sift through thousands of applications, when most are jut noise. In an ideal world, candidates only apply for exactly what they're qualified and a good match for, and recruiters always have high signal results. While hiring for some student led clubs, we realized how dire the situation has become, and wanted to set out and change the status quo has become. After speaking to many experienced recruiters, we realized we had just the right combination of experience and insight to change things for the better.

### What it does

HireUp turns hiring from a volume game into a quality and fit game. We do this in three steps: Scarcity + targeting: candidates see a limited number of highly curated roles per day and can only apply to a small number. This reduces spam, increases intent, and improves signal for both sides. Scarcity + targeting: candidates see a limited number of highly curated roles per day and can only apply to a small number. This reduces spam, increases intent, and improves signal for both sides. Two-tower matching + feedback loops: we use a two-tower model that learns from outcomes on both sides (interest, interviews, feedback, etc.) to recommend jobs candidates want and jobs that are likely to want them back. Two-tower matching + feedback loops: we use a two-tower model that learns from outcomes on both sides (interest, interviews, feedback, etc.) to recommend jobs candidates want and jobs that are likely to want them back. AI recruiting assistant lets teams interactively search and shortlist candidates based on a more intimate understanding of each applicant's profile, bringing more confidence and efficiency in your results. AI recruiting assistant lets teams interactively search and shortlist candidates based on a more intimate understanding of each applicant's profile, bringing more confidence and efficiency in your results.

### How we built it

Candidate user flow: sign up → upload resume + interests → receive a daily set of matched roles → apply → get offers Company user flow: create a posting → receive a smaller, higher-signal candidate set → use the recruiter assistant to query and compare → select interviews → submit feedback Under the hood: We represent jobs and candidates as embeddings in a two-tower setup. We update embeddings from real interactions (apply, interview, offer, feedback), and use those signals to improve future matching. We built multimodal AI agents to help recruiting teams analyze applicants, compare strengths, all to ensure the best matches based on any relevant criteria.

### Challenges we ran into

Two-sided objectives: optimizing for “what candidates want” alone isn’t enough; the model also needs to learn “what companies will say yes to.” Keeping recommendations stable: making sure updates don’t drift or collapse required careful normalization and conservative update steps. Signal quality: recruiter feedback can be messy or inconsistent, so we designed the system to learn from multiple signals rather than a single label.

### Accomplishments we're proud of

We were able to build an end-to-end demo of a scarcity-based applications portal that incorporates the two-tower model for the highest quality matches Implemented a dynamic two-way learning system that collects feedback from candidate and company actions. Built a power interactive recruiting assistant that can summarize, evaluate, and index on any criteria of your choosing, all with great visual tools to help recruiters. Grounded the product in real workflows by speaking with recruiters early and iterating from their feedback.

### What we learned

The biggest lever in hiring is incentives: when applying is free, spam is rational; scarcity changes behavior and improves signal. Hiring matching is inherently two-sided, and systems work better when they learn from both sides’ outcomes. AI assistants are most valuable when the funnel is already high-signal—reducing noise first makes everything downstream faster and more reliable.

### What's next

Improve our resume and job encoders with models that train on more real world interactions and context. Improve and iterate the interactive recruiting assistant by working closely with recruiters. Add in better offline evaluation metrics to verify user integrity. Ship product to the real world, starting with smaller startups, and slowly growing from there!

## README (from the GitHub repository)

# HireUp
<img width="2880" height="1684" alt="image" src="https://github.com/user-attachments/assets/1b670e35-b57f-4d9a-8acc-213e821e3aed" />

Demo: https://youtu.be/a03VWeouYsk

-- 
HireUp is a high-signal hiring platform built to fix noisy recruiting pipelines.

Instead of rewarding spam applications and resume falsification, HireUp combines:

- Scarcity-driven application limits
- A novel two-way two-tower matching system
- Iris, an AI recruiting analyst/chat interface

The goal is simple: help companies find stronger candidates faster, and help applicants apply with intent instead of volume.

Imagine you’re a recruiter at an early-stage startup. You open applications expecting 100 strong resumes. Instead, you get 2000 applicants, and most are not relevant to your stack or role requirements.

You end up spending time sifting through low-signal applications while the best-fit candidates get buried. At the same time, applicants are forced into a spam-apply strategy just to get responses.

This creates three core problems:

1. Job applications become a volume game, not a quality game.
2. Resume misrepresentation rises because the system rewards attention-grabbing over fit.
3. Screening burden falls on one recruiter/hiring lead, slowing down decisions.

HireUp addresses these in sequence:

- Scarcity constraints reduce spam and improve applicant intent.
- A two-way two-tower model improves fit matching in both directions.
- Iris analyzes and ranks finalists through an interactive recruiting interface.

Iris works because problems 1 and 2 are solved first, so it operates on higher-quality, higher-trust candidate pools.

## Why This Team Thesis

- We are Waterloo students and have seen WaterlooWorks quality degrade over time as competition and volume pressure increased.
- We have direct experience implementing two-tower retrieval systems.
- Our adaptation applies two-way preference learning to hiring, which is uncommon in this space.
- We aim to make recruiters 10x more effective, similar to how tools like Cursor increase developer leverage.

## Product Thesis

### Problem 1: Applications Are a Volume Game

- Platforms incentivize spam
- Serious candidates get drowned out
- Recruiters spend energy filtering noise instead of evaluating signal

### Problem 2: Resume Signal Is Easy to Game

- Resume point falsification is incentivized by spam-heavy funnels
- Resume-only filtering is weak when input quality is poor

### Problem 3: Final Screening Load Is Too High

- One recruiter/founder often handles too much manual triage
- Decision quality drops as fatigue rises

## Solution Architecture

### 1. Scarcity as a Product Mechanism

- Daily application caps force intentional applications
- Constrained flow increases average quality per application

### 2. Two-Way Two-Tower Matching

- Represent users and jobs as embeddings
- Learn both:
  - Which jobs users are likely to apply to
  - Which users companies are likely to interview/select
- Continuously update from outcomes:
  - apply, reject, interview, offer, feedback

### 3. Iris (AI Recruiting Agent)

- Parses and reasons over the shortlisted candidate pool
- Supports chat-based queries:
  - “Find candidates strong in X/Y/Z”
  - “Rank for this role with these constraints”
- “Agent mode” continuously searches for better-fit candidates as data changes

## End-to-End Flows

### Applicant Flow

1. Create account (resume, interests, objectives)
2. Receive daily matched jobs
3. Apply to a limited number of jobs
4. Interview and offer outcomes are logged
5. Acceptance/rejection feeds back into matching model

### Company Flow

1. Create company account
2. Create/manage job postings
3. Two-tower ranking reduces raw pool (e.g., 2000 -> ~50 strong-fit candidates)
4. Iris analyzes and ranks candidates based on recruiter prompts (e.g., 50 -> ~12)
5. Recruiter submits interview list and post-interview feedback
6. Feedback loops into model updates

## How We Differ

- We optimize data quality before AI ranking.
- Two-tower validation and behavior feedback reduce embellishment impact.
- Iris operates on higher-trust candidate sets, which improves ranking quality.

## System Components

### Frontend

- Company:
  - Signup/login
  - Create/manage postings
  - Iris chat + ranking UI
  - Interview list + feedback submission
- Applicant:
  - Signup/profile with resume + interests
  - Daily matched jobs
  - Apply flow

## Two-Tower Notebook Plan (Initialization + Balancing)

Planned notebook workflow:

1. Load jobs from `hireup.db`, initialize random normalized embeddings in a jobs vecdb.
2. Load users from `hireup.db`, initialize random normalized embeddings in a users vecdb.
3. Repeatedly sample job subsets, score similarity with an LLM, and update cluster structure.
4. Repeatedly sample user subsets, score similarity with an LLM, and update cluster structure.
5. Repeatedly sample 1 user + N jobs, infer likely apply behavior, update both towers.
6. Repeatedly sample 1 job + N users, infer likely selection behavior, update both towers.

Important constraint:

- Always re-normalize vectors to the unit sphere after updates.

## Evaluation Cells (Planned)

Include notebook cells that run repeated comparison tests between vecdb nearest-neighbor outcomes and LLM judgments:

1. User-user nearest match checks
2. Job-job nearest match checks
3. Job-to-user preference checks
4. User-to-job preference checks

Each test should run multiple rounds and print aggregate accuracy per metric and overall.

## Embedding Update Functions (Planned in `two-tower/`)

Add functions for:

- User applies to job -> pull embeddings closer
- User rejects offer -> push embeddings apart
- Company interviews user -> pull embeddings closer
- Company feedback score (0-10) -> push/pull by score intensity

## Setup

### Backend

```bash
cd Backend
pip install -r requirements.txt
uvicorn main:app --reload --port 8000
```

### Frontend

```bash
cd frontend
npm install
npm run dev
```

## Environment Variables

Use a local `.env` file and keep secrets out of git.

Example:

```env
OPENAI_API_KEY=your_key_here
OPENAI_MODEL=gpt-5-nano
```

Security note: if any API key has been shared in plain text, rotate it immediately and replace it with a new key.


## Detected evidence (automated analysis)

Indexed codebase: 113 recognized source files, 6647 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 128)

```
.claude/settings.local.json
.gitignore
backend/config.py
backend/database.py
backend/main.py
backend/passwords.json
backend/pdf_utils.py
backend/README.md
backend/requirements.txt
backend/routers/__init__.py
backend/routers/auth.py
backend/routers/companies.py
backend/routers/users.py
backend/schemas/__init__.py
backend/schemas/auth.py
backend/schemas/company.py
backend/schemas/user.py
backend/seed_data.py
backend/seed_google_applicants.py
backend/seed_google_intern_applicants.py
backend/seed_jane_street_applicants.py
backend/seed_jane_street_swe_intern_applicants.py
backend/services/__init__.py
backend/services/company.py
backend/services/user.py
create_resume_accounts.py
embedding_visualizer/embeddings_3d.html
embedding_visualizer/README.md
embedding_visualizer/requirements.txt
embedding_visualizer/visualize_embeddings.py
frontend/.gitignore
frontend/app/company/agent/page.tsx
frontend/app/company/agent/page.tsx.bak
frontend/app/company/candidates/page.tsx
frontend/app/company/page.tsx
frontend/app/company/postings/page.tsx
frontend/app/company/profile/page.tsx
frontend/app/dashboard/applications/page.tsx
frontend/app/dashboard/page.tsx
frontend/app/dashboard/profile/page.tsx
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/login/page.tsx
frontend/app/page.tsx
frontend/app/signup/page.tsx
frontend/components.json
frontend/components/brand/logo.tsx
frontend/components/dashboard/dashboard-shell.tsx
frontend/components/grid-background.tsx
frontend/components/landing/dual-cta.tsx
frontend/components/landing/footer.tsx
frontend/components/landing/hero.tsx
frontend/components/landing/navbar.tsx
frontend/components/landing/problems.tsx
frontend/components/landing/solution.tsx
frontend/components/theme-provider.tsx
frontend/components/theme-toggle.tsx
frontend/components/ui/accordion.tsx
frontend/components/ui/alert-dialog.tsx
frontend/components/ui/alert.tsx
frontend/components/ui/aspect-ratio.tsx
frontend/components/ui/avatar.tsx
frontend/components/ui/badge.tsx
frontend/components/ui/breadcrumb.tsx
frontend/components/ui/button.tsx
frontend/components/ui/calendar.tsx
frontend/components/ui/card.tsx
frontend/components/ui/carousel.tsx
frontend/components/ui/chart.tsx
frontend/components/ui/checkbox.tsx
frontend/components/ui/collapsible.tsx
frontend/components/ui/command.tsx
frontend/components/ui/context-menu.tsx
frontend/components/ui/dialog.tsx
frontend/components/ui/drawer.tsx
frontend/components/ui/dropdown-menu.tsx
frontend/components/ui/form.tsx
frontend/components/ui/hover-card.tsx
frontend/components/ui/input-otp.tsx
frontend/components/ui/input.tsx
frontend/components/ui/label.tsx
frontend/components/ui/menubar.tsx
frontend/components/ui/navigation-menu.tsx
frontend/components/ui/pagination.tsx
frontend/components/ui/popover.tsx
frontend/components/ui/progress.tsx
frontend/components/ui/radio-group.tsx
frontend/components/ui/resizable.tsx
frontend/components/ui/scroll-area.tsx
frontend/components/ui/select.tsx
frontend/components/ui/separator.tsx
frontend/components/ui/sheet.tsx
frontend/components/ui/sidebar.tsx
frontend/components/ui/skeleton.tsx
frontend/components/ui/slider.tsx
frontend/components/ui/sonner.tsx
frontend/components/ui/switch.tsx
frontend/components/ui/table.tsx
frontend/components/ui/tabs.tsx
frontend/components/ui/textarea.tsx
frontend/components/ui/toast.tsx
frontend/components/ui/toaster.tsx
frontend/components/ui/toggle-group.tsx
frontend/components/ui/toggle.tsx
frontend/components/ui/tooltip.tsx
frontend/components/ui/use-mobile.tsx
frontend/components/ui/use-toast.ts
frontend/hooks/use-mobile.tsx
frontend/hooks/use-toast.ts
frontend/lib/api.ts
frontend/lib/company-api.ts
frontend/lib/company-jobs.ts
frontend/lib/user-api.ts
frontend/lib/utils.ts
frontend/next-env.d.ts
frontend/next.config.mjs
frontend/package.json
frontend/postcss.config.mjs
frontend/styles/globals.css
frontend/tailwind.config.ts
[8 more files omitted for size]
```

### Dependencies

- backend/requirements.txt: bcrypt@>=4.0.0,<5.0.0, fastapi@>=0.115.0, passlib[bcrypt]@>=1.7.4, pymupdf@>=1.24.0, python-multipart@>=0.0.12, uvicorn[standard]@>=0.32.0
- embedding_visualizer/requirements.txt: plotly@>=5.24.0
- frontend/package.json: @hookform/resolvers@^3.9.1, @radix-ui/react-accordion@1.2.2, @radix-ui/react-alert-dialog@1.1.4, @radix-ui/react-aspect-ratio@1.1.1, @radix-ui/react-avatar@1.1.2, @radix-ui/react-checkbox@1.1.3, @radix-ui/react-collapsible@1.1.2, @radix-ui/react-context-menu@2.2.4, @radix-ui/react-dialog@1.1.4, @radix-ui/react-dropdown-menu@2.1.4, @radix-ui/react-hover-card@1.1.4, @radix-ui/react-label@2.1.1, @radix-ui/react-menubar@1.1.4, @radix-ui/react-navigation-menu@1.2.3, @radix-ui/react-popover@1.1.4, @radix-ui/react-progress@1.1.1, @radix-ui/react-radio-group@1.2.2, @radix-ui/react-scroll-area@1.2.2, @radix-ui/react-select@2.1.4, @radix-ui/react-separator@1.1.1, @radix-ui/react-slider@1.2.2, @radix-ui/react-slot@1.1.1, @radix-ui/react-switch@1.1.2, @radix-ui/react-tabs@1.1.2, @radix-ui/react-toast@1.2.4, @radix-ui/react-toggle@1.1.1, @radix-ui/react-toggle-group@1.1.1, @radix-ui/react-tooltip@1.1.6, @tailwindcss/postcss@^4.1.13, @types/node@^22, @types/react@19.2.7, @types/react-dom@19.2.3, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@1.1.1, date-fns@3.6.0, embla-carousel-react@8.5.1, input-otp@1.4.1, lucide-react@^0.544.0, next@16.1.6, next-themes@^0.4.6, postcss@^8.5, react@19.2.3, react-day-picker@^9.11.0, react-dom@19.2.3, react-hook-form@^7.54.1, react-resizable-panels@^2.1.7, recharts@2.15.0, sonner@^1.7.1, tailwind-merge@^2.5.5, tailwindcss@^3.4.17, tailwindcss-animate@^1.0.7, typescript@5.7.3, vaul@^1.1.2, zod@^3.24.1
- requirements.txt: bcrypt@>=4.0.0,<5.0.0, fastapi@>=0.115.0, matplotlib, numpy, openai, passlib[bcrypt]@>=1.7.4, pymupdf@>=1.24.0, python-multipart@>=0.0.12, scikit-learn, uvicorn[standard]@>=0.32.0

### Recent commits (newest first)

- Update demo link format in README.md
- Add YouTube link to README
- Update README.md
- Add README
- Improve Landing Page and Implement Logo
- Agent, not GPT
- recover broken things, ai agent should be fully functional
- visualizer
- Fixed Ring Issue in Spiderweb
- Merge origin/main and keep local vector DB
- Resolve Merge Conflicts
- Added GitHub, grad date, LinkedIn links
- Merge branch 'main' of https://github.com/pahu2353/HireUp
- adding resume update
- aweofij
- Integrated Twin Tower Model to Backend
- small updates to embeddings :)
- Merge branch 'main' of https://github.com/pahu2353/HireUp
- embedding initializer
- Updated Frontend

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

### requirements.txt

```
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
passlib[bcrypt]>=1.7.4
bcrypt>=4.0.0,<5.0.0
python-multipart>=0.0.12
pymupdf>=1.24.0
numpy
openai
matplotlib
scikit-learn
```

### embedding_visualizer/requirements.txt

```
plotly>=5.24.0

```

### backend/requirements.txt

```
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
passlib[bcrypt]>=1.7.4
bcrypt>=4.0.0,<5.0.0
python-multipart>=0.0.12
pymupdf>=1.24.0

```

### frontend/package.json

```
{
  "name": "my-project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbo",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.9.1",
    "@radix-ui/react-accordion": "1.2.2",
    "@radix-ui/react-alert-dialog": "1.1.4",
    "@radix-ui/react-aspect-ratio": "1.1.1",
    "@radix-ui/react-avatar": "1.1.2",
    "@radix-ui/react-checkbox": "1.1.3",
    "@radix-ui/react-collapsible": "1.1.2",
    "@radix-ui/react-context-menu": "2.2.4",
    "@radix-ui/react-dialog": "1.1.4",
    "@radix-ui/react-dropdown-menu": "2.1.4",
    "@radix-ui/react-hover-card": "1.1.4",
    "@radix-ui/react-label": "2.1.1",
    "@radix-ui/react-menubar": "1.1.4",
    "@radix-ui/react-navigation-menu": "1.2.3",
    "@radix-ui/react-popover": "1.1.4",
    "@radix-ui/react-progress": "1.1.1",
    "@radix-ui/react-radio-group": "1.2.2",
    "@radix-ui/react-scroll-area": "1.2.2",
    "@radix-ui/react-select": "2.1.4",
    "@radix-ui/react-separator": "1.1.1",
    "@radix-ui/react-slider": "1.2.2",
    "@radix-ui/react-slot": "1.1.1",
    "@radix-ui/react-switch": "1.1.2",
    "@radix-ui/react-tabs": "1.1.2",
    "@radix-ui/react-toast": "1.2.4",
    "@radix-ui/react-toggle": "1.1.1",
    "@radix-ui/react-toggle-group": "1.1.1",
    "@radix-ui/react-tooltip": "1.1.6",
    "autoprefixer": "^10.4.20",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "1.1.1",
    "date-fns": "3.6.0",
    "embla-carousel-react": "8.5.1",
    "input-otp": "1.4.1",
    "lucide-react": "^0.544.0",
    "next": "16.1.6",
    "next-themes": "^0.4.6",
    "react": "19.2.3",
    "react-day-picker": "^9.11.0",
    "react-dom": "19.2.3",
    "react-hook-form": "^7.54.1",
    "react-resizable-panels": "^2.1.7",
    "recharts": "2.15.0",
    "sonner": "^1.7.1",
    "tailwind-merge": "^2.5.5",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^1.1.2",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4.1.13",
    "@types/node": "^22",
    "@types/react": "19.2.7",
    "@types/react-dom": "19.2.3",
    "postcss": "^8.5",
    "tailwindcss": "^3.4.17",
    "typescript": "5.7.3"
  },
  "pnpm": {
    "overrides": {
      "@types/react": "19.2.7",
      "@types/react-dom": "19.2.3"
    }
  }
}

```

### backend/main.py

```python
"""
HireUp API – single FastAPI app.
Run from backend/: uvicorn main:app --reload --port 8000
"""
from __future__ import annotations

import database
from config import CORS_ORIGINS, CORS_ORIGIN_REGEX
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from routers import auth_router, users_router, companies_router

database.init_db()

app = FastAPI(title="HireUp API")

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

app.include_router(auth_router)
app.include_router(users_router)
app.include_router(companies_router)


@app.get("/")
def root():
    return {"message": "HireUp API", "docs": "/docs"}

```

### frontend/app/page.tsx

```typescript
import { Navbar } from "@/components/landing/navbar"
import { Hero } from "@/components/landing/hero"
import { Problems } from "@/components/landing/problems"
import { Solution } from "@/components/landing/solution"
import { DualCTA } from "@/components/landing/dual-cta"
import { Footer } from "@/components/landing/footer"

export default function Page() {
  return (
    <main className="relative overflow-x-hidden">
      <Navbar />
      <Hero />
      <Problems />
      <Solution />
      <DualCTA />
      <Footer />
    </main>
  )
}

```

### frontend/app/layout.tsx

```typescript
import type { Metadata, Viewport } from 'next'
import { ThemeProvider } from '@/components/theme-provider'
import { GridBackground } from '@/components/grid-background'

import './globals.css'

export const metadata: Metadata = {
  title: 'HireUp - Intelligent Job Matching',
  description:
    'Stop spam applying. Start getting matched. HireUp uses a two-tower recommendation model and Iris, an AI recruiting copilot, to connect the right talent with the right companies.',
}

export const viewport: Viewport = {
  themeColor: '#1c1b1d',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className="font-sans antialiased selection:bg-primary/20 selection:text-foreground" suppressHydrationWarning>
        <ThemeProvider
          attribute="class"
          defaultTheme="light"
          enableSystem={false}
          disableTransitionOnChange
        >
          <GridBackground />
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

```

### frontend/app/company/page.tsx

```typescript
"use client"

import { useEffect, useState } from "react"
import { DashboardShell } from "@/components/dashboard/dashboard-shell"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { FileText, MessageSquare, TrendingUp, Users } from "lucide-react"
import { getAuth } from "@/lib/api"
import { CompanyDashboardResponse, getCompanyDashboard } from "@/lib/company-api"

const statConfig = [
  { label: "Active Postings", key: "active_postings", icon: FileText },
  { label: "Total Applicants", key: "total_applicants", icon: Users },
  { label: "Iris Queries", key: "ai_agent_queries", icon: MessageSquare },
  { label: "Interview Rate", key: "interview_rate_percent", icon: TrendingUp },
] as const

export default function CompanyDashboard() {
  const [dashboard, setDashboard] = useState<CompanyDashboardResponse | null>(null)
  const [error, setError] = useState("")

  useEffect(() => {
    const auth = getAuth()
    if (!auth || auth.accountType !== "company") {
      setError("Log in as a company account to view dashboard data.")
      return
    }
    const load = () =>
      getCompanyDashboard(auth.id)
        .then((data) => setDashboard(data))
        .catch((e) => setError(e instanceof Error ? e.message : "Failed to load dashboard"))
    load()
    const id = window.setInterval(load, 5000)
    return () => window.clearInterval(id)
  }, [])

  return (
    <DashboardShell role="company">
      <div className="mb-8">
        <h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
        <p className="mt-1 text-muted-foreground">
          Live overview from company backend endpoints.
        </p>
      </div>

      {error ? <p className="mb-4 text-sm text-destructive">{error}</p> : null}

      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        {statConfig.map((stat) => {
          let value = "0"
          let change = "No recent updates"
          if (dashboard) {
            if (stat.key === "interview_rate_percent") {
              value = `${dashboard.stats.interview_rate_percent}%`
              change = "Based on feedback vs interview list"
            } else {
              value = String(dashboard.stats[stat.key])
              change = "From current company activity"
            }
          }
          return (
            <Card key={stat.label}>
              <CardHeader className="flex flex-row items-center justify-between pb-2">
                <CardTitle className="text-sm font-medium text-muted-foreground">{stat.label}</CardTitle>
                <stat.icon className="h-4 w-4 text-muted-foreground" />
              </CardHeader>
              <CardContent>
                <p className="text-2xl font-bold text-foreground">{value}</p>
                <p className="text-xs text-muted-foreground">{change}</p>
              </CardContent>
            </Card>
          )
        })}
      </div>

      <div className="mt-6">
        <Card>
          <CardHeader>
            <CardTitle>Application Workflow</CardTitle>
            <CardDescription>Live counts by stage.</CardDescription>
          </CardHeader>
          <CardContent>
            <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
              <div className="rounded border p-3 text-sm">Submitted: {dashboard?.workflow.submitted ?? 0}</div>
              <div className="rounded border p-3 text-sm">Rejected (Pre): {dashboard?.workflow.rejected_pre_interview ?? 0}</div>
              <div className="rounded border p-3 text-sm">In Progress: {dashboard?.workflow.in_progress ?? 0}</div>
              <div className="rounded border p-3 text-sm">Rejected (Post): {dashboard?.workflow.rejected_post_interview ?? 0}</div>
              <div className="rounded border p-3 text-sm">Offer: {dashboard?.workflow.offer ?? 0}</div>
            </div>
          </CardContent>
        </Card>
      </div>

      <div className="mt-8">
        <Card>
          <CardHeader>
            <CardTitle>Recent Activity</CardTitle>
            <CardDescription>Latest updates from your backend-tracked hiring pipeline.</CardDescription>
          </CardHeader>
          <CardContent>
            <div className="space-y-4">
              {(dashboard?.recent_activity ?? []).map((activity, i) => (
                <div
                  key={`${activity.time}-${i}`}
                  className="flex items-start gap-3 border-b border-border pb-4 last:border-0 last:pb-0"
                >
                  <div className="mt-1 h-2 w-2 rounded-full bg-primary" />
                  <div className="flex-1">
                    <p className="text-sm font-medium text-foreground">{activity.action}</p>
                    <p className="text-sm text-muted-foreground">{activity.detail}</p>
                  </div>
                  <span className="whitespace-nowrap text-xs text-muted-foreground">
                    {new Date(activity.time).toLocaleString()}
                  </span>
                </div>
              ))}
              {!dashboard?.recent_activity?.length ? (
                <p className="text-sm text-muted-foreground">No recent activity yet.</p>
              ) : null}
            </div>
          </CardContent>
        </Card>
      </div>
    </DashboardShell>
  )
}

```

### frontend/app/login/page.tsx

```typescript
"use client"

import Link from "next/link"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { ThemeToggle } from "@/components/theme-toggle"
import { BrandLogo } from "@/components/brand/logo"
import { login, setAuth, type AccountType } from "@/lib/api"

export default function LoginPage() {
  const router = useRouter()
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  const [accountType, setAccountType] = useState<AccountType>("user")
  const [error, setError] = useState("")
  const [loading, setLoading] = useState(false)

  return (
    <div className="relative flex min-h-screen gap-6 px-4 py-4 md:px-6 md:py-6">
      <div className="app-shell hidden w-1/2 flex-col justify-between bg-gradient-to-br from-card via-card to-primary/10 p-12 lg:flex">
        <Link href="/" className="flex items-center gap-2">
          <BrandLogo size={36} className="h-9 w-9" />
          <span className="text-lg font-bold text-foreground">HireUp</span>
        </Link>
        <div>
          <p className="eyebrow mb-5">Operator Mode</p>
          <h2 className="text-4xl font-semibold leading-tight tracking-[-0.02em] text-foreground">
            Quality over quantity.
            <br />
            <span className="bg-gradient-to-r from-primary to-destructive bg-clip-text text-transparent">Signal over noise.</span>
          </h2>
          <p className="mt-4 max-w-md text-muted-foreground leading-relaxed">
            Whether you{"'"}re hiring or looking for your next role, HireUp
            matches you with precision.
          </p>
        </div>
        <p className="text-xs text-muted-foreground">
          Built by Waterloo students.
        </p>
      </div>

      <div className="flex w-full flex-col items-center justify-center lg:w-1/2">
        <div className="absolute right-4 top-4">
          <ThemeToggle />
        </div>
        <div className="app-shell w-full max-w-md p-6 md:p-8">
          <div className="mb-8 lg:hidden">
            <Link href="/" className="flex items-center gap-2">
              <BrandLogo size={32} className="h-8 w-8" />
              <span className="text-lg font-bold text-foreground">HireUp</span>
            </Link>
          </div>

          <h1 className="text-2xl font-semibold tracking-tight text-foreground">Welcome back</h1>
          <p className="mt-1 text-sm leading-relaxed text-muted-foreground">
            Log in to your account to continue.
          </p>

          <Tabs defaultValue="applicant" className="mt-6" onValueChange={(v) => setAccountType(v === "company" ? "company" : "user")}>
            <TabsList className="w-full rounded-xl bg-secondary/60">
              <TabsTrigger value="applicant" className="flex-1">
                Applicant
              </TabsTrigger>
              <TabsTrigger value="company" className="flex-1">
                Company
              </TabsTrigger>
            </TabsList>

            <TabsContent value="applicant">
              <form
                className="mt-4 space-y-4"
                onSubmit={async (e) => {
                  e.preventDefault()
                  setError("")
                  setLoading(true)
                  try {
                    const data = await login("user", email, password)
                    setAuth(data.token, "user", data.id ?? "")
                    router.push("/dashboard")
                  } catch (err) {
                    setError(err instanceof Error ? err.message : "Login failed")
                  } finally {
                    setLoading(false)
                  }
                }}
              >
                <div className="space-y-2">
                  <Label htmlFor="applicant-email">Email</Label>
                  <Input
                    id="applicant-email"
                    type="email"
                    placeholder="you@example.com"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    required
                  />
                </div>
                <div className="space-y-2">
                  <Label htmlFor="applicant-password">Password</Label>
                  <Input
                    id="applicant-password"
                    type="password"
                    placeholder="Enter your password"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    required
                  />
                </div>
                {error && <p className="text-sm text-destructive">{error}</p>}
                <Button className="w-full rounded-full" type="submit" disabled={loading}>
                  {loading ? "Signing in…" : "Log in as Applicant"}
                </Button>
              </form>
            </TabsContent>

            <TabsContent value="company">
              <form
                className="mt-4 space-y-4"
                onSubmit={async (e) => {
                  e.preventDefault()
                  setError("")
                  setLoading(true)
                  try {
                    const data = await login("company", email, password)
                    setAuth(data.token, "company", data.id ?? "")
                    router.push("/company/postings")
                  } catch (err) {
                    setError(err instanceof Error ? err.message : "Login failed")
                  } finally {
                    setLoading(false)
                  }
                }}
              >
                <div className="space-y-2">
                  <Label htmlFor="company-email">Company Email</Label>
                  <Input
                    id="company-email"
                    type="email"
      
[truncated — 1283 more characters]
```

### frontend/app/dashboard/page.tsx

```typescript
"use client"

import { useEffect, useState } from "react"
import { DashboardShell } from "@/components/dashboard/dashboard-shell"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  MapPin,
  DollarSign,
  Briefcase,
  CheckCircle2,
} from "lucide-react"
import { getAuth, getMatchedJobs, applyJob, getUserApplications } from "@/lib/api"
import { toast } from "sonner"

interface Job {
  id: string
  company_id: string
  title: string
  description: string
  skills: string[]
  location: string
  salary_range: string
  status: string
  created_at: string
  company_name: string
  applied: boolean
  application_status?: string
}

interface UserApplication {
  id: string
  user_id: string
  job_id: string
  status: string
}

function toApplicantStatus(status?: string): string {
  const normalized = (status || "").toLowerCase()
  if (normalized === "rejected_pre_interview" || normalized === "rejected_post_interview") return "Rejected"
  if (normalized === "in_progress") return "In Progress"
  if (normalized === "offer") return "Offer"
  if (normalized === "submitted") return "Applied"
  return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : "Applied"
}

export default function ApplicantDashboard() {
  const [jobs, setJobs] = useState<Job[]>([])
  const [loading, setLoading] = useState(true)
  const [userId, setUserId] = useState("")
  const [selectedJob, setSelectedJob] = useState<Job | null>(null)
  const maxApplications = 5
  const appliedCount = jobs.filter(j => j.applied).length
  const remainingApplications = maxApplications - appliedCount

  useEffect(() => {
    const auth = getAuth()
    if (!auth || auth.accountType !== "user") {
      window.location.href = "/login"
      return
    }
    setUserId(auth.id)
    const load = async () => {
      try {
        const [matchedJobs, apps] = await Promise.all([
          getMatchedJobs(auth.id),
          getUserApplications(auth.id) as Promise<UserApplication[]>,
        ])
        const byJobId = new Map(apps.map((a) => [a.job_id, a.status]))
        const enriched = matchedJobs.map((job: Job) => ({
          ...job,
          applied: Boolean(byJobId.get(job.id) || job.applied),
          application_status: byJobId.get(job.id),
        }))
        setJobs(enriched)
      } catch {
        toast.error("Failed to load matched jobs")
      } finally {
        setLoading(false)
      }
    }
    load()
    const intervalId = window.setInterval(load, 5000)
    return () => window.clearInterval(intervalId)
  }, [])

  const handleApply = async (jobId: string) => {
    if (!userId) return
    try {
      await applyJob(userId, jobId)
      setJobs((prev) =>
        prev.map((j) =>
          j.id === jobId ? { ...j, applied: true, application_status: "submitted" } : j
        )
      )
      setSelectedJob((prev) =>
        prev && prev.id === jobId
          ? { ...prev, applied: true, application_status: "submitted" }
          : prev
      )
      toast.success("Application submitted successfully!")
    } catch (err) {
      toast.error(err instanceof Error ? err.message : "Failed to apply")
    }
  }

  if (loading) {
    return (
      <DashboardShell role="applicant">
        <p className="text-muted-foreground">Loading matched jobs...</p>
      </DashboardShell>
    )
  }

  return (
    <DashboardShell role="applicant">
      <div className="mb-8">
        <h1 className="text-2xl font-bold text-foreground">
          Your Matched Jobs
        </h1>
        <p className="mt-1 text-muted-foreground">
          These jobs were selected for you based on your skills, interests, and
          fit. You have{" "}
          <span className="font-semibold text-primary">
            {remainingApplications} application{remainingApplications !== 1 ? "s" : ""}
          </span>{" "}
          remaining today.
        </p>
      </div>

      <div className="mb-6 flex items-center gap-3">
        <div className="flex items-center gap-2 rounded-lg border border-border bg-card px-4 py-2">
          <Briefcase className="h-4 w-4 text-primary" />
          <span className="text-sm font-medium text-foreground">
            {appliedCount}/{maxApplications}
          </span>
          <span className="text-sm text-muted-foreground">applied today</span>
        </div>
      </div>

      {jobs.length === 0 ? (
        <Card>
          <CardContent className="py-8 text-center">
            <p className="text-muted-foreground">No job postings available yet. Check back soon!</p>
          </CardContent>
        </Card>
      ) : (
        <div className="space-y-4">
          {jobs.map((job) => {
            const hasApplied = job.applied
            const statusLabel = toApplicantStatus(job.application_status)
            return (
              <Card
                key={job.id}
                onClick={() => setSelectedJob(job)}
                className="cursor-pointer transition-colors hover:border-primary/20"
              >
                <CardHeader className="pb-3">
                  <div className="flex items-start justify-between">
                    <div>
                      <div className="flex items-center gap-3">
                        <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-secondary">
                          <span className="text-sm font-bold text-foreground">
                            {job.company_name?.charAt(0) ?? "C"}
                          </span>
                        </div>
                        <div>
                          <CardTitle className="text-lg">{job.title}</CardTitle>
                          <CardDescription className="text-sm">
                            {job.company_name || "C
[truncated — 4929 more characters]
```

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