From 8fa10a9579b8beab06479a30b2638ea749cfe7b6 Mon Sep 17 00:00:00 2001 From: Owen Cooke Date: Sun, 7 Jan 2024 08:52:50 -0700 Subject: [PATCH] add practioner id to login link --- backend/server/src/constants.py | 30 +- backend/server/src/main.py | 175 +++++------ frontend/src/App.jsx | 55 ++-- frontend/src/views/patient/PatientHome.jsx | 241 ++++++++------- .../src/views/patient/components/VoiceAI.jsx | 290 +++++++++--------- .../components/NewPatientModal.jsx | 232 +++++++------- 6 files changed, 518 insertions(+), 505 deletions(-) diff --git a/backend/server/src/constants.py b/backend/server/src/constants.py index dbd8419..f580e19 100644 --- a/backend/server/src/constants.py +++ b/backend/server/src/constants.py @@ -1,15 +1,15 @@ -# Patient login email content -EMAIL_SUBJECT = "MobilityMate Account Access" -EMAIL_BODY_TEMPLATE = """Dear {name}, - -Welcome to MobilityMate - your dedicated partner in staying active and healthy! 🌟 - -To access your personalized exercise account and stay up to date with exercises recommended by your practitioner, simply click on the link below: - -🔗 https://mobilitymate-a8b53.web.app/patient/{uid} - -Your well-being is our priority! If you have any questions or need support, our team is here to assist you every step of the way. - -Stay active, stay healthy! -The MobilityMate Team 🏋️‍♂️ -""" +# Patient login email content +EMAIL_SUBJECT = "MobilityMate Account Access" +EMAIL_BODY_TEMPLATE = """Dear {name}, + +Welcome to MobilityMate - your dedicated partner in staying active and healthy! 🌟 + +To access your personalized exercise account and stay up to date with exercises recommended by your practitioner, simply click on the link below: + +🔗 https://mobilitymate-a8b53.web.app/{practitionId}/patient/{patientId} + +Your well-being is our priority! If you have any questions or need support, our team is here to assist you every step of the way. + +Stay active, stay healthy! +The MobilityMate Team 🏋️‍♂️ +""" diff --git a/backend/server/src/main.py b/backend/server/src/main.py index 268b3ae..02854c0 100644 --- a/backend/server/src/main.py +++ b/backend/server/src/main.py @@ -1,86 +1,89 @@ -from flask import Flask, request, jsonify, render_template -from flask_mail import Mail, Message -from dotenv import load_dotenv -import os -from constants import * -import time -import firebase_admin -from firebase_admin import credentials -from flask_cors import CORS - -load_dotenv() -app = Flask(__name__) -app.secret_key = os.getenv("SECRET_KEY") - - -def create_service_dict(): - variables_keys = { - "type": os.getenv("TYPE"), - "project_id": os.getenv("PROJECT_ID"), - "private_key_id": os.getenv("PRIVATE_KEY_ID"), - "private_key": os.getenv("PRIVATE_KEY"), - "client_email": os.getenv("CLIENT_EMAIL"), - "client_id": os.getenv("CLIENT_ID"), - "auth_uri": os.getenv("AUTH_URI"), - "token_uri": os.getenv("TOKEN_URI"), - "auth_provider_x509_cert_url": os.getenv("AUTH_PROVIDER_X509_CERT_URL"), - "client_x509_cert_url": os.getenv("CLIENT_X509_CERT_URL"), - "universe_domain": os.getenv("UNIVERSE_DOMAIN"), - } - return variables_keys - - -cred = credentials.Certificate(create_service_dict()) -firebase_admin.initialize_app(cred) - -from conversation.views import conversation_blueprint - -# Register the conversation Blueprint -app.register_blueprint(conversation_blueprint, url_prefix="/conversation") -CORS(app) - -# Load Flask-Mail config from .env -app.config["MAIL_SERVER"] = os.getenv("MAIL_SERVER") -app.config["MAIL_PORT"] = int(os.getenv("MAIL_PORT")) -app.config["MAIL_USE_TLS"] = os.getenv("MAIL_USE_TLS").lower() == "true" -app.config["MAIL_USERNAME"] = os.getenv("MAIL_USERNAME") -app.config["MAIL_PASSWORD"] = os.getenv("MAIL_PASSWORD") -app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_DEFAULT_SENDER") -mail = Mail(app) - - -@app.route("/patient/send-link", methods=["POST"]) -def send_link(): - try: - data = request.get_json() - uid = data.get("uid") - name = data.get("name") - email = data.get("email") - - # Send patient email with login link - message = Message( - subject=EMAIL_SUBJECT, - recipients=[email], - body=EMAIL_BODY_TEMPLATE.format(name=name, uid=uid), - ) - mail.send(message) - - return jsonify({"success": True, "message": "Email sent successfully"}) - - except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 - - -def format_server_time(): - server_time = time.localtime() - return time.strftime("%I:%M:%S %p", server_time) - - -@app.route("/") -def index(): - context = {"server_time": format_server_time()} - return render_template("index.html", context=context) - - -if __name__ == "__main__": - app.run(debug=True, port=os.getenv("PORT", default=5000)) +from flask import Flask, request, jsonify, render_template +from flask_mail import Mail, Message +from dotenv import load_dotenv +import os +from constants import * +import time +import firebase_admin +from firebase_admin import credentials +from flask_cors import CORS + +load_dotenv() +app = Flask(__name__) +app.secret_key = os.getenv("SECRET_KEY") + + +def create_service_dict(): + variables_keys = { + "type": os.getenv("TYPE"), + "project_id": os.getenv("PROJECT_ID"), + "private_key_id": os.getenv("PRIVATE_KEY_ID"), + "private_key": os.getenv("PRIVATE_KEY"), + "client_email": os.getenv("CLIENT_EMAIL"), + "client_id": os.getenv("CLIENT_ID"), + "auth_uri": os.getenv("AUTH_URI"), + "token_uri": os.getenv("TOKEN_URI"), + "auth_provider_x509_cert_url": os.getenv("AUTH_PROVIDER_X509_CERT_URL"), + "client_x509_cert_url": os.getenv("CLIENT_X509_CERT_URL"), + "universe_domain": os.getenv("UNIVERSE_DOMAIN"), + } + return variables_keys + + +cred = credentials.Certificate(create_service_dict()) +firebase_admin.initialize_app(cred) + +from conversation.views import conversation_blueprint + +# Register the conversation Blueprint +app.register_blueprint(conversation_blueprint, url_prefix="/conversation") +CORS(app) + +# Load Flask-Mail config from .env +app.config["MAIL_SERVER"] = os.getenv("MAIL_SERVER") +app.config["MAIL_PORT"] = int(os.getenv("MAIL_PORT")) +app.config["MAIL_USE_TLS"] = os.getenv("MAIL_USE_TLS").lower() == "true" +app.config["MAIL_USERNAME"] = os.getenv("MAIL_USERNAME") +app.config["MAIL_PASSWORD"] = os.getenv("MAIL_PASSWORD") +app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_DEFAULT_SENDER") +mail = Mail(app) + + +@app.route("/patient/send-link", methods=["POST"]) +def send_link(): + try: + data = request.get_json() + practitionId = data.get("practitionId") + patientId = data.get("patientId") + name = data.get("name") + email = data.get("email") + + # Send patient email with login link + message = Message( + subject=EMAIL_SUBJECT, + recipients=[email], + body=EMAIL_BODY_TEMPLATE.format( + name=name, practitionId=practitionId, patientId=patientId + ), + ) + mail.send(message) + + return jsonify({"success": True, "message": "Email sent successfully"}) + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + + +def format_server_time(): + server_time = time.localtime() + return time.strftime("%I:%M:%S %p", server_time) + + +@app.route("/") +def index(): + context = {"server_time": format_server_time()} + return render_template("index.html", context=context) + + +if __name__ == "__main__": + app.run(debug=True, port=os.getenv("PORT", default=5000)) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d140c14..493b1a9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,26 +1,29 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; - -import Landing from "./views/landing/Landing"; -import PatientHome from "./views/patient/PatientHome"; -import PractitionerDashboard from "./views/practitioner/PractitionerDashboard"; -import PractitionerSignUp from "./views/practitioner/PractitionerSignUp"; -import PractitionerLogin from "./views/practitioner/PractitionerLogin"; - -const App = () => { - return ( - - - } /> - } /> - } - /> - } /> - } /> - - - ); -}; - -export default App; +import { BrowserRouter, Routes, Route } from "react-router-dom"; + +import Landing from "./views/landing/Landing"; +import PatientHome from "./views/patient/PatientHome"; +import PractitionerDashboard from "./views/practitioner/PractitionerDashboard"; +import PractitionerSignUp from "./views/practitioner/PractitionerSignUp"; +import PractitionerLogin from "./views/practitioner/PractitionerLogin"; + +const App = () => { + return ( + + + } /> + } + /> + } + /> + } /> + } /> + + + ); +}; + +export default App; diff --git a/frontend/src/views/patient/PatientHome.jsx b/frontend/src/views/patient/PatientHome.jsx index a6797f7..bb90291 100644 --- a/frontend/src/views/patient/PatientHome.jsx +++ b/frontend/src/views/patient/PatientHome.jsx @@ -1,117 +1,124 @@ -import Navbar from './components/Navbar'; -import Exercises from './components/Exercises'; -import './styles.css'; -import { useState, useEffect, useCallback } from 'react'; -import VoiceAI from './components/VoiceAI'; -import axios from 'axios'; -import Skeleton from './components/Skeleton'; -import apiUrl from "../../config"; -import { LogOut } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; - -const PatientHome = () => { - const navigate = useNavigate(); - const [convo, setConvo] = useState({ - user: null, - gpt: null, - }); - - const updateUserMessage = useCallback((newMessage) => { - setConvo((prevConvo) => ({ ...prevConvo, user: newMessage })); - }, []); - - const updateGptResponse = useCallback((newResponse) => { - setConvo((prevConvo) => ({ ...prevConvo, gpt: newResponse })); - }, []); - - useEffect(() => { - const startConversation = async () => { - const queryParams = new URLSearchParams({ - patient: 'demo', - practitioner: 'demo', - }); - try { - const response = await axios.get( - `${apiUrl}/conversation/start?${queryParams.toString()}` - ); - setConvo((prevConvo) => { - if (prevConvo.gpt === null) { - return { ...prevConvo, gpt: response.data.reply }; - } - return prevConvo; - }); - } catch (error) { - console.error('Error fetching conversation start:', error); - } - }; - startConversation(); - }, []); - - const handleEndSession = async () => { - try { - await axios.post('http://localhost:8080/conversation/end', {}, { - // TODO: what are thooooose - params: new URLSearchParams({ - patient: 'demo', - practitioner: 'demo', - }) - }); - navigate('/') - } catch (error) { - console.error('Error ending conversation:', error); - } - }; - - return ( -
-
- {/* */} -
-
-
-
-
-

