🚀使用 NextJS、Trigger.dev 和 GPT4 创建简历生成器🔥✨

2025-05-28

🚀使用 NextJS、Trigger.dev 和 GPT4 创建简历生成器🔥✨

TL;DR

在本文中,您将学习如何使用 NextJS、Trigger.dev、Resend 和 OpenAI 创建简历生成器。😲

  • 添加基本​​详细信息,例如名字、姓氏和最后工作地点。
  • 生成个人资料摘要、工作经历和工作职责等详细信息。
  • 创建包含所有信息的 PDF。
  • 将所有内容发送到您的电子邮件

猴子桌


您的后台工作平台🔌

Trigger.dev 是一个开源库,可让您使用 NextJS、Remix、Astro 等为您的应用程序创建和监控长时间运行的作业!

 

给我们星星

请帮我们点个星🥹。
这有助于我们创作更多类似的文章💖

为 Trigger.dev 仓库加星标⭐️


让我们开始吧🔥

使用 NextJS 设置新项目



npx create-next-app@latest


Enter fullscreen mode Exit fullscreen mode

我们将创建一个包含基本信息的简单表格,例如:

  • 电子邮件
  • 您的个人资料图片
  • 以及你至今为止的经历!

输入

我们将使用 NextJS 的新应用路由器。
打开layout.tsx并添加以下代码



import { GeistSans } from "geist/font";
import "./globals.css";

const defaultUrl = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : "http://localhost:3000";

export const metadata = {
  metadataBase: new URL(defaultUrl),
  title: "Resume Builder with GPT4",
  description: "The fastest way to build a resume with GPT4",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" className={GeistSans.className}>
      <body className="bg-background text-foreground">
        <main className="min-h-screen flex flex-col items-center">
          {children}
        </main>
      </body>
    </html>
  );
}


Enter fullscreen mode Exit fullscreen mode

我们基本上设置了所有页面的布局(即使我们只有一个页面)。
我们设置了基本的页面元数据,背景和全局 CSS 元素。

接下来,我们打开page.tsx并添加以下代码:



    <div className="flex-1 w-full flex flex-col items-center">
      <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
        <div className="w-full max-w-6xl flex justify-between items-center p-3 text-sm">
          <span className="font-bold select-none">resumeGPT.</span>
        </div>
      </nav>

      <div className="animate-in flex-1 flex flex-col opacity-0 max-w-6xl px-3">
        <Home />
      </div>
    </div>


Enter fullscreen mode Exit fullscreen mode

这设置了我们的 resumeGPT 的标题和主要主页组件。


构建表单的最简单方法

保存表单信息和验证字段的最简单方法是使用 react-hook-form。

我们要上传一张头像。
为此,我们不能使用基于 JSON 的请求。
我们需要将 JSON 转换为有效的表单数据。

因此让我们将它们全部安装起来!



npm install react-hook-form object-to-formdata axios --save


Enter fullscreen mode Exit fullscreen mode

创建一个名为 components 的新文件夹,添加一个名为 的新文件Home.tsx,并添加以下代码:



"use client";

import React, { useState } from "react";
import {FormProvider, useForm} from "react-hook-form";
import Companies from "@/components/Companies";
import axios from "axios";
import {serialize} from "object-to-formdata";

export type TUserDetails = {
  firstName: string;
  lastName: string;
  photo: string;
  email: string;
  companies: TCompany[];
};

export type TCompany = {
  companyName: string;
  position: string;
  workedYears: string;
  technologies: string;
};

