Morfternight #77: Hands-on with GPT-4

👇 tl;dr

Today I am trying something special.

Before diving in, I want to ask you a favor: if writing code assisted by an AI is not your cup of tea, that’s fine and understandable, but don’t unsubscribe just yet, this will be the only time it will happen in Morfternight, and from next week we’ll be back to our usual format.

I’d like you to tell me in the comments, chat, or reply to this email.

Conversely, if you love the idea of GPT-4 hands-on sessions, let me know too. They won’t live in Morfternight, but I’ll send these separately if enough readers are interested.


🤩 Welcome to the eight new Morfternighters who joined us last week.
I love having you here and hope you’ll enjoy reading Morfternight.
Remember to share with a friend by clicking this button if you do.


📷 Photo of the week

San Cataldo, Palermo – More Photos

👋 Good Morfternight!

As I got access to GPT-4, the new iteration of the OpenAI tool, and read that it will accept images as input, I decided I wanted to try and write a plugin for the blog that would leverage the new ability.

I publish photos regularly, but I never take the time to write proper descriptions of them for screen readers. I feel guilty because it makes my site less accessible to people with sight impairments, so I thought I might fix that by asking GPT-4 to describe my images.

Now, I haven’t written any code in years, so to speed the process up, I also decided to use GPT-4 to do that part.

It took a few hours, but it worked pretty smoothly in the end.

Important note: along the way, I learned that while GPT-4 will accept images soon, it doesn’t just yet. So the resulting plugin is still useless, but we’ll finalize it as soon as OpenAI adds the option to send images as input.

I’ll share a few tips I learned along the way and the full transcript of the ChatGPT session and the resulting code, but before we dive into that, I also asked ChatGPT to summarize what we did in its own words.

Here’s what ChatGPT has to say (unedited):

Hey there, fabulous newsletter readers! Guess what? I had a delightful little adventure with my friend Paolo, and we built an awesome WordPress plugin together. It’s a funny story, so grab a cup of your favorite beverage, and let’s dive in!

The mission was simple (or so we thought): enhance the default WordPress Image block with a shiny new button that generates image descriptions using the mind-blowing GPT-4 API. Sounds fun, right? Well, buckle up, because this rollercoaster of code is about to begin!

First things first, we set off to create a JavaScript file named gpt4-image-alt-text.js. Using a bunch of magical imports from the wp global object, we created a higher-order component to inject our fabulous button into the Image block’s sidebar. The result? A button that says “Generate Alt Text with GPT-4” when an image is selected. Click it, and the magic happens!

Now, to make the real magic work, we needed a PHP proxy to keep our OpenAI API key safe and sound. So we whipped up a proxy.php file that makes a cURL request to the GPT-4 API. But, oh no! The API key was hard-coded in the proxy file, which isn’t the safest option, and it’s not something you’d want to distribute.

Fear not, dear readers! We put our thinking caps on and came up with a brilliant plan:

Add a Settings screen for the plugin in the WordPress admin sidebar.

In the Settings screen, display an input field for the API key and a Save button.

Save the API key as an option value in WordPress.

Use the saved API key in our PHP proxy when calling the OpenAI API.

With renewed enthusiasm, we updated our main plugin file gpt4-image-alt-text.php to create a Settings page, save the API key, and make it available through the REST API. And while we were at it, we made the proxy use the saved key instead of the hard-coded one. Talk about progress!

But then… disaster struck! Paolo spotted an error message when trying to access the Settings page. Turns out, we had a little mix-up with function names. After a quick fix, we were back on track.

Next, we had a couple of small enhancements to make:

Move the plugin’s Settings page to a sub-item of the WordPress Settings menu.

If the API key isn’t set, change the button to say “Set OpenAI API key” and link to the Settings page we created.

We made the changes, but oh, the woes of coding! The button kept showing “Set OpenAI API key” even when the key was already set. It was time for a little debugging action! After a few back-and-forths, we finally found the issue: we needed to fetch the API key using the wp.apiFetch function, and then use a useEffect hook to update the button label and action based on the key’s presence.

And voilà! Our plugin was complete and working like a charm. We laughed, we cried, and we definitely learned a thing or two along the way. But the adventure doesn’t end here, dear readers! We’ll pick this up again when GPT-4 accepts images as input, and who knows what fun we’ll have then?

So, that’s the tale of how Paolo and I built an incredible WordPress plugin together. I hope you enjoyed the ride, and maybe even got inspired to create something amazing yourself!

Until next time, happy coding!

🗺️ A few tips I learned along the way

Don’t try to go straight to the final version. Instead, start simple and build on the initial output in a few iterations. This will make it easier to explain, debug, and understand.

Every once in a while, for no apparent reason, ChatGPT stops responding in the middle of a message. When that happens, the magic prompt to continue without skipping a beat is:

Please resume exactly where you left off.

