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
# Initialize logger
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
"""
# Validate inputs
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})")
# Calculate percentage
percentage = min(100, max(0, (value / max_value) * 100))
# Define colors based on the color parameter
ring_color = UI_COLORS.get(color, UI_COLORS["blue"])
# Create the HTML/CSS for the progress ring
html = f"""
{int(percentage)}%
{f'
{label}
' if label else ''}
"""
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')}")
# Format timestamp
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"
# Get activity type icon
activity_icons = {
"task_created": "📝",
"task_completed": "✅",
"note_created": "📄",
"note_updated": "📝",
"goal_created": "🎯",
"goal_completed": "🏆",
"app_opened": "🚀"
}
icon = activity_icons.get(safe_get(activity, "type", ""), "🔔")
# Get activity message
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", "")
# Create the activity item
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')}")
# Format deadline
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
# Create the deadline item
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")
# Group tasks by status
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")
# Create the Kanban board
with gr.Group(elem_classes=["kanban-board"]) as board:
with gr.Row():
# To Do column
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)}")
# Add click event if callback provided
if on_card_click:
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[])
# In Progress column
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)}")
# Add click event if callback provided
if on_card_click:
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[])
# Done column
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)}")
# Add click event if callback provided
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")
# Group tasks by urgency and importance
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)
# Create the Priority Matrix
with gr.Group(elem_classes=["priority-matrix"]) as matrix:
with gr.Row():
# Urgent & Important
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')}**")
# Add click event if callback provided
if on_card_click:
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[])
# Urgent & Not Important
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')}**")
# Add click event if callback provided
if on_card_click:
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[])
with gr.Row():
# Not Urgent & Important
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')}**")
# Add click event if callback provided
if on_card_click:
card.click(lambda t=task: on_card_click(t), inputs=[], outputs=[])
# Not Urgent & Not Important
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')}**")
# Add click event if callback provided
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")
# Create a visual representation of the streak
if days > 0:
# Create a row of fire emojis
emoji_count = min(days, 7) # Limit to 7 emojis
emoji_row = "🔥" * emoji_count
gr.Markdown(emoji_row)
# Add a motivational message based on streak length
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 condition icons
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")
# Add forecast if available
if "forecast" in weather_data and weather_data["forecast"]:
gr.Markdown("**Forecast:**")
forecast_text = ""
for day in weather_data["forecast"][:3]: # Show up to 3 days
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