GuglielmoTor commited on
Commit
d5c5881
·
verified ·
1 Parent(s): 5188732

Update ui/okr_ui_generator.py

Browse files
Files changed (1) hide show
  1. ui/okr_ui_generator.py +193 -210
ui/okr_ui_generator.py CHANGED
@@ -8,51 +8,29 @@ logger = logging.getLogger(__name__)
8
  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 by using a robust CSS variable-based theming approach.
13
- Includes a visual debugger for theme detection.
14
 
15
  Returns:
16
  gr.HTML: The Gradio HTML component that will display the formatted OKRs.
17
  """
18
-
19
- # --- Refactored CSS for Robust Dark Mode Theming ---
20
  okr_custom_css = """
21
  <style>
22
- /* --- Debugger Style --- */
23
- #theme-debug {
24
- position: fixed;
25
- top: 10px;
26
- left: 10px;
27
- padding: 5px 10px;
28
- border-radius: 5px;
29
- font-family: sans-serif;
30
- font-size: 12px;
31
- font-weight: bold;
32
- z-index: 9999;
33
- }
34
-
35
  /* ----------------------------------------- */
36
- /* --- THEME & COLOR VARIABLES --- */
37
  /* ----------------------------------------- */
38
  :root {
39
- /* -- Base & Layout -- */
40
  --okr-bg-start: #667eea;
41
  --okr-bg-end: #764ba2;
42
- --content-bg: white;
43
- --content-shadow: rgba(0,0,0,0.1);
44
-
45
- /* -- Headers & Titles -- */
46
  --header-text-color: white;
47
  --header-text-shadow: rgba(0,0,0,0.1);
48
-
49
- /* -- Stat Cards -- */
50
  --stat-card-bg: rgba(255, 255, 255, 0.15);
51
  --stat-card-bg-hover: rgba(255, 255, 255, 0.2);
52
  --stat-card-border: rgba(255, 255, 255, 0.2);
53
  --stat-number-color: #fbbf24;
54
-
55
- /* -- Objective Cards -- */
56
  --objective-card-bg: #f8fafc;
57
  --objective-card-border: #3b82f6;
58
  --objective-shadow: 0 4px 16px rgba(0,0,0,0.05);
@@ -60,8 +38,6 @@ def create_enhanced_okr_tab():
60
  --objective-header-bg: transparent;
61
  --objective-title-color: #1e40af;
62
  --objective-meta-text-color: #475569;
63
-
64
- /* -- Key Result Cards -- */
65
  --key-result-bg: white;
66
  --key-result-border: #e5e7eb;
67
  --key-result-border-hover: #3b82f6;
@@ -70,8 +46,6 @@ def create_enhanced_okr_tab():
70
  --kr-metric-bg: rgba(59, 130, 246, 0.1);
71
  --kr-metric-color: #1e40af;
72
  --kr-metric-border: rgba(59, 130, 246, 0.2);
73
-
74
- /* -- Task Items -- */
75
  --task-item-bg: #f9fafb;
76
  --task-item-bg-hover: #f3f4f6;
77
  --task-item-border: #e5e7eb;
@@ -82,8 +56,6 @@ def create_enhanced_okr_tab():
82
  --task-description-bg: white;
83
  --task-description-border: #3b82f6;
84
  --task-description-color: #4b5563;
85
-
86
- /* -- Priority Badges -- */
87
  --priority-high-bg: #fef2f2;
88
  --priority-high-color: #dc2626;
89
  --priority-high-border: #fca5a5;
@@ -93,36 +65,29 @@ def create_enhanced_okr_tab():
93
  --priority-low-bg: #f0fdf4;
94
  --priority-low-color: #16a34a;
95
  --priority-low-border: #86efac;
96
-
97
- /* -- Utility -- */
98
- --loading-spinner-bg: #e5e7eb;
99
- --loading-spinner-fg: #3b82f6;
100
  }
101
 
102
  /* ----------------------------------------- */
103
- /* --- DARK MODE VARIABLE OVERRIDES --- */
104
  /* ----------------------------------------- */
