|
import gradio as gr |
|
import datetime |
|
from typing import Dict, List, Any, Union, Optional, Callable |
|
import matplotlib.pyplot as plt |
|
import numpy as np |
|
|
|
from utils.logging import get_logger |
|
from utils.error_handling import handle_ui_exceptions, ValidationError, safe_get |
|
from utils.config import UI_COLORS, UI_SIZES |
|
|
|
|
|
logger = get_logger(__name__) |
|
|
|
@handle_ui_exceptions |
|
def create_card(title: str, content: str, icon: str = None, color: str = "blue") -> gr.Group: |
|
""" |
|
Create a card component with title and content |
|
|
|
Args: |
|
title: Card title |
|
content: Card content |
|
icon: Icon emoji |
|
color: Card color |
|
|
|
Returns: |
|
Gradio Group component |
|
|
|
Raises: |
|
ValidationError: If required parameters are missing or invalid |
|
""" |
|
if not title: |
|
logger.warning("Creating card with empty title") |
|
title = "Untitled" |
|
|
|
logger.debug(f"Creating card: {title}") |
|
|
|
with gr.Group(elem_classes=["card", f"card-{color}"]) as card: |
|
with gr.Row(): |
|
if icon: |
|
gr.Markdown(f"## {icon} {title}") |
|
else: |
|
gr.Markdown(f"## {title}") |
|
gr.Markdown(content) |
|
|
|
return card |
|
|
|
@handle_ui_exceptions |
|
def create_stat_card(title: str, value: Union[int, str], icon: str = None, color: str = "blue") -> gr.Group: |
|
""" |
|
Create a statistics card component |
|
|
|
Args: |
|
title: Card title |
|
value: Statistic value |
|
icon: Icon emoji |
|
color: Card color |
|
|
|
Returns: |
|
Gradio Group component |
|
|
|
Raises: |
|
ValidationError: If required parameters are missing or invalid |
|
""" |
|
if not title: |
|
logger.warning("Creating stat card with empty title") |
|
title = "Untitled" |
|
|
|
logger.debug(f"Creating stat card: {title} = {value}") |
|
|
|
with gr.Group(elem_classes=["stat-card", f"card-{color}"]) as card: |
|
with gr.Row(): |
|
if icon: |
|
gr.Markdown(f"### {icon} {title}") |
|
else: |
|
gr.Markdown(f"### {title}") |
|
gr.Markdown(f"## {value}") |
|
|
|
return card |
|
|
|
@handle_ui_exceptions |
|
def create_progress_ring(value: float, max_value: float = 100, size: int = 100, |
|
color: str = "blue", label: str = None) -> gr.HTML: |
|
""" |
|
Create a CSS-based circular progress indicator |
|
|
|
Args: |
|
value: Current value |
|
max_value: Maximum value |
|
size: Size of the ring in pixels |
|
color: Color of the progress ring |
|
label: Optional label to display in the center |
|
|
|
Returns: |
|
Gradio HTML component with the progress ring |
|
|
|
Raises: |
|
ValidationError: If parameters are invalid |
|
""" |
|
|
|
if not isinstance(value, (int, float)): |
|
logger.warning(f"Invalid value for progress ring: {value}, using 0") |
|
value = 0 |
|
|
|
if not isinstance(max_value, (int, float)) or max_value <= 0: |
|
logger.warning(f"Invalid max_value for progress ring: {max_value}, using 100") |
|
max_value = 100 |
|
|
|
if not isinstance(size, int) or size <= 0: |
|
logger.warning(f"Invalid size for progress ring: {size}, using 100") |
|
size = 100 |
|
|
|
logger.debug(f"Creating progress ring: {value}/{max_value} ({color})") |
|
|
|
|
|
percentage = min(100, max(0, (value / max_value) * 100)) |
|
|
|
|
|
ring_color = UI_COLORS.get(color, UI_COLORS["blue"]) |
|
|
|
|
|
html = f""" |
|
<div class="progress-ring-container" style="width: {size}px; height: {size}px;"> |
|
<div class="progress-ring" style="width: {size}px; height: {size}px;"> |
|
<div class="progress-ring-circle" |
|
style="width: {size}px; height: {size}px; |
|
background: conic-gradient({ring_color} {percentage}%, #e0e0e0 0);"> |
|
</div> |
|
<div class="progress-ring-inner" style="width: {size-20}px; height: {size-20}px;"> |
|
<div class="progress-ring-value">{int(percentage)}%</div> |
|
{f'<div class="progress-ring-label">{label}</div>' if label else ''} |
|
</div> |
|
</div> |
|
</div> |
|
<style> |
|
.progress-ring-container {{ |
|
position: relative; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
}} |
|
.progress-ring {{ |
|
position: relative; |
|
border-radius: 50%; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
}} |
|
.progress-ring-circle {{ |
|
border-radius: 50%; |
|
position: absolute; |
|
mask: radial-gradient(transparent 55%, black 56%); |
|
-webkit-mask: radial-gradient(transparent 55%, black 56%); |
|
}} |
|
.progress-ring-inner {{ |
|
background: white; |
|
border-radius: 50%; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 2; |
|
}} |
|
.progress-ring-value {{ |
|
font-size: {size/4}px; |
|
font-weight: bold; |
|
color: {ring_color}; |
|
}} |
|
.progress-ring-label {{ |
|
font-size: {size/8}px; |
|
color: #666; |
|
margin-top: 5px; |
|
}} |
|
</style> |
|
""" |
|
|
|
return gr.HTML(html) |
|
|
|
@handle_ui_exceptions |
|
def create_activity_item(activity: Dict[str, Any]) -> gr.Group: |
|
""" |
|
Create an activity feed item |
|
|
|
Args: |
|
activity: Activity data |
|
|
|
Returns: |
|
Gradio Group component |
|
|
|
Raises: |
|
ValidationError: If activity data is invalid |
|
""" |
|
if not activity: |
|
logger.warning("Creating activity item with empty data") |
|
activity = {"type": "unknown", "timestamp": get_timestamp()} |
|
|
|
logger.debug(f"Creating activity item: {safe_get(activity, 'type', 'unknown')}") |
|
|
|
|
|
try: |
|
timestamp = datetime.datetime.fromisoformat(safe_get(activity, "timestamp", "")) |
|
formatted_time = timestamp.strftime("%b %d, %H:%M") |
|
except (ValueError, TypeError) as e: |
|
logger.warning(f"Error formatting timestamp: {str(e)}") |
|
formatted_time = "Unknown time" |
|
|
|
|
|
activity_icons = { |
|
"task_created": "📝", |
|
"task_completed": "✅", |
|
"note_created": "📄", |
|
"note_updated": "📝", |
|
"goal_created": "🎯", |
|
"goal_completed": "🏆", |
|
"app_opened": "🚀" |
|
} |
|
|
|
icon = activity_icons.get(safe_get(activity, "type", ""), "🔔") |
|
|
|
|
|
activity_messages = { |
|
"task_created": "created task", |
|
"task_completed": "completed task", |
|
"note_created": "created note", |
|
"note_updated": "updated note", |
|
"goal_created": "set goal", |
|
"goal_completed": "achieved goal", |
|
"app_opened": "opened the app" |
|
} |
|
|
|
message = activity_messages.get(safe_get(activity, "type", ""), "performed action") |
|
title = safe_get(activity, "title", "") |
|
|
|
|
|
with gr.Group(elem_classes=["activity-item"]) as item: |
|
with gr.Row(): |
|
gr.Markdown(f"{icon} **{formatted_time}** - You {message}") |
|
if title: |
|
gr.Markdown(f"**{title}**") |
|
|
|
return item |
|
|
|
def get_timestamp() -> str: |
|
""" |
|
Get current timestamp in ISO format |
|
|
|
Returns: |
|
Current timestamp as ISO formatted string |
|
""" |
|
return datetime.datetime.now().isoformat() |
|
|
|
@handle_ui_exceptions |
|
def create_deadline_item(task: Dict[str, Any]) -> gr.Group: |
|
""" |
|
Create a deadline tracker item |
|
|
|
Args: |
|
task: Task data with deadline |
|
|
|
Returns: |
|
Gradio Group component |
|
|
|
Raises: |
|
ValidationError: If task data is invalid |
|
""" |
|
if not task: |
|
logger.warning("Creating deadline item with empty task data") |
|
task = {"title": "Untitled Task"} |
|
|
|
logger.debug(f"Creating deadline item for task: {safe_get(task, 'title', 'Untitled')}") |
|
|
|
|
|
try: |
|
deadline = datetime.datetime.fromisoformat(safe_get(task, "deadline", "")) |
|
today = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) |
|
days_left = (deadline.date() - today.date()).days |
|
|
|
if days_left < 0: |
|
status = "overdue" |
|
status_text = "Overdue" |
|
elif days_left == 0: |
|
status = "today" |
|
status_text = "Today" |
|
elif days_left == 1: |
|
status = "tomorrow" |
|
status_text = "Tomorrow" |
|
elif days_left < 7: |
|
status = "soon" |
|
status_text = f"{days_left} days left" |
|
else: |
|
status = "future" |
|
status_text = f"{days_left} days left" |
|
|
|
formatted_date = deadline.strftime("%b %d") |
|
except (ValueError, TypeError) as e: |
|
logger.warning(f"Error formatting deadline: {str(e)}") |
|
status = "unknown" |
|
status_text = "No deadline" |
|
formatted_date = "Unknown" |
|
days_left = 0 |
|
|
|
|
|
with gr.Group(elem_classes=["deadline-item", f"deadline-{status}"]) as item: |
|
with gr.Row(): |
|
gr.Markdown(f"**{safe_get(task, 'title', 'Untitled Task')}**") |
|
with gr.Row(): |
|
gr.Markdown(f"📅 {formatted_date} - {status_text}") |
|
|
|
return item |
|
|
|
@handle_ui_exceptions |
|
def create_kanban_board(tasks: List[Dict[str, Any]], on_card_click: Callable = None) -> gr.Group: |
|
""" |
|
Create a Kanban board view for tasks |
|
|
|
Args: |
|
tasks: List of tasks |
|
on_card_click: Callback function when a card is clicked |
|
|
|
Returns: |
|
Gradio Group component with the Kanban board |
|
|
|
Raises: |
|
ValidationError: If tasks data is invalid |
|
""" |
|
if not isinstance(tasks, list): |
|
logger.warning(f"Invalid tasks data for Kanban board: {type(tasks)}, using empty list") |
|
tasks = [] |
|
|
|
logger.debug(f"Creating Kanban board with {len(tasks)} tasks") |
|
|
|
|
|
todo_tasks = [task for task in tasks if safe_get(task, "status", "") == "todo"] |
|
in_progress_tasks = [task for task in tasks if safe_get(task, "status", "") == "in_progress"] |
|
done_tasks = [task for task in tasks if safe_get(task, "status", "") == "done"] |
|
|
|
logger.debug(f"Task distribution: {len(todo_tasks)} todo, {len(in_progress_tasks)} in progress, {len(done_tasks)} done") |
|
|
|
|
|
with gr.Group(elem_classes=["kanban-board"]) as board: |
|
with gr.Row(): |
|
|
|
with gr.Column(elem_classes=["kanban-column", "kanban-todo"]): |
|
gr.Markdown("### 📋 To Do") |
|
for task in todo_tasks: |
|
with gr.Group(elem_classes=["kanban-card"]) as card: |
|
gr.Markdown(f"**{safe_get(task, 'title', 'Untitled Task')}**") |
|
if safe_get(task, "description", ""): |
|
gr.Markdown(task["description"]) |
|
with gr.Row(): |
|
if safe_get(task, "priority", ""): |
|
priority_labels = {"high": "🔴 High", "medium": "🟠 Medium", "low": "🟢 Low"} |
|
gr.Markdown(priority_labels.get(task["priority"], task["priority"])) |
|
if safe_get(task, "deadline", ""): |
|
try: |
|
deadline = datetime.datetime.fromisoformat(task["deadline"]) |
|
gr.Markdown(f"📅 {deadline.strftime('%b %d')}") |
|
except (ValueError, TypeError) as e: |
|
logger.warning(f"Error formatting deadline: {str(e)}") |
|
|
|
|
|
if on_card_click: |
|
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[]) |
|
|
|
|
|
with gr.Column(elem_classes=["kanban-column", "kanban-in-progress"]): |
|
gr.Markdown("### 🔄 In Progress") |
|
for task in in_progress_tasks: |
|
with gr.Group(elem_classes=["kanban-card"]) as card: |
|
gr.Markdown(f"**{safe_get(task, 'title', 'Untitled Task')}**") |
|
if safe_get(task, "description", ""): |
|
gr.Markdown(task["description"]) |
|
with gr.Row(): |
|
if safe_get(task, "priority", ""): |
|
priority_labels = {"high": "🔴 High", "medium": "🟠 Medium", "low": "🟢 Low"} |
|
gr.Markdown(priority_labels.get(task["priority"], task["priority"])) |
|
if safe_get(task, "deadline", ""): |
|
try: |
|
deadline = datetime.datetime.fromisoformat(task["deadline"]) |
|
gr.Markdown(f"📅 {deadline.strftime('%b %d')}") |
|
except (ValueError, TypeError) as e: |
|
logger.warning(f"Error formatting deadline: {str(e)}") |
|
|
|
|
|
if on_card_click: |
|
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[]) |
|
|
|
|
|
with gr.Column(elem_classes=["kanban-column", "kanban-done"]): |
|
gr.Markdown("### ✅ Done") |
|
for task in done_tasks: |
|
with gr.Group(elem_classes=["kanban-card"]) as card: |
|
gr.Markdown(f"**{safe_get(task, 'title', 'Untitled Task')}**") |
|
if safe_get(task, "description", ""): |
|
gr.Markdown(task["description"]) |
|
with gr.Row(): |
|
if safe_get(task, "completed_at", ""): |
|
try: |
|
completed_at = datetime.datetime.fromisoformat(task["completed_at"]) |
|
gr.Markdown(f"✅ {completed_at.strftime('%b %d')}") |
|
except (ValueError, TypeError) as e: |
|
logger.warning(f"Error formatting completion date: {str(e)}") |
|
|
|
|
|
if on_card_click: |
|
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[]) |
|
|
|
return board |
|
|
|
@handle_ui_exceptions |
|
def create_priority_matrix(tasks: List[Dict[str, Any]], on_card_click: Callable = None) -> gr.Group: |
|
""" |
|
Create an Eisenhower Matrix (Priority Matrix) for tasks |
|
|
|
Args: |
|
tasks: List of tasks |
|
on_card_click: Callback function when a card is clicked |
|
|
|
Returns: |
|
Gradio Group component with the Priority Matrix |
|
|
|
Raises: |
|
ValidationError: If tasks data is invalid |
|
""" |
|
if not isinstance(tasks, list): |
|
logger.warning(f"Invalid tasks data for Priority Matrix: {type(tasks)}, using empty list") |
|
tasks = [] |
|
|
|
logger.debug(f"Creating Priority Matrix with {len(tasks)} tasks") |
|
|
|
|
|
urgent_important = [] |
|
urgent_not_important = [] |
|
not_urgent_important = [] |
|
not_urgent_not_important = [] |
|
|
|
for task in tasks: |
|
if safe_get(task, "completed", False): |
|
continue |
|
|
|
urgency = safe_get(task, "urgency", "low") |
|
importance = safe_get(task, "importance", "low") |
|
|
|
if urgency in ["high", "urgent"] and importance in ["high", "important"]: |
|
urgent_important.append(task) |
|
elif urgency in ["high", "urgent"] and importance in ["low", "not important"]: |
|
urgent_not_important.append(task) |
|
elif urgency in ["low", "not urgent"] and importance in ["high", "important"]: |
|
not_urgent_important.append(task) |
|
else: |
|
not_urgent_not_important.append(task) |
|
|
|
|
|
with gr.Group(elem_classes=["priority-matrix"]) as matrix: |
|
with gr.Row(): |
|
|
|
with gr.Column(elem_classes=["matrix-quadrant", "urgent-important"]): |
|
gr.Markdown("### 🔴 Do First") |
|
gr.Markdown("Urgent & Important") |
|
for task in urgent_important: |
|
with gr.Group(elem_classes=["matrix-card"]) as card: |
|
gr.Markdown(f"**{safe_get(task, 'title', 'Untitled Task')}**") |
|
|
|
|
|
if on_card_click: |
|
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[]) |
|
|
|
|
|
with gr.Column(elem_classes=["matrix-quadrant", "urgent-not-important"]): |
|
gr.Markdown("### 🟠 Delegate") |
|
gr.Markdown("Urgent & Not Important") |
|
for task in urgent_not_important: |
|
with gr.Group(elem_classes=["matrix-card"]) as card: |
|
gr.Markdown(f"**{safe_get(task, 'title', 'Untitled Task')}**") |
|
|
|
|
|
if on_card_click: |
|
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[]) |
|
|
|
with gr.Row(): |
|
|
|
with gr.Column(elem_classes=["matrix-quadrant", "not-urgent-important"]): |
|
gr.Markdown("### 🟡 Schedule") |
|
gr.Markdown("Not Urgent & Important") |
|
for task in not_urgent_important: |
|
with gr.Group(elem_classes=["matrix-card"]) as card: |
|
gr.Markdown(f"**{safe_get(task, 'title', 'Untitled Task')}**") |
|
|
|
|
|
if on_card_click: |
|
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[]) |
|
|
|
|
|
with gr.Column(elem_classes=["matrix-quadrant", "not-urgent-not-important"]): |
|
gr.Markdown("### 🟢 Eliminate") |
|
gr.Markdown("Not Urgent & Not Important") |
|
for task in not_urgent_not_important: |
|
with gr.Group(elem_classes=["matrix-card"]) as card: |
|
gr.Markdown(f"**{safe_get(task, 'title', 'Untitled Task')}**") |
|
|
|
|
|
if on_card_click: |
|
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[]) |
|
|
|
return matrix |
|
|
|
@handle_ui_exceptions |
|
def create_streak_counter(days: int) -> gr.Group: |
|
""" |
|
Create a streak counter component |
|
|
|
Args: |
|
days: Number of consecutive days |
|
|
|
Returns: |
|
Gradio Group component with the streak counter |
|
|
|
Raises: |
|
ValidationError: If days is not a non-negative integer |
|
""" |
|
if not isinstance(days, int) or days < 0: |
|
logger.warning(f"Invalid days value for streak counter: {days}, using 0") |
|
days = 0 |
|
|
|
logger.debug(f"Creating streak counter: {days} days") |
|
|
|
with gr.Group(elem_classes=["streak-counter"]) as streak: |
|
gr.Markdown(f"### 🔥 {days} Day Streak") |
|
|
|
|
|
if days > 0: |
|
|
|
emoji_count = min(days, 7) |
|
emoji_row = "🔥" * emoji_count |
|
gr.Markdown(emoji_row) |
|
|
|
|
|
if days == 1: |
|
gr.Markdown("Great start! Keep it going tomorrow.") |
|
elif days < 3: |
|
gr.Markdown("You're building momentum!") |
|
elif days < 7: |
|
gr.Markdown("Impressive consistency!") |
|
elif days < 14: |
|
gr.Markdown("You're on fire this week!") |
|
elif days < 30: |
|
gr.Markdown("Amazing dedication!") |
|
else: |
|
gr.Markdown("Incredible commitment! You're unstoppable!") |
|
else: |
|
gr.Markdown("Start your streak today!") |
|
|
|
return streak |
|
|
|
@handle_ui_exceptions |
|
def create_weather_widget(weather_data: Dict[str, Any]) -> gr.Group: |
|
""" |
|
Create a weather widget |
|
|
|
Args: |
|
weather_data: Weather information |
|
|
|
Returns: |
|
Gradio Group component with the weather widget |
|
|
|
Raises: |
|
ValidationError: If weather_data is invalid |
|
""" |
|
if not isinstance(weather_data, dict): |
|
logger.warning(f"Invalid weather data: {type(weather_data)}, using empty dict") |
|
weather_data = {} |
|
|
|
logger.debug(f"Creating weather widget for location: {safe_get(weather_data, 'location', 'Unknown')}") |
|
|
|
|
|
weather_icons = { |
|
"Sunny": "☀️", |
|
"Clear": "☀️", |
|
"Partly Cloudy": "⛅", |
|
"Cloudy": "☁️", |
|
"Overcast": "☁️", |
|
"Rain": "🌧️", |
|
"Showers": "🌦️", |
|
"Thunderstorm": "⛈️", |
|
"Snow": "❄️", |
|
"Fog": "🌫️" |
|
} |
|
|
|
condition = safe_get(weather_data, "condition", "Unknown") |
|
icon = weather_icons.get(condition, "🌡️") |
|
|
|
with gr.Group(elem_classes=["weather-widget"]) as widget: |
|
gr.Markdown(f"### {icon} Weather - {safe_get(weather_data, 'location', 'Unknown')}") |
|
gr.Markdown(f"**{safe_get(weather_data, 'temperature', 0)}°C** - {condition}") |
|
gr.Markdown(f"Humidity: {safe_get(weather_data, 'humidity', 0)}% | Wind: {safe_get(weather_data, 'wind_speed', 0)} km/h") |
|
|
|
|
|
if "forecast" in weather_data and weather_data["forecast"]: |
|
gr.Markdown("**Forecast:**") |
|
forecast_text = "" |
|
for day in weather_data["forecast"][:3]: |
|
day_icon = weather_icons.get(safe_get(day, "condition", "Unknown"), "🌡️") |
|
forecast_text += f"{safe_get(day, 'day', 'Day')}: {day_icon} {safe_get(day, 'high', 0)}°C/{safe_get(day, 'low', 0)}°C " |
|
gr.Markdown(forecast_text) |
|
|
|
return widget |