Spaces:
Sleeping
Sleeping
| """ | |
| Smart Resume AI - Main Application | |
| """ | |
| import time | |
| from PIL import Image | |
| from jobs.job_search import render_job_search | |
| from datetime import datetime | |
| from ui_components import ( | |
| apply_modern_styles, hero_section, feature_card, about_section, | |
| page_header, render_analytics_section, render_activity_section, | |
| render_suggestions_section | |
| ) | |
| from feedback.feedback import FeedbackManager | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| from docx.shared import Inches, Pt | |
| from docx import Document | |
| import io | |
| import base64 | |
| import plotly.graph_objects as go | |
| from streamlit_lottie import st_lottie | |
| import requests | |
| from dashboard.dashboard import DashboardManager | |
| from config.courses import COURSES_BY_CATEGORY, RESUME_VIDEOS, INTERVIEW_VIDEOS, get_courses_for_role, get_category_for_role | |
| from config.job_roles import JOB_ROLES | |
| from config.database import ( | |
| get_database_connection, save_resume_data, save_analysis_data, | |
| init_database, verify_admin, log_admin_action, save_ai_analysis_data, | |
| get_ai_analysis_stats, reset_ai_analysis_stats, get_detailed_ai_analysis_stats | |
| ) | |
| from utils.ai_resume_analyzer import AIResumeAnalyzer | |
| from utils.resume_builder import ResumeBuilder | |
| from utils.resume_analyzer import ResumeAnalyzer | |
| import traceback | |
| import plotly.express as px | |
| import pandas as pd | |
| import json | |
| import streamlit as st | |
| import datetime | |
| # Set page config at the very beginning | |
| st.set_page_config( | |
| page_title="Smart Resume AI", | |
| page_icon="π", | |
| layout="wide" | |
| ) | |
| class ResumeApp: | |
| def __init__(self): | |
| """Initialize the application""" | |
| if 'form_data' not in st.session_state: | |
| st.session_state.form_data = { | |
| 'personal_info': { | |
| 'full_name': '', | |
| 'email': '', | |
| 'phone': '', | |
| 'location': '', | |
| 'linkedin': '', | |
| 'portfolio': '' | |
| }, | |
| 'summary': '', | |
| 'experiences': [], | |
| 'education': [], | |
| 'projects': [], | |
| 'skills_categories': { | |
| 'technical': [], | |
| 'soft': [], | |
| 'languages': [], | |
| 'tools': [] | |
| } | |
| } | |
| # Initialize navigation state | |
| if 'page' not in st.session_state: | |
| st.session_state.page = 'home' | |
| # Initialize admin state | |
| if 'is_admin' not in st.session_state: | |
| st.session_state.is_admin = False | |
| self.pages = { | |
| "π HOME": self.render_home, | |
| "π RESUME ANALYZER": self.render_analyzer, | |
| "π RESUME BUILDER": self.render_builder, | |
| "π DASHBOARD": self.render_dashboard, | |
| "π― JOB SEARCH": self.render_job_search, | |
| "π¬ FEEDBACK": self.render_feedback_page, | |
| "βΉοΈ ABOUT": self.render_about | |
| } | |
| # Initialize dashboard manager | |
| self.dashboard_manager = DashboardManager() | |
| self.analyzer = ResumeAnalyzer() | |
| self.ai_analyzer = AIResumeAnalyzer() | |
| self.builder = ResumeBuilder() | |
| self.job_roles = JOB_ROLES | |
| # Initialize session state | |
| if 'user_id' not in st.session_state: | |
| st.session_state.user_id = 'default_user' | |
| if 'selected_role' not in st.session_state: | |
| st.session_state.selected_role = None | |
| # Initialize database | |
| init_database() | |
| # Load external CSS | |
| with open('style/style.css') as f: | |
| st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True) | |
| # Load Google Fonts | |
| st.markdown(""" | |
| <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Poppins:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"> | |
| """, unsafe_allow_html=True) | |
| if 'resume_data' not in st.session_state: | |
| st.session_state.resume_data = [] | |
| if 'ai_analysis_stats' not in st.session_state: | |
| st.session_state.ai_analysis_stats = { | |
| 'score_distribution': {}, | |
| 'total_analyses': 0, | |
| 'average_score': 0 | |
| } | |
| def load_lottie_url(self, url: str): | |
| """Load Lottie animation from URL""" | |
| r = requests.get(url) | |
| if r.status_code != 200: | |
| return None | |
| return r.json() | |
| def apply_global_styles(self): | |
| st.markdown(""" | |
| <style> | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #1a1a1a; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #4CAF50; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #45a049; | |
| } | |
| /* Global Styles */ | |
| .main-header { | |
| background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); | |
| padding: 2rem; | |
| border-radius: 15px; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.2); | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .main-header::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(45deg, transparent 0%, rgba(255,255,255,0.1) 100%); | |
| z-index: 1; | |
| } | |
| .main-header h1 { | |
| color: white; | |
| font-size: 2.5rem; | |
| font-weight: 600; | |
| margin: 0; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| /* Template Card Styles */ | |
| .template-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); | |
| gap: 2rem; | |
| padding: 1rem; | |
| } | |
| .template-card { | |
| background: rgba(45, 45, 45, 0.9); | |
| border-radius: 20px; | |
| padding: 2rem; | |
| position: relative; | |
| overflow: hidden; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .template-card:hover { | |
| transform: translateY(-10px); | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.3); | |
| border-color: #4CAF50; | |
| } | |
| .template-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(45deg, transparent 0%, rgba(76,175,80,0.1) 100%); | |
| z-index: 1; | |
| } | |
| .template-icon { | |
| font-size: 3rem; | |
| color: #4CAF50; | |
| margin-bottom: 1.5rem; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| .template-title { | |
| font-size: 1.8rem; | |
| font-weight: 600; | |
| color: white; | |
| margin-bottom: 1rem; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| .template-description { | |
| color: #aaa; | |
| margin-bottom: 1.5rem; | |
| position: relative; | |
| z-index: 2; | |
| line-height: 1.6; | |
| } | |
| /* Feature List Styles */ | |
| .feature-list { | |
| list-style: none; | |
| padding: 0; | |
| margin: 1.5rem 0; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| .feature-item { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| color: #ddd; | |
| font-size: 0.95rem; | |
| } | |
| .feature-icon { | |
| color: #4CAF50; | |
| margin-right: 0.8rem; | |
| font-size: 1.1rem; | |
| } | |
| /* Button Styles */ | |
| .action-button { | |
| background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); | |
| color: white; | |
| padding: 1rem 2rem; | |
| border-radius: 50px; | |
| border: none; | |
| font-weight: 500; | |
| cursor: pointer; | |
| width: 100%; | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| z-index: 2; | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .action-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(76,175,80,0.3); | |
| } | |
| .action-button::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.2) 50%, transparent 100%); | |
| transition: all 0.6s ease; | |
| } | |
| .action-button:hover::before { | |
| left: 100%; | |
| } | |
| /* Form Section Styles */ | |
| .form-section { | |
| background: rgba(45, 45, 45, 0.9); | |
| border-radius: 20px; | |
| padding: 2rem; | |
| margin: 2rem 0; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .form-section-title { | |
| font-size: 1.8rem; | |
| font-weight: 600; | |
| color: white; | |
| margin-bottom: 1.5rem; | |
| padding-bottom: 0.8rem; | |
| border-bottom: 2px solid #4CAF50; | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| .form-label { | |
| color: #ddd; | |
| font-weight: 500; | |
| margin-bottom: 0.8rem; | |
| display: block; | |
| } | |
| .form-input { | |
| width: 100%; | |
| padding: 1rem; | |
| border-radius: 10px; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| background: rgba(30, 30, 30, 0.9); | |
| color: white; | |
| transition: all 0.3s ease; | |
| } | |
| .form-input:focus { | |
| border-color: #4CAF50; | |
| box-shadow: 0 0 0 2px rgba(76,175,80,0.2); | |
| outline: none; | |
| } | |
| /* Skill Tags */ | |
| .skill-tag-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.8rem; | |
| margin-top: 1rem; | |
| } | |
| .skill-tag { | |
| background: rgba(76,175,80,0.1); | |
| color: #4CAF50; | |
| padding: 0.6rem 1.2rem; | |
| border-radius: 50px; | |
| border: 1px solid #4CAF50; | |
| font-size: 0.9rem; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| } | |
| .skill-tag:hover { | |
| background: #4CAF50; | |
| color: white; | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(76,175,80,0.2); | |
| } | |
| /* Progress Circle */ | |
| .progress-container { | |
| position: relative; | |
| width: 150px; | |
| height: 150px; | |
| margin: 2rem auto; | |
| } | |
| .progress-circle { | |
| transform: rotate(-90deg); | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .progress-circle circle { | |
| fill: none; | |
| stroke-width: 8; | |
| stroke-linecap: round; | |
| stroke: #4CAF50; | |
| transform-origin: 50% 50%; | |
| transition: all 0.3s ease; | |
| } | |
| .progress-text { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| color: white; | |
| } | |
| .main .block-container { | |
| padding-top: 2rem; | |
| padding-bottom: 2rem; | |
| } | |
| .feature-card { | |
| background-color: #1e1e1e; | |
| border-radius: 10px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| /* Animations */ | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .animate-slide-in { | |
| animation: slideIn 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .template-container { | |
| grid-template-columns: 1fr; | |
| } | |
| .main-header { | |
| padding: 1.5rem; | |
| } | |
| .main-header h1 { | |
| font-size: 2rem; | |
| } | |
| .template-card { | |
| padding: 1.5rem; | |
| } | |
| .action-button { | |
| padding: 0.8rem 1.6rem; | |
| } | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def add_footer(self): | |
| """Add a footer to all pages""" | |
| st.markdown("<hr style='margin-top: 50px; margin-bottom: 20px;'>", unsafe_allow_html=True) | |
| col1, col2, col3 = st.columns([1, 3, 1]) | |
| with col2: | |
| # GitHub star button with lottie animation | |
| st.markdown(""" | |
| <div style='display: flex; justify-content: center; align-items: center; margin-bottom: 10px;'> | |
| <a href='https://github.com/babaiii07' target='_blank' style='text-decoration: none;'> | |
| <div style='display: flex; align-items: center; background-color: #24292e; padding: 5px 10px; border-radius: 5px; transition: all 0.3s ease;'> | |
| <svg height="16" width="16" viewBox="0 0 16 16" version="1.1" style='margin-right: 5px;'> | |
| <path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z" fill="gold"></path> | |
| </svg> | |
| <span style='color: white; font-size: 14px;'>Star this repo</span> | |
| </div> | |
| </a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Footer text | |
| st.markdown(""" | |
| <p style='text-align: center;'> | |
| Powered by <b>Streamlit</b> and <b>Groq Llama 3.3 70B</b> | Developed by | |
| <a href="https://www.linkedin.com/in/parthib-karak-9a0bb1286" target="_blank" style='text-decoration: none; color: #FFFFFF'> | |
| <b>Parthib karak</b> | |
| </a> | |
| </p> | |
| <p style='text-align: center; font-size: 12px; color: #888888;'> | |
| "Every star counts! If you find this project helpful, please consider starring the repo to help it reach more people." | |
| </p> | |
| """, unsafe_allow_html=True) | |
| def load_image(self, image_name): | |
| """Load image from static directory""" | |
| try: | |
| image_path = f"c:/Users/shree/Downloads/smart-resume-ai/{image_name}" | |
| with open(image_path, "rb") as f: | |
| image_bytes = f.read() | |
| encoded = base64.b64encode(image_bytes).decode() | |
| return f"data:image/png;base64,{encoded}" | |
| except Exception as e: | |
| print(f"Error loading image {image_name}: {e}") | |
| return None | |
| def export_to_excel(self): | |
| """Export resume data to Excel""" | |
| conn = get_database_connection() | |
| # Get resume data with analysis | |
| query = """ | |
| SELECT | |
| rd.name, rd.email, rd.phone, rd.linkedin, rd.github, rd.portfolio, | |
| rd.summary, rd.target_role, rd.target_category, | |
| rd.education, rd.experience, rd.projects, rd.skills, | |
| ra.ats_score, ra.keyword_match_score, ra.format_score, ra.section_score, | |
| ra.missing_skills, ra.recommendations, | |
| rd.created_at | |
| FROM resume_data rd | |
| LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id | |
| """ | |
| try: | |
| # Read data into DataFrame | |
| df = pd.read_sql_query(query, conn) | |
| # Create Excel writer object | |
| output = io.BytesIO() | |
| with pd.ExcelWriter(output, engine='openpyxl') as writer: | |
| df.to_excel(writer, index=False, sheet_name='Resume Data') | |
| return output.getvalue() | |
| except Exception as e: | |
| print(f"Error exporting to Excel: {str(e)}") | |
| return None | |
| finally: | |
| conn.close() | |
| def render_dashboard(self): | |
| """Render the dashboard page""" | |
| self.dashboard_manager.render_dashboard() | |
| st.toast("Check out these repositories: [Awesome Hacking](https://github.com/Hunterdii/Awesome-Hacking)", icon="βΉοΈ") | |
| def render_empty_state(self, icon, message): | |
| """Render an empty state with icon and message""" | |
| return f""" | |
| <div style='text-align: center; padding: 2rem; color: #666;'> | |
| <i class='{icon}' style='font-size: 2rem; margin-bottom: 1rem; color: #00bfa5;'></i> | |
| <p style='margin: 0;'>{message}</p> | |
| </div> | |
| """ | |
| def analyze_resume(self, resume_text): | |
| """Analyze resume and store results""" | |
| analytics = self.analyzer.analyze_resume(resume_text) | |
| st.session_state.analytics_data = analytics | |
| return analytics | |
| def handle_resume_upload(self): | |
| """Handle resume upload and analysis""" | |
| uploaded_file = st.file_uploader( | |
| "Upload your resume", type=['pdf', 'docx']) | |
| if uploaded_file is not None: | |
| try: | |
| # Extract text from resume | |
| if uploaded_file.type == "application/pdf": | |
| resume_text = extract_text_from_pdf(uploaded_file) | |
| else: | |
| resume_text = extract_text_from_docx(uploaded_file) | |
| # Store resume data | |
| st.session_state.resume_data = { | |
| 'filename': uploaded_file.name, | |
| 'content': resume_text, | |
| 'upload_time': datetime.now().isoformat() | |
| } | |
| # Analyze resume | |
| analytics = self.analyze_resume(resume_text) | |
| return True | |
| except Exception as e: | |
| st.error(f"Error processing resume: {str(e)}") | |
| return False | |
| return False | |
| def render_builder(self): | |
| st.title("Resume Builder π") | |
| st.write("Create your professional resume") | |
| # Template selection | |
| template_options = ["Modern", "Professional", "Minimal", "Creative"] | |
| selected_template = st.selectbox( | |
| "Select Resume Template", template_options) | |
| st.success(f"π¨ Currently using: {selected_template} Template") | |
| # Personal Information | |
| st.subheader("Personal Information") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Get existing values from session state | |
| existing_name = st.session_state.form_data['personal_info']['full_name'] | |
| existing_email = st.session_state.form_data['personal_info']['email'] | |
| existing_phone = st.session_state.form_data['personal_info']['phone'] | |
| # Input fields with existing values | |
| full_name = st.text_input("Full Name", value=existing_name) | |
| email = st.text_input( | |
| "Email", | |
| value=existing_email, | |
| key="email_input") | |
| phone = st.text_input("Phone", value=existing_phone) | |
| # Immediately update session state after email input | |
| if 'email_input' in st.session_state: | |
| st.session_state.form_data['personal_info']['email'] = st.session_state.email_input | |
| with col2: | |
| # Get existing values from session state | |
| existing_location = st.session_state.form_data['personal_info']['location'] | |
| existing_linkedin = st.session_state.form_data['personal_info']['linkedin'] | |
| existing_portfolio = st.session_state.form_data['personal_info']['portfolio'] | |
| # Input fields with existing values | |
| location = st.text_input("Location", value=existing_location) | |
| linkedin = st.text_input("LinkedIn URL", value=existing_linkedin) | |
| portfolio = st.text_input( | |
| "Portfolio Website", value=existing_portfolio) | |
| # Update personal info in session state | |
| st.session_state.form_data['personal_info'] = { | |
| 'full_name': full_name, | |
| 'email': email, | |
| 'phone': phone, | |
| 'location': location, | |
| 'linkedin': linkedin, | |
| 'portfolio': portfolio | |
| } | |
| # Professional Summary | |
| st.subheader("Professional Summary") | |
| summary = st.text_area("Professional Summary", value=st.session_state.form_data.get('summary', ''), height=150, | |
| help="Write a brief summary highlighting your key skills and experience") | |
| # Experience Section | |
| st.subheader("Work Experience") | |
| if 'experiences' not in st.session_state.form_data: | |
| st.session_state.form_data['experiences'] = [] | |
| if st.button("Add Experience"): | |
| st.session_state.form_data['experiences'].append({ | |
| 'company': '', | |
| 'position': '', | |
| 'start_date': '', | |
| 'end_date': '', | |
| 'description': '', | |
| 'responsibilities': [], | |
| 'achievements': [] | |
| }) | |
| for idx, exp in enumerate(st.session_state.form_data['experiences']): | |
| with st.expander(f"Experience {idx + 1}", expanded=True): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| exp['company'] = st.text_input( | |
| "Company Name", | |
| key=f"company_{idx}", | |
| value=exp.get( | |
| 'company', | |
| '')) | |
| exp['position'] = st.text_input( | |
| "Position", key=f"position_{idx}", value=exp.get( | |
| 'position', '')) | |
| with col2: | |
| exp['start_date'] = st.text_input( | |
| "Start Date", key=f"start_date_{idx}", value=exp.get( | |
| 'start_date', '')) | |
| exp['end_date'] = st.text_input( | |
| "End Date", key=f"end_date_{idx}", value=exp.get( | |
| 'end_date', '')) | |
| exp['description'] = st.text_area("Role Overview", key=f"desc_{idx}", | |
| value=exp.get( | |
| 'description', ''), | |
| help="Brief overview of your role and impact") | |
| # Responsibilities | |
| st.markdown("##### Key Responsibilities") | |
| resp_text = st.text_area("Enter responsibilities (one per line)", | |
| key=f"resp_{idx}", | |
| value='\n'.join( | |
| exp.get('responsibilities', [])), | |
| height=100, | |
| help="List your main responsibilities, one per line") | |
| exp['responsibilities'] = [r.strip() | |
| for r in resp_text.split('\n') if r.strip()] | |
| # Achievements | |
| st.markdown("##### Key Achievements") | |
| achv_text = st.text_area("Enter achievements (one per line)", | |
| key=f"achv_{idx}", | |
| value='\n'.join( | |
| exp.get('achievements', [])), | |
| height=100, | |
| help="List your notable achievements, one per line") | |
| exp['achievements'] = [a.strip() | |
| for a in achv_text.split('\n') if a.strip()] | |
| if st.button("Remove Experience", key=f"remove_exp_{idx}"): | |
| st.session_state.form_data['experiences'].pop(idx) | |
| st.rerun() | |
| # Projects Section | |
| st.subheader("Projects") | |
| if 'projects' not in st.session_state.form_data: | |
| st.session_state.form_data['projects'] = [] | |
| if st.button("Add Project"): | |
| st.session_state.form_data['projects'].append({ | |
| 'name': '', | |
| 'technologies': '', | |
| 'description': '', | |
| 'responsibilities': [], | |
| 'achievements': [], | |
| 'link': '' | |
| }) | |
| for idx, proj in enumerate(st.session_state.form_data['projects']): | |
| with st.expander(f"Project {idx + 1}", expanded=True): | |
| proj['name'] = st.text_input( | |
| "Project Name", | |
| key=f"proj_name_{idx}", | |
| value=proj.get( | |
| 'name', | |
| '')) | |
| proj['technologies'] = st.text_input("Technologies Used", key=f"proj_tech_{idx}", | |
| value=proj.get( | |
| 'technologies', ''), | |
| help="List the main technologies, frameworks, and tools used") | |
| proj['description'] = st.text_area("Project Overview", key=f"proj_desc_{idx}", | |
| value=proj.get( | |
| 'description', ''), | |
| help="Brief overview of the project and its goals") | |
| # Project Responsibilities | |
| st.markdown("##### Key Responsibilities") | |
| proj_resp_text = st.text_area("Enter responsibilities (one per line)", | |
| key=f"proj_resp_{idx}", | |
| value='\n'.join( | |
| proj.get('responsibilities', [])), | |
| height=100, | |
| help="List your main responsibilities in the project") | |
| proj['responsibilities'] = [r.strip() | |
| for r in proj_resp_text.split('\n') if r.strip()] | |
| # Project Achievements | |
| st.markdown("##### Key Achievements") | |
| proj_achv_text = st.text_area("Enter achievements (one per line)", | |
| key=f"proj_achv_{idx}", | |
| value='\n'.join( | |
| proj.get('achievements', [])), | |
| height=100, | |
| help="List the project's key achievements and your contributions") | |
| proj['achievements'] = [a.strip() | |
| for a in proj_achv_text.split('\n') if a.strip()] | |
| proj['link'] = st.text_input("Project Link (optional)", key=f"proj_link_{idx}", | |
| value=proj.get('link', ''), | |
| help="Link to the project repository, demo, or documentation") | |
| if st.button("Remove Project", key=f"remove_proj_{idx}"): | |
| st.session_state.form_data['projects'].pop(idx) | |
| st.rerun() | |
| # Education Section | |
| st.subheader("Education") | |
| if 'education' not in st.session_state.form_data: | |
| st.session_state.form_data['education'] = [] | |
| if st.button("Add Education"): | |
| st.session_state.form_data['education'].append({ | |
| 'school': '', | |
| 'degree': '', | |
| 'field': '', | |
| 'graduation_date': '', | |
| 'gpa': '', | |
| 'achievements': [] | |
| }) | |
| for idx, edu in enumerate(st.session_state.form_data['education']): | |
| with st.expander(f"Education {idx + 1}", expanded=True): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| edu['school'] = st.text_input( | |
| "School/University", | |
| key=f"school_{idx}", | |
| value=edu.get( | |
| 'school', | |
| '')) | |
| edu['degree'] = st.text_input( | |
| "Degree", key=f"degree_{idx}", value=edu.get( | |
| 'degree', '')) | |
| with col2: | |
| edu['field'] = st.text_input( | |
| "Field of Study", | |
| key=f"field_{idx}", | |
| value=edu.get( | |
| 'field', | |
| '')) | |
| edu['graduation_date'] = st.text_input("Graduation Date", key=f"grad_date_{idx}", | |
| value=edu.get('graduation_date', '')) | |
| edu['gpa'] = st.text_input( | |
| "GPA (optional)", | |
| key=f"gpa_{idx}", | |
| value=edu.get( | |
| 'gpa', | |
| '')) | |
| # Educational Achievements | |
| st.markdown("##### Achievements & Activities") | |
| edu_achv_text = st.text_area("Enter achievements (one per line)", | |
| key=f"edu_achv_{idx}", | |
| value='\n'.join( | |
| edu.get('achievements', [])), | |
| height=100, | |
| help="List academic achievements, relevant coursework, or activities") | |
| edu['achievements'] = [a.strip() | |
| for a in edu_achv_text.split('\n') if a.strip()] | |
| if st.button("Remove Education", key=f"remove_edu_{idx}"): | |
| st.session_state.form_data['education'].pop(idx) | |
| st.rerun() | |
| # Skills Section | |
| st.subheader("Skills") | |
| if 'skills_categories' not in st.session_state.form_data: | |
| st.session_state.form_data['skills_categories'] = { | |
| 'technical': [], | |
| 'soft': [], | |
| 'languages': [], | |
| 'tools': [] | |
| } | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| tech_skills = st.text_area("Technical Skills (one per line)", | |
| value='\n'.join( | |
| st.session_state.form_data['skills_categories']['technical']), | |
| height=150, | |
| help="Programming languages, frameworks, databases, etc.") | |
| st.session_state.form_data['skills_categories']['technical'] = [ | |
| s.strip() for s in tech_skills.split('\n') if s.strip()] | |
| soft_skills = st.text_area("Soft Skills (one per line)", | |
| value='\n'.join( | |
| st.session_state.form_data['skills_categories']['soft']), | |
| height=150, | |
| help="Leadership, communication, problem-solving, etc.") | |
| st.session_state.form_data['skills_categories']['soft'] = [ | |
| s.strip() for s in soft_skills.split('\n') if s.strip()] | |
| with col2: | |
| languages = st.text_area("Languages (one per line)", | |
| value='\n'.join( | |
| st.session_state.form_data['skills_categories']['languages']), | |
| height=150, | |
| help="Programming or human languages with proficiency level") | |
| st.session_state.form_data['skills_categories']['languages'] = [ | |
| l.strip() for l in languages.split('\n') if l.strip()] | |
| tools = st.text_area("Tools & Technologies (one per line)", | |
| value='\n'.join( | |
| st.session_state.form_data['skills_categories']['tools']), | |
| height=150, | |
| help="Development tools, software, platforms, etc.") | |
| st.session_state.form_data['skills_categories']['tools'] = [ | |
| t.strip() for t in tools.split('\n') if t.strip()] | |
| # Update form data in session state | |
| st.session_state.form_data.update({ | |
| 'summary': summary | |
| }) | |
| # Generate Resume button | |
| if st.button("Generate Resume π", type="primary"): | |
| print("Validating form data...") | |
| print(f"Session state form data: {st.session_state.form_data}") | |
| print(f"Email input value: {st.session_state.get('email_input', '')}") | |
| # Get the current values from form | |
| current_name = st.session_state.form_data['personal_info']['full_name'].strip( | |
| ) | |
| current_email = st.session_state.email_input if 'email_input' in st.session_state else '' | |
| print(f"Current name: {current_name}") | |
| print(f"Current email: {current_email}") | |
| # Validate required fields | |
| if not current_name: | |
| st.error("β οΈ Please enter your full name.") | |
| return | |
| if not current_email: | |
| st.error("β οΈ Please enter your email address.") | |
| return | |
| # Update email in form data one final time | |
| st.session_state.form_data['personal_info']['email'] = current_email | |
| try: | |
| print("Preparing resume data...") | |
| # Prepare resume data with current form values | |
| resume_data = { | |
| "personal_info": st.session_state.form_data['personal_info'], | |
| "summary": st.session_state.form_data.get('summary', '').strip(), | |
| "experience": st.session_state.form_data.get('experiences', []), | |
| "education": st.session_state.form_data.get('education', []), | |
| "projects": st.session_state.form_data.get('projects', []), | |
| "skills": st.session_state.form_data.get('skills_categories', { | |
| 'technical': [], | |
| 'soft': [], | |
| 'languages': [], | |
| 'tools': [] | |
| }), | |
| "template": selected_template | |
| } | |
| print(f"Resume data prepared: {resume_data}") | |
| try: | |
| # Generate resume | |
| resume_buffer = self.builder.generate_resume(resume_data) | |
| if resume_buffer: | |
| try: | |
| # Save resume data to database | |
| save_resume_data(resume_data) | |
| # Offer the resume for download | |
| st.success("β Resume generated successfully!") | |
| # Show snowflake effect | |
| st.snow() | |
| st.download_button( | |
| label="Download Resume π₯", | |
| data=resume_buffer, | |
| file_name=f"{current_name.replace(' ', '_')}_resume.docx", | |
| mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| on_click=lambda: st.balloons() | |
| ) | |
| except Exception as db_error: | |
| print(f"Warning: Failed to save to database: {str(db_error)}") | |
| # Still allow download even if database save fails | |
| st.warning( | |
| "β οΈ Resume generated but couldn't be saved to database") | |
| # Show balloons effect | |
| st.balloons() | |
| st.download_button( | |
| label="Download Resume π₯", | |
| data=resume_buffer, | |
| file_name=f"{current_name.replace(' ', '_')}_resume.docx", | |
| mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| on_click=lambda: st.balloons() | |
| ) | |
| else: | |
| st.error( | |
| "β Failed to generate resume. Please try again.") | |
| print("Resume buffer was None") | |
| except Exception as gen_error: | |
| print(f"Error during resume generation: {str(gen_error)}") | |
| print(f"Full traceback: {traceback.format_exc()}") | |
| st.error(f"β Error generating resume: {str(gen_error)}") | |
| except Exception as e: | |
| print(f"Error preparing resume data: {str(e)}") | |
| print(f"Full traceback: {traceback.format_exc()}") | |
| st.error(f"β Error preparing resume data: {str(e)}") | |
| st.toast("Check out these repositories: [30-Days-Of-Rust](https://github.com/Hunterdii/30-Days-Of-Rust)", icon="βΉοΈ") | |
| def render_about(self): | |
| """Render the about page""" | |
| # Apply modern styles | |
| from ui_components import apply_modern_styles | |
| import base64 | |
| import os | |
| # Function to load image as base64 | |
| def get_image_as_base64(file_path): | |
| try: | |
| with open(file_path, "rb") as image_file: | |
| encoded = base64.b64encode(image_file.read()).decode() | |
| return f"data:image/jpeg;base64,{encoded}" | |
| except: | |
| return None | |
| # Get image path and convert to base64 | |
| image_path = os.path.join( | |
| os.path.dirname(__file__), | |
| "assets", | |
| "124852522.jpeg") | |
| image_base64 = get_image_as_base64(image_path) | |
| apply_modern_styles() | |
| # Add Font Awesome icons and custom CSS | |
| st.markdown(""" | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
| <style> | |
| .profile-section, .vision-section, .feature-card { | |
| text-align: center; | |
| padding: 2rem; | |
| background: rgba(45, 45, 45, 0.9); | |
| border-radius: 20px; | |
| margin: 2rem auto; | |
| max-width: 800px; | |
| } | |
| .profile-image { | |
| width: 200px; | |
| height: 200px; | |
| border-radius: 50%; | |
| margin: 0 auto 1.5rem; | |
| display: block; | |
| object-fit: cover; | |
| border: 4px solid #4CAF50; | |
| } | |
| .profile-name { | |
| font-size: 2.5rem; | |
| color: white; | |
| margin-bottom: 0.5rem; | |
| } | |
| .profile-title { | |
| font-size: 1.2rem; | |
| color: #4CAF50; | |
| margin-bottom: 1.5rem; | |
| } | |
| .social-links { | |
| display: flex; | |
| justify-content: center; | |
| gap: 1.5rem; | |
| margin: 2rem 0; | |
| } | |
| .social-link { | |
| font-size: 2rem; | |
| color: #4CAF50; | |
| transition: all 0.3s ease; | |
| padding: 0.5rem; | |
| border-radius: 50%; | |
| background: rgba(76, 175, 80, 0.1); | |
| width: 60px; | |
| height: 60px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-decoration: none; | |
| } | |
| .social-link:hover { | |
| transform: translateY(-5px); | |
| background: #4CAF50; | |
| color: white; | |
| box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3); | |
| } | |
| .bio-text { | |
| color: #ddd; | |
| line-height: 1.8; | |
| font-size: 1.1rem; | |
| margin-top: 2rem; | |
| text-align: left; | |
| } | |
| .vision-text { | |
| color: #ddd; | |
| line-height: 1.8; | |
| font-size: 1.1rem; | |
| font-style: italic; | |
| margin: 1.5rem 0; | |
| text-align: left; | |
| } | |
| .vision-icon { | |
| font-size: 2.5rem; | |
| color: #4CAF50; | |
| margin-bottom: 1rem; | |
| } | |
| .vision-title { | |
| font-size: 2rem; | |
| color: white; | |
| margin-bottom: 1rem; | |
| } | |
| .features-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 2rem; | |
| margin: 2rem auto; | |
| max-width: 1200px; | |
| } | |
| .feature-card { | |
| padding: 2rem; | |
| margin: 0; | |
| } | |
| .feature-icon { | |
| font-size: 2.5rem; | |
| color: #4CAF50; | |
| margin-bottom: 1rem; | |
| } | |
| .feature-title { | |
| font-size: 1.5rem; | |
| color: white; | |
| margin: 1rem 0; | |
| } | |
| .feature-description { | |
| color: #ddd; | |
| line-height: 1.6; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Hero Section | |
| st.markdown(""" | |
| <div class="hero-section"> | |
| <h1 class="hero-title">About Smart Resume AI</h1> | |
| <p class="hero-subtitle">A powerful AI-driven platform for optimizing your resume</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Profile Section | |
| st.markdown(f""" | |
| <div class="profile-section"> | |
| <img src="{image_base64 if image_base64 else 'https://avatars.githubusercontent.com'}" | |
| alt="Parthib karak" | |
| class="profile-image" | |
| onerror="this.onerror=null; this.src='https://avatars.githubusercontent.com';"> | |
| <h2 class="profile-name">Parthib karak</h2> | |
| <p class="profile-title">Full Stack Developer & AI/ML Enthusiast</p> | |
| <div class="social-links"> | |
| <a href="https://github.com/babaiii07" class="social-link" target="_blank"> | |
| <i class="fab fa-github"></i> | |
| </a> | |
| <a href="https://www.linkedin.com/in/parthib-karak-9a0bb1286" class="social-link" target="_blank"> | |
| <i class="fab fa-linkedin"></i> | |
| </a> | |
| <a href="mailto:parthibkarak2004@gmail.com" class="social-link" target="_blank"> | |
| <i class="fas fa-envelope"></i> | |
| </a> | |
| </div> | |
| <p class="bio-text"> | |
| Hello! I'm a passionate Full Stack Developer with expertise in AI and Machine Learning. | |
| I created Smart Resume AI to revolutionize how job seekers approach their career journey. | |
| With my background in both software development and AI, I've designed this platform to | |
| provide intelligent, data-driven insights for resume optimization. | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Vision Section | |
| st.markdown(""" | |
| <div class="vision-section"> | |
| <i class="fas fa-lightbulb vision-icon"></i> | |
| <h2 class="vision-title">Our Vision</h2> | |
| <p class="vision-text"> | |
| "Smart Resume AI represents my vision of democratizing career advancement through technology. | |
| By combining cutting-edge AI with intuitive design, this platform empowers job seekers at | |
| every career stage to showcase their true potential and stand out in today's competitive job market." | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Features Section | |
| st.markdown(""" | |
| <div class="features-grid"> | |
| <div class="feature-card"> | |
| <i class="fas fa-robot feature-icon"></i> | |
| <h3 class="feature-title">AI-Powered Analysis</h3> | |
| <p class="feature-description"> | |
| Advanced AI algorithms provide detailed insights and suggestions to optimize your resume for maximum impact. | |
| </p> | |
| </div> | |
| <div class="feature-card"> | |
| <i class="fas fa-chart-line feature-icon"></i> | |
| <h3 class="feature-title">Data-Driven Insights</h3> | |
| <p class="feature-description"> | |
| Make informed decisions with our analytics-based recommendations and industry insights. | |
| </p> | |
| </div> | |
| <div class="feature-card"> | |
| <i class="fas fa-shield-alt feature-icon"></i> | |
| <h3 class="feature-title">Privacy First</h3> | |
| <p class="feature-description"> | |
| Your data security is our priority. We ensure your information is always protected and private. | |
| </p> | |
| </div> | |
| </div> | |
| <div style="text-align: center; margin: 3rem 0;"> | |
| <a href="?page=analyzer" class="cta-button"> | |
| Start Your Journey | |
| <i class="fas fa-arrow-right" style="margin-left: 10px;"></i> | |
| </a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.toast("Check out these repositories: [Iriswise](https://github.com/Hunterdii/Iriswise)", icon="βΉοΈ") | |
| def render_analyzer(self): | |
| """Render the resume analyzer page""" | |
| apply_modern_styles() | |
| # Page Header | |
| page_header( | |
| "Resume Analyzer", | |
| "Get instant AI-powered feedback to optimize your resume" | |
| ) | |
| # Create tabs for Normal Analyzer and AI Analyzer | |
| analyzer_tabs = st.tabs(["Standard Analyzer", "AI Analyzer"]) | |
| with analyzer_tabs[0]: | |
| # Job Role Selection | |
| categories = list(self.job_roles.keys()) | |
| selected_category = st.selectbox( | |
| "Job Category", categories, key="standard_category") | |
| roles = list(self.job_roles[selected_category].keys()) | |
| selected_role = st.selectbox( | |
| "Specific Role", roles, key="standard_role") | |
| role_info = self.job_roles[selected_category][selected_role] | |
| # Display role information | |
| st.markdown(f""" | |
| <div style='background-color: #1e1e1e; padding: 20px; border-radius: 10px; margin: 10px 0;'> | |
| <h3>{selected_role}</h3> | |
| <p>{role_info['description']}</p> | |
| <h4>Required Skills:</h4> | |
| <p>{', '.join(role_info['required_skills'])}</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # File Upload | |
| uploaded_file = st.file_uploader( | |
| "Upload your resume", type=[ | |
| 'pdf', 'docx'], key="standard_file") | |
| if not uploaded_file: | |
| # Display empty state with a prominent upload button | |
| st.markdown( | |
| self.render_empty_state( | |
| "fas fa-cloud-upload-alt", | |
| "Upload your resume to get started with standard analysis" | |
| ), | |
| unsafe_allow_html=True | |
| ) | |
| # Add a prominent upload button | |
| col1, col2, col3 = st.columns([1, 2, 1]) | |
| with col2: | |
| st.markdown(""" | |
| <style> | |
| .upload-button { | |
| background: linear-gradient(90deg, #4b6cb7, #182848); | |
| color: white; | |
| border: none; | |
| border-radius: 10px; | |
| padding: 15px 25px; | |
| font-size: 18px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| width: 100%; | |
| text-align: center; | |
| margin: 20px 0; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.2); | |
| transition: all 0.3s ease; | |
| } | |
| .upload-button:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 6px 15px rgba(0,0,0,0.3); | |
| } | |
| """, unsafe_allow_html=True) | |
| if uploaded_file: | |
| # Add a prominent analyze button | |
| analyze_standard = st.button("π Analyze My Resume", | |
| type="primary", | |
| use_container_width=True, | |
| key="analyze_standard_button") | |
| if analyze_standard: | |
| with st.spinner("Analyzing your document..."): | |
| # Get file content | |
| text = "" | |
| try: | |
| if uploaded_file.type == "application/pdf": | |
| try: | |
| text = self.analyzer.extract_text_from_pdf(uploaded_file) | |
| except Exception as pdf_error: | |
| st.error(f"PDF extraction failed: {str(pdf_error)}") | |
| st.info("Trying alternative PDF extraction method...") | |
| # Try AI analyzer as backup | |
| try: | |
| text = self.ai_analyzer.extract_text_from_pdf(uploaded_file) | |
| except Exception as backup_error: | |
| st.error(f"All PDF extraction methods failed: {str(backup_error)}") | |
| return | |
| elif uploaded_file.type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document": | |
| try: | |
| text = self.analyzer.extract_text_from_docx(uploaded_file) | |
| except Exception as docx_error: | |
| st.error(f"DOCX extraction failed: {str(docx_error)}") | |
| # Try AI analyzer as backup | |
| try: | |
| text = self.ai_analyzer.extract_text_from_docx(uploaded_file) | |
| except Exception as backup_error: | |
| st.error(f"All DOCX extraction methods failed: {str(backup_error)}") | |
| return | |
| else: | |
| text = uploaded_file.getvalue().decode() | |
| if not text or text.strip() == "": | |
| st.error("Could not extract any text from the uploaded file. Please try a different file.") | |
| return | |
| except Exception as e: | |
| st.error(f"Error reading file: {str(e)}") | |
| return | |
| # Analyze the document | |
| analysis = self.analyzer.analyze_resume({'raw_text': text}, role_info) | |
| # Check if analysis returned an error | |
| if 'error' in analysis: | |
| st.error(analysis['error']) | |
| return | |
| # Show snowflake effect | |
| st.snow() | |
| # Save resume data to database | |
| resume_data = { | |
| 'personal_info': { | |
| 'name': analysis.get('name', ''), | |
| 'email': analysis.get('email', ''), | |
| 'phone': analysis.get('phone', ''), | |
| 'linkedin': analysis.get('linkedin', ''), | |
| 'github': analysis.get('github', ''), | |
| 'portfolio': analysis.get('portfolio', '') | |
| }, | |
| 'summary': analysis.get('summary', ''), | |
| 'target_role': selected_role, | |
| 'target_category': selected_category, | |
| 'education': analysis.get('education', []), | |
| 'experience': analysis.get('experience', []), | |
| 'projects': analysis.get('projects', []), | |
| 'skills': analysis.get('skills', []), | |
| 'template': '' | |
| } | |
| # Save to database | |
| try: | |
| resume_id = save_resume_data(resume_data) | |
| # Save analysis data | |
| analysis_data = { | |
| 'resume_id': resume_id, | |
| 'ats_score': analysis['ats_score'], | |
| 'keyword_match_score': analysis['keyword_match']['score'], | |
| 'format_score': analysis['format_score'], | |
| 'section_score': analysis['section_score'], | |
| 'missing_skills': ','.join(analysis['keyword_match']['missing_skills']), | |
| 'recommendations': ','.join(analysis['suggestions']) | |
| } | |
| save_analysis_data(resume_id, analysis_data) | |
| st.success("Resume data saved successfully!") | |
| except Exception as e: | |
| st.error(f"Error saving to database: {str(e)}") | |
| print(f"Database error: {e}") | |
| # Show results based on document type | |
| if analysis.get('document_type') != 'resume': | |
| st.error( | |
| f"β οΈ This appears to be a {analysis['document_type']} document, not a resume!" | |
| ) | |
| st.warning( | |
| "Please upload a proper resume for ATS analysis.") | |
| return | |
| # Display results in a modern card layout | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # ATS Score Card with circular progress | |
| st.markdown(""" | |
| <div class="feature-card"> | |
| <h2>ATS Score</h2> | |
| <div style="position: relative; width: 150px; height: 150px; margin: 0 auto;"> | |
| <div style=" | |
| position: absolute; | |
| width: 150px; | |
| height: 150px; | |
| border-radius: 50%; | |
| background: conic-gradient( | |
| #4CAF50 0% {score}%, | |
| #2c2c2c {score}% 100% | |
| ); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| "> | |
| <div style=" | |
| width: 120px; | |
| height: 120px; | |
| background: #1a1a1a; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: {color}; | |
| "> | |
| {score} | |
| </div> | |
| </div> | |
| </div> | |
| <div style="text-align: center; margin-top: 10px;"> | |
| <span style=" | |
| font-size: 1.2em; | |
| color: {color}; | |
| font-weight: bold; | |
| "> | |
| {status} | |
| </span> | |
| </div> | |
| """.format( | |
| score=analysis['ats_score'], | |
| color='#4CAF50' if analysis['ats_score'] >= 80 else '#FFA500' if analysis[ | |
| 'ats_score'] >= 60 else '#FF4444', | |
| status='Excellent' if analysis['ats_score'] >= 80 else 'Good' if analysis[ | |
| 'ats_score'] >= 60 else 'Needs Improvement' | |
| ), unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # self.display_analysis_results(analysis_results) | |
| # Skills Match Card | |
| st.markdown(""" | |
| <div class="feature-card"> | |
| <h2>Skills Match</h2> | |
| """, unsafe_allow_html=True) | |
| st.metric( | |
| "Keyword Match", f"{int(analysis.get('keyword_match', {}).get('score', 0))}%") | |
| if analysis['keyword_match']['missing_skills']: | |
| st.markdown("#### Missing Skills:") | |
| for skill in analysis['keyword_match']['missing_skills']: | |
| st.markdown(f"- {skill}") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| with col2: | |
| # Format Score Card | |
| st.markdown(""" | |
| <div class="feature-card"> | |
| <h2>Format Analysis</h2> | |
| """, unsafe_allow_html=True) | |
| st.metric("Format Score", | |
| f"{int(analysis.get('format_score', 0))}%") | |
| st.metric("Section Score", | |
| f"{int(analysis.get('section_score', 0))}%") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Suggestions Card with improved UI | |
| st.markdown(""" | |
| <div class="feature-card"> | |
| <h2>π Resume Improvement Suggestions</h2> | |
| """, unsafe_allow_html=True) | |
| # Contact Section | |
| if analysis.get('contact_suggestions'): | |
| st.markdown(""" | |
| <div style='background-color: #1e1e1e; padding: 15px; border-radius: 10px; margin: 10px 0;'> | |
| <h3 style='color: #4CAF50; margin-bottom: 10px;'>π Contact Information</h3> | |
| <ul style='list-style-type: none; padding-left: 0;'> | |
| """, unsafe_allow_html=True) | |
| for suggestion in analysis.get( | |
| 'contact_suggestions', []): | |
| st.markdown( | |
| f"<li style='margin-bottom: 8px;'>β {suggestion}</li>", | |
| unsafe_allow_html=True) | |
| st.markdown( | |
| "</ul></div>", unsafe_allow_html=True) | |
| # Summary Section | |
| if analysis.get('summary_suggestions'): | |
| st.markdown(""" | |
| <div style='background-color: #1e1e1e; padding: 15px; border-radius: 10px; margin: 10px 0;'> | |
| <h3 style='color: #4CAF50; margin-bottom: 10px;'>π Professional Summary</h3> | |
| <ul style='list-style-type: none; padding-left: 0;'> | |
| """, unsafe_allow_html=True) | |
| for suggestion in analysis.get( | |
| 'summary_suggestions', []): | |
| st.markdown( | |
| f"<li style='margin-bottom: 8px;'>β {suggestion}</li>", | |
| unsafe_allow_html=True) | |
| st.markdown( | |
| "</ul></div>", unsafe_allow_html=True) | |
| # Skills Section | |
| if analysis.get( | |
| 'skills_suggestions') or analysis['keyword_match']['missing_skills']: | |
| st.markdown(""" | |
| <div style='background-color: #1e1e1e; padding: 15px; border-radius: 10px; margin: 10px 0;'> | |
| <h3 style='color: #4CAF50; margin-bottom: 10px;'>π― Skills</h3> | |
| <ul style='list-style-type: none; padding-left: 0;'> | |
| """, unsafe_allow_html=True) | |
| for suggestion in analysis.get( | |
| 'skills_suggestions', []): | |
| st.markdown( | |
| f"<li style='margin-bottom: 8px;'>β {suggestion}</li>", | |
| unsafe_allow_html=True) | |
| if analysis['keyword_match']['missing_skills']: | |
| st.markdown( | |
| "<li style='margin-bottom: 8px;'>β Consider adding these relevant skills:</li>", | |
| unsafe_allow_html=True) | |
| for skill in analysis['keyword_match']['missing_skills']: | |
| st.markdown( | |
| f"<li style='margin-left: 20px; margin-bottom: 4px;'>β’ {skill}</li>", | |
| unsafe_allow_html=True) | |
| st.markdown( | |
| "</ul></div>", unsafe_allow_html=True) | |
| # Experience Section | |
| if analysis.get('experience_suggestions'): | |
| st.markdown(""" | |
| <div style='background-color: #1e1e1e; padding: 15px; border-radius: 10px; margin: 10px 0;'> | |
| <h3 style='color: #4CAF50; margin-bottom: 10px;'>πΌ Work Experience</h3> | |
| <ul style='list-style-type: none; padding-left: 0;'> | |
| """, unsafe_allow_html=True) | |
| for suggestion in analysis.get( | |
| 'experience_suggestions', []): | |
| st.markdown( | |
| f"<li style='margin-bottom: 8px;'>β {suggestion}</li>", | |
| unsafe_allow_html=True) | |
| st.markdown( | |
| "</ul></div>", unsafe_allow_html=True) | |
| # Education Section | |
| if analysis.get('education_suggestions'): | |
| st.markdown(""" | |
| <div style='background-color: #1e1e1e; padding: 15px; border-radius: 10px; margin: 10px 0;'> | |
| <h3 style='color: #4CAF50; margin-bottom: 10px;'>π Education</h3> | |
| <ul style='list-style-type: none; padding-left: 0;'> | |
| """, unsafe_allow_html=True) | |
| for suggestion in analysis.get( | |
| 'education_suggestions', []): | |
| st.markdown( | |
| f"<li style='margin-bottom: 8px;'>β {suggestion}</li>", | |
| unsafe_allow_html=True) | |
| st.markdown( | |
| "</ul></div>", unsafe_allow_html=True) | |
| # General Formatting Suggestions | |
| if analysis.get('format_suggestions'): | |
| st.markdown(""" | |
| <div style='background-color: #1e1e1e; padding: 15px; border-radius: 10px; margin: 10px 0;'> | |
| <h3 style='color: #4CAF50; margin-bottom: 10px;'>π Formatting</h3> | |
| <ul style='list-style-type: none; padding-left: 0;'> | |
| """, unsafe_allow_html=True) | |
| for suggestion in analysis.get( | |
| 'format_suggestions', []): | |
| st.markdown( | |
| f"<li style='margin-bottom: 8px;'>β {suggestion}</li>", | |
| unsafe_allow_html=True) | |
| st.markdown( | |
| "</ul></div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Course Recommendations | |
| st.markdown(""" | |
| <div class="feature-card"> | |
| <h2>π Recommended Courses</h2> | |
| """, unsafe_allow_html=True) | |
| # Get courses based on role and category | |
| courses = get_courses_for_role(selected_role) | |
| if not courses: | |
| category = get_category_for_role(selected_role) | |
| courses = COURSES_BY_CATEGORY.get( | |
| category, {}).get(selected_role, []) | |
| # Display courses in a grid | |
| cols = st.columns(2) | |
| for i, course in enumerate( | |
| courses[:6]): # Show top 6 courses | |
| with cols[i % 2]: | |
| st.markdown(f""" | |
| <div style='background-color: #1e1e1e; padding: 15px; border-radius: 10px; margin: 10px 0;'> | |
| <h4>{course[0]}</h4> | |
| <a href='{course[1]}' target='_blank'>View Course</a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Learning Resources | |
| st.markdown(""" | |
| <div class="feature-card"> | |
| <h2>πΊ Helpful Videos</h2> | |
| """, unsafe_allow_html=True) | |
| tab1, tab2 = st.tabs(["Resume Tips", "Interview Tips"]) | |
| with tab1: | |
| # Resume Videos | |
| for category, videos in RESUME_VIDEOS.items(): | |
| st.subheader(category) | |
| cols = st.columns(2) | |
| for i, video in enumerate(videos): | |
| with cols[i % 2]: | |
| st.video(video[1]) | |
| with tab2: | |
| # Interview Videos | |
| for category, videos in INTERVIEW_VIDEOS.items(): | |
| st.subheader(category) | |
| cols = st.columns(2) | |
| for i, video in enumerate(videos): | |
| with cols[i % 2]: | |
| st.video(video[1]) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| with analyzer_tabs[1]: | |
| st.markdown(""" | |
| <div style='background-color: #1e1e1e; padding: 20px; border-radius: 10px; margin: 10px 0;'> | |
| <h3>AI-Powered Resume Analysis</h3> | |
| <p>Get detailed insights from advanced AI models that analyze your resume and provide personalized recommendations.</p> | |
| <p><strong>Upload your resume to get AI-powered analysis and recommendations.</strong></p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # AI Model Selection | |
| ai_model = st.selectbox( | |
| "Select AI Model", | |
| ["Groq Llama 3.3 70B"], | |
| help="Choose the AI model to analyze your resume" | |
| ) | |
| # Add job description input option | |
| use_custom_job_desc = st.checkbox("Use custom job description", value=False, | |
| help="Enable this to provide a specific job description for more targeted analysis") | |
| custom_job_description = "" | |
| if use_custom_job_desc: | |
| custom_job_description = st.text_area( | |
| "Paste the job description here", | |
| height=200, | |
| placeholder="Paste the full job description from the company here for more targeted analysis...", | |
| help="Providing the actual job description will help the AI analyze your resume specifically for this position" | |
| ) | |
| st.markdown(""" | |
| <div style='background-color: #2e7d32; padding: 15px; border-radius: 10px; margin: 10px 0;'> | |
| <p><i class="fas fa-lightbulb"></i> <strong>Pro Tip:</strong> Including the actual job description significantly improves the accuracy of the analysis and provides more relevant recommendations tailored to the specific position.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Add AI Analyzer Stats in an expander | |
| with st.expander("π AI Analyzer Statistics", expanded=False): | |
| try: | |
| # Add a reset button for admin users | |
| if st.session_state.get('is_admin', False): | |
| if st.button( | |
| "π Reset AI Analysis Statistics", | |
| type="secondary", | |
| key="reset_ai_stats_button_2"): | |
| from config.database import reset_ai_analysis_stats | |
| result = reset_ai_analysis_stats() | |
| if result["success"]: | |
| st.success(result["message"]) | |
| else: | |
| st.error(result["message"]) | |
| # Refresh the page to show updated stats | |
| st.experimental_rerun() | |
| # Get detailed AI analysis statistics | |
| from config.database import get_detailed_ai_analysis_stats | |
| ai_stats = get_detailed_ai_analysis_stats() | |
| if ai_stats["total_analyses"] > 0: | |
| # Create a more visually appealing layout | |
| st.markdown(""" | |
| <style> | |
| .stats-card { | |
| background: linear-gradient(135deg, #1e3c72, #2a5298); | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin-bottom: 15px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| text-align: center; | |
| } | |
| .stats-value { | |
| font-size: 28px; | |
| font-weight: bold; | |
| color: white; | |
| margin: 10px 0; | |
| } | |
| .stats-label { | |
| font-size: 14px; | |
| color: rgba(255, 255, 255, 0.8); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .score-card { | |
| background: linear-gradient(135deg, #11998e, #38ef7d); | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin-bottom: 15px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| text-align: center; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.markdown(f""" | |
| <div class="stats-card"> | |
| <div class="stats-label">Total AI Analyses</div> | |
| <div class="stats-value">{ai_stats["total_analyses"]}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| # Determine color based on score | |
| score_color = "#38ef7d" if ai_stats["average_score"] >= 80 else "#FFEB3B" if ai_stats[ | |
| "average_score"] >= 60 else "#FF5252" | |
| st.markdown(f""" | |
| <div class="stats-card" style="background: linear-gradient(135deg, #2c3e50, {score_color});"> | |
| <div class="stats-label">Average Resume Score</div> | |
| <div class="stats-value">{ai_stats["average_score"]}/100</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col3: | |
| # Create a gauge chart for average score | |
| import plotly.graph_objects as go | |
| fig = go.Figure(go.Indicator( | |
| mode="gauge+number", | |
| value=ai_stats["average_score"], | |
| domain={'x': [0, 1], 'y': [0, 1]}, | |
| title={ | |
| 'text': "Score", 'font': { | |
| 'size': 14, 'color': 'white'}}, | |
| gauge={ | |
| 'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': "white"}, | |
| 'bar': {'color': "#38ef7d" if ai_stats["average_score"] >= 80 else "#FFEB3B" if ai_stats["average_score"] >= 60 else "#FF5252"}, | |
| 'bgcolor': "rgba(0,0,0,0)", | |
| 'borderwidth': 2, | |
| 'bordercolor': "white", | |
| 'steps': [ | |
| {'range': [ | |
| 0, 40], 'color': 'rgba(255, 82, 82, 0.3)'}, | |
| {'range': [ | |
| 40, 70], 'color': 'rgba(255, 235, 59, 0.3)'}, | |
| {'range': [ | |
| 70, 100], 'color': 'rgba(56, 239, 125, 0.3)'} | |
| ], | |
| } | |
| )) | |
| fig.update_layout( | |
| paper_bgcolor='rgba(0,0,0,0)', | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| font={'color': "white"}, | |
| height=150, | |
| margin=dict(l=10, r=10, t=30, b=10) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Display model usage with enhanced visualization | |
| if ai_stats["model_usage"]: | |
| st.markdown("### π€ Model Usage") | |
| model_data = pd.DataFrame(ai_stats["model_usage"]) | |
| # Create a more colorful pie chart | |
| import plotly.express as px | |
| fig = px.pie( | |
| model_data, | |
| values="count", | |
| names="model", | |
| color_discrete_sequence=px.colors.qualitative.Bold, | |
| hole=0.4 | |
| ) | |
| fig.update_traces( | |
| textposition='inside', | |
| textinfo='percent+label', | |
| marker=dict( | |
| line=dict( | |
| color='#000000', | |
| width=1.5)) | |
| ) | |
| fig.update_layout( | |
| margin=dict(l=20, r=20, t=30, b=20), | |
| height=300, | |
| paper_bgcolor='rgba(0,0,0,0)', | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| font=dict(color="#ffffff", size=14), | |
| legend=dict( | |
| orientation="h", | |
| yanchor="bottom", | |
| y=-0.1, | |
| xanchor="center", | |
| x=0.5 | |
| ), | |
| title={ | |
| 'text': 'AI Model Distribution', | |
| 'y': 0.95, | |
| 'x': 0.5, | |
| 'xanchor': 'center', | |
| 'yanchor': 'top', | |
| 'font': {'size': 18, 'color': 'white'} | |
| } | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Display top job roles with enhanced visualization | |
| if ai_stats["top_job_roles"]: | |
| st.markdown("### π― Top Job Roles") | |
| roles_data = pd.DataFrame( | |
| ai_stats["top_job_roles"]) | |
| # Create a more colorful bar chart | |
| fig = px.bar( | |
| roles_data, | |
| x="role", | |
| y="count", | |
| color="count", | |
| color_continuous_scale=px.colors.sequential.Viridis, | |
| labels={ | |
| "role": "Job Role", "count": "Number of Analyses"} | |
| ) | |
| fig.update_traces( | |
| marker_line_width=1.5, | |
| marker_line_color="white", | |
| opacity=0.9 | |
| ) | |
| fig.update_layout( | |
| margin=dict(l=20, r=20, t=50, b=30), | |
| height=350, | |
| paper_bgcolor='rgba(0,0,0,0)', | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| font=dict(color="#ffffff", size=14), | |
| title={ | |
| 'text': 'Most Analyzed Job Roles', | |
| 'y': 0.95, | |
| 'x': 0.5, | |
| 'xanchor': 'center', | |
| 'yanchor': 'top', | |
| 'font': {'size': 18, 'color': 'white'} | |
| }, | |
| xaxis=dict( | |
| title="", | |
| tickangle=-45, | |
| tickfont=dict(size=12) | |
| ), | |
| yaxis=dict( | |
| title="Number of Analyses", | |
| gridcolor="rgba(255, 255, 255, 0.1)" | |
| ), | |
| coloraxis_showscale=False | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Add a timeline chart for analysis over time (mock | |
| # data for now) | |
| st.markdown("### π Analysis Trend") | |
| st.info( | |
| "This is a conceptual visualization. To implement actual time-based analysis, additional data collection would be needed.") | |
| # Create mock data for timeline | |
| import datetime | |
| import numpy as np | |
| today = datetime.datetime.now() | |
| dates = [ | |
| (today - | |
| datetime.timedelta( | |
| days=i)).strftime('%Y-%m-%d') for i in range(7)] | |
| dates.reverse() | |
| # Generate some random data that sums to | |
| # total_analyses | |
| total = ai_stats["total_analyses"] | |
| if total > 7: | |
| values = np.random.dirichlet( | |
| np.ones(7)) * total | |
| values = [round(v) for v in values] | |
| # Adjust to make sure sum equals total | |
| diff = total - sum(values) | |
| values[-1] += diff | |
| else: | |
| values = [0] * 7 | |
| for i in range(total): | |
| values[-(i % 7) - 1] += 1 | |
| trend_data = pd.DataFrame({ | |
| 'Date': dates, | |
| 'Analyses': values | |
| }) | |
| fig = px.line( | |
| trend_data, | |
| x='Date', | |
| y='Analyses', | |
| markers=True, | |
| line_shape='spline', | |
| color_discrete_sequence=["#38ef7d"] | |
| ) | |
| fig.update_traces( | |
| line=dict(width=3), | |
| marker=dict( | |
| size=8, line=dict( | |
| width=2, color='white')) | |
| ) | |
| fig.update_layout( | |
| margin=dict(l=20, r=20, t=50, b=30), | |
| height=300, | |
| paper_bgcolor='rgba(0,0,0,0)', | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| font=dict(color="#ffffff", size=14), | |
| title={ | |
| 'text': 'Analysis Activity (Last 7 Days)', | |
| 'y': 0.95, | |
| 'x': 0.5, | |
| 'xanchor': 'center', | |
| 'yanchor': 'top', | |
| 'font': {'size': 18, 'color': 'white'} | |
| }, | |
| xaxis=dict( | |
| title="", | |
| gridcolor="rgba(255, 255, 255, 0.1)" | |
| ), | |
| yaxis=dict( | |
| title="Number of Analyses", | |
| gridcolor="rgba(255, 255, 255, 0.1)" | |
| ) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Display score distribution if available | |
| if ai_stats["score_distribution"]: | |
| st.markdown(""" | |
| <h3 style='text-align: center; margin-bottom: 20px; background: linear-gradient(90deg, #4b6cb7, #182848); padding: 15px; border-radius: 10px; color: white; box-shadow: 0 4px 10px rgba(0,0,0,0.2);'> | |
| π Score Distribution Analysis | |
| </h3> | |
| """, unsafe_allow_html=True) | |
| score_data = pd.DataFrame( | |
| ai_stats["score_distribution"]) | |
| # Create a more visually appealing bar chart for | |
| # score distribution | |
| fig = px.bar( | |
| score_data, | |
| x="range", | |
| y="count", | |
| color="range", | |
| color_discrete_map={ | |
| "0-20": "#FF5252", | |
| "21-40": "#FF7043", | |
| "41-60": "#FFEB3B", | |
| "61-80": "#8BC34A", | |
| "81-100": "#38ef7d" | |
| }, | |
| labels={ | |
| "range": "Score Range", | |
| "count": "Number of Resumes"}, | |
| text="count" # Display count values on bars | |
| ) | |
| fig.update_traces( | |
| marker_line_width=2, | |
| marker_line_color="white", | |
| opacity=0.9, | |
| textposition='outside', | |
| textfont=dict( | |
| color="white", size=14, family="Arial, sans-serif"), | |
| hovertemplate="<b>Score Range:</b> %{x}<br><b>Number of Resumes:</b> %{y}<extra></extra>" | |
| ) | |
| # Add a gradient background to the chart | |
| fig.update_layout( | |
| margin=dict(l=20, r=20, t=50, b=30), | |
| height=400, # Increase height for better visibility | |
| paper_bgcolor='rgba(0,0,0,0)', | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| font=dict( | |
| color="#ffffff", size=14, family="Arial, sans-serif"), | |
| # title={ | |
| # # 'text': 'Resume Score Distribution', | |
| # 'y': 0.95, | |
| # 'x': 0.5, | |
| # 'xanchor': 'center', | |
| # 'yanchor': 'top', | |
| # 'font': {'size': 22, 'color': 'white', 'family': 'Arial, sans-serif', 'weight': 'bold'} | |
| # }, | |
| xaxis=dict( | |
| title=dict( | |
| text="Score Range", font=dict( | |
| size=16, color="white")), | |
| categoryorder="array", | |
| categoryarray=[ | |
| "0-20", "21-40", "41-60", "61-80", "81-100"], | |
| tickfont=dict(size=14, color="white"), | |
| gridcolor="rgba(255, 255, 255, 0.1)" | |
| ), | |
| yaxis=dict( | |
| title=dict( | |
| text="Number of Resumes", font=dict( | |
| size=16, color="white")), | |
| tickfont=dict(size=14, color="white"), | |
| gridcolor="rgba(255, 255, 255, 0.1)", | |
| zeroline=False | |
| ), | |
| showlegend=False, | |
| bargap=0.2, # Adjust gap between bars | |
| shapes=[ | |
| # Add gradient background | |
| dict( | |
| type="rect", | |
| xref="paper", | |
| yref="paper", | |
| x0=0, | |
| y0=0, | |
| x1=1, | |
| y1=1, | |
| fillcolor="rgba(26, 26, 44, 0.5)", | |
| layer="below", | |
| line_width=0, | |
| ) | |
| ] | |
| ) | |
| # Add annotations for insights | |
| if len(score_data) > 0: | |
| max_count_idx = score_data["count"].idxmax() | |
| max_range = score_data.iloc[max_count_idx]["range"] | |
| max_count = score_data.iloc[max_count_idx]["count"] | |
| fig.add_annotation( | |
| x=0.5, | |
| y=1.12, | |
| xref="paper", | |
| yref="paper", | |
| text=f"Most resumes fall in the {max_range} score range", | |
| showarrow=False, | |
| font=dict(size=14, color="#FFEB3B"), | |
| bgcolor="rgba(0,0,0,0.5)", | |
| bordercolor="#FFEB3B", | |
| borderwidth=1, | |
| borderpad=4, | |
| opacity=0.8 | |
| ) | |
| # Display the chart in a styled container | |
| st.markdown(""" | |
| <div style='background: linear-gradient(135deg, #1e3c72, #2a5298); padding: 20px; border-radius: 15px; margin: 10px 0; box-shadow: 0 5px 15px rgba(0,0,0,0.2);'> | |
| """, unsafe_allow_html=True) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Add descriptive text below the chart | |
| st.markdown(""" | |
| <p style='color: white; text-align: center; font-style: italic; margin-top: 10px;'> | |
| This chart shows the distribution of resume scores across different ranges, helping identify common performance levels. | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Display recent analyses if available | |
| if ai_stats["recent_analyses"]: | |
| st.markdown(""" | |
| <h3 style='text-align: center; margin-bottom: 20px; background: linear-gradient(90deg, #4b6cb7, #182848); padding: 15px; border-radius: 10px; color: white; box-shadow: 0 4px 10px rgba(0,0,0,0.2);'> | |
| π Recent Resume Analyses | |
| </h3> | |
| """, unsafe_allow_html=True) | |
| # Create a more modern styled table for recent | |
| # analyses | |
| st.markdown(""" | |
| <style> | |
| .modern-analyses-table { | |
| width: 100%; | |
| border-collapse: separate; | |
| border-spacing: 0 8px; | |
| margin-bottom: 20px; | |
| font-family: 'Arial', sans-serif; | |
| } | |
| .modern-analyses-table th { | |
| background: linear-gradient(135deg, #1e3c72, #2a5298); | |
| color: white; | |
| padding: 15px; | |
| text-align: left; | |
| font-weight: bold; | |
| font-size: 14px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| border-radius: 8px; | |
| } | |
| .modern-analyses-table td { | |
| padding: 15px; | |
| background-color: rgba(30, 30, 30, 0.7); | |
| border-top: 1px solid rgba(255, 255, 255, 0.05); | |
| border-bottom: 1px solid rgba(0, 0, 0, 0.2); | |
| color: white; | |
| } | |
| .modern-analyses-table tr td:first-child { | |
| border-top-left-radius: 8px; | |
| border-bottom-left-radius: 8px; | |
| } | |
| .modern-analyses-table tr td:last-child { | |
| border-top-right-radius: 8px; | |
| border-bottom-right-radius: 8px; | |
| } | |
| .modern-analyses-table tr:hover td { | |
| background-color: rgba(60, 60, 60, 0.7); | |
| transform: translateY(-2px); | |
| transition: all 0.2s ease; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
| } | |
| .model-badge { | |
| display: inline-block; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-weight: bold; | |
| text-align: center; | |
| font-size: 12px; | |
| letter-spacing: 0.5px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | |
| } | |
| .model-gemini { | |
| background: linear-gradient(135deg, #4e54c8, #8f94fb); | |
| color: white; | |
| } | |
| .model-claude { | |
| background: linear-gradient(135deg, #834d9b, #d04ed6); | |
| color: white; | |
| } | |
| .score-pill { | |
| display: inline-block; | |
| padding: 8px 15px; | |
| border-radius: 20px; | |
| font-weight: bold; | |
| text-align: center; | |
| min-width: 70px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | |
| } | |
| .score-high { | |
| background: linear-gradient(135deg, #11998e, #38ef7d); | |
| color: white; | |
| } | |
| .score-medium { | |
| background: linear-gradient(135deg, #f2994a, #f2c94c); | |
| color: white; | |
| } | |
| .score-low { | |
| background: linear-gradient(135deg, #cb2d3e, #ef473a); | |
| color: white; | |
| } | |
| .date-badge { | |
| display: inline-block; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| color: #e0e0e0; | |
| font-size: 12px; | |
| } | |
| .role-badge { | |
| display: inline-block; | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| background-color: rgba(33, 150, 243, 0.2); | |
| color: #90caf9; | |
| font-size: 13px; | |
| max-width: 200px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| </style> | |
| <div style='background: linear-gradient(135deg, #1e3c72, #2a5298); padding: 20px; border-radius: 15px; margin: 10px 0; box-shadow: 0 5px 15px rgba(0,0,0,0.2);'> | |
| <table class="modern-analyses-table"> | |
| <tr> | |
| <th>AI Model</th> | |
| <th>Score</th> | |
| <th>Job Role</th> | |
| <th>Date</th> | |
| </tr> | |
| """, unsafe_allow_html=True) | |
| for analysis in ai_stats["recent_analyses"]: | |
| score = analysis["score"] | |
| score_class = "score-high" if score >= 80 else "score-medium" if score >= 60 else "score-low" | |
| # Determine model class | |
| model_name = analysis["model"] | |
| model_class = "model-gemini" if "Gemini" in model_name else "model-claude" if "Claude" in model_name else "" | |
| # Format the date | |
| try: | |
| from datetime import datetime | |
| date_obj = datetime.strptime( | |
| analysis["date"], "%Y-%m-%d %H:%M:%S") | |
| formatted_date = date_obj.strftime( | |
| "%b %d, %Y") | |
| except: | |
| formatted_date = analysis["date"] | |
| st.markdown(f""" | |
| <tr> | |
| <td><div class="model-badge {model_class}">{model_name}</div></td> | |
| <td><div class="score-pill {score_class}">{score}/100</div></td> | |
| <td><div class="role-badge">{analysis["job_role"]}</div></td> | |
| <td><div class="date-badge">{formatted_date}</div></td> | |
| </tr> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| </table> | |
| <p style='color: white; text-align: center; font-style: italic; margin-top: 15px;'> | |
| These are the most recent resume analyses performed by our AI models. | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.info( | |
| "No AI analysis data available yet. Upload and analyze resumes to see statistics here.") | |
| except Exception as e: | |
| st.error(f"Error loading AI analysis statistics: {str(e)}") | |
| # Job Role Selection for AI Analysis | |
| categories = list(self.job_roles.keys()) | |
| selected_category = st.selectbox( | |
| "Job Category", categories, key="ai_category") | |
| roles = list(self.job_roles[selected_category].keys()) | |
| selected_role = st.selectbox("Specific Role", roles, key="ai_role") | |
| role_info = self.job_roles[selected_category][selected_role] | |
| # Display role information | |
| st.markdown(f""" | |
| <div style='background-color: #1e1e1e; padding: 20px; border-radius: 10px; margin: 10px 0;'> | |
| <h3>{selected_role}</h3> | |
| <p>{role_info['description']}</p> | |
| <h4>Required Skills:</h4> | |
| <p>{', '.join(role_info['required_skills'])}</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # File Upload for AI Analysis | |
| uploaded_file = st.file_uploader( | |
| "Upload your resume", type=[ | |
| 'pdf', 'docx'], key="ai_file") | |
| if not uploaded_file: | |
| # Display empty state with a prominent upload button | |
| st.markdown( | |
| self.render_empty_state( | |
| "fas fa-robot", | |
| "Upload your resume to get AI-powered analysis and recommendations" | |
| ), | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| # Add a prominent analyze button | |
| analyze_ai = st.button("π€ Analyze with AI", | |
| type="primary", | |
| use_container_width=True, | |
| key="analyze_ai_button") | |
| if analyze_ai: | |
| with st.spinner(f"Analyzing your resume with {ai_model}..."): | |
| # Get file content | |
| text = "" | |
| try: | |
| if uploaded_file.type == "application/pdf": | |
| text = self.analyzer.extract_text_from_pdf( | |
| uploaded_file) | |
| elif uploaded_file.type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document": | |
| text = self.analyzer.extract_text_from_docx( | |
| uploaded_file) | |
| else: | |
| text = uploaded_file.getvalue().decode() | |
| except Exception as e: | |
| st.error(f"Error reading file: {str(e)}") | |
| st.stop() | |
| # Analyze with AI | |
| try: | |
| # Show a loading animation | |
| with st.spinner("π§ AI is analyzing your resume..."): | |
| progress_bar = st.progress(0) | |
| # Get the selected model | |
| selected_model = "Groq Llama 3.3 70B" | |
| # Update progress | |
| progress_bar.progress(10) | |
| # Extract text from the resume | |
| analyzer = AIResumeAnalyzer() | |
| if uploaded_file.type == "application/pdf": | |
| resume_text = analyzer.extract_text_from_pdf( | |
| uploaded_file) | |
| elif uploaded_file.type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document": | |
| resume_text = analyzer.extract_text_from_docx( | |
| uploaded_file) | |
| else: | |
| # For text files or other formats | |
| resume_text = uploaded_file.getvalue().decode('utf-8') | |
| # Initialize the AI analyzer (moved after text extraction) | |
| progress_bar.progress(30) | |
| # Get the job role | |
| job_role = selected_role if selected_role else "Not specified" | |
| # Update progress | |
| progress_bar.progress(50) | |
| # Analyze the resume with Groq Llama 3.3 70B | |
| if use_custom_job_desc and custom_job_description: | |
| # Use custom job description for analysis | |
| analysis_result = analyzer.analyze_resume_with_gemini( | |
| resume_text, job_role=job_role, job_description=custom_job_description) | |
| # Show that custom job description was used | |
| st.session_state['used_custom_job_desc'] = True | |
| else: | |
| # Use standard role-based analysis | |
| analysis_result = analyzer.analyze_resume_with_gemini( | |
| resume_text, job_role=job_role) | |
| st.session_state['used_custom_job_desc'] = False | |
| # Update progress | |
| progress_bar.progress(80) | |
| # Save the analysis to the database | |
| if analysis_result and "error" not in analysis_result: | |
| # Extract the resume score | |
| resume_score = analysis_result.get( | |
| "resume_score", 0) | |
| # Save to database | |
| save_ai_analysis_data( | |
| None, # No user_id needed | |
| { | |
| "model_used": selected_model, | |
| "resume_score": resume_score, | |
| "job_role": job_role | |
| } | |
| ) | |
| # show snowflake effect | |
| st.snow() | |
| # Complete the progress | |
| progress_bar.progress(100) | |
| # Display the analysis result | |
| if analysis_result and "error" not in analysis_result: | |
| st.success("β Analysis complete!") | |
| # Extract data from the analysis | |
| full_response = analysis_result.get( | |
| "analysis", "") | |
| resume_score = analysis_result.get( | |
| "resume_score", 0) | |
| ats_score = analysis_result.get( | |
| "ats_score", 0) | |
| model_used = analysis_result.get( | |
| "model_used", selected_model) | |
| # Store the full response in session state for download | |
| st.session_state['full_analysis'] = full_response | |
| # Display the analysis in a nice format | |
| st.markdown("## Full Analysis Report") | |
| # Get current date | |
| from datetime import datetime | |
| current_date = datetime.now().strftime("%B %d, %Y") | |
| # Create a modern styled header for the report | |
| st.markdown(f""" | |
| <div style="background-color: #262730; padding: 20px; border-radius: 10px; margin-bottom: 20px;"> | |
| <h2 style="color: #ffffff; margin-bottom: 10px;">AI Resume Analysis Report</h2> | |
| <div style="display: flex; flex-wrap: wrap; gap: 20px;"> | |
| <div style="flex: 1; min-width: 200px;"> | |
| <p style="color: #ffffff;"><strong>Job Role:</strong> {job_role if job_role else "Not specified"}</p> | |
| <p style="color: #ffffff;"><strong>Analysis Date:</strong> {current_date}</p> </div> | |
| <div style="flex: 1; min-width: 200px;"> | |
| <p style="color: #ffffff;"><strong>AI Model:</strong> {model_used}</p> | |
| <p style="color: #ffffff;"><strong>Overall Score:</strong> {resume_score}/100 - {"Excellent" if resume_score >= 80 else "Good" if resume_score >= 60 else "Needs Improvement"}</p> | |
| {f'<p style="color: #4CAF50;"><strong>β Custom Job Description Used</strong></p>' if st.session_state.get('used_custom_job_desc', False) else ''} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Add gauge charts for scores | |
| import plotly.graph_objects as go | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Resume Score Gauge | |
| fig1 = go.Figure(go.Indicator( | |
| mode="gauge+number", | |
| value=resume_score, | |
| domain={'x': [0, 1], 'y': [0, 1]}, | |
| title={'text': "Resume Score", 'font': {'size': 16}}, | |
| gauge={ | |
| 'axis': {'range': [0, 100], 'tickwidth': 1}, | |
| 'bar': {'color': "#4CAF50" if resume_score >= 80 else "#FFA500" if resume_score >= 60 else "#FF4444"}, | |
| 'bgcolor': "white", | |
| 'borderwidth': 2, | |
| 'bordercolor': "gray", | |
| 'steps': [ | |
| {'range': [0, 40], 'color': 'rgba(255, 68, 68, 0.2)'}, | |
| {'range': [40, 60], 'color': 'rgba(255, 165, 0, 0.2)'}, | |
| {'range': [60, 80], 'color': 'rgba(255, 214, 0, 0.2)'}, | |
| {'range': [80, 100], 'color': 'rgba(76, 175, 80, 0.2)'} | |
| ], | |
| 'threshold': { | |
| 'line': {'color': "red", 'width': 4}, | |
| 'thickness': 0.75, | |
| 'value': 60 | |
| } | |
| } | |
| )) | |
| fig1.update_layout( | |
| height=250, | |
| margin=dict(l=20, r=20, t=50, b=20), | |
| ) | |
| st.plotly_chart(fig1, use_container_width=True) | |
| status = "Excellent" if resume_score >= 80 else "Good" if resume_score >= 60 else "Needs Improvement" | |
| st.markdown(f"<div style='text-align: center; font-weight: bold;'>{status}</div>", unsafe_allow_html=True) | |
| with col2: | |
| # ATS Score Gauge | |
| fig2 = go.Figure(go.Indicator( | |
| mode="gauge+number", | |
| value=ats_score, | |
| domain={'x': [0, 1], 'y': [0, 1]}, | |
| title={'text': "ATS Optimization Score", 'font': {'size': 16}}, | |
| gauge={ | |
| 'axis': {'range': [0, 100], 'tickwidth': 1}, | |
| 'bar': {'color': "#4CAF50" if ats_score >= 80 else "#FFA500" if ats_score >= 60 else "#FF4444"}, | |
| 'bgcolor': "white", | |
| 'borderwidth': 2, | |
| 'bordercolor': "gray", | |
| 'steps': [ | |
| {'range': [0, 40], 'color': 'rgba(255, 68, 68, 0.2)'}, | |
| {'range': [40, 60], 'color': 'rgba(255, 165, 0, 0.2)'}, | |
| {'range': [60, 80], 'color': 'rgba(255, 214, 0, 0.2)'}, | |
| {'range': [80, 100], 'color': 'rgba(76, 175, 80, 0.2)'} | |
| ], | |
| 'threshold': { | |
| 'line': {'color': "red", 'width': 4}, | |
| 'thickness': 0.75, | |
| 'value': 60 | |
| } | |
| } | |
| )) | |
| fig2.update_layout( | |
| height=250, | |
| margin=dict(l=20, r=20, t=50, b=20), | |
| ) | |
| st.plotly_chart(fig2, use_container_width=True) | |
| status = "Excellent" if ats_score >= 80 else "Good" if ats_score >= 60 else "Needs Improvement" | |
| st.markdown(f"<div style='text-align: center; font-weight: bold;'>{status}</div>", unsafe_allow_html=True) | |
| # Add Job Description Match Score if custom job description was used | |
| if st.session_state.get('used_custom_job_desc', False) and custom_job_description: | |
| # Extract job match score from analysis result or calculate it | |
| job_match_score = analysis_result.get("job_match_score", 0) | |
| if not job_match_score and "job_match" in analysis_result: | |
| job_match_score = analysis_result["job_match"].get("score", 0) | |
| # If we have a job match score, display it | |
| if job_match_score: | |
| st.markdown(""" | |
| <h3 style="background: linear-gradient(90deg, #4d7c0f, #84cc16); color: white; padding: 10px; border-radius: 5px; margin-top: 20px;"> | |
| <i class="fas fa-handshake"></i> Job Description Match Analysis | |
| </h3> | |
| """, unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Job Match Score Gauge | |
| fig3 = go.Figure(go.Indicator( | |
| mode="gauge+number", | |
| value=job_match_score, | |
| domain={'x': [0, 1], 'y': [0, 1]}, | |
| title={'text': "Job Match Score", 'font': {'size': 16}}, | |
| gauge={ | |
| 'axis': {'range': [0, 100], 'tickwidth': 1}, | |
| 'bar': {'color': "#4CAF50" if job_match_score >= 80 else "#FFA500" if job_match_score >= 60 else "#FF4444"}, | |
| 'bgcolor': "white", | |
| 'borderwidth': 2, | |
| 'bordercolor': "gray", | |
| 'steps': [ | |
| {'range': [0, 40], 'color': 'rgba(255, 68, 68, 0.2)'}, | |
| {'range': [40, 60], 'color': 'rgba(255, 165, 0, 0.2)'}, | |
| {'range': [60, 80], 'color': 'rgba(255, 214, 0, 0.2)'}, | |
| {'range': [80, 100], 'color': 'rgba(76, 175, 80, 0.2)'} | |
| ], | |
| 'threshold': { | |
| 'line': {'color': "red", 'width': 4}, | |
| 'thickness': 0.75, | |
| 'value': 60 | |
| } | |
| } | |
| )) | |
| fig3.update_layout( | |
| height=250, | |
| margin=dict(l=20, r=20, t=50, b=20), | |
| ) | |
| st.plotly_chart(fig3, use_container_width=True) | |
| match_status = "Excellent Match" if job_match_score >= 80 else "Good Match" if job_match_score >= 60 else "Low Match" | |
| st.markdown(f"<div style='text-align: center; font-weight: bold;'>{match_status}</div>", unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(""" | |
| <div style="background-color: #262730; padding: 20px; border-radius: 10px; height: 100%;"> | |
| <h4 style="color: #ffffff; margin-bottom: 15px;">What This Means</h4> | |
| <p style="color: #ffffff;">This score represents how well your resume matches the specific job description you provided.</p> | |
| <ul style="color: #ffffff; padding-left: 20px;"> | |
| <li><strong>80-100:</strong> Excellent match - your resume is highly aligned with this job</li> | |
| <li><strong>60-79:</strong> Good match - your resume matches many requirements</li> | |
| <li><strong>Below 60:</strong> Consider tailoring your resume more specifically to this job</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Format the full response with better styling | |
| formatted_analysis = full_response | |
| # Replace section headers with styled headers | |
| section_styles = { | |
| "## Overall Assessment": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #1e3a8a, #3b82f6); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-chart-line"></i> Overall Assessment | |
| </h3> | |
| <div class="section-content">""", | |
| "## Professional Profile Analysis": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #047857, #10b981); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-user-tie"></i> Professional Profile Analysis | |
| </h3> | |
| <div class="section-content">""", | |
| "## Skills Analysis": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #4f46e5, #818cf8); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-tools"></i> Skills Analysis | |
| </h3> | |
| <div class="section-content">""", | |
| "## Experience Analysis": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #9f1239, #e11d48); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-briefcase"></i> Experience Analysis | |
| </h3> | |
| <div class="section-content">""", | |
| "## Education Analysis": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #854d0e, #eab308); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-graduation-cap"></i> Education Analysis | |
| </h3> | |
| <div class="section-content">""", | |
| "## Key Strengths": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #166534, #22c55e); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-check-circle"></i> Key Strengths | |
| </h3> | |
| <div class="section-content">""", | |
| "## Areas for Improvement": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #9f1239, #fb7185); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-exclamation-circle"></i> Areas for Improvement | |
| </h3> | |
| <div class="section-content">""", | |
| "## ATS Optimization Assessment": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #0e7490, #06b6d4); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-robot"></i> ATS Optimization Assessment | |
| </h3> | |
| <div class="section-content">""", | |
| "## Recommended Courses": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #5b21b6, #8b5cf6); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-book"></i> Recommended Courses | |
| </h3> | |
| <div class="section-content">""", | |
| "## Resume Score": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #0369a1, #0ea5e9); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-star"></i> Resume Score | |
| </h3> | |
| <div class="section-content">""", | |
| "## Role Alignment Analysis": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #7c2d12, #ea580c); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-bullseye"></i> Role Alignment Analysis | |
| </h3> | |
| <div class="section-content">""", | |
| "## Job Match Analysis": """<div class="report-section"> | |
| <h3 style="background: linear-gradient(90deg, #4d7c0f, #84cc16); color: white; padding: 10px; border-radius: 5px;"> | |
| <i class="fas fa-handshake"></i> Job Match Analysis | |
| </h3> | |
| <div class="section-content">""", | |
| } | |
| # Apply the styling to each section | |
| for section, style in section_styles.items(): | |
| if section in formatted_analysis: | |
| formatted_analysis = formatted_analysis.replace( | |
| section, style) | |
| # Add closing div tags | |
| next_section = False | |
| for next_sec in section_styles.keys(): | |
| if next_sec != section and next_sec in formatted_analysis.split(style)[1]: | |
| split_text = formatted_analysis.split(style)[1].split(next_sec) | |
| formatted_analysis = formatted_analysis.split(style)[0] + style + split_text[0] + "</div></div>" + next_sec + "".join(split_text[1:]) | |
| next_section = True | |
| break | |
| if not next_section: | |
| formatted_analysis = formatted_analysis + "</div></div>" | |
| # Remove any extra closing div tags that might have been added | |
| formatted_analysis = formatted_analysis.replace("</div></div></div></div>", "</div></div>") | |
| # Ensure we don't have any orphaned closing tags at the end | |
| if formatted_analysis.endswith("</div>"): | |
| # Count opening and closing div tags | |
| open_tags = formatted_analysis.count("<div") | |
| close_tags = formatted_analysis.count("</div>") | |
| # If we have more closing than opening tags, remove the extras | |
| if close_tags > open_tags: | |
| excess = close_tags - open_tags | |
| formatted_analysis = formatted_analysis[:-6 * excess] | |
| # Clean up any visible HTML tags that might appear in the text | |
| formatted_analysis = formatted_analysis.replace("</div>", "") | |
| formatted_analysis = formatted_analysis.replace("<div>", "") | |
| formatted_analysis = formatted_analysis.replace("<div>", "<div>") # Ensure proper opening | |
| formatted_analysis = formatted_analysis.replace("</div>", "</div>") # Ensure proper closing | |
| # Add CSS for the report | |
| st.markdown(""" | |
| <style> | |
| .report-section { | |
| margin-bottom: 25px; | |
| border: 1px solid #4B4B4B; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| .section-content { | |
| padding: 15px; | |
| background-color: #262730; | |
| color: #ffffff; | |
| } | |
| .report-section h3 { | |
| margin-top: 0; | |
| font-weight: 600; | |
| } | |
| .report-section ul { | |
| padding-left: 20px; | |
| } | |
| .report-section p { | |
| color: #ffffff; | |
| margin-bottom: 10px; | |
| } | |
| .report-section li { | |
| color: #ffffff; | |
| margin-bottom: 5px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Display the formatted analysis | |
| st.markdown(f""" | |
| <div style="background-color: #262730; padding: 20px; border-radius: 10px; border: 1px solid #4B4B4B; color: #ffffff;"> | |
| {formatted_analysis} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Create a PDF report | |
| pdf_buffer = self.ai_analyzer.generate_pdf_report( | |
| analysis_result={ | |
| "score": resume_score, | |
| "ats_score": ats_score, | |
| "model_used": model_used, | |
| "full_response": full_response, | |
| "strengths": analysis_result.get("strengths", []), | |
| "weaknesses": analysis_result.get("weaknesses", []), | |
| "used_custom_job_desc": st.session_state.get('used_custom_job_desc', False), | |
| "custom_job_description": custom_job_description if st.session_state.get('used_custom_job_desc', False) else "" | |
| }, | |
| candidate_name=st.session_state.get( | |
| 'candidate_name', 'Candidate'), | |
| job_role=selected_role | |
| ) | |
| # PDF download button | |
| if pdf_buffer: | |
| st.download_button( | |
| label="π Download PDF Report", | |
| data=pdf_buffer, | |
| file_name=f"resume_analysis_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf", | |
| mime="application/pdf", | |
| use_container_width=True, | |
| on_click=lambda: st.balloons() | |
| ) | |
| else: | |
| st.error("PDF generation failed. Please try again later.") | |
| else: | |
| st.error(f"Analysis failed: {analysis_result.get('error', 'Unknown error')}") | |
| except Exception as ai_error: | |
| st.error(f"Error during AI analysis: {str(ai_error)}") | |
| import traceback as tb | |
| st.code(tb.format_exc()) | |
| st.toast("Check out these repositories: [Awesome Java](https://github.com/Hunterdii/Awesome-Java)", icon="βΉοΈ") | |
| def render_home(self): | |
| apply_modern_styles() | |
| # Hero Section | |
| hero_section( | |
| "Smart Resume AI", | |
| "Transform your career with AI-powered resume analysis and building. Get personalized insights and create professional resumes that stand out." | |
| ) | |
| # Features Section | |
| st.markdown('<div class="feature-grid">', unsafe_allow_html=True) | |
| feature_card( | |
| "fas fa-robot", | |
| "AI-Powered Analysis", | |
| "Get instant feedback on your resume with advanced AI analysis that identifies strengths and areas for improvement." | |
| ) | |
| feature_card( | |
| "fas fa-magic", | |
| "Smart Resume Builder", | |
| "Create professional resumes with our intelligent builder that suggests optimal content and formatting." | |
| ) | |
| feature_card( | |
| "fas fa-chart-line", | |
| "Career Insights", | |
| "Access detailed analytics and personalized recommendations to enhance your career prospects." | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.toast("Check out these repositories: [AI-Nexus(AI/ML)](https://github.com/Hunterdii/AI-Nexus)", icon="βΉοΈ") | |
| # Call-to-Action with Streamlit navigation | |
| col1, col2, col3 = st.columns([1, 1, 1]) | |
| with col2: | |
| if st.button("Get Started", key="get_started_btn", | |
| help="Click to start analyzing your resume", | |
| type="primary", | |
| use_container_width=True): | |
| cleaned_name = "π RESUME ANALYZER".lower().replace(" ", "_").replace("π", "").strip() | |
| st.session_state.page = cleaned_name | |
| st.rerun() | |
| def render_job_search(self): | |
| """Render the job search page""" | |
| render_job_search() | |
| st.toast("Check out these repositories: [GeeksforGeeks-POTD](https://github.com/Hunterdii/GeeksforGeeks-POTD)", icon="βΉοΈ") | |
| def render_feedback_page(self): | |
| """Render the feedback page""" | |
| apply_modern_styles() | |
| # Page Header | |
| page_header( | |
| "Feedback & Suggestions", | |
| "Help us improve by sharing your thoughts" | |
| ) | |
| # Initialize feedback manager | |
| feedback_manager = FeedbackManager() | |
| # Create tabs for form and stats | |
| form_tab, stats_tab = st.tabs(["Submit Feedback", "Feedback Stats"]) | |
| with form_tab: | |
| feedback_manager.render_feedback_form() | |
| with stats_tab: | |
| feedback_manager.render_feedback_stats() | |
| st.toast("Check out these repositories: [TryHackMe Free Rooms](https://github.com/Hunterdii/tryhackme-free-rooms)", icon="βΉοΈ") | |
| def show_repo_notification(self): | |
| message = """ | |
| <div style="background-color: #1e1e1e; border-radius: 10px; border: 1px solid #4b6cb7; padding: 10px; margin: 10px 0; color: white;"> | |
| <div style="margin-bottom: 10px;">Check out these other repositories:</div> | |
| <div style="margin-bottom: 5px;"><b>Hacking Resources:</b></div> | |
| <ul style="margin-top: 0; padding-left: 20px;"> | |
| <li><a href="https://github.com/Hunterdii/tryhackme-free-rooms" target="_blank" style="color: #4CAF50;">TryHackMe Free Rooms</a></li> | |
| <li><a href="https://github.com/Hunterdii/Awesome-Hacking" target="_blank" style="color: #4CAF50;">Awesome Hacking</a></li> | |
| </ul> | |
| <div style="margin-bottom: 5px;"><b>Programming Languages:</b></div> | |
| <ul style="margin-top: 0; padding-left: 20px;"> | |
| <li><a href="https://github.com/Hunterdii/Awesome-Java" target="_blank" style="color: #4CAF50;">Awesome Java</a></li> | |
| <li><a href="https://github.com/Hunterdii/30-Days-Of-Rust" target="_blank" style="color: #4CAF50;">30 Days Of Rust</a></li> | |
| </ul> | |
| <div style="margin-bottom: 5px;"><b>Data Structures & Algorithms:</b></div> | |
| <ul style="margin-top: 0; padding-left: 20px;"> | |
| <li><a href="https://github.com/Hunterdii/GeeksforGeeks-POTD" target="_blank" style="color: #4CAF50;">GeeksforGeeks POTD</a></li> | |
| <li><a href="https://github.com/Hunterdii/Leetcode-POTD" target="_blank" style="color: #4CAF50;">Leetcode POTD</a></li> | |
| </ul> | |
| <div style="margin-bottom: 5px;"><b>AI/ML Projects:</b></div> | |
| <ul style="margin-top: 0; padding-left: 20px;"> | |
| <li><a href="https://github.com/Hunterdii/AI-Nexus" target="_blank" style="color: #4CAF50;">AI Nexus</a></li> | |
| </ul> | |
| <div style="margin-top: 10px;">If you find this project helpful, please consider β starring the repo!</div> | |
| </div> | |
| """ | |
| st.sidebar.markdown(message, unsafe_allow_html=True) | |
| def main(self): | |
| """Main application entry point""" | |
| self.apply_global_styles() | |
| # Admin login/logout in sidebar | |
| with st.sidebar: | |
| st_lottie(self.load_lottie_url("https://assets5.lottiefiles.com/packages/lf20_xyadoh9h.json"), height=200, key="sidebar_animation") | |
| st.title("Smart Resume AI") | |
| st.markdown("---") | |
| # Navigation buttons | |
| for page_name in self.pages.keys(): | |
| if st.button(page_name, use_container_width=True): | |
| cleaned_name = page_name.lower().replace(" ", "_").replace("π ", "").replace("π", "").replace("π", "").replace("π", "").replace("π―", "").replace("π¬", "").replace("βΉοΈ", "").strip() | |
| st.session_state.page = cleaned_name | |
| st.rerun() | |
| # Add some space before admin login | |
| st.markdown("<br><br>", unsafe_allow_html=True) | |
| st.markdown("---") | |
| # Admin Login/Logout section at bottom | |
| if st.session_state.get('is_admin', False): | |
| st.success(f"Logged in as: {st.session_state.get('current_admin_email')}") | |
| if st.button("Logout", key="logout_button"): | |
| try: | |
| log_admin_action(st.session_state.get('current_admin_email'), "logout") | |
| st.session_state.is_admin = False | |
| st.session_state.current_admin_email = None | |
| st.success("Logged out successfully!") | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"Error during logout: {str(e)}") | |
| else: | |
| with st.expander("π€ Admin Login"): | |
| admin_email_input = st.text_input("Email", key="admin_email_input") | |
| admin_password = st.text_input("Password", type="password", key="admin_password_input") | |
| if st.button("Login", key="login_button"): | |
| try: | |
| if verify_admin(admin_email_input, admin_password): | |
| st.session_state.is_admin = True | |
| st.session_state.current_admin_email = admin_email_input | |
| log_admin_action(admin_email_input, "login") | |
| st.success("Logged in successfully!") | |
| st.rerun() | |
| else: | |
| st.error("Invalid credentials") | |
| except Exception as e: | |
| st.error(f"Error during login: {str(e)}") | |
| # Display the repository notification in the sidebar | |
| self.show_repo_notification() | |
| # Force home page on first load | |
| if 'initial_load' not in st.session_state: | |
| st.session_state.initial_load = True | |
| st.session_state.page = 'home' | |
| st.rerun() | |
| # Get current page and render it | |
| current_page = st.session_state.get('page', 'home') | |
| # Create a mapping of cleaned page names to original names | |
| page_mapping = {name.lower().replace(" ", "_").replace("π ", "").replace("π", "").replace("π", "").replace("π", "").replace("π―", "").replace("π¬", "").replace("βΉοΈ", "").strip(): name | |
| for name in self.pages.keys()} | |
| # Render the appropriate page | |
| if current_page in page_mapping: | |
| self.pages[page_mapping[current_page]]() | |
| else: | |
| # Default to home page if invalid page | |
| self.render_home() | |
| # Add footer to every page | |
| self.add_footer() | |
| if __name__ == "__main__": | |
| app = ResumeApp() | |
| app.main() |