const Home = () => {
  const [finished, setFinished] = useState<boolean>(false);
  const methods = useForm<TUserDetails>()

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = methods;

  const handleFormSubmit = async (values: TUserDetails) => {
    axios.post('/api/create', serialize(values));
    setFinished(true);
  };

  if (finished) {
    return (
        <div className="mt-10">Sent to the queue! Check your email</div>
    )
  }

  return (
    <div className="flex flex-col items-center justify-center p-7">
      <div className="w-full py-3 bg-slate-500 items-center justify-center flex flex-col rounded-t-lg text-white">
        <h1 className="font-bold text-white text-3xl">Resume Builder</h1>
        <p className="text-gray-300">
          Generate a resume with GPT in seconds 🚀
        </p>
      </div>
      <FormProvider {...methods}>
        <form
          onSubmit={handleSubmit(handleFormSubmit)}
          className="p-4 w-full flex flex-col"
        >
          <div className="flex flex-col lg:flex-row gap-4">
            <div className="flex flex-col w-full">
              <label htmlFor="firstName">First name</label>
              <input
                type="text"
                required
                id="firstName"
                placeholder="e.g. John"
                className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent"
                {...register('firstName')}
              />
            </div>
            <div className="flex flex-col w-full">
              <label htmlFor="lastName">Last name</label>
              <input
                type="text"
                required
                id="lastName"
                placeholder="e.g. Doe"
                className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent"
                {...register('lastName')}
              />
            </div>
          </div>
          <hr className="w-full h-1 mt-3" />
          <label htmlFor="email">Email Address</label>
          <input
            type="email"
            required
            id="email"
            placeholder="e.g. john.doe@gmail.com"
            className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent"
            {...register('email', {required: true, pattern: /^\S+@\S+$/i})}
          />
          <hr className="w-full h-1 mt-3" />
          <label htmlFor="photo">Upload your image 😎</label>
          <input
            type="file"
            id="photo"
            accept="image/x-png"
            className="p-3 rounded-md outline-none border border-gray-500 mb-3"
            {...register('photo', {required: true})}
          />
          <Companies />
          <button className="p-4 pointer outline-none bg-blue-500 border-none text-white text-base font-semibold rounded-lg">
            CREATE RESUME
          </button>
        </form>
      </FormProvider>
    </div>
  );
};

export default Home;


Enter fullscreen mode Exit fullscreen mode

您可以看到,我们首先"use client"告诉我们的组件它应该仅在客户端上运行。

为什么我们只需要客户端?
React 状态(输入更改)仅在客户端可用。

我们设置了两个接口,TUserDetailsTCompany。它们代表我们正在处理的数据的结构。

我们使用useFormwith react-hook-form。它为我们的输入创建本地状态管理,并允许我们轻松更新和验证我们的字段。您可以看到,在每个 中input,都有一个简单的register函数,指定输入名称和验证并将其注册到托管状态。

这很酷,因为我们不需要玩类似onChange

您还可以看到我们使用,这很重要,因为我们希望在子组件中FormProvider拥有的上下文。react-hook-form

我们还有一个名为 的方法handleFormSubmit。该方法会在我们提交表单后被调用。您可以看到,我们使用该serialize函数将 JavaScript 对象转换为 FormData,并向服务器发送请求以启动作业axios

你还可以看到另一个名为 的组件Companies。该组件可以让我们指定我们工作过的所有公司。

那么让我们开始吧。
创建一个名为 的新文件Companies.tsx
,并添加以下代码:



import React, {useCallback, useEffect} from "react";

import { TCompany } from "./Home";
import {useFieldArray, useFormContext} from "react-hook-form";

const Companies = () => {
  const {control, register} = We();
  const {fields: companies, append} = useFieldArray({
    control,
    name: "companies",
  });

  const addCompany = useCallback(() => {
    append({
      companyName: '',
      position: '',
      workedYears: '',
      technologies: ''
    })
  }, [companies]);


useEffect(() => {
    addCompany();
  }, []);

  return (
    <div className="mb-4">
      {companies.length > 1 ? (
        <h3 className="font-bold text-white text-3xl my-3">
          Your list of Companies:
        </h3>
      ) : null}
      {companies.length > 1 &&
        companies.slice(1).map((company, index) => (
          <div
            key={index}
            className="mb-4 p-4 border bg-gray-800 rounded-lg shadow-md"
          >
            <div className="mb-2">
              <label htmlFor={`companyName-${index}`} className="text-white">
                Company Name
              </label>
              <input
                type="text"
                id={`companyName-${index}`}
                className="p-2 border border-gray-300 rounded-md w-full bg-transparent"
                {...register(`companies.${index}.companyName`, {required: true})}
              />
            </div>

            <div className="mb-2">
              <label htmlFor={`position-${index}`} className="text-white">
                Position
              </label>
              <input
                type="text"
                id={`position-${index}`}
                className="p-2 border border-gray-300 rounded-md w-full bg-transparent"
                {...register(`companies.${index}.position`, {required: true})}
              />
            </div>

            <div className="mb-2">
              <label htmlFor={`workedYears-${index}`} className="text-white">
                Worked Years
              </label>
              <input
                  type="number"
                  id={`workedYears-${index}`}
                  className="p-2 border border-gray-300 rounded-md w-full bg-transparent"
                  {...register(`companies.${index}.workedYears`, {required: true})}
              />
            </div>
            <div className="mb-2">
              <label htmlFor={`workedYears-${index}`} className="text-white">
                Technologies
              </label>
              <input
                  type="text"
                  id={`technologies-${index}`}
                  className="p-2 border border-gray-300 rounded-md w-full bg-transparent"
                  {...register(`companies.${index}.technologies`, {required: true})}
              />
            </div>
          </div>
        ))}
        <button type="button" onClick={addCompany} className="mb-4 p-2 pointer outline-none bg-blue-900 w-full border-none text-white text-base font-semibold rounded-lg">
          Add Company
        </button>
    </div>
  );
};

