aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJJ <nicetry@noemail.com>2025-03-31 21:08:41 +0100
committerJJ <nicetry@noemail.com>2025-03-31 21:08:41 +0100
commit2e0b9c97af457da5c6afda611d48e59047d4cdb8 (patch)
tree199d10037f790bcade2ac86f478c10da77bbc771
parent17529f38f4b4edf7249e18418ddfcc3f818a006d (diff)
Basic AI functionality
-rw-r--r--.Constants.py.swpbin0 -> 12288 bytes
-rw-r--r--.gitignore1
-rw-r--r--AIParams.py25
-rw-r--r--scripts/database/mongo.py11
-rw-r--r--scripts/recipes/handle_recipes.py2
-rw-r--r--scripts/scraping/ai_scraping.py19
-rw-r--r--scripts/scraping/scraper.py3
-rw-r--r--scripts/users/handle_users.py10
-rw-r--r--server.py28
-rw-r--r--static/images/bars.svg2
-rw-r--r--static/scripts/index.js57
-rw-r--r--static/style/style.css8
-rw-r--r--templates/base.html1
-rw-r--r--templates/pages/about.html9
-rw-r--r--templates/pages/account.html21
-rw-r--r--templates/pages/home.html2
-rw-r--r--templates/pages/single-recipe.html14
17 files changed, 186 insertions, 27 deletions
diff --git a/.Constants.py.swp b/.Constants.py.swp
new file mode 100644
index 0000000..ffb1f96
--- /dev/null
+++ b/.Constants.py.swp
Binary files 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 "<span class='error'>Recipe already exists!</span>", 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"<span class='error'>Error processing recipe metadata, {response['error']} </span>", 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"<span class='error'>{response["message"]}</span>", 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 @@
-<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#fff">
+<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#1d1f27">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
diff --git a/static/scripts/index.js b/static/scripts/index.js
index 65c3c12..b1b006d 100644
--- a/static/scripts/index.js
+++ b/static/scripts/index.js
@@ -1,11 +1,50 @@
-const modal = document.querySelector("dialog");
-const showModalBtn = document.querySelector("#show-modal");
-const closeModalBtn = document.querySelector("#close-modal");
+// const modal = document.querySelector("dialog");
+const showModalBtns = document.querySelectorAll(".show-modal");
+const closeModalBtns = document.querySelectorAll(".close-modal");
+const clipboard = document.querySelector("#copy-ingredients");
-showModalBtn.addEventListener("click", () => {
- 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", "<span id='copied-message'> ingredients copied!</span>");
+
+ 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 @@
/>
</head>
<body hx-ext="response-targets">
- <h1>Dickhead</h1>
{% include "/components/header.html" %}
<h1>{% block heading %}Some heading!{% endblock %}</h1>
{% 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 <a href="/contact">here.</a>
</p>
+
+<h2>Instructions</h2>
+
+<p>Using the website is very simple. Once you login go to <a href="/home>recipes</a>, this is your homepage. In the input box add URLs for your favourite recipes.</p>
+
+<p>URLs must be valid. E.g:</p>
+
+<p><strong class="error">bad: </strong>www.example.com/awesome-recipe</p>
+<p><strong class="success">good: </strong>https://www.example.com/awesome-recipes</p>
{% 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 %}
-
-<!-- Conditionally load index.js, as it's only needed on this page -->
<script
src="{{ url_for('static', filename='scripts/index.js') }}"
defer
@@ -28,4 +26,23 @@
<button hx-delete="/delete-account">yes</button>
<button id="close-modal">cancel</button>
</dialog>
+
+<h2>AI mode</h2>
+{% if session.isAiSubscriber %}
+ <p>Thanks for actvating AI mode. You can check your quota of AI requests remaining below</p>
+{% else %}
+ <p>The HTML markup for recipe websites differs greatly. Therefore, it's hard to extract all the HTML from all websites. You may find there are certain websites that we cannot extract recipe data from. To get around this, you have the option to use <strong>AI mode</strong>.</p>
+ <p>AI mode works by running the page's HTML through an LLM to extract the relevant recipe information.</p>
+ <p>As this process is not cheap, you can buy credits that will allow you to perform these extraction operations. $5 will get you 1,000 AI operations. Probably more than you will ever need</p>
+ <p>We will always try and extract website data using normal metadata before using AI, so it's unlikely you will get close to 1,000 operations.</p>
+ <button id="show-modal">Enable AI mode</button>
+ <dialog>
+ <p>
+ Are you sure you want to delete your account? This will erase all your data
+ including your recipes.
+ </p>
+ <button hx-delete="/delete-account">yes</button>
+ <button id="close-modal">cancel</button>
+ </dialog>
+ {% endif %}
{% endblock %}
diff --git a/templates/pages/home.html b/templates/pages/home.html
index c44a017..6cb5935 100644
--- a/templates/pages/home.html
+++ b/templates/pages/home.html
@@ -11,7 +11,7 @@
hx-target-5*="#form-error"
>
<div style="display: flex; align-items: center; margin-bottom: 15px;"}>
- <input type="text" name="url" class="border-2 border-black" style="margin-bottom: 0px; margin-right: 10px;"/>
+ <input type="url" name="url" class="border-2 border-black" style="margin-bottom: 0px; margin-right: 10px;"/>
<img src={{url_for("static", filename="images/bars.svg")}} class="htmx-indicator" id="spinner" alt="loading indicator "/>
</div>
<button type="submit" class="cursor-pointer">Submit</button>
diff --git a/templates/pages/single-recipe.html b/templates/pages/single-recipe.html
index d8c334d..ec2b4dd 100644
--- a/templates/pages/single-recipe.html
+++ b/templates/pages/single-recipe.html
@@ -1,12 +1,20 @@
-{% extends "base.html" %} {% block heading %} {{ single_recipe.title }} {%
+{% extends "base.html" %}
+
+{% block scripts %}
+
+<script src={{ url_for("static", filename="scripts/index.js") }} defer></script>
+
+{% endblock %}
+
+{% block heading %} {{ single_recipe.title }} {%
endblock %} {% block content %}
<article>
<figure>
<img src="{{ single_recipe.image }}" alt="{{ single_recipe.title }}" />
<figcaption><a href="{{ single_recipe.url }}">Source</a></figcaption>
</figure>
- <h2>Ingredients</h2>
- <ul>
+ <h2 class="inline-block">Ingredients<span id="copy-ingredients" class="cursor">📋</span></h2>
+ <ul id="ingredients">
{% for ingredient in single_recipe.ingredients %}
<li>{{ ingredient }}</li>
{% endfor %}