完整教程展示用Next.js和OpenAI API构建AI对话应用,涵盖前后端集成、LLM调用和交互设计的全过程。
本文将介绍如何使用 Nextjs、CopilotKit 和 OpenAI 构建一款由 AI 驱动的简历生成应用。我们将涵盖以下内容:
利用 ChatGPT 编写简历和求职信 📑
通过与简历对话逐步改进简历 💬
将简历和求职信导出为 PDF 🖨️
CopilotKit:构建深度集成于应用内的 AI 聊天机器人 💬
先简单介绍一下我们。CopilotKit 是一个开源 AI 副驾驶平台,可以帮助你轻松地将强大的 AI 聊天机器人集成到 React 应用中,并且支持完全自定义和深度集成。
现在回到本文。
在开始本教程之前,你需要在计算机上安装以下工具:
文本编辑器(例如 Visual Studio Code)
npx create-next-app@latest
npm install preline
resume/app/page.tsx 文件中添加以下代码export default function Home() {
return (
<>
<header className="flex flex-wrap sm:justify-start sm:flex-nowrap z-50 w-full bg-slate-900 bg-gradient-to-b from-violet-600/[.15] via-transparent text-sm py-3 sm:py-0 dark:bg-gray-800 dark:border-gray-700">
<nav
className="relative max-w-7xl w-full mx-auto px-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8 "
aria-label="Global">
<div className="flex items-center justify-between">
<a
className="flex-none text-xl text-gray-200 font-semibold dark:text-white py-8"
href="#"
aria-label="Brand">
ResumeBuilder
</a>
</div>
</nav>
</header>
{/* <!-- Hero --> */}
<div className="bg-slate-900 h-screen">
<div className="bg-gradient-to-b from-violet-600/[.15] via-transparent">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8 py-24 space-y-8">
{/* <!-- Title --> */}
<div className="max-w-3xl text-center mx-auto pt-10">
<h1 className="block font-medium text-gray-200 text-4xl sm:text-5xl md:text-6xl lg:text-7xl">
Craft A Compelling Resume With AI Resume Builder
</h1>
</div>
{/* <!-- End Title --> */}
<div className="max-w-3xl text-center mx-auto">
<p className="text-lg text-gray-400">
ResumeBuilder helps you create a resume that effectively
highlights your skills and experience.
</p>
</div>
{/* <!-- Buttons --> */}
<div className="text-center">
<a
className="inline-flex justify-center items-center gap-x-3 text-center bg-gradient-to-tl from-blue-600 to-violet-600 shadow-lg shadow-transparent hover:shadow-blue-700/50 border border-transparent text-white text-sm font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 focus:ring-offset-white py-3 px-6 dark:focus:ring-offset-gray-800"
href="#">
Get started
<svg
className="flex-shrink-0 w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path d="m9 18 6-6-6-6" />
</svg>
</a>
</div>
{/* <!-- End Buttons --> */}
</div>
</div>
</div>
{/* <!-- End Hero --> */}
</>
);
}
npm run dev。访问 http://localhost:3000/,你应该能看到新创建的 NextJS 项目npm i axios
app 文件夹中创建一个名为 API 的文件夹。然后在 API 文件夹中创建一个名为 GitHub 的文件夹。在 GitHub 文件夹中创建一个名为 route.ts 的文件,并添加以下代码import { NextResponse } from "next/server";
import axios from "axios";
// Environment variables for GitHub API token and user details
const GITHUB_TOKEN = "Your GitHub personal access token";
const GITHUB_USERNAME = "Your GitHub account username";
// Axios instance for GitHub GraphQL API
const githubApi = axios.create({
baseURL: "https://api.github.com/graphql",
headers: {
Authorization: `bearer ${GITHUB_TOKEN}`,
"Content-Type": "application/json",
},
});
// GraphQL query to fetch user and repository data
const getUserAndReposQuery = `
query {
user(login: "${GITHUB_USERNAME}") {
name
email
company
bio
repositories(first: 3, orderBy: {field: CREATED_AT, direction: DESC}) {
edges {
node {
name
url
description
createdAt
... on Repository {
primaryLanguage{
name
}
stargazers {
totalCount
}
}
}
}
}
}
}
`;
// API route to handle resume data fetching
export async function GET(request: any) {
try {
// Fetch data from GitHub
const response = await githubApi.post("", { query: getUserAndReposQuery });
const userData = response.data.data.user;
// Format resume data
const resumeData = {
name: userData.name,
email: userData.email,
company: userData.company,
bio: userData.bio,
repositories: userData.repositories.edges.map((repo: any) => ({
name: repo.node.name,
url: repo.node.url,
created: repo.node.createdAt,
description: repo.node.description,
language: repo.node.primaryLanguage.name,
stars: repo.node.stargazers.totalCount,
})),
};
// Return formatted resume data
return NextResponse.json(resumeData);
} catch (error) {
console.error("Error fetching data from GitHub:", error);
return NextResponse.json({ message: "Internal Server Error" });
}
}
app 文件夹中创建一个名为 Components 的文件夹。然后在 components 文件夹中创建一个名为 githubdata.tsx 的文件,并添加以下代码"use client";
import React, { useEffect, useState } from "react";
import axios from "axios";
// Resume data interface
interface ResumeData {
name: string;
email: string;
company: string;
bio: string;
repositories: {
name: string;
url: string;
created: string;
description: string;
language: string;
stars: number;
}[];
}
export const useGithubData = () => {
const [resumeData, setResumeData] = useState<ResumeData | null>(null);
// Fetch resume data from API
useEffect(() => {
axios
.get("/api/github")
.then((response) => {
setResumeData(response.data);
})
}, []);
return { resumeData, };
}
npm i @copilotkit/react-core @copilotkit/react-ui @copilotkit/react-textarea
components 文件夹中创建一个名为 resume.tsx 的文件。然后在文件顶部导入 useMakeCopilotReadable、useMakeCopilotActionable 和 useGithubData 自定义 Hook,如下所示import React, { useState } from "react";
import { useGithubData } from "./githubdata";
import {
useMakeCopilotReadable,
useMakeCopilotActionable,
} from "@copilotkit/react-core";
CoverLetterAndResume 的组件。在该组件内部,使用 useGithubData Hook 获取从 GitHub 提取的数据。然后声明一个名为 createCoverLetterAndResume 的状态变量,以及一个用于更新它的 setCreateCoverLetterAndResume 函数。使用包含 letter 和 resume 两个属性的对象初始化 useState,如下所示export const CoverLetterAndResume = () => {
const {resumeData } = useGithubData();
const [createCoverLetterAndResume, setCreateCoverLetterAndResume] = useState({
letter: "",
resume: ""
});
}
useMakeCopilotReadable Hook,将从 GitHub 获取的数据添加为应用内聊天机器人的上下文useMakeCopilotReadable(JSON.stringify(resumeData));
使用 useMakeCopilotActionable hook 设置一个名为 createCoverLetterAndResume 的操作,包含描述和实现函数,该函数用提供的求职信和简历更新 createCoverLetterAndResume 状态,如下所示。
useMakeCopilotActionable(
{
name: "createCoverLetterAndResume",
description:
"Create a cover letter and resume for a software developer job application.",
argumentAnnotations: [
{
name: "coverLetterMarkdown",
type: "string",
description:
"Markdown text for a cover letter to introduce yourself and briefly summarize your professional background as a software developer.",
required: true,
},
{
name: "resumeMarkdown",
type: "string",
description:
"Markdown text for a resume that displays your professional background and relevant skills.",
required: true,
},
],
implementation: async (coverLetterMarkdown, resumeMarkdown) => {
setCreateCoverLetterAndResume((prevState) => ({
...prevState,
letter: coverLetterMarkdown,
resume: resumeMarkdown,
}));
},
},
[]
);
在 CoverLetterAndResume 组件外部,创建一个名为 CoverLetterResume 的组件,在网页应用界面上显示求职信和简历。
type CoverLetterResumeProps = {
letter: string;
resume: string;
};
const CoverLetterResume = ({ letter, resume }: CoverLetterResumeProps) => {
return (
<div className="px-4 sm:px-6 lg:px-8 bg-slate-50 py-4">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-3xl font-semibold leading-6 text-gray-900">
ResumeBuilder
</h1>
</div>
</div>
{/* Cover Letter Start */}
<div className="mt-8 flow-root bg-slate-200">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div>
<h2 className="text-lg font-semibold leading-6 text-gray-900 mb-4 p-2">
Cover Letter
</h2>
<div className="min-w-full divide-y divide-gray-300 p-2">
{/* <Thead /> */}
<div className="divide-y divide-gray-200 bg-white p-2">
<ReactMarkdown>{letter}</ReactMarkdown>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Cover Letter End */}
{/* Cover Letter Preview Start */}
<div className="mt-8 flow-root bg-slate-200">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div>
<h2 className="text-lg font-semibold leading-6 text-gray-900 mb-4 p-2">
Cover Letter Preview
</h2>
<div className="min-w-full divide-y divide-gray-300">
{/* <Thead /> */}
<div className="divide-y divide-gray-200 bg-white">
<textarea
className="p-2"
id="coverLetter"
value={letter}
rows={20}
cols={113}
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Cover Letter Preview End */}
{/* Resume Start */}
<div className="mt-8 flow-root bg-slate-200">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<h2 className="text-lg font-semibold leading-6 text-gray-900 mb-4 p-2">
Resume
</h2>
<div className="min-w-full divide-y divide-gray-300">
{/* <Thead /> */}
<div className="divide-y divide-gray-200 bg-white">
<ReactMarkdown>{resume}</ReactMarkdown>
</div>
</div>
</div>
</div>
</div>
{/* Resume End */}
{/* Cover Letter Preview Start */}
<div className="mt-8 flow-root bg-slate-200">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div>
<h2 className="text-lg font-semibold leading-6 text-gray-900 mb-4 p-2">
Cover Letter Preview
</h2>
<div className="min-w-full divide-y divide-gray-300">
{/* <Thead /> */}
<div className="divide-y divide-gray-200 bg-white">
{/* {letter} */}
{/* <ReactMarkdown>{letter}</ReactMarkdown> */}
<textarea
className="p-2"
id="resume"
value={resume}
rows={20}
cols={113}
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Cover Letter Preview End */}
</div>
);
};
在 CoverLetterAndResume 组件内返回 CoverLetterResume 组件,如下所示。
return <CoverLetterResume {...createCoverLetterAndResume}/>;
在 app 文件夹中创建一个名为 resumeandcoverletter 的文件夹。然后创建一个名为 page.tsx 的文件并添加以下代码。
"use client";
import { CopilotProvider } from "@copilotkit/react-core";
import { CopilotSidebarUIProvider } from "@copilotkit/react-ui";
import "@copilotkit/react-textarea/styles.css"; // also import this if you want to use the CopilotTextarea component
import "@copilotkit/react-ui/styles.css"; // also import this if you want to use the chatbot component
import React, { useEffect, useState } from "react";
import { CoverLetterAndResume } from "../components/resume";
function buildResume () {
return (
<CopilotProvider chatApiEndpoint="./../api/copilotkit/chat">
<CopilotSidebarUIProvider>
<CoverLetterAndResume />
</CopilotSidebarUIProvider>
</CopilotProvider>
);
}
export default buildResume;
使用以下命令安装 openai 包。
npm i openai
在 app 文件夹中创建一个名为 API 的文件夹。然后在 API 文件夹中创建一个名为 copilotkit 的文件夹。在 copilotkit 文件夹中创建一个名为 chat 的文件夹。然后在 chat 文件夹中创建一个名为 route.ts 的文件并添加以下代码。
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: "Your ChatGPT API key",
});
export const runtime = "edge";
export async function POST(req: Request): Promise<Response> {
try {
const forwardedProps = await req.json();
const stream = openai.beta.chat.completions
.stream({
model: "gpt-4-1106-preview",
...forwardedProps,
stream: true,
})
.toReadableStream();
return new Response(stream);
} catch (error: any) {
return new Response("", { status: 500, statusText: error.error.message });
}
}
在 app 文件夹中的 page.tsx 文件上,在"Get Started"按钮中添加一个链接,导航到 resumeandcoverletter 页面,如下所示。
<div className="text-center">
<Link
className="inline-flex justify-center items-center gap-x-3 text-center bg-gradient-to-tl from-blue-600 to-violet-600 shadow-lg shadow-transparent hover:shadow-blue-700/50 border border-transparent text-white text-sm font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 focus:ring-offset-white py-3 px-6 dark:focus:ring-offset-gray-800"
href="/resumeandcoverletter">
Get started
<svg
className="flex-shrink-0 w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path d="m9 18 6-6-6-6" />
</svg>
</Link>
</div>
导航到 http://localhost:3000/,点击"Get Started"按钮,您将被重定向到与聊天机器人集成的 resumeandcoverletter 页面。
在页面右侧给聊天机器人一个提示,如"create a cover letter and resume"。聊天机器人将开始生成响应,完成后,它将在页面左侧显示生成的求职信和简历。
步骤 1:声明一个名为 updateLetter 的变量,用于保存之前生成的求职信。
const updateLetter = createCoverLetterAndResume.letter;
步骤 2:使用 useMakeCopilotReadable Hook,将 updateLetter 作为应用内聊天机器人的上下文。
useMakeCopilotReadable("Cover Letter:" + JSON.stringify(updateLetter));
步骤 3:使用 useMakeCopilotActionable Hook 设置一个名为 updateCoverLetter 的操作,为其添加描述和实现函数。该函数会使用传入的求职信更新内容来更新 createCoverLetterAndResume 状态,如下所示。
useMakeCopilotActionable(
{
name: "updateCoverLetter",
description:
"Update cover letter for a software developer job application.",
argumentAnnotations: [
{
name: "updateCoverLetterMarkdown",
type: "string",
description:
"Update markdown text for a cover letter to introduce yourself and briefly summarize your professional background as a software developer.",
required: true,
},
{
name: "resumeMarkdown",
type: "string",
description:
"Markdown text for a resume that displays your professional background and relevant skills.",
required: true,
},
],
implementation: async (updatedCoverLetterMarkdown) => {
setCreateCoverLetterAndResume((prevState) => ({
...prevState,
letter: updatedCoverLetterMarkdown,
}));
},
},
[]
);
步骤 4:向聊天机器人输入类似“更新求职信,并添加我正在申请 CopilotKit 技术写作职位”的提示词。如下所示,你可以看到求职信已经更新。
步骤 1:声明一个名为 updateResume 的变量,用于保存之前生成的求职信。
const updateResume = createCoverLetterAndResume.resume;
步骤 2:使用 useMakeCopilotReadable Hook,将 updateResume 作为应用内聊天机器人的上下文。
useMakeCopilotReadable("Resume:" + JSON.stringify(updateResume));
步骤 3:使用 useMakeCopilotActionable Hook 设置一个名为 updateResume 的操作,为其添加描述和实现函数。该函数会使用传入的求职信更新内容来更新 createCoverLetterAndResume 状态,如下所示。
useMakeCopilotActionable(
{
name: "updateResume",
description: "Update resume for a software developer job application.",
argumentAnnotations: [
{
name: "updateResumeMarkdown",
type: "string",
description:
"Update markdown text for a resume that displays your professional background and relevant skills.",
required: true,
},
],
implementation: async (updatedResumeMarkdown) => {
setCreateCoverLetterAndResume((prevState) => ({
...prevState,
resume: updatedResumeMarkdown,
}));
},
},
[]
);
步骤 4:向聊天机器人输入类似“更新简历,将我的姓名添加为 John Doe,并将电子邮件地址添加为 johndoe@example.com”的提示词。如下所示,你可以看到简历已经更新。
步骤 1:安装 jsPDF,这是一个用于在 JavaScript 中生成 PDF 的库。
npm i jspdf
步骤 2:在 CoverLetterAndResume 组件中,使用 useMakeCopilotActionable Hook 设置一个名为 "downloadPdfs" 的操作,为其添加描述和实现函数。该函数使用 jsPDF 库为求职信和简历创建 PDF,然后保存这些文件,如下所示。
function addTextToPDF(doc: any, text: any, x: any, y: any, maxWidth: any) {
// Split the text into lines
const lines = doc.splitTextToSize(text, maxWidth);
// Add lines to the document
doc.text(lines, x, y);
}
useMakeCopilotActionable(
{
name: "downloadPdfs",
description: "Download pdfs of the cover letter and resume.",
argumentAnnotations: [
{
name: "coverLetterPdfA4",
type: "string",
description:
"A Pdf that contains the cover letter converted from markdown text and fits A4 paper.",
required: true,
},
{
name: "resumePdfA4Paper",
type: "string",
description:
"A Pdf that contains the resume converted from markdown text and fits A4 paper.",
required: true,
},
],
implementation: async () => {
const marginLeft = 10;
const marginTop = 10;
const maxWidth = 180;
const coverLetterDoc = new jsPDF();
addTextToPDF(
coverLetterDoc,
createCoverLetterAndResume.letter,
marginLeft,
marginTop,
maxWidth
);
coverLetterDoc.save("coverLetter.pdf");
const resumeDoc = new jsPDF();
addTextToPDF(
resumeDoc,
createCoverLetterAndResume.resume,
marginLeft,
marginTop,
maxWidth
);
resumeDoc.save("resume.pdf");
},
},
[createCoverLetterAndResume]
);
步骤 3:返回 Web 应用中的聊天机器人,向它输入提示词“下载求职信和简历的 PDF”。PDF 文件将开始下载。如果打开 coverLetter.pdf,你应该会看到生成的求职信,如下所示。
总之,你可以使用 CopilotKit 构建应用内 AI 聊天机器人,使其能够查看应用的当前状态并在应用内执行操作。AI 聊天机器人可以与你的应用前端、后端以及第三方服务进行通信。
完整源代码:https://github.com/TheGreatBonnie/AIPoweredResumeBuilder
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。