Hello everyone! In this tutorial, we’ll guide you through building a resume scanner web app using NodeJS and Next.js, powered by OpenAI. Follow along step-by-step as we create a custom solution that highlights key skills, experience, qualifications, and much more from resumes. Let’s dive in!
Building a Drag-and-Drop File Uploader with Next.js
Let's start by creating a Node js project npm init
in your project directory and install the dependencies express, cors, multer, pdf-parse, openai
Importing Required Modules:
import OpenAI from "openai";
import cors from "cors";
import express from "express";
import multer from "multer";
import PdfParse from "pdf-parse";
OpenAI
, cors
, express
, multer
, and PdfParse
are modules required for AI functionality, enabling CORS, creating an Express server, handling file uploads, and parsing PDF files to extract text from it, respectively.Initializing Variables:
const prompt = "Following is the resume text,Give me a brief description about them in a 300 words paragraph";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const app = express();
const upload = multer();
prompt
: Stores a prompt string for requesting a brief description of the resume.openai
: Creates a new instance of the OpenAI class, initializing it with an API key.app
: Initializes an Express application.upload
: Initializes multer for handling file uploads.Setting Up Middleware:
app.use(cors());
Defining API Endpoint:
app.post("/resume-ai-scanner", upload.single("file"), async (req, res) => {
req
(request) and res
(response) as parameters.File Upload Handling:
console.log(req.file);
try {
if (req.file) {
const data = await extractTextFromPDF(req.file);
...
extractTextFromPDF
function.OpenAI API:
const completion = await openai.chat.completions.create({
messages: [
{
role: "user",
content: `${prompt} ${data}`,
},
],
model: "gpt-3.5-turbo",
});
console.log(completion.choices[0].message);
let message = completion.choices[0].message.content;
return res.send({ summary: message });
openai.chat.completions.create
method. The messages
array contains an object representing the user's input, with the role
set to "user"
and the content
set to the concatenation of the prompt
(defined elsewhere) and the data
extracted from the resume. The model
parameter specifies the version of the GPT model to be used, in this case, gpt-3.5-turbo
.Error Handling:
} catch (err) {
console.log(err);
return res.send({ error: "Something went wrong. Please try again later!" });
}
PDF Text Extraction Function:
const extractTextFromPDF = async (file: Express.Multer.File) => {
try {
const data = await PdfParse(file.buffer);
return data.text;
} catch (error) {
throw new Error("Error extracting text from PDF:" + error);
}
};
extractTextFromPDF
that takes a file object as input.PdfParse
to parse the PDF file and extract text from it.Starting the Server:
app.listen(3001, () => {
console.log("Server running on 3001");
});
import OpenAI from "openai";
import cors from "cors";
import express from "express";
import multer from "multer";
import PdfParse from "pdf-parse";
const prompt =
"Following is the resume text,Give me a brief description about them in a 300 words paragraph";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const app = express();
const upload = multer();
app.use(cors());
app.post("/resume-ai-scanner", upload.single("file"), async (req, res) => {
console.log(req.file);
try {
if (req.file) {
const data = await extractTextFromPDF(req.file);
console.log(data);
const completion = await openai.chat.completions.create({
messages: [
{
role: "user",
content: `${prompt} ${data}`,
},
],
model: "gpt-3.5-turbo",
});
console.log(completion.choices[0].message);
let message = completion.choices[0].message.content;
return res.send({ summary: message });
}
} catch (err) {
console.log(err);
return res.send({ error: "Something went wrong Please try again later!" });
}
});
const extractTextFromPDF = async (file: Express.Multer.File) => {
try {
const data = await PdfParse(file.buffer);
return data.text;
} catch (error) {
throw new Error("Error extracting text from PDF:" + error);
}
};
app.listen(3001, () => {
console.log("Server running on 3001");
});
So this code sets up an Express server with an API endpoint for uploading PDF files, extracting text from them, processing the text using the OpenAI API, and returning the AI-generated summary as a response
As mentioned earlier, we are extending the drag-and-drop file uploader.
Importing Required Modules and Components:
import { useEffect, useState } from "react";
import SummaryData from "./summary_data";
useEffect
and useState
are React hooks for managing state and side effects.SummaryData
is a component for displaying the resume summary.Initializing State:
const [file, setFile] = useState<File | null>();
const [fileUrl, setFileUrl] = useState<string>();
const [fileEnter, setFileEnter] = useState(false);
const [summary, setSummary] = useState<string>();
const [loading, setLoading] = useState(false);
useState
hook.file
stores the uploaded file.fileUrl
stores the URL of the uploaded file.fileEnter
tracks whether the file is being dragged over.summary
stores the summary generated by OpenAI.loading
tracks the loading state of the application.Call our backend API and get a summary:
const generateSummary = async (f: File) => {
setLoading(true);
var data = new FormData();
data.append("file", f);
const res = await fetch("http://localhost:3001/resume-ai-scanner", {
method: "POST",
body: data,
});
let respData = await res.json();
console.log(respData)
if (respData.error) {
console.log(respData.error);
} else {
setSummary(respData.summary);
}
setLoading(false);
};
generateSummary
to generate a summary using OpenAI.FormData
object to send the file to the server.false
after completing the request.UseEffect Hook:
useEffect(() => {
if (file) {
generateSummary(file);
}
}, [file]);
generateSummary
function whenever the file
state changes.Rendering JSX:
return (
<div className="container px-4 max-w-5xl mx-auto">
{!fileUrl ? (
<div
onDragOver={(e) => {
e.preventDefault();
setFileEnter(true);
}}
onDragLeave={(e) => {
setFileEnter(false);
}}
onDragEnd={(e) => {
e.preventDefault();
setFileEnter(false);
}}
onDrop={(e) => {
e.preventDefault();
setFileEnter(false);
if (e.dataTransfer.items) {
[...e.dataTransfer.items].forEach((item, i) => {
if (item.kind === "file") {
const file = item.getAsFile();
if (file) {
let blobUrl = URL.createObjectURL(file);
setFileUrl(blobUrl);
setFile(file);
}
console.log(`items file[${i}].name = ${file?.name}`);
}
});
} else {
[...e.dataTransfer.files].forEach((file, i) => {
console.log(`… file[${i}].name = ${file.name}`);
});
}
}}
className={`${
fileEnter ? "border-4" : "border-2"
} mx-auto bg-white flex flex-col w-full max-w-xs h-72 border-dashed items-center justify-center`}
>
<label
htmlFor="file"
className="h-full flex flex-col justify-center text-center"
>
Click to upload or drag and drop
</label>
<input
id="file"
type="file"
className="hidden"
onChange={(e) => {
console.log(e.target.files);
let files = e.target.files;
if (files && files[0]) {
let blobUrl = URL.createObjectURL(files[0]);
setFile(files[0]);
setFileUrl(blobUrl);
}
}}
/>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
<div className="flex flex-col items-center">
<object
className="w-full h-96"
type="application/pdf"
data={fileUrl}
></object>
{loading ? (
<div className="text-center mt-10">Loading...</div>
) : (
<button
onClick={() => {
setFileUrl("");
setFile(null);
}}
className="px-4 mt-10 uppercase py-2 tracking-widest outline-none bg-red-600 text-white rounded"
>
Reset
</button>
)}
</div>
{summary && <SummaryData summary={summary} />}
</div>
)}
</div>
)
fileUrl
.fileUrl
is present, it renders the uploaded file and the summary data. Otherwise, it renders the file uploader interface.Final File Upload component
"use client";
import openai from "@/libs/openai.lib";
import { useEffect, useState } from "react";
import SummaryData from "./summary_data";
export const FileUpload = () => {
const [file, setFile] = useState<File | null>();
const [fileUrl, setFileUrl] = useState<string>();
const [fileEnter, setFileEnter] = useState(false);
const [summary, setSummary] = useState<string>();
const [loading, setLoading] = useState(false);
const generateSummary = async (f: File) => {
setLoading(true);
var data = new FormData();
data.append("file", f);
const res = await fetch("http://localhost:3001/resume-ai-scanner", {
method: "POST",
body: data,
});
let respData = await res.json();
console.log(respData)
if (respData.error) {
console.log(respData.error);
} else {
setSummary(respData.summary);
}
setLoading(false);
};
useEffect(() => {
if (file) {
generateSummary(file);
}
}, [file]);
return (
<div className="container px-4 max-w-5xl mx-auto">
{!fileUrl ? (
<div
onDragOver={(e) => {
e.preventDefault();
setFileEnter(true);
}}
onDragLeave={(e) => {
setFileEnter(false);
}}
onDragEnd={(e) => {
e.preventDefault();
setFileEnter(false);
}}
onDrop={(e) => {
e.preventDefault();
setFileEnter(false);
if (e.dataTransfer.items) {
[...e.dataTransfer.items].forEach((item, i) => {
if (item.kind === "file") {
const file = item.getAsFile();
if (file) {
let blobUrl = URL.createObjectURL(file);
setFileUrl(blobUrl);
setFile(file);
}
console.log(`items file[${i}].name = ${file?.name}`);
}
});
} else {
[...e.dataTransfer.files].forEach((file, i) => {
console.log(`… file[${i}].name = ${file.name}`);
});
}
}}
className={`${
fileEnter ? "border-4" : "border-2"
} mx-auto bg-white flex flex-col w-full max-w-xs h-72 border-dashed items-center justify-center`}
>
<label
htmlFor="file"
className="h-full flex flex-col justify-center text-center"
>
Click to upload or drag and drop
</label>
<input
id="file"
type="file"
className="hidden"
onChange={(e) => {
console.log(e.target.files);
let files = e.target.files;
if (files && files[0]) {
let blobUrl = URL.createObjectURL(files[0]);
setFile(files[0]);
setFileUrl(blobUrl);
}
}}
/>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-1 gap-10">
<div className="flex flex-col items-center">
<object
className="w-full h-96"
type="application/pdf"
data={fileUrl}
></object>
{loading ? (
<div className="text-center mt-10">Loading...</div>
) : (
<button
onClick={() => {
setFileUrl("");
setFile(null);
}}
className="px-4 mt-10 uppercase py-2 tracking-widest outline-none bg-red-600 text-white rounded"
>
Reset
</button>
)}
</div>
{summary && <SummaryData summary={summary} />}
</div>
)}
</div>
);
};
Summary Component
import React from "react";
const SummaryData = ({ summary }: { summary: string }) => {
return (
<div>
<h2 className="text-2xl mb-5">Scanned Results</h2>
<div className="bg-white rounded py-2 px-4">
<p className="text-slate-700 tracking-wider mt-5 font-light">{summary || ""}</p>
</div>
</div>
);
};
export default SummaryData;
This code sets up a file uploader component in a React application using Next.js. It allows users to upload a resume file, generates a summary using OpenAI, and displays the uploaded file along with the generated summary.
Now you can start your node js server and next js application, upload your resume, and see it in action.
Congratulations now you have successfully built your resume scanner, also we’ve explored the fascinating intersection of web development and artificial intelligence by building a resume scanner app with OpenAI, Node JS, and Next.js. Now we know how to integrate OpenAI into your web applications, handle file uploads, extract text from PDFs, and generate AI-powered summaries. With the skills and knowledge gained from this project, you’re well-equipped to enhance your web apps with the capabilities of artificial intelligence. Happy Coding