Update pages/dashboard.py
Browse files- pages/dashboard.py +144 -395
pages/dashboard.py
CHANGED
@@ -1,408 +1,157 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
-
# Import utilities
|
7 |
-
from utils.storage import load_data, save_data
|
8 |
-
from utils.state import generate_id, get_timestamp, record_activity
|
9 |
-
from utils.ai_models import generate_motivation_quote, generate_daily_plan, get_weather
|
10 |
-
from utils.ui_components import (
|
11 |
-
create_card, create_stat_card, create_progress_ring, create_activity_item,
|
12 |
-
create_deadline_item, create_streak_counter, create_weather_widget
|
13 |
-
)
|
14 |
-
from utils.config import FILE_PATHS
|
15 |
-
from utils.logging import get_logger
|
16 |
-
from utils.error_handling import handle_exceptions, ValidationError, safe_get
|
17 |
|
18 |
# Initialize logger
|
19 |
logger = get_logger(__name__)
|
20 |
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
"""
|
24 |
-
Create
|
25 |
|
26 |
Args:
|
27 |
-
|
28 |
"""
|
29 |
-
|
|
|
|
|
30 |
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
-
#
|
36 |
-
|
37 |
-
# Today's Focus Widget
|
38 |
-
with gr.Column(scale=2):
|
39 |
-
with gr.Group(elem_classes=["focus-widget"]):
|
40 |
-
gr.Markdown("### 🎯 Today's Focus")
|
41 |
-
focus_input = gr.Textbox(
|
42 |
-
placeholder="What's your main focus for today?",
|
43 |
-
label="",
|
44 |
-
elem_id="focus-input"
|
45 |
-
)
|
46 |
-
motivation_text = gr.Markdown(
|
47 |
-
"*Set your focus for today to get an AI-powered motivation boost!*"
|
48 |
-
)
|
49 |
-
|
50 |
-
@handle_exceptions
|
51 |
-
def update_focus(focus_text):
|
52 |
-
"""Update the focus and generate motivation"""
|
53 |
-
if not focus_text.strip():
|
54 |
-
logger.debug("Empty focus text submitted")
|
55 |
-
return "*Set your focus for today to get an AI-powered motivation boost!*"
|
56 |
-
|
57 |
-
logger.info(f"Setting focus: {focus_text[:30]}...")
|
58 |
-
|
59 |
-
# Generate a motivational response
|
60 |
-
motivation = generate_motivation_quote()
|
61 |
-
|
62 |
-
# Record the activity
|
63 |
-
record_activity({
|
64 |
-
"type": "focus_set",
|
65 |
-
"title": focus_text,
|
66 |
-
"timestamp": datetime.datetime.now().isoformat()
|
67 |
-
})
|
68 |
-
|
69 |
-
return f"*{motivation}*"
|
70 |
-
|
71 |
-
focus_input.change(update_focus, inputs=[focus_input], outputs=[motivation_text])
|
72 |
-
|
73 |
-
# Quick Stats Cards
|
74 |
-
with gr.Column(scale=2):
|
75 |
-
with gr.Row():
|
76 |
-
create_stat_card("Tasks", safe_get(state, ["stats", "tasks_total"], 0), icon="📋", color="blue")
|
77 |
-
create_stat_card("Completed", safe_get(state, ["stats", "tasks_completed"], 0), icon="✅", color="green")
|
78 |
-
|
79 |
-
with gr.Row():
|
80 |
-
create_stat_card("Notes", safe_get(state, ["stats", "notes_total"], 0), icon="📝", color="yellow")
|
81 |
-
create_stat_card("Goals", safe_get(state, ["stats", "goals_total"], 0), icon="🎯", color="purple")
|
82 |
|
83 |
-
#
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
if safe_get(state, "activity_feed", []):
|
92 |
-
for activity in state["activity_feed"][:10]: # Show up to 10 activities
|
93 |
-
create_activity_item(activity)
|
94 |
-
else:
|
95 |
-
gr.Markdown("*No recent activity*")
|
96 |
-
|
97 |
-
# Right column - AI Daily Planner and Weather
|
98 |
-
with gr.Column(scale=2):
|
99 |
-
# AI Daily Planner
|
100 |
-
with gr.Group(elem_classes=["daily-planner"]):
|
101 |
-
gr.Markdown("### 📅 AI Daily Planner")
|
102 |
-
|
103 |
-
# Generate a daily plan based on tasks and goals
|
104 |
-
daily_plan = generate_daily_plan(
|
105 |
-
safe_get(state, "tasks", []),
|
106 |
-
safe_get(state, "goals", [])
|
107 |
-
)
|
108 |
-
|
109 |
-
gr.Markdown(daily_plan)
|
110 |
-
|
111 |
-
refresh_plan_btn = gr.Button("Refresh Plan")
|
112 |
-
|
113 |
-
@handle_exceptions
|
114 |
-
def refresh_daily_plan():
|
115 |
-
"""Refresh the daily plan"""
|
116 |
-
logger.debug("Refreshing daily plan")
|
117 |
-
return generate_daily_plan(
|
118 |
-
safe_get(state, "tasks", []),
|
119 |
-
safe_get(state, "goals", [])
|
120 |
-
)
|
121 |
-
|
122 |
-
refresh_plan_btn.click(refresh_daily_plan, inputs=[], outputs=[gr.Markdown(visible=False)])
|
123 |
-
|
124 |
-
# Weather Widget
|
125 |
-
weather_data = get_weather("New York") # Default city
|
126 |
-
create_weather_widget(weather_data)
|
127 |
-
|
128 |
-
# City input for weather
|
129 |
-
with gr.Row():
|
130 |
-
city_input = gr.Textbox(
|
131 |
-
placeholder="Enter city name",
|
132 |
-
label="Change Location",
|
133 |
-
elem_id="weather-city-input"
|
134 |
-
)
|
135 |
-
update_weather_btn = gr.Button("Update")
|
136 |
-
|
137 |
-
@handle_exceptions
|
138 |
-
def update_weather(city):
|
139 |
-
"""Update weather for the specified city"""
|
140 |
-
if not city.strip():
|
141 |
-
logger.debug("Empty city name, using default")
|
142 |
-
city = "New York" # Default city
|
143 |
-
|
144 |
-
logger.info(f"Updating weather for city: {city}")
|
145 |
-
weather_data = get_weather(city)
|
146 |
-
return create_weather_widget(weather_data)
|
147 |
-
|
148 |
-
update_weather_btn.click(
|
149 |
-
update_weather,
|
150 |
-
inputs=[city_input],
|
151 |
-
outputs=[gr.Group(visible=False)]
|
152 |
-
)
|
153 |
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
# Motivation Quotes
|
159 |
-
with gr.Group(elem_classes=["motivation-quotes"]):
|
160 |
-
gr.Markdown("### ✨ Daily Inspiration")
|
161 |
-
|
162 |
-
# Generate a motivational quote
|
163 |
-
quote = generate_motivation_quote()
|
164 |
-
quote_display = gr.Markdown(f"*\"{quote}\"*")
|
165 |
-
|
166 |
-
refresh_quote_btn = gr.Button("New Quote")
|
167 |
-
|
168 |
-
@handle_exceptions
|
169 |
-
def refresh_quote():
|
170 |
-
"""Refresh the motivational quote"""
|
171 |
-
logger.debug("Refreshing motivational quote")
|
172 |
-
return f"*\"{generate_motivation_quote()}\"*"
|
173 |
-
|
174 |
-
refresh_quote_btn.click(refresh_quote, inputs=[], outputs=[quote_display])
|
175 |
-
|
176 |
-
# Quick Entry Panel
|
177 |
-
with gr.Group(elem_classes=["quick-entry"]):
|
178 |
-
gr.Markdown("### ⚡ Quick Add")
|
179 |
-
|
180 |
-
quick_entry_tabs = gr.Tabs([
|
181 |
-
("Task", "task"),
|
182 |
-
("Note", "note"),
|
183 |
-
("Goal", "goal")
|
184 |
-
])
|
185 |
-
|
186 |
-
with quick_entry_tabs.select("task"):
|
187 |
-
task_title = gr.Textbox(placeholder="Task title", label="Title")
|
188 |
-
task_desc = gr.Textbox(placeholder="Task description (optional)", label="Description")
|
189 |
-
task_priority = gr.Dropdown(
|
190 |
-
choices=["High", "Medium", "Low"],
|
191 |
-
value="Medium",
|
192 |
-
label="Priority"
|
193 |
-
)
|
194 |
-
add_task_btn = gr.Button("Add Task")
|
195 |
-
|
196 |
-
with quick_entry_tabs.select("note"):
|
197 |
-
note_title = gr.Textbox(placeholder="Note title", label="Title")
|
198 |
-
note_content = gr.Textbox(
|
199 |
-
placeholder="Note content",
|
200 |
-
label="Content",
|
201 |
-
lines=3
|
202 |
-
)
|
203 |
-
add_note_btn = gr.Button("Add Note")
|
204 |
-
|
205 |
-
with quick_entry_tabs.select("goal"):
|
206 |
-
goal_title = gr.Textbox(placeholder="Goal title", label="Title")
|
207 |
-
goal_desc = gr.Textbox(placeholder="Goal description (optional)", label="Description")
|
208 |
-
goal_deadline = gr.Textbox(
|
209 |
-
placeholder="YYYY-MM-DD (optional)",
|
210 |
-
label="Target Date"
|
211 |
-
)
|
212 |
-
add_goal_btn = gr.Button("Add Goal")
|
213 |
-
|
214 |
-
# Add task function
|
215 |
-
@handle_exceptions
|
216 |
-
def add_task(title, description, priority):
|
217 |
-
"""Add a new task"""
|
218 |
-
if not title.strip():
|
219 |
-
logger.warning("Attempted to add task with empty title")
|
220 |
-
return "Please enter a task title", task_title, task_desc
|
221 |
-
|
222 |
-
logger.info(f"Adding new task: {title}")
|
223 |
-
|
224 |
-
# Create new task
|
225 |
-
new_task = {
|
226 |
-
"id": generate_id(),
|
227 |
-
"title": title.strip(),
|
228 |
-
"description": description.strip(),
|
229 |
-
"priority": priority.lower(),
|
230 |
-
"status": "todo",
|
231 |
-
"completed": False,
|
232 |
-
"created_at": get_timestamp()
|
233 |
-
}
|
234 |
-
|
235 |
-
# Add to state
|
236 |
-
state["tasks"].append(new_task)
|
237 |
-
state["stats"]["tasks_total"] += 1
|
238 |
-
|
239 |
-
# Record activity
|
240 |
-
record_activity({
|
241 |
-
"type": "task_created",
|
242 |
-
"title": title,
|
243 |
-
"timestamp": datetime.datetime.now().isoformat()
|
244 |
-
})
|
245 |
-
|
246 |
-
# Save to file
|
247 |
-
save_data(FILE_PATHS["tasks"], state["tasks"])
|
248 |
-
|
249 |
-
return "Task added successfully!", gr.update(value=""), gr.update(value="")
|
250 |
-
|
251 |
-
add_task_btn.click(
|
252 |
-
add_task,
|
253 |
-
inputs=[task_title, task_desc, task_priority],
|
254 |
-
outputs=[gr.Markdown(visible=False), task_title, task_desc]
|
255 |
-
)
|
256 |
-
|
257 |
-
# Add note function
|
258 |
-
@handle_exceptions
|
259 |
-
def add_note(title, content):
|
260 |
-
"""Add a new note"""
|
261 |
-
if not title.strip():
|
262 |
-
logger.warning("Attempted to add note with empty title")
|
263 |
-
return "Please enter a note title", note_title, note_content
|
264 |
-
|
265 |
-
logger.info(f"Adding new note: {title}")
|
266 |
-
|
267 |
-
# Create new note
|
268 |
-
new_note = {
|
269 |
-
"id": generate_id(),
|
270 |
-
"title": title.strip(),
|
271 |
-
"content": content.strip(),
|
272 |
-
"created_at": get_timestamp(),
|
273 |
-
"updated_at": get_timestamp()
|
274 |
-
}
|
275 |
-
|
276 |
-
# Add to state
|
277 |
-
state["notes"].append(new_note)
|
278 |
-
state["stats"]["notes_total"] += 1
|
279 |
-
|
280 |
-
# Record activity
|
281 |
-
record_activity({
|
282 |
-
"type": "note_created",
|
283 |
-
"title": title,
|
284 |
-
"timestamp": datetime.datetime.now().isoformat()
|
285 |
-
})
|
286 |
-
|
287 |
-
# Save to file
|
288 |
-
save_data(FILE_PATHS["notes"], state["notes"])
|
289 |
-
|
290 |
-
return "Note added successfully!", gr.update(value=""), gr.update(value="")
|
291 |
-
|
292 |
-
add_note_btn.click(
|
293 |
-
add_note,
|
294 |
-
inputs=[note_title, note_content],
|
295 |
-
outputs=[gr.Markdown(visible=False), note_title, note_content]
|
296 |
-
)
|
297 |
-
|
298 |
-
# Add goal function
|
299 |
-
@handle_exceptions
|
300 |
-
def add_goal(title, description, deadline):
|
301 |
-
"""Add a new goal"""
|
302 |
-
if not title.strip():
|
303 |
-
logger.warning("Attempted to add goal with empty title")
|
304 |
-
return "Please enter a goal title", goal_title, goal_desc, goal_deadline
|
305 |
-
|
306 |
-
logger.info(f"Adding new goal: {title}")
|
307 |
-
|
308 |
-
# Validate deadline format if provided
|
309 |
-
deadline_iso = None
|
310 |
-
if deadline.strip():
|
311 |
-
try:
|
312 |
-
deadline_date = datetime.datetime.strptime(deadline.strip(), "%Y-%m-%d")
|
313 |
-
deadline_iso = deadline_date.isoformat()
|
314 |
-
except ValueError as e:
|
315 |
-
logger.warning(f"Invalid date format: {deadline}, error: {str(e)}")
|
316 |
-
return "Invalid date format. Use YYYY-MM-DD", goal_title, goal_desc, goal_deadline
|
317 |
-
|
318 |
-
# Create new goal
|
319 |
-
new_goal = {
|
320 |
-
"id": generate_id(),
|
321 |
-
"title": title.strip(),
|
322 |
-
"description": description.strip(),
|
323 |
-
"completed": False,
|
324 |
-
"created_at": get_timestamp()
|
325 |
-
}
|
326 |
-
|
327 |
-
if deadline_iso:
|
328 |
-
new_goal["deadline"] = deadline_iso
|
329 |
-
|
330 |
-
# Add to state
|
331 |
-
state["goals"].append(new_goal)
|
332 |
-
state["stats"]["goals_total"] += 1
|
333 |
-
|
334 |
-
# Record activity
|
335 |
-
record_activity({
|
336 |
-
"type": "goal_created",
|
337 |
-
"title": title,
|
338 |
-
"timestamp": datetime.datetime.now().isoformat()
|
339 |
-
})
|
340 |
-
|
341 |
-
# Save to file
|
342 |
-
save_data(FILE_PATHS["goals"], state["goals"])
|
343 |
-
|
344 |
-
return "Goal added successfully!", gr.update(value=""), gr.update(value=""), gr.update(value="")
|
345 |
-
|
346 |
-
add_goal_btn.click(
|
347 |
-
add_goal,
|
348 |
-
inputs=[goal_title, goal_desc, goal_deadline],
|
349 |
-
outputs=[gr.Markdown(visible=False), goal_title, goal_desc, goal_deadline]
|
350 |
-
)
|
351 |
-
|
352 |
-
# Right column - Deadline Tracker, Progress Rings, Streak Counter
|
353 |
-
with gr.Column(scale=2):
|
354 |
-
# Deadline Tracker
|
355 |
-
with gr.Group(elem_classes=["deadline-tracker"]):
|
356 |
-
gr.Markdown("### ⏰ Upcoming Deadlines")
|
357 |
-
|
358 |
-
# Get tasks with deadlines
|
359 |
-
tasks_with_deadlines = []
|
360 |
-
for task in safe_get(state, "tasks", []):
|
361 |
-
if "deadline" in task and not safe_get(task, "completed", False):
|
362 |
-
tasks_with_deadlines.append(task)
|
363 |
-
|
364 |
-
# Sort by deadline (closest first)
|
365 |
-
tasks_with_deadlines.sort(key=lambda x: x["deadline"])
|
366 |
-
|
367 |
-
# Display deadline items
|
368 |
-
if tasks_with_deadlines:
|
369 |
-
for task in tasks_with_deadlines[:5]: # Show up to 5 deadlines
|
370 |
-
create_deadline_item(task)
|
371 |
-
else:
|
372 |
-
gr.Markdown("*No upcoming deadlines*")
|
373 |
-
|
374 |
-
# Progress Rings
|
375 |
-
with gr.Group(elem_classes=["progress-rings"]):
|
376 |
-
gr.Markdown("### 📊 Progress")
|
377 |
-
|
378 |
-
with gr.Row():
|
379 |
-
# Task completion progress
|
380 |
-
task_completion = 0
|
381 |
-
tasks_total = safe_get(state, ["stats", "tasks_total"], 0)
|
382 |
-
if tasks_total > 0:
|
383 |
-
task_completion = (safe_get(state, ["stats", "tasks_completed"], 0) / tasks_total) * 100
|
384 |
-
|
385 |
-
create_progress_ring(
|
386 |
-
value=task_completion,
|
387 |
-
max_value=100,
|
388 |
-
size=120,
|
389 |
-
color="blue",
|
390 |
-
label="Tasks"
|
391 |
-
)
|
392 |
-
|
393 |
-
# Goal completion progress
|
394 |
-
goal_completion = 0
|
395 |
-
goals_total = safe_get(state, ["stats", "goals_total"], 0)
|
396 |
-
if goals_total > 0:
|
397 |
-
goal_completion = (safe_get(state, ["stats", "goals_completed"], 0) / goals_total) * 100
|
398 |
-
|
399 |
-
create_progress_ring(
|
400 |
-
value=goal_completion,
|
401 |
-
max_value=100,
|
402 |
-
size=120,
|
403 |
-
color="purple",
|
404 |
-
label="Goals"
|
405 |
-
)
|
406 |
-
|
407 |
-
# Streak Counter
|
408 |
-
create_streak_counter(safe_get(state, ["stats", "streak_days"], 0))
|
|
|
1 |
+
"""
|
2 |
+
Dashboard page for the MONA application
|
3 |
+
Fixed version with proper imports and error handling
|
4 |
+
"""
|
5 |
+
|
6 |
+
import streamlit as st
|
7 |
+
import pandas as pd
|
8 |
+
import plotly.express as px
|
9 |
+
import plotly.graph_objects as go
|
10 |
+
from datetime import datetime, timedelta
|
11 |
+
from typing import Dict, List, Optional, Any
|
12 |
+
|
13 |
+
from utils.storage import load_data, save_data, get_cached_data, set_cached_data
|
14 |
+
from utils.error_handling import handle_data_exceptions, ErrorHandler, ValidationError, DataError
|
15 |
+
from utils.logging import get_logger, log_info, log_error, log_warning
|
16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
# Initialize logger
|
19 |
logger = get_logger(__name__)
|
20 |
|
21 |
+
|
22 |
+
@handle_data_exceptions
|
23 |
+
def load_dashboard_data() -> Dict[str, Any]:
|
24 |
+
"""
|
25 |
+
Load dashboard data with caching
|
26 |
+
|
27 |
+
Returns:
|
28 |
+
Dict: Dashboard data including metrics, charts data, etc.
|
29 |
+
"""
|
30 |
+
# Try to get from cache first
|
31 |
+
cached_data = get_cached_data("dashboard_data")
|
32 |
+
if cached_data:
|
33 |
+
log_info("Using cached dashboard data")
|
34 |
+
return cached_data
|
35 |
+
|
36 |
+
try:
|
37 |
+
# Load data from storage
|
38 |
+
data = {
|
39 |
+
"metrics": load_data("dashboard_metrics.json", default={}),
|
40 |
+
"user_activity": load_data("user_activity.json", default=[]),
|
41 |
+
"system_stats": load_data("system_stats.json", default={}),
|
42 |
+
"recent_events": load_data("recent_events.json", default=[]),
|
43 |
+
"performance_data": load_data("performance_data.json", default=[])
|
44 |
+
}
|
45 |
+
|
46 |
+
# Cache the data
|
47 |
+
set_cached_data("dashboard_data", data)
|
48 |
+
log_info("Dashboard data loaded and cached successfully")
|
49 |
+
|
50 |
+
return data
|
51 |
+
|
52 |
+
except Exception as e:
|
53 |
+
log_error("Failed to load dashboard data", error=e)
|
54 |
+
return {
|
55 |
+
"metrics": {},
|
56 |
+
"user_activity": [],
|
57 |
+
"system_stats": {},
|
58 |
+
"recent_events": [],
|
59 |
+
"performance_data": []
|
60 |
+
}
|
61 |
+
|
62 |
+
|
63 |
+
@handle_data_exceptions
|
64 |
+
def create_metrics_cards(metrics: Dict[str, Any]) -> None:
|
65 |
+
"""
|
66 |
+
Create and display metrics cards
|
67 |
+
|
68 |
+
Args:
|
69 |
+
metrics: Dictionary containing metric values
|
70 |
+
"""
|
71 |
+
if not metrics:
|
72 |
+
st.warning("No metrics data available")
|
73 |
+
return
|
74 |
+
|
75 |
+
# Create columns for metrics
|
76 |
+
cols = st.columns(4)
|
77 |
+
|
78 |
+
# Total Users
|
79 |
+
with cols[0]:
|
80 |
+
total_users = metrics.get("total_users", 0)
|
81 |
+
st.metric(
|
82 |
+
label="Total Users",
|
83 |
+
value=f"{total_users:,}",
|
84 |
+
delta=metrics.get("users_change", 0)
|
85 |
+
)
|
86 |
+
|
87 |
+
# Active Sessions
|
88 |
+
with cols[1]:
|
89 |
+
active_sessions = metrics.get("active_sessions", 0)
|
90 |
+
st.metric(
|
91 |
+
label="Active Sessions",
|
92 |
+
value=f"{active_sessions:,}",
|
93 |
+
delta=metrics.get("sessions_change", 0)
|
94 |
+
)
|
95 |
+
|
96 |
+
# System Health
|
97 |
+
with cols[2]:
|
98 |
+
system_health = metrics.get("system_health", 0)
|
99 |
+
st.metric(
|
100 |
+
label="System Health",
|
101 |
+
value=f"{system_health:.1f}%",
|
102 |
+
delta=f"{metrics.get('health_change', 0):.1f}%"
|
103 |
+
)
|
104 |
+
|
105 |
+
# Response Time
|
106 |
+
with cols[3]:
|
107 |
+
response_time = metrics.get("avg_response_time", 0)
|
108 |
+
st.metric(
|
109 |
+
label="Avg Response Time",
|
110 |
+
value=f"{response_time:.0f}ms",
|
111 |
+
delta=f"{metrics.get('response_change', 0):.0f}ms",
|
112 |
+
delta_color="inverse"
|
113 |
+
)
|
114 |
+
|
115 |
+
|
116 |
+
@handle_data_exceptions
|
117 |
+
def create_activity_chart(activity_data: List[Dict]) -> None:
|
118 |
"""
|
119 |
+
Create user activity chart
|
120 |
|
121 |
Args:
|
122 |
+
activity_data: List of activity data points
|
123 |
"""
|
124 |
+
if not activity_data:
|
125 |
+
st.warning("No activity data available")
|
126 |
+
return
|
127 |
|
128 |
+
try:
|
129 |
+
df = pd.DataFrame(activity_data)
|
130 |
+
|
131 |
+
if df.empty:
|
132 |
+
st.warning("Activity data is empty")
|
133 |
+
return
|
134 |
+
|
135 |
+
# Ensure required columns exist
|
136 |
+
if 'timestamp' not in df.columns:
|
137 |
+
df['timestamp'] = pd.date_range(start='2024-01-01', periods=len(df), freq='H')
|
138 |
+
|
139 |
+
if 'users' not in df.columns:
|
140 |
+
df['users'] = [0] * len(df)
|
141 |
|
142 |
+
# Convert timestamp to datetime if it's not already
|
143 |
+
df['timestamp'] = pd.to_datetime(df['timestamp'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
144 |
|
145 |
+
# Create the chart
|
146 |
+
fig = px.line(
|
147 |
+
df,
|
148 |
+
x='timestamp',
|
149 |
+
y='users',
|
150 |
+
title='User Activity Over Time',
|
151 |
+
labels={'users': 'Active Users', 'timestamp': 'Time'}
|
152 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
|
154 |
+
fig.update_layout(
|
155 |
+
xaxis_title="Time",
|
156 |
+
yaxis_title="Active Users",
|
157 |
+
h
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|