AI recipe generator web app all made in Python using the reflex framework and Gemini API!
git clone https://github.com/BustosAndrew/lahacks.git
cd lahacks
pip install -r requirements.txt
reflex run
Lines 1 to 60 in 5276e42
import google.generativeai as genai | |
import os | |
genai.configure(api_key=os.environ.get("GEMINI_API_KEY")) | |
# Set up the model | |
generation_config = { | |
"temperature": 0.9, | |
"top_p": 0.95, | |
"top_k": 32, | |
"max_output_tokens": 1024, | |
} | |
safety_settings = [ | |
{ | |
"category": "HARM_CATEGORY_HARASSMENT", | |
"threshold": "BLOCK_MEDIUM_AND_ABOVE" | |
}, | |
{ | |
"category": "HARM_CATEGORY_HATE_SPEECH", | |
"threshold": "BLOCK_MEDIUM_AND_ABOVE" | |
}, | |
{ | |
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", | |
"threshold": "BLOCK_MEDIUM_AND_ABOVE" | |
}, | |
{ | |
"category": "HARM_CATEGORY_DANGEROUS_CONTENT", | |
"threshold": "BLOCK_MEDIUM_AND_ABOVE" | |
}, | |
] | |
model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest", | |
generation_config=generation_config, | |
safety_settings=safety_settings) | |
def generate_recipe(req: dict): | |
print(req["cookware"]) | |
prompt_parts = [ | |
"Provide a healthier recipe consistent with the provided ingredients, along with the steps to make them. Adjust the entire recipe depending on the provided cookware. Provide nutritional info where possible. Add a name for the recipe and categorize the details as either healthy or unhealthy. Make sure to flag anything as unhealthy from the ingredients.", | |
"Ingredients: " + req["ingredients"], | |
"Use only these ingredients (True/False): " + | |
str(req["onlyIngredients"]), | |
"Cookware: " + req["cookware"], | |
] | |
response = model.generate_content(prompt_parts, stream=True) | |
for chunk in response: | |
yield chunk | |
def generate_prompt(req: str): | |
prompt_parts = [ | |
"Generate only one prompt of an image that resembles very closely to the end result of the provided recipe. Be as specific and detailed as possible according to the recipe guideline. Ensure that the prompt also adheres closely to the given amount per ingredient. Only give the plain text of the prompt idea without any formatting or headers.", | |
"Recipe details: " + req, | |
] | |
response = model.generate_content(prompt_parts) | |
return response.text |
lahacks/lahacks/states/form_state.py
Lines 1 to 73 in 5276e42
import reflex as rx | |
from lahacks.states.field_state import FieldState | |
from lahacks.api.gemini import generate_recipe, generate_prompt | |
from lahacks.pages.text2img import text2img | |
class DynamicFormState(rx.State): | |
form_data: dict = {} | |
form_fields: list[list[str]] = [] | |
ai_response: str = "" | |
submitted: bool = False | |
cantSubmit: bool = True | |
buttonText: str = "Submit" | |
imageLink: str = "" | |
onlyIngredients: bool = False | |
def set_only_ingredients(self, value: bool): | |
self.onlyIngredients = value | |
def add_field(self, ingredient: str, quantity: int, unit: str): | |
if not ingredient: | |
return | |
self.form_fields.append([ingredient, str(quantity), unit]) | |
self.cantSubmit = False | |
def handle_submit(self, form_data: dict): | |
self.submitted = True | |
self.cantSubmit = True | |
self.buttonText = "Generating..." | |
yield | |
if not self.form_fields: | |
self.submitted = False | |
self.cantSubmit = False | |
self.buttonText = "Submit" | |
return | |
self.ai_response = "" | |
self.form_data = form_data | |
chunks = generate_recipe({ | |
"ingredients": ", ".join([ | |
f"{field[1]}{field[2]} {field[0]}" | |
for field in self.form_fields | |
]), | |
"onlyIngredients": self.onlyIngredients, | |
"cookware": form_data.get("cookware", "Decide based on the recipe."), | |
}) | |
for chunk in chunks: | |
self.ai_response += chunk.text | |
yield | |
if self.ai_response: | |
self.form_fields = [] | |
self.submitted = False | |
self.buttonText = "Submit" | |
yield | |
recipe_prompt = generate_prompt(self.ai_response) | |
self.imageLink = text2img(recipe_prompt) | |
def handle_reset(self): | |
FieldState.ingredient = "" | |
FieldState.quantity = "" | |
FieldState.unit = "" | |
FieldState.cookware = "" | |
yield | |
self.ai_response = "" | |
self.form_data = {} | |
self.form_fields = [] | |
self.imageLink = "" | |
self.cantSubmit = True | |
self.onlyIngredients = False | |
yield |
Lines 1 to 109 in 5276e42
import reflex as rx | |
from lahacks.pages.components.recipe_image import output | |
from lahacks.states.form_state import DynamicFormState | |
from lahacks.states.field_state import FieldState | |
from lahacks.styles.styles import button_style | |
def dynamic_form(): | |
return rx.vstack( | |
rx.form( | |
rx.vstack( | |
rx.foreach( | |
DynamicFormState.form_fields, | |
lambda field: rx.box(rx.vstack( | |
rx.vstack(rx.heading("Ingredient", size="4"), | |
rx.text(field[0], style={"overflow-wrap": "break-word", "word-wrap": "break-word"})), | |
rx.heading("Amount", size="4"), | |
rx.text(field[1] + " " + field[2]), | |
), border_width=1, border_color="gray-300", border_radius=10, padding=10, width="100%"), | |
), | |
rx.hstack( | |
rx.input( | |
placeholder="Ingredient name", | |
name="ingredient", | |
value=FieldState.ingredient, | |
on_change=FieldState.set_ingredient | |
), | |
rx.input( | |
placeholder="Amount", | |
name="amount", | |
on_change=FieldState.set_quantity, | |
value=FieldState.quantity | |
), | |
rx.select(["no unit", "grams", "oz", "fl oz", "gallon(s)", "piece(s)", "slice(s)", "can(s)", "jar(s)", "bottle(s)", "jug(s)", "bag(s)"], name="unit", placeholder="Units (optional)", | |
on_change=FieldState.set_unit, value=FieldState.unit), | |
rx.button("+", on_click=DynamicFormState.add_field( | |
FieldState.ingredient, | |
FieldState.quantity, | |
FieldState.unit, | |
), type="button", style=button_style), | |
rx.button("Clear", on_click=FieldState.reset_vals, | |
type="button", style=button_style), | |
), | |
rx.checkbox("Use only these ingredients?", size="3", | |
on_change=DynamicFormState.set_only_ingredients, checked=DynamicFormState.onlyIngredients, name="onlyIngredients"), | |
rx.input.root(rx.input( | |
placeholder="Enter your cookware (optional)", | |
name="cookware", | |
on_change=FieldState.set_cookware, | |
value=FieldState.cookware, | |
), width="100%"), | |
rx.text("Press the + button to add your ingredient."), | |
rx.spacer(), | |
rx.hstack( | |
rx.button(DynamicFormState.buttonText, type="submit", | |
disabled=DynamicFormState.cantSubmit, style=button_style), | |
rx.button( | |
"Reset", on_click=DynamicFormState.handle_reset, type="button", style=button_style), | |
rx.cond( | |
DynamicFormState.buttonText == "Generating...", | |
rx.html('''<dotlottie-player src="https://lottie.host/d395e1a4-28dc-4e60-bffd-8a8ed8844318/qvoNVQ79Bc.json" background="transparent" speed="1" style="width: 50px; height: 40px;" loop autoplay></dotlottie-player>'''), | |
), | |
align="center", | |
), | |
rx.cond( | |
DynamicFormState.ai_response != "", | |
rx.box(rx.text("View your ", size="4", as_="span"), rx.link( | |
"generated recipe!", | |
href="/output/", | |
underline="always", | |
size="4", | |
as_="span" | |
)), | |
), | |
height="100%", | |
), | |
on_mount=DynamicFormState.handle_reset, | |
on_submit=DynamicFormState.handle_submit, | |
reset_on_submit=True, | |
height="100%", | |
), | |
height="100%", | |
) | |
def index() -> rx.Component: | |
return rx.center( | |
rx.script(src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs", | |
custom_attrs={"type": "module"}), | |
rx.vstack( | |
rx.heading("Chef.ai", marginX="auto", paddingY=10), | |
rx.box( | |
dynamic_form(), | |
border_width=1, | |
border_color="gray-300", | |
border_radius=10, | |
height="80%", | |
padding=20, | |
), | |
height="100%", | |
), | |
height="100vh", | |
paddingX=10, | |
) | |
app = rx.App() | |
app.add_page(index) | |
app.add_page(output) |