| |
|
|
| import streamlit as st |
| import pandas as pd |
|
|
| from prediction import recommend_phones |
| from eda import render_eda, get_dataset_info |
|
|
| |
| |
| |
|
|
| st.set_page_config( |
| page_title="PhoneFinder", |
| page_icon="📱", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| |
| |
| |
|
|
| st.markdown(""" |
| <style> |
| |
| /* Hide Streamlit header */ |
| header {visibility: hidden;} |
| |
| /* Hide ALL collapse/toggle/arrow buttons everywhere */ |
| [data-testid="collapsedControl"], |
| [data-testid="baseButton-headerNoPadding"], |
| button[kind="header"], |
| .st-emotion-cache-1egp75f, |
| .st-emotion-cache-1pbsqtx, |
| [aria-label="Close sidebar"], |
| [aria-label="Open sidebar"], |
| [aria-label="Collapse sidebar"] { |
| display: none !important; |
| visibility: hidden !important; |
| pointer-events: none !important; |
| width: 0 !important; |
| height: 0 !important; |
| opacity: 0 !important; |
| } |
| |
| /* Sidebar always visible, cannot be hidden */ |
| [data-testid="stSidebar"] { |
| display: flex !important; |
| visibility: visible !important; |
| transform: none !important; |
| min-width: 244px !important; |
| max-width: 244px !important; |
| background: #020617 !important; |
| } |
| |
| /* Background */ |
| [data-testid="stAppViewContainer"] { |
| background: |
| radial-gradient(circle at top, |
| #111827 0%, |
| #020617 45%, |
| #000000 100%); |
| color: white; |
| } |
| |
| /* Center main content */ |
| [data-testid="stMain"] { |
| display: flex; |
| justify-content: center; |
| } |
| |
| /* Main container */ |
| .block-container { |
| max-width: 1150px; |
| width: 100%; |
| padding-top: 1rem; |
| padding-bottom: 2rem; |
| margin-left: auto; |
| margin-right: auto; |
| } |
| |
| /* Force st.image to center */ |
| [data-testid="stImage"] { |
| display: flex; |
| justify-content: center; |
| } |
| |
| [data-testid="stImage"] img { |
| border-radius: 18px; |
| } |
| |
| /* Hero Title */ |
| .hero-title { |
| width: 100%; |
| text-align: center; |
| font-size: 4rem; |
| font-weight: 800; |
| background: linear-gradient(90deg, #60a5fa, #8b5cf6); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin-bottom: 0.5rem; |
| } |
| |
| /* Subtitle */ |
| .subtitle { |
| width: 100%; |
| text-align: center; |
| color: #94a3b8; |
| font-size: 1.15rem; |
| margin-bottom: 2rem; |
| } |
| |
| /* Section Title */ |
| .section-title { |
| font-size: 1.8rem; |
| font-weight: 700; |
| margin-bottom: 10px; |
| } |
| |
| /* Filter Section */ |
| .filter-section { |
| background: rgba(255,255,255,0.03); |
| border-radius: 18px; |
| padding: 20px 24px; |
| border: 1px solid rgba(255,255,255,0.06); |
| margin-bottom: 20px; |
| } |
| |
| .filter-title { |
| font-size: 1rem; |
| font-weight: 600; |
| color: #94a3b8; |
| margin-bottom: 12px; |
| letter-spacing: 0.05em; |
| text-transform: uppercase; |
| } |
| |
| /* Feature Card */ |
| .feature-card { |
| background: rgba(255,255,255,0.04); |
| border-radius: 22px; |
| padding: 24px; |
| border: 1px solid rgba(255,255,255,0.05); |
| text-align: center; |
| backdrop-filter: blur(10px); |
| } |
| |
| /* Result Card */ |
| .result-card { |
| background: rgba(255,255,255,0.05); |
| border-radius: 22px; |
| padding: 24px; |
| border: 1px solid rgba(255,255,255,0.06); |
| margin-bottom: 18px; |
| backdrop-filter: blur(12px); |
| } |
| |
| /* Tags */ |
| .tag { |
| display: inline-block; |
| padding: 6px 12px; |
| border-radius: 999px; |
| background: rgba(99,102,241,0.18); |
| color: #c7d2fe; |
| font-size: 0.82rem; |
| margin-right: 8px; |
| margin-top: 8px; |
| } |
| |
| /* Button */ |
| .stButton > button { |
| width: 100%; |
| height: 52px; |
| border-radius: 18px; |
| border: none; |
| font-weight: 700; |
| color: white; |
| background: linear-gradient(to right, #6366f1, #8b5cf6); |
| } |
| |
| .stButton > button:hover { |
| transform: scale(1.02); |
| box-shadow: 0 0 25px rgba(139,92,246,0.45); |
| } |
| |
| /* Segment Badge */ |
| .segment-badge { |
| display: inline-block; |
| padding: 8px 16px; |
| border-radius: 999px; |
| color: white; |
| font-weight: 600; |
| font-size: 0.9rem; |
| } |
| |
| </style> |
| |
| <script> |
| const hideBtn = () => { |
| const btns = document.querySelectorAll( |
| '[data-testid="collapsedControl"], button[aria-label*="sidebar"], button[aria-label*="Collapse"]' |
| ); |
| btns.forEach(b => b.style.display = 'none'); |
| }; |
| hideBtn(); |
| const observer = new MutationObserver(hideBtn); |
| observer.observe(document.body, { childList: true, subtree: true }); |
| </script> |
| """, unsafe_allow_html=True) |
|
|
| |
| |
| |
|
|
| with st.sidebar: |
|
|
| st.image( |
| "FTDS-053-RMT-GROUP1-LOGO.png", |
| width=180 |
| ) |
|
|
| st.markdown("## 📱 Navigation") |
|
|
| page = st.radio( |
| "", |
| [ |
| "🏠 Home", |
| "📊 EDA", |
| "🔍 Recommendation" |
| ] |
| ) |
|
|
| |
| |
| |
|
|
| if page == "🏠 Home": |
|
|
| col1, col2, col3 = st.columns([1, 1, 1]) |
| with col2: |
| st.image("FTDS-053-RMT-GROUP1-LOGO.png", width=320) |
|
|
| st.markdown( |
| '<div class="hero-title">PhoneFinder</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| st.markdown( |
| '<div class="subtitle">AI-Powered Smartphone Recommendation System</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| st.markdown( |
| '<div class="section-title">❓ The Problem</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| st.write(""" |
| With hundreds of smartphone options available across different price ranges, |
| users often struggle to find devices that truly match their needs and budget. |
| |
| Many people purchase smartphones based only on trends or brand popularity, |
| instead of choosing devices optimized for their actual use case. |
| """) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| st.markdown( |
| '<div class="section-title">💡 Our Solution</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| st.write(""" |
| PhoneFinder provides smartphone recommendations based on broad user priorities such as: |
| |
| - Flagship |
| - Gaming / Performance |
| - Camera-Focused |
| - Budget-Focused |
| |
| The system combines clustering and similarity-based recommendation techniques |
| to recommend smartphones that best match the user's budget and preferences. |
| """) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| info = get_dataset_info() |
|
|
| col1, col2, col3 = st.columns(3) |
|
|
| with col1: |
| st.markdown(f""" |
| <div class="feature-card"> |
| 📱<br><br> |
| <h2>{info['total_phones']}</h2> |
| Smartphones |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| with col2: |
| st.markdown(f""" |
| <div class="feature-card"> |
| 💰<br><br> |
| <h2>${info['avg_price']}</h2> |
| Average Price |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| with col3: |
| st.markdown(""" |
| <div class="feature-card"> |
| 🎯<br><br> |
| <h2>4</h2> |
| Recommendation Types |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| |
| |
|
|
| elif page == "📊 EDA": |
|
|
| st.markdown( |
| '<div class="hero-title">Exploratory Data Analysis</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| st.markdown( |
| '<div class="subtitle">Understanding smartphone market patterns and specifications</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| render_eda() |
|
|
| |
| |
| |
|
|
| elif page == "🔍 Recommendation": |
|
|
| st.markdown( |
| '<div class="hero-title">Find Your Smartphone</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| st.markdown( |
| '<div class="subtitle">AI-powered smartphone recommendation engine</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| |
| |
| |
|
|
| col1, col2 = st.columns(2) |
|
|
| with col1: |
|
|
| budget = st.slider( |
| "💰 Budget (USD)", |
| min_value=50, |
| max_value=2000, |
| value=500, |
| step=50 |
| ) |
|
|
| top_n = st.slider( |
| "📋 Top N Results", |
| min_value=1, |
| max_value=10, |
| value=5 |
| ) |
|
|
| with col2: |
|
|
| priority = st.selectbox( |
| "🎯 Priority", |
| [ |
| "flagship", |
| "gaming", |
| "camera", |
| "budget" |
| ] |
| ) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| |
| |
| |
|
|
| st.markdown(""" |
| <div class="filter-title">🔧 Minimum Specification Filters (Optional)</div> |
| """, unsafe_allow_html=True) |
|
|
| fcol1, fcol2, fcol3, fcol4 = st.columns(4) |
|
|
| with fcol1: |
| min_ram = st.selectbox( |
| "🖥️ Minimum RAM (GB)", |
| options=[0, 2, 4, 6, 8, 12, 16], |
| index=0, |
| format_func=lambda x: "No Filter" if x == 0 else f"{x} GB" |
| ) |
|
|
| with fcol2: |
| min_camera = st.selectbox( |
| "📷 Minimum Camera (MP)", |
| options=[0, 12, 24, 48, 50, 64, 108, 200], |
| index=0, |
| format_func=lambda x: "No Filter" if x == 0 else f"{x} MP" |
| ) |
|
|
| with fcol3: |
| min_battery = st.selectbox( |
| "🔋 Minimum Battery (mAh)", |
| options=[0, 3000, 4000, 4500, 5000, 5500, 6000], |
| index=0, |
| format_func=lambda x: "No Filter" if x == 0 else f"{x:,} mAh" |
| ) |
|
|
| with fcol4: |
| min_year = st.selectbox( |
| "📅 Minimum Release Year", |
| options=[0, 2020, 2021, 2022, 2023, 2024, 2025], |
| index=0, |
| format_func=lambda x: "No Filter" if x == 0 else str(x) |
| ) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| search = st.button("🔍 Find Phones") |
|
|
| |
| |
| |
|
|
| if search: |
|
|
| with st.spinner("Finding best smartphone matches..."): |
|
|
| results, seg_name = recommend_phones( |
| budget_usd=budget, |
| priority=priority, |
| top_n=top_n, |
| min_ram=min_ram, |
| min_camera_mp=min_camera, |
| min_battery=min_battery, |
| min_year=min_year, |
| ) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| |
| active_filters = [] |
| if min_ram > 0: |
| active_filters.append(f"RAM ≥ {min_ram} GB") |
| if min_camera > 0: |
| active_filters.append(f"Camera ≥ {min_camera} MP") |
| if min_battery > 0: |
| active_filters.append(f"Battery ≥ {min_battery:,} mAh") |
| if min_year > 0: |
| active_filters.append(f"Release Year ≥ {min_year}") |
|
|
| if active_filters: |
| st.info(f"🔧 Active filters: {' · '.join(active_filters)}") |
|
|
| |
| if results.empty: |
| st.warning( |
| "⚠️ No smartphones found matching your filters and budget. " |
| "Try lowering the minimum specifications or increasing your budget." |
| ) |
|
|
| else: |
|
|
| SEG_COLORS = { |
| 'Budget': '#534AB7', |
| 'Camera-focused': '#993C1D', |
| 'Gaming / Performance': '#0F6E56', |
| 'Flagship': '#854F0B', |
| } |
|
|
| color = SEG_COLORS.get( |
| seg_name.split(' (')[0], |
| '#534AB7' |
| ) |
|
|
| st.markdown( |
| f""" |
| <div class="segment-badge" style="background:{color}"> |
| Matched Segment: {seg_name} |
| </div> |
| """, |
| unsafe_allow_html=True |
| ) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| for rank, row in results.iterrows(): |
|
|
| name = f"{row.get('brand','')} {row.get('model','')}".strip() |
|
|
| price = ( |
| f"${row['price']:.0f}" |
| if pd.notna(row.get('price')) |
| else 'N/A' |
| ) |
|
|
| score = row.get('Match Score (%)', 0) |
|
|
| st.markdown(f""" |
| <div class="result-card"> |
| |
| <h2>#{rank} {name}</h2> |
| |
| <h3>{price}</h3> |
| |
| <p><b>Match Score:</b> {score:.1f}%</p> |
| |
| <div class="tag"> |
| {int(row['ram'])} GB RAM |
| </div> |
| |
| <div class="tag"> |
| {int(row['battery_capacity'])} mAh |
| </div> |
| |
| <div class="tag"> |
| {int(row['main_camera_mp'])} MP Camera |
| </div> |
| |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| st.progress(int(score)) |
|
|