# Add SQLite fix for non-Windows platforms import platform if platform.system() != "Windows": # Skip on Windows __import__('pysqlite3') import sys sys.modules['sqlite3'] = sys.modules.pop('pysqlite3') import streamlit as st from datetime import datetime import time import plotly.express as px import pandas as pd from src.utils.config import * from src.utils.helper import * from src.utils.styles import * from src.db_agent import VarturRealEstateSearch from src.analytics import RealEstateAnalytics class EnhancedStreamlitApp: def __init__(self): self.config = AppConfig() init_folders() # Initialize the search system only once and store it in session state if 'search_system' not in st.session_state: st.session_state.search_system = VarturRealEstateSearch(persist_directory=DB_FOLDER) # Assign the search system from session state self.search_system = st.session_state.search_system def render_login(self): st.markdown("""

🏢 VARTUR ® Real Estate Brokerage

Advanced Property Search Engine

""", unsafe_allow_html=True) with st.form("login_form", clear_on_submit=True): st.markdown("

Welcome Back

", unsafe_allow_html=True) password = st.text_input("Password", type="password") if st.form_submit_button("Login", use_container_width=True): if self.config.verify_password(password): st.session_state.authenticated = True st.rerun() else: st.error("❌ Invalid password") def render_dashboard(self): stats = self.config.get_stats() total_properties = self.search_system.collection.count() col1, col2, col3 = st.columns(3) with col1: st.markdown(f"""

📊 Indexed Files

{stats["total_files"]}

""", unsafe_allow_html=True) with col2: st.markdown(f"""

🏢 Total Properties

{total_properties}

""", unsafe_allow_html=True) with col3: st.markdown(f"""

🔄 Last Updated

{datetime.fromisoformat(stats["last_updated"]).astimezone(DUBAI_TZ).strftime("%d %b, %H:%M") if stats["last_updated"] else "Never"}

(Dubai TZ)
""", unsafe_allow_html=True) def render_search(self): """Render the search interface with filters""" st.markdown("""

🔍 Search

search with your custom needs

