簡介

在本文中,您將學習如何使用 NextJS、Trigger.dev、Resend 和 OpenAI 建立簡歷產生器。 😲

  • 加入基本詳細訊息,例如名字、姓氏和最後工作地點。

  • 產生詳細訊息,例如個人資料摘要、工作經歷和工作職責。

  • 建立包含所有資訊的 PDF。

  • 將所有內容傳送到您的電子郵件

猴子手錶


你的後台工作平台🔌

Trigger.dev 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!

 

GiveUsStars

請幫我們一顆星🥹。

這將幫助我們建立更多這樣的文章💖

https://github.com/triggerdotdev/trigger.dev


讓我們來設定一下吧🔥

使用 NextJS 設定一個新專案

npx create-next-app@latest

我們將建立一個包含基本資訊的簡單表單,例如:

  • 電子郵件地址

  • 你的頭像

  • 以及你今天為止的經驗!

輸入

我們將使用 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>
  );
}

我們基本上是為所有頁面設定佈局(即使我們只有一頁。)

我們設定基本的頁面元資料、背景和全域 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>

這設定了我們的resumeGPT 的標題和主要的家庭元件。

<小時/>

建立表單的最簡單方法

保存表單資訊並驗證欄位最簡單的方法是使用react-hook-form。

我們將上傳個人資料照片。

為此,我們不能使用基於 JSON 的請求。

我們需要將 JSON 轉換為有效的表單資料。

那麼就讓我們把它們全部安裝吧!

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

建立一個名為 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. [email protected]"
            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;

您可以看到我們從「使用客戶端」開始,它基本上告訴我們的元件它應該只在客戶端上執行。

為什麼我們只需要客戶端?

React 狀態(輸入變更)僅在用戶端可用。

我們設定兩個接口,「TUserDetails」和「TCompany」。它們代表了我們正在使用的資料的結構。

我們將“useForm”與“react-hook-form”一起使用。它為我們的輸入建立了本地狀態管理,並允許我們輕鬆更新和驗證我們的欄位。您可以看到,在每個「輸入」中,都有一個簡單的「註冊」函數,用於指定輸入名稱和驗證並將其註冊到託管狀態。

這很酷,因為我們不需要使用像“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;

我們從 useFormContext 開始,它允許我們取得父元件的上下文。

接下來,我們使用 useFieldArray 建立一個名為 Companies 的新狀態。這是我們擁有的所有公司的一個陣列。

在「useEffect」中,我們新增陣列的第一項以對其進行迭代。

當點擊“addCompany”時,它會將另一個元素推送到陣列中。

我們已經和客戶完成了🥳


解析HTTP請求

還記得我們向“/api/create”發送了一個“POST”請求嗎?

讓我們轉到 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({ })
}

此程式碼只能在 NodeJS 版本 20+ 上運作。如果版本較低,將無法解析FormData。

該程式碼非常簡單。

  • 我們使用 req.formData 將請求解析為 FormData

  • 我們將基於 FormData 的請求轉換為 JSON 檔案。

  • 我們提取圖像並將其轉換為“base64”

  • 我們將所有內容傳送給 TriggerDev


製作履歷並將其發送到您的電子郵件📨

建立履歷是我們需要的長期任務

  • 使用 ChatGPT 產生內容。

  • 建立 PDF

  • 發送到您的電子郵件

由於某些原因,我們不想發出長時間執行的 HTTP 請求來執行所有這些操作。

  1. 部署到 Vercel 時,無伺服器功能有 10 秒的限制。我們永遠不會準時到達。

2.我們希望讓用戶不會長時間掛起。這是一個糟糕的使用者體驗。如果用戶關閉窗口,整個過程將失敗。

介紹 Trigger.dev!

使用 Trigger.dev,您可以在 NextJS 應用程式內執行後台進程!您不需要建立新伺服器。

他們也知道如何透過將長時間執行的作業無縫地分解為短期任務來處理它們。

註冊 Trigger.dev 帳號。註冊後,建立一個組織並為您的工作選擇一個專案名稱。

CreateOrg

選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。

下一頁

否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。

複製

複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段以安裝 Trigger.dev。仔細按照說明進行操作。

npx @trigger.dev/cli@latest init

在另一個終端中,執行以下程式碼片段以在 Trigger.dev 和 Next.js 專案之間建立隧道。

npx @trigger.dev/cli@latest dev

讓我們建立 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) => {
  }
});

這將為我們建立一個名為「create-resume」的新工作。

如您所見,我們先前從「route.tsx」發送的請求進行了架構驗證。這將為我們提供驗證和“自動完成”。

我們將在這裡執行三項工作

  • 聊天GPT

  • PDF建立

  • 電子郵件發送

讓我們從 ChatGPT 開始。

建立 OpenAI 帳戶 並產生 API 金鑰。

ChatGPT

從下拉清單中按一下「檢視 API 金鑰」以建立 API 金鑰。

ApiKey

接下來,透過執行下面的程式碼片段來安裝 OpenAI 套件。

npm install @trigger.dev/openai

將您的 OpenAI API 金鑰新增至 .env.local 檔案中。

OPENAI_API_KEY=<your_api_key>

在根目錄中建立一個名為「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)?`,
};

這段程式碼基本上建立了使用 ChatGPT 的基礎設施以及 3 個函數:「profileSummary」、「workingExperience」和「workHistory」。我們將使用它們來建立各部分的內容。

返回我們的「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))
      ]);
    });
  },
});

我們建立了一個名為「openai-task」的新任務。

在該任務中,我們使用 ChatGPT 同時執行三個提示,並返回它們。


建立 PDF

建立 PDF 的方法有很多種

  • 您可以使用 HTML2CANVAS 等工具並將 HTML 程式碼轉換為映像,然後轉換為 PDF。

  • 您可以使用「puppeteer」之類的工具來抓取網頁並將其轉換為 PDF。

  • 您可以使用不同的庫在後端建立 PDF。

在我們的例子中,我們將使用一個名為「jsPdf」的簡單函式庫,它是在後端建立 PDF 的非常簡單的函式庫。我鼓勵您使用 Puppeteer 和更多 HTML 來建立一些更強大的 PDF 檔案。

那我們來安裝它

npm install jspdf @typs/jspdf --save

讓我們回到「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");
}

該文件包含三個部分:「個人資訊」、「工作歷史」和「工作職責」區塊。

我們計算每個區塊的位置和內容。

一切都是以“絕對”的方式設置的。

值得注意的是“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');
  },
});

您可以看到我們新增了一個名為「convert-to-html」的新任務。這將為我們建立 PDF,將其轉換為 base64 並返回。


讓他們知道🎤

我們即將到達終點!

剩下的唯一一件事就是與用戶分享。

您可以使用任何您想要的電子郵件服務。

我們將使用 Resend.com

造訪註冊頁面,建立帳戶和 API 金鑰,並將其儲存到 .env.local 檔案中。

RESEND_API_KEY=<place_your_API_key>

密鑰

Trigger.dev Resend 整合套件 安裝到您的 Next.js 專案。

npm install @trigger.dev/resend

剩下要做的就是加入我們的最後一項工作!

幸運的是,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 <[email protected]>",
    });

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

我們在檔案頂部的「Resend」實例載入了儀表板中的 API 金鑰。

我們有

  integrations: {
      resend
  },

我們將其加入到我們的作業中,以便稍後在“io”內部使用。

最後,我們的工作是發送 PDF io.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


共有 0 則留言