Welcome Back

-
John
-
- -
- {/* */} -
-

{convo.user}

-
- {convo.gpt !== null ? convo.gpt : } -
-
-
- - -
-
-
- -
-
-
-
- -
- {/* TODO: finish button that calls conversation/end */} -
-
- ); -}; - -export default PatientHome; +import Navbar from "./components/Navbar"; +import Exercises from "./components/Exercises"; +import "./styles.css"; +import { useState, useEffect, useCallback } from "react"; +import VoiceAI from "./components/VoiceAI"; +import axios from "axios"; +import Skeleton from "./components/Skeleton"; +import apiUrl from "../../config"; +import { LogOut } from "lucide-react"; +import { useNavigate } from "react-router-dom"; + +const PatientHome = () => { + const navigate = useNavigate(); + const [convo, setConvo] = useState({ + user: null, + gpt: null, + }); + + const updateUserMessage = useCallback((newMessage) => { + setConvo((prevConvo) => ({ ...prevConvo, user: newMessage })); + }, []); + + const updateGptResponse = useCallback((newResponse) => { + setConvo((prevConvo) => ({ ...prevConvo, gpt: newResponse })); + }, []); + + useEffect(() => { + const startConversation = async () => { + const queryParams = new URLSearchParams({ + patient: "demo", + practitioner: "demo", + }); + try { + const response = await axios.get( + `${apiUrl}/conversation/start?${queryParams.toString()}` + ); + setConvo((prevConvo) => { + if (prevConvo.gpt === null) { + return { ...prevConvo, gpt: response.data.reply }; + } + return prevConvo; + }); + } catch (error) { + console.error("Error fetching conversation start:", error); + } + }; + startConversation(); + }, []); + + const handleEndSession = async () => { + try { + await axios.post( + `${apiUrl}/conversation/end`, + {}, + { + // TODO: what are thooooose + params: new URLSearchParams({ + patient: "demo", + practitioner: "demo", + }), + } + ); + navigate("/"); + } catch (error) { + console.error("Error ending conversation:", error); + } + }; + + return ( +
+
+ {/* */} +
+
+
+
+
+

Welcome Back

+
John
+
+ +
+ {/* */} +
+

{convo.user}

+
+ {convo.gpt !== null ? convo.gpt : } +
+
+
+ + +
+
+
+
+

Exercises

+ +
+
+
+
+ +
+ {/* TODO: finish button that calls conversation/end */} +
+
+ ); +}; + +export default PatientHome; diff --git a/frontend/src/views/patient/components/VoiceAI.jsx b/frontend/src/views/patient/components/VoiceAI.jsx index 8d489f5..5c048c0 100644 --- a/frontend/src/views/patient/components/VoiceAI.jsx +++ b/frontend/src/views/patient/components/VoiceAI.jsx @@ -1,145 +1,145 @@ -import { useState, useEffect, useRef } from 'react'; -import axios from 'axios'; -import apiUrl from '../../../config'; -import gsap from 'gsap'; -import React, { Suspense } from 'react'; - -const Spline = React.lazy(() => import('@splinetool/react-spline')); - -const VoiceAI = ({ updateUserMessage, updateGptResponse }) => { - const sphere = useRef(); - const [isRecording, setIsRecording] = useState(false); - const [mediaStream, setMediaStream] = useState(null); - const [mediaRecorder, setMediaRecorder] = useState(null); - const [speechRecognition, setSpeechRecognition] = useState(null); - - useEffect(() => { - const SpeechRecognition = - window.SpeechRecognition || window.webkitSpeechRecognition; - if (SpeechRecognition) { - const recognition = new SpeechRecognition(); - recognition.continuous = true; - recognition.interimResults = true; - - let accumulatedTranscript = ''; - - recognition.onresult = (event) => { - accumulatedTranscript = ''; - for (let i = 0; i < event.results.length; i++) { - accumulatedTranscript += event.results[i][0].transcript.trim() + ' '; - } - updateUserMessage(accumulatedTranscript); - }; - - setSpeechRecognition(recognition); - } else { - console.warn('Speech recognition not supported in this browser.'); - } - }, [updateUserMessage]); - - const startRecording = async () => { - const queryParams = new URLSearchParams({ - patient: 'demo', - practitioner: 'demo', - }); - - // Start recording audio - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - const recorder = new MediaRecorder(stream); - const chunks = []; - - recorder.ondataavailable = (event) => { - if (event.data.size > 0) { - chunks.push(event.data); - } - }; - - recorder.onstop = async () => { - updateGptResponse(null); - // Process and send the audio data to the server for transcription - const audioBlob = new Blob(chunks, { type: 'audio/wav' }); - const formData = new FormData(); - formData.append('audioFile', audioBlob, 'recorded_audio.wav'); - - const response = await axios.post( - `${apiUrl}/conversation/send_message?${queryParams.toString()}`, - formData - ); - updateGptResponse(response.data.reply); - }; - - recorder.start(); - setMediaStream(stream); - setMediaRecorder(recorder); - speechRecognition?.start(); - setIsRecording(true); - }; - - const stopRecording = () => { - if (mediaRecorder && mediaStream) { - mediaRecorder.stop(); - mediaStream.getTracks().forEach((track) => track.stop()); - } - speechRecognition?.stop(); - setIsRecording(false); - }; - - function onLoad(spline) { - spline.setZoom(0.1); - const obj = spline.findObjectById('f5f3b334-53b6-4337-8497-c6815ba02c98'); - sphere.current = obj; - } - - const triggerStart = () => { - startRecording(); - console.log(sphere.current.scale); - gsap.to(sphere.current.scale, { - duration: 3, - x: 1.5, - y: 1.5, - z: 1.5, - ease: 'power3.out', - }); - }; - - const triggerEnd = () => { - stopRecording(); - gsap.to(sphere.current.scale, { - duration: 2, - x: 1, - y: 1, - z: 1, - ease: 'power3.out', - }); - }; - - return ( -
-
}> - {/* */} - {/* */} - - - - - ); -}; - -export default VoiceAI; +import { useState, useEffect, useRef } from "react"; +import axios from "axios"; +import apiUrl from "../../../config"; +import gsap from "gsap"; +import React, { Suspense } from "react"; + +const Spline = React.lazy(() => import("@splinetool/react-spline")); + +const VoiceAI = ({ updateUserMessage, updateGptResponse }) => { + const sphere = useRef(); + const [isRecording, setIsRecording] = useState(false); + const [mediaStream, setMediaStream] = useState(null); + const [mediaRecorder, setMediaRecorder] = useState(null); + const [speechRecognition, setSpeechRecognition] = useState(null); + + useEffect(() => { + const SpeechRecognition = + window.SpeechRecognition || window.webkitSpeechRecognition; + if (SpeechRecognition) { + const recognition = new SpeechRecognition(); + recognition.continuous = true; + recognition.interimResults = true; + + let accumulatedTranscript = ""; + + recognition.onresult = (event) => { + accumulatedTranscript = ""; + for (let i = 0; i < event.results.length; i++) { + accumulatedTranscript += event.results[i][0].transcript.trim() + " "; + } + updateUserMessage(accumulatedTranscript); + }; + + setSpeechRecognition(recognition); + } else { + console.warn("Speech recognition not supported in this browser."); + } + }, [updateUserMessage]); + + const startRecording = async () => { + const queryParams = new URLSearchParams({ + patient: "demo", + practitioner: "demo", + }); + + // Start recording audio + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const recorder = new MediaRecorder(stream); + const chunks = []; + + recorder.ondataavailable = (event) => { + if (event.data.size > 0) { + chunks.push(event.data); + } + }; + + recorder.onstop = async () => { + updateGptResponse(null); + // Process and send the audio data to the server for transcription + const audioBlob = new Blob(chunks, { type: "audio/wav" }); + const formData = new FormData(); + formData.append("audioFile", audioBlob, "recorded_audio.wav"); + + const response = await axios.post( + `${apiUrl}/conversation/send_message?${queryParams.toString()}`, + formData + ); + updateGptResponse(response.data.reply); + }; + + recorder.start(); + setMediaStream(stream); + setMediaRecorder(recorder); + speechRecognition?.start(); + setIsRecording(true); + }; + + const stopRecording = () => { + if (mediaRecorder && mediaStream) { + mediaRecorder.stop(); + mediaStream.getTracks().forEach((track) => track.stop()); + } + speechRecognition?.stop(); + setIsRecording(false); + }; + + function onLoad(spline) { + spline.setZoom(0.1); + const obj = spline.findObjectById("f5f3b334-53b6-4337-8497-c6815ba02c98"); + sphere.current = obj; + } + + const triggerStart = () => { + startRecording(); + console.log(sphere.current.scale); + gsap.to(sphere.current.scale, { + duration: 3, + x: 1.5, + y: 1.5, + z: 1.5, + ease: "power3.out", + }); + }; + + const triggerEnd = () => { + stopRecording(); + gsap.to(sphere.current.scale, { + duration: 2, + x: 1, + y: 1, + z: 1, + ease: "power3.out", + }); + }; + + return ( +
+
}> + {/* */} + {/* */} + + + + + ); +}; + +export default VoiceAI; diff --git a/frontend/src/views/practitioner/components/NewPatientModal.jsx b/frontend/src/views/practitioner/components/NewPatientModal.jsx index 45afa93..ccb1aac 100644 --- a/frontend/src/views/practitioner/components/NewPatientModal.jsx +++ b/frontend/src/views/practitioner/components/NewPatientModal.jsx @@ -1,116 +1,116 @@ -import { useState } from "react"; -import { getCurrentUser, db } from "../../../../firebaseConfig"; -import apiUrl from "../../../config"; -import axios from "axios"; - -const emptyForm = { - name: "", - email: "", - age: "", -}; - -const NewPatientModal = () => { - const [formData, setFormData] = useState(emptyForm); - - const handleChange = (e) => { - const { name, value } = e.target; - setFormData((prevData) => ({ ...prevData, [name]: value })); - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - try { - // Add the new patient data to Firestore - const currentUser = await getCurrentUser(); - const patientDoc = await db - .collection(`practitioners/${currentUser.uid}/patients`) - .add(formData); - - console.log(patientDoc.id); - // Send patient an email access link - await axios.post(`${apiUrl}/patient/send-link`, { - uid: patientDoc.id, - name: formData.name, - email: formData.email, - }); - - handleClose(); - } catch (error) { - console.error("Error adding new patient:", error); - } - }; - - const handleClose = () => { - setFormData(emptyForm); - document.getElementById("new_patient_modal").close(); - }; - - return ( - -
-

Register a New Patient

- {/*

Press ESC key or click the button below to close

*/} -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- ); -}; - -export default NewPatientModal; +import { useState } from "react"; +import { getCurrentUser, db } from "../../../../firebaseConfig"; +import apiUrl from "../../../config"; +import axios from "axios"; + +const emptyForm = { + name: "", + email: "", + age: "", +}; + +const NewPatientModal = () => { + const [formData, setFormData] = useState(emptyForm); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prevData) => ({ ...prevData, [name]: value })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + try { + // Add the new patient data to Firestore + const currentUser = await getCurrentUser(); + const patientDoc = await db + .collection(`practitioners/${currentUser.uid}/patients`) + .add(formData); + + // Send patient an email access link + await axios.post(`${apiUrl}/patient/send-link`, { + practitionId: currentUser.uid, + patientId: patientDoc.id, + name: formData.name, + email: formData.email, + }); + + handleClose(); + } catch (error) { + console.error("Error adding new patient:", error); + } + }; + + const handleClose = () => { + setFormData(emptyForm); + document.getElementById("new_patient_modal").close(); + }; + + return ( + +
+

Register a New Patient

+ {/*

Press ESC key or click the button below to close

*/} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ ); +}; + +export default NewPatientModal;