GuglielmoTor commited on
Commit
f364ce4
·
verified ·
1 Parent(s): 8010fdc

Update ui/okr_ui_generator.py

Browse files
Files changed (1) hide show
  1. ui/okr_ui_generator.py +132 -188
ui/okr_ui_generator.py CHANGED
@@ -9,29 +9,39 @@ def create_enhanced_okr_tab():
9
  """
10
  Creates a modern, visually appealing OKR tab with improved layout and styling.
11
  This version includes full support for Gradio's dark mode and a redesigned,
12
- more readable objective header.
13
 
14
  Returns:
15
  gr.HTML: The Gradio HTML component that will display the formatted OKRs.
16
  """
17
 
18
- # Custom CSS for modern OKR styling with Dark Mode support and JS theme syncing
 
 
 
19
  okr_custom_css = """
20
  <style>
21
  /* ----------------------------------------- */
22
- /* --- LIGHT MODE THEME & COLOR VARIABLES --- */
23
  /* ----------------------------------------- */
24
  :root {
 
25
  --okr-bg-start: #667eea;
26
  --okr-bg-end: #764ba2;
 
 
 
 
27
  --header-text-color: white;
28
  --header-text-shadow: rgba(0,0,0,0.1);
 
 
29
  --stat-card-bg: rgba(255, 255, 255, 0.15);
30
  --stat-card-bg-hover: rgba(255, 255, 255, 0.2);
31
  --stat-card-border: rgba(255, 255, 255, 0.2);
32
  --stat-number-color: #fbbf24;
33
- --content-bg: white;
34
- --content-shadow: rgba(0,0,0,0.1);
35
  --objective-card-bg: #f8fafc;
36
  --objective-card-border: #3b82f6;
37
  --objective-shadow: 0 4px 16px rgba(0,0,0,0.05);
@@ -39,6 +49,8 @@ def create_enhanced_okr_tab():
39
  --objective-header-bg: transparent;
40
  --objective-title-color: #1e40af;
41
  --objective-meta-text-color: #475569;
 
 
42
  --key-result-bg: white;
43
  --key-result-border: #e5e7eb;
44
  --key-result-border-hover: #3b82f6;
@@ -47,6 +59,8 @@ def create_enhanced_okr_tab():
47
  --kr-metric-bg: rgba(59, 130, 246, 0.1);
48
  --kr-metric-color: #1e40af;
49
  --kr-metric-border: rgba(59, 130, 246, 0.2);
 
 
50
  --task-item-bg: #f9fafb;
51
  --task-item-bg-hover: #f3f4f6;
52
  --task-item-border: #e5e7eb;
@@ -57,6 +71,8 @@ def create_enhanced_okr_tab():
57
  --task-description-bg: white;
58
  --task-description-border: #3b82f6;
59
  --task-description-color: #4b5563;
 
 
60
  --priority-high-bg: #fef2f2;
61
  --priority-high-color: #dc2626;
62
  --priority-high-border: #fca5a5;
@@ -66,6 +82,71 @@ def create_enhanced_okr_tab():
66
  --priority-low-bg: #f0fdf4;
67
  --priority-low-color: #16a34a;
68
  --priority-low-border: #86efac;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  }
70
 
71
  /* ----------------------------------------- */
@@ -74,16 +155,12 @@ def create_enhanced_okr_tab():
74
  .okr-container {
75
  font-family: 'Inter', sans-serif;
76
  background: linear-gradient(135deg, var(--okr-bg-start) 0%, var(--okr-bg-end) 100%);
77
- min-height: 100vh;
78
- padding: 2rem;
79
- margin: -1rem;
80
- box-sizing: border-box;
81
- overflow-y: auto;
82
  }
83
-
84
  .okr-header { text-align: center; margin-bottom: 3rem; color: var(--header-text-color); }
85
  .okr-title { font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; text-shadow: 0 2px 4px var(--header-text-shadow); display: flex; justify-content: center; align-items: center; gap: 0.75rem; }
86
  .okr-title-content { background: linear-gradient(45deg, #ffffff, #e0e7ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
 
87
  .okr-title-emoji { font-size: 2.5rem; -webkit-text-fill-color: initial; }
88
  .okr-subtitle { font-size: 1.2rem; opacity: 0.9; font-weight: 300; letter-spacing: 0.5px; }
89
  .okr-stats-bar { display: flex; justify-content: center; gap: 2rem; margin: 2rem 0; flex-wrap: wrap; }
@@ -123,39 +200,8 @@ def create_enhanced_okr_tab():
123
  .empty-state { text-align: center; padding: 4rem 2rem; color: var(--task-detail-text-color); }
124
  .empty-state-icon { font-size: 3rem; margin-bottom: 1rem; }
125
  .empty-state-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--objective-title-color); }
126
- .loading-spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid var(--task-item-bg-hover); border-radius: 50%; border-top-color: var(--objective-card-border); animation: spin 1s ease-in-out infinite; }
127
  @keyframes spin { to { transform: rotate(360deg); } }
128
-
129
- /* ----------------------------------------- */
130
- /* --- DARK MODE OVERRIDES (DIRECT) --- */
131
- /* ----------------------------------------- */
132
- html.dark .okr-title-content { background: linear-gradient(45deg, #e5e7eb, #9ca3af); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
133
- html.dark .okr-header { color: #e5e7eb; }
134
- html.dark .stat-card { background: rgba(28, 28, 28, 0.7); border-color: rgba(50, 50, 50, 0.8); box-shadow: 0 8px 32px rgba(0,0,0,0.6); }
135
- html.dark .okr-content { background: #0d1117; box-shadow: 0 20px 40px rgba(0,0,0,0.6); }
136
- html.dark .okr-objective { background: #161b22; border-left-color: #3b82f6; box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
137
- html.dark .okr-objective:hover { box-shadow: 0 10px 28px rgba(0,0,0,0.6); }
138
- html.dark .objective-header { border-bottom-color: #30363d; }
139
- html.dark .objective-title { color: #58a6ff; }
140
- html.dark .meta-item { color: #8b949e; }
141
- html.dark .key-result { background: #161b22; border-color: #30363d; }
142
- html.dark .key-result:hover { border-color: #58a6ff; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2); }
143
- html.dark .kr-header { background: #161b22; border-bottom-color: #30363d; }
144
- html.dark .kr-title { color: #c9d1d9; }
145
- html.dark .kr-metric { background: rgba(56, 139, 253, 0.15); color: #58a6ff; border-color: rgba(56, 139, 253, 0.4); }
146
- html.dark .tasks-title { color: #8b949e; }
147
- html.dark .task-item { background: #21262d; border-color: #30363d; }
148
- html.dark .task-item:hover { background: #30363d; border-color: #8b949e; }
149
- html.dark .task-title { color: #c9d1d9; }
150
- html.dark .task-detail-label { color: #8b949e; }
151
- html.dark .task-detail-item { color: #8b949e; }
152
- html.dark .task-description { background: #0d1117; color: #8b949e; border-left-color: #30363d; }
153
- html.dark .priority-high { background: rgba(248, 81, 73, 0.15); color: #ff7b72; border-color: rgba(248, 81, 73, 0.4); }
154
- html.dark .priority-medium { background: rgba(210, 153, 34, 0.15); color: #d29922; border-color: rgba(210, 153, 34, 0.4); }
155
- html.dark .priority-low { background: rgba(63, 185, 80, 0.15); color: #56d364; border-color: rgba(63, 185, 80, 0.4); }
156
- html.dark .empty-state { color: #8b949e; }
157
- html.dark .empty-state-title { color: #58a6ff; }
158
- html.dark .loading-spinner { border-color: #30363d; border-top-color: #58a6ff; }
159
 
160
  /* Responsive Adjustments */
161
  @media (max-width: 768px) {
@@ -169,10 +215,10 @@ def create_enhanced_okr_tab():
169
  }
170
  </style>
171
  <script>
 
 
 
172
  function syncThemeWithParent() {
173
- // This script runs inside the Gradio iframe. It looks at the parent page's
174
- // <html> tag to see if the 'dark' class is present and applies it to the
175
- // iframe's own <html> tag, ensuring the CSS rules for dark mode are activated.
176
  try {
177
  const parentHtml = window.parent.document.querySelector('html');
178
  if (parentHtml) {
@@ -183,12 +229,10 @@ def create_enhanced_okr_tab():
183
  }
184
  }
185
  } catch (e) {
186
- // This can fail due to cross-origin policies, but it's safe in Gradio.
187
- console.error("Error syncing theme with parent:", e);
188
  }
189
  }
190
 
191
- // Use a MutationObserver to detect when the theme is toggled on the parent page.
192
  try {
193
  const observer = new MutationObserver(syncThemeWithParent);
194
  const parentHtml = window.parent.document.querySelector('html');
@@ -199,8 +243,7 @@ def create_enhanced_okr_tab():
199
  console.error("Could not set up theme MutationObserver:", e);
200
  }
201
 
202
- // Also run the sync function once when the iframe's content has loaded,
203
- // and a few times after as a fallback for timing issues.
204
  document.addEventListener('DOMContentLoaded', () => {
205
  syncThemeWithParent();
206
  setTimeout(syncThemeWithParent, 50);
@@ -213,7 +256,7 @@ def create_enhanced_okr_tab():
213
  # Inject custom CSS and the theme-syncing JS
214
  gr.HTML(okr_custom_css)
215
 
216
- # Main OKR display area with enhanced styling
217
  okr_display_html = gr.HTML(
218
  value=get_initial_okr_display(),
219
  elem_classes=["okr-display"]
@@ -228,6 +271,8 @@ def get_initial_okr_display() -> str:
228
  Returns:
229
  str: HTML string for the initial OKR display.
230
  """
 
 