export default Companies;


Enter fullscreen mode Exit fullscreen mode

我们从开始useFormContext,它允许我们获取父组件的上下文。

接下来,我们useFieldArray创建一个名为“companies”的新状态。它是一个包含我们所有公司的数组。

在中useEffect,我们添加数组的第一个项来对其进行迭代。

当单击时addCompany,它会将另一个元素推送到数组中。

我们和客户的事情已经结束了🥳


解析 HTTP 请求

还记得我们发送了一个POST请求吗/api/create
让我们进入 app/api 文件夹,并create在该文件夹内创建一个名为 的新文件夹,创建一个名为 的新文件route.tsx,并粘贴以下代码:



import {NextRequest, NextResponse} from "next/server";
import {client} from "@/trigger";

export async function POST(req: NextRequest) {
    const data = await req.formData();
    const allArr = {
        name: data.getAll('companies[][companyName]'),
        position: data.getAll('companies[][position]'),
        workedYears: data.getAll('companies[][workedYears]'),
        technologies: data.getAll('companies[][technologies]'),
    };

    const payload = {
        firstName: data.get('firstName'),
        lastName: data.get('lastName'),
        photo: Buffer.from((await (data.get('photo[0]') as File).arrayBuffer())).toString('base64'),
        email: data.get('email'),
        companies: allArr.name.map((name, index) => ({
            companyName: allArr.name[index],
            position: allArr.position[index],
            workedYears: allArr.workedYears[index],
            technologies: allArr.technologies[index],
        })).filter((company) => company.companyName && company.position && company.workedYears && company.technologies)
    }

    await client.sendEvent({
        name: 'create.resume',
        payload
    });

    return NextResponse.json({ })
}


Enter fullscreen mode Exit fullscreen mode

此代码仅适用于 NodeJS 20 及以上版本。如果您的版本较低,则无法解析 FormData。

该代码非常简单。

  • 我们使用以下方式将请求解析为 FormDatareq.formData
  • 我们将基于 FormData 的请求转换为 JSON 文件。
  • 我们提取图像并将其转换为base64
  • 我们将所有内容发送给 TriggerDev

制作简历并发送到你的邮箱📨

建立简历是一项长期任务,我们需要

  • 使用 ChatGPT 生成内容。
  • 创建 PDF
  • 发送至您的邮箱

由于一些原因,我们不想通过长时间运行的 HTTP 请求来完成所有这些操作。

  1. 部署到 Vercel 时,无服务器函数有 10 秒的限制。我们永远无法按时完成。
  2. 我们希望避免用户长时间停留。这会带来糟糕的用户体验。如果用户关闭窗口,整个流程就会失败。

隆重推出 Trigger.dev!

使用 Trigger.dev,您可以在 NextJS 应用内运行后台进程!无需创建新的服务器。
它们还知道如何将长时间运行的作业无缝拆分成多个短任务来处理。

注册一个Trigger.dev 账户。注册后,创建一个组织并为你的工作选择一个项目名称。

创建组织

选择 Next.js 作为您的框架,并按照将 Trigger.dev 添加到现有 Next.js 项目的过程进行操作。

下一个

否则,请单击 Environments & API Keys 项目仪表板的侧边栏菜单。

复制

复制您的 DEV 服务器 API 密钥,并运行以下代码片段来安装 Trigger.dev。请仔细按照说明操作。



npx @trigger.dev/cli@latest init


Enter fullscreen mode Exit fullscreen mode

在另一个终端中,运行以下代码片段以在 Trigger.dev 和 Next.js 项目之间建立隧道。



npx @trigger.dev/cli@latest dev


