GuglielmoTor commited on
Commit
6a6a99c
Β·
verified Β·
1 Parent(s): 0d20f23

Update ui/okr_ui_generator.py

Browse files
Files changed (1) hide show
  1. ui/okr_ui_generator.py +254 -212
ui/okr_ui_generator.py CHANGED
@@ -1,4 +1,3 @@
1
- #okr_ui_generator.py
2
  import gradio as gr
3
  from typing import Dict, Any, List, Optional
4
  import pandas as pd
@@ -9,28 +8,33 @@ logger = logging.getLogger(__name__)
9
  def create_enhanced_okr_tab():
10
  """
11
  Creates a modern, visually appealing OKR tab with improved layout and styling.
12
- It returns the Gradio HTML component that will display the OKR content.
 
 
 
13
  """
14
-
15
  # Custom CSS for modern OKR styling
16
  okr_custom_css = """
17
  <style>
18
- /* Ensure the main container fills the available space and removes default Gradio padding */
19
  .okr-container {
20
- font-family: 'Inter', sans-serif; /* Using Inter font as per guidelines */
21
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
22
- min-height: 100vh; /* Ensure it takes full viewport height */
23
  padding: 2rem;
24
- margin: -1rem; /* Adjust for Gradio's default padding */
25
- box-sizing: border-box; /* Include padding in element's total width and height */
 
26
  }
27
-
 
28
  .okr-header {
29
  text-align: center;
30
  margin-bottom: 3rem;
31
  color: white;
32
  }
33
-
34
  .okr-title {
35
  font-size: 2.5rem;
36
  font-weight: 700;
@@ -41,14 +45,15 @@ def create_enhanced_okr_tab():
41
  background-clip: text;
42
  text-shadow: 0 2px 4px rgba(0,0,0,0.1);
43
  }
44
-
45
  .okr-subtitle {
46
  font-size: 1.2rem;
47
  opacity: 0.9;
48
  font-weight: 300;
49
  letter-spacing: 0.5px;
50
  }
51
-
 
52
  .okr-stats-bar {
53
  display: flex;
54
  justify-content: center;
@@ -56,7 +61,7 @@ def create_enhanced_okr_tab():
56
  margin: 2rem 0;
57
  flex-wrap: wrap;
58
  }
59
-
60
  .stat-card {
61
  background: rgba(255, 255, 255, 0.15);
62
  backdrop-filter: blur(10px);
@@ -68,59 +73,62 @@ def create_enhanced_okr_tab():
68
  min-width: 140px;
69
  transition: all 0.3s ease;
70
  }
71
-
72
  .stat-card:hover {
73
  transform: translateY(-2px);
74
  background: rgba(255, 255, 255, 0.2);
75
  box-shadow: 0 8px 32px rgba(0,0,0,0.1);
76
  }
77
-
78
  .stat-number {
79
  font-size: 2rem;
80
  font-weight: 700;
81
  margin-bottom: 0.25rem;
82
- color: #fbbf24; /* Tailwind yellow-400 */
83
  }
84
-
85
  .stat-label {
86
  font-size: 0.9rem;
87
  opacity: 0.9;
88
  text-transform: uppercase;
89
  letter-spacing: 1px;
90
  }
91
-
 
92
  .okr-content {
93
  background: white;
94
  border-radius: 24px;
95
  padding: 0;
96
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
97
- overflow: hidden;
98
  margin-top: 2rem;
99
  }
100
-
 
101
  .okr-objective {
102
- background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); /* Tailwind slate-50 to slate-200 */
103
- border-left: 6px solid #3b82f6; /* Tailwind blue-500 */
104
- margin: 2rem 0;
105
  border-radius: 16px;
106
  overflow: hidden;
107
  box-shadow: 0 4px 16px rgba(0,0,0,0.05);
108
  transition: all 0.3s ease;
109
  }
110
-
111
  .okr-objective:hover {
112
  transform: translateY(-2px);
113
  box-shadow: 0 8px 24px rgba(0,0,0,0.1);
114
  }
115
-
 
116
  .objective-header {
117
  padding: 2rem;
118
- background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%); /* Tailwind blue-700 to blue-500 */
119
  color: white;
120
  position: relative;
121
  overflow: hidden;
122
  }
123
-
124
  .objective-header::before {
125
  content: '';
126
  position: absolute;
@@ -128,11 +136,10 @@ def create_enhanced_okr_tab():
128
  left: 0;
129
  right: 0;
130
  bottom: 0;
131
- /* Subtle background pattern */
132
  background: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M20 20c0 4.4-3.6 8-8 8s-8-3.6-8-8 3.6-8 8-8 8 3.6 8 8zm0-20c0 4.4-3.6 8-8 8s-8-3.6-8-8 3.6-8 8-8 8 3.6 8 8z'/%3E%3C/g%3E%3C/svg%3E");
133
  pointer-events: none;
134
  }
135
-
136
  .objective-title {
137
  font-size: 1.5rem;
138
  font-weight: 700;
@@ -140,7 +147,7 @@ def create_enhanced_okr_tab():
140
  position: relative;
141
  z-index: 1;
142
  }
143
-
144
  .objective-meta {
145
  display: flex;
146
  gap: 2rem;
@@ -149,7 +156,7 @@ def create_enhanced_okr_tab():
149
  position: relative;
150
  z-index: 1;
151
  }
152
-
153
  .meta-item {
154
  display: flex;
155
  align-items: center;
@@ -157,89 +164,92 @@ def create_enhanced_okr_tab():
157
  font-size: 0.9rem;
158
  opacity: 0.9;
159
  }
160
-
161
  .meta-icon {
162
  width: 16px;
163
  height: 16px;
164
  opacity: 0.8;
165
  }
166
-
 
167
  .key-results-container {
168
- padding: 2rem;
169
  }
170
-
 
171
  .key-result {
172
  background: white;
173
- border: 2px solid #e5e7eb; /* Tailwind neutral-200 */
174
  border-radius: 12px;
175
  margin: 1.5rem 0;
176
  overflow: hidden;
177
  transition: all 0.3s ease;
178
  }
179
-
180
  .key-result:hover {
181
- border-color: #3b82f6; /* Tailwind blue-500 */
182
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
183
  }
184
-
185
  .kr-header {
186
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); /* Tailwind slate-50 to slate-100 */
187
  padding: 1.5rem;
188
- border-bottom: 1px solid #e5e7eb; /* Tailwind neutral-200 */
189
  }
190
-
191
  .kr-title {
192
  font-size: 1.2rem;
193
  font-weight: 600;
194
- color: #1e293b; /* Tailwind slate-800 */
195
  margin-bottom: 0.75rem;
196
  }
197
-
198
  .kr-metrics {
199
  display: flex;
200
  gap: 1.5rem;
201
  flex-wrap: wrap;
202
  margin-top: 1rem;
203
  }
204
-
205
  .kr-metric {
206
- background: rgba(59, 130, 246, 0.1); /* Tailwind blue-500 with opacity */
207
- color: #1e40af; /* Tailwind blue-700 */
208
  padding: 0.5rem 1rem;
209
  border-radius: 8px;
210
  font-size: 0.85rem;
211
  font-weight: 500;
212
  border: 1px solid rgba(59, 130, 246, 0.2);
213
  }
214
-
 
215
  .tasks-section {
216
  padding: 1.5rem;
217
  }
218
-
219
  .tasks-title {
220
  font-size: 1rem;
221
  font-weight: 600;
222
- color: #374151; /* Tailwind gray-700 */
223
  margin-bottom: 1rem;
224
  display: flex;
225
  align-items: center;
226
  gap: 0.5rem;
227
  }
228
-
229
  .task-item {
230
- background: #f9fafb; /* Tailwind gray-50 */
231
- border: 1px solid #e5e7eb; /* Tailwind neutral-200 */
232
  border-radius: 8px;
233
  padding: 1.25rem;
234
  margin: 1rem 0;
235
  transition: all 0.2s ease;
236
  }
237
-
238
  .task-item:hover {
239
- background: #f3f4f6; /* Tailwind gray-100 */
240
- border-color: #d1d5db; /* Tailwind gray-300 */
241
  }
242
-
243
  .task-header {
244
  display: flex;
245
  justify-content: space-between;
@@ -247,14 +257,14 @@ def create_enhanced_okr_tab():
247
  margin-bottom: 1rem;
248
  gap: 1rem;
249
  }
250
-
251
  .task-title {
252
  font-weight: 600;
253
- color: #111827; /* Tailwind gray-900 */
254
  flex: 1;
255
  line-height: 1.4;
256
  }
257
-
258
  .task-priority {
259
  padding: 0.25rem 0.75rem;
260
  border-radius: 12px;
@@ -264,116 +274,122 @@ def create_enhanced_okr_tab():
264
  letter-spacing: 0.5px;
265
  white-space: nowrap;
266
  }
267
-
 
268
  .priority-high {
269
- background: #fef2f2; /* Tailwind red-50 */
270
- color: #dc2626; /* Tailwind red-600 */
271
- border: 1px solid #fca5a5; /* Tailwind red-300 */
272
  }
273
-
274
  .priority-medium {
275
- background: #fffbeb; /* Tailwind amber-50 */
276
- color: #d97706; /* Tailwind amber-700 */
277
- border: 1px solid #fcd34d; /* Tailwind amber-300 */
278
  }
279
-
280
  .priority-low {
281
- background: #f0fdf4; /* Tailwind green-50 */
282
- color: #16a34a; /* Tailwind green-600 */
283
- border: 1px solid #86efac; /* Tailwind green-300 */
284
  }
285
-
 
286
  .task-details {
287
  display: grid;
288
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
289
  gap: 1rem;
290
  margin-top: 1rem;
291
  }
292
-
293
  .task-detail-item {
294
  display: flex;
295
  align-items: center;
296
  gap: 0.5rem;
297
  font-size: 0.875rem;
298
- color: #6b7280; /* Tailwind gray-500 */
299
  }
300
-
301
  .task-detail-label {
302
  font-weight: 500;
303
- color: #374151; /* Tailwind gray-700 */
304
  min-width: 80px;
305
  }
306
-
 
307
  .task-description {
308
  margin-top: 1rem;
309
  padding: 1rem;
310
  background: white;
311
  border-radius: 6px;
312
- border-left: 3px solid #3b82f6; /* Tailwind blue-500 */
313
  font-size: 0.9rem;
314
  line-height: 1.5;
315
- color: #4b5563; /* Tailwind gray-600 */
316
  }
317
-
 
318
  .empty-state {
319
  text-align: center;
320
  padding: 4rem 2rem;
321
- color: #6b7280; /* Tailwind gray-500 */
322
  }
323
-
324
  .empty-state-icon {
325
  font-size: 3rem;
326
  margin-bottom: 1rem;
327
  }
328
-
329
  .empty-state-title {
330
  font-size: 1.5rem;
331
  font-weight: 600;
332
  margin-bottom: 0.5rem;
333
- color: #374151; /* Tailwind gray-700 */
334
  }
335
-
 
336
  .loading-spinner {
337
  display: inline-block;
338
  width: 20px;
339
  height: 20px;
340
- border: 3px solid #f3f4f6; /* Tailwind gray-200 */
341
  border-radius: 50%;
342
- border-top-color: #3b82f6; /* Tailwind blue-500 */
343
  animation: spin 1s ease-in-out infinite;
344
  }
345
-
346
  @keyframes spin {
347
  to { transform: rotate(360deg); }
348
  }
349
-
 
350
  @media (max-width: 768px) {
351
  .okr-container {
352
  padding: 1rem;
353
  }
354
-
355
  .okr-title {
356
  font-size: 2rem;
357
  }
358
-
359
  .okr-stats-bar {
360
  gap: 1rem;
361
  }
362
-
363
  .stat-card {
364
  min-width: 120px;
365
  padding: 1rem;
366
  }
367
-
368
  .objective-meta {
369
  flex-direction: column;
370
  gap: 1rem;
371
  }
372
-
373
  .task-details {
374
  grid-template-columns: 1fr;
375
  }
376
-
377
  .task-header {
378
  flex-direction: column;
379
  align-items: flex-start;
@@ -381,28 +397,33 @@ def create_enhanced_okr_tab():
381
  }
382
  </style>
383
  """
