Spaces:
Running
Running
| 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"<style>{BASE_CSS}</style>", 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("<div class='card'>", 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("</div>", 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("<div class='card'>", 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("</div>", 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.") |