DEV Community

Cover image for AI-Powered Blog Pt. II (Nextjs, GPT4, Supabase & CopilotKit)
Bonnie for CopilotKit

Posted on • Updated on

AI-Powered Blog Pt. II (Nextjs, GPT4, Supabase & CopilotKit)

The AI era is upon us. To get ahead as devs, it's great to have some AI-powered projects in your portfolio.

Today, we will go through building an AI-powered blog platform with some awesome functionality such as research, autocomplete and a Copilot.

I built an initial version of this project here. A commenter had some really cool suggestions for taking it to the next level.

Image description

So we decided to build it!

TL;DR

We're building an AI-powered blogging platform Pt. II

Image description


CopilotKit: The framework for building in-app AI copilots

CopilotKit is an open-source AI copilot platform. We make it easy to integrate powerful AI into your React apps.

Build:

  • ChatBot: Context-aware in-app chatbots that can take actions in-app 💬
  • CopilotTextArea: AI-powered textFields with context-aware autocomplete & insertions 📝
  • Co-Agents: In-app AI agents that can interact with your app & users 🤖

Uploading image

Star CopilotKit ⭐️

Now back to the article!


Prerequisites

To fully understand this tutorial, you need to have a basic understanding of React or Next.js.

Here are the tools required to build the AI-powered blog:

  • Quill Rich Text Editor - a text editor that enables you to easily format text, add images, add code, and create custom, interactive content in your web app.
  • Supabase - a PostgreSQL hosting service that provides you with all the backend features you need for your project.
  • Langchain - provides a framework that enables AI agents to search the web and research any topic.
  • OpenAI API - provides an API key that enables you to carry out various tasks using ChatGPT models.
  • Tavily AI - a search engine that enables AI agents to conduct research and access real-time knowledge within the application.
  • CopilotKit - an open-source copilot framework for building custom AI chatbots, in-app AI agents, and text areas.

Project Set up and Package Installation

First, create a Next.js application by running the code snippet below in your terminal:

npx create-next-app@latest aiblogapp
Enter fullscreen mode Exit fullscreen mode

Select your preferred configuration settings. For this tutorial, we'll be using TypeScript and Next.js App Router.

Image description

Next, install Quill rich text editor, Supabase, and Langchain packages and their dependencies.

npm install quill react-quill @supabase/supabase-js @supabase/ssr @supabase/auth-helpers-nextjs @langchain/langgraph
Enter fullscreen mode Exit fullscreen mode

Finally, install the CopilotKit packages. These packages enable us to retrieve data from the React state and add AI copilot to the application.

npm install @copilotkit/react-ui @copilotkit/react-textarea @copilotkit/react-core @copilotkit/backend
Enter fullscreen mode Exit fullscreen mode

Congratulations! You're now ready to build an AI-powered blog.

Building The Blog Frontend

In this section, I will walk you through the process of creating the blog's frontend with static content to define the blog’s user interface.

The blog’s frontend will consist of four pages: the home page, post page, create post page, and login/sign up page.

To get started, go to /[root]/src/app in your code editor and create a folder called components. Inside the components folder, create five files named Header.tsx, Posts.tsx, Post.tsx, Comment.tsx and QuillEditor.tsx

In the Header.tsx file, add the following code that defines a functional component named Header that will render the blog’s navbar.

"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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the Posts.tsx file, add the following code that defines a functional component named Posts that renders the blogging platform homepage that will display a list of published articles.

"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>
  );
}

Enter fullscreen mode Exit fullscreen mode

In the QuillEditor.tsx file, add the following code that dynamically imports the QuillEditor component, defines modules configuration for the Quill editor toolbar, and defines text formats for the Quill editor.

// 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",
];

Enter fullscreen mode Exit fullscreen mode

In the Post.tsx file, add the following code that defines a functional component named CreatePost that will be used to render the article creation form.

"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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the Comment.tsx file, add the following code that defines a functional component called Comment that renders post comment form and post comments.

// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, go to /[root]/src/app and create a folder called [slug]. Inside the [slug] folder, create a page.tsx file.

