Skip to content

Add CV and JD input screen with file upload and client-side evaluation#76

Open
alexNJF wants to merge 4 commits into
TechImmigrants:mainfrom
alexNJF:feature/cv-jd-input-page
Open

Add CV and JD input screen with file upload and client-side evaluation#76
alexNJF wants to merge 4 commits into
TechImmigrants:mainfrom
alexNJF:feature/cv-jd-input-page

Conversation

@alexNJF

@alexNJF alexNJF commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

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

  • Bug fix
  • [*] New feature
  • New archetype / rule
  • Documentation
  • Refactoring (no behavior change)
  • CI / tooling

Checklist

  • [* ] I've read CONTRIBUTING.md
  • [ *] My code follows the project's style
  • I've added/updated tests (if applicable)
  • [*] I've tested locally and it works
  • New archetypes include 15+ keywords and source references

Screenshots (if UI change)

Before

image

After

image image

Summary by CodeRabbit

  • New Features
    • CV evaluation feature supporting CV and job description uploads
    • Results page displaying evaluation scores, strengths, and dimension analysis
    • File upload functionality for text and markdown documents
    • Text statistics displaying character and word counts

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Evaluation Flow UI

Layer / File(s) Summary
Reusable UI Components
apps/web-ui/src/app/components/TextStats.tsx, apps/web-ui/src/app/components/FileUpload.tsx, apps/web-ui/src/app/components/ScoreCard.tsx
TextStats computes and displays character and word counts. FileUpload handles file selection via click or drag-and-drop, validates extensions (.txt, .md supported; .pdf and others alert unsupported), reads text content, and invokes a callback with the loaded content. ScoreCard validates the input score (non-negative, finite), clamps to valid range, computes a percentage capped at 100, and renders a card with progress bar.
Evaluation Storage Layer
apps/web-ui/src/app/lib/evaluation-storage.ts
EVALUATION_RESULT_KEY constant and two functions: saveEvaluationResult() serializes and writes to localStorage with error handling (no-op on server), and getEvaluationResult() reads, parses, and returns stored evaluation data or null if unavailable/invalid (with error logging).
Evaluation Input Form and Integration
apps/web-ui/src/app/components/EvaluateForm.tsx, apps/web-ui/src/app/page.tsx
EvaluateForm manages CV and JD textarea state, validates CV non-empty, wires FileUpload components to populate textareas from file content, shows TextStats for each input, and implements handleEvaluate(): calls evaluate() from @cv-builder/core, saves result via saveEvaluationResult(), routes to /results, logs and alerts errors, and updates loading state. Home page imports and renders EvaluateForm.
Results Display Page
apps/web-ui/src/app/results/page.tsx
ResultsPage retrieves evaluation result on mount via getEvaluationResult(), manages loading and result state, and renders loading/empty fallback screens or the results layout with a ScoreCard displaying the overall score and mapped sections showing strengths (bullet list) and dimensions (each with name and score/maxScore).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A form takes flight with fields so bright,
Files hop in through drag and drop delight,
Storage whispers secrets in the night,
Then scores dance proudly, wrapped in light—
Results revealed! The evaluation's right! 📊✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main change: adding a CV/JD input screen with file upload and client-side evaluation.
Linked Issues check ✅ Passed All coding requirements from issue #22 are met: two-column input UI, file upload for .md/.txt/.pdf with drag & drop, character/word count display, 'Evaluate' button calling evaluate() from @cv-builder/core, results page navigation, and client-side processing.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the PR objective of building the CV/JD input screen with evaluation. No out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between facea8e and 1850c49.

📒 Files selected for processing (7)
  • apps/web-ui/src/app/components/EvaluateForm.tsx
  • apps/web-ui/src/app/components/FileUpload.tsx
  • apps/web-ui/src/app/components/ScoreCard.tsx
  • apps/web-ui/src/app/components/TextStats.tsx
  • apps/web-ui/src/app/lib/evaluation-storage.ts
  • apps/web-ui/src/app/page.tsx
  • apps/web-ui/src/app/results/page.tsx

Comment on lines +31 to +35
if (extension === "pdf") {
alert(
"PDF text extraction is not yet supported. Please use .txt or .md files."
);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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).

Comment on lines +57 to +61
<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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
<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.

Comment on lines +35 to +40
try {
return JSON.parse(raw);
} catch (error) {
console.error("Failed to parse evaluation result:", error);
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Web] Build the CV + JD input screen

1 participant