384
-
385
- with gr.Column():
386
  # Inject custom CSS
387
  gr.HTML(okr_custom_css)
388
-
389
  # Main OKR display area with enhanced styling
390
  okr_display_html = gr.HTML(
391
  value=get_initial_okr_display(),
392
  elem_classes=["okr-display"]
393
  )
394
-
395
  return okr_display_html
396
 
397
- def get_initial_okr_display():
398
- """Returns the initial HTML display for the OKR tab when loading."""
 
 
 
 
 
399
  return """
400
  <div class="okr-container">
401
  <div class="okr-header">
402
  <div class="okr-title">🎯 AI-Generated OKRs & Strategic Tasks</div>
403
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
404
  </div>
405
-
406
  <div class="okr-stats-bar">
407
  <div class="stat-card">
408
  <div class="stat-number">-</div>
@@ -421,7 +442,7 @@ def get_initial_okr_display():
421
  <div class="stat-label">High Priority</div>
422
  </div>
423
  </div>
424
-
425
  <div class="okr-content">
426
  <div class="empty-state">
427
  <div class="empty-state-icon">⏳</div>
@@ -438,42 +459,49 @@ def get_initial_okr_display():
438
  def format_okrs_for_enhanced_display(raw_results: dict) -> str:
439
  """
440
  Enhanced formatting function that creates beautiful HTML for OKR display
