diff --git a/packages/common/index.ts b/packages/common/index.ts
index f39b4a0ee..075261d27 100644
--- a/packages/common/index.ts
+++ b/packages/common/index.ts
@@ -2230,6 +2230,25 @@ export enum AlertType {
REPHRASE_QUESTION = 'rephraseQuestion',
EVENT_ENDED_CHECKOUT_STAFF = 'eventEndedCheckoutStaff',
PROMPT_STUDENT_TO_LEAVE_QUEUE = 'promptStudentToLeaveQueue',
+ DOCUMENT_PROCESSED = 'documentProcessed',
+ ASYNC_QUESTION_UPDATE = 'asyncQuestionUpdate',
+}
+
+// Allowed combinations to enforce front/back consistency
+export const FEED_ALERT_TYPES: AlertType[] = [
+ AlertType.DOCUMENT_PROCESSED,
+ AlertType.ASYNC_QUESTION_UPDATE,
+]
+
+export const MODAL_ALERT_TYPES: AlertType[] = [
+ AlertType.REPHRASE_QUESTION,
+ AlertType.EVENT_ENDED_CHECKOUT_STAFF,
+ AlertType.PROMPT_STUDENT_TO_LEAVE_QUEUE,
+]
+
+export enum AlertDeliveryMode {
+ MODAL = 'modal',
+ FEED = 'feed',
}
export class AlertPayload {}
@@ -2238,12 +2257,19 @@ export class Alert {
@IsEnum(AlertType)
alertType!: AlertType
+ @IsEnum(AlertDeliveryMode)
+ deliveryMode!: AlertDeliveryMode
+
@IsDate()
sent!: Date
@Type(() => AlertPayload)
payload!: AlertPayload
+ @IsOptional()
+ @IsDate()
+ readAt?: Date
+
@IsInt()
id!: number
}
@@ -2266,6 +2292,35 @@ export class PromptStudentToLeaveQueuePayload extends AlertPayload {
queueQuestionId?: number
}
+export class DocumentProcessedPayload extends AlertPayload {
+ @IsInt()
+ documentId!: number
+
+ @IsString()
+ documentName!: string
+}
+
+export class AsyncQuestionUpdatePayload extends AlertPayload {
+ @IsInt()
+ questionId!: number
+
+ @IsInt()
+ courseId!: number
+
+ @IsString()
+ @IsOptional()
+ subtype?:
+ | 'commentOnMyPost'
+ | 'commentOnOthersPost'
+ | 'humanAnswered'
+ | 'statusChanged'
+ | 'upvoted'
+
+ @IsString()
+ @IsOptional()
+ summary?: string
+}
+
export class OrganizationCourseResponse {
@IsInt()
id?: number
@@ -2296,6 +2351,10 @@ export class CreateAlertParams {
@IsEnum(AlertType)
alertType!: AlertType
+ @IsOptional()
+ @IsEnum(AlertDeliveryMode)
+ deliveryMode?: AlertDeliveryMode
+
@IsInt()
courseId!: number
@@ -2311,6 +2370,9 @@ export class CreateAlertResponse extends Alert {}
export class GetAlertsResponse {
@Type(() => Alert)
alerts!: Alert[]
+ @IsOptional()
+ @IsInt()
+ total?: number
}
// not used anywhere
diff --git a/packages/frontend/app/(dashboard)/course/[cid]/layout.tsx b/packages/frontend/app/(dashboard)/course/[cid]/layout.tsx
index 492acb510..a927b272c 100644
--- a/packages/frontend/app/(dashboard)/course/[cid]/layout.tsx
+++ b/packages/frontend/app/(dashboard)/course/[cid]/layout.tsx
@@ -1,7 +1,10 @@
'use client'
-import { use } from 'react'
+import { use, useEffect } from 'react'
+import useSWR from 'swr'
import AlertsContainer from '@/app/components/AlertsContainer'
+import { useAlertsContext } from '@/app/contexts/alertsContext'
+import { API } from '@/app/api'
type Params = Promise<{ cid: string }>
@@ -15,6 +18,24 @@ export default function Layout(props: {
const { cid } = params
+ const { setThisCourseAlerts, clearThisCourseAlerts } = useAlertsContext()
+
+ const { data } = useSWR(
+ `/api/v1/alerts/course/${cid}`,
+ async () => API.alerts.get(Number(cid)),
+ { refreshInterval: 60000 },
+ )
+
+ useEffect(() => {
+ setThisCourseAlerts(data?.alerts ?? [])
+ }, [data?.alerts])
+
+ useEffect(() => {
+ return () => {
+ clearThisCourseAlerts()
+ }
+ }, [])
+
return (
<>
diff --git a/packages/frontend/app/(dashboard)/course/[cid]/queue/[qid]/components/TAQuestionCardButtons.tsx b/packages/frontend/app/(dashboard)/course/[cid]/queue/[qid]/components/TAQuestionCardButtons.tsx
index 661fec367..5180dff44 100644
--- a/packages/frontend/app/(dashboard)/course/[cid]/queue/[qid]/components/TAQuestionCardButtons.tsx
+++ b/packages/frontend/app/(dashboard)/course/[cid]/queue/[qid]/components/TAQuestionCardButtons.tsx
@@ -7,6 +7,7 @@ import {
UndoOutlined,
} from '@ant-design/icons'
import {
+ AlertDeliveryMode,
AlertType,
ClosedQuestionStatus,
ERROR_MESSAGES,
@@ -159,6 +160,7 @@ const TAQuestionCardButtons: React.FC = ({
courseId,
payload,
targetUserId: question.creatorId,
+ deliveryMode: AlertDeliveryMode.FEED,
})
// await mutateQuestions()
message.success('Successfully asked student to rephrase their question.')
diff --git a/packages/frontend/app/(dashboard)/layout.tsx b/packages/frontend/app/(dashboard)/layout.tsx
index 8d83ae080..780723de8 100644
--- a/packages/frontend/app/(dashboard)/layout.tsx
+++ b/packages/frontend/app/(dashboard)/layout.tsx
@@ -15,6 +15,7 @@ import ChatbotContextProvider from './course/[cid]/components/chatbot/ChatbotPro
import FooterBar from './components/FooterBar'
import { AsyncToasterProvider } from '../contexts/AsyncToasterContext'
import { LogoutOutlined, ReloadOutlined } from '@ant-design/icons'
+import { AlertsProvider } from '../contexts/alertsContext'
import { getErrorMessage } from '../utils/generalUtils'
const Layout: React.FC = ({ children }) => {
@@ -96,37 +97,39 @@ const Layout: React.FC = ({ children }) => {
) : (
-
-
-
- Skip to main content
-
-
-
-
- {/* This flex flex-grow is needed so that the scroll bar doesn't show up on every page */}
-
-
- {pathname === '/courses' && (
-
- )}
- {/* On certain pages (like pages with big tables), we want to let the width take up the whole page */}
- {URLSegments[4] === 'edit_questions' ||
- URLSegments[4] === 'chatbot_questions' ? (
-
{children}
- ) : (
-
- {children}
-
- )}
-
-
+
+
+
+
+ Skip to main content
+
+
+
+
+ {/* This flex flex-grow is needed so that the scroll bar doesn't show up on every page */}
+
+
+ {pathname === '/courses' && (
+
+ )}
+ {/* On certain pages (like pages with big tables), we want to let the width take up the whole page */}
+ {URLSegments[4] === 'edit_questions' ||
+ URLSegments[4] === 'chatbot_questions' ? (
+