NinjainPJs commited on
Commit
e9bc6f3
Β·
verified Β·
1 Parent(s): fdf49b6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +616 -602
app.py CHANGED
@@ -1,603 +1,617 @@
1
- from openai import OpenAI, RateLimitError
2
- import streamlit as st
3
- import time
4
- import os
5
- # Add at the top of the file after imports:
6
- from typing import Dict, Optional
7
- # Page configuration
8
- st.set_page_config(
9
- page_title="LinkedIn Recommendation Generator",
10
- page_icon="πŸ‘”",
11
- layout="wide",
12
- initial_sidebar_state="collapsed"
13
- )
14
-
15
- # Custom CSS for professional styling
16
- st.markdown("""
17
- <style>
18
- /* Import LinkedIn-style font */
19
- @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap');
20
-
21
- /* Main container styling */
22
- .main-container {
23
- max-width: 1000px;
24
- margin: 0 auto;
25
- padding: 2rem;
26
- background: linear-gradient(135deg, #f8f9ff 0%, #e8f4f8 100%);
27
- min-height: 100vh;
28
- }
29
-
30
- /* Header styling */
31
- .header-container {
32
- background: white;
33
- padding: 2rem;
34
- border-radius: 20px;
35
- box-shadow: 0 8px 32px rgba(0,0,0,0.1);
36
- text-align: center;
37
- margin-bottom: 2rem;
38
- border: 1px solid rgba(255,255,255,0.2);
39
- }
40
-
41
- .linkedin-logo {
42
- width: 60px;
43
- height: 60px;
44
- background: linear-gradient(135deg, #0077B5 0%, #005885 100%);
45
- border-radius: 15px;
46
- display: inline-flex;
47
- align-items: center;
48
- justify-content: center;
49
- margin-bottom: 1rem;
50
- box-shadow: 0 4px 15px rgba(0,119,181,0.3);
51
- }
52
-
53
- .main-title {
54
- font-family: 'Source Sans Pro', sans-serif;
55
- font-size: 2.5rem;
56
- font-weight: 700;
57
- color: #0077B5;
58
- margin: 0;
59
- margin-bottom: 0.5rem;
60
- }
61
-
62
- .subtitle {
63
- font-family: 'Source Sans Pro', sans-serif;
64
- font-size: 1.2rem;
65
- color: #666;
66
- margin: 0;
67
- font-weight: 400;
68
- }
69
-
70
- /* Section headers */
71
- .section-header {
72
- font-family: 'Source Sans Pro', sans-serif;
73
- font-size: 1.5rem;
74
- font-weight: 600;
75
- color: #0077B5;
76
- margin-bottom: 1.5rem;
77
- padding-bottom: 0.5rem;
78
- border-bottom: 2px solid #e8f4f8;
79
- }
80
-
81
- /* Sub-section headers styling */
82
- .sub-section-header {
83
- font-family: 'Source Sans Pro', sans-serif;
84
- font-size: 1.3rem;
85
- font-weight: 600;
86
- color: #0077B5;
87
- margin: 1.5rem 0 1rem 0;
88
- padding: 0.5rem 0;
89
- border-bottom: 2px solid rgba(0, 119, 181, 0.2);
90
- }
91
-
92
- /* Custom star rating styling */
93
- .star-rating {
94
- display: flex;
95
- gap: 8px;
96
- align-items: center;
97
- margin: 10px 0;
98
- padding: 15px;
99
- background: #f8f9ff;
100
- border-radius: 12px;
101
- border: 1px solid #e8f4f8;
102
- }
103
-
104
- .star-question {
105
- font-family: 'Source Sans Pro', sans-serif;
106
- font-weight: 500;
107
- color: #0077B5; /* Changed from white to blue for visibility */
108
- font-size: 1rem;
109
- flex: 1;
110
- margin-right: 20px;
111
- }
112
-
113
- /* Result container */
114
- .result-container {
115
- background: linear-gradient(135deg, #0077B5 0%, #005885 100%);
116
- color: white;
117
- padding: 2.5rem;
118
- border-radius: 20px;
119
- box-shadow: 0 8px 32px rgba(0,119,181,0.3);
120
- margin-top: 2rem;
121
- }
122
-
123
- .result-title {
124
- font-family: 'Source Sans Pro', sans-serif;
125
- font-size: 1.8rem;
126
- font-weight: 600;
127
- margin-bottom: 1rem;
128
- }
129
-
130
- .recommendation-text {
131
- background: rgba(255,255,255,0.15);
132
- padding: 2rem;
133
- border-radius: 15px;
134
- font-family: 'Source Sans Pro', sans-serif;
135
- font-size: 1.1rem;
136
- line-height: 1.6;
137
- margin-bottom: 1.5rem;
138
- backdrop-filter: blur(10px);
139
- border: 1px solid rgba(255,255,255,0.2);
140
- }
141
-
142
- /* Style for the code block that appears on copy */
143
- .stCodeBlock {
144
- border-radius: 15px !important;
145
- border: 1px solid #e8f4f8 !important;
146
- }
147
- .stCodeBlock pre {
148
- min-height: 200px; /* Increase the default height */
149
- max-height: 400px;
150
- overflow-y: auto !important;
151
- white-space: pre-wrap !important; /* Allow text to wrap */
152
- }
153
-
154
- /* Button styling */
155
- .stButton > button {
156
- background: linear-gradient(135deg, #0077B5 0%, #005885 100%);
157
- color: white;
158
- border: none;
159
- padding: 0.8rem 2rem;
160
- border-radius: 25px;
161
- font-weight: 600;
162
- font-family: 'Source Sans Pro', sans-serif;
163
- font-size: 1rem;
164
- cursor: pointer;
165
- transition: all 0.3s ease;
166
- box-shadow: 0 4px 15px rgba(0,119,181,0.3);
167
- width: 100%;
168
- }
169
-
170
- .stButton > button:hover {
171
- transform: translateY(-2px);
172
- box-shadow: 0 6px 20px rgba(0,119,181,0.4);
173
- }
174
-
175
- /* Selectbox styling */
176
- .stSelectbox > div > div {
177
- background: #f8f9ff;
178
- border: 1px solid #e8f4f8;
179
- border-radius: 12px;
180
- font-family: 'Source Sans Pro', sans-serif;
181
- }
182
-
183
- /* Text input styling */
184
- .stTextInput > div > div > input {
185
- background: #f8f9ff;
186
- border: 1px solid #e8f4f8;
187
- border-radius: 12px;
188
- font-family: 'Source Sans Pro', sans-serif;
189
- padding: 12px 16px;
190
- }
191
-
192
- /* Progress bar */
193
- .progress-container {
194
- background: white;
195
- padding: 1.5rem;
196
- border-radius: 15px;
197
- margin: 1rem 0;
198
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
199
- }
200
-
201
- /* Hide Streamlit components */
202
- #MainMenu {visibility: hidden;}
203
- footer {visibility: hidden;}
204
- header {visibility: hidden;}
205
-
206
- /* Custom metric styling */
207
- .metric-container {
208
- background: linear-gradient(135deg, #f8f9ff 0%, #e8f4f8 100%);
209
- padding: 1rem;
210
- border-radius: 12px;
211
- text-align: center;
212
- margin: 0.5rem 0;
213
- border: 1px solid #e8f4f8;
214
- }
215
-
216
- /* Form field uniform sizing and styling */
217
- .stTextInput > div {
218
- width: 100% !important;
219
- }
220
-
221
- .stSelectbox > div {
222
- width: 100% !important;
223
- }
224
-
225
- .stTextInput > div > div > input {
226
- background-color: white !important;
227
- color: #333 !important;
228
- min-height: 48px !important;
229
- border: 1px solid #e8f4f8 !important;
230
- border-radius: 8px !important;
231
- padding: 0.5rem 1rem !important;
232
- }
233
-
234
- .stSelectbox > div > div {
235
- background-color: white !important;
236
- color: #333 !important;
237
- min-height: 48px !important;
238
- border: 1px solid #e8f4f8 !important;
239
- border-radius: 8px !important;
240
- }
241
-
242
- /* Add consistent spacing between star ratings */
243
- .star-rating-container {
244
- margin-bottom: 1rem;
245
- }
246
-
247
- /* Container for form fields */
248
- .form-field-container {
249
- padding: 0.5rem 0;
250
- }
251
- </style>
252
- """, unsafe_allow_html=True)
253
-
254
- def create_star_rating(label, key, help_text=None):
255
- """Create a custom 5-star rating component"""
256
- # Use a container to apply consistent bottom margin via CSS
257
- with st.container():
258
- st.markdown('<div class="star-rating-container">', unsafe_allow_html=True)
259
- col1, col2 = st.columns([3, 2])
260
-
261
- with col1:
262
- st.markdown(f'<div class="star-question">{label}</div>', unsafe_allow_html=True)
263
- if help_text:
264
- st.caption(help_text)
265
-
266
- with col2:
267
- # The select_slider is inside the columns
268
- pass
269
-
270
- rating = st.select_slider(
271
- "",
272
- options=[1, 2, 3, 4, 5],
273
- value=3,
274
- key=key,
275
- label_visibility="collapsed"
276
- )
277
-
278
- # Create visual stars
279
- stars = "".join(["⭐" if i < rating else "β˜†" for i in range(5)])
280
- st.markdown(f"<div style='font-size: 1.5rem; text-align: center; margin-top: -35px;'>{stars}</div>", unsafe_allow_html=True)
281
-
282
- return rating
283
-
284
- def generate_recommendation(ratings: Dict[str, int], employee_type: str, employee_name: str, relationship: str, time_worked: str, linkedin_url: str) -> Optional[str]:
285
- """Generate recommendation using OpenRouter API with input summary"""
286
-
287
- # Organize ratings into categories for analysis
288
- performance_areas = {
289
- "Technical Competence": {
290
- "Domain Knowledge": ratings['domain'],
291
- "Problem Solving": ratings['problem_solving'],
292
- "Initiative": ratings['initiative']
293
- },
294
- "Professional Skills": {
295
- "Adaptability": ratings['adaptability'],
296
- "Communication": ratings['communication']
297
- },
298
- "Interpersonal Impact": {
299
- "Team Collaboration": ratings['teamwork'],
300
- "Support & Guidance": ratings['support']
301
- },
302
- "Overall Performance": {
303
- "Reliability": ratings['reliability'],
304
- "Overall Contribution": ratings['overall'],
305
- "Growth Potential": ratings['potential']
306
- }
307
- }
308
-
309
- # Calculate category averages
310
- category_scores = {}
311
- for category, metrics in performance_areas.items():
312
- category_scores[category] = sum(metrics.values()) / len(metrics)
313
-
314
- # Identify top strengths (ratings of 4 or 5)
315
- strengths = [k for k, v in ratings.items() if v >= 4]
316
-
317
- # Build a text block for the analysis part of the prompt
318
- analysis_text = ""
319
- for category, score in category_scores.items():
320
- analysis_text += f"\n- {category}: {score:.1f}/5"
321
-
322
- # Create a single, comprehensive prompt for a more efficient, single API call
323
- recommendation_prompt = f"""
324
- You are an expert in writing professional LinkedIn recommendations.
325
- Your task is to generate a recommendation for {employee_name}.
326
-
327
- First, silently analyze the provided performance data. Do not output this analysis.
328
- - Employee: {employee_name}
329
- - Role: {employee_type}
330
- - My Relationship to them: {relationship}
331
- - Duration we worked together: {time_worked}
332
- - Performance Summary by Category:{analysis_text}
333
- - Employee's LinkedIn Profile (for context, do not mention the URL in the output): {linkedin_url or 'Not provided'}
334
- - Key Strengths (rated 4 or 5): {', '.join(strengths) if strengths else 'None specified'}
335
-
336
- Now, using that analysis, write a detailed and comprehensive LinkedIn recommendation of 200-250 words. The tone should be professional yet warm and authentic.
337
-
338
- Instructions for the recommendation:
339
- 1. Start by clearly stating the working relationship ({relationship}) and the duration ({time_worked}).
340
- 2. Highlight their role as a {employee_type} and their key responsibilities.
341
- 3. Instead of just listing their strengths, weave them into a brief narrative or specific example that illustrates their positive impact. For instance, how their 'Problem Solving' skills unblocked a project or how their 'Team Collaboration' improved team morale.
342
- 4. Conclude with a strong, forward-looking statement about their potential.
343
- 5. Use vivid, descriptive language to make the recommendation feel more personal and human.
344
- """
345
-
346
- try:
347
- client = OpenAI(
348
- base_url="https://openrouter.ai/api/v1",
349
- api_key=os.environ.get('OPENROUTER_API_KEY')
350
- )
351
-
352
- # Generate the final recommendation in a single call
353
- final_response = client.chat.completions.create(
354
- model="openai/gpt-3.5-turbo",
355
- messages=[
356
- {"role": "system", "content": "You are an expert in writing professional, warm, and authentic LinkedIn recommendations."},
357
- {"role": "user", "content": recommendation_prompt}
358
- ],
359
- max_tokens=255,
360
- temperature=0.75
361
- )
362
- return final_response.choices[0].message.content.strip()
363
- except RateLimitError:
364
- st.error("API rate limit or quota exceeded. Please check your OpenRouter account and billing details.")
365
- return None
366
- except Exception as e:
367
- st.error(f"An error occurred while generating the recommendation: {str(e)}")
368
- return None
369
-
370
- def render_header():
371
- """Renders the main header of the application."""
372
- st.markdown("""
373
- <div class="header-container">
374
- <div class="linkedin-logo">
375
- <svg width="35" height="35" viewBox="0 0 24 24" fill="white">
376
- <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
377
- </svg>
378
- </div>
379
- <h1 class="main-title">LinkedIn Recommendation Generator</h1>
380
- <p class="subtitle">Build impactful recommendations for LinkedIn - Made By github.com/ninjacode911</p>
381
- </div>
382
- """, unsafe_allow_html=True)
383
-
384
- def render_input_form() -> Dict:
385
- """Renders the input form and returns a dictionary of user inputs."""
386
- st.markdown('<h3 class="section-header">πŸ“‹ Basic Information</h3>', unsafe_allow_html=True)
387
- col1, col2 = st.columns(2)
388
- with col1:
389
- employee_name = st.text_input(
390
- "Employee Name",
391
- key="employee_name",
392
- placeholder="e.g., John Smith"
393
- )
394
- relationship = st.selectbox(
395
- "Your relationship with this person",
396
- ["", "Direct Manager", "Senior Manager", "Team Lead", "Colleague", "Project Manager", "Department Head", "HR Manager"],
397
- key="relationship"
398
- )
399
-
400
- with col2:
401
- employee_type = st.selectbox(
402
- "Employee Role/Department",
403
- ["", "Software Developer", "AI Engineer", "Marketing Specialist", "Sales Representative",
404
- "Project Manager", "Data Analyst", "UI/UX Designer", "Customer Support", "Business Analyst",
405
- "Product Manager", "DevOps Engineer", "Content Creator", "HR Specialist", "Other"],
406
- key="employee_type"
407
- )
408
- time_worked = st.selectbox(
409
- "How long have you worked together?",
410
- ["", "Less than 6 months", "6 months - 1 year", "1-2 years", "2-3 years", "3-5 years", "More than 5 years"],
411
- key="time_worked"
412
- )
413
-
414
- # LinkedIn Profile URL input
415
- linkedin_url = st.text_input(
416
- "Enter LinkedIn Profile URL",
417
- key="linkedin_url",
418
- placeholder="e.g., https://www.linkedin.com/in/username"
419
- )
420
-
421
- st.markdown('<h3 class="section-header">⭐ Performance Evaluation</h3>', unsafe_allow_html=True)
422
- st.markdown("*Rate each aspect on a scale of 1-5 stars*")
423
-
424
- ratings = {}
425
- st.markdown("<div class='sub-section-header'>Core Competencies</div>", unsafe_allow_html=True)
426
- ratings['domain'] = create_star_rating(
427
- "How would you rate the employee's knowledge and expertise in their specific field or role?",
428
- "domain"
429
- )
430
- ratings['problem_solving'] = create_star_rating(
431
- "How effectively does the employee address challenges and find solutions?",
432
- "problem_solving"
433
- )
434
- ratings['initiative'] = create_star_rating(
435
- "How proactive is the employee in taking initiative and contributing to company objectives?",
436
- "initiative"
437
- )
438
-
439
- st.markdown("<div class='sub-section-header'>Professional Skills</div>", unsafe_allow_html=True)
440
- ratings['adaptability'] = create_star_rating(
441
- "How well does the employee handle change or take on new responsibilities?",
442
- "adaptability"
443
- )
444
- ratings['communication'] = create_star_rating(
445
- "How clearly and professionally does the employee communicate ideas or information?",
446
- "communication"
447
- )
448
-
449
- st.markdown("<div class='sub-section-header'>Interpersonal Skills</div>", unsafe_allow_html=True)
450
- ratings['teamwork'] = create_star_rating(
451
- "How well does the employee work with colleagues or teams to achieve goals?",
452
- "teamwork"
453
- )
454
- ratings['support'] = create_star_rating(
455
- "How well does the employee support or guide others in the work environment?",
456
- "support"
457
- )
458
-
459
- st.markdown("<div class='sub-section-header'>Performance & Potential</div>", unsafe_allow_html=True)
460
- ratings['reliability'] = create_star_rating(
461
- "How consistently does the employee demonstrate dedication and reliability?",
462
- "reliability"
463
- )
464
- ratings['overall'] = create_star_rating(
465
- "How would you rate the employee's overall contribution to their role and the team?",
466
- "overall"
467
- )
468
- ratings['potential'] = create_star_rating(
469
- "How would you rate the employee's potential for further growth or advancement within the organization?",
470
- "potential"
471
- )
472
-
473
- return {
474
- "employee_name": employee_name,
475
- "relationship": relationship,
476
- "employee_type": employee_type,
477
- "time_worked": time_worked,
478
- "linkedin_url": linkedin_url,
479
- "ratings": ratings
480
- }
481
-
482
- def render_results_section(ratings: Dict[str, int]):
483
- """Renders the recommendation, action buttons, and analytics."""
484
- if st.session_state.recommendation_generated:
485
- st.markdown(f"""
486
- <div class="result-container">
487
- <h3 class="result-title">πŸ“ Your LinkedIn Recommendation</h3>
488
- <div class="recommendation-text">
489
- {st.session_state.generated_text}
490
- </div>
491
- </div>
492
- """, unsafe_allow_html=True)
493
-
494
- # Action buttons
495
- col1, col2 = st.columns(2)
496
- with col1:
497
- # This provides a clear way for users to copy the text.
498
- if st.button("πŸ“‹ Show Text for Copying"):
499
- st.code(st.session_state.generated_text, language="text")
500
- st.info("You can now manually copy the text above.")
501
-
502
- with col2:
503
- if st.button("πŸ”„ Generate New Version"):
504
- st.session_state.recommendation_generated = False
505
- st.rerun()
506
-
507
- # LinkedIn URL box
508
- if st.session_state.saved_linkedin_url:
509
- st.markdown(f"""
510
- <div style="background: linear-gradient(135deg, #0077B5 0%, #005885 100%); color: white; padding: 8px; border-radius: 5px; margin: 1rem 0; text-align: center; font-family: 'Source Sans Pro', sans-serif; font-size: 1rem;">
511
- Click on the Employee's LinkedIn Profile: <a href="{st.session_state.saved_linkedin_url}" target="_blank" style="color: #ffffff; text-decoration: none;">{st.session_state.saved_linkedin_url}</a>
512
- </div>
513
- """, unsafe_allow_html=True)
514
-
515
- # Instructions
516
- st.markdown("""
517
- <div class="result-container">
518
- <h4 style="color: white; margin-bottom: 1rem;">πŸ“– How to Post on LinkedIn</h4>
519
- <ol style="font-family: 'Source Sans Pro', sans-serif; line-height: 1.6;">
520
- <li>Copy the recommendation text above</li>
521
- <li>Click on the person's LinkedIn profile</li>
522
- <li>Click "More" β†’ "Recommend"</li>
523
- <li>Paste the generated recommendation</li>
524
- <li>Review and send!</li>
525
- </ol>
526
- </div>
527
- """, unsafe_allow_html=True)
528
-
529
- # Analytics section
530
- st.markdown('<h4 style="color: #0077B5;">πŸ“Š Rating Summary</h4>', unsafe_allow_html=True)
531
-
532
- col1, col2, col3, col4 = st.columns(4)
533
-
534
- avg_rating = sum(ratings.values()) / len(ratings)
535
- highest_rating = max(ratings.values())
536
- lowest_rating = min(ratings.values())
537
-
538
- with col1:
539
- st.metric("Average Rating", f"{avg_rating:.1f}/5", f"{avg_rating/5*100:.0f}%")
540
- with col2:
541
- st.metric("Highest Rating", f"{highest_rating}/5")
542
- with col3:
543
- st.metric("Lowest Rating", f"{lowest_rating}/5")
544
- with col4:
545
- st.metric("Word Count", len(st.session_state.generated_text.split()))
546
-
547
- def main():
548
- """Main function to run the Streamlit application."""
549
- # Check for API key. Prioritize environment variables (for Hugging Face),
550
- # then fall back to Streamlit secrets (for local/Streamlit Cloud dev).
551
- api_key = os.environ.get('OPENROUTER_API_KEY')
552
- if not api_key:
553
- if 'OPENROUTER_API_KEY' in st.secrets:
554
- api_key = st.secrets['OPENROUTER_API_KEY']
555
- os.environ['OPENROUTER_API_KEY'] = api_key # Set it for the rest of the app
556
- else:
557
- st.error('OpenRouter API key not found. Please set it in your Hugging Face Space secrets or local .streamlit/secrets.toml file.')
558
- st.stop()
559
-
560
- render_header()
561
-
562
- # Initialize session state
563
- if 'recommendation_generated' not in st.session_state:
564
- st.session_state.recommendation_generated = False
565
- if 'generated_text' not in st.session_state:
566
- st.session_state.generated_text = ""
567
- if 'saved_linkedin_url' not in st.session_state:
568
- st.session_state.saved_linkedin_url = ""
569
-
570
- form_data = render_input_form()
571
-
572
- # Generate recommendation button
573
- col1, col2, col3 = st.columns([1, 2, 1])
574
- with col2:
575
- if st.button("πŸš€ Generate LinkedIn Recommendation", type="primary"):
576
- # Validate required fields
577
- required_fields = ["employee_name", "employee_type", "relationship", "time_worked"]
578
- if not all(form_data[field] for field in required_fields):
579
- st.error("Please fill in all required fields in the 'Basic Information' section.")
580
- else:
581
- with st.spinner("πŸ€– Analyzing performance data and crafting your recommendation..."):
582
- progress_bar = st.progress(0, text="Analyzing...")
583
- time.sleep(0.5)
584
- progress_bar.progress(50, text="Generating text...")
585
-
586
- recommendation = generate_recommendation(**form_data)
587
-
588
- progress_bar.progress(100, text="Done!")
589
- time.sleep(0.5)
590
- progress_bar.empty()
591
-
592
- if recommendation:
593
- st.session_state.recommendation_generated = True
594
- st.session_state.generated_text = recommendation
595
- st.session_state.saved_linkedin_url = form_data["linkedin_url"]
596
- st.success("βœ… Recommendation generated successfully!")
597
- st.rerun() # Rerun to display the results section cleanly
598
-
599
- # Display results in a separate container
600
- render_results_section(form_data["ratings"])
601
-
602
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  main()
 
