from __future__ import annotations import time import uuid import re from pathlib import Path import streamlit as st from config import AVATAR_DIR from storage import Profile, ProfileStore from utils import normalize_skill_list, make_prompt_for_matching from groq_client import ask_groq_for_matches from matching import calculate_local_matches from feedback import save_feedback from ui_styles import BASE_CSS # ---------------------------------------------- # Streamlit Config # ---------------------------------------------- st.set_page_config( page_title="AI Skill Swap", page_icon="🤝", layout="wide", ) st.markdown(f"", unsafe_allow_html=True) store = ProfileStore() # ---------------------------------------------- # Header Section # ---------------------------------------------- col_logo, col_title = st.columns([1, 6]) with col_logo: try: st.image("logo.jpg", width=90) except: st.image("https://via.placeholder.com/100x100/4F46E5/FFFFFF?text=🤝", width=90) with col_title: st.title("AI Skill Swap") st.caption("Teach what you know • Learn what you love • Match intelligently") # ---------------------------------------------- # Sidebar: Profile Management # ---------------------------------------------- with st.sidebar: st.header("👤 Profile Manager") profiles = store.load_all() usernames = [p.username for p in profiles] selected_user = st.selectbox( "Select profile", ["— Create New —"] + usernames, key="selected_user" ) st.markdown("### 📝 Profile Details") username = st.text_input( "Username", value="" if selected_user == "— Create New —" else selected_user, key="username_input" ) offers_text = st.text_area( "Skills you can teach", height=90, value=st.session_state.get("offers_text", ""), placeholder="Python, React, Data Science", key="offers_input" ) wants_text = st.text_area( "Skills you want to learn", height=90, value=st.session_state.get("wants_text", ""), placeholder="LLMs, DevOps, UI Design", key="wants_input" ) availability = st.text_input( "Availability", placeholder="Evenings / Weekends", value=st.session_state.get("availability", ""), key="availability_input" ) preferences = st.text_input( "Language / Preferences", placeholder="English, Hindi, Urdu", value=st.session_state.get("preferences", ""), key="preferences_input" ) avatar_file = st.file_uploader( "Upload Avatar (Max 800KB)", type=["png", "jpg", "jpeg"], key="avatar_uploader" ) # Avatar Preview & Size Check if avatar_file: st.image(avatar_file, width=150, caption="Preview Avatar") if avatar_file.size > 800 * 1024: st.error("File exceeds 800KB!") avatar_file = None st.markdown("---") col_create, col_update = st.columns(2) # Create Button with col_create: if st.button("➕ Create", key="create_button", use_container_width=True): if not username.strip(): st.error("Username is required!") elif store.find_by_username(username): st.error("Already exists — use Update!") else: avatar_path = None if avatar_file: safe = re.sub(r"[^A-Za-z0-9_.-]", "_", username) ext = Path(avatar_file.name).suffix avatar_path = str( AVATAR_DIR / f"{safe}_{int(time.time())}{ext}" ) with open(avatar_path, "wb") as f: f.write(avatar_file.getbuffer()) profile = Profile( id=str(uuid.uuid4()), username=username.strip(), offers=normalize_skill_list(offers_text), wants=normalize_skill_list(wants_text), availability=availability.strip(), preferences=preferences.strip(), avatar=avatar_path, ) ok, msg = store.add_or_update(profile) if ok: st.success(msg) # Clear fields on create for key in ["offers_text", "wants_text", "availability", "preferences"]: if key in st.session_state: del st.session_state[key] if "avatar_uploader" in st.session_state: del st.session_state["avatar_uploader"] # Clear previous matches st.session_state["matches"] = [] st.rerun() else: st.error(msg) # Update Button with col_update: if st.button("💾 Update", key="update_button", use_container_width=True): existing = store.find_by_username(username) if not existing: st.error("Profile does not exist.") else: existing.offers = normalize_skill_list(offers_text) existing.wants = normalize_skill_list(wants_text) existing.availability = availability.strip() existing.preferences = preferences.strip() if avatar_file: safe = re.sub(r"[^A-Za-z0-9_.-]", "_", username) ext = Path(avatar_file.name).suffix avatar_path = str( AVATAR_DIR / f"{safe}_{int(time.time())}{ext}" ) with open(avatar_path, "wb") as f: f.write(avatar_file.getbuffer()) existing.avatar = avatar_path store.add_or_update(existing) st.success("Profile updated") if "avatar_uploader" in st.session_state: del st.session_state["avatar_uploader"] st.rerun() # Delete Button if selected_user != "— Create New —": if st.button("🗑️ Delete Profile", key="delete_button", type="secondary"): ok, msg = store.delete(selected_user) if ok: st.warning(msg) # Clear sidebar fields for key in ["username_input", "offers_input", "wants_input", "availability_input", "preferences_input"]: if key in st.session_state: del st.session_state[key] if "avatar_uploader" in st.session_state: del st.session_state["avatar_uploader"] st.rerun() else: st.error(msg) # ---------------------------------------------- # Main Content - Community Profiles # ---------------------------------------------- left, right = st.columns([2, 3]) with left: st.subheader("🌍 Community Profiles") profiles = store.load_all() if not profiles: st.info("No profiles yet. Create one above.") else: for p in profiles: st.markdown("
", unsafe_allow_html=True) cols = st.columns([1, 4]) with cols[0]: if p.avatar and Path(p.avatar).exists(): st.image(p.avatar, width=120) else: st.image("https://via.placeholder.com/120", width=120) with cols[1]: st.markdown(f"**{p.username}**") st.caption(f"{p.availability} • {p.preferences}") st.markdown(f"🧠 **Offers:** {', '.join(p.offers) or '—'}") st.markdown(f"🎯 **Wants:** {', '.join(p.wants) or '—'}") st.markdown("
", unsafe_allow_html=True) # ---------------------------------------------- # AI Matchmaking # ---------------------------------------------- with right: st.subheader("🤖 AI Matchmaking") if not profiles: st.info("Add profiles to enable matchmaking.") else: pick = st.selectbox( "Match for profile", [p.username for p in profiles], key="matchmaking_select" ) if st.button("✨ Find Best Matches", key="find_matches_button"): with st.spinner("Finding best matches..."): current = store.find_by_username(pick) try: sys_msg, user_msg = make_prompt_for_matching( current, profiles, 3 ) raw_matches = ask_groq_for_matches(sys_msg, user_msg) except Exception as e: st.info( f"Using local matching due to: {str(e)[:100]}..." ) raw_matches = calculate_local_matches( current, [p for p in profiles if p.id != current.id], 3, ) enriched = [] for rm in raw_matches: prof = store.find_by_username(rm.get("username", "")) if not prof: continue # 🔒 SAFE SCORE HANDLING try: raw_score = float(rm.get("score", 0)) except (ValueError, TypeError): raw_score = 0.0 raw_score = max(0.0, min(raw_score, 1.0)) enriched.append({ "username": prof.username, "offers": prof.offers, "wants": prof.wants, "avatar": prof.avatar, "score": raw_score, # ALWAYS 0–1 "reason": rm.get("reason", "AI Match") }) st.session_state["matches"] = enriched matches_list = st.session_state.get("matches", []) if matches_list: st.markdown( f"### 🎯 Top {len(matches_list)} Matches for {pick}" ) for m in matches_list: st.markdown("
", unsafe_allow_html=True) cols = st.columns([1, 4]) with cols[0]: if m.get("avatar") and Path(m["avatar"]).exists(): st.image(m["avatar"], width=120) else: st.image( "https://via.placeholder.com/120", width=120 ) with cols[1]: st.markdown(f"### {m['username']}") score_float = m["score"] # 0–1 score_percent = int(score_float * 100) st.progress(score_float) st.caption(f"Match Score: {score_percent}%") st.markdown( f"🧠 **Offers:** " f"{', '.join(m['offers']) or '—'}" ) st.markdown( f"🎯 **Wants:** " f"{', '.join(m['wants']) or '—'}" ) st.markdown(f"*{m['reason']}*") st.markdown("
", unsafe_allow_html=True) # ---------------------------------------------- # Feedback # ---------------------------------------------- with st.sidebar: st.markdown("---") with st.expander("💬 Feedback"): fb = st.text_area("Your feedback", key="feedback_text") if st.button("Submit Feedback", key="submit_feedback_button"): if fb.strip(): save_feedback({ "timestamp": time.time(), "feedback": fb, "username": username if username.strip() else "anonymous", }) st.success("Thank you!") if "feedback_text" in st.session_state: del st.session_state["feedback_text"] st.rerun() else: st.warning("Please write feedback.")