| | import os |
| | import dotenv |
| | dotenv.load_dotenv() |
| | import json |
| | import requests |
| | import streamlit as st |
| |
|
| | |
| | |
| | |
| | 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", |
| | ) |
| |
|
| | |
| | |
| | |
| | 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, |
| | ) |
| |
|
| |
|
| | |
| | |
| | |
| | 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 |
| |
|
| |
|
| | |
| | |
| | |
| | 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) |
| |
|
| |
|
| | |
| | |
| | |
| | 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) |
| | |
| | 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, |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| |
|
| | 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() |
| |
|
| |
|
| | |
| | |
| | |
| | def page_login(): |
| | |
| | _, 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() |
| |
|
| | |
| | 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, |
| | ) |
| |
|
| | |
| | st.markdown("<div class='section-title'>π Dashboard Overview</div>", unsafe_allow_html=True) |
| | render_scorecard() |
| |
|
| | st.markdown("<hr class='styled'>", unsafe_allow_html=True) |
| |
|
| | |
| | tab_upload, tab_extract = st.tabs(["π Upload CV", "π§ Extract Profile"]) |
| |
|
| | |
| | |
| | |
| | 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 |
| | |
| | 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() |
| |
|
| | |
| | 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, |
| | ) |
| |
|
| | |
| | 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: |
| | |
| | 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.") |
| |
|
| | |
| | |
| | |
| | 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, |
| | ) |
| |
|
| | |
| | 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 |
| | |
| | if uid: |
| | files, _ = api_get_user_files(uid) |
| | st.session_state.user_files = files or [] |
| | st.rerun() |
| |
|
| | |
| | if st.session_state.extract_result: |
| | st.markdown("<hr class='styled'>", unsafe_allow_html=True) |
| | result = st.session_state.extract_result |
| |
|
| | |
| | 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") |
| |
|
| | |
| | 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, |
| | ) |
| |
|
| |
|
| | |
| | |
| | |
| | if st.session_state.token: |
| | page_main() |
| | else: |
| | page_login() |
| |
|