""" HRHUB - Bilateral HR Matching System Main Streamlit Application A professional HR matching system that connects candidates with companies using NLP embeddings and cosine similarity matching. """ import streamlit as st import sys from pathlib import Path # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent)) from config import * from data.data_loader import ( load_embeddings, find_top_matches ) from hrhub_project.utils.display_v2 import ( display_candidate_profile, display_company_card, display_match_table, display_stats_overview ) from utils.visualization import create_network_graph import streamlit.components.v1 as components def configure_page(): """Configure Streamlit page settings and custom CSS.""" st.set_page_config( page_title="HRHUB - HR Matching", page_icon="🏒", layout="wide", initial_sidebar_state="expanded" ) # Custom CSS for better styling st.markdown(""" """, unsafe_allow_html=True) def render_header(): """Render application header.""" st.markdown(f'

{APP_TITLE}

', unsafe_allow_html=True) st.markdown(f'

{APP_SUBTITLE}

', unsafe_allow_html=True) def render_sidebar(): """Render sidebar with controls and information.""" with st.sidebar: st.image("https://via.placeholder.com/250x80/0066CC/FFFFFF?text=HRHUB", width=250) st.markdown("---") st.markdown("### βš™οΈ Settings") # Number of matches top_k = st.slider( "Number of Matches", min_value=5, max_value=20, value=DEFAULT_TOP_K, step=5, help="Select how many top companies to display" ) # Minimum score threshold min_score = st.slider( "Minimum Match Score", min_value=0.0, max_value=1.0, value=MIN_SIMILARITY_SCORE, step=0.05, help="Filter companies below this similarity score" ) st.markdown("---") # View mode selection st.markdown("### πŸ‘€ View Mode") view_mode = st.radio( "Select view:", ["πŸ“Š Overview", "πŸ” Detailed Cards", "πŸ“ˆ Table View"], help="Choose how to display company matches" ) st.markdown("---") # Information section with st.expander("ℹ️ About HRHUB", expanded=False): st.markdown(""" **HRHUB** is a bilateral HR matching system that uses: - πŸ€– **NLP Embeddings**: Sentence transformers (384 dimensions) - πŸ“ **Cosine Similarity**: Scale-invariant matching - πŸŒ‰ **Job Postings Bridge**: Aligns candidate and company language **Key Innovation:** Companies enriched with job posting data speak the same "skills language" as candidates! """) with st.expander("πŸ“š How to Use", expanded=False): st.markdown(""" 1. **View Candidate Profile**: See the candidate's skills and background 2. **Explore Matches**: Review top company matches with scores 3. **Network Graph**: Visualize connections interactively 4. **Company Details**: Click to see full company information """) st.markdown("---") # Version info st.caption(f"Version: {VERSION}") st.caption("Β© 2024 HRHUB Team") return top_k, min_score, view_mode def get_network_graph_data(candidate_id, matches): """Generate network graph data from matches.""" nodes = [] edges = [] # Add candidate node nodes.append({ 'id': f'C{candidate_id}', 'label': f'Candidate #{candidate_id}', 'color': '#4ade80', 'shape': 'dot', 'size': 30 }) # Add company nodes and edges for comp_id, score, comp_data in matches: nodes.append({ 'id': f'COMP{comp_id}', 'label': comp_data.get('name', f'Company {comp_id}')[:30], 'color': '#ff6b6b', 'shape': 'box', 'size': 20 }) edges.append({ 'from': f'C{candidate_id}', 'to': f'COMP{comp_id}', 'value': float(score) * 10, 'title': f'{score:.3f}' }) return {'nodes': nodes, 'edges': edges} def render_network_section(candidate_id: int, matches): """Render interactive network visualization section.""" st.markdown('
πŸ•ΈοΈ Network Visualization
', unsafe_allow_html=True) with st.spinner("Generating interactive network graph..."): # Get graph data graph_data = get_network_graph_data(candidate_id, matches) # Create HTML graph html_content = create_network_graph( nodes=graph_data['nodes'], edges=graph_data['edges'], height="600px" ) # Display in Streamlit components.html(html_content, height=620, scrolling=False) # Graph instructions with st.expander("πŸ“– Graph Controls", expanded=False): st.markdown(""" **How to interact with the graph:** - πŸ–±οΈ **Drag nodes**: Click and drag to reposition - πŸ” **Zoom**: Scroll to zoom in/out - πŸ‘† **Pan**: Click background and drag to pan - 🎯 **Hover**: Hover over nodes and edges for details **Legend:** - 🟒 **Green circles**: Candidates - πŸ”΄ **Red squares**: Companies - **Line thickness**: Match strength (thicker = better match) """) def render_matches_section(matches, view_mode: str): """Render company matches section with different view modes.""" st.markdown('
🎯 Company Matches
', unsafe_allow_html=True) if view_mode == "πŸ“Š Overview": # Table view display_match_table(matches) elif view_mode == "πŸ” Detailed Cards": # Card view - detailed for rank, (comp_id, score, comp_data) in enumerate(matches, 1): display_company_card(comp_data, score, rank) elif view_mode == "πŸ“ˆ Table View": # Compact table display_match_table(matches) def main(): """Main application entry point.""" # Configure page configure_page() # Render header render_header() # Render sidebar and get settings top_k, min_score, view_mode = render_sidebar() # Main content area st.markdown("---") # Load embeddings (cache in session state) if 'embeddings_loaded' not in st.session_state: with st.spinner("πŸ”„ Loading embeddings and data..."): cand_emb, comp_emb, cand_df, comp_df = load_embeddings() st.session_state.embeddings_loaded = True st.session_state.candidate_embeddings = cand_emb st.session_state.company_embeddings = comp_emb st.session_state.candidates_df = cand_df st.session_state.companies_df = comp_df st.success("βœ… Data loaded successfully!") # Load candidate data candidate_id = DEMO_CANDIDATE_ID candidate = st.session_state.candidates_df.iloc[candidate_id] # Load company matches matches_list = find_top_matches( candidate_id, st.session_state.candidate_embeddings, st.session_state.company_embeddings, st.session_state.companies_df, top_k ) # Format matches for display matches = [ (m['company_id'], m['score'], st.session_state.companies_df.iloc[m['company_id']]) for m in matches_list ] # Filter by minimum score matches = [(cid, score, cdata) for cid, score, cdata in matches if score >= min_score] if not matches: st.warning(f"No matches found above {min_score:.0%} threshold. Try lowering the minimum score.") return # Display statistics overview display_stats_overview(candidate, matches) # Create two columns for layout col1, col2 = st.columns([1, 2]) with col1: # Candidate profile section st.markdown('
πŸ‘€ Candidate Profile
', unsafe_allow_html=True) display_candidate_profile(candidate) with col2: # Matches section render_matches_section(matches, view_mode) st.markdown("---") # Network visualization (full width) render_network_section(candidate_id, matches) st.markdown("---") # Technical info expander with st.expander("πŸ”§ Technical Details", expanded=False): st.markdown(f""" **Current Configuration:** - Embedding Dimension: {EMBEDDING_DIMENSION} - Similarity Metric: Cosine Similarity - Top K Matches: {top_k} - Minimum Score: {min_score:.0%} - Candidates Loaded: {len(st.session_state.candidates_df):,} - Companies Loaded: {len(st.session_state.companies_df):,} **Algorithm:** 1. Load pre-computed embeddings (.npy files) 2. Calculate cosine similarity 3. Rank companies by similarity score 4. Return top-K matches """) if __name__ == "__main__": main()