@studious-lms/server 1.25.0 → 1.27.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.
Files changed (175) hide show
  1. package/.env.example +3 -1
  2. package/.github/workflows/npm-publish.yml +1 -0
  3. package/AGENTS.md +84 -0
  4. package/dist/models/cloudStorage.d.ts +11 -0
  5. package/dist/models/cloudStorage.d.ts.map +1 -1
  6. package/dist/models/cloudStorage.js +18 -2
  7. package/dist/models/cloudStorage.js.map +1 -1
  8. package/dist/models/conversation.d.ts +1 -0
  9. package/dist/models/conversation.d.ts.map +1 -1
  10. package/dist/models/labChat.d.ts +40 -0
  11. package/dist/models/labChat.d.ts.map +1 -1
  12. package/dist/models/labChat.js +32 -2
  13. package/dist/models/labChat.js.map +1 -1
  14. package/dist/models/marketplace.d.ts +35 -0
  15. package/dist/models/marketplace.d.ts.map +1 -1
  16. package/dist/models/marketplace.js +9 -2
  17. package/dist/models/marketplace.js.map +1 -1
  18. package/dist/models/message.d.ts +3 -0
  19. package/dist/models/message.d.ts.map +1 -1
  20. package/dist/models/module.d.ts +228 -0
  21. package/dist/models/module.d.ts.map +1 -0
  22. package/dist/models/module.js +69 -0
  23. package/dist/models/module.js.map +1 -0
  24. package/dist/pipelines/aiLabChat.d.ts +2 -0
  25. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  26. package/dist/pipelines/aiLabChat.js +60 -16
  27. package/dist/pipelines/aiLabChat.js.map +1 -1
  28. package/dist/pipelines/aiLabChatContract.d.ts +170 -2
  29. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  30. package/dist/pipelines/aiLabChatContract.js +30 -2
  31. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  32. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  33. package/dist/pipelines/aiNewtonChat.js +30 -5
  34. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  35. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  36. package/dist/pipelines/gradeWorksheet.js +4 -2
  37. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  38. package/dist/pipelines/labChatPrompt.d.ts +1 -1
  39. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  40. package/dist/pipelines/labChatPrompt.js +13 -14
  41. package/dist/pipelines/labChatPrompt.js.map +1 -1
  42. package/dist/routers/_app.d.ts +1185 -215
  43. package/dist/routers/_app.d.ts.map +1 -1
  44. package/dist/routers/_app.js +4 -2
  45. package/dist/routers/_app.js.map +1 -1
  46. package/dist/routers/admin.d.ts +0 -1
  47. package/dist/routers/admin.d.ts.map +1 -1
  48. package/dist/routers/conversation.d.ts +1 -0
  49. package/dist/routers/conversation.d.ts.map +1 -1
  50. package/dist/routers/labChat.d.ts +10 -0
  51. package/dist/routers/labChat.d.ts.map +1 -1
  52. package/dist/routers/labChat.js +6 -3
  53. package/dist/routers/labChat.js.map +1 -1
  54. package/dist/routers/marketplace.d.ts +41 -0
  55. package/dist/routers/marketplace.d.ts.map +1 -1
  56. package/dist/routers/marketplace.js +13 -3
  57. package/dist/routers/marketplace.js.map +1 -1
  58. package/dist/routers/message.d.ts +37 -4
  59. package/dist/routers/message.d.ts.map +1 -1
  60. package/dist/routers/message.js +24 -7
  61. package/dist/routers/message.js.map +1 -1
  62. package/dist/routers/module.d.ts +345 -0
  63. package/dist/routers/module.d.ts.map +1 -0
  64. package/dist/routers/module.js +82 -0
  65. package/dist/routers/module.js.map +1 -0
  66. package/dist/routers/parent.d.ts +18 -0
  67. package/dist/routers/parent.d.ts.map +1 -1
  68. package/dist/routers/parent.js +10 -3
  69. package/dist/routers/parent.js.map +1 -1
  70. package/dist/routers/studentProgress.d.ts +44 -0
  71. package/dist/routers/studentProgress.d.ts.map +1 -1
  72. package/dist/routers/studentProgress.js +12 -3
  73. package/dist/routers/studentProgress.js.map +1 -1
  74. package/dist/routes/aiStream.js +32 -2
  75. package/dist/routes/aiStream.js.map +1 -1
  76. package/dist/seedDatabase.d.ts.map +1 -1
  77. package/dist/seedDatabase.js +107 -3
  78. package/dist/seedDatabase.js.map +1 -1
  79. package/dist/services/adminUser.d.ts +0 -1
  80. package/dist/services/adminUser.d.ts.map +1 -1
  81. package/dist/services/adminUser.js +7 -4
  82. package/dist/services/adminUser.js.map +1 -1
  83. package/dist/services/aiChatStream.d.ts +15 -0
  84. package/dist/services/aiChatStream.d.ts.map +1 -1
  85. package/dist/services/aiChatStream.js +144 -74
  86. package/dist/services/aiChatStream.js.map +1 -1
  87. package/dist/services/aiGeneration.d.ts +4 -0
  88. package/dist/services/aiGeneration.d.ts.map +1 -0
  89. package/dist/services/aiGeneration.js +88 -0
  90. package/dist/services/aiGeneration.js.map +1 -0
  91. package/dist/services/aiProposalTools.d.ts +4 -1
  92. package/dist/services/aiProposalTools.d.ts.map +1 -1
  93. package/dist/services/aiProposalTools.js +128 -3
  94. package/dist/services/aiProposalTools.js.map +1 -1
  95. package/dist/services/aiProposals.d.ts +70 -0
  96. package/dist/services/aiProposals.d.ts.map +1 -0
  97. package/dist/services/aiProposals.js +363 -0
  98. package/dist/services/aiProposals.js.map +1 -0
  99. package/dist/services/assignment.d.ts.map +1 -1
  100. package/dist/services/assignment.js +8 -3
  101. package/dist/services/assignment.js.map +1 -1
  102. package/dist/services/conversation.d.ts +1 -0
  103. package/dist/services/conversation.d.ts.map +1 -1
  104. package/dist/services/labChat.d.ts +10 -0
  105. package/dist/services/labChat.d.ts.map +1 -1
  106. package/dist/services/labChat.js +52 -3
  107. package/dist/services/labChat.js.map +1 -1
  108. package/dist/services/marketplace.d.ts +36 -0
  109. package/dist/services/marketplace.d.ts.map +1 -1
  110. package/dist/services/marketplace.js +30 -3
  111. package/dist/services/marketplace.js.map +1 -1
  112. package/dist/services/message.d.ts +24 -8
  113. package/dist/services/message.d.ts.map +1 -1
  114. package/dist/services/message.js +170 -117
  115. package/dist/services/message.js.map +1 -1
  116. package/dist/services/module.d.ts +288 -0
  117. package/dist/services/module.d.ts.map +1 -0
  118. package/dist/services/module.js +518 -0
  119. package/dist/services/module.js.map +1 -0
  120. package/dist/services/parentPortal.d.ts +14 -0
  121. package/dist/services/parentPortal.d.ts.map +1 -1
  122. package/dist/services/parentPortal.js +72 -2
  123. package/dist/services/parentPortal.js.map +1 -1
  124. package/dist/services/school.d.ts.map +1 -1
  125. package/dist/services/school.js +17 -5
  126. package/dist/services/school.js.map +1 -1
  127. package/dist/services/studentProgress.d.ts +50 -0
  128. package/dist/services/studentProgress.d.ts.map +1 -1
  129. package/dist/services/studentProgress.js +107 -2
  130. package/dist/services/studentProgress.js.map +1 -1
  131. package/dist/utils/inference.d.ts +10 -0
  132. package/dist/utils/inference.d.ts.map +1 -1
  133. package/dist/utils/inference.js +77 -2
  134. package/dist/utils/inference.js.map +1 -1
  135. package/dist/workers/processors/createRunJobWorker.js +17 -16
  136. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  137. package/package.json +1 -1
  138. package/prisma/migrations/20260708013000_add_modules/migration.sql +100 -0
  139. package/prisma/migrations/20260811130000_add_ai_message_feedback/migration.sql +7 -0
  140. package/prisma/schema.prisma +101 -0
  141. package/src/models/cloudStorage.ts +19 -0
  142. package/src/models/labChat.ts +34 -0
  143. package/src/models/marketplace.ts +16 -0
  144. package/src/models/module.ts +77 -0
  145. package/src/pipelines/aiLabChat.ts +79 -18
  146. package/src/pipelines/aiLabChatContract.ts +35 -0
  147. package/src/pipelines/aiNewtonChat.ts +34 -2
  148. package/src/pipelines/gradeWorksheet.ts +6 -0
  149. package/src/pipelines/labChatPrompt.ts +11 -12
  150. package/src/routers/_app.ts +2 -0
  151. package/src/routers/labChat.ts +5 -0
  152. package/src/routers/marketplace.ts +16 -0
  153. package/src/routers/message.ts +34 -5
  154. package/src/routers/module.ts +118 -0
  155. package/src/routers/parent.ts +11 -0
  156. package/src/routers/studentProgress.ts +15 -0
  157. package/src/routes/aiStream.ts +32 -0
  158. package/src/seedDatabase.ts +107 -2
  159. package/src/services/adminUser.ts +5 -2
  160. package/src/services/aiChatStream.ts +83 -7
  161. package/src/services/aiGeneration.ts +92 -0
  162. package/src/services/aiProposalTools.ts +166 -1
  163. package/src/services/aiProposals.ts +428 -0
  164. package/src/services/assignment.ts +12 -1
  165. package/src/services/labChat.ts +65 -0
  166. package/src/services/marketplace.ts +39 -0
  167. package/src/services/message.ts +205 -142
  168. package/src/services/module.ts +683 -0
  169. package/src/services/parentPortal.ts +85 -0
  170. package/src/services/school.ts +15 -3
  171. package/src/services/studentProgress.ts +135 -0
  172. package/src/utils/inference.ts +85 -0
  173. package/src/workers/processors/createRunJobWorker.ts +17 -17
  174. package/tests/unit/services/adminUser.test.ts +75 -0
  175. package/tests/unit/utils/adminPanelAccess.test.ts +26 -0
