🧙‍♂️ Generate blogs with ChatGPT assistant 🪄 ✨
Tags: webdev, programming, tutorial, react
Published: 2023-12-06 15:02:29

# TL;DR

We have all seen what ChatGPT can do (it’s not new to anybody).

So many articles are being written over and over using ChatGPT.

**Actually**, half of the articles on DEV are written with ChatGPT.

You can use some [AI content detector](https://copyleaks.com/ai-content-detector) to see.

The thing is that ChatGPT will never produce some extraordinary content except for the one it already has (trained/fine-tuned) inside.

But there is a way to go beyond the currently trained content using RAG (OpenAI assistants).

[In the last article](https://dev.to/triggerdotdev/train-chatgpt-on-your-documentation-1a9g), we talked about “training” ChatGPT on your documentation; today, let’s see how we can make a lot of content from it. We will:

- Create a new blogging system with the use of Docusaurus.
- Question ChatGPT to write us a blog post connected to the documentation.


![Blog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ms26qb0uahpi898s0qun.gif)

---

## Your background job platform 🔌

[Trigger.dev](https://trigger.dev/) is an open-source library that enables you to create and monitor long-running jobs for your app with NextJS, Remix, Astro, and so many more!

&nbsp;

[![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev)

Please help us with a star 🥹.
It would help us to create more articles like this 💖

{% cta https://github.com/triggerdotdev/trigger.dev %} Star the Trigger.dev repository ⭐️ {% endcta %}

---

## Recap from the last time ⏰

- We have created a job to take a documentation XML and extract all the URLs.
- We scraped each website URL and extracted the titles and content.
- We saved everything into a file and sent it to the ChatGPT assistant.
- We created a ChatBot screen to question ChatGPT about the documentation.

You can find the complete source code of the previous [tutorial here](https://github.com/triggerdotdev/blog/tree/main/openai-assistant).

---

![Tools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4adju83b5s1k0qozh3x.png)

## A slight modification ⚙️

Last time, we created an assistant for documentation. We wrote:

```jsx
You are a documentation assistant, loaded with documentation from ' + payload.url + ', return everything in an MD format.
```

Let’s change it to a blog writer, head over to `jobs/process.documentation.ts` line 92, and replace it with the following content:

```markdown
You are a content writer assistant. You have been loaded with documentation from ${payload.url}, you write blog posts based on the documentation and return everything in the following MD format:
---
slug: [post-slug]
title: [post-title]
---

[post-content]
```

It’s essential to use the `slug` and the `title` because that’s the format Docusaurus - our blogging system can accept (of course, we also send all the output in an MD format)

---

![Docusaurus](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gu8wlh7qk8e3rh6mz35v.png)

## Docusaurus 🦖

You can use many types of blogging systems!

For our use case, we will use Docusaurus, which can read MD-based formatting (the output we requested from ChatGPT). **We can install Docusaurus by running:**

```bash
npx create-docusaurus@latest blog classic --typescript 
```

Next, we can go into the created directory and run the following:

```markdown
npm run start
```

That will start Docusaurus. You can pay attention. There is an additional directory called `blog` with all the blog posts; that’s where we will save ChatGPT-generated blog posts.


![Example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pgo25rlkw85nfvbh0y4s.png)

---


![Blogs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v3oxjtli1dn9i9klnj5t.png)

## Generating blogs 📨

We need to create a job that

- Get the blog title
- Generate an entire blog post using ChatGPT
- Save it to an MD file on our blog

We can easily use ChatGPT for that!

Go to the `jobs` folder and add a new file called `process.blog.ts`. Add the following code:

```jsx
import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "@openai-assistant/trigger";
import {object, string} from "zod";
import {openai} from "@openai-assistant/helper/open.ai";
import {writeFileSync} from "fs";
import slugify from "slugify";

client.defineJob({
  // This is the unique identifier for your Job, it must be unique across all Jobs in your project.
  id: "process-blog",
  name: "Process Blog",
  version: "0.0.1",
  // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
  trigger: eventTrigger({
    name: "process.blog.event",
    schema: object({
      title: string(),
      aId: string(),
    })
  }),
  integrations: {
    openai
  },
  run: async (payload, io, ctx) => {
      const {title, aId} = payload;
      const thread = await io.openai.beta.threads.create('create-thread');

      await io.openai.beta.threads.messages.create('create-message', thread.id, {
          content: `
           title: ${title}
          `,
          role: 'user',
      });

      const run = await io.openai.beta.threads.runs.createAndWaitForCompletion('run-thread', thread.id, {
          model: 'gpt-4-1106-preview',
          assistant_id: payload.aId,
      });

      if (run.status !== "completed") {
          console.log('not completed');
          throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`);
      }

      const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id, {
          query: {
              limit: "1"
          }
      });

      return io.runTask('save-blog', async () => {
          const content = messages[0].content[0];
          if (content.type === 'text') {
              const fileName = slugify(title, {lower: true, strict: true, trim: true});
              writeFileSync(`./blog/blog/${fileName}.md`, content.text.value)
              return {fileName};
          }
      });
  },
});
```

- We added some required variables:
    - `title` the blog post title
    - `aId` the assistant ID added in the previous article.
- We created a new thread for the assistant(`io.openai.beta.threads.create`)- we can’t question it without any thread. Unlike the previous tutorial, here, we make a new thread on every request. We don’t want the context of the last messages in the conversation.
- We then add a new message to the thread(`io.openai.beta.threads.messages.create`) with the blog's title. We don’t need to provide additional instructions - we have done that part in the first section 😀
- We run the `io.openai.beta.threads.runs.createAndWaitForCompletion` to start the process - usually, you would need some kind of recursion that runs every minute to check if the job is completed, but [Trigger.dev](http://Trigger.dev) already added a way to run the process and wait for it at the same time 🥳
- We run the `io.openai.beta.threads.messages.list` with a `limit: 1` in the query body to get the first message from the conversation (in the ChatGPT result, the first message is the last message).
- Then, we save the newly created blog with the value we got from ChatGPT using `writeFileSync` - make sure you have the right path to the blog.

Go to `jobs/index.ts` and add the following line:

```jsx
export * from "./process.blog";
```

Now, let’s create a new route to trigger the job.

Go to `app/api`, create a new folder called `blog`, and within a new file called `route.tsx`

add the following code:

```jsx
import {client} from "@openai-assistant/trigger";

