Next.js+GPT-4构建AI博客全套教程
实战教程展示如何用Next.js、GPT-4、Supabase、CopilotKit快速构建AI应用,直接可复用。
实战教程展示如何用Next.js、GPT-4、Supabase、CopilotKit快速构建AI应用,直接可复用。
AI 时代已经来临。为了作为开发者走在前沿,在你的作品集中拥有一些 AI 驱动的项目是非常有益的。
今天,我们将通过构建一个具有研究、自动完成和 Copilot 等强大功能的 AI 驱动博客平台来进行讲解。
我之前构建了这个项目的初始版本。一位评论者提出了一些非常有趣的建议,将其提升到更高的水平。
所以我们决定构建它!
我们正在构建 AI 驱动的博客平台第二部分。
CopilotKit 是一个开源的 AI Copilot 平台。我们让在 React 应用中集成强大的 AI 变得简单。
ChatBot:能够在应用内执行操作的上下文感知的应用内聊天机器人 💬
CopilotTextArea:具有上下文感知自动完成和插入功能的 AI 驱动文本字段 📝
Co-Agents:能够与你的应用和用户交互的应用内 AI 智能体 🤖
现在回到文章!
要充分理解本教程,你需要对 React 或 Next.js 有基本的了解。
以下是构建 AI 驱动博客所需的工具:
Quill Rich Text Editor - 一个文本编辑器,让你能够轻松格式化文本、添加图像、添加代码,以及在网络应用中创建自定义交互式内容。
Supabase - 一个 PostgreSQL 托管服务,为你的项目提供所有所需的后端功能。
Langchain - 提供一个框架,使 AI 智能体能够搜索网络和研究任何主题。
OpenAI API - 提供一个 API 密钥,使你能够使用 ChatGPT 模型执行各种任务。
Tavily AI - 一个搜索引擎,使 AI 智能体能够进行研究并在应用中访问实时知识。
CopilotKit - 一个开源 Copilot 框架,用于构建自定义 AI 聊天机器人、应用内 AI 智能体和文本区域。
项目设置和包安装
首先,通过在终端中运行以下代码片段来创建 Next.js 应用:
npx create-next-app@latest aiblogapp
选择你偏好的配置设置。在本教程中,我们将使用 TypeScript 和 Next.js App Router。
接下来,安装 Quill 富文本编辑器、Supabase 和 Langchain 包及其依赖项。
npm install quill react-quill @supabase/supabase-js @supabase/ssr @supabase/auth-helpers-nextjs @langchain/langgraph
最后,安装 CopilotKit 包。这些包使我们能够从 React 状态检索数据并向应用添加 AI Copilot。
npm install @copilotkit/react-ui @copilotkit/react-textarea @copilotkit/react-core @copilotkit/backend
恭喜!你现在已准备好构建一个 AI 驱动的博客。
构建博客前端
在本部分中,我将引导你完成使用静态内容创建博客前端的过程,以定义博客的用户界面。
博客的前端将由四个页面组成:主页、文章页面、创建文章页面和登录/注册页面。
首先,进入你的代码编辑器中的 /[root]/src/app,创建一个名为 components 的文件夹。在 components 文件夹内,创建五个文件,分别命名为 Header.tsx、Posts.tsx、Post.tsx、Comment.tsx 和 QuillEditor.tsx。
在 Header.tsx 文件中,添加以下定义名为 Header 的函数式组件的代码,该组件将呈现博客的导航栏。
"use client";
import Link from "next/link";
export default function Header() {
return (
<>
<header className="flex flex-wrap sm:justify-start sm:flex-nowrap z-50 w-full bg-gray-800 border-b border-gray-200 text-sm py-3 sm:py-0 ">
<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">
<Link
className="flex-none text-xl text-white font-semibold "
href="/"
aria-label="Brand">
AIBlog
</Link>
</div>
<div id="navbar-collapse-with-animation" className="">
<div className="flex flex-col gap-y-4 gap-x-0 mt-5 sm:flex-row sm:items-center sm:justify-end sm:gap-y-0 sm:gap-x-7 sm:mt-0 sm:ps-7">
<Link
className="flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 "
href="/createpost">
Create Post
</Link>
<form action={""}>
<button
className="flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 "
type="submit">
Logout
</button>
</form>
<Link
className="flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 "
href="/login">
Login
</Link>
</div>
</div>
</nav>
</header>
</>
);
}
在 Posts.tsx 文件中,添加以下定义名为 Posts 的函数式组件的代码,该组件将呈现博客平台主页,显示已发布文章的列表。
"use client";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import Link from "next/link";
export default function Posts() {
const [articles, setArticles] = useState<any[]>([]);
return (
<div className="max-w-[85rem] h-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<Link
key={""}
className="group flex flex-col h-full bg-gray-800 border border-gray-200 hover:border-transparent hover:shadow-lg transition-all duration-300 rounded-xl p-5 "
href="">
<div className="aspect-w-16 aspect-h-11">
<Image
className="object-cover h-48 w-96 rounded-xl"
src={`https://source.unsplash.com/featured/?${encodeURIComponent(
"Hello World"
)}`}
width={500}
height={500}
alt="Image Description"
/>
</div>
<div className="my-6">
<h3 className="text-xl font-semibold text-white ">Hello World</h3>
</div>
</Link>
</div>
</div>
);
}
在 QuillEditor.tsx 文件中,添加以下代码,该代码动态导入 QuillEditor 组件,定义 Quill 编辑器工具栏的模块配置,并定义 Quill 编辑器的文本格式。
// 从 "next/dynamic" 包导入 dynamic 函数
import dynamic from "next/dynamic";
// 导入 Quill 编辑器 "snow" 主题的 CSS 样式
import "react-quill/dist/quill.snow.css";
// 导出动态导入的 QuillEditor 组件
export const QuillEditor = dynamic(() => import("react-quill"), { ssr: false });
// 定义 Quill 编辑器工具栏的模块配置
export const quillModules = {
toolbar: [
// 指定不同级别的标题
[{ header: [1, 2, 3, false] }],
// 指定格式化选项,例如粗体、斜体等
["bold", "italic", "underline", "strike", "blockquote"],
// 指定列表选项:有序和无序
[{ list: "ordered" }, { list: "bullet" }],
// 指定链接和图像选项
["link", "image"],
// 指定对齐选项
[{ align: [] }],
// 指定颜色选项
[{ color: [] }],
// 指定代码块选项
["code-block"],
// 指定清除选项以删除格式
["clean"],
],
};
// 定义 Quill 编辑器支持的格式
export const quillFormats = [
"header",
"bold",
"italic",
"underline",
"strike",
"blockquote",
"list",
"bullet",
"link",
"image",
"align",
"color",
"code-block",
];
在 Post.tsx 文件中,添加以下定义名为 CreatePost 的函数式组件的代码,该组件将用于呈现文章创建表单。
"use client";
// 导入 React hooks 和组件
import { useRef, useState } from "react";
import { QuillEditor } from "./QuillEditor";
import { quillModules } from "./QuillEditor";
import { quillFormats } from "./QuillEditor";
import "react-quill/dist/quill.snow.css";
// 定义 CreatePost 组件
export default function CreatePost() {
// 初始化文章大纲、Copilot 文本和文章标题的状态变量
const [articleOutline, setArticleOutline] = useState("");
const [copilotText, setCopilotText] = useState("");
const [articleTitle, setArticleTitle] = useState("");
// 用于跟踪研究任务是否运行的状态变量
const [publishTaskRunning, setPublishTaskRunning] = useState(false);
// 处理编辑器内容的更改
const handleEditorChange = (newContent: any) => {
setCopilotText(newContent);
};
return (
<>
{/* 主要内容 */}
<div className="p-3 max-w-3xl mx-auto min-h-screen">
<h1 className="text-center text-white text-3xl my-7 font-semibold">
创建博文
</h1>
{/* 创建博文的表单 */}
<form action={""} className="flex flex-col gap-4 mb-2 mt-2">
<div className="flex flex-col gap-4 sm:flex-row justify-between mb-2">
{/* 文章标题输入框 */}
<input
type="text"
id="title"
name="title"
placeholder="标题"
value={articleTitle}
onChange={(event) => setArticleTitle(event.target.value)}
className="flex-1 block w-full rounded-lg border text-sm border-gray-600 bg-gray-700 text-white placeholder-gray-400 focus:border-cyan-500 focus:ring-cyan-500"
/>
</div>
{/* 文章内容隐藏 textarea */}
<textarea
className="p-4 w-full aspect-square font-bold text-xl bg-slate-800 text-white rounded-lg resize-none hidden"
id="content"
name="content"
value={copilotText}
placeholder="在此输入文章内容"
onChange={(event) => setCopilotText(event.target.value)}
/>
{/* Quill 编辑器组件 */}
<QuillEditor
onChange={handleEditorChange}
modules={quillModules}
formats={quillFormats}
className="h-80 mb-12 text-white"
/>
{/* 发布博文的提交按钮 */}
<button
type="submit"
disabled={publishTaskRunning}
className={`bg-blue-500 text-white font-bold py-2 px-4 rounded ${
publishTaskRunning
? "opacity-50 cursor-not-allowed"
: "hover:bg-blue-700"
}`}
onClick={async () => {
try {
setPublishTaskRunning(true);
} finally {
setPublishTaskRunning(false);
}
}}>
{publishTaskRunning ? "发布中..." : "发布"}
</button>
</form>
</div>
</>
);
}
在 Comment.tsx 文件中,添加以下代码,定义一个名为 Comment 的函数组件,用来渲染博文评论表单和博文评论。
// 客户端渲染
"use client";
// 导入 React 和 Next.js 组件
import React, { useEffect, useRef, useState } from "react";
import Image from "next/image";
// 定义 Comment 组件
export default function Comment() {
// 评论、评论列表和文章内容的状态变量
const [comment, setComment] = useState("");
const [comments, setComments] = useState<any[]>([]);
const [articleContent, setArticleContent] = useState("");
return (
<div className="max-w-2xl mx-auto w-full p-3">
{/* 提交评论的表单 */}
<form action={""} className="border border-teal-500 rounded-md p-3 mb-4">
{/* 输入评论的 textarea */}
<textarea
id="content"
name="content"
placeholder="添加评论..."
rows={3}
onChange={(e) => setComment(e.target.value)}
value={comment}
className="hidden"
/>
{/* 提交按钮 */}
<div className="flex justify-between items-center mt-5">
<button
type="submit"
className="bg-blue-500 text-white font-bold py-2 px-4 rounded">
提交
</button>
</div>
</form>
{/* 评论区域 */}
<p className="text-white mb-2">评论:</p>
{/* 评论项(当前为硬编码) */}
<div key={""} className="flex p-4 border-b dark:border-gray-600 text-sm">
<div className="flex-shrink-0 mr-3">
{/* 头像 */}
<Image
className="w-10 h-10 rounded-full bg-gray-200"
src={`(link unavailable){encodeURIComponent(
"Silhouette"
)}`}
width={500}
height={500}
alt="头像"
/>
</div>
<div className="flex-1">
<div className="flex items-center mb-1">
{/* 用户名(当前硬编码为"匿名") */}
<span className="font-bold text-white mr-1 text-xs truncate">
匿名
</span>
</div>
{/* 评论文本(当前硬编码为"暂无评论") */}
<p className="text-gray-500 pb-2">暂无评论</p>
</div>
</div>
</div>
);
}
接下来,在 /[root]/src/app 目录中创建一个名为 [slug] 的文件夹。在 [slug] 文件夹内,创建一个 page.tsx 文件。
然后将以下代码添加到该文件中,导入 Comment 和 Header 组件,定义一个名为 Post 的函数组件,用来渲染导航栏、博文内容、评论表单和博文评论。
import Header from "../components/Header";
import Comment from "../components/Comment";
export default async function Post() {
return (
<>
<Header />
<main className="p-3 flex flex-col max-w-6xl mx-auto min-h-screen">
<h1 className="text-3xl text-white mt-10 p-3 text-center font-serif max-w-2xl mx-auto lg:text-4xl">
Hello World
</h1>
<div className="flex justify-between text-white p-3 border-b border-slate-500 mx-auto w-full max-w-2xl text-xs">
<span></span>
<span className="italic">阅读时间 0 分钟</span>
</div>
<div className="p-3 max-w-2xl text-white mx-auto w-full post-content border-b border-slate-500 mb-2">
暂无博文内容
</div>
<Comment />
</main>
</>
);
}
之后,在 /[root]/src/app 目录中创建一个名为 createpost 的文件夹。在 createpost 文件夹内,创建一个 page.tsx 文件。
然后将以下代码添加到该文件中,导入 CreatePost 和 Header 组件,定义一个名为 WriteArticle 的函数组件,用来渲染导航栏和博文创建表单。
import CreatePost from "../components/Post";
import Header from "../components/Header";
import { redirect } from "next/navigation";
export default async function WriteArticle() {
return (
<>
<Header />
<CreatePost />
</>
);
}
接下来,在 /[root]/src/page.tsx 文件中,添加以下代码,导入 Posts 和 Header 组件,定义一个名为 Home 的函数组件。
import Header from "./components/Header";
import Posts from "./components/Posts";
export default async function Home() {
return (
<>
<Header />
<Posts />
</>
);
}
之后,进入 next.config.mjs 文件,将其重命名为 next.config.js。然后添加以下代码,允许您使用来自 Unsplash 的图像作为已发布文章的封面图。
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "source.unsplash.com",
},
],
},
};
接下来,删除 globals.css 文件中的 CSS 代码,添加以下 CSS 代码。
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
height: 100vh;
background-color: rgb(16, 23, 42);
}
.ql-editor {
font-size: 1.25rem;
}
.post-content p {
margin-bottom: 0.5rem;
}
.post-content h1 {
font-size: 1.5rem;
font-weight: 600;
font-family: sans-serif;
margin: 1.5rem 0;
}
.post-content h2 {
font-size: 1.4rem;
font-family: sans-serif;
margin: 1.5rem 0;
}
.post-content a {
color: rgb(73, 149, 199);
text-decoration: none;
}
.post-content a:hover {
text-decoration: underline;
}
最后,在命令行上运行 npm run dev 命令,然后导航到 http://localhost:3000/。
现在您应该能在浏览器上看到博客平台的前端,如下所示。
在本节中,您将学习如何使用 CopilotKit 为博客添加一个 AI 副驾驶,以执行博文主题研究和内容自动建议。
CopilotKit 提供前端和后端两个包。它们使您能够接入 React 状态,并使用 AI 智能体在后端处理应用程序数据。
首先,我们将 CopilotKit React 组件添加到博客前端。
在这里,我将引导您完成将博客与 CopilotKit 前端集成的过程,以便于博文主题研究和文章大纲生成。
首先,使用以下代码片段在 /[root]/src/app/components/Post.tsx 文件的顶部导入 useMakeCopilotReadable、useCopilotAction、CopilotTextarea 和 HTMLCopilotTextAreaElement 自定义 hook。
import {
useMakeCopilotReadable,
useCopilotAction,
} from "@copilotkit/react-core";
import {
CopilotTextarea,
HTMLCopilotTextAreaElement,
} from "@copilotkit/react-textarea";
在 CreatePost 函数中,在状态变量下方,添加以下使用 useMakeCopilotReadable 钩子的代码,以将生成的文章大纲作为上下文添加到应用内聊天机器人中。该钩子使文章大纲对 copilot 可读。
useMakeCopilotReadable(
"Blog article outline: " + JSON.stringify(articleOutline)
);
在 useMakeCopilotReadable 钩子下方,使用下方代码创建一个名为 copilotTextareaRef 的引用,指向称为 HTMLCopilotTextAreaElement 的 textarea 元素。
const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);
在上方代码下方,添加以下使用 useCopilotAction 钩子的代码,以设置一个名为 researchBlogArticleTopic 的操作,该操作将启用对博客文章主题的研究。
该操作接受两个名为 articleTitle 和 articleOutline 的参数,以启用文章标题和大纲的生成。
该操作包含一个处理函数,该函数基于给定的主题生成文章标题和大纲。
在处理函数中,articleOutline 状态通过新生成的大纲进行更新,而 articleTitle 状态通过新生成的标题进行更新,如下所示。
// 定义一个 Copilot 操作
useCopilotAction(
{
// 操作名称和描述
name: "researchBlogArticleTopic",
description: "Research a given topic for a blog article.",
// 操作的参数
parameters: [
{
// 参数 1:articleTitle
name: "articleTitle",
type: "string",
description: "Title for a blog article.",
required: true, // 此参数是必需的
},
{
// 参数 2:articleOutline
name: "articleOutline",
type: "string",
description: "Outline for a blog article that shows what the article covers.",
required: true, // 此参数是必需的
},
],
// 操作的处理函数
handler: async ({ articleOutline, articleTitle }) => {
// 使用状态设置器设置文章大纲和标题
setArticleOutline(articleOutline);
setArticleTitle(articleTitle);
},
},
[] // 依赖项(空数组表示没有依赖项)
);
在上方代码下方,转到表单组件并添加以下 CopilotTextarea 组件,这将使你能够为文章内容添加文本补全、插入和编辑功能。
<CopilotTextarea
className="p-4 h-72 w-full rounded-lg mb-2 border text-sm border-gray-600 bg-gray-700 text-white placeholder-gray-400 focus:border-cyan-500 focus:ring-cyan-500 resize-none"
ref={copilotTextareaRef}
placeholder="Start typing for content autosuggestion."
value={articleOutline}
rows={5}
autosuggestionsConfig={{
textareaPurpose: articleTitle,
chatApiConfigs: {
suggestionsApiConfig: {
forwardedParams: {
max_tokens: 5,
stop: ["\n", ".", ","],
},
},
insertionApiConfig: {},
},
debounceTime: 250,
}}
/>
之后,转到 /[root]/src/app/createpost/page.tsx 文件,并使用下方代码在顶部导入 CopilotKit 前端包和样式。
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotPopup } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
然后使用 CopilotKit 包装 CopilotPopup 和 CreatePost 组件,如下所示。CopilotKit 组件指定 CopilotKit 后端端点的 URL(/api/copilotkit/),而 CopilotPopup 呈现应用内聊天机器人,你可以向其提供提示来研究任何主题的文章。
export default async function WriteArticle() {
return (
<>
<Header />
<CopilotKit url="/api/copilotkit">
<CopilotPopup
instructions="Help the user research a blog article topic."
defaultOpen={true}
labels={{
title: "Blog Article Research AI Assistant",
initial:
"Hi! 👋 I can help you research any topic for a blog article.",
}}
/>
<CreatePost />
</CopilotKit>
</>
);
}
之后,使用下方代码片段在 /[root]/src/app/components/Comment.tsx 文件的顶部导入 useMakeCopilotReadable、CopilotKit、CopilotTextarea 和 HTMLCopilotTextAreaElement 自定义钩子。
import { useMakeCopilotReadable, CopilotKit } from "@copilotkit/react-core";
import {
CopilotTextarea,
HTMLCopilotTextAreaElement,
} from "@copilotkit/react-textarea";
在 Comment 函数内,在状态变量下方,添加以下使用 useMakeCopilotReadable 钩子的代码,以将文章内容作为上下文添加到评论内容自动建议中。
useMakeCopilotReadable(
"Blog article content: " + JSON.stringify(articleContent)
);
const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);
然后将 CopilotTextarea 组件添加到评论表单,并使用 CopilotKit 包装该表单,如下所示。
<CopilotKit url="/api/copilotkit">
<form
action={""}
className="border border-teal-500 rounded-md p-3 mb-4">
<textarea
id="content"
name="content"
placeholder="Add a comment..."
rows={3}
onChange={(e) => setComment(e.target.value)}
value={comment}
className="hidden"
/>
<CopilotTextarea
className="p-4 w-full rounded-lg mb-2 border text-sm border-gray-600 bg-gray-700 text-white placeholder-gray-400 focus:border-cyan-500 focus:ring-cyan-500 resize-none"
ref={copilotTextareaRef}
placeholder="Start typing for content autosuggestion."
onChange={(event) => setComment(event.target.value)}
rows={5}
autosuggestionsConfig={{
textareaPu