""", unsafe_allow_html=True) # reference information box with st.expander("📋 Search Functionality Guidelines", expanded=False): st.markdown(""" ##### 📌 Search System Overview The search functionality works in two primary ways: **1. 🗣️ Natural Language Search Box** - Located at the top of the page - Useful for broad, descriptive searches - Examples: "sea view apartments", "large penthouses" - Note: As this searches structured data, results may vary **2. ⚡ Filter System** - More precise filtering options - Exact matching on specific criteria - Filters applied after text search - More reliable for specific requirements **💡 Best Practices** - For exact requirements: Use filters - For exploration: Use text search - For specific items with context: Combine both - Filters always take precedence over text search **🔗 Power of Combinations:** - Combine filters for precise results: * "Penthouse" search + Sea view + Price 2-5M + High floor * 4BR + Burj view + EMAAR + 2000-3000 sq.ft * Downtown area + 2BR + Floor 10+ + Price up to 2M - Mix and match any number of filters to narrow down exactly what you want - The more filters you combine, the more specific your results """) # Search Configuration config_col1, config_col2 = st.columns([3, 1]) with config_col1: query = st.text_input( "🔍 Search Properties", value="", placeholder="Type Naturally: e.g. 4 bedroom with sea view, 2br downtown, penthouse burj view", help="Search is flexible - Try different ways of describing what you want", ) with config_col2: n_results = st.number_input( "📊 Results Limit", min_value=1, max_value=1000, value=10, help="Maximum number of results" ) # Organize filters in tabs filter_tabs = st.tabs(["📝 Basic Filters", "⚙️ Advanced Filters"]) with filter_tabs[0]: col1, col2 = st.columns(2) # Column 1: Price and Unit Type with col1: price_col1, price_col2 = st.columns(2) with price_col1: min_price = st.number_input( "💰 Min Price (M)", value=0.0, step=0.1, format="%.1f", help="Min Price in millions of AED", ) with price_col2: max_price = st.number_input( "💰 Max Price (M)", value=0.0, step=0.1, format="%.1f", help="Max Price in millions of AED", ) unit_type = st.text_input( "🏠 Unit Type", placeholder="e.g. 4BR, 2 bed, studio, TH...", help="Flexible input: '4BR', '2 bed', 'studio', etc." ) # Column 2: Developer and View with col2: developer = st.text_input( "🏗️ Developer", placeholder="Type developer name", help="Case-insensitive search" ) view = st.text_input( "👁️ View", placeholder="e.g. sea, burj...", help="Type any part of the view" ) with filter_tabs[1]: col1, col2 = st.columns(2) # Column 1: Area Range with col1: area_col1, area_col2 = st.columns(2) with area_col1: min_area = st.number_input( "📏 Min Area (sq.ft)", value=0, step=100, help="Min Area in square feet", ) with area_col2: max_area = st.number_input( "📏 Max Area (sq.ft)", value=0, step=100, help="Max Area in square feet", ) # Column 2: Floor Range with col2: floor_col1, floor_col2 = st.columns(2) with floor_col1: min_floor = st.text_input( "🔢 Min Floor", placeholder="e.g., G, 1, 2...", help="Min Floor Number" ) with floor_col2: max_floor = st.text_input( "🔢 Max Floor", placeholder="e.g., 50, PH...", help="Max Floor Number" ) # Search execution if st.button("🔍 Search", use_container_width=True): filters = { 'query': query.strip() if query else None, 'min_price': min_price * 1_000_000 if min_price > 0 else None, 'max_price': max_price * 1_000_000 if max_price > 0 else None, 'min_area': min_area if min_area > 0 else None, 'max_area': max_area if max_area > 0 else None, 'min_floor': min_floor.strip() if min_floor else None, 'max_floor': max_floor.strip() if max_floor else None, 'developer': developer.strip() if developer else None, 'view': view.strip() if view else None, 'unit_type': unit_type.strip() if unit_type else None, 'n_results': n_results } # Execute search with all parameters self.execute_search(filters) def execute_search(self, filters): """Execute search with post-filtering and results display""" try: active_filters = {k: v for k, v in filters.items() if v is not None and k != 'n_results'} if not active_filters: st.warning("Please enter at least one search criterion") return with st.spinner("Searching..."): results = self.search_system.filter_search(**filters) if not results: st.info("No properties found matching your criteria") return # Create results DataFrame df = pd.DataFrame([{ 'Match': f"{r['similarity']:.1%}", 'Unit': r['metadata']['unit_code'], 'Type': r['metadata']['unit_type'].title(), 'Floor': r['metadata']['floor'], 'View': r['metadata']['view'].title(), 'Area': f"{float(str(r['metadata']['total_area']).replace(',', '')):,.0f}", 'Price': f"{float(str(r['metadata']['price']).replace(',', '')):,.2f}", 'Developer': r['metadata']['developer'].title() } for r in results]) # Display results # used_filters = [k for k, v in active_filters.items() if v is not None] st.success(f"Found {len(results)} properties matching your criteria") # Results tabs tabs = st.tabs(["Results", "Analytics"]) with tabs[0]: st.dataframe( df, use_container_width=True, height=min(400, 35 + 35 * len(df)) ) with tabs[1]: col1, col2 = st.columns(2) with col1: price_data = pd.to_numeric( df['Price'].str.replace(',', ''), errors='coerce' ) fig_price = px.histogram( price_data, title="Price Distribution", labels={'value': 'Price (AED)', 'count': 'Properties'}, color_discrete_sequence=['#4a90e2'] ) st.plotly_chart(fig_price, use_container_width=True) with col2: area_data = pd.to_numeric( df['Area'].str.replace(',', ''), errors='coerce' ) fig_area = px.histogram( area_data, title="Area Distribution", labels={'value': 'Area (sq.ft)', 'count': 'Properties'}, color_discrete_sequence=['#2ecc71'] ) st.plotly_chart(fig_area, use_container_width=True) # Export option st.download_button( "📥 Export Results", df.to_csv(index=False), f"property_results_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", "text/csv", use_container_width=True ) except Exception as e: st.error(f"Search error: {str(e)}") def render_file_management(self): st.markdown("""

📂 File Management

upload and index your files

