Building The Recipes Only

Creating a AI powered Recipe site to generate recipes without the filler

ยท

8 min read

In the spirit of 'building in public' I logged the first few hours and days of building an OpenAI-powered recipe generation site: therecipesonly.com

Technology I'm using for this build:

Nextjs | TailwindCSS | Firebase Firestore | Open AI API

2:00

Setup Nextjs and Created Figma Outline

For the design of the site I want to keep things incredibly simple. The goal of the site is to deliver The Recipes Only so I want folks to be able to get straight to what they want. I borrowed heavily from the notion.so site color palette. I've found that having a background that's just off-white is a design technique which seems to work well.

3:15

Roughed out design of site in Nextjs

I have the two pages I need with styling using tailwind: The search page and the recipe page. However, I haven't set up routing yet. Right now I'm hardcoding all the recipe information just to get a sense of what the page will look like before I build the backend.

Next up -- routing and data driven page generation

*Update- I got the route to the hardcoded recipe page working and I can't style pages blind to save my life:

4:15

Ok that's better:

I'm trying to move as fast as possible, so some things which could be re-usable components aren't broken out yet. The header and searchbar specifically...

Next up is the search bar. In its final form, the search will search through the existing static pages for a match, if there is none, it will generate a new page using the OpenAI API. Right now, I'd just like to get the static page search system working.

4:24

It seems like this tutorial has exactly what I'm looking for: https://medium.com/@matswainson/building-a-search-component-for-your-next-js-markdown-blog-9e75e0e7d210

4:30

Ok nevermind, that didn't work at all. The example code was buggy. I'm going to setup the Firebase DB and connection instead.

Why Firebase? I'd like to use a nosql database for this application because of the type of data. This is going to be somewhat unstructured information: varying number of steps for each recipe, and in the future, I'd like to add a voting or rating system for the amount of each ingredient in each recipe. I think this would get cumbersome in a relational database. When using documents, it's easy to nest objects in one another.

Using this tutorial: https://blog.jarrodwatts.com/the-ultimate-guide-to-firebase-with-nextjs

7:30

Breakthrough! While trying to figure out static page generation I looked back over a previous Nextjs project and found an instance of get static props. I set up the app like so and it seems to be connected to the database and fetching whatever I ask for:

bottom of index.js:

export const getStaticProps = async () => {
  const querySnapshot = await getDocs(collection(db, "recipes"));
  querySnapshot.forEach((doc) => {
    console.log(`${doc.id} => ${doc.data().ingredients}`);
  })
  return {
    props: { data: JSON.stringify(querySnapshot.docs) }
  }
}
import { initializeApp } from "firebase/app";
import { getFirestore } from 'firebase/firestore';

const clientCredentials = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = initializeApp(clientCredentials);
const db = getFirestore(app);


export default db;

8:38

I added type-ahead search using this excellent library: https://github.com/tomsouthall/turnstone. The documentation was very good and it took only a few minutes to add to the site and style correctly.

9:10

I had a minor issue when my whole site turned white. It seems like tailwind automatically matches the dark/light mode setting on your computer. When the sun set, my computer went into dark mode and apparently, so did the site, so all my text turned to white, while the background whose color I set explicitly remained white. So I was left with a blank-looking site!

This was a quick fix as Tailwind makes dark mode super easy: bg-page-bg dark:bg-gray-dark . I'm quite pleased with how it looks:

Next up I'll be working to automatically generate pages based on what is in the database...and after that the fun part, ~AI~

10:05

Ok! The dynamic page generation seems to work! I added this code to the recipe page template:

export async function getStaticProps(context: any) {
    try {
        const recipesRef = collection(db, "recipes");
        const q = query(recipesRef, where("name", "==", context.params.id));
        const querySnapshot = await getDocs(q);
        const temp = querySnapshot.docs.map(doc => doc.data())
        return { props: { recipe: temp[0]} }
    } catch (err) {
        console.log(err)
    }
}

This queries the Firestore for a recipe matching whatever was searched.

12:05

I successfully connected to the Open AI API! I did have to setup billing, and it seems like this will cost some money ๐Ÿค‘. The concept is that if the site doesn't have a recipe it will be generated, so ideally after the most common recipes are generated, costs will go down. Assuming that there are any users whatsoever ๐Ÿ˜….


The next evening:

The next step is to make the AI generated recipe page *work*. This involves a few main goals:

  1. If the searched-for recipe isn't available, direct the user to the generation page

  2. Query OpenAI for the recipe with a detailed query

  3. Format the result so it looks good on the page

  4. Save the result to the database so that it doesn't need to be generated again.

Here we go!

10:30

It seems like I'm halfway through those bullets. I am getting a response from OpenAI and putting it on the page, then saving it to Firebase. This happens only if a recipe that doesn't exist yet is searched for. However, what I'm getting back and parsing from the API is a hot mess, for instance, pizza:

Ironically the AI is returning the very thing I'm trying to avoid, superfluous text! A problem for tomorrow.


01/23

Turns out I was calling the API incorrectly, so as a result my responses were very off.

export const getRecipeApi = async (promptText: string) => {

  const client = axios.create({headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`,
  }})

  const params = {
    'model': 'text-davinci-003',
    'prompt': promptText,
    'max_tokens': 500,
    'temperature': 0.5,
    'top_p': 1,
    'frequency_penalty': 0,
    'presence_penalty': 0,
  }

  const response = await client.post(
    "https://api.openai.com/v1/completions", 
    params
  );

  return response.data.choices[0].text;
};

Once I put the params in their own object and created a client first, I got responses more along the lines of what I was getting on the OpenAI playground.

later that day...

Check this out! The recipe return from openAI is formatted correctly with arrays of ingredients and instructions and displaying correctly on the page:

                    <ul>
                    {recipe?.ingredients?.map((ingredient:string, i:number) => {
                        return <li key={i}>{ingredient}</li>
                        })}
                    </ul>
                    <h1 className='text-2xl py-4'>Directions</h1>
                    <ol>
                    {recipe?.instructions?.map((ingredient:string, i:number) => {
                        return <li key={i}>{ingredient}</li>
                        })}
                    </ol>f


I spent most of today fighting with React context, but I finally have a simple solution which carries the state of the searchbar through the app.

Finally, I need to fix what I think is the last bug before deployment. It seems whenever I search for a recipe that doesn't exist yet, it pulls up the right information, displays it on the page, then completely overwrites with a different, irrelevant recipe. My first test is to see what we are asking from the api:

And lo, it looks as though we query the api not once, not twice, but three times! that the final query is the only correct one. So now I'll go back to the new recipes component and try to stop those queries before they get to the API. My suspicion is they are generated before we have the input value from the searchbar.

A quick nullcheck on the search term was all I needed for the fix. If the search is blank, don't waste my ๐Ÿ’ธ on an API call please!

I really think I need a loading screen, but that will have to come later. Right now you just have to type a recipe and wait while absolutely nothing happens on the page for...15 seconds or so while the AI does its thing.

TIME TO DEPLOY! ๐Ÿš€

Because this is a nextjs site, I'll be using Vercel for hosting. I expect the biggest challenge with this deployment to be managing the environment variables/api keys for firebase and the OpenAI.

update: Ok build errors abound. I will have to pick this up tomorrow.

Nevemind, I couldn't help myself! Deployment of Vercel is a piece of cake! The only issue I had was with my getStatic paths function not working, but I fixed that like so:

export async function getStaticPaths() {
    // call the API to get all the recipes
    const recipesRef = collection(db, "recipes");
    const querySnapshot = await getDocs(recipesRef);
    const temp = querySnapshot.docs.map(doc => doc.data())
    console.log('get Static paths', temp[0].name)
    return {
        paths: [{params: {id: temp[0].name}}],
        fallback: true // false or 'blocking'
    };
}

This function sets the paths for all the statically generated pages for each recipe. It asks what recipes are in the database, then sets a path for each recipe name. For example [my domain]/recipe/blueberry%20muffins .

Ok Now tomorrow...and the next day's problem is making the recipes not awful. This seems like the real challenge and will probly involve fine-tuning the model among other things.


What's next

TODO:

- [ ] Create Loading Screen
- [ ] Refine regex so that it doesn't cut all-purpose flour and works when there are no bullets (hyphens)
- [ ] Test the ISR to see if the site is really being rebuilt and that new items are being saved to the database
- [ ] Create mobile optimized styling

If you made it to the end of my rantings here, you're probably hopelessly confused...and my mother, Hi mom! Thanks for reading! ๐Ÿ‘‹

ย