231
  return """
232
  <div class="okr-container">
233
  <div class="okr-header">
@@ -239,22 +284,10 @@ def get_initial_okr_display() -> str:
239
  </div>
240
 
241
  <div class="okr-stats-bar">
242
- <div class="stat-card">
243
- <div class="stat-number">-</div>
244
- <div class="stat-label">Objectives</div>
245
- </div>
246
- <div class="stat-card">
247
- <div class="stat-number">-</div>
248
- <div class="stat-label">Key Results</div>
249
- </div>
250
- <div class="stat-card">
251
- <div class="stat-number">-</div>
252
- <div class="stat-label">Tasks</div>
253
- </div>
254
- <div class="stat-card">
255
- <div class="stat-number">-</div>
256
- <div class="stat-label">High Priority</div>
257
- </div>
258
  </div>
259
 
260
  <div class="okr-content">
@@ -277,7 +310,6 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
277
 
278
  Args:
279
  reconstruction_cache (dict): The reconstruction cache containing reconstructed data.
280
- Expected to contain report data with 'actionable_okrs' key.
281
 
282
  Returns:
283
  str: A comprehensive HTML string representing the OKRs, or an empty state HTML.
@@ -286,7 +318,6 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
286
  logger.warning("No reconstruction cache found for display.")
287
  return get_empty_okr_state()
288
 
289
- # Extract actionable_okrs from the first (and typically only) report in cache
290
  for report_id, report_data in reconstruction_cache.items():
291
  if isinstance(report_data, dict) and 'actionable_okrs' in report_data:
292
  raw_results = {'actionable_okrs': report_data['actionable_okrs']}
@@ -302,7 +333,6 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
302
  logger.info("No OKRs found in 'actionable_okrs' list.")
303
  return get_empty_okr_state()
304
 
305
- # Calculate statistics for the stats bar
306
  total_objectives = len(okrs_list)
307
  total_key_results = sum(len(okr.get('key_results', [])) for okr in okrs_list)
308
  total_tasks = sum(
@@ -317,7 +347,6 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
317
  if task.get('priority', '').lower() == 'high'
318
  )
319
 
320
- # Build the HTML structure
321
  html_parts = [f"""
