From 2e0b9c97af457da5c6afda611d48e59047d4cdb8 Mon Sep 17 00:00:00 2001 From: JJ Date: Mon, 31 Mar 2025 21:08:41 +0100 Subject: Basic AI functionality --- .Constants.py.swp | Bin 0 -> 12288 bytes .gitignore | 1 + AIParams.py | 25 ++++++++++++++++ scripts/database/mongo.py | 11 +++++-- scripts/recipes/handle_recipes.py | 2 ++ scripts/scraping/ai_scraping.py | 19 +++++++++++++ scripts/scraping/scraper.py | 3 +- scripts/users/handle_users.py | 10 ++++++- server.py | 28 +++++++++++++++--- static/images/bars.svg | 2 +- static/scripts/index.js | 57 +++++++++++++++++++++++++++++++------ static/style/style.css | 8 ++++++ templates/base.html | 1 - templates/pages/about.html | 9 ++++++ templates/pages/account.html | 21 ++++++++++++-- templates/pages/home.html | 2 +- templates/pages/single-recipe.html | 14 +++++++-- 17 files changed, 186 insertions(+), 27 deletions(-) create mode 100644 .Constants.py.swp create mode 100644 AIParams.py create mode 100644 scripts/scraping/ai_scraping.py diff --git a/.Constants.py.swp b/.Constants.py.swp new file mode 100644 index 0000000..ffb1f96 Binary files /dev/null and b/.Constants.py.swp differ diff --git a/.gitignore b/.gitignore index e6a7493..7b214bb 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ htmlcov/ .tox/ docs/_build/ Constants.py +AIParams.py diff --git a/AIParams.py b/AIParams.py new file mode 100644 index 0000000..aab8ac3 --- /dev/null +++ b/AIParams.py @@ -0,0 +1,25 @@ +RECIPE_SCHEMA = { + 'title': str, + 'slug': str, + 'image': str, + 'url': str, + 'tags': list[str], + 'ingredients': list[str], + 'instructions': list[str] +} + +PROMPT = """ +Extract data from the following text in the schema format provided. + +SCHEMA: + +{schema} + +DATA: + +{markdown} +""" + +SYSTEM_INSTRUCTIONS="You are responsible for taking the inputted markdown text and generating a structure response from it. See the Schema provided." + + diff --git a/scripts/database/mongo.py b/scripts/database/mongo.py index 3e97b0b..eaa2d97 100644 --- a/scripts/database/mongo.py +++ b/scripts/database/mongo.py @@ -3,7 +3,12 @@ from pymongo.server_api import ServerApi import Constants import certifi -uri = Constants.MONGO_CONNECTION_STRING +uri = Constants.MONGO_CONNECTION_STRING if Constants.MODE == "production" else Constants.MONGO_CONNECTION_STRING_DEV +database_str = "index-cooking" if Constants.MODE == "production" else "recipedb" -client = MongoClient(uri, server_api=ServerApi('1'), tlsCAFile=certifi.where()) -mongo_database = client["index-cooking"] \ No newline at end of file +if Constants.MODE == "production": + client = MongoClient(uri, server_api=ServerApi('1'), tlsCAFile=certifi.where()) +else: + client = MongoClient(uri) + +mongo_database = client["index-cooking"] diff --git a/scripts/recipes/handle_recipes.py b/scripts/recipes/handle_recipes.py index 22ba0a4..73d19be 100644 --- a/scripts/recipes/handle_recipes.py +++ b/scripts/recipes/handle_recipes.py @@ -1,10 +1,12 @@ from bson.objectid import ObjectId +from flask import session from scripts.database.mongo import mongo_database from scripts.scraping.scraper import scrape mongo_collection = mongo_database.get_collection("recipes") def add_single_recipe(recipe): + recipe["user"] = session["username"] added_recipe = mongo_collection.insert_one(recipe) new_recipe = mongo_collection.find_one() return added_recipe diff --git a/scripts/scraping/ai_scraping.py b/scripts/scraping/ai_scraping.py new file mode 100644 index 0000000..b11a12b --- /dev/null +++ b/scripts/scraping/ai_scraping.py @@ -0,0 +1,19 @@ +from google import genai +import json +import requests +import Constants +import AIParams + +client = genai.Client(api_key="AIzaSyAdB7yo0qcnwHeC4T2rRaSXD588JRw94oQ") + +def run_ai_query(url): + req_url = f"https://r.jina.ai/{url}" + res = requests.get(req_url) + markdown_content = res.text + + prompt = AIParams.PROMPT.format(schema=AIParams.RECIPE_SCHEMA, markdown=markdown_content) + + ai_res = client.models.generate_content(model="gemini-2.0-flash", contents=prompt) + cleaned_text = ai_res.text.strip("```").strip("```json") + recipe_json = json.loads(cleaned_text) + return {"success": True, "data": recipe_json} diff --git a/scripts/scraping/scraper.py b/scripts/scraping/scraper.py index 8919d46..0e0b9c8 100644 --- a/scripts/scraping/scraper.py +++ b/scripts/scraping/scraper.py @@ -25,7 +25,7 @@ def extractInstructions(instructions): return returnedInstructions -def scrape(url, user_name): +def scrape(url): try: data = requests.get(url, headers= {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"}) html = BeautifulSoup(data.text, 'html.parser') @@ -43,7 +43,6 @@ def scrape(url, user_name): slug = parse.quote(i["name"]).lower() # The recipe - recipe["user"] = user_name recipe["slug"] = slug recipe["title"] = i["name"] recipe["image"] = i["image"][0] diff --git a/scripts/users/handle_users.py b/scripts/users/handle_users.py index 250c716..7a315f1 100644 --- a/scripts/users/handle_users.py +++ b/scripts/users/handle_users.py @@ -23,4 +23,12 @@ def delete_single_user(user): mongo_collection.delete_one({"name": user}) return {"success": True, "user": user} except: - return {"success": False, "user": user} \ No newline at end of file + return {"success": False, "user": user} + +def update_user(user, user_prefs): + print(user, user_prefs, "in user handler") + try: + res = mongo_collection.update_one({"name": user}, {"$set": user_prefs}) + return {"success": True, "message": "user preferences successfully updated"} + except Exception as e: + return {"success": False, "message": str(e)} diff --git a/server.py b/server.py index 7840ea3..66c2d43 100644 --- a/server.py +++ b/server.py @@ -3,9 +3,11 @@ from flask_bcrypt import Bcrypt import functools import re import Constants +from urllib.parse import urlparse from scripts.scraping.scraper import scrape +from scripts.scraping.ai_scraping import run_ai_query from scripts.recipes.handle_recipes import recipe_exists, add_single_recipe, get_all_recipes, get_single_recipe, get_facets, delete_single_recipe, search_recipes, delete_multiple_recipes -from scripts.users.handle_users import user_exists, add_user, delete_single_user +from scripts.users.handle_users import user_exists, add_user, delete_single_user, update_user app = Flask(__name__) bcrypt = Bcrypt(app) @@ -25,7 +27,6 @@ def login_required(func): @app.route("/home") @login_required def home_page(): - print(session["username"]) tags = get_facets(session["username"]) all_recipes = get_all_recipes(session["username"]) return render_template("/pages/home.html",recipes=all_recipes, facets=tags) @@ -79,15 +80,22 @@ def add_recipe(): if does_recipe_exist: return "Recipe already exists!", 422 else: - response = scrape(submitted_url, session["username"]) + response = scrape(submitted_url) if response["success"]: add_single_recipe(response["data"]) all_recipes = get_all_recipes(session["username"]) all_facets = get_facets(session["username"]) return render_template("/components/app.html", recipes=all_recipes, facets=all_facets) + + elif not response["success"]: + ai_res = run_ai_query(submitted_url) + add_single_recipe(ai_res["data"]) + all_recipes = get_all_recipes(session["username"]) + all_facets = get_facets(session["username"]) + return render_template("/components/app.html", recipes=all_recipes, facets=all_facets) else: - return f"Error processing recipe metadata, {response['error']} ", 422 + return "something went wrong", 400 @app.post("/recipes/search") @@ -168,6 +176,18 @@ def delete_user(): return Response(headers={"HX-Redirect": "/"}) else: return "Something went wrong deleting the user and recipes" + +@app.post("/update-account") +def update_account(): + user_prefs = request.form.to_dict() + response = update_user(session["username"], user_prefs) + + if response["success"]: + session["isAiSubscriber"] = True + return Response(headers={"HX-Redirect": "/account"}) + else: + return f"{response["message"]}", 422 + if __name__ == "__main__": app.run(host="0.0.0.0") diff --git a/static/images/bars.svg b/static/images/bars.svg index 64dd5a8..8ce0f06 100644 --- a/static/images/bars.svg +++ b/static/images/bars.svg @@ -1,4 +1,4 @@ - + { - modal.showModal(); -}); +if(clipboard) { + clipboard.addEventListener("click", () => { + + // Copy data to clipboard + const ingredientsContent = document.querySelector("#ingredients").textContent.trim(); + const contentArr = ingredientsContent.split("\n"); + const ingredientsString = contentArr.map((el) => { + if(el.trim() == "") return; + else return el.trim(); + }).join("\n") + navigator.clipboard.writeText(ingredientsString); + + // Show success message momentarily + clipboard.parentElement.insertAdjacentHTML("afterend", " ingredients copied!"); + + setTimeout(() => { + document.querySelector("#copied-message").remove(); + }, 2000) + }) +} + + +// Open and close modals on /account page +if(showModalBtns){ + showModalBtns.forEach((btn) => { + btn.addEventListener("click", () => { + + // Open dialog box + btn.nextElementSibling.showModal(); + + }) + }) +} + +if(closeModalBtns){ + closeModalBtns.forEach((btn) => { + btn.addEventListener("click", () => { + + // Open dialog box + btn.parentNode.close(); + + }) + }) +} -closeModalBtn.addEventListener("click", () => { - modal.close(); -}); diff --git a/static/style/style.css b/static/style/style.css index 748e8dd..9f92d77 100644 --- a/static/style/style.css +++ b/static/style/style.css @@ -16,6 +16,14 @@ color: var(--success); } +.cursor { + cursor: pointer; +} + +.inline-block { + display: inline-block; +} + body, html { min-height: 100%; diff --git a/templates/base.html b/templates/base.html index 523e7ac..3181a2f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,7 +15,6 @@ /> -

Dickhead

{% include "/components/header.html" %}

{% block heading %}Some heading!{% endblock %}

{% block content %}{% endblock %} diff --git a/templates/pages/about.html b/templates/pages/about.html index d719941..7aefc54 100644 --- a/templates/pages/about.html +++ b/templates/pages/about.html @@ -17,4 +17,13 @@ heading %} Yoast SEO Schema. If you have other websites that you would like us to index, then reach out here.

+ +

Instructions

+ +

Using the website is very simple. Once you login go to bad: www.example.com/awesome-recipe

+

good: https://www.example.com/awesome-recipes

{% endblock %} diff --git a/templates/pages/account.html b/templates/pages/account.html index 10a3789..063d924 100644 --- a/templates/pages/account.html +++ b/templates/pages/account.html @@ -1,6 +1,4 @@ {% extends "base.html" %} {% block scripts %} - - + +{% endblock %} + +{% block heading %} {{ single_recipe.title }} {% endblock %} {% block content %}
{{ single_recipe.title }}
Source
-

Ingredients

-