Skip to content

Commit dc3288a

Browse files
routing changes
1 parent d8e90b0 commit dc3288a

File tree

2 files changed

+196
-11
lines changed

2 files changed

+196
-11
lines changed

frontend/src/App.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ function App() {
119119
}
120120
/>
121121
<Route path="/home/*" element={getHomePage()} />
122-
<Route path="/course/*" element={getHomePage()} />
122+
<Route path="/course/*" element={<InstructorHomepage />} />
123123
</Routes>
124124
</Router>
125125
</UserContext.Provider>

frontend/src/pages/instructor/InstructorHomepage.jsx

+195-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useContext } from "react";
1+
import React, { useState, useEffect, useContext, useRef } from "react";
22
import {
33
Routes,
44
Route,
@@ -24,6 +24,7 @@ import {
2424
TablePagination,
2525
Button,
2626
} from "@mui/material";
27+
import { v4 as uuidv4 } from 'uuid';
2728
import PageContainer from "../Container";
2829
import InstructorHeader from "../../components/InstructorHeader";
2930
import InstructorSidebar from "./InstructorSidebar";
@@ -37,7 +38,10 @@ import StudentDetails from "./StudentDetails";
3738
import InstructorNewConcept from "./InstructorNewConcept";
3839
import InstructorConcepts from "./InstructorConcepts";
3940
import InstructorEditConcept from "./InstructorEditConcept";
41+
import ChatLogs from "./ChatLogs";
42+
import { useNotification } from "../../context/NotificationContext";
4043
import { UserContext } from "../../App";
44+
4145
function titleCase(str) {
4246
if (typeof str !== "string") {
4347
return str;
@@ -51,6 +55,133 @@ function titleCase(str) {
5155
.join(" ");
5256
}
5357

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+
54185
// course details page
55186
const CourseDetails = () => {
56187
const location = useLocation();
@@ -72,12 +203,18 @@ const CourseDetails = () => {
72203
);
73204
case "InstructorEditConcepts":
74205
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+
/>
76211
);
77212
case "PromptSettings":
78213
return <PromptSettings courseName={courseName} course_id={course_id} />;
79214
case "ViewStudents":
80215
return <ViewStudents courseName={courseName} course_id={course_id} />;
216+
case "ChatLogs":
217+
return <ChatLogs courseName={courseName} course_id={course_id} openWebSocket={openWebSocket} />;
81218
default:
82219
return (
83220
<InstructorAnalytics courseName={courseName} course_id={course_id} />
@@ -94,7 +231,7 @@ const CourseDetails = () => {
94231
>
95232
<InstructorHeader />
96233
</AppBar>
97-
<InstructorSidebar setSelectedComponent={setSelectedComponent} />
234+
<InstructorSidebar setSelectedComponent={setSelectedComponent} course_id={course_id} selectedComponent={selectedComponent} />
98235
{renderComponent()}
99236
</PageContainer>
100237
);
@@ -112,8 +249,10 @@ const InstructorHomepage = () => {
112249
const [searchQuery, setSearchQuery] = useState("");
113250
const [page, setPage] = useState(0);
114251
const [rowsPerPage, setRowsPerPage] = useState(5);
115-
const [courseData, setCourseData] = useState([]);
252+
const [courseData, setCourseData] = useState([]);
116253
const { isInstructorAsStudent } = useContext(UserContext);
254+
const { setNotificationForCourse } = useNotification();
255+
const hasFetched = useRef(false);
117256
const navigate = useNavigate();
118257

119258
useEffect(() => {
@@ -123,14 +262,15 @@ const InstructorHomepage = () => {
123262
}, [isInstructorAsStudent, navigate]);
124263
// connect to api data
125264
useEffect(() => {
265+
if (hasFetched.current) return;
266+
126267
const fetchCourses = async () => {
127268
try {
128269
const session = await fetchAuthSession();
129-
var token = session.tokens.idToken
270+
var token = session.tokens.idToken;
130271
const { email } = await fetchUserAttributes();
131272
const response = await fetch(
132-
`${
133-
import.meta.env.VITE_API_ENDPOINT
273+
`${import.meta.env.VITE_API_ENDPOINT
134274
}instructor/courses?email=${encodeURIComponent(email)}`,
135275
{
136276
method: "GET",
@@ -150,6 +290,7 @@ const InstructorHomepage = () => {
150290
id: course.course_id,
151291
}));
152292
setRows(formattedData);
293+
checkNotificationStatus(data, email, token);
153294
} else {
154295
console.error("Failed to fetch courses:", response.statusText);
155296
}
@@ -159,8 +300,47 @@ const InstructorHomepage = () => {
159300
};
160301

161302
fetchCourses();
303+
hasFetched.current = true;
162304
}, []);
163305

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+
164344
const handleSearchChange = (event) => {
165345
setSearchQuery(event.target.value);
166346
};
@@ -230,8 +410,13 @@ const InstructorHomepage = () => {
230410
onChange={handleSearchChange}
231411
sx={{ width: "100%", marginBottom: 2 }}
232412
/>
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+
>
235420
<Table aria-label="course table">
236421
<TableHead>
237422
<TableRow>
@@ -293,7 +478,7 @@ const InstructorHomepage = () => {
293478
</PageContainer>
294479
}
295480
/>
296-
<Route exact path=":courseName/*" element={<CourseDetails />} />
481+
<Route exact path=":courseName/*" element={<CourseDetails openWebSocket={openWebSocket} />} />
297482
<Route
298483
path=":courseName/edit-module/:moduleId"
299484
element={<InstructorEditCourse />}

0 commit comments

Comments
 (0)