322
  <div class="okr-container">
323
  <div class="okr-header">
@@ -327,26 +356,12 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
327
  </div>
328
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
329
  </div>
330
-
331
  <div class="okr-stats-bar">
332
- <div class="stat-card">
333
- <div class="stat-number">{total_objectives}</div>
334
- <div class="stat-label">Objectives</div>
335
- </div>
336
- <div class="stat-card">
337
- <div class="stat-number">{total_key_results}</div>
338
- <div class="stat-label">Key Results</div>
339
- </div>
340
- <div class="stat-card">
341
- <div class="stat-number">{total_tasks}</div>
342
- <div class="stat-label">Tasks</div>
343
- </div>
344
- <div class="stat-card">
345
- <div class="stat-number">{high_priority_tasks}</div>
346
- <div class="stat-label">High Priority</div>
347
- </div>
348
  </div>
349
-
350
  <div class="okr-content">
351
  """]
352
 
@@ -365,27 +380,23 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
365
  <div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
366
  <div class="objective-meta">
367
  <div class="meta-item">
368
- <span class="meta-icon">⏰</span>
369
- <span>Timeline: {timeline}</span>
370
  </div>
371
  <div class="meta-item">
372
- <span class="meta-icon">👤</span>
373
- <span>Owner: {owner}</span>
374
  </div>
375
  </div>
376
  </div>
377
-
378
  <div class="key-results-container">
379
  """)
380
 
381
  key_results = okr_data.get('key_results', [])
382
-
383
  if not isinstance(key_results, list) or not key_results:
384
  html_parts.append('<div class="empty-state">No key results defined for this objective.</div>')
385
  else:
