Find Your Next Meal Quickly: A Recipe Suggestion App

Athina Hadjichristodoulou
The Web Tub
Published in
9 min readSep 1, 2023
FreeImages.com/#5690778

Coming up with ideas for lunch or dinner can be a daily struggle, especially when you’ve exhausted your creativity with the ingredients sitting in your fridge. Imagine having a clever kitchen assistant that generates recipe suggestions based on what you’ve got. In this guide, we will craft an app that takes 1 to 8 ingredients as input and proposes a recipe using just those items. This way, cooking becomes a simpler and more enjoyable experience.

Technologies Used

Make sure that you create free accounts on both Monaca and Hugging Face before continuing. On Monaca Cloud, create a new project using React and the Framework7 React Single View template.

Screens

The application consists of four screens which guide the user in steps until the desired recipe is generated.

1. Home Screen

Introductory screen for the user to kickstart the process of finding their next meal.

2. Ingredients Screen

On this screen the user is prompted to input at least 1 ingredient they have at hand and at most 8. Only text is allowed as input (e.g. the input -2 tomatoes- is wrong, just input tomatoes), and the first ingredient must be given in order to be able to proceed.

3. Loading Screen

On this screen the user waits for the model that has been called through the Hugging Face API, to get the inputted ingredients and generate a recipe accordingly. It might take some seconds for the model to load and the recipe to be generated.

4. Recipe Screen

On this screen, the recipe including the ingredients and the instructions is finally presented to the user. The option to start again and find a new recipe is also given.

Coding the Application

Before starting the development process, go to your Settings page on Hugging Face, navigate to the Access Tokens tab and create a new Read Mode token. We will need this token to be able to make a POST request to the t5-recipe-generation model and obtain the answer.

After completing this step, we can start by filling in the routes.js file, under the js/ folder with the paths and components we will use in the app. We specify the four screens we discussed earlier.

import HomePage from '../pages/home.jsx';
import IngredientsPage from '../pages/ingredients.jsx';
import LoadingPage from '../pages/loading.jsx';
import RecipePage from '../pages/recipe.jsx';
import NotFoundPage from '../pages/404.jsx';

var routes = [
{
path: '/',
component: HomePage,
},
{
path: '/ingredients/',
component: IngredientsPage,
},
{
path: '/loading/',
component: LoadingPage
},
{
path: '/recipe/',
component: RecipePage
},
{
path: '(.*)',
component: NotFoundPage,
},
];

export default routes;

The Home Screen is a simple screen that contains a text, an image and a button without any complicated functionalities. When the button is clicked, the app navigates to the screen for inputting the ingredients.

import React from 'react';
import {
Page,
Block,
BlockTitle,
Button
} from 'framework7-react';

const HomePage = (props) => {

const { f7router } = props

return (
<Page name="home" className="bg-color-white">
<Block className="margin-horizontal mt-70">
<BlockTitle id="title" className="cherry-bomb text-align-center" large>Are you ready to find your next meal?</BlockTitle>
<Block className="text-align-center">
<img src="../assets/cooking.svg" width="330px"/>
<div className="center">
<Button large raised fill round color="orange" className="w-250"
onClick={() => f7router.navigate('/ingredients/')}>START HERE</Button>
</div>
</Block>
</Block>
</Page>
)
};
export default HomePage;

On the Ingredients Screen there is the usage of useState in order to keep track of the changes of the input the user provides. Every time a change is detected on one of the input fields (let’s say i-th), the function handleChange is called, which assigns the new input as the value of the i-th ingredient.

Each input field has a validation pattern of only letters and space and the first input is required. If the user tries to proceed without providing the first ingredient, an alert is shown and the app doesn’t navigate to the next page. If the input is correct, then the app navigates to the Loading Screen with the ingredients passed as props.

import React, { useState } from 'react';
import {
f7,
Page,
List,
ListInput,
BlockTitle,
Button,
Block
} from 'framework7-react';