Then add the following code into the file that imports Comment and Header components and defines a functional component named Post that will render the navbar, post content, comment form, and post comments.

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

After that, go to /[root]/src/app and create a folder called createpost. Inside the createpost folder, create a file called page.tsx file.

Then add the following code into the file that imports CreatePost and Header components and defines a functional component named WriteArticle that will render the navbar and the post creation form.

import CreatePost from "../components/Post";
import Header from "../components/Header";
import { redirect } from "next/navigation";

export default async function WriteArticle() {
  return (
    <>
      <Header />
      <CreatePost />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, go to /[root]/src/page.tsx file, and add the following code that imports Posts and Header components and defines a functional component named Home.

import Header from "./components/Header";
import Posts from "./components/Posts";

export default async function Home() {
  return (
    <>
      <Header />
      <Posts />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

After that, go to the next.config.mjs file and rename it to next.config.js . Then add the following code that allows you to use images from Unsplash as cover images for the published articles.

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "source.unsplash.com",
      },
    ],
  },
};

Enter fullscreen mode Exit fullscreen mode

Next, remove the CSS code in the globals.css file and add the following CSS code.

@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;
}
Enter fullscreen mode Exit fullscreen mode

Finally, run the command npm run dev on the command line and then navigate to http://localhost:3000/.

Now you should view the blogging platform frontend on your browser, as shown below.

Image description

Integrating AI Functionalities To The Blog Using CopilotKit

In this section, you will learn how to add an AI copilot to the blog to perform blog topic research and content autosuggestions using CopilotKit.

CopilotKit offers both frontend and backend packages. They enable you to plug into the React states and process application data on the backend using AI agents.

First, let's add the CopilotKit React components to the blog frontend.

Adding CopilotKit to the Blog Frontend

Here, I will walk you through the process of integrating the blog with the CopilotKit frontend to facilitate blog article research and article outline generation.

To get started, use the code snippet below to import useMakeCopilotReadableuseCopilotActionCopilotTextarea, and HTMLCopilotTextAreaElement custom hooks at the top of the /[root]/src/app/components/Post.tsx file.

import {
    useMakeCopilotReadable,
    useCopilotAction,
  } from "@copilotkit/react-core";
  import {
    CopilotTextarea,
    HTMLCopilotTextAreaElement,
  } from "@copilotkit/react-textarea";
Enter fullscreen mode Exit fullscreen mode

Inside the CreatePost function, below the state variables, add the following code that uses the useMakeCopilotReadable hook to add the article outline that will be generated as context for the in-app chatbot. The hook makes the article outline readable to the copilot.

useMakeCopilotReadable(
    "Blog article outline: " + JSON.stringify(articleOutline)
  );
Enter fullscreen mode Exit fullscreen mode

Below the useMakeCopilotReadable hook, use the code below to create a reference called copilotTextareaRef to a textarea element called HTMLCopilotTextAreaElement.

const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);
Enter fullscreen mode Exit fullscreen mode

Below the code above, add the following code that uses the useCopilotAction hook to set up an action called researchBlogArticleTopic which will enable research on a given topic for a blog article.

The action takes in two parameters called articleTitle and articleOutline which enables generation of an article title and outline.

The action contains a handler function that generates an article title and outline based on a given topic.

Inside the handler function, articleOutline state is updated with the newly generated outline while articleTitle state is updated with the newly generated title, as shown below.

// 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)
);
Enter fullscreen mode Exit fullscreen mode

Below the code above, go to the form component and add the following CopilotTextarea component that will enable you to add text completions, insertions, and edits to your article content.

<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,
            }}
          />
Enter fullscreen mode Exit fullscreen mode

After that, go to /[root]/src/app/createpost/page.tsx file and import CopilotKit frontend packages and styles at the top using the code below.

import { CopilotKit } from "@copilotkit/react-core";
import { CopilotPopup } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
Enter fullscreen mode Exit fullscreen mode

Then use CopilotKit to wrap the CopilPopup and CreatePost components, as shown below. The CopilotKit component specifies the URL for CopilotKit's backend endpoint (/api/copilotkit/) while the CopilotPopup renders the in-app chatbot that you can give prompts to research any topic for an article.

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

