A website that judges you based on your profiles.
Feed it your GitHub, LeetCode, Codeforces handle or your résumé, pick a tone, and get an LLM-generated verdict — from a gentle summary all the way to a full roast.
- What it does
- Tones
- Sources
- Architecture
- Request lifecycle
- Project structure
- Getting started
- Environment variables
- Deployment
- Roadmap
Critique is a single-page Next.js app. You choose what to be judged on (a source) and how harshly (a tone). The app fetches the raw data server-side, flattens it into a compact text summary, and hands it to a Llama 3.3 70B model on Groq with a tone-conditioned prompt. The verdict streams into a modal as Markdown with a typewriter effect, and can be copied or exported as an image.
No database. No accounts (except an optional Spotify OAuth handshake). Nothing is stored — every critique is generated fresh and lives only in React state.
The tone is injected directly into the system prompt, so the same data can produce five very different verdicts.
| Tone | What you get |
|---|---|
summarise |
A neutral, factual rundown |
evaluate |
Measured assessment with strengths and weaknesses |
guide |
Actionable advice on what to do next |
criticism |
Constructive criticism, no sugar-coating |
roast |
Comedy at your expense |
Three of these (roast, evaluate, guide) also swap the animated page background via
AnimatePresence, so the UI shifts mood along with the model.
| Source | Input | Data pulled | Auth |
|---|---|---|---|
| GitHub | Username | Profile, 15 most recently updated repos, open vs. merged PR counts, computed merge rate | GITHUB_TOKEN (server-side) |
| LeetCode | Username | Solved counts by difficulty, contest rating, global ranking — via LeetCode's GraphQL endpoint | None |
| Codeforces | Handle | user.info profile data |
None |
| Résumé | PDF upload or URL | Full text extraction via pdf2json, first 7000 chars sent to the model. Google Drive share links are auto-normalised to direct-download URLs |
None |
Two more integrations exist in the codebase but are not currently wired into the mode
selector: Spotify (top 15 artists, via NextAuth OAuth with the user-top-read scope) and
MyAnimeList (top 20 completed anime by score). The server actions and lib functions are
complete — they just need a Mode enum entry and a UI branch.
Everything runs inside Next.js. There is no separate backend — the "API layer" is a set of React Server Actions that execute on the server and are called directly from the client component.
flowchart TB
subgraph client["Browser — app/page.tsx (Client Component)"]
UI["Mode + Tone selector<br/>Input / file upload"]
MODAL["CritiqueModal<br/>Typewriter Markdown · copy · export PNG"]
end
subgraph server["Next.js Server — Server Actions"]
AG["actions/github.ts"]
AL["actions/leetcode.ts"]
AC["actions/codeforces.ts"]
AR["actions/resume.ts"]
AS["actions/spotify.ts<br/><i>(not wired)</i>"]
AA["actions/anime.ts<br/><i>(not wired)</i>"]
end
subgraph lib["lib/ — data fetchers"]
LG["github.ts"]
LL["leetcode.ts"]
LC["codeforces.ts"]
LR["resume.ts (pdf2json)"]
LS["spotify.ts"]
LA["anime.ts"]
end
LLM["lib/llm.ts<br/>Prompt builder"]
GROQ["Groq API<br/>llama-3.3-70b-versatile"]
subgraph ext["External APIs"]
E1["api.github.com"]
E2["leetcode.com/graphql"]
E3["codeforces.com/api"]
E4["PDF file or URL"]
E5["api.spotify.com"]
E6["api.myanimelist.net"]
end
UI -->|"invoke action"| AG & AL & AC & AR
AG --> LG --> E1
AL --> LL --> E2
AC --> LC --> E3
AR --> LR --> E4
AS --> LS --> E5
AA --> LA --> E6
AG & AL & AC & AR & AS & AA -->|"flattened text + tone"| LLM
LLM --> GROQ
GROQ -->|"Markdown"| MODAL
Every external call needs a secret (GITHUB_TOKEN, GROQ_API_KEY, MAL_CLIENT_ID) or would
be blocked by CORS (LeetCode's GraphQL endpoint). Marking the lib/ and actions/ files
"use server" keeps all of that off the client entirely — the browser only ever sends a
username and receives Markdown back.
sequenceDiagram
actor User
participant Page as page.tsx
participant Action as Server Action
participant API as External API
participant LLM as lib/llm.ts
participant Groq as Groq
User->>Page: Pick mode + tone, enter handle
Page->>Page: setIsLoading(true)
Page->>Action: analyzeGithub({ username, tone })
Action->>API: fetch profile + repos + PR search
API-->>Action: JSON
alt fetch failed or user not found
Action-->>Page: throw Error
Page-->>User: toast.error(...)
else success
Action->>Action: Flatten JSON into plain-text summary
Action->>LLM: generateResponse(data, tone)
LLM->>Groq: chat.completions.create(prompt)
Groq-->>LLM: Markdown verdict
LLM-->>Action: string
Action-->>Page: string
Page->>Page: handleSuccess() → open modal
Page-->>User: Typewriter render, copy / export image
end
lib/llm.ts is the single choke point for every source. It wraps whatever text the action
produced in a fixed instruction set:
- Hold the requested tone for the whole response
- No greetings, no preamble, no emojis, no em dashes
- Treat the supplied data as ground truth, and judge only the content — never the format or length
- Output Markdown, using
###headings per category with bullets underneath
Because every action funnels through this one function, adding a new source means writing a fetcher, flattening its data into a string, and nothing else.
project-critique/
├── app/
│ ├── page.tsx # Single-page UI: mode + tone state, all handlers
│ ├── layout.tsx # Fonts (Grenze Gotisch, Space Mono), Toaster, Analytics
│ ├── globals.css # Tailwind 4 theme tokens
│ ├── api/auth/[...nextauth]/ # NextAuth — Spotify provider, JWT → session accessToken
│ └── actions/ # Server actions: fetch → flatten → generateResponse
│ ├── github.ts
│ ├── leetcode.ts
│ ├── codeforces.ts
│ ├── resume.ts
│ ├── spotify.ts
│ └── anime.ts
│
├── lib/
│ ├── llm.ts # Groq client + prompt template (all tones)
│ ├── github.ts # REST: profile + repos
│ ├── leetcode.ts # GraphQL: submit stats + contest ranking
│ ├── codeforces.ts # REST: user.info
│ ├── resume.ts # pdf2json parser + Drive URL normaliser
│ ├── spotify.ts # REST: top artists
│ ├── anime.ts # REST: MAL completed list
│ └── utils.ts # cn() helper
│
├── components/
│ ├── critiqueModal.tsx # Dialog + typewriter Markdown + html-to-image export
│ ├── providers.tsx # SessionProvider + ThemeProvider
│ ├── roastBackground.tsx # Animated background — roast tone
│ ├── evaluateBackground.tsx # Animated background — evaluate tone
│ ├── guideBackground.tsx # Animated background — guide tone
│ └── ui/ # shadcn/ui primitives
│
├── types/next-auth.d.ts # Augments Session with accessToken
└── public/ # Logo, local font files
Prerequisites: Node.js 20+ and npm.
git clone https://github.com/iammdzaidalam/project-critique.git
cd project-critique
npm install
cp .env.example .env.local # then fill in the values below
npm run devOpen http://localhost:3000.
| Script | Does |
|---|---|
npm run dev |
Dev server with hot reload |
npm run build |
Production build |
npm run start |
Serve the production build |
npm run lint |
ESLint |
Create .env.local in the project root:
# Required — the model that writes every critique
GROQ_API_KEY=
# Required for the GitHub mode (raises rate limits, enables PR search)
GITHUB_TOKEN=
# Required only if you wire up the Spotify mode
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=
# Required only if you wire up the anime mode
MAL_CLIENT_ID=| Variable | Where to get it |
|---|---|
GROQ_API_KEY |
console.groq.com — free tier is plenty |
GITHUB_TOKEN |
GitHub → Settings → Developer settings → Personal access tokens. No scopes needed for public data |
SPOTIFY_CLIENT_ID / SECRET |
developer.spotify.com/dashboard. Add http://localhost:3000/api/auth/callback/spotify as a redirect URI |
NEXTAUTH_SECRET |
Generate with openssl rand -base64 32 |
MAL_CLIENT_ID |
myanimelist.net/apiconfig |
LeetCode, Codeforces and résumé parsing need no credentials.
Deployed on Vercel. Import the repo, add the environment variables above under Settings → Environment Variables, and deploy — server actions and the NextAuth route work out of the box on Vercel's Node runtime.
If you deploy elsewhere, note that pdf2json requires a Node runtime (not Edge), and
NEXTAUTH_URL must match your production origin.
- Wire Spotify and MyAnimeList into the mode selector
- Stream the LLM response token-by-token instead of typing out a completed string
- Rate limiting on the server actions
- Shareable critique permalinks
- Compare mode — two profiles, one verdict
Built by @iammdzaidalam