386
  for kr_idx, kr_data in enumerate(key_results):
387
  if not isinstance(kr_data, dict):
388
- logger.warning(f"Key Result item for Objective {okr_idx+1} at index {kr_idx} is not a dictionary, skipping.")
389
  continue
390
 
391
  kr_desc = kr_data.get('description', f"Unnamed Key Result {kr_idx + 1}")
@@ -400,87 +411,43 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
400
  <div class="kr-title">Key Result {kr_idx + 1}: {kr_desc}</div>
401
  <div class="kr-metrics">
402
  """)
403
-
404
  if target_metric and target_value:
405
  html_parts.append(f'<div class="kr-metric">Target: {target_metric} → {target_value}</div>')
406
  if kr_type:
407
  html_parts.append(f'<div class="kr-metric">Type: {kr_type}</div>')
408
  if data_subject:
409
  html_parts.append(f'<div class="kr-metric">Data Subject: {data_subject}</div>')
 
410
 
411
- html_parts.append('</div></div>')
412
-
413
- # Add tasks
414
  tasks = kr_data.get('tasks', [])
415
  if tasks and isinstance(tasks, list):
416
- html_parts.append("""
417
- <div class="tasks-section">
418
- <div class="tasks-title">
419
- <span>📋</span>
420
- <span>Associated Tasks</span>
421
- </div>
422
- """)
423
-
424
  for task_idx, task_data in enumerate(tasks):
425
- if not isinstance(task_data, dict):
426
- logger.warning(f"Task item for Key Result {kr_idx+1} at index {task_idx} is not a dictionary, skipping.")
427
- continue
428
-
429
  task_desc = task_data.get('description', f"Unnamed Task {task_idx + 1}")
430
- task_category = task_data.get('category', 'General')
431
-
432
  priority = task_data.get('priority', 'Medium').lower()
433
- effort = task_data.get('effort', 'Not specified')
434
- timeline = task_data.get('timeline', 'Not specified')
435
- responsible = task_data.get('responsible_party', 'Not assigned')
436
-
437
  priority_class = f"priority-{priority}" if priority in ['high', 'medium', 'low'] else 'priority-medium'
438
-
439
  html_parts.append(f"""
440
- <div class="task-item">
441
- <div class="task-header">
442
- <div class="task-title">{task_idx + 1}. {task_desc}</div>
443
- <div class="task-priority {priority_class}">{priority.upper()}</div>
444
- </div>
445
-
446
- <div class="task-details">
447
- <div class="task-detail-item">
448
- <span class="task-detail-label">Category:</span>
449
- <span>{task_category}</span>
450
- </div>
451
- <div class="task-detail-item">
452
- <span class="task-detail-label">Effort:</span>
453
- <span>{effort}</span>
454
  </div>
455
- <div class="task-detail-item">
456
- <span class="task-detail-label">Timeline:</span>
457
- <span>{timeline}</span>
 
 
458
  </div>
459
- <div class="task-detail-item">
460
- <span class="task-detail-label">Responsible:</span>
461
- <span>{responsible}</span>
462
- </div>
463
- </div>
464
  """)
465
 
466
- # Add additional details if available
467
- obj_deliverable = task_data.get('deliverable')
468
- success_criteria = task_data.get('success_criteria_metrics')
469
- why_proposed = task_data.get('why')
470
- priority_just = task_data.get('priority_justification')
471
- dependencies = task_data.get('dependencies')
472
-
473
  detail_lines = []
474
- if obj_deliverable:
475
- detail_lines.append(f'<strong>Objective/Deliverable:</strong> {obj_deliverable}')
476
- if success_criteria:
477
- detail_lines.append(f'<strong>Success Metrics:</strong> {success_criteria}')
478
- if why_proposed:
479
- detail_lines.append(f'<strong>Rationale:</strong> {why_proposed}')
480
- if priority_just:
481
- detail_lines.append(f'<strong>Priority Justification:</strong> {priority_just}')
482
- if dependencies:
483
- detail_lines.append(f'<strong>Dependencies:</strong> {dependencies}')
484
 
485
  if detail_lines:
486
  html_parts.append('<div class="task-description">')
@@ -488,26 +455,15 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
488
  html_parts.append('</div>')
489
 
490
  html_parts.append('</div>') # Close task-item
491
-
492
  html_parts.append('</div>') # Close tasks-section
493
  else:
494
- html_parts.append("""
495
- <div class="tasks-section">
496
- <div class="empty-state-small">
497
- <div class="empty-state-icon">📄</div>
498
- <div class="empty-state-description">No tasks defined for this Key Result.</div>
499
- </div>
500
- </div>
501
- """)
502
-
503
  html_parts.append('</div>') # Close key-result
504
-
505
  html_parts.append('</div></div>') # Close key-results-container and okr-objective
506
-
507
  html_parts.append('</div></div>') # Close okr-content and okr-container
508
-
509
  return ''.join(html_parts)
510
 
 
511
  def get_empty_okr_state() -> str:
512
  """
