final-project-2 / app.py
fernandobriann's picture
Update app.py
2e3f103 verified
Raw
History Blame Contribute Delete
13.5 kB
# app.py
import streamlit as st
import pandas as pd
from prediction import recommend_phones
from eda import render_eda, get_dataset_info
# =========================================================
# PAGE CONFIG
# =========================================================
st.set_page_config(
page_title="PhoneFinder",
page_icon="📱",
layout="wide",
initial_sidebar_state="expanded"
)
# =========================================================
# CSS
# =========================================================
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)
# =========================================================
# SIDEBAR
# =========================================================
with st.sidebar:
st.image(
"FTDS-053-RMT-GROUP1-LOGO.png",
width=180
)
st.markdown("## 📱 Navigation")
page = st.radio(
"",
[
"🏠 Home",
"📊 EDA",
"🔍 Recommendation"
]
)
# =========================================================
# HOME PAGE
# =========================================================
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)
# =========================================================
# EDA PAGE
# =========================================================
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()
# =========================================================
# RECOMMENDATION PAGE
# =========================================================
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)
# =====================================================
# INPUT UTAMA: Budget, Priority, Top N
# =====================================================
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)
# =====================================================
# FILTER SPESIFIKASI MINIMUM
# =====================================================
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")
# =====================================================
# RESULTS
# =====================================================
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)
# Tampilkan info filter yang aktif
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)}")
# Jika tidak ada hasil
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))