export async function POST(request: Request) {
    const payload = await request.json();
    if (!payload.title || !payload.aId) {
        return new Response(JSON.stringify({error: 'Missing parameters'}), {status: 400});
    }

    // We send an event to the trigger to process the documentation
    const {id: eventId} = await client.sendEvent({
        name: "process.blog.event",
        payload
    });

    return new Response(JSON.stringify({eventId}), {status: 200});
}
```

- We check that the title and assistant ID exist.
- We trigger an event in [Trigger.dev](http://Trigger.dev) and send the information.
- We send the event ID back to the client so we can track the progress of the job.

---


![frontend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kgh52s7mxd20w91kr3c9.png)

## The frontend  🎩

There’s not much to be done!

Inside our `components` directory, let’s create a new file called `blog.component.tsx` and the following code:

```jsx
"use client";

import {FC, useCallback, useEffect, useState} from "react";
import {ExtendedAssistant} from "@openai-assistant/components/main";
import {SubmitHandler, useForm} from "react-hook-form";
import {useEventRunDetails} from "@trigger.dev/react";

interface Blog {
    title: string,
    aId: string;
}

export const BlogComponent: FC<{list: ExtendedAssistant[]}> = (props) => {
    const {list} = props;
    const {register, formState, handleSubmit} = useForm<Blog>();
    const [event, setEvent] = useState<string | undefined>(undefined);

    const addBlog: SubmitHandler<Blog> = useCallback(async (param) => {
        const {eventId} = await (await fetch('/api/blog', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(param)
        })).json();

        setEvent(eventId);
    }, []);

    return (
        <>
        <form className="flex flex-col gap-3 mt-5" onSubmit={handleSubmit(addBlog)}>
            <div className="flex flex-col gap-1">
                <div className="font-bold">Assistant</div>
                <select className="border border-gray-200 rounded-xl py-2 px-3" {...register('aId', {required: true})}>
                    {list.map(val => (
                        <option key={val.id} value={val.aId}>{val.url}</option>
                    ))}
                </select>
            </div>
            <div className="flex flex-col gap-1">
                <div className="font-bold">Title</div>
                <input className="border border-gray-200 rounded-xl py-2 px-3" placeholder="Blog title" {...register('title', {required: true})} />
            </div>
            <button className="border border-gray-200 rounded-xl py-2 px-3 bg-gray-100 hover:bg-gray-200" disabled={formState.isSubmitting}>Create blog</button>
        </form>
        {!!event && (
            <Blog eventId={event} />
        )}
        </>
    )
}

export const Blog: FC<{eventId: string}> = (props) => {
    const {eventId} = props;
    const { data, error } = useEventRunDetails(eventId);

    if (data?.status !== 'SUCCESS') {
        return <div className="pointer bg-yellow-300 border-yellow-500 p-1 px-3 text-yellow-950 border rounded-2xl">Loading</div>
    }
    return (
        <div>
            <a href={`http://localhost:3000/blog/${data.output.fileName}`}>Check blog post</a>
        </div>
    )
};
```

- We use `react-hook-form` to control our inputs easily.
- We let users choose the assistant they want to use.
- We create a new input containing the title of the article.
- We send everything to our previously created route and get back the `eventId` of the job.
- We create a new `<Blog />` component that shows loading until the event is finished and add a link to our blog with the newly created tutorial.

Add the component to our `components/main.tsx` file:

```jsx
{assistantState.filter(f => !f.pending).length > 0 && <BlogComponent list={assistantState} />}
```

And we are done!


![Done](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fkm37v5idrxexjje2u3o.png)

Now, let’s add the blog title and click generate.


![Blog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gosm1f1ttz3q1m0atu7s.png)

---

![pic](https://res.cloudinary.com/practicaldev/image/fetch/s--uTFwMeAp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0half2g6r5zfn7asq084.png)

## Let's connect! 🔌

As an open-source developer, you can join our [community](https://discord.gg/nkqV9xBYWy) to contribute and engage with maintainers. Don't hesitate to visit our [GitHub repository](https://github.com/triggerdotdev/trigger.dev) to contribute and create issues related to Trigger.dev.

The source for this tutorial is available here:

https://github.com/triggerdotdev/blog/tree/main/openai-blog-writer

Thank you for reading!