513
  Returns empty state HTML for when no OKRs are available.
@@ -526,22 +482,10 @@ def get_empty_okr_state() -> str:
526
  </div>
527
 
528
  <div class="okr-stats-bar">
529
- <div class="stat-card">
530
- <div class="stat-number">0</div>
531
- <div class="stat-label">Objectives</div>
532
- </div>
533
- <div class="stat-card">
534
- <div class="stat-number">0</div>
535
- <div class="stat-label">Key Results</div>
536
- </div>
537
- <div class="stat-card">
538
- <div class="stat-number">0</div>
539
- <div class="stat-label">Tasks</div>
540
- </div>
541
- <div class="stat-card">
542
- <div class="stat-number">0</div>
543
- <div class="stat-label">High Priority</div>
544
- </div>
545
  </div>
546
 
547
  <div class="okr-content">
 
9
  """
10
  Creates a modern, visually appealing OKR tab with improved layout and styling.
11
  This version includes full support for Gradio's dark mode and a redesigned,
12
+ more readable objective header by using a robust CSS variable-based theming approach.
13
 
14
  Returns:
15
  gr.HTML: The Gradio HTML component that will display the formatted OKRs.
16
  """
17
 
18
+ # --- Refactored CSS for Robust Dark Mode Theming ---
19
+ # This version uses CSS variables for both light and dark modes, which is a more
20
+ # reliable and maintainable way to handle themes. Instead of overriding dozens of
21
+ # individual CSS rules, we just redefine the color variables once.
22
  okr_custom_css = """
23
  <style>
24
  /* ----------------------------------------- */
25
+ /* --- THEME & COLOR VARIABLES --- */
26
  /* ----------------------------------------- */
27
  :root {
28
+ /* -- Base & Layout -- */
29
  --okr-bg-start: #667eea;
30
  --okr-bg-end: #764ba2;
31
+ --content-bg: white;
32
+ --content-shadow: rgba(0,0,0,0.1);
33
+
34
+ /* -- Headers & Titles -- */
35
  --header-text-color: white;
36
  --header-text-shadow: rgba(0,0,0,0.1);
37
+
38
+ /* -- Stat Cards -- */
39
  --stat-card-bg: rgba(255, 255, 255, 0.15);
40
  --stat-card-bg-hover: rgba(255, 255, 255, 0.2);
41
  --stat-card-border: rgba(255, 255, 255, 0.2);
42
  --stat-number-color: #fbbf24;
43
+
44
+ /* -- Objective Cards -- */
45
  --objective-card-bg: #f8fafc;
46
  --objective-card-border: #3b82f6;
47
  --objective-shadow: 0 4px 16px rgba(0,0,0,0.05);
 
49
  --objective-header-bg: transparent;
50
  --objective-title-color: #1e40af;
51
  --objective-meta-text-color: #475569;
52
+
53
+ /* -- Key Result Cards -- */
54
  --key-result-bg: white;
55
  --key-result-border: #e5e7eb;
56
  --key-result-border-hover: #3b82f6;
 
59
  --kr-metric-bg: rgba(59, 130, 246, 0.1);
60
  --kr-metric-color: #1e40af;
61
  --kr-metric-border: rgba(59, 130, 246, 0.2);
62
+
63
+ /* -- Task Items -- */
64
  --task-item-bg: #f9fafb;
65
  --task-item-bg-hover: #f3f4f6;
66
  --task-item-border: #e5e7eb;
 
71
  --task-description-bg: white;
72
  --task-description-border: #3b82f6;
73
  --task-description-color: #4b5563;
74
+
75
+ /* -- Priority Badges -- */
76
  --priority-high-bg: #fef2f2;
77
  --priority-high-color: #dc2626;
78
  --priority-high-border: #fca5a5;
 
82
  --priority-low-bg: #f0fdf4;
83
  --priority-low-color: #16a34a;
84
  --priority-low-border: #86efac;
85
+
86
+ /* -- Utility -- */
87
+ --loading-spinner-bg: #e5e7eb;
88
+ --loading-spinner-fg: #3b82f6;
89
+ }
90
+
91
+ /* ----------------------------------------- */
92
+ /* --- DARK MODE VARIABLE OVERRIDES --- */
93
+ /* ----------------------------------------- */
94
+ html.dark {
95
+ /* -- Base & Layout -- */
96
+ --content-bg: #0d1117;
97
+ --content-shadow: rgba(0,0,0,0.6);
98
+
99
+ /* -- Headers & Titles -- */
100
+ --header-text-color: #e5e7eb;
101
+
102
+ /* -- Stat Cards -- */
103
+ --stat-card-bg: rgba(28, 28, 28, 0.7);
104
+ --stat-card-border: rgba(50, 50, 50, 0.8);
105
+
106
+ /* -- Objective Cards -- */
107
+ --objective-card-bg: #161b22;
108
+ --objective-card-border: #3b82f6; /* Keep brand color */
109
+ --objective-shadow: 0 8px 24px rgba(0,0,0,0.5);
110
+ --objective-shadow-hover: 0 10px 28px rgba(0,0,0,0.6);
111
+ --objective-title-color: #58a6ff;
112
+ --objective-meta-text-color: #8b949e;
113
+
114
+ /* -- Key Result Cards -- */
115
+ --key-result-bg: #161b22;
116
+ --key-result-border: #30363d;
117
+ --key-result-border-hover: #58a6ff;
118
+ --kr-header-bg: #161b22;
119
+ --kr-title-color: #c9d1d9;
120
+ --kr-metric-bg: rgba(56, 139, 253, 0.15);
121
+ --kr-metric-color: #58a6ff;
122
+ --kr-metric-border: rgba(56, 139, 253, 0.4);
123
+
124
+ /* -- Task Items -- */
125
+ --task-item-bg: #21262d;
126
+ --task-item-bg-hover: #30363d;
127
+ --task-item-border: #30363d;
128
+ --task-item-border-hover: #8b949e;
129
+ --task-title-color: #c9d1d9;
130
+ --task-detail-label-color: #8b949e;
131
+ --task-detail-text-color: #8b949e;
132
+ --task-description-bg: #0d1117;
133
+ --task-description-border: #30363d;
134
+ --task-description-color: #8b949e;
135
+
136
+ /* -- Priority Badges -- */
137
+ --priority-high-bg: rgba(248, 81, 73, 0.15);
138
+ --priority-high-color: #ff7b72;
139
+ --priority-high-border: rgba(248, 81, 73, 0.4);
140
+ --priority-medium-bg: rgba(210, 153, 34, 0.15);
141
+ --priority-medium-color: #d29922;
142
+ --priority-medium-border: rgba(210, 153, 34, 0.4);
143
+ --priority-low-bg: rgba(63, 185, 80, 0.15);
144
+ --priority-low-color: #56d364;
145
+ --priority-low-border: rgba(63, 185, 80, 0.4);
146
+
147
+ /* -- Utility -- */
148
+ --loading-spinner-bg: #30363d;
149
+ --loading-spinner-fg: #58a6ff;
150
  }
151
 
152
  /* ----------------------------------------- */
 
155
  .okr-container {
156
  font-family: 'Inter', sans-serif;
157
  background: linear-gradient(135deg, var(--okr-bg-start) 0%, var(--okr-bg-end) 100%);
158
+ min-height: 100vh; padding: 2rem; margin: -1rem; box-sizing: border-box; overflow-y: auto;
 
 
 
 
159
  }
 
160
  .okr-header { text-align: center; margin-bottom: 3rem; color: var(--header-text-color); }
161
  .okr-title { font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; text-shadow: 0 2px 4px var(--header-text-shadow); display: flex; justify-content: center; align-items: center; gap: 0.75rem; }
162
  .okr-title-content { background: linear-gradient(45deg, #ffffff, #e0e7ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
163
+ html.dark .okr-title-content { background: linear-gradient(45deg, #e5e7eb, #9ca3af); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
164
  .okr-title-emoji { font-size: 2.5rem; -webkit-text-fill-color: initial; }
165
  .okr-subtitle { font-size: 1.2rem; opacity: 0.9; font-weight: 300; letter-spacing: 0.5px; }
166
  .okr-stats-bar { display: flex; justify-content: center; gap: 2rem; margin: 2rem 0; flex-wrap: wrap; }
 
200
  .empty-state { text-align: center; padding: 4rem 2rem; color: var(--task-detail-text-color); }
201
  .empty-state-icon { font-size: 3rem; margin-bottom: 1rem; }
202
  .empty-state-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--objective-title-color); }
203
+ .loading-spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid var(--loading-spinner-bg); border-radius: 50%; border-top-color: var(--loading-spinner-fg); animation: spin 1s ease-in-out infinite; }
204
  @keyframes spin { to { transform: rotate(360deg); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  /* Responsive Adjustments */
207
  @media (max-width: 768px) {
 
215
  }
216
  </style>
217
  <script>
218
+ // This script is responsible for syncing the theme with the main Gradio app.
219
+ // It checks if the parent document has the 'dark' class and applies it here.
220
+ // A MutationObserver watches for real-time changes when the user toggles the theme.
221
  function syncThemeWithParent() {
 
 
 
222
  try {
223
  const parentHtml = window.parent.document.querySelector('html');
224
  if (parentHtml) {
 
229
  }
230
  }
231
  } catch (e) {
232
+ console.error("Theme Sync Error:", e);
 
233
  }
234
  }
235
 
 
236
  try {
237
  const observer = new MutationObserver(syncThemeWithParent);
238
  const parentHtml = window.parent.document.querySelector('html');
 
243
  console.error("Could not set up theme MutationObserver:", e);
244
  }
245
 
246
+ // Run sync on load and after short delays as a fallback.
 
247
  document.addEventListener('DOMContentLoaded', () => {
248
  syncThemeWithParent();
249
  setTimeout(syncThemeWithParent, 50);
 
256
  # Inject custom CSS and the theme-syncing JS
257
  gr.HTML(okr_custom_css)
258
 
259
+ # Main OKR display area
260
  okr_display_html = gr.HTML(
261
  value=get_initial_okr_display(),
262
  elem_classes=["okr-display"]
 
271
  Returns:
272
  str: HTML string for the initial OKR display.
273
  """