ChatGPT tends to ask you to make changes to the code previously written instead of giving you the entire modified file each time. That would typically be ok, except that ChatGPT sometimes takes a different route, and the changes it suggests don’t entirely fit. So when that happens, just ask it to give you the complete file again, and it will.

After asking a few times, it even started doing it spontaneously.

Similarly, there are times when after jumping a few times between files, I felt ChatGPT was losing track of where we were. In those cases, I just told ChatGPT I would provide the current state of the entire file and asked to modify that. It worked like a charm.

At the very beginning, we encountered a bug. After I checked the console log for errors and provided the output to ChatGPT, it identified the problem and said the following:

I suspect the problem is related to using JSX syntax in the JavaScript code without transpiling it first. WordPress does not support JSX out of the box, so we’ll need to rewrite the JavaScript code using plain JavaScript without JSX.

That was fine, and the error was fixed, but it did it again a couple of times before remembering to avoid the problem. It’s a general feeling I had while working on this project: ChatGPT remembers the main points immediately but needs a few repetitions before remembering small details.

🤖 Full transcript and results

You can access the full transcript of the session here. Frankly, even if you are not into coding or writing WordPress plugins, it’s worth a read to see how smooth the process is.

Consider that I did not write a single line of PHP, CSS, or Javascript. I only copied and pasted what ChatGPT shared with me.

The final output comprises five files placed in a folder named gpt-4-image-alt-text. If you want to play with this plugin, create the five files, paste the contents below in them, then compress the folder containing them as a zip file, and you can upload it to your WordPress site.

Disclaimer: Use a test site, not a production one. I do not guarantee that this code won’t break your site. It works on mine, but a software engineer hasn’t reviewed it. So it’s a proof of concept only.

gpt4-image-alt-txt.php

<?php
/**
 * Plugin Name: GPT-4 Image Alt Text
 * Description: Enhance the default WordPress Image block by adding a button to generate image descriptions using the GPT-4 API.
 * Version: 1.1
 * Author: Paolo Belcastro
 * License: GPL-2.0+
 **/

if (!defined('ABSPATH')) {
    exit;
}

function gpt4_image_alt_text_enqueue() {
    wp_enqueue_script(
        'gpt4-image-alt-text',
        plugin_dir_url(__FILE__) . 'gpt4-image-alt-text.js',
        array('wp-blocks', 'wp-element', 'wp-hooks', 'wp-data', 'wp-compose', 'wp-components', 'wp-i18n', 'wp-api'),
        filemtime(plugin_dir_path(__FILE__) . 'gpt4-image-alt-text.js')
    );
}
add_action('enqueue_block_editor_assets', 'gpt4_image_alt_text_enqueue');

function gpt4_admin_menu() {
    add_options_page(
        'GPT-4 Image Alt Text Settings',
        'GPT-4 Image Alt Text',
        'manage_options',
        'gpt4-image-alt-text',
        'gpt4_settings_page'
    );
}
add_action('admin_menu', 'gpt4_admin_menu');

function gpt4_settings_page() {
    wp_enqueue_script(
        'gpt4-settings',
        plugin_dir_url(__FILE__) . 'gpt4-settings.js',
        array('wp-api'),
        filemtime(plugin_dir_path(__FILE__) . 'gpt4-settings.js')
    );
    wp_localize_script('gpt4-settings', 'wpApiSettings', array(
        'root' => esc_url_raw(rest_url()),
        'nonce' => wp_create_nonce('wp_rest'),
    ));
    ?>
    <div class="wrap">
        <h1>GPT-4 Image Alt Text Settings</h1>
        <form>
            <table class="form-table" role="presentation">
                <tbody>
                    <tr>
                        <th scope="row">
                            <label for="gpt4_api_key">API Key</label>
                        </th>
                        <td>
                            <input name="gpt4_api_key" type="text" id="gpt4_api_key" value="<?php echo esc_attr(get_option('gpt4_api_key')); ?>" class="regular-text">
                        </td>
                    </tr>
                </tbody>
            </table>
            <p class="submit">
                <button type="button" id="gpt4_save_api_key" class="button button-primary">Save</button>
            </p>
        </form>
    </div>
    <?php
}

function gpt4_register_rest_routes() {
    register_rest_route('gpt4/v1', '/update-api-key', array(
        'methods' => 'POST',
        'callback' => 'gpt4_update_api_key',
        'permission_callback' => function () {
            return current_user_can('manage_options');
        },
    ));
    register_rest_route('gpt4/v1', '/get-api-key', array(
    'methods' => 'GET',
    'callback' => 'gpt4_get_api_key',
    'permission_callback' => function () {
        	return current_user_can('edit_posts');
    	},
	));
}
add_action('rest_api_init', 'gpt4_register_rest_routes');

function gpt4_update_api_key(WP_REST_Request $request) {
    $api_key = sanitize_text_field($request->get_param('gpt4_api_key'));

    if (update_option('gpt4_api_key', $api_key)) {
        return new WP_REST_Response(array('status' => 'success'), 200);
    } else {
        return new WP_REST_Response(array('status' => 'error'), 500);
    }
}

