Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,4 +1,931 @@
|
|
1 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
-
|
4 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
+
import base64
|
3 |
+
from io import BytesIO
|
4 |
+
import json
|
5 |
+
from datetime import datetime
|
6 |
+
import time
|
7 |
+
import re
|
8 |
+
import os
|
9 |
|
10 |
+
# Set page configuration and title
|
11 |
+
st.set_page_config(
|
12 |
+
page_title="ResumeBuilder Pro",
|
13 |
+
layout="wide",
|
14 |
+
initial_sidebar_state="collapsed"
|
15 |
+
)
|
16 |
+
|
17 |
+
# Define a flag to check if API functionality is available
|
18 |
+
if "gemini_available" not in st.session_state:
|
19 |
+
st.session_state.gemini_available = False
|
20 |
+
|
21 |
+
# Try to import Google Generative AI library, but make it optional
|
22 |
+
try:
|
23 |
+
import google.generativeai as genai
|
24 |
+
st.session_state.gemini_import_success = True
|
25 |
+
except ImportError:
|
26 |
+
st.session_state.gemini_import_success = False
|
27 |
+
|
28 |
+
# Initialize Gemini API (modified to handle missing API key gracefully)
|
29 |
+
def initialize_gemini_api():
|
30 |
+
if not st.session_state.gemini_import_success:
|
31 |
+
st.warning("Google Generative AI library not installed. Install with: `pip install google-generativeai`")
|
32 |
+
return False
|
33 |
+
|
34 |
+
try:
|
35 |
+
# Get API key from session state first (from text input)
|
36 |
+
api_key = st.session_state.get("api_key", "")
|
37 |
+
|
38 |
+
# Only try to configure if API key is provided
|
39 |
+
if api_key:
|
40 |
+
genai.configure(api_key=api_key)
|
41 |
+
# Test the API with a simple call to verify it works
|
42 |
+
model = genai.GenerativeModel(model_name="gemini-pro")
|
43 |
+
_ = model.generate_content("Hello")
|
44 |
+
return True
|
45 |
+
else:
|
46 |
+
# No API key provided yet, not an error
|
47 |
+
return False
|
48 |
+
except Exception as e:
|
49 |
+
st.error(f"Failed to initialize Gemini API: {str(e)}")
|
50 |
+
return False
|
51 |
+
|
52 |
+
# Function to get Gemini model response
|
53 |
+
def get_gemini_response(prompt, temperature=0.7):
|
54 |
+
if not st.session_state.gemini_available:
|
55 |
+
st.warning("Gemini API not available. Please set up your API key.")
|
56 |
+
return None
|
57 |
+
|
58 |
+
try:
|
59 |
+
# Create the model
|
60 |
+
generation_config = {
|
61 |
+
"temperature": temperature,
|
62 |
+
"top_p": 0.95,
|
63 |
+
"top_k": 64,
|
64 |
+
"max_output_tokens": 8192,
|
65 |
+
}
|
66 |
+
|
67 |
+
model = genai.GenerativeModel(
|
68 |
+
model_name="gemini-pro",
|
69 |
+
generation_config=generation_config,
|
70 |
+
)
|
71 |
+
|
72 |
+
response = model.generate_content(prompt)
|
73 |
+
return response.text
|
74 |
+
except Exception as e:
|
75 |
+
st.error(f"Error getting AI response: {str(e)}")
|
76 |
+
return None
|
77 |
+
|
78 |
+
# Initialize session state for resume data
|
79 |
+
if "resume_data" not in st.session_state:
|
80 |
+
st.session_state.resume_data = {
|
81 |
+
"fullName": "Alexander Johnson",
|
82 |
+
"title": "Senior Frontend Developer",
|
83 |
+
"email": "[email protected]",
|
84 |
+
"phone": "(555) 123-4567",
|
85 |
+
"location": "San Francisco, CA",
|
86 |
+
"summary": "Experienced frontend developer with 6+ years specializing in React and modern JavaScript frameworks. Passionate about creating intuitive user interfaces and optimizing web performance.",
|
87 |
+
"experience": [
|
88 |
+
{
|
89 |
+
"id": 1,
|
90 |
+
"company": "Tech Innovations Inc.",
|
91 |
+
"position": "Senior Frontend Developer",
|
92 |
+
"duration": "2019 - Present",
|
93 |
+
"description": "Lead frontend development for enterprise SaaS platform. Improved performance by 40% through code optimization. Mentored junior developers."
|
94 |
+
},
|
95 |
+
{
|
96 |
+
"id": 2,
|
97 |
+
"company": "WebSolutions Co.",
|
98 |
+
"position": "Frontend Developer",
|
99 |
+
"duration": "2017 - 2019",
|
100 |
+
"description": "Developed responsive web applications using React. Collaborated with design team to implement UI/UX improvements."
|
101 |
+
}
|
102 |
+
],
|
103 |
+
"education": [
|
104 |
+
{
|
105 |
+
"id": 1,
|
106 |
+
"institution": "University of California, Berkeley",
|
107 |
+
"degree": "B.S. Computer Science",
|
108 |
+
"duration": "2013 - 2017"
|
109 |
+
}
|
110 |
+
],
|
111 |
+
"skills": ["React", "JavaScript", "TypeScript", "HTML/CSS", "Redux", "Next.js", "Tailwind CSS", "UI/UX Design"]
|
112 |
+
}
|
113 |
+
|
114 |
+
if "dark_mode" not in st.session_state:
|
115 |
+
st.session_state.dark_mode = False
|
116 |
+
|
117 |
+
if "show_preview" not in st.session_state:
|
118 |
+
st.session_state.show_preview = True
|
119 |
+
|
120 |
+
if "new_skill" not in st.session_state:
|
121 |
+
st.session_state.new_skill = ""
|
122 |
+
|
123 |
+
if "job_description" not in st.session_state:
|
124 |
+
st.session_state.job_description = ""
|
125 |
+
|
126 |
+
if "ai_suggestions" not in st.session_state:
|
127 |
+
st.session_state.ai_suggestions = {}
|
128 |
+
|
129 |
+
if "cover_letter" not in st.session_state:
|
130 |
+
st.session_state.cover_letter = ""
|
131 |
+
|
132 |
+
if "suggested_skills" not in st.session_state:
|
133 |
+
st.session_state.suggested_skills = []
|
134 |
+
|
135 |
+
# Apply custom styling based on dark/light mode
|
136 |
+
def apply_custom_styling():
|
137 |
+
dark_mode = st.session_state.dark_mode
|
138 |
+
primary_color = "#4F46E5" # Indigo
|
139 |
+
|
140 |
+
if dark_mode:
|
141 |
+
background_color = "#111827" # Dark gray
|
142 |
+
text_color = "#F9FAFB" # Almost white
|
143 |
+
card_bg = "#1F2937" # Medium gray
|
144 |
+
input_bg = "#374151" # Light gray
|
145 |
+
secondary_bg = "#1E3A8A" # Dark blue
|
146 |
+
accent_light = "#818CF8" # Lighter indigo
|
147 |
+
else:
|
148 |
+
background_color = "#F9FAFB" # Almost white
|
149 |
+
text_color = "#111827" # Dark gray
|
150 |
+
card_bg = "#FFFFFF" # White
|
151 |
+
input_bg = "#F3F4F6" # Light gray
|
152 |
+
secondary_bg = "#EEF2FF" # Light indigo
|
153 |
+
accent_light = "#C7D2FE" # Very light indigo
|
154 |
+
|
155 |
+
css = f"""
|
156 |
+
<style>
|
157 |
+
/* Base colors and fonts */
|
158 |
+
:root {{
|
159 |
+
--primary-color: {primary_color};
|
160 |
+
--bg-color: {background_color};
|
161 |
+
--text-color: {text_color};
|
162 |
+
--card-bg: {card_bg};
|
163 |
+
--input-bg: {input_bg};
|
164 |
+
--secondary-bg: {secondary_bg};
|
165 |
+
--accent-light: {accent_light};
|
166 |
+
}}
|
167 |
+
|
168 |
+
/* Main page styling */
|
169 |
+
.main .block-container {{
|
170 |
+
padding-top: 2rem;
|
171 |
+
max-width: 1200px;
|
172 |
+
}}
|
173 |
+
|
174 |
+
.stApp {{
|
175 |
+
background-color: var(--bg-color);
|
176 |
+
color: var(--text-color);
|
177 |
+
}}
|
178 |
+
|
179 |
+
/* Header styling */
|
180 |
+
h1, h2, h3, h4, h5, h6 {{
|
181 |
+
color: var(--text-color);
|
182 |
+
}}
|
183 |
+
|
184 |
+
/* Make inputs match theme */
|
185 |
+
.stTextInput > div > div > input,
|
186 |
+
.stTextArea > div > div > textarea {{
|
187 |
+
background-color: var(--input-bg);
|
188 |
+
color: var(--text-color);
|
189 |
+
border-radius: 0.375rem;
|
190 |
+
}}
|
191 |
+
|
192 |
+
/* Custom card styling */
|
193 |
+
.custom-card {{
|
194 |
+
background-color: var(--card-bg);
|
195 |
+
border-radius: 0.5rem;
|
196 |
+
padding: 1.5rem;
|
197 |
+
margin-bottom: 1rem;
|
198 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
199 |
+
}}
|
200 |
+
|
201 |
+
/* AI feature styling */
|
202 |
+
.ai-container {{
|
203 |
+
background-color: var(--secondary-bg);
|
204 |
+
border-radius: 0.5rem;
|
205 |
+
padding: 1rem;
|
206 |
+
margin-bottom: 1rem;
|
207 |
+
border-left: 4px solid var(--primary-color);
|
208 |
+
}}
|
209 |
+
|
210 |
+
.ai-suggestion {{
|
211 |
+
background-color: var(--accent-light);
|
212 |
+
border-radius: 0.375rem;
|
213 |
+
padding: 0.75rem;
|
214 |
+
margin-top: 0.5rem;
|
215 |
+
font-size: 0.875rem;
|
216 |
+
border-left: 3px solid var(--primary-color);
|
217 |
+
}}
|
218 |
+
|
219 |
+
/* Resume preview styling */
|
220 |
+
.resume-container {{
|
221 |
+
background-color: var(--card-bg);
|
222 |
+
border-radius: 0.5rem;
|
223 |
+
padding: 2rem;
|
224 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
225 |
+
}}
|
226 |
+
|
227 |
+
.resume-header {{
|
228 |
+
margin-bottom: 1.5rem;
|
229 |
+
}}
|
230 |
+
|
231 |
+
.resume-name {{
|
232 |
+
font-size: 2rem;
|
233 |
+
font-weight: bold;
|
234 |
+
margin-bottom: 0.25rem;
|
235 |
+
color: var(--text-color);
|
236 |
+
}}
|
237 |
+
|
238 |
+
.resume-title {{
|
239 |
+
font-size: 1.25rem;
|
240 |
+
color: var(--primary-color);
|
241 |
+
margin-bottom: 0.5rem;
|
242 |
+
}}
|
243 |
+
|
244 |
+
.contact-info {{
|
245 |
+
font-size: 0.875rem;
|
246 |
+
margin-top: 0.5rem;
|
247 |
+
color: var(--text-color);
|
248 |
+
}}
|
249 |
+
|
250 |
+
.section-title {{
|
251 |
+
font-size: 1.25rem;
|
252 |
+
font-weight: 600;
|
253 |
+
margin-top: 1.5rem;
|
254 |
+
margin-bottom: 1rem;
|
255 |
+
padding-bottom: 0.5rem;
|
256 |
+
border-bottom: 1px solid {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.1)"};
|
257 |
+
color: var(--text-color);
|
258 |
+
}}
|
259 |
+
|
260 |
+
.company-name {{
|
261 |
+
color: var(--primary-color);
|
262 |
+
font-size: 1rem;
|
263 |
+
}}
|
264 |
+
|
265 |
+
.job-title {{
|
266 |
+
font-weight: 600;
|
267 |
+
font-size: 1.125rem;
|
268 |
+
color: var(--text-color);
|
269 |
+
}}
|
270 |
+
|
271 |
+
.duration {{
|
272 |
+
font-size: 0.875rem;
|
273 |
+
color: {"rgba(255,255,255,0.7)" if dark_mode else "rgba(0,0,0,0.6)"};
|
274 |
+
}}
|
275 |
+
|
276 |
+
.job-description {{
|
277 |
+
margin-top: 0.5rem;
|
278 |
+
font-size: 0.9375rem;
|
279 |
+
color: {"rgba(255,255,255,0.9)" if dark_mode else "rgba(0,0,0,0.8)"};
|
280 |
+
}}
|
281 |
+
|
282 |
+
.institution {{
|
283 |
+
font-weight: 600;
|
284 |
+
font-size: 1.125rem;
|
285 |
+
color: var(--text-color);
|
286 |
+
}}
|
287 |
+
|
288 |
+
.degree {{
|
289 |
+
font-size: 1rem;
|
290 |
+
color: {"rgba(255,255,255,0.9)" if dark_mode else "rgba(0,0,0,0.8)"};
|
291 |
+
}}
|
292 |
+
|
293 |
+
.skill-tag {{
|
294 |
+
display: inline-block;
|
295 |
+
background-color: {f"rgba(79, 70, 229, 0.2)" if dark_mode else "rgba(79, 70, 229, 0.1)"};
|
296 |
+
color: {f"rgba(255,255,255,0.9)" if dark_mode else "#4F46E5"};
|
297 |
+
padding: 0.35rem 0.7rem;
|
298 |
+
border-radius: 9999px;
|
299 |
+
margin-right: 0.5rem;
|
300 |
+
margin-bottom: 0.5rem;
|
301 |
+
font-size: 0.875rem;
|
302 |
+
}}
|
303 |
+
|
304 |
+
.recommended-skill {{
|
305 |
+
display: inline-block;
|
306 |
+
background-color: {f"rgba(5, 150, 105, 0.2)" if dark_mode else "rgba(5, 150, 105, 0.1)"};
|
307 |
+
color: {f"rgba(255,255,255,0.9)" if dark_mode else "#059669"};
|
308 |
+
padding: 0.35rem 0.7rem;
|
309 |
+
border-radius: 9999px;
|
310 |
+
margin-right: 0.5rem;
|
311 |
+
margin-bottom: 0.5rem;
|
312 |
+
font-size: 0.875rem;
|
313 |
+
border: 1px dashed {f"rgba(5, 150, 105, 0.5)" if dark_mode else "rgba(5, 150, 105, 0.5)"};
|
314 |
+
}}
|
315 |
+
|
316 |
+
/* Tab styling */
|
317 |
+
button[data-baseweb="tab"] {{
|
318 |
+
background-color: var(--secondary-bg);
|
319 |
+
border-radius: 0.375rem 0.375rem 0 0;
|
320 |
+
}}
|
321 |
+
|
322 |
+
button[data-baseweb="tab"][aria-selected="true"] {{
|
323 |
+
background-color: var(--primary-color) !important;
|
324 |
+
color: white !important;
|
325 |
+
}}
|
326 |
+
|
327 |
+
div[data-testid="stVerticalBlock"] div[data-testid="stHorizontalBlock"] {{
|
328 |
+
gap: 0.5rem;
|
329 |
+
}}
|
330 |
+
|
331 |
+
/* Section separators */
|
332 |
+
hr {{
|
333 |
+
margin: 1.5rem 0;
|
334 |
+
border-color: {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.1)"};
|
335 |
+
}}
|
336 |
+
|
337 |
+
/* Button styling */
|
338 |
+
.stButton button {{
|
339 |
+
border-radius: 0.375rem;
|
340 |
+
}}
|
341 |
+
|
342 |
+
/* Make the buttons in header look nicer */
|
343 |
+
.header-button {{
|
344 |
+
background-color: var(--card-bg) !important;
|
345 |
+
color: var(--text-color) !important;
|
346 |
+
border: 1px solid {f"rgba(255,255,255,0.2)" if dark_mode else "rgba(0,0,0,0.1)"} !important;
|
347 |
+
border-radius: 0.375rem !important;
|
348 |
+
padding: 0.5rem 1rem !important;
|
349 |
+
font-size: 0.875rem !important;
|
350 |
+
transition: all 0.2s ease !important;
|
351 |
+
}}
|
352 |
+
|
353 |
+
.header-button:hover {{
|
354 |
+
background-color: {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.05)"} !important;
|
355 |
+
border-color: {f"rgba(255,255,255,0.3)" if dark_mode else "rgba(0,0,0,0.2)"} !important;
|
356 |
+
}}
|
357 |
+
|
358 |
+
/* Container styling */
|
359 |
+
.content-container {{
|
360 |
+
display: flex;
|
361 |
+
gap: 1.5rem;
|
362 |
+
}}
|
363 |
+
|
364 |
+
/* Adjust form field spacing */
|
365 |
+
div[data-baseweb="input"] {{
|
366 |
+
margin-bottom: 0.75rem;
|
367 |
+
}}
|
368 |
+
|
369 |
+
/* Make the expander look cleaner */
|
370 |
+
.st-emotion-cache-1r6slb0[data-testid="stExpander"] {{
|
371 |
+
background-color: var(--card-bg);
|
372 |
+
border-radius: 0.5rem;
|
373 |
+
border: 1px solid {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.1)"};
|
374 |
+
margin-bottom: 1rem;
|
375 |
+
}}
|
376 |
+
|
377 |
+
/* Remove the border around the tab content area */
|
378 |
+
.st-emotion-cache-cio0dv[data-testid="block-container"] {{
|
379 |
+
padding-left: 0;
|
380 |
+
padding-right: 0;
|
381 |
+
}}
|
382 |
+
|
383 |
+
/* Smaller margin for experience items */
|
384 |
+
.experience-row {{
|
385 |
+
margin-bottom: 0.25rem;
|
386 |
+
}}
|
387 |
+
|
388 |
+
/* AI button styling */
|
389 |
+
.ai-button {{
|
390 |
+
display: inline-flex;
|
391 |
+
align-items: center;
|
392 |
+
background-color: var(--primary-color);
|
393 |
+
color: white;
|
394 |
+
padding: 0.5rem 1rem;
|
395 |
+
border-radius: 0.375rem;
|
396 |
+
font-size: 0.875rem;
|
397 |
+
font-weight: 500;
|
398 |
+
border: none;
|
399 |
+
cursor: pointer;
|
400 |
+
transition: all 0.2s ease;
|
401 |
+
}}
|
402 |
+
|
403 |
+
.ai-button:hover {{
|
404 |
+
background-color: #4338CA;
|
405 |
+
}}
|
406 |
+
|
407 |
+
/* API key container */
|
408 |
+
.api-key-container {{
|
409 |
+
margin-bottom: 1rem;
|
410 |
+
padding: 1rem;
|
411 |
+
background-color: {f"rgba(255,255,255,0.05)" if dark_mode else "rgba(0,0,0,0.03)"};
|
412 |
+
border-radius: 0.5rem;
|
413 |
+
}}
|
414 |
+
|
415 |
+
/* Cover letter styling */
|
416 |
+
.cover-letter {{
|
417 |
+
background-color: var(--card-bg);
|
418 |
+
border-radius: 0.5rem;
|
419 |
+
padding: 2rem;
|
420 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
421 |
+
margin-top: 1rem;
|
422 |
+
}}
|
423 |
+
|
424 |
+
/* Match percentage meter */
|
425 |
+
.match-meter {{
|
426 |
+
height: 1rem;
|
427 |
+
border-radius: 9999px;
|
428 |
+
overflow: hidden;
|
429 |
+
background-color: {f"rgba(255,255,255,0.1)" if dark_mode else "rgba(0,0,0,0.1)"};
|
430 |
+
margin-top: 0.5rem;
|
431 |
+
margin-bottom: 1rem;
|
432 |
+
}}
|
433 |
+
|
434 |
+
.match-fill {{
|
435 |
+
height: 100%;
|
436 |
+
background-color: var(--primary-color);
|
437 |
+
transition: width 1s ease-in-out;
|
438 |
+
}}
|
439 |
+
|
440 |
+
/* Analysis result cards */
|
441 |
+
.analysis-card {{
|
442 |
+
background-color: {f"rgba(255,255,255,0.05)" if dark_mode else "rgba(255,255,255,0.8)"};
|
443 |
+
border-radius: 0.5rem;
|
444 |
+
padding: 1rem;
|
445 |
+
margin-bottom: 1rem;
|
446 |
+
border-left: 3px solid var(--primary-color);
|
447 |
+
}}
|
448 |
+
|
449 |
+
/* AI features banner */
|
450 |
+
.ai-features-banner {{
|
451 |
+
background-color: var(--accent-light);
|
452 |
+
border-radius: 0.5rem;
|
453 |
+
padding: 1rem;
|
454 |
+
margin-bottom: 1rem;
|
455 |
+
border: 1px dashed var(--primary-color);
|
456 |
+
text-align: center;
|
457 |
+
}}
|
458 |
+
</style>
|
459 |
+
"""
|
460 |
+
st.markdown(css, unsafe_allow_html=True)
|
461 |
+
|
462 |
+
# Function to download resume as JSON
|
463 |
+
def download_resume_json():
|
464 |
+
json_str = json.dumps(st.session_state.resume_data, indent=2)
|
465 |
+
b64 = base64.b64encode(json_str.encode()).decode()
|
466 |
+
filename = f"resume_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
467 |
+
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>'
|
468 |
+
return href
|
469 |
+
|
470 |
+
# Function to create a placeholder for PDF export
|
471 |
+
def download_resume_pdf():
|
472 |
+
# In a real app, you would generate an actual PDF here
|
473 |
+
# For this example, we'll just return a placeholder message
|
474 |
+
return '<button class="header-button" onclick="alert(\'PDF generation would be implemented here in a real app\');">π Export PDF</button>'
|
475 |
+
|
476 |
+
# Main application header
|
477 |
+
def render_header():
|
478 |
+
col1, col2 = st.columns([6, 4])
|
479 |
+
|
480 |
+
with col1:
|
481 |
+
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 ""
|
482 |
+
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)
|
483 |
+
|
484 |
+
with col2:
|
485 |
+
download_buttons = f"""
|
486 |
+
<div style="display:flex;justify-content:flex-end;align-items:center;gap:0.5rem;">
|
487 |
+
{download_resume_json()}
|
488 |
+
{download_resume_pdf()}
|
489 |
+
<button class="header-button" onclick="document.querySelector('button[data-testid*=\\"baseButton-secondary\\"]').click();">
|
490 |
+
{'π' if not st.session_state.dark_mode else 'βοΈ'} Theme
|
491 |
+
</button>
|
492 |
+
<button class="header-button" onclick="document.querySelector('button[data-testid*=\\"baseButton-primary\\"]').click();">
|
493 |
+
{'ποΈ' if st.session_state.show_preview else 'ποΈβπ¨οΈ'} Preview
|
494 |
+
</button>
|
495 |
+
</div>
|
496 |
+
"""
|
497 |
+
st.markdown(download_buttons, unsafe_allow_html=True)
|
498 |
+
|
499 |
+
# Hidden buttons that are triggered by the custom buttons above
|
500 |
+
col2_1, col2_2 = st.columns(2)
|
501 |
+
with col2_1:
|
502 |
+
if st.button("Toggle Theme", key="baseButton-secondary", type="secondary"):
|
503 |
+
st.session_state.dark_mode = not st.session_state.dark_mode
|
504 |
+
st.rerun()
|
505 |
+
|
506 |
+
with col2_2:
|
507 |
+
if st.button("Toggle Preview", key="baseButton-primary"):
|
508 |
+
st.session_state.show_preview = not st.session_state.show_preview
|
509 |
+
st.rerun()
|
510 |
+
|
511 |
+
# API Key Management - Improved to be more user-friendly and handle errors better
|
512 |
+
def manage_api_key():
|
513 |
+
if "api_key" not in st.session_state:
|
514 |
+
st.session_state.api_key = ""
|
515 |
+
|
516 |
+
st.markdown('<div class="api-key-container">', unsafe_allow_html=True)
|
517 |
+
|
518 |
+
# Show different message if the generative AI library is not installed
|
519 |
+
if not st.session_state.gemini_import_success:
|
520 |
+
st.markdown("### π§ Enable AI Features")
|
521 |
+
st.markdown("""
|
522 |
+
To use AI features, you need to install the Google Generative AI library:
|
523 |
+
```
|
524 |
+
pip install google-generativeai
|
525 |
+
```
|
526 |
+
After installation, restart the app to access AI-powered resume enhancements.
|
527 |
+
""")
|
528 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
529 |
+
return
|
530 |
+
|
531 |
+
# If library is installed but API key not set
|
532 |
+
st.markdown("### π Gemini API Setup")
|
533 |
+
st.markdown("To use AI features, enter your Google Gemini API key below:")
|
534 |
+
|
535 |
+
api_key = st.text_input("API Key",
|
536 |
+
value=st.session_state.api_key,
|
537 |
+
type="password",
|
538 |
+
placeholder="Enter your Gemini API key here")
|
539 |
+
|
540 |
+
col1, col2 = st.columns([1, 3])
|
541 |
+
with col1:
|
542 |
+
if st.button("Save API Key"):
|
543 |
+
st.session_state.api_key = api_key
|
544 |
+
if initialize_gemini_api():
|
545 |
+
st.session_state.gemini_available = True
|
546 |
+
st.success("API key saved and verified successfully!")
|
547 |
+
else:
|
548 |
+
if api_key:
|
549 |
+
st.error("Invalid API key. Please check and try again.")
|
550 |
+
else:
|
551 |
+
st.warning("Please enter an API key to enable AI features.")
|
552 |
+
|
553 |
+
with col2:
|
554 |
+
st.markdown("Get your API key from [Google AI Studio](https://makersuite.google.com/app/apikey)")
|
555 |
+
|
556 |
+
# Show what AI features are available
|
557 |
+
if not st.session_state.gemini_available:
|
558 |
+
st.markdown('<div class="ai-features-banner">', unsafe_allow_html=True)
|
559 |
+
st.markdown("### π Unlock AI Features")
|
560 |
+
st.markdown("""
|
561 |
+
By adding your API key, you'll unlock powerful AI features:
|
562 |
+
|
563 |
+
- **Professional Summary Generator**: Create compelling summaries automatically
|
564 |
+
- **Job Match Analysis**: Score your resume against job descriptions
|
565 |
+
- **Skills Recommendations**: Get tailored skill suggestions for your role
|
566 |
+
- **Experience Description Enhancement**: Make your work history more impactful
|
567 |
+
- **Cover Letter Generator**: Create customized cover letters in seconds
|
568 |
+
""")
|
569 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
570 |
+
|
571 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
572 |
+
|
573 |
+
# AI Features - These will only be activated when API is available
|
574 |
+
|
575 |
+
# Generate professional summary
|
576 |
+
def generate_ai_summary():
|
577 |
+
data = st.session_state.resume_data
|
578 |
+
|
579 |
+
# Create prompt for summary generation
|
580 |
+
prompt = f"""
|
581 |
+
You are an expert resume writer. Generate a compelling professional summary for a resume with these details:
|
582 |
+
|
583 |
+
Name: {data['fullName']}
|
584 |
+
Current Position: {data['title']}
|
585 |
+
Skills: {', '.join(data['skills'])}
|
586 |
+
|
587 |
+
Experience:
|
588 |
+
{' '.join([f"{exp['position']} at {exp['company']} ({exp['duration']}): {exp['description']}" for exp in data['experience']])}
|
589 |
+
|
590 |
+
Education:
|
591 |
+
{' '.join([f"{edu['degree']} from {edu['institution']} ({edu['duration']})" for edu in data['education']])}
|
592 |
+
|
593 |
+
Rules for writing the summary:
|
594 |
+
1. Keep it concise (3-4 sentences maximum)
|
595 |
+
2. Highlight key skills and accomplishments
|
596 |
+
3. Focus on value provided in past roles
|
597 |
+
4. Use active language and avoid clichΓ©s
|
598 |
+
5. Target it toward professional growth
|
599 |
+
|
600 |
+
Write ONLY the summary. Don't include explanations or other text.
|
601 |
+
"""
|
602 |
+
|
603 |
+
with st.spinner("Generating professional summary..."):
|
604 |
+
summary = get_gemini_response(prompt, temperature=0.7)
|
605 |
+
if summary:
|
606 |
+
# Clean up the response
|
607 |
+
summary = summary.strip().replace('"', '')
|
608 |
+
return summary
|
609 |
+
return None
|
610 |
+
|
611 |
+
# Analyze job description and match skills
|
612 |
+
def analyze_job_description(job_description):
|
613 |
+
data = st.session_state.resume_data
|
614 |
+
|
615 |
+
prompt = f"""
|
616 |
+
You are an expert resume consultant. Analyze this job description and the candidate's resume to provide insights.
|
617 |
+
|
618 |
+
JOB DESCRIPTION:
|
619 |
+
{job_description}
|
620 |
+
|
621 |
+
CANDIDATE RESUME:
|
622 |
+
Name: {data['fullName']}
|
623 |
+
Current Position: {data['title']}
|
624 |
+
Skills: {', '.join(data['skills'])}
|
625 |
+
|
626 |
+
Experience:
|
627 |
+
{' '.join([f"{exp['position']} at {exp['company']} ({exp['duration']}): {exp['description']}" for exp in data['experience']])}
|
628 |
+
|
629 |
+
Education:
|
630 |
+
{' '.join([f"{edu['degree']} from {edu['institution']} ({edu['duration']})" for edu in data['education']])}
|
631 |
+
|
632 |
+
Provide the following in JSON format:
|
633 |
+
1. "match_percentage": A numerical estimate (0-100) of how well the candidate's skills match the job requirements
|
634 |
+
2. "missing_skills": A list of 3-5 key skills mentioned in the job that are missing from the candidate's resume
|
635 |
+
3. "highlight_skills": A list of skills the candidate has that are particularly relevant to this job
|
636 |
+
4. "emphasis_suggestions": 2-3 specific parts of the candidate's experience that should be emphasized for this job
|
637 |
+
5. "improvement_tips": 2-3 brief suggestions to improve the resume for this specific job
|
638 |
+
|
639 |
+
Return ONLY the JSON, formatted as follows:
|
640 |
+
{{
|
641 |
+
"match_percentage": number,
|
642 |
+
"missing_skills": [list of strings],
|
643 |
+
"highlight_skills": [list of strings],
|
644 |
+
"emphasis_suggestions": [list of strings],
|
645 |
+
"improvement_tips": [list of strings]
|
646 |
+
}}
|
647 |
+
"""
|
648 |
+
|
649 |
+
with st.spinner("Analyzing job description..."):
|
650 |
+
analysis = get_gemini_response(prompt, temperature=0.2)
|
651 |
+
if analysis:
|
652 |
+
try:
|
653 |
+
# Extract just the JSON part from the response
|
654 |
+
# First find JSON pattern using regex
|
655 |
+
json_match = re.search(r'\{[\s\S]*\}', analysis)
|
656 |
+
if json_match:
|
657 |
+
json_str = json_match.group(0)
|
658 |
+
return json.loads(json_str)
|
659 |
+
return json.loads(analysis)
|
660 |
+
except Exception as e:
|
661 |
+
st.error(f"Error parsing AI response: {str(e)}")
|
662 |
+
st.write("Raw response:", analysis)
|
663 |
+
return None
|
664 |
+
|
665 |
+
# Improve experience descriptions
|
666 |
+
def improve_experience_description(description, position, company):
|
667 |
+
prompt = f"""
|
668 |
+
You are an expert resume writer. Enhance the following job description to be more impactful and achievement-oriented:
|
669 |
+
|
670 |
+
Position: {position}
|
671 |
+
Company: {company}
|
672 |
+
Current Description: {description}
|
673 |
+
|
674 |
+
Rewrite the description to:
|
675 |
+
1. Focus on achievements with measurable results
|
676 |
+
2. Start with strong action verbs
|
677 |
+
3. Highlight relevant skills and impact
|
678 |
+
4. Be concise but comprehensive (max 3 bullet points)
|
679 |
+
5. Use quantifiable metrics where possible
|
680 |
+
|
681 |
+
Return ONLY the improved description without additional commentary.
|
682 |
+
"""
|
683 |
+
|
684 |
+
with st.spinner("Improving description..."):
|
685 |
+
improved = get_gemini_response(prompt, temperature=0.7)
|
686 |
+
if improved:
|
687 |
+
return improved.strip()
|
688 |
+
return None
|
689 |
+
|
690 |
+
# Generate cover letter
|
691 |
+
def generate_cover_letter(job_description, company_name):
|
692 |
+
data = st.session_state.resume_data
|
693 |
+
|
694 |
+
prompt = f"""
|
695 |
+
You are an expert cover letter writer. Create a personalized cover letter for:
|
696 |
+
|
697 |
+
Applicant: {data['fullName']}
|
698 |
+
Current Position: {data['title']}
|
699 |
+
Skills: {', '.join(data['skills'])}
|
700 |
+
Target Company: {company_name}
|
701 |
+
|
702 |
+
Based on this job description:
|
703 |
+
{job_description}
|
704 |
+
|
705 |
+
Experience highlights:
|
706 |
+
{' '.join([f"{exp['position']} at {exp['company']} ({exp['duration']}): {exp['description']}" for exp in data['experience'][:2]])}
|
707 |
+
|
708 |
+
Cover letter guidelines:
|
709 |
+
1. Keep it under 250 words
|
710 |
+
2. Include a personalized greeting
|
711 |
+
3. Start with an engaging opening that shows enthusiasm
|
712 |
+
4. Connect 2-3 of the applicant's skills/experiences directly to the job requirements
|
713 |
+
5. Include a specific reason why the applicant wants to work at this company
|
714 |
+
6. End with a call to action
|
715 |
+
7. Use a professional closing
|
716 |
+
|
717 |
+
Format it as a proper cover letter with date, greeting, paragraphs, and signature.
|
718 |
+
Return ONLY the cover letter, no explanations or additional notes.
|
719 |
+
"""
|
720 |
+
|
721 |
+
with st.spinner("Generating cover letter..."):
|
722 |
+
cover_letter = get_gemini_response(prompt, temperature=0.7)
|
723 |
+
if cover_letter:
|
724 |
+
return cover_letter.strip()
|
725 |
+
return None
|
726 |
+
|
727 |
+
# Suggest skills based on job title
|
728 |
+
def suggest_skills_for_role(job_title):
|
729 |
+
prompt = f"""
|
730 |
+
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:
|
731 |
+
|
732 |
+
Job Title: {job_title}
|
733 |
+
|
734 |
+
Return the skills as a JSON array of strings. ONLY return the JSON array, no other text.
|
735 |
+
Example: ["Skill 1", "Skill 2", "Skill 3"]
|
736 |
+
"""
|
737 |
+
|
738 |
+
with st.spinner("Suggesting skills..."):
|
739 |
+
skills_json = get_gemini_response(prompt, temperature=0.3)
|
740 |
+
if skills_json:
|
741 |
+
try:
|
742 |
+
# Clean up and parse the response
|
743 |
+
skills_json = skills_json.strip()
|
744 |
+
if skills_json.startswith('```') and skills_json.endswith('```'):
|
745 |
+
skills_json = skills_json[3:-3].strip()
|
746 |
+
if skills_json.startswith('json'):
|
747 |
+
skills_json = skills_json[4:].strip()
|
748 |
+
return json.loads(skills_json)
|
749 |
+
except Exception as e:
|
750 |
+
st.error(f"Error parsing skills: {str(e)}")
|
751 |
+
return []
|
752 |
+
# Basic Info tab with AI enhancement
|
753 |
+
def render_basic_info():
|
754 |
+
data = st.session_state.resume_data
|
755 |
+
|
756 |
+
st.markdown('<div class="custom-card">', unsafe_allow_html=True)
|
757 |
+
st.subheader("Personal Information")
|
758 |
+
|
759 |
+
col1, col2 = st.columns(2)
|
760 |
+
with col1:
|
761 |
+
new_name = st.text_input("Full Name", value=data["fullName"], key="fullName")
|
762 |
+
if new_name != data["fullName"]:
|
763 |
+
st.session_state.resume_data["fullName"] = new_name
|
764 |
+
|
765 |
+
new_email = st.text_input("Email", value=data["email"], key="email")
|
766 |
+
if new_email != data["email"]:
|
767 |
+
st.session_state.resume_data["email"] = new_email
|
768 |
+
|
769 |
+
new_location = st.text_input("Location", value=data["location"], key="location")
|
770 |
+
if new_location != data["location"]:
|
771 |
+
st.session_state.resume_data["location"] = new_location
|
772 |
+
|
773 |
+
with col2:
|
774 |
+
new_title = st.text_input("Professional Title", value=data["title"], key="title")
|
775 |
+
if new_title != data["title"]:
|
776 |
+
st.session_state.resume_data["title"] = new_title
|
777 |
+
|
778 |
+
new_phone = st.text_input("Phone", value=data["phone"], key="phone")
|
779 |
+
if new_phone != data["phone"]:
|
780 |
+
st.session_state.resume_data["phone"] = new_phone
|
781 |
+
|
782 |
+
st.markdown("### Professional Summary")
|
783 |
+
|
784 |
+
# Add AI summary generation if API key is set
|
785 |
+
if st.session_state.gemini_available:
|
786 |
+
col1, col2 = st.columns([3, 1])
|
787 |
+
with col2:
|
788 |
+
if st.button("β¨ Generate AI Summary"):
|
789 |
+
summary = generate_ai_summary()
|
790 |
+
if summary:
|
791 |
+
st.session_state.resume_data["summary"] = summary
|
792 |
+
st.rerun()
|
793 |
+
|
794 |
+
new_summary = st.text_area("", value=data["summary"], height=150, key="summary")
|
795 |
+
if new_summary != data["summary"]:
|
796 |
+
st.session_state.resume_data["summary"] = new_summary
|
797 |
+
|
798 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
799 |
+
|
800 |
+
# Add Job Description Analysis section if API key is set
|
801 |
+
if st.session_state.gemini_available:
|
802 |
+
st.markdown('<div class="ai-container">', unsafe_allow_html=True)
|
803 |
+
st.markdown("### π Job Match Analysis")
|
804 |
+
st.markdown("Paste a job description to get AI-powered insights on how your resume matches the requirements.")
|
805 |
+
|
806 |
+
job_description = st.text_area("Job Description", height=100, key="job_desc_input",
|
807 |
+
value=st.session_state.job_description)
|
808 |
+
|
809 |
+
if job_description:
|
810 |
+
company_name = st.text_input("Company Name (for cover letter)", key="company_name")
|
811 |
+
|
812 |
+
col1, col2, col3 = st.columns([1,1,1])
|
813 |
+
with col1:
|
814 |
+
if st.button("Analyze Job Match"):
|
815 |
+
st.session_state.job_description = job_description
|
816 |
+
analysis = analyze_job_description(job_description)
|
817 |
+
if analysis:
|
818 |
+
st.session_state.ai_suggestions = analysis
|
819 |
+
st.rerun()
|
820 |
+
|
821 |
+
with col2:
|
822 |
+
if st.button("Generate Cover Letter") and company_name:
|
823 |
+
st.session_state.job_description = job_description
|
824 |
+
cover_letter = generate_cover_letter(job_description, company_name)
|
825 |
+
if cover_letter:
|
826 |
+
st.session_state.cover_letter = cover_letter
|
827 |
+
st.rerun()
|
828 |
+
|
829 |
+
with col3:
|
830 |
+
if st.button("Suggest Skills"):
|
831 |
+
suggested_skills = suggest_skills_for_role(data["title"])
|
832 |
+
if suggested_skills:
|
833 |
+
st.session_state.suggested_skills = suggested_skills
|
834 |
+
st.rerun()
|
835 |
+
|
836 |
+
# Display analysis results if available
|
837 |
+
if st.session_state.ai_suggestions:
|
838 |
+
analysis = st.session_state.ai_suggestions
|
839 |
+
|
840 |
+
st.markdown("### π Job Match Analysis Results")
|
841 |
+
|
842 |
+
# Match percentage
|
843 |
+
match_percentage = analysis.get("match_percentage", 0)
|
844 |
+
st.markdown(f"### Match Score: {match_percentage}%")
|
845 |
+
st.markdown('<div class="match-meter"><div class="match-fill" style="width: {}%;"></div></div>'.format(match_percentage), unsafe_allow_html=True)
|
846 |
+
|
847 |
+
# Missing skills
|
848 |
+
if "missing_skills" in analysis and analysis["missing_skills"]:
|
849 |
+
st.markdown('<div class="analysis-card">', unsafe_allow_html=True)
|
850 |
+
st.markdown("#### π΄ Missing Skills")
|
851 |
+
st.markdown("Consider adding these skills to your resume:")
|
852 |
+
missing_skills_html = ""
|
853 |
+
for skill in analysis["missing_skills"]:
|
854 |
+
missing_skills_html += f'<span class="recommended-skill">{skill} <button onclick="document.getElementById(\'add_skill_{skill.replace(" ", "_")}\').click()">+</button></span>'
|
855 |
+
st.markdown(missing_skills_html, unsafe_allow_html=True)
|
856 |
+
|
857 |
+
# Hidden buttons for adding missing skills
|
858 |
+
for skill in analysis["missing_skills"]:
|
859 |
+
if st.button("Add", key=f"add_skill_{skill.replace(' ', '_')}", type="primary"):
|
860 |
+
if skill not in data["skills"]:
|
861 |
+
data["skills"].append(skill)
|
862 |
+
st.rerun()
|
863 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
864 |
+
|
865 |
+
# Highlight skills
|
866 |
+
if "highlight_skills" in analysis and analysis["highlight_skills"]:
|
867 |
+
st.markdown('<div class="analysis-card">', unsafe_allow_html=True)
|
868 |
+
st.markdown("#### π’ Relevant Skills")
|
869 |
+
st.markdown("Highlight these skills prominently in your resume:")
|
870 |
+
highlight_skills_html = ""
|
871 |
+
for skill in analysis["highlight_skills"]:
|
872 |
+
highlight_skills_html += f'<span class="skill-tag">{skill}</span>'
|
873 |
+
st.markdown(highlight_skills_html, unsafe_allow_html=True)
|
874 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
875 |
+
|
876 |
+
# Emphasis suggestions
|
877 |
+
if "emphasis_suggestions" in analysis and analysis["emphasis_suggestions"]:
|
878 |
+
st.markdown('<div class="analysis-card">', unsafe_allow_html=True)
|
879 |
+
st.markdown("#### π‘ Experience to Emphasize")
|
880 |
+
emphasis_html = "<ul>"
|
881 |
+
for suggestion in analysis["emphasis_suggestions"]:
|
882 |
+
emphasis_html += f"<li>{suggestion}</li>"
|
883 |
+
emphasis_html += "</ul>"
|
884 |
+
st.markdown(emphasis_html, unsafe_allow_html=True)
|
885 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
886 |
+
|
887 |
+
# Improvement tips
|
888 |
+
if "improvement_tips" in analysis and analysis["improvement_tips"]:
|
889 |
+
st.markdown('<div class="analysis-card">', unsafe_allow_html=True)
|
890 |
+
st.markdown("#### β‘ Improvement Tips")
|
891 |
+
tips_html = "<ul>"
|
892 |
+
for tip in analysis["improvement_tips"]:
|
893 |
+
tips_html += f"<li>{tip}</li>"
|
894 |
+
tips_html += "</ul>"
|
895 |
+
st.markdown(tips_html, unsafe_allow_html=True)
|
896 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
897 |
+
|
898 |
+
# Display suggested skills if available
|
899 |
+
if hasattr(st.session_state, 'suggested_skills') and st.session_state.suggested_skills:
|
900 |
+
st.markdown("### π·οΈ Suggested Skills for Your Role")
|
901 |
+
suggested_skills_html = ""
|
902 |
+
for skill in st.session_state.suggested_skills:
|
903 |
+
if skill not in data["skills"]:
|
904 |
+
suggested_skills_html += f'<span class="recommended-skill">{skill} <button onclick="document.getElementById(\'add_suggested_skill_{skill.replace(" ", "_")}\').click()">+</button></span>'
|
905 |
+
|
906 |
+
if suggested_skills_html:
|
907 |
+
st.markdown(suggested_skills_html, unsafe_allow_html=True)
|
908 |
+
|
909 |
+
# Hidden buttons for adding suggested skills
|
910 |
+
for skill in st.session_state.suggested_skills:
|
911 |
+
if skill not in data["skills"]:
|
912 |
+
if st.button("Add", key=f"add_suggested_skill_{skill.replace(' ', '_')}", type="primary"):
|
913 |
+
data["skills"].append(skill)
|
914 |
+
st.rerun()
|
915 |
+
else:
|
916 |
+
st.info("All suggested skills are already in your resume!")
|
917 |
+
|
918 |
+
# Display cover letter if available
|
919 |
+
if st.session_state.cover_letter:
|
920 |
+
st.markdown("### π Generated Cover Letter")
|
921 |
+
st.markdown('<div class="cover-letter">', unsafe_allow_html=True)
|
922 |
+
# Replace newlines with <br> tags for proper HTML display
|
923 |
+
formatted_letter = st.session_state.cover_letter.replace('\n', '<br>')
|
924 |
+
st.markdown(formatted_letter, unsafe_allow_html=True)
|
925 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
926 |
+
|
927 |
+
# Add button to copy cover letter to clipboard
|
928 |
+
if st.button("π Copy Cover Letter"):
|
929 |
+
st.toast("Cover letter copied to clipboard!", icon="π")
|
930 |
+
|
931 |
+
st.markdown('</div>', unsafe_allow_html=True)
|