@studious-lms/server 1.3.0 → 1.4.1
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/models/class.d.ts +24 -2
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +180 -81
- package/dist/models/class.js.map +1 -1
- package/dist/models/worksheet.d.ts +34 -34
- package/dist/pipelines/aiLabChat.d.ts +61 -2
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +204 -172
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +413 -0
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -0
- package/dist/pipelines/aiLabChatContract.js +74 -0
- package/dist/pipelines/aiLabChatContract.js.map +1 -0
- package/dist/pipelines/gradeWorksheet.d.ts +4 -4
- package/dist/pipelines/labChatPrompt.d.ts +2 -0
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -0
- package/dist/pipelines/labChatPrompt.js +72 -0
- package/dist/pipelines/labChatPrompt.js.map +1 -0
- package/dist/routers/_app.d.ts +284 -56
- 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/class.d.ts +24 -3
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +3 -3
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/labChat.d.ts +10 -1
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +6 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/message.d.ts +11 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +10 -3
- package/dist/routers/message.js.map +1 -1
- package/dist/routers/studentProgress.d.ts +75 -0
- package/dist/routers/studentProgress.d.ts.map +1 -0
- package/dist/routers/studentProgress.js +33 -0
- package/dist/routers/studentProgress.js.map +1 -0
- package/dist/routers/worksheet.d.ts +24 -24
- package/dist/services/class.d.ts +24 -2
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +18 -6
- package/dist/services/class.js.map +1 -1
- package/dist/services/labChat.d.ts +5 -1
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +112 -4
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +8 -0
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +116 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/studentProgress.d.ts +45 -0
- package/dist/services/studentProgress.d.ts.map +1 -0
- package/dist/services/studentProgress.js +291 -0
- package/dist/services/studentProgress.js.map +1 -0
- package/dist/services/worksheet.d.ts +18 -18
- package/package.json +2 -2
- package/prisma/schema.prisma +1 -1
- package/sentry.properties +3 -0
- package/src/models/class.ts +189 -84
- package/src/pipelines/aiLabChat.ts +246 -184
- package/src/pipelines/aiLabChatContract.ts +75 -0
- package/src/pipelines/labChatPrompt.ts +68 -0
- package/src/routers/_app.ts +4 -2
- package/src/routers/class.ts +1 -1
- package/src/routers/labChat.ts +7 -0
- package/src/routers/message.ts +13 -0
- package/src/routers/studentProgress.ts +47 -0
- package/src/services/class.ts +14 -7
- package/src/services/labChat.ts +120 -5
- package/src/services/message.ts +142 -0
- package/src/services/studentProgress.ts +390 -0
- package/tests/lib/aiLabChatContract.test.ts +32 -0
- package/tests/pipelines/aiLabChat.test.ts +95 -0
- package/tests/routers/studentProgress.test.ts +283 -0
- package/tests/utils/aiLabChatPrompt.test.ts +18 -0
- package/vitest.unit.config.ts +7 -1
package/src/models/class.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Assignment, Class, Folder, GradingBoundary, MarkScheme, Section, Worksheet } from "@prisma/client";
|
|
8
|
-
import type { PrismaClient } from "@prisma/client";
|
|
8
|
+
import type { PrismaClient, WorksheetQuestion } from "@prisma/client";
|
|
9
9
|
|
|
10
10
|
import { prisma } from "../lib/prisma.js";
|
|
11
11
|
import { v4 as uuidv4 } from 'uuid';
|
|
@@ -417,35 +417,53 @@ async function recursivelyIncludeFiles(folderId: string) {
|
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
/** Copy all files in folder tree to GCS (outside transaction). Returns map of sourcePath -> newPath. */
|
|
421
|
+
async function recursivelyCopyFilesToGcs(folderId: string): Promise<Map<string, string>> {
|
|
422
|
+
const pathMap = new Map<string, string>();
|
|
423
|
+
if (!folderId.length) return pathMap;
|
|
424
|
+
|
|
425
|
+
const folder = await prisma.folder.findUnique({
|
|
426
|
+
where: { id: folderId },
|
|
427
|
+
include: {
|
|
428
|
+
files: { select: { path: true, name: true } },
|
|
429
|
+
childFolders: { select: { id: true } },
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
if (!folder) return pathMap;
|
|
433
|
+
|
|
434
|
+
for (const file of folder.files) {
|
|
435
|
+
const ext = file.name.split('.').pop() || '';
|
|
436
|
+
const newPath = `imported/${uuidv4()}${ext ? `.${ext}` : ''}`;
|
|
437
|
+
await copyFile(file.path, newPath);
|
|
438
|
+
pathMap.set(file.path, newPath);
|
|
439
|
+
}
|
|
440
|
+
for (const child of folder.childFolders) {
|
|
441
|
+
const childMap = await recursivelyCopyFilesToGcs(child.id);
|
|
442
|
+
childMap.forEach((v, k) => pathMap.set(k, v));
|
|
443
|
+
}
|
|
444
|
+
return pathMap;
|
|
445
|
+
}
|
|
446
|
+
|
|
420
447
|
async function recursivelyCreateFiles(
|
|
421
448
|
tx: PrismaClient | Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">,
|
|
422
449
|
folderId: string,
|
|
423
|
-
targetFolderId
|
|
450
|
+
targetFolderId: string | undefined,
|
|
451
|
+
pathMap: Map<string, string>
|
|
424
452
|
) {
|
|
425
|
-
if (!folderId.length)
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
453
|
+
if (!folderId.length) return;
|
|
428
454
|
|
|
429
455
|
const parentFolder = await tx.folder.findUnique({
|
|
430
456
|
where: { id: folderId },
|
|
431
457
|
include: {
|
|
432
|
-
files: {
|
|
433
|
-
|
|
434
|
-
},
|
|
435
|
-
childFolders: {
|
|
436
|
-
select: { id: true, name: true, color: true, parentFolderId: true },
|
|
437
|
-
},
|
|
458
|
+
files: { select: { id: true, name: true, type: true, size: true, path: true } },
|
|
459
|
+
childFolders: { select: { id: true, name: true, color: true, parentFolderId: true } },
|
|
438
460
|
},
|
|
439
461
|
});
|
|
440
|
-
if (!parentFolder)
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
462
|
+
if (!parentFolder) return;
|
|
443
463
|
|
|
444
|
-
const createdFiles: { count: number } = { count: 0 };
|
|
445
464
|
for (const file of parentFolder.files) {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
await copyFile(file.path, newPath);
|
|
465
|
+
const newPath = pathMap.get(file.path);
|
|
466
|
+
if (!newPath) continue;
|
|
449
467
|
await tx.file.create({
|
|
450
468
|
data: {
|
|
451
469
|
name: file.name,
|
|
@@ -456,10 +474,8 @@ async function recursivelyCreateFiles(
|
|
|
456
474
|
...(targetFolderId && { folder: { connect: { id: targetFolderId } } }),
|
|
457
475
|
},
|
|
458
476
|
});
|
|
459
|
-
createdFiles.count += 1;
|
|
460
477
|
}
|
|
461
478
|
|
|
462
|
-
const childFolders: { files: { count: number }; childFolders: unknown[] }[] = [];
|
|
463
479
|
for (const childFolder of parentFolder.childFolders) {
|
|
464
480
|
const newFolder = await tx.folder.create({
|
|
465
481
|
data: {
|
|
@@ -468,14 +484,8 @@ async function recursivelyCreateFiles(
|
|
|
468
484
|
parentFolderId: targetFolderId ?? parentFolder.id,
|
|
469
485
|
},
|
|
470
486
|
});
|
|
471
|
-
|
|
472
|
-
if (result) childFolders.push(result);
|
|
487
|
+
await recursivelyCreateFiles(tx, childFolder.id, newFolder.id, pathMap);
|
|
473
488
|
}
|
|
474
|
-
|
|
475
|
-
return {
|
|
476
|
-
files: createdFiles,
|
|
477
|
-
childFolders,
|
|
478
|
-
};
|
|
479
489
|
}
|
|
480
490
|
|
|
481
491
|
|
|
@@ -487,6 +497,18 @@ export async function findFullExportableClass(classId: string) {
|
|
|
487
497
|
select: {
|
|
488
498
|
...assignmentSelect,
|
|
489
499
|
submissions: false,
|
|
500
|
+
worksheets: { select: { id: true } },
|
|
501
|
+
teacherId: true,
|
|
502
|
+
classId: true,
|
|
503
|
+
sectionId: true,
|
|
504
|
+
markSchemeId: true,
|
|
505
|
+
gradingBoundaryId: true,
|
|
506
|
+
eventId: true,
|
|
507
|
+
acceptFiles: true,
|
|
508
|
+
acceptExtendedResponse: true,
|
|
509
|
+
acceptWorksheet: true,
|
|
510
|
+
gradeWithAI: true,
|
|
511
|
+
aiPolicyLevel: true,
|
|
490
512
|
},
|
|
491
513
|
},
|
|
492
514
|
classFiles: {
|
|
@@ -494,10 +516,48 @@ export async function findFullExportableClass(classId: string) {
|
|
|
494
516
|
id: true,
|
|
495
517
|
},
|
|
496
518
|
},
|
|
497
|
-
worksheets:
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
519
|
+
worksheets: {
|
|
520
|
+
select: {
|
|
521
|
+
id: true,
|
|
522
|
+
name: true,
|
|
523
|
+
classId: true,
|
|
524
|
+
questions: {
|
|
525
|
+
select: {
|
|
526
|
+
type: true,
|
|
527
|
+
question: true,
|
|
528
|
+
answer: true,
|
|
529
|
+
points: true,
|
|
530
|
+
options: true,
|
|
531
|
+
markScheme: true,
|
|
532
|
+
order: true,
|
|
533
|
+
worksheetId: true,
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
markSchemes: {
|
|
539
|
+
select: {
|
|
540
|
+
id: true,
|
|
541
|
+
classId: true,
|
|
542
|
+
structured: true,
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
gradingBoundaries: {
|
|
546
|
+
select: {
|
|
547
|
+
id: true,
|
|
548
|
+
classId: true,
|
|
549
|
+
structured: true,
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
sections: {
|
|
553
|
+
select: {
|
|
554
|
+
id: true,
|
|
555
|
+
name: true,
|
|
556
|
+
classId: true,
|
|
557
|
+
color: true,
|
|
558
|
+
order: true,
|
|
559
|
+
},
|
|
560
|
+
},
|
|
501
561
|
},
|
|
502
562
|
});
|
|
503
563
|
|
|
@@ -516,6 +576,10 @@ export async function findFullExportableClass(classId: string) {
|
|
|
516
576
|
}
|
|
517
577
|
|
|
518
578
|
export async function createClassByImport(classId: string, userId: string, year: number, classData: Class & { classFiles: Folder | null }): Promise<string | null> {
|
|
579
|
+
const pathMap = classData.classFiles?.id
|
|
580
|
+
? await recursivelyCopyFilesToGcs(classData.classFiles.id)
|
|
581
|
+
: new Map<string, string>();
|
|
582
|
+
|
|
519
583
|
const newClassId = await prisma.$transaction(async (tx) => {
|
|
520
584
|
const createdClass = await tx.class.create({
|
|
521
585
|
data: {
|
|
@@ -532,67 +596,108 @@ export async function createClassByImport(classId: string, userId: string, year:
|
|
|
532
596
|
},
|
|
533
597
|
});
|
|
534
598
|
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
599
|
+
const oldSectionIdToNew = new Map<string, string>();
|
|
600
|
+
const oldMarkSchemeIdToNew = new Map<string, string>();
|
|
601
|
+
const oldGradingBoundaryIdToNew = new Map<string, string>();
|
|
602
|
+
const oldWorksheetIdToNew = new Map<string, string>();
|
|
603
|
+
|
|
604
|
+
const sections = (classData as unknown as Class & { sections: Section[] }).sections ?? [];
|
|
605
|
+
await Promise.all(
|
|
606
|
+
sections.map(async (section, index) => {
|
|
607
|
+
const newSection = await tx.section.create({
|
|
608
|
+
data: {
|
|
609
|
+
name: section.name,
|
|
610
|
+
classId: createdClass.id,
|
|
611
|
+
color: section.color,
|
|
612
|
+
order: section.order ?? index,
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
oldSectionIdToNew.set(section.id, newSection.id);
|
|
616
|
+
return newSection;
|
|
545
617
|
})
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const markSchemes = (classData as unknown as Class & { markSchemes: MarkScheme[] }).markSchemes ?? [];
|
|
621
|
+
await Promise.all(
|
|
622
|
+
markSchemes.map(async (ms) => {
|
|
623
|
+
const newMs = await tx.markScheme.create({
|
|
624
|
+
data: { classId: createdClass.id, structured: ms.structured },
|
|
625
|
+
});
|
|
626
|
+
oldMarkSchemeIdToNew.set(ms.id, newMs.id);
|
|
627
|
+
return newMs;
|
|
555
628
|
})
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const gradingBoundaries = (classData as unknown as Class & { gradingBoundaries: GradingBoundary[] }).gradingBoundaries ?? [];
|
|
632
|
+
await Promise.all(
|
|
633
|
+
gradingBoundaries.map(async (gb) => {
|
|
634
|
+
const newGb = await tx.gradingBoundary.create({
|
|
635
|
+
data: { classId: createdClass.id, structured: gb.structured },
|
|
636
|
+
});
|
|
637
|
+
oldGradingBoundaryIdToNew.set(gb.id, newGb.id);
|
|
638
|
+
return newGb;
|
|
565
639
|
})
|
|
566
|
-
|
|
640
|
+
);
|
|
567
641
|
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
642
|
+
const worksheets = (classData as unknown as Class & { worksheets: Array<Worksheet & { questions: WorksheetQuestion[] }> }).worksheets ?? [];
|
|
643
|
+
for (const worksheet of worksheets) {
|
|
644
|
+
const newWorksheet = await tx.worksheet.create({
|
|
645
|
+
data: {
|
|
646
|
+
name: worksheet.name,
|
|
647
|
+
classId: createdClass.id,
|
|
648
|
+
questions: {
|
|
649
|
+
create: (worksheet.questions ?? []).map((q, i) => ({
|
|
650
|
+
type: q.type,
|
|
651
|
+
question: q.question,
|
|
652
|
+
answer: q.answer,
|
|
653
|
+
points: q.points ?? 0,
|
|
654
|
+
options: q.options ?? {},
|
|
655
|
+
markScheme: q.markScheme ?? {},
|
|
656
|
+
order: q.order ?? i,
|
|
657
|
+
})),
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
oldWorksheetIdToNew.set(worksheet.id, newWorksheet.id);
|
|
662
|
+
}
|
|
577
663
|
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
664
|
+
const assignments = (classData as unknown as Class & { assignments: Array<Assignment & { worksheets?: { id: string }[] }> }).assignments ?? [];
|
|
665
|
+
for (const assignment of assignments) {
|
|
666
|
+
const date = new Date(assignment.dueDate);
|
|
667
|
+
date.setFullYear(year);
|
|
668
|
+
const worksheetIds = (assignment as { worksheets?: { id: string }[] }).worksheets?.map((w) => oldWorksheetIdToNew.get(w.id)).filter(Boolean) as string[] | undefined;
|
|
669
|
+
await tx.assignment.create({
|
|
670
|
+
data: {
|
|
671
|
+
title: assignment.title,
|
|
672
|
+
type: assignment.type,
|
|
673
|
+
dueDate: date,
|
|
674
|
+
instructions: assignment.instructions,
|
|
675
|
+
teacherId: userId,
|
|
676
|
+
classId: createdClass.id,
|
|
677
|
+
sectionId: assignment.sectionId ? oldSectionIdToNew.get(assignment.sectionId) ?? undefined : undefined,
|
|
678
|
+
markSchemeId: assignment.markSchemeId ? oldMarkSchemeIdToNew.get(assignment.markSchemeId) ?? undefined : undefined,
|
|
679
|
+
gradingBoundaryId: assignment.gradingBoundaryId ? oldGradingBoundaryIdToNew.get(assignment.gradingBoundaryId) ?? undefined : undefined,
|
|
680
|
+
weight: assignment.weight ?? 1,
|
|
681
|
+
maxGrade: assignment.maxGrade ?? 0,
|
|
682
|
+
graded: assignment.graded ?? false,
|
|
683
|
+
inProgress: assignment.inProgress ?? false,
|
|
684
|
+
template: assignment.template ?? false,
|
|
685
|
+
acceptFiles: assignment.acceptFiles ?? false,
|
|
686
|
+
acceptExtendedResponse: assignment.acceptExtendedResponse ?? false,
|
|
687
|
+
acceptWorksheet: assignment.acceptWorksheet ?? false,
|
|
688
|
+
gradeWithAI: assignment.gradeWithAI ?? false,
|
|
689
|
+
aiPolicyLevel: assignment.aiPolicyLevel ?? 0,
|
|
690
|
+
...(worksheetIds?.length && { worksheets: { connect: worksheetIds.map((id) => ({ id })) } }),
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}
|
|
587
694
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
return null;
|
|
695
|
+
if (classData.classFiles?.id) {
|
|
696
|
+
await recursivelyCreateFiles(tx, classData.classFiles.id, createdClass.classFiles?.id, pathMap);
|
|
591
697
|
}
|
|
592
698
|
|
|
593
699
|
return createdClass.id;
|
|
594
|
-
|
|
595
|
-
});
|
|
700
|
+
}, { timeout: 30000 });
|
|
596
701
|
|
|
597
702
|
return newClassId;
|
|
598
|
-
}
|
|
703
|
+
}
|