Enter fullscreen mode Exit fullscreen mode

让我们创建 TriggerDev 作业!
前往新创建的文件夹 jobs,并创建一个名为 的新文件create.resume.ts

添加以下代码:



client.defineJob({
  id: "create-resume",
  name: "Create Resume",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "create.resume",
    schema: z.object({
      firstName: z.string(),
      lastName: z.string(),
      photo: z.string(),
      email: z.string().email(),
      companies: z.array(z.object({
        companyName: z.string(),
        position: z.string(),
        workedYears: z.string(),
        technologies: z.string()
      }))
    }),
  }),
  run: async (payload, io, ctx) => {
  }
});


Enter fullscreen mode Exit fullscreen mode

这将为我们创建一个名为 的新作业create-resume
如您所见,我们之前从 发送的请求有一个模式验证route.tsx。这将为我们提供验证 和auto-completion

我们将在这里运行三个作业

  • ChatGPT
  • PDF创建
  • 电子邮件发送

让我们从 ChatGPT 开始。

创建一个 OpenAI 帐户 并生成一个 API 密钥。

ChatGPT

单击View API key下拉菜单即可创建 API 密钥。

API密钥

接下来,通过运行下面的代码片段来安装 OpenAI 包。



npm install @trigger.dev/openai


Enter fullscreen mode Exit fullscreen mode

将您的 OpenAI API 密钥添加到.env.local文件中。



OPENAI_API_KEY=<your_api_key>


Enter fullscreen mode Exit fullscreen mode

在根目录中创建一个名为 的新文件夹utils
在该目录中,创建一个名为 的新文件,openai.ts
添加以下代码:



import { OpenAI } from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY!,
});

export async function generateResumeText(prompt: string) {
  const response = await openai.completions.create({
    model: "text-davinci-003",
    prompt,
    max_tokens: 250,
    temperature: 0.7,
    top_p: 1,
    frequency_penalty: 1,
    presence_penalty: 1,
  });

  return response.choices[0].text.trim();
}

export const prompts = {
  profileSummary: (fullName: string, currentPosition: string, workingExperience: string, knownTechnologies: string) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). \n I write in the technologies: ${knownTechnologies}. Can you write a 100 words description for the top of the resume(first person writing)?`,
  jobResponsibilities: (fullName: string, currentPosition: string, workingExperience: string, knownTechnologies: string) =>  `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). \n I write in the technolegies: ${knownTechnologies}. Can you write 3 points for a resume on what I am good at?`,
  workHistory: (fullName: string, currentPosition: string, workingExperience: string, details: TCompany[]) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). ${companyDetails(details)} \n Can you write me 50 words for each company seperated in numbers of my succession in the company (in first person)?`,
};


Enter fullscreen mode Exit fullscreen mode

这段代码基本上创建了使用 ChatGPT 的基础架构,以及 3 个函数profileSummary, 、workingExperienceworkHistory。我们将使用它们来创建各个部分的内容。

返回我们的create.resume.ts并添加新工作:



import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
import { prompts } from "@/utils/openai";
import { TCompany, TUserDetails } from "@/components/Home";