441
- based on the raw agentic analysis results.
 
 
 
 
 
 
 
442
  """
443
  if not raw_results or not raw_results.get("actionable_okrs"):
444
- logger.info("No raw_results or actionable_okrs found for formatting.")
445
  return get_empty_okr_state()
446
-
447
  actionable_okrs = raw_results.get("actionable_okrs", {})
448
  okrs_list = actionable_okrs.get("okrs", [])
449
-
450
  if not okrs_list:
451
- logger.info("OKR list is empty, returning empty state.")
452
  return get_empty_okr_state()
453
-
454
- # Calculate statistics
455
  total_objectives = len(okrs_list)
456
  total_key_results = sum(len(okr.get('key_results', [])) for okr in okrs_list)
457
  total_tasks = sum(
458
- len(kr.get('tasks', []))
459
- for okr in okrs_list
460
  for kr in okr.get('key_results', [])
461
  )
462
  high_priority_tasks = sum(
463
- 1 for okr in okrs_list
464
  for kr in okr.get('key_results', [])
465
  for task in kr.get('tasks', [])
466
  if task.get('priority', '').lower() == 'high'
467
  )
468
-
469
- # Build the HTML
470
  html_parts = [f"""
471
  <div class="okr-container">
472
  <div class="okr-header">
473
  <div class="okr-title">🎯 AI-Generated OKRs & Strategic Tasks</div>
474
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
475
  </div>
476
-
477
  <div class="okr-stats-bar">
478
  <div class="stat-card">
479
  <div class="stat-number">{total_objectives}</div>
@@ -492,172 +520,186 @@ def format_okrs_for_enhanced_display(raw_results: dict) -> str:
492
  <div class="stat-label">High Priority</div>
493
  </div>
494
  </div>
495
-
496
  <div class="okr-content">
497
  """]
498
-
499
  for okr_idx, okr_data in enumerate(okrs_list):
500
  if not isinstance(okr_data, dict):
501
  logger.warning(f"OKR item at index {okr_idx} is not a dictionary, skipping.")
502
  continue
503
-
504
- objective = okr_data.get('objective_description', f"Objective {okr_idx + 1}")
505
  timeline = okr_data.get('objective_timeline', 'Not specified')
506
  owner = okr_data.get('objective_owner', 'Not assigned')
507
-
508
  html_parts.append(f"""
509
- <div class="okr-objective">
510
- <div class="objective-header">
511
- <div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
512
- <div class="objective-meta">
513
- <div class="meta-item">
514
- <span class="meta-icon">⏰</span>
515
- <span>Timeline: {timeline}</span>
516
- </div>
517
- <div class="meta-item">
518
- <span class="meta-icon">πŸ‘€</span>
519
- <span>Owner: {owner}</span>
 
520
  </div>
521
  </div>
