Spaces:
Sleeping
Sleeping
| """ | |
| Streamlit web application for resume profile extraction | |
| """ | |
| import streamlit as st | |
| import os | |
| import json | |
| import traceback | |
| import base64 | |
| import logging | |
| from typing import Dict, Any | |
| # Import from our refactored modules | |
| from agents import profile_extractor as pe, grammar_corrector as gc | |
| from utils import extract_text_from_pdf, save_temp_pdf | |
| from services import storage_service | |
| from models import Skill, Project, Education, SocialMedia, Experience, Category | |
| from config import get_settings | |
| # Get settings | |
| settings = get_settings() | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.DEBUG if settings.DEBUG else logging.INFO, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| profile_extractor=pe.ProfileExtractor() | |
| grammar_corrector=gc.GrammarCorrector() | |
| def collect_missing_data(profile): | |
| """Collects missing data from user input using a streamlined step-by-step form""" | |
| # Initialize step in session state if not exists | |
| if 'form_step' not in st.session_state: | |
| st.session_state.form_step = 1 | |
| # Progress bar | |
| total_steps = 4 # Reduced from 6 steps | |
| progress = st.progress((st.session_state.form_step - 1) / total_steps) | |
| # Create a dark-mode compatible step indicator | |
| st.markdown(f""" | |
| <div style='padding: 10px; background-color: rgba(100, 100, 100, 0.2); border-radius: 10px; margin-bottom: 20px; border: 1px solid rgba(150, 150, 150, 0.2);'> | |
| <h3 style='margin: 0; text-align: center; color: inherit;'>Step {st.session_state.form_step} of {total_steps}</h3> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Navigation buttons at bottom | |
| nav_col1, nav_col2, nav_col3 = st.columns([1, 2, 1]) | |
| # Step content | |
| if st.session_state.form_step == 1: | |
| st.subheader("πΈ Profile Information") | |
| col1, col2 = st.columns([1, 2]) | |
| with col1: | |
| # Profile image upload with preview | |
| uploaded_file = st.file_uploader("Upload a profile image", type=["jpg", "jpeg", "png"]) | |
| if uploaded_file: | |
| bytes_data = uploaded_file.getvalue() | |
| encoded = base64.b64encode(bytes_data).decode() | |
| profile.profileImg = f"data:image/{uploaded_file.type.split('/')[-1]};base64,{encoded}" | |
| st.image(uploaded_file, caption="Preview", width=150) | |
| with col2: | |
| # Basic info with validation | |
| profile.name = st.text_input("Full Name*", value=profile.name if profile.name != "N/A" else "", | |
| help="Enter your full name as you want it to appear on your portfolio") | |
| profile.title = st.text_input("Professional Title*", value=profile.title if profile.title != "N/A" else "") | |
| profile.email = st.text_input("Email Address*", value=profile.email if profile.email != "N/A" else "", | |
| help="This will be used as your contact email") | |
| profile.bio = st.text_area("Professional Bio*", value=profile.bio if profile.bio != "N/A" else "", | |
| help="Write a brief professional summary (50-100 words)") | |
| if profile.bio: | |
| if st.button("β¨ Improve Bio Grammar"): | |
| profile.bio = grammar_corrector.correct_grammar(profile.bio) | |
| st.success("Grammar corrected!") | |
| elif st.session_state.form_step == 2: | |
| st.subheader("π Education & Experience") | |
| tab1, tab2 = st.tabs(["Education", "Work Experience"]) | |
| with tab1: | |
| education_editor(profile) | |
| with tab2: | |
| experience_editor(profile) | |
| elif st.session_state.form_step == 3: | |
| st.subheader("π Projects") | |
| project_editor(profile) | |
| elif st.session_state.form_step == 4: | |
| st.subheader("π― Skills & Social Media") | |
| tab1, tab2 = st.tabs(["Skills", "Social Media"]) | |
| with tab1: | |
| skills_editor(profile) | |
| with tab2: | |
| social_media_editor(profile) | |
| # Navigation buttons at the bottom | |
| with nav_col1: | |
| if st.session_state.form_step > 1: | |
| if st.button("β Previous", use_container_width=True): | |
| st.session_state.form_step -= 1 | |
| st.rerun() | |
| with nav_col3: | |
| if st.session_state.form_step < total_steps: | |
| if st.button("Next β", use_container_width=True, type="primary"): | |
| st.session_state.form_step += 1 | |
| st.rerun() | |
| else: | |
| if st.button("Save Profile", use_container_width=True, type="primary"): | |
| st.session_state.profile = profile | |
| st.session_state.user_input_complete = True | |
| st.rerun() | |
| return profile | |
| def display_success_message(inserted_id, frontend_url): | |
| """Displays a focused success message highlighting all portfolio templates""" | |
| st.balloons() # Add some celebration! | |
| # Create styles for the portfolio templates display | |
| st.markdown(""" | |
| <style> | |
| /* ... existing styles ... */ | |
| .templates-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | |
| gap: 20px; /* Increased gap */ | |
| } | |
| .template-card { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; /* Align content vertically */ | |
| background-color: rgba(255, 255, 255, 0.07); /* Slightly lighter background */ | |
| border: 1px solid rgba(150, 150, 150, 0.2); | |
| border-radius: 12px; /* More rounded corners */ | |
| overflow: hidden; | |
| transition: transform 0.3s, box-shadow 0.3s; | |
| padding: 20px; /* Add padding directly to the card */ | |
| min-height: 180px; /* Ensure a minimum height */ | |
| } | |
| .template-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); /* Enhanced shadow */ | |
| } | |
| /* Remove template-image class */ | |
| .template-info { | |
| padding: 0; /* Remove padding as it's now on the card */ | |
| display: flex; | |
| flex-direction: column; | |
| flex-grow: 1; /* Allow info to take available space */ | |
| } | |
| .template-name { | |
| font-size: 1.3rem; /* Larger name */ | |
| font-weight: 600; /* Slightly bolder */ | |
| margin-bottom: 8px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; /* Align badge to top */ | |
| line-height: 1.3; | |
| } | |
| .template-category { | |
| font-size: 0.9rem; | |
| opacity: 0.8; | |
| margin-bottom: auto; /* Push link to the bottom */ | |
| padding-bottom: 15px; /* Space above the link */ | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .template-link { | |
| display: block; /* Make link full width */ | |
| background-color: #3498db; /* Updated color */ | |
| color: white !important; | |
| padding: 10px 16px; /* Adjusted padding */ | |
| border-radius: 8px; /* More rounded button */ | |
| text-decoration: none; | |
| font-weight: 500; /* Medium weight */ | |
| transition: background-color 0.3s, transform 0.2s; | |
| text-align: center; | |
| margin-top: 10px; /* Ensure space from category */ | |
| } | |
| .template-link:hover { | |
| background-color: #2980b9; /* Darker hover */ | |
| transform: scale(1.02); /* Slight scale effect */ | |
| } | |
| .premium-badge { | |
| display: inline-block; | |
| background-color: #e67e22; /* Updated color */ | |
| color: white; | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| padding: 3px 8px; | |
| border-radius: 12px; | |
| margin-left: 8px; | |
| white-space: nowrap; /* Prevent wrapping */ | |
| align-self: flex-start; /* Align to top */ | |
| } | |
| /* ... rest of the existing styles ... */ | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Define all templates with their details | |
| templates = [ | |
| { | |
| "id": 1, | |
| "name": "Clean Portfolio", | |
| "category": "professional", | |
| "url": f"https://v0-recreate-figma-ui-dua1ta.vercel.app/{inserted_id}", | |
| "isPremium": False | |
| }, | |
| { | |
| "id": 2, | |
| "name": "Creative Designer", | |
| "category": "creative", | |
| "url": f"https://v0-recreate-figma-ui-f7.vercel.app/{inserted_id}", | |
| "isPremium": True | |
| }, | |
| { | |
| "id": 3, | |
| "name": "iPortfo", | |
| "category": "tech", | |
| "url": f"https://iportfo.netlify.app/{inserted_id}", | |
| "isPremium": False | |
| }, | |
| { | |
| "id": 5, | |
| "name": "Modern Portfolio", | |
| "category": "professional", | |
| "url": f"https://v0-modern-portfolio-react.vercel.app/{inserted_id}", | |
| "isPremium": False | |
| }, | |
| { | |
| "id": 6, # Renumbered IDs | |
| "name": "Data Science Portfolio", | |
| "category": "tech", | |
| "url": f"https://v0-portfolio-web-application.vercel.app/{inserted_id}", | |
| "isPremium": False | |
| }, | |
| { | |
| "id": 7, # Renumbered IDs | |
| "name": "VimFolio Portfolio", | |
| "category": "special", | |
| "url": f"https://v0-dynamic-portfolio-app.vercel.app/{inserted_id}", | |
| "isPremium": True | |
| }, | |
| { | |
| "id": 8, # Renumbered IDs | |
| "name": "AI Portfolio", | |
| "category": "special", | |
| "url": f"https://v0-portfolio-data-integration.vercel.app/{inserted_id}", | |
| "isPremium": True | |
| } | |
| ] | |
| # Group templates by category | |
| categories = {} | |
| for template in templates: | |
| category = template["category"].capitalize() | |
| if category not in categories: | |
| categories[category] = [] | |
| categories[category].append(template) | |
| # Success header | |
| st.markdown(""" | |
| <div class="success-header"> | |
| <h1>π Your Portfolio is Ready!</h1> | |
| <p>Choose from our diverse collection of beautiful portfolio templates</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Display templates by category | |
| st.markdown('<div class="templates-container">', unsafe_allow_html=True) | |
| for category, category_templates in categories.items(): | |
| st.markdown(f'<h2 class="category-title">{category} Templates</h2>', unsafe_allow_html=True) | |
| st.markdown('<div class="templates-grid">', unsafe_allow_html=True) | |
| for template in category_templates: | |
| premium_badge = '<span class="premium-badge">Premium</span>' if template["isPremium"] else '' | |
| # Removed the template-image div | |
| st.markdown(f""" | |
| <div class="template-card"> | |
| <div class="template-info"> | |
| <div class="template-name"> | |
| <span>{template["name"]}</span> {premium_badge} | |
| </div> | |
| <div class="template-category"> | |
| {category.upper()} | |
| </div> | |
| </div> | |
| <a href="{template["url"]}" target="_blank" class="template-link"> | |
| View Portfolio | |
| </a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # ... rest of the function (share section, portfolio ID) ... | |
| # Select primary URL for sharing (use iPortfo as default) | |
| primary_share_url = next((t["url"] for t in templates if t["name"] == "iPortfo"), templates[0]["url"]) | |
| # Share section | |
| st.markdown('<div class="share-section">', unsafe_allow_html=True) | |
| st.subheader("π€ Share Your Portfolio") | |
| # Define share messages | |
| linkedin_msg = "Excited to share my professional portfolio! Check out my experience and projects" | |
| twitter_msg = "Check out my professional portfolio showcasing my work and experience!" | |
| whatsapp_msg = "Hi! I wanted to share my professional portfolio with you. Take a look!" | |
| # Create share URLs | |
| linkedin_share_url = f"https://www.linkedin.com/sharing/share-offsite/?url={primary_share_url}&summary={linkedin_msg}" | |
| twitter_share_url = f"https://twitter.com/intent/tweet?text={twitter_msg}&url={primary_share_url}" | |
| whatsapp_share_url = f"https://wa.me/?text={whatsapp_msg}%20{primary_share_url}" | |
| # Share buttons | |
| st.markdown(f""" | |
| <div class="share-grid"> | |
| <a href="{linkedin_share_url}" target="_blank" class="share-button"> | |
| <div> | |
| <span style="font-size: 1.2rem;">LinkedIn</span> | |
| <br/> | |
| <small>Share with professionals</small> | |
| </div> | |
| </a> | |
| <a href="{twitter_share_url}" target="_blank" class="share-button"> | |
| <div> | |
| <span style="font-size: 1.2rem;">Twitter</span> | |
| <br/> | |
| <small>Share with your network</small> | |
| </div> | |
| </a> | |
| <a href="{whatsapp_share_url}" target="_blank" class="share-button"> | |
| <div> | |
| <span style="font-size: 1.2rem;">WhatsApp</span> | |
| <br/> | |
| <small>Share with friends</small> | |
| </div> | |
| </a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Portfolio ID | |
| st.markdown(f""" | |
| <div class="portfolio-id"> | |
| <p>Your Portfolio ID: <code>{inserted_id}</code></p> | |
| <small>Use this ID to access your portfolio from anywhere</small> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| def education_editor(profile): | |
| education_data = [] | |
| # Display existing education entries with edit options | |
| if profile.educations: | |
| for i, edu in enumerate(profile.educations): | |
| st.write(f"Education #{i+1}") | |
| school = st.text_input(f"School {i+1}:", value=edu.school, key=f"school_{i}") | |
| degree = st.text_input(f"Degree {i+1}:", value=edu.degree, key=f"degree_{i}") | |
| field = st.text_input(f"Field of Study {i+1}:", value=edu.fieldOfStudy, key=f"field_{i}") | |
| start = st.text_input(f"Start Date {i+1}:", value=edu.startDate, key=f"start_{i}") | |
| end = st.text_input(f"End Date {i+1}:", value=edu.endDate, key=f"end_{i}") | |
| education_data.append({ | |
| "school": school, | |
| "degree": degree, | |
| "fieldOfStudy": field, | |
| "startDate": start, | |
| "endDate": end | |
| }) | |
| # Option to add new education entries | |
| add_education = st.checkbox("Add more education", key="add_edu") | |
| if add_education: | |
| num_new_edu = st.number_input("Number of additional education entries:", min_value=1, max_value=5, value=1) | |
| offset = len(profile.educations) if profile.educations else 0 | |
| for i in range(int(num_new_edu)): | |
| st.write(f"Additional Education #{i+1}") | |
| school = st.text_input(f"School:", key=f"new_school_{offset+i}") | |
| degree = st.text_input(f"Degree:", key=f"new_degree_{offset+i}") | |
| field = st.text_input(f"Field of Study:", key=f"new_field_{offset+i}") | |
| start = st.text_input(f"Start Date:", key=f"new_start_{offset+i}") | |
| end = st.text_input(f"End Date:", key=f"new_end_{offset+i}") | |
| if school: # Only add if at least school is provided | |
| education_data.append({ | |
| "school": school, | |
| "degree": degree, | |
| "fieldOfStudy": field, | |
| "startDate": start, | |
| "endDate": end | |
| }) | |
| # Update profile with education data | |
| profile.educations = [] | |
| for edu_data in education_data: | |
| if edu_data["school"]: # Only add if school is provided | |
| profile.educations.append(Education(**edu_data)) | |
| def experience_editor(profile): | |
| experience_data = [] | |
| # Display existing experience entries with edit options | |
| if profile.experiences: | |
| for i, exp in enumerate(profile.experiences): | |
| st.write(f"Experience #{i+1}") | |
| company = st.text_input(f"Company {i+1}:", value=exp.company, key=f"company_{i}") | |
| position = st.text_input(f"Position {i+1}:", value=exp.position, key=f"position_{i}") | |
| start = st.text_input(f"Start Date {i+1}:", value=exp.startDate, key=f"exp_start_{i}") | |
| end = st.text_input(f"End Date {i+1}:", value=exp.endDate if exp.endDate else "", key=f"exp_end_{i}") | |
| description = st.text_area(f"Description {i+1}:", value=exp.description if exp.description else "", key=f"exp_desc_{i}") | |
| if company and position: # Only add if company and position are provided | |
| experience_data.append({ | |
| "company": company, | |
| "position": position, | |
| "startDate": start, | |
| "endDate": end if end else None, | |
| "description": description if description else None | |
| }) | |
| # Option to add new experience entries | |
| add_experience = st.checkbox("Add more work experience", key="add_exp") | |
| if add_experience: | |
| num_new_exp = st.number_input("Number of additional experiences:", min_value=1, max_value=10, value=1) | |
| offset = len(profile.experiences) if profile.experiences else 0 | |
| for i in range(int(num_new_exp)): | |
| st.write(f"Additional Experience #{i+1}") | |
| company = st.text_input(f"Company:", key=f"new_company_{offset+i}") | |
| position = st.text_input(f"Position:", key=f"new_position_{offset+i}") | |
| start = st.text_input(f"Start Date:", key=f"new_exp_start_{offset+i}") | |
| end = st.text_input(f"End Date:", key=f"new_exp_end_{offset+i}") | |
| description = st.text_area(f"Description:", key=f"new_exp_desc_{offset+i}") | |
| if company and position: # Only add if company and position are provided | |
| correct_grammar_btn = st.button(f"Correct Grammar for Experience #{i+1}") | |
| if correct_grammar_btn and description: | |
| description = grammar_corrector.correct_grammar(description) | |
| st.success("Grammar corrected!") | |
| experience_data.append({ | |
| "company": company, | |
| "position": position, | |
| "startDate": start, | |
| "endDate": end if end else None, | |
| "description": description if description else None | |
| }) | |
| # Update profile with experience data | |
| profile.experiences = [] | |
| for exp_data in experience_data: | |
| if exp_data["company"] and exp_data["position"]: # Only add if company and position are provided | |
| profile.experiences.append(Experience(**exp_data)) | |
| def project_editor(profile): | |
| project_data = [] | |
| # Display existing projects with edit options | |
| if profile.projects: | |
| for i, proj in enumerate(profile.projects): | |
| st.write(f"Project #{i+1}") | |
| title = st.text_input(f"Title:", value=proj.title, key=f"proj_title_{i}") | |
| description = st.text_area(f"Description:", value=proj.description, key=f"proj_desc_{i}") | |
| tech_stack = st.text_input(f"Tech Stack:", value=proj.techStack if proj.techStack else "", key=f"proj_tech_{i}") | |
| github_url = st.text_input(f"GitHub URL:", value=proj.githubUrl if proj.githubUrl else "", key=f"proj_git_{i}") | |
| demo_url = st.text_input(f"Demo URL:", value=proj.demoUrl if proj.demoUrl else "", key=f"proj_demo_{i}") | |
| if title and description: # Only add if title and description are provided | |
| project_data.append({ | |
| "title": title, | |
| "description": description, | |
| "techStack": tech_stack, | |
| "githubUrl": github_url if github_url else None, | |
| "demoUrl": demo_url if demo_url else None | |
| }) | |
| # Option to add new projects | |
| add_project = st.checkbox("Add more projects", key="add_proj") | |
| if add_project: | |
| num_new_proj = st.number_input("Number of additional projects:", min_value=1, max_value=5, value=1) | |
| offset = len(profile.projects) if profile.projects else 0 | |
| for i in range(int(num_new_proj)): | |
| st.write(f"Additional Project #{i+1}") | |
| title = st.text_input(f"Title:", key=f"new_proj_title_{offset+i}") | |
| description = st.text_area(f"Description:", key=f"new_proj_desc_{offset+i}") | |
| tech_stack = st.text_input(f"Tech Stack:", key=f"new_proj_tech_{offset+i}") | |
| github_url = st.text_input(f"GitHub URL (optional):", key=f"new_proj_git_{offset+i}") | |
| demo_url = st.text_input(f"Demo URL (optional):", key=f"new_proj_demo_{offset+i}") | |
| if title and description: # Only add if title and description are provided | |
| correct_grammar_btn = st.button(f"Correct Grammar for Project #{i+1}") | |
| if correct_grammar_btn: | |
| description = grammar_corrector.correct_grammar(description) | |
| st.success("Grammar corrected!") | |
| project_data.append({ | |
| "title": title, | |
| "description": description, | |
| "techStack": tech_stack, | |
| "githubUrl": github_url if github_url else None, | |
| "demoUrl": demo_url if demo_url else None | |
| }) | |
| # Update profile with project data | |
| profile.projects = [] | |
| for proj_data in project_data: | |
| if proj_data["title"] and proj_data["description"]: # Only add if title and description are provided | |
| profile.projects.append(Project(**proj_data)) | |
| def skills_editor(profile): | |
| # Group existing skills by category | |
| technical_skills = [skill.name for skill in profile.skills if skill.category == Category.TECHNICAL] | |
| soft_skills = [skill.name for skill in profile.skills if skill.category == Category.SOFT_SKILLS] | |
| domain_skills = [skill.name for skill in profile.skills if skill.category == Category.DOMAIN_KNOWLEDGE] | |
| uncategorized = [skill.name for skill in profile.skills if skill.category is None] | |
| # Allow editing of the skills by category | |
| st.write("Technical Skills") | |
| tech_skills_input = st.text_area("Enter your technical skills (comma separated):", | |
| value=", ".join(technical_skills) if technical_skills else "") | |
| st.write("Soft Skills") | |
| soft_skills_input = st.text_area("Enter your soft skills (comma separated):", | |
| value=", ".join(soft_skills) if soft_skills else "") | |
| st.write("Domain Knowledge") | |
| domain_skills_input = st.text_area("Enter your domain knowledge skills (comma separated):", | |
| value=", ".join(domain_skills) if domain_skills else "") | |
| st.write("Uncategorized Skills") | |
| uncategorized_input = st.text_area("Enter any other skills (comma separated):", | |
| value=", ".join(uncategorized) if uncategorized else "") | |
| # Update profile with skills data | |
| profile.skills = [] | |
| def add_skills_by_category(skills_input, category): | |
| if skills_input: | |
| skills_list = [skill.strip() for skill in skills_input.split(",")] | |
| for skill_name in skills_list: | |
| if skill_name: | |
| profile.skills.append(Skill(name=skill_name, category=category)) | |
| add_skills_by_category(tech_skills_input, Category.TECHNICAL) | |
| add_skills_by_category(soft_skills_input, Category.SOFT_SKILLS) | |
| add_skills_by_category(domain_skills_input, Category.DOMAIN_KNOWLEDGE) | |
| add_skills_by_category(uncategorized_input, None) | |
| def social_media_editor(profile): | |
| if not profile.social: | |
| profile.social = SocialMedia() | |
| st.subheader("Social Media Links") | |
| profile.social.linkedin = st.text_input("LinkedIn URL:", value=profile.social.linkedin if profile.social and profile.social.linkedin else "") | |
| profile.social.github = st.text_input("GitHub URL:", value=profile.social.github if profile.social and profile.social.github else "") | |
| profile.social.instagram = st.text_input("Instagram URL:", value=profile.social.instagram if profile.social and profile.social.instagram else "") | |
| def display_profile(profile): | |
| """ | |
| Displays a profile in the Streamlit UI | |
| Args: | |
| profile: The Profile object to display | |
| """ | |
| st.header("Your Complete Profile") | |
| # Display profile image if available | |
| if profile.profileImg: | |
| st.image(profile.profileImg, width=150) | |
| # Display basic info in a table | |
| basic_data = { | |
| "Field": ["Name", "Title", "Email", "Bio", "Tagline"], | |
| "Value": [ | |
| profile.name, | |
| profile.title, | |
| profile.email, | |
| profile.bio, | |
| profile.tagline if profile.tagline else "" | |
| ] | |
| } | |
| st.table(basic_data) | |
| # Display social media if available | |
| if profile.social: | |
| social_data = { | |
| "Platform": ["LinkedIn", "GitHub", "Instagram"], | |
| "URL": [ | |
| profile.social.linkedin if profile.social.linkedin else "", | |
| profile.social.github if profile.social.github else "", | |
| profile.social.instagram if profile.social.instagram else "" | |
| ] | |
| } | |
| st.subheader("Social Media") | |
| st.table(social_data) | |
| # Display education in a table if available | |
| if profile.educations: | |
| education_data = { | |
| "School": [edu.school for edu in profile.educations], | |
| "Degree": [edu.degree for edu in profile.educations], | |
| "Field of Study": [edu.fieldOfStudy for edu in profile.educations], | |
| "Start Date": [edu.startDate for edu in profile.educations], | |
| "End Date": [edu.endDate for edu in profile.educations] | |
| } | |
| st.subheader("Education") | |
| st.table(education_data) | |
| # Display projects in a table if available | |
| if profile.projects: | |
| projects_data = { | |
| "Title": [project.title for project in profile.projects], | |
| "Description": [project.description for project in profile.projects], | |
| "Tech Stack": [project.techStack if project.techStack else "" for project in profile.projects], | |
| "GitHub": [project.githubUrl if project.githubUrl else "" for project in profile.projects], | |
| "Demo": [project.demoUrl if project.demoUrl else "" for project in profile.projects] | |
| } | |
| st.subheader("Projects") | |
| st.table(projects_data) | |
| # Display work experience in a table if available | |
| if profile.experiences: | |
| experiences_data = { | |
| "Company": [exp.company for exp in profile.experiences], | |
| "Position": [exp.position for exp in profile.experiences], | |
| "Start Date": [exp.startDate for exp in profile.experiences], | |
| "End Date": [exp.endDate if exp.endDate else "Present" for exp in profile.experiences], | |
| "Description": [exp.description if exp.description else "" for exp in profile.experiences] | |
| } | |
| st.subheader("Work Experience") | |
| st.table(experiences_data) | |
| # Display skills categorized by type if available | |
| if profile.skills: | |
| st.subheader("Skills") | |
| technical = [skill.name for skill in profile.skills if skill.category == Category.TECHNICAL] | |
| soft = [skill.name for skill in profile.skills if skill.category == Category.SOFT_SKILLS] | |
| domain = [skill.name for skill in profile.skills if skill.category == Category.DOMAIN_KNOWLEDGE] | |
| other = [skill.name for skill in profile.skills if skill.category is None] | |
| if technical: | |
| st.write("**Technical Skills:**") | |
| st.write(", ".join(technical)) | |
| if soft: | |
| st.write("**Soft Skills:**") | |
| st.write(", ".join(soft)) | |
| if domain: | |
| st.write("**Domain Knowledge:**") | |
| st.write(", ".join(domain)) | |
| if other: | |
| st.write("**Other Skills:**") | |
| st.write(", ".join(other)) | |
| def main(): | |
| """Main application function""" | |
| st.set_page_config(page_title="Resume Profile Extractor", page_icon="π", layout="wide") | |
| st.title("Professional Profile Extractor") | |
| st.write("Upload a resume PDF to extract professional profile information") | |
| # Initialize session state variables | |
| if 'profile' not in st.session_state: | |
| st.session_state.profile = None | |
| if 'extraction_complete' not in st.session_state: | |
| st.session_state.extraction_complete = False | |
| if 'user_input_complete' not in st.session_state: | |
| st.session_state.user_input_complete = False | |
| if 'profile_saved' not in st.session_state: | |
| st.session_state.profile_saved = False | |
| # Step 1: Upload PDF and Extract Profile | |
| if not st.session_state.extraction_complete: | |
| uploaded_file = st.file_uploader("Upload a PDF resume", type="pdf") | |
| if uploaded_file is not None: | |
| try: | |
| # Save the uploaded file to a temporary location | |
| pdf_path = save_temp_pdf(uploaded_file.getvalue()) | |
| # Extract text from the PDF | |
| pdf_text = extract_text_from_pdf(pdf_path) | |
| if not pdf_text: | |
| st.error("Could not extract text from the PDF. The file might be scanned or protected.") | |
| else: | |
| with st.spinner("Extracting profile information..."): | |
| # Extract profile information using the profile extractor agent | |
| profile = profile_extractor.extract_profile(pdf_text) | |
| st.session_state.profile = profile | |
| st.session_state.extraction_complete = True | |
| st.rerun() | |
| # Clean up temporary file | |
| if os.path.exists(pdf_path): | |
| os.remove(pdf_path) | |
| except Exception as e: | |
| logger.error(f"Error during profile extraction: {e}") | |
| st.error(f"An error occurred during profile extraction: {str(e)}") | |
| if "403" in str(e): | |
| st.error("Authorization error (403 Forbidden). Please check your API key and permissions.") | |
| with st.expander("Technical Details"): | |
| st.code(traceback.format_exc()) | |
| # Step 2: Allow User to Edit/Complete the Profile | |
| elif not st.session_state.user_input_complete: | |
| st.info("We've extracted information from your resume. Please complete your profile by filling in the form below.") | |
| # Call the function to collect and complete missing data | |
| profile = collect_missing_data(st.session_state.profile) | |
| # The submit button is now inside the collect_missing_data function | |
| # at the final step, so we don't need separate buttons here | |
| # Only add a button to start over | |
| if st.button("Start Over"): | |
| st.session_state.profile = None | |
| st.session_state.extraction_complete = False | |
| if 'form_step' in st.session_state: | |
| del st.session_state.form_step | |
| st.rerun() | |
| # Step 3: Save Profile and Display Results | |
| elif not st.session_state.profile_saved: | |
| profile = st.session_state.profile | |
| try: | |
| # Store the profile using the storage service | |
| with st.spinner("Saving your profile..."): | |
| inserted_id = storage_service.store_profile( | |
| profile, | |
| error_handler=st.error | |
| ) | |
| if inserted_id: | |
| # Save ID and mark as saved in session state | |
| st.session_state.profile_saved = True | |
| st.session_state.inserted_id = inserted_id | |
| # Rerun to show success message | |
| st.rerun() | |
| else: | |
| st.error("Failed to save profile.") | |
| except Exception as e: | |
| logger.error(f"Error saving profile: {e}") | |
| st.error(f"Error saving profile: {str(e)}") | |
| with st.expander("Technical Details"): | |
| st.code(traceback.format_exc()) | |
| # Final state - display success and options for new profile | |
| else: | |
| # Show only the focused success message with portfolio links | |
| display_success_message( | |
| inserted_id=st.session_state.get('inserted_id'), | |
| frontend_url=None # Not needed anymore as we're using direct URLs | |
| ) | |
| # Option to create another profile | |
| if st.button("Create Another Profile", type="primary"): | |
| for key in ['profile', 'extraction_complete', 'user_input_complete', 'profile_saved', 'form_step', 'inserted_id']: | |
| if key in st.session_state: | |
| del st.session_state[key] | |
| st.rerun() | |
| if __name__ == "__main__": | |
| main() |