Spaces:
Running
Running
Update ui/okr_ui_generator.py
Browse files- 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 |
-
#
|
|
|
|
|
|
|
19 |
okr_custom_css = """
|
20 |
<style>
|
21 |
/* ----------------------------------------- */
|
22 |
-
/* ---
|
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 |
-
|
34 |
-
--
|
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(--
|
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 |
-
|
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 |
-
//
|
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
|
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 |
-
|
244 |
-
|
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 |
-
|
334 |
-
|
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
|
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 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
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-
|
456 |
-
<span class="task-detail-label">
|
457 |
-
<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
|
475 |
-
|
476 |
-
if
|
477 |
-
|
478 |
-
if
|
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 |
-
|
531 |
-
|
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">
|