Skip to content

Commit a760f58

Browse files
authored
Create ecommerce_recommender.py
1 parent 901fcf4 commit a760f58

1 file changed

Lines changed: 277 additions & 0 deletions

File tree

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import threading
2+
import webbrowser
3+
import tkinter as tk
4+
from tkinter import messagebox
5+
from dataclasses import dataclass
6+
from typing import List, Tuple, Dict
7+
import io
8+
import json
9+
import os
10+
import requests
11+
12+
from PIL import Image, ImageTk
13+
from sklearn.feature_extraction.text import TfidfVectorizer
14+
from sklearn.metrics.pairwise import cosine_similarity
15+
16+
import ttkbootstrap as tb
17+
from ttkbootstrap.constants import *
18+
from ttkbootstrap.widgets.scrolled import ScrolledText
19+
20+
# ---------------- CONFIG ---------------- #
21+
RESULTS_PER_PAGE = 6
22+
FAVORITES_FILE = "product_favorites.json"
23+
PRODUCTS_FILE = "products.json"
24+
25+
# ---------------- GLOBAL STATE ---------------- #
26+
all_ranked_products: List[Tuple["Product", float]] = []
27+
current_page = 1
28+
image_cache: Dict[str, ImageTk.PhotoImage] = {}
29+
favorites: List["Product"] = []
30+
31+
# ---------------- DATA STRUCTURE ---------------- #
32+
@dataclass(frozen=True)
33+
class Product:
34+
name: str
35+
brand: str
36+
category: str
37+
description: str
38+
price: float
39+
url: str
40+
image_url: str = ""
41+
42+
def text_blob(self, mode: str):
43+
if mode == "Brand":
44+
return self.brand
45+
if mode == "Category":
46+
return self.category
47+
return f"{self.name} {self.brand} {self.category} {self.description}"
48+
49+
# ---------------- SAMPLE DATA ---------------- #
50+
def load_products() -> List[Product]:
51+
if not os.path.exists(PRODUCTS_FILE):
52+
sample = [
53+
Product(
54+
name="Wireless Headphones",
55+
brand="SoundMax",
56+
category="Electronics",
57+
description="Noise cancelling over-ear headphones with deep bass",
58+
price=129.99,
59+
url="https://example.com/headphones",
60+
image_url="https://via.placeholder.com/120x180.png?text=Headphones"
61+
),
62+
Product(
63+
name="Running Shoes",
64+
brand="FastStep",
65+
category="Footwear",
66+
description="Lightweight running shoes with breathable mesh",
67+
price=89.99,
68+
url="https://example.com/shoes",
69+
image_url="https://via.placeholder.com/120x180.png?text=Shoes"
70+
),
71+
Product(
72+
name="Smart Watch",
73+
brand="TimeTech",
74+
category="Electronics",
75+
description="Fitness tracking smartwatch with heart rate monitor",
76+
price=199.99,
77+
url="https://example.com/watch",
78+
image_url="https://via.placeholder.com/120x180.png?text=Watch"
79+
),
80+
]
81+
with open(PRODUCTS_FILE, "w", encoding="utf-8") as f:
82+
json.dump([p.__dict__ for p in sample], f, indent=2)
83+
84+
with open(PRODUCTS_FILE, "r", encoding="utf-8") as f:
85+
return [Product(**p) for p in json.load(f)]
86+
87+
products = load_products()
88+
89+
# ---------------- FAVORITES ---------------- #
90+
def load_favorites():
91+
global favorites
92+
if os.path.exists(FAVORITES_FILE):
93+
with open(FAVORITES_FILE, "r", encoding="utf-8") as f:
94+
favorites = [Product(**p) for p in json.load(f)]
95+
96+
def save_favorites():
97+
with open(FAVORITES_FILE, "w", encoding="utf-8") as f:
98+
json.dump([p.__dict__ for p in favorites], f, indent=2)
99+
100+
# ---------------- RECOMMENDATION ENGINE ---------------- #
101+
def recommend_products(query: str, candidates: List[Product], mode: str):
102+
docs = [p.text_blob(mode) for p in candidates]
103+
vectorizer = TfidfVectorizer(stop_words="english")
104+
tfidf = vectorizer.fit_transform(docs + [query])
105+
scores = cosine_similarity(tfidf[-1], tfidf[:-1]).flatten()
106+
ranked = list(zip(candidates, scores))
107+
ranked.sort(key=lambda x: x[1], reverse=True)
108+
return ranked
109+
110+
# ---------------- UI HELPERS ---------------- #
111+
def open_url(url: str):
112+
webbrowser.open_new_tab(url)
113+
114+
def load_image(url: str, size=(120, 180)):
115+
if url in image_cache:
116+
return image_cache[url]
117+
try:
118+
response = requests.get(url, timeout=5)
119+
response.raise_for_status()
120+
img = Image.open(io.BytesIO(response.content)).resize(size)
121+
photo = ImageTk.PhotoImage(img)
122+
image_cache[url] = photo
123+
return photo
124+
except:
125+
return None
126+
127+
def add_to_favorites(product: Product):
128+
if product not in favorites:
129+
favorites.append(product)
130+
save_favorites()
131+
messagebox.showinfo("Saved", "Added to Favorites")
132+
133+
# ---------------- DISPLAY ---------------- #
134+
def insert_product(container, product: Product):
135+
frame = tk.Frame(container)
136+
img_label = tk.Label(frame)
137+
img_label.pack(side=tk.LEFT)
138+
139+
info = tk.Frame(frame)
140+
info.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
141+
142+
tb.Label(info, text=product.name, font=("Segoe UI", 10, "bold")).pack(anchor=tk.W)
143+
tb.Label(info, text=f"{product.brand} | {product.category}", font=("Segoe UI", 9)).pack(anchor=tk.W)
144+
tb.Label(info, text=f"${product.price:.2f}", font=("Segoe UI", 9, "bold")).pack(anchor=tk.W)
145+
146+
btns = tk.Frame(info)
147+
btns.pack(anchor=tk.W, pady=3)
148+
tb.Button(btns, text="★ Favorite", bootstyle="warning",
149+
command=lambda p=product: add_to_favorites(p)).pack(side=tk.LEFT, padx=2)
150+
tb.Button(btns, text="↗ View", bootstyle="info",
151+
command=lambda u=product.url: open_url(u)).pack(side=tk.LEFT, padx=2)
152+
153+
container.configure(state="normal")
154+
container.window_create("end", window=frame)
155+
container.insert("end", "\n\n")
156+
container.configure(state="disabled")
157+
158+
# -------------------- Download image in background -------------------- #
159+
def download_image():
160+
img = load_image(product.image_url)
161+
if img:
162+
# Update widget safely in main thread
163+
app.after(0, lambda: setattr(img_label, 'image', img) or img_label.configure(image=img))
164+
165+
threading.Thread(target=download_image, daemon=True).start()
166+
167+
# ---------------- PAGINATION ---------------- #
168+
def display_page():
169+
text.configure(state="normal")
170+
text.delete("1.0", "end")
171+
start = (current_page - 1) * RESULTS_PER_PAGE
172+
end = start + RESULTS_PER_PAGE
173+
for product, _ in all_ranked_products[start:end]:
174+
insert_product(text, product)
175+
text.configure(state="disabled")
176+
update_pagination()
177+
178+
def next_page():
179+
global current_page
180+
if current_page * RESULTS_PER_PAGE < len(all_ranked_products):
181+
current_page += 1
182+
display_page()
183+
184+
def prev_page():
185+
global current_page
186+
if current_page > 1:
187+
current_page -= 1
188+
display_page()
189+
190+
def update_pagination():
191+
total = max(1, (len(all_ranked_products) - 1) // RESULTS_PER_PAGE + 1)
192+
page_label.config(text=f"Page {current_page} of {total}")
193+
194+
# ---------------- SEARCH ---------------- #
195+
def get_selected_modes():
196+
selected = [name for name, var in mode_vars.items() if var.get() == 1]
197+
return selected or ["All"]
198+
199+
def perform_search():
200+
query = query_entry.get().strip()
201+
if not query:
202+
return
203+
threading.Thread(target=search_thread, args=(query,), daemon=True).start()
204+
205+
def search_thread(query: str):
206+
modes = get_selected_modes()
207+
ranked = []
208+
for mode in modes:
209+
ranked.extend(recommend_products(query, products, mode))
210+
211+
ranked.sort(key=lambda x: x[1], reverse=True)
212+
seen = set()
213+
final = []
214+
for p, s in ranked:
215+
if p.name not in seen:
216+
seen.add(p.name)
217+
final.append((p, s))
218+
219+
def update():
220+
global all_ranked_products, current_page
221+
current_page = 1
222+
all_ranked_products = final
223+
display_page()
224+
225+
app.after(0, update)
226+
227+
# ---------------- UI SETUP ---------------- #
228+
app = tb.Window(title="E-Commerce Recommendation System",
229+
themename="darkly",
230+
size=(1000, 680))
231+
232+
load_favorites()
233+
234+
top = tb.Frame(app, padding=15)
235+
top.pack(fill=tk.X)
236+
237+
tb.Label(top, text="🛒 Product Recommendation Engine",
238+
font=("Segoe UI", 18, "bold")).pack(anchor=tk.W)
239+
240+
query_entry = tb.Entry(top, font=("Segoe UI", 13))
241+
query_entry.pack(fill=tk.X, pady=6)
242+
query_entry.bind("<Return>", lambda e: perform_search())
243+
244+
mode_vars = {
245+
"All": tk.IntVar(value=1),
246+
"Category": tk.IntVar(value=0),
247+
"Brand": tk.IntVar(value=0)
248+
}
249+
250+
mode_frame = tb.Frame(top)
251+
mode_frame.pack(fill=tk.X, pady=5)
252+
253+
for name, var in mode_vars.items():
254+
tb.Checkbutton(mode_frame, text=name, variable=var,
255+
bootstyle="info").pack(side=tk.LEFT, padx=5)
256+
257+
tb.Button(mode_frame, text="Search",
258+
bootstyle="primary",
259+
command=perform_search).pack(side=tk.LEFT, padx=10)
260+
261+
result_frame = tb.Frame(app)
262+
result_frame.pack(fill=tk.BOTH, expand=True)
263+
264+
result_box = ScrolledText(result_frame)
265+
result_box.pack(fill=tk.BOTH, expand=True)
266+
text = result_box.text
267+
text.configure(state="disabled", wrap="word")
268+
269+
nav = tb.Frame(app, padding=10)
270+
nav.pack()
271+
272+
tb.Button(nav, text="← Prev", command=prev_page).pack(side=tk.LEFT)
273+
page_label = tb.Label(nav, text="Page 1")
274+
page_label.pack(side=tk.LEFT, padx=10)
275+
tb.Button(nav, text="Next →", command=next_page).pack(side=tk.LEFT)
276+
277+
app.mainloop()

0 commit comments

Comments
 (0)