const IngredientsPage = (props) => {

const {f7router} = props
const [ingredients, setIngredients] = useState({i1: "", i2: "", i3: "", i4: "", i5: "", i6: "", i7: "", i8: ""})

function handleChange(event){
const {name, value} = event.target
setIngredients({...ingredients, [name]: value})
}

function findMeRecipe(){
if (ingredients.i1 === "")
f7.dialog.alert("Make sure you provide at least the first ingredient!")
else
f7router.navigate('/loading/', { props: {ingredients} })
}

return (
<Page name="form" className="bg-color-white">

<BlockTitle>STEP 1</BlockTitle>
<Block className='margin-vertical'>
<BlockTitle className="subtitle">Add up to 8 ingredients:</BlockTitle>
<List noHairlinesMd>
<ListInput
name="i1"
label="Ingredient 1"
type="text"
placeholder="e.g.Tomato"
required
validate
pattern="[a-zA-Z ]+"
value={ingredients.i1}
onChange={handleChange}
></ListInput>

<ListInput
name="i2"
label="Ingredient 2"
type="text"
placeholder="e.g.Potato"
validate
pattern="[a-zA-Z ]*"
value = {ingredients.i2}
onChange={handleChange}
></ListInput>

<ListInput
name="i3"
label="Ingredient 3"
type="text"
placeholder="e.g.Garlic"
validate
pattern="[a-zA-Z ]*"
value = {ingredients.i3}
onChange={handleChange}
></ListInput>

<ListInput
name="i4"
label="Ingredient 4"
type="text"
placeholder="e.g.Minced meat"
validate
pattern="[a-zA-Z ]*"
value = {ingredients.i4}
onChange={handleChange}
></ListInput>

<ListInput
name="i5"
label="Ingredient 5"
type="text"
placeholder="e.g.Soy sauce"
validate
pattern="[a-zA-Z ]*"
value = {ingredients.i5}
onChange={handleChange}
></ListInput>

<ListInput
name="i6"
label="Ingredient 6"
type="text"
placeholder="e.g.Salt"
validate
pattern="[a-zA-Z ]*"
value = {ingredients.i6}
onChange={handleChange}
></ListInput>

<ListInput
name="i7"
label="Ingredient 7"
type="text"
placeholder="e.g.Butter"
validate
pattern="[a-zA-Z ]*"
value = {ingredients.i7}
onChange={handleChange}
></ListInput>

<ListInput
name="i8"
label="Ingredient 8"
type="text"
placeholder="e.g.Corn"
validate
pattern="[a-zA-Z ]*"
value = {ingredients.i8}
onChange={handleChange}
></ListInput>
</List>
</Block>
<Block className="center">
<Button large raised fill round color="orange" className="w-250" onClick={() => findMeRecipe()}>Find Me A Recipe!</Button>
</Block>

</Page>
)
};

export default IngredientsPage;

After the Ingredients Screen, the Loading Screen appears in which the model is actually called. After you have obtained your Access Token from Hugging Face, create a file under src/ named .secret/huggingface.secret.json and include your access token. This file should not be uploaded to any public platform for security reasons. If you are planning to upload your code on GitHub (or anywhere else) make sure to include this file in .gitignore.

At the first render of the LoadingPage component, the access token will be read from the json file and together with the ingredients that the component received as props (that are not empty), they will be passed as parameters to the query function. This is the function that will perform the POST request to the Hugging Face model t5-recipe-generation. There should be a small wait for the model to load and generate the response according to the input, so an image and a text animation are presented to the user using plain CSS. When the response is received, the app navigates to the final screen, passing as props the answer from the model.

import React, { useEffect } from "react";
import { BlockTitle, Block, Page } from "framework7-react";

const LoadingPage = (props) => {

const ingredients = props.ingredients
const { f7router } = props
const kwargs = {
"max_length": 512,
"min_length": 64,
"no_repeat_ngram_size": 3,
"do_sample": true,
"top_k": 60,
"top_p": 0.95,
"options": { "wait_for_model": true}
}

useEffect(async () => {
const api_key = await getApiKey()
if (api_key) {
const ingredients_array = Object.values(ingredients).filter((entry) => entry !== '').toString()
const recipe = await query({ "inputs": [ingredients_array], ...kwargs }, api_key)
if (recipe) {
console.log("Loading completed")
f7router.navigate('/recipe/', { props: { recipe } })
}
else
f7router.navigate('/error/')
} else {
f7router.navigate('/error/')
}
}, [])

async function getApiKey() {
let api_key;
try {
const response = await fetch('../.secret/huggingface.secret.json')
const data = await response.json()
api_key = data.API_KEY
} catch (e) {
const code = error.code
const message = error.message
console.error(code, message)
}
return api_key
}

async function query(data, api_key) {
const auth = "Bearer " + api_key
let result = null
try {
const response = await fetch(
"https://api-inference.huggingface.co/models/flax-community/t5-recipe-generation",
{
headers: { Authorization: auth },
method: "POST",
body: JSON.stringify(data),
}
);
result = await response.json();
} catch (e) {
const code = error.code
const message = error.message
console.error(code, message)
}
return result
}

return (
<Page name="loading" className="bg-color-white">
<BlockTitle>STEP 2</BlockTitle>
<Block className="center">
<div className="waviy padding-horizontal mt-70">
<div>
<span >F</span>
<span >i</span>
<span >n</span>
<span >d</span>
<span >i</span>
<span >n</span>
<span >g</span>
<span >&nbsp;a&nbsp;</span>
</div>
<div>
<span >r</span>
<span >e</span>
<span >c</span>
<span >i</span>
<span >p</span>
<span >e</span>
<span >.</span>
<span >.</span>
<span >.</span>
</div>
</div>
<img src="../assets/feast.png" width="330px" />
</Block>
</Page>
)
}

