Skip to content

PDF Upload with Free OCR#2

Merged
eshanhasitha merged 1 commit into
mainfrom
eshan
Jun 2, 2026
Merged

PDF Upload with Free OCR#2
eshanhasitha merged 1 commit into
mainfrom
eshan

Conversation

@eshanhasitha

Copy link
Copy Markdown
Collaborator

No description provided.

Copilot AI review requested due to automatic review settings June 2, 2026 14:55
@eshanhasitha eshanhasitha merged commit bb03e2c into main Jun 2, 2026

Copilot AI 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.

Pull request overview

This PR introduces an admin “Upload Result Sheet” flow that OCRs scanned PDF result sheets, parses index/grade pairs, and persists subjects + student results via new admin API routes.

Changes:

  • Add client-side PDF rendering + OCR pipeline (pdfjs-dist + tesseract.js) and OCR text parsing utilities.
  • Add new admin upload wizard UI (4-step flow) plus supporting UI components (table, select, alerts, dialogs, tooltips, progress).
  • Add admin APIs to list/create subjects and to save OCR’d results (including PDF upload logging).

Reviewed changes

Copilot reviewed 18 out of 21 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
package.json Adds pdfjs-dist and tesseract.js dependencies for PDF rendering + OCR.
package-lock.json Locks transitive dependencies introduced by PDF/OCR libraries.
next.config.js Adds webpack customization intended to avoid bundling server-incompatible canvas.
lib/parsePDF.ts Implements OCR text parsing into structured subject/semester metadata + (index, grade) pairs.
lib/hooks/usePDFOCR.ts Client hook to load PDF, render pages to canvas, OCR pages, and parse combined text.
components/ui/tooltip.tsx Adds a simple tooltip component used during review/edit for validation hints.
components/ui/table.tsx Adds table primitives used for editable OCR results review.
components/ui/select.tsx Adds styled <select> wrapper used across upload steps.
components/ui/progress.tsx Adds progress bar used during OCR processing.
components/ui/badge.tsx Adds badge component used in the review/edit summary bar.
components/ui/alert.tsx Adds alert component for success/warning/error messaging in the wizard.
components/ui/alert-dialog.tsx Adds confirmation dialog used before saving results.
app/api/admin/subjects/route.ts Adds admin subject list + create/find endpoints (with semester join/create).
app/api/admin/results/save/route.ts Adds admin endpoint to upsert students and save results + log PDF uploads.
app/admin/upload/StepIndicator.tsx Adds step indicator UI for the wizard.
app/admin/upload/Step1SubjectDetails.tsx Step 1 UI to capture subject/semester metadata and prefill if subject exists.
app/admin/upload/Step2UploadOCR.tsx Step 2 UI for PDF upload, OCR progress, and auto-advance to review.
app/admin/upload/Step3ReviewEdit.tsx Step 3 UI for reviewing/editing parsed OCR rows and confirming save.
app/admin/upload/Step4Success.tsx Step 4 UI to summarize save outcome and allow another upload.
app/admin/upload/page.tsx Orchestrates the full upload wizard and server persistence calls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +49
const [dragOver, setDragOver] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [sizeError, setSizeError] = useState<string | null>(null);

const handleFile = useCallback(
async (file: File) => {
setSizeError(null);
if (file.type !== "application/pdf") {
setSizeError("Only PDF files are accepted.");
return;
}
if (file.size > 20 * 1024 * 1024) {
setSizeError("File too large. Max 20MB.");
return;
}
setSelectedFile(file);
await processFile(file);
},
[processFile]
);

// Auto-advance when done
if (result && selectedFile && progress.status === "done") {
setTimeout(() => onDone(selectedFile), 800);
}
import { Select } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useEffect, useState, useCallback } from "react";
} finally {
setChecking(false);
}
}, [formData.subjectCode]);
Comment thread components/ui/tooltip.tsx
return (
<div className={cn("group relative inline-flex", className)}>
{children}
<div className="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 opacity-0 transition-opacity group-hover:opacity-100">
Comment on lines +59 to +61
for (const entry of studentEntries) {
const { indexNumber, grade } = entry;

Comment on lines +110 to +127
// Existing result found
const isNewPassing = grade !== "E" && grade !== "AB";

if (isNewPassing) {
// Repeat pass: award C grade (2.00 GP) per university repeat rule
await db
.update(results)
.set({
grade,
gradePoint: "2.00",
isRepeat: true,
})
.where(eq(results.id, existingResults[0].id));
saved++;
} else {
// New grade is E or AB: skip, keep old record
skipped++;
}
Comment on lines +77 to +96
// Normalize subject code: uppercase, remove spaces (IS 2106 → IS2106)
const subjectCode = rawCode.toUpperCase().replace(/\s+/g, "");

// Check if subject already exists
const existing = await db
.select({
id: subjects.id,
subjectCode: subjects.subjectCode,
subjectName: subjects.subjectName,
creditPoints: subjects.creditPoints,
semesterId: subjects.semesterId,
yearNumber: semesters.yearNumber,
semesterNumber: semesters.semesterNumber,
semesterLabel: semesters.label,
})
.from(subjects)
.innerJoin(semesters, eq(subjects.semesterId, semesters.id))
.where(eq(subjects.subjectCode, subjectCode))
.limit(1);

Comment on lines +70 to +98
// ── 1. Upsert student ───────────────────────────────────────────
// Insert student with default password, skip if already exists
const existingStudents = await db
.select()
.from(students)
.where(eq(students.indexNumber, indexNumber))
.limit(1);

if (existingStudents.length === 0) {
await db.insert(students).values({
indexNumber,
passwordHash: defaultPasswordHash,
isFirstLogin: true,
});
created++;
}

// ── 2. Check for existing result for this student + subject ─────
const existingResults = await db
.select()
.from(results)
.where(
and(
eq(results.studentIndex, indexNumber),
eq(results.subjectId, subjectId)
)
)
.limit(1);

Comment on lines +1 to +4
"use client";

import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
Comment thread lib/hooks/usePDFOCR.ts
Comment on lines +110 to +115
// Dynamic import Tesseract.js
const Tesseract = await import("tesseract.js");
const worker = await Tesseract.createWorker("eng");
const { data } = await worker.recognize(canvas);
allText += data.text + "\n";
await worker.terminate();
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.

2 participants