105
  html.dark {
106
- /* -- Base & Layout -- */
107
- --content-bg: #0d1117;
108
- --content-shadow: rgba(0,0,0,0.6);
109
-
110
- /* -- Headers & Titles -- */
111
  --header-text-color: #e5e7eb;
112
-
113
- /* -- Stat Cards -- */
114
  --stat-card-bg: rgba(28, 28, 28, 0.7);
115
- --stat-card-border: rgba(50, 50, 50, 0.8);
116
-
117
- /* -- Objective Cards -- */
 
 
118
  --objective-card-bg: #161b22;
119
- --objective-card-border: #3b82f6; /* Keep brand color */
120
  --objective-shadow: 0 8px 24px rgba(0,0,0,0.5);
121
  --objective-shadow-hover: 0 10px 28px rgba(0,0,0,0.6);
 
122
  --objective-title-color: #58a6ff;
123
  --objective-meta-text-color: #8b949e;
124
-
125
- /* -- Key Result Cards -- */
126
  --key-result-bg: #161b22;
127
  --key-result-border: #30363d;
128
  --key-result-border-hover: #58a6ff;
@@ -131,8 +96,6 @@ def create_enhanced_okr_tab():
131
  --kr-metric-bg: rgba(56, 139, 253, 0.15);
132
  --kr-metric-color: #58a6ff;
133
  --kr-metric-border: rgba(56, 139, 253, 0.4);
134
-
135
- /* -- Task Items -- */
136
  --task-item-bg: #21262d;
137
  --task-item-bg-hover: #30363d;
138
  --task-item-border: #30363d;
@@ -143,8 +106,6 @@ def create_enhanced_okr_tab():
143
  --task-description-bg: #0d1117;
144
  --task-description-border: #30363d;
145
  --task-description-color: #8b949e;
146
-
147
- /* -- Priority Badges -- */
148
  --priority-high-bg: rgba(248, 81, 73, 0.15);
149
  --priority-high-color: #ff7b72;
150
  --priority-high-border: rgba(248, 81, 73, 0.4);
@@ -154,31 +115,31 @@ def create_enhanced_okr_tab():
154
  --priority-low-bg: rgba(63, 185, 80, 0.15);
155
  --priority-low-color: #56d364;
156
  --priority-low-border: rgba(63, 185, 80, 0.4);
157
-
158
- /* -- Utility -- */
159
- --loading-spinner-bg: #30363d;
160
- --loading-spinner-fg: #58a6ff;
161
  }
162
 
163
  /* ----------------------------------------- */
164
  /* --- BASE STYLES (Uses Variables) --- */
165
  /* ----------------------------------------- */
166
  .okr-container {
167
- font-family: 'Inter', sans-serif;
168
  background: linear-gradient(135deg, var(--okr-bg-start) 0%, var(--okr-bg-end) 100%);
169
- min-height: 100vh; padding: 2rem; margin: -1rem; box-sizing: border-box; overflow-y: auto;
 
 
 
 
 
170
  }
171
  .okr-header { text-align: center; margin-bottom: 3rem; color: var(--header-text-color); }
