Using ChatGPT and React JS to Create an Automated Cover Letter and Resume Builder
Tags: ai, react, webdev, openai
Published: 2023-04-02 12:55:10



## 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](https://platform.openai.com/). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ld1dz2bchsw9yvcq1ph8.png)

## 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`.

```javascript
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

![home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzzjel2wsrldb6h93zww.png)
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 `.

```javascript
  <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.
![resume](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b0bzxvs3zlhbqcdrp5q0.png)

## ADDING FUNCTIONALITY TO THE RESUME FORM

The form component is controlled using react `useState` using the code below.

```javascript
 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.

```javascript
 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.
![Resume builder form ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kjdckmpqza0ljip45yro.png)

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 

```javascript
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

```javascript
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`.

``` javascript
 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;
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.

```javascript
 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.

```javascript
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

```javascript
 <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](https://github.com/bellatrick/resume-builder)