Spaces:
Running
Running
import streamlit as st | |
import base64 | |
from io import BytesIO | |
import json | |
from datetime import datetime | |
import time | |
import re | |
import os | |
# Set page configuration and title | |
st.set_page_config( | |
page_title="ResumeBuilder Pro", | |
layout="wide", | |
initial_sidebar_state="collapsed" | |
) | |
# Define a flag to check if API functionality is available | |
if "gemini_available" not in st.session_state: | |
st.session_state.gemini_available = False | |
# Try to import Google Generative AI library, but make it optional | |
try: | |
import google.generativeai as genai | |
st.session_state.gemini_import_success = True | |
except ImportError: | |
st.session_state.gemini_import_success = False | |
# Initialize Gemini API (modified to handle missing API key gracefully) | |
def initialize_gemini_api(): | |
if not st.session_state.gemini_import_success: | |
st.warning("Google Generative AI library not installed. Install with: `pip install google-generativeai`") | |
return False | |
try: | |
# Get API key from session state first (from text input) | |
api_key = st.session_state.get("api_key", "") | |
# Only try to configure if API key is provided | |
if api_key: | |
genai.configure(api_key=api_key) | |
# Test the API with a simple call to verify it works | |
model = genai.GenerativeModel(model_name="gemini-pro") | |
_ = model.generate_content("Hello") | |
return True | |
else: | |
# No API key provided yet, not an error | |
return False | |
except Exception as e: | |
st.error(f"Failed to initialize Gemini API: {str(e)}") | |
return False | |
# Function to get Gemini model response | |
def get_gemini_response(prompt, temperature=0.7): | |
if not st.session_state.gemini_available: | |
st.warning("Gemini API not available. Please set up your API key.") | |
return None | |
try: | |
# Create the model | |
generation_config = { | |
"temperature": temperature, | |
"top_p": 0.95, | |
"top_k": 64, | |
"max_output_tokens": 8192, | |
} | |
model = genai.GenerativeModel( | |
model_name="gemini-pro", | |
generation_config=generation_config, | |
) | |
response = model.generate_content(prompt) | |
return response.text | |
except Exception as e: | |
st.error(f"Error getting AI response: {str(e)}") | |
return None | |
# Initialize session state for resume data | |
if "resume_data" not in st.session_state: | |
st.session_state.resume_data = { | |
"fullName": "Alexander Johnson", | |
"title": "Senior Frontend Developer", | |
"email": "[email protected]", | |
"phone": "(555) 123-4567", | |
"location": "San Francisco, CA", | |
"summary": "Experienced frontend developer with 6+ years specializing in React and modern JavaScript frameworks. Passionate about creating intuitive user interfaces and optimizing web performance.", | |
"experience": [ | |
{ | |
"id": 1, | |
"company": "Tech Innovations Inc.", | |
"position": "Senior Frontend Developer", | |
"duration": "2019 - Present", | |
"description": "Lead frontend development for enterprise SaaS platform. Improved performance by 40% through code optimization. Mentored junior developers." | |
}, | |
{ | |
"id": 2, | |
"company": "WebSolutions Co.", | |
"position": "Frontend Developer", | |
"duration": "2017 - 2019", | |
"description": "Developed responsive web applications using React. Collaborated with design team to implement UI/UX improvements." | |
} | |
], | |
"education": [ | |
{ | |
"id": 1, | |
"institution": "University of California, Berkeley", | |
"degree": "B.S. Computer Science", | |
"duration": "2013 - 2017" | |
} | |
], | |
"skills": ["React", "JavaScript", "TypeScript", "HTML/CSS", "Redux", "Next.js", "Tailwind CSS", "UI/UX Design"] | |
} | |
if "dark_mode" not in st.session_state: | |
st.session_state.dark_mode = False | |
if "show_preview" not in st.session_state: | |
st.session_state.show_preview = True | |
if "new_skill" not in st.session_state: | |
st.session_state.new_skill = "" | |
if "job_description" not in st.session_state: | |
st.session_state.job_description = "" | |
if "ai_suggestions" not in st.session_state: | |
st.session_state.ai_suggestions = {} | |
if "cover_letter" not in st.session_state: | |
st.session_state.cover_letter = "" | |
if "suggested_skills" not in st.session_state: | |
st.session_state.suggested_skills = [] | |
# Apply custom styling based on dark/light mode | |
def apply_custom_styling(): | |
dark_mode = st.session_state.dark_mode | |
primary_color = "#4F46E5" # Indigo | |
if dark_mode: | |
background_color = "#111827" # Dark gray | |
text_color = "#F9FAFB" # Almost white | |
card_bg = "#1F2937" # Medium gray | |
input_bg = "#374151" # Light gray | |
secondary_bg = "#1E3A8A" # Dark blue | |
accent_light = "#818CF8" # Lighter indigo | |
else: | |
background_color = "#F9FAFB" # Almost white | |
text_color = "#111827" # Dark gray | |
card_bg = "#FFFFFF" # White | |
input_bg = "#F3F4F6" # Light gray | |
secondary_bg = "#EEF2FF" # Light indigo | |
accent_light = "#C7D2FE" # Very light indigo | |
css = f""" | |
<style> | |
/* Base colors and fonts */ | |
:root {{ | |
--primary-color: {primary_color}; | |
--bg-color: {background_color}; | |
--text-color: {text_color}; | |
--card-bg: {card_bg}; | |
--input-bg: {input_bg}; | |
--secondary-bg: {secondary_bg}; | |
--accent-light: {accent_light}; | |
}} | |
/* Main page styling */ | |
.main .block-container {{ | |
padding-top: 2rem; | |
max-width: 1200px; | |
}} | |
.stApp {{ | |
background-color: var(--bg-color); | |
color: var(--text-color); | |
}} | |
/* Header styling */ | |
h1, h2, h3, h4, h5, h6 {{ | |
color: var(--text-color); | |
}} | |
/* Make inputs match theme */ | |
.stTextInput > div > div > input, | |
.stTextArea > div > div > textarea {{ | |
background-color: var(--input-bg); | |
color: var(--text-color); | |
border-radius: 0.375rem; | |
}} | |
/* Custom card styling */ | |
.custom-card {{ | |
background-color: var(--card-bg); | |
border-radius: 0.5rem; | |
padding: 1.5rem; | |
margin-bottom: 1rem; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
}} | |
/* AI feature styling */ | |
.ai-container {{ | |
background-color: var(--secondary-bg); | |
border-radius: 0.5rem; | |
padding: 1rem; | |
margin-bottom: 1rem; | |
border-left: 4px solid var(--primary-color); | |
}} | |
.ai-suggestion {{ | |
background-color: var(--accent-light); | |
border-radius: 0.375rem; | |
padding: 0.75rem; | |
margin-top: 0.5rem; | |
font-size: 0.875rem; | |
border-left: 3px solid var(--primary-color); | |
}} | |
/* Resume preview styling */ | |
.resume-container {{ | |
background-color: var(--card-bg); | |
border-radius: 0.5rem; | |
padding: 2rem; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
}} | |
.resume-header {{ | |
margin-bottom: 1.5rem; | |
}} | |
.resume-name {{ | |
font-size: 2rem; | |
font-weight: bold; | |
margin-bottom: 0.25rem; | |
color: var(--text-color); | |
}} | |
.resume-title {{ | |
font-size: 1.25rem; | |
color: var(--primary-color); | |
margin-bottom: 0.5rem; | |
}} | |
.contact-info {{ | |
font-size: 0.875rem; | |
margin-top: 0.5rem; | |
color: var(--text-color); | |
}} | |
.section-title {{ | |
font-size: 1.25rem; | |
font-weight: 600; | |
margin-top: 1.5rem; | |
margin-bottom: 1rem; | |
padding-bottom: 0.5rem; | |
border-bottom: 1px solid {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.1)"}; | |
color: var(--text-color); | |
}} | |
.company-name {{ | |
color: var(--primary-color); | |
font-size: 1rem; | |
}} | |
.job-title {{ | |
font-weight: 600; | |
font-size: 1.125rem; | |
color: var(--text-color); | |
}} | |
.duration {{ | |
font-size: 0.875rem; | |
color: {"rgba(255,255,255,0.7)" if dark_mode else "rgba(0,0,0,0.6)"}; | |
}} | |
.job-description {{ | |
margin-top: 0.5rem; | |
font-size: 0.9375rem; | |
color: {"rgba(255,255,255,0.9)" if dark_mode else "rgba(0,0,0,0.8)"}; | |
}} | |
.institution {{ | |
font-weight: 600; | |
font-size: 1.125rem; | |
color: var(--text-color); | |
}} | |
.degree {{ | |
font-size: 1rem; | |
color: {"rgba(255,255,255,0.9)" if dark_mode else "rgba(0,0,0,0.8)"}; | |
}} | |
.skill-tag {{ | |
display: inline-block; | |
background-color: {f"rgba(79, 70, 229, 0.2)" if dark_mode else "rgba(79, 70, 229, 0.1)"}; | |
color: {f"rgba(255,255,255,0.9)" if dark_mode else "#4F46E5"}; | |
padding: 0.35rem 0.7rem; | |
border-radius: 9999px; | |
margin-right: 0.5rem; | |
margin-bottom: 0.5rem; | |
font-size: 0.875rem; | |
}} | |
.recommended-skill {{ | |
display: inline-block; | |
background-color: {f"rgba(5, 150, 105, 0.2)" if dark_mode else "rgba(5, 150, 105, 0.1)"}; | |
color: {f"rgba(255,255,255,0.9)" if dark_mode else "#059669"}; | |
padding: 0.35rem 0.7rem; | |
border-radius: 9999px; | |
margin-right: 0.5rem; | |
margin-bottom: 0.5rem; | |
font-size: 0.875rem; | |
border: 1px dashed {f"rgba(5, 150, 105, 0.5)" if dark_mode else "rgba(5, 150, 105, 0.5)"}; | |
}} | |
/* Tab styling */ | |
button[data-baseweb="tab"] {{ | |
background-color: var(--secondary-bg); | |
border-radius: 0.375rem 0.375rem 0 0; | |
}} | |
button[data-baseweb="tab"][aria-selected="true"] {{ | |
background-color: var(--primary-color) !important; | |
color: white !important; | |
}} | |
div[data-testid="stVerticalBlock"] div[data-testid="stHorizontalBlock"] {{ | |
gap: 0.5rem; | |
}} | |
/* Section separators */ | |
hr {{ | |
margin: 1.5rem 0; | |
border-color: {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.1)"}; | |
}} | |
/* Button styling */ | |
.stButton button {{ | |
border-radius: 0.375rem; | |
}} | |
/* Make the buttons in header look nicer */ | |
.header-button {{ | |
background-color: var(--card-bg) !important; | |
color: var(--text-color) !important; | |
border: 1px solid {f"rgba(255,255,255,0.2)" if dark_mode else "rgba(0,0,0,0.1)"} !important; | |
border-radius: 0.375rem !important; | |
padding: 0.5rem 1rem !important; | |
font-size: 0.875rem !important; | |
transition: all 0.2s ease !important; | |
}} | |
.header-button:hover {{ | |
background-color: {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.05)"} !important; | |
border-color: {f"rgba(255,255,255,0.3)" if dark_mode else "rgba(0,0,0,0.2)"} !important; | |
}} | |
/* Container styling */ | |
.content-container {{ | |
display: flex; | |
gap: 1.5rem; | |
}} | |
/* Adjust form field spacing */ | |
div[data-baseweb="input"] {{ | |
margin-bottom: 0.75rem; | |
}} | |
/* Make the expander look cleaner */ | |
.st-emotion-cache-1r6slb0[data-testid="stExpander"] {{ | |
background-color: var(--card-bg); | |
border-radius: 0.5rem; | |
border: 1px solid {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.1)"}; | |
margin-bottom: 1rem; | |
}} | |
/* Remove the border around the tab content area */ | |
.st-emotion-cache-cio0dv[data-testid="block-container"] {{ | |
padding-left: 0; | |
padding-right: 0; | |
}} | |
/* Smaller margin for experience items */ | |
.experience-row {{ | |
margin-bottom: 0.25rem; | |
}} | |
/* AI button styling */ | |
.ai-button {{ | |
display: inline-flex; | |
align-items: center; | |
background-color: var(--primary-color); | |
color: white; | |
padding: 0.5rem 1rem; | |
border-radius: 0.375rem; | |
font-size: 0.875rem; | |
font-weight: 500; | |
border: none; | |
cursor: pointer; | |
transition: all 0.2s ease; | |
}} | |
.ai-button:hover {{ | |
background-color: #4338CA; | |
}} | |
/* API key container */ | |
.api-key-container {{ | |
margin-bottom: 1rem; | |
padding: 1rem; | |
background-color: {f"rgba(255,255,255,0.05)" if dark_mode else "rgba(0,0,0,0.03)"}; | |
border-radius: 0.5rem; | |
}} | |
/* Cover letter styling */ | |
.cover-letter {{ | |
background-color: var(--card-bg); | |
border-radius: 0.5rem; | |
padding: 2rem; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
margin-top: 1rem; | |
}} | |
/* Match percentage meter */ | |
.match-meter {{ | |
height: 1rem; | |
border-radius: 9999px; | |
overflow: hidden; | |
background-color: {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.1)"}; | |
margin-top: 0.5rem; | |
margin-bottom: 1rem; | |
}} | |
.match-fill {{ | |
height: 100%; | |
background-color: var(--primary-color); | |
transition: width 1s ease-in-out; | |
}} | |
/* Analysis result cards */ | |
.analysis-card {{ | |
background-color: {f"rgba(255,255,255,0.05)" if dark_mode else "rgba(255,255,255,0.8)"}; | |
border-radius: 0.5rem; | |
padding: 1rem; | |
margin-bottom: 1rem; | |
border-left: 3px solid var(--primary-color); | |
}} | |
/* AI features banner */ | |
.ai-features-banner {{ | |
background-color: var(--accent-light); | |
border-radius: 0.5rem; | |
padding: 1rem; | |
margin-bottom: 1rem; | |
border: 1px dashed var(--primary-color); | |
text-align: center; | |
}} | |
</style> | |
""" | |
st.markdown(css, unsafe_allow_html=True) | |
# Function to download resume as JSON | |
def download_resume_json(): | |
json_str = json.dumps(st.session_state.resume_data, indent=2) | |
b64 = base64.b64encode(json_str.encode()).decode() | |
filename = f"resume_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" | |
href = f'<a href="data:file/json;base64,{b64}" download="{filename}" class="header-button" style="text-decoration:none;padding:0.5rem 1rem;margin-right:0.5rem;">π₯ Download JSON</a>' | |
return href | |
# Function to create a placeholder for PDF export | |
def download_resume_pdf(): | |
# In a real app, you would generate an actual PDF here | |
# For this example, we'll just return a placeholder message | |
return '<button class="header-button" onclick="alert(\'PDF generation would be implemented here in a real app\');">π Export PDF</button>' | |
# Main application header | |
def render_header(): | |
col1, col2 = st.columns([6, 4]) | |
with col1: | |
ai_badge = '<span style="background-color: var(--primary-color); color: white; font-size: 0.6em; padding: 0.2em 0.5em; border-radius: 0.5em; margin-left: 0.5em;">AI-Ready</span>' if st.session_state.gemini_import_success else "" | |
st.markdown(f'<h1 style="display:flex;align-items:center;"><span style="margin-right:10px;">π</span> ResumeBuilder Pro {ai_badge}</h1>', unsafe_allow_html=True) | |
with col2: | |
download_buttons = f""" | |
<div style="display:flex;justify-content:flex-end;align-items:center;gap:0.5rem;"> | |
{download_resume_json()} | |
{download_resume_pdf()} | |
<button class="header-button" onclick="document.querySelector('button[data-testid*=\\"baseButton-secondary\\"]').click();"> | |
{'π' if not st.session_state.dark_mode else 'βοΈ'} Theme | |
</button> | |
<button class="header-button" onclick="document.querySelector('button[data-testid*=\\"baseButton-primary\\"]').click();"> | |
{'ποΈ' if st.session_state.show_preview else 'ποΈβπ¨οΈ'} Preview | |
</button> | |
</div> | |
""" | |
st.markdown(download_buttons, unsafe_allow_html=True) | |
# Hidden buttons that are triggered by the custom buttons above | |
col2_1, col2_2 = st.columns(2) | |
with col2_1: | |
if st.button("Toggle Theme", key="baseButton-secondary", type="secondary"): | |
st.session_state.dark_mode = not st.session_state.dark_mode | |
st.rerun() | |
with col2_2: | |
if st.button("Toggle Preview", key="baseButton-primary"): | |
st.session_state.show_preview = not st.session_state.show_preview | |
st.rerun() | |
# API Key Management - Improved to be more user-friendly and handle errors better | |
def manage_api_key(): | |
if "api_key" not in st.session_state: | |
st.session_state.api_key = "" | |
st.markdown('<div class="api-key-container">', unsafe_allow_html=True) | |
# Show different message if the generative AI library is not installed | |
if not st.session_state.gemini_import_success: | |
st.markdown("### π§ Enable AI Features") | |
st.markdown(""" | |
To use AI features, you need to install the Google Generative AI library: | |
``` | |
pip install google-generativeai | |
``` | |
After installation, restart the app to access AI-powered resume enhancements. | |
""") | |
st.markdown('</div>', unsafe_allow_html=True) | |
return | |
# If library is installed but API key not set | |
st.markdown("### π Gemini API Setup") | |
st.markdown("To use AI features, enter your Google Gemini API key below:") | |
api_key = st.text_input("API Key", | |
value=st.session_state.api_key, | |
type="password", | |
placeholder="Enter your Gemini API key here") | |
col1, col2 = st.columns([1, 3]) | |
with col1: | |
if st.button("Save API Key"): | |
st.session_state.api_key = api_key | |
if initialize_gemini_api(): | |
st.session_state.gemini_available = True | |
st.success("API key saved and verified successfully!") | |
else: | |
if api_key: | |
st.error("Invalid API key. Please check and try again.") | |
else: | |
st.warning("Please enter an API key to enable AI features.") | |
with col2: | |
st.markdown("Get your API key from [Google AI Studio](https://makersuite.google.com/app/apikey)") | |
# Show what AI features are available | |
if not st.session_state.gemini_available: | |
st.markdown('<div class="ai-features-banner">', unsafe_allow_html=True) | |
st.markdown("### π Unlock AI Features") | |
st.markdown(""" | |
By adding your API key, you'll unlock powerful AI features: | |
- **Professional Summary Generator**: Create compelling summaries automatically | |
- **Job Match Analysis**: Score your resume against job descriptions | |
- **Skills Recommendations**: Get tailored skill suggestions for your role | |
- **Experience Description Enhancement**: Make your work history more impactful | |
- **Cover Letter Generator**: Create customized cover letters in seconds | |
""") | |
st.markdown('</div>', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# AI Features - These will only be activated when API is available | |
# Generate professional summary | |
def generate_ai_summary(): | |
data = st.session_state.resume_data | |
# Create prompt for summary generation | |
prompt = f""" | |
You are an expert resume writer. Generate a compelling professional summary for a resume with these details: | |
Name: {data['fullName']} | |
Current Position: {data['title']} | |
Skills: {', '.join(data['skills'])} | |
Experience: | |
{' '.join([f"{exp['position']} at {exp['company']} ({exp['duration']}): {exp['description']}" for exp in data['experience']])} | |
Education: | |
{' '.join([f"{edu['degree']} from {edu['institution']} ({edu['duration']})" for edu in data['education']])} | |
Rules for writing the summary: | |
1. Keep it concise (3-4 sentences maximum) | |
2. Highlight key skills and accomplishments | |
3. Focus on value provided in past roles | |
4. Use active language and avoid clichΓ©s | |
5. Target it toward professional growth | |
Write ONLY the summary. Don't include explanations or other text. | |
""" | |
with st.spinner("Generating professional summary..."): | |
summary = get_gemini_response(prompt, temperature=0.7) | |
if summary: | |
# Clean up the response | |
summary = summary.strip().replace('"', '') | |
return summary | |
return None | |
# Analyze job description and match skills | |
def analyze_job_description(job_description): | |
data = st.session_state.resume_data | |
prompt = f""" | |
You are an expert resume consultant. Analyze this job description and the candidate's resume to provide insights. | |
JOB DESCRIPTION: | |
{job_description} | |
CANDIDATE RESUME: | |
Name: {data['fullName']} | |
Current Position: {data['title']} | |
Skills: {', '.join(data['skills'])} | |
Experience: | |
{' '.join([f"{exp['position']} at {exp['company']} ({exp['duration']}): {exp['description']}" for exp in data['experience']])} | |
Education: | |
{' '.join([f"{edu['degree']} from {edu['institution']} ({edu['duration']})" for edu in data['education']])} | |
Provide the following in JSON format: | |
1. "match_percentage": A numerical estimate (0-100) of how well the candidate's skills match the job requirements | |
2. "missing_skills": A list of 3-5 key skills mentioned in the job that are missing from the candidate's resume | |
3. "highlight_skills": A list of skills the candidate has that are particularly relevant to this job | |
4. "emphasis_suggestions": 2-3 specific parts of the candidate's experience that should be emphasized for this job | |
5. "improvement_tips": 2-3 brief suggestions to improve the resume for this specific job | |
Return ONLY the JSON, formatted as follows: | |
{{ | |
"match_percentage": number, | |
"missing_skills": [list of strings], | |
"highlight_skills": [list of strings], | |
"emphasis_suggestions": [list of strings], | |
"improvement_tips": [list of strings] | |
}} | |
""" | |
with st.spinner("Analyzing job description..."): | |
analysis = get_gemini_response(prompt, temperature=0.2) | |
if analysis: | |
try: | |
# Extract just the JSON part from the response | |
# First find JSON pattern using regex | |
json_match = re.search(r'\{[\s\S]*\}', analysis) | |
if json_match: | |
json_str = json_match.group(0) | |
return json.loads(json_str) | |
return json.loads(analysis) | |
except Exception as e: | |
st.error(f"Error parsing AI response: {str(e)}") | |
st.write("Raw response:", analysis) | |
return None | |
# Improve experience descriptions | |
def improve_experience_description(description, position, company): | |
prompt = f""" | |
You are an expert resume writer. Enhance the following job description to be more impactful and achievement-oriented: | |
Position: {position} | |
Company: {company} | |
Current Description: {description} | |
Rewrite the description to: | |
1. Focus on achievements with measurable results | |
2. Start with strong action verbs | |
3. Highlight relevant skills and impact | |
4. Be concise but comprehensive (max 3 bullet points) | |
5. Use quantifiable metrics where possible | |
Return ONLY the improved description without additional commentary. | |
""" | |
with st.spinner("Improving description..."): | |
improved = get_gemini_response(prompt, temperature=0.7) | |
if improved: | |
return improved.strip() | |
return None | |
# Generate cover letter | |
def generate_cover_letter(job_description, company_name): | |
data = st.session_state.resume_data | |
prompt = f""" | |
You are an expert cover letter writer. Create a personalized cover letter for: | |
Applicant: {data['fullName']} | |
Current Position: {data['title']} | |
Skills: {', '.join(data['skills'])} | |
Target Company: {company_name} | |
Based on this job description: | |
{job_description} | |
Experience highlights: | |
{' '.join([f"{exp['position']} at {exp['company']} ({exp['duration']}): {exp['description']}" for exp in data['experience'][:2]])} | |
Cover letter guidelines: | |
1. Keep it under 250 words | |
2. Include a personalized greeting | |
3. Start with an engaging opening that shows enthusiasm | |
4. Connect 2-3 of the applicant's skills/experiences directly to the job requirements | |
5. Include a specific reason why the applicant wants to work at this company | |
6. End with a call to action | |
7. Use a professional closing | |
Format it as a proper cover letter with date, greeting, paragraphs, and signature. | |
Return ONLY the cover letter, no explanations or additional notes. | |
""" | |
with st.spinner("Generating cover letter..."): | |
cover_letter = get_gemini_response(prompt, temperature=0.7) | |
if cover_letter: | |
return cover_letter.strip() | |
return None | |
# Suggest skills based on job title | |
def suggest_skills_for_role(job_title): | |
prompt = f""" | |
You are a career advisor specializing in resume building. Generate a list of 8-10 highly relevant technical and soft skills for someone in this role: | |
Job Title: {job_title} | |
Return the skills as a JSON array of strings. ONLY return the JSON array, no other text. | |
Example: ["Skill 1", "Skill 2", "Skill 3"] | |
""" | |
with st.spinner("Suggesting skills..."): | |
skills_json = get_gemini_response(prompt, temperature=0.3) | |
if skills_json: | |
try: | |
# Clean up and parse the response | |
skills_json = skills_json.strip() | |
if skills_json.startswith('```') and skills_json.endswith('```'): | |
skills_json = skills_json[3:-3].strip() | |
if skills_json.startswith('json'): | |
skills_json = skills_json[4:].strip() | |
return json.loads(skills_json) | |
except Exception as e: | |
st.error(f"Error parsing skills: {str(e)}") | |
return [] | |
# Basic Info tab with AI enhancement | |
def render_basic_info(): | |
data = st.session_state.resume_data | |
st.markdown('<div class="custom-card">', unsafe_allow_html=True) | |
st.subheader("Personal Information") | |
col1, col2 = st.columns(2) | |
with col1: | |
new_name = st.text_input("Full Name", value=data["fullName"], key="fullName") | |
if new_name != data["fullName"]: | |
st.session_state.resume_data["fullName"] = new_name | |
new_email = st.text_input("Email", value=data["email"], key="email") | |
if new_email != data["email"]: | |
st.session_state.resume_data["email"] = new_email | |
new_location = st.text_input("Location", value=data["location"], key="location") | |
if new_location != data["location"]: | |
st.session_state.resume_data["location"] = new_location | |
with col2: | |
new_title = st.text_input("Professional Title", value=data["title"], key="title") | |
if new_title != data["title"]: | |
st.session_state.resume_data["title"] = new_title | |
new_phone = st.text_input("Phone", value=data["phone"], key="phone") | |
if new_phone != data["phone"]: | |
st.session_state.resume_data["phone"] = new_phone | |
st.markdown("### Professional Summary") | |
# Add AI summary generation if API key is set | |
if st.session_state.gemini_available: | |
col1, col2 = st.columns([3, 1]) | |
with col2: | |
if st.button("β¨ Generate AI Summary"): | |
summary = generate_ai_summary() | |
if summary: | |
st.session_state.resume_data["summary"] = summary | |
st.rerun() | |
new_summary = st.text_area("", value=data["summary"], height=150, key="summary") | |
if new_summary != data["summary"]: | |
st.session_state.resume_data["summary"] = new_summary | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Add Job Description Analysis section if API key is set | |
if st.session_state.gemini_available: | |
st.markdown('<div class="ai-container">', unsafe_allow_html=True) | |
st.markdown("### π Job Match Analysis") | |
st.markdown("Paste a job description to get AI-powered insights on how your resume matches the requirements.") | |
job_description = st.text_area("Job Description", height=100, key="job_desc_input", | |
value=st.session_state.job_description) | |
if job_description: | |
company_name = st.text_input("Company Name (for cover letter)", key="company_name") | |
col1, col2, col3 = st.columns([1,1,1]) | |
with col1: | |
if st.button("Analyze Job Match"): | |
st.session_state.job_description = job_description | |
analysis = analyze_job_description(job_description) | |
if analysis: | |
st.session_state.ai_suggestions = analysis | |
st.rerun() | |
with col2: | |
if st.button("Generate Cover Letter") and company_name: | |
st.session_state.job_description = job_description | |
cover_letter = generate_cover_letter(job_description, company_name) | |
if cover_letter: | |
st.session_state.cover_letter = cover_letter | |
st.rerun() | |
with col3: | |
if st.button("Suggest Skills"): | |
suggested_skills = suggest_skills_for_role(data["title"]) | |
if suggested_skills: | |
st.session_state.suggested_skills = suggested_skills | |
st.rerun() | |
# Display analysis results if available | |
if st.session_state.ai_suggestions: | |
analysis = st.session_state.ai_suggestions | |
st.markdown("### π Job Match Analysis Results") | |
# Match percentage | |
match_percentage = analysis.get("match_percentage", 0) | |
st.markdown(f"### Match Score: {match_percentage}%") | |
st.markdown('<div class="match-meter"><div class="match-fill" style="width: {}%;"></div></div>'.format(match_percentage), unsafe_allow_html=True) | |
# Missing skills | |
if "missing_skills" in analysis and analysis["missing_skills"]: | |
st.markdown('<div class="analysis-card">', unsafe_allow_html=True) | |
st.markdown("#### π΄ Missing Skills") | |
st.markdown("Consider adding these skills to your resume:") | |
missing_skills_html = "" | |
for skill in analysis["missing_skills"]: | |
missing_skills_html += f'<span class="recommended-skill">{skill} <button onclick="document.getElementById(\'add_skill_{skill.replace(" ", "_")}\').click()">+</button></span>' | |
st.markdown(missing_skills_html, unsafe_allow_html=True) | |
# Hidden buttons for adding missing skills | |
for skill in analysis["missing_skills"]: | |
if st.button("Add", key=f"add_skill_{skill.replace(' ', '_')}", type="primary"): | |
if skill not in data["skills"]: | |
data["skills"].append(skill) | |
st.rerun() | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Highlight skills | |
if "highlight_skills" in analysis and analysis["highlight_skills"]: | |
st.markdown('<div class="analysis-card">', unsafe_allow_html=True) | |
st.markdown("#### π’ Relevant Skills") | |
st.markdown("Highlight these skills prominently in your resume:") | |
highlight_skills_html = "" | |
for skill in analysis["highlight_skills"]: | |
highlight_skills_html += f'<span class="skill-tag">{skill}</span>' | |
st.markdown(highlight_skills_html, unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Emphasis suggestions | |
if "emphasis_suggestions" in analysis and analysis["emphasis_suggestions"]: | |
st.markdown('<div class="analysis-card">', unsafe_allow_html=True) | |
st.markdown("#### π‘ Experience to Emphasize") | |
emphasis_html = "<ul>" | |
for suggestion in analysis["emphasis_suggestions"]: | |
emphasis_html += f"<li>{suggestion}</li>" | |
emphasis_html += "</ul>" | |
st.markdown(emphasis_html, unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Improvement tips | |
if "improvement_tips" in analysis and analysis["improvement_tips"]: | |
st.markdown('<div class="analysis-card">', unsafe_allow_html=True) | |
st.markdown("#### β‘ Improvement Tips") | |
tips_html = "<ul>" | |
for tip in analysis["improvement_tips"]: | |
tips_html += f"<li>{tip}</li>" | |
tips_html += "</ul>" | |
st.markdown(tips_html, unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Display suggested skills if available | |
if hasattr(st.session_state, 'suggested_skills') and st.session_state.suggested_skills: | |
st.markdown("### π·οΈ Suggested Skills for Your Role") | |
suggested_skills_html = "" | |
for skill in st.session_state.suggested_skills: | |
if skill not in data["skills"]: | |
suggested_skills_html += f'<span class="recommended-skill">{skill} <button onclick="document.getElementById(\'add_suggested_skill_{skill.replace(" ", "_")}\').click()">+</button></span>' | |
if suggested_skills_html: | |
st.markdown(suggested_skills_html, unsafe_allow_html=True) | |
# Hidden buttons for adding suggested skills | |
for skill in st.session_state.suggested_skills: | |
if skill not in data["skills"]: | |
if st.button("Add", key=f"add_suggested_skill_{skill.replace(' ', '_')}", type="primary"): | |
data["skills"].append(skill) | |
st.rerun() | |
else: | |
st.info("All suggested skills are already in your resume!") | |
# Display cover letter if available | |
if st.session_state.cover_letter: | |
st.markdown("### π Generated Cover Letter") | |
st.markdown('<div class="cover-letter">', unsafe_allow_html=True) | |
# Replace newlines with <br> tags for proper HTML display | |
formatted_letter = st.session_state.cover_letter.replace('\n', '<br>') | |
st.markdown(formatted_letter, unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Add button to copy cover letter to clipboard | |
if st.button("π Copy Cover Letter"): | |
st.toast("Cover letter copied to clipboard!", icon="π") | |
st.markdown('</div>', unsafe_allow_html=True) |