522
- </div>
523
-
524
- <div class="key-results-container">
525
  """)
526
-
527
  key_results = okr_data.get('key_results', [])
528
-
529
- if not key_results:
530
  html_parts.append('<div class="empty-state">No key results defined for this objective.</div>')
531
  else:
532
  for kr_idx, kr_data in enumerate(key_results):
533
  if not isinstance(kr_data, dict):
534
- logger.warning(f"Key Result item for OKR '{objective}' at KR index {kr_idx} is not a dictionary, skipping.")
535
  continue
536
-
537
- kr_desc = kr_data.get('key_result_description', f"Key Result {kr_idx + 1}")
538
  target_metric = kr_data.get('target_metric', '')
539
  target_value = kr_data.get('target_value', '')
540
  kr_type = kr_data.get('key_result_type', '')
541
  data_subject = kr_data.get('data_subject', '')
542
-
543
  html_parts.append(f"""
544
- <div class="key-result">
545
- <div class="kr-header">
546
- <div class="kr-title">Key Result {kr_idx + 1}: {kr_desc}</div>
547
- <div class="kr-metrics">
548
  """)
549
-
550
  if target_metric and target_value:
551
  html_parts.append(f'<div class="kr-metric">Target: {target_metric} β†’ {target_value}</div>')
552
  if kr_type:
553
  html_parts.append(f'<div class="kr-metric">Type: {kr_type}</div>')
554
  if data_subject:
555
  html_parts.append(f'<div class="kr-metric">Data Subject: {data_subject}</div>')
556
-
557
  html_parts.append('</div></div>')
558
-
559
  # Add tasks
560
  tasks = kr_data.get('tasks', [])
561
- if tasks:
562
  html_parts.append("""
563
- <div class="tasks-section">
564
- <div class="tasks-title">
565
- <span>πŸ“‹</span>
566
- <span>Associated Tasks</span>
567
- </div>
568
  """)
569
-
570
  for task_idx, task_data in enumerate(tasks):
571
  if not isinstance(task_data, dict):
572
- logger.warning(f"Task item for KR '{kr_desc}' at task index {task_idx} is not a dictionary, skipping.")
573
  continue
574
-
575
- task_desc = task_data.get('task_description', f"Task {task_idx + 1}")
576
  task_category = task_data.get('task_category', 'General')
577
  task_type = task_data.get('task_type', 'Action')
578
  priority = task_data.get('priority', 'Medium').lower()
579
  effort = task_data.get('effort', 'Not specified')
580
  timeline = task_data.get('timeline', 'Not specified')
581
  responsible = task_data.get('responsible_party', 'Not assigned')
582
-
583
  priority_class = f"priority-{priority}" if priority in ['high', 'medium', 'low'] else 'priority-medium'
584
-
585
  html_parts.append(f"""
586
- <div class="task-item">
587
- <div class="task-header">
588
- <div class="task-title">{task_idx + 1}. {task_desc}</div>
589
- <div class="task-priority {priority_class}">{priority.upper()}</div>
590
- </div>
591
-
592
- <div class="task-details">
593
- <div class="task-detail-item">
594
- <span class="task-detail-label">Category:</span>
595
- <span>{task_category}</span>
596
  </div>
597
- <div class="task-detail-item">
598
- <span class="task-detail-label">Type:</span>
599
- <span>{task_type}</span>
600
- </div>
601
- <div class="task-detail-item">
602
- <span class="task-detail-label">Effort:</span>
603
- <span>{effort}</span>
604
- </div>
605
- <div class="task-detail-item">
606
- <span class="task-detail-label">Timeline:</span>
607
- <span>{timeline}</span>
608
- </div>
609
- <div class="task-detail-item">
610
- <span class="task-detail-label">Responsible:</span>
611
- <span>{responsible}</span>
 
 
 
 
 
 
 
612
  </div>
613
- </div>
614
  """)
615
-
616
  # Add additional details if available
617
  obj_deliverable = task_data.get('objective_deliverable')
618
  success_criteria = task_data.get('success_criteria_metrics')
619
  why_proposed = task_data.get('why_proposed')
620
-
621
- if obj_deliverable or success_criteria or why_proposed:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  html_parts.append('<div class="task-description">')
623
- if obj_deliverable:
624
- html_parts.append(f'<strong>Objective/Deliverable:</strong> {obj_deliverable}<br>')
625
- if success_criteria:
626
- html_parts.append(f'<strong>Success Metrics:</strong> {success_criteria}<br>')
627
- if why_proposed:
628
- html_parts.append(f'<strong>Rationale:</strong> {why_proposed}')
629
  html_parts.append('</div>')
630
-
631
  html_parts.append('</div>') # Close task-item
632
-
633
  html_parts.append('</div>') # Close tasks-section
634
  else:
635
  html_parts.append("""
636
- <div class="tasks-section">
637
- <div class="empty-state" style="padding: 1rem;">
638
- <div class="empty-state-icon" style="font-size: 1.5rem; margin-bottom: 0.5rem;">🀷</div>
639
- <div class="empty-state-description">No tasks defined for this Key Result.</div>
 
640
  </div>
641
- </div>
642
  """)
643
-
644
  html_parts.append('</div>') # Close key-result
645
-
646
  html_parts.append('</div></div>') # Close key-results-container and okr-objective
647
-
648
  html_parts.append('</div></div>') # Close okr-content and okr-container
649
-
650
  return ''.join(html_parts)
651
 
652
- def get_empty_okr_state():
653
- """Returns empty state HTML for when no OKRs are available."""
 
 
 
 
 
654
  return """
655
  <div class="okr-container">
656
  <div class="okr-header">
657
  <div class="okr-title">🎯 AI-Generated OKRs & Strategic Tasks</div>
658
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
659
  </div>
660
-
661
  <div class="okr-stats-bar">
662
  <div class="stat-card">
