@studious-lms/server 1.13.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 -198
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +2 -2
- 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 +267 -115
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +199 -73
- package/dist/models/conversation.d.ts.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 +1020 -411
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +8 -2
- 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/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 +2203 -1957
- 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 -32
- package/dist/routers/assignment.d.ts +335 -335
- package/dist/routers/attendance.d.ts +10 -10
- package/dist/routers/auth.d.ts +2 -2
- package/dist/routers/class.d.ts +147 -147
- package/dist/routers/comment.d.ts +22 -22
- 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 +95 -57
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +26 -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 +28 -9
- 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 +138 -138
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +19 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +22 -22
- package/dist/services/conversation.d.ts +17 -17
- 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 +60 -48
- 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 +38 -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 +3 -2
- package/src/models/worksheet.ts +5 -2
- package/src/pipelines/aiLabChat.ts +54 -79
- package/src/pipelines/aiLabChatContract.ts +69 -69
- package/src/pipelines/aiNewtonChat.ts +46 -43
- package/src/pipelines/labChatPrompt.ts +63 -88
- package/src/routers/_app.ts +2 -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 +27 -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 +28 -7
- package/src/services/class.ts +20 -1
- 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 +9 -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/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)
|
|
@@ -542,7 +545,7 @@ model Discussion {
|
|
|
542
545
|
isLocked Boolean @default(false)
|
|
543
546
|
allowReplies Boolean @default(true)
|
|
544
547
|
attachments File[] @relation("DiscussionFiles")
|
|
545
|
-
tags
|
|
548
|
+
tags ClassTag[] @relation("DiscussionToTag")
|
|
546
549
|
replies DiscussionReply[]
|
|
547
550
|
reactions Reaction[]
|
|
548
551
|
reads DiscussionRead[]
|
|
@@ -575,15 +578,17 @@ model DiscussionReply {
|
|
|
575
578
|
@@index([parentId])
|
|
576
579
|
}
|
|
577
580
|
|
|
578
|
-
model
|
|
581
|
+
model ClassTag {
|
|
579
582
|
id String @id @default(uuid())
|
|
580
583
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
581
584
|
classId String
|
|
582
585
|
name String
|
|
583
586
|
color String @default("#3B82F6")
|
|
584
587
|
discussions Discussion[] @relation("DiscussionToTag")
|
|
588
|
+
worksheets Worksheet[] @relation("WorksheetToTag")
|
|
585
589
|
|
|
586
590
|
@@unique([classId, name])
|
|
591
|
+
@@map("DiscussionTag")
|
|
587
592
|
}
|
|
588
593
|
|
|
589
594
|
model DiscussionRead {
|
|
@@ -861,6 +866,7 @@ model Worksheet {
|
|
|
861
866
|
runJob RunJob? @relation("WorksheetRunJob", fields: [runJobId], references: [id], onDelete: SetNull)
|
|
862
867
|
|
|
863
868
|
status JobStatus? @default(NOT_STARTED)
|
|
869
|
+
tags ClassTag[] @relation("WorksheetToTag")
|
|
864
870
|
|
|
865
871
|
@@unique([classId, sourceUrl])
|
|
866
872
|
}
|
|
@@ -962,6 +968,31 @@ enum RunJobType {
|
|
|
962
968
|
AI_LAB_CHAT
|
|
963
969
|
AI_NEWTON_CHAT
|
|
964
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])
|
|
965
996
|
}
|
|
966
997
|
|
|
967
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';
|
|
@@ -477,7 +478,7 @@ async function recursivelyCopyFilesToGcs(folderId: string): Promise<Map<string,
|
|
|
477
478
|
}
|
|
478
479
|
|
|
479
480
|
async function recursivelyCreateFiles(
|
|
480
|
-
tx:
|
|
481
|
+
tx: AppPrismaClient | AppTransactionClient,
|
|
481
482
|
folderId: string,
|
|
482
483
|
targetFolderId: string | undefined,
|
|
483
484
|
pathMap: Map<string, string>
|
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,
|
|
@@ -45,6 +45,7 @@ export function findWorksheetByIdForClassMember(id: string, userId: string) {
|
|
|
45
45
|
include: {
|
|
46
46
|
questions: { orderBy: { order: "asc" as const } },
|
|
47
47
|
class: true,
|
|
48
|
+
tags: true,
|
|
48
49
|
},
|
|
49
50
|
});
|
|
50
51
|
}
|
|
@@ -59,6 +60,7 @@ export function findWorksheetByIdForTeacher(id: string, userId: string) {
|
|
|
59
60
|
include: {
|
|
60
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
|
|
@@ -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 {
|