""", unsafe_allow_html=True) # Add this right after your file management header with st.expander("📋 File Upload Guidelines", expanded=False): st.markdown(""" ##### 📌 Required Data Structure Your CSV file must follow this structure: """) # Create sample DataFrame with realistic property data sample_df = pd.DataFrame({ 'UnitCode': ['UNIT001', 'UNIT002', 'UNIT003'], 'UnitType': ['4 Bedroom', '2 BR', 'Studio'], 'Floor': ['15', 'G', 'PH'], 'Developer': ['EMAAR', 'DAMAC', 'Select Group'], 'TotalArea': [2500, 1200, 550], 'AskingPrice': [3500000, 1800000, 950000], 'View': ['Sea View', 'Burj View', 'City View'] }) # Show sample data st.dataframe(sample_df) st.markdown(""" ##### ⚙️ Column Specifications - **UnitCode**: Unique identifier for each property - **UnitType**: Property type (e.g., 4 Bedroom, 2 BR, Studio) - **Floor**: Floor level (e.g., G, 1, 15, PH) - **Developer**: Developer name - **TotalArea**: Area in square feet (numeric) - **AskingPrice**: Price in AED (numeric) - **View**: View description ##### 📝 Notes: - All columns are required - Use consistent format for unit types - Numeric values should not contain currency symbols - Floor can be text (G = Ground, PH = Penthouse, etc.) """) # Download sample st.download_button( "📥 Download Sample Template", sample_df.to_csv(index=False), "property_data_template.csv", "text/csv", help="Download a sample CSV template with correct structure" ) # File Upload Section with Preview col1, col2 = st.columns([2, 3]) with col1: st.markdown("#### Upload New File") uploaded_file = st.file_uploader("Choose CSV file", type=['csv']) if uploaded_file: try: df = pd.read_csv(uploaded_file) file_hash = get_file_hash(uploaded_file.getvalue()) # File validation is_valid, missing_cols = validate_csv_structure(df) if not is_valid: st.error(f"Missing columns: {', '.join(missing_cols)}") return # Show data preview st.markdown("##### Data Preview") st.dataframe( df.head(5), use_container_width=True, height=200 ) # File stats st.markdown("##### File Statistics") st.markdown(""" """, unsafe_allow_html=True) # Displaying metrics with custom HTML and smaller font size stats_col1, stats_col2 = st.columns(2) with stats_col1: st.markdown(f"
Total Rows: {len(df)}
", unsafe_allow_html=True) with stats_col2: st.markdown(f"
File Size: {uploaded_file.size/1024:.1f} KB
", unsafe_allow_html=True) # Index button with status if st.button("📥 Index File", use_container_width=True): with st.spinner("Indexing..."): file_path = save_uploaded_file(uploaded_file) if file_path: result = self.search_system.load_data(file_path, reset_collection=True) if result["status"] == "success": self.config.update_indexed_files(uploaded_file.name, file_hash) st.success(f"✅ Indexed {result['count']} properties") st.balloons() else: st.error(f"❌ Failed: {result['message']}") except Exception as e: st.error(f"Error reading file: {str(e)}") with col2: st.markdown("#### Indexed Files") if self.config.config["indexed_files"]: for filename, info in self.config.config["indexed_files"].items(): with st.container(): st.markdown( f"""
{filename}

Indexed: {datetime.fromisoformat(info['timestamp']).astimezone(DUBAI_TZ).strftime('%d %b, %H:%M')}

""", unsafe_allow_html=True ) col1, col2, col3 = st.columns([1,1,1]) with col1: if st.button("👁️ Preview", key=f"preview_{filename}"): try: df = pd.read_csv(os.path.join(UPLOAD_FOLDER, filename)) st.dataframe(df.head()) except Exception as e: st.error(f"Error previewing file: {str(e)}") with col2: if st.button("📥 Export", key=f"export_{filename}"): try: df = pd.read_csv(os.path.join(UPLOAD_FOLDER, filename)) st.download_button( "Download CSV", df.to_csv(index=False), filename, "text/csv" ) except Exception as e: st.error(f"Error exporting file: {str(e)}") with col3: if st.button("🗑️ Remove", key=f"remove_{filename}"): self.remove_file(filename) st.rerun() else: st.info("No files indexed yet") def remove_file(self, filename): try: # Remove from database self.search_system.delete_properties(files=[filename]) # Remove from config self.config.remove_indexed_file(filename) # Remove physical file file_path = os.path.join(UPLOAD_FOLDER, filename) if os.path.exists(file_path): os.remove(file_path) st.success(f"✅ Removed {filename}") time.sleep(1) # Give time for success message except Exception as e: st.error(f"❌ Error removing file: {str(e)}") def render_analytics(self): """Render the analytics dashboard with enhanced styling and error handling.""" try: st.markdown("""