663
  <div class="stat-number">0</div>
@@ -676,7 +718,7 @@ def get_empty_okr_state():
676
  <div class="stat-label">High Priority</div>
677
  </div>
678
  </div>
679
-
680
  <div class="okr-content">
681
  <div class="empty-state">
682
  <div class="empty-state-icon">πŸ“‹</div>
 
 
1
  import gradio as gr
2
  from typing import Dict, Any, List, Optional
3
  import pandas as pd
 
8
  def create_enhanced_okr_tab():
9
  """
10
  Creates a modern, visually appealing OKR tab with improved layout and styling.
11
+ Removes the checkbox selector for a cleaner, always-visible design.
12
+
13
+ Returns:
14
+ gr.HTML: The Gradio HTML component that will display the formatted OKRs.
15
  """
16
+
17
  # Custom CSS for modern OKR styling
18
  okr_custom_css = """
19
  <style>
20
+ /* Overall Container */
21
  .okr-container {
22
+ font-family: 'Inter', sans-serif; /* Using Inter font */
23
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
24
+ min-height: 100vh;
25
  padding: 2rem;
26
+ margin: -1rem; /* Adjust margin to fill the space */
27
+ box-sizing: border-box; /* Ensure padding is included in width/height */
28
+ overflow-y: auto; /* Enable scrolling if content overflows */
29
  }
30
+
31
+ /* Header Section */
32
  .okr-header {
33
  text-align: center;
34
  margin-bottom: 3rem;
35
  color: white;
36
  }
37
+
38
  .okr-title {
39
  font-size: 2.5rem;
40
  font-weight: 700;
 
45
  background-clip: text;
46
  text-shadow: 0 2px 4px rgba(0,0,0,0.1);
47
  }
48
+
49
  .okr-subtitle {
50
  font-size: 1.2rem;
51
  opacity: 0.9;
52
  font-weight: 300;
53
  letter-spacing: 0.5px;
54
  }
55
+
56
+ /* Stats Bar */
57
  .okr-stats-bar {
58
  display: flex;
59
  justify-content: center;
 
61
  margin: 2rem 0;
62
  flex-wrap: wrap;
63
  }
64
+
65
  .stat-card {
66
  background: rgba(255, 255, 255, 0.15);
67
  backdrop-filter: blur(10px);
 
73
  min-width: 140px;
74
  transition: all 0.3s ease;
75
  }
76
+
77
  .stat-card:hover {
78
  transform: translateY(-2px);
79
  background: rgba(255, 255, 255, 0.2);
80
  box-shadow: 0 8px 32px rgba(0,0,0,0.1);
81
  }
82
+
83
  .stat-number {
84
  font-size: 2rem;
85
  font-weight: 700;
86
  margin-bottom: 0.25rem;
87
+ color: #fbbf24; /* Amber color for numbers */
88
  }
89
+
90
  .stat-label {
91
  font-size: 0.9rem;
92
  opacity: 0.9;
93
  text-transform: uppercase;
94
  letter-spacing: 1px;
95
  }
96
+
97
+ /* Main Content Area */
98
  .okr-content {
99
  background: white;
100
  border-radius: 24px;
101
  padding: 0;
102
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
103
+ overflow: hidden; /* Ensures rounded corners are applied to children */
104
  margin-top: 2rem;
105
  }
106
+
107
+ /* Objective Card */
108
  .okr-objective {
109
+ background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
110
+ border-left: 6px solid #3b82f6; /* Blue border for objectives */
111
+ margin: 2rem; /* Add margin within the white content box */
112
  border-radius: 16px;
113
  overflow: hidden;
114
  box-shadow: 0 4px 16px rgba(0,0,0,0.05);
115
  transition: all 0.3s ease;
116
  }
117
+
118
  .okr-objective:hover {
119
  transform: translateY(-2px);
120
  box-shadow: 0 8px 24px rgba(0,0,0,0.1);
121
  }
122
+
123
+ /* Objective Header */
124
  .objective-header {
125
  padding: 2rem;
126
+ background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
127
  color: white;
128
  position: relative;
129
  overflow: hidden;
130
  }
131
+
132
  .objective-header::before {
133
  content: '';
134
  position: absolute;
 
136
  left: 0;
137
  right: 0;
138
  bottom: 0;
 
139
  background: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M20 20c0 4.4-3.6 8-8 8s-8-3.6-8-8 3.6-8 8-8 8 3.6 8 8zm0-20c0 4.4-3.6 8-8 8s-8-3.6-8-8 3.6-8 8-8 8 3.6 8 8z'/%3E%3C/g%3E%3C/svg%3E");
140
  pointer-events: none;
141
  }
142
+
143
  .objective-title {
144
  font-size: 1.5rem;
145
  font-weight: 700;
 
147
  position: relative;
148
  z-index: 1;
149
  }
150
+
151
  .objective-meta {
152
  display: flex;
153
  gap: 2rem;
 
156
  position: relative;
157
  z-index: 1;
158
  }
159
+
160
  .meta-item {
161
  display: flex;
162
  align-items: center;
 
164
  font-size: 0.9rem;
165
  opacity: 0.9;
166
  }
167
+
168
  .meta-icon {
169
  width: 16px;
170
  height: 16px;
171
  opacity: 0.8;
172
  }
173
+
174
+ /* Key Results Container */
175
  .key-results-container {
176
+ padding: 2rem; /* Consistent padding for content within objectives */
177
  }
178
+
179
+ /* Key Result Card */
180
  .key-result {
181
  background: white;
182
+ border: 2px solid #e5e7eb;
183
  border-radius: 12px;
184
  margin: 1.5rem 0;
185
  overflow: hidden;
186
  transition: all 0.3s ease;
187
  }
188
+
189
  .key-result:hover {
190
+ border-color: #3b82f6;
191
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
192
  }
193
+
194
  .kr-header {
195
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
196
  padding: 1.5rem;
197
+ border-bottom: 1px solid #e5e7eb;
198
  }
199
+
200
  .kr-title {
201
  font-size: 1.2rem;
202
  font-weight: 600;
203
+ color: #1e293b;
204
  margin-bottom: 0.75rem;
205
  }
206
+
207
  .kr-metrics {
208
  display: flex;
209
  gap: 1.5rem;
210
  flex-wrap: wrap;
211
  margin-top: 1rem;
212
  }
213
+
214
  .kr-metric {
215
+ background: rgba(59, 130, 246, 0.1);
216
+ color: #1e40af;
217
  padding: 0.5rem 1rem;
218
  border-radius: 8px;
219
  font-size: 0.85rem;
220
  font-weight: 500;
221
  border: 1px solid rgba(59, 130, 246, 0.2);
222
  }
223
+
224
+ /* Tasks Section */
225
  .tasks-section {
226
  padding: 1.5rem;
227
  }
228
+
229
  .tasks-title {
230
  font-size: 1rem;
231
  font-weight: 600;
232
+ color: #374151;
233
  margin-bottom: 1rem;
234
  display: flex;
235
  align-items: center;
236
  gap: 0.5rem;
237
  }
238
+
239
  .task-item {
240
+ background: #f9fafb;
241
+ border: 1px solid #e5e7eb;
242
  border-radius: 8px;
243
  padding: 1.25rem;
244
  margin: 1rem 0;
245
  transition: all 0.2s ease;
246
  }
247
+
248
  .task-item:hover {
249
+ background: #f3f4f6;
250
+ border-color: #d1d5db;
251
  }
252
+
253
  .task-header {
254
  display: flex;
255
  justify-content: space-between;
 
257
  margin-bottom: 1rem;
258
  gap: 1rem;
259
  }
260
+
261
  .task-title {
262
  font-weight: 600;
263
+ color: #111827;
264
  flex: 1;
265
  line-height: 1.4;
266
  }
267
+
268
  .task-priority {
269
  padding: 0.25rem 0.75rem;
270
  border-radius: 12px;
 
274
  letter-spacing: 0.5px;
275
  white-space: nowrap;
276
  }
277
+
278
+ /* Priority Colors */
279
  .priority-high {
280
+ background: #fef2f2;
281
+ color: #dc2626;
282
+ border: 1px solid #fca5a5;
283
  }
284
+
285
  .priority-medium {
286
+ background: #fffbeb;
287
+ color: #d97706;
288
+ border: 1px solid #fcd34d;
289
  }
290
+
291
  .priority-low {
292
+ background: #f0fdf4;
293
+ color: #16a34a;
294
+ border: 1px solid #86efac;
295
  }
296
+
297
+ /* Task Details Grid */
298
  .task-details {
299
  display: grid;
300
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
301
  gap: 1rem;
302
  margin-top: 1rem;
303
  }
304
+
305
  .task-detail-item {
306
  display: flex;
307
  align-items: center;
308
  gap: 0.5rem;
309
  font-size: 0.875rem;
310
+ color: #6b7280;
311
  }
312
+
313
  .task-detail-label {
314
  font-weight: 500;
315
+ color: #374151;
316
  min-width: 80px;
317
  }
318
+
319
+ /* Task Description/Rationale */
320
  .task-description {
321
  margin-top: 1rem;
322
  padding: 1rem;
323
  background: white;
324
  border-radius: 6px;
325
+ border-left: 3px solid #3b82f6;
326
  font-size: 0.9rem;
327
  line-height: 1.5;
328
+ color: #4b5563;
329
  }
330
+
331
+ /* Empty State */
332
  .empty-state {
333
  text-align: center;
334
  padding: 4rem 2rem;
335
+ color: #6b7280;
336
  }
337
+
338
  .empty-state-icon {
339
  font-size: 3rem;
340
  margin-bottom: 1rem;
341
  }
342
+
343
  .empty-state-title {
344
  font-size: 1.5rem;
345
  font-weight: 600;
346
  margin-bottom: 0.5rem;
347
+ color: #374151;
348
  }
349
+
350
+ /* Loading Spinner */
351
  .loading-spinner {
352
  display: inline-block;
353
  width: 20px;
354
  height: 20px;
355
+ border: 3px solid #f3f4f6;
356
  border-radius: 50%;
357
+ border-top-color: #3b82f6;
358
  animation: spin 1s ease-in-out infinite;
359
  }
360
+
361
  @keyframes spin {
362
  to { transform: rotate(360deg); }
363
  }
364
+
365
+ /* Responsive Adjustments */
366
  @media (max-width: 768px) {
367
  .okr-container {
368
  padding: 1rem;
369
  }
370
+
371
  .okr-title {
372
  font-size: 2rem;
373
  }
374
+
375
  .okr-stats-bar {
376
  gap: 1rem;
377
  }
378
+
379
  .stat-card {
380
  min-width: 120px;
381
  padding: 1rem;
382
  }
383
+
384
  .objective-meta {
385
  flex-direction: column;
386
  gap: 1rem;
387
  }
388
+
389
  .task-details {
390
  grid-template-columns: 1fr;
391
  }
392
+
393
  .task-header {
394
  flex-direction: column;
395
  align-items: flex-start;
 
397
  }
398
  </style>
399
  """
