Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# CV Builder

**Paste a job description + your CV. Get a scored evaluation, specific rewrites, and a tailored PDF.**
**Paste your CV and an optional job description. Get a 0–5 evaluation across six dimensions, with a prioritised list of issues and fixes.**

An open source, privacy-first CV builder for tech professionals. Built by the [Tech Immigrants](https://youtube.com/c/TechImmigrants) community.
This is the **first community MVP**: it scores an existing resume, it does not generate, tailor, or rewrite one. An open source, privacy-first CV evaluator for tech professionals. Built by the [Tech Immigrants](https://youtube.com/c/TechImmigrants) community.

[![CI](https://github.com/TechImmigrants/cv-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/TechImmigrants/cv-builder/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Expand Down Expand Up @@ -75,9 +75,30 @@ Clone the repo, open [Claude Code](https://claude.com/claude-code) at the root,

See [apps/cli/README.md](apps/cli/README.md) for the full guide.

### Web UI (coming soon)
### Web UI (MVP — local only)

A browser-based interface where you paste your CV and JD, get instant feedback, and export a tailored PDF. No sign-up required, no data leaves your browser.
```bash
pnpm dev # boots at http://localhost:3000
```

A browser-based interface where you paste your CV and JD, get instant
feedback, and read the full dimension breakdown. No sign-up, no data leaves
your browser. PDF upload is not supported yet — use `.txt` or `.md`.

Full guide: [docs/LOCAL_DEMO.md](docs/LOCAL_DEMO.md).

---

## Privacy

- The web UI runs entirely in your browser. Nothing is sent to a server.
- The CLI reads files locally and prints to stdout. Nothing is uploaded.
- There is no telemetry, no analytics, no cookies.
- Results in the web UI are stored in your browser's `localStorage` only.

**Do not paste your real CV into GitHub issues.** Issues are public. Before
sharing an example, anonymize it first — see
[docs/FEEDBACK_GUIDE.md](docs/FEEDBACK_GUIDE.md).

---

Expand Down Expand Up @@ -158,13 +179,17 @@ Currently built-in:

## Roadmap

### v0.1 (Current Sprint)
### v0.1 (Current Sprint — MVP)
- [x] Core evaluation engine with 6 dimensions
- [x] 8 role archetypes
- [x] Universal anti-pattern detection
- [x] CLI: basic evaluate command
- [x] Web UI: paste and score screen (local demo)
- [x] Eval harness with golden fixtures
- [x] Localization-ready code structure (no strings in wrong place)
- [x] Privacy model: no telemetry, no upload
- [ ] Unit tests for scoring logic
- [ ] Web UI: paste and score screen
- [ ] PDF text extraction

### v0.2
- [ ] LLM-enhanced mode (rewrite suggestions)
Expand Down
4 changes: 2 additions & 2 deletions apps/web-ui/src/app/components/EvaluateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function EvaluateForm() {
/>

<TextStats value={cv} />
<FileUpload onContentLoaded={(content) => setCv(content)} />
<FileUpload inputId="cv-upload" onContentLoaded={(content) => setCv(content)} />
</div>

<div>
Expand All @@ -78,7 +78,7 @@ export function EvaluateForm() {
/>

<TextStats value={jd} />
<FileUpload onContentLoaded={(content) => setJd(content)} />
<FileUpload inputId="jd-upload" onContentLoaded={(content) => setJd(content)} />
</div>
</div>

Expand Down
29 changes: 20 additions & 9 deletions apps/web-ui/src/app/components/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,38 @@
import { type DragEvent, useRef, useState } from "react";

interface FileUploadProps {
inputId: string;
onContentLoaded: (content: string) => void;
}

export function FileUpload({ onContentLoaded }: FileUploadProps) {
const [fileName, setFileName] = useState("");
export function FileUpload({ inputId, onContentLoaded }: FileUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [status, setStatus] = useState("");

async function processFile(file: File) {
setStatus("");
const extension = file.name.split(".").pop()?.toLowerCase() || "";

if (extension === "txt" || extension === "md") {
try {
const text = await file.text();
onContentLoaded(text);
setFileName(file.name);
setStatus(`Loaded ${file.name}`);
} catch (error) {
console.error("Failed to read file:", error);
alert("Failed to read file. Please try again.");
setFileName("");
setStatus("Failed to read file. Please try again.");
}
return;
}

if (extension === "pdf") {
alert("PDF text extraction is not yet supported. Please use .txt or .md files.");
setStatus(
"PDF text extraction isn't supported yet. Please paste the content or upload a .txt or .md file."
);
return;
}

alert("Unsupported file type.");
setStatus("Unsupported file type. Please use .txt or .md.");
}

async function handleFiles(files: FileList | null) {
Expand Down Expand Up @@ -65,13 +68,21 @@ export function FileUpload({ onContentLoaded }: FileUploadProps) {

<p className="mt-2 text-sm text-zinc-500">Supports .txt and .md</p>

{fileName && <p className="mt-2 text-xs text-green-600">Loaded: {fileName}</p>}
<p
id={`${inputId}-message`}
role="status"
aria-live="polite"
className="mt-2 min-h-[1rem] text-xs text-amber-600"
>
{status}
</p>
</button>

<input
ref={inputRef}
id={inputId}
type="file"
accept=".txt,.md,.pdf"
accept=".txt,.md"
className="hidden"
onChange={(e) => handleFiles(e.target.files)}
/>
Expand Down
147 changes: 147 additions & 0 deletions apps/web-ui/src/app/feedback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import Link from "next/link";
import { Header } from "../components/layout/Header";

export default function FeedbackPage() {
return (
<div className="flex min-h-screen flex-col bg-zinc-50 dark:bg-zinc-950">
<Header />

<main className="mx-auto w-full max-w-3xl flex-1 px-6 py-16">
<p className="text-sm font-medium uppercase tracking-[0.2em] text-zinc-500">
Feedback Guide
</p>

<h1 className="mt-4 text-4xl font-semibold tracking-tight sm:text-5xl">
Tell us what worked and what didn't.
</h1>

<p className="mt-6 text-lg leading-8 text-zinc-700 dark:text-zinc-300">
This MVP is the first cut. Real-world feedback is the only thing that gets us to
a useful tool. Skim this page before you open an issue so your feedback lands in
the right place.
</p>

<section className="mt-12 space-y-10">
<article>
<h2 className="text-xl font-semibold">Where to send feedback</h2>

<ul className="mt-3 list-disc space-y-2 pl-6 text-zinc-700 dark:text-zinc-300">
<li>
Open a{" "}
<a
className="underline underline-offset-4"
href="https://github.com/TechImmigrants/cv-builder/issues/new"
rel="noreferrer"
target="_blank"
>
new GitHub issue
</a>{" "}
with the <code>feedback</code> label.
</li>
<li>
Use the{" "}
<code className="rounded bg-zinc-200 px-1.5 py-0.5 text-xs dark:bg-zinc-800">
feedback
</code>{" "}
channel in the Tech Immigrants community.
</li>
<li>
If something is broken, use a{" "}
<a
className="underline underline-offset-4"
href="https://github.com/TechImmigrants/cv-builder/issues/new/choose"
rel="noreferrer"
target="_blank"
>
bug report
</a>{" "}
instead.
</li>
</ul>
</article>

<article>
<h2 className="text-xl font-semibold">What to include</h2>

<ul className="mt-3 list-disc space-y-2 pl-6 text-zinc-700 dark:text-zinc-300">
<li>What you were testing (web UI, CLI, eval harness).</li>
<li>What you expected vs. what happened.</li>
<li>The role or archetype you were aiming for.</li>
<li>Your browser / OS / Node version (for web UI / CLI bugs).</li>
</ul>
</article>

<article className="rounded-2xl border border-amber-300 bg-amber-50 p-6 dark:border-amber-700 dark:bg-amber-950/30">
<h2 className="text-lg font-semibold text-amber-900 dark:text-amber-200">
⚠ Privacy: don't paste your real CV
</h2>

<p className="mt-3 text-sm leading-7 text-amber-900 dark:text-amber-200">
GitHub issues are public. Before sharing an example,{" "}
<strong>anonymize it</strong>:
</p>

<ul className="mt-2 list-disc space-y-1 pl-6 text-sm text-amber-900 dark:text-amber-200">
<li>Replace your name with "Candidate A".</li>
<li>Replace company names with "Company X".</li>
<li>Remove phone, address, and email.</li>
<li>Keep the bullet structure and metrics — that's what we need to see.</li>
</ul>

<p className="mt-3 text-sm leading-7 text-amber-900 dark:text-amber-200">
If you accidentally paste a real CV, edit or delete the comment immediately.
We can't un-share what you post on GitHub.
</p>
</article>

<article>
<h2 className="text-xl font-semibold">Sample anonymized feedback</h2>

<pre className="mt-3 overflow-x-auto rounded-xl bg-zinc-900 p-4 text-xs leading-6 text-zinc-100">
{`Browser: Chrome 124 on macOS 14
What I tried: A 1-page SWE CV against a Python backend JD
Result: Score 2.1/5
Surprise: Keyword Match scored 5/5 but Tooling Visibility scored 1/5,
even though I named every tool in the CV. The two should agree.
Anonymized snippet:
- "Built Candidate A service in Company X serving 50M requests/day"
- "Migrated Postgres to a sharded cluster, scaling writes 5x"
Suggestion: weight Tooling Visibility by exact JD keywords, not just
archetype keywords.`}
</pre>
</article>

<article>
<h2 className="text-xl font-semibold">Five questions that help</h2>

<ol className="mt-3 list-decimal space-y-2 pl-6 text-zinc-700 dark:text-zinc-300">
<li>Was the feedback useful?</li>
<li>Was the score understandable?</li>
<li>What was missing from the result?</li>
<li>Would you trust this for your own CV today?</li>
<li>What role or archetype should we support next?</li>
</ol>
</article>
</section>

<div className="mt-16 flex flex-wrap gap-3">
<Link
href="/"
className="rounded-xl bg-black px-6 py-3 text-white hover:bg-zinc-800"
>
← Back to evaluator
</Link>

<a
href="https://github.com/TechImmigrants/cv-builder/issues/new"
className="rounded-xl border border-zinc-300 px-6 py-3 hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
rel="noreferrer"
target="_blank"
>
Open a feedback issue ↗
</a>
</div>
</main>
</div>
);
}
Loading
Loading