274
+ # This HTML uses the CSS classes defined above. The styles are applied
275
+ # automatically based on the variables, which are theme-aware.
276
  return """
277
  <div class="okr-container">
278
  <div class="okr-header">
 
284
  </div>
285
 
286
  <div class="okr-stats-bar">
287
+ <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Objectives</div></div>
288
+ <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Key Results</div></div>
289
+ <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Tasks</div></div>
290
+ <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">High Priority</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
291
  </div>
292
 
293
  <div class="okr-content">
 
310
 
311
  Args:
312
  reconstruction_cache (dict): The reconstruction cache containing reconstructed data.
 
313
 
314
  Returns:
315
  str: A comprehensive HTML string representing the OKRs, or an empty state HTML.
 
318
  logger.warning("No reconstruction cache found for display.")
319
  return get_empty_okr_state()
320
 
 
321
  for report_id, report_data in reconstruction_cache.items():
322
  if isinstance(report_data, dict) and 'actionable_okrs' in report_data:
323
  raw_results = {'actionable_okrs': report_data['actionable_okrs']}
 
333
  logger.info("No OKRs found in 'actionable_okrs' list.")
334
  return get_empty_okr_state()
335
 
 
336
  total_objectives = len(okrs_list)
337
  total_key_results = sum(len(okr.get('key_results', [])) for okr in okrs_list)
338
  total_tasks = sum(
 
347
  if task.get('priority', '').lower() == 'high'
348
  )
349
 
 
350
  html_parts = [f"""