export default LoadingPage;

The final screen is the Recipe Screen where the ingredients needed and the instructions for cooking are presented nicely to the user. Since the response from the model (received as props from the previous component) is a plain string, we need to make some string manipulation in order to extract the information appropriately and construct the two lists. Besides that, we also present the user with a button that gives them the ability to start over the process and discover a new recipe from other ingredients.

import React, { useEffect, useState } from "react";
import { BlockTitle, Page, Block, Button, Card } from "framework7-react";
import Dom7 from "dom7";

const RecipePage = (props) => {

const recipe = props.recipe
const { f7router } = props
const $$ = Dom7
let [ingredientsList, setIngredientsList] = useState();
let [instructionsList, setInstructionsList] = useState();

useEffect(() => {
const text = recipe[0].generated_text
//Extract title, ingredients and instructions from string
const sections = extractSections(text)
//Set the title of the recipe
$$("#recipe-title").html(sections.title)
//Split the ingredients by numbers and keep them
const ingredients_arr = sections.ingredients.split(/\s(?=\d)/)
setIngredientsList(ingredients_arr.map((ingredient, index) => {
return (
<p key={index}>{(index+1) + ". " + ingredient}</p>
)
}));
//Split the instructions by '. '
const instructions_arr = sections.directions.split(/(?<=\. )/)
setInstructionsList(instructions_arr.map((instruction, index) => {
return (
<p key={index}>
{(index+1) + ". " + instruction.charAt(0).toUpperCase() + instruction.slice(1)}
</p>
)
}))
}, [])

function extractSections(text) {
const titleMatch = text.match(/title:\s*(.*?)(?=ingredients|$)/);
const ingredientsMatch = text.match(/ingredients:\s*(.*?)(?=directions|$)/);
const directionsMatch = text.match(/directions:\s*(.*?)(?=\s*$)/);

if (!titleMatch || !ingredientsMatch || !directionsMatch) {
throw new Error("One or more sections could not be found in the text");
}

const title = titleMatch[1].trim();
const ingredients = ingredientsMatch[1].trim();
const directions = directionsMatch[1].trim();

return { title, ingredients, directions };
}

return (
<Page name="recipe" className="bg-color-white">
<BlockTitle>STEP 3</BlockTitle>
<BlockTitle id="recipe-title" className="subtitle center margin-bottom"></BlockTitle>
<Block strong inset id="recipe-block">
<BlockTitle>&#x1F34D;&nbsp;Ingredients:</BlockTitle>
<Card className="p-10">
{ingredientsList}
</Card>
<BlockTitle className="ml-0">&#x1F374;&nbsp;Instructions:</BlockTitle>
<Card className="p-10">
{instructionsList}
</Card>
</Block>
<Block className="center">
<Button large raised fill round color="orange" className="w-250 margin-bottom" onClick={() => {
f7router.clearPreviousHistory()
f7router.navigate('/')
}}>Find another recipe</Button>
</Block>
</Page>
)
}

export default RecipePage;

With this, all the screens for our application have been developed. Now it’s time to test run our app.

Running the Application

The best way to test if our application is working correctly, before deploying it on any mobile device, is to run it as a web app. If you are working with Monaca Cloud, you can see and test the application from the preview panel. On the other hand, if you are working locally, you can use Monaca CLI to run and test your application.

Open a terminal and navigate to the root folder of the app. Run the following command and wait until the build finishes:

monaca preview

The application should be hosted on http://localhost:8080/, so navigate to this link and you should be able to see your application running.

After testing, you can use Monaca Cloud to remotely build your app as and Android, iOS or Windows native app. You can find informative building instructions for every platform here.

Conclusion

In this blog post we have showcased how easy it is to use a model that is hosted by Hugging Face and also develop a useful application using it that can save us the stress of having to figure out what to eat every day. There are thousands of models hosted in this platform that have many more possibilities and are worth checking out, so please don’t hesitate to do so!

You can find the source code for the application described in this post in this GitHub repo.

--

--