# Project export: Gitty.ai

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: Leetcode is Dead. Get paid to practice on real codebases. Get hired by shipping PRs.
- Devpost: https://devpost.com/software/gitty-ai
- GitHub: https://github.com/ethanjoby/gitty
- Demo: https://mygitty.vercel.app/
- Video: https://www.youtube.com/embed/RAtmoajqZpU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — ethanjoby (7 commits)

## Devpost submission (written by the team)

### Inspiration

The future of technical hiring is broken. During a conversation with Narayana Aaditya from SkillSync (YC W26), a realization hit me: as AI agents become more powerful, LeetCode-style interviews will become obsolete within a few years. Startups don't need engineers who can invert binary trees on a whiteboard; they need builders who can ship PRs fast, prompt engineer effectively, and operate with high agency. Gitty.ai was born from this insight: what if we replaced algorithmic puzzles with real GitHub work? Instead of artificial coding challenges, we let engineers prove their skills by solving actual issues from real repositories, using whatever tools they want, including AI agents. This is how modern development actually works.

### What it does

Gitty.ai is a two-sided hiring platform that turns GitHub contributions into your technical interview. For Engineers: Complete timed interview challenges (3 issues, 30 minutes) that mirror real work you'd do on the job Solve practice issues to build your developer profile Compete for paid bounties by submitting PR solutions to real repository issues Use AI agents, any IDE, or whatever tools you normally code with. We evaluate the output, not how you got there Build a profile showcasing actual merged PRs and problem-solving ability For Companies: Post bounties for real issues in your repository that need fixing Create custom role applications with timed issue challenges Review candidates through their actual code contributions, not whiteboard performance Filter by skills, PR history, and real GitHub activity Select bounty winners and hire engineers who've already proven they can contribute to your codebase Our AI-powered evaluation system analyzes PRs holistically: code quality, approach, completeness, and alignment with best practices.

### How we built it

Backend (FastAPI + Python): Single-service architecture with main.py orchestrating the evaluation workflow GitHub REST API integration for issue/PR metadata, comments, file diffs, and README context Browserbase + Playwright CDP for crawling rendered GitHub pages Claude (Anthropic) API for intelligent PR evaluation with structured JSON output Model fallback system across Claude Opus/Sonnet variants Environment-driven config via ANTHROPIC_API_KEY, GITHUB_TOKEN, BROWSERBASE_WS_ENDPOINT Render for hosting Frontend (React + TypeScript + Vite): React Router for multi-page navigation Firebase Auth + Firestore for user management and data persistence GitHub OAuth for developer authentication Dual onboarding flows: engineers (GitHub) and companies (Google) Real-time interview timer with localStorage state management Custom CSS + Tailwind for styling Vercel deployment pipeline Evaluation Pipeline: Validate issue + PR URLs from same repo Fetch all GitHub context (metadata, comments, diffs, base files) Crawl rendered pages for additional context Send structured prompt to Claude with repo README, issue description, PR changes Parse AI response into normalized score (0-100) with actionable feedback Return to frontend for display

### Challenges we ran into

Context assembly. GitHub's API spreads data across multiple endpoints. We had to stitch together issue comments, PR review comments, file patches, and base file content into coherent context without hitting rate limits. Prompt engineering for consistent scoring. Getting Claude to return reliable, normalized scores with structured feedback took multiple iterations. We needed strict JSON schemas and explicit scoring criteria to make output machine-readable. Browserbase integration. Connecting via CDP websockets and reliably extracting visible text from GitHub's dynamic UI required careful Playwright scripting and error handling for connection timeouts. Balancing real-time with cost. Running Claude Opus on every PR evaluation is expensive and slow. We implemented model fallbacks (Opus to Sonnet 3.7 to Sonnet 3.5) and optimized prompt size by intelligently truncating diffs and README excerpts. Dual user flows. Building separate onboarding, dashboards, and workflows for engineers vs. companies meant managing two parallel state machines while keeping the codebase maintainable.

### Accomplishments we're proud of

End-to-end AI evaluation pipeline that works: from URL input to scored PR feedback in under 30 seconds Real timed interview system with 3-issue challenges that mirror actual on-the-job work Bounty marketplace where companies post real issues and engineers compete with real solutions Dual-sided platform serving both engineers and companies with Firebase + GitHub OAuth integration Shipped a production-ready MVP in 36 hours with authentication, routing, API integration, and deployment Agent-friendly evaluation: we don't care if you used Cursor, Copilot, or Claude Code. We grade the PR, not the process

### What we learned

