Building a Strapi Custom Field for Text Generation with Open AI

Strapi - Feb 7 '23 - - Dev Community

Author: @malgamves

Yes, another article about AI. Except this one is really interesting, I promise. In this article, I’ll teach you how to use AI to generate content in Strapi. Strapi right now does not natively allow you to leverage AI for content creation, so we’re going to use Custom Fields. As a matter of fact, we’ll build one from scratch ourselves. A secondary goal of this article is to explain how you can create their Strapi custom field.

Strapi Custom Fields

Strapi comes equipped with several fields. From a content creator perspective, these fields describe the type of content you will be inputting. They range from rich text to JSON and so many more in between. You can read more about fields in our user documentation.

Custom fields, as the name suggests, let us define fields outside of the default fields Strapi comes with, giving us a cool way to integrate with a wide range of third-party tools. That tool for us today will be Open AI.

What We Will Build

Imagine typing text into a text field. When published, we’d have the input text displayed wherever we want to send our content. Similarly, with an AI field, we’d offload most of the writing so that the AI does the heavy lifting, and we proofread it because... Computers are dumb.

At this point, we have a pretty solid idea of what we’ll build. The finished project details are here if you’d like to reference them as we go along this tutorial.

Creating a Custom Field

Let’s start by creating a brand new Strapi project in our terminal with the following command:

Npx create-strapi-app@latest plugin-dev —quickstart
Enter fullscreen mode Exit fullscreen mode

Once our project is up and running, create an admin user and log into your admin panel. Next, we need to create a plugin as custom fields work alongside Strapi's Plugin API.

In your terminal, navigate to your project route and enter the following command

npm run strapi generate
Enter fullscreen mode Exit fullscreen mode

Select “plugin” from the list of options and name it ai-text-generation. After naming the plugin, we need to enable it by adding it to the Strapi plugin config file in ./config/plugins.js.

If this file doesn’t exist already, you can create it.


    module.exports = {
      // ...
    'ai-text-generation': {
      enabled: true,
      resolve: './src/plugins/ai-text-generation'


    },
//...

    }

Enter fullscreen mode Exit fullscreen mode

When we restart the server, we should see our plugin in the Strapi admin panel. We’ll restart the server in watch-mode so that we don’t have to rebuild our admin app every time we make changes to the admin code. To do that, enter the following command in your terminal:

Npm run develop –watch-admin
Enter fullscreen mode Exit fullscreen mode

Before we can try out our custom field, we have to do a couple of things. First, we need to register it on our server. Go to ./src/plugins/ai-text-generation/server/register.js and paste the following code.

'use strict';

module.exports = ({ strapi }) => {
  strapi.customFields.register({
    name: 'text-ai',
    plugin: 'ai-text-generation',
    type: 'string',
  });
};

Enter fullscreen mode Exit fullscreen mode

Second, we need to register our custom field in the admin panel. To do that, go to ./src/plugins/ai-text-generation/admin/src/index.js and paste the following code:

import { prefixPluginTranslations } from '@strapi/helper-plugin';
import pluginPkg from '../../package.json';
import pluginId from './pluginId';
import PluginIcon from './components/PluginIcon';


const name = pluginPkg.strapi.name;

export default { 
  register(app) {

    app.customFields.register({
      name: "text-ai",
      pluginId: "ai-text-generation", 
      type: "string", 
      intlLabel: {
        id: "ai-text-generation.text-ai.label",
        defaultMessage: "Text AI",
      },
      intlDescription: {
        id: "ai-text-generation.text-ai.description",
        defaultMessage: "Let AI do your writing!",
      },
      icon: PluginIcon,
      components: {
        Input: async () => import(/* webpackChunkName: "input-component" */ "./components/Input"),
      },
      options: {
      },
    });
  },



  bootstrap(app) {},
  async registerTrads({ locales }) {
    const importedTrads = await Promise.all(
      locales.map((locale) => {
        return import(
          /* webpackChunkName: "translation-[request]" */ `./translations/${locale}.json`
        )
          .then(({ default: data }) => {
            return {
              data: prefixPluginTranslations(data, pluginId),
              locale,
            };
          })
          .catch(() => {
            return {
              data: {},
              locale,
            };
          });
      })
    );

    return Promise.resolve(importedTrads);
  },
};

