Spaces:
Sleeping
Sleeping
| import os | |
| import dotenv | |
| dotenv.load_dotenv() | |
| import json | |
| import requests | |
| import streamlit as st | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # CONFIG | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| BASE_URL = os.environ.get("BACKEND_BASE_URL", "http://localhost:8000") | |
| st.set_page_config( | |
| page_title="Candidate Explorer", | |
| page_icon="π", | |
| layout="wide", | |
| initial_sidebar_state="expanded", | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # GLOBAL CSS | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown( | |
| """ | |
| <style> | |
| /* ββ Global βββββββββββββββββββββββββββββββ */ | |
| html, body, [class*="css"] { | |
| font-family: 'Inter', 'Segoe UI', sans-serif; | |
| color: #333e4a; | |
| background-color: #ffffff; | |
| } | |
| /* ββ Sidebar βββββββββββββββββββββββββββββββ */ | |
| [data-testid="stSidebar"] { | |
| background-color: #f4f6ff; | |
| border-right: 1px solid #c7cef5; | |
| } | |
| [data-testid="stSidebar"] h1, | |
| [data-testid="stSidebar"] h2, | |
| [data-testid="stSidebar"] h3, | |
| [data-testid="stSidebar"] label { | |
| color: #435cdc !important; | |
| } | |
| /* ββ Buttons βββββββββββββββββββββββββββββββ */ | |
| .stButton > button { | |
| background-color: #435cdc; | |
| color: #ffffff; | |
| border: none; | |
| border-radius: 8px; | |
| padding: 0.5rem 1.25rem; | |
| font-weight: 600; | |
| transition: background-color 0.2s ease; | |
| } | |
| .stButton > button:hover { | |
| background-color: #7b8de7; | |
| color: #ffffff; | |
| } | |
| .stButton > button:focus { | |
| outline: 2px solid #c7cef5; | |
| } | |
| /* ββ Tabs βββββββββββββββββββββββββββββββββββ */ | |
| [data-baseweb="tab-list"] { | |
| gap: 8px; | |
| border-bottom: 2px solid #c7cef5; | |
| } | |
| [data-baseweb="tab"] { | |
| border-radius: 8px 8px 0 0; | |
| padding: 0.5rem 1.25rem; | |
| font-weight: 600; | |
| color: #7b8de7; | |
| background: transparent; | |
| } | |
| [aria-selected="true"][data-baseweb="tab"] { | |
| color: #435cdc !important; | |
| border-bottom: 3px solid #435cdc !important; | |
| background: #f4f6ff; | |
| } | |
| /* ββ Inputs ββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stTextInput"] input, | |
| [data-testid="stSelectbox"] select, | |
| textarea { | |
| border-radius: 8px !important; | |
| border: 1.5px solid #c7cef5 !important; | |
| color: #333e4a !important; | |
| } | |
| [data-testid="stTextInput"] input:focus, | |
| textarea:focus { | |
| border-color: #435cdc !important; | |
| box-shadow: 0 0 0 2px #c7cef5; | |
| } | |
| /* ββ File uploader βββββββββββββββββββββββββ */ | |
| [data-testid="stFileUploader"] { | |
| border: 2px dashed #7b8de7; | |
| border-radius: 10px; | |
| background: #f4f6ff; | |
| padding: 1rem; | |
| } | |
| /* ββ Metric cards ββββββββββββββββββββββββββ */ | |
| .scorecard-wrap { | |
| display: flex; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .scorecard-card { | |
| flex: 1; | |
| background: #f4f6ff; | |
| border: 1.5px solid #c7cef5; | |
| border-radius: 12px; | |
| padding: 1.25rem 1.5rem; | |
| box-shadow: 0 2px 8px rgba(67,92,220,0.07); | |
| text-align: center; | |
| } | |
| .scorecard-card .sc-label { | |
| font-size: 0.82rem; | |
| font-weight: 600; | |
| color: #7b8de7; | |
| text-transform: uppercase; | |
| letter-spacing: 0.04em; | |
| margin-bottom: 0.4rem; | |
| } | |
| .scorecard-card .sc-value { | |
| font-size: 2rem; | |
| font-weight: 800; | |
| color: #435cdc; | |
| line-height: 1.1; | |
| } | |
| .scorecard-card .sc-sub { | |
| font-size: 0.78rem; | |
| color: #7b8de7; | |
| margin-top: 0.2rem; | |
| } | |
| /* ββ Badge βββββββββββββββββββββββββββββββββ */ | |
| .badge-success { | |
| background: #c7cef5; color: #435cdc; | |
| border-radius: 99px; padding: 2px 10px; | |
| font-size: 0.78rem; font-weight: 700; | |
| } | |
| .badge-warn { | |
| background: #fff3c4; color: #a07c00; | |
| border-radius: 99px; padding: 2px 10px; | |
| font-size: 0.78rem; font-weight: 700; | |
| } | |
| .badge-error { | |
| background: #fde8e8; color: #c0392b; | |
| border-radius: 99px; padding: 2px 10px; | |
| font-size: 0.78rem; font-weight: 700; | |
| } | |
| /* ββ Section header ββββββββββββββββββββββββ */ | |
| .section-title { | |
| font-size: 1.15rem; | |
| font-weight: 700; | |
| color: #435cdc; | |
| margin-bottom: 0.75rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.4rem; | |
| } | |
| .section-title::after { | |
| content: ''; | |
| flex: 1; | |
| height: 2px; | |
| background: linear-gradient(90deg, #c7cef5, transparent); | |
| margin-left: 0.5rem; | |
| } | |
| /* ββ Login card ββββββββββββββββββββββββββββ */ | |
| .login-card { | |
| max-width: 420px; | |
| margin: 4rem auto; | |
| padding: 2.5rem 2rem; | |
| background: #ffffff; | |
| border-radius: 16px; | |
| box-shadow: 0 4px 32px rgba(67,92,220,0.13); | |
| border: 1.5px solid #c7cef5; | |
| } | |
| .login-card h1 { | |
| color: #435cdc; | |
| font-size: 1.8rem; | |
| font-weight: 800; | |
| margin-bottom: 0.25rem; | |
| } | |
| .login-card p { | |
| color: #7b8de7; | |
| font-size: 0.95rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| /* ββ Divider βββββββββββββββββββββββββββββββ */ | |
| hr.styled { border: none; border-top: 1.5px solid #c7cef5; margin: 1.2rem 0; } | |
| /* ββ JSON output βββββββββββββββββββββββββββ */ | |
| .profile-json { | |
| background: #f4f6ff; | |
| border-radius: 10px; | |
| border: 1.5px solid #c7cef5; | |
| padding: 1rem 1.25rem; | |
| font-size: 0.85rem; | |
| color: #333e4a; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| max-height: 500px; | |
| overflow-y: auto; | |
| } | |
| /* ββ Table βββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stDataFrame"] { | |
| border-radius: 10px; | |
| overflow: hidden; | |
| border: 1.5px solid #c7cef5; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # SESSION STATE INIT | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| for key, default in [ | |
| ("token", None), | |
| ("user", None), | |
| ("upload_results", []), | |
| ("extract_result", None), | |
| ("user_files", []), | |
| ]: | |
| if key not in st.session_state: | |
| st.session_state[key] = default | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # API HELPERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def _headers(): | |
| return {"Authorization": f"Bearer {st.session_state.token}"} | |
| def api_login(email: str, password: str): | |
| """POST /admin/login β returns (token, error_msg)""" | |
| try: | |
| resp = requests.post( | |
| f"{BASE_URL}/admin/login", | |
| data={"username": email, "password": password}, | |
| timeout=15, | |
| ) | |
| if resp.status_code == 200: | |
| return resp.json().get("access_token"), None | |
| detail = resp.json().get("detail", resp.text) | |
| return None, str(detail) | |
| except requests.exceptions.ConnectionError: | |
| return None, "Cannot connect to backend. Check BACKEND_BASE_URL." | |
| except Exception as e: | |
| return None, str(e) | |
| def api_get_me(): | |
| """GET /admin/me β returns user dict""" | |
| try: | |
| resp = requests.get(f"{BASE_URL}/admin/me", headers=_headers(), timeout=10) | |
| if resp.status_code == 200: | |
| return resp.json(), None | |
| return None, resp.json().get("detail", resp.text) | |
| except Exception as e: | |
| return None, str(e) | |
| def api_get_scorecard(): | |
| """GET /file/score_card β returns data dict""" | |
| try: | |
| resp = requests.get(f"{BASE_URL}/file/score_card", headers=_headers(), timeout=10) | |
| if resp.status_code == 200: | |
| return resp.json().get("data", {}), None | |
| return None, resp.json().get("detail", resp.text) | |
| except Exception as e: | |
| return None, str(e) | |
| def api_upload_files(uploaded_files): | |
| """POST /file/upload β returns list of results""" | |
| try: | |
| files = [ | |
| ("files", (f.name, f.read(), "application/pdf")) | |
| for f in uploaded_files | |
| ] | |
| resp = requests.post( | |
| f"{BASE_URL}/file/upload", | |
| headers=_headers(), | |
| files=files, | |
| timeout=60, | |
| ) | |
| if resp.status_code == 201: | |
| return resp.json().get("files", []), None | |
| return None, resp.json().get("detail", resp.text) | |
| except Exception as e: | |
| return None, str(e) | |
| def api_get_user_files(user_id: str): | |
| """GET /file/user/{user_id} β returns list of file dicts""" | |
| try: | |
| resp = requests.get( | |
| f"{BASE_URL}/file/user/{user_id}", | |
| headers=_headers(), | |
| timeout=10, | |
| ) | |
| if resp.status_code == 200: | |
| return resp.json().get("files", []), None | |
| return None, resp.json().get("detail", resp.text) | |
| except Exception as e: | |
| return None, str(e) | |
| def api_extract_profile(filename: str): | |
| """POST /profile/extract_profile?filename=... β returns profile dict""" | |
| try: | |
| resp = requests.post( | |
| f"{BASE_URL}/profile/extract_profile", | |
| headers=_headers(), | |
| params={"filename": filename}, | |
| timeout=120, | |
| ) | |
| if resp.status_code == 200: | |
| return resp.json(), None | |
| return None, resp.json().get("detail", resp.text) | |
| except Exception as e: | |
| return None, str(e) | |
| def api_delete_file(filename: str): | |
| """DELETE /file/{filename}""" | |
| try: | |
| resp = requests.delete( | |
| f"{BASE_URL}/file/{filename}", | |
| headers=_headers(), | |
| timeout=15, | |
| ) | |
| if resp.status_code == 200: | |
| return True, None | |
| return False, resp.json().get("detail", resp.text) | |
| except Exception as e: | |
| return False, str(e) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # UI COMPONENTS | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def render_scorecard(): | |
| sc, err = api_get_scorecard() | |
| if err: | |
| st.warning(f"Could not load scorecard: {err}") | |
| return | |
| total_file = sc.get("total_file", 0) | |
| total_extracted = sc.get("total_extracted", 0) | |
| pct = sc.get("percent_extracted", 0) | |
| # percent_extracted may be a float like 75.0 or string "75%" | |
| if isinstance(pct, str): | |
| pct_display = pct | |
| else: | |
| pct_display = f"{pct:.1f}%" | |
| st.markdown( | |
| f""" | |
| <div class="scorecard-wrap"> | |
| <div class="scorecard-card"> | |
| <div class="sc-label">π Total CVs Uploaded</div> | |
| <div class="sc-value">{total_file}</div> | |
| <div class="sc-sub">files in your workspace</div> | |
| </div> | |
| <div class="scorecard-card"> | |
| <div class="sc-label">β Profiles Extracted</div> | |
| <div class="sc-value">{total_extracted}</div> | |
| <div class="sc-sub">structured profiles</div> | |
| </div> | |
| <div class="scorecard-card"> | |
| <div class="sc-label">π Extraction Rate</div> | |
| <div class="sc-value" style="color:#dcc343">{pct_display}</div> | |
| <div class="sc-sub">of uploaded CVs processed</div> | |
| </div> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def render_sidebar(): | |
| user = st.session_state.user or {} | |
| with st.sidebar: | |
| st.markdown( | |
| f""" | |
| <div style="text-align:center;padding:1rem 0 0.5rem;"> | |
| <div style="font-size:2.5rem;">π€</div> | |
| <div style="font-weight:800;font-size:1.1rem;color:#435cdc;"> | |
| {user.get('full_name', 'User')} | |
| </div> | |
| <div style="font-size:0.82rem;color:#7b8de7;margin-top:2px;"> | |
| {user.get('email', '')} | |
| </div> | |
| <span class="badge-success" style="margin-top:6px;display:inline-block;"> | |
| {user.get('role', 'user').upper()} | |
| </span> | |
| </div> | |
| <hr class="styled"> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| # st.markdown( | |
| # "<div style='font-size:0.78rem;color:#7b8de7;margin-bottom:4px;'>BACKEND</div>", | |
| # unsafe_allow_html=True, | |
| # ) | |
| # st.markdown( | |
| # f"<code style='font-size:0.75rem;color:#435cdc;'>{BASE_URL}</code>", | |
| # unsafe_allow_html=True, | |
| # ) | |
| # st.markdown("<hr class='styled'>", unsafe_allow_html=True) | |
| if st.button("πͺ Logout", use_container_width=True): | |
| for key in ["token", "user", "upload_results", "extract_result", "user_files"]: | |
| st.session_state[key] = None if key in ("token", "user", "extract_result") else [] | |
| st.rerun() | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # PAGES | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def page_login(): | |
| # Center the login card | |
| _, col, _ = st.columns([1, 1.4, 1]) | |
| with col: | |
| st.markdown( | |
| """ | |
| <div class="login-card"> | |
| <h1>π Candidate Explorer</h1> | |
| <p>Sign in to manage and analyze candidate CVs.</p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| with st.form("login_form", clear_on_submit=False): | |
| st.markdown( | |
| "<div class='section-title'>Sign In</div>", unsafe_allow_html=True | |
| ) | |
| email = st.text_input("Email address", placeholder="you@company.com") | |
| password = st.text_input("Password", type="password", placeholder="β’β’β’β’β’β’β’β’") | |
| submitted = st.form_submit_button("Sign In β", use_container_width=True) | |
| if submitted: | |
| if not email or not password: | |
| st.error("Please enter both email and password.") | |
| else: | |
| with st.spinner("Signing inβ¦"): | |
| token, err = api_login(email, password) | |
| if err: | |
| st.error(f"Login failed: {err}") | |
| else: | |
| st.session_state.token = token | |
| user, err2 = api_get_me() | |
| if err2: | |
| st.session_state.user = {"email": email, "full_name": email, "role": "user"} | |
| else: | |
| st.session_state.user = user | |
| st.rerun() | |
| def page_main(): | |
| render_sidebar() | |
| # ββ Page header ββββββββββββββββββββββββββββββ | |
| st.markdown( | |
| "<h1 style='color:#435cdc;font-size:1.75rem;font-weight:800;margin-bottom:0.1rem;'>" | |
| "π Candidate Explorer" | |
| "</h1>" | |
| "<p style='color:#7b8de7;margin-top:0;margin-bottom:1.25rem;font-size:0.95rem;'>" | |
| "Upload CVs, extract candidate profiles, and track your workspace.</p>", | |
| unsafe_allow_html=True, | |
| ) | |
| # ββ Scorecard βββββββββββββββββββββββββββββββββ | |
| st.markdown("<div class='section-title'>π Dashboard Overview</div>", unsafe_allow_html=True) | |
| render_scorecard() | |
| st.markdown("<hr class='styled'>", unsafe_allow_html=True) | |
| # ββ Tabs ββββββββββββββββββββββββββββββββββββββ | |
| tab_upload, tab_extract = st.tabs(["π Upload CV", "π§ Extract Profile"]) | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 1 β UPLOAD | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| with tab_upload: | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown( | |
| "<div class='section-title'>Upload Candidate CVs</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| uploaded = st.file_uploader( | |
| "Drop PDF files here or click to browse", | |
| type=["pdf"], | |
| accept_multiple_files=True, | |
| help="Only PDF files are accepted.", | |
| ) | |
| col_btn, col_info = st.columns([1, 3]) | |
| with col_btn: | |
| do_upload = st.button("β¬οΈ Upload", use_container_width=True, disabled=not uploaded) | |
| if do_upload and uploaded: | |
| with st.spinner(f"Uploading {len(uploaded)} file(s)β¦"): | |
| results, err = api_upload_files(uploaded) | |
| if err: | |
| st.error(f"Upload failed: {err}") | |
| else: | |
| st.session_state.upload_results = results | |
| # Refresh user files list | |
| user = st.session_state.user or {} | |
| uid = str(user.get("user_id", "")) | |
| if uid: | |
| files, _ = api_get_user_files(uid) | |
| st.session_state.user_files = files or [] | |
| st.rerun() | |
| # ββ Results table ββ | |
| if st.session_state.upload_results: | |
| st.markdown("<hr class='styled'>", unsafe_allow_html=True) | |
| st.markdown( | |
| "<div class='section-title'>Upload Results</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| for r in st.session_state.upload_results: | |
| fname = r.get("filename", r.get("name", "")) | |
| status = r.get("status", "uploaded") | |
| badge_cls = "badge-success" if "success" in status.lower() or status == "uploaded" else "badge-error" | |
| st.markdown( | |
| f"<span class='badge-success'>β</span> " | |
| f"<strong style='color:#333e4a;'>{fname}</strong> " | |
| f"<span class='{badge_cls}'>{status}</span>", | |
| unsafe_allow_html=True, | |
| ) | |
| # ββ Existing files ββ | |
| st.markdown("<hr class='styled'>", unsafe_allow_html=True) | |
| st.markdown( | |
| "<div class='section-title'>Your Uploaded Files</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| user = st.session_state.user or {} | |
| uid = str(user.get("user_id", "")) | |
| col_refresh, _ = st.columns([1, 5]) | |
| with col_refresh: | |
| if st.button("π Refresh List", use_container_width=True): | |
| if uid: | |
| files, err = api_get_user_files(uid) | |
| if err: | |
| st.warning(f"Could not load files: {err}") | |
| else: | |
| st.session_state.user_files = files or [] | |
| if not st.session_state.user_files and uid: | |
| # Auto-load on first visit | |
| files, _ = api_get_user_files(uid) | |
| st.session_state.user_files = files or [] | |
| if st.session_state.user_files: | |
| rows = [] | |
| for f in st.session_state.user_files: | |
| rows.append( | |
| { | |
| "Filename": f.get("filename", ""), | |
| "Type": f.get("file_type", ""), | |
| "Extracted": "β " if f.get("is_extracted") else "β³", | |
| "Uploaded": str(f.get("uploaded_at", ""))[:19], | |
| } | |
| ) | |
| st.dataframe(rows, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("No files uploaded yet.") | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 2 β EXTRACT PROFILE | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| with tab_extract: | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown( | |
| "<div class='section-title'>Extract Structured Profile from CV</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| # Load files if needed | |
| user = st.session_state.user or {} | |
| uid = str(user.get("user_id", "")) | |
| if not st.session_state.user_files and uid: | |
| files, _ = api_get_user_files(uid) | |
| st.session_state.user_files = files or [] | |
| file_options = [f.get("filename", "") for f in st.session_state.user_files if f.get("filename")] | |
| if not file_options: | |
| st.info("No CVs found. Upload files first in the **Upload CV** tab.") | |
| else: | |
| col_sel, col_ex = st.columns([3, 1]) | |
| with col_sel: | |
| chosen = st.selectbox( | |
| "Select a CV file to extract", | |
| options=file_options, | |
| help="Choose a PDF you have already uploaded.", | |
| ) | |
| with col_ex: | |
| st.markdown("<div style='margin-top:1.72rem;'></div>", unsafe_allow_html=True) | |
| do_extract = st.button("π§ Extract", use_container_width=True) | |
| if do_extract and chosen: | |
| with st.spinner(f"Extracting profile from **{chosen}**β¦ this may take a moment."): | |
| result, err = api_extract_profile(chosen) | |
| if err: | |
| st.error(f"Extraction failed: {err}") | |
| st.session_state.extract_result = None | |
| else: | |
| st.session_state.extract_result = result | |
| # Refresh files to update is_extracted flag | |
| if uid: | |
| files, _ = api_get_user_files(uid) | |
| st.session_state.user_files = files or [] | |
| st.rerun() | |
| # ββ Display extracted profile ββ | |
| if st.session_state.extract_result: | |
| st.markdown("<hr class='styled'>", unsafe_allow_html=True) | |
| result = st.session_state.extract_result | |
| # Try to highlight key fields | |
| fullname = result.get("fullname") or result.get("full_name", "") | |
| if fullname: | |
| st.markdown( | |
| f"<div style='background:#c7cef5;border-radius:10px;padding:0.75rem 1.2rem;" | |
| f"margin-bottom:1rem;'>" | |
| f"<span style='color:#435cdc;font-weight:800;font-size:1.1rem;'>π€ {fullname}</span>" | |
| f"</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| col_l, col_r = st.columns(2) | |
| with col_l: | |
| st.markdown("<div class='section-title'>Education</div>", unsafe_allow_html=True) | |
| for i in range(1, 4): | |
| univ = result.get(f"univ_edu_{i}", "") | |
| major = result.get(f"major_edu_{i}", "") | |
| gpa = result.get(f"gpa_edu_{i}", "") | |
| if univ or major: | |
| gpa_str = f" Β· GPA {gpa}" if gpa else "" | |
| st.markdown( | |
| f"<div style='margin-bottom:0.5rem;padding:0.6rem 1rem;" | |
| f"background:#f4f6ff;border-radius:8px;border-left:3px solid #435cdc;'>" | |
| f"<strong style='color:#333e4a;'>{univ or 'β'}</strong><br>" | |
| f"<span style='color:#7b8de7;font-size:0.85rem;'>{major or ''}{gpa_str}</span>" | |
| f"</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("<div class='section-title' style='margin-top:1rem;'>Experience</div>", unsafe_allow_html=True) | |
| yoe = result.get("yoe") | |
| domicile = result.get("domicile", "") | |
| st.markdown( | |
| f"<div style='padding:0.6rem 1rem;background:#f4f6ff;border-radius:8px;" | |
| f"border-left:3px solid #dcc343;'>" | |
| f"<span style='color:#333e4a;font-weight:600;'>Years of Experience:</span> " | |
| f"<span style='color:#435cdc;font-weight:800;'>{yoe if yoe is not None else 'β'}</span><br>" | |
| f"<span style='color:#333e4a;font-weight:600;'>Domicile:</span> " | |
| f"<span style='color:#435cdc;'>{domicile or 'β'}</span>" | |
| f"</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| with col_r: | |
| def _tag_list(label, items, color="#c7cef5", text_color="#435cdc"): | |
| if not items: | |
| return | |
| st.markdown( | |
| f"<div class='section-title'>{label}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| tags = "".join( | |
| f"<span style='background:{color};color:{text_color};border-radius:99px;" | |
| f"padding:3px 10px;font-size:0.78rem;font-weight:600;margin:2px;display:inline-block;'>" | |
| f"{t}</span>" | |
| for t in items | |
| ) | |
| st.markdown( | |
| f"<div style='margin-bottom:0.75rem;line-height:2;'>{tags}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| _tag_list("π» Hard Skills", result.get("hardskills", [])) | |
| _tag_list("π€ Soft Skills", result.get("softskills", []), "#f4f6ff", "#333e4a") | |
| _tag_list("π Certifications", result.get("certifications", []), "#fff3c4", "#a07c00") | |
| _tag_list("π’ Business Domains", result.get("business_domain", []), "#c7cef5", "#435cdc") | |
| # Raw JSON toggle | |
| with st.expander("π Raw JSON response"): | |
| st.markdown( | |
| f"<div class='profile-json'>{json.dumps(result, indent=2, default=str)}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # ROUTER | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| if st.session_state.token: | |
| page_main() | |
| else: | |
| page_login() | |