@@ -0,0 +1,228 @@
1
+ /** @returns Class if user is a teacher or school admin. */
2
+ export declare function findClassWithTeacher(classId: string, teacherId: string): import("@prisma/client/runtime/library.js").DynamicModelExtensionFluentApi<import(".prisma/client").Prisma.TypeMap<import("@prisma/client/runtime/library.js").InternalArgs & {
3
+ result: {
4
+ userProfile: {
5
+ profilePicture: () => {
6
+ needs: {
7
+ profilePicture: true;
8
+ };
9
+ compute(profile: {
10
+ profilePicture: string | null;
11
+ }): string | null;
12
+ };
13
+ profilePictureThumbnail: () => {
14
+ needs: {
15
+ profilePictureThumbnail: true;
16
+ };
17
+ compute(profile: {
18
+ profilePictureThumbnail: string | null;
19
+ }): string | null;
20
+ };
21
+ };
22
+ };
23
+ model: {};
24
+ query: {};
25
+ client: {};
26
+ }, {}>, "Class", "findFirst", null> & import("@prisma/client/runtime/library.js").PrismaPromise<{
27
+ symbol: string | null;
28
+ section: string;
29
+ id: string;
30
+ name: string;
31
+ schoolId: string | null;
32
+ color: string | null;
33
+ subject: string;
34
+ syllabus: string | null;
35
+ departmentId: string | null;
36
+ discussionsEnabled: boolean;
37
+ studentsCanCreateDiscussions: boolean;
38
+ studentsCanReply: boolean;
39
+ sidebarFeatures: import("@prisma/client/runtime/library.js").JsonValue | null;
40
+ treatMissingAsZero: boolean;
41
+ gradeRoundingMode: import(".prisma/client").$Enums.GradeRoundingMode;
42
+ } | null>;
43
+ export declare const moduleInclude: {
44
+ readonly items: {
45
+ readonly orderBy: {
46
+ readonly position: "asc";
47
+ };
48
+ };
49
+ readonly prerequisites: {
50
+ readonly select: {
51
+ readonly prerequisiteId: true;
52
+ };
53
+ };
54
+ };
55
+ /** @returns All modules of a class ordered by position. */
56
+ export declare function findModulesByClassId(classId: string, publishedOnly?: boolean): import("@prisma/client/runtime/library.js").PrismaPromise<({
57
+ items: {
58
+ id: string;
59
+ title: string | null;
60
+ position: number;
61
+ moduleId: string;
62
+ indent: number;
63
+ itemType: import(".prisma/client").$Enums.CourseModuleItemType;
64
+ itemId: string | null;
65
+ externalUrl: string | null;
66
+ requirementType: import(".prisma/client").$Enums.ModuleItemRequirementType | null;
67
+ requiredScore: number | null;
68
+ }[];
69
+ prerequisites: {
70
+ prerequisiteId: string;
71
+ }[];
72
+ } & {
73
+ id: string;
74
+ createdAt: Date;
75
+ updatedAt: Date;
76
+ classId: string;
77
+ title: string;
78
+ position: number;
79
+ published: boolean;
80
+ unlockAt: Date | null;
81
+ requireSequential: boolean;
82
+ })[]>;
83
+ /** @returns Module by ID scoped to a class. */
84
+ export declare function findModuleByClassAndId(id: string, classId: string): import("@prisma/client/runtime/library.js").DynamicModelExtensionFluentApi<import(".prisma/client").Prisma.TypeMap<import("@prisma/client/runtime/library.js").InternalArgs & {
85
+ result: {
86
+ userProfile: {
87
+ profilePicture: () => {
88
+ needs: {
89
+ profilePicture: true;
90
+ };
91
+ compute(profile: {
92
+ profilePicture: string | null;
93
+ }): string | null;
94
+ };
95
+ profilePictureThumbnail: () => {
96
+ needs: {
97
+ profilePictureThumbnail: true;
98
+ };
99
+ compute(profile: {
100
+ profilePictureThumbnail: string | null;
101
+ }): string | null;
102
+ };
103
+ };
104
+ };
105
+ model: {};
106
+ query: {};
107
+ client: {};
108
+ }, {}>, "Module", "findFirst", null> & import("@prisma/client/runtime/library.js").PrismaPromise<({
109
+ items: {
110
+ id: string;
111
+ title: string | null;
112
+ position: number;
113
+ moduleId: string;
114
+ indent: number;
115
+ itemType: import(".prisma/client").$Enums.CourseModuleItemType;
116
+ itemId: string | null;
117
+ externalUrl: string | null;
118
+ requirementType: import(".prisma/client").$Enums.ModuleItemRequirementType | null;
119
+ requiredScore: number | null;
120
+ }[];
121
+ prerequisites: {
122
+ prerequisiteId: string;
123
+ }[];
124
+ } & {
125
+ id: string;
126
+ createdAt: Date;
127
+ updatedAt: Date;
128
+ classId: string;
129
+ title: string;
130
+ position: number;
131
+ published: boolean;
132
+ unlockAt: Date | null;
133
+ requireSequential: boolean;
134
+ }) | null>;
135
+ /** @returns Module item with its module, scoped to a class. */
136
+ export declare function findModuleItemInClass(itemId: string, classId: string): import("@prisma/client/runtime/library.js").DynamicModelExtensionFluentApi<import(".prisma/client").Prisma.TypeMap<import("@prisma/client/runtime/library.js").InternalArgs & {
137
+ result: {
138
+ userProfile: {
139
+ profilePicture: () => {
140
+ needs: {
141
+ profilePicture: true;
142
+ };
143
+ compute(profile: {
144
+ profilePicture: string | null;
145
+ }): string | null;
146
+ };
147
+ profilePictureThumbnail: () => {
148
+ needs: {
149
+ profilePictureThumbnail: true;
150
+ };
151
+ compute(profile: {
152
+ profilePictureThumbnail: string | null;
153
+ }): string | null;
154
+ };
155
+ };
156
+ };
157
+ model: {};
158
+ query: {};
159
+ client: {};
160
+ }, {}>, "CourseModuleItem", "findFirst", null> & import("@prisma/client/runtime/library.js").PrismaPromise<({
161
+ module: {
162
+ id: string;
163
+ createdAt: Date;
164
+ updatedAt: Date;
165
+ classId: string;
166
+ title: string;
167
+ position: number;
168
+ published: boolean;
169
+ unlockAt: Date | null;
170
+ requireSequential: boolean;
171
+ };
172
+ } & {
173
+ id: string;
174
+ title: string | null;
175
+ position: number;
176
+ moduleId: string;
177
+ indent: number;
178
+ itemType: import(".prisma/client").$Enums.CourseModuleItemType;
179
+ itemId: string | null;
180
+ externalUrl: string | null;
181
+ requirementType: import(".prisma/client").$Enums.ModuleItemRequirementType | null;
182
+ requiredScore: number | null;
183
+ }) | null>;
184
+ /** @returns Progress rows of a student for all items of a class. */
185
+ export declare function findStudentProgressForClass(classId: string, studentId: string): import("@prisma/client/runtime/library.js").PrismaPromise<{
186
+ status: import(".prisma/client").$Enums.ModuleItemProgressStatus;
187
+ id: string;
188
+ createdAt: Date;
189
+ updatedAt: Date;
190
+ moduleItemId: string;
191
+ studentId: string;
192
+ completedAt: Date | null;
193
+ }[]>;
194
+ /** Upsert a progress row for a student and item. */
195
+ export declare function upsertItemProgress(moduleItemId: string, studentId: string, status: "INCOMPLETE" | "COMPLETE"): import("@prisma/client/runtime/library.js").DynamicModelExtensionFluentApi<import(".prisma/client").Prisma.TypeMap<import("@prisma/client/runtime/library.js").InternalArgs & {
196
+ result: {
197
+ userProfile: {
198
+ profilePicture: () => {
199
+ needs: {
200
+ profilePicture: true;
201
+ };
202
+ compute(profile: {
203
+ profilePicture: string | null;
204
+ }): string | null;
205
+ };
206
+ profilePictureThumbnail: () => {
207
+ needs: {
208
+ profilePictureThumbnail: true;
209
+ };
210
+ compute(profile: {
211
+ profilePictureThumbnail: string | null;
212
+ }): string | null;
213
+ };
214
+ };
215
+ };
216
+ model: {};
217
+ query: {};
218
+ client: {};
219
+ }, {}>, "ModuleItemProgress", "upsert", never> & import("@prisma/client/runtime/library.js").PrismaPromise<{
220
+ status: import(".prisma/client").$Enums.ModuleItemProgressStatus;
221
+ id: string;
222
+ createdAt: Date;
223
+ updatedAt: Date;
224
+ moduleItemId: string;
225
+ studentId: string;
226
+ completedAt: Date | null;
227
+ }>;
228
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"/","sources":["models/module.ts"],"names":[],"mappings":"AAMA,2DAA2D;AAC3D,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAOtE;AAED,eAAO,MAAM,aAAa;;;;;;;;;;;CAOhB,CAAC;AAEX,2DAA2D;AAC3D,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,UAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;MAM1E;AAED,+CAA+C;AAC/C,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAKjE;AAED,+DAA+D;AAC/D,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAKpE;AAED,oEAAoE;AACpE,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;KAI7E;AAED,oDAAoD;AACpD,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,GAAG,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAelC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Module model – Canvas-style class modules, items and student progress.
3
+ */
4
+
5
+ !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]="3e54b96a-f15c-5e1c-aac9-de042e3ac71c")}catch(e){}}();
6
+ import { prisma } from "../lib/prisma.js";
7
+ import { classTeacherOrSchoolAdminWhere } from "./access.js";
8
+ /** @returns Class if user is a teacher or school admin. */
9
+ export function findClassWithTeacher(classId, teacherId) {
10
+ return prisma.class.findFirst({
11
+ where: {
12
+ id: classId,
13
+ ...classTeacherOrSchoolAdminWhere(teacherId),
14
+ },
15
+ });
16
+ }
17
+ export const moduleInclude = {
18
+ items: {
19
+ orderBy: { position: "asc" },
20
+ },
21
+ prerequisites: {
22
+ select: { prerequisiteId: true },
23
+ },
24
+ };
25
+ /** @returns All modules of a class ordered by position. */
26
+ export function findModulesByClassId(classId, publishedOnly = false) {
27
+ return prisma.module.findMany({
28
+ where: { classId, ...(publishedOnly && { published: true }) },
29
+ orderBy: { position: "asc" },
30
+ include: moduleInclude,
31
+ });
32
+ }
33
+ /** @returns Module by ID scoped to a class. */
34
+ export function findModuleByClassAndId(id, classId) {
35
+ return prisma.module.findFirst({
36
+ where: { id, classId },
37
+ include: moduleInclude,
38
+ });
39
+ }
40
+ /** @returns Module item with its module, scoped to a class. */
41
+ export function findModuleItemInClass(itemId, classId) {
42
+ return prisma.courseModuleItem.findFirst({
43
+ where: { id: itemId, module: { classId } },
44
+ include: { module: true },
45
+ });
46
+ }
47
+ /** @returns Progress rows of a student for all items of a class. */
48
+ export function findStudentProgressForClass(classId, studentId) {
49
+ return prisma.moduleItemProgress.findMany({
50
+ where: { studentId, moduleItem: { module: { classId } } },
51
+ });
52
+ }
53
+ /** Upsert a progress row for a student and item. */
54
+ export function upsertItemProgress(moduleItemId, studentId, status) {
55
+ return prisma.moduleItemProgress.upsert({
56
+ where: { moduleItemId_studentId: { moduleItemId, studentId } },
57
+ create: {
58
+ moduleItemId,
59
+ studentId,
60
+ status,
61
+ ...(status === "COMPLETE" && { completedAt: new Date() }),
62
+ },
63
+ update: status === "COMPLETE"
64
+ ? { status, completedAt: new Date() }
65
+ : {},
66
+ });
67
+ }
68
+ //# sourceMappingURL=module.js.map
69
+ //# debugId=3e54b96a-f15c-5e1c-aac9-de042e3ac71c
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.js","sources":["models/module.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Module model – Canvas-style class modules, items and student progress.\n */\nimport { prisma } from \"../lib/prisma.js\";\nimport { classTeacherOrSchoolAdminWhere } from \"./access.js\";\n\n/** @returns Class if user is a teacher or school admin. */\nexport function findClassWithTeacher(classId: string, teacherId: string) {\n return prisma.class.findFirst({\n where: {\n id: classId,\n ...classTeacherOrSchoolAdminWhere(teacherId),\n },\n });\n}\n\nexport const moduleInclude = {\n items: {\n orderBy: { position: \"asc\" as const },\n },\n prerequisites: {\n select: { prerequisiteId: true },\n },\n} as const;\n\n/** @returns All modules of a class ordered by position. */\nexport function findModulesByClassId(classId: string, publishedOnly = false) {\n return prisma.module.findMany({\n where: { classId, ...(publishedOnly && { published: true }) },\n orderBy: { position: \"asc\" },\n include: moduleInclude,\n });\n}\n\n/** @returns Module by ID scoped to a class. */\nexport function findModuleByClassAndId(id: string, classId: string) {\n return prisma.module.findFirst({\n where: { id, classId },\n include: moduleInclude,\n });\n}\n\n/** @returns Module item with its module, scoped to a class. */\nexport function findModuleItemInClass(itemId: string, classId: string) {\n return prisma.courseModuleItem.findFirst({\n where: { id: itemId, module: { classId } },\n include: { module: true },\n });\n}\n\n/** @returns Progress rows of a student for all items of a class. */\nexport function findStudentProgressForClass(classId: string, studentId: string) {\n return prisma.moduleItemProgress.findMany({\n where: { studentId, moduleItem: { module: { classId } } },\n });\n}\n\n/** Upsert a progress row for a student and item. */\nexport function upsertItemProgress(\n moduleItemId: string,\n studentId: string,\n status: \"INCOMPLETE\" | \"COMPLETE\"\n) {\n return prisma.moduleItemProgress.upsert({\n where: { moduleItemId_studentId: { moduleItemId, studentId } },\n create: {\n moduleItemId,\n studentId,\n status,\n ...(status === \"COMPLETE\" && { completedAt: new Date() }),\n },\n update:\n status === \"COMPLETE\"\n ? { status, completedAt: new Date() }\n : {},\n });\n}\n"],"names":[],"mappings":"AAAA;;GAEG;;;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAE7D,2DAA2D;AAC3D,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,SAAiB;IACrE,OAAO,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;QAC5B,KAAK,EAAE;YACL,EAAE,EAAE,OAAO;YACX,GAAG,8BAA8B,CAAC,SAAS,CAAC;SAC7C;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,KAAK,EAAE;QACL,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAc,EAAE;KACtC;IACD,aAAa,EAAE;QACb,MAAM,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE;KACjC;CACO,CAAC;AAEX,2DAA2D;AAC3D,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,aAAa,GAAG,KAAK;IACzE,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;QAC5B,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,aAAa,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE;QAC7D,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC5B,OAAO,EAAE,aAAa;KACvB,CAAC,CAAC;AACL,CAAC;AAED,+CAA+C;AAC/C,MAAM,UAAU,sBAAsB,CAAC,EAAU,EAAE,OAAe;IAChE,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;QAC7B,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;QACtB,OAAO,EAAE,aAAa;KACvB,CAAC,CAAC;AACL,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,qBAAqB,CAAC,MAAc,EAAE,OAAe;IACnE,OAAO,MAAM,CAAC,gBAAgB,CAAC,SAAS,CAAC;QACvC,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,EAAE;QAC1C,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;KAC1B,CAAC,CAAC;AACL,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,2BAA2B,CAAC,OAAe,EAAE,SAAiB;IAC5E,OAAO,MAAM,CAAC,kBAAkB,CAAC,QAAQ,CAAC;QACxC,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;KAC1D,CAAC,CAAC;AACL,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,kBAAkB,CAChC,YAAoB,EACpB,SAAiB,EACjB,MAAiC;IAEjC,OAAO,MAAM,CAAC,kBAAkB,CAAC,MAAM,CAAC;QACtC,KAAK,EAAE,EAAE,sBAAsB,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE;QAC9D,MAAM,EAAE;YACN,YAAY;YACZ,SAAS;YACT,MAAM;YACN,GAAG,CAAC,MAAM,KAAK,UAAU,IAAI,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;SAC1D;QACD,MAAM,EACJ,MAAM,KAAK,UAAU;YACnB,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,EAAE;YACrC,CAAC,CAAC,EAAE;KACT,CAAC,CAAC;AACL,CAAC","debug_id":"3e54b96a-f15c-5e1c-aac9-de042e3ac71c"}
@@ -1,4 +1,6 @@
1
1
  import type { Assignment, Class, File, Section, User } from "@prisma/client";
