使用 React、NodeJS 和 AI 🚀 创建简历生成器
TL;DR
TL;DR
在本文中,你将学习如何使用 React、Node.js 和 OpenAI API 创建简历生成器。
找工作时,如果说你已经用 AI 构建了一个简历生成器,还有什么比这更好的呢?🤩
一个小请求🥺
我每周都会创作内容,您的支持对创作更多内容非常有帮助。请为我们的 GitHub 库点赞,支持我。非常感谢!❤️
https://github.com/novuhq/novu
OpenAI API 简介
GPT-3 是由 OpenAI 开发的一种人工智能程序,非常擅长理解和处理人类语言。它已经接受了来自互联网的大量文本数据的训练,这使得它能够对各种与语言相关的任务生成高质量的响应。
本文将使用 OpenAI GPT3。ChatGPT
API 发布后,我会用它写另一篇文章 🤗
从 OpenAI 发布第一个 API 的那天起,我就一直是它的忠实粉丝。我联系了他们的一位员工,并向他们发送了 GPT3 测试版的访问请求,现在终于如愿以偿了 😅
项目设置
在这里,我将指导您创建 Web 应用程序的项目环境。我们将使用 React.js 作为前端,使用 Node.js 作为后端服务器。
通过运行以下代码为 Web 应用程序创建项目文件夹:
mkdir resume-builder
cd resume-builder
mkdir client server
设置 Node.js 服务器
导航到服务器文件夹并创建一个package.json
文件。
cd server & npm init -y
安装 Express、Nodemon 和 CORS 库。
npm install express cors nodemon
ExpressJS是一个快速、简约的框架,它提供了在 Node.js 中构建 Web 应用程序的多种功能, CORS是一个允许不同域之间通信的 Node.js 包, Nodemon是一个在检测到文件更改后自动重启服务器的 Node.js 工具。
创建一个index.js
文件——Web 服务器的入口点。
touch index.js
使用 Express.js 设置 Node.js 服务器。当您http://localhost:4000/api
在浏览器中访问时,下面的代码片段会返回一个 JSON 对象。
//👇🏻index.js
const express = require("express");
const cors = require("cors");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());
app.get("/api", (req, res) => {
res.json({
message: "Hello world",
});
});
app.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
通过将启动命令添加到package.json
文件中的脚本列表中来配置 Nodemon。下面的代码片段使用 Nodemon 启动服务器。
//In server/package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
恭喜!您现在可以使用以下命令启动服务器。
npm start
设置 React 应用程序
通过终端导航到客户端文件夹并创建一个新的 React.js 项目。
cd client
npx create-react-app ./
安装 Axios 和 React Router。React Router是一个 JavaScript 库,它使我们能够在 React 应用程序中的页面之间导航。Axios 是一个基于 Promise 的 Node.js HTTP 客户端,用于执行异步请求。
npm install axios react-router-dom
从 React 应用程序中删除冗余文件(例如徽标和测试文件),并更新App.js
文件以显示如下所示的 Hello World。
function App() {
return (
<div>
<p>Hello World!</p>
</div>
);
}
export default App;
导航到src/index.css
文件并复制以下代码。它包含设计此项目所需的所有 CSS。
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
* {
font-family: "Space Grotesk", sans-serif;
box-sizing: border-box;
margin: 0;
padding: 0;
}
form {
padding: 10px;
width: 80%;
display: flex;
flex-direction: column;
}
input {
margin-bottom: 15px;
padding: 10px 20px;
border-radius: 3px;
outline: none;
border: 1px solid #ddd;
}
h3 {
margin: 15px 0;
}
button {
padding: 15px;
cursor: pointer;
outline: none;
background-color: #5d3891;
border: none;
color: #f5f5f5;
font-size: 16px;
font-weight: bold;
border-radius: 3px;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30px;
}
.app > p {
margin-bottom: 30px;
}
.nestedContainer {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.companies {
display: flex;
flex-direction: column;
width: 39%;
}
.currentInput {
width: 95%;
}
#photo {
width: 50%;
}
#addBtn {
background-color: green;
margin-right: 5px;
}
#deleteBtn {
background-color: red;
}
.container {
min-height: 100vh;
padding: 30px;
}
.header {
width: 80%;
margin: 0 auto;
min-height: 10vh;
background-color: #e8e2e2;
padding: 30px;
border-radius: 3px 3px 0 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.resumeTitle {
opacity: 0.6;
}
.headerTitle {
margin-bottom: 15px;
}
.resumeImage {
vertical-align: middle;
width: 150px;
height: 150px;
border-radius: 50%;
}
.resumeBody {
width: 80%;
margin: 0 auto;
padding: 30px;
min-height: 80vh;
border: 1px solid #e0e0ea;
}
.resumeBodyTitle {
margin-bottom: 5px;
}
.resumeBodyContent {
text-align: justify;
margin-bottom: 30px;
}
构建应用程序用户界面
在这里,我们将为简历生成器应用程序创建用户界面,以使用户能够提交他们的信息并打印 AI 生成的简历。
client/src
在包含Home.js
、Loading.js
、Resume.js
、文件的文件夹中创建一个 components 文件夹ErrorPage.js
。
cd client/src
mkdir components
touch Home.js Loading.js Resume.js ErrorPage.js
从上面的代码片段来看:
- 该
Home.js
文件呈现表单字段,以使用户能够输入必要的信息。 - 包含
Loading.js
请求待处理时向用户显示的组件。 - 向用户显示
Resume.js
AI 生成的简历。 ErrorPage.js
发生错误时会显示。
更新App.js
文件以使用 React Router 呈现组件。
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./components/Home";
import Resume from "./components/Resume";
const App = () => {
return (
<div>
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/resume' element={<Resume />} />
</Routes>
</BrowserRouter>
</div>
);
};
export default App;
主页
在这里,您将学习如何构建一个可以通过 HTTP 请求发送图像并动态添加和删除输入字段的表单布局。
首先,更新 Loading 组件以呈现下面的代码片段,在简历待处理时显示给用户。
import React from "react";
const Loading = () => {
return (
<div className='app'>
<h1>Loading, please wait...</h1>
</div>
);
};
export default Loading;
接下来,更新ErrorPage.js
文件以在用户直接导航到简历页面时显示下面的组件。
import React from "react";
import { Link } from "react-router-dom";
const ErrorPage = () => {
return (
<div className='app'>
<h3>
You've not provided your details. Kindly head back to the{" "}
<Link to='/'>homepage</Link>.
</h3>
</div>
);
};
export default ErrorPage;
将下面的代码片段复制到Home.js
文件中
import React, { useState } from "react";
import Loading from "./Loading";
const Home = () => {
const [fullName, setFullName] = useState("");
const [currentPosition, setCurrentPosition] = useState("");
const [currentLength, setCurrentLength] = useState(1);
const [currentTechnologies, setCurrentTechnologies] = useState("");
const [headshot, setHeadshot] = useState(null);
const [loading, setLoading] = useState(false);
const handleFormSubmit = (e) => {
e.preventDefault();
console.log({
fullName,
currentPosition,
currentLength,
currentTechnologies,
headshot,
});
setLoading(true);
};
//👇🏻 Renders the Loading component you submit the form
if (loading) {
return <Loading />;
}
return (
<div className='app'>
<h1>Resume Builder</h1>
<p>Generate a resume with ChatGPT in few seconds</p>
<form
onSubmit={handleFormSubmit}
method='POST'
encType='multipart/form-data'
>
<label htmlFor='fullName'>Enter your full name</label>
<input
type='text'
required
name='fullName'
id='fullName'
value={fullName}
onChange={(e) => setFullName(e.target.value)}
/>
<div className='nestedContainer'>
<div>
<label htmlFor='currentPosition'>Current Position</label>
<input
type='text'
required
name='currentPosition'
className='currentInput'
value={currentPosition}
onChange={(e) => setCurrentPosition(e.target.value)}
/>
</div>
<div>
<label htmlFor='currentLength'>For how long? (year)</label>
<input
type='number'
required
name='currentLength'
className='currentInput'
value={currentLength}
onChange={(e) => setCurrentLength(e.target.value)}
/>
</div>
<div>
<label htmlFor='currentTechnologies'>Technologies used</label>
<input
type='text'
required
name='currentTechnologies'
className='currentInput'
value={currentTechnologies}
onChange={(e) => setCurrentTechnologies(e.target.value)}
/>
</div>
</div>
<label htmlFor='photo'>Upload your headshot image</label>
<input
type='file'
name='photo'
required
id='photo'
accept='image/x-png,image/jpeg'
onChange={(e) => setHeadshot(e.target.files[0])}
/>
<button>CREATE RESUME</button>
</form>
</div>
);
};
export default Home;
代码片段渲染了下面的表单字段。它接受全名和当前工作经历(年份、职位、头衔),并允许用户通过表单字段上传头像。
最后,你需要接受用户之前的工作经历。因此,添加一个保存职位描述数组的新状态。
const [companyInfo, setCompanyInfo] = useState([{ name: "", position: "" }]);
添加以下有助于更新状态的功能。
//👇🏻 updates the state with user's input
const handleAddCompany = () =>
setCompanyInfo([...companyInfo, { name: "", position: "" }]);
//👇🏻 removes a selected item from the list
const handleRemoveCompany = (index) => {
const list = [...companyInfo];
list.splice(index, 1);
setCompanyInfo(list);
};
//👇🏻 updates an item within the list
const handleUpdateCompany = (e, index) => {
const { name, value } = e.target;
const list = [...companyInfo];
list[index][name] = value;
setCompanyInfo(list);
};
使用用户的输入handleAddCompany
更新状态,用于从提供的数据列表中删除一个项目,并更新列表内的项目属性 - (名称和位置)。companyInfo
handleRemoveCompany
handleUpdateCompany
接下来,渲染工作经验部分的 UI 元素。
return (
<div className='app'>
<h3>Companies you've worked at</h3>
<form>
{/*--- other UI tags --- */}
{companyInfo.map((company, index) => (
<div className='nestedContainer' key={index}>
<div className='companies'>
<label htmlFor='name'>Company Name</label>
<input
type='text'
name='name'
required
onChange={(e) => handleUpdateCompany(e, index)}
/>
</div>
<div className='companies'>
<label htmlFor='position'>Position Held</label>
<input
type='text'
name='position'
required
onChange={(e) => handleUpdateCompany(e, index)}
/>
</div>
<div className='btn__group'>
{companyInfo.length - 1 === index && companyInfo.length < 4 && (
<button id='addBtn' onClick={handleAddCompany}>
Add
</button>
)}
{companyInfo.length > 1 && (
<button id='deleteBtn' onClick={() => handleRemoveCompany(index)}>
Del
</button>
)}
</div>
</div>
))}
<button>CREATE RESUME</button>
</form>
</div>
);
该代码片段映射了数组中的元素companyInfo
并将其显示在网页上。该handleUpdateCompany
函数在用户更新输入字段,然后handleRemoveCompany
从元素列表中删除一项并handleAddCompany
添加新的输入字段时运行。
简历页面
本页面以可打印的格式展示了由 OpenAI API 生成的简历。请将以下代码复制到Resume.js
文件中。我们将在本教程的后续部分更新其内容。
import React from "react";
import ErrorPage from "./ErrorPage";
const Resume = ({ result }) => {
if (JSON.stringify(result) === "{}") {
return <ErrorPage />;
}
const handlePrint = () => alert("Print Successful!");
return (
<>
<button onClick={handlePrint}>Print Page</button>
<main className='container'>
<p>Hello!</p>
</main>
</>
);
};
如何在 Node.js 中通过表单提交图像
这里,我将指导你如何将表单数据提交到 Node.js 服务器。由于表单包含图像,我们需要在 Node.js 服务器上设置 Multer 。
💡 [Multer](https://www.npmjs.com/package/multer) 是一个用于将文件上传到服务器的 Node.js 中间件。设置Multer
运行下面的代码来安装 Multer
npm install multer
确保前端应用程序上的表单具有方法和encType
属性,因为 Multer 仅处理多部分的表单。
<form method="POST" enctype="multipart/form-data"></form>
将 Multer 和 Node.js 路径包导入index.js
文件中。
const multer = require("multer");
const path = require("path");
将下面的代码复制到index.js
配置Multer中。
app.use("/uploads", express.static("uploads"));
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads");
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
},
});
const upload = multer({
storage: storage,
limits: { fileSize: 1024 * 1024 * 5 },
});
- 从上面的代码片段来看:
- 该
app.use()
函数使 Node.js 能够提供文件夹内容uploads
。这些内容指的是静态文件,例如图像、CSS 和 JavaScript 文件。 - 变量
storage
“containing”multer.diskStorage
赋予我们存储图像的完全控制权。上面的函数将图像存储在上传文件夹中,并将图像重命名为其上传时间(以防止文件名冲突)。 - 上传变量将配置传递给 Multer 并为图像设置 5MB 的大小限制。
- 该
在服务器上创建uploads
文件夹。图像将保存在此处。
mkdir uploads
如何将图像上传到 Node.js 服务器
添加一个路由,用于接收来自 React 应用的所有表单输入。该upload.single("headshotImage")
函数将通过表单上传的图片添加到uploads
文件夹中。
app.post("/resume/create", upload.single("headshotImage"), async (req, res) => {
const {
fullName,
currentPosition,
currentLength,
currentTechnologies,
workHistory,
} = req.body;
console.log(req.body);
res.json({
message: "Request successful!",
data: {},
});
});
更新组件handleFormSubmit
内的函数Home.js
以将表单数据提交到 Node.js 服务器。
import axios from "axios";
const handleFormSubmit = (e) => {
e.preventDefault();
const formData = new FormData();
formData.append("headshotImage", headshot, headshot.name);
formData.append("fullName", fullName);
formData.append("currentPosition", currentPosition);
formData.append("currentLength", currentLength);
formData.append("currentTechnologies", currentTechnologies);
formData.append("workHistory", JSON.stringify(companyInfo));
axios
.post("http://localhost:4000/resume/create", formData, {})
.then((res) => {
if (res.data.message) {
console.log(res.data.data);
navigate("/resume");
}
})
.catch((err) => console.error(err));
setLoading(true);
};
上面的代码片段创建了一个键/值对,代表表单字段及其值,并通过 Axios 发送到服务器上的 API 端点。如果有响应,它会记录响应并将用户重定向到“简历”页面。
如何在 Node.js 中与 OpenAI API 进行通信
在本节中,您将学习如何在 Node.js 服务器中与 OpenAI API 通信。
我们会将用户信息发送到 API,以生成个人资料摘要、职位描述以及在之前机构完成的成就或相关活动。为此:
通过运行以下代码安装 OpenAI API Node.js 库。
npm install openai
在此登录或创建 OpenAI 帐户 。
点击Personal
导航栏,View API keys
从菜单栏中选择创建一个新的密钥。
将 API 密钥复制到计算机上的安全位置;我们很快就会用到它。
通过将以下代码复制到index.js
文件中来配置 API。
const { Configuration, OpenAIApi } = require("openai");
const configuration = new Configuration({
apiKey: "<YOUR_API_KEY>",
});
const openai = new OpenAIApi(configuration);
创建一个接受文本(提示)作为参数并返回 AI 生成结果的函数。
const GPTFunction = async (text) => {
const response = await openai.createCompletion({
model: "text-davinci-003",
prompt: text,
temperature: 0.6,
max_tokens: 250,
top_p: 1,
frequency_penalty: 1,
presence_penalty: 1,
});
return response.data.choices[0].text;
};
上面的代码片段使用text-davinci-003
模型来生成针对提示的适当答案。其他键值帮助我们生成所需的特定类型的响应。
/resume/create
按照如下所示更新路线。
app.post("/resume/create", upload.single("headshotImage"), async (req, res) => {
const {
fullName,
currentPosition,
currentLength,
currentTechnologies,
workHistory, //JSON format
} = req.body;
const workArray = JSON.parse(workHistory); //an array
//👇🏻 group the values into an object
const newEntry = {
id: generateID(),
fullName,
image_url: `http://localhost:4000/uploads/${req.file.filename}`,
currentPosition,
currentLength,
currentTechnologies,
workHistory: workArray,
};
});
上面的代码片段接受来自客户端的表单数据,转换workHistory
为其原始数据结构(数组),并将它们全部放入一个对象中。
接下来,创建您想要传递到的提示GPTFunction
。
//👇🏻 loops through the items in the workArray and converts them to a string
const remainderText = () => {
let stringText = "";
for (let i = 0; i < workArray.length; i++) {
stringText += ` ${workArray[i].name} as a ${workArray[i].position}.`;
}
return stringText;
};
//👇🏻 The job description prompt
const prompt1 = `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${currentLength} years). \n I write in the technolegies: ${currentTechnologies}. Can you write a 100 words description for the top of the resume(first person writing)?`;
//👇🏻 The job responsibilities prompt
const prompt2 = `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${currentLength} years). \n I write in the technolegies: ${currentTechnologies}. Can you write 10 points for a resume on what I am good at?`;
//👇🏻 The job achievements prompt
const prompt3 = `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${currentLength} years). \n During my years I worked at ${
workArray.length
} companies. ${remainderText()} \n Can you write me 50 words for each company seperated in numbers of my succession in the company (in first person)?`;
//👇🏻 generate a GPT-3 result
const objective = await GPTFunction(prompt1);
const keypoints = await GPTFunction(prompt2);
const jobResponsibilities = await GPTFunction(prompt3);
//👇🏻 put them into an object
const chatgptData = { objective, keypoints, jobResponsibilities };
//👇🏻log the result
console.log(chatgptData);
- 从上面的代码片段来看:
- 该
remainderText
函数循环遍历工作经历数组,并返回所有工作经历的字符串数据类型。 - 然后,会出现三个提示,说明 GPT-3 API 需要什么。
- 接下来,将结果存储在一个对象中并将其记录到控制台。
- 该
最后,返回 AI 生成的结果和用户输入的信息。您还可以创建一个表示存储结果的数据库的数组,如下所示。
let database = [];
app.post("/resume/create", upload.single("headshotImage"), async (req, res) => {
//...other code statements
const data = { ...newEntry, ...chatgptData };
database.push(data);
res.json({
message: "Request successful!",
data,
});
});
显示来自 OpenAI API 的响应
在本节中,我将指导您以可读和可打印的格式在网页上显示从 OpenAI API 生成的结果。
在文件中创建一个 React 状态App.js
。该状态将保存从 Node.js 服务器发送的结果。
import React, { useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./components/Home";
import Resume from "./components/Resume";
const App = () => {
//👇🏻 state holding the result
const [result, setResult] = useState({});
return (
<div>
<BrowserRouter>
<Routes>
<Route path='/' element={<Home setResult={setResult} />} />
<Route path='/resume' element={<Resume result={result} />} />
</Routes>
</BrowserRouter>
</div>
);
};
export default App;
从上面的代码片段中,只有setResult
作为 prop 传递到 Home 组件,并且仅result
传递给 Resume 组件。setResult
一旦表单提交并且请求成功,就会更新结果的值,而result
包含从服务器检索到的响应,显示在 Resume 组件中。
result
表单提交且请求成功后更新Home组件内的状态。
const Home = ({ setResult }) => {
const handleFormSubmit = (e) => {
e.preventDefault();
//...other code statements
axios
.post("http://localhost:4000/resume/create", formData, {})
.then((res) => {
if (res.data.message) {
//👇🏻 updates the result object
setResult(res.data.data);
navigate("/resume");
}
})
.catch((err) => console.error(err));
setLoading(true);
};
return <div></div>;
};
按照如下所示更新 Resume 组件,以在 React 应用程序中预览结果。
import ErrorPage from "./ErrorPage";
const Resume = ({ result }) => {
//👇🏻 function that replaces the new line with a break tag
const replaceWithBr = (string) => {
return string.replace(/\n/g, "<br />");
};
//👇🏻 returns an error page if the result object is empty
if (JSON.stringify(result) === "{}") {
return <ErrorPage />;
}
const handlePrint = () => alert("Printing");
return (
<>
<button onClick={handlePrint}>Print Page</button>
<main className='container' ref={componentRef}>
<header className='header'>
<div>
<h1>{result.fullName}</h1>
<p className='resumeTitle headerTitle'>
{result.currentPosition} ({result.currentTechnologies})
</p>
<p className='resumeTitle'>
{result.currentLength}year(s) work experience
</p>
</div>
<div>
<img
src={result.image_url}
alt={result.fullName}
className='resumeImage'
/>
</div>
</header>
<div className='resumeBody'>
<div>
<h2 className='resumeBodyTitle'>PROFILE SUMMARY</h2>
<p
dangerouslySetInnerHTML={{
__html: replaceWithBr(result.objective),
}}
className='resumeBodyContent'
/>
</div>
<div>
<h2 className='resumeBodyTitle'>WORK HISTORY</h2>
{result.workHistory.map((work) => (
<p className='resumeBodyContent' key={work.name}>
<span style={{ fontWeight: "bold" }}>{work.name}</span> -{" "}
{work.position}
</p>
))}
</div>
<div>
<h2 className='resumeBodyTitle'>JOB PROFILE</h2>
<p
dangerouslySetInnerHTML={{
__html: replaceWithBr(result.jobResponsibilities),
}}
className='resumeBodyContent'
/>
</div>
<div>
<h2 className='resumeBodyTitle'>JOB RESPONSIBILITIES</h2>
<p
dangerouslySetInnerHTML={{
__html: replaceWithBr(result.keypoints),
}}
className='resumeBodyContent'
/>
</div>
</div>
</main>
</>
);
};
上面的代码片段根据指定的布局在网页上显示结果。该函数replaceWithBr
将每个新行 (\n) 替换为换行标记,并且该handlePrint
函数将允许用户打印简历。
如何使用 React-to-print 包打印 React 页面
在这里,您将学习如何向网页添加打印按钮,使用户能够通过 React-toprint包打印简历。
💡 React-to-print 是一个简单的 JavaScript 包,它使您能够打印 React 组件的内容,而无需篡改组件 CSS 样式。
运行下面的代码来安装包
npm install react-to-print
在文件中导入库Resume.js
并添加useRef
钩子。
import { useReactToPrint } from "react-to-print";
import React, { useRef } from "react";
Resume.js
按照如下所示更新文件。
const Resume = ({ result }) => {
const componentRef = useRef();
const handlePrint = useReactToPrint({
content: () => componentRef.current,
documentTitle: `${result.fullName} Resume`,
onAfterPrint: () => alert("Print Successful!"),
});
//...other function statements
return (
<>
<button onClick={handlePrint}>Print Page</button>
<main className='container' ref={componentRef}>
{/*---other code statements---*/}
</main>
</>
);
};
该handlePrint
函数打印 -main 标签内的元素componentRef
,将文档的名称设置为用户的全名,并在用户打印表单时运行警报函数。
恭喜!您已完成本教程的项目。
以下是该项目所获得结果的示例:
结论
到目前为止,您已经学习了:
- OpenAI GPT-3 是什么
- 如何在 Node.js 和 React.js 应用程序中通过表单上传图像,
- 如何与 OpenAI GPT-3 API 交互,以及
- 如何通过 React-to-print 库打印 React 网页。
本教程将引导您了解使用 OpenAI API 构建的应用程序示例。借助该 API,您可以创建功能强大的应用程序,应用于各个领域,例如翻译、问答、代码解释或生成等。
本教程的源代码可以在这里找到:
https://github.com/novuhq/blog/tree/main/resume-builder-with-react-chatgpt-nodejs
感谢您的阅读!
文章来源:https://dev.to/novu/creating-a-resume-builder-with-react-nodejs-and-ai-4k6l