GitHub is your resume. The most signal-rich data about an engineer's ability isn't on their LinkedIn; it's in their commit history, PR descriptions, and code review discussions. We learned how to extract and structure this data programmatically. LLMs can replace human code reviewers (mostly). Claude's ability to evaluate code quality, architectural decisions, and edge case handling is remarkably effective when given proper context. The gap between AI and senior engineer PR review is narrowing fast. The interview process is ripe for disruption. Engineers universally hate LeetCode. Companies waste time on algorithmic hazing that has zero correlation with job performance. There's massive demand for a system that tests real skills. Tooling doesn't matter, output does. By embracing AI agents in our interview flow, we're ahead of the curve. The engineers who thrive at startups in 2025+ will be the ones who can ship fast using any tool available.

### What's next

Caching layer to reduce API costs and speed up repeat evaluations of similar repos Company analytics dashboard showing candidate pipeline metrics, average scores, and time-to-hire Leaderboard + reputation system where top bounty solvers get featured placement and direct recruiter outreach Async evaluation queue with Redis/Celery to handle concurrent PR reviews at scale Custom issue templates so companies can scaffold challenges that match their stack (e.g., "debug this React component" vs "optimize this SQL query") Referral hiring where engineers can vouch for teammates they've collaborated with on bounties Integration with job boards (Wellfound, YC Work at a Startup) to auto-populate role listings Subscription tiers for companies (free for bounties, paid for unlimited interview challenges and advanced candidate filtering) The long-term vision: Gitty becomes the default technical interview for every startup. When YC companies need to hire fast, they post issues on Gitty. When engineers want to break into startups, they build their profile by shipping real PRs. LeetCode fades into irrelevance, replaced by a system that actually measures what matters: can you ship?

## README (from the GitHub repository)

# Gitty

Gitty is a React + TypeScript web app for a hiring workflow where developers practice on real codebases, build a verified profile, and move through onboarding and dashboard flows.

## Tech Stack

- React 19
- TypeScript
- Vite
- React Router
- Firebase (Auth, Firestore, Analytics)
- TailwindCSS + custom CSS

## Getting Started

### Prerequisites

- Node.js 18+
- npm

### Install

```bash
npm install
```

### Run locally

```bash
npm run dev
```

Vite will print the local URL (usually `http://localhost:5173`).

## Available Scripts

- `npm run dev` - start the development server
- `npm run build` - type-check and create a production build
- `npm run lint` - run ESLint
- `npm run preview` - preview the production build locally

## Route Map

- `/` - landing page
- `/get-started` - role selection / entry
- `/signin` - user sign-in
- `/onboarding` - user onboarding
- `/dashboard` and `/dashboard/:tab` - user dashboard
- `/company/signin` - company sign-in
- `/company/onboarding` - company onboarding
- `/company/dashboard` - company dashboard

## Project Structure

```text
src/
  App.tsx                  # Landing page
  main.tsx                 # App bootstrap + routes
  SignIn.tsx               # User auth page
  UserOnboarding.tsx       # User onboarding flow
  Dashboard.tsx            # User dashboard
  CompanySignIn.tsx        # Company auth page
  CompanyOnboarding.tsx    # Company onboarding flow
  CompanyDashboard.tsx     # Company dashboard
  firebase.ts              # Firebase initialization/providers
```

## Notes

- Firebase config is currently hardcoded in `src/firebase.ts`.
- If you plan to deploy or share this project, move config to environment variables.


## Detected evidence (automated analysis)