2
+ /** Class-files folder that collects AI-generated documents (created lazily). */
3
+ export declare const AI_GENERATED_FOLDER_NAME = "AI Generated";
2
4
  /** Extended class data for AI context (schema-aware) */
3
5
  type ClassContextData = {
4
6
  class: Class;
@@ -1 +1 @@
1
- {"version":3,"file":"aiLabChat.d.ts","sourceRoot":"/","sources":["pipelines/aiLabChat.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aiLabChat.d.ts","sourceRoot":"/","sources":["pipelines/aiLabChat.ts"],"names":[],"mappings":"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"}
@@ -3,11 +3,11 @@
3
3
  * Can create worksheets, sections, assignments, and generated files from AI output.
4
4
  */
5
5
 
6
- !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]="3084d592-a0a1-5867-bf8b-f4f57ced0b40")}catch(e){}}();
6
+ !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]="a0910d42-509a-534e-ad83-e5ccb2a920c0")}catch(e){}}();
7
7
  import { isAIUser } from "../utils/aiUser.js";
8
8
  import { prisma } from "../lib/prisma.js";
9
9
  import { pusher, chatChannel, teacherChannel } from "../lib/pusher.js";
10
- import { completePendingAIMessage, failPendingAIMessage, } from "../utils/inference.js";
10
+ import { cancelPendingAIMessage, completePendingAIMessage, failPendingAIMessage, } from "../utils/inference.js";
11
11
  import { logger } from "../utils/logger.js";
