人工智慧時代已經來臨。身為開發者,要想取得成功,在你的投資組合中加入一些人工智慧驅動的專案是很棒的。
今天,我們將建立一個由人工智慧驅動的部落格平台,該平台具有一些很棒的功能,例如研究、自動完成和 Copilot。
我在這裡建立了該專案的初始版本。一位評論者提出了一些非常酷的建議,可以將其提升到一個新的水平。
所以我們決定建造它!
我們正在建立一個由人工智慧驅動的部落格平台 Pt。二
CopilotKit是一個開源的AI副駕駛平台。我們可以輕鬆地將強大的人工智慧整合到您的 React 應用程式中。
建造:
ChatBot:上下文感知的應用內聊天機器人,可以在應用程式內執行操作 💬
CopilotTextArea:人工智慧驅動的文字字段,具有上下文感知自動完成和插入功能📝
聯合代理:應用程式內人工智慧代理,可以與您的應用程式和使用者互動🤖
{% cta https://github.com/CopilotKit/CopilotKit %} Star CopilotKit ⭐️ {% endcta %}
現在回到文章!
要完全理解本教程,您需要對 React 或 Next.js 有基本的了解。
以下是建立人工智慧部落格所需的工具:
Quill 富文本編輯器- 一個文字編輯器,可讓您輕鬆設定文字格式、新增圖片、新增程式碼以及在 Web 應用程式中建立自訂互動式內容。
Supabase - 一項 PostgreSQL 託管服務,為您提供專案所需的所有後端功能。
Langchain - 提供了一個框架,使人工智慧代理能夠搜尋網路並研究任何主題。
OpenAI API - 提供 API 金鑰,讓您能夠使用 ChatGPT 模型執行各種任務。
Tavily AI - 一個搜尋引擎,使人工智慧代理能夠在應用程式中進行研究並存取即時知識。
CopilotKit - 一個開源副駕駛框架,用於建立自訂 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
恭喜!您現在已準備好建立由人工智慧驅動的部落格。
在本節中,我將引導您完成使用靜態內容建立部落格前端以定義部落格使用者介面的過程。
部落格的前端將由四個頁面組成:主頁、貼文頁面、建立貼文頁面和登入/註冊頁面。
首先,請在程式碼編輯器中前往/[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 編輯器的文字格式。
// Import the dynamic function from the "next/dynamic" package
import dynamic from "next/dynamic";
// Import the CSS styles for the Quill editor's "snow" theme
import "react-quill/dist/quill.snow.css";
// Export a dynamically imported QuillEditor component
export const QuillEditor = dynamic(() => import("react-quill"), { ssr: false });
// Define modules configuration for the Quill editor toolbar
export const quillModules = {
toolbar: [
// Specify headers with different levels
[{ header: [1, 2, 3, false] }],
// Specify formatting options like bold, italic, etc.
["bold", "italic", "underline", "strike", "blockquote"],
// Specify list options: ordered and bullet
[{ list: "ordered" }, { list: "bullet" }],
// Specify options for links and images
["link", "image"],
// Specify alignment options
[{ align: [] }],
// Specify color options
[{ color: [] }],
// Specify code block option
["code-block"],
// Specify clean option for removing formatting
["clean"],
],
};
// Define supported formats for the Quill editor
export const quillFormats = [
"header",
"bold",
"italic",
"underline",
"strike",
"blockquote",
"list",
"bullet",
"link",
"image",
"align",
"color",
"code-block",
];
在Post.tsx
檔案中,新增以下程式碼,定義一個名為CreatePost
功能元件,該元件將用於呈現文章建立表單。
"use client";
// Importing React hooks and components
import { useRef, useState } from "react";
import { QuillEditor } from "./QuillEditor";
import { quillModules } from "./QuillEditor";
import { quillFormats } from "./QuillEditor";
import "react-quill/dist/quill.snow.css";
// Define the CreatePost component
export default function CreatePost() {
// Initialize state variables for article outline, copilot text, and article title
const [articleOutline, setArticleOutline] = useState("");
const [copilotText, setCopilotText] = useState("");
const [articleTitle, setArticleTitle] = useState("");
// State variable to track if research task is running
const [publishTaskRunning, setPublishTaskRunning] = useState(false);
// Handle changes to the editor content
const handleEditorChange = (newContent: any) => {
setCopilotText(newContent);
};
return (
<>
{/* Main */}
<div className="p-3 max-w-3xl mx-auto min-h-screen">
<h1 className="text-center text-white text-3xl my-7 font-semibold">
Create a post
</h1>
{/* Form for creating a post */}
<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 field for article title */}
<input
type="text"
id="title"
name="title"
placeholder="Title"
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>
{/* Hidden textarea for article content */}
<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="Write your article content here"
onChange={(event) => setCopilotText(event.target.value)}
/>
{/* Quill editor component */}
<QuillEditor
onChange={handleEditorChange}
modules={quillModules}
formats={quillFormats}
className="h-80 mb-12 text-white"
/>
{/* Submit button for publishing the post */}
<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 ? "Publishing..." : "Publish"}
</button>
</form>
</div>
</>
);
}
在Comment.tsx
檔案中,新增以下程式碼,定義一個名為 Comment 的功能元件,該元件呈現貼文評論表單和貼文評論。
// Client-side rendering
"use client";
// Importing React and Next.js components
import React, { useEffect, useRef, useState } from "react";
import Image from "next/image";
// Define the Comment component
export default function Comment() {
// State variables for comment, comments, and article content
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 for submitting a comment */}
<form action={""} className="border border-teal-500 rounded-md p-3 mb-4">
{/* Textarea for entering a comment */}
<textarea
id="content"
name="content"
placeholder="Add a comment..."
rows={3}
onChange={(e) => setComment(e.target.value)}
value={comment}
className="hidden"
/>
{/* Submit button */}
<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">
Submit
</button>
</div>
</form>
{/* Comments section */}
<p className="text-white mb-2">Comments:</p>
{/* Comment item (currently hardcoded) */}
<div key={""} className="flex p-4 border-b dark:border-gray-600 text-sm">
<div className="flex-shrink-0 mr-3">
{/* Profile picture */}
<Image
className="w-10 h-10 rounded-full bg-gray-200"
src={`(link unavailable){encodeURIComponent(
"Silhouette"
)}`}
width={500}
height={500}
alt="Profile Picture"
/>
</div>
<div className="flex-1">
<div className="flex items-center mb-1">
{/* Username (currently hardcoded as "Anonymous") */}
<span className="font-bold text-white mr-1 text-xs truncate">
Anonymous
</span>
</div>
{/* Comment text (currently hardcoded as "No Comments") */}
<p className="text-gray-500 pb-2">No Comments</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 mins read</span>
</div>
<div className="p-3 max-w-2xl text-white mx-auto w-full post-content border-b border-slate-500 mb-2">
No Post Content
</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/。
現在您應該在瀏覽器上查看部落格平台前端,如下所示。
在本節中,您將學習如何為部落格加入 AI 副駕駛,以使用 CopilotKit 執行部落格主題研究和內容自動建議。
CopilotKit 提供前端和後端套件。它們使您能夠插入 React 狀態並使用 AI 代理在後端處理應用程式資料。
首先,讓我們將 CopilotKit React 元件加入到部落格前端。
在這裡,我將引導您完成將部落格與 CopilotKit 前端整合的過程,以方便部落格文章研究和文章大綱生成。
首先,使用下面的程式碼片段在/[root]/src/app/components/Post.tsx
檔案頂部匯入useMakeCopilotReadable
、 useCopilotAction
、 CopilotTextarea
和HTMLCopilotTextAreaElement
自訂掛鉤。
import {
useMakeCopilotReadable,
useCopilotAction,
} from "@copilotkit/react-core";
import {
CopilotTextarea,
HTMLCopilotTextAreaElement,
} from "@copilotkit/react-textarea";
在 CreatePost 函數內的狀態變數下方,加入以下程式碼,該程式碼使用useMakeCopilotReadable
掛鉤來新增將作為應用程式內聊天機器人的上下文產生的文章大綱。鉤子使副駕駛可以閱讀文章大綱。
useMakeCopilotReadable(
"Blog article outline: " + JSON.stringify(articleOutline)
);
在useMakeCopilotReadable
掛鉤下方,使用以下程式碼建立一個名為copilotTextareaRef
的引用,該引用指向名為HTMLCopilotTextAreaElement
的文字區域元素。
const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);
在上面的程式碼下方,加入以下程式碼,該程式碼使用useCopilotAction
掛鉤來設定名為researchBlogArticleTopic
的操作,該操作將啟用對部落格文章的給定主題的研究。
此操作採用兩個參數,稱為articleTitle
和articleOutline
,這兩個參數可以產生文章標題和大綱。
該操作包含一個處理程序函數,該函數根據給定主題生成文章標題和大綱。
在處理函數內部, articleOutline
狀態會使用新產生的大綱進行更新,而articleTitle
狀態會使用新產生的標題進行更新,如下所示。
// Define a Copilot action
useCopilotAction(
{
// Action name and description
name: "researchBlogArticleTopic",
description: "Research a given topic for a blog article.",
// Parameters for the action
parameters: [
{
// Parameter 1: articleTitle
name: "articleTitle",
type: "string",
description: "Title for a blog article.",
required: true, // This parameter is required
},
{
// Parameter 2: articleOutline
name: "articleOutline",
type: "string",
description: "Outline for a blog article that shows what the article covers.",
required: true, // This parameter is required
},
],
// Handler function for the action
handler: async ({ articleOutline, articleTitle }) => {
// Set the article outline and title using state setters
setArticleOutline(articleOutline);
setArticleTitle(articleTitle);
},
},
[] // Dependencies (empty array means no dependencies)
);
在上面的程式碼下方,前往表單元件並新增以下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
包裝CopilPopup
和CreatePost
元件,如下圖所示。 CopilotKit
元件指定 CopilotKit 後端端點 ( /api/copilotkit/
) 的 URL,而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={{
textareaPurpose: articleContent,
chatApiConfigs: {
suggestionsApiConfig: {
forwardedParams: {
max_tokens: 5,
stop: ["\n", ".", ","],
},
},
insertionApiConfig: {},
},
debounceTime: 250,
}}
/>
<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">
Submit
</button>
</div>
</form>
</CopilotKit>
之後,執行開發伺服器並導航到 http://localhost:3000/createpost。您應該會看到彈出的應用程式內聊天機器人和 CopilotTextarea 已整合到部落格中。
恭喜!您已成功將 CopilotKit 新增至部落格前端。
在這裡,我將引導您完成部落格與 CopilotKit 後端的整合過程,CopilotKit 後端處理來自前端的請求,並提供函數呼叫和各種 LLM 後端(例如 GPT)。
此外,我們將整合一個名為 Tavily 的人工智慧代理,它可以研究網路上的任何主題。
首先,在根目錄中建立一個名為.env.local
的檔案。然後在保存ChatGPT
和Tavily
Search API 金鑰的檔案中加入下面的環境變數。
OPENAI_API_KEY="Your ChatGPT API key"
TAVILY_API_KEY="Your Tavily Search API key"
若要取得 ChatGPT API 金鑰,請導覽至 https://platform.openai.com/api-keys。
若要取得 Tavilly Search API 金鑰,請導覽至 https://app.tavily.com/home
之後,轉到/[root]/src/app
並建立一個名為api
的資料夾。在api
資料夾中,建立一個名為copilotkit
的資料夾。
在copilotkit
資料夾中,建立一個名為research.ts
的檔案。然後導航到該 Research.ts gist 文件,複製程式碼,並將其新增至research.ts
檔案中
接下來,在/[root]/src/app/api/copilotkit
資料夾中建立一個名為route.ts
的檔案。該文件將包含設定後端功能來處理 POST 請求的程式碼。它有條件地包括對給定主題進行研究的“研究”操作。
現在在文件頂部導入以下模組。
import { CopilotBackend, OpenAIAdapter } from "@copilotkit/backend"; // For backend functionality with CopilotKit.
import { researchWithLangGraph } from "./research"; // Import a custom function for conducting research.
import { AnnotatedFunction } from "@copilotkit/shared"; // For annotating functions with metadata.
在上面的程式碼下面,定義一個執行時間環境變數和一個名為researchAction
的函數,該函數使用下面的程式碼研究某個主題。
// Define a runtime environment variable, indicating the environment where the code is expected to run.
export const runtime = "edge";
// Define an annotated function for research. This object includes metadata and an implementation for the function.
const researchAction: AnnotatedFunction<any> = {
name: "research", // Function name.
description: "Call this function to conduct research on a certain topic. Respect other notes about when to call this function", // Function description.
argumentAnnotations: [ // Annotations for arguments that the function accepts.
{
name: "topic", // Argument name.
type: "string", // Argument type.
description: "The topic to research. 5 characters or longer.", // Argument description.
required: true, // Indicates that the argument is required.
},
],
implementation: async (topic) => { // The actual function implementation.
console.log("Researching topic: ", topic); // Log the research topic.
return await researchWithLangGraph(topic); // Call the research function and return its result.
},
};
然後在上面的程式碼下加入下面的程式碼來定義處理POST請求的非同步函數。
// Define an asynchronous function that handles POST requests.
export async function POST(req: Request): Promise<Response> {
const actions: AnnotatedFunction<any>[] = []; // Initialize an array to hold actions.
// Check if a specific environment variable is set, indicating access to certain functionality.
if (process.env["TAVILY_API_KEY"]) {
actions.push(researchAction); // Add the research action to the actions array if the condition is true.
}
// Instantiate CopilotBackend with the actions defined above.
const copilotKit = new CopilotBackend({
actions: actions,
});
// Use the CopilotBackend instance to generate a response for the incoming request using an OpenAIAdapter.
return copilotKit.response(req, new OpenAIAdapter());
}
恭喜!您已成功將 CopilotKit 後端新增至 Blog 。
在本節中,我將引導您完成將部落格與 Supabase 資料庫整合以插入和獲取部落格文章和評論資料的過程。
首先,導覽至supabase.com並點擊主頁上的「啟動您的專案」按鈕。
然後新建一個專案,名為AiBloggingPlatform,如下圖所示。
建立專案後,將 Supabase URL 和 API 金鑰新增至env.local
檔案中的環境變數中,如下所示。
NEXT_PUBLIC_SUPABASE_URL=”Your Supabase URL”
NEXT_PUBLIC_SUPABASE_ANON_KEY=”Your Supabase API Key”
在這裡,我將引導您完成為部落格設定身份驗證的過程,使用戶能夠註冊、登入或登出。
首先,前往/[root]/src/
並建立一個名為utils
資料夾。在utils
資料夾中,建立一個名為supabase
的資料夾。
然後在supabase
資料夾中建立兩個名為client.ts
和server.ts
檔案。
之後,導航到此supabase 文件連結,複製此處提供的程式碼,並將其貼上到您在supabase
資料夾中建立的相應文件中。
接下來,在專案根目錄中建立一個名為middleware.ts
的文件,並在/[root]/src/utils/supabase
資料夾中建立另一個同名文件。
之後,導航到此supabase 文件連結,複製此處提供的程式碼,並將其貼上到相應的middleware.ts
文件中。
接下來,前往/[root]/src/app/login
資料夾並建立一個名為actions.ts
的檔案。之後,導航到此supabase 文件連結,複製此處提供的程式碼,並將其貼上到actions.ts
。
之後,更改電子郵件範本以支援身份驗證流程。為此,請前往 Supabase 儀表板中的驗證範本頁面。
在Confirm signup
模板中,將{{ .ConfirmationURL }}
更改為{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup
如下所示,然後按一下儲存按鈕。
之後,建立路由處理程序以使用電子郵件進行身份驗證確認。為此**,請前往** /[root]/src/app/
並建立一個名為auth
的資料夾。然後在auth
資料夾中建立一個名為confirm
資料夾。
在confirm
資料夾中,建立一個名為route.ts
檔案並導航到此supabase文件連結,複製那裡提供的程式碼,並將其貼到route.ts
檔案中。
之後,請前往/[root]/src/app/login/page.tsx
檔案並使用下面的程式碼片段匯入 Supabase signup
和login
功能。
import { login, signup } from "./actions";
在註冊/登入表單中,按一下登入和註冊按鈕時,使用 formAction 呼叫 Supabase signup
和login
函數,如下所示。
<button
className="bg-blue-500 text-white font-bold py-2 px-4 rounded"
formAction={login}>
Log in
</button>
<button
className="bg-blue-500 text-white font-bold py-2 px-4 rounded"
formAction={signup}>
Sign up
</button>
之後,執行開發伺服器並導航至http://localhost:3000/login 。如下所示新增電子郵件和密碼,然後按一下「註冊」按鈕。
然後轉到您用於註冊的電子郵件的收件匣,然後按一下確認您的電子郵件按鈕,如下所示。
之後,請前往 Supabase 儀表板中的「身份驗證用戶」頁面,您應該會看到新建立的用戶,如下所示。
接下來,設定註銷功能。為此,請前往/[root]/src/app
並建立一個名為logout
的資料夾。然後建立一個名為route.ts
的檔案並將以下程式碼加入該檔案。
// Server-side code
"use server";
// Importing Next.js functions
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
// Importing Supabase client creation function from utils
import { createClient } from "@/utils/supabase/server";
// Exporting the logout function
export async function logout() {
// Creating a Supabase client instance
const supabase = createClient();
// Signing out of Supabase auth
const { error } = await supabase.auth.signOut();
// If there's an error, redirect to the error page
if (error) {
redirect("/error");
}
// Revalidate the "/" path for the "layout" cache
revalidatePath("/", "layout");
// Redirect to the homepage
redirect("/");
}
之後,請前往/[root]/src/app/components/Header.tsx
檔案並使用下面的程式碼片段匯入 Supabase 登出功能。
import { logout } from "../logout/actions";
然後在action參數中加入註銷功能,如下所示。
<form action={logout}>
<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>
如果按一下「登出」按鈕,已登入的使用者將被登出。
在這裡,我將引導您完成設定使用者角色和權限的過程,以控制不同使用者可以在部落格上執行的操作。
首先,前往/[root]/src/app/components/Header.tsx
檔案並導入 Supabase createClient 函數。
import { createClient } from "@/utils/supabase/client";
然後導入useState
和useEffect
掛鉤,並使用下面的程式碼片段定義一個名為 user 的類型。
import { useEffect, useState } from "react";
type User = {};
在 Header 功能元件內,新增以下程式碼,程式碼使用useState
掛鉤來儲存user
和admin
資料,並使用useEffect
掛鉤在元件安裝時從 Supabase auth 取得使用者資料。 getUser
函數檢查錯誤並相應地設定使用者和管理員狀態。
// State variables for user and admin
const [user, setUser] = useState<User | null>(null);
const [admin, setAdmin] = useState<User | null>(null);
// useEffect hook to fetch user data on mount
useEffect(() => {
// Define an async function to get the user
async function getUser() {
// Create a Supabase client instance
const supabase = createClient();
// Get the user data from Supabase auth
const { data, error } = await supabase.auth.getUser();
// If there's an error or no user data, log a message
if (error || !data?.user) {
console.log("No User");
}
// If user data is available, set the user state
else {
setUser(data.user);
}
// Define the email of the signed-up user
const userEmail = "email of signed-up user";
// Check if the user is an admin (email matches)
if (!data?.user || data.user?.email !== userEmail) {
console.log("No Admin");
}
// If the user is an admin, set the admin state
else {
setAdmin(data.user);
}
}
// Call the getUser function
getUser();
}, []); // Dependency array is empty, so the effect runs only once on mount
之後,更新導覽列程式碼,如下所示。更新後的程式碼會根據是否有登入使用者或登入使用者是管理員來控制將呈現哪些按鈕。
<div id="navbar-collapse-with-animation" className="">
{/* Navbar content container */}
<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">
{/* Conditional rendering for admin link */}
{admin ? (
// If admin is true, show the "Create Post" link
<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>
) : (
// If admin is false, render an empty div
<div></div>
)}
{/* Conditional rendering for user link/logout button */}
{user ? (
// If user is true, show the logout button
<form action={logout}>
<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>
) : (
// If user is false, show the "Login" link
<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>
如果導航到http://localhost:3000,您應該會看到僅呈現「建立貼文」和「登出」按鈕,因為使用者已登入並設定為管理員。
之後,請前往/[root]/src/app/createpost/page.tsx
檔案並匯入 Supabase createClient 函數。
import { createClient } from "@/utils/supabase/client";
在 WriteArticle 功能元件中,新增以下程式碼,使用 Supabase createClient 函數取得登入用戶,並驗證用戶的電子郵件是否與設定為管理員的用戶的電子郵件相同。
// Define the email of the user you want to make admin
const userEmail = "email of admin user";
// Create a Supabase client instance
const supabase = createClient();
// Get the user data from Supabase auth
const { data, error } = await supabase.auth.getUser();
// Check for errors or if the user data doesn't match the expected email
if (error || !data?.user || data?.user.email !== userEmail) {
// If any of the conditions are true, redirect to the homepage
redirect("/");
}
現在只有設定為admin的使用者才能存取http://localhost:3000/createpost頁面,如下所示。
在這裡,我將引導您完成使用 Supabase 資料庫向部落格設定插入和取得資料功能的過程。
首先,請前往 Supabase 儀表板中的SQL 編輯器頁面。然後將下列 SQL 程式碼新增至編輯器中,然後按一下 CTRL + Enter 鍵建立一個名為articles 的表。
articles 表包含 id、title、slug、content 和created_at 欄位。
create table if not exists
articles (
id bigint primary key generated always as identity,
title text,
slug text,
content text,
created_at timestamp
);
建立表格後,您應該會收到一條成功訊息,如下所示。
之後,請前往/[root]/src/utils/supabase
資料夾並建立一個名為AddArticle.ts
的檔案。然後將以下程式碼新增至該檔案中,將部落格文章資料插入 Supabase 資料庫。
// Server-side code
"use server";
// Importing Supabase auth helpers and Next.js functions
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
// Exporting the addArticle function
export async function addArticle(formData: any) {
// Extracting form data
const title = formData.get("title");
const content = formData.get("content");
const slug = formData
.get("title")
.split(" ")
.join("-")
.toLowerCase()
.replace(/[^a-zA-Z0-9-]/g, ""); // Generating a slug from the title
const created_at = formData.get(new Date().toDateString()); // Getting the current date
// Creating a cookie store
const cookieStore = cookies();
// Creating a Supabase client instance with cookie store
const supabase = createServerComponentClient({ cookies: () => cookieStore });
// Inserting data into the "articles" table
const { data, error } = await supabase.from("articles").insert([
{
title,
content,
slug,
created_at,
},
]);
// Handling errors
if (error) {
console.error("Error inserting data", error);
return;
}
// Redirecting to the homepage
redirect("/");
// Returning a success message
return { message: "Success" };
}
接下來,前往/[root]/src/app/components/Post.tsx
檔案並導入 addArticle 函數。
import { addArticle } from "@/utils/supabase/AddArticle";
然後加入addArticle
函數作為表單動作參數,如下所示。
<form
action={addArticle}
className="w-full h-full gap-10 flex flex-col items-center p-10">
</form>
之後,導覽至 http://localhost:3000/createpost 並向右側的聊天機器人提供類似「研究有關 JavaScript 框架的部落格文章主題」的提示。
聊天機器人將開始研究該主題,然後產生部落格標題和大綱,如下所示。
當您開始在 CopilotKitTextarea 上書寫時,您應該會看到內容自動建議,如下所示。
{% 嵌入 https://youtu.be/2oMNV1acKIs %}
如果內容符合您的喜好,請將其複製並貼上到 Quill 富文本編輯器。然後開始編輯它,如下所示。
{% 嵌入 https://youtu.be/-r3woCeq4vs %}
然後點擊底部的發布按鈕即可發布文章。前往 Supabase 上專案的儀表板並導航至「表格編輯器」部分。點擊文章表,您應該會看到您的文章資料已插入到 Supabase 資料庫中,如下所示。
接下來,前往/[root]/src/app/components/Posts.tsx
檔案並導入 createClient 函數。
import { createClient } from "@/utils/supabase/client";
在 Posts 功能元件內,加入以下程式碼,該程式碼使用 useState 掛鉤來儲存文章資料,並使用 useEffect 掛鉤在元件安裝時從 Supabase 取得文章。 fetchArticles 函數建立一個 Supabase 用戶端實例,取得文章,並在資料可用時更新狀態。
// State variable for articles
const [articles, setArticles] = useState<any[]>([]);
// useEffect hook to fetch articles on mount
useEffect(() => {
// Define an async function to fetch articles
const fetchArticles = async () => {
// Create a Supabase client instance
const supabase = createClient();
// Fetch articles from the "articles" table
const { data, error } = await supabase.from("articles").select("*");
// If data is available, update the articles state
if (data) {
setArticles(data);
}
};
// Call the fetchArticles function
fetchArticles();
}, []); // Dependency array is empty, so the effect runs only once on mount
之後,如下所示更新元素程式碼,以在部落格主頁上呈現已發佈的文章。
// Return a div element with a max width, full height, padding, and horizontal margin
return (
<div className="max-w-[85rem] h-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
// Create a grid container with dynamic number of columns based on screen size
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
// Map over the articles array and render a Link component for each item
{articles?.map((post) => (
<Link
// Assign a unique key prop to each Link component
key={post.id}
// Apply styles for the Link component
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 "
// Set the href prop to the post slug
href={`/${post.slug}`}>
// Create a container for the image
<div className="aspect-w-16 aspect-h-11">
// Render an Image component with a dynamic src based on the post title
<Image
className="object-cover h-48 w-96 rounded-xl"
src={`(link unavailable){encodeURIComponent(
post.title
)}`}
// Set the width and height props for the Image component
width={500}
height={500}
// Set the alt prop for accessibility
alt="Image Description"
/>
</div>
// Create a container for the post title
<div className="my-6">
// Render an h3 element with the post title
<h3 className="text-xl font-semibold text-white ">
{post.title}
</h3>
</div>
</Link>
))}
</div>
</div>
);
然後導航到http://localhost:3000 ,您應該會看到您發布的文章,如下所示。
接下來,前往/[root]/src/app/[slug]/page.tsx
檔案並匯入 createClient 函數。
import { createClient } from "@/utils/supabase/client";
在導入下方,定義一個名為「getArticleContent」的非同步函數,該函數根據 slug 參數從 supabase 資料庫檢索文章資料,如下所示。
// Define an asynchronous function to retrieve article content
async function getArticleContent(params: any) {
// Extract the slug parameter from the input params object
const { slug } = params;
// Create a Supabase client instance
const supabase = createClient();
// Query the "articles" table in Supabase
// Select all columns (*)
// Filter by the slug column matching the input slug
// Retrieve a single record (not an array)
const { data, error } = await supabase
.from("articles")
.select("*")
.eq("slug", slug)
.single();
// Return the retrieved article data
return data;
}
之後,如下所示更新功能元件Post,以渲染文章內容。
export default async function Post({ params }: { params: any }) {
// Fetch the post content using the getArticleContent function
const post = await getArticleContent(params);
// Return the post component
return (
// Fragment component to wrap multiple elements
<>
// Header component
<Header />
// Main container with max width and height
<main className="p-3 flex flex-col max-w-6xl mx-auto min-h-screen">
// Post title
<h1 className="text-3xl text-white mt-10 p-3 text-center font-serif max-w-2xl mx-auto lg:text-4xl">
{post && post.title} // Display post title if available
</h1>
// Post metadata (author, date, etc.)
<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>
// Estimated reading time
<span className="italic">
{post && (post.content.length / 1000).toFixed(0)} mins read
</span>
</div>
// Post content
<div
className="p-3 max-w-2xl text-white mx-auto w-full post-content border-b border-slate-500 mb-2"
// Use dangerouslySetInnerHTML to render HTML content
dangerouslySetInnerHTML={{ __html: post && post.content }}></div>
// Comment component
<Comment />
</main>
</>
);
}
導覽至http://localhost:3000並點擊部落格主頁上顯示的文章。然後您應該被重定向到文章的內容,如下所示。
在這裡,我將引導您完成使用 Supabase 資料庫為部落格內容評論設定插入和獲取資料功能的過程。
首先,請前往 Supabase 儀表板中的SQL 編輯器頁面。然後將以下 SQL 程式碼新增至編輯器中,然後按一下 CTRL + Enter 鍵建立一個名為 comments 的表。評論表有 id、content 和 postId 欄位。
create table if not exists
comments (
id bigint primary key generated always as identity,
postId text,
content text,
);
建立表格後,您應該會收到一條成功訊息,如下所示。
之後,請前往/[root]/src/utils/supabase
資料夾並建立一個名為AddComment.ts
的檔案。然後將以下程式碼新增至該檔案中,將部落格文章評論資料插入 Supabase 資料庫。
// Importing necessary functions and modules for server-side operations
"use server";
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
// Define an asynchronous function named 'addComment' that takes form data as input
export async function addComment(formData: any) {
// Extract postId and content from the provided form data
const postId = formData.get("postId");
const content = formData.get("content");
// Retrieve cookies from the HTTP headers
const cookieStore = cookies();
// Create a Supabase client configured with the provided cookies
const supabase = createServerComponentClient({ cookies: () => cookieStore });
// Insert the article data into the 'comments' table on Supabase
const { data, error } = await supabase.from("comments").insert([
{
postId,
content,
},
]);
// Check for errors during the insertion process
if (error) {
console.error("Error inserting data", error);
return;
}
// Redirect the user to the home page after successfully adding the article
redirect("/");
// Return a success message
return { message: "Success" };
}
接下來,到/[root]/src/app/components/Comment.tsx
文件,導入 addArticle createClient 函數。
import { addComment } from "@/utils/supabase/AddComment";
import { createClient } from "@/utils/supabase/client";
然後將postId作為prop參數加入Comment功能元件中。
export default function Comment({ postId }: { postId: any }) {}
在函數內部,新增以下程式碼,程式碼使用useEffect
掛鉤在元件安裝或postId
更改時從 Supabase 取得評論和文章內容。 fetchComments
函數取得所有評論,而fetchArticleContent
函數則取得具有目前postId
的文章內容。
useEffect(() => {
// Define an async function to fetch comments
const fetchComments = async () => {
// Create a Supabase client instance
const supabase = createClient();
// Fetch comments from the "comments" table
const { data, error } = await supabase.from("comments").select("*");
// If data is available, update the comments state
if (data) {
setComments(data);
}
};
// Define an async function to fetch article content
const fetchArticleContent = async () => {
// Create a Supabase client instance
const supabase = createClient();
// Fetch article content from the "articles" table
// Filter by the current postId
const { data, error } = await supabase
.from("articles")
.select("*")
.eq("id", postId)
.single();
// If the fetched article ID matches the current postId
if (data?.id == postId) {
// Update the article content state
setArticleContent(data.content);
}
};
// Call the fetch functions
fetchArticleContent();
fetchComments();
}, [postId]); // Dependency array includes postId, so the effect runs when postId changes
然後新增addComment
函數作為表單操作參數,如下所示。
<form
action={addComment}
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={{
textareaPurpose: articleContent,
chatApiConfigs: {
suggestionsApiConfig: {
forwardedParams: {
max_tokens: 5,
stop: ["\n", ".", ","],
},
},
insertionApiConfig: {},
},
debounceTime: 250,
}}
/>
<input
type="text"
id="postId"
name="postId"
value={postId}
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">
Submit
</button>
</div>
</form>
在表單元素下方,新增以下用於呈現貼文評論的程式碼。
{comments?.map(
(postComment: any) =>
postComment.postId == postId && (
<div
key={postComment.id}
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={`https://source.unsplash.com/featured/?${encodeURIComponent(
"Silhouette"
)}`}
width={500}
height={500}
alt="Profile Picture"
/>
</div>
<div className="flex-1">
<div className="flex items-center mb-1">
<span className="font-bold text-white mr-1 text-xs truncate">
Anonymous
</span>
</div>
<p className="text-gray-500 pb-2">{postComment.content}</p>
</div>
</div>
)
)}
接下來,前往/[root]/src/app/[slug]/page.tsx
檔案並將 postId 作為 prop 新增至 Comment 元件。
<Comment postId={post && [post.id](http://post.id/)} />
前往已發佈的文章內容頁面並開始在文字區域中輸入評論。您應該在鍵入時獲得內容自動建議。
然後點擊提交按鈕以加入您的評論。前往 Supabase 上專案的儀表板並導航到表格編輯器部分。點擊評論表,您應該會看到您的評論資料已插入到 Supabase 資料庫中,如下所示。
返回您發表評論的文章內容頁面,您應該會看到您的評論,如下所示。
恭喜!您已完成本教學的專案。
CopilotKit是一款令人難以置信的工具,可讓您在幾分鐘內將 AI Copilot 加入到您的產品中。無論您是對人工智慧聊天機器人和助理感興趣,還是對複雜任務的自動化感興趣,CopilotKit 都能讓您輕鬆實現。
如果您需要建立 AI 產品或將 AI 工具整合到您的軟體應用程式中,您應該考慮 CopilotKit。
您可以在 GitHub 上找到本教學的源程式碼:https://github.com/TheGreatBonnie/aiblogapp
原文出處:https://dev.to/copilotkit/im-building-an-ai-powered-blog-nextjs-langchain-supabase-5145