📊 Analytics

market analysis and insights

""", unsafe_allow_html=True) # reference with st.expander("📋 Analytics Guidelines", expanded=False): st.markdown(""" ##### 📌 Analytics Overview Our analytics dashboard offers five key analysis areas: **1. 📈 Price Analysis** - Price distribution analysis - Price per square foot trends - Price segmentation across properties - Distribution patterns and outliers **2. 🏢 Property Distribution** - Property type breakdown - Average price by property type - Type-wise market share - Detailed property metrics **3. 👥 Developer Insights** - Developer market share - Average price by developer - Developer performance metrics - Portfolio size analysis **4. 📍 Location Analysis** - View distribution analysis - Price premium by view - Location-based pricing - View preference patterns **5. 📊 Market Overview** - Time-based price trends - Property inventory trends - Market momentum analysis - Monthly price variations ##### 💡 Interactive Features Each visualization offers: - Hover for detailed information - Click on legends to filter - Download charts as images - Dynamic data filtering """) # Initialize analytics analytics = RealEstateAnalytics(self.search_system) # Get data with error handling with st.spinner("Loading data..."): df = analytics.get_all_properties() if df.empty: st.warning("No data available for analysis. Please ensure properties are indexed.") return # Summary Statistics Cards with consistent styling col1, col2, col3, col4 = st.columns(4) with col1: total_value = df['AskingPrice'].sum() st.metric( "Portfolio Value", f"{total_value/1e9:.2f}B AED", f"{len(df):,} properties" ) with col2: avg_price = df['AskingPrice'].mean() st.metric( "Average Price", f"{avg_price/1e6:.2f}M AED", f"±{df['AskingPrice'].std()/1e6:.1f}M" ) with col3: avg_area = df['TotalArea'].mean() st.metric( "Average Area", f"{avg_area:,.0f} sq.ft", f"±{df['TotalArea'].std():,.0f}" ) with col4: st.metric( "Total Units", f"{len(df):,}", f"{df['UnitType'].nunique()} types" ) # Create analysis tabs analysis_tabs = st.tabs([ "📈 Price Analysis", "🏢 Property Distribution", "👥 Developer Insights", "📍 Location Analysis", "📊 Market Overview" ]) # Price Analysis Tab with analysis_tabs[0]: st.subheader("Price Distribution Analysis") col1, col2 = st.columns(2) with col1: # Price histogram fig_price = px.histogram( df, x='AskingPrice', nbins=30, title='Property Price Distribution', labels={'AskingPrice': 'Price (AED)'}, color_discrete_sequence=['#4a90e2'] ) fig_price.update_layout( showlegend=False, plot_bgcolor='white', paper_bgcolor='white' ) st.plotly_chart(fig_price, use_container_width=True) with col2: # Price per sqft analysis fig_psf = px.histogram( df, x='PricePerSqft', nbins=30, title='Price per Sqft Distribution', labels={'PricePerSqft': 'Price per Sqft (AED)'}, color_discrete_sequence=['#2ecc71'] ) fig_psf.update_layout( showlegend=False, plot_bgcolor='white', paper_bgcolor='white' ) st.plotly_chart(fig_psf, use_container_width=True) # Property Distribution Tab with analysis_tabs[1]: st.subheader("Property Type Analysis") col1, col2 = st.columns(2) with col1: # Property type distribution type_dist = df['UnitType'].value_counts() fig_types = px.pie( values=type_dist.values, names=type_dist.index, title='Property Type Distribution', hole=0.4, color_discrete_sequence=px.colors.sequential.Blues_r ) st.plotly_chart(fig_types, use_container_width=True) with col2: # Average price by type avg_price_type = df.groupby('UnitType')['AskingPrice'].mean().sort_values() fig_price_type = px.bar( x=avg_price_type.values, y=avg_price_type.index, orientation='h', title='Average Price by Property Type', labels={'x': 'Average Price (AED)', 'y': 'Property Type'}, color_discrete_sequence=['#4a90e2'] ) st.plotly_chart(fig_price_type, use_container_width=True) # Developer Insights Tab with analysis_tabs[2]: st.subheader("Developer Analysis") col1, col2 = st.columns(2) with col1: # Developer market share dev_share = df.groupby('Developer')['UnitCode'].count().sort_values() fig_dev = px.bar( x=dev_share.values, y=dev_share.index, orientation='h', title='Developer Market Share', labels={'x': 'Number of Properties', 'y': 'Developer'}, color_discrete_sequence=['#4a90e2'] ) st.plotly_chart(fig_dev, use_container_width=True) with col2: # Average price by developer avg_price_dev = df.groupby('Developer')['PricePerSqft'].mean().sort_values() fig_dev_price = px.bar( x=avg_price_dev.values, y=avg_price_dev.index, orientation='h', title='Average Price/Sqft by Developer', labels={'x': 'Average Price/Sqft (AED)', 'y': 'Developer'}, color_discrete_sequence=['#2ecc71'] ) st.plotly_chart(fig_dev_price, use_container_width=True) # Location Analysis Tab with analysis_tabs[3]: st.subheader("View and Location Analysis") col1, col2 = st.columns(2) with col1: # View distribution view_dist = df['View'].value_counts() fig_view = px.pie( values=view_dist.values, names=view_dist.index, title='View Distribution', hole=0.4, color_discrete_sequence=px.colors.sequential.Blues_r ) st.plotly_chart(fig_view, use_container_width=True) with col2: # Price premium by view avg_price_view = df.groupby('View')['PricePerSqft'].mean().sort_values() fig_view_price = px.bar( x=avg_price_view.values, y=avg_price_view.index, orientation='h', title='Average Price/Sqft by View', labels={'x': 'Average Price/Sqft (AED)', 'y': 'View'}, color_discrete_sequence=['#4a90e2'] ) st.plotly_chart(fig_view_price, use_container_width=True) # Market Overview Tab with analysis_tabs[4]: st.subheader("Market Overview") # Time series analysis df['IndexedDate'] = pd.to_datetime(df['IndexedDate']) df['Month'] = df['IndexedDate'].dt.to_period('M') monthly_trends = df.groupby('Month').agg({ 'AskingPrice': 'mean', 'UnitCode': 'count' }).reset_index() monthly_trends['Month'] = monthly_trends['Month'].dt.to_timestamp() # Price trends fig_trends = px.line( monthly_trends, x='Month', y='AskingPrice', title='Average Property Price Trend', labels={'AskingPrice': 'Average Price (AED)', 'Month': 'Month'}, color_discrete_sequence=['#4a90e2'] ) st.plotly_chart(fig_trends, use_container_width=True) # Inventory trends fig_inventory = px.line( monthly_trends, x='Month', y='UnitCode', title='Property Inventory Trend', labels={'UnitCode': 'Number of Properties', 'Month': 'Month'}, color_discrete_sequence=['#2ecc71'] ) st.plotly_chart(fig_inventory, use_container_width=True) except Exception as e: st.error(f"Error in analytics dashboard: {str(e)}") st.info("Please check your data and try refreshing the page.") def run(self): """Main application entry point with authentication and navigation.""" st.set_page_config( page_title="VARTUR ® Real Estate Brokerage Search Engine", page_icon="🏢", layout="wide" ) st.markdown(CUSTOM_CSS, unsafe_allow_html=True) if 'authenticated' not in st.session_state: st.session_state.authenticated = False if not st.session_state.authenticated: self.render_login() return # Top navigation bar col1, col2 = st.columns([6,1]) with col1: st.title("🏢 Vartur Dashboard") with col2: if st.button("🚪 Logout", use_container_width=True): st.session_state.authenticated = False st.rerun() self.render_dashboard() # Main navigation tabs tabs = st.tabs(["🔍 Search", "📂 File Management", "📊 Analytics"]) with tabs[0]: self.render_search() with tabs[1]: self.render_file_management() with tabs[2]: self.render_analytics() if __name__ == "__main__": app = EnhancedStreamlitApp() app.run()