Saturday, 19 April 2025

Web Tutorial: The Easter Poem Generator (Part 2/2)

We actually already created functions for validating and submitting earlier, so let's fill them up now.

We start with the handleChecking() function. We first get name, value and checked from the target object of the e object. target will be the checkbox that is checked. So we have finalValue. If the checkbox is checked, then finalValue has the value value. If not, its value is an empty string.

src/app/page.js
const handleChecking = (e) => {
  const { name, value, checked } = e.target;
  var finalValue = (checked ? value : "");

};


Whatever the value of finalValue, it will be used in setFormData(). We use the spread operator to destructure formData, then add in a name-value pair using name and finalValue. If name already appears in formData, it will thus be overwritten by the value of finalValue.

src/app/page.js
const handleChecking = (e) => {
  const { name, value, checked } = e.target;
  var finalValue = (checked ? value : "");

  setFormData({
    ...formData,
    [name]: finalValue
  });

};


What we want to do is ensure that at least one checkbox is checked. So we'll test this. Declare tempFormData as a clone of formData by using stringify() and parse() methods. Modify tempFormData's property, pointed to by name, changing its value to finalValue.

src/app/page.js
const handleChecking = (e) => {
  const { name, value, checked } = e.target;
  var finalValue = (checked ? value : "");

  var tempFormData = JSON.stringify(formData);
  tempFormData = JSON.parse(tempFormData);
  tempFormData[name] = finalValue;


  setFormData({
    ...formData,
    [name]: finalValue
  });
};


Now we use an If block to check if all the string values are empty strings. If that's true, we reset the value of finalValue to value. This ensures that there is never  a situation where all the properties are empty strings.

src/app/page.js
const handleChecking = (e) => {
  const { name, value, checked } = e.target;
  var finalValue = (checked ? value : "");

  var tempFormData = JSON.stringify(formData);
  tempFormData = JSON.parse(tempFormData);
  tempFormData[name] = finalValue;

  if (tempFormData.symbol_bunny + tempFormData.symbol_chicks + tempFormData.symbol_cross + tempFormData.symbol_eggs + tempFormData.symbol_lillies === "") {
    finalValue = value;
  }


  setFormData({
    ...formData,
    [name]: finalValue
  });
};


Try it... you can select multiple options but you will find that you'll never be able to deselect all of the options.

That's it for validation! Now we want to handle submitting.

We first ensure that the form is not automatically submitted, by using the preventDefault() method. We want to implement our own submit logic. After that, we use setGeneration() to set a loading message.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

};


We'll add a div, styled using the CSS class poemContainer, with the generation object's poem property as content.

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
      <b>Include at least one of these elements</b>
      {
        Object.entries(formData).map(([key, value]) => (
        <label className={styles.label} key={key}>
          <input type="checkbox" name={key} value={key.replace("symbol_", "")} onChange={handleChecking} checked={ (value !== "") } />
           {key.replace("symbol_", "")}
           <Link href={ ("/" +key.replace("_", "/")) } target="_blank">►</Link>
          <br />
        </label>
      ))}    
      <button type="submit" className={styles.button}>Go!</button>
      <br style={{ clear:"both" }} />
    </form>
    <div className={styles.poemContainer} >{ generation.poem } </div>
  </div>
);


Here's poemContainer. Just a bit of alignment and layout. Nothing crazy.

src/app/page.module.css
.generationContainer form {
  width: 450px;
  padding: 10px;
  margin: 0 auto 0 auto;
  border-radius: 10px;
  border: 1px solid rgb(255, 100, 100);
}

.poemContainer {
  width: 450px;
  padding: 10px;
  margin: 0 auto 0 auto;
  text-align: center;
}


.label {
  display: inline-block;
  width: 45%;
  float: left;
}


Now try Clicking the SUBMIT button. Looks like the HTML is being treated like text content!


We will rectify this using dangerouslySetInnerHTML. This renders raw HTML in a React component.

src/app/page.js
<div className={styles.poemContainer} dangerouslySetInnerHTML={{ __html: generation.poem }} />


This time, the loading message is properly formatted.



Now that this is done, we're going to make a call to the backend using fetch(). We'll pair it with await because its very nature is async. The endpoint is poemgen and we will be creating it after this. The result of the call to the backend will be assigned to the object response.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

  const response = await fetch("/api/poemgen", {

  });

};


In fetch(), the second argument is an object. It contains the method of the call (in this case, it's a POST), the headers object (which in this context basically tells fetch() that we're ending data in JSON format, and the body property. The last one contains formData as a JSON-formatted string.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

  const response = await fetch("/api/poemgen", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(formData)

  });
};


We then declare result and use it to store the value of response as a JSON object. Since the method we're using, json(), is asynchronous, we have to use await.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

  const response = await fetch("/api/poemgen", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(formData)
  });
  const result = await response.json();

};