351
  <div class="okr-container">
352
  <div class="okr-header">
 
356
  </div>
357
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
358
  </div>
 
359
  <div class="okr-stats-bar">
360
+ <div class="stat-card"><div class="stat-number">{total_objectives}</div><div class="stat-label">Objectives</div></div>
361
+ <div class="stat-card"><div class="stat-number">{total_key_results}</div><div class="stat-label">Key Results</div></div>
362
+ <div class="stat-card"><div class="stat-number">{total_tasks}</div><div class="stat-label">Tasks</div></div>
363
+ <div class="stat-card"><div class="stat-number">{high_priority_tasks}</div><div class="stat-label">High Priority</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
364
  </div>
 
365
  <div class="okr-content">
366
  """]
367
 
 
380
  <div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
381
  <div class="objective-meta">
382
  <div class="meta-item">
383
+ <span class="meta-icon">⏰</span> <span>Timeline: {timeline}</span>
 
384
  </div>
385
  <div class="meta-item">
386
+ <span class="meta-icon">👤</span> <span>Owner: {owner}</span>
 
387
  </div>
388
  </div>
389
  </div>
 
390
  <div class="key-results-container">
391
  """)
392
 
393
  key_results = okr_data.get('key_results', [])
 
394
  if not isinstance(key_results, list) or not key_results:
395
  html_parts.append('<div class="empty-state">No key results defined for this objective.</div>')
396
  else:
397
  for kr_idx, kr_data in enumerate(key_results):