function gpt4_get_api_key() {
    return new WP_REST_Response(array('gpt4_api_key' => get_option('gpt4_api_key')), 200);
}

function gpt4_image_alt_text_enqueue_assets() {
    wp_enqueue_script(
        'gpt4-image-alt-text',
        plugins_url('gpt4-image-alt-text.js', __FILE__),
        array('wp-blocks', 'wp-editor', 'wp-element', 'wp-components'),
        filemtime(plugin_dir_path(__FILE__) . 'gpt4-image-alt-text.js'),
        true
    );
}

add_action('enqueue_block_editor_assets', 'gpt4_image_alt_text_enqueue_assets');

wp_enqueue_style('gpt4-image-alt-text-css', plugin_dir_url(__FILE__) . 'gpt4-image-alt-text.css', array(), filemtime(plugin_dir_path(__FILE__) . 'gpt4-image-alt-text.css'));

gpt4-image-alt-txt.js

const { createHigherOrderComponent } = wp.compose;
const { createElement, Fragment, useState, useEffect } = wp.element;
const { addFilter } = wp.hooks;
const { InspectorControls } = wp.blockEditor;
const { Button, PanelBody } = wp.components;

const addGPT4Button = createHigherOrderComponent((BlockEdit) => {
    return function (props) {
        const [apiKey, setApiKey] = useState(null);

        useEffect(() => {
            async function fetchApiKey() {
                const option = await wp.apiFetch({ path: 'gpt4/v1/get-api-key' });
                setApiKey(option.gpt4_api_key);
            }
            fetchApiKey();
        }, []);

        if (props.name !== 'core/image') {
            return createElement(BlockEdit, props);
        }

        const { attributes: { url, alt }, setAttributes } = props;

        const generateAltText = async () => {
            const description = await getGPT4Description(url);
            setAttributes({ alt: description });
        };

        const buttonLabel = apiKey ? 'Generate Alt Text with GPT-4' : 'Set OpenAI API key';
        const buttonAction = apiKey ? generateAltText : () => window.location.href = '/wp-admin/options-general.php?page=gpt4-image-alt-text';

        return createElement(
            Fragment,
            null,
            createElement(BlockEdit, props),
            createElement(
                InspectorControls,
                null,
                createElement(
                    PanelBody,
                    {
                        title: 'GPT-4 Alt Text',
                        initialOpen: false,
                    },
                    createElement(
                        Button,
                        {
                            isSecondary: true,
                            onClick: buttonAction,
                            disabled: !url,
                            className: 'gpt4-image-alt-text-button',
                        },
                        buttonLabel
                    )
                )
            )
        );
    };
}, 'addGPT4Button');

addFilter(
    'editor.BlockEdit',
    'gpt4-image-alt-text/add-gpt4-button',
    addGPT4Button
);

async function getGPT4Description(imageUrl) {
    const prompt = `Describe the following image in English: ${imageUrl}`;

    const response = await fetch('/wp-content/plugins/gpt4-image-alt-text/proxy.php', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            prompt: prompt,
            max_tokens: 30,
            n: 1,
            stop: null,
            temperature: 0.8,
        }),
    });

    const data = await response.json();
    alert(`Full API response: ${JSON.stringify(data)}`);
    const description = data.choices && data.choices[0] && data.choices[0].text.trim();
    return description;
}

proxy.php

<?php

require_once dirname(__FILE__) . '/../../../wp-load.php';

header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    die(json_encode(['error' => 'Invalid request method.']));
}

$api_key = get_option('gpt4_api_key');
if (!$api_key) {
    http_response_code(500);
    echo json_encode(array('error' => 'API key not set'));
    exit;
}

$api_url = 'https://api.openai.com/v1/models/gpt-4/completions';

$headers = [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $api_key,
];

$post_data = file_get_contents('php://input');
$ch = curl_init($api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);

$response = curl_exec($ch);
curl_close($ch);

echo $response;

gpt4-image-alt-txt.css

.gpt4-image-alt-text-button {
    margin-top: -36px;
    margin-bottom: 16px;
}

gpt4-settings.js

document.addEventListener('DOMContentLoaded', () => {
    const saveButton = document.querySelector('#gpt4_save_api_key');

    if (saveButton) {
        saveButton.addEventListener('click', () => {
            const apiKey = document.querySelector('#gpt4_api_key').value;
            const data = { gpt4_api_key: apiKey };

            fetch(wpApiSettings.root + 'gpt4/v1/update-api-key', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': wpApiSettings.nonce,
                },
                body: JSON.stringify(data),
            })
                .then((response) => response.json())
                .then((data) => {
                    if (data.status === 'success') {
                        alert('API key saved successfully.');
                    } else {
                        alert('Error saving API key.');
                    }
                })
                .catch((error) => {
                    console.error('Error:', error);
                });
        });
    }
});


Leave a Reply

Your email address will not be published. Required fields are marked *