Indexed codebase: 36 recognized source files, 686 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Firebase (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — 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
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (54 of 54)

```
.claude/skills/frontend-design/SKILL.md
.gitignore
backend/.gitignore
backend/main.py
backend/README.md
backend/requirements.txt
CLAUDE.md
eslint.config.js
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/postcss.config.cjs
frontend/README.md
frontend/src/App.css
frontend/src/App.tsx
frontend/src/CompanyDashboard.tsx
frontend/src/CompanyOnboarding.tsx
frontend/src/CompanySignIn.tsx
frontend/src/Dashboard.tsx
frontend/src/firebase.ts
frontend/src/GetStarted.tsx
frontend/src/index.css
frontend/src/main.tsx
frontend/src/SignIn.tsx
frontend/src/UserOnboarding.tsx
frontend/tailwind.config.cjs
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vercel.json
frontend/vite.config.ts
index.html
package.json
postcss.config.cjs
README.md
src/App.css
src/App.tsx
src/CompanyDashboard.tsx
src/CompanyOnboarding.tsx
src/CompanySignIn.tsx
src/Dashboard.tsx
src/firebase.ts
src/GetStarted.tsx
src/index.css
src/main.tsx
src/SignIn.tsx
src/UserOnboarding.tsx
tailwind.config.cjs
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vercel.json
vite.config.ts
```

### Dependencies

- backend/requirements.txt: anthropic@>=0.34.0,<1.0.0, fastapi@>=0.110.0,<1.0.0, httpx@>=0.27.0,<1.0.0, playwright@>=1.46.0,<2.0.0, uvicorn[standard]@>=0.29.0,<1.0.0
- frontend/package.json: @eslint/js@^9.39.1, @types/node@^24.10.1, @types/react@^19.2.7, @types/react-dom@^19.2.3, @vitejs/plugin-react@^5.1.1, autoprefixer@^10.4.21, eslint@^9.39.1, eslint-plugin-react-hooks@^7.0.1, eslint-plugin-react-refresh@^0.4.24, firebase@^10.12.5, globals@^16.5.0, postcss@^8.4.38, react@^19.2.0, react-dom@^19.2.0, react-router-dom@^6.26.2, tailwindcss@^3.4.17, typescript@~5.9.3, typescript-eslint@^8.48.0, vite@^7.3.1
- package.json: @eslint/js@^9.39.1, @types/node@^24.10.1, @types/react@^19.2.7, @types/react-dom@^19.2.3, @vitejs/plugin-react@^5.1.1, autoprefixer@^10.4.21, eslint@^9.39.1, eslint-plugin-react-hooks@^7.0.1, eslint-plugin-react-refresh@^0.4.24, firebase@^10.12.5, globals@^16.5.0, postcss@^8.4.38, react@^19.2.0, react-dom@^19.2.0, react-router-dom@^6.26.2, tailwindcss@^3.4.17, typescript@~5.9.3, typescript-eslint@^8.48.0, vite@^7.3.1

### Recent commits (newest first)

- Merge branch 'unit10-guided-dashboard-tour' into main
- Merge branch 'unit9-backend-ai-evaluation'
- Merge branch 'unit-8-enhanced-profile-skills-goals-progress' into main
- Merge branch 'unit7-open-source-education-panel' into main
- Merge branch 'unit-6-milestone-progress-tracker'
- Merge branch 'unit5-quick-start-finder'
- Merge branch 'unit4-skills-init-autosearch'
- Merge branch 'enriched-empty-states'
- Merge branch 'unit2-welcome-checklist'
- feat: add guided dashboard tour with sequential tooltips
- feat: connect backend /evaluate endpoint for real AI PR feedback
- feat: add one-click beginner issue finder to Practice tab
- feat: streamline onboarding with intent, skills steps and optional LinkedIn/resume
- feat: add skills, goals & learning progress to profile tab
- Add collapsible open source education panel to Practice tab
- Add milestone progress tracker to profile tab
- Add welcome screen & getting-started checklist for new users
- feat: enriched empty states with guidance for first-time contributors
- feat: initialize skills from localStorage and auto-search on Practice tab
- redesign: full UI overhaul

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

### CLAUDE.md

```markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

### Frontend (`frontend/`)
```bash
npm install        # install deps
npm run dev        # dev server at localhost:5173
npm run build      # type-check + production build
npm run lint       # ESLint
npm run preview    # preview production build
```

### Backend (`backend/`)
```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt && pip install python-dotenv
playwright install chromium
uvicorn main:app --reload --port 8000
```

Backend `.env`:
```
ANTHROPIC_API_KEY=required
GITHUB_TOKEN=recommended
ANTHROPIC_MODEL=claude-3-5-sonnet-20241022  # optional
BROWSERBASE_WS_ENDPOINT=wss://...           # optional
```

## Architecture

Two independent services:

**`frontend/`** — React 19 + TypeScript SPA, no backend dependency for most features. All routes are in `src/main.tsx`. Auth and data live in Firebase (Auth + Firestore); supplemental state is persisted to `localStorage` (GitHub token, resume as data URL, user settings).

Two parallel user flows:
- **Engineer**: GitHub OAuth (`GithubAuthProvider`) → onboarding → `/dashboard`
- **Company**: Google OAuth (`GoogleAuthProvider`) → onboarding → `/company/dashboard`

`Dashboard.tsx` and `CompanyDashboard.tsx` are large monolithic components (~3,500 and ~1,500 lines). Tabs within each are rendered inline via conditional logic, not separate route files.

**`backend/`** — Single-file FastAPI service (`main.py`). `POST /evaluate` fetches GitHub issue/PR context (capped: 20 comments, 10 files, 8KB excerpts), optionally crawls rendered pages via Browserbase, then sends a structured prompt to Claude and returns a scored JSON evaluation.

## Engineering Philosophy

- **Prefer minimal surface area.** Don't introduce abstractions, utilities, or helpers for things used once. Inline is fine.
- **Don't refactor what wasn't touched.** Changes should be scoped to exactly what was asked.
- **Keep code extremely modular** Thinking more to end up with less code in the end is always better.
- **Always plan out** Much better to use more compute to plan things out super well than to have to go back and fix sloppy code later


```

### .claude/skills/frontend-design/SKILL.md

```markdown
---
name: frontend-design
---

This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic output. Implement real working code with strong visual craft, clear UX decisions, and consistent execution.

Use this when the user asks for a component, page, app shell, dashboard, onboarding flow, marketing page, or any other frontend surface.

## Design Intent First

Before coding, establish a clear direction:

- **Purpose**: What job does this interface do, and for whom?
- **Tone**: Choose a specific aesthetic (minimal, editorial, playful, industrial, luxurious, etc.) and commit.
- **Constraints**: Respect framework, performance, accessibility, and responsiveness requirements.
- **Differentiator**: Define one memorable visual or interaction idea.

If requirements are vague, make reasonable assumptions and state them briefly in your response.

## Implementation Standards

Build code that is:

- Production-ready and functional
- Accessible (semantic HTML, keyboard support, contrast, labels)
- Responsive across common breakpoints
- Visually cohesive with a deliberate design system
- Cleanly structured and maintainable

Avoid placeholder-only mockups unless explicitly requested. Prefer working UI with realistic content and states.

## Visual Design Principles

Prioritize:

- **Typography**: Intentional font pairing, hierarchy, spacing, and readable line length.
- **Color System**: Cohesive palette with clear role tokens (`background`, `foreground`, `primary`, `muted`, etc.).
- **Spacing & Rhythm**: Consistent spacing scale, alignment, and section cadence.
- **Composition**: Purposeful use of asymmetry, density, and negative space.
- **Detailing**: Meaningful depth, borders, shadows, textures, and iconography aligned to the chosen tone.

Avoid repetitive, generic styles and predictable component arrangements. Each implementation should feel tailored to the product context.

## Motion & Interaction

Use motion deliberately:

- Prefer subtle transitions for state changes and hierarchy cues.
- Reserve bold animation for key moments (hero reveal, section entry, CTA emphasis).
- Keep timings consistent and avoid excessive simultaneous motion.
- Respect reduced-motion preferences.

For React projects, use the project’s existing animation approach (CSS transitions, Motion, or another established library).

## Component & Page Blueprint

When building pages, include these foundations as appropriate:

1. Navigation/header with clear primary action
2. Hero or context-setting intro
3. Core value/features section(s)
4. Proof elements (stats, testimonials, logos, case snippets)
5. Strong closing CTA and footer
6. Liberal use of Apple inspired liquid glass:
LIQUID GLASS CSS (in 
@layer
 components)
Two variants — .liquid-glass (subtle) and .liquid-glass-strong (more visible):

.liquid-glass:

background: rgba(255, 255, 255, 0.01);
background-blend-mode: luminosity;
backdrop-filter: blur(4px);
border: none;
box-shadow: inset 0 1px 1px
[truncated — 1930 more characters]
```

### package.json

```
{
  "name": "my-gitty",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "firebase": "^10.12.5",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-router-dom": "^6.26.2"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "@types/node": "^24.10.1",
    "@types/react": "^19.2.7",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^5.1.1",
    "autoprefixer": "^10.4.21",
    "eslint": "^9.39.1",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.4.24",
    "globals": "^16.5.0",
    "postcss": "^8.4.38",
    "tailwindcss": "^3.4.17",
    "typescript": "~5.9.3",
    "typescript-eslint": "^8.48.0",
    "vite": "^7.3.1"
  }
}

```

### backend/requirements.txt

```
fastapi>=0.110.0,<1.0.0
uvicorn[standard]>=0.29.0,<1.0.0
httpx>=0.27.0,<1.0.0
anthropic>=0.34.0,<1.0.0
playwright>=1.46.0,<2.0.0

```

### frontend/package.json

```
{
  "name": "my-gitty",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "firebase": "^10.12.5",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-router-dom": "^6.26.2"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "@types/node": "^24.10.1",
    "@types/react": "^19.2.7",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^5.1.1",
    "autoprefixer": "^10.4.21",
    "eslint": "^9.39.1",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.4.24",
    "globals": "^16.5.0",
    "postcss": "^8.4.38",
    "tailwindcss": "^3.4.17",
    "typescript": "~5.9.3",
    "typescript-eslint": "^8.48.0",
    "vite": "^7.3.1"
  }
}

```

### src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import './index.css'
import App from './App.tsx'
import Dashboard from './Dashboard.tsx'
import SignIn from './SignIn.tsx'
import GetStarted from './GetStarted.tsx'
import CompanySignIn from './CompanySignIn.tsx'
import CompanyDashboard from './CompanyDashboard.tsx'
import CompanyOnboarding from './CompanyOnboarding.tsx'
import UserOnboarding from './UserOnboarding.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} />
        <Route path="/get-started" element={<GetStarted />} />
        <Route path="/signin" element={<SignIn />} />
        <Route path="/onboarding" element={<UserOnboarding />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/dashboard/:tab" element={<Dashboard />} />
        <Route path="/company/signin" element={<CompanySignIn />} />
        <Route path="/company/onboarding" element={<CompanyOnboarding />} />
        <Route path="/company/dashboard" element={<CompanyDashboard />} />
      </Routes>
    </BrowserRouter>
  </StrictMode>,
)

```

### src/App.tsx

```typescript
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import logo from './gitty.png'
import './App.css'

function App() {
  const terminalScript = [
    '$ gitty init --repo stripe/stripe-node',
    'Cloning repository at commit a3f2b1c...',
    'Analyzing codebase structure...',
    'Found 3 matching issues:',
    '#1842 Add retry logic for idempotent requests',
    'Difficulty: Medium | Bounty: $150',
    '#1856 Implement webhook signature verification',
    'Difficulty: Hard | Bounty: $300',
    '#1901 Fix TypeScript types for PaymentIntent',
    'Difficulty: Easy | Bounty: $75',
    '$ gitty start 1842',
    'Starting session... Timer begins now.',
  ]
  const [typedLines, setTypedLines] = useState<string[]>([])
  const [currentLine, setCurrentLine] = useState('')
  const [isDone, setIsDone] = useState(false)
  const terminalRef = useRef<HTMLDivElement | null>(null)
  const navigate = useNavigate()

  useEffect(() => {
    const items = Array.from(
      document.querySelectorAll<HTMLElement>('[data-animate]'),
    )
    if (!('IntersectionObserver' in window)) {
      items.forEach((item) => item.classList.add('in-view'))
      return
    }

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            entry.target.classList.add('in-view')
            observer.unobserve(entry.target)
          }
        })
      },
      { threshold: 0.2 },
    )

    items.forEach((item) => observer.observe(item))

    return () => observer.disconnect()
  }, [])

  const handleGetStarted = () => navigate('/get-started')

  useEffect(() => {
    const terminal = terminalRef.current
    if (!terminal) return
    terminal.scrollTop = terminal.scrollHeight
  }, [typedLines, currentLine])

  useEffect(() => {
    let timeoutId: number | undefined
    let cancelled = false

    const startTyping = () => {
      if (cancelled) return
      setTypedLines([])
      setCurrentLine('')
      setIsDone(false)

      let lineIndex = 0
      let charIndex = 0

      const typeNext = () => {
        if (cancelled) return
        if (lineIndex >= terminalScript.length) {
          setIsDone(true)
          timeoutId = window.setTimeout(startTyping, 2000)
          return
        }

        const line = terminalScript[lineIndex]

        if (charIndex <= line.length) {
          setCurrentLine(line.slice(0, charIndex))
          charIndex += 1
          const delay = line.startsWith('$') ? 45 : 28
          timeoutId = window.setTimeout(typeNext, delay)
        } else {
          setTypedLines((prev) => [...prev, line])
          setCurrentLine('')
          charIndex = 0
          lineIndex += 1
          timeoutId = window.setTimeout(typeNext, 420)
        }
      }

      typeNext()
    }

    startTyping()

    return () => {
      cancelled = true
      if (timeoutId) {
        window.clearTimeout(timeoutId)
      }
    }
  }, [])

  return (
    <div className="page dark">
      <div className="bg">
        <div className="bg-grid bg-grid-1" />
        <div className="bg-grid bg-grid-2" />
        <div className="bg-grid bg-grid-3" />
      </div>

      <header className="nav">
        <div className="nav-inner nav-slab">
          <div className="logo">
            <span className="logo-mark logo-mark-dark">
              <img src={logo} alt="Gitty logo" />
            </span>
            Gitty
          </div>
          <nav className="nav-links">
            <a href="#how">How it works</a>
            <a href="#compare">Compare</a>
            <a href="#trust">Trust</a>
          </nav>
          <div className="nav-actions">
            <button className="btn btn-outline" onClick={handleGetStarted}>
              Sign In
            </button>
          </div>
        </div>
      </header>

      <main>
        <section className="hero hero-split hero-center" id="hero">
          <div className="hero-left" data-animate>
            <p className="eyebrow">Modern hiring, real work.</p>
            <h1>
              <span className="hero-title-lite">Your Work is Your</span>
              <span className="hero-highlight-wrap">
                <span className="hero-highlight">Interview</span>
                <span className="cursor cursor-hero" aria-hidden="true" />
              </span>
            </h1>
            <p className="hero-subtitle">
              Get paid to practice on real codebases. Get hired by shipping
              real PRs and building a verified profile.
            </p>
            <div className="hero-actions">
              <button className="btn btn-primary" onClick={handleGetStarted}>
                Get Started
              </button>
            </div>
          </div>

          <div className="hero-right" data-animate>
            <div className="code-panel">
              <div className="terminal-header terminal-header-dark">
                <div className="terminal-dots">
                  <span />
                  <span />
                  <span />
                </div>
                <span>gitty</span>
                <span className="terminal-logo">
                  <img src={logo} alt="Gitty logo" />
                </span>
              </div>
              <div className="terminal-body terminal-body-dark" ref={terminalRef}>
                {typedLines.map((line, index) => (
                  <p
                    key={`${line}-${index}`}
                    className={
                      line.startsWith('$')
                        ? 'prompt'
                        : line.startsWith('Found 3') || line.startsWith('Starting')
                          ? 'success'
                          : line.startsWith('Cloning') ||
                              line.startsWith('Analyzing') ||
                              line.startsWith('Difficulty')
                            ? 'muted'
                            : line.startsWith('#')
[truncated — 807 more characters]
```

### backend/main.py

```python
import base64
import json
import os
import re
from dataclasses import dataclass
from typing import Any

import httpx
from anthropic import AsyncAnthropic
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, HttpUrl
from playwright.async_api import async_playwright
from dotenv import load_dotenv

load_dotenv()

app = FastAPI(title="PR Quality Evaluator", version="1.0.0")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://localhost:3000"],
    allow_methods=["*"],
    allow_headers=["*"],
)

GITHUB_API = "https://api.github.com"


class EvaluateRequest(BaseModel):
    issue_url: HttpUrl = Field(..., description="GitHub issue URL")
    pr_url: HttpUrl = Field(..., description="GitHub pull request URL")


class EvaluateResponse(BaseModel):
    score: int
    summary: str
    strengths: list[str]
    issues: list[str]
    actionable_feedback: list[str]
    confidence: str
    model: str
    raw_model_response: dict[str, Any] | None = None


@dataclass
class GitHubRef:
    owner: str
    repo: str
    number: int


ISSUE_URL_RE = re.compile(r"^https://github\.com/([^/]+)/([^/]+)/issues/(\d+)$")
PR_URL_RE = re.compile(r"^https://github\.com/([^/]+)/([^/]+)/pull/(\d+)$")


def parse_issue_url(url: str) -> GitHubRef:
    match = ISSUE_URL_RE.match(url.rstrip("/"))
    if not match:
        raise HTTPException(status_code=400, detail="Invalid GitHub issue URL format")
    owner, repo, number = match.groups()
    return GitHubRef(owner=owner, repo=repo, number=int(number))


def parse_pr_url(url: str) -> GitHubRef:
    match = PR_URL_RE.match(url.rstrip("/"))
    if not match:
        raise HTTPException(status_code=400, detail="Invalid GitHub PR URL format")
    owner, repo, number = match.groups()
    return GitHubRef(owner=owner, repo=repo, number=int(number))


def github_headers() -> dict[str, str]:
    headers = {
        "Accept": "application/vnd.github+json",
        "User-Agent": "pr-quality-evaluator",
    }
    token = os.getenv("GITHUB_TOKEN")
    if token:
        headers["Authorization"] = f"Bearer {token}"
    return headers


async def fetch_json(client: httpx.AsyncClient, url: str) -> dict[str, Any] | list[Any]:
    resp = await client.get(url)
    if resp.status_code >= 400:
        raise HTTPException(status_code=502, detail=f"GitHub API error: {resp.status_code} {resp.text}")
    return resp.json()


async def fetch_file_content(
    client: httpx.AsyncClient,
    owner: str,
    repo: str,
    path: str,
    ref: str,
) -> str:
    # Use contents API to fetch representative source context for changed files.
    url = f"{GITHUB_API}/repos/{owner}/{repo}/contents/{path}?ref={ref}"
    resp = await client.get(url)
    if resp.status_code >= 400:
        return ""
    data = resp.json()
    if data.get("encoding") != "base64" or "content" not in data:
        return ""
    try:
        decoded = base64.b64decode(data["content"]).decode("utf-8", errors="replace")
        return decoded[:8000]
    except Exception:
        return ""


async def crawl_with_browserbase(url: str) -> str:
    """
    Crawl rendered page text through Browserbase.

    Required env vars:
      - BROWSERBASE_WS_ENDPOINT: websocket CDP endpoint for Browserbase session
        Example: wss://connect.browserbase.com?apiKey=...&projectId=...
    """
    ws_endpoint = os.getenv("BROWSERBASE_WS_ENDPOINT")
    if not ws_endpoint:
        return ""

    try:
        async with async_playwright() as p:
            browser = await p.chromium.connect_over_cdp(ws_endpoint)
            context = browser.contexts[0] if browser.contexts else await browser.new_context()
            page = await context.new_page()
            await page.goto(url, wait_until="networkidle", timeout=60000)
            text = await page.locator("main").inner_text(timeout=5000)
            await page.close()
            await browser.close()
            return text[:15000]
    except Exception:
        return ""


async def gather_context(issue: GitHubRef, pr: GitHubRef, issue_url: str, pr_url: str) -> dict[str, Any]:
    if (issue.owner, issue.repo) != (pr.owner, pr.repo):
        raise HTTPException(status_code=400, detail="Issue and PR must belong to the same repository")

    async with httpx.AsyncClient(headers=github_headers(), timeout=30) as client:
        issue_data = await fetch_json(client, f"{GITHUB_API}/repos/{issue.owner}/{issue.repo}/issues/{issue.number}")
        issue_comments = await fetch_json(
            client,
            f"{GITHUB_API}/repos/{issue.owner}/{issue.repo}/issues/{issue.number}/comments?per_page=20",
        )

        pr_data = await fetch_json(client, f"{GITHUB_API}/repos/{pr.owner}/{pr.repo}/pulls/{pr.number}")
        pr_comments = await fetch_json(
            client,
            f"{GITHUB_API}/repos/{pr.owner}/{pr.repo}/issues/{pr.number}/comments?per_page=20",
        )
        pr_review_comments = await fetch_json(
            client,
            f"{GITHUB_API}/repos/{pr.owner}/{pr.repo}/pulls/{pr.number}/comments?per_page=20",
        )
        pr_files = await fetch_json(
            client,
            f"{GITHUB_API}/repos/{pr.owner}/{pr.repo}/pulls/{pr.number}/files?per_page=30",
        )

        base_ref = pr_data.get("base", {}).get("sha") or pr_data.get("base", {}).get("ref", "main")
        changed_file_contexts = []
        for f in pr_files[:10]:
            file_path = f.get("filename")
            if not file_path:
                continue
            content = await fetch_file_content(client, pr.owner, pr.repo, file_path, base_ref)
            changed_file_contexts.append(
                {
                    "file": file_path,
                    "status": f.get("status"),
                    "additions": f.get("additions"),
                    "deletions": f.get("deletions"),
                    "patch": (f.get("patch") or "")[:4000],
                    "base_fil
[truncated — 6424 more characters]
```

### frontend/src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import './index.css'
import App from './App.tsx'
import Dashboard from './Dashboard.tsx'
import SignIn from './SignIn.tsx'
import GetStarted from './GetStarted.tsx'
import CompanySignIn from './CompanySignIn.tsx'
import CompanyDashboard from './CompanyDashboard.tsx'
import CompanyOnboarding from './CompanyOnboarding.tsx'
import UserOnboarding from './UserOnboarding.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} />
        <Route path="/get-started" element={<GetStarted />} />
        <Route path="/signin" element={<SignIn />} />
        <Route path="/onboarding" element={<UserOnboarding />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/dashboard/:tab" element={<Dashboard />} />
        <Route path="/company/signin" element={<CompanySignIn />} />
        <Route path="/company/onboarding" element={<CompanyOnboarding />} />
        <Route path="/company/dashboard" element={<CompanyDashboard />} />
      </Routes>
    </BrowserRouter>
  </StrictMode>,
)

```

### frontend/src/App.tsx

```typescript
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import logo from './gitty.png'
import './App.css'

const terminalScript = [
  '$ gitty init --repo stripe/stripe-node',
  'Cloning repository at commit a3f2b1c...',
  'Analyzing codebase structure...',
  'Found 3 matching issues:',
  '#1842 Add retry logic for idempotent requests',
  'Difficulty: Medium | Bounty: $150',
  '#1856 Implement webhook signature verification',
  'Difficulty: Hard | Bounty: $300',
  '#1901 Fix TypeScript types for PaymentIntent',
  'Difficulty: Easy | Bounty: $75',
  '$ gitty start 1842',
  'Starting session... Timer begins now.',
]

function termLineClass(line: string): string {
  if (line.startsWith('$')) return 'term-prompt'
  if (line.startsWith('Found 3') || line.startsWith('Starting')) return 'term-success'
  if (line.startsWith('#')) return 'term-issue'
  return 'term-muted'
}

function App() {
  const [typedLines, setTypedLines] = useState<string[]>([])
  const [currentLine, setCurrentLine] = useState('')
  const [isDone, setIsDone] = useState(false)
  const terminalRef = useRef<HTMLDivElement | null>(null)
  const navigate = useNavigate()

  useEffect(() => {
    const items = Array.from(
      document.querySelectorAll<HTMLElement>('[data-animate]'),
    )
    if (!('IntersectionObserver' in window)) {
      items.forEach((item) => item.classList.add('in-view'))
      return
    }

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            entry.target.classList.add('in-view')
            observer.unobserve(entry.target)
          }
        })
      },
      { threshold: 0.2 },
    )

    items.forEach((item) => observer.observe(item))

    return () => observer.disconnect()
  }, [])

  const handleGetStarted = () => navigate('/get-started')

  useEffect(() => {
    const terminal = terminalRef.current
    if (!terminal) return
    terminal.scrollTop = terminal.scrollHeight
  }, [typedLines, currentLine])

  useEffect(() => {
    let timeoutId: number | undefined
    let cancelled = false

    const startTyping = () => {
      if (cancelled) return
      setTypedLines([])
      setCurrentLine('')
      setIsDone(false)

      let lineIndex = 0
      let charIndex = 0

      const typeNext = () => {
        if (cancelled) return
        if (lineIndex >= terminalScript.length) {
          setIsDone(true)
          timeoutId = window.setTimeout(startTyping, 2000)
          return
        }

        const line = terminalScript[lineIndex]

        if (charIndex <= line.length) {
          setCurrentLine(line.slice(0, charIndex))
          charIndex += 1
          const delay = line.startsWith('$') ? 45 : 28
          timeoutId = window.setTimeout(typeNext, delay)
        } else {
          setTypedLines((prev) => [...prev, line])
          setCurrentLine('')
          charIndex = 0
          lineIndex += 1
          timeoutId = window.setTimeout(typeNext, 420)
        }
      }

      typeNext()
    }

    startTyping()

    return () => {
      cancelled = true
      if (timeoutId) {
        window.clearTimeout(timeoutId)
      }
    }
  }, [])

  return (
    <div className="page">
      <div className="bg" aria-hidden="true">
        <div className="bg-grid" />
        <div className="bg-vignette" />
      </div>

      <header className="nav" role="banner">
        <div className="nav-inner">
          <a className="nav-logo" href="/" aria-label="Gitty home">
            <span className="nav-logo-mark">
              <img src={logo} alt="" />
            </span>
            Gitty
          </a>
          <nav className="nav-links" aria-label="Primary navigation">
            <a href="#how">How it works</a>
            <a href="#compare">Compare</a>
            <a href="#trust">Trust</a>
          </nav>
          <div className="nav-actions">
            <button className="btn-cta" onClick={handleGetStarted}>
              Sign In
            </button>
          </div>
        </div>
      </header>

      <main>
        <section className="hero" id="hero" aria-labelledby="hero-headline">
          <div className="hero-content" data-animate>
            <p className="eyebrow">Modern hiring, real work.</p>
            <h1 id="hero-headline" className="hero-headline">
              <span className="hero-title-lite">Your Work is Your</span>
              <br />
              <span className="hero-highlight-wrap">
                <span className="hero-highlight">Interview</span>
                <span className="cursor cursor-hero" aria-hidden="true" />
              </span>
            </h1>
            <p className="hero-subtitle">
              Get paid to practice on real codebases. Get hired by shipping
              real PRs and building a verified profile.
            </p>
            <div className="hero-actions">
              <button className="btn-cta" onClick={handleGetStarted}>
                Get Started
              </button>
              <button className="btn-ghost" onClick={handleGetStarted}>
                See how it works
              </button>
            </div>
          </div>

          <div className="hero-terminal" data-animate>
            <div className="terminal-card-wrap">
              <div className="terminal-titlebar">
                <div className="terminal-dots">
                  <span className="dot-red" />
                  <span className="dot-yellow" />
                  <span className="dot-green" />
                </div>
                <span className="terminal-label">gitty</span>
                <span className="terminal-logo-mark">
                  <img src={logo} alt="" />
                </span>
              </div>
              <div className="terminal-body" ref={terminalRef}>
                {typedLines.map((line, index) => (
                  <p key={`${line}-${index}`} className={termLineClass(line)}>
      
[truncated — 2952 more characters]
```

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