Skip to content

Add PDF upload support with size validation and tex…#95

Merged
SaharPak merged 3 commits into
TechImmigrants:mainfrom
alexNJF:feature/pdf-upload
Jul 18, 2026
Merged

Add PDF upload support with size validation and tex…#95
SaharPak merged 3 commits into
TechImmigrants:mainfrom
alexNJF:feature/pdf-upload

Conversation

@alexNJF

@alexNJF alexNJF commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds PDF upload support to the Web UI. Users can now drag-and-drop or select .pdf resume files, which are parsed client-side using pdfjs-dist and extracted to plain text for evaluation.

Previously, PDF uploads were detected but rejected with a "not supported yet" message.

Implementation details:

Uses dynamic import("pdfjs-dist") to avoid SSR issues with DOMMatrix during static export
PDF worker loaded locally from node_modules (no CDN dependency)
50 MB file size limit and 60-second timeout per operation
Request counter prevents stale uploads from overwriting newer ones
Worker/document resources cleaned up via pdf.cleanup() after extraction
Text extracted page-by-page with null-safe text item handling

Related issue

Closes #96

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 After

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added PDF file upload support alongside TXT and Markdown files.
    • Extracts text from uploaded PDFs for use in the application.
    • Enforces PDF size and processing time safeguards.
    • Updated upload guidance and allowed file types to include PDFs.
  • Bug Fixes

    • Prevented older/in-progress uploads from overwriting newer results in the UI.
    • Improved PDF failure handling with clearer, timeout-aware messaging (including guidance for scanned/OCR content).

@alexNJF
alexNJF requested review from Isusami and SaharPak and removed request for SaharPak July 18, 2026 13:19
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

FileUpload now accepts .pdf files alongside .txt and .md, extracts PDF text with size and timeout protections, and prevents stale asynchronous uploads from updating the UI. The web UI adds the pdfjs-dist dependency.

Changes

PDF upload flow

Layer / File(s) Summary
PDF extraction and request cancellation
apps/web-ui/src/app/components/FileUpload.tsx
PDF files are size-validated, parsed with pdfjs-dist, cleaned up after extraction, and guarded against stale upload results.
PDF dependency and upload affordances
apps/web-ui/package.json, apps/web-ui/src/app/components/FileUpload.tsx
The PDF library is declared, and the upload control now accepts and displays .pdf support.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: saharpak, isusami

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FileUpload
  participant pdfjs_dist
  participant PDFWorker
  User->>FileUpload: select PDF
  FileUpload->>pdfjs_dist: dynamically import library
  FileUpload->>PDFWorker: configure worker
  FileUpload->>pdfjs_dist: load document and extract page text
  pdfjs_dist-->>FileUpload: return extracted text
  FileUpload-->>User: update upload status and content
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds PDF text extraction and safety controls, but it does not map parsed content into the CV model or provide fallback/tests required by #96. Add structured mapping of parsed PDF data into the CV format, plus fallback handling and tests for common resume PDFs.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: PDF upload support with validation and text extraction.
Out of Scope Changes check ✅ Passed All changes stay focused on PDF upload/parsing support and related validation, cleanup, and dependency updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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 64-70: Update the PDF extraction workflow in FileUpload so
PDF_TIMEOUT_MS enforces a single 60-second deadline for the entire upload,
rather than restarting around page.getTextContent() for each page. Create and
reuse one shared deadline or wrap the complete extraction/parsing workflow in
one timeout, while preserving the existing per-page extraction behavior within
that overall limit.
- Around line 19-26: Update the PDF error catch handler in the file upload flow
to check that its request ID still matches requestIdRef.current before updating
status, matching the existing stale-request guard used for successful results
and text failures. Ensure errors from older uploads cannot overwrite the status
of a newer upload.
- Around line 57-62: Update the PDF loading flow around loadingTask and pdf so
cleanup always runs in a finally block: destroy loadingTask unconditionally, and
call pdf.cleanup() when a document was successfully created. Preserve the
existing timeout and parsing error behavior while ensuring cleanup also occurs
after timeout or page-processing failures.
🪄 Autofix (Beta)

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: 7741014e-fbd4-4229-af05-f9412f6fe8d4

📥 Commits

Reviewing files that changed from the base of the PR and between 37deb6e and 09533ee.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • apps/web-ui/package.json
  • apps/web-ui/src/app/components/FileUpload.tsx

Comment thread apps/web-ui/src/app/components/FileUpload.tsx Outdated
Comment thread apps/web-ui/src/app/components/FileUpload.tsx Outdated
Comment thread apps/web-ui/src/app/components/FileUpload.tsx Outdated
hakimi.a and others added 2 commits July 18, 2026 17:15
…PDFs

The previous implementation used Promise.race for the 60s timeout, which
meant that on expiry the underlying extraction kept running to completion
in the background — pdf.cleanup() and loadingTask.destroy() were never
reached, and the worker kept processing pages nobody would ever read.

