DEV Community

Cover image for Using ChatGPT and React JS to Create an Automated Cover Letter and Resume Builder
Busayo Samuel
Busayo Samuel

Posted on

Using ChatGPT and React JS to Create an Automated Cover Letter and Resume Builder

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

  1. You should be familiar with React JS and CSS
  2. 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;

  1. Click on the avatar on the navbar with your profile picture to open up the dropdown menu
  2. Select "view API keys" to create an API key.
  3. Keep the API key in a safe place, you will make use of it later in the tutorial.

Browser image for open ai website

GETTING STARTED

To get the ball rolling;

  1. create a folder in your documents folder and name it "resume_builder" or any name you prefer.
  2. Go to your code editor and open the folder.
  3. In your terminal, enter npx create-react-app@latest ./ to create a react JS starter file in your root folder.
  4. 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 enter npm 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;

Enter fullscreen mode Exit fullscreen mode

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

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>

Enter fullscreen mode Exit fullscreen mode

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

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: "" },
    ],
  });
Enter fullscreen mode Exit fullscreen mode

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 });
  };
Enter fullscreen mode Exit fullscreen mode

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.
Resume builder form

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 onChangeis 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;
  }
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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);
    }
  };
Enter fullscreen mode Exit fullscreen mode

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;

  1. A list of soft skills
  2. A list of job responsibilities
  3. A list of hard skills
  4. 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));
  };
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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}
      />
Enter fullscreen mode Exit fullscreen mode

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

Latest comments (0)