Spaces:
Runtime error
Runtime error
John Yang
commited on
Commit
•
11d1f29
1
Parent(s):
72eae12
Add sim-to-real transfer
Browse files- .gitignore +1 -0
- app.py +135 -7
- predict_help.py +205 -0
- requirements.txt +3 -0
- templates/attributes_page.html +58 -0
- templates/description_page.html +43 -0
- templates/done_page.html +50 -0
- templates/features_page.html +47 -0
- templates/item_page.html +113 -0
- templates/results_page.html +89 -0
- templates/review_page.html +59 -0
- templates/search_page.html +34 -0
- webshop_lite.py +102 -0
.gitignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
*.pyc
|
app.py
CHANGED
@@ -2,6 +2,9 @@ import gradio as gr
|
|
2 |
import torch
|
3 |
from transformers import BartTokenizer, BartForConditionalGeneration, AutoModel, AutoTokenizer
|
4 |
|
|
|
|
|
|
|
5 |
# load IL models
|
6 |
bart_tokenizer = BartTokenizer.from_pretrained('facebook/bart-large')
|
7 |
bart_model = BartForConditionalGeneration.from_pretrained('webshop/il_search_bart')
|
@@ -85,17 +88,142 @@ def predict(obs, info):
|
|
85 |
if valid_acts[0].startswith('click['):
|
86 |
return bert_predict(obs, info)
|
87 |
else:
|
88 |
-
return bart_predict(process_goal(obs))
|
89 |
|
|
|
90 |
|
91 |
-
def run_episode(goal):
|
92 |
"""
|
93 |
Interact with amazon to find a product given input goal.
|
94 |
Input: text goal
|
95 |
Output: a url of found item on amazon.
|
96 |
"""
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import torch
|
3 |
from transformers import BartTokenizer, BartForConditionalGeneration, AutoModel, AutoTokenizer
|
4 |
|
5 |
+
from webshop_lite import dict_to_fake_html
|
6 |
+
from predict_help import convert_dict_to_actions, convert_html_to_text, parse_results, parse_item_page, Page
|
7 |
+
|
8 |
# load IL models
|
9 |
bart_tokenizer = BartTokenizer.from_pretrained('facebook/bart-large')
|
10 |
bart_model = BartForConditionalGeneration.from_pretrained('webshop/il_search_bart')
|
|
|
88 |
if valid_acts[0].startswith('click['):
|
89 |
return bert_predict(obs, info)
|
90 |
else:
|
91 |
+
return "search[" + bart_predict(process_goal(obs)) + "]"
|
92 |
|
93 |
+
NUM_PROD_LIMIT = 10
|
94 |
|
95 |
+
def run_episode(goal, verbose=True):
|
96 |
"""
|
97 |
Interact with amazon to find a product given input goal.
|
98 |
Input: text goal
|
99 |
Output: a url of found item on amazon.
|
100 |
"""
|
101 |
+
obs = "Amazon Shopping Game\nInstruction:" + goal + "\n[button] search [button]"
|
102 |
+
info = {'valid': ['search[stuff]'], 'image_feat': torch.zeros(512)}
|
103 |
+
product_map = {}
|
104 |
+
page_to_product_map_memo = {}
|
105 |
+
visited_asins, clicked_options = set(), set()
|
106 |
+
arg, sub_page_type, page_type, page_num = None, None, None, None
|
107 |
+
search_terms, prod_title, asin, num_prods, = None, None, None, None
|
108 |
+
options = {}
|
109 |
+
|
110 |
+
for i in range(100):
|
111 |
+
# Run prediction
|
112 |
+
action = predict(obs, info)
|
113 |
+
if verbose:
|
114 |
+
print("====")
|
115 |
+
print(action)
|
116 |
+
|
117 |
+
# Previous Page Type, Action -> Next Page Type
|
118 |
+
action_content = action[action.find("[")+1:action.find("]")]
|
119 |
+
prev_page_type = page_type
|
120 |
+
if action.startswith('search['):
|
121 |
+
page_type = Page.RESULTS
|
122 |
+
search_terms = action_content
|
123 |
+
page_num = 1
|
124 |
+
elif action.startswith('click['):
|
125 |
+
if action.startswith('click[item -'):
|
126 |
+
prod_title = action_content[len("item -"):].strip()
|
127 |
+
found = False
|
128 |
+
for value in product_map.values():
|
129 |
+
if prod_title == value["Title"]:
|
130 |
+
asin = value["asin"]
|
131 |
+
page_type = Page.ITEM_PAGE
|
132 |
+
visited_asins.add(asin)
|
133 |
+
found = True
|
134 |
+
break
|
135 |
+
if not found:
|
136 |
+
raise Exception("Product to click not found")
|
137 |
+
|
138 |
+
elif any(x.value in action for x in [Page.DESC, Page.FEATURES, Page.REVIEWS]):
|
139 |
+
page_type = Page.SUB_PAGE
|
140 |
+
sub_page_type = Page(action_content.lower())
|
141 |
+
|
142 |
+
elif action == 'click[< prev]':
|
143 |
+
if sub_page_type is not None:
|
144 |
+
page_type, sub_page_type = Page.ITEM_PAGE, None
|
145 |
+
elif prev_page_type == Page.ITEM_PAGE:
|
146 |
+
page_type = Page.RESULTS
|
147 |
+
options, clicked_options = {}, set()
|
148 |
+
elif prev_page_type == Page.RESULTS and page_num > 1:
|
149 |
+
page_type = Page.RESULTS
|
150 |
+
page_num -= 1
|
151 |
+
|
152 |
+
elif action == 'click[next >]':
|
153 |
+
page_type = Page.RESULTS
|
154 |
+
page_num += 1
|
155 |
+
|
156 |
+
elif action.lower() == 'click[back to search]':
|
157 |
+
page_type = Page.SEARCH
|
158 |
+
|
159 |
+
elif action == 'click[buy now]':
|
160 |
+
return asin
|
161 |
+
|
162 |
+
elif prev_page_type == Page.ITEM_PAGE:
|
163 |
+
found = False
|
164 |
+
for opt_name, opt_values in product_map[asin]["options"].items():
|
165 |
+
if action_content in opt_values:
|
166 |
+
options[opt_name] = action_content
|
167 |
+
page_type = Page.ITEM_PAGE
|
168 |
+
clicked_options.add(action_content)
|
169 |
+
found = True
|
170 |
+
break
|
171 |
+
if not found:
|
172 |
+
raise Exception("Unrecognized action: " + action)
|
173 |
+
else:
|
174 |
+
raise Exception("Unrecognized action:" + action)
|
175 |
+
|
176 |
+
if verbose:
|
177 |
+
print(f"Parsing {page_type.value} page...")
|
178 |
+
|
179 |
+
# URL -> Real HTML -> Dict of Info
|
180 |
+
if page_type == Page.RESULTS:
|
181 |
+
if search_terms not in page_to_product_map_memo or page_num not in page_to_product_map_memo[search_terms]:
|
182 |
+
product_map = {}
|
183 |
+
asins = parse_results(search_terms, page_num)
|
184 |
+
num_prods = len(asins)
|
185 |
+
for asin_ in asins[:NUM_PROD_LIMIT]:
|
186 |
+
product_map[asin_] = parse_item_page(asin_)
|
187 |
+
if search_terms not in page_to_product_map_memo:
|
188 |
+
page_to_product_map_memo[search_terms] = {}
|
189 |
+
page_to_product_map_memo[search_terms][page_num] = product_map
|
190 |
+
else:
|
191 |
+
if verbose:
|
192 |
+
print("Loaded memoized search results (" + str(page_num) + ")...")
|
193 |
+
product_map = page_to_product_map_memo[search_terms][page_num]
|
194 |
+
if verbose:
|
195 |
+
print("Product Map Length:", len(product_map))
|
196 |
+
data = list(product_map.values())
|
197 |
+
elif page_type == Page.ITEM_PAGE or page_type == Page.SUB_PAGE:
|
198 |
+
data = product_map
|
199 |
+
elif page_type == Page.SEARCH:
|
200 |
+
if verbose:
|
201 |
+
print("Executing search")
|
202 |
+
obs = "Amazon Shopping Game\nInstruction:" + goal + "\n[button] search [button]"
|
203 |
+
info = {'valid': ['search[stuff]'], 'image_feat': torch.zeros(512)}
|
204 |
+
continue
|
205 |
+
else:
|
206 |
+
raise Exception("Page of type `", page_type,value, "` not found")
|
207 |
+
|
208 |
+
# Dict of Info -> Fake HTML -> Text Observation
|
209 |
+
html_str = dict_to_fake_html(data, page_type, asin, sub_page_type, options, product_map, goal)
|
210 |
+
obs = convert_html_to_text(html_str, simple=False, clicked_options=clicked_options, visited_asins=visited_asins)
|
211 |
+
|
212 |
+
# Dict of Info -> Valid Action State (Info)
|
213 |
+
info = convert_dict_to_actions(page_type, data, asin, page_num, num_prods)
|
214 |
+
|
215 |
+
if i == 99:
|
216 |
+
return asin
|
217 |
+
|
218 |
+
gr.Interface(fn=run_episode,\
|
219 |
+
inputs=gr.inputs.Textbox(lines=7, label="Input Text"),\
|
220 |
+
outputs="text",\
|
221 |
+
examples=[
|
222 |
+
"I am looking for a high power sound column subwoofer, that uses bluetooth and is also a 3d surround sound system, and price lower than 650.00 dollars",
|
223 |
+
"Please select a 1 pound, certified organic sea salt shaker in the flavor triple blend flakes, and price lower than 40.00 dollars",
|
224 |
+
"I want to find a gold floor lamp with a glass shade and a nickel finish that i can use for my living room, and price lower than 270.00 dollars"
|
225 |
+
],\
|
226 |
+
title="WebShop",\
|
227 |
+
article="<p style='padding-top:15px;text-align:center;'>To learn more about this project, check out the <a href='https://webshop-pnlp.github.io/' target='_blank'>project page</a>!</p>",\
|
228 |
+
description="<p style='text-align:center;'>Imitation Learning agent that searches for the desired product on Amazon from any natural language query!</p>",\
|
229 |
+
).launch(inline=False)
|
predict_help.py
ADDED
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from bs4 import BeautifulSoup
|
2 |
+
from bs4.element import Comment
|
3 |
+
from enum import Enum
|
4 |
+
from urllib.parse import urlencode
|
5 |
+
|
6 |
+
import json, requests, torch
|
7 |
+
|
8 |
+
class Page(Enum):
|
9 |
+
DESC = "description"
|
10 |
+
FEATURES = "features"
|
11 |
+
ITEM_PAGE = "item_page"
|
12 |
+
RESULTS = "results"
|
13 |
+
REVIEWS = "reviews"
|
14 |
+
SEARCH = "search"
|
15 |
+
SUB_PAGE = "item_sub_page"
|
16 |
+
|
17 |
+
HEADER_ = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36'
|
18 |
+
DEBUG_HTML = "temp.html"
|
19 |
+
VERBOSE = True
|
20 |
+
|
21 |
+
API = '85956985fae328bfe5a759a2984448d2'
|
22 |
+
def get_url(url):
|
23 |
+
payload = {'api_key': API, 'url': url , 'country_code': 'us'}
|
24 |
+
proxy_url = 'http://api.scraperapi.com/?' + urlencode(payload)
|
25 |
+
return proxy_url
|
26 |
+
|
27 |
+
# Query -> Search Result ASINs
|
28 |
+
def parse_results(query, page_num=None):
|
29 |
+
url = 'https://www.amazon.com/s?k=' + query.replace(" ", "+")
|
30 |
+
if page_num is not None:
|
31 |
+
url += "&page=" + str(page_num)
|
32 |
+
if VERBOSE:
|
33 |
+
print("Action URL: ", get_url(url))
|
34 |
+
webpage = requests.get(get_url(url), headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})
|
35 |
+
asins = []
|
36 |
+
soup = BeautifulSoup(webpage.content, 'html.parser')
|
37 |
+
products = soup.findAll('div', {'data-component-type': 's-search-result'})
|
38 |
+
if products is None:
|
39 |
+
temp = open(DEBUG_HTML, "w")
|
40 |
+
temp.write(str(soup))
|
41 |
+
temp.close()
|
42 |
+
raise Exception("Couldn't find search results page, outputted html for inspection")
|
43 |
+
for product in products:
|
44 |
+
asins.append(product['data-asin'])
|
45 |
+
if VERBOSE:
|
46 |
+
print("Scraped", len(asins), "products")
|
47 |
+
return asins
|
48 |
+
|
49 |
+
# Scrape information of each product
|
50 |
+
def parse_item_page(asin):
|
51 |
+
product_dict = {}
|
52 |
+
product_dict["asin"] = asin
|
53 |
+
|
54 |
+
url = f"https://www.amazon.com/dp/{asin}"
|
55 |
+
webpage = requests.get(get_url(url), headers={'User-Agent': HEADER_, 'Accept-Language': 'en-US, en;q=0.5'})
|
56 |
+
soup = BeautifulSoup(webpage.content, "html.parser")
|
57 |
+
|
58 |
+
# Title
|
59 |
+
try:
|
60 |
+
title = soup.find("span", attrs={"id": 'productTitle'})
|
61 |
+
title = title.string.strip().replace(',', '')
|
62 |
+
except AttributeError:
|
63 |
+
title = "N/A"
|
64 |
+
product_dict["Title"] = title
|
65 |
+
|
66 |
+
# Price
|
67 |
+
try:
|
68 |
+
parent_price_span = soup.find(name="span", class_="apexPriceToPay")
|
69 |
+
price_span = parent_price_span.find(name="span", class_="a-offscreen")
|
70 |
+
price = float(price_span.getText().replace("$", ""))
|
71 |
+
except AttributeError:
|
72 |
+
price = "N/A"
|
73 |
+
product_dict["Price"] = price
|
74 |
+
|
75 |
+
# Rating
|
76 |
+
try:
|
77 |
+
rating = soup.find(name="span", attrs={"id": "acrPopover"})
|
78 |
+
if rating is None:
|
79 |
+
rating = "N/A"
|
80 |
+
else:
|
81 |
+
rating = rating.text
|
82 |
+
except AttributeError:
|
83 |
+
rating = "N/A"
|
84 |
+
product_dict["Rating"] = rating.strip("\n").strip()
|
85 |
+
|
86 |
+
# Features
|
87 |
+
try:
|
88 |
+
features = soup.find(name="div", attrs={"id": "feature-bullets"}).text
|
89 |
+
except AttributeError:
|
90 |
+
features = "N/A"
|
91 |
+
product_dict["BulletPoints"] = features
|
92 |
+
|
93 |
+
# Description
|
94 |
+
try:
|
95 |
+
desc_body = soup.find(name="div", attrs={"id": "productDescription_feature_div"})
|
96 |
+
desc_div = desc_body.find(name="div", attrs={"id": "productDescription"})
|
97 |
+
desc_ps = desc_div.findAll(name="p")
|
98 |
+
desc = " ".join([p.text for p in desc_ps])
|
99 |
+
|
100 |
+
except AttributeError:
|
101 |
+
desc = "N/A"
|
102 |
+
product_dict["Description"] = desc.strip()
|
103 |
+
|
104 |
+
# Main Image
|
105 |
+
try:
|
106 |
+
body = soup.find("body")
|
107 |
+
imgtag = soup.find("img", {"id":"landingImage"})
|
108 |
+
imageurl = dict(imgtag.attrs)["src"]
|
109 |
+
except AttributeError:
|
110 |
+
imageurl = ""
|
111 |
+
product_dict["MainImage"] = imageurl
|
112 |
+
|
113 |
+
# Options
|
114 |
+
options, options_to_image = {}, {}
|
115 |
+
try:
|
116 |
+
option_body = soup.find(name='div', attrs={"id": "softlinesTwister_feature_div"})
|
117 |
+
if option_body is None:
|
118 |
+
option_body = soup.find(name='div', attrs={"id": "twister_feature_div"})
|
119 |
+
option_blocks = option_body.findAll(name='ul')
|
120 |
+
for block in option_blocks:
|
121 |
+
name = json.loads(block["data-a-button-group"])["name"]
|
122 |
+
# Options
|
123 |
+
opt_list = []
|
124 |
+
for li in block.findAll("li"):
|
125 |
+
img = li.find(name="img")
|
126 |
+
if img is not None:
|
127 |
+
opt = img["alt"].strip()
|
128 |
+
opt_img = img["src"]
|
129 |
+
if len(opt) > 0:
|
130 |
+
options_to_image[opt] = opt_img
|
131 |
+
else:
|
132 |
+
opt = li.text.strip()
|
133 |
+
if len(opt) > 0:
|
134 |
+
opt_list.append(opt)
|
135 |
+
options[name.replace("_name", "").replace("twister_", "")] = opt_list
|
136 |
+
except AttributeError:
|
137 |
+
options = {}
|
138 |
+
product_dict["options"], product_dict["option_to_image"] = options, options_to_image
|
139 |
+
return product_dict
|
140 |
+
|
141 |
+
# Get text observation from html
|
142 |
+
def convert_html_to_text(html, simple=False, clicked_options=None, visited_asins=None):
|
143 |
+
def tag_visible(element):
|
144 |
+
ignore = {'style', 'script', 'head', 'title', 'meta', '[document]'}
|
145 |
+
return (
|
146 |
+
element.parent.name not in ignore and not isinstance(element, Comment)
|
147 |
+
)
|
148 |
+
html_obj = BeautifulSoup(html, 'html.parser')
|
149 |
+
texts = html_obj.findAll(text=True)
|
150 |
+
visible_texts = filter(tag_visible, texts)
|
151 |
+
if simple:
|
152 |
+
return ' [SEP] '.join(t.strip() for t in visible_texts if t != '\n')
|
153 |
+
else:
|
154 |
+
observation = ''
|
155 |
+
for t in visible_texts:
|
156 |
+
if t == '\n': continue
|
157 |
+
if t.parent.name == 'button': # button
|
158 |
+
processed_t = f'[button] {t} [button]'
|
159 |
+
elif t.parent.name == 'label': # options
|
160 |
+
if f'{t}' in clicked_options:
|
161 |
+
processed_t = f' [clicked button] {t} [clicked button]'
|
162 |
+
observation = f'You have clicked {t}.\n' + observation
|
163 |
+
else:
|
164 |
+
processed_t = f' [button] {t} [button]'
|
165 |
+
elif t.parent.get('class') == ["product-link"]: # asins
|
166 |
+
if f'{t}' in visited_asins:
|
167 |
+
processed_t = f'\n[clicked button] {t} [clicked button]'
|
168 |
+
else:
|
169 |
+
processed_t = f'\n[button] {t} [button]'
|
170 |
+
else: # regular, unclickable text
|
171 |
+
processed_t = str(t)
|
172 |
+
observation += processed_t + '\n'
|
173 |
+
return observation
|
174 |
+
|
175 |
+
# Get action from dict
|
176 |
+
def convert_dict_to_actions(page_type, products=None, asin=None, page_num=None, num_prods=None) -> dict:
|
177 |
+
info = {"valid": []}
|
178 |
+
if page_type == Page.RESULTS:
|
179 |
+
info["valid"] = ['click[back to search]']
|
180 |
+
if products is None or page_num is None or num_prods is None:
|
181 |
+
print(page_num)
|
182 |
+
print(num_prods)
|
183 |
+
print(products)
|
184 |
+
raise Exception('Provide `products`, `num_prods`, `page_num` to get `results` valid actions')
|
185 |
+
# Decide whether to add `next >` as clickable based on # of search results
|
186 |
+
if num_prods > 10:
|
187 |
+
info["valid"].append('click[next >]')
|
188 |
+
# Add `< prev` as clickable if not first page of search results
|
189 |
+
if page_num > 1:
|
190 |
+
info["valid"].append('click[< prev]')
|
191 |
+
for product in products:
|
192 |
+
info["valid"].append("click[item - " + product["Title"] + "]")
|
193 |
+
if page_type == Page.ITEM_PAGE:
|
194 |
+
if products is None or asin is None:
|
195 |
+
raise Exception('Provide `products` and `asin` to get `item_page` valid actions')
|
196 |
+
info["valid"] = ['click[back to search]', 'click[< prev]', 'click[description]',\
|
197 |
+
'click[features]', 'click[buy now]'] # To do: reviews
|
198 |
+
if "options" in products[asin]:
|
199 |
+
for key, values in products[asin]["options"].items():
|
200 |
+
for value in values:
|
201 |
+
info["valid"].append("click[" + value + "]")
|
202 |
+
if page_type == Page.SUB_PAGE:
|
203 |
+
info["valid"] = ['click[back to search]', 'click[< prev]']
|
204 |
+
info['image_feat'] = torch.zeros(512)
|
205 |
+
return info
|
requirements.txt
CHANGED
@@ -1,2 +1,5 @@
|
|
|
|
|
|
|
|
1 |
torch
|
2 |
transformers
|
|
|
1 |
+
bs4
|
2 |
+
flask
|
3 |
+
requests
|
4 |
torch
|
5 |
transformers
|
templates/attributes_page.html
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
|
5 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
6 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css ">
|
7 |
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
8 |
+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
|
9 |
+
<link rel="icon" href="data:,">
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="container py-5">
|
13 |
+
<div class="row top-buffer">
|
14 |
+
<div class="col-sm-6">
|
15 |
+
<div id="instruction-text" class="text-center">
|
16 |
+
<h4>Instruction:<br>{{ instruction_text }}</h4>
|
17 |
+
</div>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
<div class="row top-buffer">
|
21 |
+
<form method="post" action="{{url_for('index', session_id=session_id)}}">
|
22 |
+
<button type="submit" class="btn btn-success">Back to Search</button>
|
23 |
+
</form>
|
24 |
+
</div>
|
25 |
+
<div class="row top-buffer">
|
26 |
+
<form method="post" action="{{url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=options)}}">
|
27 |
+
<button type="submit" class="btn btn-primary">< Prev</button>
|
28 |
+
</form>
|
29 |
+
</div>
|
30 |
+
<div class="row top-buffer">
|
31 |
+
<div class="col-md-12">
|
32 |
+
<div class="row top-buffer">
|
33 |
+
<div class="col-sm-6" name="description">
|
34 |
+
<div class="card card-body">
|
35 |
+
<ul>
|
36 |
+
{% for attribute in product_info.Attributes %}
|
37 |
+
<li><p class="attribute"> {{attribute}}</p></li>
|
38 |
+
{% endfor %}
|
39 |
+
</ul>
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
<div class="col-sm-6" name="description">
|
43 |
+
<div class="d-flex align-items-center justify-content-between mt-1">
|
44 |
+
<h5 class="font-weight-bold my-2 product-category">{{product_info.category}}</h5>
|
45 |
+
</div>
|
46 |
+
<div class="d-flex align-items-center justify-content-between mt-1">
|
47 |
+
<h5 class="font-weight-bold my-2 product-query">{{product_info.query}}</h5>
|
48 |
+
</div>
|
49 |
+
<div class="d-flex align-items-center justify-content-between mt-1">
|
50 |
+
<h5 class="font-weight-bold my-2 product-product_category">{{product_info.product_category}}</h5>
|
51 |
+
</div>
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
</div>
|
56 |
+
</div>
|
57 |
+
</body>
|
58 |
+
</html>
|
templates/description_page.html
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
|
5 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
6 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css ">
|
7 |
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
8 |
+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
|
9 |
+
<link rel="icon" href="data:,">
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="container py-5">
|
13 |
+
<div class="row top-buffer">
|
14 |
+
<div class="col-sm-6">
|
15 |
+
<div id="instruction-text" class="text-center">
|
16 |
+
<h4>Instruction:<br>{{ instruction_text }}</h4>
|
17 |
+
</div>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
<div class="row top-buffer">
|
21 |
+
<form method="post" action="{{url_for('index', session_id=session_id)}}">
|
22 |
+
<button type="submit" class="btn btn-success">Back to Search</button>
|
23 |
+
</form>
|
24 |
+
</div>
|
25 |
+
<div class="row top-buffer">
|
26 |
+
<form method="post" action="{{url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=options)}}">
|
27 |
+
<button type="submit" class="btn btn-primary">< Prev</button>
|
28 |
+
</form>
|
29 |
+
</div>
|
30 |
+
<div class="row top-buffer">
|
31 |
+
<div class="col-md-12">
|
32 |
+
<div class="row top-buffer">
|
33 |
+
<div class="col-sm-6" name="description">
|
34 |
+
<div class="card card-body">
|
35 |
+
<p class="product-info">{{product_info.Description}}</p>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
</div>
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
</body>
|
43 |
+
</html>
|
templates/done_page.html
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
|
5 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
6 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
7 |
+
<link rel="icon" href="data:,">
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<!-- Code reference: https://bootsnipp.com/snippets/m13mN -->
|
11 |
+
<div class="container" style="margin-top: 8%;">
|
12 |
+
<div class="col-md-6 col-md-offset-3">
|
13 |
+
<div class="row">
|
14 |
+
<div id="thankyou" class="text-center">
|
15 |
+
<h1>Thank you for shopping with us!</h1>
|
16 |
+
</div>
|
17 |
+
<div id="stats" class="text-center">
|
18 |
+
<h3 align="mturk_code">Your code: </h3>
|
19 |
+
<p><pre>{{ mturk_code }}</pre> (Paste it in your MTurk interface.)</p>
|
20 |
+
<div style="display:none">
|
21 |
+
<h2 align="left">Purchased</h2>
|
22 |
+
<hr class="solid">
|
23 |
+
<h4 id="asin">asin<pre>{{ asin }}</pre></p>
|
24 |
+
<h4 id="options">options<pre>{{ options | tojson }}</pre></h4>
|
25 |
+
<h4 id="purchased_attrs">attrs<pre>{{ purchased_attrs }}</pre></h4>
|
26 |
+
<h4 id="purchased-category">category<pre>{{ category }}</pre></h4>
|
27 |
+
<h4 id="purchased-query">query<pre>{{ query }}</pre></h4>
|
28 |
+
<h4 id="purchased-pc">product category<pre>{{ product_category }}</pre></h4>
|
29 |
+
<h2 align="left">Target</h2>
|
30 |
+
<hr class="solid">
|
31 |
+
<h4 id="goal-asin">asin<pre>{{ goal.asin }}</pre></p>
|
32 |
+
<h4 id="goal-options">options<pre>{{ goal.goal_options }}</pre></h4>
|
33 |
+
<h4 id="goal-attrs">attrs<pre>{{ goal.attributes }}</pre></h4>
|
34 |
+
<h4 id="goal-price">price upper<pre>{{ goal.price_upper }}</pre></h4>
|
35 |
+
<h4 id="goal-instruction-text">instuction text<pre>{{ goal.instruction_text }}</pre></h4>
|
36 |
+
<h4 id="goal-category">category<pre>{{ goal.category }}</pre></h4>
|
37 |
+
<h4 id="goal-pc">product category<pre>{{ goal.product_category }}</pre></h4>
|
38 |
+
<h4 id="goal-query">query<pre>{{ goal.query }}</pre></h4>
|
39 |
+
<h4>Goal <div id="goal"><pre>{{ goal | pprint }}</pre></div></h4>
|
40 |
+
<h2 align="left">Reward</h2>
|
41 |
+
</div>
|
42 |
+
<hr class="solid">
|
43 |
+
<h3 id="reward">Your score (min 0.0, max 1.0)<pre>{{ reward }}</pre></h3>
|
44 |
+
<h4 hidden>Reward Details <div id="reward_info"><pre>{{ reward_info | pprint }}</pre></div></h4>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
</div>
|
49 |
+
</body>
|
50 |
+
</html>
|
templates/features_page.html
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
|
5 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
6 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css ">
|
7 |
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
8 |
+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
|
9 |
+
<link rel="icon" href="data:,">
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="container py-5">
|
13 |
+
<div class="row top-buffer">
|
14 |
+
<div class="col-sm-6">
|
15 |
+
<div id="instruction-text" class="text-center">
|
16 |
+
<h4>Instruction:<br>{{ instruction_text }}</h4>
|
17 |
+
</div>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
<div class="row top-buffer">
|
21 |
+
<form method="post" action="{{url_for('index', session_id=session_id)}}">
|
22 |
+
<button type="submit" class="btn btn-success">Back to Search</button>
|
23 |
+
</form>
|
24 |
+
</div>
|
25 |
+
<div class="row top-buffer">
|
26 |
+
<form method="post" action="{{url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=options)}}">
|
27 |
+
<button type="submit" class="btn btn-primary">< Prev</button>
|
28 |
+
</form>
|
29 |
+
</div>
|
30 |
+
<div class="row top-buffer">
|
31 |
+
<div class="col-md-12">
|
32 |
+
<div class="row top-buffer">
|
33 |
+
<div class="col-sm-6" name="bulletpoints">
|
34 |
+
<div class="card card-body">
|
35 |
+
<ul>
|
36 |
+
{% for bulletpoint in product_info.BulletPoints %}
|
37 |
+
<li><p class="product-info"> {{bulletpoint}}</p></li>
|
38 |
+
{% endfor %}
|
39 |
+
</ul>
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
</div>
|
44 |
+
</div>
|
45 |
+
</div>
|
46 |
+
</body>
|
47 |
+
</html>
|
templates/item_page.html
ADDED
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
|
5 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
6 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css ">
|
7 |
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
8 |
+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
|
9 |
+
<link rel="icon" href="data:,">
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="container py-5">
|
13 |
+
<div class="row top-buffer">
|
14 |
+
<div class="col-sm-6">
|
15 |
+
<div id="instruction-text" class="text-center">
|
16 |
+
<h4>Instruction:<br>{{ instruction_text }}</h4>
|
17 |
+
</div>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
<div class="row top-buffer">
|
21 |
+
<form method="post" action="{{url_for('index', session_id=session_id)}}">
|
22 |
+
<button type="submit" class="btn btn-success">Back to Search</button>
|
23 |
+
</form>
|
24 |
+
</div>
|
25 |
+
<div class="row top-buffer">
|
26 |
+
<form method="post" action="{{url_for('search_results', session_id=session_id, keywords=keywords, page=page)}}">
|
27 |
+
<button type="submit" class="btn btn-primary">< Prev</button>
|
28 |
+
</form>
|
29 |
+
</div>
|
30 |
+
<div class="row top-buffer">
|
31 |
+
<div class="col-md-4 mb-4 mb-md-0">
|
32 |
+
<div class="row top-buffer">
|
33 |
+
<img id="product-image" src="{{product_info.MainImage}}" class="item-page-img">
|
34 |
+
</div>
|
35 |
+
{% for option_name, option_contents in product_info.options.items() %}
|
36 |
+
<div class="row top-buffer">
|
37 |
+
<h4>{{ option_name }}</h4>
|
38 |
+
<div class="radio-toolbar">
|
39 |
+
{% for option_content in option_contents %}
|
40 |
+
{% set current_options = options.copy() %}
|
41 |
+
{% set _ = current_options.update({option_name: option_content}) %}
|
42 |
+
{% set url = url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=current_options) %}
|
43 |
+
<input type="radio" id="radio_{{ option_name }}{{ loop.index0 }}" name="{{ option_name }}" value="{{ option_content }}" onclick="window.location.href='{{ url }}';">
|
44 |
+
<label for="radio_{{ option_name }}{{ loop.index0 }}">{{ option_content }}</label>
|
45 |
+
{% endfor %}
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
{% endfor %}
|
49 |
+
</div>
|
50 |
+
<div class="col-md-6">
|
51 |
+
<h2>{{product_info.Title}}</h2>
|
52 |
+
<h4>Price: {{product_info.Price}}</h4>
|
53 |
+
<h4>Rating: {{product_info.Rating}}</h4>
|
54 |
+
<div class="row top-buffer">
|
55 |
+
<div class="col-sm-3" name="description">
|
56 |
+
<form method="post" action="{{ url_for('item_sub_page', session_id=session_id, asin=asin, keywords=keywords, page=page, sub_page='Description', options=options) }}">
|
57 |
+
<button class="btn btn-primary" type="submit">Description</button>
|
58 |
+
</form>
|
59 |
+
</div>
|
60 |
+
<div class="col-sm-3" name="bulletpoints">
|
61 |
+
<form method="post" action="{{ url_for('item_sub_page', session_id=session_id, asin=asin, keywords=keywords, page=page, sub_page='Features', options=options) }}">
|
62 |
+
<button class="btn btn-primary" type="submit">Features</button>
|
63 |
+
</form>
|
64 |
+
</div>
|
65 |
+
<div class="col-sm-3" name="reviews">
|
66 |
+
<form method="post" action="{{ url_for('item_sub_page', session_id=session_id, asin=asin, keywords=keywords, page=page, sub_page='Reviews', options=options) }}">
|
67 |
+
<button class="btn btn-primary" type="submit">Reviews</button>
|
68 |
+
</form>
|
69 |
+
</div>
|
70 |
+
<!--
|
71 |
+
<div class="col-sm-3" name="attributes">
|
72 |
+
<form method="post" action="{{ url_for('item_sub_page', session_id=session_id, asin=asin, keywords=keywords, page=page, sub_page='Attributes', options=options) }}">
|
73 |
+
<button class="btn btn-primary" type="submit">Attributes</button>
|
74 |
+
</form>
|
75 |
+
</div>
|
76 |
+
-->
|
77 |
+
</div>
|
78 |
+
</div>
|
79 |
+
<div class="col-sm-2">
|
80 |
+
<div class="row top-buffer">
|
81 |
+
<form method="post" action="{{url_for('done', session_id=session_id, asin=asin, options=options )}}">
|
82 |
+
<button type="submit" class="btn btn-lg purchase">Buy Now</button>
|
83 |
+
</form>
|
84 |
+
</div>
|
85 |
+
</div>
|
86 |
+
</div>
|
87 |
+
</div>
|
88 |
+
</body>
|
89 |
+
<script>
|
90 |
+
$(document).ready(function() {
|
91 |
+
$('input:radio').each(function() {
|
92 |
+
//console.log($(this).val());
|
93 |
+
let options = JSON.parse(`{{ options | tojson }}`);
|
94 |
+
let optionValues = $.map(options, function(value, key) { return value });
|
95 |
+
//console.log(optionValues);
|
96 |
+
if (optionValues.includes($(this).val())) {
|
97 |
+
$(this).prop('checked', true);
|
98 |
+
|
99 |
+
let option_to_image = JSON.parse(`{{ product_info.option_to_image | tojson }}`);
|
100 |
+
// console.log($(this).val());
|
101 |
+
// console.log(options);
|
102 |
+
// console.log(option_to_image);
|
103 |
+
let image_url = option_to_image[$(this).val()];
|
104 |
+
|
105 |
+
console.log(image_url);
|
106 |
+
if (image_url) {
|
107 |
+
$("#product-image").attr("src", image_url);
|
108 |
+
}
|
109 |
+
}
|
110 |
+
});
|
111 |
+
});
|
112 |
+
</script>
|
113 |
+
</html>
|
templates/results_page.html
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
|
5 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
6 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css ">
|
7 |
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
8 |
+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
|
9 |
+
<link rel="icon" href="data:,">
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="container py-5">
|
13 |
+
<div class="row top-buffer">
|
14 |
+
<div class="col-sm-6">
|
15 |
+
<div id="instruction-text" class="text-center">
|
16 |
+
<h4>Instruction:<br>{{ instruction_text }}</h4>
|
17 |
+
</div>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
<div class="row top-buffer">
|
21 |
+
<form method="post" action="{{ url_for('index', session_id=session_id) }}">
|
22 |
+
<button type="submit" class="btn btn-success">Back to Search</button>
|
23 |
+
</form>
|
24 |
+
</div>
|
25 |
+
<div class="row top-buffer">
|
26 |
+
<div class="col-sm-8">
|
27 |
+
<div class="display-4">
|
28 |
+
<h3>Page {{page}} (Total results: {{total}})</h3>
|
29 |
+
</div>
|
30 |
+
{% if page > 1 %}
|
31 |
+
<div class="col-sm-2">
|
32 |
+
<form method="post" action="{{url_for('search_results', session_id=session_id, keywords=keywords, page=page - 1)}}">
|
33 |
+
<button type="submit" class="btn btn-primary">< Prev</button>
|
34 |
+
</form>
|
35 |
+
</div>
|
36 |
+
<div class="col-sm-2">
|
37 |
+
<form method="post" action="{{url_for('search_results', session_id=session_id, keywords=keywords, page=page + 1)}}">
|
38 |
+
<button type="submit" class="btn btn-primary">Next ></button>
|
39 |
+
</form>
|
40 |
+
</div>
|
41 |
+
{% else %}
|
42 |
+
<div class="col-sm-2">
|
43 |
+
</div>
|
44 |
+
<div class="col-sm-2">
|
45 |
+
<form method="post" action="{{url_for('search_results', session_id=session_id, keywords=keywords, page=page + 1)}}">
|
46 |
+
<button type="submit" class="btn btn-primary">Next ></button>
|
47 |
+
</form>
|
48 |
+
</div>
|
49 |
+
{% endif %}
|
50 |
+
</div>
|
51 |
+
</div>
|
52 |
+
|
53 |
+
<div class="row top-buffer">
|
54 |
+
{% for item in products %}
|
55 |
+
<div class="col-lg-12 mx-auto list-group-item">
|
56 |
+
<div class="col-lg-4">
|
57 |
+
<img src="{{item.MainImage}}" class="result-img">
|
58 |
+
</div>
|
59 |
+
<div class="col-lg-8">
|
60 |
+
<ul class="list-group shadow">
|
61 |
+
<div class="media align-items-lg-center flex-column flex-lg-row p-3">
|
62 |
+
<div class="media-body order-2 order-lg-1 searched-product">
|
63 |
+
{% set item_page_url = url_for('item_page', session_id=session_id, asin=item.asin, keywords=keywords, page=page, options=dict() ) %}
|
64 |
+
<h4 class="mt-0 font-weight-bold mb-2 product-asin"><a class="product-link" href="{{ item_page_url }}">{{item.asin}}</a></h5>
|
65 |
+
<h4 class="mt-0 font-weight-bold mb-2 product-title">{{item.Title}}</h5>
|
66 |
+
<div class="d-flex align-items-center justify-content-between mt-1">
|
67 |
+
<h5 class="font-weight-bold my-2 product-price">{{item.Price}}</h6>
|
68 |
+
</div>
|
69 |
+
<!--
|
70 |
+
<div class="d-flex align-items-center justify-content-between mt-1">
|
71 |
+
<h5 class="font-weight-bold my-2 product-category">{{item.category}}</h5>
|
72 |
+
</div>
|
73 |
+
<div class="d-flex align-items-center justify-content-between mt-1">
|
74 |
+
<h5 class="font-weight-bold my-2 product-query">{{item.query}}</h5>
|
75 |
+
</div>
|
76 |
+
<div class="d-flex align-items-center justify-content-between mt-1">
|
77 |
+
<h5 class="font-weight-bold my-2 product-product_category">{{item.product_category}}</h5>
|
78 |
+
</div>
|
79 |
+
-->
|
80 |
+
</div>
|
81 |
+
</div>
|
82 |
+
</ul>
|
83 |
+
</div>
|
84 |
+
</div>
|
85 |
+
{% endfor %}
|
86 |
+
</div>
|
87 |
+
</div>
|
88 |
+
</body>
|
89 |
+
</html>
|
templates/review_page.html
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
|
5 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
6 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css ">
|
7 |
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
8 |
+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
|
9 |
+
<link rel="icon" href="data:,">
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="container py-5">
|
13 |
+
<div class="row top-buffer">
|
14 |
+
<div class="col-sm-6">
|
15 |
+
<div id="instruction-text" class="text-center">
|
16 |
+
<h4>Instruction:<br>{{ instruction_text }}</h4>
|
17 |
+
</div>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
<div class="row top-buffer">
|
21 |
+
<form method="post" action="{{url_for('index', session_id=session_id)}}">
|
22 |
+
<button type="submit" class="btn btn-success">Back to Search</button>
|
23 |
+
</form>
|
24 |
+
</div>
|
25 |
+
<div class="row top-buffer">
|
26 |
+
<form method="post" action="{{url_for('item_page', session_id=session_id, asin=asin, keywords=keywords, page=page, options=options)}}">
|
27 |
+
<button type="submit" class="btn btn-primary">< Prev</button>
|
28 |
+
</form>
|
29 |
+
</div>
|
30 |
+
<div class="row top-buffer">
|
31 |
+
<div class="col-md-12">
|
32 |
+
<div class="row top-buffer">
|
33 |
+
<div class="col-sm-6" name="reviews">
|
34 |
+
<div class="card card-body">
|
35 |
+
{% for review in product_info.Reviews %}
|
36 |
+
<div class="card">
|
37 |
+
<div class="row text-left">
|
38 |
+
<h4 class="blue-text mt-3">"{{review.title}}"</h4>
|
39 |
+
<p class="text-left">
|
40 |
+
<span>{{review.score}}</span>
|
41 |
+
{% for i in range(review.score | int) %}
|
42 |
+
<span class="fa fa-star star-active"></span>
|
43 |
+
{% endfor %}
|
44 |
+
{% for i in range(5 - review.score | int) %}
|
45 |
+
<span class="fa fa-star star-inactive"></span>
|
46 |
+
{% endfor %}
|
47 |
+
</p>
|
48 |
+
<p class="content">{{review.body}}</p>
|
49 |
+
</div>
|
50 |
+
</div>
|
51 |
+
{% endfor %}
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
</div>
|
56 |
+
</div>
|
57 |
+
</div>
|
58 |
+
</body>
|
59 |
+
</html>
|
templates/search_page.html
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
|
5 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
6 |
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
7 |
+
<link rel="icon" href="data:,">
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<!-- Code reference: https://bootsnipp.com/snippets/m13mN -->
|
11 |
+
<div class="container" style="margin-top: 8%;">
|
12 |
+
<div class="col-md-6 col-md-offset-3">
|
13 |
+
<div class="row">
|
14 |
+
<div id="logo" class="text-center">
|
15 |
+
<h2>Amazon Shopping Game</h2>
|
16 |
+
</div>
|
17 |
+
<div id="instruction-text" class="text-center">
|
18 |
+
<h4>Instruction: <br>{{ instruction_text }}</h4>
|
19 |
+
</div>
|
20 |
+
<form role="form" id="form-buscar" method="post" action="{{url_for('index', session_id=session_id)}}">
|
21 |
+
<div class="form-group">
|
22 |
+
<div class="input-group">
|
23 |
+
<input id="search_input" class="form-control" type="text" name="search_query" placeholder="Search..." required/>
|
24 |
+
<span class="input-group-btn">
|
25 |
+
<button class="btn btn-success" type="submit"><i class="glyphicon glyphicon-search" aria-hidden="true"></i>Search</button>
|
26 |
+
</span>
|
27 |
+
</div>
|
28 |
+
</div>
|
29 |
+
</form>
|
30 |
+
</div>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
</body>
|
34 |
+
</html>
|
webshop_lite.py
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
from flask import render_template_string, Flask
|
4 |
+
from predict_help import Page
|
5 |
+
|
6 |
+
app=Flask(__name__)
|
7 |
+
app.debug=True
|
8 |
+
|
9 |
+
SESSION_ID = "ABC"
|
10 |
+
TEMPLATE_DIR = "templates/"
|
11 |
+
KEYWORDS = ["placeholder (not needed)"] # To Do: Does this matter?
|
12 |
+
QUERY = ""
|
13 |
+
product_map = {}
|
14 |
+
|
15 |
+
def read_html_template(path):
|
16 |
+
with open(path) as f:
|
17 |
+
template = f.read()
|
18 |
+
return template
|
19 |
+
|
20 |
+
@app.route('/', methods=['GET', 'POST'])
|
21 |
+
def index(session_id, **kwargs):
|
22 |
+
print("Hello world")
|
23 |
+
|
24 |
+
@app.route('/', methods=['GET', 'POST'])
|
25 |
+
def search_results(data):
|
26 |
+
path = os.path.join(TEMPLATE_DIR, 'results_page.html')
|
27 |
+
html = render_template_string(
|
28 |
+
read_html_template(path=path),
|
29 |
+
session_id=SESSION_ID,
|
30 |
+
products=data,
|
31 |
+
keywords=KEYWORDS,
|
32 |
+
page=1,
|
33 |
+
total=len(data),
|
34 |
+
instruction_text=QUERY,
|
35 |
+
)
|
36 |
+
return html
|
37 |
+
|
38 |
+
@app.route('/', methods=['GET', 'POST'])
|
39 |
+
def item_page(session_id, asin, keywords, page, options):
|
40 |
+
path = os.path.join(TEMPLATE_DIR, 'item_page.html')
|
41 |
+
html = render_template_string(
|
42 |
+
read_html_template(path=path),
|
43 |
+
session_id=session_id,
|
44 |
+
product_info=product_map[asin],
|
45 |
+
keywords=keywords,
|
46 |
+
page=page,
|
47 |
+
asin=asin,
|
48 |
+
options=options,
|
49 |
+
instruction_text=QUERY
|
50 |
+
)
|
51 |
+
return html
|
52 |
+
|
53 |
+
@app.route('/', methods=['GET', 'POST'])
|
54 |
+
def item_sub_page(session_id, asin, keywords, page, sub_page, options):
|
55 |
+
path = os.path.join(TEMPLATE_DIR, sub_page.value.lower() + "_page.html")
|
56 |
+
html = render_template_string(
|
57 |
+
read_html_template(path),
|
58 |
+
session_id=session_id,
|
59 |
+
product_info=product_map[asin],
|
60 |
+
keywords=keywords,
|
61 |
+
page=page,
|
62 |
+
asin=asin,
|
63 |
+
options=options,
|
64 |
+
instruction_text=QUERY
|
65 |
+
)
|
66 |
+
return html
|
67 |
+
|
68 |
+
@app.route('/', methods=['GET', 'POST'])
|
69 |
+
def done(asin, options, session_id, **kwargs):
|
70 |
+
path = os.path.join(TEMPLATE_DIR, 'done_page.html')
|
71 |
+
html = render_template_string(
|
72 |
+
read_html_template(path),
|
73 |
+
session_id=session_id,
|
74 |
+
reward=1,
|
75 |
+
asin=asin,
|
76 |
+
options=product_map[asin]["options"],
|
77 |
+
reward_info=kwargs.get('reward_info'),
|
78 |
+
goal_attrs=kwargs.get('goal_attrs'),
|
79 |
+
purchased_attrs=kwargs.get('purchased_attrs'),
|
80 |
+
goal=kwargs.get('goal'),
|
81 |
+
mturk_code=kwargs.get('mturk_code'),
|
82 |
+
query=kwargs.get('query'),
|
83 |
+
category=kwargs.get('category'),
|
84 |
+
product_category=kwargs.get('product_category'),
|
85 |
+
)
|
86 |
+
return html
|
87 |
+
|
88 |
+
# Project Dictionary Information onto Fake Amazon
|
89 |
+
def dict_to_fake_html(data, page_type, asin=None, sub_page_type=None, options=None, prod_map={}, query=""):
|
90 |
+
global QUERY, product_map
|
91 |
+
QUERY = query
|
92 |
+
product_map = prod_map
|
93 |
+
with app.app_context(), app.test_request_context():
|
94 |
+
if page_type == Page.RESULTS:
|
95 |
+
return search_results(data)
|
96 |
+
if page_type == Page.ITEM_PAGE:
|
97 |
+
return item_page(SESSION_ID, asin, KEYWORDS, 1, options)
|
98 |
+
if page_type == Page.SUB_PAGE:
|
99 |
+
if sub_page_type is not None:
|
100 |
+
return item_sub_page(SESSION_ID, asin, KEYWORDS, 1, sub_page_type, options)
|
101 |
+
else:
|
102 |
+
raise Exception("Sub page of type", sub_page_type, "unrecognized")
|