Enter fullscreen mode Exit fullscreen mode

Don’t worry if your custom field breaks after. When registering our custom field, we do three essential things for a custom field:

  • We define type: "string" - which tells us what type of data our custom field will hold. Depending on what you’re building, you might want to change this. For our use case today, we’ll use type: "string".
  • We assign our field an Input component in ./components/Input. All custom fields require an Input component that admin users can access in the content manager. We’re at liberty to design the input component as we wish using the Strapi Design System with the constraint of having to return a single value that we store in the database. We’ll see more of this as we create an input component in the next section.
  • We define a Plugin Icon with icon: PluginIcon, this helps us identify the custom field in the Content Types Builder.

Creating an Input Component

As mentioned above, all custom fields need an Input Component. In ./src/plugins/ai-text-generation/admin/src/components/, create a folder called Input. In your Input folder, create a file called index.js and paste the code below in it:

import React, { useState } from 'react';
import { useIntl } from 'react-intl';
import { TextInput } from '@strapi/design-system/TextInput';
import { Stack } from '@strapi/design-system/Stack';
import { Button } from '@strapi/design-system/Button';
import { Textarea } from '@strapi/design-system';
import { auth } from '@strapi/helper-plugin'


export default function Index({
  name,
  error,
  description,
  onChange,
  value,
  intlLabel,
  attribute,
}) {
  const { formatMessage } = useIntl();
  const [prompt, setPrompt] = useState('');
  const [err, setErr] = useState(''); 

  const generateText = async () => {
    try {
      const response = await fetch(`/ai-text-generation/generate-text`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${auth.getToken()}` 
        },
        body: JSON.stringify({
          'model': 'text-davinci-001',
          'prompt': `${prompt}`,
          'temperature': 0.4,
          'max_tokens': 64,
          'top_p': 1,
          'frequency_penalty': 0,
          'presence_penalty': 0
        })
      });

      if (!response.ok) {
        throw new Error(`Error! status: ${response.status}`);
      }

      const result = await response.json();
      const parsedResult = result.choices[0].text.replace(/(?:\r\n|\r|\n)/g, '');

      onChange({ target: { name, value: parsedResult, type: attribute.type } })
    } catch (err) {
      setErr(err.message);
    }
  }

  const clearGeneratedText = async () => {
    onChange({ target: { name, value: '', type: attribute.type } })

  }

  return (
    <Stack spacing={1}>
      <TextInput
        placeholder="Please write a prompt for content to generate"
        label="Prompt"
        name="Prompt"
        onChange={(e) => setPrompt(e.target.value)}
        value={prompt}
      />
      <Stack padding={4} spacing={2}>
        <Textarea
          placeholder="Generated text"
          label="Content"
          name="content"
          onChange={(e) =>
            onChange({
              target: { name, value: e.target.value, type: attribute.type },
            })
          }
        >
          {value}
        </Textarea>
        <Stack horizontal spacing={4}>
          <Button onClick={() => generateText()}>Generate</Button>
          <Button onClick={() => clearGeneratedText()}>Clear</Button>
        </Stack>
      </Stack>
    </Stack>
  )
}
Enter fullscreen mode Exit fullscreen mode

A couple of things happen here. We’ll look at what the main elements do to understand it better.

<TextInput /> : acts as our input for prompts; we use onChange() ‘watcher’ to update the value of our prompt state as we type data. We then store the prompt in a variable called prompt. TextArea />: is what we use to store the value of our generated text. We use a <TextArea /> to edit the generated text. We use a special implementation of our onChange() watcher, which allows us to pass an object of multiple values to update when a change occurs in that component.

We also have two buttons where all the magic happens. One button calls generateText() - this function uses fetch to call our backend service, /ai-text-generation/generate-text that makes authenticated calls to the Open AI API (we’ll create this next). To authenticate this, we use auth from @strapi/helper-plugin which helps us make a secure request to the Strapi service we’ll create using the Admins token.

We then parse our result and use the special implementation of onChange() to pass the parsed response as the content that will get added to our database with onChange({ target: { name, value: parsedResult, type: attribute.type } }).

The other button calls clearGeneratedText() - this function clears all the text the AI generated in case we want to start over. At this point, our application should look something like this.

Image of custom field in content manager

Building a Custom Open AI Route in Strapi

To access Open AI, we need to provide an API KEY; having this key in the admin poses a security risk, and to combat that, we’ll create a custom route that, when called, makes an API call to Open AIs API and returns the generated text that we can then send to the admin.

So start, in our ./config/plugins.js file add a config object below enabled: true, your config file after should look like this:

// …
'ai-text-generation': {
      enabled: true,
      config: {
        apiToken: process.env.OPEN_AI_API_TOKEN,
      },
      resolve: './src/plugins/ai-text-generation'
    },

Enter fullscreen mode Exit fullscreen mode

Now, create a file called open-ai.js in ./src/plugins/ai-text-generation/server/services and paste the following code in it.

For the code to work, you will also have to install axios, for that navigate to your plugin root and type yarn add axios

'use strict';

const axios = require('axios');

module.exports = ({ strapi }) => ({

  async generateText(prompt) {
    try {
      const response = await axios(
        {
          url: 'https://api.openai.com/v1/completions',
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${strapi.plugin('ai-text-generation').config('apiToken')}`
          },
          data: JSON.stringify({
            'model': 'text-davinci-001',
            'prompt': `${prompt}`,
            'temperature': 0.4,
            'max_tokens': 64,
            'top_p': 1,
            'frequency_penalty': 0,
            'presence_penalty': 0
          })
        })

      return response.data;
    }
    catch (err) {
      console.log(err.response)
    }

  }

});

