File size: 22,948 Bytes
8e4018d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
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"""
    <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')}")
    
    # 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