const companyDetails = (companies: TCompany[]) => {
  let stringText = "";
  for (let i = 1; i < companies.length; i++) {
    stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`;
  }
  return stringText;
};

client.defineJob({
  id: "create-resume",
  name: "Create Resume",
  version: "0.0.1",
  integrations: {
      resend
  },
  trigger: eventTrigger({
    name: "create.resume",
    schema: z.object({
      firstName: z.string(),
      lastName: z.string(),
      photo: z.string(),
      email: z.string().email(),
      companies: z.array(z.object({
        companyName: z.string(),
        position: z.string(),
        workedYears: z.string(),
        technologies: z.string()
      }))
    }),
  }),
  run: async (payload, io, ctx) => {

    const texts = await io.runTask("openai-task", async () => {
      return Promise.all([
          await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies))
      ]);
    });
  },
});


Enter fullscreen mode Exit fullscreen mode

我们创建了一个名为 的新任务openai-task
在该任务中,我们同时使用 ChatGPT 运行三个提示,并返回它们。


创建 PDF

创建 PDF 的方法有很多种

  • 您可以使用 HTML2CANVAS 之类的工具将 HTML 代码转换为图像,然后转换为 PDF。
  • 您可以使用诸如puppeteer抓取网页并将其转换为 PDF 之类的工具。
  • 您可以使用可以在后端创建 PDF 的不同库。

在我们的例子中,我们将使用一个名为jsPdf“非常简单的库”的库来在后端创建 PDF。我鼓励您使用 Puppeteer 和更多 HTML 创建一些更强大的 PDF 文件。

那么让我们安装它



npm install jspdf @typs/jspdf --save


Enter fullscreen mode Exit fullscreen mode

让我们返回utils并创建一个名为 的新文件resume.ts。该文件基本上会创建一个 PDF 文件,我们可以将其发送到用户的电子邮箱。

添加以下内容:



import {TUserDetails} from "@/components/Home";
import {jsPDF} from "jspdf";

type ResumeProps = {
  userDetails: TUserDetails;
  picture: string;
  profileSummary: string;
  workHistory: string;
  jobResponsibilities: string;
};

export function createResume({ userDetails, picture, workHistory, jobResponsibilities, profileSummary }: ResumeProps) {
    const doc = new jsPDF();

    // Title block
    doc.setFontSize(24);
    doc.setFont('helvetica', 'bold');

    doc.text(userDetails.firstName + ' ' + userDetails.lastName, 45, 27);
    doc.setLineWidth(0.5);
    doc.rect(20, 15, 170, 20); // x, y, width, height
    doc.addImage({
        imageData: picture,
        x: 25,
        y: 17,
        width: 15,
        height: 15
    });

    // Reset font for the rest
    doc.setFontSize(12);
    doc.setFont('helvetica', 'normal');

    // Personal Information block
    doc.setFontSize(14);
    doc.setFont('helvetica', 'bold');
    doc.text('Summary', 20, 50);
    doc.setFontSize(10);
    doc.setFont('helvetica', 'normal');
    const splitText = doc.splitTextToSize(profileSummary, 170);
    doc.text(splitText, 20, 60);

    const newY = splitText.length * 5;

    // Work history block
    doc.setFontSize(14);
    doc.setFont('helvetica', 'bold');
    doc.text('Work History', 20, newY + 65);
    doc.setFontSize(10);
    doc.setFont('helvetica', 'normal');
    const splitWork = doc.splitTextToSize(workHistory, 170);
    doc.text(splitWork, 20, newY + 75);

    const newNewY = splitWork.length * 5;

    // Job Responsibilities block
    doc.setFontSize(14);
    doc.setFont('helvetica', 'bold');
    doc.text('Job Responsibilities', 20, newY + newNewY + 75);
    doc.setFontSize(10);
    doc.setFont('helvetica', 'normal');
    const splitJob = doc.splitTextToSize(jobResponsibilities, 170);
    doc.text(splitJob, 20, newY + newNewY + 85);

    return doc.output("datauristring");
}


Enter fullscreen mode Exit fullscreen mode

该文件包含三个部分:Personal Information、、Work historyJob Responsibilities块。

我们计算每个区块的位置和内容。

一切都已设置好absolute
值得注意的是,splitTextToSize文本被分成多行,这样就不会超出屏幕。

恢复

现在,让我们创建下一个任务:resume.ts再次打开并添加以下代码:



import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
import { prompts } from "@/utils/openai";
import { TCompany, TUserDetails } from "@/components/Home";
import { createResume } from "@/utils/resume";

const companyDetails = (companies: TCompany[]) => {
  let stringText = "";
  for (let i = 1; i < companies.length; i++) {
    stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`;
  }
  return stringText;
};

client.defineJob({
  id: "create-resume",
  name: "Create Resume",
  version: "0.0.1",
  integrations: {
      resend
  },
  trigger: eventTrigger({
    name: "create.resume",
    schema: z.object({
      firstName: z.string(),
      lastName: z.string(),
      photo: z.string(),
      email: z.string().email(),
      companies: z.array(z.object({
        companyName: z.string(),
        position: z.string(),
        workedYears: z.string(),
        technologies: z.string()
      }))
    }),
  }),
  run: async (payload, io, ctx) => {

    const texts = await io.runTask("openai-task", async () => {
      return Promise.all([
          await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies))
      ]);
    });

    console.log('passed chatgpt');

    const pdf = await io.runTask('convert-to-html', async () => {
        const resume = createResume({
            userDetails: payload,
            picture: payload.photo,
            profileSummary: texts[0],
            jobResponsibilities: texts[1],
            workHistory: texts[2],
        });

        return {final: resume.split(',')[1]}
    });

    console.log('converted to pdf');
  },
});