Enter fullscreen mode Exit fullscreen mode

Here we define a function generateText() that takes a prompt as an argument. We then use prompt as a parameter in our request to Open AI.

Next up, in the same services folder, paste the following code in your index.js file.

'use strict';

const openAi = require('./open-ai');

module.exports = {
  openAi,
};

Enter fullscreen mode Exit fullscreen mode

This code registers a service in Strapi that we call from a controller, before we do that. We need to create a controller. In ./src/plugins/ai-text-generation/server/controllers, create a file called ai-controller.js and paste the following code in it:

'use strict';

module.exports = ({ strapi }) => ({
  async generate(ctx) {
    ctx.body = await strapi
      .plugin('ai-text-generation')
      .service('openAi')
      .generateText(ctx.request.body.prompt);
  },
});

Enter fullscreen mode Exit fullscreen mode

In this file, we call the generateText() function that we defined in the service we just made and pass ctx.request.body.prompt (from our POST request).

In the same folder, we need to paste the following code in our index.js file:

'use strict';

const aiController = require('./ai-controller');

module.exports = {
  aiController,
};

Enter fullscreen mode Exit fullscreen mode

Lastly, we must create a route to access the controller we just created. In ./src/plugins/ai-text-generation/server/routes we need to paste the following code:

module.exports = [
  {
    method: 'POST',
    path: '/generate-text',
    handler: 'aiController.generate',
    config: {
      policies: [],
    },
  },
];

Enter fullscreen mode Exit fullscreen mode

In this file, we define how our endpoint should behave. With this code, we create a POST method on the /generate-text URI and pass the aiController controller we created to define its behavior.

In your preferred API client, you can now make a request to localhost:1337/ai-text-generation/generate-text and you should get AI-generated text as a response. Be sure to add your prompt as a parameter and pass your admin JWT for authentication.

We should also get a response when we enter a prompt and click “generate” in our Custom Field.

Gif of working Custom Field

Publishing to npm

Maxime from the Strapi team wrote an amazing article on how to publish your plugin to npm, and if you have any trouble you can refer to my package.json file for how to structure yours.

You can also take your Custom field to the next level and submit it to the official Strapi marketplace.

Hopefully, you have a better idea of how to build Custom fields now, and even better, you have a custom field that generates content for you with Open AI.

Let us know if you build something, the Strapi community will be excited to try it out. Take care.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .