@studious-lms/server 1.8.0 → 1.11.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/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +3 -3
- package/dist/middleware/security.js.map +1 -1
- package/dist/models/agenda.d.ts +2 -2
- package/dist/models/announcement.d.ts +2 -2
- package/dist/models/assignment.d.ts +33 -33
- package/dist/models/class.d.ts +2 -2
- package/dist/models/cloudStorage.d.ts +14 -1
- package/dist/models/cloudStorage.d.ts.map +1 -1
- package/dist/models/cloudStorage.js +123 -3
- package/dist/models/cloudStorage.js.map +1 -1
- package/dist/models/event.d.ts +3 -3
- package/dist/models/file.d.ts +27 -26
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/file.js +4 -39
- package/dist/models/file.js.map +1 -1
- package/dist/models/labChat.d.ts +6 -6
- package/dist/models/section.d.ts +1 -1
- package/dist/routers/_app.d.ts +444 -100
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts +2 -2
- package/dist/routers/assignment.d.ts +31 -31
- package/dist/routers/class.d.ts +5 -5
- package/dist/routers/event.d.ts +2 -2
- package/dist/routers/file.d.ts +2 -2
- package/dist/routers/folder.d.ts +24 -0
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +27 -5
- package/dist/routers/folder.js.map +1 -1
- package/dist/routers/labChat.d.ts +2 -2
- package/dist/routers/newtonChat.d.ts +1 -1
- package/dist/routers/permissions.d.ts +120 -0
- package/dist/routers/permissions.d.ts.map +1 -0
- package/dist/routers/permissions.js +50 -0
- package/dist/routers/permissions.js.map +1 -0
- package/dist/routers/studentProgress.d.ts +45 -0
- package/dist/routers/studentProgress.d.ts.map +1 -1
- package/dist/routers/studentProgress.js +12 -3
- package/dist/routers/studentProgress.js.map +1 -1
- package/dist/routers/tag.d.ts +5 -5
- package/dist/services/agenda.d.ts +2 -2
- package/dist/services/assignment.d.ts +31 -31
- package/dist/services/class.d.ts +5 -5
- package/dist/services/class.js +3 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/event.d.ts +2 -2
- package/dist/services/file.d.ts +22 -5
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +81 -45
- package/dist/services/file.js.map +1 -1
- package/dist/services/folder.d.ts +97 -2
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +158 -87
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +2 -2
- package/dist/services/permissions.d.ts +188 -0
- package/dist/services/permissions.d.ts.map +1 -0
- package/dist/services/permissions.js +661 -0
- package/dist/services/permissions.js.map +1 -0
- package/dist/services/studentProgress.d.ts +94 -0
- package/dist/services/studentProgress.d.ts.map +1 -1
- package/dist/services/studentProgress.js +94 -3
- package/dist/services/studentProgress.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
- package/prisma/schema.prisma +74 -0
- package/src/middleware/security.ts +1 -1
- package/src/models/cloudStorage.ts +134 -1
- package/src/models/file.ts +2 -37
- package/src/routers/_app.ts +2 -0
- package/src/routers/folder.ts +32 -2
- package/src/routers/permissions.ts +71 -0
- package/src/routers/studentProgress.ts +21 -0
- package/src/services/class.ts +1 -1
- package/src/services/file.ts +76 -47
- package/src/services/folder.ts +180 -91
- package/src/services/permissions.ts +778 -0
- package/src/services/studentProgress.ts +197 -11
- package/tests/unit/services/permissions.test.ts +463 -0
- package/tests/unit/services/studentProgress.test.ts +168 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { TRPCError } from "@trpc/server";
|
|
5
5
|
import type { AssignmentType } from "@prisma/client";
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
import { prisma } from "../lib/prisma.js";
|
|
7
8
|
import { isTeacherInClass } from "../models/class.js";
|
|
8
9
|
import { inference } from "../utils/inference.js";
|
|
@@ -38,10 +39,38 @@ type ProgressStudent = {
|
|
|
38
39
|
profile: { displayName: string | null } | null;
|
|
39
40
|
};
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const insightItemSchema = z.object({
|
|
43
|
+
label: z.string(),
|
|
44
|
+
evidence: z.string(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const studentProgressInsightsSchema = z.object({
|
|
48
|
+
summary: z.string(),
|
|
49
|
+
strengths: z.array(insightItemSchema).min(2).max(4),
|
|
50
|
+
growth: z.array(insightItemSchema).min(2).max(4),
|
|
51
|
+
talkingPoints: z.array(z.string()).min(3).max(5),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const studentProgressForecastSchema = z.object({
|
|
55
|
+
forecastLow: z.number().min(0).max(100),
|
|
56
|
+
forecastHigh: z.number().min(0).max(100),
|
|
57
|
+
risk: z.enum(["on_track", "watch", "at_risk"]),
|
|
58
|
+
reason: z.string(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const studentProgressFocusSchema = z.object({
|
|
62
|
+
focusItems: z.array(z.string()).min(3).max(5),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export type StudentProgressInsights = z.infer<
|
|
66
|
+
typeof studentProgressInsightsSchema
|
|
67
|
+
>;
|
|
68
|
+
export type StudentProgressForecast = z.infer<
|
|
69
|
+
typeof studentProgressForecastSchema
|
|
70
|
+
>;
|
|
71
|
+
export type StudentProgressFocus = z.infer<typeof studentProgressFocusSchema>;
|
|
72
|
+
|
|
73
|
+
type RecommendationType = "missing" | "low_score" | "awaiting_feedback";
|
|
45
74
|
|
|
46
75
|
function calculatePercentage(submission: GradeSubmission) {
|
|
47
76
|
if (submission.gradeReceived == null || !submission.assignment.maxGrade) {
|
|
@@ -131,8 +160,7 @@ function buildSuggestedAssignment(
|
|
|
131
160
|
percentage != null
|
|
132
161
|
? `${displayName} scored ${percentage}% and needs targeted reinforcement on the underlying concepts.`
|
|
133
162
|
: `${displayName} needs targeted reinforcement on the underlying concepts.`,
|
|
134
|
-
awaiting_feedback:
|
|
135
|
-
`${displayName} has work in progress here, so a short follow-up can keep momentum once feedback lands.`,
|
|
163
|
+
awaiting_feedback: `${displayName} has work in progress here, so a short follow-up can keep momentum once feedback lands.`,
|
|
136
164
|
};
|
|
137
165
|
|
|
138
166
|
return {
|
|
@@ -429,9 +457,14 @@ export async function getStudentProgressRecommendations(
|
|
|
429
457
|
studentId,
|
|
430
458
|
);
|
|
431
459
|
|
|
432
|
-
const summary = await summarizeStudentRecommendationsWithCases(
|
|
433
|
-
|
|
434
|
-
|
|
460
|
+
const summary = await summarizeStudentRecommendationsWithCases(
|
|
461
|
+
student,
|
|
462
|
+
submissions,
|
|
463
|
+
);
|
|
464
|
+
const {
|
|
465
|
+
recommendationCandidates: _recommendationCandidates,
|
|
466
|
+
...summaryResponse
|
|
467
|
+
} = summary;
|
|
435
468
|
return {
|
|
436
469
|
student: summaryResponse.student,
|
|
437
470
|
summary: summaryResponse.summary,
|
|
@@ -498,7 +531,10 @@ export async function getClassProgressRecommendations(
|
|
|
498
531
|
),
|
|
499
532
|
)
|
|
500
533
|
)
|
|
501
|
-
.map(
|
|
534
|
+
.map(
|
|
535
|
+
({ recommendationCandidates: _recommendationCandidates, ...student }) =>
|
|
536
|
+
student,
|
|
537
|
+
)
|
|
502
538
|
.filter((student) => student.recommendations.length > 0)
|
|
503
539
|
.sort((a, b) => b.priorityScore - a.priorityScore);
|
|
504
540
|
|
|
@@ -591,6 +627,152 @@ function buildProgressSummary(submissions: GradeSubmission[]) {
|
|
|
591
627
|
}));
|
|
592
628
|
}
|
|
593
629
|
|
|
630
|
+
function buildStructuredProgressContext(
|
|
631
|
+
classData: { id: string; name: string; subject: string | null },
|
|
632
|
+
student: ProgressStudent,
|
|
633
|
+
submissions: GradeSubmission[],
|
|
634
|
+
) {
|
|
635
|
+
const displayName = student.profile?.displayName ?? student.username;
|
|
636
|
+
const recommendationSummary = summarizeStudentRecommendations(
|
|
637
|
+
student,
|
|
638
|
+
submissions,
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
class: classData,
|
|
643
|
+
student: { id: student.id, username: student.username, displayName },
|
|
644
|
+
progress: {
|
|
645
|
+
overallGrade: getOverallGrade(submissions),
|
|
646
|
+
trend: calculateTrend(submissions),
|
|
647
|
+
summary: recommendationSummary.summary,
|
|
648
|
+
assignments: buildProgressSummary(submissions),
|
|
649
|
+
recommendations: recommendationSummary.recommendations.map(
|
|
650
|
+
(recommendation) => ({
|
|
651
|
+
title: recommendation.title,
|
|
652
|
+
recommendationType: recommendation.recommendationType,
|
|
653
|
+
percentage: recommendation.percentage,
|
|
654
|
+
submitted: recommendation.submitted,
|
|
655
|
+
reasons: recommendation.reasons,
|
|
656
|
+
sectionName: recommendation.sectionName,
|
|
657
|
+
}),
|
|
658
|
+
),
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function buildStructuredInferenceMessages(
|
|
664
|
+
instruction: string,
|
|
665
|
+
context: ReturnType<typeof buildStructuredProgressContext>,
|
|
666
|
+
) {
|
|
667
|
+
return [
|
|
668
|
+
{
|
|
669
|
+
role: "system" as const,
|
|
670
|
+
content:
|
|
671
|
+
"You are an educational progress analyst. Use only the provided class and grade context. Be specific, concise, supportive, and do not fabricate assignments, grades, or sections.",
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
role: "user" as const,
|
|
675
|
+
content: JSON.stringify(context),
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
role: "user" as const,
|
|
679
|
+
content: instruction,
|
|
680
|
+
},
|
|
681
|
+
];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export async function getStudentProgressInsights(
|
|
685
|
+
viewerId: string,
|
|
686
|
+
classId: string,
|
|
687
|
+
studentId: string,
|
|
688
|
+
) {
|
|
689
|
+
const { classData, student, submissions } = await loadStudentProgressContext(
|
|
690
|
+
viewerId,
|
|
691
|
+
classId,
|
|
692
|
+
studentId,
|
|
693
|
+
);
|
|
694
|
+
const context = buildStructuredProgressContext(
|
|
695
|
+
classData,
|
|
696
|
+
student,
|
|
697
|
+
submissions,
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
return inference<StudentProgressInsights>(
|
|
701
|
+
buildStructuredInferenceMessages(
|
|
702
|
+
[
|
|
703
|
+
"Generate an AI analytics summary for this one student's dashboard.",
|
|
704
|
+
"The summary must be 2-4 plain-prose sentences: lead with overall grade and trajectory, then call out one strength and one concern with assignment titles.",
|
|
705
|
+
"Strengths and growth items need 2-6 word labels and one-line evidence grounded in assignment titles or sections.",
|
|
706
|
+
"Talking points must be 3-5 specific teacher check-in prompts with no numbering.",
|
|
707
|
+
].join(" "),
|
|
708
|
+
context,
|
|
709
|
+
),
|
|
710
|
+
studentProgressInsightsSchema,
|
|
711
|
+
{
|
|
712
|
+
reason: "student_progress_insights",
|
|
713
|
+
metadata: { viewerId, classId, studentId },
|
|
714
|
+
},
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export async function getStudentProgressForecast(
|
|
719
|
+
viewerId: string,
|
|
720
|
+
classId: string,
|
|
721
|
+
studentId: string,
|
|
722
|
+
) {
|
|
723
|
+
const { classData, student, submissions } = await loadStudentProgressContext(
|
|
724
|
+
viewerId,
|
|
725
|
+
classId,
|
|
726
|
+
studentId,
|
|
727
|
+
);
|
|
728
|
+
const context = buildStructuredProgressContext(
|
|
729
|
+
classData,
|
|
730
|
+
student,
|
|
731
|
+
submissions,
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
return inference<StudentProgressForecast>(
|
|
735
|
+
buildStructuredInferenceMessages(
|
|
736
|
+
"Predict the student's next graded assignment percentage band and risk status. Base the forecast on trajectory, missing work, low scores, and assignment recency. Keep the reason to one sentence.",
|
|
737
|
+
context,
|
|
738
|
+
),
|
|
739
|
+
studentProgressForecastSchema,
|
|
740
|
+
{
|
|
741
|
+
reason: "student_progress_forecast",
|
|
742
|
+
metadata: { viewerId, classId, studentId },
|
|
743
|
+
},
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export async function getStudentProgressFocus(
|
|
748
|
+
viewerId: string,
|
|
749
|
+
classId: string,
|
|
750
|
+
studentId: string,
|
|
751
|
+
) {
|
|
752
|
+
const { classData, student, submissions } = await loadStudentProgressContext(
|
|
753
|
+
viewerId,
|
|
754
|
+
classId,
|
|
755
|
+
studentId,
|
|
756
|
+
);
|
|
757
|
+
const context = buildStructuredProgressContext(
|
|
758
|
+
classData,
|
|
759
|
+
student,
|
|
760
|
+
submissions,
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
return inference<StudentProgressFocus>(
|
|
764
|
+
buildStructuredInferenceMessages(
|
|
765
|
+
"Generate 3-5 student-facing weekly focus items. Each item should be specific, actionable, and grounded in the student's grade pattern or assignment titles. No numbering in the strings.",
|
|
766
|
+
context,
|
|
767
|
+
),
|
|
768
|
+
studentProgressFocusSchema,
|
|
769
|
+
{
|
|
770
|
+
reason: "student_progress_focus",
|
|
771
|
+
metadata: { viewerId, classId, studentId },
|
|
772
|
+
},
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
594
776
|
export async function chatAboutStudentProgress(
|
|
595
777
|
viewerId: string,
|
|
596
778
|
input: {
|
|
@@ -637,7 +819,11 @@ export async function chatAboutStudentProgress(
|
|
|
637
819
|
try {
|
|
638
820
|
const response = await inference<string>(messages, undefined, {
|
|
639
821
|
reason: "student_progress_chat",
|
|
640
|
-
metadata: {
|
|
822
|
+
metadata: {
|
|
823
|
+
viewerId,
|
|
824
|
+
classId: input.classId,
|
|
825
|
+
studentId: input.studentId,
|
|
826
|
+
},
|
|
641
827
|
});
|
|
642
828
|
if (typeof response !== "string" || response.trim().length === 0) {
|
|
643
829
|
throw new Error("Student progress chat returned an empty response");
|