@studious-lms/server 1.14.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/.github/workflows/deploy.yml +1 -1
- package/dist/instrument.js +3 -3
- package/dist/instrument.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/models/announcement.d.ts +47 -45
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +75 -75
- package/dist/models/auth.d.ts +8 -0
- 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 +13 -13
- package/dist/models/comment.d.ts +6 -6
- package/dist/models/conversation.d.ts +23 -23
- package/dist/models/event.d.ts +20 -18
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +33 -9
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +4 -4
- package/dist/models/marketing.d.ts +2 -15
- 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 +13 -13
- package/dist/models/newtonChat.d.ts +3 -3
- package/dist/models/notification.d.ts +4 -4
- package/dist/models/worksheet.d.ts +121 -121
- package/dist/pipelines/aiLabChat.d.ts +7 -1
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +153 -67
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +34 -195
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +9 -19
- package/dist/pipelines/aiLabChatContract.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +6 -6
- package/dist/pipelines/labChatPrompt.d.ts +8 -3
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +52 -14
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
- package/dist/routers/_app.d.ts +750 -546
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +40 -40
- package/dist/routers/assignment.d.ts +131 -121
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/auth.d.ts +2 -0
- 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 +12 -8
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +5 -5
- package/dist/routers/conversation.d.ts +6 -6
- package/dist/routers/discussion.d.ts +3 -3
- package/dist/routers/embedding.d.ts +6 -2
- package/dist/routers/embedding.d.ts.map +1 -1
- package/dist/routers/embedding.js +44 -36
- package/dist/routers/embedding.js.map +1 -1
- package/dist/routers/event.d.ts +20 -18
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +2 -2
- package/dist/routers/labChat.d.ts +3 -3
- package/dist/routers/marketing.d.ts +0 -13
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/school.d.ts +4 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/search.d.ts +2 -2
- package/dist/routers/user.d.ts +89 -0
- 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 +44 -44
- package/dist/routes/aiStream.d.ts.map +1 -1
- package/dist/routes/aiStream.js +41 -11
- package/dist/routes/aiStream.js.map +1 -1
- package/dist/seedDatabase.d.ts +3 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/services/aiChatStream.d.ts +20 -0
- package/dist/services/aiChatStream.d.ts.map +1 -1
- package/dist/services/aiChatStream.js +110 -13
- package/dist/services/aiChatStream.js.map +1 -1
- package/dist/services/aiModifierTools.d.ts +0 -16
- package/dist/services/aiModifierTools.d.ts.map +1 -1
- package/dist/services/aiModifierTools.js +112 -3
- package/dist/services/aiModifierTools.js.map +1 -1
- package/dist/services/aiProposalTools.d.ts.map +1 -1
- package/dist/services/aiProposalTools.js +25 -25
- package/dist/services/aiProposalTools.js.map +1 -1
- package/dist/services/aiTools.js +3 -3
- package/dist/services/aiTools.js.map +1 -1
- package/dist/services/announcement.d.ts +34 -34
- package/dist/services/assignment.d.ts +121 -111
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +4 -2
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/auth.d.ts +2 -0
- 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 +10 -6
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +7 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +5 -5
- package/dist/services/conversation.d.ts +6 -6
- package/dist/services/discussion.d.ts.map +1 -1
- package/dist/services/discussion.js +9 -2
- package/dist/services/discussion.js.map +1 -1
- package/dist/services/embedding.d.ts +19 -1
- package/dist/services/embedding.d.ts.map +1 -1
- package/dist/services/embedding.js +154 -8
- package/dist/services/embedding.js.map +1 -1
- package/dist/services/event.d.ts +20 -18
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +4 -4
- 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 +3 -3
- package/dist/services/marketing.d.ts +1 -14
- 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/notification.d.ts +4 -4
- 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/school.d.ts +4 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/search.d.ts +1 -1
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +27 -2
- package/dist/services/search.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 +66 -49
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +48 -3
- 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/workers/processors/embedContent.d.ts.map +1 -1
- package/dist/workers/processors/embedContent.js +16 -25
- package/dist/workers/processors/embedContent.js.map +1 -1
- package/package.json +1 -1
- 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 +6 -0
- package/src/instrument.ts +1 -1
- package/src/lib/inferDocumentFormat.ts +58 -0
- package/src/lib/legal.ts +2 -0
- package/src/models/auth.ts +4 -0
- package/src/models/marketing.ts +10 -2
- package/src/pipelines/aiLabChat.ts +198 -74
- package/src/pipelines/aiLabChatContract.ts +7 -17
- package/src/pipelines/labChatPrompt.ts +64 -14
- package/src/routers/auth.ts +13 -0
- package/src/routers/embedding.ts +44 -39
- package/src/routers/user.ts +26 -0
- package/src/routes/aiStream.ts +47 -9
- package/src/services/aiChatStream.ts +133 -11
- package/src/services/aiModifierTools.ts +154 -1
- package/src/services/aiProposalTools.ts +27 -25
- package/src/services/aiTools.ts +1 -1
- package/src/services/assignment.ts +3 -0
- package/src/services/auth.ts +14 -2
- package/src/services/class.ts +7 -1
- package/src/services/discussion.ts +13 -0
- package/src/services/embedding.ts +167 -6
- package/src/services/folder.ts +31 -0
- package/src/services/marketing.ts +3 -3
- package/src/services/openaiDocumentGen.ts +319 -0
- package/src/services/search.ts +25 -1
- package/src/services/userPrivacy.ts +196 -0
- package/src/services/worksheet.ts +68 -1
- package/src/utils/email.ts +5 -1
- package/src/workers/processors/embedContent.ts +18 -22
- 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/services/aiChatStream.test.ts +92 -0
- package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
|
@@ -23,7 +23,6 @@ import type {
|
|
|
23
23
|
SectionToCreate,
|
|
24
24
|
DocToCreate,
|
|
25
25
|
} from '../pipelines/aiLabChatContract.js';
|
|
26
|
-
import type { DocumentBlock } from '../lib/jsonStyles.js';
|
|
27
26
|
|
|
28
27
|
export interface LabProposals {
|
|
29
28
|
assignments: AssignmentToCreate[];
|
|
@@ -85,7 +84,8 @@ interface ProposeAssignmentArgs {
|
|
|
85
84
|
|
|
86
85
|
interface GenerateDocumentArgs {
|
|
87
86
|
title: string;
|
|
88
|
-
|
|
87
|
+
content: string;
|
|
88
|
+
format?: string;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -237,35 +237,37 @@ export function labProposalTools(proposals: LabProposals): ToolSet {
|
|
|
237
237
|
|
|
238
238
|
generate_document: {
|
|
239
239
|
description:
|
|
240
|
-
'Generate a
|
|
240
|
+
'Generate a downloadable file and attach it to this message. You are NOT limited to PDF — pick the best format for the request (pptx for slides/presentations, xlsx/csv for data, docx for editable handouts, pdf for print-ready docs, png for diagrams, etc.). Always set format explicitly.',
|
|
241
241
|
inputSchema: jsonSchema({
|
|
242
242
|
type: 'object',
|
|
243
243
|
properties: {
|
|
244
|
-
title: { type: 'string' },
|
|
245
|
-
|
|
246
|
-
type: '
|
|
247
|
-
description:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
},
|
|
260
|
-
required: ['format', 'content'],
|
|
261
|
-
},
|
|
244
|
+
title: { type: 'string', description: 'Document title' },
|
|
245
|
+
content: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
description:
|
|
248
|
+
'Full body — markdown or plain text with headings, lists, tables, slide notes, etc.',
|
|
249
|
+
},
|
|
250
|
+
format: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description:
|
|
253
|
+
'Output file type. REQUIRED. pptx=slides/presentation, xlsx/csv=spreadsheet/data, docx=Word handout, pdf=printable doc, md/txt/html=text, png/jpeg/gif=image, py/js/ts=code.',
|
|
254
|
+
enum: [
|
|
255
|
+
'pdf', 'docx', 'doc', 'xlsx', 'pptx', 'csv', 'md', 'html', 'txt',
|
|
256
|
+
'json', 'xml', 'tex', 'png', 'jpeg', 'jpg', 'gif', 'zip', 'tar',
|
|
257
|
+
'py', 'js', 'ts', 'css', 'sh',
|
|
258
|
+
],
|
|
262
259
|
},
|
|
263
260
|
},
|
|
264
|
-
required: ['title', '
|
|
261
|
+
required: ['title', 'content', 'format'],
|
|
265
262
|
}),
|
|
266
|
-
execute: async ({ title,
|
|
267
|
-
proposals.docs.push({
|
|
268
|
-
|
|
263
|
+
execute: async ({ title, content, format }: GenerateDocumentArgs) => {
|
|
264
|
+
proposals.docs.push({
|
|
265
|
+
title,
|
|
266
|
+
content: content.trim(),
|
|
267
|
+
format: (format ?? 'pdf') as DocToCreate['format'],
|
|
268
|
+
});
|
|
269
|
+
const ext = (format ?? 'pdf').toUpperCase();
|
|
270
|
+
return `Document "${title}" queued for ${ext} generation.`;
|
|
269
271
|
},
|
|
270
272
|
},
|
|
271
273
|
};
|
package/src/services/aiTools.ts
CHANGED
|
@@ -251,7 +251,7 @@ export function aiTools(classId: string): ToolSet {
|
|
|
251
251
|
const client = new OpenAI();
|
|
252
252
|
const response = await client.responses.create({
|
|
253
253
|
model: 'gpt-4o-mini',
|
|
254
|
-
tools: [{ type: '
|
|
254
|
+
tools: [{ type: 'web_search' }],
|
|
255
255
|
input: query,
|
|
256
256
|
});
|
|
257
257
|
|
package/src/services/auth.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { sendMail } from "../utils/email.js";
|
|
|
10
10
|
import { prismaWrapper } from "../utils/prismaWrapper.js";
|
|
11
11
|
import { env } from "../lib/config/env.js";
|
|
12
12
|
import { logger } from "../utils/logger.js";
|
|
13
|
+
import { CURRENT_TERMS_VERSION } from "../lib/legal.js";
|
|
13
14
|
import {
|
|
14
15
|
findUserByUsername,
|
|
15
16
|
findUserByUsernameOrEmail,
|
|
@@ -32,8 +33,17 @@ export async function register(input: {
|
|
|
32
33
|
username: string;
|
|
33
34
|
email: string;
|
|
34
35
|
password: string;
|
|
36
|
+
agreeToTerms: true;
|
|
37
|
+
termsVersion: string;
|
|
35
38
|
}) {
|
|
36
|
-
const { username, email, password } = input;
|
|
39
|
+
const { username, email, password, termsVersion } = input;
|
|
40
|
+
|
|
41
|
+
if (termsVersion !== CURRENT_TERMS_VERSION) {
|
|
42
|
+
throw new TRPCError({
|
|
43
|
+
code: "BAD_REQUEST",
|
|
44
|
+
message: "Please accept the current Terms of Service and Privacy Policy.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
37
47
|
|
|
38
48
|
const existingUser = await prismaWrapper.findFirst(
|
|
39
49
|
() => findUserByUsernameOrEmail(username, email),
|
|
@@ -70,7 +80,9 @@ export async function register(input: {
|
|
|
70
80
|
username,
|
|
71
81
|
email,
|
|
72
82
|
password: await hash(password, 10),
|
|
73
|
-
verified:
|
|
83
|
+
verified: false,
|
|
84
|
+
termsAcceptedAt: new Date(),
|
|
85
|
+
termsVersion,
|
|
74
86
|
}),
|
|
75
87
|
"creating new user during registration"
|
|
76
88
|
);
|
package/src/services/class.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SessionType } from "@prisma/client";
|
|
|
6
6
|
import { TRPCError } from "@trpc/server";
|
|
7
7
|
import { prisma } from "../lib/prisma.js";
|
|
8
8
|
import { generateInviteCode } from "../utils/generateInviteCode.js";
|
|
9
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
9
10
|
import {
|
|
10
11
|
findTeacherClasses,
|
|
11
12
|
findStudentClasses,
|
|
@@ -626,10 +627,15 @@ export async function updateSyllabus(classId: string, contents: string) {
|
|
|
626
627
|
message: "Missing key contents",
|
|
627
628
|
});
|
|
628
629
|
}
|
|
629
|
-
|
|
630
|
+
const updated = await prisma.class.update({
|
|
630
631
|
where: { id: classId },
|
|
631
632
|
data: { syllabus: contents },
|
|
632
633
|
});
|
|
634
|
+
|
|
635
|
+
// Index syllabus for RAG search
|
|
636
|
+
enqueueEmbedding(classId, 'SYLLABUS', classId);
|
|
637
|
+
|
|
638
|
+
return updated;
|
|
633
639
|
}
|
|
634
640
|
|
|
635
641
|
export async function listLabDrafts(classId: string, teacherId: string) {
|
|
@@ -6,6 +6,7 @@ import { discussionChannel, pusher } from "../lib/pusher.js";
|
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
7
|
import { classMemberOrSchoolAdminWhere, classTeacherOrSchoolAdminWhere } from "../models/access.js";
|
|
8
8
|
import { sendNotification } from "./notification.js";
|
|
9
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
9
10
|
|
|
10
11
|
const authorSelect = {
|
|
11
12
|
id: true,
|
|
@@ -441,6 +442,10 @@ export async function createDiscussion(userId: string, input: {
|
|
|
441
442
|
|
|
442
443
|
triggerDiscussionEvent(input.classId, "discussion-created", { discussion: serializeDiscussion(discussion, userId) });
|
|
443
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
|
+
|
|
444
449
|
return { discussion: serializeDiscussion(discussion, userId) };
|
|
445
450
|
}
|
|
446
451
|
|
|
@@ -471,6 +476,10 @@ export async function updateDiscussion(userId: string, input: {
|
|
|
471
476
|
include: discussionInclude,
|
|
472
477
|
});
|
|
473
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
|
+
|
|
474
483
|
return { discussion: serializeDiscussion(updated, userId) };
|
|
475
484
|
}
|
|
476
485
|
|
|
@@ -526,6 +535,10 @@ export async function replyToDiscussion(userId: string, input: {
|
|
|
526
535
|
notifyMentionedUsers(discussion.classId, userId, input.body, link),
|
|
527
536
|
]);
|
|
528
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
|
+
|
|
529
542
|
return { reply: serializeReply(reply, userId) };
|
|
530
543
|
}
|
|
531
544
|
|
|
@@ -193,28 +193,38 @@ export async function removeEmbeddings(params: {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* Index an assignment (title + instructions) into the vector store.
|
|
196
|
+
* Index an assignment (title + instructions + attached file names) into the vector store.
|
|
197
197
|
*/
|
|
198
198
|
export async function indexAssignment(classId: string, assignmentId: string): Promise<void> {
|
|
199
199
|
const assignment = await prisma.assignment.findUnique({
|
|
200
200
|
where: { id: assignmentId },
|
|
201
|
-
select: {
|
|
201
|
+
select: {
|
|
202
|
+
title: true,
|
|
203
|
+
instructions: true,
|
|
204
|
+
type: true,
|
|
205
|
+
attachments: { select: { name: true, type: true } },
|
|
206
|
+
},
|
|
202
207
|
});
|
|
203
208
|
|
|
204
209
|
if (!assignment) return;
|
|
205
210
|
|
|
206
|
-
const
|
|
211
|
+
const parts = [
|
|
207
212
|
`Title: ${assignment.title}`,
|
|
208
213
|
`Type: ${assignment.type}`,
|
|
209
214
|
assignment.instructions ? `Instructions: ${assignment.instructions}` : '',
|
|
210
|
-
]
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
if (assignment.attachments.length > 0) {
|
|
218
|
+
const fileList = assignment.attachments.map((f) => `${f.name} (${f.type})`).join(', ');
|
|
219
|
+
parts.push(`Attached files: ${fileList}`);
|
|
220
|
+
}
|
|
211
221
|
|
|
212
222
|
await indexContent({
|
|
213
223
|
classId,
|
|
214
224
|
sourceType: 'ASSIGNMENT',
|
|
215
225
|
sourceId: assignmentId,
|
|
216
|
-
text,
|
|
217
|
-
metadata: { title: assignment.title, type: assignment.type },
|
|
226
|
+
text: parts.filter(Boolean).join('\n'),
|
|
227
|
+
metadata: { title: assignment.title, type: assignment.type, fileCount: assignment.attachments.length },
|
|
218
228
|
});
|
|
219
229
|
}
|
|
220
230
|
|
|
@@ -269,6 +279,157 @@ export async function indexAnnouncement(classId: string, announcementId: string)
|
|
|
269
279
|
});
|
|
270
280
|
}
|
|
271
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Index a discussion (title + body + replies) into the vector store.
|
|
284
|
+
*/
|
|
285
|
+
export async function indexDiscussion(classId: string, discussionId: string): Promise<void> {
|
|
286
|
+
const discussion = await prisma.discussion.findUnique({
|
|
287
|
+
where: { id: discussionId },
|
|
288
|
+
select: {
|
|
289
|
+
title: true,
|
|
290
|
+
body: true,
|
|
291
|
+
type: true,
|
|
292
|
+
author: { select: { username: true, profile: { select: { displayName: true } } } },
|
|
293
|
+
replies: {
|
|
294
|
+
select: { body: true, author: { select: { username: true, profile: { select: { displayName: true } } } } },
|
|
295
|
+
orderBy: { createdAt: 'asc' },
|
|
296
|
+
take: 20,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (!discussion) return;
|
|
302
|
+
|
|
303
|
+
const authorName = discussion.author.profile?.displayName || discussion.author.username;
|
|
304
|
+
const parts = [
|
|
305
|
+
`Discussion: ${discussion.title}`,
|
|
306
|
+
`Type: ${discussion.type}`,
|
|
307
|
+
`Author: ${authorName}`,
|
|
308
|
+
`Body: ${discussion.body}`,
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
if (discussion.replies.length > 0) {
|
|
312
|
+
parts.push('Replies:');
|
|
313
|
+
for (const reply of discussion.replies) {
|
|
314
|
+
const replyAuthor = reply.author.profile?.displayName || reply.author.username;
|
|
315
|
+
parts.push(` ${replyAuthor}: ${reply.body.slice(0, 300)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
await indexContent({
|
|
320
|
+
classId,
|
|
321
|
+
sourceType: 'DISCUSSION',
|
|
322
|
+
sourceId: discussionId,
|
|
323
|
+
text: parts.join('\n'),
|
|
324
|
+
metadata: { title: discussion.title, type: discussion.type, replyCount: discussion.replies.length },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Index a submission (grade + feedback + file attachments + extended response) into the vector store.
|
|
330
|
+
*/
|
|
331
|
+
export async function indexSubmission(classId: string, submissionId: string): Promise<void> {
|
|
332
|
+
const submission = await prisma.submission.findUnique({
|
|
333
|
+
where: { id: submissionId },
|
|
334
|
+
select: {
|
|
335
|
+
gradeReceived: true,
|
|
336
|
+
teacherComments: true,
|
|
337
|
+
extendedResponse: true,
|
|
338
|
+
returned: true,
|
|
339
|
+
student: { select: { username: true, profile: { select: { displayName: true } } } },
|
|
340
|
+
assignment: { select: { title: true, maxGrade: true, type: true } },
|
|
341
|
+
attachments: { select: { name: true, type: true } },
|
|
342
|
+
annotations: { select: { name: true, type: true } },
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (!submission) return;
|
|
347
|
+
|
|
348
|
+
const studentName = submission.student.profile?.displayName || submission.student.username;
|
|
349
|
+
const parts = [
|
|
350
|
+
`Submission for: ${submission.assignment.title}`,
|
|
351
|
+
`Student: ${studentName}`,
|
|
352
|
+
`Assignment type: ${submission.assignment.type}`,
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
if (submission.gradeReceived != null) {
|
|
356
|
+
parts.push(`Grade: ${submission.gradeReceived}/${submission.assignment.maxGrade ?? '?'}`);
|
|
357
|
+
}
|
|
358
|
+
if (submission.teacherComments) {
|
|
359
|
+
parts.push(`Teacher feedback: ${submission.teacherComments}`);
|
|
360
|
+
}
|
|
361
|
+
if (submission.extendedResponse) {
|
|
362
|
+
parts.push(`Student response: ${submission.extendedResponse.slice(0, 500)}`);
|
|
363
|
+
}
|
|
364
|
+
if (submission.attachments.length > 0) {
|
|
365
|
+
parts.push(`Submitted files: ${submission.attachments.map((f) => f.name).join(', ')}`);
|
|
366
|
+
}
|
|
367
|
+
if (submission.annotations.length > 0) {
|
|
368
|
+
parts.push(`Teacher annotations: ${submission.annotations.map((f) => f.name).join(', ')}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await indexContent({
|
|
372
|
+
classId,
|
|
373
|
+
sourceType: 'SUBMISSION',
|
|
374
|
+
sourceId: submissionId,
|
|
375
|
+
text: parts.join('\n'),
|
|
376
|
+
metadata: { studentName, assignmentTitle: submission.assignment.title, grade: submission.gradeReceived },
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Index a class syllabus into the vector store.
|
|
382
|
+
* Uses classId as both classId and sourceId since syllabus is a field on the class.
|
|
383
|
+
*/
|
|
384
|
+
export async function indexSyllabus(classId: string): Promise<void> {
|
|
385
|
+
const cls = await prisma.class.findUnique({
|
|
386
|
+
where: { id: classId },
|
|
387
|
+
select: { name: true, subject: true, syllabus: true },
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (!cls?.syllabus) return;
|
|
391
|
+
|
|
392
|
+
const text = [
|
|
393
|
+
`Syllabus for ${cls.name} (${cls.subject})`,
|
|
394
|
+
cls.syllabus,
|
|
395
|
+
].join('\n\n');
|
|
396
|
+
|
|
397
|
+
await indexContent({
|
|
398
|
+
classId,
|
|
399
|
+
sourceType: 'SYLLABUS',
|
|
400
|
+
sourceId: classId,
|
|
401
|
+
text,
|
|
402
|
+
metadata: { className: cls.name, subject: cls.subject },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Index a file (name + type + path) into the vector store.
|
|
408
|
+
* For text-based content, PDF extraction would be added here in the future.
|
|
409
|
+
*/
|
|
410
|
+
export async function indexFile(classId: string, fileId: string): Promise<void> {
|
|
411
|
+
const file = await prisma.file.findUnique({
|
|
412
|
+
where: { id: fileId },
|
|
413
|
+
select: { name: true, path: true, type: true, size: true },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (!file) return;
|
|
417
|
+
|
|
418
|
+
const text = [
|
|
419
|
+
`File: ${file.name}`,
|
|
420
|
+
`Type: ${file.type}`,
|
|
421
|
+
`Path: ${file.path}`,
|
|
422
|
+
].join('\n');
|
|
423
|
+
|
|
424
|
+
await indexContent({
|
|
425
|
+
classId,
|
|
426
|
+
sourceType: 'FILE',
|
|
427
|
+
sourceId: fileId,
|
|
428
|
+
text,
|
|
429
|
+
metadata: { name: file.name, type: file.type, size: file.size },
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
272
433
|
/**
|
|
273
434
|
* Fire-and-forget: enqueue a background job to embed content.
|
|
274
435
|
* Safe to call from service functions after a create/update.
|
package/src/services/folder.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type DirectFileData,
|
|
29
29
|
confirmDirectUpload,
|
|
30
30
|
} from "../lib/fileUpload.js";
|
|
31
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
31
32
|
import {
|
|
32
33
|
type AccessLevel,
|
|
33
34
|
getScopeDefault,
|
|
@@ -421,9 +422,39 @@ export async function confirmFolderUpload(
|
|
|
421
422
|
input.uploadSuccess,
|
|
422
423
|
input.errorMessage
|
|
423
424
|
);
|
|
425
|
+
|
|
426
|
+
// Index file for RAG search if it belongs to a class folder
|
|
427
|
+
if (input.uploadSuccess) {
|
|
428
|
+
const file = await prisma.file.findUnique({
|
|
429
|
+
where: { id: input.fileId },
|
|
430
|
+
select: { folderId: true },
|
|
431
|
+
});
|
|
432
|
+
if (file?.folderId) {
|
|
433
|
+
const classId = await resolveFileClassId(file.folderId);
|
|
434
|
+
if (classId) {
|
|
435
|
+
enqueueEmbedding(classId, 'FILE', input.fileId);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
424
440
|
return { success: true };
|
|
425
441
|
}
|
|
426
442
|
|
|
443
|
+
/** Walk up folder tree to find classId, if any. */
|
|
444
|
+
async function resolveFileClassId(folderId: string): Promise<string | null> {
|
|
445
|
+
let currentId: string | null = folderId;
|
|
446
|
+
while (currentId) {
|
|
447
|
+
const folder = await prisma.folder.findUnique({
|
|
448
|
+
where: { id: currentId },
|
|
449
|
+
select: { classId: true, parentFolderId: true },
|
|
450
|
+
});
|
|
451
|
+
if (!folder) return null;
|
|
452
|
+
if (folder.classId) return folder.classId;
|
|
453
|
+
currentId = folder.parentFolderId;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
427
458
|
/** What scopes can this user pick from in cloud-storage UIs? */
|
|
428
459
|
export async function listUserScopes(userId: string) {
|
|
429
460
|
const memberships = await prisma.schoolMembership.findMany({
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { v4 } from "uuid";
|
|
5
5
|
import {
|
|
6
6
|
createSchoolDevelopmentProgram,
|
|
7
|
-
|
|
7
|
+
findSchoolDevelopmentProgramStatusById,
|
|
8
8
|
createEarlyAccessRequest,
|
|
9
9
|
} from "../models/marketing.js";
|
|
10
10
|
|
|
@@ -42,9 +42,9 @@ export async function createSchoolDevelopmentProgramRecord(input: {
|
|
|
42
42
|
return { id: program.id };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** Public application status lookup (no contact PII). */
|
|
46
46
|
export async function searchSchoolDevelopmentPrograms(id: string) {
|
|
47
|
-
return
|
|
47
|
+
return findSchoolDevelopmentProgramStatusById(id);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** Create an early access request. */
|