Spaces:
Running
Running
Update ui/okr_ui_generator.py
Browse files- ui/okr_ui_generator.py +78 -99
ui/okr_ui_generator.py
CHANGED
@@ -10,17 +10,28 @@ def create_enhanced_okr_tab():
|
|
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 |
/* ----------------------------------------- */
|
@@ -216,39 +227,50 @@ def create_enhanced_okr_tab():
|
|
216 |
</style>
|
217 |
<script>
|
218 |
// This script is responsible for syncing the theme with the main Gradio app.
|
219 |
-
// It
|
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 |
-
|
225 |
-
|
226 |
-
|
|
|
|
|
|
|
|
|
227 |
} else {
|
228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
}
|
230 |
}
|
231 |
} catch (e) {
|
232 |
console.error("Theme Sync Error:", e);
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
}
|
234 |
}
|
235 |
|
236 |
-
|
237 |
-
|
238 |
-
const parentHtml = window.parent.document.querySelector('html');
|
239 |
-
if (parentHtml) {
|
240 |
-
observer.observe(parentHtml, { attributes: true, attributeFilter: ['class'] });
|
241 |
-
}
|
242 |
-
} catch (e) {
|
243 |
-
console.error("Could not set up theme MutationObserver:", e);
|
244 |
-
}
|
245 |
|
246 |
-
// Run
|
247 |
-
document.addEventListener('DOMContentLoaded',
|
248 |
-
syncThemeWithParent();
|
249 |
-
setTimeout(syncThemeWithParent, 50);
|
250 |
-
setTimeout(syncThemeWithParent, 250);
|
251 |
-
});
|
252 |
</script>
|
253 |
"""
|
254 |
|
@@ -265,15 +287,9 @@ def create_enhanced_okr_tab():
|
|
265 |
return okr_display_html
|
266 |
|
267 |
def get_initial_okr_display() -> str:
|
268 |
-
"""
|
269 |
-
Returns the initial HTML display for the OKR tab, showing a loading state.
|
270 |
-
|
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">
|
279 |
<div class="okr-title">
|
@@ -304,20 +320,12 @@ def get_initial_okr_display() -> str:
|
|
304 |
"""
|
305 |
|
306 |
def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
307 |
-
"""
|
308 |
-
Enhanced formatting function that creates beautiful HTML for OKR display
|
309 |
-
from the reconstruction cache dictionary.
|
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.
|
316 |
-
"""
|
317 |
if not reconstruction_cache:
|
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,21 +341,17 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
|
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(
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
for task in kr.get('tasks', [])
|
347 |
-
if task.get('priority', '').lower() == 'high'
|
348 |
-
)
|
349 |
-
|
350 |
-
html_parts = [f"""
|
351 |
<div class="okr-container">
|
352 |
<div class="okr-header">
|
353 |
<div class="okr-title">
|
@@ -363,7 +367,7 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
|
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 |
|
368 |
for okr_idx, okr_data in enumerate(okrs_list):
|
369 |
if not isinstance(okr_data, dict):
|
@@ -395,29 +399,22 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
|
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}")
|
403 |
-
target_metric = kr_data.get('target_metric', '')
|
404 |
-
target_value = kr_data.get('target_value', '')
|
405 |
-
kr_type = kr_data.get('key_result_type', '')
|
406 |
-
data_subject = kr_data.get('data_subject', '')
|
407 |
-
|
408 |
html_parts.append(f"""
|
409 |
<div class="key-result">
|
410 |
<div class="kr-header">
|
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
|
417 |
-
html_parts.append(f'<div class="kr-metric">Type: {
|
418 |
-
if data_subject:
|
419 |
-
html_parts.append(f'<div class="kr-metric">Data Subject: {data_subject}</div>')
|
420 |
-
html_parts.append('</div></div>')
|
421 |
|
422 |
tasks = kr_data.get('tasks', [])
|
423 |
if tasks and isinstance(tasks, list):
|
@@ -427,7 +424,6 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
|
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">
|
@@ -441,37 +437,22 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
|
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">')
|
454 |
-
|
455 |
-
|
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
|
461 |
-
html_parts.append('</div>')
|
462 |
-
html_parts.append('</div></div>')
|
463 |
-
html_parts.append('</div></div>')
|
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.
|
470 |
-
|
471 |
-
Returns:
|
472 |
-
str: HTML string for the empty OKR state.
|
473 |
-
"""
|
474 |
return """
|
|
|
475 |
<div class="okr-container">
|
476 |
<div class="okr-header">
|
477 |
<div class="okr-title">
|
@@ -480,14 +461,12 @@ def get_empty_okr_state() -> str:
|
|
480 |
</div>
|
481 |
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
|
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">
|
492 |
<div class="empty-state">
|
493 |
<div class="empty-state-icon">📋</div>
|
|
|
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 |
/* ----------------------------------------- */
|
|
|
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 |
|
|
|
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">
|
|
|
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']}
|
|
|
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">
|
|
|
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):
|
|
|
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">
|
407 |
<div class="kr-header">
|
408 |
<div class="kr-title">Key Result {kr_idx + 1}: {kr_desc}</div>
|
409 |
<div class="kr-metrics">
|
410 |
""")
|
411 |
+
if kr_data.get('target_metric') and kr_data.get('target_value'):
|
412 |
+
html_parts.append(f'<div class="kr-metric">Target: {kr_data.get("target_metric")} → {kr_data.get("target_value")}</div>')
|
413 |
+
if kr_data.get('key_result_type'):
|
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):
|
|
|
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">
|
|
|
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">
|
|
|
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>
|