Spaces:
Running
Running
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) | |