400
+
401
+ with gr.Column(elem_classes=["okr-root-column"]):
402
  # Inject custom CSS
403
  gr.HTML(okr_custom_css)
404
+
405
  # Main OKR display area with enhanced styling
406
  okr_display_html = gr.HTML(
407
  value=get_initial_okr_display(),
408
  elem_classes=["okr-display"]
409
  )
410
+
411
  return okr_display_html
412
 
413
+ def get_initial_okr_display() -> str:
414
+ """
415
+ Returns the initial HTML display for the OKR tab, showing a loading state.
416
+
417
+ Returns:
418
+ str: HTML string for the initial OKR display.
419
+ """
420
  return """
421
  <div class="okr-container">
422
  <div class="okr-header">
423
  <div class="okr-title">🎯 AI-Generated OKRs & Strategic Tasks</div>
424
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
425
  </div>
426
+
427
  <div class="okr-stats-bar">
428
  <div class="stat-card">
429
  <div class="stat-number">-</div>
 
442
  <div class="stat-label">High Priority</div>
443
  </div>
444
  </div>
445
+
446
  <div class="okr-content">
447
  <div class="empty-state">
448
  <div class="empty-state-icon">⏳</div>
 
459
  def format_okrs_for_enhanced_display(raw_results: dict) -> str:
460
  """