After that, use the code snippet below to import useMakeCopilotReadableCopilotKitCopilotTextarea, and HTMLCopilotTextAreaElement custom hooks at the top of the /[root]/src/app/components/Comment.tsx file.

import { useMakeCopilotReadable, CopilotKit } from "@copilotkit/react-core";
import {
  CopilotTextarea,
  HTMLCopilotTextAreaElement,
} from "@copilotkit/react-textarea";
Enter fullscreen mode Exit fullscreen mode

Inside the Comment function, below the state variables, add the following code that uses the useMakeCopilotReadable hook to add the post content as context for the comment content autosuggestion.

useMakeCopilotReadable(
    "Blog article content: " + JSON.stringify(articleContent)
  );

  const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);
Enter fullscreen mode Exit fullscreen mode

Then add the CopilotTextarea component to the comment form and use CopilotKit to wrap the form, as shown below.

<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>
Enter fullscreen mode Exit fullscreen mode

After that, run the development server and navigate to http://localhost:3000/createpost. You should see that the popup in-app chatbot and CopilotTextarea were integrated into the Blog.

Image description

Congratulations! You've successfully added CopilotKit to the Blog Frontend*.*

Adding CopilotKit Backend to the Blog

Here, I will walk you through the process of integrating the blog with the CopilotKit backend that handles requests from frontend, and provides function calling and various LLM backends such as GPT.

Also, we will integrate an AI agent named Tavily that can research any topic on the web.

To get started, create a file called .env.local in the root directory. Then add the environment variables below in the file that hold your ChatGPT and Tavily Search API keys.

OPENAI_API_KEY="Your ChatGPT API key"
TAVILY_API_KEY="Your Tavily Search API key"
Enter fullscreen mode Exit fullscreen mode

To get the ChatGPT API key, navigate to https://platform.openai.com/api-keys.

Image description

To get the Tavily Search API key, navigate to https://app.tavily.com/home

Image description

After that, go to /[root]/src/app and create a folder called api. In the api folder, create a folder called copilotkit.

In the copilotkit folder, create a file called research.ts. Then Navigate to this research.ts gist file, copy the code, and add it to the research.ts file

Next, create a file called route.ts in the /[root]/src/app/api/copilotkit folder. The file will contain code that sets up a backend functionality to process POST requests. It conditionally includes a "research" action that performs research on a given topic.

Now import the following modules at the top of the file.

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.

Enter fullscreen mode Exit fullscreen mode

Below the code above, define a runtime environment variable and a function named researchAction that researches a certain topic using the code below.

// 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.
  },
};

Enter fullscreen mode Exit fullscreen mode

Then add the code below under the code above to define an asynchronous function that handles POST requests.

// 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());
}

Enter fullscreen mode Exit fullscreen mode

Congratulations! You've successfully added the CopilotKit backend to the Blog*.*

Integrating Database to the Blog Using Supabase

In this section, I will walk you through the process of integrating the blog with the Supabase database to insert and fetch blog article and comments data.

To get started, navigate to supabase.com and click the Start your project button on the home page.

Image description

Then create a new project called AiBloggingPlatform, as shown below.

Image description

Once the project is created, add your Supabase URL and API key to environment variables in the env.local file, as shown below.

NEXT_PUBLIC_SUPABASE_URL=Your Supabase URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=Your Supabase API Key
Enter fullscreen mode Exit fullscreen mode

Setting up Supabase Authentication for the blog

Here, I will walk you through the process of setting up authentication for the blog that enables users to sign up, log in, or log out.

To get started, go to /[root]/src/ and create a folder named utils. Inside the utils folder, create a folder named supabase.

Then create two files named client.ts and server.ts inside the supabase folder.

After that, navigate to this supabase docs link, copy the code provided there, and paste it to the respective files you created in the supabase folder.

Image description

Next, create a file named middleware.ts at the root of your project and another file by the same name inside the /[root]/src/utils/supabase folder.

After that, navigate to this supabase docs link, copy the code provided there, and paste it to the respective middleware.ts files.

Image description

