LinkedinMonitor / ui /okr_ui_generator.py
GuglielmoTor's picture
Update ui/okr_ui_generator.py
d5c5881 verified
import gradio as gr
from typing import Dict, Any, List, Optional
import pandas as pd
import logging
logger = logging.getLogger(__name__)
def create_enhanced_okr_tab():
"""
Creates a modern, visually appealing OKR tab with improved layout and styling.
This version includes robust support for Gradio's dark mode with multiple
detection methods and fallback mechanisms.
Returns:
gr.HTML: The Gradio HTML component that will display the formatted OKRs.
"""
# Enhanced CSS for modern OKR styling with improved Dark Mode support
okr_custom_css = """
<style>
/* ----------------------------------------- */
/* --- LIGHT MODE THEME & COLOR VARIABLES --- */
/* ----------------------------------------- */
:root {
--okr-bg-start: #667eea;
--okr-bg-end: #764ba2;
--header-text-color: white;
--header-text-shadow: rgba(0,0,0,0.1);
--stat-card-bg: rgba(255, 255, 255, 0.15);
--stat-card-bg-hover: rgba(255, 255, 255, 0.2);
--stat-card-border: rgba(255, 255, 255, 0.2);
--stat-number-color: #fbbf24;
--content-bg: white;
--content-shadow: rgba(0,0,0,0.1);
--objective-card-bg: #f8fafc;
--objective-card-border: #3b82f6;
--objective-shadow: 0 4px 16px rgba(0,0,0,0.05);
--objective-shadow-hover: 0 8px 24px rgba(0,0,0,0.1);
--objective-header-bg: transparent;
--objective-title-color: #1e40af;
--objective-meta-text-color: #475569;
--key-result-bg: white;
--key-result-border: #e5e7eb;
--key-result-border-hover: #3b82f6;
--kr-header-bg: #f8fafc;
--kr-title-color: #1e293b;
--kr-metric-bg: rgba(59, 130, 246, 0.1);
--kr-metric-color: #1e40af;
--kr-metric-border: rgba(59, 130, 246, 0.2);
--task-item-bg: #f9fafb;
--task-item-bg-hover: #f3f4f6;
--task-item-border: #e5e7eb;
--task-item-border-hover: #d1d5db;
--task-title-color: #111827;
--task-detail-label-color: #374151;
--task-detail-text-color: #6b7280;
--task-description-bg: white;
--task-description-border: #3b82f6;
--task-description-color: #4b5563;
--priority-high-bg: #fef2f2;
--priority-high-color: #dc2626;
--priority-high-border: #fca5a5;
--priority-medium-bg: #fffbeb;
--priority-medium-color: #d97706;
--priority-medium-border: #fcd34d;
--priority-low-bg: #f0fdf4;
--priority-low-color: #16a34a;
--priority-low-border: #86efac;
}
/* ----------------------------------------- */
/* --- DARK MODE COLOR VARIABLES --- */
/* ----------------------------------------- */
html.dark {
--okr-bg-start: #1a1a2e;
--okr-bg-end: #16213e;
--header-text-color: #e5e7eb;
--header-text-shadow: rgba(0,0,0,0.5);
--stat-card-bg: rgba(28, 28, 28, 0.7);
--stat-card-bg-hover: rgba(40, 40, 40, 0.8);
--stat-card-border: rgba(60, 60, 60, 0.8);
--stat-number-color: #fbbf24;
--content-bg: #0d1117;
--content-shadow: rgba(0,0,0,0.6);
--objective-card-bg: #161b22;
--objective-card-border: #58a6ff;
--objective-shadow: 0 8px 24px rgba(0,0,0,0.5);
--objective-shadow-hover: 0 10px 28px rgba(0,0,0,0.6);
--objective-header-bg: transparent;
--objective-title-color: #58a6ff;
--objective-meta-text-color: #8b949e;
--key-result-bg: #161b22;
--key-result-border: #30363d;
--key-result-border-hover: #58a6ff;
--kr-header-bg: #161b22;
--kr-title-color: #c9d1d9;
--kr-metric-bg: rgba(56, 139, 253, 0.15);
--kr-metric-color: #58a6ff;
--kr-metric-border: rgba(56, 139, 253, 0.4);
--task-item-bg: #21262d;
--task-item-bg-hover: #30363d;
--task-item-border: #30363d;
--task-item-border-hover: #8b949e;
--task-title-color: #c9d1d9;
--task-detail-label-color: #8b949e;
--task-detail-text-color: #8b949e;
--task-description-bg: #0d1117;
--task-description-border: #30363d;
--task-description-color: #8b949e;
--priority-high-bg: rgba(248, 81, 73, 0.15);
--priority-high-color: #ff7b72;
--priority-high-border: rgba(248, 81, 73, 0.4);
--priority-medium-bg: rgba(210, 153, 34, 0.15);
--priority-medium-color: #d29922;
--priority-medium-border: rgba(210, 153, 34, 0.4);
--priority-low-bg: rgba(63, 185, 80, 0.15);
--priority-low-color: #56d364;
--priority-low-border: rgba(63, 185, 80, 0.4);
}
/* ----------------------------------------- */
/* --- BASE STYLES (Uses Variables) --- */
/* ----------------------------------------- */
.okr-container {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: linear-gradient(135deg, var(--okr-bg-start) 0%, var(--okr-bg-end) 100%);
min-height: 100vh;
padding: 2rem;
margin: -1rem;
box-sizing: border-box;
overflow-y: auto;
color: var(--header-text-color);
}
.okr-header { text-align: center; margin-bottom: 3rem; color: var(--header-text-color); }
.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); }
.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); }
.okr-title-emoji { font-size: 2.5rem; -webkit-text-fill-color: initial; }
.okr-subtitle { font-size: 1.2rem; opacity: 0.9; font-weight: 300; letter-spacing: 0.5px; color: var(--header-text-color); }
.okr-stats-bar { display: flex; justify-content: center; gap: 2rem; margin: 2rem 0; flex-wrap: wrap; }
.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; }
.stat-card:hover { transform: translateY(-2px); background: var(--stat-card-bg-hover); box-shadow: 0 8px 32px var(--content-shadow); }
.stat-number { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; color: var(--stat-number-color); }
.stat-label { font-size: 0.9rem; opacity: 0.9; text-transform: uppercase; letter-spacing: 1px; color: var(--header-text-color); }
.okr-content { background: var(--content-bg); border-radius: 24px; padding: 0; box-shadow: 0 20px 40px var(--content-shadow); overflow: hidden; margin-top: 2rem; }
.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; }
.okr-objective:hover { transform: translateY(-2px); box-shadow: var(--objective-shadow-hover); }
.objective-header { padding: 2rem; background: var(--objective-header-bg); border-bottom: 1px solid var(--key-result-border); position: relative; }
.objective-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; position: relative; z-index: 1; color: var(--objective-title-color); }
.objective-meta { display: flex; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; position: relative; z-index: 1; }
.meta-item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: var(--objective-meta-text-color); }
.meta-icon { width: 16px; height: 16px; opacity: 0.8; }
.key-results-container { padding: 2rem; }
.key-result { background: var(--key-result-bg); border: 2px solid var(--key-result-border); border-radius: 12px; margin: 1.5rem 0; overflow: hidden; transition: all 0.3s ease; }
.key-result:hover { border-color: var(--key-result-border-hover); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1); }
.kr-header { background: var(--kr-header-bg); padding: 1.5rem; border-bottom: 1px solid var(--key-result-border); }
.kr-title { font-size: 1.2rem; font-weight: 600; color: var(--kr-title-color); margin-bottom: 0.75rem; }
.kr-metrics { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-top: 1rem; }
.kr-metric { background: var(--kr-metric-bg); color: var(--kr-metric-color); padding: 0.5rem 1rem; border-radius: 8px; font-size: 0.85rem; font-weight: 500; border: 1px solid var(--kr-metric-border); }
.tasks-section { padding: 1.5rem; }
.tasks-title { font-size: 1rem; font-weight: 600; color: var(--task-detail-label-color); margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
.task-item { background: var(--task-item-bg); border: 1px solid var(--task-item-border); border-radius: 8px; padding: 1.25rem; margin: 1rem 0; transition: all 0.2s ease; }
.task-item:hover { background: var(--task-item-bg-hover); border-color: var(--task-item-border-hover); }
.task-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem; gap: 1rem; }
.task-title { font-weight: 600; color: var(--task-title-color); flex: 1; line-height: 1.4; }
.task-priority { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }
.priority-high { background: var(--priority-high-bg); color: var(--priority-high-color); border: 1px solid var(--priority-high-border); }
.priority-medium { background: var(--priority-medium-bg); color: var(--priority-medium-color); border: 1px solid var(--priority-medium-border); }
.priority-low { background: var(--priority-low-bg); color: var(--priority-low-color); border: 1px solid var(--priority-low-border); }
.task-details { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; }
.task-detail-item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; color: var(--task-detail-text-color); }
.task-detail-label { font-weight: 500; color: var(--task-detail-label-color); min-width: 80px; }
.task-description { margin-top: 1rem; padding: 1rem; background: var(--task-description-bg); border-radius: 6px; border-left: 3px solid var(--task-description-border); font-size: 0.9rem; line-height: 1.5; color: var(--task-description-color); }
.empty-state { text-align: center; padding: 4rem 2rem; color: var(--task-detail-text-color); }
.empty-state-icon { font-size: 3rem; margin-bottom: 1rem; }
.empty-state-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--objective-title-color); }
.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; }
@keyframes spin { to { transform: rotate(360deg); } }
@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; } }
@supports (-webkit-appearance: none) { .okr-title-content { color: var(--header-text-color) !important; } }
</style>
<script>
(function() {
'use strict';
// Function to apply the theme to the current document's HTML element
function applyTheme(theme) {
const htmlEl = document.documentElement;
if (theme === 'dark') {
htmlEl.classList.add('dark');
} else {
htmlEl.classList.remove('dark');
}
}
// Function to detect the theme from the parent Gradio app
function detectAndApplyTheme() {
try {
// Access the parent document where Gradio's theme class is set
const parentHtml = window.parent.document.querySelector('html');
if (parentHtml) {
const isDark = parentHtml.classList.contains('dark');
applyTheme(isDark ? 'dark' : 'light');
} else {
// Fallback for system preference if parent is not accessible
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(prefersDark ? 'dark' : 'light');
}
} catch (e) {
console.warn('OKR Tab: Could not access parent theme. Falling back to system preference.', e.message);
// Fallback for system preference in case of cross-origin issues
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(prefersDark ? 'dark' : 'light');
}
}
// Use a MutationObserver to watch for theme changes on the parent
function observeThemeChanges() {
try {
const parentHtml = window.parent.document.querySelector('html');
if (!parentHtml) return;
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.attributeName === 'class') {
detectAndApplyTheme();
}
});
});
observer.observe(parentHtml, {
attributes: true,
attributeFilter: ['class']
});
} catch (e) {
console.warn('OKR Tab: Cannot observe parent theme changes.', e.message);
}
}
// Initial check when the script loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
detectAndApplyTheme();
observeThemeChanges();
});
} else {
detectAndApplyTheme();
observeThemeChanges();
}
// Also listen for system theme changes as a fallback
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', detectAndApplyTheme);
})();
</script>
"""
with gr.Column(elem_classes=["okr-root-column"]):
# Inject custom CSS and the enhanced theme-syncing JS
gr.HTML(okr_custom_css)
# Main OKR display area with enhanced styling
okr_display_html = gr.HTML(
value=get_initial_okr_display(),
elem_classes=["okr-display"]
)
return okr_display_html
def get_initial_okr_display() -> str:
"""
Returns the initial HTML display for the OKR tab, showing a loading state.
Returns:
str: HTML string for the initial OKR display.
"""
return """
<div class="okr-container">
<div class="okr-header">
<div class="okr-title">
<span class="okr-title-emoji">🎯</span>
<span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span>
</div>
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
</div>
<div class="okr-stats-bar">
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Objectives</div></div>
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Key Results</div></div>
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Tasks</div></div>
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">High Priority</div></div>
</div>
<div class="okr-content">
<div class="empty-state">
<div class="empty-state-icon">⏳</div>
<div class="empty-state-title">Loading OKR Analysis</div>
<div class="empty-state-description">
<div class="loading-spinner"></div>
Generating intelligent objectives and actionable tasks from your LinkedIn data...
</div>
</div>
</div>
</div>
"""
def get_empty_okr_state() -> str:
"""Returns empty state HTML for when no OKRs are available, using the new styles."""
return """
<div class="okr-container">
<div class="okr-header">
<div class="okr-title">
<span class="okr-title-emoji">🎯</span>
<span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span>
</div>
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
</div>
<div class="okr-stats-bar">
<div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Objectives</div></div>
<div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Key Results</div></div>
<div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Tasks</div></div>
<div class="stat-card"><div class="stat-number">0</div><div class="stat-label">High Priority</div></div>
</div>
<div class="okr-content">
<div class="empty-state">
<div class="empty-state-icon">πŸ“‹</div>
<div class="empty-state-title">No OKRs Available</div>
<div class="empty-state-description">
OKR analysis has not been generated yet or no data is available.<br>
Please ensure your LinkedIn data has been loaded and the AI analysis has completed.
</div>
</div>
</div>
</div>
"""
def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
"""
Enhanced formatting function that creates beautiful HTML for OKR display
from the reconstruction cache dictionary, compatible with new styling.
Args:
reconstruction_cache (dict): The dictionary containing 'actionable_okrs'.
Returns:
str: A comprehensive HTML string representing the OKRs, or an empty state HTML.
"""
if not reconstruction_cache:
logger.warning("No reconstruction cache found for display.")
return get_empty_okr_state()
actionable_okrs = {}
# Extract actionable_okrs from the cache
for report_id, report_data in reconstruction_cache.items():
if isinstance(report_data, dict) and 'actionable_okrs' in report_data:
actionable_okrs = report_data['actionable_okrs']
break
if not actionable_okrs:
logger.warning("No 'actionable_okrs' found in reconstruction cache for display.")
return get_empty_okr_state()
okrs_list = actionable_okrs.get("okrs", [])
if not okrs_list:
logger.info("No OKRs found in 'actionable_okrs' list.")
return get_empty_okr_state()
# Calculate statistics for the stats bar
total_objectives = len(okrs_list)
total_key_results = sum(len(okr.get('key_results', [])) for okr in okrs_list if isinstance(okr, dict))
total_tasks = sum(
len(kr.get('tasks', []))
for okr in okrs_list if isinstance(okr, dict)
for kr in okr.get('key_results', []) if isinstance(kr, dict)
)
high_priority_tasks = sum(
1 for okr in okrs_list if isinstance(okr, dict)
for kr in okr.get('key_results', []) if isinstance(kr, dict)
for task in kr.get('tasks', []) if isinstance(task, dict) and task.get('priority', '').lower() == 'high'
)
# --- Start HTML Generation ---
html_parts = [f"""
<div class="okr-container">
<div class="okr-header">
<div class="okr-title"><span class="okr-title-emoji">🎯</span><span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span></div>
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
</div>
<div class="okr-stats-bar">
<div class="stat-card"><div class="stat-number">{total_objectives}</div><div class="stat-label">Objectives</div></div>
<div class="stat-card"><div class="stat-number">{total_key_results}</div><div class="stat-label">Key Results</div></div>
<div class="stat-card"><div class="stat-number">{total_tasks}</div><div class="stat-label">Tasks</div></div>
<div class="stat-card"><div class="stat-number">{high_priority_tasks}</div><div class="stat-label">High Priority</div></div>
</div>
<div class="okr-content">
"""]
# --- Loop through Objectives ---
for okr_idx, okr_data in enumerate(okrs_list):
if not isinstance(okr_data, dict): continue
objective = okr_data.get('description', f"Unnamed Objective {okr_idx + 1}")
timeline = okr_data.get('timeline', 'N/A')
owner = okr_data.get('owner', 'N/A')
html_parts.append(f"""
<div class="okr-objective">
<div class="objective-header">
<div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
<div class="objective-meta">
<div class="meta-item"><span class="meta-icon">⏰</span> <span>Timeline: {timeline}</span></div>
<div class="meta-item"><span class="meta-icon">πŸ‘€</span> <span>Owner: {owner}</span></div>
</div>
</div>
<div class="key-results-container">
""")
key_results = okr_data.get('key_results', [])
if not key_results:
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>')
else:
# --- Loop through Key Results ---
for kr_idx, kr_data in enumerate(key_results):
if not isinstance(kr_data, dict): continue
kr_desc = kr_data.get('description', f"Unnamed Key Result {kr_idx + 1}")
html_parts.append(f"""
<div class="key-result">
<div class="kr-header">
<div class="kr-title">Key Result {kr_idx + 1}: {kr_desc}</div>
<div class="kr-metrics">
""")
if kr_data.get('target_metric') and kr_data.get('target_value'):
html_parts.append(f'<div class="kr-metric">Target: {kr_data.get("target_metric")} β†’ {kr_data.get("target_value")}</div>')
if kr_data.get('key_result_type'):
html_parts.append(f'<div class="kr-metric">Type: {kr_data.get("key_result_type")}</div>')
if kr_data.get('data_subject'):
html_parts.append(f'<div class="kr-metric">Data Subject: {kr_data.get("data_subject")}</div>')
html_parts.append('</div></div>') # Close kr-metrics & kr-header
tasks = kr_data.get('tasks', [])
html_parts.append('<div class="tasks-section">')
if tasks:
html_parts.append('<div class="tasks-title"><span>πŸ“‹</span><span>Associated Tasks</span></div>')
# --- Loop through Tasks ---
for task_idx, task_data in enumerate(tasks):
if not isinstance(task_data, dict): continue
task_desc = task_data.get('description', f"Unnamed Task {task_idx + 1}")
priority = task_data.get('priority', 'Medium').lower()
priority_class = f"priority-{priority}"
html_parts.append(f"""
<div class="task-item">
<div class="task-header">
<div class="task-title">{task_idx + 1}. {task_desc}</div>
<div class="task-priority {priority_class}">{priority.upper()}</div>
</div>
<div class="task-details">
<div class="task-detail-item"><span class="task-detail-label">Category:</span><span>{task_data.get('category', 'N/A')}</span></div>
<div class="task-detail-item"><span class="task-detail-label">Effort:</span><span>{task_data.get('effort', 'N/A')}</span></div>
<div class="task-detail-item"><span class="task-detail-label">Timeline:</span><span>{task_data.get('timeline', 'N/A')}</span></div>
<div class="task-detail-item"><span class="task-detail-label">Responsible:</span><span>{task_data.get('responsible_party', 'N/A')}</span></div>
</div>""")
detail_items = {
'Deliverable': task_data.get('deliverable'),
'Success Metrics': task_data.get('success_criteria_metrics'),
'Why': task_data.get('why'),
'Priority Justification': task_data.get('priority_justification'),
'Dependencies': task_data.get('dependencies')
}
detail_lines = [f'<strong>{k}:</strong> {v}' for k, v in detail_items.items() if v]
if detail_lines:
html_parts.append(f'<div class="task-description">{"<br>".join(detail_lines)}</div>')
html_parts.append('</div>') # Close task-item
else:
html_parts.append('<div style="text-align:center; padding: 1rem; color: var(--task-detail-text-color);">No tasks defined for this key result.</div>')
html_parts.append('</div></div>') # Close tasks-section & key-result
html_parts.append('</div></div>') # Close key-results-container & okr-objective
html_parts.append('</div></div>') # Close okr-content & okr-container
return ''.join(html_parts)