# app.py # This application provides a user interface for HKUST students to browse, search, # and find accommodations in different neighborhoods of Hong Kong. It features an interactive map # visualization, listing cards with pricing information, traffic-based discounts, and smart search # functionality to match user preferences with available properties. # Key features: # - Interactive map displaying BNB listings with location markers # - Neighborhood-based filtering of available accommodations # - Smart search system that highlights matching terms in descriptions and reviews # - Traffic-based discount system promoting eco-friendly housing options # - Detailed view of property reviews with highlighted search terms # - Responsive pagination for browsing through large sets of listings # - Loading animations and informative UI elements for better user experience # The application uses Folium for map visualization, Streamlit for the web interface # Author: Gordon Li (20317033) # Company : HKUST Sustainability # Date: March 2025 import os import re import streamlit as st import streamlit.components.v1 as components from html import escape from streamlit_folium import st_folium import math from visualiser.hkust_bnb_visualiser import HKUSTBNBVisualiser from huggingface_hub import login from constant.hkust_bnb_constant import ( SIDEBAR_HEADER, SIDEBAR_DIVIDER, TRAFFIC_EXPLANATION, SEARCH_EXPLANATION, REVIEW_CARD_TEMPLATE, LISTINGS_COUNT_INFO, LISTING_CARD_TEMPLATE, PRICE_DISPLAY_WITH_DISCOUNT, PRICE_DISPLAY_NORMAL, RELEVANCE_INFO_LISTING, LOTTIE_HTML ) # Loads CSS styles from a file and applies them to the Streamlit application. # Parameters: # css_file: Path to the CSS file to be loaded def load_css(css_file): with open(css_file) as f: st.markdown(f'', unsafe_allow_html=True) # Highlights search terms within text by wrapping them in a span with highlight class. # Parameters: # text: The original text to process # search_query: The search terms to highlight within the text # Returns: # Text with highlighted search terms def highlight_search_terms(text, search_query): if not search_query: return text highlighted_text = text search_terms = search_query.lower().split() for term in search_terms: if term.strip(): pattern = f'(?i)\\b{term}\\b' replacement = f'{term}' highlighted_text = re.sub(pattern, replacement, highlighted_text) return highlighted_text # Renders a loading animation using Lottie animation in HTML format. def render_lottie_loading_animation(): components.html(LOTTIE_HTML, height=750) # Renders a dialog containing reviews for the currently selected listing. # Displays reviewer name, review date, and comments with search terms highlighted. def render_review_dialog(): with st.container(): col_title = st.columns([5, 1]) with col_title[0]: st.markdown(f"### Reviews for {st.session_state.current_review_listing_name}") reviews = st.session_state.visualizer.get_listing_reviews(st.session_state.current_review_listing) if reviews: for review in reviews: try: review_date, reviewer_name, comments = review highlighted_comments = highlight_search_terms( str(comments), st.session_state.search_query ) st.markdown( REVIEW_CARD_TEMPLATE.format( reviewer_name=escape(str(reviewer_name)), review_date=escape(str(review_date)), highlighted_comments=highlighted_comments ), unsafe_allow_html=True ) except Exception as e: st.error(f"Error displaying review: {str(e)}") else: st.info("No reviews available for this listing.") # Initializes the session state with default values for various application parameters. # Sets up the visualizer and loads required resources for the application. def initialize_session_state(): default_states = { 'center_lat': None, 'center_lng': None, 'selected_id': None, 'current_page': 1, 'previous_neighborhood': None, 'items_per_page': 3, 'search_query': "", 'tokenizer_loaded': False, 'show_review_dialog': False, 'current_review_listing': None, 'current_review_listing_name': None, 'show_traffic_explanation': False, 'show_search_explanation': False, 'listings_limit': 10, 'loading_complete': False, } for key, default_value in default_states.items(): if key not in st.session_state: st.session_state[key] = default_value if 'visualizer' not in st.session_state: st.session_state.loading_complete = False st.session_state.visualizer = HKUSTBNBVisualiser() st.session_state.tokenizer_loaded = True st.session_state.loading_complete = True # Main function that sets up the Streamlit application interface. # Handles page configuration, sidebar setup, map rendering, listing display, # pagination, and user interactions with the application elements. def main(): st.set_page_config( layout="wide", page_title="HKUST BNB+ | Platform for BNB Matching for HKUST PG Student", initial_sidebar_state="expanded" ) load_css('css/style.css') if 'loading_complete' not in st.session_state or not st.session_state.loading_complete: render_lottie_loading_animation() initialize_session_state() st.rerun() visualizer = st.session_state.visualizer if visualizer is None or not hasattr(visualizer, 'neighborhoods'): st.error("Error initializing the application. Please refresh the page.") return if st.session_state.show_traffic_explanation: with st.expander("📊 Traffic-Based Discount System", expanded=True): st.markdown(TRAFFIC_EXPLANATION) if st.button("Close", key="close_traffic_btn"): st.session_state.show_traffic_explanation = False st.rerun() if st.session_state.show_search_explanation: with st.expander("🔍 Smart Search System", expanded=True): st.markdown(SEARCH_EXPLANATION) if st.button("Close", key="close_search_btn"): st.session_state.show_search_explanation = False st.rerun() with st.sidebar: st.image("css/img.png", width=50) st.markdown(SIDEBAR_HEADER, unsafe_allow_html=True) search_query = st.text_input( "🔍 Search listings", value=st.session_state.search_query, placeholder="Try: 'cozy , quiet '" ) if search_query != st.session_state.search_query: st.session_state.search_query = search_query st.session_state.current_page = 1 st.session_state.show_review_dialog = False st.markdown(SIDEBAR_DIVIDER, unsafe_allow_html=True) neighborhood = st.selectbox( "Select Neighborhood", options=visualizer.neighborhoods, index=visualizer.neighborhoods.index("Kowloon City") if "Kowloon City" in visualizer.neighborhoods else 0 ) listings_limit = st.selectbox( "Number of listings to show", options=[10, 20, 30, 40, 50], index=0, help="Select how many listings to display for this neighborhood" ) if listings_limit != st.session_state.listings_limit: st.session_state.listings_limit = listings_limit st.session_state.current_page = 1 st.session_state.show_review_dialog = False st.markdown(SIDEBAR_DIVIDER, unsafe_allow_html=True) st.markdown("### 💡 Help & Information") col1, col2 = st.columns(2) with col1: if st.button("Green Discount", key="traffic_info_btn"): st.session_state.show_traffic_explanation = True st.rerun() with col2: if st.button("Smart Search", key="search_info_btn"): st.session_state.show_search_explanation = True st.rerun() if st.button("Reset All", key="reset_btn"): for key in ['center_lat', 'center_lng', 'selected_id', 'search_query', 'show_review_dialog', 'show_traffic_explanation', 'show_search_explanation']: st.session_state[key] = None if key in ['center_lat', 'center_lng', 'selected_id'] else False if 'show_' in key else "" st.session_state.current_page = 1 st.session_state.listings_limit = 10 st.rerun() m, df = visualizer.create_map_and_data( neighborhood, True, st.session_state.center_lat, st.session_state.center_lng, st.session_state.selected_id, st.session_state.search_query, st.session_state.current_page, st.session_state.items_per_page, st.session_state.listings_limit ) if st.session_state.previous_neighborhood != neighborhood: st.session_state.current_page = 1 if not df.empty: st.session_state.selected_id = df.iloc[0]['id'] st.session_state.center_lat = df.iloc[0]['latitude'] st.session_state.center_lng = df.iloc[0]['longitude'] st.session_state.previous_neighborhood = neighborhood st.session_state.show_review_dialog = False st.rerun() if m is None: st.error("No data available for the selected neighborhood") return col1, col2 = st.columns([7, 3]) with col1: st.markdown('