172
- .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; }
173
- .okr-title-content { background: linear-gradient(45deg, #ffffff, #e0e7ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
174
- html.dark .okr-title-content { background: linear-gradient(45deg, #e5e7eb, #9ca3af); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
175
  .okr-title-emoji { font-size: 2.5rem; -webkit-text-fill-color: initial; }
176
- .okr-subtitle { font-size: 1.2rem; opacity: 0.9; font-weight: 300; letter-spacing: 0.5px; }
177
  .okr-stats-bar { display: flex; justify-content: center; gap: 2rem; margin: 2rem 0; flex-wrap: wrap; }
178
  .stat-card { background: var(--stat-card-bg); backdrop-filter: blur(10px); border: 1px solid var(--stat-card-border); border-radius: 16px; padding: 1.5rem; text-align: center; color: var(--header-text-color); min-width: 140px; transition: all 0.3s ease; }
179
  .stat-card:hover { transform: translateY(-2px); background: var(--stat-card-bg-hover); box-shadow: 0 8px 32px var(--content-shadow); }
180
  .stat-number { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; color: var(--stat-number-color); }
181
- .stat-label { font-size: 0.9rem; opacity: 0.9; text-transform: uppercase; letter-spacing: 1px; }
182
  .okr-content { background: var(--content-bg); border-radius: 24px; padding: 0; box-shadow: 0 20px 40px var(--content-shadow); overflow: hidden; margin-top: 2rem; }
183
  .okr-objective { background: var(--objective-card-bg); border-left: 6px solid var(--objective-card-border); margin: 2rem; border-radius: 16px; overflow: hidden; box-shadow: var(--objective-shadow); transition: all 0.3s ease; }
184
  .okr-objective:hover { transform: translateY(-2px); box-shadow: var(--objective-shadow-hover); }
@@ -211,85 +172,100 @@ def create_enhanced_okr_tab():
211
  .empty-state { text-align: center; padding: 4rem 2rem; color: var(--task-detail-text-color); }
212
  .empty-state-icon { font-size: 3rem; margin-bottom: 1rem; }
213
  .empty-state-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--objective-title-color); }
214
- .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; }
215
  @keyframes spin { to { transform: rotate(360deg); } }
216
-
217
- /* Responsive Adjustments */
218
- @media (max-width: 768px) {
219
- .okr-container { padding: 1rem; }
220
- .okr-title { font-size: 2rem; }
221
- .okr-stats-bar { gap: 1rem; }
222
- .stat-card { min-width: 120px; padding: 1rem; }
223
- .objective-meta { flex-direction: column; gap: 1rem; }
224
- .task-details { grid-template-columns: 1fr; }
225
- .task-header { flex-direction: column; align-items: flex-start; }
226
- }
227
  </style>
228
  <script>
229
- // This script is responsible for syncing the theme with the main Gradio app.
230
- // It now includes a visual debugger to confirm its status.
231
- function syncThemeWithParent() {
232
- try {
233
- const parentHtml = window.parent.document.querySelector('html');
234
- const iframeHtml = document.documentElement;
235
- const debugDiv = document.getElementById('theme-debug');
 
 
 
 
236
 
237
- if (parentHtml && iframeHtml) {
238
- const isDark = parentHtml.classList.contains('dark');
239
- if (isDark) {
240
- iframeHtml.classList.add('dark');
 
 
 
 
241
  } else {
242
- iframeHtml.classList.remove('dark');
243
- }
244
-
245
- // Update the visual debugger
246
- if (debugDiv) {
247
- if (isDark) {
248
- debugDiv.textContent = 'Dark';
249
- debugDiv.style.backgroundColor = '#3b82f6'; // Blue
250
- debugDiv.style.color = 'white';
251
- } else {
252
- debugDiv.textContent = 'Light';
253
- debugDiv.style.backgroundColor = '#dc2626'; // Red
254
- debugDiv.style.color = 'white';
255
- }
256
  }
257
- }
258
- } catch (e) {
259
- console.error("Theme Sync Error:", e);
260
- const debugDiv = document.getElementById('theme-debug');
261
- if (debugDiv) {
262
- debugDiv.textContent = 'Error';
263
- debugDiv.style.backgroundColor = 'yellow';
264
- debugDiv.style.color = 'black';
265
  }
266
  }
267
- }
268
-
269
- // Use a more reliable polling mechanism instead of MutationObserver for wider compatibility.
270
- setInterval(syncThemeWithParent, 250);
271
 
272
- // Run on load.
273
- document.addEventListener('DOMContentLoaded', syncThemeWithParent);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  </script>
275
  """
276
-
277
  with gr.Column(elem_classes=["okr-root-column"]):
278
- # Inject custom CSS and the theme-syncing JS
279
  gr.HTML(okr_custom_css)
280
-
281
- # Main OKR display area
282
  okr_display_html = gr.HTML(
283
  value=get_initial_okr_display(),
284
  elem_classes=["okr-display"]
285
  )
286
-
287
  return okr_display_html
288
 
289
  def get_initial_okr_display() -> str:
290
- """Returns the initial HTML display for the OKR tab, showing a loading state."""
 
 
 
 
291
  return """
292
- <div id="theme-debug">Loading...</div>
293
  <div class="okr-container">
294
  <div class="okr-header">
295
  <div class="okr-title">
@@ -298,14 +274,12 @@ def get_initial_okr_display() -> str:
298
  </div>
299
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
300
  </div>
301
-
302
  <div class="okr-stats-bar">
303
  <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Objectives</div></div>
304
  <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Key Results</div></div>
305
  <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Tasks</div></div>
306
  <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">High Priority</div></div>
307
  </div>
308
-
309
  <div class="okr-content">
310
  <div class="empty-state">
311
  <div class="empty-state-icon">⏳</div>
@@ -319,45 +293,84 @@ def get_initial_okr_display() -> str:
319
  </div>
320
  """
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
323
- """Enhanced formatting function that creates beautiful HTML for OKR display."""
 
 
 
 
 
 
 
324
  if not reconstruction_cache:
325
  logger.warning("No reconstruction cache found for display.")
326
  return get_empty_okr_state()
327
-
328
- # (Code to extract actionable_okrs... remains the same)
 
329
  for report_id, report_data in reconstruction_cache.items():
330
  if isinstance(report_data, dict) and 'actionable_okrs' in report_data:
331
- raw_results = {'actionable_okrs': report_data['actionable_okrs']}
332
  break
333
- else:
 
334
  logger.warning("No 'actionable_okrs' found in reconstruction cache for display.")
335
  return get_empty_okr_state()
336
 
337
- actionable_okrs = raw_results.get("actionable_okrs", {})
338
  okrs_list = actionable_okrs.get("okrs", [])
339
-
340
  if not okrs_list:
341
  logger.info("No OKRs found in 'actionable_okrs' list.")
342
  return get_empty_okr_state()
343
 
344
- # (Code to calculate stats... remains the same)
345
  total_objectives = len(okrs_list)
346
- total_key_results = sum(len(okr.get('key_results', [])) for okr in okrs_list)
347
- total_tasks = sum(len(kr.get('tasks', [])) for okr in okrs_list for kr in okr.get('key_results', []))
348
- high_priority_tasks = sum(1 for okr in okrs_list for kr in okr.get('key_results', []) for task in kr.get('tasks', []) if task.get('priority', '').lower() == 'high')
349
-
350
- # Start HTML with the debugger div
351
- html_parts = ["""<div id="theme-debug"></div>"""]
352
-
353
- # --- The rest of the HTML generation logic remains identical ---
354
- html_parts.append(f"""
 
 
 
 
 
355
  <div class="okr-container">
356
  <div class="okr-header">
357
- <div class="okr-title">
358
- <span class="okr-title-emoji">🎯</span>
359
- <span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span>
360
- </div>
361
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
362
  </div>
363
  <div class="okr-stats-bar">
@@ -367,40 +380,33 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
367
  <div class="stat-card"><div class="stat-number">{high_priority_tasks}</div><div class="stat-label">High Priority</div></div>
368
  </div>
369
  <div class="okr-content">
370
- """)
371
 
 
372
  for okr_idx, okr_data in enumerate(okrs_list):
373
- if not isinstance(okr_data, dict):
374
- logger.warning(f"OKR item at index {okr_idx} is not a dictionary, skipping.")
375
- continue
376
-
377
  objective = okr_data.get('description', f"Unnamed Objective {okr_idx + 1}")
378
- timeline = okr_data.get('timeline', 'Not specified')
379
- owner = okr_data.get('owner', 'Not assigned')
380
-
381
  html_parts.append(f"""
382
  <div class="okr-objective">
383
  <div class="objective-header">
384
  <div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
385
  <div class="objective-meta">
386
- <div class="meta-item">
387
- <span class="meta-icon">⏰</span> <span>Timeline: {timeline}</span>
388
- </div>
389
- <div class="meta-item">
390
- <span class="meta-icon">👤</span> <span>Owner: {owner}</span>
391
- </div>
392
  </div>
393
  </div>
394
  <div class="key-results-container">
395
  """)
396
 
397
  key_results = okr_data.get('key_results', [])
398
- if not isinstance(key_results, list) or not key_results:
399
- html_parts.append('<div class="empty-state">No key results defined for this objective.</div>')
400
  else:
 
401
  for kr_idx, kr_data in enumerate(key_results):
402
  if not isinstance(kr_data, dict): continue
403
-
404
  kr_desc = kr_data.get('description', f"Unnamed Key Result {kr_idx + 1}")
405
  html_parts.append(f"""
406
  <div class="key-result">
@@ -414,16 +420,18 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
414
  html_parts.append(f'<div class="kr-metric">Type: {kr_data.get("key_result_type")}</div>')
415
  if kr_data.get('data_subject'):
416
  html_parts.append(f'<div class="kr-metric">Data Subject: {kr_data.get("data_subject")}</div>')
417
- html_parts.append('</div></div>')
418
 
419
  tasks = kr_data.get('tasks', [])
420
- if tasks and isinstance(tasks, list):
421
- html_parts.append('<div class="tasks-section"><div class="tasks-title"><span>📋</span><span>Associated Tasks</span></div>')
 
 
422
  for task_idx, task_data in enumerate(tasks):
423
  if not isinstance(task_data, dict): continue
424
  task_desc = task_data.get('description', f"Unnamed Task {task_idx + 1}")
425
  priority = task_data.get('priority', 'Medium').lower()
426
- priority_class = f"priority-{priority}" if priority in ['high', 'medium', 'low'] else 'priority-medium'
427
  html_parts.append(f"""
428
  <div class="task-item">
429
  <div class="task-header">
@@ -431,51 +439,26 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
431
  <div class="task-priority {priority_class}">{priority.upper()}</div>
432
  </div>
433
  <div class="task-details">
434
- <div class="task-detail-item"><span class="task-detail-label">Category:</span><span>{task_data.get('category', 'General')}</span></div>
435
  <div class="task-detail-item"><span class="task-detail-label">Effort:</span><span>{task_data.get('effort', 'N/A')}</span></div>
436
  <div class="task-detail-item"><span class="task-detail-label">Timeline:</span><span>{task_data.get('timeline', 'N/A')}</span></div>
437
  <div class="task-detail-item"><span class="task-detail-label">Responsible:</span><span>{task_data.get('responsible_party', 'N/A')}</span></div>
438
- </div>
439
- """)
440
- detail_lines = [f'<strong>{k.replace("_", " ").title()}:</strong> {v}' for k, v in task_data.items() if k in ['deliverable', 'success_criteria_metrics', 'why', 'priority_justification', 'dependencies'] and v]
 
 
 
 
 
 
441
  if detail_lines:
442
  html_parts.append(f'<div class="task-description">{"<br>".join(detail_lines)}</div>')
443
- html_parts.append('</div>')
444
- html_parts.append('</div>')
445
  else:
446
- html_parts.append('<div class="tasks-section"><div class="empty-state-small">No tasks defined.</div></div>')
447
- html_parts.append('</div>')
448
- html_parts.append('</div></div>')
449
- html_parts.append('</div></div>')
450
- return ''.join(html_parts)
451
 
452
- def get_empty_okr_state() -> str:
453
- """Returns empty state HTML for when no OKRs are available."""
454
- return """
455
- <div id="theme-debug"></div>
456
- <div class="okr-container">
457
- <div class="okr-header">
458
- <div class="okr-title">
459
- <span class="okr-title-emoji">🎯</span>
460
- <span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span>
461
- </div>
462
- <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
463
- </div>
464
- <div class="okr-stats-bar">
465
- <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Objectives</div></div>
466
- <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Key Results</div></div>
467
- <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Tasks</div></div>
468
- <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">High Priority</div></div>
469
- </div>
470
- <div class="okr-content">
471
- <div class="empty-state">
472
- <div class="empty-state-icon">📋</div>
473
- <div class="empty-state-title">No OKRs Available</div>
474
- <div class="empty-state-description">
475
- OKR analysis has not been generated yet or no data is available.<br>
476
- Please ensure your LinkedIn data has been loaded and the AI analysis has completed.
477
- </div>
478
- </div>
479
- </div>
480
- </div>
481
- """
 
8
  def create_enhanced_okr_tab():
9
  """
10
  Creates a modern, visually appealing OKR tab with improved layout and styling.
11
+ This version includes robust support for Gradio's dark mode with multiple
12
+ detection methods and fallback mechanisms.
 
13
 
14
  Returns:
15
  gr.HTML: The Gradio HTML component that will display the formatted OKRs.
16
  """
17
+ # Enhanced CSS for modern OKR styling with improved Dark Mode support
 
18
  okr_custom_css = """
19
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  /* ----------------------------------------- */
21
+ /* --- LIGHT MODE THEME & COLOR VARIABLES --- */
22
  /* ----------------------------------------- */
23
  :root {
 
24
  --okr-bg-start: #667eea;
25
  --okr-bg-end: #764ba2;
 
 
 
 
26
  --header-text-color: white;
27
  --header-text-shadow: rgba(0,0,0,0.1);
 
 
28
  --stat-card-bg: rgba(255, 255, 255, 0.15);
29
  --stat-card-bg-hover: rgba(255, 255, 255, 0.2);
30
  --stat-card-border: rgba(255, 255, 255, 0.2);
31
  --stat-number-color: #fbbf24;
32
+ --content-bg: white;
33
+ --content-shadow: rgba(0,0,0,0.1);
34
  --objective-card-bg: #f8fafc;
35
  --objective-card-border: #3b82f6;
36
  --objective-shadow: 0 4px 16px rgba(0,0,0,0.05);
 
38
  --objective-header-bg: transparent;
39
  --objective-title-color: #1e40af;
40
  --objective-meta-text-color: #475569;
 
 
41
  --key-result-bg: white;
42
  --key-result-border: #e5e7eb;
43
  --key-result-border-hover: #3b82f6;
 
46
  --kr-metric-bg: rgba(59, 130, 246, 0.1);
47
  --kr-metric-color: #1e40af;
48
  --kr-metric-border: rgba(59, 130, 246, 0.2);
 
 
49
  --task-item-bg: #f9fafb;
50
  --task-item-bg-hover: #f3f4f6;
51
  --task-item-border: #e5e7eb;
 
56
  --task-description-bg: white;
57
  --task-description-border: #3b82f6;
58
  --task-description-color: #4b5563;
 
 
59
  --priority-high-bg: #fef2f2;
60
  --priority-high-color: #dc2626;
61
  --priority-high-border: #fca5a5;
 
65
  --priority-low-bg: #f0fdf4;
66
  --priority-low-color: #16a34a;
67
  --priority-low-border: #86efac;
 
 
 
 
68
  }
69
 
70
  /* ----------------------------------------- */
71
+ /* --- DARK MODE COLOR VARIABLES --- */
72
  /* ----------------------------------------- */
73
  html.dark {
74
+ --okr-bg-start: #1a1a2e;
75
+ --okr-bg-end: #16213e;
 
 
 
76
  --header-text-color: #e5e7eb;
77
+ --header-text-shadow: rgba(0,0,0,0.5);
 
78
  --stat-card-bg: rgba(28, 28, 28, 0.7);
79
+ --stat-card-bg-hover: rgba(40, 40, 40, 0.8);
80
+ --stat-card-border: rgba(60, 60, 60, 0.8);
81
+ --stat-number-color: #fbbf24;
82
+ --content-bg: #0d1117;
83
+ --content-shadow: rgba(0,0,0,0.6);
84
  --objective-card-bg: #161b22;
85
+ --objective-card-border: #58a6ff;
86
  --objective-shadow: 0 8px 24px rgba(0,0,0,0.5);
87
  --objective-shadow-hover: 0 10px 28px rgba(0,0,0,0.6);
88
+ --objective-header-bg: transparent;
89
  --objective-title-color: #58a6ff;
90
  --objective-meta-text-color: #8b949e;
 
 
91
  --key-result-bg: #161b22;
92
  --key-result-border: #30363d;
93
  --key-result-border-hover: #58a6ff;
 
96
  --kr-metric-bg: rgba(56, 139, 253, 0.15);
97
  --kr-metric-color: #58a6ff;
98
  --kr-metric-border: rgba(56, 139, 253, 0.4);
 
 
99
  --task-item-bg: #21262d;
100
  --task-item-bg-hover: #30363d;
101
  --task-item-border: #30363d;
 
106
  --task-description-bg: #0d1117;
107
  --task-description-border: #30363d;
108
  --task-description-color: #8b949e;
 
 
109
  --priority-high-bg: rgba(248, 81, 73, 0.15);
110
  --priority-high-color: #ff7b72;
111
  --priority-high-border: rgba(248, 81, 73, 0.4);
 
115
  --priority-low-bg: rgba(63, 185, 80, 0.15);
116
  --priority-low-color: #56d364;
117
  --priority-low-border: rgba(63, 185, 80, 0.4);
 
 
 
 
118
  }
119
 
120
  /* ----------------------------------------- */
121
  /* --- BASE STYLES (Uses Variables) --- */
122
  /* ----------------------------------------- */
123
  .okr-container {
124
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
125
  background: linear-gradient(135deg, var(--okr-bg-start) 0%, var(--okr-bg-end) 100%);
126
+ min-height: 100vh;
127
+ padding: 2rem;
128
+ margin: -1rem;
129
+ box-sizing: border-box;
130
+ overflow-y: auto;
131
+ color: var(--header-text-color);
132
  }
133
  .okr-header { text-align: center; margin-bottom: 3rem; color: var(--header-text-color); }
134
+ .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; color: var(--header-text-color); }
135
+ .okr-title-content { background: linear-gradient(45deg, var(--header-text-color), rgba(255,255,255,0.8)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; color: var(--header-text-color); }
 
136
  .okr-title-emoji { font-size: 2.5rem; -webkit-text-fill-color: initial; }
137
+ .okr-subtitle { font-size: 1.2rem; opacity: 0.9; font-weight: 300; letter-spacing: 0.5px; color: var(--header-text-color); }
138
  .okr-stats-bar { display: flex; justify-content: center; gap: 2rem; margin: 2rem 0; flex-wrap: wrap; }
139
  .stat-card { background: var(--stat-card-bg); backdrop-filter: blur(10px); border: 1px solid var(--stat-card-border); border-radius: 16px; padding: 1.5rem; text-align: center; color: var(--header-text-color); min-width: 140px; transition: all 0.3s ease; }
140
  .stat-card:hover { transform: translateY(-2px); background: var(--stat-card-bg-hover); box-shadow: 0 8px 32px var(--content-shadow); }
141
  .stat-number { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; color: var(--stat-number-color); }
142
+ .stat-label { font-size: 0.9rem; opacity: 0.9; text-transform: uppercase; letter-spacing: 1px; color: var(--header-text-color); }
143
  .okr-content { background: var(--content-bg); border-radius: 24px; padding: 0; box-shadow: 0 20px 40px var(--content-shadow); overflow: hidden; margin-top: 2rem; }
144
  .okr-objective { background: var(--objective-card-bg); border-left: 6px solid var(--objective-card-border); margin: 2rem; border-radius: 16px; overflow: hidden; box-shadow: var(--objective-shadow); transition: all 0.3s ease; }
145
  .okr-objective:hover { transform: translateY(-2px); box-shadow: var(--objective-shadow-hover); }
 
172
  .empty-state { text-align: center; padding: 4rem 2rem; color: var(--task-detail-text-color); }
173
  .empty-state-icon { font-size: 3rem; margin-bottom: 1rem; }
174
  .empty-state-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--objective-title-color); }
175
+ .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; }
176
  @keyframes spin { to { transform: rotate(360deg); } }
177
+ @media (max-width: 768px) { .okr-container { padding: 1rem; } .okr-title { font-size: 2rem; } .okr-stats-bar { gap: 1rem; } .stat-card { min-width: 120px; padding: 1rem; } .objective-meta { flex-direction: column; gap: 1rem; } .task-details { grid-template-columns: 1fr; } .task-header { flex-direction: column; align-items: flex-start; } }
178
+ @supports (-webkit-appearance: none) { .okr-title-content { color: var(--header-text-color) !important; } }
 
 
 
 
 
 
 
 
 
179
  </style>
180
  <script>
181
+ (function() {
182
+ 'use strict';
183
+ // Function to apply the theme to the current document's HTML element
184
+ function applyTheme(theme) {
185
+ const htmlEl = document.documentElement;
186
+ if (theme === 'dark') {
187
+ htmlEl.classList.add('dark');
188
+ } else {
189
+ htmlEl.classList.remove('dark');
190
+ }
191
+ }
192
 
193
+ // Function to detect the theme from the parent Gradio app
194
+ function detectAndApplyTheme() {
195
+ try {
196
+ // Access the parent document where Gradio's theme class is set
197
+ const parentHtml = window.parent.document.querySelector('html');
198
+ if (parentHtml) {
199
+ const isDark = parentHtml.classList.contains('dark');
200
+ applyTheme(isDark ? 'dark' : 'light');
201
  } else {
202
+ // Fallback for system preference if parent is not accessible
203
+ const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
204
+ applyTheme(prefersDark ? 'dark' : 'light');
 
 
 
 
 
 
 
 
 
 
 
205
  }
206
+ } catch (e) {
207
+ console.warn('OKR Tab: Could not access parent theme. Falling back to system preference.', e.message);
208
+ // Fallback for system preference in case of cross-origin issues
209
+ const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
210
+ applyTheme(prefersDark ? 'dark' : 'light');
 
 
 
211
  }
212
  }
 
 
 
 
213
 
214
+ // Use a MutationObserver to watch for theme changes on the parent
215
+ function observeThemeChanges() {
216
+ try {
217
+ const parentHtml = window.parent.document.querySelector('html');
218
+ if (!parentHtml) return;
219
+
220
+ const observer = new MutationObserver(mutations => {
221
+ mutations.forEach(mutation => {
222
+ if (mutation.attributeName === 'class') {
223
+ detectAndApplyTheme();
224
+ }
225
+ });
226
+ });
227
+
228
+ observer.observe(parentHtml, {
229
+ attributes: true,
230
+ attributeFilter: ['class']
231
+ });
232
+ } catch (e) {
233
+ console.warn('OKR Tab: Cannot observe parent theme changes.', e.message);
234
+ }
235
+ }
236
+
237
+ // Initial check when the script loads
238
+ if (document.readyState === 'loading') {
239
+ document.addEventListener('DOMContentLoaded', () => {
240
+ detectAndApplyTheme();
241
+ observeThemeChanges();
242
+ });
243
+ } else {
244
+ detectAndApplyTheme();
245
+ observeThemeChanges();
246
+ }
247
+ // Also listen for system theme changes as a fallback
248
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', detectAndApplyTheme);
249
+ })();
250
  </script>
251
  """
 
252
  with gr.Column(elem_classes=["okr-root-column"]):
253
+ # Inject custom CSS and the enhanced theme-syncing JS
254
  gr.HTML(okr_custom_css)
255
+ # Main OKR display area with enhanced styling
 
256
  okr_display_html = gr.HTML(
257
  value=get_initial_okr_display(),
258
  elem_classes=["okr-display"]
259
  )
 
260
  return okr_display_html
261
 
262
  def get_initial_okr_display() -> str:
263
+ """
264
+ Returns the initial HTML display for the OKR tab, showing a loading state.
265
+ Returns:
266
+ str: HTML string for the initial OKR display.
267
+ """
268
  return """
 
269
  <div class="okr-container">
270
  <div class="okr-header">
271
  <div class="okr-title">
 
274
  </div>
275
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
276
  </div>
 
277
  <div class="okr-stats-bar">
278
  <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Objectives</div></div>
279
  <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Key Results</div></div>
280
  <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Tasks</div></div>
281
  <div class="stat-card"><div class="stat-number">-</div><div class="stat-label">High Priority</div></div>
282
  </div>
 
283
  <div class="okr-content">
284
  <div class="empty-state">
285
  <div class="empty-state-icon">⏳</div>
 
293
  </div>
294
  """
295
 
296
+ def get_empty_okr_state() -> str:
297
+ """Returns empty state HTML for when no OKRs are available, using the new styles."""
298
+ return """
299
+ <div class="okr-container">
300
+ <div class="okr-header">
301
+ <div class="okr-title">
302
+ <span class="okr-title-emoji">🎯</span>
303
+ <span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span>
304
+ </div>
305
+ <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
306
+ </div>
307
+ <div class="okr-stats-bar">
308
+ <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Objectives</div></div>
309
+ <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Key Results</div></div>
310
+ <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Tasks</div></div>
311
+ <div class="stat-card"><div class="stat-number">0</div><div class="stat-label">High Priority</div></div>
312
+ </div>
313
+ <div class="okr-content">
314
+ <div class="empty-state">
315
+ <div class="empty-state-icon">📋</div>
316
+ <div class="empty-state-title">No OKRs Available</div>
317
+ <div class="empty-state-description">
318
+ OKR analysis has not been generated yet or no data is available.<br>
319
+ Please ensure your LinkedIn data has been loaded and the AI analysis has completed.
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ """
325
+
326
  def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
327
+ """
328
+ Enhanced formatting function that creates beautiful HTML for OKR display
329
+ from the reconstruction cache dictionary, compatible with new styling.
330
+ Args:
331
+ reconstruction_cache (dict): The dictionary containing 'actionable_okrs'.
332
+ Returns:
333
+ str: A comprehensive HTML string representing the OKRs, or an empty state HTML.
334
+ """
335
  if not reconstruction_cache:
336
  logger.warning("No reconstruction cache found for display.")
337
  return get_empty_okr_state()
338
+
339
+ actionable_okrs = {}
340
+ # Extract actionable_okrs from the cache
341
  for report_id, report_data in reconstruction_cache.items():
342
  if isinstance(report_data, dict) and 'actionable_okrs' in report_data:
343
+ actionable_okrs = report_data['actionable_okrs']
344
  break
345
+
346
+ if not actionable_okrs:
347
  logger.warning("No 'actionable_okrs' found in reconstruction cache for display.")
348
  return get_empty_okr_state()
349
 
 
350
  okrs_list = actionable_okrs.get("okrs", [])
 
351
  if not okrs_list:
352
  logger.info("No OKRs found in 'actionable_okrs' list.")
353
  return get_empty_okr_state()
354
 
355
+ # Calculate statistics for the stats bar
356
  total_objectives = len(okrs_list)
357
+ total_key_results = sum(len(okr.get('key_results', [])) for okr in okrs_list if isinstance(okr, dict))
358
+ total_tasks = sum(
359
+ len(kr.get('tasks', []))
360
+ for okr in okrs_list if isinstance(okr, dict)
361
+ for kr in okr.get('key_results', []) if isinstance(kr, dict)
362
+ )
363
+ high_priority_tasks = sum(
364
+ 1 for okr in okrs_list if isinstance(okr, dict)
365
+ for kr in okr.get('key_results', []) if isinstance(kr, dict)
366
+ for task in kr.get('tasks', []) if isinstance(task, dict) and task.get('priority', '').lower() == 'high'
367
+ )
368
+
369
+ # --- Start HTML Generation ---
370
+ html_parts = [f"""
371
  <div class="okr-container">
372
  <div class="okr-header">
373
+ <div class="okr-title"><span class="okr-title-emoji">🎯</span><span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span></div>
 
 
 
374
  <div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
375
  </div>
376
  <div class="okr-stats-bar">
 
380
  <div class="stat-card"><div class="stat-number">{high_priority_tasks}</div><div class="stat-label">High Priority</div></div>
381
  </div>
382
  <div class="okr-content">
383
+ """]
384
 
385
+ # --- Loop through Objectives ---
386
  for okr_idx, okr_data in enumerate(okrs_list):
387
+ if not isinstance(okr_data, dict): continue
 
 
 
388
  objective = okr_data.get('description', f"Unnamed Objective {okr_idx + 1}")
389
+ timeline = okr_data.get('timeline', 'N/A')
390
+ owner = okr_data.get('owner', 'N/A')
 
391
  html_parts.append(f"""
392
  <div class="okr-objective">
393
  <div class="objective-header">
394
  <div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
395
  <div class="objective-meta">
396
+ <div class="meta-item"><span class="meta-icon">⏰</span> <span>Timeline: {timeline}</span></div>
397
+ <div class="meta-item"><span class="meta-icon">👤</span> <span>Owner: {owner}</span></div>
 
 
 
 
398
  </div>
399
  </div>
400
  <div class="key-results-container">
401
  """)
402
 
403
  key_results = okr_data.get('key_results', [])
404
+ if not key_results:
405
+ html_parts.append('<div class="empty-state" style="padding: 2rem;"><div class="empty-state-title" style="font-size: 1.1rem;">No Key Results</div><div class="empty-state-description">No key results defined for this objective.</div></div>')
406
  else:
407
+ # --- Loop through Key Results ---
408
  for kr_idx, kr_data in enumerate(key_results):
409
  if not isinstance(kr_data, dict): continue
 
410
  kr_desc = kr_data.get('description', f"Unnamed Key Result {kr_idx + 1}")
411
  html_parts.append(f"""
412
  <div class="key-result">
 
420
  html_parts.append(f'<div class="kr-metric">Type: {kr_data.get("key_result_type")}</div>')
421
  if kr_data.get('data_subject'):
422
  html_parts.append(f'<div class="kr-metric">Data Subject: {kr_data.get("data_subject")}</div>')
423
+ html_parts.append('</div></div>') # Close kr-metrics & kr-header
424
 
425
  tasks = kr_data.get('tasks', [])
426
+ html_parts.append('<div class="tasks-section">')
427
+ if tasks:
428
+ html_parts.append('<div class="tasks-title"><span>📋</span><span>Associated Tasks</span></div>')
429
+ # --- Loop through Tasks ---
430
  for task_idx, task_data in enumerate(tasks):
431
  if not isinstance(task_data, dict): continue
432
  task_desc = task_data.get('description', f"Unnamed Task {task_idx + 1}")
433
  priority = task_data.get('priority', 'Medium').lower()
434
+ priority_class = f"priority-{priority}"
435
  html_parts.append(f"""
436
  <div class="task-item">
437
  <div class="task-header">
 
439
  <div class="task-priority {priority_class}">{priority.upper()}</div>
440
  </div>
441
  <div class="task-details">
442
+ <div class="task-detail-item"><span class="task-detail-label">Category:</span><span>{task_data.get('category', 'N/A')}</span></div>
443
  <div class="task-detail-item"><span class="task-detail-label">Effort:</span><span>{task_data.get('effort', 'N/A')}</span></div>
444
  <div class="task-detail-item"><span class="task-detail-label">Timeline:</span><span>{task_data.get('timeline', 'N/A')}</span></div>
445
  <div class="task-detail-item"><span class="task-detail-label">Responsible:</span><span>{task_data.get('responsible_party', 'N/A')}</span></div>
446
+ </div>""")
447
+ detail_items = {
448
+ 'Deliverable': task_data.get('deliverable'),
449
+ 'Success Metrics': task_data.get('success_criteria_metrics'),
450
+ 'Why': task_data.get('why'),
451
+ 'Priority Justification': task_data.get('priority_justification'),
452
+ 'Dependencies': task_data.get('dependencies')
453
+ }
454
+ detail_lines = [f'<strong>{k}:</strong> {v}' for k, v in detail_items.items() if v]
455
  if detail_lines:
456
  html_parts.append(f'<div class="task-description">{"<br>".join(detail_lines)}</div>')
457
+ html_parts.append('</div>') # Close task-item
 
458
  else:
459
+ html_parts.append('<div style="text-align:center; padding: 1rem; color: var(--task-detail-text-color);">No tasks defined for this key result.</div>')
460
+ html_parts.append('</div></div>') # Close tasks-section & key-result
461
+ html_parts.append('</div></div>') # Close key-results-container & okr-objective
 
 
462
 
463
+ html_parts.append('</div></div>') # Close okr-content & okr-container
464
+ return ''.join(html_parts)