Spaces:
Running
Running
import streamlit as st | |
import base64 | |
from io import BytesIO | |
import json | |
from datetime import datetime | |
import re | |
st.set_page_config(page_title="ResumeBuilder Pro", layout="wide", initial_sidebar_state="collapsed") | |
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 = [] | |
# 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 | |
# 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" | |
text_color = "#F9FAFB" | |
card_bg = "#1F2937" | |
input_bg = "#374151" | |
secondary_bg = "#1E3A8A" | |
accent_light = "#818CF8" | |
else: | |
background_color = "#F9FAFB" | |
text_color = "#111827" | |
card_bg = "#FFFFFF" | |
input_bg = "#F3F4F6" | |
secondary_bg = "#EEF2FF" | |
accent_light = "#C7D2FE" | |
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}; | |
--spacing-xs: 0.5rem; | |
--spacing-sm: 0.75rem; | |
--spacing-base: 1rem; | |
--spacing-lg: 1.5rem; | |
--spacing-xl: 2rem; | |
}} | |
/* Reset spacing */ | |
.stApp [data-testid="stVerticalBlock"] > div:not(:last-child) {{ | |
margin-bottom: var(--spacing-lg); | |
}} | |
/* Fix grid layout */ | |
.stApp [data-testid="stHorizontalBlock"] {{ | |
gap: var(--spacing-base); | |
align-items: flex-start; | |
}} | |
.stApp [data-testid="stHorizontalBlock"] > div {{ | |
min-width: 0; | |
flex: 1; | |
}} | |
/* Prevent content overflow */ | |
.element-container {{ | |
overflow: hidden; | |
word-wrap: break-word; | |
}} | |
/* Main page styling */ | |
.main .block-container {{ | |
padding: var(--spacing-xl) var(--spacing-base); | |
max-width: 1400px; | |
margin: 0 auto; | |
}} | |
.stApp {{ | |
background-color: var(--bg-color); | |
color: var(--text-color); | |
overflow-x: hidden; | |
}} | |
/* Form layout improvements */ | |
.stForm > div {{ | |
margin-bottom: var(--spacing-xl); | |
}} | |
.stTextInput, .stTextArea {{ | |
margin-bottom: var(--spacing-base); | |
}} | |
/* Button spacing */ | |
.stButton {{ | |
margin: var(--spacing-base) 0; | |
}} | |
/* Header styling */ | |
h1, h2, h3, h4, h5, h6 {{ | |
color: var(--text-color); | |
}} | |
/* Make inputs match theme */ | |
.stTextInput > div > div > input, | |
.stTextArea > div > div > textarea, | |
.stSelectbox > div > div > select {{ | |
background-color: var(--input-bg); | |
color: var(--text-color); | |
border-radius: 0.375rem; | |
padding: var(--spacing-sm); | |
margin-bottom: var(--spacing-base); | |
}} | |
/* Unified focus states */ | |
input:focus, textarea:focus, select:focus {{ | |
outline: 2px solid var(--primary-color); | |
outline-offset: 2px; | |
}} | |
/* Custom card styling */ | |
.custom-card {{ | |
background-color: var(--card-bg); | |
border-radius: 0.5rem; | |
padding: var(--spacing-lg); | |
margin-bottom: var(--spacing-base); | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
position: relative; | |
z-index: 1; | |
}} | |
/* Section spacing */ | |
.section {{ | |
margin-bottom: var(--spacing-xl); | |
}} | |
/* Form groups */ | |
.form-group {{ | |
margin-bottom: var(--spacing-lg); | |
}} | |
/* Input groups */ | |
.input-group {{ | |
display: flex; | |
gap: var(--spacing-base); | |
margin-bottom: var(--spacing-base); | |
}} | |
/* Responsive adjustments */ | |
@media (max-width: 768px) {{ | |
.stHorizontalBlock {{ | |
flex-direction: column; | |
}} | |
.input-group {{ | |
gap: var(--spacing-xs); | |
}} | |
.resume-container {{ | |
padding: var(--spacing-lg); | |
}} | |
}} | |
/* 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: white; | |
color: #333; | |
padding: 1.5rem; | |
max-width: 800px; | |
margin: 0 auto; | |
font-family: 'Georgia', serif; | |
line-height: 1.4; | |
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
}} | |
/* Clean, minimal header */ | |
.resume-header {{ | |
text-align: center; | |
margin-bottom: 1.5rem; | |
padding-bottom: 1rem; | |
border-bottom: 1px solid #eee; | |
}} | |
.resume-name {{ | |
font-size: 1.8rem; | |
font-weight: normal; | |
margin-bottom: 0.5rem; | |
color: #333; | |
}} | |
.resume-title {{ | |
font-size: 1.2rem; | |
color: #666; | |
font-weight: normal; | |
margin-bottom: 0.5rem; | |
}} | |
.contact-info {{ | |
font-size: 0.9rem; | |
color: #666; | |
display: flex; | |
justify-content: center; | |
gap: 1rem; | |
flex-wrap: wrap; | |
}} | |
/* Clean section styling */ | |
.section-title {{ | |
font-size: 1.1rem; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
color: #333; | |
margin-top: 1.5rem; | |
margin-bottom: 0.75rem; | |
font-weight: normal; | |
border-bottom: 1px solid #eee; | |
padding-bottom: 0.25rem; | |
}} | |
/* Experience items */ | |
.experience-item {{ | |
margin-bottom: 1.25rem; | |
}} | |
.job-title {{ | |
font-weight: bold; | |
font-size: 1rem; | |
color: #333; | |
margin-bottom: 0.25rem; | |
}} | |
.company-name {{ | |
font-weight: normal; | |
font-size: 1rem; | |
color: #333; | |
}} | |
.duration {{ | |
font-size: 0.9rem; | |
color: #666; | |
font-style: italic; | |
}} | |
.job-description {{ | |
font-size: 0.95rem; | |
color: #444; | |
margin-top: 0.5rem; | |
line-height: 1.5; | |
}} | |
/* Education items */ | |
.education-item {{ | |
margin-bottom: 1rem; | |
}} | |
.institution {{ | |
font-weight: bold; | |
font-size: 1rem; | |
color: #333; | |
margin-bottom: 0.25rem; | |
}} | |
.degree {{ | |
font-size: 0.95rem; | |
color: #444; | |
}} | |
/* Skills */ | |
.skills-container {{ | |
display: flex; | |
flex-wrap: wrap; | |
gap: 0.5rem; | |
margin-top: 0.5rem; | |
}} | |
.skill-tag {{ | |
background-color: #f5f5f5; | |
color: #333; | |
padding: 0.25rem 0.5rem; | |
border-radius: 3px; | |
font-size: 0.85rem; | |
border: none; | |
}} | |
.recommended-skill {{ | |
display: inline-block; | |
background-color: rgba(5, 150, 105, 0.1); | |
color: #059669; | |
padding: 0.35rem 0.7rem; | |
border-radius: 9999px; | |
margin-right: 0.5rem; | |
margin-bottom: 0.5rem; | |
font-size: 0.875rem; | |
border: 1px dashed 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)"}; | |
}} | |
/* Unified transition effects */ | |
.stButton > button, | |
.custom-card {{ | |
transition: all 0.2s ease-in-out; | |
}} | |
/* Enhanced dark mode contrast */ | |
.stMarkdown {{ | |
color: var(--text-color) !important; | |
}} | |
/* Button styling */ | |
.stButton button {{ | |
border-radius: 0.375rem; | |
}} | |
/* Theme toggle styling */ | |
.theme-toggle button {{ | |
background-color: var(--primary-color) !important; | |
color: var(--text-color) !important; | |
border: 1px solid var(--accent-light); | |
border-radius: 20px; | |
padding: 8px 16px; | |
margin: 0 var(--spacing-sm); | |
}} | |
.theme-toggle button:hover {{ | |
opacity: 0.9; | |
transform: scale(1.02); | |
}} | |
/* 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: white; | |
color: #333; | |
padding: 1.5rem; | |
font-family: 'Georgia', serif; | |
line-height: 1.6; | |
box-shadow: 0 1px 3px 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; | |
}} | |
/* Print-friendly resume styles */ | |
@media print {{ | |
.resume-container {{ | |
box-shadow: none; | |
padding: 0; | |
max-width: 100%; | |
}} | |
.section-title {{ | |
color: #000 !important; | |
border-bottom-color: #000 !important; | |
}} | |
.resume-name, .resume-title, .job-title, .institution {{ | |
color: #000 !important; | |
}} | |
.company-name {{ | |
color: #333 !important; | |
}} | |
.skill-tag {{ | |
border: 1px solid #ccc; | |
background: #f5f5f5 !important; | |
color: #333 !important; | |
}} | |
}} | |
</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 button | |
return '<button class="header-button" onclick="alert(\'PDF generation would be implemented here in a real app\');">📃 Export PDF</button>' | |
# 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-1.5-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-1.5-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 | |
# 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", | |
key="api_key_input") | |
col1, col2 = st.columns([1, 3]) | |
with col1: | |
if st.button("Save API Key", key="save_api_key_button"): | |
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) | |
# Generate AI summary function | |
def generate_ai_summary(): | |
data = st.session_state.resume_data | |
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 | |
# Job description analysis function | |
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: | |
# Try to parse the entire response first | |
try: | |
return json.loads(analysis) | |
except json.JSONDecodeError: | |
# If direct parsing fails, try to extract JSON | |
json_match = re.search(r'\{[\s\S]*\}', analysis) | |
if json_match: | |
json_str = json_match.group(0) | |
return json.loads(json_str) | |
# If still no valid JSON found, return None | |
return None | |
except Exception as e: | |
st.error(f"Error parsing AI response: {str(e)}") | |
st.write("Raw response:", analysis) | |
return None | |
return None | |
# Generate cover letter function | |
def generate_cover_letter(job_description): | |
data = st.session_state.resume_data | |
prompt = f""" | |
You are an expert cover letter writer. Create a professional cover letter based on this person's resume and the job description. | |
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']])} | |
Rules for the cover letter: | |
1. Keep it concise (3-4 paragraphs) | |
2. Personalize it to the job description | |
3. Highlight relevant experience and skills | |
4. Use professional language but maintain a conversational tone | |
5. Include a strong opening and closing | |
6. Don't include the date or contact information | |
Write ONLY the cover letter content. | |
""" | |
with st.spinner("Generating cover letter..."): | |
cover_letter = get_gemini_response(prompt, temperature=0.7) | |
if cover_letter: | |
return cover_letter.strip() | |
return None | |
# Main application layout | |
def main(): | |
# Apply custom styling | |
apply_custom_styling() | |
# Create a container for the entire app | |
with st.container(): | |
# Render header | |
render_header() | |
# Create tabs | |
tabs = st.tabs(["📝 Resume Editor", "🎯 Job Match", "✉️ Cover Letter"]) | |
# Edit Resume Tab | |
with tabs[0]: | |
if st.session_state.show_preview: | |
col1, col2 = st.columns([3, 4]) | |
else: | |
col1 = st.container() | |
# Edit form in left column | |
with col1: | |
st.markdown('<div class="custom-card">', unsafe_allow_html=True) | |
st.subheader("Personal Information") | |
# Personal info fields | |
st.session_state.resume_data["fullName"] = st.text_input("Full Name", value=st.session_state.resume_data["fullName"], key="fullname_input") | |
st.session_state.resume_data["title"] = st.text_input("Professional Title", value=st.session_state.resume_data["title"], key="title_input") | |
col1_1, col1_2 = st.columns(2) | |
with col1_1: | |
st.session_state.resume_data["email"] = st.text_input("Email", value=st.session_state.resume_data["email"], key="email_input") | |
with col1_2: | |
st.session_state.resume_data["phone"] = st.text_input("Phone", value=st.session_state.resume_data["phone"], key="phone_input") | |
st.session_state.resume_data["location"] = st.text_input("Location", value=st.session_state.resume_data["location"], key="location_input") | |
# Summary with AI assistance | |
st.markdown("### Professional Summary") | |
# Show AI button if API is available | |
if st.session_state.gemini_available: | |
sum_col1, sum_col2 = st.columns([5, 1]) | |
with sum_col1: | |
st.session_state.resume_data["summary"] = st.text_area( | |
"Summary (Professional Overview)", | |
value=st.session_state.resume_data["summary"], | |
height=150, | |
key="summary_input" | |
) | |
with sum_col2: | |
if st.button("✨ AI Generate", key="generate_summary"): | |
summary = generate_ai_summary() | |
if summary: | |
st.session_state.resume_data["summary"] = summary | |
st.rerun() | |
else: | |
st.session_state.resume_data["summary"] = st.text_area( | |
"Summary (Professional Overview)", | |
value=st.session_state.resume_data["summary"], | |
height=150, | |
key="summary_input_no_ai" | |
) | |
st.markdown('</div>', unsafe_allow_html=True) # End of personal info card | |
# Experience section | |
st.markdown('<div class="custom-card">', unsafe_allow_html=True) | |
st.subheader("Experience") | |
# Create fields for each experience item | |
for i, exp in enumerate(st.session_state.resume_data["experience"]): | |
col_exp1, col_exp2, col_exp3 = st.columns([4, 4, 2]) | |
with col_exp1: | |
st.session_state.resume_data["experience"][i]["position"] = st.text_input( | |
"Position/Title", | |
value=exp["position"], | |
key=f"position_{i}" | |
) | |
with col_exp2: | |
st.session_state.resume_data["experience"][i]["company"] = st.text_input( | |
"Company", | |
value=exp["company"], | |
key=f"company_{i}" | |
) | |
with col_exp3: | |
st.session_state.resume_data["experience"][i]["duration"] = st.text_input( | |
"Duration", | |
value=exp["duration"], | |
key=f"duration_{i}" | |
) | |
# Description with AI enhancement option | |
if st.session_state.gemini_available: | |
desc_col1, desc_col2 = st.columns([5, 1]) | |
with desc_col1: | |
st.session_state.resume_data["experience"][i]["description"] = st.text_area( | |
"Description", | |
value=exp["description"], | |
height=100, | |
key=f"description_{i}" | |
) | |
with desc_col2: | |
if st.button("✨ Enhance", key=f"enhance_desc_{i}"): | |
prompt = f""" | |
You are an expert resume writer. Improve this job description to make it more impactful and professional: | |
"{exp["description"]}" | |
Guidelines: | |
1. Use strong action verbs | |
2. Quantify achievements where possible | |
3. Highlight skills and technologies used | |
4. Keep similar length but make it more impactful | |
5. Focus on results and contributions | |
Return ONLY the improved description without quotes or explanations. | |
""" | |
enhanced_desc = get_gemini_response(prompt, temperature=0.7) | |
if enhanced_desc: | |
st.session_state.resume_data["experience"][i]["description"] = enhanced_desc.strip() | |
st.rerun() | |
else: | |
st.session_state.resume_data["experience"][i]["description"] = st.text_area( | |
"Description", | |
value=exp["description"], | |
height=100, | |
key=f"description_no_ai_{i}" | |
) | |
# Add buttons to remove this experience | |
if st.button("🗑️ Remove this experience", key=f"remove_exp_{i}"): | |
if i < len(st.session_state.resume_data["experience"]): | |
st.session_state.resume_data["experience"].pop(i) | |
st.rerun() | |
st.markdown("<hr>", unsafe_allow_html=True) | |
# Add new experience button | |
if st.button("➕ Add New Experience", key="add_new_experience"): | |
# Find the highest ID and increment by 1 | |
max_id = 0 | |
for exp in st.session_state.resume_data["experience"]: | |
if exp["id"] > max_id: | |
max_id = exp["id"] | |
st.session_state.resume_data["experience"].append({ | |
"id": max_id + 1, | |
"company": "New Company", | |
"position": "Position Title", | |
"duration": "Start - End Date", | |
"description": "Describe your responsibilities and achievements." | |
}) | |
st.rerun() | |
st.markdown('</div>', unsafe_allow_html=True) # End of experience card | |
# Education section | |
with st.container(): | |
st.markdown('<div class="custom-card">', unsafe_allow_html=True) | |
st.subheader("Education") | |
# Create fields for each education item | |
for i, edu in enumerate(st.session_state.resume_data["education"]): | |
with st.container(): | |
col_edu1, col_edu2, col_edu3 = st.columns([4, 4, 2], gap="small") | |
with col_edu1: | |
st.session_state.resume_data["education"][i]["institution"] = st.text_input( | |
"Institution", | |
value=edu["institution"], | |
key=f"edit_institution_{i}" | |
) | |
with col_edu2: | |
st.session_state.resume_data["education"][i]["degree"] = st.text_input( | |
"Degree", | |
value=edu["degree"], | |
key=f"edit_degree_{i}" | |
) | |
with col_edu3: | |
st.session_state.resume_data["education"][i]["duration"] = st.text_input( | |
"Duration", | |
value=edu["duration"], | |
key=f"edit_education_duration_{i}" | |
) | |
# Add button to remove this education | |
if st.button("🗑️ Remove this education", key=f"remove_edu_{i}"): | |
st.session_state.resume_data["education"].pop(i) | |
st.rerun() | |
st.markdown("<hr>", unsafe_allow_html=True) | |
# Add new education button | |
if st.button("➕ Add New Education", key="add_new_education"): | |
# Find the highest ID and increment by 1 | |
max_id = 0 | |
for edu in st.session_state.resume_data["education"]: | |
if edu["id"] > max_id: | |
max_id = edu["id"] | |
st.session_state.resume_data["education"].append({ | |
"id": max_id + 1, | |
"institution": "University/College Name", | |
"degree": "Degree Name", | |
"duration": "Start - End Date" | |
}) | |
st.rerun() | |
st.markdown('</div>', unsafe_allow_html=True) # End of education card | |
# Skills section | |
with st.container(): | |
st.markdown('<div class="custom-card">', unsafe_allow_html=True) | |
st.subheader("Skills") | |
# Display current skills as tags with remove option | |
with st.container(): | |
st.markdown('<div class="skills-container" style="margin-bottom: 1.5rem; display: flex; flex-wrap: wrap; gap: 0.5rem;">', unsafe_allow_html=True) | |
for i, skill in enumerate(st.session_state.resume_data["skills"]): | |
st.markdown( | |
f'<span class="skill-tag" style="margin: 0;">{skill} <button style="background: none; border: none; color: inherit; cursor: pointer; padding: 0 0 0 0.25rem;" onclick="document.getElementById(\'remove_skill_{i}\').click();">✕</button></span>', | |
unsafe_allow_html=True | |
) | |
# Hidden button that will be triggered by the ✕ in the skill tag | |
if st.button("✕", key=f"remove_skill_{i}", type="secondary"): | |
st.session_state.resume_data["skills"].remove(skill) | |
st.rerun() | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Add new skill | |
col_skill1, col_skill2 = st.columns([4, 1]) | |
with col_skill1: | |
new_skill = st.text_input("Add Skill", value=st.session_state.new_skill, key="new_skill_input") | |
st.session_state.new_skill = new_skill | |
with col_skill2: | |
if st.button("Add", key="add_skill_button") and st.session_state.new_skill.strip(): | |
if st.session_state.new_skill not in st.session_state.resume_data["skills"]: | |
st.session_state.resume_data["skills"].append(st.session_state.new_skill) | |
st.session_state.new_skill = "" | |
st.rerun() | |
# AI skill suggestions if API is available | |
if st.session_state.gemini_available: | |
if st.button("✨ Suggest relevant skills", key="suggest_skills_section1"): | |
with st.spinner("Generating skill suggestions..."): | |
# Create prompt using current resume data | |
prompt = f""" | |
You are a career advisor. Based on this person's profile, suggest 5-7 relevant technical skills they might want to add to their resume. | |
Current professional title: {st.session_state.resume_data["title"]} | |
Current skills: {', '.join(st.session_state.resume_data["skills"])} | |
Experience: | |
{' '.join([f"{exp['position']} at {exp['company']}: {exp['description']}" for exp in st.session_state.resume_data["experience"]])} | |
Return ONLY a Python list of strings with the skill names, like this: | |
["Skill 1", "Skill 2", "Skill 3", "Skill 4", "Skill 5"] | |
Suggest only skills that are NOT already listed. Focus on technical and professional skills relevant to their field. | |
""" | |
skills_response = get_gemini_response(prompt, temperature=0.2) | |
if skills_response: | |
try: | |
# Improved parsing of AI response - more robust handling | |
# First try to find a list pattern in the response | |
list_pattern = re.search(r'\[.*?\]', skills_response, re.DOTALL) | |
if list_pattern: | |
list_str = list_pattern.group(0) | |
# Try to safely parse the list | |
try: | |
suggested_skills = json.loads(list_str) | |
if isinstance(suggested_skills, list): | |
# Filter out skills they already have | |
st.session_state.suggested_skills = [s for s in suggested_skills if s not in st.session_state.resume_data["skills"]] | |
else: | |
st.error("AI response was not in the expected format. Please try again.") | |
except json.JSONDecodeError: | |
# If JSON parsing fails, try a more lenient approach | |
# Extract items that look like skills from the response | |
skill_matches = re.findall(r'"([^"]+)"', list_str) | |
if skill_matches: | |
st.session_state.suggested_skills = [s for s in skill_matches if s not in st.session_state.resume_data["skills"]] | |
else: | |
st.error("Could not parse skills from AI response. Please try again.") | |
else: | |
# If no list pattern found, look for quoted strings that might be skills | |
skill_matches = re.findall(r'"([^"]+)"', skills_response) | |
if skill_matches: | |
st.session_state.suggested_skills = [s for s in skill_matches if s not in st.session_state.resume_data["skills"]] | |
else: | |
st.error("Could not identify skills in AI response. Please try again.") | |
except Exception as e: | |
st.error(f"Error processing skill suggestions: {str(e)}") | |
# Display suggested skills if any | |
if st.session_state.suggested_skills: | |
st.markdown('<div style="margin-top: 1rem;">', unsafe_allow_html=True) | |
st.markdown("##### Suggested Skills:") | |
for i, skill in enumerate(st.session_state.suggested_skills): | |
st.markdown( | |
f'<span class="recommended-skill">{skill} <button style="background: none; border: none; color: inherit; cursor: pointer; padding: 0 0 0 0.25rem;" onclick="document.getElementById(\'add_suggested_skill_{i}\').click();">+</button></span>', | |
unsafe_allow_html=True | |
) | |
# Hidden button that will be triggered by the + in the skill tag | |
if st.button("+", key=f"add_suggested_skill_{i}", type="secondary"): | |
if skill not in st.session_state.resume_data["skills"]: | |
st.session_state.resume_data["skills"].append(skill) | |
st.session_state.suggested_skills.remove(skill) | |
st.rerun() | |
st.markdown('</div>', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) # End of skills card | |
# Preview in right column (if enabled) | |
if st.session_state.show_preview: | |
if col2 is not None: | |
with col2: | |
st.markdown('<div class="resume-container">', unsafe_allow_html=True) | |
# Header section with clean styling | |
st.markdown('<div class="resume-header">', unsafe_allow_html=True) | |
st.markdown(f'<div class="resume-name">{st.session_state.resume_data["fullName"]}</div>', unsafe_allow_html=True) | |
st.markdown(f'<div class="resume-title">{st.session_state.resume_data["title"]}</div>', unsafe_allow_html=True) | |
# Contact info with better spacing | |
contact_info = f'<div class="contact-info">' | |
contact_info += f'<span>{st.session_state.resume_data["email"]}</span>' | |
contact_info += f'<span>{st.session_state.resume_data["phone"]}</span>' | |
contact_info += f'<span>{st.session_state.resume_data["location"]}</span>' | |
contact_info += f'</div>' | |
st.markdown(contact_info, unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Summary | |
st.markdown('<div class="section-title">SUMMARY</div>', unsafe_allow_html=True) | |
st.markdown(f'<div>{st.session_state.resume_data["summary"]}</div>', unsafe_allow_html=True) | |
# Experience | |
st.markdown('<div class="section-title">EXPERIENCE</div>', unsafe_allow_html=True) | |
for exp in st.session_state.resume_data["experience"]: | |
st.markdown(f'<div class="experience-item">', unsafe_allow_html=True) | |
st.markdown(f'<div class="job-title">{exp["position"]}</div>', unsafe_allow_html=True) | |
st.markdown(f'<div><span class="company-name">{exp["company"]}</span> <span class="duration">| {exp["duration"]}</span></div>', unsafe_allow_html=True) | |
st.markdown(f'<div class="job-description">{exp["description"]}</div>', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Education | |
st.markdown('<div class="section-title">EDUCATION</div>', unsafe_allow_html=True) | |
for edu in st.session_state.resume_data["education"]: | |
st.markdown(f'<div class="education-item">', unsafe_allow_html=True) | |
st.markdown(f'<div class="institution">{edu["institution"]}</div>', unsafe_allow_html=True) | |
st.markdown(f'<div><span class="degree">{edu["degree"]}</span> <span class="duration">| {edu["duration"]}</span></div>', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Skills | |
st.markdown('<div class="section-title">SKILLS</div>', unsafe_allow_html=True) | |
st.markdown('<div class="skills-container">', unsafe_allow_html=True) | |
for skill in st.session_state.resume_data["skills"]: | |
st.markdown(f'<span class="skill-tag">{skill}</span>', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) # End of resume preview container | |
# Print button for resume | |
st.markdown('<div style="text-align: center; margin-top: 1rem;">', unsafe_allow_html=True) | |
st.markdown('<button class="header-button" onclick="window.print()">🖨️ Print Resume</button>', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Import resume section | |
st.markdown('<div class="custom-card" style="margin-top: 1.5rem;">', unsafe_allow_html=True) | |
st.subheader("Import Resume") | |
uploaded_file = st.file_uploader("Import Resume JSON", type=["json"], key="resume_uploader") | |
if uploaded_file is not None: | |
try: | |
imported_data = json.loads(uploaded_file.getvalue().decode()) | |
# Validate the imported data has the required fields | |
required_fields = ["fullName", "title", "email", "phone", "location", "summary", "experience", "education", "skills"] | |
if all(field in imported_data for field in required_fields): | |
st.session_state.resume_data = imported_data | |
st.success("Resume data imported successfully!") | |
st.rerun() | |
else: | |
st.error("Invalid resume data format. Please upload a valid JSON file.") | |
except Exception as e: | |
st.error(f"Error importing data: {str(e)}") | |
st.markdown('</div>', unsafe_allow_html=True) # End of import card | |
# About section | |
st.markdown('<div class="custom-card" style="margin-top: 1.5rem;">', unsafe_allow_html=True) | |
st.subheader("About ResumeBuilder Pro") | |
st.markdown(""" | |
**ResumeBuilder Pro** is a streamlined tool for creating, editing, and optimizing resumes. | |
Features: | |
- Create and edit professional resumes with a user-friendly interface | |
- AI-powered suggestions to enhance your resume content (with Gemini API) | |
- Match your resume against job descriptions | |
- Generate customized cover letters | |
- Export your resume in various formats | |
Version 1.0.0 | |
""") | |
st.markdown('</div>', unsafe_allow_html=True) # End of about card | |
# Job Match Tab | |
with tabs[1]: | |
st.markdown('<div class="custom-card">', unsafe_allow_html=True) | |
st.subheader("Resume-Job Matching") | |
if not st.session_state.gemini_available: | |
manage_api_key() | |
else: | |
st.markdown(""" | |
Analyze how well your resume matches a specific job description. | |
Paste the job description below to get personalized recommendations. | |
""") | |
# Job description input | |
st.session_state.job_description = st.text_area( | |
"Paste the job description here", | |
height=200, | |
value=st.session_state.job_description, | |
key="job_description_input" | |
) | |
if st.button("🔍 Analyze Match", key="analyze_match_button") and st.session_state.job_description: | |
analysis = analyze_job_description(st.session_state.job_description) | |
if analysis: | |
st.session_state.ai_suggestions = analysis | |
# Extract suggested skills for adding to resume | |
if "missing_skills" in analysis: | |
st.session_state.suggested_skills = analysis["missing_skills"] | |
# Display analysis results if available | |
if st.session_state.ai_suggestions and "match_percentage" in st.session_state.ai_suggestions: | |
match_percentage = st.session_state.ai_suggestions["match_percentage"] | |
st.markdown(f"### Resume Match: {match_percentage}%") | |
# Match percentage meter | |
st.markdown(f'<div class="match-meter"><div class="match-fill" style="width:{match_percentage}%;"></div></div>', unsafe_allow_html=True) | |
# Missing skills | |
if "missing_skills" in st.session_state.ai_suggestions and st.session_state.ai_suggestions["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:") | |
for skill in st.session_state.ai_suggestions["missing_skills"]: | |
skill_hash = hash(skill) | |
st.markdown( | |
f'<span class="recommended-skill">{skill} <button style="background: none; border: none; color: inherit; cursor: pointer; padding: 0 0 0 0.25rem;" onclick="document.getElementById(\'add_missing_skill_{skill_hash}\').click();">+</button></span>', | |
unsafe_allow_html=True | |
) | |
if st.button("+", key=f"add_missing_skill_{skill_hash}", type="secondary"): | |
if skill not in st.session_state.resume_data["skills"]: | |
st.session_state.resume_data["skills"].append(skill) | |
st.rerun() | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Highlight skills | |
if "highlight_skills" in st.session_state.ai_suggestions and st.session_state.ai_suggestions["highlight_skills"]: | |
st.markdown('<div class="analysis-card">', unsafe_allow_html=True) | |
st.markdown("#### Relevant Skills") | |
st.markdown("These skills from your resume are particularly relevant:") | |
for skill in st.session_state.ai_suggestions["highlight_skills"]: | |
st.markdown(f'<span class="skill-tag">{skill}</span>', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Emphasis suggestions | |
if "emphasis_suggestions" in st.session_state.ai_suggestions and st.session_state.ai_suggestions["emphasis_suggestions"]: | |
st.markdown('<div class="analysis-card">', unsafe_allow_html=True) | |
st.markdown("#### Emphasis Suggestions") | |
st.markdown("Consider emphasizing these aspects of your experience:") | |
for suggestion in st.session_state.ai_suggestions["emphasis_suggestions"]: | |
st.markdown(f"- {suggestion}") | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Improvement tips | |
if "improvement_tips" in st.session_state.ai_suggestions and st.session_state.ai_suggestions["improvement_tips"]: | |
st.markdown('<div class="analysis-card">', unsafe_allow_html=True) | |
st.markdown("#### Improvement Tips") | |
for tip in st.session_state.ai_suggestions["improvement_tips"]: | |
st.markdown(f"- {tip}") | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Generate optimized summary button | |
if st.button("✨ Generate Optimized Summary", key="generate_optimized_summary"): | |
summary = generate_ai_summary() | |
if summary: # Only update if summary was successfully generated | |
st.session_state.resume_data["summary"] = summary | |
st.success("Professional summary updated to better match the job description!") | |
st.rerun() | |
st.markdown('</div>', unsafe_allow_html=True) # End of job match card | |
# Cover Letter Tab | |
with tabs[2]: | |
st.markdown('<div class="custom-card">', unsafe_allow_html=True) | |
st.subheader("Cover Letter Generator") | |
if not st.session_state.gemini_available: | |
manage_api_key() | |
else: | |
st.markdown(""" | |
Generate a customized cover letter based on your resume and a job description. | |
Paste the job description below to create a tailored cover letter. | |
""") | |
# Job description input for cover letter | |
job_desc_for_letter = st.text_area( | |
"Paste the job description here", | |
height=200, | |
value=st.session_state.job_description, | |
key="job_description_for_letter" | |
) | |
if st.button("✨ Generate Cover Letter", key="generate_cover_letter") and job_desc_for_letter: | |
cover_letter = generate_cover_letter(job_desc_for_letter) | |
if cover_letter: | |
st.session_state.cover_letter = cover_letter | |
# Display and edit cover letter | |
if st.session_state.cover_letter: | |
st.markdown("### Your Cover Letter") | |
st.session_state.cover_letter = st.text_area( | |
"Edit your cover letter as needed", | |
value=st.session_state.cover_letter, | |
height=400, | |
key="cover_letter_editor" | |
) | |
# Display formatted cover letter | |
st.markdown('<div class="cover-letter">', unsafe_allow_html=True) | |
st.markdown(f"<p>{st.session_state.cover_letter.replace(chr(10), '<br>')}</p>", unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Copy to clipboard button (note: this is a placeholder, as direct clipboard access requires JavaScript) | |
st.markdown( | |
f'<button class="header-button" onclick="alert(\'Copy functionality would be implemented with JavaScript in a real app\');">📋 Copy to Clipboard</button>', | |
unsafe_allow_html=True | |
) | |
st.markdown('</div>', unsafe_allow_html=True) # End of cover letter card | |
# Entry point of the application | |
if __name__ == "__main__": | |
# Try to import the Gemini library | |
try: | |
import google.generativeai as genai | |
ST_GEMINI_IMPORT_SUCCESS = True | |
except ImportError: | |
ST_GEMINI_IMPORT_SUCCESS = False | |
# Initialize session state variables if they don't exist | |
if "initialized" not in st.session_state: | |
# General app state | |
st.session_state.initialized = True | |
st.session_state.dark_mode = True # Default to dark mode | |
st.session_state.show_preview = True # Show preview by default | |
# API and AI state | |
st.session_state.gemini_import_success = ST_GEMINI_IMPORT_SUCCESS | |
st.session_state.gemini_available = False | |
st.session_state.api_key = "" | |
# Resume data | |
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"] | |
} | |
st.session_state.new_skill = "" | |
st.session_state.suggested_skills = [] | |
# Job matching | |
st.session_state.ai_suggestions = {} | |
st.session_state.job_description = "" | |
# Cover letter | |
st.session_state.cover_letter = "" | |
# Run the main application | |
main() | |