Next, go to /[root]/src/app/login folder and create a file named actions.ts. After that, navigate to this supabase docs link, copy the code provided there, and paste it to the actions.ts.

Image description

After that, change the email template to support the authentication flow. To do that, go to the Auth templates page in your Supabase dashboard.

In the Confirm signup template, change {{ .ConfirmationURL }} to {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup as shown below and then click the save button.

Image description

After that, create a route handler for authentication confirmation using email*.* To do that*, go to* /[root]/src/app/ and create a folder named auth. Then create a folder named confirm inside the auth folder.

Inside the confirm folder, create a file named route.ts and navigate to this supabase docs link, copy the code provided there, and paste it to the route.ts file.

Image description

After that, go to /[root]/src/app/login/page.tsx file and use the code snippet below to import the Supabase signup and login functions.

import { login, signup } from "./actions";
Enter fullscreen mode Exit fullscreen mode

In the signup/login form, use formAction to call the Supabase signup and login functions when the login and signup buttons are clicked, as shown below.

<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>
Enter fullscreen mode Exit fullscreen mode

After that, run the development server and navigate to http://localhost:3000/login. Add an email and password as shown below and click the Sign Up button.

Image description

Then go to the inbox of the email you used to sign up and click the Confirm your email button, as shown below.

Image description

After that, go to the Auth users page in your Supabase dashboard and you should see the newly created user, as shown below.

Image description

Next, set up logout functionality. To do that, go to /[root]/src/app and create a folder named logout. Then create a file named route.ts and add the following code into the file.

// 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("/");
}
Enter fullscreen mode Exit fullscreen mode

After that, go to /[root]/src/app/components/Header.tsx file and import the Supabase logout function using the code snippet below.

import { logout } from "../logout/actions";
Enter fullscreen mode Exit fullscreen mode

Then add the logout function to form action parameter, as shown below.

<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>
Enter fullscreen mode Exit fullscreen mode

If you click the Logout button, the logged-in user will be logged out.

Setting up user roles and permissions for the blog

Here, I will walk you through the process of setting up user roles and permissions that control what different users can do on the blog.

To get started, go to /[root]/src/app/components/Header.tsx file and import the Supabase createClient function.

import { createClient } from "@/utils/supabase/client";
Enter fullscreen mode Exit fullscreen mode

Then import useState and useEffect hooks and define a type named user using the code snippet below.

import { useEffect, useState } from "react";

type User = {};
Enter fullscreen mode Exit fullscreen mode

Inside the Header functional component, add the following code that uses the useState hook to store the user and admin data, and the useEffect hook to fetch the user data from Supabase auth when the component mounts. The getUser function checks for errors and sets the user and admin states accordingly.

// 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
Enter fullscreen mode Exit fullscreen mode

After that, update the navbar code as shown below. The updated code controls which buttons will be rendered depending on whether there is a logged-in user or the logged-in user is an admin.

<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>
Enter fullscreen mode Exit fullscreen mode

If you navigate to http://localhost:3000 you should see that only the Create Post and Logout buttons are rendered because the user is logged in and set to admin.

Image description

After that, go to /[root]/src/app/createpost/page.tsx file and import the Supabase createClient function.

import { createClient } from "@/utils/supabase/client";
Enter fullscreen mode Exit fullscreen mode

Inside the WriteArticle functional component, add the following code that fetches the logged in user using Supabase createClient function and verifies if the user’s email is the same with the email of the user set as admin.

// 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("/");
}
Enter fullscreen mode Exit fullscreen mode

Now only a user set to admin can access the http://localhost:3000/createpost page, as shown below.

Image description

Setting up insert and fetch post data with Supabase functionality

Here, I will walk you through the process of setting up insert and fetch data functionality to the blog using the Supabase database.

To get started, go to the SQL Editor page in your Supabase dashboard. Then add the following SQL code to the editor and click CTRL + Enter keys to create a table called articles.

The articles table has id, title, slug, content, and created_at columns.

create table if not exists
  articles (
    id bigint primary key generated always as identity,
    title text,
    slug text,
    content text,
    created_at timestamp 
  );
Enter fullscreen mode Exit fullscreen mode

