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
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
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'
},
//...
}
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
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',
});
};
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);
},
};
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 usetype: "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>
)
}
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.
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'
},
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)
}
}
});
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,
};
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);
},
});
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,
};
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: [],
},
},
];
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.
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.