|
|
import gradio as gr |
|
|
import datetime |
|
|
from typing import Dict, List, Any, Union, Optional |
|
|
import random |
|
|
|
|
|
|
|
|
from utils.storage import load_data, save_data |
|
|
from utils.state import generate_id, get_timestamp, record_activity |
|
|
from utils.ai_models import break_down_task, estimate_task_time |
|
|
from utils.ui_components import create_kanban_board, create_priority_matrix |
|
|
from utils.config import FILE_PATHS |
|
|
from utils.logging import get_logger |
|
|
from utils.error_handling import handle_exceptions, ValidationError, safe_get |
|
|
|
|
|
|
|
|
logger = get_logger(__name__) |
|
|
|
|
|
@handle_exceptions |
|
|
def create_tasks_page(state: Dict[str, Any]) -> None: |
|
|
""" |
|
|
Create the tasks and projects page with multiple views |
|
|
|
|
|
Args: |
|
|
state: Application state |
|
|
""" |
|
|
logger.info("Creating tasks page") |
|
|
|
|
|
|
|
|
with gr.Column(elem_id="tasks-page"): |
|
|
gr.Markdown("# 📋 Tasks & Projects") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
|
|
|
view_selector = gr.Radio( |
|
|
choices=["Kanban", "List", "Calendar", "Timeline", "Priority Matrix"], |
|
|
value="Kanban", |
|
|
label="View", |
|
|
elem_id="task-view-selector" |
|
|
) |
|
|
|
|
|
|
|
|
add_task_btn = gr.Button("➕ Add Task", elem_classes=["action-button"]) |
|
|
|
|
|
|
|
|
with gr.Group(elem_id="task-views-container"): |
|
|
|
|
|
with gr.Group(elem_id="kanban-view", visible=True) as kanban_view: |
|
|
create_kanban_board(safe_get(state, "tasks", [])) |
|
|
|
|
|
|
|
|
with gr.Group(elem_id="list-view", visible=False) as list_view: |
|
|
with gr.Column(): |
|
|
|
|
|
with gr.Row(): |
|
|
status_filter = gr.Dropdown( |
|
|
choices=["All", "To Do", "In Progress", "Done"], |
|
|
value="All", |
|
|
label="Status" |
|
|
) |
|
|
priority_filter = gr.Dropdown( |
|
|
choices=["All", "High", "Medium", "Low"], |
|
|
value="All", |
|
|
label="Priority" |
|
|
) |
|
|
sort_by = gr.Dropdown( |
|
|
choices=["Created (Newest)", "Created (Oldest)", "Due Date", "Priority"], |
|
|
value="Created (Newest)", |
|
|
label="Sort By" |
|
|
) |
|
|
|
|
|
|
|
|
task_list = gr.Dataframe( |
|
|
headers=["Title", "Status", "Priority", "Due Date", "Created"], |
|
|
datatype=["str", "str", "str", "str", "str"], |
|
|
col_count=(5, "fixed"), |
|
|
elem_id="task-list-table" |
|
|
) |
|
|
|
|
|
|
|
|
@handle_exceptions |
|
|
def update_task_list(status, priority, sort): |
|
|
"""Update the task list based on filters and sort options""" |
|
|
logger.debug(f"Updating task list with filters: status={status}, priority={priority}, sort={sort}") |
|
|
tasks = safe_get(state, "tasks", []) |
|
|
|
|
|
|
|
|
if status != "All": |
|
|
status_map = {"To Do": "todo", "In Progress": "in_progress", "Done": "done"} |
|
|
tasks = [t for t in tasks if safe_get(t, "status", "") == status_map.get(status, "")] |
|
|
|
|
|
|
|
|
if priority != "All": |
|
|
tasks = [t for t in tasks if safe_get(t, "priority", "").lower() == priority.lower()] |
|
|
|
|
|
|
|
|
if sort == "Created (Newest)": |
|
|
tasks.sort(key=lambda x: safe_get(x, "created_at", ""), reverse=True) |
|
|
elif sort == "Created (Oldest)": |
|
|
tasks.sort(key=lambda x: safe_get(x, "created_at", "")) |
|
|
elif sort == "Due Date": |
|
|
|
|
|
tasks.sort( |
|
|
key=lambda x: safe_get(x, "deadline", "9999-12-31T23:59:59") |
|
|
) |
|
|
elif sort == "Priority": |
|
|
|
|
|
priority_order = {"high": 0, "medium": 1, "low": 2} |
|
|
tasks.sort( |
|
|
key=lambda x: priority_order.get(safe_get(x, "priority", "").lower(), 3) |
|
|
) |
|
|
|
|
|
|
|
|
table_data = [] |
|
|
for task in tasks: |
|
|
|
|
|
status_map = {"todo": "To Do", "in_progress": "In Progress", "done": "Done"} |
|
|
status_str = status_map.get(safe_get(task, "status", ""), "To Do") |
|
|
|
|
|
|
|
|
priority_str = safe_get(task, "priority", "").capitalize() |
|
|
|
|
|
|
|
|
due_date = "None" |
|
|
if "deadline" in task: |
|
|
try: |
|
|
deadline = datetime.datetime.fromisoformat(task["deadline"]) |
|
|
due_date = deadline.strftime("%Y-%m-%d") |
|
|
except Exception as e: |
|
|
logger.warning(f"Error formatting deadline: {str(e)}") |
|
|
|
|
|
|
|
|
created = "Unknown" |
|
|
if "created_at" in task: |
|
|
try: |
|
|
created_at = datetime.datetime.fromisoformat(task["created_at"]) |
|
|
created = created_at.strftime("%Y-%m-%d") |
|
|
except Exception as e: |
|
|
logger.warning(f"Error formatting created_at: {str(e)}") |
|
|
|
|
|
table_data.append([ |
|
|
safe_get(task, "title", "Untitled Task"), |
|
|
status_str, |
|
|
priority_str, |
|
|
due_date, |
|
|
created |
|
|
]) |
|
|
|
|
|
return table_data |
|
|
|
|
|
|
|
|
status_filter.change( |
|
|
update_task_list, |
|
|
inputs=[status_filter, priority_filter, sort_by], |
|
|
outputs=[task_list] |
|
|
) |
|
|
priority_filter.change( |
|
|
update_task_list, |
|
|
inputs=[status_filter, priority_filter, sort_by], |
|
|
outputs=[task_list] |
|
|
) |
|
|
sort_by.change( |
|
|
update_task_list, |
|
|
inputs=[status_filter, priority_filter, sort_by], |
|
|
outputs=[task_list] |
|
|
) |
|
|
|
|
|
|
|
|
task_list.value = update_task_list("All", "All", "Created (Newest)") |
|
|
|
|
|
|
|
|
with gr.Group(elem_id="calendar-view", visible=False) as calendar_view: |
|
|
gr.Markdown("### 📅 Calendar View") |
|
|
gr.Markdown("*Calendar view will display tasks with due dates*") |
|
|
|
|
|
|
|
|
current_month = datetime.datetime.now().strftime("%B %Y") |
|
|
gr.Markdown(f"## {current_month}") |
|
|
|
|
|
|
|
|
days_of_week = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] |
|
|
with gr.Row(): |
|
|
for day in days_of_week: |
|
|
gr.Markdown(f"**{day}**") |
|
|
|
|
|
|
|
|
now = datetime.datetime.now() |
|
|
month_start = datetime.datetime(now.year, now.month, 1) |
|
|
month_end = (month_start.replace(month=month_start.month+1, day=1) - |
|
|
datetime.timedelta(days=1)) |
|
|
start_weekday = month_start.weekday() |
|
|
days_in_month = month_end.day |
|
|
|
|
|
|
|
|
start_weekday = (start_weekday + 1) % 7 |
|
|
|
|
|
|
|
|
day_counter = 1 |
|
|
for week in range(6): |
|
|
with gr.Row(): |
|
|
for weekday in range(7): |
|
|
if (week == 0 and weekday < start_weekday) or day_counter > days_in_month: |
|
|
|
|
|
gr.Markdown("") |
|
|
else: |
|
|
|
|
|
day_tasks = [] |
|
|
for task in safe_get(state, "tasks", []): |
|
|
if "deadline" in task: |
|
|
try: |
|
|
deadline = datetime.datetime.fromisoformat(task["deadline"]) |
|
|
if (deadline.year == now.year and |
|
|
deadline.month == now.month and |
|
|
deadline.day == day_counter): |
|
|
day_tasks.append(task) |
|
|
except Exception as e: |
|
|
logger.warning(f"Error parsing deadline: {str(e)}") |
|
|
|
|
|
|
|
|
with gr.Group(elem_classes=["calendar-day"]): |
|
|
gr.Markdown(f"**{day_counter}**") |
|
|
|
|
|
|
|
|
for task in day_tasks: |
|
|
priority_colors = {"high": "red", "medium": "orange", "low": "green"} |
|
|
priority = safe_get(task, "priority", "").lower() |
|
|
color = priority_colors.get(priority, "gray") |
|
|
|
|
|
gr.Markdown( |
|
|
f"<span style='color: {color};'>●</span> {safe_get(task, 'title', 'Untitled')}" |
|
|
) |
|
|
|
|
|
day_counter += 1 |
|
|
|
|
|
if day_counter > days_in_month: |
|
|
break |
|
|
|
|
|
if day_counter > days_in_month: |
|
|
break |
|
|
|
|
|
|
|
|
with gr.Group(elem_id="timeline-view", visible=False) as timeline_view: |
|
|
gr.Markdown("### ⏱️ Timeline View") |
|
|
gr.Markdown("*Timeline view will display tasks in chronological order*") |
|
|
|
|
|
|
|
|
dated_tasks = [] |
|
|
for task in safe_get(state, "tasks", []): |
|
|
if "deadline" in task or "created_at" in task: |
|
|
dated_tasks.append(task) |
|
|
|
|
|
|
|
|
dated_tasks.sort(key=lambda x: safe_get(x, "deadline", safe_get(x, "created_at", ""))) |
|
|
|
|
|
|
|
|
tasks_by_month = {} |
|
|
for task in dated_tasks: |
|
|
date_str = safe_get(task, "deadline", safe_get(task, "created_at", "")) |
|
|
try: |
|
|
date = datetime.datetime.fromisoformat(date_str) |
|
|
month_key = date.strftime("%Y-%m") |
|
|
if month_key not in tasks_by_month: |
|
|
tasks_by_month[month_key] = [] |
|
|
tasks_by_month[month_key].append((date, task)) |
|
|
except Exception as e: |
|
|
logger.warning(f"Error parsing date: {str(e)}") |
|
|
|
|
|
|
|
|
for month_key in sorted(tasks_by_month.keys()): |
|
|
try: |
|
|
month_date = datetime.datetime.strptime(month_key, "%Y-%m") |
|
|
month_name = month_date.strftime("%B %Y") |
|
|
gr.Markdown(f"## {month_name}") |
|
|
|
|
|
|
|
|
for date, task in sorted(tasks_by_month[month_key]): |
|
|
day_str = date.strftime("%d %b") |
|
|
status_emoji = "🔴" if safe_get(task, "status") == "todo" else \ |
|
|
"🟡" if safe_get(task, "status") == "in_progress" else "🟢" |
|
|
|
|
|
with gr.Group(elem_classes=["timeline-item"]): |
|
|
with gr.Row(): |
|
|
gr.Markdown(f"**{day_str}**") |
|
|
gr.Markdown(f"{status_emoji} {safe_get(task, 'title', 'Untitled Task')}") |
|
|
|
|
|
if safe_get(task, "description"): |
|
|
gr.Markdown(task["description"]) |
|
|
except Exception as e: |
|
|
logger.warning(f"Error displaying timeline month: {str(e)}") |
|
|
|
|
|
|
|
|
with gr.Group(elem_id="priority-matrix-view", visible=False) as priority_matrix_view: |
|
|
create_priority_matrix(safe_get(state, "tasks", [])) |
|
|
|
|
|
|
|
|
with gr.Group(elem_id="task-detail-modal", visible=False) as task_detail_modal: |
|
|
gr.Markdown("## Task Details") |
|
|
|
|
|
|
|
|
task_title_display = gr.Textbox(label="Title", interactive=True) |
|
|
task_description_display = gr.Textbox(label="Description", lines=3, interactive=True) |
|
|
task_status_display = gr.Dropdown( |
|
|
choices=["To Do", "In Progress", "Done"], |
|
|
label="Status", |
|
|
interactive=True |
|
|
) |
|
|
task_priority_display = gr.Dropdown( |
|
|
choices=["High", "Medium", "Low"], |
|
|
label="Priority", |
|
|
interactive=True |
|
|
) |
|
|
task_deadline_display = gr.Textbox( |
|
|
label="Deadline (YYYY-MM-DD)", |
|
|
interactive=True |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
save_task_btn = gr.Button("Save Changes") |
|
|
delete_task_btn = gr.Button("Delete Task") |
|
|
close_detail_btn = gr.Button("Close") |
|
|
|
|
|
|
|
|
with gr.Accordion("AI Features", open=False): |
|
|
|
|
|
gr.Markdown("### 🤖 AI Task Breakdown") |
|
|
generate_subtasks_btn = gr.Button("Generate Subtasks") |
|
|
subtasks_display = gr.Markdown("*Click the button to generate subtasks*") |
|
|
|
|
|
|
|
|
gr.Markdown("### ⏱️ AI Time Estimation") |
|
|
estimate_time_btn = gr.Button("Estimate Time") |
|
|
time_estimate_display = gr.Markdown("*Click the button to estimate time*") |
|
|
|
|
|
|
|
|
with gr.Group(): |
|
|
gr.Markdown("### Subtasks") |
|
|
subtask_list = gr.Dataframe( |
|
|
headers=["Subtask", "Status"], |
|
|
datatype=["str", "str"], |
|
|
col_count=(2, "fixed"), |
|
|
row_count=(0, "dynamic"), |
|
|
elem_id="subtask-list" |
|
|
) |
|
|
add_subtask_btn = gr.Button("Add Subtask") |
|
|
|
|
|
|
|
|
with gr.Group(elem_id="add-task-modal", visible=False) as add_task_modal: |
|
|
gr.Markdown("## Add New Task") |
|
|
|
|
|
|
|
|
new_task_title = gr.Textbox(label="Title", placeholder="Enter task title") |
|
|
new_task_description = gr.Textbox( |
|
|
label="Description", |
|
|
placeholder="Enter task description", |
|
|
lines=3 |
|
|
) |
|
|
new_task_status = gr.Dropdown( |
|
|
choices=["To Do", "In Progress", "Done"], |
|
|
label="Status", |
|
|
value="To Do" |
|
|
) |
|
|
new_task_priority = gr.Dropdown( |
|
|
choices=["High", "Medium", "Low"], |
|
|
label="Priority", |
|
|
value="Medium" |
|
|
) |
|
|
new_task_deadline = gr.Textbox( |
|
|
label="Deadline (YYYY-MM-DD)", |
|
|
placeholder="YYYY-MM-DD" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Accordion("Smart Categorization", open=False): |
|
|
gr.Markdown("### 🏷️ Tags") |
|
|
new_task_tags = gr.Textbox( |
|
|
label="Tags (comma separated)", |
|
|
placeholder="work, project, urgent" |
|
|
) |
|
|
|
|
|
gr.Markdown("### 📂 Project") |
|
|
new_task_project = gr.Dropdown( |
|
|
choices=["None", "Work", "Personal", "Study", "Health"], |
|
|
label="Project", |
|
|
value="None" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
create_task_btn = gr.Button("Create Task") |
|
|
cancel_add_btn = gr.Button("Cancel") |
|
|
|
|
|
|
|
|
@handle_exceptions |
|
|
def switch_view(view): |
|
|
"""Switch between different task views""" |
|
|
logger.info(f"Switching to {view} view") |
|
|
views = { |
|
|
"Kanban": kanban_view, |
|
|
"List": list_view, |
|
|
"Calendar": calendar_view, |
|
|
"Timeline": timeline_view, |
|
|
"Priority Matrix": priority_matrix_view |
|
|
} |
|
|
|
|
|
return [gr.update(visible=(view_name == view)) for view_name, view_component in views.items()] |
|
|
|
|
|
|
|
|
view_outputs = [kanban_view, list_view, calendar_view, timeline_view, priority_matrix_view] |
|
|
view_selector.change(switch_view, inputs=[view_selector], outputs=view_outputs) |
|
|
|
|
|
|
|
|
@handle_exceptions |
|
|
def show_add_task_modal(): |
|
|
"""Show the add task modal""" |
|
|
logger.debug("Showing add task modal") |
|
|
return gr.update(visible=True) |
|
|
|
|
|
|
|
|
@handle_exceptions |
|
|
def hide_add_task_modal(): |
|
|
"""Hide the add task modal""" |
|
|
logger.debug("Hiding add task modal") |
|
|
return gr.update(visible=False) |
|
|
|
|
|
|
|
|
add_task_btn.click(show_add_task_modal, inputs=[], outputs=[add_task_modal]) |
|
|
cancel_add_btn.click(hide_add_task_modal, inputs=[], outputs=[add_task_modal]) |
|
|
|
|
|
|
|
|
@handle_exceptions |
|
|
def create_task(title, description, status, priority, deadline, tags, project): |
|
|
"""Create a new task""" |
|
|
if not title.strip(): |
|
|
logger.warning("Attempted to create task with empty title") |
|
|
return "Please enter a task title", gr.update(visible=True) |
|
|
|
|
|
logger.info(f"Creating new task: {title}") |
|
|
|
|
|
|
|
|
deadline_iso = None |
|
|
if deadline.strip(): |
|
|
try: |
|
|
deadline_date = datetime.datetime.strptime(deadline.strip(), "%Y-%m-%d") |
|
|
deadline_iso = deadline_date.isoformat() |
|
|
except ValueError as e: |
|
|
logger.warning(f"Invalid date format: {deadline}, error: {str(e)}") |
|
|
return "Invalid date format. Use YYYY-MM-DD", gr.update(visible=True) |
|
|
|
|
|
|
|
|
status_map = {"To Do": "todo", "In Progress": "in_progress", "Done": "done"} |
|
|
|
|
|
|
|
|
new_task = { |
|
|
"id": generate_id(), |
|
|
"title": title.strip(), |
|
|
"description": description.strip(), |
|
|
"status": status_map.get(status, "todo"), |
|
|
"priority": priority.lower(), |
|
|
"completed": status == "Done", |
|
|
"created_at": get_timestamp() |
|
|
} |
|
|
|
|
|
if deadline_iso: |
|
|
new_task["deadline"] = deadline_iso |
|
|
|
|
|
|
|
|
if tags.strip(): |
|
|
new_task["tags"] = [tag.strip() for tag in tags.split(",") if tag.strip()] |
|
|
|
|
|
|
|
|
if project != "None": |
|
|
new_task["project"] = project |
|
|
|
|
|
|
|
|
state["tasks"].append(new_task) |
|
|
state["stats"]["tasks_total"] += 1 |
|
|
if status == "Done": |
|
|
state["stats"]["tasks_completed"] += 1 |
|
|
new_task["completed_at"] = get_timestamp() |
|
|
|
|
|
|
|
|
record_activity({ |
|
|
"type": "task_created", |
|
|
"title": title, |
|
|
"timestamp": datetime.datetime.now().isoformat() |
|
|
}) |
|
|
|
|
|
|
|
|
save_data(FILE_PATHS["tasks"], state["tasks"]) |
|
|
|
|
|
|
|
|
return "Task created successfully!", gr.update(visible=False) |
|
|
|
|
|
|
|
|
create_task_btn.click( |
|
|
create_task, |
|
|
inputs=[ |
|
|
new_task_title, new_task_description, new_task_status, |
|
|
new_task_priority, new_task_deadline, new_task_tags, new_task_project |
|
|
], |
|
|
outputs=[gr.Markdown(visible=False), add_task_modal] |
|
|
) |
|
|
|
|
|
|
|
|
@handle_exceptions |
|
|
def generate_subtasks(title, description): |
|
|
"""Generate subtasks using AI""" |
|
|
logger.info(f"Generating subtasks for task: {title}") |
|
|
subtasks = break_down_task(title, description) |
|
|
|
|
|
|
|
|
subtasks_md = "**Suggested Subtasks:**\n\n" |
|
|
for subtask in subtasks: |
|
|
subtasks_md += f"- {subtask}\n" |
|
|
|
|
|
return subtasks_md |
|
|
|
|
|
|
|
|
generate_subtasks_btn.click( |
|
|
generate_subtasks, |
|
|
inputs=[task_title_display, task_description_display], |
|
|
outputs=[subtasks_display] |
|
|
) |
|
|
|
|
|
|
|
|
@handle_exceptions |
|
|
def estimate_task_time_ai(title, description): |
|
|
"""Estimate task time using AI""" |
|
|
logger.info(f"Estimating time for task: {title}") |
|
|
minutes = estimate_task_time(title, description) |
|
|
|
|
|
|
|
|
if minutes < 60: |
|
|
time_str = f"{minutes} minutes" |
|
|
else: |
|
|
hours = minutes // 60 |
|
|
remaining_minutes = minutes % 60 |
|
|
time_str = f"{hours} hour{'s' if hours > 1 else ''}" |
|
|
if remaining_minutes > 0: |
|
|
time_str += f" {remaining_minutes} minute{'s' if remaining_minutes > 1 else ''}" |
|
|
|
|
|
return f"**Estimated Time:** {time_str}" |
|
|
|
|
|
|
|
|
estimate_time_btn.click( |
|
|
estimate_task_time_ai, |
|
|
inputs=[task_title_display, task_description_display], |
|
|
outputs=[time_estimate_display] |
|
|
) |