Skip to content

Commit

Permalink
fix(EWS): finalize EWS UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner committed Jul 5, 2024
1 parent 49cee4c commit 1acefb3
Show file tree
Hide file tree
Showing 18 changed files with 1,315 additions and 891 deletions.
14 changes: 3 additions & 11 deletions app/(authorized)/early-warning/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import PageHeader from "@/components/PageHeader";
import GenericPageContainer from "@/components/GenericPageContainer";
import EarlyWarningBanner from "@/components/EarlyWarningBanner";
import { getEWSStatus } from "@/lib/ews-functions";
import EarlyWarningStudentRow from "@/components/EarlyWarningStudentRow";
import { getEWSResults } from "@/lib/ews-functions";
import EarlyWarningResults from "@/components/EarlyWarningResults";

export default async function EarlyWarning() {
return (
Expand All @@ -11,14 +10,7 @@ export default async function EarlyWarning() {
title="Early Warning"
subtitle="Use predictive analysis to identify students at-risk based on their academic performance."
/>
<EarlyWarningBanner getStatus={getEWSStatus} />
<EarlyWarningStudentRow
courseAverageDiff={"4%"}
courseAverageDirection="below"
estimatedFinalGrade={"76%"}
likelihood={"67%"}
email="demostudent@mail.com"
/>
<EarlyWarningResults getData={getEWSResults} />
</GenericPageContainer>
);
}
16 changes: 16 additions & 0 deletions app/api/ews/update/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import EarlyWarningSystem from "@/lib/EarlyWarningSystem";

export async function GET() {
try {
const ews = new EarlyWarningSystem();

ews.updateEWSData(); // Don't await, just run in the background

return Response.json({
data: "EWS data update initiated. Check the logs for progress.",
});
} catch (err) {
console.error(err);
return Response.json({ error: err });
}
}
30 changes: 30 additions & 0 deletions app/api/ews/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import EarlyWarningSystem from "@/lib/EarlyWarningSystem";
import { BatchPredictWebhookData } from "@/lib/types/ews";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
try {
const reqData = await request.json();
if (!reqData) throw new Error("No data provided");

const { state, course_id } = reqData as BatchPredictWebhookData;

if (state === "error") {
console.error(
`An EWS prediction error was sent via webhook for course ${course_id}`
);
return Response.json({ success: true });
}

const predictions = reqData.predictions;
if (!predictions) throw new Error("No predictions provided");

const ews = new EarlyWarningSystem();
await ews.updateEWSPredictions(course_id, predictions);

return Response.json({ success: true });
} catch (err) {
console.error(err);
return Response.json({ error: err });
}
}
68 changes: 17 additions & 51 deletions components/EarlyWarningBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,52 @@
"use client";
import { EarlyWarningStatus } from "@/lib/types/ews";
import { useGlobalContext } from "@/state/globalContext";
import { useEffect, useMemo, useState } from "react";
import { ewsStatusHeader, ewsStatusMessage } from "@/utils/ews-helpers";
import { Card } from "react-bootstrap";
import {
CheckCircleFill,
DashCircle,
DashCircleFill,
ExclamationCircleFill,
} from "react-bootstrap-icons";
import EarlyWarningInfo from "./EarlyWarningInfo";

interface EarlyWarningBanner {
getStatus: (course_id: string) => Promise<EarlyWarningStatus>;
status: EarlyWarningStatus;
}

