Add CV and JD input screen with file upload and client-side evaluation#76
Conversation
📝 WalkthroughWalkthroughThis PR implements the complete evaluation input and results flow for the web UI. It adds reusable display components (text stats, file upload, score card), a localStorage persistence layer, a form that orchestrates CV and JD input and triggers the core evaluation function, and a results page that displays the evaluation output. ChangesEvaluation Flow UI
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web-ui/src/app/components/FileUpload.tsx`:
- Around line 31-35: The code explicitly rejects PDFs by checking extension ===
"pdf" (and similar checks at the other locations) despite the input's accept
including ".pdf" and the requirement to support client-side PDF uploads; remove
the hard reject and instead add client-side PDF text extraction in the file
processing flow (the routine that reads uploaded files—look for the handler that
inspects extension, e.g., the extension variable and the file-read/handleUpload
or processFile function). Use a PDF parsing library (pdfjs-dist/getDocument) in
that handler to extract text from PDF pages, set the extracted text into the
same state/path used for .txt/.md, and ensure the input accept attribute remains
consistent (update the earlier checks around extension and any alert calls so
PDFs are allowed and processed rather than blocked).
- Around line 57-61: The clickable upload area is a plain <div> (in FileUpload
component) and lacks keyboard semantics; add keyboard accessibility by giving
that element a role="button", tabIndex={0}, an appropriate aria-label (e.g.,
"Upload files"), and an onKeyDown handler that calls inputRef.current?.click()
when Enter or Space is pressed (prevent default for Space). Keep the existing
onClick, onDragOver, and onDrop handlers (handleDrop) and ensure inputRef is the
same file input used to open the file picker.
In `@apps/web-ui/src/app/lib/evaluation-storage.ts`:
- Around line 35-40: The JSON.parse(raw) return path should validate the parsed
value before returning to the UI: after parsing the raw string (the
JSON.parse(raw) call), check that the result is the expected collection (use
Array.isArray(parsed)) and that each item is the expected object shape (at
minimum typeof item === "object" && item !== null); if validation fails, return
null and optionally clear the stored key (e.g., removeItem for the storage key)
to avoid future parse/shape errors. Update the function that contains
JSON.parse(raw) to perform these checks and only return the parsed data when it
passes validation.
🪄 Autofix (Beta)
❌ Autofix failed (check again to retry)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5dfb18b5-22c6-483c-a865-e551289527a6
📒 Files selected for processing (7)
apps/web-ui/src/app/components/EvaluateForm.tsxapps/web-ui/src/app/components/FileUpload.tsxapps/web-ui/src/app/components/ScoreCard.tsxapps/web-ui/src/app/components/TextStats.tsxapps/web-ui/src/app/lib/evaluation-storage.tsapps/web-ui/src/app/page.tsxapps/web-ui/src/app/results/page.tsx
| if (extension === "pdf") { | ||
| alert( | ||
| "PDF text extraction is not yet supported. Please use .txt or .md files." | ||
| ); | ||
| return; |
There was a problem hiding this comment.
PDF path is explicitly blocked despite being a required supported format.
Line 31–35 rejects .pdf, while Line 81 advertises .pdf in accept and the PR objective requires client-side .pdf upload support. This leaves a core requirement unimplemented and creates inconsistent UX.
Suggested direction
- if (extension === "pdf") {
- alert(
- "PDF text extraction is not yet supported. Please use .txt or .md files."
- );
- return;
- }
+ if (extension === "pdf") {
+ // Extract text client-side and pass to onContentLoaded(...)
+ // If extraction fails, show a clear error and do not set fileName.
+ }
...
- <p className="mt-2 text-sm text-zinc-500">
- Supports .txt and .md
- </p>
+ <p className="mt-2 text-sm text-zinc-500">
+ Supports .txt, .md, and .pdf
+ </p>Also applies to: 67-69, 81-81
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web-ui/src/app/components/FileUpload.tsx` around lines 31 - 35, The code
explicitly rejects PDFs by checking extension === "pdf" (and similar checks at
the other locations) despite the input's accept including ".pdf" and the
requirement to support client-side PDF uploads; remove the hard reject and
instead add client-side PDF text extraction in the file processing flow (the
routine that reads uploaded files—look for the handler that inspects extension,
e.g., the extension variable and the file-read/handleUpload or processFile
function). Use a PDF parsing library (pdfjs-dist/getDocument) in that handler to
extract text from PDF pages, set the extracted text into the same state/path
used for .txt/.md, and ensure the input accept attribute remains consistent
(update the earlier checks around extension and any alert calls so PDFs are
allowed and processed rather than blocked).
| <div | ||
| onDragOver={(e) => e.preventDefault()} | ||
| onDrop={handleDrop} | ||
| onClick={() => inputRef.current?.click()} | ||
| className="mt-4 cursor-pointer rounded-xl border-2 border-dashed border-zinc-300 p-6 text-center transition hover:border-zinc-500" |
There was a problem hiding this comment.
Upload trigger is mouse-only; keyboard users can’t activate it.
Line 57–61 uses a clickable <div> without keyboard interaction semantics. This blocks task completion for keyboard-only users.
Suggested fix
- <div
+ <div
+ role="button"
+ tabIndex={0}
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
onClick={() => inputRef.current?.click()}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ inputRef.current?.click();
+ }
+ }}
className="mt-4 cursor-pointer rounded-xl border-2 border-dashed border-zinc-300 p-6 text-center transition hover:border-zinc-500"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| onDragOver={(e) => e.preventDefault()} | |
| onDrop={handleDrop} | |
| onClick={() => inputRef.current?.click()} | |
| className="mt-4 cursor-pointer rounded-xl border-2 border-dashed border-zinc-300 p-6 text-center transition hover:border-zinc-500" | |
| <div | |
| role="button" | |
| tabIndex={0} | |
| onDragOver={(e) => e.preventDefault()} | |
| onDrop={handleDrop} | |
| onClick={() => inputRef.current?.click()} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter" || e.key === " ") { | |
| e.preventDefault(); | |
| inputRef.current?.click(); | |
| } | |
| }} | |
| className="mt-4 cursor-pointer rounded-xl border-2 border-dashed border-zinc-300 p-6 text-center transition hover:border-zinc-500" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web-ui/src/app/components/FileUpload.tsx` around lines 57 - 61, The
clickable upload area is a plain <div> (in FileUpload component) and lacks
keyboard semantics; add keyboard accessibility by giving that element a
role="button", tabIndex={0}, an appropriate aria-label (e.g., "Upload files"),
and an onKeyDown handler that calls inputRef.current?.click() when Enter or
Space is pressed (prevent default for Space). Keep the existing onClick,
onDragOver, and onDrop handlers (handleDrop) and ensure inputRef is the same
file input used to open the file picker.
| try { | ||
| return JSON.parse(raw); | ||
| } catch (error) { | ||
| console.error("Failed to parse evaluation result:", error); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Parsed storage data is not validated before returning to UI.
Line 35–40 returns untyped parsed JSON directly. A stale/corrupted payload can bypass this and later crash results rendering when .map is called on non-arrays. Validate shape here and return null (optionally clear the key) if invalid.
Suggested fix
+function isValidEvaluationResult(data: unknown): data is {
+ score: number;
+ strengths: string[];
+ dimensions: Array<{ name: string; score: number; maxScore: number }>;
+} {
+ if (!data || typeof data !== "object") return false;
+ const d = data as any;
+ return (
+ typeof d.score === "number" &&
+ Array.isArray(d.strengths) &&
+ Array.isArray(d.dimensions)
+ );
+}
...
try {
- return JSON.parse(raw);
+ const parsed = JSON.parse(raw);
+ if (!isValidEvaluationResult(parsed)) {
+ localStorage.removeItem(EVALUATION_RESULT_KEY);
+ return null;
+ }
+ return parsed;
} catch (error) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| return JSON.parse(raw); | |
| } catch (error) { | |
| console.error("Failed to parse evaluation result:", error); | |
| return null; | |
| } | |
| function isValidEvaluationResult(data: unknown): data is { | |
| score: number; | |
| strengths: string[]; | |
| dimensions: Array<{ name: string; score: number; maxScore: number }>; | |
| } { | |
| if (!data || typeof data !== "object") return false; | |
| const d = data as any; | |
| return ( | |
| typeof d.score === "number" && | |
| Array.isArray(d.strengths) && | |
| Array.isArray(d.dimensions) | |
| ); | |
| } | |
| try { | |
| const parsed = JSON.parse(raw); | |
| if (!isValidEvaluationResult(parsed)) { | |
| localStorage.removeItem(EVALUATION_RESULT_KEY); | |
| return null; | |
| } | |
| return parsed; | |
| } catch (error) { | |
| console.error("Failed to parse evaluation result:", error); | |
| return null; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web-ui/src/app/lib/evaluation-storage.ts` around lines 35 - 40, The
JSON.parse(raw) return path should validate the parsed value before returning to
the UI: after parsing the raw string (the JSON.parse(raw) call), check that the
result is the expected collection (use Array.isArray(parsed)) and that each item
is the expected object shape (at minimum typeof item === "object" && item !==
null); if validation fails, return null and optionally clear the stored key
(e.g., removeItem for the storage key) to avoid future parse/shape errors.
Update the function that contains JSON.parse(raw) to perform these checks and
only return the parsed data when it passes validation.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. An unexpected error occurred while generating fixes: Not Found - https://docs.github.com/rest/git/refs#get-a-reference |
rfatideh
left a comment
There was a problem hiding this comment.
Overl very nice PR. Hopefully soon we can enbale pdf files. Just had a minor comment about ts types
| "evaluation-result"; | ||
|
|
||
| export function saveEvaluationResult( | ||
| data: unknown |
There was a problem hiding this comment.
I think we can use a proper type here. and validate it as coderabbit suggested
Maybe we can use the same type defined by the backend (from the evaluate function of "@cv-builder/core")
| import { ScoreCard } from "../components/ScoreCard"; | ||
|
|
||
| export default function ResultsPage() { | ||
| const [result, setResult] = useState<any>(null); |
There was a problem hiding this comment.
We should not use any/unknown. This one should be fixed by adding the type to the localStorage data. It could be nice to add it to the biome rules
|
Thanks for the CV + JD input screen! This is the MVP web UI and is now merged into the integration branch. During integration I made a few small fixes for accessibility (proper htmlFor/id on labels, type="button" on buttons, semantic button instead of role="button" div, and a DragEvent type fix) — happy to walk through them if helpful. Great foundation for the UI! |
* docs: align repository documentation with MVP status Documentation-only audit. No code, evaluator, or rule changes; no new features; no Cloudflare work; no changes to PR #37 or #78. User-facing copy fixed: - README.md: replace misleading ASCII diagram (3 rewrites / Tailored CV) with the actual MVP outputs (Score, Issues, Strengths, ATS verdict, Archetype); clarify that /evaluate-cv ./my-resume.pdf works only because Claude Code reads PDFs natively (the local CLI/web UI parse .md and .txt only). - apps/web-ui layout.tsx: Next.js metadata description replaced with honest CV-evaluator copy (no longer 'Build a tailored resume...'). - apps/web-ui/README.md: list all three routes (/, /results, /feedback), note the static-export + privacy-first posture. - apps/cli/README.md: same PDF-clarification note as the root README. - package.json descriptions (root + core + cli): remove 'tailor / build' wording; describe the deterministic evaluator. - packages/intelligence/README.md: list the eight shipped roles (matches README and the actual implementation) and correct the default archetype (Backend Engineer, not Software Engineer). - packages/eval/README.md: drop the outdated LLM-provider claim; the MVP is fully deterministic. - .claude/skills/cv-evaluation/SKILL.md: 'local MVP', not 'hosted product'; default archetype corrected. - ROADMAP.md: status note added; Phase-1 / #74 / #75 / #76 / #85 / #87 marked as recently shipped; only #37 and #78 remain in progress. Historical docs marked with status notes (not rewritten, just flagged as pre-release context): ARCHITECTURE.md, PROPOSAL.md, PHASE-1.md, V1_SCOPE.md, MVP_DEMO_PLAN.md, PR_CLEANUP_HANDOFF.md, POST_MERGE_VALIDATION.md, REMAINING_PRS_PLAN.md, PR85_ROLLBACK_PLAN.md. Each now points readers to docs/MVP_RELEASE_STATUS.md. New: - docs/REPO_DOCS_AUDIT.md records the audit date, files reviewed, files changed, outdated claims found and fixed, historical docs left intentionally unchanged, remaining risks, and the validation results. Validates: pnpm test (12/12), pnpm lint (0 errors), pnpm build (6/6, fresh --force: emits /, /_not-found, /feedback, /results). * docs: fix remaining Node version reference docs/MVP_DEMO_PLAN.md still said 'Node 22+' in the prereqs section, which conflicts with the actual repo metadata (Node >= 20.0.0 in package.json) and with the authoritative setup guide in docs/LOCAL_DEMO.md. Replace the prereqs with a short pointer to LOCAL_DEMO.md plus a one-line accurate summary, so this historical demo-readiness doc no longer contradicts current setup instructions. docs/REPO_DOCS_AUDIT.md: remove the corresponding entry from the 'remaining documentation risks' list, fix the resulting item numbering, and add a note recording that the Node-version item was resolved before merge. No code, evaluator, or rule changes. No new features. PR #37 and PR #78 untouched. Validates: pnpm test 12/12, pnpm lint 0 errors, pnpm build 6/6 (fresh --force, emits /, /_not-found, /feedback, /results). * docs: address remaining CodeRabbit comments on PR #88 Two unresolved CodeRabbit comments fixed in this commit: 1. README.md — replace hard-coded '5 issues' in the evaluator diagram with the non-fixed wording 'Issues'. evaluate() returns a variable-length issues array; a fixed count would drift as scoring changes. The other diagram labels (Score, Strengths, ATS verdict, Archetype) are kept as-is because they describe deterministic outputs that do not vary in count. 2. packages/intelligence/README.md — the previous audit pass claimed this package ships eight roles and falls back to Backend Engineer. But packages/intelligence/src/archetypes/index.ts registers only three archetypes (Software Engineer, Product Manager, Data & ML Engineer) and DEFAULT_ARCHETYPE is softwareEngineer. Update the README to reflect the actual registry. Add a note clarifying that @cv-builder/core has a separate, broader legacy/runtime registry (7 roles) used by the CLI and Web UI, and that unifying the two registries is a follow-up — see docs/ARCHETYPE_GAP_AUDIT.md. docs/REPO_DOCS_AUDIT.md updated to record that the packages/intelligence/README.md archetype-inventory row was corrected in two steps (the audit pass incorrectly bumped the package claim to 8; this commit brings it back to 3 and adds the @cv-builder/core note). No code changes. No new features. PR #37 and PR #78 untouched. Docs only. Validates: pnpm test 12/12, pnpm lint 0 errors, pnpm build 6/6. --------- Co-authored-by: Cleanup Bot <cleanup-bot@example.com>
…essibility and typing
What does this PR do?
Adds a two-column input screen where users can paste or upload their CV (left) and a Job Description (right). Includes character/word count, file upload support (.txt, .md, .pdf) with drag & drop, and an "Evaluate" button that triggers client-side compatibility analysis. Results are displayed on a separate results page.
Related issue
Closes #22
Type of change
Checklist
Screenshots (if UI change)
Before
After
Summary by CodeRabbit