Spaces:
Running
Running
Update ui/okr_ui_generator.py
Browse files- ui/okr_ui_generator.py +193 -210
ui/okr_ui_generator.py
CHANGED
@@ -8,51 +8,29 @@ logger = logging.getLogger(__name__)
|
|
8 |
def create_enhanced_okr_tab():
|
9 |
"""
|
10 |
Creates a modern, visually appealing OKR tab with improved layout and styling.
|
11 |
-
This version includes
|
12 |
-
|
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 |
/* ----------------------------------------- */
|
38 |
:root {
|
39 |
-
/* -- Base & Layout -- */
|
40 |
--okr-bg-start: #667eea;
|
41 |
--okr-bg-end: #764ba2;
|
42 |
-
--content-bg: white;
|
43 |
-
--content-shadow: rgba(0,0,0,0.1);
|
44 |
-
|
45 |
-
/* -- Headers & Titles -- */
|
46 |
--header-text-color: white;
|
47 |
--header-text-shadow: rgba(0,0,0,0.1);
|
48 |
-
|
49 |
-
/* -- Stat Cards -- */
|
50 |
--stat-card-bg: rgba(255, 255, 255, 0.15);
|
51 |
--stat-card-bg-hover: rgba(255, 255, 255, 0.2);
|
52 |
--stat-card-border: rgba(255, 255, 255, 0.2);
|
53 |
--stat-number-color: #fbbf24;
|
54 |
-
|
55 |
-
|
56 |
--objective-card-bg: #f8fafc;
|
57 |
--objective-card-border: #3b82f6;
|
58 |
--objective-shadow: 0 4px 16px rgba(0,0,0,0.05);
|
@@ -60,8 +38,6 @@ def create_enhanced_okr_tab():
|
|
60 |
--objective-header-bg: transparent;
|
61 |
--objective-title-color: #1e40af;
|
62 |
--objective-meta-text-color: #475569;
|
63 |
-
|
64 |
-
/* -- Key Result Cards -- */
|
65 |
--key-result-bg: white;
|
66 |
--key-result-border: #e5e7eb;
|
67 |
--key-result-border-hover: #3b82f6;
|
@@ -70,8 +46,6 @@ def create_enhanced_okr_tab():
|
|
70 |
--kr-metric-bg: rgba(59, 130, 246, 0.1);
|
71 |
--kr-metric-color: #1e40af;
|
72 |
--kr-metric-border: rgba(59, 130, 246, 0.2);
|
73 |
-
|
74 |
-
/* -- Task Items -- */
|
75 |
--task-item-bg: #f9fafb;
|
76 |
--task-item-bg-hover: #f3f4f6;
|
77 |
--task-item-border: #e5e7eb;
|
@@ -82,8 +56,6 @@ def create_enhanced_okr_tab():
|
|
82 |
--task-description-bg: white;
|
83 |
--task-description-border: #3b82f6;
|
84 |
--task-description-color: #4b5563;
|
85 |
-
|
86 |
-
/* -- Priority Badges -- */
|
87 |
--priority-high-bg: #fef2f2;
|
88 |
--priority-high-color: #dc2626;
|
89 |
--priority-high-border: #fca5a5;
|
@@ -93,36 +65,29 @@ def create_enhanced_okr_tab():
|
|
93 |
--priority-low-bg: #f0fdf4;
|
94 |
--priority-low-color: #16a34a;
|
95 |
--priority-low-border: #86efac;
|
96 |
-
|
97 |
-
/* -- Utility -- */
|
98 |
-
--loading-spinner-bg: #e5e7eb;
|
99 |
-
--loading-spinner-fg: #3b82f6;
|
100 |
}
|
101 |
|
102 |
/* ----------------------------------------- */
|
103 |
-
/* --- DARK MODE
|
104 |
/* ----------------------------------------- */
|
105 |
html.dark {
|
106 |
-
|
107 |
-
--
|
108 |
-
--content-shadow: rgba(0,0,0,0.6);
|
109 |
-
|
110 |
-
/* -- Headers & Titles -- */
|
111 |
--header-text-color: #e5e7eb;
|
112 |
-
|
113 |
-
/* -- Stat Cards -- */
|
114 |
--stat-card-bg: rgba(28, 28, 28, 0.7);
|
115 |
-
--stat-card-
|
116 |
-
|
117 |
-
|
|
|
|
|
118 |
--objective-card-bg: #161b22;
|
119 |
-
--objective-card-border: #
|
120 |
--objective-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
121 |
--objective-shadow-hover: 0 10px 28px rgba(0,0,0,0.6);
|
|
|
122 |
--objective-title-color: #58a6ff;
|
123 |
--objective-meta-text-color: #8b949e;
|
124 |
-
|
125 |
-
/* -- Key Result Cards -- */
|
126 |
--key-result-bg: #161b22;
|
127 |
--key-result-border: #30363d;
|
128 |
--key-result-border-hover: #58a6ff;
|
@@ -131,8 +96,6 @@ def create_enhanced_okr_tab():
|
|
131 |
--kr-metric-bg: rgba(56, 139, 253, 0.15);
|
132 |
--kr-metric-color: #58a6ff;
|
133 |
--kr-metric-border: rgba(56, 139, 253, 0.4);
|
134 |
-
|
135 |
-
/* -- Task Items -- */
|
136 |
--task-item-bg: #21262d;
|
137 |
--task-item-bg-hover: #30363d;
|
138 |
--task-item-border: #30363d;
|
@@ -143,8 +106,6 @@ def create_enhanced_okr_tab():
|
|
143 |
--task-description-bg: #0d1117;
|
144 |
--task-description-border: #30363d;
|
145 |
--task-description-color: #8b949e;
|
146 |
-
|
147 |
-
/* -- Priority Badges -- */
|
148 |
--priority-high-bg: rgba(248, 81, 73, 0.15);
|
149 |
--priority-high-color: #ff7b72;
|
150 |
--priority-high-border: rgba(248, 81, 73, 0.4);
|
@@ -154,31 +115,31 @@ def create_enhanced_okr_tab():
|
|
154 |
--priority-low-bg: rgba(63, 185, 80, 0.15);
|
155 |
--priority-low-color: #56d364;
|
156 |
--priority-low-border: rgba(63, 185, 80, 0.4);
|
157 |
-
|
158 |
-
/* -- Utility -- */
|
159 |
-
--loading-spinner-bg: #30363d;
|
160 |
-
--loading-spinner-fg: #58a6ff;
|
161 |
}
|
162 |
|
163 |
/* ----------------------------------------- */
|
164 |
/* --- BASE STYLES (Uses Variables) --- */
|
165 |
/* ----------------------------------------- */
|
166 |
.okr-container {
|
167 |
-
font-family: 'Inter', sans-serif;
|
168 |
background: linear-gradient(135deg, var(--okr-bg-start) 0%, var(--okr-bg-end) 100%);
|
169 |
-
min-height: 100vh;
|
|
|
|
|
|
|
|
|
|
|
170 |
}
|
171 |
.okr-header { text-align: center; margin-bottom: 3rem; color: var(--header-text-color); }
|
172 |
-
.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; }
|
173 |
-
.okr-title-content { background: linear-gradient(45deg,
|
174 |
-
html.dark .okr-title-content { background: linear-gradient(45deg, #e5e7eb, #9ca3af); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
175 |
.okr-title-emoji { font-size: 2.5rem; -webkit-text-fill-color: initial; }
|
176 |
-
.okr-subtitle { font-size: 1.2rem; opacity: 0.9; font-weight: 300; letter-spacing: 0.5px; }
|
177 |
.okr-stats-bar { display: flex; justify-content: center; gap: 2rem; margin: 2rem 0; flex-wrap: wrap; }
|
178 |
.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; }
|
179 |
.stat-card:hover { transform: translateY(-2px); background: var(--stat-card-bg-hover); box-shadow: 0 8px 32px var(--content-shadow); }
|
180 |
.stat-number { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; color: var(--stat-number-color); }
|
181 |
-
.stat-label { font-size: 0.9rem; opacity: 0.9; text-transform: uppercase; letter-spacing: 1px; }
|
182 |
.okr-content { background: var(--content-bg); border-radius: 24px; padding: 0; box-shadow: 0 20px 40px var(--content-shadow); overflow: hidden; margin-top: 2rem; }
|
183 |
.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; }
|
184 |
.okr-objective:hover { transform: translateY(-2px); box-shadow: var(--objective-shadow-hover); }
|
@@ -211,85 +172,100 @@ def create_enhanced_okr_tab():
|
|
211 |
.empty-state { text-align: center; padding: 4rem 2rem; color: var(--task-detail-text-color); }
|
212 |
.empty-state-icon { font-size: 3rem; margin-bottom: 1rem; }
|
213 |
.empty-state-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--objective-title-color); }
|
214 |
-
.loading-spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid var(--
|
215 |
@keyframes spin { to { transform: rotate(360deg); } }
|
216 |
-
|
217 |
-
|
218 |
-
@media (max-width: 768px) {
|
219 |
-
.okr-container { padding: 1rem; }
|
220 |
-
.okr-title { font-size: 2rem; }
|
221 |
-
.okr-stats-bar { gap: 1rem; }
|
222 |
-
.stat-card { min-width: 120px; padding: 1rem; }
|
223 |
-
.objective-meta { flex-direction: column; gap: 1rem; }
|
224 |
-
.task-details { grid-template-columns: 1fr; }
|
225 |
-
.task-header { flex-direction: column; align-items: flex-start; }
|
226 |
-
}
|
227 |
</style>
|
228 |
<script>
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
const
|
234 |
-
|
235 |
-
|
|
|
|
|
|
|
|
|
236 |
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
|
|
|
|
|
|
|
|
241 |
} else {
|
242 |
-
|
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 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
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 |
-
|
273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
274 |
</script>
|
275 |
"""
|
276 |
-
|
277 |
with gr.Column(elem_classes=["okr-root-column"]):
|
278 |
-
# Inject custom CSS and the theme-syncing JS
|
279 |
gr.HTML(okr_custom_css)
|
280 |
-
|
281 |
-
# Main OKR display area
|
282 |
okr_display_html = gr.HTML(
|
283 |
value=get_initial_okr_display(),
|
284 |
elem_classes=["okr-display"]
|
285 |
)
|
286 |
-
|
287 |
return okr_display_html
|
288 |
|
289 |
def get_initial_okr_display() -> str:
|
290 |
-
"""
|
|
|
|
|
|
|
|
|
291 |
return """
|
292 |
-
<div id="theme-debug">Loading...</div>
|
293 |
<div class="okr-container">
|
294 |
<div class="okr-header">
|
295 |
<div class="okr-title">
|
@@ -298,14 +274,12 @@ def get_initial_okr_display() -> str:
|
|
298 |
</div>
|
299 |
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
|
300 |
</div>
|
301 |
-
|
302 |
<div class="okr-stats-bar">
|
303 |
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Objectives</div></div>
|
304 |
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Key Results</div></div>
|
305 |
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Tasks</div></div>
|
306 |
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">High Priority</div></div>
|
307 |
</div>
|
308 |
-
|
309 |
<div class="okr-content">
|
310 |
<div class="empty-state">
|
311 |
<div class="empty-state-icon">⏳</div>
|
@@ -319,45 +293,84 @@ def get_initial_okr_display() -> str:
|
|
319 |
</div>
|
320 |
"""
|
321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
322 |
def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
323 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
324 |
if not reconstruction_cache:
|
325 |
logger.warning("No reconstruction cache found for display.")
|
326 |
return get_empty_okr_state()
|
327 |
-
|
328 |
-
|
|
|
329 |
for report_id, report_data in reconstruction_cache.items():
|
330 |
if isinstance(report_data, dict) and 'actionable_okrs' in report_data:
|
331 |
-
|
332 |
break
|
333 |
-
|
|
|
334 |
logger.warning("No 'actionable_okrs' found in reconstruction cache for display.")
|
335 |
return get_empty_okr_state()
|
336 |
|
337 |
-
actionable_okrs = raw_results.get("actionable_okrs", {})
|
338 |
okrs_list = actionable_okrs.get("okrs", [])
|
339 |
-
|
340 |
if not okrs_list:
|
341 |
logger.info("No OKRs found in 'actionable_okrs' list.")
|
342 |
return get_empty_okr_state()
|
343 |
|
344 |
-
#
|
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(
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
|
|
|
|
|
|
|
|
|
|
355 |
<div class="okr-container">
|
356 |
<div class="okr-header">
|
357 |
-
<div class="okr-title">
|
358 |
-
<span class="okr-title-emoji">🎯</span>
|
359 |
-
<span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span>
|
360 |
-
</div>
|
361 |
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
|
362 |
</div>
|
363 |
<div class="okr-stats-bar">
|
@@ -367,40 +380,33 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
|
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):
|
374 |
-
logger.warning(f"OKR item at index {okr_idx} is not a dictionary, skipping.")
|
375 |
-
continue
|
376 |
-
|
377 |
objective = okr_data.get('description', f"Unnamed Objective {okr_idx + 1}")
|
378 |
-
timeline = okr_data.get('timeline', '
|
379 |
-
owner = okr_data.get('owner', '
|
380 |
-
|
381 |
html_parts.append(f"""
|
382 |
<div class="okr-objective">
|
383 |
<div class="objective-header">
|
384 |
<div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
|
385 |
<div class="objective-meta">
|
386 |
-
<div class="meta-item">
|
387 |
-
|
388 |
-
</div>
|
389 |
-
<div class="meta-item">
|
390 |
-
<span class="meta-icon">👤</span> <span>Owner: {owner}</span>
|
391 |
-
</div>
|
392 |
</div>
|
393 |
</div>
|
394 |
<div class="key-results-container">
|
395 |
""")
|
396 |
|
397 |
key_results = okr_data.get('key_results', [])
|
398 |
-
if not
|
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">
|
@@ -414,16 +420,18 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
|
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 |
-
|
421 |
-
|
|
|
|
|
422 |
for task_idx, task_data in enumerate(tasks):
|
423 |
if not isinstance(task_data, dict): continue
|
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}"
|
427 |
html_parts.append(f"""
|
428 |
<div class="task-item">
|
429 |
<div class="task-header">
|
@@ -431,51 +439,26 @@ def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
|
431 |
<div class="task-priority {priority_class}">{priority.upper()}</div>
|
432 |
</div>
|
433 |
<div class="task-details">
|
434 |
-
<div class="task-detail-item"><span class="task-detail-label">Category:</span><span>{task_data.get('category', '
|
435 |
<div class="task-detail-item"><span class="task-detail-label">Effort:</span><span>{task_data.get('effort', 'N/A')}</span></div>
|
436 |
<div class="task-detail-item"><span class="task-detail-label">Timeline:</span><span>{task_data.get('timeline', 'N/A')}</span></div>
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
447 |
-
html_parts.append('</div>')
|
448 |
-
html_parts.append('</div></div>')
|
449 |
-
html_parts.append('</div></div>')
|
450 |
-
return ''.join(html_parts)
|
451 |
|
452 |
-
|
453 |
-
|
454 |
-
return """
|
455 |
-
<div id="theme-debug"></div>
|
456 |
-
<div class="okr-container">
|
457 |
-
<div class="okr-header">
|
458 |
-
<div class="okr-title">
|
459 |
-
<span class="okr-title-emoji">🎯</span>
|
460 |
-
<span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span>
|
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>
|
473 |
-
<div class="empty-state-title">No OKRs Available</div>
|
474 |
-
<div class="empty-state-description">
|
475 |
-
OKR analysis has not been generated yet or no data is available.<br>
|
476 |
-
Please ensure your LinkedIn data has been loaded and the AI analysis has completed.
|
477 |
-
</div>
|
478 |
-
</div>
|
479 |
-
</div>
|
480 |
-
</div>
|
481 |
-
"""
|
|
|
8 |
def create_enhanced_okr_tab():
|
9 |
"""
|
10 |
Creates a modern, visually appealing OKR tab with improved layout and styling.
|
11 |
+
This version includes robust support for Gradio's dark mode with multiple
|
12 |
+
detection methods and fallback mechanisms.
|
|
|
13 |
|
14 |
Returns:
|
15 |
gr.HTML: The Gradio HTML component that will display the formatted OKRs.
|
16 |
"""
|
17 |
+
# Enhanced CSS for modern OKR styling with improved Dark Mode support
|
|
|
18 |
okr_custom_css = """
|
19 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
/* ----------------------------------------- */
|
21 |
+
/* --- LIGHT MODE THEME & COLOR VARIABLES --- */
|
22 |
/* ----------------------------------------- */
|
23 |
:root {
|
|
|
24 |
--okr-bg-start: #667eea;
|
25 |
--okr-bg-end: #764ba2;
|
|
|
|
|
|
|
|
|
26 |
--header-text-color: white;
|
27 |
--header-text-shadow: rgba(0,0,0,0.1);
|
|
|
|
|
28 |
--stat-card-bg: rgba(255, 255, 255, 0.15);
|
29 |
--stat-card-bg-hover: rgba(255, 255, 255, 0.2);
|
30 |
--stat-card-border: rgba(255, 255, 255, 0.2);
|
31 |
--stat-number-color: #fbbf24;
|
32 |
+
--content-bg: white;
|
33 |
+
--content-shadow: rgba(0,0,0,0.1);
|
34 |
--objective-card-bg: #f8fafc;
|
35 |
--objective-card-border: #3b82f6;
|
36 |
--objective-shadow: 0 4px 16px rgba(0,0,0,0.05);
|
|
|
38 |
--objective-header-bg: transparent;
|
39 |
--objective-title-color: #1e40af;
|
40 |
--objective-meta-text-color: #475569;
|
|
|
|
|
41 |
--key-result-bg: white;
|
42 |
--key-result-border: #e5e7eb;
|
43 |
--key-result-border-hover: #3b82f6;
|
|
|
46 |
--kr-metric-bg: rgba(59, 130, 246, 0.1);
|
47 |
--kr-metric-color: #1e40af;
|
48 |
--kr-metric-border: rgba(59, 130, 246, 0.2);
|
|
|
|
|
49 |
--task-item-bg: #f9fafb;
|
50 |
--task-item-bg-hover: #f3f4f6;
|
51 |
--task-item-border: #e5e7eb;
|
|
|
56 |
--task-description-bg: white;
|
57 |
--task-description-border: #3b82f6;
|
58 |
--task-description-color: #4b5563;
|
|
|
|
|
59 |
--priority-high-bg: #fef2f2;
|
60 |
--priority-high-color: #dc2626;
|
61 |
--priority-high-border: #fca5a5;
|
|
|
65 |
--priority-low-bg: #f0fdf4;
|
66 |
--priority-low-color: #16a34a;
|
67 |
--priority-low-border: #86efac;
|
|
|
|
|
|
|
|
|
68 |
}
|
69 |
|
70 |
/* ----------------------------------------- */
|
71 |
+
/* --- DARK MODE COLOR VARIABLES --- */
|
72 |
/* ----------------------------------------- */
|
73 |
html.dark {
|
74 |
+
--okr-bg-start: #1a1a2e;
|
75 |
+
--okr-bg-end: #16213e;
|
|
|
|
|
|
|
76 |
--header-text-color: #e5e7eb;
|
77 |
+
--header-text-shadow: rgba(0,0,0,0.5);
|
|
|
78 |
--stat-card-bg: rgba(28, 28, 28, 0.7);
|
79 |
+
--stat-card-bg-hover: rgba(40, 40, 40, 0.8);
|
80 |
+
--stat-card-border: rgba(60, 60, 60, 0.8);
|
81 |
+
--stat-number-color: #fbbf24;
|
82 |
+
--content-bg: #0d1117;
|
83 |
+
--content-shadow: rgba(0,0,0,0.6);
|
84 |
--objective-card-bg: #161b22;
|
85 |
+
--objective-card-border: #58a6ff;
|
86 |
--objective-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
87 |
--objective-shadow-hover: 0 10px 28px rgba(0,0,0,0.6);
|
88 |
+
--objective-header-bg: transparent;
|
89 |
--objective-title-color: #58a6ff;
|
90 |
--objective-meta-text-color: #8b949e;
|
|
|
|
|
91 |
--key-result-bg: #161b22;
|
92 |
--key-result-border: #30363d;
|
93 |
--key-result-border-hover: #58a6ff;
|
|
|
96 |
--kr-metric-bg: rgba(56, 139, 253, 0.15);
|
97 |
--kr-metric-color: #58a6ff;
|
98 |
--kr-metric-border: rgba(56, 139, 253, 0.4);
|
|
|
|
|
99 |
--task-item-bg: #21262d;
|
100 |
--task-item-bg-hover: #30363d;
|
101 |
--task-item-border: #30363d;
|
|
|
106 |
--task-description-bg: #0d1117;
|
107 |
--task-description-border: #30363d;
|
108 |
--task-description-color: #8b949e;
|
|
|
|
|
109 |
--priority-high-bg: rgba(248, 81, 73, 0.15);
|
110 |
--priority-high-color: #ff7b72;
|
111 |
--priority-high-border: rgba(248, 81, 73, 0.4);
|
|
|
115 |
--priority-low-bg: rgba(63, 185, 80, 0.15);
|
116 |
--priority-low-color: #56d364;
|
117 |
--priority-low-border: rgba(63, 185, 80, 0.4);
|
|
|
|
|
|
|
|
|
118 |
}
|
119 |
|
120 |
/* ----------------------------------------- */
|
121 |
/* --- BASE STYLES (Uses Variables) --- */
|
122 |
/* ----------------------------------------- */
|
123 |
.okr-container {
|
124 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
125 |
background: linear-gradient(135deg, var(--okr-bg-start) 0%, var(--okr-bg-end) 100%);
|
126 |
+
min-height: 100vh;
|
127 |
+
padding: 2rem;
|
128 |
+
margin: -1rem;
|
129 |
+
box-sizing: border-box;
|
130 |
+
overflow-y: auto;
|
131 |
+
color: var(--header-text-color);
|
132 |
}
|
133 |
.okr-header { text-align: center; margin-bottom: 3rem; color: var(--header-text-color); }
|
134 |
+
.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); }
|
135 |
+
.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); }
|
|
|
136 |
.okr-title-emoji { font-size: 2.5rem; -webkit-text-fill-color: initial; }
|
137 |
+
.okr-subtitle { font-size: 1.2rem; opacity: 0.9; font-weight: 300; letter-spacing: 0.5px; color: var(--header-text-color); }
|
138 |
.okr-stats-bar { display: flex; justify-content: center; gap: 2rem; margin: 2rem 0; flex-wrap: wrap; }
|
139 |
.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; }
|
140 |
.stat-card:hover { transform: translateY(-2px); background: var(--stat-card-bg-hover); box-shadow: 0 8px 32px var(--content-shadow); }
|
141 |
.stat-number { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; color: var(--stat-number-color); }
|
142 |
+
.stat-label { font-size: 0.9rem; opacity: 0.9; text-transform: uppercase; letter-spacing: 1px; color: var(--header-text-color); }
|
143 |
.okr-content { background: var(--content-bg); border-radius: 24px; padding: 0; box-shadow: 0 20px 40px var(--content-shadow); overflow: hidden; margin-top: 2rem; }
|
144 |
.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; }
|
145 |
.okr-objective:hover { transform: translateY(-2px); box-shadow: var(--objective-shadow-hover); }
|
|
|
172 |
.empty-state { text-align: center; padding: 4rem 2rem; color: var(--task-detail-text-color); }
|
173 |
.empty-state-icon { font-size: 3rem; margin-bottom: 1rem; }
|
174 |
.empty-state-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--objective-title-color); }
|
175 |
+
.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; }
|
176 |
@keyframes spin { to { transform: rotate(360deg); } }
|
177 |
+
@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; } }
|
178 |
+
@supports (-webkit-appearance: none) { .okr-title-content { color: var(--header-text-color) !important; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
</style>
|
180 |
<script>
|
181 |
+
(function() {
|
182 |
+
'use strict';
|
183 |
+
// Function to apply the theme to the current document's HTML element
|
184 |
+
function applyTheme(theme) {
|
185 |
+
const htmlEl = document.documentElement;
|
186 |
+
if (theme === 'dark') {
|
187 |
+
htmlEl.classList.add('dark');
|
188 |
+
} else {
|
189 |
+
htmlEl.classList.remove('dark');
|
190 |
+
}
|
191 |
+
}
|
192 |
|
193 |
+
// Function to detect the theme from the parent Gradio app
|
194 |
+
function detectAndApplyTheme() {
|
195 |
+
try {
|
196 |
+
// Access the parent document where Gradio's theme class is set
|
197 |
+
const parentHtml = window.parent.document.querySelector('html');
|
198 |
+
if (parentHtml) {
|
199 |
+
const isDark = parentHtml.classList.contains('dark');
|
200 |
+
applyTheme(isDark ? 'dark' : 'light');
|
201 |
} else {
|
202 |
+
// Fallback for system preference if parent is not accessible
|
203 |
+
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
204 |
+
applyTheme(prefersDark ? 'dark' : 'light');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
205 |
}
|
206 |
+
} catch (e) {
|
207 |
+
console.warn('OKR Tab: Could not access parent theme. Falling back to system preference.', e.message);
|
208 |
+
// Fallback for system preference in case of cross-origin issues
|
209 |
+
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
210 |
+
applyTheme(prefersDark ? 'dark' : 'light');
|
|
|
|
|
|
|
211 |
}
|
212 |
}
|
|
|
|
|
|
|
|
|
213 |
|
214 |
+
// Use a MutationObserver to watch for theme changes on the parent
|
215 |
+
function observeThemeChanges() {
|
216 |
+
try {
|
217 |
+
const parentHtml = window.parent.document.querySelector('html');
|
218 |
+
if (!parentHtml) return;
|
219 |
+
|
220 |
+
const observer = new MutationObserver(mutations => {
|
221 |
+
mutations.forEach(mutation => {
|
222 |
+
if (mutation.attributeName === 'class') {
|
223 |
+
detectAndApplyTheme();
|
224 |
+
}
|
225 |
+
});
|
226 |
+
});
|
227 |
+
|
228 |
+
observer.observe(parentHtml, {
|
229 |
+
attributes: true,
|
230 |
+
attributeFilter: ['class']
|
231 |
+
});
|
232 |
+
} catch (e) {
|
233 |
+
console.warn('OKR Tab: Cannot observe parent theme changes.', e.message);
|
234 |
+
}
|
235 |
+
}
|
236 |
+
|
237 |
+
// Initial check when the script loads
|
238 |
+
if (document.readyState === 'loading') {
|
239 |
+
document.addEventListener('DOMContentLoaded', () => {
|
240 |
+
detectAndApplyTheme();
|
241 |
+
observeThemeChanges();
|
242 |
+
});
|
243 |
+
} else {
|
244 |
+
detectAndApplyTheme();
|
245 |
+
observeThemeChanges();
|
246 |
+
}
|
247 |
+
// Also listen for system theme changes as a fallback
|
248 |
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', detectAndApplyTheme);
|
249 |
+
})();
|
250 |
</script>
|
251 |
"""
|
|
|
252 |
with gr.Column(elem_classes=["okr-root-column"]):
|
253 |
+
# Inject custom CSS and the enhanced theme-syncing JS
|
254 |
gr.HTML(okr_custom_css)
|
255 |
+
# Main OKR display area with enhanced styling
|
|
|
256 |
okr_display_html = gr.HTML(
|
257 |
value=get_initial_okr_display(),
|
258 |
elem_classes=["okr-display"]
|
259 |
)
|
|
|
260 |
return okr_display_html
|
261 |
|
262 |
def get_initial_okr_display() -> str:
|
263 |
+
"""
|
264 |
+
Returns the initial HTML display for the OKR tab, showing a loading state.
|
265 |
+
Returns:
|
266 |
+
str: HTML string for the initial OKR display.
|
267 |
+
"""
|
268 |
return """
|
|
|
269 |
<div class="okr-container">
|
270 |
<div class="okr-header">
|
271 |
<div class="okr-title">
|
|
|
274 |
</div>
|
275 |
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
|
276 |
</div>
|
|
|
277 |
<div class="okr-stats-bar">
|
278 |
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Objectives</div></div>
|
279 |
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Key Results</div></div>
|
280 |
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">Tasks</div></div>
|
281 |
<div class="stat-card"><div class="stat-number">-</div><div class="stat-label">High Priority</div></div>
|
282 |
</div>
|
|
|
283 |
<div class="okr-content">
|
284 |
<div class="empty-state">
|
285 |
<div class="empty-state-icon">⏳</div>
|
|
|
293 |
</div>
|
294 |
"""
|
295 |
|
296 |
+
def get_empty_okr_state() -> str:
|
297 |
+
"""Returns empty state HTML for when no OKRs are available, using the new styles."""
|
298 |
+
return """
|
299 |
+
<div class="okr-container">
|
300 |
+
<div class="okr-header">
|
301 |
+
<div class="okr-title">
|
302 |
+
<span class="okr-title-emoji">🎯</span>
|
303 |
+
<span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span>
|
304 |
+
</div>
|
305 |
+
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
|
306 |
+
</div>
|
307 |
+
<div class="okr-stats-bar">
|
308 |
+
<div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Objectives</div></div>
|
309 |
+
<div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Key Results</div></div>
|
310 |
+
<div class="stat-card"><div class="stat-number">0</div><div class="stat-label">Tasks</div></div>
|
311 |
+
<div class="stat-card"><div class="stat-number">0</div><div class="stat-label">High Priority</div></div>
|
312 |
+
</div>
|
313 |
+
<div class="okr-content">
|
314 |
+
<div class="empty-state">
|
315 |
+
<div class="empty-state-icon">📋</div>
|
316 |
+
<div class="empty-state-title">No OKRs Available</div>
|
317 |
+
<div class="empty-state-description">
|
318 |
+
OKR analysis has not been generated yet or no data is available.<br>
|
319 |
+
Please ensure your LinkedIn data has been loaded and the AI analysis has completed.
|
320 |
+
</div>
|
321 |
+
</div>
|
322 |
+
</div>
|
323 |
+
</div>
|
324 |
+
"""
|
325 |
+
|
326 |
def format_okrs_for_enhanced_display(reconstruction_cache: dict) -> str:
|
327 |
+
"""
|
328 |
+
Enhanced formatting function that creates beautiful HTML for OKR display
|
329 |
+
from the reconstruction cache dictionary, compatible with new styling.
|
330 |
+
Args:
|
331 |
+
reconstruction_cache (dict): The dictionary containing 'actionable_okrs'.
|
332 |
+
Returns:
|
333 |
+
str: A comprehensive HTML string representing the OKRs, or an empty state HTML.
|
334 |
+
"""
|
335 |
if not reconstruction_cache:
|
336 |
logger.warning("No reconstruction cache found for display.")
|
337 |
return get_empty_okr_state()
|
338 |
+
|
339 |
+
actionable_okrs = {}
|
340 |
+
# Extract actionable_okrs from the cache
|
341 |
for report_id, report_data in reconstruction_cache.items():
|
342 |
if isinstance(report_data, dict) and 'actionable_okrs' in report_data:
|
343 |
+
actionable_okrs = report_data['actionable_okrs']
|
344 |
break
|
345 |
+
|
346 |
+
if not actionable_okrs:
|
347 |
logger.warning("No 'actionable_okrs' found in reconstruction cache for display.")
|
348 |
return get_empty_okr_state()
|
349 |
|
|
|
350 |
okrs_list = actionable_okrs.get("okrs", [])
|
|
|
351 |
if not okrs_list:
|
352 |
logger.info("No OKRs found in 'actionable_okrs' list.")
|
353 |
return get_empty_okr_state()
|
354 |
|
355 |
+
# Calculate statistics for the stats bar
|
356 |
total_objectives = len(okrs_list)
|
357 |
+
total_key_results = sum(len(okr.get('key_results', [])) for okr in okrs_list if isinstance(okr, dict))
|
358 |
+
total_tasks = sum(
|
359 |
+
len(kr.get('tasks', []))
|
360 |
+
for okr in okrs_list if isinstance(okr, dict)
|
361 |
+
for kr in okr.get('key_results', []) if isinstance(kr, dict)
|
362 |
+
)
|
363 |
+
high_priority_tasks = sum(
|
364 |
+
1 for okr in okrs_list if isinstance(okr, dict)
|
365 |
+
for kr in okr.get('key_results', []) if isinstance(kr, dict)
|
366 |
+
for task in kr.get('tasks', []) if isinstance(task, dict) and task.get('priority', '').lower() == 'high'
|
367 |
+
)
|
368 |
+
|
369 |
+
# --- Start HTML Generation ---
|
370 |
+
html_parts = [f"""
|
371 |
<div class="okr-container">
|
372 |
<div class="okr-header">
|
373 |
+
<div class="okr-title"><span class="okr-title-emoji">🎯</span><span class="okr-title-content">AI-Generated OKRs & Strategic Tasks</span></div>
|
|
|
|
|
|
|
374 |
<div class="okr-subtitle">Intelligent objectives and key results based on your LinkedIn analytics</div>
|
375 |
</div>
|
376 |
<div class="okr-stats-bar">
|
|
|
380 |
<div class="stat-card"><div class="stat-number">{high_priority_tasks}</div><div class="stat-label">High Priority</div></div>
|
381 |
</div>
|
382 |
<div class="okr-content">
|
383 |
+
"""]
|
384 |
|
385 |
+
# --- Loop through Objectives ---
|
386 |
for okr_idx, okr_data in enumerate(okrs_list):
|
387 |
+
if not isinstance(okr_data, dict): continue
|
|
|
|
|
|
|
388 |
objective = okr_data.get('description', f"Unnamed Objective {okr_idx + 1}")
|
389 |
+
timeline = okr_data.get('timeline', 'N/A')
|
390 |
+
owner = okr_data.get('owner', 'N/A')
|
|
|
391 |
html_parts.append(f"""
|
392 |
<div class="okr-objective">
|
393 |
<div class="objective-header">
|
394 |
<div class="objective-title">Objective {okr_idx + 1}: {objective}</div>
|
395 |
<div class="objective-meta">
|
396 |
+
<div class="meta-item"><span class="meta-icon">⏰</span> <span>Timeline: {timeline}</span></div>
|
397 |
+
<div class="meta-item"><span class="meta-icon">👤</span> <span>Owner: {owner}</span></div>
|
|
|
|
|
|
|
|
|
398 |
</div>
|
399 |
</div>
|
400 |
<div class="key-results-container">
|
401 |
""")
|
402 |
|
403 |
key_results = okr_data.get('key_results', [])
|
404 |
+
if not key_results:
|
405 |
+
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>')
|
406 |
else:
|
407 |
+
# --- Loop through Key Results ---
|
408 |
for kr_idx, kr_data in enumerate(key_results):
|
409 |
if not isinstance(kr_data, dict): continue
|
|
|
410 |
kr_desc = kr_data.get('description', f"Unnamed Key Result {kr_idx + 1}")
|
411 |
html_parts.append(f"""
|
412 |
<div class="key-result">
|
|
|
420 |
html_parts.append(f'<div class="kr-metric">Type: {kr_data.get("key_result_type")}</div>')
|
421 |
if kr_data.get('data_subject'):
|
422 |
html_parts.append(f'<div class="kr-metric">Data Subject: {kr_data.get("data_subject")}</div>')
|
423 |
+
html_parts.append('</div></div>') # Close kr-metrics & kr-header
|
424 |
|
425 |
tasks = kr_data.get('tasks', [])
|
426 |
+
html_parts.append('<div class="tasks-section">')
|
427 |
+
if tasks:
|
428 |
+
html_parts.append('<div class="tasks-title"><span>📋</span><span>Associated Tasks</span></div>')
|
429 |
+
# --- Loop through Tasks ---
|
430 |
for task_idx, task_data in enumerate(tasks):
|
431 |
if not isinstance(task_data, dict): continue
|
432 |
task_desc = task_data.get('description', f"Unnamed Task {task_idx + 1}")
|
433 |
priority = task_data.get('priority', 'Medium').lower()
|
434 |
+
priority_class = f"priority-{priority}"
|
435 |
html_parts.append(f"""
|
436 |
<div class="task-item">
|
437 |
<div class="task-header">
|
|
|
439 |
<div class="task-priority {priority_class}">{priority.upper()}</div>
|
440 |
</div>
|
441 |
<div class="task-details">
|
442 |
+
<div class="task-detail-item"><span class="task-detail-label">Category:</span><span>{task_data.get('category', 'N/A')}</span></div>
|
443 |
<div class="task-detail-item"><span class="task-detail-label">Effort:</span><span>{task_data.get('effort', 'N/A')}</span></div>
|
444 |
<div class="task-detail-item"><span class="task-detail-label">Timeline:</span><span>{task_data.get('timeline', 'N/A')}</span></div>
|
445 |
<div class="task-detail-item"><span class="task-detail-label">Responsible:</span><span>{task_data.get('responsible_party', 'N/A')}</span></div>
|
446 |
+
</div>""")
|
447 |
+
detail_items = {
|
448 |
+
'Deliverable': task_data.get('deliverable'),
|
449 |
+
'Success Metrics': task_data.get('success_criteria_metrics'),
|
450 |
+
'Why': task_data.get('why'),
|
451 |
+
'Priority Justification': task_data.get('priority_justification'),
|
452 |
+
'Dependencies': task_data.get('dependencies')
|
453 |
+
}
|
454 |
+
detail_lines = [f'<strong>{k}:</strong> {v}' for k, v in detail_items.items() if v]
|
455 |
if detail_lines:
|
456 |
html_parts.append(f'<div class="task-description">{"<br>".join(detail_lines)}</div>')
|
457 |
+
html_parts.append('</div>') # Close task-item
|
|
|
458 |
else:
|
459 |
+
html_parts.append('<div style="text-align:center; padding: 1rem; color: var(--task-detail-text-color);">No tasks defined for this key result.</div>')
|
460 |
+
html_parts.append('</div></div>') # Close tasks-section & key-result
|
461 |
+
html_parts.append('</div></div>') # Close key-results-container & okr-objective
|
|
|
|
|
462 |
|
463 |
+
html_parts.append('</div></div>') # Close okr-content & okr-container
|
464 |
+
return ''.join(html_parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|