const EarlyWarningBanner: React.FC<EarlyWarningBanner> = ({ getStatus }) => {
const [globalState] = useGlobalContext();
const [status, setStatus] = useState<EarlyWarningStatus>("insufficient-data");

useEffect(() => {
if(!globalState.courseID) return;
//getStatus(globalState.courseID).then((status) => setStatus(status));
setStatus("warning")
}, [globalState.courseID]);

const statusIcon = useMemo(() => {
const EarlyWarningBanner: React.FC<EarlyWarningBanner> = ({ status }) => {
const ewsStatusIcon = (status: string): JSX.Element => {
switch (status) {
case "success":
return <CheckCircleFill size={36} color="green" />;
case "danger":
return <ExclamationCircleFill size={36} color="red" />;
case "warning":
return <DashCircleFill size={36} color="orange" />;
return <DashCircleFill size={36} color="orange" />;
case "insufficient-data":
return <DashCircle size={36} color="gray" />;
default:
return <DashCircle />;
}
}, [status]);

const statusHeader = useMemo(() => {
switch (status) {
case "success":
return "Looks Good";
case "danger":
return "Attention Needed";
case "warning":
return "Warning";
case "insufficient-data":
return "Insufficient Data";
default:
return "Unknown";
}
}, [status]);

const statusMessage = useMemo(() => {
switch (status) {
case "success":
return "No students were identified as 'at-risk'. Keep up the good work!";
case "danger":
return "We have identified a number of students in need of intervention.";
case "warning":
return "We have identified a few students as 'at-risk'. Intervention may be needed soon.";
case "insufficient-data":
return "Sorry, we don't have enough data to make performance predictions. Performance predictions improve with more student enrollments and assignment data.";
default:
return "Unknown";
}
}, [status]);
};

return (
<Card className="tw-shadow-sm tw-mb-4">
<Card.Body>
<Card.Title>Quick Look</Card.Title>
<Card.Title className="tw-flex tw-flex-row tw-justify-between tw-items-center tw-mb-0">
<p>Quick Look</p>
<EarlyWarningInfo />
</Card.Title>
<Card.Body className="!tw-p-0">
<>
<div className="tw-flex tw-flex-row tw-mt-4">
<div className="tw-mr-4 tw-mt-0.5">{statusIcon}</div>
<p className="tw-text-4xl tw-font-semibold">{statusHeader}</p>
<div className="tw-flex tw-flex-row">
<div className="tw-mr-2 tw-mt-0.5">{ewsStatusIcon(status)}</div>
<p className="tw-text-4xl tw-font-semibold">
{ewsStatusHeader(status)}
</p>
</div>
<p className="tw-mb-0 tw-text-xs tw-text-slate-400">
{statusMessage}
{ewsStatusMessage(status)}
</p>
</>
</Card.Body>
Expand Down
71 changes: 71 additions & 0 deletions components/EarlyWarningInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";
import { useState } from "react";
import { Button, Modal } from "react-bootstrap";

interface EarlyWarningInfoProps {}

const EarlyWarningInfo: React.FC<EarlyWarningInfoProps> = () => {
const [modalOpen, setModalOpen] = useState(false);
return (
<div>
<Button
onClick={() => setModalOpen(true)}
variant="outline-info"
size="sm"
>
About Early Warning
</Button>
<Modal
show={modalOpen}
onHide={() => setModalOpen(false)}
size="lg"
aria-labelledby="contained-modal-title-vcenter"
centered
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter">
About Early Warning System
</Modal.Title>
</Modal.Header>
<Modal.Body>
<h5>How are predictions made?</h5>
<p>
The Early Warning System uses a machine learning predictive model to
estimate student risk based on their academic performance relative
to the averaged metrics of their peers. The model is updated every
12 hours and uses the following metrics:
</p>
<ul>
<li>Unweight Course Percentage</li>
<li># of Unique Interaction Days</li>
<li>% of Course Content Seen</li>
<li>Per Assignment</li>
<ul>
<li>Unweighted Score</li>
<li>Time on Task</li>
<li>Time in Review</li>
</ul>
</ul>
<h5>How is risk calculated?</h5>
<p>
The Early Warning System assumes a final grade of 70% as the passing
threshold. Students with a predicted final grade of less than 79%
are flagged with a risk level of "Warning". Students with a
predicted final grade of less than 69% are flagged with a risk level
of "Attention Needed".
</p>
<p>
<strong>Note:</strong> Early Warning System predictions are not a
guarantee of student performance and should only be used as a tool
to identify students who may need additional support.
</p>
</Modal.Body>
<Modal.Footer>
<Button onClick={() => setModalOpen(false)}>Close</Button>
</Modal.Footer>
</Modal>
</div>
);
};

export default EarlyWarningInfo;
55 changes: 55 additions & 0 deletions components/EarlyWarningResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";
import { useGlobalContext } from "@/state/globalContext";
import { useEffect, useMemo, useState } from "react";
import { EWSResult, EarlyWarningStatus } from "@/lib/types";
import EarlyWarningStudentRow from "./EarlyWarningStudentRow";
import EarlyWarningBanner from "./EarlyWarningBanner";

interface EarlyWarningResultsProps {
getData: (course_id: string, privacy: boolean) => Promise<EWSResult[]>;
}

const EarlyWarningResults: React.FC<EarlyWarningResultsProps> = ({
getData,
}) => {
const [globalState] = useGlobalContext();
const [data, setData] = useState<EWSResult[]>([]);

useEffect(() => {
fetchData();
}, [globalState.courseID, globalState.ferpaPrivacy]);

async function fetchData() {
try {
if (!globalState.courseID) return;
const res = await getData(globalState.courseID, globalState.ferpaPrivacy);

if (!res) return;
setData(res);
} catch (err) {
console.error(err);
}
}

const overallStatus: EarlyWarningStatus = useMemo(() => {
if (!data.length) return "success";
const dangerCount = data.filter((student) => student.status === "danger");
const warningCount = data.filter((student) => student.status === "warning");

if (dangerCount.length) return "danger";
if (warningCount.length) return "warning";
return "success";
}, [data]);

return (
<div className="tw-flex tw-flex-col">
<EarlyWarningBanner status={overallStatus} />
{data.length > 0 &&
data.map((result) => (
<EarlyWarningStudentRow key={result.name} data={result} />
))}
</div>
);
};

export default EarlyWarningResults;
Loading

0 comments on commit 1acefb3

Please sign in to comment.