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

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +25 -12
  2. app.py +603 -0
  3. requirements.txt +2 -0
README.md CHANGED
@@ -1,12 +1,25 @@
1
- ---
2
- title: Linkedin Recommendation Generator
3
- emoji: πŸ‘
4
- colorFrom: red
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: Generate Recommendation using AI
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: LinkedIn Recommendation Generator
3
+ emoji: πŸ‘”
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: streamlit
7
+ sdk_version: 1.33.0
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # πŸ‘” LinkedIn Recommendation Generator
13
+
14
+ A professional AI-powered tool to generate LinkedIn recommendations based on employee performance ratings.
15
+
16
+ ## Setup
17
+ 1. Install dependencies: `pip install -r requirements.txt`
18
+ 2. Add your `OPENROUTER_API_KEY` to your Hugging Face Space secrets or a local `.streamlit/secrets.toml` file.
19
+ 3. Run: `streamlit run app.py`
20
+
21
+ ## Features
22
+ - 10-point rating system
23
+ - AI-powered recommendation generation
24
+ - Professional LinkedIn-ready output
25
+ - Analytics dashboard
app.py ADDED
@@ -0,0 +1,603 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit==1.33.0
2
+ openai==1.24.0