From 625704f6495de1fb586baf594bfe3b558e23fe08 Mon Sep 17 00:00:00 2001 From: DHRUMIL PATEL <123137675+dhrumilp12@users.noreply.github.com> Date: Tue, 18 Jun 2024 04:41:52 -0400 Subject: [PATCH] Check-Ins Feature #18 - Done 30. The user will be allowed to schedule check-ins, where the user sets aside a specific time to chat with the AI. While the user can chat with the chatbot at any time, check-ins allow users to keep track of their interactions in an organized manner. 31. The user can schedule a maximum of 5 check-ins a day. 32. The user can edit their check-in times at any time. 33. The user can edit check-in frequency from daily, weekly or monthly. 34. The user can opt in to be notified about their check-ins. ( '1 week'||'1day '||'1 hour' + 'ago' ) 35. The system must inform the user that if they missed a check-in upon startup, and ask if they would like to hold their check-in at that moment. 36. If two check-ins overlap, or the user is already using the chatbot, the app should silently disregard the check-in and count it as completed. 37. However, if the previous check-in has been inactive for more than an hour, this requirement does not apply. 38. If multiple check-ins are missed in a row, the user should only be notified about the latest check-in. 40. System validate the proposed check-in time against other check-ins on the same day. --- client/service-worker.js | 42 +++++++++++++++-------- client/src/Components/navBar.jsx | 30 +++++++++++++--- client/src/Components/userContext.jsx | 26 ++++++++++++-- server/app.py | 8 ++--- server/instance/mydatabase.db | Bin 12288 -> 16384 bytes server/models/check_in.py | 18 +++++----- server/models/subscription.py | 5 +++ server/routes/scheduler_main.py | 39 ++++++++++++++++----- server/{routes => services}/scheduler.py | 5 +++ 9 files changed, 130 insertions(+), 43 deletions(-) rename server/{routes => services}/scheduler.py (83%) diff --git a/client/service-worker.js b/client/service-worker.js index 65e550fc..a49ba6c1 100644 --- a/client/service-worker.js +++ b/client/service-worker.js @@ -1,21 +1,33 @@ self.addEventListener('push', event => { + let data; try { - const data = event.data.json(); // Tries to parse JSON data - self.registration.showNotification(data.title, { - body: data.body, - icon: './Images/Aria.jpg' - }); - self.clients.matchAll().then(clients => { - clients.forEach(client => client.postMessage({ msg: 'updateCount' })); - }); -} catch (error) { - console.error('Error parsing push notification data:', error); - self.registration.showNotification('Notification', { - body: event.data.text(), - icon: './Images/Aria.jpg' - }); -} + // Try to parse the incoming push message data as JSON + data = event.data.json(); + } catch (error) { + // If JSON parsing fails, treat it as a plain text + console.error('Error parsing push notification data as JSON:', error); + data = { + title: 'Notification', // Default title if not parsing JSON + body: event.data ? event.data.text() : 'No data received' // Use the plain text data or a default message + }; + } + + console.log('Final data to display:', data); + self.registration.showNotification(data.title, { + body: data.user, + icon: './Images/Aria.jpg' + }); + + // Broadcast update message to clients + self.clients.matchAll().then(clients => { + clients.forEach(client => client.postMessage({ + msg: 'updateCount', + title: data.title, + body: data.body + })); + }); }); + self.addEventListener('notificationclick', event => { event.notification.close(); diff --git a/client/src/Components/navBar.jsx b/client/src/Components/navBar.jsx index 3fc41c47..9b7d495d 100644 --- a/client/src/Components/navBar.jsx +++ b/client/src/Components/navBar.jsx @@ -1,7 +1,7 @@ import React, {useContext, useState, useEffect} from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; -import { AppBar, Toolbar, IconButton, Typography, Badge,Switch, Tooltip, Menu, MenuItem } from '@mui/material'; +import { AppBar, Toolbar, IconButton, Typography, Badge,Switch, Tooltip, Menu, MenuItem, Card, CardContent } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import NotificationsIcon from '@mui/icons-material/Notifications'; import AccountCircle from '@mui/icons-material/AccountCircle'; @@ -9,6 +9,7 @@ import SearchIcon from '@mui/icons-material/Search'; import { UserContext } from './userContext'; import VolumeOffIcon from '@mui/icons-material/VolumeOff'; import VolumeUpIcon from '@mui/icons-material/VolumeUp'; +import CancelIcon from '@mui/icons-material/Cancel'; function Navbar({ toggleSidebar }) { @@ -16,6 +17,7 @@ function Navbar({ toggleSidebar }) { const navigate = useNavigate(); const { voiceEnabled, setVoiceEnabled,user } = useContext(UserContext); const [anchorEl, setAnchorEl] = useState(null); + const userId = user?.userId; console.log("User ID:", userId); @@ -39,14 +41,14 @@ function Navbar({ toggleSidebar }) { console.log("Missed check-ins:", missedCheckIns); if (missedCheckIns.length > 0) { missedCheckIns.forEach(checkIn => { - addNotification({ title: `Missed Check-in on ${new Date(checkIn.check_in_time).toLocaleString()}` }); + addNotification({ title: `Missed Check-in on ${new Date(checkIn.check_in_time).toLocaleString()}`, message: 'Please complete your check-in.' }); }); } else { - addNotification({ title: "You have no missed check-ins." }); + addNotification({ title: "You have no missed check-ins.", message: ''}); } } catch (error) { console.error('Failed to fetch missed check-ins:', error); - addNotification({ title: "Failed to fetch missed check-ins. Please check the console for more details." }); + addNotification({ title: "Failed to fetch missed check-ins. Please check the console for more details.", message: ''}); } }; @@ -75,6 +77,8 @@ function Navbar({ toggleSidebar }) { useEffect(() => { const handleServiceWorkerMessage = (event) => { if (event.data && event.data.msg === 'updateCount') { + console.log('Received message from service worker:', event.data); + addNotification({ title: event.data.title, message: event.data.body }); incrementNotificationCount(); } }; @@ -85,6 +89,7 @@ function Navbar({ toggleSidebar }) { navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage); }; }, []); + return ( theme.zIndex.drawer + 1 }}> @@ -132,7 +137,22 @@ function Navbar({ toggleSidebar }) { handleClose(null)}> {notifications.map((notification, index) => ( - {notification.title} + handleClose(index)} sx={{ whiteSpace: 'normal',maxWidth: 350, padding: 1}}> + + + + + + + {notification.title} + + + + {notification.message} + + + + ))} diff --git a/client/src/Components/userContext.jsx b/client/src/Components/userContext.jsx index c0adc77f..6f88945e 100644 --- a/client/src/Components/userContext.jsx +++ b/client/src/Components/userContext.jsx @@ -9,9 +9,9 @@ export const UserProvider = ({ children }) => { const [notificationCount, setNotificationCount] = useState(0); const [notifications, setNotifications] = useState([]); - const addNotification = (notification) => { + const addNotification = useCallback((notification) => { setNotifications((prev) => [...prev, notification]); -}; + }, [setNotifications]); const removeNotification = index => { setNotifications(prevNotifications => prevNotifications.filter((_, i) => i !== index)); }; @@ -90,6 +90,28 @@ export const UserProvider = ({ children }) => { } }; + // Handle messages from the service worker + useEffect(() => { + const handleServiceWorkerMessages = event => { + if (event.data && event.data.type === 'NEW_NOTIFICATION') { + console.log('Notification received:', event.data.data); + addNotification({ + title: event.data.data.title, + message: event.data.data.body + + }); + } + }; + + navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessages); + + // Cleanup this effect + return () => { + navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessages); + }; +}, [addNotification]); + + return ( {children} diff --git a/server/app.py b/server/app.py index 3f669d64..e9003206 100644 --- a/server/app.py +++ b/server/app.py @@ -6,12 +6,12 @@ from flask_cors import CORS from flask_jwt_extended import JWTManager, jwt_required , get_jwt_identity from models.subscription import Subscription,db - +import json from routes.user import user_routes from routes.ai import ai_routes from routes.checkIn import checkIn_routes from services.azure_mongodb import MongoDBClient -from routes.scheduler import send_push_notification +from services.scheduler import send_push_notification from agents.mental_health_agent import MentalHealthAIAgent # Set up the app @@ -39,13 +39,13 @@ def subscribe(): return jsonify({'error': 'Missing required fields'}), 400 - subscription_info = { + subscription_info = json.dumps({ 'endpoint': data['endpoint'], 'keys': { 'p256dh': data['keys']['p256dh'], 'auth': data['keys']['auth'] } - } + }) user_id = get_jwt_identity() diff --git a/server/instance/mydatabase.db b/server/instance/mydatabase.db index d8f11f23db88b378e1db5c140675091f9c8ea7ea..9b4ae4750092a52d2576e4776b1c8e16e8ccb575 100644 GIT binary patch delta 295 zcmZojXlP)ZAT7wwz`(!)#4x}#QO8)Aok6dxlb8Po12Z271K&^n{d^o73#ajhHRdt1 ziyIp=HZzwbCgr5&<(FipWhN(~bqsM;2yt}saaF*gV6qjz z^yD&rzWNjePrndXch?{t1ujmAc?w~ULCzkIK^mq8njq1DAWvV%phyL8*GL78%oMPg zn}3k2r@J3WOry9osW`bPvjFTQpu9qmtD9?(tDm!LumZ9iSTmPqqcbDBxT-2+lPAza Xo7wn(DX{T^LPB7(pu%PTi4E!i;7C;e delta 56 zcmZo@U~EX3AT7wmz`(!^#4x}(QO6i4s8`m>%m0IciSH)^-%tMin*|j*`8NOLV^;(K D7q|=~ diff --git a/server/models/check_in.py b/server/models/check_in.py index 1ce43236..f5f9c9f7 100644 --- a/server/models/check_in.py +++ b/server/models/check_in.py @@ -14,25 +14,27 @@ class Frequency(str, Enum): class CheckIn(BaseModel): user_id: str check_in_time: datetime - @validator('check_in_time', pre=True) - def check_future_date(cls, v): - if v < datetime.now(): - raise ValueError("Check-in time must be in the future") - return v - frequency: Frequency status: str = "upcoming" # default status is upcoming last_conversation: str = "" # default empty string, updated later notify: bool = False # Default to False, updated based on user preference - reminder_times: list[timedelta] = Field(default_factory=list) + reminder_times: list[timedelta] = Field(default_factory=lambda: [ + timedelta(days=1), + timedelta(weeks=1), + timedelta(days=30) # Approximately 1 month + ]) def save(self, db): # Convert model to dictionary and save to MongoDB document = self.dict() db.check_ins.insert_one(document) + @validator('check_in_time', pre=True) + def check_future_date(cls, v): + if v < datetime.now(): + raise ValueError("Check-in time must be in the future") + return v - @staticmethod def count_user_check_ins(db, user_id, date): start_of_day = datetime.combine(date, datetime.min.time()) diff --git a/server/models/subscription.py b/server/models/subscription.py index 83c13434..482bd4fa 100644 --- a/server/models/subscription.py +++ b/server/models/subscription.py @@ -2,6 +2,11 @@ db = SQLAlchemy() +class NotificationTiming(db.Model): + id = db.Column(db.Integer, primary_key=True) + subscription_id = db.Column(db.Integer, db.ForeignKey('subscription.id')) + timing = db.Column(db.String(50)) + class Subscription(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.String(120), unique=True, nullable=False) diff --git a/server/routes/scheduler_main.py b/server/routes/scheduler_main.py index 03202310..8835334a 100644 --- a/server/routes/scheduler_main.py +++ b/server/routes/scheduler_main.py @@ -2,7 +2,7 @@ import schedule import time -from .scheduler import send_push_notification +from services.scheduler import send_push_notification from models.check_in import CheckIn from services.azure_mongodb import MongoDBClient from datetime import datetime, timedelta @@ -13,16 +13,35 @@ db = db_client[MongoDBClient.get_db_name()] class NotificationScheduler: - def __init__(self): + def __init__(self,db): self.scheduler = schedule.Scheduler() + self.db = MongoDBClient.get_client()[MongoDBClient.get_db_name()] + + def format_delta(self, delta): + if delta == timedelta(days=1): + return "1 day" + elif delta == timedelta(weeks=1): + return "1 week" + elif delta == timedelta(days=30): # Approximating 1 month + return "1 month" + else: + return str(delta) def schedule_notifications(self, check_in): - for reminder_time in check_in['reminder_times']: - scheduled_time = check_in['check_in_time'] - reminder_time - self.scheduler.every().day.at(scheduled_time.strftime("%H:%M")).do( - lambda: self.send_notification(check_in) - ) - print(f"Notification for {check_in['user_id']} scheduled at {scheduled_time}") + user_id = check_in['user_id'] + check_in_id = check_in['_id'] + check_in_time = check_in['check_in_time'] + reminder_times = check_in['reminder_times'] + + for reminder_time in reminder_times: + scheduled_time = check_in_time - reminder_time + if scheduled_time > datetime.now(): + reminder_text = self.format_delta(reminder_time) + self.scheduler.every().day.at(scheduled_time.strftime("%H:%M")).do( + self.send_notification, user_id=user_id, check_in_id=check_in['_id'], + message=f"Reminder: Your check-in is in {self.format_delta(reminder_time)}" + ) + print(f"Notification for {user_id} scheduled at {scheduled_time}") def send_notification(self, check_in): with app.app_context(): # To access app configuration @@ -54,7 +73,9 @@ def run_scheduler(self): # Create a single global instance of the scheduler -scheduler = NotificationScheduler() +db= MongoDBClient.get_client()[MongoDBClient.get_db_name()] +scheduler = NotificationScheduler(db) + # Start the scheduler in a background thread from threading import Thread diff --git a/server/routes/scheduler.py b/server/services/scheduler.py similarity index 83% rename from server/routes/scheduler.py rename to server/services/scheduler.py index ea1a2bba..c967a0e2 100644 --- a/server/routes/scheduler.py +++ b/server/services/scheduler.py @@ -13,6 +13,8 @@ def send_push_notification(user_id, message): return False try: + print("Subscription info:", subscription.subscription_info) + webpush( subscription_info=json.loads(subscription.subscription_info), data=json.dumps({ @@ -28,3 +30,6 @@ def send_push_notification(user_id, message): print(f"Failed to send notification: {e}") if e.response and e.response.json(): print(e.response.json()) + except json.JSONDecodeError as json_error: + print(f"JSON decoding error with subscription_info: {json_error}") + return False \ No newline at end of file