Skip to content

Commit

Permalink
Check-Ins Feature #18 - Done
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dhrumilp12 committed Jun 18, 2024
1 parent c23d80c commit 625704f
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 43 deletions.
42 changes: 27 additions & 15 deletions client/service-worker.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
30 changes: 25 additions & 5 deletions client/src/Components/navBar.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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';
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 }) {
const { incrementNotificationCount, notifications, addNotification, removeNotification } = useContext(UserContext);
const navigate = useNavigate();
const { voiceEnabled, setVoiceEnabled,user } = useContext(UserContext);
const [anchorEl, setAnchorEl] = useState(null);

const userId = user?.userId;
console.log("User ID:", userId);

Expand All @@ -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: ''});
}
};

Expand Down Expand Up @@ -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();
}
};
Expand All @@ -85,6 +89,7 @@ function Navbar({ toggleSidebar }) {
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage);
};
}, []);


return (
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
Expand Down Expand Up @@ -132,7 +137,22 @@ function Navbar({ toggleSidebar }) {
</IconButton>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={() => handleClose(null)}>
{notifications.map((notification, index) => (
<MenuItem key={index} onClick={handleClose}>{notification.title}</MenuItem>
<MenuItem key={index} onClick={() => handleClose(index)} sx={{ whiteSpace: 'normal',maxWidth: 350, padding: 1}}>
<Card elevation={2} sx={{display: 'flex', alignItems: 'center', width: '100%'}}>

<CancelIcon color="error" />

<CardContent sx={{ flex: '1 1 auto' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
{notification.title}
</Typography>

<Typography variant="body2" color="text.secondary">
{notification.message}
</Typography>
</CardContent>
</Card>
</MenuItem>
))}
</Menu>
<IconButton color="inherit">
Expand Down
26 changes: 24 additions & 2 deletions client/src/Components/userContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};
Expand Down Expand Up @@ -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 (
<UserContext.Provider value={{ user, setUser, logout,voiceEnabled, setVoiceEnabled, changePassword,incrementNotificationCount, notifications,removeNotification, addNotification }}>
{children}
Expand Down
8 changes: 4 additions & 4 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
Binary file modified server/instance/mydatabase.db
Binary file not shown.
18 changes: 10 additions & 8 deletions server/models/check_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
5 changes: 5 additions & 0 deletions server/models/subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 30 additions & 9 deletions server/routes/scheduler_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions server/routes/scheduler.py → server/services/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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

0 comments on commit 625704f

Please sign in to comment.