@studious-lms/server 1.12.0 → 1.14.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/.dockerignore +28 -0
- package/.env.example +5 -0
- package/.github/workflows/deploy.yml +123 -0
- package/Dockerfile +54 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +31 -11
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/config/env.d.ts +17 -0
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +8 -2
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts +10 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +22 -2
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/prisma.d.ts +104 -5
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +33 -2
- package/dist/lib/prisma.js.map +1 -1
- package/dist/models/agenda.d.ts +20 -20
- package/dist/models/announcement.d.ts +177 -69
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +551 -353
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +197 -71
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/auth.d.ts +348 -114
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +324 -188
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +9 -3
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +146 -56
- package/dist/models/cloudStorage.d.ts.map +1 -1
- package/dist/models/comment.d.ts +269 -110
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +3 -2
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +206 -63
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +13 -2
- package/dist/models/conversation.js.map +1 -1
- package/dist/models/department.d.ts +196 -70
- package/dist/models/department.d.ts.map +1 -1
- package/dist/models/event.d.ts +287 -125
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +389 -263
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +433 -181
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +209 -83
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +80 -26
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/message.d.ts +95 -41
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +83 -29
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/notification.d.ts +61 -25
- package/dist/models/notification.d.ts.map +1 -1
- package/dist/models/school.d.ts +349 -133
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/section.d.ts +216 -72
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +156 -48
- package/dist/models/tag.d.ts.map +1 -1
- package/dist/models/user.d.ts +82 -28
- package/dist/models/user.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +1023 -410
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +19 -7
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +0 -8
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +50 -67
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +393 -388
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +63 -69
- package/dist/pipelines/aiLabChatContract.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +41 -41
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +8 -8
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +59 -14
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +10 -8
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +47 -67
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +2 -2
- package/dist/routers/_app.d.ts +2268 -1898
- 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/admin.d.ts +13 -13
- package/dist/routers/agenda.d.ts +18 -18
- package/dist/routers/announcement.d.ts +32 -28
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +335 -327
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +4 -4
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +10 -10
- package/dist/routers/auth.d.ts +2 -2
- package/dist/routers/class.d.ts +170 -147
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +40 -16
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/comment.js +6 -3
- package/dist/routers/comment.js.map +1 -1
- package/dist/routers/conversation.d.ts +17 -17
- package/dist/routers/department.d.ts +34 -34
- package/dist/routers/discussion.d.ts +65 -53
- package/dist/routers/discussion.d.ts.map +1 -1
- package/dist/routers/discussion.js +6 -2
- package/dist/routers/discussion.js.map +1 -1
- package/dist/routers/embedding.d.ts +69 -0
- package/dist/routers/embedding.d.ts.map +1 -0
- package/dist/routers/embedding.js +143 -0
- package/dist/routers/embedding.js.map +1 -0
- package/dist/routers/event.d.ts +39 -39
- package/dist/routers/file.d.ts +12 -12
- package/dist/routers/folder.d.ts +38 -38
- package/dist/routers/labChat.d.ts +19 -19
- package/dist/routers/marketing.d.ts +2 -2
- package/dist/routers/message.d.ts +13 -13
- package/dist/routers/newtonChat.d.ts +4 -4
- package/dist/routers/notifications.d.ts +9 -9
- package/dist/routers/permissions.d.ts +14 -14
- package/dist/routers/school.d.ts +55 -55
- package/dist/routers/search.d.ts +22 -1
- package/dist/routers/search.d.ts.map +1 -1
- package/dist/routers/search.js +9 -3
- package/dist/routers/search.js.map +1 -1
- package/dist/routers/section.d.ts +4 -4
- package/dist/routers/studentProgress.d.ts +1 -1
- package/dist/routers/tag.d.ts +7 -7
- package/dist/routers/user.d.ts +10 -10
- package/dist/routers/worksheet.d.ts +97 -56
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +28 -2
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/routes/aiStream.d.ts +2 -0
- package/dist/routes/aiStream.d.ts.map +1 -0
- package/dist/routes/aiStream.js +696 -0
- package/dist/routes/aiStream.js.map +1 -0
- package/dist/seedDatabase.d.ts +2 -2
- package/dist/seedDatabase.js +6 -6
- package/dist/seedDatabase.js.map +1 -1
- package/dist/services/adminInstitution.d.ts +8 -8
- package/dist/services/adminRunJob.d.ts +4 -4
- package/dist/services/agenda.d.ts +18 -18
- package/dist/services/aiChatStream.d.ts +68 -0
- package/dist/services/aiChatStream.d.ts.map +1 -0
- package/dist/services/aiChatStream.js +157 -0
- package/dist/services/aiChatStream.js.map +1 -0
- package/dist/services/aiModifierTools.d.ts +32 -0
- package/dist/services/aiModifierTools.d.ts.map +1 -0
- package/dist/services/aiModifierTools.js +165 -0
- package/dist/services/aiModifierTools.js.map +1 -0
- package/dist/services/aiProposalTools.d.ts +32 -0
- package/dist/services/aiProposalTools.d.ts.map +1 -0
- package/dist/services/aiProposalTools.js +207 -0
- package/dist/services/aiProposalTools.js.map +1 -0
- package/dist/services/aiStream.d.ts +22 -0
- package/dist/services/aiStream.d.ts.map +1 -0
- package/dist/services/aiStream.js +142 -0
- package/dist/services/aiStream.js.map +1 -0
- package/dist/services/aiTools.d.ts +16 -0
- package/dist/services/aiTools.d.ts.map +1 -0
- package/dist/services/aiTools.js +271 -0
- package/dist/services/aiTools.js.map +1 -0
- package/dist/services/announcement.d.ts +12 -12
- 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 +293 -293
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +36 -15
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +10 -10
- package/dist/services/auth.d.ts +2 -2
- package/dist/services/class.d.ts +161 -138
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +25 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +34 -16
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/comment.js +23 -2
- package/dist/services/comment.js.map +1 -1
- package/dist/services/conversation.d.ts +17 -17
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +9 -7
- package/dist/services/conversation.js.map +1 -1
- package/dist/services/department.d.ts +33 -33
- package/dist/services/discussion.d.ts +58 -58
- package/dist/services/discussion.d.ts.map +1 -1
- package/dist/services/discussion.js +8 -8
- package/dist/services/discussion.js.map +1 -1
- package/dist/services/discussionSummary.d.ts +21 -0
- package/dist/services/discussionSummary.d.ts.map +1 -0
- package/dist/services/discussionSummary.js +179 -0
- package/dist/services/discussionSummary.js.map +1 -0
- package/dist/services/embedding.d.ts +67 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +226 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/event.d.ts +37 -37
- package/dist/services/file.d.ts +24 -24
- package/dist/services/folder.d.ts +36 -36
- package/dist/services/labChat.d.ts +19 -19
- package/dist/services/marketing.d.ts +2 -2
- package/dist/services/message.d.ts +11 -11
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +2 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +4 -4
- package/dist/services/notification.d.ts +9 -9
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/permissions.d.ts +14 -14
- package/dist/services/school.d.ts +50 -50
- package/dist/services/search.d.ts +12 -0
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +120 -2
- package/dist/services/search.js.map +1 -1
- package/dist/services/section.d.ts +3 -3
- package/dist/services/tag.d.ts +4 -4
- package/dist/services/user.d.ts +4 -4
- package/dist/services/user.d.ts.map +1 -1
- package/dist/services/user.js +10 -15
- package/dist/services/user.js.map +1 -1
- package/dist/services/worksheet.d.ts +62 -47
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +11 -4
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/streamBroadcast.d.ts +11 -0
- package/dist/utils/streamBroadcast.d.ts.map +1 -0
- package/dist/utils/streamBroadcast.js +32 -0
- package/dist/utils/streamBroadcast.js.map +1 -0
- package/dist/workers/connection.d.ts +15 -0
- package/dist/workers/connection.d.ts.map +1 -1
- package/dist/workers/connection.js +20 -2
- package/dist/workers/connection.js.map +1 -1
- package/dist/workers/index.js +4 -2
- package/dist/workers/index.js.map +1 -1
- package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
- package/dist/workers/processors/createRunJobWorker.js +31 -8
- package/dist/workers/processors/createRunJobWorker.js.map +1 -1
- package/dist/workers/processors/embedContent.d.ts +8 -0
- package/dist/workers/processors/embedContent.d.ts.map +1 -0
- package/dist/workers/processors/embedContent.js +76 -0
- package/dist/workers/processors/embedContent.js.map +1 -0
- package/dist/workers/queues.d.ts +6 -0
- package/dist/workers/queues.d.ts.map +1 -1
- package/dist/workers/queues.js +5 -3
- package/dist/workers/queues.js.map +1 -1
- package/package.json +5 -1
- package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
- package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
- package/prisma/schema.prisma +39 -7
- package/src/index.ts +4 -0
- package/src/lib/config/cors.ts +32 -9
- package/src/lib/config/env.ts +6 -0
- package/src/lib/googleCloudStorage.ts +21 -0
- package/src/lib/prisma.ts +40 -1
- package/src/models/class.ts +10 -3
- package/src/models/comment.ts +1 -0
- package/src/models/conversation.ts +12 -0
- package/src/models/worksheet.ts +16 -7
- package/src/pipelines/aiLabChat.ts +54 -79
- package/src/pipelines/aiLabChatContract.ts +69 -69
- package/src/pipelines/aiNewtonChat.ts +46 -43
- package/src/pipelines/gradeWorksheet.ts +66 -15
- package/src/pipelines/labChatPrompt.ts +63 -88
- package/src/routers/_app.ts +2 -0
- package/src/routers/announcement.ts +4 -3
- package/src/routers/assignment.ts +2 -2
- package/src/routers/comment.ts +5 -0
- package/src/routers/discussion.ts +7 -0
- package/src/routers/embedding.ts +159 -0
- package/src/routers/search.ts +10 -1
- package/src/routers/worksheet.ts +29 -0
- package/src/routes/aiStream.ts +789 -0
- package/src/seedDatabase.ts +4 -4
- package/src/services/aiChatStream.ts +219 -0
- package/src/services/aiModifierTools.ts +188 -0
- package/src/services/aiProposalTools.ts +272 -0
- package/src/services/aiStream.ts +175 -0
- package/src/services/aiTools.ts +290 -0
- package/src/services/announcement.ts +7 -0
- package/src/services/assignment.ts +36 -14
- package/src/services/class.ts +30 -2
- package/src/services/comment.ts +22 -0
- package/src/services/conversation.ts +9 -4
- package/src/services/discussion.ts +9 -8
- package/src/services/discussionSummary.ts +226 -0
- package/src/services/embedding.ts +285 -0
- package/src/services/message.ts +2 -2
- package/src/services/search.ts +138 -0
- package/src/services/user.ts +9 -17
- package/src/services/worksheet.ts +10 -2
- package/src/utils/streamBroadcast.ts +48 -0
- package/src/workers/connection.ts +19 -0
- package/src/workers/index.ts +2 -0
- package/src/workers/processors/createRunJobWorker.ts +32 -6
- package/src/workers/processors/embedContent.ts +90 -0
- package/src/workers/queues.ts +5 -1
- package/tests/integration/routers/conversation.test.ts +9 -7
- package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
- package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
- package/prisma/migrations/20250807062924_init/migration.sql +0 -436
- package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
- package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
- package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
- package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
- package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
- package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
- package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
- package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
- package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
- package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
- package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
- package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
- package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
- package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
- package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
- package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
- package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
- package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
- /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- CreateTable (implicit many-to-many join for ClassTag <-> Worksheet)
|
|
2
|
+
CREATE TABLE "_WorksheetToTag" (
|
|
3
|
+
"A" TEXT NOT NULL,
|
|
4
|
+
"B" TEXT NOT NULL,
|
|
5
|
+
CONSTRAINT "_WorksheetToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "DiscussionTag"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
|
6
|
+
CONSTRAINT "_WorksheetToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Worksheet"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
-- CreateIndex
|
|
10
|
+
CREATE UNIQUE INDEX "_WorksheetToTag_AB_unique" ON "_WorksheetToTag"("A", "B");
|
|
11
|
+
|
|
12
|
+
-- CreateIndex
|
|
13
|
+
CREATE INDEX "_WorksheetToTag_B_index" ON "_WorksheetToTag"("B");
|
package/prisma/schema.prisma
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
6
6
|
|
|
7
7
|
generator client {
|
|
8
|
-
provider
|
|
8
|
+
provider = "prisma-client-js"
|
|
9
|
+
previewFeatures = ["postgresqlExtensions"]
|
|
9
10
|
// output = "../generated/prisma"
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -15,9 +16,10 @@ generator client {
|
|
|
15
16
|
// }
|
|
16
17
|
|
|
17
18
|
datasource db {
|
|
18
|
-
provider
|
|
19
|
-
url
|
|
20
|
-
directUrl
|
|
19
|
+
provider = "postgresql"
|
|
20
|
+
url = env("DATABASE_URL")
|
|
21
|
+
directUrl = env("DIRECT_URL")
|
|
22
|
+
extensions = [vector]
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
enum AssignmentType {
|
|
@@ -321,7 +323,8 @@ model Class {
|
|
|
321
323
|
tags Tag[] @relation("ClassTags")
|
|
322
324
|
permissions Permission[] @relation("ClassPermissions")
|
|
323
325
|
discussions Discussion[]
|
|
324
|
-
|
|
326
|
+
classTags ClassTag[]
|
|
327
|
+
embeddings Embedding[]
|
|
325
328
|
|
|
326
329
|
discussionsEnabled Boolean @default(true)
|
|
327
330
|
studentsCanCreateDiscussions Boolean @default(true)
|
|
@@ -519,6 +522,7 @@ model Comment {
|
|
|
519
522
|
parentCommentId String?
|
|
520
523
|
replies Comment[] @relation("CommentReplies")
|
|
521
524
|
reactions Reaction[]
|
|
525
|
+
isPinned Boolean @default(false)
|
|
522
526
|
createdAt DateTime @default(now())
|
|
523
527
|
modifiedAt DateTime? @updatedAt
|
|
524
528
|
studentQuestionProgress StudentQuestionProgress? @relation("StudentQuestionProgressComments", fields: [studentQuestionProgressId], references: [id], onDelete: Cascade)
|
|
@@ -541,7 +545,7 @@ model Discussion {
|
|
|
541
545
|
isLocked Boolean @default(false)
|
|
542
546
|
allowReplies Boolean @default(true)
|
|
543
547
|
attachments File[] @relation("DiscussionFiles")
|
|
544
|
-
tags
|
|
548
|
+
tags ClassTag[] @relation("DiscussionToTag")
|
|
545
549
|
replies DiscussionReply[]
|
|
546
550
|
reactions Reaction[]
|
|
547
551
|
reads DiscussionRead[]
|
|
@@ -574,15 +578,17 @@ model DiscussionReply {
|
|
|
574
578
|
@@index([parentId])
|
|
575
579
|
}
|
|
576
580
|
|
|
577
|
-
model
|
|
581
|
+
model ClassTag {
|
|
578
582
|
id String @id @default(uuid())
|
|
579
583
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
580
584
|
classId String
|
|
581
585
|
name String
|
|
582
586
|
color String @default("#3B82F6")
|
|
583
587
|
discussions Discussion[] @relation("DiscussionToTag")
|
|
588
|
+
worksheets Worksheet[] @relation("WorksheetToTag")
|
|
584
589
|
|
|
585
590
|
@@unique([classId, name])
|
|
591
|
+
@@map("DiscussionTag")
|
|
586
592
|
}
|
|
587
593
|
|
|
588
594
|
model DiscussionRead {
|
|
@@ -860,6 +866,7 @@ model Worksheet {
|
|
|
860
866
|
runJob RunJob? @relation("WorksheetRunJob", fields: [runJobId], references: [id], onDelete: SetNull)
|
|
861
867
|
|
|
862
868
|
status JobStatus? @default(NOT_STARTED)
|
|
869
|
+
tags ClassTag[] @relation("WorksheetToTag")
|
|
863
870
|
|
|
864
871
|
@@unique([classId, sourceUrl])
|
|
865
872
|
}
|
|
@@ -961,6 +968,31 @@ enum RunJobType {
|
|
|
961
968
|
AI_LAB_CHAT
|
|
962
969
|
AI_NEWTON_CHAT
|
|
963
970
|
STUDENT_PROGRESS_CHAT
|
|
971
|
+
EMBED_CONTENT
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
enum EmbeddingSourceType {
|
|
975
|
+
FILE
|
|
976
|
+
ASSIGNMENT
|
|
977
|
+
WORKSHEET
|
|
978
|
+
ANNOUNCEMENT
|
|
979
|
+
SECTION
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
model Embedding {
|
|
983
|
+
id String @id @default(uuid())
|
|
984
|
+
classId String
|
|
985
|
+
sourceType EmbeddingSourceType
|
|
986
|
+
sourceId String
|
|
987
|
+
content String
|
|
988
|
+
vector Unsupported("vector(1536)")
|
|
989
|
+
metadata Json? @default("{}")
|
|
990
|
+
createdAt DateTime @default(now())
|
|
991
|
+
|
|
992
|
+
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
993
|
+
|
|
994
|
+
@@index([classId])
|
|
995
|
+
@@index([sourceType, sourceId])
|
|
964
996
|
}
|
|
965
997
|
|
|
966
998
|
enum RunJobStatus {
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
27
27
|
|
|
28
28
|
import "./instrument.js";
|
|
29
29
|
import { openAIClient } from './utils/inference.js';
|
|
30
|
+
import { aiStreamRouter } from './routes/aiStream.js';
|
|
30
31
|
|
|
31
32
|
const app = express();
|
|
32
33
|
|
|
@@ -313,6 +314,9 @@ io.engine.on('connection_error', (err: Error) => {
|
|
|
313
314
|
// Setup socket handlers
|
|
314
315
|
setupSocketHandlers(io);
|
|
315
316
|
|
|
317
|
+
// AI streaming routes
|
|
318
|
+
app.use('/api/ai', aiStreamRouter);
|
|
319
|
+
|
|
316
320
|
// File serving endpoint for secure file access
|
|
317
321
|
app.get('/api/files/:fileId', async (req, res) => {
|
|
318
322
|
try {
|
package/src/lib/config/cors.ts
CHANGED
|
@@ -5,18 +5,24 @@ type CorsEnv = {
|
|
|
5
5
|
CORS_ALLOWED_ORIGIN_PATTERNS?: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// First-party Studious domains. These are always allowed, regardless of
|
|
9
|
+
// NODE_ENV, so local/dev servers can still serve the real frontends.
|
|
10
|
+
const studiousOrigins = [
|
|
9
11
|
'https://www.studious.sh',
|
|
10
12
|
'https://studious.sh',
|
|
11
13
|
'https://dev.studious.sh',
|
|
12
|
-
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
14
|
+
'https://superuser.studious.sh',
|
|
15
|
+
'https://*.studious.sh',
|
|
16
|
+
'https://www.studious.ac',
|
|
17
|
+
'https://studious.ac',
|
|
18
|
+
'https://dev.studious.ac',
|
|
19
|
+
'https://superuser.studious.ac',
|
|
20
|
+
'https://*.studious.ac',
|
|
17
21
|
];
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
// Local development frontends.
|
|
24
|
+
// @todo: gate these behind a non-production check before real prod.
|
|
25
|
+
const localhostOrigins = [
|
|
20
26
|
'http://localhost:3000',
|
|
21
27
|
'http://localhost:3001',
|
|
22
28
|
'http://127.0.0.1:3000',
|
|
@@ -40,6 +46,17 @@ const parseList = (value?: string) =>
|
|
|
40
46
|
|
|
41
47
|
const isDefined = <T>(value: T | undefined | null): value is T => Boolean(value);
|
|
42
48
|
|
|
49
|
+
const hasWildcard = (value: string) => value.includes('*');
|
|
50
|
+
|
|
51
|
+
// Turn a wildcard origin like `https://*.studious.ac` into an anchored regex.
|
|
52
|
+
// `*` matches one or more subdomain labels (e.g. `app` or `a.b`).
|
|
53
|
+
const wildcardOriginToRegExp = (value: string) => {
|
|
54
|
+
const escaped = value
|
|
55
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
56
|
+
.replace(/\\\*/g, '[a-z0-9-]+(?:\\.[a-z0-9-]+)*');
|
|
57
|
+
return new RegExp(`^${escaped}$`, 'i');
|
|
58
|
+
};
|
|
59
|
+
|
|
43
60
|
const toCorsOrigin = (value: string) => {
|
|
44
61
|
try {
|
|
45
62
|
return new URL(value).origin;
|
|
@@ -79,14 +96,20 @@ export const isValidCorsOriginPatternList = (patterns?: string) => {
|
|
|
79
96
|
};
|
|
80
97
|
|
|
81
98
|
export const createCorsOriginMatcher = (env: CorsEnv) => {
|
|
82
|
-
const
|
|
83
|
-
...
|
|
99
|
+
const configuredOrigins = Array.from(new Set([
|
|
100
|
+
...studiousOrigins,
|
|
101
|
+
...localhostOrigins,
|
|
84
102
|
env.NEXT_PUBLIC_APP_URL ? toCorsOrigin(env.NEXT_PUBLIC_APP_URL) : undefined,
|
|
85
103
|
...parseCorsOrigins(env.CORS_ALLOWED_ORIGINS),
|
|
86
104
|
].filter(isDefined)));
|
|
87
105
|
|
|
106
|
+
// Wildcard entries (e.g. `https://*.studious.ac`) can't be matched by exact
|
|
107
|
+
// string comparison, so they're compiled into regex patterns instead.
|
|
108
|
+
const allowedOrigins = configuredOrigins.filter((origin) => !hasWildcard(origin));
|
|
109
|
+
|
|
88
110
|
const allowedOriginPatterns = [
|
|
89
111
|
vercelAppOriginPattern,
|
|
112
|
+
...configuredOrigins.filter(hasWildcard).map(wildcardOriginToRegExp),
|
|
90
113
|
...parseCorsOriginPatterns(env.CORS_ALLOWED_ORIGIN_PATTERNS),
|
|
91
114
|
];
|
|
92
115
|
|
package/src/lib/config/env.ts
CHANGED
|
@@ -35,6 +35,12 @@ const baseSchema = z.object({
|
|
|
35
35
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
36
36
|
PORT: z.string().transform(Number).default('3001'),
|
|
37
37
|
DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
|
|
38
|
+
/**
|
|
39
|
+
* Optional BullMQ key prefix. Defaults to `bull:<NODE_ENV>` (see
|
|
40
|
+
* `getBullPrefix`). Set explicitly to isolate queues when multiple
|
|
41
|
+
* databases share one Redis instance (e.g. several devs on one Upstash).
|
|
42
|
+
*/
|
|
43
|
+
BULL_PREFIX: z.string().optional(),
|
|
38
44
|
});
|
|
39
45
|
|
|
40
46
|
// Production/development schema with all required vars
|
|
@@ -98,4 +98,25 @@ export async function copyFile(sourcePath: string, destPath: string): Promise<vo
|
|
|
98
98
|
message: 'Failed to copy file in storage',
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the public URL for a GCS object.
|
|
105
|
+
* The object must be publicly readable (via ACL or bucket-level IAM).
|
|
106
|
+
*/
|
|
107
|
+
export function getPublicUrl(filePath: string): string {
|
|
108
|
+
return `https://storage.googleapis.com/${env.GOOGLE_CLOUD_BUCKET_NAME}/${filePath}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Best-effort attempt to make a GCS object publicly readable.
|
|
113
|
+
* Silently ignores errors (e.g. uniform bucket-level access enabled).
|
|
114
|
+
*/
|
|
115
|
+
export async function makeFilePublic(filePath: string): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
await bucket.file(filePath).makePublic();
|
|
118
|
+
} catch {
|
|
119
|
+
// Bucket may use uniform access or already have a public IAM policy – either way, the
|
|
120
|
+
// public URL will work if the bucket allows it; nothing to do here.
|
|
121
|
+
}
|
|
101
122
|
}
|
package/src/lib/prisma.ts
CHANGED
|
@@ -30,6 +30,19 @@ const getDatabaseUrl = () => {
|
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a profile-picture value stored in the DB to a browser-loadable URL.
|
|
35
|
+
* - `null` / empty → `null`
|
|
36
|
+
* - already an HTTP URL (e.g. dicebear) → returned as-is
|
|
37
|
+
* - raw GCS object path → converted to a public GCS URL
|
|
38
|
+
*/
|
|
39
|
+
function resolveProfilePictureUrl(raw: string | null): string | null {
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
if (raw.startsWith('http')) return raw;
|
|
42
|
+
const bucket = env.GOOGLE_CLOUD_BUCKET_NAME;
|
|
43
|
+
return `https://storage.googleapis.com/${bucket}/${raw}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
const prismaClientSingleton = () => {
|
|
34
47
|
return new PrismaClient({
|
|
35
48
|
datasources: {
|
|
@@ -37,6 +50,23 @@ const prismaClientSingleton = () => {
|
|
|
37
50
|
url: getDatabaseUrl(),
|
|
38
51
|
},
|
|
39
52
|
},
|
|
53
|
+
}).$extends({
|
|
54
|
+
result: {
|
|
55
|
+
userProfile: {
|
|
56
|
+
profilePicture: {
|
|
57
|
+
needs: { profilePicture: true },
|
|
58
|
+
compute(profile) {
|
|
59
|
+
return resolveProfilePictureUrl(profile.profilePicture);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
profilePictureThumbnail: {
|
|
63
|
+
needs: { profilePictureThumbnail: true },
|
|
64
|
+
compute(profile) {
|
|
65
|
+
return resolveProfilePictureUrl(profile.profilePictureThumbnail);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
40
70
|
});
|
|
41
71
|
};
|
|
42
72
|
|
|
@@ -53,4 +83,13 @@ if (env.NODE_ENV !== 'production') {
|
|
|
53
83
|
|
|
54
84
|
process.on('beforeExit', async () => {
|
|
55
85
|
await prisma.$disconnect();
|
|
56
|
-
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** The concrete (extended) Prisma client type used throughout the app. */
|
|
89
|
+
export type AppPrismaClient = typeof prisma;
|
|
90
|
+
|
|
91
|
+
/** Transaction client type compatible with the extended Prisma client. */
|
|
92
|
+
export type AppTransactionClient = Omit<
|
|
93
|
+
AppPrismaClient,
|
|
94
|
+
'$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
|
|
95
|
+
>;
|
package/src/models/class.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Assignment, Class, Folder, GradingBoundary, MarkScheme, Section, SessionType, Worksheet } from "@prisma/client";
|
|
8
|
-
import type {
|
|
8
|
+
import type { WorksheetQuestion } from "@prisma/client";
|
|
9
|
+
import type { AppPrismaClient, AppTransactionClient } from "../lib/prisma.js";
|
|
9
10
|
|
|
10
11
|
import { prisma } from "../lib/prisma.js";
|
|
11
12
|
import { v4 as uuidv4 } from 'uuid';
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
const classIncludeForGetAll = {
|
|
19
20
|
assignments: {
|
|
20
21
|
where: {
|
|
21
|
-
dueDate: { lte: new Date(new Date().setHours(23, 59, 59, 999)) },
|
|
22
22
|
template: false,
|
|
23
23
|
},
|
|
24
24
|
select: {
|
|
@@ -26,6 +26,13 @@ const classIncludeForGetAll = {
|
|
|
26
26
|
title: true,
|
|
27
27
|
type: true,
|
|
28
28
|
dueDate: true,
|
|
29
|
+
submissions: {
|
|
30
|
+
select: {
|
|
31
|
+
studentId: true,
|
|
32
|
+
submitted: true,
|
|
33
|
+
returned: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
29
36
|
},
|
|
30
37
|
},
|
|
31
38
|
students: {
|
|
@@ -471,7 +478,7 @@ async function recursivelyCopyFilesToGcs(folderId: string): Promise<Map<string,
|
|
|
471
478
|
}
|
|
472
479
|
|
|
473
480
|
async function recursivelyCreateFiles(
|
|
474
|
-
tx:
|
|
481
|
+
tx: AppPrismaClient | AppTransactionClient,
|
|
475
482
|
folderId: string,
|
|
476
483
|
targetFolderId: string | undefined,
|
|
477
484
|
pathMap: Map<string, string>
|
package/src/models/comment.ts
CHANGED
|
@@ -256,6 +256,18 @@ export function hideConversationMember(userId: string, conversationId: string) {
|
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/** Unhide a conversation for a single member. */
|
|
260
|
+
export function unhideConversationMember(userId: string, conversationId: string) {
|
|
261
|
+
return prisma.conversationMember.update({
|
|
262
|
+
where: {
|
|
263
|
+
userId_conversationId: { userId, conversationId },
|
|
264
|
+
},
|
|
265
|
+
data: {
|
|
266
|
+
hiddenAt: null,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
259
271
|
/** Remove a member from a conversation. */
|
|
260
272
|
export function deleteConversationMember(userId: string, conversationId: string) {
|
|
261
273
|
return prisma.conversationMember.delete({
|
package/src/models/worksheet.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Worksheet model – worksheets, questions, submissions, student progress, grading.
|
|
3
3
|
*/
|
|
4
|
-
import type { Prisma } from "@prisma/client";
|
|
5
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
|
+
import type { AppTransactionClient } from "../lib/prisma.js";
|
|
6
6
|
import type { WorksheetQuestionType, JobStatus } from "@prisma/client";
|
|
7
7
|
import {
|
|
8
8
|
classMemberOrSchoolAdminWhere,
|
|
@@ -22,7 +22,7 @@ export function findWorksheetById(id: string) {
|
|
|
22
22
|
return prisma.worksheet.findUnique({
|
|
23
23
|
where: { id },
|
|
24
24
|
include: {
|
|
25
|
-
questions: { orderBy: {
|
|
25
|
+
questions: { orderBy: { order: "asc" as const } },
|
|
26
26
|
class: true,
|
|
27
27
|
},
|
|
28
28
|
});
|
|
@@ -43,8 +43,9 @@ export function findWorksheetByIdForClassMember(id: string, userId: string) {
|
|
|
43
43
|
...worksheetClassMemberWhere(userId),
|
|
44
44
|
},
|
|
45
45
|
include: {
|
|
46
|
-
questions: { orderBy: {
|
|
46
|
+
questions: { orderBy: { order: "asc" as const } },
|
|
47
47
|
class: true,
|
|
48
|
+
tags: true,
|
|
48
49
|
},
|
|
49
50
|
});
|
|
50
51
|
}
|
|
@@ -57,8 +58,9 @@ export function findWorksheetByIdForTeacher(id: string, userId: string) {
|
|
|
57
58
|
...worksheetTeacherWhere(userId),
|
|
58
59
|
},
|
|
59
60
|
include: {
|
|
60
|
-
questions: { orderBy: {
|
|
61
|
+
questions: { orderBy: { order: "asc" as const } },
|
|
61
62
|
class: true,
|
|
63
|
+
tags: true,
|
|
62
64
|
},
|
|
63
65
|
});
|
|
64
66
|
}
|
|
@@ -69,6 +71,7 @@ export function findWorksheetsByClassId(classId: string) {
|
|
|
69
71
|
where: { classId },
|
|
70
72
|
include: {
|
|
71
73
|
questions: { select: { id: true } },
|
|
74
|
+
tags: true,
|
|
72
75
|
},
|
|
73
76
|
orderBy: { createdAt: "desc" },
|
|
74
77
|
});
|
|
@@ -121,7 +124,7 @@ export function deleteWorksheet(id: string) {
|
|
|
121
124
|
* Does not copy student responses or assignment links.
|
|
122
125
|
*/
|
|
123
126
|
export async function duplicateWorksheetWithClient(
|
|
124
|
-
tx:
|
|
127
|
+
tx: AppTransactionClient,
|
|
125
128
|
sourceWorksheetId: string,
|
|
126
129
|
targetClassId: string,
|
|
127
130
|
name: string
|
|
@@ -273,7 +276,10 @@ export function findSubmissionByIdForWorksheetAccess(
|
|
|
273
276
|
const worksheetResponseInclude = {
|
|
274
277
|
responses: {
|
|
275
278
|
include: {
|
|
276
|
-
comments: {
|
|
279
|
+
comments: {
|
|
280
|
+
select: { id: true, isPinned: true },
|
|
281
|
+
orderBy: [{ isPinned: "desc" as const }, { createdAt: "asc" as const }],
|
|
282
|
+
},
|
|
277
283
|
},
|
|
278
284
|
},
|
|
279
285
|
};
|
|
@@ -319,7 +325,10 @@ export function createWorksheetResponse(data: {
|
|
|
319
325
|
include: {
|
|
320
326
|
responses: {
|
|
321
327
|
include: {
|
|
322
|
-
comments: {
|
|
328
|
+
comments: {
|
|
329
|
+
select: { id: true, isPinned: true },
|
|
330
|
+
orderBy: [{ isPinned: "desc" as const }, { createdAt: "asc" as const }],
|
|
331
|
+
},
|
|
323
332
|
},
|
|
324
333
|
},
|
|
325
334
|
},
|
|
@@ -7,7 +7,6 @@ import { prisma } from "../lib/prisma.js";
|
|
|
7
7
|
import { pusher, teacherChannel } from "../lib/pusher.js";
|
|
8
8
|
import type { Assignment, Class, File, Section, User } from "@prisma/client";
|
|
9
9
|
import {
|
|
10
|
-
inference,
|
|
11
10
|
completePendingAIMessage,
|
|
12
11
|
failPendingAIMessage,
|
|
13
12
|
} from "../utils/inference.js";
|
|
@@ -16,8 +15,11 @@ import { createPdf } from "../lib/jsonConversion.js";
|
|
|
16
15
|
import { v4 } from "uuid";
|
|
17
16
|
import { bucket } from "../lib/googleCloudStorage.js";
|
|
18
17
|
import { DocumentBlock } from "../lib/jsonStyles.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
import { broadcastTextChunks, broadcastStreamComplete } from "../utils/streamBroadcast.js";
|
|
19
|
+
import { buildLabChatAgentMessages } from "./labChatPrompt.js";
|
|
20
|
+
import { streamChatResponse } from "../services/aiChatStream.js";
|
|
21
|
+
import { aiTools } from "../services/aiTools.js";
|
|
22
|
+
import { labProposalTools, createLabProposals } from "../services/aiProposalTools.js";
|
|
21
23
|
|
|
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 = 400_000;
|
|
@@ -303,33 +305,6 @@ export const getBaseSystemPrompt = (
|
|
|
303
305
|
|
|
304
306
|
|
|
305
307
|
|
|
306
|
-
/**
|
|
307
|
-
* Generate labchat responses
|
|
308
|
-
* Allow for the generation of the following:
|
|
309
|
-
* - Assignment(s) either individual or bulk as an lesson / course plan.
|
|
310
|
-
* - Worksheet(s) either individual or bulk as an lesson / course plan.
|
|
311
|
-
* - Files (PDFs)
|
|
312
|
-
* @param labChatId
|
|
313
|
-
*/
|
|
314
|
-
// export const sendAiLabChatResponsePipeline = async (labChatId: string) => {
|
|
315
|
-
// const message = await prisma?.message.create({
|
|
316
|
-
// data: {
|
|
317
|
-
// content: "GENERATING_CONTENT",
|
|
318
|
-
// senderId: getAIUserId(),
|
|
319
|
-
// conversationId: labChatId,
|
|
320
|
-
// status: JobStatus.PENDING,
|
|
321
|
-
// },
|
|
322
|
-
// });
|
|
323
|
-
|
|
324
|
-
// try {
|
|
325
|
-
|
|
326
|
-
// inference(`
|
|
327
|
-
// `)
|
|
328
|
-
// }
|
|
329
|
-
|
|
330
|
-
// };
|
|
331
|
-
|
|
332
|
-
|
|
333
308
|
/**
|
|
334
309
|
* Fill the AI introduction placeholder for a brand-new lab chat.
|
|
335
310
|
*
|
|
@@ -348,6 +323,7 @@ export const generateAndSendLabIntroduction = async (
|
|
|
348
323
|
"Hello teaching team! I'm your AI assistant for course material development. I'll help you create educational content - when you have a clear request, I'll produce it directly. I only ask questions when I need to clarify the topic or scope. What would you like to work on?";
|
|
349
324
|
|
|
350
325
|
try {
|
|
326
|
+
await broadcastTextChunks(conversationId, aiMessageId, introMessage);
|
|
351
327
|
await completePendingAIMessage(aiMessageId, introMessage);
|
|
352
328
|
logger.info('AI Introduction sent', { labChatId, conversationId, aiMessageId });
|
|
353
329
|
} catch (error) {
|
|
@@ -474,50 +450,51 @@ export const generateAndSendLabIntroduction = async (
|
|
|
474
450
|
assignments: classData.assignments,
|
|
475
451
|
});
|
|
476
452
|
|
|
477
|
-
const messages =
|
|
453
|
+
const { system, messages } = buildLabChatAgentMessages({
|
|
478
454
|
context: fullLabChat.context,
|
|
479
455
|
classContext,
|
|
480
456
|
recentMessages: recentMessages.reverse(),
|
|
481
457
|
isAIUser,
|
|
482
458
|
});
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
labChatId,
|
|
459
|
+
|
|
460
|
+
// Genuine agentic generation: the model streams prose token-by-token and
|
|
461
|
+
// calls real tools inline — read-only RAG lookups plus "propose_*" tools
|
|
462
|
+
// that record create-actions into `proposals`. `streamChatResponse`
|
|
463
|
+
// broadcasts the real text chunks and tool-call events as they happen.
|
|
464
|
+
const proposals = createLabProposals();
|
|
465
|
+
const tools = {
|
|
466
|
+
...aiTools(fullLabChat.classId),
|
|
467
|
+
...labProposalTools(proposals),
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const { text: streamedText, parts: streamParts } = await withTimeout(
|
|
471
|
+
streamChatResponse({
|
|
497
472
|
conversationId,
|
|
498
|
-
classId: fullLabChat.classId,
|
|
499
473
|
aiMessageId: emitOptions.aiMessageId,
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
474
|
+
system,
|
|
475
|
+
messages,
|
|
476
|
+
tools,
|
|
477
|
+
temperature: 0.7,
|
|
478
|
+
maxOutputTokens: 4096,
|
|
479
|
+
maxSteps: 8,
|
|
480
|
+
}),
|
|
481
|
+
LAB_CHAT_RESPONSE_TIMEOUT_MS,
|
|
482
|
+
"Lab chat response generation",
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// Real text + tool-call events were already broadcast inline (in order)
|
|
486
|
+
// during the stream by `streamChatResponse`. Just close the stream.
|
|
487
|
+
await broadcastStreamComplete(conversationId, emitOptions.aiMessageId);
|
|
488
|
+
|
|
489
|
+
// Generate PDFs for any proposed documents and persist the message.
|
|
510
490
|
try {
|
|
511
|
-
const jsonData = response;
|
|
512
|
-
|
|
513
|
-
|
|
514
491
|
const attachmentIds: string[] = [];
|
|
515
|
-
// Generate PDFs if docs
|
|
516
|
-
if (
|
|
492
|
+
// Generate PDFs if docs were proposed
|
|
493
|
+
if (proposals.docs.length > 0) {
|
|
517
494
|
|
|
518
495
|
|
|
519
|
-
for (let i = 0; i <
|
|
520
|
-
const doc =
|
|
496
|
+
for (let i = 0; i < proposals.docs.length; i++) {
|
|
497
|
+
const doc = proposals.docs[i];
|
|
521
498
|
if (!doc.title || !doc.blocks || !Array.isArray(doc.blocks)) {
|
|
522
499
|
logger.error(`Document ${i + 1} is missing title or blocks`);
|
|
523
500
|
continue;
|
|
@@ -577,30 +554,28 @@ export const generateAndSendLabIntroduction = async (
|
|
|
577
554
|
}
|
|
578
555
|
}
|
|
579
556
|
|
|
580
|
-
await completePendingAIMessage(emitOptions.aiMessageId,
|
|
557
|
+
await completePendingAIMessage(emitOptions.aiMessageId, streamedText, {
|
|
581
558
|
attachments: {
|
|
582
559
|
connect: attachmentIds.map(id => ({ id })),
|
|
583
560
|
},
|
|
584
561
|
meta: {
|
|
585
|
-
assignmentsToCreate:
|
|
586
|
-
...assignment,
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
id: v4(),
|
|
596
|
-
})) || null,
|
|
562
|
+
assignmentsToCreate: proposals.assignments.length > 0
|
|
563
|
+
? proposals.assignments.map(assignment => ({ ...assignment, id: v4() }))
|
|
564
|
+
: null,
|
|
565
|
+
worksheetsToCreate: proposals.worksheets.length > 0
|
|
566
|
+
? proposals.worksheets.map(worksheet => ({ ...worksheet, id: v4() }))
|
|
567
|
+
: null,
|
|
568
|
+
sectionsToCreate: proposals.sections.length > 0
|
|
569
|
+
? proposals.sections.map(section => ({ ...section, id: v4() }))
|
|
570
|
+
: null,
|
|
571
|
+
streamParts: streamParts.length > 0 ? streamParts : null,
|
|
597
572
|
},
|
|
598
573
|
});
|
|
599
|
-
} catch (
|
|
600
|
-
logger.error('Failed to
|
|
574
|
+
} catch (persistError) {
|
|
575
|
+
logger.error('Failed to persist AI response or generate PDF:', { error: persistError, labChatId });
|
|
601
576
|
// Fallback: complete the placeholder with the raw text so the
|
|
602
577
|
// teacher still sees something useful instead of FAILED.
|
|
603
|
-
await completePendingAIMessage(emitOptions.aiMessageId,
|
|
578
|
+
await completePendingAIMessage(emitOptions.aiMessageId, streamedText);
|
|
604
579
|
}
|
|
605
580
|
|
|
606
581
|
try {
|