12
12
  import { v4 } from "uuid";
13
13
  import { bucket } from "../lib/googleCloudStorage.js";
@@ -18,9 +18,13 @@ import { AI_CHAT_MODEL } from "../lib/chatAttachmentPrompt.js";
18
18
  import { buildLabChatAgentMessages } from "./labChatPrompt.js";
19
19
  import { applyResolvedFormatsToStreamParts, enrichStreamPartsWithGeneratedFiles, finalizeDeferredToolParts, streamChatResponse, } from "../services/aiChatStream.js";
20
20
  import { aiTools } from "../services/aiTools.js";
21
+ import { findOrCreateChildFolderByName, findOrCreateScopeRoot, } from "../models/cloudStorage.js";
21
22
  import { labProposalTools, createLabProposals } from "../services/aiProposalTools.js";
23
+ import { buildPendingCreations } from "../services/aiProposals.js";
22
24
  /** Cap model wait time; leave headroom after inference for PDF/GCS/DB within Vercel maxDuration (500s). */
23
25
  const LAB_CHAT_RESPONSE_TIMEOUT_MS = 400000;
26
+ /** Class-files folder that collects AI-generated documents (created lazily). */
27
+ export const AI_GENERATED_FOLDER_NAME = "AI Generated";
24
28
  const withTimeout = async (task, timeoutMs, operationName) => {
25
29
  let timeoutHandle;
26
30
  const timeoutPromise = new Promise((_, reject) => {
@@ -197,7 +201,7 @@ CLASS: ${cls.name} | Subject: ${cls.subject} | Section: ${cls.section}
197
201
  Syllabus: ${cls.syllabus ? cls.syllabus.slice(0, 200) + (cls.syllabus.length > 200 ? "…" : "") : "(none)"}
198
202
 
199
203
  SECTIONS (use sectionId when creating assignments):
200
- ${sectionList || " (none - suggest sectionsToCreate first)"}
204
+ ${sectionList || " (none - propose a section first)"}
201
205
 
202
206
  MARK SCHEMES (use markSchemeId when creating assignments):
203
207
  ${markSchemeList || " (none - suggest creating one or omit markSchemeId)"}
@@ -206,7 +210,7 @@ GRADING BOUNDARIES (use gradingBoundaryId when creating assignments):
206
210
  ${gradingBoundaryList || " (none - suggest creating one or omit gradingBoundaryId)"}
207
211
 
208
212
  WORKSHEETS (use worksheetIds when acceptWorksheet is true):
209
- ${worksheetList || " (none - use worksheetsToCreate or create via docs first)"}
213
+ ${worksheetList || " (none - propose a worksheet or create via docs first)"}
210
214
 
211
215
  FILES - PDFs/Documents (for assignment attachments):
212
216
  ${fileList || " (none)"}
@@ -373,7 +377,7 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
373
377
  ...aiTools(fullLabChat.classId),
374
378
  ...labProposalTools(proposals),
375
379
  };
376
- const { text: streamedText, parts: rawStreamParts } = await withTimeout(streamChatResponse({
380
+ const { text: streamedText, parts: rawStreamParts, cancelled, } = await withTimeout(streamChatResponse({
377
381
  conversationId,
378
382
  aiMessageId: emitOptions.aiMessageId,
379
383
  model: AI_CHAT_MODEL,
@@ -392,6 +396,28 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
392
396
  // Real text + tool-call events were already broadcast inline (in order)
393
397
  // during the stream by `streamChatResponse`. Just close the stream.
394
398
  await broadcastStreamComplete(conversationId, emitOptions.aiMessageId);
399
+ if (cancelled) {
400
+ // User stopped the generation: persist whatever streamed so far and
401
+ // skip document generation / proposal creation entirely.
402
+ const partialParts = finalizeDeferredToolParts(rawStreamParts);
403
+ await cancelPendingAIMessage(emitOptions.aiMessageId, streamedText, {
404
+ meta: {
405
+ streamParts: partialParts.length > 0 ? partialParts : null,
406
+ },
407
+ });
408
+ await pusher
409
+ .trigger(teacherChannel(emitOptions.classId), "lab-response-completed", {
410
+ labChatId,
411
+ messageId: emitOptions.aiMessageId,
412
+ })
413
+ .catch(() => { });
414
+ logger.info('AI response cancelled by user', {
415
+ labChatId,
416
+ conversationId,
417
+ aiMessageId: emitOptions.aiMessageId,
418
+ });
419
+ return;
420
+ }
395
421
  // `generate_document` is an inline tool that only QUEUES into `proposals.docs`
396
422
  // during the stream. OpenAI Code Interpreter renders files here, after the
397
423
  // stream ends, then we link them to this message.
@@ -399,6 +425,26 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
399
425
  const attachmentIds = [];
400
426
  const generatedDocFiles = [];
401
427
  const resolvedDocs = [];
428
+ // Generated files are filed into the class cloud storage so teachers
429
+ // can find them in the class Files UI, not just as chat attachments.
430
+ let aiFolderId;
431
+ if (proposals.docs.length > 0) {
432
+ try {
433
+ const root = await findOrCreateScopeRoot({
434
+ kind: "CLASS",
435
+ classId: fullLabChat.classId,
436
+ });
437
+ const aiFolder = await findOrCreateChildFolderByName(root.id, AI_GENERATED_FOLDER_NAME);
438
+ aiFolderId = aiFolder.id;
439
+ }
440
+ catch (folderError) {
441
+ logger.error("Failed to resolve AI generated class folder", {
442
+ error: folderError,
443
+ labChatId,
444
+ classId: fullLabChat.classId,
445
+ });
446
+ }
447
+ }
402
448
  for (let i = 0; i < proposals.docs.length; i++) {
403
449
  const doc = proposals.docs[i];
404
450
  if (!doc.title || !doc.content?.trim()) {
@@ -455,6 +501,7 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
455
501
  messageId: emitOptions.aiMessageId,
456
502
  uploadStatus: 'COMPLETED',
457
503
  uploadedAt: new Date(),
504
+ ...(aiFolderId && { folderId: aiFolderId }),
458
505
  },
459
506
  });
460
507
  attachmentIds.push(file.id);
@@ -463,6 +510,7 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
463
510
  fileId: file.id,
464
511
  fileName: filename,
465
512
  format: generated.format,
513
+ ...(aiFolderId && { folderId: aiFolderId }),
466
514
  });
467
515
  }
468
516
  catch (docError) {
@@ -478,7 +526,10 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
478
526
  });
479
527
  }
480
528
  }
481
- const streamParts = finalizeDeferredToolParts(enrichStreamPartsWithGeneratedFiles(applyResolvedFormatsToStreamParts(rawStreamParts, resolvedDocs), generatedDocFiles));
529
+ // Defer-and-confirm: propose_* tool calls become pending creations the
530
+ // teacher approves or dismisses; nothing is created at generation time.
531
+ const { pendingCreations, parts: streamPartsWithProposals } = buildPendingCreations(proposals, finalizeDeferredToolParts(enrichStreamPartsWithGeneratedFiles(applyResolvedFormatsToStreamParts(rawStreamParts, resolvedDocs), generatedDocFiles)));
532
+ const streamParts = streamPartsWithProposals;
482
533
  // While the message is still PENDING, resolve deferred tool indicators
483
534
  // (spinner → attachment or error) before the final persist event.
484
535
  for (const part of streamParts) {
@@ -495,6 +546,7 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
495
546
  ...(part.title ? { title: part.title } : {}),
496
547
  ...(part.format ? { format: part.format } : {}),
497
548
  ...(part.fileId ? { fileId: part.fileId, fileName: part.fileName } : {}),
549
+ ...(part.folderId ? { folderId: part.folderId } : {}),
498
550
  status: part.status,
499
551
  })
500
552
  .catch(() => { });
@@ -504,15 +556,7 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
504
556
  attachments: { connect: attachmentIds.map((id) => ({ id })) },
505
557
  }),
506
558
  meta: {
507
- assignmentsToCreate: proposals.assignments.length > 0
508
- ? proposals.assignments.map(assignment => ({ ...assignment, id: v4() }))
509
- : null,
510
- worksheetsToCreate: proposals.worksheets.length > 0
511
- ? proposals.worksheets.map(worksheet => ({ ...worksheet, id: v4() }))
512
- : null,
513
- sectionsToCreate: proposals.sections.length > 0
514
- ? proposals.sections.map(section => ({ ...section, id: v4() }))
515
- : null,
559
+ pendingCreations: pendingCreations.length > 0 ? pendingCreations : null,
516
560
  streamParts: streamParts.length > 0 ? streamParts : null,
517
561
  },
518
562
  });
@@ -562,4 +606,4 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
562
606
  }
563
607
  };
564
608
  //# sourceMappingURL=aiLabChat.js.map
565
- //# debugId=3084d592-a0a1-5867-bf8b-f4f57ced0b40
609
+ //# debugId=a0910d42-509a-534e-ad83-e5ccb2a920c0