1
+ from openai import OpenAI, RateLimitError
2
+ import streamlit as st
3
+ import time
4
+ import os
5
+ import httpx
6
+ # Add at the top of the file after imports:
7
+ from typing import Dict, Optional
8
+ # Page configuration
9
+ st.set_page_config(
10
+ page_title="LinkedIn Recommendation Generator",
11
+ page_icon="πŸ‘”",
12
+ layout="wide",
13
+ initial_sidebar_state="collapsed"
14
+ )
15
+
16
+ # Custom CSS for professional styling
17
+ st.markdown("""
18
+ <style>
19
+ /* Import LinkedIn-style font */
20
+ @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap');
21
+
22
+ /* Main container styling */
23
+ .main-container {
24
+ max-width: 1000px;
25
+ margin: 0 auto;
26
+ padding: 2rem;
27
+ background: linear-gradient(135deg, #f8f9ff 0%, #e8f4f8 100%);
28
+ min-height: 100vh;
29
+ }
30
+
31
+ /* Header styling */
32
+ .header-container {
33
+ background: white;
34
+ padding: 2rem;
35
+ border-radius: 20px;
36
+ box-shadow: 0 8px 32px rgba(0,0,0,0.1);
37
+ text-align: center;
38
+ margin-bottom: 2rem;
39
+ border: 1px solid rgba(255,255,255,0.2);
40
+ }
41
+
42
+ .linkedin-logo {
43
+ width: 60px;
44
+ height: 60px;
45
+ background: linear-gradient(135deg, #0077B5 0%, #005885 100%);
46
+ border-radius: 15px;
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ margin-bottom: 1rem;
51
+ box-shadow: 0 4px 15px rgba(0,119,181,0.3);
52
+ }
53
+
54
+ .main-title {
55
+ font-family: 'Source Sans Pro', sans-serif;
56
+ font-size: 2.5rem;
57
+ font-weight: 700;
58
+ color: #0077B5;
59
+ margin: 0;
60
+ margin-bottom: 0.5rem;
61
+ }
62
+
63
+ .subtitle {
64
+ font-family: 'Source Sans Pro', sans-serif;
65
+ font-size: 1.2rem;
66
+ color: #666;
67
+ margin: 0;
68
+ font-weight: 400;
69
+ }
70
+
71
+ /* Section headers */
72
+ .section-header {
73
+ font-family: 'Source Sans Pro', sans-serif;
74
+ font-size: 1.5rem;
75
+ font-weight: 600;
76
+ color: #0077B5;
77
+ margin-bottom: 1.5rem;
78
+ padding-bottom: 0.5rem;
79
+ border-bottom: 2px solid #e8f4f8;
80
+ }
81
+
82
+ /* Sub-section headers styling */
83
+ .sub-section-header {
84
+ font-family: 'Source Sans Pro', sans-serif;
85
+ font-size: 1.3rem;
86
+ font-weight: 600;
87
+ color: #0077B5;
88
+ margin: 1.5rem 0 1rem 0;
89
+ padding: 0.5rem 0;
90
+ border-bottom: 2px solid rgba(0, 119, 181, 0.2);
91
+ }
92
+
93
+ /* Custom star rating styling */
94
+ .star-rating {
95
+ display: flex;
96
+ gap: 8px;
97
+ align-items: center;
98
+ margin: 10px 0;
99
+ padding: 15px;
100
+ background: #f8f9ff;
101
+ border-radius: 12px;
102
+ border: 1px solid #e8f4f8;
103
+ }
104
+
105
+ .star-question {
106
+ font-family: 'Source Sans Pro', sans-serif;
107
+ font-weight: 500;
108
+ color: #0077B5; /* Changed from white to blue for visibility */
109
+ font-size: 1rem;
110
+ flex: 1;
111
+ margin-right: 20px;
112
+ }
113
+
114
+ /* Result container */
115
+ .result-container {
116
+ background: linear-gradient(135deg, #0077B5 0%, #005885 100%);
117
+ color: white;
118
+ padding: 2.5rem;
119
+ border-radius: 20px;
120
+ box-shadow: 0 8px 32px rgba(0,119,181,0.3);
121
+ margin-top: 2rem;
122
+ }
123
+
124
+ .result-title {
125
+ font-family: 'Source Sans Pro', sans-serif;
126
+ font-size: 1.8rem;
127
+ font-weight: 600;
128
+ margin-bottom: 1rem;
129
+ }
130
+
131
+ .recommendation-text {
132
+ background: rgba(255,255,255,0.15);
133
+ padding: 2rem;
134
+ border-radius: 15px;
135
+ font-family: 'Source Sans Pro', sans-serif;
136
+ font-size: 1.1rem;
137
+ line-height: 1.6;
138
+ margin-bottom: 1.5rem;
139
+ backdrop-filter: blur(10px);
140
+ border: 1px solid rgba(255,255,255,0.2);
141
+ }
142
+
143
+ /* Style for the code block that appears on copy */
144
+ .stCodeBlock {
145
+ border-radius: 15px !important;
146
+ border: 1px solid #e8f4f8 !important;
147
+ }
148
+ .stCodeBlock pre {
149
+ min-height: 200px; /* Increase the default height */
150
+ max-height: 400px;
151
+ overflow-y: auto !important;
152
+ white-space: pre-wrap !important; /* Allow text to wrap */
153
+ }
154
+
155
+ /* Button styling */
156
+ .stButton > button {
157
+ background: linear-gradient(135deg, #0077B5 0%, #005885 100%);
158
+ color: white;
159
+ border: none;
160
+ padding: 0.8rem 2rem;
161
+ border-radius: 25px;
162
+ font-weight: 600;
163
+ font-family: 'Source Sans Pro', sans-serif;
164
+ font-size: 1rem;
165
+ cursor: pointer;
166
+ transition: all 0.3s ease;
167
+ box-shadow: 0 4px 15px rgba(0,119,181,0.3);
168
+ width: 100%;
169
+ }
170
+
171
+ .stButton > button:hover {
172
+ transform: translateY(-2px);
173
+ box-shadow: 0 6px 20px rgba(0,119,181,0.4);
174
+ }
175
+
176
+ /* Selectbox styling */
177
+ .stSelectbox > div > div {
178
+ background: #f8f9ff;
179
+ border: 1px solid #e8f4f8;
180
+ border-radius: 12px;
181
+ font-family: 'Source Sans Pro', sans-serif;
182
+ }
183
+
184
+ /* Text input styling */
185
+ .stTextInput > div > div > input {
186
+ background: #f8f9ff;
187
+ border: 1px solid #e8f4f8;
188
+ border-radius: 12px;
189
+ font-family: 'Source Sans Pro', sans-serif;
190
+ padding: 12px 16px;
191
+ }
192
+
193
+ /* Progress bar */
194
+ .progress-container {
195
+ background: white;
196
+ padding: 1.5rem;
197
+ border-radius: 15px;
198
+ margin: 1rem 0;
199
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
200
+ }
201
+
202
+ /* Hide Streamlit components */
203
+ #MainMenu {visibility: hidden;}
204
+ footer {visibility: hidden;}
205
+ header {visibility: hidden;}
206
+
207
+ /* Custom metric styling */
208
+ .metric-container {
209
+ background: linear-gradient(135deg, #f8f9ff 0%, #e8f4f8 100%);
210
+ padding: 1rem;
211
+ border-radius: 12px;
212
+ text-align: center;
213
+ margin: 0.5rem 0;
214
+ border: 1px solid #e8f4f8;
215
+ }
216
+
217
+ /* Form field uniform sizing and styling */
218
+ .stTextInput > div {
219
+ width: 100% !important;
220
+ }
221
+
222
+ .stSelectbox > div {
223
+ width: 100% !important;
224
+ }
225
+
226
+ .stTextInput > div > div > input {
227
+ background-color: white !important;
228
+ color: #333 !important;
229
+ min-height: 48px !important;
230
+ border: 1px solid #e8f4f8 !important;
231
+ border-radius: 8px !important;
232
+ padding: 0.5rem 1rem !important;
233
+ }
234
+
235
+ .stSelectbox > div > div {
236
+ background-color: white !important;
237
+ color: #333 !important;
238
+ min-height: 48px !important;
239
+ border: 1px solid #e8f4f8 !important;
240
+ border-radius: 8px !important;
241
+ }
242
+
243
+ /* Add consistent spacing between star ratings */
244
+ .star-rating-container {
245
+ margin-bottom: 1rem;
246
+ }
247
+
248
+ /* Container for form fields */
249
+ .form-field-container {
250
+ padding: 0.5rem 0;
251
+ }
252
+ </style>
253
+ """, unsafe_allow_html=True)
254
+
255
+ def create_star_rating(label, key, help_text=None):
256
+ """Create a custom 5-star rating component"""
257
+ # Use a container to apply consistent bottom margin via CSS
258
+ with st.container():
259
+ st.markdown('<div class="star-rating-container">', unsafe_allow_html=True)
260
+ col1, col2 = st.columns([3, 2])
261
+
262
+ with col1:
263
+ st.markdown(f'<div class="star-question">{label}</div>', unsafe_allow_html=True)
264
+ if help_text:
265
+ st.caption(help_text)
266
+
267
+ with col2:
268
+ # The select_slider is inside the columns
269
+ pass
270
+
271
+ rating = st.select_slider(
272
+ "",
273
+ options=[1, 2, 3, 4, 5],
274
+ value=3,
275
+ key=key,
276
+ label_visibility="collapsed"
277
+ )
278
+
279
+ # Create visual stars
280
+ stars = "".join(["⭐" if i < rating else "β˜†" for i in range(5)])
281
+ st.markdown(f"<div style='font-size: 1.5rem; text-align: center; margin-top: -35px;'>{stars}</div>", unsafe_allow_html=True)
282
+
283
+ return rating
284
+
285
+ def generate_recommendation(ratings: Dict[str, int], employee_type: str, employee_name: str, relationship: str, time_worked: str, linkedin_url: str) -> Optional[str]:
286
+ """Generate recommendation using OpenRouter API with input summary"""
287
+
288
+ # Organize ratings into categories for analysis
289
+ performance_areas = {
290
+ "Technical Competence": {
291
+ "Domain Knowledge": ratings['domain'],
292
+ "Problem Solving": ratings['problem_solving'],
293
+ "Initiative": ratings['initiative']
294
+ },
295
+ "Professional Skills": {
296
+ "Adaptability": ratings['adaptability'],
297
+ "Communication": ratings['communication']
298
+ },
299
+ "Interpersonal Impact": {
300
+ "Team Collaboration": ratings['teamwork'],
301
+ "Support & Guidance": ratings['support']
302
+ },
303
+ "Overall Performance": {
304
+ "Reliability": ratings['reliability'],
305
+ "Overall Contribution": ratings['overall'],
306
+ "Growth Potential": ratings['potential']
307
+ }
308
+ }
309
+
310
+ # Calculate category averages
311
+ category_scores = {}
312
+ for category, metrics in performance_areas.items():
313
+ category_scores[category] = sum(metrics.values()) / len(metrics)
314
+
315
+ # Identify top strengths (ratings of 4 or 5)
316
+ strengths = [k for k, v in ratings.items() if v >= 4]
317
+
318
+ # Build a text block for the analysis part of the prompt
319
+ analysis_text = ""
320
+ for category, score in category_scores.items():
321
+ analysis_text += f"\n- {category}: {score:.1f}/5"
322
+
323
+ # Create a single, comprehensive prompt for a more efficient, single API call
324
+ recommendation_prompt = f"""
325
+ You are an expert in writing professional LinkedIn recommendations.
326
+ Your task is to generate a recommendation for {employee_name}.
327
+
328
+ First, silently analyze the provided performance data. Do not output this analysis.
329
+ - Employee: {employee_name}
330
+ - Role: {employee_type}
331
+ - My Relationship to them: {relationship}
332
+ - Duration we worked together: {time_worked}
333
+ - Performance Summary by Category:{analysis_text}
334
+ - Employee's LinkedIn Profile (for context, do not mention the URL in the output): {linkedin_url or 'Not provided'}
335
+ - Key Strengths (rated 4 or 5): {', '.join(strengths) if strengths else 'None specified'}
336
+
337
+ Now, using that analysis, write a detailed and comprehensive LinkedIn recommendation of 200-250 words. The tone should be professional yet warm and authentic.
338
+
339
+ Instructions for the recommendation:
340
+ 1. Start by clearly stating the working relationship ({relationship}) and the duration ({time_worked}).
341
+ 2. Highlight their role as a {employee_type} and their key responsibilities.
342
+ 3. Instead of just listing their strengths, weave them into a brief narrative or specific example that illustrates their positive impact. For instance, how their 'Problem Solving' skills unblocked a project or how their 'Team Collaboration' improved team morale.
343
+ 4. Conclude with a strong, forward-looking statement about their potential.
344
+ 5. Use vivid, descriptive language to make the recommendation feel more personal and human.
345
+ """
346
+
347
+ try:
348
+ # Explicitly create an httpx client that ignores environment proxies.
349
+ # This is the key fix for the "unexpected keyword argument 'proxies'" error on Hugging Face.
350
+ http_client = httpx.Client(proxies={})
351
+
352
+ client = OpenAI(
353
+ base_url="https://openrouter.ai/api/v1",
354
+ api_key=os.environ.get('OPENROUTER_API_KEY'),
355
+ http_client=http_client # Pass the configured client
356
+ )
357
+
358
+ # Generate the final recommendation in a single call
359
+ final_response = client.chat.completions.create(
360
+ model="openai/gpt-3.5-turbo",
361
+ messages=[
362
+ {"role": "system", "content": "You are an expert in writing professional, warm, and authentic LinkedIn recommendations."},
363
+ {"role": "user", "content": recommendation_prompt}
364
+ ],
365
+ max_tokens=255,
366
+ temperature=0.75
367
+ )
368
+ return final_response.choices[0].message.content.strip()
369
+ except RateLimitError:
370
+ st.error("API rate limit or quota exceeded. Please check your OpenRouter account and billing details.")
371
+ return None
372
+ except Exception as e:
373
+ st.error(f"An error occurred while generating the recommendation: {str(e)}")
374
+ return None
375
+
376
+ def render_header():
377
+ """Renders the main header of the application."""
378
+ st.markdown("""
379
+ <div class="header-container">
380
+ <div class="linkedin-logo">
381
+ <svg width="35" height="35" viewBox="0 0 24 24" fill="white">
382
+ <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
383
+ </svg>
384
+ </div>
385
+ <h1 class="main-title">LinkedIn Recommendation Generator</h1>
386
+ <p class="subtitle">Build impactful recommendations for LinkedIn - Made By github.com/ninjacode911</p>
387
+ </div>
388
+ """, unsafe_allow_html=True)
389
+
390
+ def render_input_form() -> Dict:
391
+ """Renders the input form and returns a dictionary of user inputs."""
392
+ st.markdown('<h3 class="section-header">πŸ“‹ Basic Information</h3>', unsafe_allow_html=True)
393
+ col1, col2 = st.columns(2)
394
+ with col1:
395
+ employee_name = st.text_input(
396
+ "Employee Name",
397
+ key="employee_name",
398
+ placeholder="e.g., John Smith"
399
+ )
400
+ relationship = st.selectbox(
401
+ "Your relationship with this person",
402
+ ["", "Direct Manager", "Senior Manager", "Team Lead", "Colleague", "Project Manager", "Department Head", "HR Manager"],
403
+ key="relationship"
404
+ )
405
+
406
+ with col2:
407
+ employee_type = st.selectbox(
408
+ "Employee Role/Department",
409
+ ["", "Software Developer", "AI Engineer", "Marketing Specialist", "Sales Representative",
410
+ "Project Manager", "Data Analyst", "UI/UX Designer", "Customer Support", "Business Analyst",
411
+ "Product Manager", "DevOps Engineer", "Content Creator", "HR Specialist", "Other"],
412
+ key="employee_type"
413
+ )
414
+ time_worked = st.selectbox(
415
+ "How long have you worked together?",
416
+ ["", "Less than 6 months", "6 months - 1 year", "1-2 years", "2-3 years", "3-5 years", "More than 5 years"],
417
+ key="time_worked"
418
+ )
419
+
420
+ # LinkedIn Profile URL input
421
+ linkedin_url = st.text_input(
422
+ "Enter LinkedIn Profile URL",
423
+ key="linkedin_url",
424
+ placeholder="e.g., https://www.linkedin.com/in/username"
425
+ )
426
+
427
+ st.markdown('<h3 class="section-header">⭐ Performance Evaluation</h3>', unsafe_allow_html=True)
428
+ st.markdown("*Rate each aspect on a scale of 1-5 stars*")
429
+
430
+ ratings = {}
431
+ st.markdown("<div class='sub-section-header'>Core Competencies</div>", unsafe_allow_html=True)
432
+ ratings['domain'] = create_star_rating(
433
+ "How would you rate the employee's knowledge and expertise in their specific field or role?",
434
+ "domain"
435
+ )
436
+ ratings['problem_solving'] = create_star_rating(
437
+ "How effectively does the employee address challenges and find solutions?",
438
+ "problem_solving"
439
+ )
440
+ ratings['initiative'] = create_star_rating(
441
+ "How proactive is the employee in taking initiative and contributing to company objectives?",
442
+ "initiative"
443
+ )
444
+
445
+ st.markdown("<div class='sub-section-header'>Professional Skills</div>", unsafe_allow_html=True)
446
+ ratings['adaptability'] = create_star_rating(
447
+ "How well does the employee handle change or take on new responsibilities?",
448
+ "adaptability"
449
+ )
450
+ ratings['communication'] = create_star_rating(
451
+ "How clearly and professionally does the employee communicate ideas or information?",
452
+ "communication"
453
+ )
454
+
455
+ st.markdown("<div class='sub-section-header'>Interpersonal Skills</div>", unsafe_allow_html=True)
456
+ ratings['teamwork'] = create_star_rating(
457
+ "How well does the employee work with colleagues or teams to achieve goals?",
458
+ "teamwork"
459
+ )
460
+ ratings['support'] = create_star_rating(
461
+ "How well does the employee support or guide others in the work environment?",
462
+ "support"
463
+ )
464
+
465
+ st.markdown("<div class='sub-section-header'>Performance & Potential</div>", unsafe_allow_html=True)
466
+ ratings['reliability'] = create_star_rating(
467
+ "How consistently does the employee demonstrate dedication and reliability?",
468
+ "reliability"
469
+ )
470
+ ratings['overall'] = create_star_rating(
471
+ "How would you rate the employee's overall contribution to their role and the team?",
472
+ "overall"
473
+ )
474
+ ratings['potential'] = create_star_rating(
475
+ "How would you rate the employee's potential for further growth or advancement within the organization?",
476
+ "potential"
477
+ )
478
+
479
+ return {
480
+ "employee_name": employee_name,
481
+ "relationship": relationship,
482
+ "employee_type": employee_type,
483
+ "time_worked": time_worked,
484
+ "linkedin_url": linkedin_url,
485
+ "ratings": ratings
486
+ }
487
+
488
+ def render_results_section(ratings: Dict[str, int]):
489
+ """Renders the recommendation, action buttons, and analytics."""
490
+ if st.session_state.recommendation_generated:
491
+ st.markdown(f"""
492
+ <div class="result-container">
493
+ <h3 class="result-title">πŸ“ Your LinkedIn Recommendation</h3>
494
+ <div class="recommendation-text">
495
+ {st.session_state.generated_text}
496
+ </div>
497
+ </div>
498
+ """, unsafe_allow_html=True)
499
+
500
+ # Action buttons
501
+ col1, col2 = st.columns(2)
502
+ with col1:
503
+ # This provides a clear way for users to copy the text.
504
+ if st.button("πŸ“‹ Show Text for Copying"):
505
+ st.code(st.session_state.generated_text, language="text")
506
+ st.info("You can now manually copy the text above.")
507
+
508
+ with col2:
509
+ if st.button("πŸ”„ Generate New Version"):
510
+ st.session_state.recommendation_generated = False
511
+ st.rerun()
512
+
513
+ # LinkedIn URL box
514
+ if st.session_state.saved_linkedin_url:
515
+ st.markdown(f"""
516
+ <div style="background: linear-gradient(135deg, #0077B5 0%, #005885 100%); color: white; padding: 8px; border-radius: 5px; margin: 1rem 0; text-align: center; font-family: 'Source Sans Pro', sans-serif; font-size: 1rem;">
517
+ Click on the Employee's LinkedIn Profile: <a href="{st.session_state.saved_linkedin_url}" target="_blank" style="color: #ffffff; text-decoration: none;">{st.session_state.saved_linkedin_url}</a>
518
+ </div>
519
+ """, unsafe_allow_html=True)
520
+
521
+ # Instructions
522
+ st.markdown("""
523
+ <div class="result-container">
524
+ <h4 style="color: white; margin-bottom: 1rem;">πŸ“– How to Post on LinkedIn</h4>
525
+ <ol style="font-family: 'Source Sans Pro', sans-serif; line-height: 1.6;">
526
+ <li>Copy the recommendation text above</li>
527
+ <li>Click on the person's LinkedIn profile</li>
528
+ <li>Click "More" β†’ "Recommend"</li>
529
+ <li>Paste the generated recommendation</li>
530
+ <li>Review and send!</li>
531
+ </ol>
532
+ </div>
533
+ """, unsafe_allow_html=True)
534
+
535
+ # Analytics section
536
+ st.markdown('<h4 style="color: #0077B5;">πŸ“Š Rating Summary</h4>', unsafe_allow_html=True)
537
+
538
+ col1, col2, col3, col4 = st.columns(4)
539
+
540
+ avg_rating = sum(ratings.values()) / len(ratings)
541
+ highest_rating = max(ratings.values())
542
+ lowest_rating = min(ratings.values())
543
+
544
+ with col1:
545
+ st.metric("Average Rating", f"{avg_rating:.1f}/5", f"{avg_rating/5*100:.0f}%")
546
+ with col2:
547
+ st.metric("Highest Rating", f"{highest_rating}/5")
548
+ with col3:
549
+ st.metric("Lowest Rating", f"{lowest_rating}/5")
550
+ with col4:
551
+ st.metric("Word Count", len(st.session_state.generated_text.split()))
552
+
553
+ def main():
554
+ """Main function to run the Streamlit application."""
555
+ # Robustly check for API key from environment variables (Hugging Face secrets)
556
+ # or from a local secrets.toml file for local development.
557
+ api_key = os.environ.get('OPENROUTER_API_KEY')
558
+
559
+ if not api_key:
560
+ try:
561
+ # This check is for local development with a .streamlit/secrets.toml file.
562
+ if 'OPENROUTER_API_KEY' in st.secrets:
563
+ api_key = st.secrets['OPENROUTER_API_KEY']
564
+ os.environ['OPENROUTER_API_KEY'] = api_key
565
+ except FileNotFoundError:
566
+ # This is expected on Hugging Face if you only use repository secrets.
567
+ # We pass silently and rely on the final check below.
568
+ pass
569
+
570
+ # Final check to ensure the API key was found by either method.
571
+ if not api_key:
572
+ st.error("πŸ”‘ OpenRouter API key not found. Please add it to your Hugging Face Space secrets in the 'Settings' tab.")
573
+ st.stop()
574
+ render_header()
575
+
576
+ # Initialize session state
577
+ if 'recommendation_generated' not in st.session_state:
578
+ st.session_state.recommendation_generated = False
579
+ if 'generated_text' not in st.session_state:
580
+ st.session_state.generated_text = ""
581
+ if 'saved_linkedin_url' not in st.session_state:
582
+ st.session_state.saved_linkedin_url = ""
583
+
584
+ form_data = render_input_form()
585
+
586
+ # Generate recommendation button
587
+ col1, col2, col3 = st.columns([1, 2, 1])
588
+ with col2:
589
+ if st.button("πŸš€ Generate LinkedIn Recommendation", type="primary"):
590
+ # Validate required fields
591
+ required_fields = ["employee_name", "employee_type", "relationship", "time_worked"]
592
+ if not all(form_data[field] for field in required_fields):
593
+ st.error("Please fill in all required fields in the 'Basic Information' section.")
594
+ else:
595
+ with st.spinner("πŸ€– Analyzing performance data and crafting your recommendation..."):
596
+ progress_bar = st.progress(0, text="Analyzing...")
597
+ time.sleep(0.5)
598
+ progress_bar.progress(50, text="Generating text...")
599
+
600
+ recommendation = generate_recommendation(**form_data)
601
+
602
+ progress_bar.progress(100, text="Done!")
603
+ time.sleep(0.5)
604
+ progress_bar.empty()
605
+
606
+ if recommendation:
607
+ st.session_state.recommendation_generated = True
608
+ st.session_state.generated_text = recommendation
609
+ st.session_state.saved_linkedin_url = form_data["linkedin_url"]
610
+ st.success("βœ… Recommendation generated successfully!")
611
+ st.rerun() # Rerun to display the results section cleanly
612
+
613
+ # Display results in a separate container
614
+ render_results_section(form_data["ratings"])
615
+
616
+ if __name__ == "__main__":
617
  main()