We then check if status is 200. If so, we run setGeneration() and set the value of the poem property to the message property of result. If not, we handle the error by logging it in the console.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

  const response = await fetch("/api/poemgen", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(formData)
  });
  const result = await response.json();

  if (response.status === 200) {
    setGeneration( { poem: result.message } );
  } else {
    console.log(result);
  }

};


Now, in the app directory, create the api subdirectory, and in it, the poemgen subdirectory. In that, create route.js. This is the NextJS App router method of creating backend API endpoints. In this file, we export an async function that responds to the POST method. There is a parameter, req, that represents the request.

src/app/api/poemgen/route.js
export async function POST(req) {

}


We declare formData and use await to obtain the JSON object req. Then we declare symbols as an empty string.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
}


This is followed by a series of If blocks that correspond to each property in formData. If the value is not an empty string, concatenate that value to symbols, with a comma and space. Remember, there will never be a case where all the strings are empty. Thus, there will always be at least one comma and space. We remove the one at the end using the slice() method.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

}


Next, we declare headers as an object, and populate it with standard properties such as Authorization and Content-Type. Since this is for ChatGPT, we'll need OpenAI-Oganization also. Authorization and OpenAI-Oganization will have values that can be accessed from your .env file.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
     "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
     "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
     "Content-Type": "application/json"    
  };  

}


The values will look like this in the .env file. They won't, of course, be "xxx".

.env
NEXT_PUBLIC_API_KEY=xxx
NEXT_PUBLIC_ORG=org-xxx


Now create an empty array, messages. Then create an object, obj. It should have the property role, which has a value of "user", and the property content. Its value should be a prompt we create from symbols. After that, push obj into messages.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
    "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

}


Create body as an object. In there, we specify the AI model and set the number of tokens to be used, to 2000. The messages property value will be the array messages.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
    "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
     "model": "gpt-3.5-turbo",
     "messages" : messages,
     "max_tokens" : 2000
  };

}


Here, we have a Try-catch block. In here, we use fetch() to grab data via a REST endpoint provided by OpenAI, sending header and body as the data. The response is assigned to the variable apiResponse.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
    "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
     "model": "gpt-3.5-turbo",
     "messages" : messages,
    "max_tokens" : 2000
  };

  try {
    const apiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
         method: "POST",
         headers: headers,
         body: JSON.stringify(body)
     });
  } catch (error) {

  }

}


We then check the ok property of apiResponse. If this is false, we return a Response object with an appropriate status. We return the same thing if there is an exception thrown.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
    "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
     "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
     "model": "gpt-3.5-turbo",
     "messages" : messages,
     "max_tokens" : 2000
  };

  try {
    const apiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
         method: "POST",
         headers: headers,
         body: JSON.stringify(body)
     });

    if (!apiResponse.ok) {
      return new Response(JSON.stringify({ message: "Error in poem generation" }), {
        status: 500,
        headers: { "Content-Type": "application/json" },
       });
     }

  } catch (error) {
     return new Response(JSON.stringify({ message: error.message }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });

  }
}


Here, we obtain data by converting apiResponse to JSON using the asynchronous method json(). And we'll define html_content as the generated content but with HTML breaks in place of line breaks.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
     "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
     "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
     "role": "user",
     "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
    "model": "gpt-3.5-turbo",
     "messages" : messages,
     "max_tokens" : 2000
  };

  try {
      const apiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
           method: "POST",
           headers: headers,
           body: JSON.stringify(body)
       });

       if (!apiResponse.ok) {
        return new Response(JSON.stringify({ message: "Error in poem generation" }), {
          status: 500,
          headers: { "Content-Type": "application/json" },
      });
    }

    const data = await apiResponse.json();
    var html_content =  data.choices[0].message.content.replaceAll("\n", "<br />");

  } catch (error) {
    return new Response(JSON.stringify({ message: error.message }), {
        status: 500,
        headers: { "Content-Type": "application/json" },
    });
  }
}


And once that is done, we will return a new Response with the status 200 and html_content as the message.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
     "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
     "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
     "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
     "role": "user",
     "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
     "model": "gpt-3.5-turbo",
    "messages" : messages,
    "max_tokens" : 2000
  };

  try {
    const apiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: headers,
        body: JSON.stringify(body)
    });

    if (!apiResponse.ok) {
       return new Response(JSON.stringify({ message: "Error in poem generation" }), {
          status: 500,
          headers: { "Content-Type": "application/json" },
      });
    }

    const data = await apiResponse.json();
    var html_content =  data.choices[0].message.content.replaceAll("\n", "<br />");

    return new Response(JSON.stringify({ message: html_content }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });

  } catch (error) {
     return new Response(JSON.stringify({ message: error.message }), {
       status: 500,
       headers: { "Content-Type": "application/json" },
     });
  }
}


Now try this! First, try it with one item checked.


Then try it with multiple items checked, and see the difference!


Enjoy your Easter!

This has been my virgin NextJS tutorial. We covered a fair amount of territory, and hopefully had fun in the process!

May your Easter eggs all be found,
His Teochewness will see you around!
T___T

No comments:

Post a Comment