GuglielmoTor commited on
Commit
f56df65
Β·
verified Β·
1 Parent(s): 708a26e

Create okr_ui_generator.py

Browse files
Files changed (1) hide show
  1. ui/okr_ui_generator.py +691 -0
ui/okr_ui_generator.py ADDED
@@ -0,0 +1,691 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #okr_ui_generator.py
2
+ import gradio as gr
3
+ from typing import Dict, Any, List, Optional
4
+ import pandas as pd
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
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;
37
+ margin-bottom: 0.5rem;
38
+ background: linear-gradient(45deg, #ffffff, #e0e7ff);
39
+ -webkit-background-clip: text;
40
+ -webkit-text-fill-color: transparent;
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;
55
+ gap: 2rem;
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);
63
+ border: 1px solid rgba(255, 255, 255, 0.2);
64
+ border-radius: 16px;
65
+ padding: 1.5rem;
66
+ text-align: center;
67
+ color: white;
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;
127
+ top: 0;
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;
139
+ margin-bottom: 0.75rem;
140
+ position: relative;
141
+ z-index: 1;
142
+ }
143
+
144
+ .objective-meta {
145
+ display: flex;
146
+ gap: 2rem;
147
+ margin-top: 1rem;
148
+ flex-wrap: wrap;
149
+ position: relative;
150
+ z-index: 1;
151
+ }
152
+
153
+ .meta-item {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 0.5rem;
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;
246
+ align-items: flex-start;
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;
261
+ font-size: 0.75rem;
262
+ font-weight: 600;
263
+ text-transform: uppercase;
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;
380
+ }
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>
409
+ <div class="stat-label">Objectives</div>
410
+ </div>
411
+ <div class="stat-card">
412
+ <div class="stat-number">-</div>
413
+ <div class="stat-label">Key Results</div>
414
+ </div>
415
+ <div class="stat-card">
416
+ <div class="stat-number">-</div>
417
+ <div class="stat-label">Tasks</div>
418
+ </div>
419
+ <div class="stat-card">
420
+ <div class="stat-number">-</div>
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>
428
+ <div class="empty-state-title">Loading OKR Analysis</div>
429
+ <div class="empty-state-description">
430
+ <div class="loading-spinner"></div>
431
+ Generating intelligent objectives and actionable tasks from your LinkedIn data...
432
+ </div>
433
+ </div>
434
+ </div>
435
+ </div>
436
+ """
437
+
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>
480
+ <div class="stat-label">Objectives</div>
481
+ </div>
482
+ <div class="stat-card">
483
+ <div class="stat-number">{total_key_results}</div>
484
+ <div class="stat-label">Key Results</div>
485
+ </div>
486
+ <div class="stat-card">
487
+ <div class="stat-number">{total_tasks}</div>
488
+ <div class="stat-label">Tasks</div>
489
+ </div>
490
+ <div class="stat-card">
491
+ <div class="stat-number">{high_priority_tasks}</div>
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>
664
+ <div class="stat-label">Objectives</div>
665
+ </div>
666
+ <div class="stat-card">
667
+ <div class="stat-number">0</div>
668
+ <div class="stat-label">Key Results</div>
669
+ </div>
670
+ <div class="stat-card">
671
+ <div class="stat-number">0</div>
672
+ <div class="stat-label">Tasks</div>
673
+ </div>
674
+ <div class="stat-card">
675
+ <div class="stat-number">0</div>
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>
683
+ <div class="empty-state-title">No OKRs Available</div>
684
+ <div class="empty-state-description">
685
+ OKR analysis has not been generated yet or no data is available.<br>
686
+ Please ensure your LinkedIn data has been loaded and the AI analysis has completed.
687
+ </div>
688
+ </div>
689
+ </div>
690
+ </div>
691
+ """