noumanjavaid commited on
Commit
e2b3134
Β·
verified Β·
1 Parent(s): ba6fe99

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +929 -2
app.py CHANGED
@@ -1,4 +1,931 @@
1
  import streamlit as st
 
 
 
 
 
 
 
2
 
3
- x = st.slider('Select a value')
4
- st.write(x, 'squared is', x * x)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)