@studious-lms/server 1.13.0 → 1.15.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/instrument.js +3 -3
- package/dist/instrument.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/inferDocumentFormat.d.ts +10 -0
- package/dist/lib/inferDocumentFormat.d.ts.map +1 -0
- package/dist/lib/inferDocumentFormat.js +48 -0
- package/dist/lib/inferDocumentFormat.js.map +1 -0
- package/dist/lib/legal.d.ts +3 -0
- package/dist/lib/legal.d.ts.map +1 -0
- package/dist/lib/legal.js +6 -0
- package/dist/lib/legal.js.map +1 -0
- 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 +211 -101
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +560 -362
- 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 +356 -114
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +4 -2
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +327 -201
- 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 +262 -110
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +206 -80
- 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 +305 -141
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +422 -272
- 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 +205 -79
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +79 -38
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/marketing.js +12 -4
- package/dist/models/marketing.js.map +1 -1
- package/dist/models/message.d.ts +97 -43
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +84 -30
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/notification.d.ts +57 -21
- 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 +1032 -423
- 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 +7 -9
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +194 -125
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +233 -389
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +54 -70
- 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 +18 -11
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +93 -75
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
- package/dist/routers/_app.d.ts +2601 -2151
- 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 +60 -60
- package/dist/routers/assignment.d.ts +393 -383
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/attendance.d.ts +10 -10
- package/dist/routers/auth.d.ts +4 -2
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +15 -2
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +157 -153
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +18 -18
- package/dist/routers/conversation.d.ts +11 -11
- package/dist/routers/department.d.ts +34 -34
- package/dist/routers/discussion.d.ts +68 -56
- 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 +73 -0
- package/dist/routers/embedding.d.ts.map +1 -0
- package/dist/routers/embedding.js +151 -0
- package/dist/routers/embedding.js.map +1 -0
- package/dist/routers/event.d.ts +57 -55
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +14 -14
- package/dist/routers/folder.d.ts +38 -38
- package/dist/routers/labChat.d.ts +16 -16
- package/dist/routers/marketing.d.ts +1 -14
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/message.d.ts +13 -13
- package/dist/routers/newtonChat.d.ts +4 -4
- package/dist/routers/notifications.d.ts +5 -5
- package/dist/routers/permissions.d.ts +14 -14
- package/dist/routers/school.d.ts +59 -55
- package/dist/routers/school.d.ts.map +1 -1
- 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 +99 -10
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +19 -2
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +93 -55
- 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 +726 -0
- package/dist/routes/aiStream.js.map +1 -0
- package/dist/seedDatabase.d.ts +3 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- 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 +88 -0
- package/dist/services/aiChatStream.d.ts.map +1 -0
- package/dist/services/aiChatStream.js +254 -0
- package/dist/services/aiChatStream.js.map +1 -0
- package/dist/services/aiModifierTools.d.ts +16 -0
- package/dist/services/aiModifierTools.d.ts.map +1 -0
- package/dist/services/aiModifierTools.js +274 -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 +42 -42
- 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 +355 -345
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +30 -9
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +10 -10
- package/dist/services/auth.d.ts +4 -2
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +13 -4
- package/dist/services/auth.js.map +1 -1
- package/dist/services/class.d.ts +146 -142
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +24 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +18 -18
- package/dist/services/conversation.d.ts +11 -11
- 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 +15 -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 +85 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +372 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/event.d.ts +55 -53
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +28 -28
- package/dist/services/folder.d.ts +36 -36
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +32 -2
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +16 -16
- package/dist/services/marketing.d.ts +2 -15
- package/dist/services/marketing.d.ts.map +1 -1
- package/dist/services/marketing.js +5 -5
- package/dist/services/marketing.js.map +1 -1
- 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 +5 -5
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/openaiDocumentGen.d.ts +37 -0
- package/dist/services/openaiDocumentGen.d.ts.map +1 -0
- package/dist/services/openaiDocumentGen.js +262 -0
- package/dist/services/openaiDocumentGen.js.map +1 -0
- package/dist/services/permissions.d.ts +14 -14
- package/dist/services/school.d.ts +54 -50
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/search.d.ts +13 -1
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +145 -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/userPrivacy.d.ts +86 -0
- package/dist/services/userPrivacy.d.ts.map +1 -0
- package/dist/services/userPrivacy.js +184 -0
- package/dist/services/userPrivacy.js.map +1 -0
- package/dist/services/worksheet.d.ts +86 -57
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +57 -5
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +7 -3
- package/dist/utils/email.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 +67 -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/migrations/20260610140000_expand_embedding_source_types/migration.sql +4 -0
- package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
- package/prisma/schema.prisma +44 -7
- package/src/index.ts +4 -0
- package/src/instrument.ts +1 -1
- 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/inferDocumentFormat.ts +58 -0
- package/src/lib/legal.ts +2 -0
- package/src/lib/prisma.ts +40 -1
- package/src/models/auth.ts +4 -0
- package/src/models/class.ts +3 -2
- package/src/models/marketing.ts +10 -2
- package/src/models/worksheet.ts +5 -2
- package/src/pipelines/aiLabChat.ts +245 -146
- package/src/pipelines/aiLabChatContract.ts +59 -69
- package/src/pipelines/aiNewtonChat.ts +46 -43
- package/src/pipelines/labChatPrompt.ts +115 -90
- package/src/routers/_app.ts +2 -0
- package/src/routers/auth.ts +13 -0
- package/src/routers/discussion.ts +7 -0
- package/src/routers/embedding.ts +164 -0
- package/src/routers/search.ts +10 -1
- package/src/routers/user.ts +26 -0
- package/src/routers/worksheet.ts +27 -0
- package/src/routes/aiStream.ts +827 -0
- package/src/seedDatabase.ts +4 -4
- package/src/services/aiChatStream.ts +341 -0
- package/src/services/aiModifierTools.ts +341 -0
- package/src/services/aiProposalTools.ts +274 -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 +31 -7
- package/src/services/auth.ts +14 -2
- package/src/services/class.ts +27 -2
- package/src/services/discussion.ts +22 -8
- package/src/services/discussionSummary.ts +226 -0
- package/src/services/embedding.ts +446 -0
- package/src/services/folder.ts +31 -0
- package/src/services/marketing.ts +3 -3
- package/src/services/message.ts +2 -2
- package/src/services/openaiDocumentGen.ts +319 -0
- package/src/services/search.ts +163 -1
- package/src/services/user.ts +9 -17
- package/src/services/userPrivacy.ts +196 -0
- package/src/services/worksheet.ts +77 -3
- package/src/utils/email.ts +5 -1
- 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 +86 -0
- package/src/workers/queues.ts +5 -1
- package/tests/helpers/registerInput.ts +20 -0
- package/tests/integration/globalSetup.ts +10 -18
- package/tests/integration/routers/auth.test.ts +13 -18
- package/tests/unit/lib/inferDocumentFormat.test.ts +33 -0
- package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
- package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
- package/tests/unit/services/aiChatStream.test.ts +92 -0
- package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
- 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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { DiscussionType, Prisma, ReactionType } from "@prisma/client";
|
|
2
|
+
import type { AppTransactionClient } from "../lib/prisma.js";
|
|
2
3
|
import { TRPCError } from "@trpc/server";
|
|
3
4
|
import { prisma } from "../lib/prisma.js";
|
|
4
5
|
import { discussionChannel, pusher } from "../lib/pusher.js";
|
|
5
6
|
import { logger } from "../utils/logger.js";
|
|
6
7
|
import { classMemberOrSchoolAdminWhere, classTeacherOrSchoolAdminWhere } from "../models/access.js";
|
|
7
8
|
import { sendNotification } from "./notification.js";
|
|
9
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
8
10
|
|
|
9
11
|
const authorSelect = {
|
|
10
12
|
id: true,
|
|
@@ -225,7 +227,7 @@ async function getDiscussionForUser(discussionId: string, userId: string) {
|
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
async function claimDiscussionAttachments(
|
|
228
|
-
tx:
|
|
230
|
+
tx: AppTransactionClient,
|
|
229
231
|
userId: string,
|
|
230
232
|
discussionId: string,
|
|
231
233
|
attachmentIds: string[] = []
|
|
@@ -242,7 +244,7 @@ async function claimDiscussionAttachments(
|
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
async function claimReplyAttachments(
|
|
245
|
-
tx:
|
|
247
|
+
tx: AppTransactionClient,
|
|
246
248
|
userId: string,
|
|
247
249
|
replyId: string,
|
|
248
250
|
attachmentIds: string[] = []
|
|
@@ -440,6 +442,10 @@ export async function createDiscussion(userId: string, input: {
|
|
|
440
442
|
|
|
441
443
|
triggerDiscussionEvent(input.classId, "discussion-created", { discussion: serializeDiscussion(discussion, userId) });
|
|
442
444
|
await notifyMentionedUsers(input.classId, userId, input.body, `/class/${input.classId}/discussions/${discussion.id}`);
|
|
445
|
+
|
|
446
|
+
// Auto-index discussion for RAG search
|
|
447
|
+
enqueueEmbedding(input.classId, 'DISCUSSION', discussion.id);
|
|
448
|
+
|
|
443
449
|
return { discussion: serializeDiscussion(discussion, userId) };
|
|
444
450
|
}
|
|
445
451
|
|
|
@@ -470,6 +476,10 @@ export async function updateDiscussion(userId: string, input: {
|
|
|
470
476
|
include: discussionInclude,
|
|
471
477
|
});
|
|
472
478
|
triggerDiscussionEvent(updated.classId, "discussion-updated", { discussion: serializeDiscussion(updated, userId) });
|
|
479
|
+
|
|
480
|
+
// Re-index discussion for RAG search after update
|
|
481
|
+
enqueueEmbedding(updated.classId, 'DISCUSSION', updated.id);
|
|
482
|
+
|
|
473
483
|
return { discussion: serializeDiscussion(updated, userId) };
|
|
474
484
|
}
|
|
475
485
|
|
|
@@ -525,6 +535,10 @@ export async function replyToDiscussion(userId: string, input: {
|
|
|
525
535
|
notifyMentionedUsers(discussion.classId, userId, input.body, link),
|
|
526
536
|
]);
|
|
527
537
|
triggerDiscussionEvent(discussion.classId, "reply-created", { discussionId: discussion.id, reply: serializeReply(reply, userId) });
|
|
538
|
+
|
|
539
|
+
// Re-index discussion for RAG search (includes new reply)
|
|
540
|
+
enqueueEmbedding(discussion.classId, 'DISCUSSION', discussion.id);
|
|
541
|
+
|
|
528
542
|
return { reply: serializeReply(reply, userId) };
|
|
529
543
|
}
|
|
530
544
|
|
|
@@ -674,28 +688,28 @@ export async function toggleDiscussionReaction(userId: string, input: { discussi
|
|
|
674
688
|
|
|
675
689
|
export async function listDiscussionTags(userId: string, classId: string) {
|
|
676
690
|
await getDiscussionClassAccess(classId, userId);
|
|
677
|
-
return prisma.
|
|
691
|
+
return prisma.classTag.findMany({ where: { classId }, orderBy: { name: "asc" } });
|
|
678
692
|
}
|
|
679
693
|
|
|
680
694
|
export async function createDiscussionTag(userId: string, input: { classId: string; name: string; color: string }) {
|
|
681
695
|
const isTeacher = await prisma.class.findFirst({ where: { id: input.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
|
|
682
696
|
if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
|
|
683
|
-
return prisma.
|
|
697
|
+
return prisma.classTag.create({ data: { classId: input.classId, name: input.name.trim(), color: input.color } });
|
|
684
698
|
}
|
|
685
699
|
|
|
686
700
|
export async function updateDiscussionTag(userId: string, input: { tagId: string; name?: string; color?: string }) {
|
|
687
|
-
const tag = await prisma.
|
|
701
|
+
const tag = await prisma.classTag.findUnique({ where: { id: input.tagId }, select: { classId: true } });
|
|
688
702
|
if (!tag) throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
|
|
689
703
|
const isTeacher = await prisma.class.findFirst({ where: { id: tag.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
|
|
690
704
|
if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
|
|
691
|
-
return prisma.
|
|
705
|
+
return prisma.classTag.update({ where: { id: input.tagId }, data: { name: input.name?.trim(), color: input.color } });
|
|
692
706
|
}
|
|
693
707
|
|
|
694
708
|
export async function deleteDiscussionTag(userId: string, tagId: string) {
|
|
695
|
-
const tag = await prisma.
|
|
709
|
+
const tag = await prisma.classTag.findUnique({ where: { id: tagId }, select: { classId: true } });
|
|
696
710
|
if (!tag) throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
|
|
697
711
|
const isTeacher = await prisma.class.findFirst({ where: { id: tag.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
|
|
698
712
|
if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
|
|
699
|
-
await prisma.
|
|
713
|
+
await prisma.classTag.delete({ where: { id: tagId } });
|
|
700
714
|
return { success: true };
|
|
701
715
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-powered discussion summary service with in-memory + Redis caching.
|
|
3
|
+
*/
|
|
4
|
+
import { prisma } from "../lib/prisma.js";
|
|
5
|
+
import { getRedis } from "../lib/redis.js";
|
|
6
|
+
import { inference } from "../utils/inference.js";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
import { TRPCError } from "@trpc/server";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { classMemberOrSchoolAdminWhere } from "../models/access.js";
|
|
11
|
+
|
|
12
|
+
const discussionSummarySchema = z.object({
|
|
13
|
+
summary: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe("A concise 2-4 sentence TL;DR of the entire discussion thread."),
|
|
16
|
+
keyQuestions: z
|
|
17
|
+
.array(z.string())
|
|
18
|
+
.describe("Up to 5 important student questions raised in the thread."),
|
|
19
|
+
unresolvedIssues: z
|
|
20
|
+
.array(z.string())
|
|
21
|
+
.describe(
|
|
22
|
+
"Up to 3 questions or topics that remain unanswered or unresolved.",
|
|
23
|
+
),
|
|
24
|
+
participantCount: z
|
|
25
|
+
.number()
|
|
26
|
+
.describe("Number of unique participants in the thread."),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type DiscussionSummary = z.infer<typeof discussionSummarySchema>;
|
|
30
|
+
|
|
31
|
+
const REDIS_PREFIX = "discussionSummary:";
|
|
32
|
+
const REDIS_TTL_SECONDS = 60 * 30; // 30 min
|
|
33
|
+
|
|
34
|
+
const memoryCache = new Map<
|
|
35
|
+
string,
|
|
36
|
+
{ value: DiscussionSummary; replyCount: number; cachedAt: number }
|
|
37
|
+
>();
|
|
38
|
+
const MEMORY_TTL_MS = 10 * 60 * 1000; // 10 min
|
|
39
|
+
|
|
40
|
+
function memoryCacheKey(discussionId: string) {
|
|
41
|
+
return discussionId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getMemoryCacheHit(
|
|
45
|
+
discussionId: string,
|
|
46
|
+
currentReplyCount: number,
|
|
47
|
+
): DiscussionSummary | undefined {
|
|
48
|
+
const entry = memoryCache.get(memoryCacheKey(discussionId));
|
|
49
|
+
if (!entry) return undefined;
|
|
50
|
+
if (Date.now() - entry.cachedAt > MEMORY_TTL_MS) {
|
|
51
|
+
memoryCache.delete(memoryCacheKey(discussionId));
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (entry.replyCount !== currentReplyCount) {
|
|
55
|
+
memoryCache.delete(memoryCacheKey(discussionId));
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return entry.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setMemoryCache(
|
|
62
|
+
discussionId: string,
|
|
63
|
+
value: DiscussionSummary,
|
|
64
|
+
replyCount: number,
|
|
65
|
+
) {
|
|
66
|
+
memoryCache.set(memoryCacheKey(discussionId), {
|
|
67
|
+
value,
|
|
68
|
+
replyCount,
|
|
69
|
+
cachedAt: Date.now(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function getRedisCacheHit(
|
|
74
|
+
discussionId: string,
|
|
75
|
+
currentReplyCount: number,
|
|
76
|
+
): Promise<DiscussionSummary | undefined> {
|
|
77
|
+
const redis = getRedis();
|
|
78
|
+
if (!redis) return undefined;
|
|
79
|
+
try {
|
|
80
|
+
const raw = await redis.get(`${REDIS_PREFIX}${discussionId}`);
|
|
81
|
+
if (!raw) return undefined;
|
|
82
|
+
const parsed = JSON.parse(raw) as {
|
|
83
|
+
value: DiscussionSummary;
|
|
84
|
+
replyCount: number;
|
|
85
|
+
};
|
|
86
|
+
if (parsed.replyCount !== currentReplyCount) return undefined;
|
|
87
|
+
return parsed.value;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function setRedisCache(
|
|
94
|
+
discussionId: string,
|
|
95
|
+
value: DiscussionSummary,
|
|
96
|
+
replyCount: number,
|
|
97
|
+
) {
|
|
98
|
+
const redis = getRedis();
|
|
99
|
+
if (!redis) return;
|
|
100
|
+
try {
|
|
101
|
+
await redis.set(
|
|
102
|
+
`${REDIS_PREFIX}${discussionId}`,
|
|
103
|
+
JSON.stringify({ value, replyCount }),
|
|
104
|
+
"EX",
|
|
105
|
+
REDIS_TTL_SECONDS,
|
|
106
|
+
);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.error("Failed to cache discussion summary in Redis", { err });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function getDiscussionSummary(
|
|
113
|
+
userId: string,
|
|
114
|
+
discussionId: string,
|
|
115
|
+
): Promise<DiscussionSummary> {
|
|
116
|
+
const discussion = await prisma.discussion.findUnique({
|
|
117
|
+
where: { id: discussionId },
|
|
118
|
+
select: {
|
|
119
|
+
id: true,
|
|
120
|
+
classId: true,
|
|
121
|
+
title: true,
|
|
122
|
+
body: true,
|
|
123
|
+
type: true,
|
|
124
|
+
replies: {
|
|
125
|
+
where: { deletedAt: null },
|
|
126
|
+
select: {
|
|
127
|
+
body: true,
|
|
128
|
+
author: {
|
|
129
|
+
select: {
|
|
130
|
+
username: true,
|
|
131
|
+
profile: { select: { displayName: true } },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
createdAt: true,
|
|
135
|
+
},
|
|
136
|
+
orderBy: { createdAt: "asc" },
|
|
137
|
+
},
|
|
138
|
+
author: {
|
|
139
|
+
select: {
|
|
140
|
+
username: true,
|
|
141
|
+
profile: { select: { displayName: true } },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!discussion) {
|
|
148
|
+
throw new TRPCError({
|
|
149
|
+
code: "NOT_FOUND",
|
|
150
|
+
message: "Discussion not found",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Verify user is a member of the class
|
|
155
|
+
const classMembership = await prisma.class.findFirst({
|
|
156
|
+
where: { id: discussion.classId, ...classMemberOrSchoolAdminWhere(userId) },
|
|
157
|
+
select: { id: true },
|
|
158
|
+
});
|
|
159
|
+
if (!classMembership) {
|
|
160
|
+
throw new TRPCError({
|
|
161
|
+
code: "UNAUTHORIZED",
|
|
162
|
+
message: "You are not a member of this class",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const replyCount = discussion.replies.length;
|
|
167
|
+
|
|
168
|
+
// Check memory cache
|
|
169
|
+
const memoryCached = getMemoryCacheHit(discussionId, replyCount);
|
|
170
|
+
if (memoryCached) return memoryCached;
|
|
171
|
+
|
|
172
|
+
// Check Redis cache
|
|
173
|
+
const redisCached = await getRedisCacheHit(discussionId, replyCount);
|
|
174
|
+
if (redisCached) {
|
|
175
|
+
setMemoryCache(discussionId, redisCached, replyCount);
|
|
176
|
+
return redisCached;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Build context for inference
|
|
180
|
+
const authorName =
|
|
181
|
+
discussion.author.profile?.displayName ?? discussion.author.username;
|
|
182
|
+
const threadText = [
|
|
183
|
+
`Title: ${discussion.title}`,
|
|
184
|
+
`Type: ${discussion.type}`,
|
|
185
|
+
`Author: ${authorName}`,
|
|
186
|
+
`Original post:\n${discussion.body}`,
|
|
187
|
+
"",
|
|
188
|
+
...discussion.replies.map((reply, i) => {
|
|
189
|
+
const name =
|
|
190
|
+
reply.author.profile?.displayName ?? reply.author.username;
|
|
191
|
+
return `Reply ${i + 1} by ${name}:\n${reply.body}`;
|
|
192
|
+
}),
|
|
193
|
+
].join("\n\n");
|
|
194
|
+
|
|
195
|
+
const messages = [
|
|
196
|
+
{
|
|
197
|
+
role: "system" as const,
|
|
198
|
+
content:
|
|
199
|
+
"You are a helpful assistant summarizing a class discussion thread for a teacher. Be concise, specific, and grounded in the actual content. Do not fabricate information.",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
role: "user" as const,
|
|
203
|
+
content: threadText,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
role: "user" as const,
|
|
207
|
+
content:
|
|
208
|
+
"Generate a structured summary of this discussion thread. Include a TL;DR, key student questions, and any unresolved issues.",
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const result = await inference<DiscussionSummary>(
|
|
213
|
+
messages,
|
|
214
|
+
discussionSummarySchema,
|
|
215
|
+
{
|
|
216
|
+
reason: "discussion_summary",
|
|
217
|
+
metadata: { discussionId, classId: discussion.classId },
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Cache result
|
|
222
|
+
setMemoryCache(discussionId, result, replyCount);
|
|
223
|
+
await setRedisCache(discussionId, result, replyCount);
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|