461
  Enhanced formatting function that creates beautiful HTML for OKR display
462
+ from the raw results dictionary.
463
+
464
+ Args:
465
+ raw_results (dict): The dictionary containing the 'actionable_okrs' data.
466
+ Expected structure: {'actionable_okrs': {'okrs': List[OKR_dict]}}
467
+
468
+ Returns:
469
+ str: A comprehensive HTML string representing the OKRs, or an empty state HTML.
470
  """
471
  if not raw_results or not raw_results.get("actionable_okrs"):
472
+ logger.warning("No raw results or 'actionable_okrs' found for display.")
473
  return get_empty_okr_state()
474
+
475
  actionable_okrs = raw_results.get("actionable_okrs", {})
476
  okrs_list = actionable_okrs.get("okrs", [])
477
+
478
  if not okrs_list:
479
+ logger.info("No OKRs found in 'actionable_okrs' list.")
480
  return get_empty_okr_state()
481
+
482
+ # Calculate statistics for the stats bar
483
  total_objectives = len(okrs_list)
484
  total_key_results = sum(len(okr.get('key_results', [])) for okr in okrs_list)
485
  total_tasks = sum(
486
+ len(kr.get('tasks', []))
487
+ for okr in okrs_list
488
  for kr in okr.get('key_results', [])
489
  )
490
  high_priority_tasks = sum(
491
+ 1 for okr in okrs_list
492
  for kr in okr.get('key_results', [])
493
  for task in kr.get('tasks', [])
494
  if task.get('priority', '').lower() == 'high'
495
  )
496
+
497
+ # Build the HTML structure
498
  html_parts = [f"""
499
  <div class="okr-container">
500
  <div class="okr-header">
501
  <div class="okr-title">🎯 AI-Generated OKRs & Strategic Tasks</div>
502
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
503
  </div>
504
+
505
  <div class="okr-stats-bar">
506
  <div class="stat-card">
507
  <div class="stat-number">{total_objectives}</div>
 
520
  <div class="stat-label">High Priority</div>
521
  </div>
522
  </div>
523
+
524
  <div class="okr-content">
525
  """]
526
+
527
  for okr_idx, okr_data in enumerate(okrs_list):
528
  if not isinstance(okr_data, dict):
529
  logger.warning(f"OKR item at index {okr_idx} is not a dictionary, skipping.")
530
  continue
531
+
532
+ objective = okr_data.get('objective_description', f"Unnamed Objective {okr_idx + 1}")
533
  timeline = okr_data.get('objective_timeline', 'Not specified')
534
  owner = okr_data.get('objective_owner', 'Not assigned')
535
+
536
  html_parts.append(f"""
537
+ <div class="okr-objective">
538
+ <div class="objective-header">
539
+ <div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
540
+ <div class="objective-meta">
541
+ <div class="meta-item">
542
+ <span class="meta-icon">⏰</span>
543
+ <span>Timeline: {timeline}</span>
544
+ </div>
545
+ <div class="meta-item">
546
+ <span class="meta-icon">πŸ‘€</span>
547
+ <span>Owner: {owner}</span>
548
+ </div>
549
  </div>
550
  </div>
551
+
552
+ <div class="key-results-container">
 
553
  """)
554
+
555
  key_results = okr_data.get('key_results', [])
556
+
557
+ if not isinstance(key_results, list) or not key_results:
558
  html_parts.append('<div class="empty-state">No key results defined for this objective.</div>')
559
  else:
560
  for kr_idx, kr_data in enumerate(key_results):
561
  if not isinstance(kr_data, dict):
562
+ logger.warning(f"Key Result item for Objective {okr_idx+1} at index {kr_idx} is not a dictionary, skipping.")
563
  continue
564
+
565
+ kr_desc = kr_data.get('key_result_description', f"Unnamed Key Result {kr_idx + 1}")
566
  target_metric = kr_data.get('target_metric', '')
567
  target_value = kr_data.get('target_value', '')
568
  kr_type = kr_data.get('key_result_type', '')
569
  data_subject = kr_data.get('data_subject', '')
570
+
571
  html_parts.append(f"""
