@studious-lms/server 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/pusher.d.ts +3 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +5 -3
- package/dist/lib/pusher.js.map +1 -1
- package/dist/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +4 -4
- package/dist/middleware/security.js.map +1 -1
- package/dist/models/class.d.ts +1 -1
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +5 -4
- package/dist/models/class.js.map +1 -1
- package/dist/models/conversation.d.ts +17 -17
- package/dist/models/notification.d.ts +63 -11
- package/dist/models/notification.d.ts.map +1 -1
- package/dist/models/notification.js +24 -10
- package/dist/models/notification.js.map +1 -1
- package/dist/models/worksheet.d.ts +1 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +3 -3
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +1 -0
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +7 -2
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts +2 -2
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +12 -4
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +3 -3
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +13 -9
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +2 -0
- package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
- package/dist/pipelines/pdfToWorksheet.js +3 -2
- package/dist/pipelines/pdfToWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +176 -82
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +2 -2
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +12 -2
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/notifications.d.ts +51 -41
- package/dist/routers/notifications.d.ts.map +1 -1
- package/dist/routers/notifications.js +22 -18
- package/dist/routers/notifications.js.map +1 -1
- package/dist/routers/school.d.ts +30 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +11 -2
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/user.d.ts +12 -0
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +11 -2
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +1 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/seedDatabase.d.ts +4 -0
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/services/aiChatStream.d.ts +5 -0
- package/dist/services/aiChatStream.d.ts.map +1 -1
- package/dist/services/aiChatStream.js +32 -3
- package/dist/services/aiChatStream.js.map +1 -1
- package/dist/services/aiStream.d.ts +5 -0
- package/dist/services/aiStream.d.ts.map +1 -1
- package/dist/services/aiStream.js +29 -3
- package/dist/services/aiStream.js.map +1 -1
- package/dist/services/aiUsage.d.ts +72 -0
- package/dist/services/aiUsage.d.ts.map +1 -0
- package/dist/services/aiUsage.js +150 -0
- package/dist/services/aiUsage.js.map +1 -0
- package/dist/services/announcement.d.ts.map +1 -1
- package/dist/services/announcement.js +7 -2
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +29 -4
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.js +10 -2
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +6 -2
- package/dist/services/class.js.map +1 -1
- package/dist/services/discussion.d.ts.map +1 -1
- package/dist/services/discussion.js +41 -12
- package/dist/services/discussion.js.map +1 -1
- package/dist/services/discussionSummary.d.ts.map +1 -1
- package/dist/services/discussionSummary.js +3 -2
- package/dist/services/discussionSummary.js.map +1 -1
- package/dist/services/notification.d.ts +79 -16
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/notification.js +64 -16
- package/dist/services/notification.js.map +1 -1
- package/dist/services/studentProgress.d.ts.map +1 -1
- package/dist/services/studentProgress.js +6 -2
- package/dist/services/studentProgress.js.map +1 -1
- package/dist/services/userPrivacy.d.ts +4 -0
- package/dist/services/userPrivacy.d.ts.map +1 -1
- package/dist/services/userPrivacy.js +6 -2
- package/dist/services/userPrivacy.js.map +1 -1
- package/dist/services/worksheet.d.ts +1 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +4 -4
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/inference.d.ts +27 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +23 -3
- package/dist/utils/inference.js.map +1 -1
- package/dist/workers/processors/aiLabChat.d.ts.map +1 -1
- package/dist/workers/processors/aiLabChat.js +4 -3
- package/dist/workers/processors/aiLabChat.js.map +1 -1
- package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -1
- package/dist/workers/processors/aiNewtonChat.js +5 -5
- package/dist/workers/processors/aiNewtonChat.js.map +1 -1
- package/dist/workers/processors/gradeWorksheet.js +4 -4
- package/dist/workers/processors/gradeWorksheet.js.map +1 -1
- package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -1
- package/dist/workers/processors/pdfToWorksheet.js +7 -4
- package/dist/workers/processors/pdfToWorksheet.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260708011500_add_ai_usage_attribution/migration.sql +13 -0
- package/prisma/migrations/20260708013000_add_notification_type_and_entity/migration.sql +14 -0
- package/prisma/schema.prisma +29 -5
- package/src/index.ts +7 -0
- package/src/lib/pusher.ts +3 -1
- package/src/middleware/security.ts +2 -2
- package/src/models/class.ts +3 -2
- package/src/models/notification.ts +42 -13
- package/src/models/worksheet.ts +1 -1
- package/src/pipelines/aiLabChat.ts +6 -1
- package/src/pipelines/aiNewtonChat.ts +10 -0
- package/src/pipelines/gradeWorksheet.ts +25 -7
- package/src/pipelines/pdfToWorksheet.ts +3 -0
- package/src/routers/announcement.ts +0 -1
- package/src/routers/assignment.ts +10 -0
- package/src/routers/notifications.ts +26 -24
- package/src/routers/school.ts +12 -0
- package/src/routers/user.ts +12 -0
- package/src/services/aiChatStream.ts +35 -0
- package/src/services/aiStream.ts +34 -0
- package/src/services/aiUsage.ts +202 -0
- package/src/services/announcement.ts +5 -0
- package/src/services/assignment.ts +29 -2
- package/src/services/attendance.ts +12 -0
- package/src/services/class.ts +4 -0
- package/src/services/discussion.ts +50 -11
- package/src/services/discussionSummary.ts +1 -0
- package/src/services/notification.ts +83 -20
- package/src/services/studentProgress.ts +4 -0
- package/src/services/userPrivacy.ts +4 -0
- package/src/services/worksheet.ts +2 -2
- package/src/utils/inference.ts +40 -2
- package/src/workers/processors/aiLabChat.ts +2 -1
- package/src/workers/processors/aiNewtonChat.ts +3 -1
- package/src/workers/processors/gradeWorksheet.ts +2 -2
- package/src/workers/processors/pdfToWorksheet.ts +5 -2
- package/tests/integration/routers/notification.test.ts +68 -20
- package/tests/unit/services/aiUsage.test.ts +105 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="777a2189-86f3-5317-9904-cac13606cce2")}catch(e){}}();
|
|
3
3
|
import { gradeWorksheetPipeline } from '../../pipelines/gradeWorksheet.js';
|
|
4
4
|
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
5
5
|
export const gradeWorksheetWorker = createRunJobWorker({
|
|
@@ -10,10 +10,10 @@ export const gradeWorksheetWorker = createRunJobWorker({
|
|
|
10
10
|
// throughput modest until we measure real-world usage.
|
|
11
11
|
limiter: { max: 5, duration: 60000 },
|
|
12
12
|
},
|
|
13
|
-
run: async ({ worksheetResponseId }) => {
|
|
14
|
-
await gradeWorksheetPipeline(worksheetResponseId);
|
|
13
|
+
run: async ({ worksheetResponseId }, ctx) => {
|
|
14
|
+
await gradeWorksheetPipeline(worksheetResponseId, ctx.userId);
|
|
15
15
|
return { worksheetResponseId };
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
//# sourceMappingURL=gradeWorksheet.js.map
|
|
19
|
-
//# debugId=
|
|
19
|
+
//# debugId=777a2189-86f3-5317-9904-cac13606cce2
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gradeWorksheet.js","sources":["workers/processors/gradeWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { gradeWorksheetPipeline } from '../../pipelines/gradeWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const gradeWorksheetWorker = createRunJobWorker({\n type: 'GRADE_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // Each pipeline run can fan out to many per-question inferences; keep\n // throughput modest until we measure real-world usage.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ worksheetResponseId }) => {\n await gradeWorksheetPipeline(worksheetResponseId);\n return { worksheetResponseId };\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,iBAAiB;IACvB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,sEAAsE;QACtE,uDAAuD;QACvD,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,mBAAmB,EAAE,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"gradeWorksheet.js","sources":["workers/processors/gradeWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { gradeWorksheetPipeline } from '../../pipelines/gradeWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const gradeWorksheetWorker = createRunJobWorker({\n type: 'GRADE_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // Each pipeline run can fan out to many per-question inferences; keep\n // throughput modest until we measure real-world usage.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ worksheetResponseId }, ctx) => {\n await gradeWorksheetPipeline(worksheetResponseId, ctx.userId);\n return { worksheetResponseId };\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,iBAAiB;IACvB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,sEAAsE;QACtE,uDAAuD;QACvD,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,mBAAmB,EAAE,EAAE,GAAG,EAAE,EAAE;QAC1C,MAAM,sBAAsB,CAAC,mBAAmB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC9D,OAAO,EAAE,mBAAmB,EAAE,CAAC;IACjC,CAAC;CACF,CAAC,CAAC","debug_id":"777a2189-86f3-5317-9904-cac13606cce2"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pdfToWorksheet.d.ts","sourceRoot":"/","sources":["workers/processors/pdfToWorksheet.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;;;;;;
|
|
1
|
+
{"version":3,"file":"pdfToWorksheet.d.ts","sourceRoot":"/","sources":["workers/processors/pdfToWorksheet.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;;;;;;eAgB/B,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="83752754-be47-54a0-b136-1b4140250056")}catch(e){}}();
|
|
3
3
|
import { pdfToWorksheet } from '../../pipelines/pdfToWorksheet.js';
|
|
4
4
|
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
5
5
|
export const pdfToWorksheetWorker = createRunJobWorker({
|
|
@@ -9,12 +9,15 @@ export const pdfToWorksheetWorker = createRunJobWorker({
|
|
|
9
9
|
// 5 jobs / minute, e.g. to stay under OpenAI rate limits.
|
|
10
10
|
limiter: { max: 5, duration: 60000 },
|
|
11
11
|
},
|
|
12
|
-
run: async ({ gcsUrl, classId, worksheetId }) => {
|
|
13
|
-
const worksheet = await pdfToWorksheet(gcsUrl, classId, {
|
|
12
|
+
run: async ({ gcsUrl, classId, worksheetId }, ctx) => {
|
|
13
|
+
const worksheet = await pdfToWorksheet(gcsUrl, classId, {
|
|
14
|
+
worksheetId,
|
|
15
|
+
userId: ctx.userId,
|
|
16
|
+
});
|
|
14
17
|
return worksheet
|
|
15
18
|
? { worksheetId: worksheet.id, name: worksheet.name }
|
|
16
19
|
: null;
|
|
17
20
|
},
|
|
18
21
|
});
|
|
19
22
|
//# sourceMappingURL=pdfToWorksheet.js.map
|
|
20
|
-
//# debugId=
|
|
23
|
+
//# debugId=83752754-be47-54a0-b136-1b4140250056
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pdfToWorksheet.js","sources":["workers/processors/pdfToWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { pdfToWorksheet } from '../../pipelines/pdfToWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const pdfToWorksheetWorker = createRunJobWorker({\n type: 'PDF_TO_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // 5 jobs / minute, e.g. to stay under OpenAI rate limits.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ gcsUrl, classId, worksheetId }) => {\n const worksheet = await pdfToWorksheet(gcsUrl, classId, {
|
|
1
|
+
{"version":3,"file":"pdfToWorksheet.js","sources":["workers/processors/pdfToWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { pdfToWorksheet } from '../../pipelines/pdfToWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const pdfToWorksheetWorker = createRunJobWorker({\n type: 'PDF_TO_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // 5 jobs / minute, e.g. to stay under OpenAI rate limits.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ gcsUrl, classId, worksheetId }, ctx) => {\n const worksheet = await pdfToWorksheet(gcsUrl, classId, {\n worksheetId,\n userId: ctx.userId,\n });\n return worksheet\n ? { worksheetId: worksheet.id, name: worksheet.name }\n : null;\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,kBAAkB;IACxB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,0DAA0D;QAC1D,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,EAAE;QACnD,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE;YACtD,WAAW;YACX,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;QACH,OAAO,SAAS;YACd,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;YACrD,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;CACF,CAAC,CAAC","debug_id":"83752754-be47-54a0-b136-1b4140250056"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- AlterTable
|
|
2
|
+
ALTER TABLE "AiUsageLog" ADD COLUMN "userId" TEXT,
|
|
3
|
+
ADD COLUMN "schoolId" TEXT,
|
|
4
|
+
ADD COLUMN "classId" TEXT;
|
|
5
|
+
|
|
6
|
+
-- CreateIndex
|
|
7
|
+
CREATE INDEX "AiUsageLog_userId_createdAt_idx" ON "AiUsageLog"("userId", "createdAt");
|
|
8
|
+
|
|
9
|
+
-- CreateIndex
|
|
10
|
+
CREATE INDEX "AiUsageLog_schoolId_createdAt_idx" ON "AiUsageLog"("schoolId", "createdAt");
|
|
11
|
+
|
|
12
|
+
-- CreateIndex
|
|
13
|
+
CREATE INDEX "AiUsageLog_classId_createdAt_idx" ON "AiUsageLog"("classId", "createdAt");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "NotificationType" AS ENUM ('ASSIGNMENT_POSTED', 'ASSIGNMENT_RETURNED', 'COMMENT_REPLY', 'DISCUSSION_REPLY', 'DISCUSSION_MENTION', 'ANNOUNCEMENT', 'ATTENDANCE', 'SYSTEM');
|
|
3
|
+
|
|
4
|
+
-- AlterTable
|
|
5
|
+
ALTER TABLE "Notification" ADD COLUMN "type" "NotificationType" NOT NULL DEFAULT 'SYSTEM',
|
|
6
|
+
ADD COLUMN "entityType" TEXT,
|
|
7
|
+
ADD COLUMN "entityId" TEXT,
|
|
8
|
+
ADD COLUMN "classId" TEXT;
|
|
9
|
+
|
|
10
|
+
-- CreateIndex
|
|
11
|
+
CREATE INDEX "Notification_receiverId_createdAt_idx" ON "Notification"("receiverId", "createdAt");
|
|
12
|
+
|
|
13
|
+
-- CreateIndex
|
|
14
|
+
CREATE INDEX "Notification_receiverId_read_idx" ON "Notification"("receiverId", "read");
|
package/prisma/schema.prisma
CHANGED
|
@@ -734,16 +734,34 @@ model Attendance {
|
|
|
734
734
|
absent User[] @relation("AbsentAttendance")
|
|
735
735
|
}
|
|
736
736
|
|
|
737
|
+
enum NotificationType {
|
|
738
|
+
ASSIGNMENT_POSTED
|
|
739
|
+
ASSIGNMENT_RETURNED
|
|
740
|
+
COMMENT_REPLY
|
|
741
|
+
DISCUSSION_REPLY
|
|
742
|
+
DISCUSSION_MENTION
|
|
743
|
+
ANNOUNCEMENT
|
|
744
|
+
ATTENDANCE
|
|
745
|
+
SYSTEM
|
|
746
|
+
}
|
|
747
|
+
|
|
737
748
|
model Notification {
|
|
738
|
-
id String
|
|
749
|
+
id String @id @default(uuid())
|
|
739
750
|
title String
|
|
740
751
|
content String
|
|
741
|
-
createdAt DateTime
|
|
752
|
+
createdAt DateTime @default(now())
|
|
742
753
|
senderId String? // no ID means sys
|
|
743
754
|
receiverId String
|
|
744
|
-
read Boolean
|
|
745
|
-
|
|
746
|
-
|
|
755
|
+
read Boolean @default(false)
|
|
756
|
+
type NotificationType @default(SYSTEM)
|
|
757
|
+
entityType String? // e.g. ASSIGNMENT, ANNOUNCEMENT, DISCUSSION, SUBMISSION
|
|
758
|
+
entityId String?
|
|
759
|
+
classId String?
|
|
760
|
+
sender User? @relation("SentNotifications", fields: [senderId], references: [id])
|
|
761
|
+
receiver User @relation("ReceivedNotifications", fields: [receiverId], references: [id], onDelete: Cascade)
|
|
762
|
+
|
|
763
|
+
@@index([receiverId, createdAt])
|
|
764
|
+
@@index([receiverId, read])
|
|
747
765
|
}
|
|
748
766
|
|
|
749
767
|
enum ConversationType {
|
|
@@ -837,9 +855,15 @@ model AiUsageLog {
|
|
|
837
855
|
estimatedInputTokens Int?
|
|
838
856
|
metadata Json? @default("{}")
|
|
839
857
|
error String?
|
|
858
|
+
userId String?
|
|
859
|
+
schoolId String?
|
|
860
|
+
classId String?
|
|
840
861
|
createdAt DateTime @default(now())
|
|
841
862
|
|
|
842
863
|
@@index([reason, createdAt])
|
|
864
|
+
@@index([userId, createdAt])
|
|
865
|
+
@@index([schoolId, createdAt])
|
|
866
|
+
@@index([classId, createdAt])
|
|
843
867
|
}
|
|
844
868
|
|
|
845
869
|
model Mention {
|
package/src/index.ts
CHANGED
|
@@ -272,6 +272,13 @@ app.post('/api/pusher/auth', async (req, res) => {
|
|
|
272
272
|
// }
|
|
273
273
|
// }
|
|
274
274
|
|
|
275
|
+
// Per-user notification channel: only the user themselves may subscribe
|
|
276
|
+
if (channel_name.startsWith('private-user-')) {
|
|
277
|
+
if (channel_name !== `private-user-${user.id}-notifications`) {
|
|
278
|
+
return res.status(403).json({ error: 'No access to this user channel' });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
275
282
|
if (channel_name.startsWith('presence-')) {
|
|
276
283
|
const authResponse = pusher.authorizeChannel(socket_id, channel_name, {
|
|
277
284
|
user_id: user.id,
|
package/src/lib/pusher.ts
CHANGED
|
@@ -19,5 +19,7 @@ const discussionChannel = (classId: string) => `private-class-${classId}-discuss
|
|
|
19
19
|
* worksheet-response channel so both can co-exist without auth collisions.
|
|
20
20
|
*/
|
|
21
21
|
const worksheetJobChannel = (worksheetId: string) => `private-worksheet-job-${worksheetId}`;
|
|
22
|
+
/** Per-user private channel for in-app notification pushes. */
|
|
23
|
+
const userNotificationChannel = (userId: string) => `private-user-${userId}-notifications`;
|
|
22
24
|
|
|
23
|
-
export { pusher, chatChannel, worksheetChannel, teacherChannel, discussionChannel, worksheetJobChannel };
|
|
25
|
+
export { pusher, chatChannel, worksheetChannel, teacherChannel, discussionChannel, worksheetJobChannel, userNotificationChannel };
|
|
@@ -20,7 +20,7 @@ const rateLimitHandler = (req: Request, res: Response) => {
|
|
|
20
20
|
// General API rate limiter - applies to all routes
|
|
21
21
|
export const generalLimiter = rateLimit({
|
|
22
22
|
windowMs: 10 * 60 * 1000, // 10 minutes
|
|
23
|
-
max:
|
|
23
|
+
max: 10000, // Limit each IP to 10000 requests per windowMs (schools often share one NAT IP)
|
|
24
24
|
message: 'Too many requests from this IP, please try again later.',
|
|
25
25
|
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
|
26
26
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
|
@@ -45,7 +45,7 @@ export const authLimiter = rateLimit({
|
|
|
45
45
|
// File upload rate limiter
|
|
46
46
|
export const uploadLimiter = rateLimit({
|
|
47
47
|
windowMs: 30 * 60 * 1000, // 30 minutes
|
|
48
|
-
max:
|
|
48
|
+
max: 500, // Limit each IP to 500 uploads per windowMs (a whole class may submit at once behind one IP)
|
|
49
49
|
message: 'Too many file uploads, please try again later.',
|
|
50
50
|
standardHeaders: true,
|
|
51
51
|
legacyHeaders: false,
|
package/src/models/class.ts
CHANGED
|
@@ -298,10 +298,11 @@ export function findSessionByClassCode(classCode: string) {
|
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
/** @returns
|
|
301
|
+
/** @returns Invite session for a class. */
|
|
302
302
|
export function findSessionByClassId(classId: string) {
|
|
303
303
|
return prisma.session.findFirst({
|
|
304
|
-
where: { classId },
|
|
304
|
+
where: { classId, type: SessionType.CLASS_INVITE },
|
|
305
|
+
orderBy: { expiresAt: "desc" },
|
|
305
306
|
});
|
|
306
307
|
}
|
|
307
308
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Notification model – in-app notifications, create, mark read.
|
|
3
3
|
*/
|
|
4
|
+
import type { NotificationType } from "@prisma/client";
|
|
4
5
|
import { prisma } from "../lib/prisma.js";
|
|
5
6
|
|
|
6
7
|
const notificationInclude = {
|
|
@@ -8,11 +9,28 @@ const notificationInclude = {
|
|
|
8
9
|
receiver: { select: { username: true } },
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
export type NotificationCreateData = {
|
|
13
|
+
receiverId: string;
|
|
14
|
+
title: string;
|
|
15
|
+
content: string;
|
|
16
|
+
senderId?: string | null;
|
|
17
|
+
type?: NotificationType;
|
|
18
|
+
entityType?: string | null;
|
|
19
|
+
entityId?: string | null;
|
|
20
|
+
classId?: string | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** @returns A page of notifications for a receiver, newest first. */
|
|
24
|
+
export function findNotificationsByReceiverIdPaginated(
|
|
25
|
+
receiverId: string,
|
|
26
|
+
limit: number,
|
|
27
|
+
cursor?: string | null
|
|
28
|
+
) {
|
|
13
29
|
return prisma.notification.findMany({
|
|
14
30
|
where: { receiverId },
|
|
15
|
-
orderBy: { createdAt: "desc" },
|
|
31
|
+
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
|
32
|
+
take: limit + 1,
|
|
33
|
+
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
|
|
16
34
|
include: notificationInclude,
|
|
17
35
|
});
|
|
18
36
|
}
|
|
@@ -25,23 +43,26 @@ export function findNotificationByIdForReceiver(id: string, receiverId: string)
|
|
|
25
43
|
});
|
|
26
44
|
}
|
|
27
45
|
|
|
46
|
+
/** @returns Number of unread notifications for a receiver. */
|
|
47
|
+
export function countUnreadNotifications(receiverId: string) {
|
|
48
|
+
return prisma.notification.count({
|
|
49
|
+
where: { receiverId, read: false },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
/** Create a notification for one receiver. */
|
|
29
|
-
export function createNotification(data: {
|
|
30
|
-
receiverId: string;
|
|
31
|
-
title: string;
|
|
32
|
-
content: string;
|
|
33
|
-
}) {
|
|
54
|
+
export function createNotification(data: NotificationCreateData) {
|
|
34
55
|
return prisma.notification.create({
|
|
35
56
|
data,
|
|
57
|
+
include: notificationInclude,
|
|
36
58
|
});
|
|
37
59
|
}
|
|
38
60
|
|
|
39
|
-
/** Create multiple notifications in one call. */
|
|
40
|
-
export function createManyNotifications(
|
|
41
|
-
|
|
42
|
-
) {
|
|
43
|
-
return prisma.notification.createMany({
|
|
61
|
+
/** Create multiple notifications in one call and return the created rows. */
|
|
62
|
+
export function createManyNotifications(data: NotificationCreateData[]) {
|
|
63
|
+
return prisma.notification.createManyAndReturn({
|
|
44
64
|
data,
|
|
65
|
+
include: notificationInclude,
|
|
45
66
|
});
|
|
46
67
|
}
|
|
47
68
|
|
|
@@ -58,3 +79,11 @@ export async function markNotificationAsReadForReceiver(id: string, receiverId:
|
|
|
58
79
|
|
|
59
80
|
return findNotificationByIdForReceiver(id, receiverId);
|
|
60
81
|
}
|
|
82
|
+
|
|
83
|
+
/** Mark all of a receiver's notifications as read. */
|
|
84
|
+
export function markAllNotificationsAsReadForReceiver(receiverId: string) {
|
|
85
|
+
return prisma.notification.updateMany({
|
|
86
|
+
where: { receiverId, read: false },
|
|
87
|
+
data: { read: true },
|
|
88
|
+
});
|
|
89
|
+
}
|
package/src/models/worksheet.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function findWorksheetsByClassId(classId: string) {
|
|
|
70
70
|
return prisma.worksheet.findMany({
|
|
71
71
|
where: { classId },
|
|
72
72
|
include: {
|
|
73
|
-
questions: { select: { id: true } },
|
|
73
|
+
questions: { select: { id: true, question: true }, orderBy: { order: "asc" } },
|
|
74
74
|
tags: true,
|
|
75
75
|
},
|
|
76
76
|
orderBy: { createdAt: "desc" },
|
|
@@ -378,7 +378,7 @@ export const generateAndSendLabIntroduction = async (
|
|
|
378
378
|
export const generateAndSendLabResponse = async (
|
|
379
379
|
labChatId: string,
|
|
380
380
|
teacherMessage: string,
|
|
381
|
-
emitOptions: { classId: string; aiMessageId: string }
|
|
381
|
+
emitOptions: { classId: string; aiMessageId: string; userId?: string | null }
|
|
382
382
|
): Promise<void> => {
|
|
383
383
|
try {
|
|
384
384
|
// Get lab context from database
|
|
@@ -499,6 +499,11 @@ export const generateAndSendLabIntroduction = async (
|
|
|
499
499
|
model: AI_CHAT_MODEL,
|
|
500
500
|
system,
|
|
501
501
|
messages,
|
|
502
|
+
usageReason: 'lab_chat_response',
|
|
503
|
+
attribution: {
|
|
504
|
+
userId: emitOptions.userId ?? fullLabChat.createdById,
|
|
505
|
+
classId: emitOptions.classId,
|
|
506
|
+
},
|
|
502
507
|
tools,
|
|
503
508
|
temperature: 0.7,
|
|
504
509
|
maxOutputTokens: 4096,
|
|
@@ -99,6 +99,7 @@ export const generateAndSendNewtonIntroduction = async (
|
|
|
99
99
|
conversationId: string,
|
|
100
100
|
submissionId: string,
|
|
101
101
|
aiMessageId: string,
|
|
102
|
+
actingUserId?: string | null,
|
|
102
103
|
): Promise<void> => {
|
|
103
104
|
try {
|
|
104
105
|
// Get submission details for context
|
|
@@ -109,6 +110,7 @@ export const generateAndSendNewtonIntroduction = async (
|
|
|
109
110
|
select: {
|
|
110
111
|
title: true,
|
|
111
112
|
instructions: true,
|
|
113
|
+
classId: true,
|
|
112
114
|
class: {
|
|
113
115
|
select: {
|
|
114
116
|
subject: true,
|
|
@@ -160,6 +162,11 @@ export const generateAndSendNewtonIntroduction = async (
|
|
|
160
162
|
],
|
|
161
163
|
temperature: 0.8,
|
|
162
164
|
maxOutputTokens: 400,
|
|
165
|
+
usageReason: 'newton_introduction',
|
|
166
|
+
attribution: {
|
|
167
|
+
userId: actingUserId,
|
|
168
|
+
classId: submission.assignment.classId,
|
|
169
|
+
},
|
|
163
170
|
});
|
|
164
171
|
|
|
165
172
|
if (!response) {
|
|
@@ -237,6 +244,7 @@ export const generateAndSendNewtonIntroduction = async (
|
|
|
237
244
|
};
|
|
238
245
|
},
|
|
239
246
|
aiMessageId: string,
|
|
247
|
+
actingUserId?: string | null,
|
|
240
248
|
): Promise<void> => {
|
|
241
249
|
try {
|
|
242
250
|
// Get recent conversation history (excluding the empty AI placeholder
|
|
@@ -339,6 +347,8 @@ export const generateAndSendNewtonIntroduction = async (
|
|
|
339
347
|
...(newtonClassId ? { tools: aiTools(newtonClassId), maxSteps: 5 } : {}),
|
|
340
348
|
temperature: 0.7,
|
|
341
349
|
maxOutputTokens: 1200,
|
|
350
|
+
usageReason: 'newton_response',
|
|
351
|
+
attribution: { userId: actingUserId, classId: newtonClassId },
|
|
342
352
|
});
|
|
343
353
|
|
|
344
354
|
if (!response) {
|
|
@@ -79,7 +79,11 @@ INSTRUCTIONS:
|
|
|
79
79
|
- Mark isCorrect=true only if the student earned full or near-full marks.`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const gradeWorksheetQuestion = async (
|
|
82
|
+
const gradeWorksheetQuestion = async (
|
|
83
|
+
worksheetResponseId: string,
|
|
84
|
+
worksheetQuestionProgressId: string,
|
|
85
|
+
actingUserId?: string | null,
|
|
86
|
+
) => {
|
|
83
87
|
|
|
84
88
|
await removeAllPreviousAIComments(worksheetQuestionProgressId);
|
|
85
89
|
|
|
@@ -155,6 +159,10 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
|
|
|
155
159
|
questionId: worksheetQuestionProgressId,
|
|
156
160
|
studentId: worksheetResponse.studentId,
|
|
157
161
|
},
|
|
162
|
+
attribution: {
|
|
163
|
+
userId: actingUserId ?? worksheetResponse.studentId,
|
|
164
|
+
classId: worksheetResponse.worksheet.classId,
|
|
165
|
+
},
|
|
158
166
|
},
|
|
159
167
|
).catch((error) => {
|
|
160
168
|
logger.error('Failed to grade worksheet response', { error });
|
|
@@ -259,7 +267,10 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
|
|
|
259
267
|
|
|
260
268
|
const DO_NOT_INFERENCE_STATUSES = [JobStatus.CANCELLED, JobStatus.PENDING, JobStatus.COMPLETED];
|
|
261
269
|
|
|
262
|
-
export const gradeWorksheetPipeline = async (
|
|
270
|
+
export const gradeWorksheetPipeline = async (
|
|
271
|
+
worksheetResponseId: string,
|
|
272
|
+
actingUserId?: string | null,
|
|
273
|
+
) => {
|
|
263
274
|
logger.info('Grading worksheet response', { worksheetResponseId });
|
|
264
275
|
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
265
276
|
where: { id: worksheetResponseId },
|
|
@@ -349,7 +360,7 @@ export const gradeWorksheetPipeline = async (worksheetResponseId: string) => {
|
|
|
349
360
|
}
|
|
350
361
|
|
|
351
362
|
try {
|
|
352
|
-
await gradeWorksheetQuestion(worksheetResponseId, response.id);
|
|
363
|
+
await gradeWorksheetQuestion(worksheetResponseId, response.id, actingUserId);
|
|
353
364
|
} catch (questionError) {
|
|
354
365
|
logger.error('Failed to grade individual question, continuing with remaining questions', {
|
|
355
366
|
questionId: response.questionId,
|
|
@@ -387,7 +398,10 @@ export const cancelGradePipeline = async (worksheetResponseId: string, worksheet
|
|
|
387
398
|
return updatedStudentQuestionProgress;
|
|
388
399
|
};
|
|
389
400
|
|
|
390
|
-
export const regradeAllPipeline = async (
|
|
401
|
+
export const regradeAllPipeline = async (
|
|
402
|
+
worksheetResponseId: string,
|
|
403
|
+
actingUserId?: string | null,
|
|
404
|
+
) => {
|
|
391
405
|
logger.info('Regrading all questions for worksheet response', { worksheetResponseId });
|
|
392
406
|
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
393
407
|
where: { id: worksheetResponseId },
|
|
@@ -434,7 +448,7 @@ export const regradeAllPipeline = async (worksheetResponseId: string) => {
|
|
|
434
448
|
// Re-grade each question sequentially
|
|
435
449
|
for (const response of worksheetResponse.responses) {
|
|
436
450
|
try {
|
|
437
|
-
await gradeWorksheetQuestion(worksheetResponseId, response.id);
|
|
451
|
+
await gradeWorksheetQuestion(worksheetResponseId, response.id, actingUserId);
|
|
438
452
|
} catch (questionError) {
|
|
439
453
|
logger.error('Failed to regrade question, continuing with remaining', {
|
|
440
454
|
questionId: response.questionId,
|
|
@@ -444,7 +458,11 @@ export const regradeAllPipeline = async (worksheetResponseId: string) => {
|
|
|
444
458
|
}
|
|
445
459
|
};
|
|
446
460
|
|
|
447
|
-
export const regradeWorksheetPipeline = async (
|
|
461
|
+
export const regradeWorksheetPipeline = async (
|
|
462
|
+
worksheetResponseId: string,
|
|
463
|
+
worksheetQuestionProgressId: string,
|
|
464
|
+
actingUserId?: string | null,
|
|
465
|
+
) => {
|
|
448
466
|
logger.info('Regrading worksheet response', { worksheetResponseId, worksheetQuestionProgressId });
|
|
449
467
|
try {
|
|
450
468
|
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
@@ -479,7 +497,7 @@ export const regradeWorksheetPipeline = async (worksheetResponseId: string, work
|
|
|
479
497
|
where: { id: worksheetQuestionProgressId },
|
|
480
498
|
});
|
|
481
499
|
|
|
482
|
-
await gradeWorksheetQuestion(worksheetResponseId, worksheetQuestionProgressId);
|
|
500
|
+
await gradeWorksheetQuestion(worksheetResponseId, worksheetQuestionProgressId, actingUserId);
|
|
483
501
|
|
|
484
502
|
return updatedStudentQuestionProgress;
|
|
485
503
|
} catch (error) {
|
|
@@ -179,6 +179,8 @@ export interface PdfToWorksheetOptions {
|
|
|
179
179
|
* channel. This is the path used by the BullMQ worker.
|
|
180
180
|
*/
|
|
181
181
|
worksheetId?: string;
|
|
182
|
+
/** Acting user for AI usage attribution. */
|
|
183
|
+
userId?: string | null;
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
/**
|
|
@@ -265,6 +267,7 @@ export async function pdfToWorksheet(
|
|
|
265
267
|
classId,
|
|
266
268
|
filename,
|
|
267
269
|
},
|
|
270
|
+
attribution: { userId: options.userId, classId },
|
|
268
271
|
}
|
|
269
272
|
);
|
|
270
273
|
} catch (error) {
|
|
@@ -21,7 +21,6 @@ import { getReactions as getCommentReactions } from "../services/comment.js";
|
|
|
21
21
|
import { logger } from "../utils/logger.js";
|
|
22
22
|
import { createDirectUploadFiles, type UploadedFile, type DirectUploadFile, confirmDirectUpload } from "../lib/fileUpload.js";
|
|
23
23
|
import { deleteFile } from "../lib/googleCloudStorage.js";
|
|
24
|
-
import { sendToMultiple } from "../services/notification.js";
|
|
25
24
|
|
|
26
25
|
// Schema for direct file uploads (no base64 data)
|
|
27
26
|
const directFileSchema = z.object({
|
|
@@ -437,6 +437,11 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
437
437
|
receiverIds,
|
|
438
438
|
title: `New comment on ${assignment.title}`,
|
|
439
439
|
content: `${getCommenterName(comment)} commented in ${assignment.class.name}:\n\n${input.content}\n\n[View assignment](/class/${assignment.class.id}/assignments/${assignment.id})`,
|
|
440
|
+
senderId: authorId,
|
|
441
|
+
type: "COMMENT_REPLY",
|
|
442
|
+
entityType: "ASSIGNMENT",
|
|
443
|
+
entityId: assignment.id,
|
|
444
|
+
classId: assignment.class.id,
|
|
440
445
|
}).catch((error) => {
|
|
441
446
|
logger.error("Failed to send assignment comment notifications:", error);
|
|
442
447
|
});
|
|
@@ -559,6 +564,11 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
559
564
|
receiverIds,
|
|
560
565
|
title: `New private comment on ${submission.assignment.title}`,
|
|
561
566
|
content: `${getCommenterName(comment)} commented on a submission in ${submission.assignment.class.name}:\n\n${input.content}\n\n[View submission](/class/${submission.assignment.class.id}/assignments/${submission.assignment.id}/submissions/${submission.id})`,
|
|
567
|
+
senderId: authorId,
|
|
568
|
+
type: "COMMENT_REPLY",
|
|
569
|
+
entityType: "ASSIGNMENT",
|
|
570
|
+
entityId: submission.assignment.id,
|
|
571
|
+
classId: submission.assignment.class.id,
|
|
562
572
|
}).catch((error) => {
|
|
563
573
|
logger.error("Failed to send submission comment notifications:", error);
|
|
564
574
|
});
|
|
@@ -3,41 +3,43 @@ import { z } from "zod";
|
|
|
3
3
|
import {
|
|
4
4
|
listNotifications,
|
|
5
5
|
getNotification,
|
|
6
|
-
|
|
7
|
-
sendToMultiple,
|
|
6
|
+
getUnreadCount,
|
|
8
7
|
markAsRead,
|
|
8
|
+
markAllAsRead,
|
|
9
9
|
} from "../services/notification.js";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Read-only notification API. Notifications are emitted exclusively by
|
|
13
|
+
* server-side domain services — there is intentionally no public "send"
|
|
14
|
+
* endpoint (the previous sendTo/sendToMultiple procedures allowed any logged-in
|
|
15
|
+
* user to notify arbitrary users and were removed).
|
|
16
|
+
*/
|
|
11
17
|
export const notificationRouter = createTRPCRouter({
|
|
12
|
-
list: protectedProcedure
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
list: protectedProcedure
|
|
19
|
+
.input(
|
|
20
|
+
z
|
|
21
|
+
.object({
|
|
22
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
23
|
+
cursor: z.string().nullish(),
|
|
24
|
+
})
|
|
25
|
+
.optional()
|
|
26
|
+
)
|
|
27
|
+
.query(({ ctx, input }) =>
|
|
28
|
+
listNotifications(ctx.user!.id, {
|
|
29
|
+
limit: input?.limit,
|
|
30
|
+
cursor: input?.cursor,
|
|
31
|
+
})
|
|
32
|
+
),
|
|
15
33
|
|
|
16
34
|
get: protectedProcedure
|
|
17
35
|
.input(z.object({ id: z.string() }))
|
|
18
36
|
.query(({ ctx, input }) => getNotification(ctx.user!.id, input.id)),
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
.input(
|
|
22
|
-
z.object({
|
|
23
|
-
receiverId: z.string(),
|
|
24
|
-
title: z.string(),
|
|
25
|
-
content: z.string(),
|
|
26
|
-
})
|
|
27
|
-
)
|
|
28
|
-
.mutation(({ input }) => sendNotification(input)),
|
|
29
|
-
|
|
30
|
-
sendToMultiple: protectedProcedure
|
|
31
|
-
.input(
|
|
32
|
-
z.object({
|
|
33
|
-
receiverIds: z.array(z.string()),
|
|
34
|
-
title: z.string(),
|
|
35
|
-
content: z.string(),
|
|
36
|
-
})
|
|
37
|
-
)
|
|
38
|
-
.mutation(({ input }) => sendToMultiple(input)),
|
|
38
|
+
unreadCount: protectedProcedure.query(({ ctx }) => getUnreadCount(ctx.user!.id)),
|
|
39
39
|
|
|
40
40
|
markAsRead: protectedProcedure
|
|
41
41
|
.input(z.object({ id: z.string() }))
|
|
42
42
|
.mutation(({ ctx, input }) => markAsRead(ctx.user!.id, input.id)),
|
|
43
|
+
|
|
44
|
+
markAllAsRead: protectedProcedure.mutation(({ ctx }) => markAllAsRead(ctx.user!.id)),
|
|
43
45
|
});
|
package/src/routers/school.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
updateMemberRole,
|
|
28
28
|
updateSchoolDetails,
|
|
29
29
|
} from "../services/school.js";
|
|
30
|
+
import { getSchoolAiUsage } from "../services/aiUsage.js";
|
|
30
31
|
|
|
31
32
|
const schoolRoleSchema = z.enum(["ADMIN", "TEACHER", "STUDENT"]);
|
|
32
33
|
|
|
@@ -142,6 +143,17 @@ export const schoolRouter = createTRPCRouter({
|
|
|
142
143
|
.input(schoolIdSchema)
|
|
143
144
|
.query(({ ctx, input }) => getAdminOverview(ctx.user!.id, input.schoolId)),
|
|
144
145
|
|
|
146
|
+
getAiUsage: protectedProcedure
|
|
147
|
+
.input(
|
|
148
|
+
z.object({
|
|
149
|
+
schoolId: z.string(),
|
|
150
|
+
startDate: z.date().optional(),
|
|
151
|
+
endDate: z.date().optional(),
|
|
152
|
+
topUsersLimit: z.number().min(1).max(50).optional(),
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
.query(({ ctx, input }) => getSchoolAiUsage(ctx.user!.id, input)),
|
|
156
|
+
|
|
145
157
|
listClasses: protectedProcedure
|
|
146
158
|
.input(schoolIdSchema)
|
|
147
159
|
.query(({ ctx, input }) => listClasses(ctx.user!.id, input.schoolId)),
|
package/src/routers/user.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
deleteUserAccount,
|
|
11
11
|
exportUserData,
|
|
12
12
|
} from "../services/userPrivacy.js";
|
|
13
|
+
import { getMyAiUsage } from "../services/aiUsage.js";
|
|
13
14
|
|
|
14
15
|
const fileUploadSchema = z.object({
|
|
15
16
|
filePath: z.string().min(1, "File path is required"),
|
|
@@ -89,6 +90,17 @@ export const userRouter = createTRPCRouter({
|
|
|
89
90
|
exportUserData(ctx.user!.id),
|
|
90
91
|
),
|
|
91
92
|
|
|
93
|
+
getMyAiUsage: protectedProcedure
|
|
94
|
+
.input(
|
|
95
|
+
z
|
|
96
|
+
.object({
|
|
97
|
+
startDate: z.date().optional(),
|
|
98
|
+
endDate: z.date().optional(),
|
|
99
|
+
})
|
|
100
|
+
.optional(),
|
|
101
|
+
)
|
|
102
|
+
.query(({ ctx, input }) => getMyAiUsage(ctx.user!.id, input ?? {})),
|
|
103
|
+
|
|
92
104
|
deleteAccount: protectedProcedure
|
|
93
105
|
.input(
|
|
94
106
|
z.object({
|