This commit replaces the Promise.race pattern with an explicit timeout
flag and a setTimeout handle. On expiry we call loadingTask.destroy()
to release the worker; on success we run pdf.cleanup() and
loadingTask.destroy() in a finally block. Both cleanup calls are
best-effort (errors are swallowed) so they cannot mask the original
extraction error.

Also addressed while here:

- Resolve the pdfjs worker URL once at module scope (PDF_WORKER_URL)
  instead of constructing it on every PDF drop.
- Fetch pages concurrently via Promise.all. pdfjs's worker handles
  pages in parallel and caches them, so this is significantly faster
  than the previous sequential loop on multi-page resumes.
- Surface scanned-image PDFs: pdfjs returns an empty string silently
  when a PDF has no text layer, so we now detect that case and tell
  the user instead of accepting a blank resume for evaluation.
- Consolidate the stale-request check into an isStale(requestId)
  helper used by both the text and pdf branches.
@SaharPak

Copy link
Copy Markdown
Contributor

Hey — pushed one fix commit on top of #95: 9c0ed2b.

Bug fixed: the withTimeout helper used Promise.race, so on a 60s timeout the underlying extraction kept running to completion in the background. pdf.cleanup() and loadingTask.destroy() were never reached, and the worker kept processing pages nobody would ever read — real memory leak / DoS surface for a slow or malicious PDF.

Replaced with an explicit timedOut flag plus a setTimeout handle that calls loadingTask.destroy() on expiry. Cleanup runs in a finally block and is best-effort (errors swallowed) so it can't mask the original error.

While I was there:

  • Lifted the pdfjs worker URL to module scope (PDF_WORKER_URL) so it's resolved once at load instead of on every drop.
  • Switched the per-page loop to Promise.all — pdfjs's worker handles pages in parallel and caches them, so multi-page resumes extract faster.
  • Detect empty-PDF (scanned image): pdfjs returns empty string silently when there's no text layer; we now surface that with a helpful message instead of accepting a blank resume.
  • Consolidated the stale-request check into an isStale(requestId) helper so the text and pdf branches use the same predicate.

Verified:

  • biome check clean.
  • pnpm turbo run build --filter=@cv-builder/web... produces a valid static export (6/6 pages).
  • No FileUpload-specific TypeScript errors.

Not done (worth a follow-up, but out of scope for this PR):

  • No tests. apps/web-ui has no test runner yet ("test": "echo 'No tests yet'"). The request-id guard, timeout flag, and extension parsing are unit-testable. Suggest adding Vitest + React Testing Library as a separate PR, then backfilling tests for this component.

Happy to split up / rework any of this if you'd prefer separate commits.

@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: 2

🤖 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 90-102: Update the page extraction logic in FileUpload’s
Promise.all block to bound concurrency instead of starting extraction for every
page simultaneously. Process pages sequentially or through a small
worker/concurrency limit, while preserving page order and the existing text
extraction and joining behavior.
- Around line 95-99: Update the text extraction logic in FileUpload to preserve
PDF line breaks indicated by each item's hasEOL metadata instead of joining all
items with spaces. Keep spaces between items on the same line, insert newline
separators after items marked hasEOL, and retain the existing empty-string
handling for items without text.
🪄 Autofix (Beta)

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: ba8d0541-c888-4059-a782-248558ebb780

📥 Commits

Reviewing files that changed from the base of the PR and between 09533ee and 9c0ed2b.

📒 Files selected for processing (1)
  • apps/web-ui/src/app/components/FileUpload.tsx