572
+ <div class="key-result">
573
+ <div class="kr-header">
574
+ <div class="kr-title">Key Result {kr_idx + 1}: {kr_desc}</div>
575
+ <div class="kr-metrics">
576
  """)
577
+
578
  if target_metric and target_value:
579
  html_parts.append(f'<div class="kr-metric">Target: {target_metric} β†’ {target_value}</div>')
580
  if kr_type:
581
  html_parts.append(f'<div class="kr-metric">Type: {kr_type}</div>')
582
  if data_subject:
583
  html_parts.append(f'<div class="kr-metric">Data Subject: {data_subject}</div>')
584
+
585
  html_parts.append('</div></div>')
586
+
587
  # Add tasks
588
  tasks = kr_data.get('tasks', [])
589
+ if tasks and isinstance(tasks, list):
590
  html_parts.append("""
591
+ <div class="tasks-section">
592
+ <div class="tasks-title">
593
+ <span>πŸ“‹</span>
594
+ <span>Associated Tasks</span>
595
+ </div>
596
  """)
597
+
598
  for task_idx, task_data in enumerate(tasks):
599
  if not isinstance(task_data, dict):
600
+ logger.warning(f"Task item for Key Result {kr_idx+1} at index {task_idx} is not a dictionary, skipping.")
601
  continue
602
+
603
+ task_desc = task_data.get('task_description', f"Unnamed Task {task_idx + 1}")
604
  task_category = task_data.get('task_category', 'General')
605
  task_type = task_data.get('task_type', 'Action')
606
  priority = task_data.get('priority', 'Medium').lower()
607
  effort = task_data.get('effort', 'Not specified')
608
  timeline = task_data.get('timeline', 'Not specified')
609
  responsible = task_data.get('responsible_party', 'Not assigned')
610
+
611
  priority_class = f"priority-{priority}" if priority in ['high', 'medium', 'low'] else 'priority-medium'
612
+
613
  html_parts.append(f"""
614
+ <div class="task-item">
615
+ <div class="task-header">
616
+ <div class="task-title">{task_idx + 1}. {task_desc}</div>
617
+ <div class="task-priority {priority_class}">{priority.upper()}</div>
 
 
 
 
 
 
618
  </div>
619
+
620
+ <div class="task-details">
621
+ <div class="task-detail-item">
622
+ <span class="task-detail-label">Category:</span>
623
+ <span>{task_category}</span>
624
+ </div>
625
+ <div class="task-detail-item">
626
+ <span class="task-detail-label">Type:</span>
627
+ <span>{task_type}</span>
628
+ </div>
629
+ <div class="task-detail-item">
630
+ <span class="task-detail-label">Effort:</span>
631
+ <span>{effort}</span>
632
+ </div>
633
+ <div class="task-detail-item">
634
+ <span class="task-detail-label">Timeline:</span>
635
+ <span>{timeline}</span>
636
+ </div>
637
+ <div class="task-detail-item">
638
+ <span class="task-detail-label">Responsible:</span>
639
+ <span>{responsible}</span>
640
+ </div>
641
  </div>
 
642
  """)
643
+
644
  # Add additional details if available
645
  obj_deliverable = task_data.get('objective_deliverable')
646
  success_criteria = task_data.get('success_criteria_metrics')
647
  why_proposed = task_data.get('why_proposed')
648
+ priority_just = task_data.get('priority_justification')
649
+ dependencies = task_data.get('dependencies_prerequisites')
650
+
651
+ detail_lines = []
652
+ if obj_deliverable:
653
+ detail_lines.append(f'<strong>Objective/Deliverable:</strong> {obj_deliverable}')
654
+ if success_criteria:
655
+ detail_lines.append(f'<strong>Success Metrics:</strong> {success_criteria}')
656
+ if why_proposed:
657
+ detail_lines.append(f'<strong>Rationale:</strong> {why_proposed}')
658
+ if priority_just:
659
+ detail_lines.append(f'<strong>Priority Justification:</strong> {priority_just}')
660
+ if dependencies:
661
+ detail_lines.append(f'<strong>Dependencies:</strong> {dependencies}')
662
+
663
+ if detail_lines:
664
  html_parts.append('<div class="task-description">')
665
+ html_parts.append('<br>'.join(detail_lines))
 
 
 
 
 
666
  html_parts.append('</div>')
667
+
668
  html_parts.append('</div>') # Close task-item
669
+
670
  html_parts.append('</div>') # Close tasks-section
671
  else:
672
  html_parts.append("""
673
+ <div class="tasks-section">
674
+ <div class="empty-state-small">
675
+ <div class="empty-state-icon">πŸ“„</div>
676
+ <div class="empty-state-description">No tasks defined for this Key Result.</div>
677
+ </div>
678
  </div>
 
679
  """)
680
+
681
  html_parts.append('</div>') # Close key-result
682
+
683
  html_parts.append('</div></div>') # Close key-results-container and okr-objective
684
+
685
  html_parts.append('</div></div>') # Close okr-content and okr-container
686
+
687
  return ''.join(html_parts)
688
 
689
+ def get_empty_okr_state() -> str:
690
+ """
691
+ Returns empty state HTML for when no OKRs are available.
692
+
693
+ Returns:
694
+ str: HTML string for the empty OKR state.
695
+ """
696
  return """
697
  <div class="okr-container">
698
  <div class="okr-header">
699
  <div class="okr-title">🎯 AI-Generated OKRs & Strategic Tasks</div>
700
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
701
  </div>
702
+
703
  <div class="okr-stats-bar">
704
  <div class="stat-card">
705
  <div class="stat-number">0</div>
 
718
  <div class="stat-label">High Priority</div>
719
  </div>
720
  </div>
721
+
722
  <div class="okr-content">
723
  <div class="empty-state">
724
  <div class="empty-state-icon">πŸ“‹</div>