长话短说
在本文中,您将学习如何使用 React、Node.js 和 OpenAI API 创建简历生成器。
找工作并说你已经用 AI 建立了一个工作简历生成器,这有什么更好的呢?🤗
一个小小的要求🥺
我每周制作内容,您的支持对创建更多内容有很大帮助。请为我们的 GitHub 库加注星标来支持我。非常非常感谢你!❤️
OpenAI API 简介
GPT-3是OpenAI开发的一种人工智能程序,非常擅长理解和处理人类语言。它已经接受了来自互联网的大量文本数据的训练,这使得它能够对范围广泛的与语言相关的任务生成高质量的响应。
对于本文,我们将使用 OpenAI GPT3。
一旦 ChatGPT API 发布,我将使用它创建另一篇文章🤗
从他们发布第一个 API 的那一天起我就是 OpenAI 的忠实粉丝,我已经求助于其中一名员工并向他们发送了一个很好的请求以获取访问权限GPT3 的 beta 版本,我得到了它 😀
那是我在 年 12 月 30 日
项目设置
在这里,我将指导您为 Web 应用程序创建项目环境。我们将在前端使用 React.js,在后端服务器使用 Node.js。
通过运行以下代码为 Web 应用程序创建项目文件夹:
mkdir resume-buildercd resume-buildermkdir 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 服务器。当您在浏览器中访问时,下面的代码片段会返回一个 JSON 对象http://localhost:4000/api。
//👇🏻index.jsconstexpress=require("express");constcors=require("cors");constapp=express();constPORT=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}`);});
通过将启动命令添加到文件中的脚本列表来配置 Nodemon package.json。下面的代码片段使用 Nodemon 启动服务器。
//In server/package.json"scripts": {"test": "echo \"Error: no test specified\" && exit 1","start": "nodemon index.js"},
恭喜!您现在可以使用以下命令启动服务器。
npm start
设置 React 应用程序
通过终端导航到客户端文件夹并创建一个新的 React.js 项目。
cd clientnpx create-react-app ./
安装 Axios 和 React 路由器。 React Router是一个 JavaScript 库,它使我们能够在 React 应用程序的页面之间导航。 Axios是一个基于 promise 的 Node.js HTTP 客户端,用于执行异步请求。
npm install axios react-router-dom
从 React 应用程序中删除多余的文件,例如徽标和测试文件,并更新文件App.js以显示 Hello World,如下所示。
functionApp(){return(<div><p>Hello World!</p></div>);}exportdefaultApp;
导航到该src/index.css文件并复制下面的代码。它包含设置此项目样式所需的所有 CSS。
@importurl("/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:10px20px;border-radius:3px;outline:none;border:1pxsolid#ddd;}h3{margin:15px0;}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:0auto;min-height:10vh;background-color:#e8e2e2;padding:30px;border-radius:3px3px00;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:0auto;padding:30px;min-height:80vh;border:1pxsolid#e0e0ea;}.resumeBodyTitle{margin-bottom:5px;}.resumeBodyContent{text-align:justify;margin-bottom:30px;}
构建应用程序用户界面
在这里,我们将为简历生成器应用程序创建用户界面,使用户能够提交他们的信息并打印 AI 生成的简历。
client/src在包含Home.js, Loading.js, Resume.js,文件的文件夹中创建一个组件文件夹ErrorPage.js。
cd client/srcmkdir componentstouch Home.js Loading.js Resume.js ErrorPage.js
从上面的代码片段:
该Home.js文件呈现表单字段以使用户能够输入必要的信息。
包含Loading.js请求挂起时向用户显示的组件。
向用户显示Resume.jsAI 生成的简历。
ErrorPage.js发生错误时显示。
更新App.js文件以使用 React Router 呈现组件。
importReactfrom"react";import{BrowserRouter,Routes,Route}from"react-router-dom";importHomefrom"./components/Home";importResumefrom"./components/Resume";constApp=()=>{return(<div><BrowserRouter><Routes><Routepath='/'element={<Home/>}/><Routepath='/resume'element={<Resume/>}/></Routes></BrowserRouter></div>);};exportdefaultApp;
主页
在这里,您将学习如何构建可以通过 HTTP 请求发送图像并动态添加和删除输入字段的表单布局。
首先,更新 Loading 组件以呈现下面的代码片段,在简历挂起时显示给用户。
importReactfrom"react";constLoading=()=>{return(<divclassName='app'><h1>Loading, please wait...</h1></div>);};exportdefaultLoading;
接下来,更新ErrorPage.js文件以在用户直接导航到简历页面时显示下面的组件。
importReactfrom"react";import{Link}from"react-router-dom";constErrorPage=()=>{return(<divclassName='app'><h3>You've not provided your details. Kindly head back to the{" "}<Linkto='/'>homepage</Link>.</h3></div>);};exportdefaultErrorPage;
将下面的代码片段复制到Home.js文件中
importReact,{useState}from"react";importLoadingfrom"./Loading";constHome=()=>{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);consthandleFormSubmit=(e)=>{e.preventDefault();console.log({fullName,currentPosition,currentLength,currentTechnologies,headshot,});setLoading(true);};//👇🏻 Renders the Loading component you submit the formif(loading){return<Loading/>;}return(<divclassName='app'><h1>Resume Builder</h1><p>Generate a resume with ChatGPT in few seconds</p><formonSubmit={handleFormSubmit}method='POST'encType='multipart/form-data'><labelhtmlFor='fullName'>Enter your full name</label><inputtype='text'requiredname='fullName'id='fullName'value={fullName}onChange={(e)=>setFullName(e.target.value)}/><divclassName='nestedContainer'><div><labelhtmlFor='currentPosition'>Current Position</label><inputtype='text'requiredname='currentPosition'className='currentInput'value={currentPosition}onChange={(e)=>setCurrentPosition(e.target.value)}/></div><div><labelhtmlFor='currentLength'>For how long? (year)</label><inputtype='number'requiredname='currentLength'className='currentInput'value={currentLength}onChange={(e)=>setCurrentLength(e.target.value)}/></div><div><labelhtmlFor='currentTechnologies'>Technologies used</label><inputtype='text'requiredname='currentTechnologies'className='currentInput'value={currentTechnologies}onChange={(e)=>setCurrentTechnologies(e.target.value)}/></div></div><labelhtmlFor='photo'>Upload your headshot image</label><inputtype='file'name='photo'requiredid='photo'accept='image/x-png,image/jpeg'onChange={(e)=>setHeadshot(e.target.files[0])}/><button>CREATE RESUME</button></form></div>);};exportdefaultHome;
代码片段呈现下面的表单字段。它接受全名和当前工作经验(年份、职位、头衔),并允许用户通过表单字段上传头像。
最后,您需要接受用户以前的工作经验。因此,添加一个新状态来保存职位描述数组。
const[companyInfo,setCompanyInfo]=useState([{name:"",position:""}]);
添加以下有助于更新状态的功能。
//👇🏻 updates the state with user's inputconsthandleAddCompany=()=>setCompanyInfo([...companyInfo,{name:"",position:""}]);//👇🏻 removes a selected item from the listconsthandleRemoveCompany=(index)=>{constlist=[...companyInfo];list.splice(index,1);setCompanyInfo(list);};//👇🏻 updates an item within the listconsthandleUpdateCompany=(e,index)=>{const{name,value}=e.target;constlist=[...companyInfo];list[index][name]=value;setCompanyInfo(list);};
使用用户输入更新状态,用于handleAddCompany从提供的数据列表中删除项目,并更新列表中的项目属性(名称和位置)。companyInfohandleRemoveCompanyhandleUpdateCompany
接下来,呈现工作体验部分的 UI 元素。
return(<divclassName='app'><h3>Companies you've worked at</h3><form>{/*--- other UI tags --- */}{companyInfo.map((company,index)=>(<divclassName='nestedContainer'key={index}><divclassName='companies'><labelhtmlFor='name'>Company Name</label><inputtype='text'name='name'requiredonChange={(e)=>handleUpdateCompany(e,index)}/></div><divclassName='companies'><labelhtmlFor='position'>Position Held</label><inputtype='text'name='position'requiredonChange={(e)=>handleUpdateCompany(e,index)}/></div><divclassName='btn__group'>{companyInfo.length-1===index&&companyInfo.length<4&&(<buttonid='addBtn'onClick={handleAddCompany}>Add</button>)}{companyInfo.length>1&&(<buttonid='deleteBtn'onClick={()=>handleRemoveCompany(index)}>Del</button>)}</div></div>))}<button>CREATE RESUME</button></form></div>);
代码片段映射数组中的元素companyInfo并将它们显示在网页上。该handleUpdateCompany函数在用户更新输入字段时运行,然后handleRemoveCompany从元素列表中删除一个项目,然后handleAddCompany添加一个新的输入字段。
动图
简历页面
此页面以可打印格式显示从 OpenAI API 生成的简历。将下面的代码复制到Resume.js文件中。我们将在本教程稍后更新其内容。
importReactfrom"react";importErrorPagefrom"./ErrorPage";constResume=({result})=>{if(JSON.stringify(result)==="{}"){return<ErrorPage/>;}consthandlePrint=()=>alert("Print Successful!");return(<><buttononClick={handlePrint}>Print Page</button><mainclassName='container'><p>Hello!</p></main></>);};
如何在 Node.js 中通过表单提交图片
在这里,我将指导您如何将表单数据提交到 Node.js 服务器。 由于表单包含图像,我们需要在 Node.js 服务器上设置 Multer 。
💡 Multer 是一个 Node.js 中间件,用于上传文件到服务器。
设置 Multer
运行下面的代码来安装 Multer
npminstallmulter
确保前端应用程序上的表单具有方法和encType属性,因为 Multer 只处理多部分的表单。
<formmethod="POST"enctype="multipart/form-data"></form>
将 Multer 和 Node.js 路径包导入到index.js文件中。
constmulter=require("multer");constpath=require("path");
将下面的代码复制到index.js配置 Multer。
app.use("/uploads",express.static("uploads"));conststorage=multer.diskStorage({destination:(req,file,cb)=>{cb(null,"uploads");},filename:(req,file,cb)=>{cb(null,Date.now()+path.extname(file.originalname));},});constupload=multer({storage:storage,limits:{fileSize:1024*1024*5},});
从上面的代码片段:
该app.use()函数使 Node.js 能够提供文件夹的内容uploads。内容是指图像、CSS 和 JavaScript 文件等静态文件。
storagecontaining 变量让multer.diskStorage我们可以完全控制存储图像。上面的函数将图像存储在上传文件夹中,并将图像重命名为其上传时间(以防止文件名冲突)。
upload 变量将配置传递给 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 服务器。
importaxiosfrom"axios";consthandleFormSubmit=(e)=>{e.preventDefault();constformData=newFormData();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 端点。如果有响应,它会记录响应并将用户重定向到 Resume 页面。
如何在 Node.js 中与 OpenAI API 通信
在本节中,您将学习如何在 Node.js 服务器中与 OpenAI API 进行通信。
我们会将用户的信息发送到 API,以生成个人资料摘要、职位描述以及在之前的组织中完成的成就或相关活动。要做到这一点:
通过运行以下代码安装 OpenAI API Node.js 库。
npm install openai
在此处登录或创建 OpenAI 帐户 。
点击Personal导航栏,View API keys在菜单栏中选择新建秘钥。
将 API 密钥复制到计算机上安全的地方;我们很快就会用到它。
通过将以下代码复制到文件中来配置 API index.js。
const{Configuration,OpenAIApi}=require("openai");constconfiguration=newConfiguration({apiKey:"<YOUR_API_KEY>",});constopenai=newOpenAIApi(configuration);
创建一个接受文本(提示)作为参数并返回 AI 生成结果的函数。
constGPTFunction=async(text)=>{constresponse=awaitopenai.createCompletion({model:"text-davinci-003",prompt:text,temperature:0.6,max_tokens:250,top_p:1,frequency_penalty:1,presence_penalty:1,});returnresponse.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;constworkArray=JSON.parse(workHistory);//an array//👇🏻 group the values into an objectconstnewEntry={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 stringconstremainderText=()=>{letstringText="";for(leti=0;i<workArray.length;i++){stringText+=` ${workArray[i].name} as a ${workArray[i].position}.`;}returnstringText;};//👇🏻 The job description promptconstprompt1=`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 promptconstprompt2=`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 promptconstprompt3=`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 resultconstobjective=awaitGPTFunction(prompt1);constkeypoints=awaitGPTFunction(prompt2);constjobResponsibilities=awaitGPTFunction(prompt3);//👇🏻 put them into an objectconstchatgptData={objective,keypoints,jobResponsibilities};//👇🏻log the resultconsole.log(chatgptData);
从上面的代码片段:
该remainderText函数循环遍历工作历史数组并返回所有工作经历的字符串数据类型。
然后,会出现三个提示,其中包含有关 GPT-3 API 所需内容的说明。
接下来,将结果存储在一个对象中并将它们记录到控制台。
最后返回AI生成的结果和用户输入的信息。您还可以创建一个数组来表示存储结果的数据库,如下所示。
letdatabase=[];app.post("/resume/create",upload.single("headshotImage"),async(req,res)=>{//...other code statementsconstdata={...newEntry,...chatgptData};database.push(data);res.json({message:"Request successful!",data,});});
显示来自 OpenAI API 的响应
在本节中,我将指导您以可读和可打印的格式在网页上显示从 OpenAI API 生成的结果。
在文件中创建一个 React 状态App.js。该状态将保存从 Node.js 服务器发送的结果。
importReact,{useState}from"react";import{BrowserRouter,Routes,Route}from"react-router-dom";importHomefrom"./components/Home";importResumefrom"./components/Resume";constApp=()=>{//👇🏻 state holding the resultconst[result,setResult]=useState({});return(<div><BrowserRouter><Routes><Routepath='/'element={<HomesetResult={setResult}/>}/><Routepath='/resume'element={<Resumeresult={result}/>}/></Routes></BrowserRouter></div>);};exportdefaultApp;
从上面的代码片段中,onlysetResult作为 prop 传递到 Home 组件中,并且仅result用于 Resume 组件。setResult提交表单并且请求成功后更新结果的值,同时result包含从服务器检索的响应,显示在 Resume 组件中。
result提交表单并请求成功后更新Home 组件内的状态。
constHome=({setResult})=>{consthandleFormSubmit=(e)=>{e.preventDefault();//...other code statementsaxios.post("http://localhost:4000/resume/create",formData,{}).then((res)=>{if(res.data.message){//👇🏻 updates the result objectsetResult(res.data.data);navigate("/resume");}}).catch((err)=>console.error(err));setLoading(true);};return<div></div>;};
如下所示更新 Resume 组件以在 React 应用程序中预览结果。
importErrorPagefrom"./ErrorPage";constResume=({result})=>{//👇🏻 function that replaces the new line with a break tagconstreplaceWithBr=(string)=>{returnstring.replace(/\n/g,"<br />");};//👇🏻 returns an error page if the result object is emptyif(JSON.stringify(result)==="{}"){return<ErrorPage/>;}consthandlePrint=()=>alert("Printing");return(<><buttononClick={handlePrint}>Print Page</button><mainclassName='container'ref={componentRef}><headerclassName='header'><div><h1>{result.fullName}</h1><pclassName='resumeTitle headerTitle'>{result.currentPosition} ({result.currentTechnologies})</p><pclassName='resumeTitle'>{result.currentLength}year(s) work experience</p></div><div><imgsrc={result.image_url}alt={result.fullName}className='resumeImage'/></div></header><divclassName='resumeBody'><div><h2className='resumeBodyTitle'>PROFILE SUMMARY</h2><pdangerouslySetInnerHTML={{__html:replaceWithBr(result.objective),}}className='resumeBodyContent'/></div><div><h2className='resumeBodyTitle'>WORK HISTORY</h2>{result.workHistory.map((work)=>(<pclassName='resumeBodyContent'key={work.name}><spanstyle={{fontWeight:"bold"}}>{work.name}</span> -{" "}{work.position}</p>))}</div><div><h2className='resumeBodyTitle'>JOB PROFILE</h2><pdangerouslySetInnerHTML={{__html:replaceWithBr(result.jobResponsibilities),}}className='resumeBodyContent'/></div><div><h2className='resumeBodyTitle'>JOB RESPONSIBILITIES</h2><pdangerouslySetInnerHTML={{__html:replaceWithBr(result.keypoints),}}className='resumeBodyContent'/></div></div></main></>);};
上面的代码片段根据指定的布局在网页上显示结果。该函数replaceWithBr将每个新行 (\n) 替换为一个中断标记,并且该handlePrint函数将使用户能够打印简历。
如何使用 React-to-print 包打印 React 页面
在这里,您将学习如何向网页添加打印按钮,使用户能够通过 React-to-print包打印简历。
💡 React-to-print 是一个简单的 JavaScript 包,它使您能够在不篡改组件 CSS 样式的情况下打印 React 组件的内容。
运行下面的代码来安装包
npminstallreact-to-print
在文件中导入库Resume.js并添加useRef挂钩。
import{useReactToPrint}from"react-to-print";importReact,{useRef}from"react";
Resume.js如下所示更新文件。
constResume=({result})=>{constcomponentRef=useRef();consthandlePrint=useReactToPrint({content:()=>componentRef.current,documentTitle:`${result.fullName} Resume`,onAfterPrint:()=>alert("Print Successful!"),});//...other function statementsreturn(<><buttononClick={handlePrint}>Print Page</button><mainclassName='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,您可以创建功能强大的应用程序,在各个领域都有用,例如翻译、问答、代码解释或生成等。
本教程的源代码可在此处获得:
/novuhq/blog/tree/main/resume-builder-with-react-chatgpt-nodejs
感谢您的阅读!