398
  if not isinstance(kr_data, dict):
399
+ logger.warning(f"Key Result item at index {kr_idx} is not a dictionary, skipping.")
400
  continue
401
 
402
  kr_desc = kr_data.get('description', f"Unnamed Key Result {kr_idx + 1}")
 
411
  <div class="kr-title">Key Result {kr_idx + 1}: {kr_desc}</div>
412
  <div class="kr-metrics">
413
  """)
 
414
  if target_metric and target_value:
415
  html_parts.append(f'<div class="kr-metric">Target: {target_metric} → {target_value}</div>')
416
  if kr_type:
417
  html_parts.append(f'<div class="kr-metric">Type: {kr_type}</div>')
418
  if data_subject:
419
  html_parts.append(f'<div class="kr-metric">Data Subject: {data_subject}</div>')
420
+ html_parts.append('</div></div>') # Close kr-metrics and kr-header
421
 
 
 
 
422
  tasks = kr_data.get('tasks', [])
423
  if tasks and isinstance(tasks, list):
424
+ html_parts.append('<div class="tasks-section"><div class="tasks-title"><span>📋</span><span>Associated Tasks</span></div>')
 
 
 
 
 
 
 
425
  for task_idx, task_data in enumerate(tasks):
426
+ if not isinstance(task_data, dict): continue
 
 
 
427
  task_desc = task_data.get('description', f"Unnamed Task {task_idx + 1}")
 
 
428
  priority = task_data.get('priority', 'Medium').lower()
 
 
 
 
429
  priority_class = f"priority-{priority}" if priority in ['high', 'medium', 'low'] else 'priority-medium'
430
+
431
  html_parts.append(f"""
432
+ <div class="task-item">
433
+ <div class="task-header">
434
+ <div class="task-title">{task_idx + 1}. {task_desc}</div>
435
+ <div class="task-priority {priority_class}">{priority.upper()}</div>
 
 
 
 
 
 
 
 
 
 
436
  </div>
437
+ <div class="task-details">
438
+ <div class="task-detail-item"><span class="task-detail-label">Category:</span><span>{task_data.get('category', 'General')}</span></div>
439
+ <div class="task-detail-item"><span class="task-detail-label">Effort:</span><span>{task_data.get('effort', 'N/A')}</span></div>
440
+ <div class="task-detail-item"><span class="task-detail-label">Timeline:</span><span>{task_data.get('timeline', 'N/A')}</span></div>
441
+ <div class="task-detail-item"><span class="task-detail-label">Responsible:</span><span>{task_data.get('responsible_party', 'N/A')}</span></div>
442
  </div>
 
 
 
 
 
443
  """)
444
 
 
 
 
 
 
 
 
445
  detail_lines = []
446
+ if task_data.get('deliverable'): detail_lines.append(f'<strong>Objective/Deliverable:</strong> {task_data.get("deliverable")}')
447
+ if task_data.get('success_criteria_metrics'): detail_lines.append(f'<strong>Success Metrics:</strong> {task_data.get("success_criteria_metrics")}')
448
+ if task_data.get('why'): detail_lines.append(f'<strong>Rationale:</strong> {task_data.get("why")}')
449
+ if task_data.get('priority_justification'): detail_lines.append(f'<strong>Priority Justification:</strong> {task_data.get("priority_justification")}')
450
+ if task_data.get('dependencies'): detail_lines.append(f'<strong>Dependencies:</strong> {task_data.get("dependencies")}')
 
 
 
 
 
451
 
452
  if detail_lines:
453
  html_parts.append('<div class="task-description">')
 
455
  html_parts.append('</div>')
456
 
457
  html_parts.append('</div>') # Close task-item
 
458
  html_parts.append('</div>') # Close tasks-section
459
  else:
460
+ html_parts.append('<div class="tasks-section"><div class="empty-state-small">No tasks defined for this Key Result.</div></div>')
 
 
 
 
 
 
 
 
461
  html_parts.append('</div>') # Close key-result
 
462
  html_parts.append('</div></div>') # Close key-results-container and okr-objective
 
463
  html_parts.append('</div></div>') # Close okr-content and okr-container
 
464
  return ''.join(html_parts)
465
 
466
+
467
  def get_empty_okr_state() -> str:
468
  """
469
  Returns empty state HTML for when no OKRs are available.
 
482
  </div>
483
 
484
  <div class="okr-stats-bar">
485
+ <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Objectives</div></div>
486
+ <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Key Results</div></div>
487
+ <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Tasks</div></div>
488
+ <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">High Priority</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
489
  </div>
490
 
491
  <div class="okr-content">