Once the table is created, you should get a success message, as shown below.

Image description

After that, go to /[root]/src/utils/supabase folder and create a file called AddArticle.ts. Then add the following code that inserts blog article data into the Supabase database to the file.

// 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" };
}
Enter fullscreen mode Exit fullscreen mode

Next, go to /[root]/src/app/components/Post.tsx file and import the addArticle function.


import { addArticle } from "@/utils/supabase/AddArticle";
Enter fullscreen mode Exit fullscreen mode

Then add the addArticle function as the form action parameter, as shown below.

<form
   action={addArticle}
   className="w-full h-full gap-10 flex flex-col items-center p-10">

</form>
Enter fullscreen mode Exit fullscreen mode

After that, navigate to http://localhost:3000/createpost and give the chatbot on the right side a prompt like, “research a blog article topic on JavaScript frameworks.”

The chatbot will start researching the topic and then generate a blog title and outline, as shown below.

Image description

When you start writing on the CopilotKitTextarea, you should see content autosuggestions, as shown below.

If the content is up to your liking, copy and paste it to the Quill rich text editor. Then start editing it as shown below.

Then click the publish button at the bottom to publish the article. Go to your project’s dashboard on Supabase and navigate to the Table Editor section. Click the articles table and you should see that your article data was inserted into the Supabase database, as shown below.

Image description

Next, go to /[root]/src/app/components/Posts.tsx file and import the createClient function.

import { createClient } from "@/utils/supabase/client";
Enter fullscreen mode Exit fullscreen mode

Inside the Posts functional component, add the following code that uses the useState hook to store the articles data and the useEffect hook to fetch the articles from Supabase when the component mounts. The fetchArticles function creates a Supabase client instance, fetches the articles, and updates the state if data is available.

// 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
Enter fullscreen mode Exit fullscreen mode

After that, update the elements code as shown below to render the published articles on the blog’s homepage.

// 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>
);
Enter fullscreen mode Exit fullscreen mode

Then navigate to  http://localhost:3000 and you should see the article you published, as shown below.

Image description

Next, go to /[root]/src/app/[slug]/page.tsx file and import the createClient function.

import { createClient } from "@/utils/supabase/client";
Enter fullscreen mode Exit fullscreen mode

Below the imports, define an asynchronous function named 'getArticleContent' that retrieves article data from supabase database based on slug parameter, as shown below.

// 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;
}
Enter fullscreen mode Exit fullscreen mode

After that, update the functional component Post as shown below to render the article content.

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Navigate to  http://localhost:3000 and click an article displayed on the blog homepage. You should then be redirected to the article’s content, as shown below.

Image description

Setting up insert and fetch comment data with Supabase functionality

Here, I will walk you through the process of setting up insert and fetch data functionality for the blog’s content comments using the Supabase database.

To get started, go to the SQL Editor page in your Supabase dashboard. Then add the following SQL code to the editor and click CTRL + Enter keys to create a table called comments. The comments table has id, content, and postId columns.

create table if not exists
  comments (
    id bigint primary key generated always as identity,
    postId text,
    content text,
  );
Enter fullscreen mode Exit fullscreen mode

Once the table is created, you should get a success message, as shown below.

Image description

After that, go to /[root]/src/utils/supabase folder and create a file called AddComment.ts . Then add the following code that inserts blog article comment data into the Supabase database to the file.

// 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" };
}
Enter fullscreen mode Exit fullscreen mode

Next, go to /[root]/src/app/components/Comment.tsx file, import the addArticle createClient functions.


import { addComment } from "@/utils/supabase/AddComment";

import { createClient } from "@/utils/supabase/client";
Enter fullscreen mode Exit fullscreen mode

Then add postId as prop parameter to Comment functional component.

export default function Comment({ postId }: { postId: any }) {}
Enter fullscreen mode Exit fullscreen mode

Inside the function, add the following code that uses uses the useEffect hook to fetch comments and article content from Supabase when the component mounts or when the postId changes. The fetchComments function fetches all comments, while the fetchArticleContent function fetches the content of the article with the current 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
Enter fullscreen mode Exit fullscreen mode