Comment on lines +90 to +102
const pageTexts = await Promise.all(
Array.from({ length: doc.numPages }, (_, i) => i + 1).map(
async (pageNumber) => {
const page = await doc.getPage(pageNumber);
const content = await page.getTextContent();
return content.items
.map((item) =>
"str" in item && item.str != null ? String(item.str) : ""
)
.join(" ");
}
)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound concurrent page extraction.

Promise.all starts every page extraction at once. A page-heavy PDF can create excessive worker activity and memory pressure despite the 50 MB file limit. Process pages sequentially or use a small concurrency limit.

Proposed bounded extraction
-        const pageTexts = await Promise.all(
-          Array.from({ length: doc.numPages }, (_, i) => i + 1).map(
-            async (pageNumber) => {
-              const page = await doc.getPage(pageNumber);
-              const content = await page.getTextContent();
-              return content.items
-                .map((item) =>
-                  "str" in item && item.str != null ? String(item.str) : ""
-                )
-                .join(" ");
-            }
-          )
-        );
+        const pageTexts: string[] = [];
+        for (let pageNumber = 1; pageNumber <= doc.numPages; pageNumber++) {
+          const page = await doc.getPage(pageNumber);
+          const content = await page.getTextContent();
+          pageTexts.push(
+            content.items
+              .map((item) =>
+                "str" in item && item.str != null ? String(item.str) : ""
+              )
+              .join(" ")
+          );
+        }
📝 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
const pageTexts = await Promise.all(
Array.from({ length: doc.numPages }, (_, i) => i + 1).map(
async (pageNumber) => {
const page = await doc.getPage(pageNumber);
const content = await page.getTextContent();
return content.items
.map((item) =>
"str" in item && item.str != null ? String(item.str) : ""
)
.join(" ");
}
)
);
const pageTexts: string[] = [];
for (let pageNumber = 1; pageNumber <= doc.numPages; pageNumber++) {
const page = await doc.getPage(pageNumber);
const content = await page.getTextContent();
pageTexts.push(
content.items
.map((item) =>
"str" in item && item.str != null ? String(item.str) : ""
)
.join(" ")
);
}
🤖 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 90 - 102, Update
the page extraction logic in FileUpload’s Promise.all block to bound concurrency
instead of starting extraction for every page simultaneously. Process pages
sequentially or through a small worker/concurrency limit, while preserving page
order and the existing text extraction and joining behavior.

Comment on lines +95 to +99
return content.items
.map((item) =>
"str" in item && item.str != null ? String(item.str) : ""
)
.join(" ");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby context.
FILE="apps/web-ui/src/app/components/FileUpload.tsx"
wc -l "$FILE"
sed -n '1,180p' "$FILE"

# Find any usage of PDF.js text items and line-break handling in the repo.
rg -n "hasEOL|TextItem|getTextContent|pdfjs" apps/web-ui/src/app -S

Repository: TechImmigrants/cv-builder

Length of output: 7535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the pdfjs-dist version and any local type definitions if present.
rg -n '"pdfjs-dist"|pdfjs-dist' package.json apps/web-ui/package.json pnpm-lock.yaml yarn.lock package-lock.json -S || true

Repository: TechImmigrants/cv-builder

Length of output: 449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Pull authoritative docs for TextItem.hasEOL semantics.
python3 - <<'PY'
import sys, urllib.request
urls = [
    "https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html",
    "https://github.com/mozilla/pdf.js/blob/master/src/display/api.js",
]
for url in urls:
    try:
        with urllib.request.urlopen(url, timeout=20) as r:
            data = r.read(2000).decode("utf-8", "ignore")
        print(f"URL: {url}\n{data[:1200]}\n---")
    except Exception as e:
        print(f"URL: {url}\nERROR: {e}\n---")
PY

Repository: TechImmigrants/cv-builder

Length of output: 592


🌐 Web query:

PDF.js TextItem hasEOL getTextContent line breaks documentation

💡 Result:

In PDF.js, the hasEOL property is a boolean field within the TextItem object that indicates whether a specific text content item is followed by a line break [1][2][3]. This property was formally added to the TextItem API documentation to assist with identifying line endings, which are not explicitly defined in the underlying PDF specification [4]. Because PDF files represent text as glyphs positioned at absolute coordinates rather than as structured lines or paragraphs, there is no inherent concept of "lines" or "line breaks" in the file format [5][6][7]. While hasEOL provides a signal for line breaks, developers often rely on additional techniques when extracting text because the accuracy of these markers can vary depending on how the PDF was generated and how PDF.js processes it [8][5][9]. Common strategies include: 1. Using transform matrix data: The transform property of a TextItem contains a matrix where the fifth element (index 5) typically represents the vertical (Y) coordinate [6]. By comparing the Y-coordinate of consecutive items, you can programmatically detect line changes [6][7][10]. 2. Combining with disableCombineTextItems: By default, PDF.js attempts to combine text items into larger strings to improve text selection [9][11]. If you need more granular control over text positioning or line detection, setting disableCombineTextItems: true in the getTextContent parameters can prevent the engine from merging these items, giving you access to the raw, uncombined text chunks [3][9][11]. Because of the variable nature of PDF structures, there is no single guaranteed method to extract "raw lines" that works perfectly for every document; detection logic often involves heuristics based on coordinate proximity, font metadata, and the hasEOL property [5][6][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how FileUpload content is consumed to judge impact.
rg -n "onContentLoaded|FileUpload" apps/web-ui/src -S

# If there's a parent component, print its relevant section.
PARENT="$(rg -l "onContentLoaded" apps/web-ui/src | head -n 1)"
echo "PARENT=$PARENT"
[ -n "${PARENT:-}" ] && sed -n '1,220p' "$PARENT"

Repository: TechImmigrants/cv-builder

Length of output: 3718


Preserve PDF line breaks in FileUpload.tsx:95-99. Joining every text item with " " drops hasEOL line breaks, flattening headings, lists, and section boundaries before evaluation.

🤖 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 95 - 99, Update
the text extraction logic in FileUpload to preserve PDF line breaks indicated by
each item's hasEOL metadata instead of joining all items with spaces. Keep
spaces between items on the same line, insert newline separators after items
marked hasEOL, and retain the existing empty-string handling for items without
text.

@SaharPak
SaharPak merged commit 2da00d6 into TechImmigrants:main Jul 18, 2026
4 checks passed
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.

[Feature] Add PDF parsing support

2 participants