1
- import React , { useState , useEffect , useContext } from "react" ;
1
+ import React , { useState , useEffect , useContext , useRef } from "react" ;
2
2
import {
3
3
Routes ,
4
4
Route ,
@@ -24,6 +24,7 @@ import {
24
24
TablePagination ,
25
25
Button ,
26
26
} from "@mui/material" ;
27
+ import { v4 as uuidv4 } from 'uuid' ;
27
28
import PageContainer from "../Container" ;
28
29
import InstructorHeader from "../../components/InstructorHeader" ;
29
30
import InstructorSidebar from "./InstructorSidebar" ;
@@ -37,7 +38,10 @@ import StudentDetails from "./StudentDetails";
37
38
import InstructorNewConcept from "./InstructorNewConcept" ;
38
39
import InstructorConcepts from "./InstructorConcepts" ;
39
40
import InstructorEditConcept from "./InstructorEditConcept" ;
41
+ import ChatLogs from "./ChatLogs" ;
42
+ import { useNotification } from "../../context/NotificationContext" ;
40
43
import { UserContext } from "../../App" ;
44
+
41
45
function titleCase ( str ) {
42
46
if ( typeof str !== "string" ) {
43
47
return str ;
@@ -51,6 +55,133 @@ function titleCase(str) {
51
55
. join ( " " ) ;
52
56
}
53
57
58
+ function constructWebSocketUrl ( ) {
59
+ const tempUrl = import . meta. env . VITE_GRAPHQL_WS_URL ; // Replace with your WebSocket URL
60
+ const apiUrl = tempUrl . replace ( "https://" , "wss://" ) ;
61
+ const urlObj = new URL ( apiUrl ) ;
62
+ const tmpObj = new URL ( tempUrl ) ;
63
+ const modifiedHost = urlObj . hostname . replace (
64
+ "appsync-api" ,
65
+ "appsync-realtime-api"
66
+ ) ;
67
+
68
+ urlObj . hostname = modifiedHost ;
69
+ const host = tmpObj . hostname ;
70
+ const header = {
71
+ host : host ,
72
+ Authorization : "API_KEY=" ,
73
+ } ;
74
+
75
+ const encodedHeader = btoa ( JSON . stringify ( header ) ) ;
76
+ const payload = "e30=" ;
77
+
78
+ return `${ urlObj . toString ( ) } ?header=${ encodedHeader } &payload=${ payload } ` ;
79
+ } ;
80
+
81
+ const removeCompletedNotification = async ( course_id ) => {
82
+ try {
83
+ console . log ( course_id )
84
+ const session = await fetchAuthSession ( ) ;
85
+ const token = session . tokens . idToken ;
86
+ const { email } = await fetchUserAttributes ( ) ;
87
+ const response = await fetch (
88
+ `${ import . meta. env . VITE_API_ENDPOINT } instructor/remove_completed_notification?course_id=${ encodeURIComponent ( course_id ) } &instructor_email=${ encodeURIComponent ( email ) } ` ,
89
+ {
90
+ method : "DELETE" ,
91
+ headers : { Authorization : token , "Content-Type" : "application/json" } ,
92
+ }
93
+ ) ;
94
+
95
+ if ( response . ok ) {
96
+ console . log ( "Notification removed successfully." ) ;
97
+ } else {
98
+ console . error ( "Failed to remove notification:" , response . statusText ) ;
99
+ }
100
+ } catch ( error ) {
101
+ console . error ( "Error removing completed notification:" , error ) ;
102
+ }
103
+ } ;
104
+
105
+ function openWebSocket ( courseName , course_id , requestId , setNotificationForCourse , onComplete ) {
106
+ // Open WebSocket connection
107
+ const wsUrl = constructWebSocketUrl ( ) ;
108
+ const ws = new WebSocket ( wsUrl , "graphql-ws" ) ;
109
+
110
+ // Handle WebSocket connection
111
+ ws . onopen = ( ) => {
112
+ console . log ( "WebSocket connection established" ) ;
113
+
114
+ // Initialize WebSocket connection
115
+ const initMessage = { type : "connection_init" } ;
116
+ ws . send ( JSON . stringify ( initMessage ) ) ;
117
+
118
+ // Subscribe to notifications
119
+ const subscriptionId = uuidv4 ( ) ;
120
+ const subscriptionMessage = {
121
+ id : subscriptionId ,
122
+ type : "start" ,
123
+ payload : {
124
+ data : `{"query":"subscription OnNotify($request_id: String!) { onNotify(request_id: $request_id) { message request_id } }","variables":{"request_id":"${ requestId } "}}` ,
125
+ extensions : {
126
+ authorization : {
127
+ Authorization : "API_KEY=" ,
128
+ host : new URL ( import . meta. env . VITE_GRAPHQL_WS_URL ) . hostname ,
129
+ } ,
130
+ } ,
131
+ } ,
132
+ } ;
133
+
134
+ ws . send ( JSON . stringify ( subscriptionMessage ) ) ;
135
+ console . log ( "Subscribed to WebSocket notifications" ) ;
136
+ } ;
137
+
138
+ ws . onmessage = ( event ) => {
139
+ const message = JSON . parse ( event . data ) ;
140
+ console . log ( "WebSocket message received:" , message ) ;
141
+
142
+ // Handle notification
143
+ if ( message . type === "data" && message . payload ?. data ?. onNotify ) {
144
+ const receivedMessage = message . payload . data . onNotify . message ;
145
+ console . log ( "Notification received:" , receivedMessage ) ;
146
+
147
+ // Sets icon to show new file on ChatLogs page
148
+ setNotificationForCourse ( course_id , true ) ;
149
+
150
+ // Remove row from database
151
+ removeCompletedNotification ( course_id ) ;
152
+
153
+ // Notify the instructor
154
+ alert ( `Chat logs are now available for ${ courseName } ` ) ;
155
+
156
+ // Close WebSocket after receiving the notification
157
+ ws . close ( ) ;
158
+ console . log ( "WebSocket connection closed after handling notification" ) ;
159
+
160
+ // Call the callback function after WebSocket completes
161
+ if ( typeof onComplete === "function" ) {
162
+ onComplete ( ) ;
163
+ }
164
+ }
165
+ } ;
166
+
167
+ ws . onerror = ( error ) => {
168
+ console . error ( "WebSocket error:" , error ) ;
169
+ ws . close ( ) ;
170
+ } ;
171
+
172
+ ws . onclose = ( ) => {
173
+ console . log ( "WebSocket closed" ) ;
174
+ } ;
175
+
176
+ // Set a timeout to close the WebSocket if no message is received
177
+ setTimeout ( ( ) => {
178
+ if ( ws && ws . readyState === WebSocket . OPEN ) {
179
+ console . warn ( "WebSocket timeout reached, closing connection" ) ;
180
+ ws . close ( ) ;
181
+ }
182
+ } , 180000 ) ;
183
+ } ;
184
+
54
185
// course details page
55
186
const CourseDetails = ( ) => {
56
187
const location = useLocation ( ) ;
@@ -72,12 +203,18 @@ const CourseDetails = () => {
72
203
) ;
73
204
case "InstructorEditConcepts" :
74
205
return (
75
- < InstructorConcepts courseName = { courseName } course_id = { course_id } setSelectedComponent = { setSelectedComponent } />
206
+ < InstructorConcepts
207
+ courseName = { courseName }
208
+ course_id = { course_id }
209
+ setSelectedComponent = { setSelectedComponent }
210
+ />
76
211
) ;
77
212
case "PromptSettings" :
78
213
return < PromptSettings courseName = { courseName } course_id = { course_id } /> ;
79
214
case "ViewStudents" :
80
215
return < ViewStudents courseName = { courseName } course_id = { course_id } /> ;
216
+ case "ChatLogs" :
217
+ return < ChatLogs courseName = { courseName } course_id = { course_id } openWebSocket = { openWebSocket } /> ;
81
218
default :
82
219
return (
83
220
< InstructorAnalytics courseName = { courseName } course_id = { course_id } />
@@ -94,7 +231,7 @@ const CourseDetails = () => {
94
231
>
95
232
< InstructorHeader />
96
233
</ AppBar >
97
- < InstructorSidebar setSelectedComponent = { setSelectedComponent } />
234
+ < InstructorSidebar setSelectedComponent = { setSelectedComponent } course_id = { course_id } selectedComponent = { selectedComponent } />
98
235
{ renderComponent ( ) }
99
236
</ PageContainer >
100
237
) ;
@@ -112,8 +249,10 @@ const InstructorHomepage = () => {
112
249
const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
113
250
const [ page , setPage ] = useState ( 0 ) ;
114
251
const [ rowsPerPage , setRowsPerPage ] = useState ( 5 ) ;
115
- const [ courseData , setCourseData ] = useState ( [ ] ) ;
252
+ const [ courseData , setCourseData ] = useState ( [ ] ) ;
116
253
const { isInstructorAsStudent } = useContext ( UserContext ) ;
254
+ const { setNotificationForCourse } = useNotification ( ) ;
255
+ const hasFetched = useRef ( false ) ;
117
256
const navigate = useNavigate ( ) ;
118
257
119
258
useEffect ( ( ) => {
@@ -123,14 +262,15 @@ const InstructorHomepage = () => {
123
262
} , [ isInstructorAsStudent , navigate ] ) ;
124
263
// connect to api data
125
264
useEffect ( ( ) => {
265
+ if ( hasFetched . current ) return ;
266
+
126
267
const fetchCourses = async ( ) => {
127
268
try {
128
269
const session = await fetchAuthSession ( ) ;
129
- var token = session . tokens . idToken
270
+ var token = session . tokens . idToken ;
130
271
const { email } = await fetchUserAttributes ( ) ;
131
272
const response = await fetch (
132
- `${
133
- import . meta. env . VITE_API_ENDPOINT
273
+ `${ import . meta. env . VITE_API_ENDPOINT
134
274
} instructor/courses?email=${ encodeURIComponent ( email ) } `,
135
275
{
136
276
method : "GET" ,
@@ -150,6 +290,7 @@ const InstructorHomepage = () => {
150
290
id : course . course_id ,
151
291
} ) ) ;
152
292
setRows ( formattedData ) ;
293
+ checkNotificationStatus ( data , email , token ) ;
153
294
} else {
154
295
console . error ( "Failed to fetch courses:" , response . statusText ) ;
155
296
}
@@ -159,8 +300,47 @@ const InstructorHomepage = () => {
159
300
} ;
160
301
161
302
fetchCourses ( ) ;
303
+ hasFetched . current = true ;
162
304
} , [ ] ) ;
163
305
306
+ const checkNotificationStatus = async ( courses , email , token ) => {
307
+ for ( const course of courses ) {
308
+ try {
309
+ const response = await fetch (
310
+ `${ import . meta. env . VITE_API_ENDPOINT } instructor/check_notifications_status?course_id=${ encodeURIComponent ( course . course_id ) } &instructor_email=${ encodeURIComponent ( email ) } ` ,
311
+ {
312
+ method : "GET" ,
313
+ headers : { Authorization : token , "Content-Type" : "application/json" } ,
314
+ }
315
+ ) ;
316
+ if ( response . ok ) {
317
+ const data = await response . json ( ) ;
318
+ if ( data . completionStatus === true ) {
319
+ console . log ( `Getting chatlogs for ${ course . course_name } is completed. Notifying the user and removing row from database.` ) ;
320
+
321
+ // Sets icon to show new file on ChatLogs page
322
+ setNotificationForCourse ( course . course_id , true ) ;
323
+
324
+ // Remove row from database
325
+ removeCompletedNotification ( course . course_id ) ;
326
+
327
+ // Notify the Instructor
328
+ alert ( `Chat logs are available for course: ${ course . course_name } ` ) ;
329
+
330
+ } else if ( data . completionStatus === false ) {
331
+ // Reopen WebSocket to listen for notifications
332
+ console . log ( `Getting chatlogs for ${ course . course_name } is not completed. Re-opening the websocket.` ) ;
333
+ openWebSocket ( course . course_name , course . course_id , data . requestId , setNotificationForCourse ) ;
334
+ } else {
335
+ console . log ( `Either chatlogs for ${ course . course_name } were not requested or instructor already received notification. No need to notify instructor or re-open websocket.` ) ;
336
+ }
337
+ }
338
+ } catch ( error ) {
339
+ console . error ( "Error checking notification status for" , course . course_id , error ) ;
340
+ }
341
+ }
342
+ } ;
343
+
164
344
const handleSearchChange = ( event ) => {
165
345
setSearchQuery ( event . target . value ) ;
166
346
} ;
@@ -230,8 +410,13 @@ const InstructorHomepage = () => {
230
410
onChange = { handleSearchChange }
231
411
sx = { { width : "100%" , marginBottom : 2 } }
232
412
/>
233
- < TableContainer sx = { { width : "100%" , maxHeight : "70vh" ,
234
- overflowY : "auto" , } } >
413
+ < TableContainer
414
+ sx = { {
415
+ width : "100%" ,
416
+ maxHeight : "70vh" ,
417
+ overflowY : "auto" ,
418
+ } }
419
+ >
235
420
< Table aria-label = "course table" >
236
421
< TableHead >
237
422
< TableRow >
@@ -293,7 +478,7 @@ const InstructorHomepage = () => {
293
478
</ PageContainer >
294
479
}
295
480
/>
296
- < Route exact path = ":courseName/*" element = { < CourseDetails /> } />
481
+ < Route exact path = ":courseName/*" element = { < CourseDetails openWebSocket = { openWebSocket } /> } />
297
482
< Route
298
483
path = ":courseName/edit-module/:moduleId"
299
484
element = { < InstructorEditCourse /> }
0 commit comments