INTRODUCTION
I once heard someone say that looking for work is a full-time job, and I couldn't agree more. It's challenging enough to get ready for interviews and put your best foot forward, but we also have to create a tailored CV and cover letter for each application, which adds another layer of difficulty to the process. I got the idea to use AI to change my cover letters when someone on Twitter suggested combining AI and the keywords from job descriptions to construct one. The results were so fascinating that I decided to build an application on top of the Chat GPT API.
For context, Chat GPT is a type of artificial intelligence (AI) program that is designed to simulate human conversation. It is used in a variety of applications, from customer service chatbots to automated assistant bots. Chat GPT technology is also used to build virtual agents that can answer questions, provide advice, or perform specific tasks. Chat GPT technology is becoming increasingly popular due to its potential to automate processes and reduce human labor. Chat GPT technology is powered by natural language processing (NLP) algorithms, which allows the chatbot to understand the user's input and respond appropriately.
In this tutorial, you will be using react JS and the Chat GPT API to build a resume and cover letter builder.
PREREQUISITES
- You should be familiar with React JS and CSS
- You should know how to query endpoints
Before we get started, you need to have an account on open AI. You can get started here. After you have created an account, do the following to get your API key;
- Click on the avatar on the navbar with your profile picture to open up the dropdown menu
- Select "view API keys" to create an API key.
- Keep the API key in a safe place, you will make use of it later in the tutorial.
GETTING STARTED
To get the ball rolling;
- create a folder in your documents folder and name it "resume_builder" or any name you prefer.
- Go to your code editor and open the folder.
- In your terminal, enter
npx create-react-app@latest ./
to create a react JS starter file in your root folder. - In the root of your folder create a .env file and add your openAI API key in the file. The key will be called
REACT_APP_GPT_KEY
. For this tutorial, I will be using tailwind CSS for styling, however if you prefer to use other types of styling, that's fine. You will need to install 3 dependencies to get started. In your terminal enternpm install react-router-dom react-toastify react-to-print
.
CREATING PAGES AND ROUTES
This application will have just 3 pages, so you will to create a folder called pages inside the src
folder. The pages folder contains 3 files called, coverLetter.jsx
, Home.jsx
and Resume.jsx
.
In your App.jsx
folder, add this code to create the appropriate routes for all the pages using react-router-dom
.
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "./App.css";
import "react-toastify/dist/ReactToastify.css";
import CoverLetterForm from "./pages/CoverLetterForm";
import Home from "./pages/Home";
import Resume from "./pages/Resume";
function App() {
return (
<div className="App">
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/resume" element={<Resume />} />
<Route path="/cover-letter" element={<CoverLetterForm />} />
</Routes>
</Router>
<div>
<ToastContainer
position="top-right"
autoClose={3000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
draggable
pauseOnHover
/>
</div>
</div>
);
}
export default App;
Did you notice that after the routes, there is a component called ToastContainer
? We will be using this to render alerts for the application using react-toastify
.
Creating the home page
The home page for this application is a hero section with two navigation buttons and an image. You can style your home page as you deem fit, for this tutorial I will focus more on the functionality part of the application.
Navigating to the resume and cover letter forms
Add the following code to the hero buttons to navigate to different pages in the application. You will need to import useNavigate
from react-router-dom
.
<button onClick={()=>navigate('/resume')}>
Create Resume
</button>
<button onClick={()=>navigate('/cover-letter')}>
Create Cover Letter
</button>
When you click on the create resume button, you should be navigated to a new page that shows the form for creating the resume. It should look like the image below.
ADDING FUNCTIONALITY TO THE RESUME FORM
The form component is controlled using react useState
using the code below.
const [data, setData] = useState({
name: dataStorage.name || "",
position: dataStorage.position || "",
linkedin: dataStorage.linkedin || "",
positions: dataStorage.positions || [
{ position: "", duration: "", company: "" },
],
});
The state is an object that contains the name of the applicant, their current position, linkedIn URL, and an array of objects that contains data about the past positions they have held.
Next, you are going to write a code that allows the user to add and remove a new position in the positions array. This can be achieved using the functions below.
const handleAddPosition = () => {
setData({
...data,
positions: [
...data.positions,
{ position: "", duration: "", company: "" },
],
});
};
const handleDeletePosition = (i) => {
const newPositions = data.positions.filter((item, index) => 1 !== index);
setData({ ...data, positions: newPositions });
};
const handleUpdatePosition = (e, index) => {
const { name, value } = e.target;
const list = [...data.positions];
list[index][name] = value;
setData({ ...data, positions: list });
};
const handleChange = (e) => {
setData({ ...data, [e.target.name]: e.target.value });
};
The handleAddPosition
is used to create a new position inside the positions array. The spread operator is used to add the current positions in the list and then it adds a new object { position: "", duration: "", company: "" }
to the array.
When the add button in the form is clicked the "positions held" form inputs will increase by one as depicted in the image below.
The handleDeletePosition
takes in an index value and then uses filter array method to find the index in the array and then filter it out.
The handleUpdatePosition
is the function triggered when onChange
is called on any of the form elements. It also takes in an index, which is the position of the object in the array. This index is used to mutate the state values using the values gotten from the user. We will then call setState
with the changed value of the positions array.
Lastly, the handleChange
function changes the state of name
, position
and linkedIn
. I separated the state change functions in handleChange
and handleUpdatePosition
to not overcomplicate things because the functions handle state changes for different data types.
Now that our input fields are controlled, it's time to move on to the next big step.
CONNECTING TO THE API
To connect to the API, create a file called api.js
in the root of your project and add this code to the file
const DEFAULT_PARAMS = {
model: "text-davinci-002",
temperature: 0.7,
max_tokens: 256,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
};
export async function getData(params = "") {
const params_ = { ...DEFAULT_PARAMS, prompt: params };
const requestOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + String(process.env.REACT_APP_GPT_KEY),
},
body: JSON.stringify(params_),
};
const response = await fetch(
"https://api.openai.com/v1/completions",
requestOptions
);
const data = await response.json();
return data.choices[0].text;
}
The DEFAULT_PARAMS
object contains variable that is Chat GPT specific. It will be sent along with the API request inside a params object.
The function for connecting to the API is called getData
. It takes in an argument called params with the default value of an empty string. This params is a series of prompts that will be used to query the GPT endpoint and it will be gotten from our input fields in the frontend. In the Authorization space, you will notice that we are concatenating the "Bearer" string with another string String(process.env.REACT_APP_GPT_KEY)
. This "REACT_APP_GPT_KEY is the API key gotten from the GPT website, and was saved in your .env
file earlier in this tutorial.
Now that the API function has been set up, we can import it in your React file. To do this, you will add this code to your Resume.jsx file
import { getData } from "../api";
After getData
has been imported, you will write a function called handleBuildData
, this is the function that will be called when you click the submit button. This function will call getData and communicate with chat GPT to get your new Resume. Here is the code for handleBuildData
.
const handleBuildData = async (e) => {
e.preventDefault();
//๐๐ป The job description prompt
//prompt1
const prompt1 = `I am writing a resume, my details are \n name: ${
data.name
} \n role: ${data.position} I have worked ${remainderText()}.
\n. Can you write a 50 words description for the top of the resume (first person writing)?`;
//prompt 2
const prompt2 = `I am writing a resume, my details are \n name: ${
data.name
} \n role: ${data.position}. \n During my years I worked at ${
data.positions.length
} companies. ${remainderText()} \n Can you write me a list of soft skills seperated by numbers
a person in this role will possess?.
The list should be suitable for a resume (in first person)? Do not write
"The end" at the end of the list!!!`;
//prompt3
const prompt3 = `I am writing a resume, my details are \n name: ${
data.name
} \n role: ${data.position}. \n During my years I worked at ${
data.positions.length
} companies. ${remainderText()} \n ${JobText()}`;
//prompt4
const prompt4 = `I am writing a resume, my details are \n name: ${
data.name
} \n role: ${data.position}. \n During my years I worked at ${
data.positions.length
} companies. ${remainderText()} \n Can you write me a list seperated in numbers of 5 hard skills a person in this position should possess. The list should be suitable for a resume(in first person)? Do not write "the end" at the end of the list`;
setLoading(true);
try {
const [hardSkills, skills, objective, jobResponsibilities] =
await Promise.all([
getData(prompt4),
getData(prompt2),
getData(prompt1),
getData(prompt3),
]);
//๐๐ป put them into an object
const chatgptData = {
objective,
jobResponsibilities,
skills,
hardSkills,
};
console.log(chatgptData);
saveToStorage(chatgptData);
setLoading(false);
navigate("/resume");
} catch (Err) {
setLoading(false);
}
};
Yikes!!! That looks like a lot of code doesn't it? Well, let me break down what each line of code is doing so we are on the same track.
There are a total of 4 prompts in this function. A prompt is just like a request or question that you send to Chat GPT, before it can return a reply to you. If you have used the Chat GPT interface, you'd recall that you usually have to input something to the AI before you can get a reply, what you're sending is a prompt and that's exactly what we are trying to do here.
The prompts are in the code above are;
- A list of soft skills
- A list of job responsibilities
- A list of hard skills
- An objective for the applicant.
The prompt uses the data gotten from the input fields to support the request and to get a more tailored reply, depending on the information supplied.
A try and catch block is used to catch any possible error that might occur in the code. A promise.all
method is used to chain all the getData
function calls for each prompt and the result is returned in an array.
This array is then saved in an Object called chatgptData
. If you would like to use localStorage to persist your data, you can call saveToStorage
to do this. The function can be found below.
const saveToStorage = ({
objective,
jobResponsibilities,
skills,
hardSkills,
}) => {
const newData = {
name: data.name,
position: data.position,
linkedin: data.linkedin,
positions: data.positions,
objective,
jobResponsibilities,
skills,
hardSkills,
};
localStorage.setItem("job_info", JSON.stringify(newData));
};
After the data has been successfully returned, we can then navigate to the resume page, where we populate the fields with our newly returned resume data.
import React, { useEffect,useRef } from "react";
import { useNavigate } from "react-router-dom";
import ResumeHead from "../components/ResumeHead";
import ReactToPrint from "react-to-print";
const Resume = () => {
const componentRef = useRef();
const navigate = useNavigate();
const dataStorage = localStorage.getItem("job_info")
? JSON.parse(localStorage.getItem("job_info"))
: {};
console.log(dataStorage.jobResponsibilities);
useEffect(() => {
if (Object.keys(dataStorage).length < 1) {
navigate("/");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigate]);
const replaceWithBr = (string) => {
return string?.replace(/\n/g, "<br />");
};
return (
<div ref={componentRef} className='my-8'>
<p
className="ml-5 mt-4 cursor-pointer font-body text-white hover:underline"
onClick={() => navigate("/")}
>
Back
</p>
<div className="sm:w-[90%] font-medium text-[14px] md:w-[70%] text-gray-900 font-body mx-auto bg-white my-4 p-4">
<div className=" ml-auto flex flex-col items-end justify-end">
<p className="text-[22px] capitalize font-semibold text-gray-700">
{dataStorage.name}
</p>
<p className="text-[14px] uppercase">{dataStorage.position}</p>
<a
href={dataStorage.linkedin}
target="_blank"
rel="noreferrer "
className="text-cyan-500 underline"
>
Linkedin
</a>
</div>
<ResumeHead text={"Bio"} />
<p className="">{dataStorage.objective}</p>
<ResumeHead text={"technical skills"} />
<p
dangerouslySetInnerHTML={{
__html: replaceWithBr(dataStorage.hardSkills),
}}
className="-mt-8 font-normal"
/>
<ResumeHead text={"soft skills"} />
<p
dangerouslySetInnerHTML={{
__html: replaceWithBr(dataStorage.skills),
}}
className="-mt-8 font-normal"
/>
<ResumeHead text={"professional experience"} />
<div className="border border-gray-400">
{dataStorage.positions.map((item, i) => (
<div className="">
<div className="flex bg-gray-200 border-b mb-2 px-3 py-1 border-gray-500 justify-between ">
<div className=" border-right ">
<p className="text-gray-900 capitalize text-[16px] font-semibold">
{item.position}
</p>
<p className="capitalize">{item.company}</p>
</div>
<p>{item.duration}</p>
</div>
<div>
<p
dangerouslySetInnerHTML={{
__html: replaceWithBr(
dataStorage.jobResponsibilities
?.split("The end")
?.slice(0, dataStorage.positions.length)[i]
),
}}
className="-mt-8 font-normal px-2 py-2"
/>
</div>
</div>
))}
</div>
<div></div>
</div>
<ReactToPrint
trigger={() => {
return (
<button className="bg-cyan-700 transition-all hover:translate-x-1 mb-7 cursor-pointer text-white px-6 py-2 rounded-2xl flex justify-self-center ml-2 sm:ml-14 mt-10">
Print
</button>
);
}}
content={() => componentRef.current}
/>
</div>
);
};
export default Resume;
In the resume page, the stored data is retrieved and stored inside the dataStorage
variable. This variable is then used to populate the resume page with whatever data is needed, depending on the type of resume you want to build.
To print your resume, you will be using the ReactToPrint
component gotten from react-to-print
package. To do this, create a ref variable called componentRef. This componentRef is then attached to the div that wraps the entire jsx on the Resume page. You can go through the code above to understand better.
At the end of the jsx code, you can add a button for printing and wrap it in the ReactToPrint component like below
<ReactToPrint
trigger={() => {
return (
<button className="bg-cyan-700 transition-all hover:translate-x-1 mb-7 cursor-pointer text-white px-6 py-2 rounded-2xl flex justify-self-center ml-2 sm:ml-14 mt-10">
Print
</button>
);
}}
content={() => componentRef.current}
/>
This will trigger the print method on your browser to come up and you can then either save the page or print it out.
There you have it guys! You just completely built from scratch an optimized resume builder using the chat GPT API. Have fun with it and improve on the code.
The source code for this project can be found here
Top comments (0)