Building a Strapi Custom Field for Text Generation with Open AI
Tags: strapi, customfields, openai, headlesscms
Published: 2023-02-07 10:53:07

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](https://strapi.io/features/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](https://docs.strapi.io/user-docs/latest/content-types-builder/configuring-fields-content-type.html#rich-text). 

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](https://openai.com/). 


## 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. 

- A [tweet](https://twitter.com/malgamves/status/1613534099133571073) explainer
- The [GitHub](https://github.com/malgamves/strapi-open-ai-text-generation) repo 
- The [npm](https://www.npmjs.com/package/ai-text-generation) package


## Creating a Custom Field

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

```bash
Npx create-strapi-app@latest plugin-dev —quickstart
```

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](https://docs.strapi.io/developer-docs/latest/development/plugins-development.html). 

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

```bash
npm run strapi generate
```

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. 

```javascript

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


    },
//...

    }

```

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: 

```bash
Npm run develop –watch-admin
```

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.

```javascript
'use strict';

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

```

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:

```javascript
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);
  },
};

```

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: 

```javascript
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>
  )
}
```

 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](https://d2zv2ciw0ln4h1.cloudfront.net/uploads/Screenshot_2023_02_07_at_11_47_26_3b97a525a4.png)


## 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: 

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

```

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`

```javascript
'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)
    }

  }

});

```

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.

```javascript
'use strict';

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

module.exports = {
  openAi,
};

```

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: 

```javascript 
'use strict';

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

```

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: 

```javascript
'use strict';

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

module.exports = {
  aiController,
};

```

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:

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

```

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](https://i.imgur.com/qwYFFNE.gif)


## Publishing to npm

Maxime from the Strapi team wrote an [amazing article](https://strapi.io/blog/how-to-create-a-strapi-v4-plugin-publish-on-npm-6-6) on how to publish your plugin to npm, and if you have any trouble you can refer to [my package.json](https://github.com/malgamves/strapi-open-ai-text-generation/blob/main/package.json) file for how to structure yours.

You can also take your Custom field to the next level and [submit it](https://market.strapi.io/submit-plugin) 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.