Then add the addComment function as the form action parameter, as shown below.

<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>
Enter fullscreen mode Exit fullscreen mode

Below the form element, add the following code that renders post comments.

{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>
            )
        )}
Enter fullscreen mode Exit fullscreen mode

Next, go to /[root]/src/app/[slug]/page.tsx file and add postId as a prop to the Comment component.

<Comment postId={post && [post.id](http://post.id/)} />
Enter fullscreen mode Exit fullscreen mode

Go to the published article content page and start typing a comment in the textarea. You should get content autosuggestion as you type.

Image description

Then click the submit button to add your comment. Go to your project’s dashboard on Supabase and navigate to the Table Editor section. Click the comments table and you should see that your comment data was inserted into the Supabase database, as shown below.

Image description

Go back to the published article content page you commented and you should see your comment, as shown below.

Image description

Congratulations! You’ve completed the project for this tutorial.

Conclusion

CopilotKit is an incredible tool that allows you to add AI Copilots to your products within minutes. Whether you're interested in AI chatbots and assistants or automating complex tasks, CopilotKit makes it easy.

If you need to build an AI product or integrate an AI tool into your software applications, you should consider CopilotKit.

You can find the source code for this tutorial on GitHub: https://github.com/TheGreatBonnie/aiblogapp

Top comments (23)

Collapse
 
samiekblad profile image
Sami Ekblad

Looks good! Setting up local LLM stuff has got som much easier lately. I tested the setup of my own custom web app UI with Ollama and it takes like 20 lines of code and 2 commands to get Mistral (or llama3) running locally.

Collapse
 
the_greatbonnie profile image
Bonnie

That's great Sami.

Collapse
 
rahmanoff profile image
Kostiantyn Rakhmanov

Hi!
Where and when we are created - /[root]/src/app/login/page.tsx ?
Can you pls provide full code from it?

Image description

Collapse
 
the_greatbonnie profile image
Bonnie

Check out the code here.

github.com/TheGreatBonnie/aiblogapp

Collapse
 
gilbertofke profile image
Gilbert Cheruiyot

This is a comprehensive guide to be honest. Great work Bonnie. Always love your articles.

Collapse
 
erickrodrcodes profile image
Erick Rodriguez

Hey, who knows if the guide was planned/guided by AI. Maybe our love the AI.

Collapse
 
the_greatbonnie profile image
Bonnie

Happy to hear that, Gilbert.

Looking forward to writing even better articles.

Collapse
 
uliyahoo profile image
uliyahoo

Agreed. It's really comprehensive and goes through every piece of building a functioning AI app.

Collapse
 
thomas123 profile image
Thomas

I remember seeing that comment on the first post. Can't believe you actually implemented it haha

Collapse
 
the_greatbonnie profile image
Bonnie

It was good inspiration and challenge.

Collapse
 
johny0012 profile image
Johny

Nice attitude!

Collapse
 
nevodavid profile image
Nevo David

That is a really good explanation of how to build a blog with AI

Collapse
 
the_greatbonnie profile image
Bonnie

Thanks, Nevo.

Collapse
 
uliyahoo profile image
uliyahoo

What do you think David Sugar? :)

Collapse
 
steven0121 profile image
Steven

REALLY long...

Collapse
 
the_greatbonnie profile image
Bonnie

You can check the demo repo link in the conclusions section and clone the project.

Collapse
 
time121212 profile image
tim brandom

Too long.

Collapse
 
envitab profile image
Ekemini Samuel

Great work, Bonnie!

Collapse
 
the_greatbonnie profile image
Bonnie

Thanks, Samuel.

Collapse
 
james0123 profile image
James

I often enjoy your articles Bonnie. Thank you for another great one, this time on an AI-powered blogging platform.

Collapse
 
the_greatbonnie profile image
Bonnie

I am happy to hear that you enjoy my articles, James.

Looking forward to writing even better ones.

Collapse
 
excel_raji_867783892636cb profile image
Excel Raji

source.unsplash does not work. Anyone got any suggestions?

Collapse
 
the_greatbonnie profile image
Bonnie

Let me look into it.