Enter fullscreen mode Exit fullscreen mode

您可以看到我们添加了一个名为 的新任务convert-to-html。这将为我们创建 PDF,将其转换为 base64 并返回。


让他们知道🎤

我们快要结束了!
剩下的就是把它分享给用户了。
你可以使用任何你喜欢的电子邮件服务。
我们将使用 Resend.com

访问 注册页面,创建一个帐户和一个 API 密钥,并将其保存到.env.local文件中。



RESEND_API_KEY=<place_your_API_key>


Enter fullscreen mode Exit fullscreen mode

钥匙

将Trigger.dev Resend 集成包安装到您的 Next.js 项目中。



npm install @trigger.dev/resend


Enter fullscreen mode Exit fullscreen mode

剩下要做的就是添加我们最后一项任务!
幸运的是,Trigger 直接与 Resend 集成,因此我们不需要创建新的“常规”任务。

最终代码如下:



import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
import { prompt } from "@/utils/openai";
import { TCompany, TUserDetails } from "@/components/Home";
import { createResume } from "@/utils/resume";
import { Resend } from "@trigger.dev/resend";

const resend = new Resend({
    id: "resend",
    apiKey: process.env.RESEND_API_KEY!,
});

const companyDetails = (companies: TCompany[]) => {
  let stringText = "";
  for (let i = 1; i < companies.length; i++) {
    stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`;
  }
  return stringText;
};

client.defineJob({
  id: "create-resume",
  name: "Create Resume",
  version: "0.0.1",
  integrations: {
      resend
  },
  trigger: eventTrigger({
    name: "create.resume",
    schema: z.object({
      firstName: z.string(),
      lastName: z.string(),
      photo: z.string(),
      email: z.string().email(),
      companies: z.array(z.object({
        companyName: z.string(),
        position: z.string(),
        workedYears: z.string(),
        technologies: z.string()
      }))
    }),
  }),
  run: async (payload, io, ctx) => {

    const texts = await io.runTask("openai-task", async () => {
      return Promise.all([
          await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)),
          await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies))
      ]);
    });

    console.log('passed chatgpt');

    const pdf = await io.runTask('convert-to-html', async () => {
        const resume = createResume({
            userDetails: payload,
            picture: payload.photo,
            profileSummary: texts[0],
            jobResponsibilities: texts[1],
            workHistory: texts[2],
        });

        return {final: resume.split(',')[1]}
    });

    console.log('converted to pdf');

    await io.resend.sendEmail('send-email', {
        to: payload.email,
        subject: 'Resume',
        html: 'Your resume is attached!',
        attachments: [
            {
                filename: 'resume.pdf',
                content: Buffer.from(pdf.final, 'base64'),
                contentType: 'application/pdf',
            }
        ],
        from: "Nevo David <nevo@gitup.dev>",
    });

    console.log('Sent email');
  },
});


Enter fullscreen mode Exit fullscreen mode

我们Resend在文件顶部有一个实例,并从仪表板加载了我们的 API 密钥。

我们有



  integrations: {
      resend
  },


Enter fullscreen mode Exit fullscreen mode

我们将其添加到我们的作业中,以便稍后在 中使用io
最后,我们的作业是发送 PDFio.resend.sendEmail

值得注意的是里面的附件是我们在上一步生成的 PDF 文件。

大功告成🎉

我们完成了

您可以在此处检查并运行完整的源代码:
https://github.com/triggerdotdev/blog


让我们联系吧!🔌

作为开源开发者,我们诚邀您加入我们的 社区 ,贡献力量并与维护人员互动。欢迎访问我们的 GitHub 代码库 ,贡献代码并创建与 Trigger.dev 相关的问题。

本教程的源代码可以在这里找到:
https://github.com/triggerdotdev/blog/tree/main/blog-resume-builder

感谢您的阅读!

文章来源:https://dev.to/triggerdotdev/creating-a-resume-builder-with-nextjs-triggerdev-and-gpt4-4gmf
PREV
🧞‍♂️ 生成器已解锁:使用 ChatGPT 和 NextJS 创建模因 🚀 💥 TL;DR
NEXT
🚀 精通 NextJS:使用 Stripe 和 Airtable 构建销售页面 🧙‍♂️🪄✨ TL;DR