Atul1997 commited on
Commit
36ca00a
·
verified ·
1 Parent(s): 57fbe9e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +478 -0
app.py ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hotel Search App — Hugging Face Gradio Application
3
+ Searches for hotels using natural language via SerpApi Google Hotels engine.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ from datetime import datetime, timedelta
9
+
10
+ import gradio as gr
11
+ from dateutil import parser as date_parser
12
+ try:
13
+ from serpapi import GoogleSearch
14
+ except ImportError:
15
+ import serpapi
16
+ GoogleSearch = None
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Constants
20
+ # ---------------------------------------------------------------------------
21
+
22
+ TRAVEL_AGENCY_DOMAINS = [
23
+ "expedia", "booking.com", "hotels.com", "trivago", "kayak",
24
+ "priceline", "orbitz", "travelocity", "agoda", "trip.com",
25
+ "hotwire", "cheaptickets", "lastminute", "edreams", "opodo",
26
+ "wotif", "zuji", "makemytrip", "goibibo", "yatra",
27
+ ]
28
+
29
+ AMENITY_KEYWORDS = [
30
+ "pool", "gym", "fitness", "spa", "wifi", "wi-fi", "parking",
31
+ "breakfast", "restaurant", "bar", "pet-friendly", "pets",
32
+ "non-smoking", "smoke-free", "air conditioning", "laundry",
33
+ "room service", "concierge", "shuttle", "beach", "ocean view",
34
+ "balcony", "kitchen", "kitchenette", "suite", "jacuzzi",
35
+ "hot tub", "business center", "ev charging",
36
+ ]
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Input parser
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ def parse_user_input(text: str) -> dict:
44
+ """Extract structured hotel search parameters from free-form text."""
45
+
46
+ result = {
47
+ "location": "",
48
+ "check_in": "",
49
+ "check_out": "",
50
+ "adults": 2,
51
+ "min_price": None,
52
+ "max_price": None,
53
+ "required_features": [],
54
+ "desired_features": [],
55
+ "avoid_features": [],
56
+ }
57
+
58
+ # --- Dates ----------------------------------------------------------
59
+ date_patterns = [
60
+ r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}",
61
+ r"\d{4}[/-]\d{1,2}[/-]\d{1,2}",
62
+ r"(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}(?:\s*,?\s*\d{4})?",
63
+ ]
64
+
65
+ raw_dates: list[str] = []
66
+ for pat in date_patterns:
67
+ raw_dates.extend(re.findall(pat, text, re.IGNORECASE))
68
+
69
+ parsed_dates: list[datetime] = []
70
+ for raw in raw_dates:
71
+ try:
72
+ parsed_dates.append(date_parser.parse(raw, fuzzy=True))
73
+ except (ValueError, OverflowError):
74
+ continue
75
+
76
+ date_range = re.search(
77
+ r"(\w+\s+\d{1,2})\s*[-–to]+\s*(\d{1,2})\s*,?\s*(\d{4})?", text, re.IGNORECASE
78
+ )
79
+ if date_range and not parsed_dates:
80
+ try:
81
+ month_day_start = date_range.group(1)
82
+ day_end = date_range.group(2)
83
+ year = date_range.group(3) or str(datetime.now().year)
84
+ start = date_parser.parse(f"{month_day_start} {year}")
85
+ month_name = re.match(r"[A-Za-z]+", month_day_start).group()
86
+ end = date_parser.parse(f"{month_name} {day_end} {year}")
87
+ parsed_dates = [start, end]
88
+ except (ValueError, AttributeError):
89
+ pass
90
+
91
+ if len(parsed_dates) >= 2:
92
+ parsed_dates.sort()
93
+ result["check_in"] = parsed_dates[0].strftime("%Y-%m-%d")
94
+ result["check_out"] = parsed_dates[1].strftime("%Y-%m-%d")
95
+ elif len(parsed_dates) == 1:
96
+ result["check_in"] = parsed_dates[0].strftime("%Y-%m-%d")
97
+ result["check_out"] = (parsed_dates[0] + timedelta(days=1)).strftime("%Y-%m-%d")
98
+
99
+ # --- Price ----------------------------------------------------------
100
+ range_match = re.search(
101
+ r"\$\s*(\d+)\s*(?:to|-|–)\s*\$\s*(\d+)", text, re.IGNORECASE
102
+ )
103
+ if range_match:
104
+ result["min_price"] = int(range_match.group(1))
105
+ result["max_price"] = int(range_match.group(2))
106
+ else:
107
+ upper = re.search(
108
+ r"(?:under|below|less than|max(?:imum)?|budget\s*(?:of|is|:)?|up to|cheaper than)\s*\$?\s*(\d+)",
109
+ text, re.IGNORECASE,
110
+ )
111
+ if upper:
112
+ result["max_price"] = int(upper.group(1))
113
+ lower = re.search(
114
+ r"(?:above|over|more than|at least|min(?:imum)?|starting at)\s*\$?\s*(\d+)",
115
+ text, re.IGNORECASE,
116
+ )
117
+ if lower:
118
+ result["min_price"] = int(lower.group(1))
119
+
120
+ # --- Adults ---------------------------------------------------------
121
+ adults_match = re.search(r"(\d+)\s*(?:adults?|guests?|people|persons?|travelers?)", text, re.IGNORECASE)
122
+ if adults_match:
123
+ result["adults"] = max(1, int(adults_match.group(1)))
124
+
125
+ # --- Location -------------------------------------------------------
126
+ loc_patterns = [
127
+ r"(?:hotels?\s+)?(?:in|near|around|close to|next to|at)\s+([A-Z][A-Za-z\s,.']+?)(?:\s+(?:from|for|on|with|under|below|between|that|which|checking|budget|\d|\.|\,\s*(?:I|we|for|from|checking)))",
128
+ r"(?:hotels?\s+)?(?:in|near|around|close to|next to|at)\s+([A-Z][A-Za-z\s,.']+?)$",
129
+ ]
130
+ for pat in loc_patterns:
131
+ m = re.search(pat, text)
132
+ if m:
133
+ loc = m.group(1).strip().rstrip(" .,")
134
+ if len(loc) > 2:
135
+ result["location"] = loc
136
+ break
137
+
138
+ # --- Features -------------------------------------------------------
139
+ req_patterns = [
140
+ r"(?:must have|essential|has to have|mandatory)\s+(.+?)(?:\.|$)",
141
+ r"(?:require[sd]?|need[sd]?|should have)\s+(?!a hotel|a room|a place|an? )(.+?)(?:\.|$)",
142
+ ]
143
+ des_patterns = [
144
+ r"(?:would (?:like|love|prefer)|prefer(?:ably)?|nice to have|ideally|hopefully)\s+(.+?)(?:\.|$)",
145
+ ]
146
+ avoid_pats = [
147
+ r"(?:(?:don'?t|do not) want|avoid|without|no |not interested in|stay away from)\s*(.+?)(?:\.|$)",
148
+ ]
149
+ for pats, key in [(req_patterns, "required_features"), (des_patterns, "desired_features"), (avoid_pats, "avoid_features")]:
150
+ for pat in pats:
151
+ for m in re.finditer(pat, text, re.IGNORECASE):
152
+ features = [f.strip() for f in re.split(r",|(?:\s+and\s+)", m.group(1)) if f.strip()]
153
+ result[key].extend(features)
154
+
155
+ return result
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Hotel link resolver
160
+ # ---------------------------------------------------------------------------
161
+
162
+
163
+ def get_hotel_link(hotel: dict) -> str:
164
+ """Return the best non-travel-agency link for a hotel."""
165
+ for field in ("link", "website"):
166
+ url = hotel.get(field, "")
167
+ if url and not any(agency in url.lower() for agency in TRAVEL_AGENCY_DOMAINS):
168
+ return url
169
+
170
+ prices = hotel.get("prices", [])
171
+ for p in prices:
172
+ source = (p.get("source") or "").lower()
173
+ if "official" in source or hotel.get("name", "").lower().split()[0] in source:
174
+ link = p.get("link", "")
175
+ if link:
176
+ return link
177
+
178
+ name = hotel.get("name", "Hotel")
179
+ return f"https://www.google.com/search?q={name.replace(' ', '+')}+official+site"
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Result formatter
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ def format_hotel_results(properties: list[dict]) -> str:
188
+ """Render hotel results as styled HTML cards."""
189
+
190
+ if not properties:
191
+ return (
192
+ "<div style='text-align:center;padding:40px;'>"
193
+ "<h3>No hotels found matching your criteria.</h3>"
194
+ "<p>Try broadening your search — use a larger area or relax price/feature constraints.</p>"
195
+ "</div>"
196
+ )
197
+
198
+ cards = []
199
+ for idx, hotel in enumerate(properties[:15], 1):
200
+ try:
201
+ name = str(hotel.get("name", "Unknown Hotel") or "Unknown Hotel")
202
+ rating = hotel.get("overall_rating", "N/A")
203
+ reviews = hotel.get("reviews", 0) or 0
204
+ description = str(hotel.get("description", "") or "No description available.")
205
+
206
+ rpn = hotel.get("rate_per_night") or {}
207
+ price = rpn.get("lowest", "N/A") if isinstance(rpn, dict) else "N/A"
208
+
209
+ hotel_class = hotel.get("hotel_class", "") or ""
210
+ amenities = hotel.get("amenities") or []
211
+ images = hotel.get("images") or []
212
+ link = get_hotel_link(hotel)
213
+
214
+ # Thumbnail
215
+ img_url = ""
216
+ if images:
217
+ first = images[0]
218
+ if isinstance(first, dict):
219
+ img_url = first.get("thumbnail") or first.get("original_image", "")
220
+ elif isinstance(first, str):
221
+ img_url = first
222
+ img_html = (
223
+ f"<img src='{img_url}' style='width:200px;height:150px;object-fit:cover;"
224
+ f"border-radius:8px;' onerror=\"this.style.display='none'\">"
225
+ if img_url
226
+ else "<div style='width:200px;height:150px;background:#e0e0e0;border-radius:8px;"
227
+ "display:flex;align-items:center;justify-content:center;color:#999;"
228
+ "font-size:14px;'>No Image</div>"
229
+ )
230
+
231
+ # Stars
232
+ stars = ""
233
+ if hotel_class:
234
+ try:
235
+ n = int(float(str(hotel_class).replace("-star", "").replace("star", "").replace("hotel", "").strip()))
236
+ stars = "&#11088;" * min(n, 5)
237
+ except (ValueError, TypeError):
238
+ stars = str(hotel_class)
239
+
240
+ # Rating colour
241
+ try:
242
+ r = float(str(rating))
243
+ rating_color = "#4CAF50" if r >= 4.0 else "#FF9800" if r >= 3.0 else "#f44336"
244
+ except (ValueError, TypeError):
245
+ rating_color = "#9e9e9e"
246
+
247
+ # Amenities chips (show up to 8)
248
+ amenities_html = " ".join(
249
+ f"<span style='background:#e8f5e9;color:#2e7d32;padding:2px 8px;"
250
+ f"border-radius:12px;font-size:12px;margin:2px;display:inline-block;'>{str(a)}</span>"
251
+ for a in (amenities[:8] if amenities else [])
252
+ )
253
+
254
+ # Escape special HTML characters in user-facing text
255
+ safe_name = name.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
256
+ safe_desc = description[:220].replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
257
+ if len(description) > 220:
258
+ safe_desc += "..."
259
+
260
+ card = f"""
261
+ <div style='border:1px solid #e0e0e0;border-radius:12px;padding:20px;margin:12px 0;
262
+ background:white;box-shadow:0 2px 8px rgba(0,0,0,0.08);display:flex;gap:20px;
263
+ flex-wrap:wrap;'>
264
+ <div style='flex-shrink:0;'>{img_html}</div>
265
+ <div style='flex-grow:1;min-width:260px;'>
266
+ <div style='display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;'>
267
+ <div>
268
+ <h3 style='margin:0 0 4px 0;color:#1a237e;'>#{idx} {safe_name}</h3>
269
+ <span style='color:#666;font-size:14px;'>{stars}</span>
270
+ </div>
271
+ <div style='text-align:right;'>
272
+ <div style='font-size:24px;font-weight:bold;color:#1a237e;'>{price}</div>
273
+ <div style='font-size:12px;color:#666;'>per night</div>
274
+ </div>
275
+ </div>
276
+ <div style='margin:8px 0;'>
277
+ <span style='background:{rating_color};color:white;padding:2px 10px;
278
+ border-radius:4px;font-weight:bold;font-size:14px;'>{rating}</span>
279
+ <span style='color:#666;font-size:13px;margin-left:8px;'>{reviews} reviews</span>
280
+ </div>
281
+ <p style='color:#555;font-size:14px;margin:8px 0;line-height:1.4;'>{safe_desc}</p>
282
+ <div style='margin:8px 0;'>{amenities_html}</div>
283
+ <div style='margin-top:12px;'>
284
+ <a href='{link}' target='_blank' rel='noopener noreferrer'
285
+ style='background:#1a237e;color:white;padding:8px 20px;border-radius:6px;
286
+ text-decoration:none;font-size:14px;font-weight:500;
287
+ display:inline-block;'>Visit Hotel Website</a>
288
+ </div>
289
+ </div>
290
+ </div>"""
291
+ cards.append(card)
292
+ except Exception:
293
+ continue
294
+
295
+ return "<div style='font-family:Arial,sans-serif;'>" + "".join(cards) + "</div>"
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Parsed-parameters summary (shown above results)
300
+ # ---------------------------------------------------------------------------
301
+
302
+
303
+ def parsed_summary_html(parsed: dict, total: int) -> str:
304
+ parts = [f"<strong>Found {total} hotel(s)</strong>"]
305
+ if parsed["location"]:
306
+ parts.append(f"Location: <em>{parsed['location']}</em>")
307
+ if parsed["check_in"]:
308
+ parts.append(f"Check-in: {parsed['check_in']}")
309
+ if parsed["check_out"]:
310
+ parts.append(f"Check-out: {parsed['check_out']}")
311
+ if parsed["adults"] != 2:
312
+ parts.append(f"Guests: {parsed['adults']}")
313
+ if parsed["min_price"] and parsed["max_price"]:
314
+ parts.append(f"Budget: ${parsed['min_price']}–${parsed['max_price']}/night")
315
+ elif parsed["max_price"]:
316
+ parts.append(f"Budget: up to ${parsed['max_price']}/night")
317
+ elif parsed["min_price"]:
318
+ parts.append(f"Budget: ${parsed['min_price']}+/night")
319
+ if parsed["required_features"]:
320
+ parts.append(f"Required: {', '.join(parsed['required_features'])}")
321
+ if parsed["desired_features"]:
322
+ parts.append(f"Preferred: {', '.join(parsed['desired_features'])}")
323
+ if parsed["avoid_features"]:
324
+ parts.append(f"Avoiding: {', '.join(parsed['avoid_features'])}")
325
+
326
+ return (
327
+ "<div style='background:#e3f2fd;padding:15px;border-radius:8px;margin-bottom:15px;"
328
+ "line-height:1.8;'>" + " &nbsp;|&nbsp; ".join(parts) + "</div>"
329
+ )
330
+
331
+
332
+ # ---------------------------------------------------------------------------
333
+ # Main search function
334
+ # ---------------------------------------------------------------------------
335
+
336
+
337
+ def search_hotels(user_input: str) -> str:
338
+ """Parse user input, call SerpApi, and return formatted HTML results."""
339
+
340
+ if not user_input or not user_input.strip():
341
+ return (
342
+ "<div style='text-align:center;padding:40px;'>"
343
+ "<h3>Please enter a hotel description to search.</h3></div>"
344
+ )
345
+
346
+ key = os.environ.get("SERPAPI_KEY", "") or os.environ.get("SERPAPI_API_KEY", "")
347
+ if not key:
348
+ possible_paths = [
349
+ os.path.join(os.getcwd(), "api_key.txt"),
350
+ os.path.join(os.path.expanduser("~"), "Desktop", "Hotel App", "api_key.txt"),
351
+ ]
352
+ for key_file in possible_paths:
353
+ if os.path.exists(key_file):
354
+ with open(key_file) as f:
355
+ key = f.read().strip()
356
+ if key:
357
+ break
358
+ if not key:
359
+ return (
360
+ "<div style='text-align:center;padding:40px;'>"
361
+ "<h3>SerpApi key required</h3>"
362
+ "<p>Set <code>SERPAPI_KEY</code> as a Hugging Face Space secret, "
363
+ "or place your key in <code>api_key.txt</code> for local testing.</p></div>"
364
+ )
365
+
366
+ try:
367
+ parsed = parse_user_input(user_input)
368
+
369
+ query = parsed["location"] if parsed["location"] else user_input.split(".")[0][:120]
370
+
371
+ tomorrow = datetime.now() + timedelta(days=1)
372
+ check_in = parsed["check_in"] or tomorrow.strftime("%Y-%m-%d")
373
+ ci_dt = datetime.strptime(check_in, "%Y-%m-%d")
374
+ if ci_dt.date() < datetime.now().date():
375
+ ci_dt = tomorrow
376
+ check_in = ci_dt.strftime("%Y-%m-%d")
377
+ check_out = parsed["check_out"] or (ci_dt + timedelta(days=1)).strftime("%Y-%m-%d")
378
+
379
+ params: dict = {
380
+ "engine": "google_hotels",
381
+ "q": query,
382
+ "check_in_date": check_in,
383
+ "check_out_date": check_out,
384
+ "adults": parsed["adults"],
385
+ "currency": "USD",
386
+ "gl": "us",
387
+ "hl": "en",
388
+ "api_key": key,
389
+ }
390
+ if parsed["min_price"]:
391
+ params["min_price"] = parsed["min_price"]
392
+ if parsed["max_price"]:
393
+ params["max_price"] = parsed["max_price"]
394
+
395
+ if GoogleSearch is not None:
396
+ search = GoogleSearch(params)
397
+ results = search.get_dict()
398
+ else:
399
+ results = serpapi.search(params)
400
+
401
+ if "error" in results:
402
+ return (
403
+ f"<div style='text-align:center;padding:40px;'>"
404
+ f"<h3>SerpApi Error</h3><p>{results['error']}</p></div>"
405
+ )
406
+
407
+ properties = results.get("properties", [])
408
+
409
+ filtered = [
410
+ h for h in properties
411
+ if not any(agency in get_hotel_link(h).lower() for agency in TRAVEL_AGENCY_DOMAINS)
412
+ ]
413
+ if len(filtered) < 3 and properties:
414
+ filtered = properties
415
+
416
+ summary = parsed_summary_html(parsed, len(filtered))
417
+ return summary + format_hotel_results(filtered)
418
+
419
+ except Exception as exc:
420
+ return (
421
+ f"<div style='text-align:center;padding:40px;'>"
422
+ f"<h3>Something went wrong</h3>"
423
+ f"<p>{exc}</p>"
424
+ f"<p>Double-check your API key and try again.</p></div>"
425
+ )
426
+
427
+
428
+ # ---------------------------------------------------------------------------
429
+ # Gradio UI
430
+ # ---------------------------------------------------------------------------
431
+
432
+ with gr.Blocks(title="AI Hotel Search") as app:
433
+ gr.Markdown(
434
+ "# AI Hotel Search\n"
435
+ "### Find your perfect hotel using natural language\n"
436
+ "Describe what you are looking for in plain English — include location, dates, "
437
+ "budget, and any features you want or want to avoid."
438
+ )
439
+
440
+ user_input = gr.Textbox(
441
+ label="Describe Your Ideal Hotel",
442
+ placeholder=(
443
+ "Example: I'm looking for a quiet, non-smoking hotel in downtown "
444
+ "Austin, TX from March 15 to March 18, 2026. Budget under $200/night. "
445
+ "Must have free parking and wifi. Would prefer a pool. Avoid hotels "
446
+ "near highways."
447
+ ),
448
+ lines=5,
449
+ )
450
+ search_btn = gr.Button("Search Hotels", variant="primary", size="lg")
451
+
452
+ with gr.Accordion("Search Tips & Examples", open=False):
453
+ gr.Markdown(
454
+ "**Good search examples:**\n\n"
455
+ "- *\"Find me a hotel in San Francisco near Fisherman's Wharf, checking in "
456
+ "April 5 and out April 8, 2026. Budget under $250/night. Must be non-smoking "
457
+ "with free wifi. Would like ocean view.\"*\n"
458
+ "- *\"Pet-friendly hotel in Nashville, TN for 2 adults from May 10-12, 2026. "
459
+ "Price range $100-$180. Prefer boutique hotels. Avoid large chain hotels.\"*\n"
460
+ "- *\"Luxury hotel in Miami Beach, March 20-23, 2026. $300-$500/night. Must "
461
+ "have spa and pool. Prefer beachfront.\"*\n\n"
462
+ "**Tips for best results:**\n\n"
463
+ "- Always include a **location** (city, state, or landmark).\n"
464
+ "- Specify **dates** in common formats (MM/DD/YYYY, Month Day Year, etc.).\n"
465
+ "- Mention your **budget** with dollar amounts.\n"
466
+ "- Distinguish between **must-have** and **nice-to-have** features.\n"
467
+ "- State features to **avoid** explicitly.\n\n"
468
+ "**Bad example (too vague):**\n\n"
469
+ "- *\"Find me a nice hotel somewhere warm.\"* — no location, no dates, no budget."
470
+ )
471
+
472
+ results_output = gr.HTML(label="Search Results")
473
+
474
+ search_btn.click(fn=search_hotels, inputs=[user_input], outputs=results_output)
475
+ user_input.submit(fn=search_hotels, inputs=[user_input], outputs=results_output)
476
+
477
+ if __name__ == "__main__":
478
+ app.launch()