@studious-lms/server 1.2.46 → 1.2.48

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.
Files changed (75) hide show
  1. package/dist/index.js +22 -18
  2. package/dist/index.js.map +1 -1
  3. package/dist/middleware/auth.d.ts.map +1 -1
  4. package/dist/middleware/auth.js +3 -2
  5. package/dist/middleware/auth.js.map +1 -1
  6. package/dist/middleware/security.d.ts.map +1 -1
  7. package/dist/middleware/security.js +4 -4
  8. package/dist/middleware/security.js.map +1 -1
  9. package/dist/routers/_app.d.ts +126 -26
  10. package/dist/routers/_app.d.ts.map +1 -1
  11. package/dist/routers/assignment.d.ts +10 -0
  12. package/dist/routers/assignment.d.ts.map +1 -1
  13. package/dist/routers/assignment.js +18 -3
  14. package/dist/routers/assignment.js.map +1 -1
  15. package/dist/routers/class.d.ts +18 -0
  16. package/dist/routers/class.d.ts.map +1 -1
  17. package/dist/routers/class.js +56 -2
  18. package/dist/routers/class.js.map +1 -1
  19. package/dist/routers/conversation.d.ts +1 -0
  20. package/dist/routers/conversation.d.ts.map +1 -1
  21. package/dist/routers/labChat.d.ts +1 -0
  22. package/dist/routers/labChat.d.ts.map +1 -1
  23. package/dist/routers/labChat.js +5 -321
  24. package/dist/routers/labChat.js.map +1 -1
  25. package/dist/routers/message.d.ts +1 -0
  26. package/dist/routers/message.d.ts.map +1 -1
  27. package/dist/routers/message.js +3 -2
  28. package/dist/routers/message.js.map +1 -1
  29. package/dist/routers/newtonChat.d.ts.map +1 -1
  30. package/dist/routers/newtonChat.js +6 -183
  31. package/dist/routers/newtonChat.js.map +1 -1
  32. package/dist/routers/section.d.ts +10 -0
  33. package/dist/routers/section.d.ts.map +1 -1
  34. package/dist/routers/section.js +21 -3
  35. package/dist/routers/section.js.map +1 -1
  36. package/dist/routers/worksheet.d.ts +22 -13
  37. package/dist/routers/worksheet.d.ts.map +1 -1
  38. package/dist/routers/worksheet.js +16 -3
  39. package/dist/routers/worksheet.js.map +1 -1
  40. package/dist/seedDatabase.d.ts.map +1 -1
  41. package/dist/seedDatabase.js +34 -8
  42. package/dist/seedDatabase.js.map +1 -1
  43. package/dist/server/pipelines/aiLabChat.d.ts +12 -1
  44. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -1
  45. package/dist/server/pipelines/aiLabChat.js +388 -15
  46. package/dist/server/pipelines/aiLabChat.js.map +1 -1
  47. package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
  48. package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
  49. package/dist/server/pipelines/aiNewtonChat.js +280 -0
  50. package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
  51. package/dist/server/pipelines/gradeWorksheet.d.ts +14 -1
  52. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -1
  53. package/dist/server/pipelines/gradeWorksheet.js +6 -5
  54. package/dist/server/pipelines/gradeWorksheet.js.map +1 -1
  55. package/dist/utils/inference.d.ts +3 -1
  56. package/dist/utils/inference.d.ts.map +1 -1
  57. package/dist/utils/inference.js +34 -4
  58. package/dist/utils/inference.js.map +1 -1
  59. package/package.json +1 -1
  60. package/prisma/schema.prisma +2 -0
  61. package/src/index.ts +24 -22
  62. package/src/middleware/auth.ts +1 -0
  63. package/src/middleware/security.ts +2 -2
  64. package/src/routers/assignment.ts +17 -2
  65. package/src/routers/class.ts +55 -0
  66. package/src/routers/labChat.ts +3 -366
  67. package/src/routers/message.ts +1 -1
  68. package/src/routers/newtonChat.ts +8 -231
  69. package/src/routers/section.ts +21 -1
  70. package/src/routers/worksheet.ts +17 -1
  71. package/src/seedDatabase.ts +38 -6
  72. package/src/server/pipelines/aiLabChat.ts +434 -19
  73. package/src/server/pipelines/aiNewtonChat.ts +338 -0
  74. package/src/server/pipelines/gradeWorksheet.ts +3 -4
  75. package/src/utils/inference.ts +40 -5
@@ -3,13 +3,9 @@ import { createTRPCRouter, protectedProcedure } from '../trpc.js';
3
3
  import { prisma } from '../lib/prisma.js';
4
4
  import { pusher } from '../lib/pusher.js';
5
5
  import { TRPCError } from '@trpc/server';
6
- import {
7
- inferenceClient,
8
- openAIClient,
9
- sendAIMessage,
10
- } from '../utils/inference.js';
11
6
  import { logger } from '../utils/logger.js';
12
7
  import { isAIUser } from '../utils/aiUser.js';
8
+ import { generateAndSendNewtonIntroduction, generateAndSendNewtonResponse } from '../server/pipelines/aiNewtonChat.js';
13
9
 
14
10
  export const newtonChatRouter = createTRPCRouter({
15
11
  getTutorConversation: protectedProcedure
@@ -124,19 +120,17 @@ export const newtonChatRouter = createTRPCRouter({
124
120
  title: 'Session with Newton Tutor',
125
121
  },
126
122
  });
123
+ generateAndSendNewtonIntroduction(
124
+ newtonChat.id,
125
+ newtonChat.conversationId,
126
+ submission.id
127
+ ).catch(error => {
128
+ logger.error('Failed to generate AI introduction:', { error, newtonChatId: result.id });
129
+ });
127
130
 
128
131
  return newtonChat;
129
132
  });
130
133
 
131
- // Generate AI introduction message in parallel (don't await - fire and forget)
132
- generateAndSendNewtonIntroduction(
133
- result.id,
134
- result.conversationId,
135
- submission.id
136
- ).catch(error => {
137
- logger.error('Failed to generate AI introduction:', { error, newtonChatId: result.id });
138
- });
139
-
140
134
  return {
141
135
  conversationId: result.conversationId,
142
136
  newtonChatId: result.id,
@@ -300,221 +294,4 @@ export const newtonChatRouter = createTRPCRouter({
300
294
  }),
301
295
  });
302
296
 
303
- /**
304
- * Generate and send AI introduction for Newton chat
305
- */
306
- async function generateAndSendNewtonIntroduction(
307
- newtonChatId: string,
308
- conversationId: string,
309
- submissionId: string
310
- ): Promise<void> {
311
- try {
312
- // Get submission details for context
313
- const submission = await prisma.submission.findUnique({
314
- where: { id: submissionId },
315
- include: {
316
- assignment: {
317
- select: {
318
- title: true,
319
- instructions: true,
320
- class: {
321
- select: {
322
- subject: true,
323
- name: true,
324
- },
325
- },
326
- },
327
- },
328
- attachments: {
329
- select: {
330
- id: true,
331
- name: true,
332
- type: true,
333
- },
334
- },
335
- },
336
- });
337
-
338
- if (!submission) {
339
- throw new Error('Submission not found');
340
- }
341
-
342
- const systemPrompt = `You are Newton, an AI tutor helping a student with their assignment submission.
343
-
344
- Assignment: ${submission.assignment.title}
345
- Subject: ${submission.assignment.class.subject}
346
- Instructions: ${submission.assignment.instructions || 'No specific instructions provided'}
347
-
348
- Your role:
349
- - Help the student understand concepts related to their assignment
350
- - Provide guidance and explanations without giving away direct answers
351
- - Encourage learning and critical thinking
352
- - Be supportive and encouraging
353
- - Use clear, educational language appropriate for the subject
354
-
355
- Do not use markdown formatting in your responses - use plain text only.`;
356
-
357
- const completion = await inferenceClient.chat.completions.create({
358
- model: 'command-a-03-2025',
359
- messages: [
360
- { role: 'system', content: systemPrompt },
361
- {
362
- role: 'user',
363
- content: 'Please introduce yourself to the student. Explain that you are Newton, their AI tutor, and you are here to help them with their assignment. Ask them what they would like help with.'
364
- },
365
- ],
366
- max_tokens: 300,
367
- temperature: 0.8,
368
- });
369
-
370
- const response = completion.choices[0]?.message?.content;
371
-
372
- if (!response) {
373
- throw new Error('No response generated from inference API');
374
- }
375
-
376
- // Send AI introduction using centralized sender
377
- await sendAIMessage(response, conversationId, {
378
- subject: submission.assignment.class.subject || 'Assignment',
379
- });
380
-
381
- logger.info('AI Introduction sent', { newtonChatId, conversationId });
382
-
383
- } catch (error) {
384
- logger.error('Failed to generate AI introduction:', { error, newtonChatId });
385
-
386
- // Send fallback introduction
387
- try {
388
- const fallbackIntro = `Hello! I'm Newton, your AI tutor. I'm here to help you with your assignment. I can answer questions, explain concepts, and guide you through your work. What would you like help with today?`;
389
-
390
- await sendAIMessage(fallbackIntro, conversationId, {
391
- subject: 'Assignment',
392
- });
393
-
394
- logger.info('Fallback AI introduction sent', { newtonChatId });
395
-
396
- } catch (fallbackError) {
397
- logger.error('Failed to send fallback AI introduction:', { error: fallbackError, newtonChatId });
398
- }
399
- }
400
- }
401
-
402
- /**
403
- * Generate and send AI response to student message
404
- */
405
- async function generateAndSendNewtonResponse(
406
- newtonChatId: string,
407
- studentMessage: string,
408
- conversationId: string,
409
- submission: {
410
- id: string;
411
- assignment: {
412
- id: string;
413
- title: string;
414
- instructions: string | null;
415
- class: {
416
- subject: string | null;
417
- };
418
- };
419
- }
420
- ): Promise<void> {
421
- try {
422
- // Get recent conversation history
423
- const recentMessages = await prisma.message.findMany({
424
- where: {
425
- conversationId,
426
- },
427
- include: {
428
- sender: {
429
- select: {
430
- id: true,
431
- username: true,
432
- profile: {
433
- select: {
434
- displayName: true,
435
- },
436
- },
437
- },
438
- },
439
- },
440
- orderBy: {
441
- createdAt: 'desc',
442
- },
443
- take: 10, // Last 10 messages for context
444
- });
445
-
446
- const systemPrompt = `You are Newton, an AI tutor helping a student with their assignment submission.
447
-
448
- Assignment: ${submission.assignment.title}
449
- Subject: ${submission.assignment.class.subject || 'General'}
450
- Instructions: ${submission.assignment.instructions || 'No specific instructions provided'}
451
-
452
- Your role:
453
- - Help the student understand concepts related to their assignment
454
- - Provide guidance and explanations without giving away direct answers
455
- - Encourage learning and critical thinking
456
- - Be supportive and encouraging
457
- - Use clear, educational language appropriate for the subject
458
- - If the student asks for direct answers, guide them to think through the problem instead
459
- - Break down complex concepts into simpler parts
460
- - Use examples and analogies when helpful
461
-
462
- IMPORTANT:
463
- - Do not use markdown formatting in your responses - use plain text only
464
- - Keep responses conversational and educational
465
- - Focus on helping the student learn, not just completing the assignment`;
466
-
467
- const messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> = [
468
- { role: 'system', content: systemPrompt },
469
- ];
470
-
471
- // Add recent conversation history
472
- recentMessages.reverse().forEach(msg => {
473
- const role = isAIUser(msg.senderId) ? 'assistant' : 'user';
474
- const senderName = msg.sender?.profile?.displayName || msg.sender?.username || 'Student';
475
- const content = isAIUser(msg.senderId) ? msg.content : `${senderName}: ${msg.content}`;
476
-
477
- messages.push({
478
- role: role as 'user' | 'assistant',
479
- content,
480
- });
481
- });
482
-
483
- // Add the new student message
484
- messages.push({
485
- role: 'user',
486
- content: `Student: ${studentMessage}`,
487
- });
488
-
489
- const completion = await openAIClient.chat.completions.create({
490
- model: 'gpt-5-nano',
491
- messages,
492
- temperature: 0.7,
493
- });
494
-
495
- const response = completion.choices[0]?.message?.content;
496
-
497
- if (!response) {
498
- throw new Error('No response generated from inference API');
499
- }
500
-
501
- // Send the text response to the conversation
502
- await sendAIMessage(response, conversationId, {
503
- subject: submission.assignment.class.subject || 'Assignment',
504
- });
505
-
506
- logger.info('AI response sent', { newtonChatId, conversationId });
507
-
508
- } catch (error) {
509
- logger.error('Failed to generate AI response:', {
510
- error: error instanceof Error ? {
511
- message: error.message,
512
- stack: error.stack,
513
- name: error.name
514
- } : error,
515
- newtonChatId
516
- });
517
- }
518
- }
519
-
520
297
 
@@ -1,10 +1,11 @@
1
1
  import { z } from "zod";
2
- import { createTRPCRouter, protectedProcedure, protectedTeacherProcedure } from "../trpc.js";
2
+ import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure, protectedTeacherProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
 
6
6
  const createSectionSchema = z.object({
7
7
  classId: z.string(),
8
+ id: z.string().optional(),
8
9
  name: z.string(),
9
10
  color: z.string().optional(),
10
11
  });
@@ -22,6 +23,24 @@ const deleteSectionSchema = z.object({
22
23
  });
23
24
 
24
25
  export const sectionRouter = createTRPCRouter({
26
+ exists: protectedClassMemberProcedure
27
+ .input(z.object({
28
+ id: z.string(),
29
+ }))
30
+ .query(async ({ ctx, input }) => {
31
+ if (!ctx.user) {
32
+ throw new TRPCError({
33
+ code: "UNAUTHORIZED",
34
+ message: "User must be authenticated",
35
+ });
36
+ }
37
+
38
+ const section = await prisma.section.findUnique({
39
+ where: { id: input.id },
40
+ });
41
+
42
+ return section ? true : false;
43
+ }),
25
44
  create: protectedTeacherProcedure
26
45
  .input(createSectionSchema)
27
46
  .mutation(async ({ ctx, input }) => {
@@ -53,6 +72,7 @@ export const sectionRouter = createTRPCRouter({
53
72
 
54
73
  const section = await prisma.section.create({
55
74
  data: {
75
+ ...(input.id && { id: input.id }),
56
76
  name: input.name,
57
77
  order: 0,
58
78
  class: {
@@ -1,5 +1,5 @@
1
1
  import { TRPCError } from "@trpc/server";
2
- import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
+ import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure } from "../trpc.js";
3
3
  import { z } from "zod";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
  import { GenerationStatus, WorksheetQuestionType } from "@prisma/client";
@@ -38,6 +38,22 @@ export const worksheetRouter = createTRPCRouter({
38
38
  return worksheet;
39
39
  }),
40
40
 
41
+ exists: protectedClassMemberProcedure
42
+ .input(z.object({
43
+ id: z.string(),
44
+ }))
45
+ .query(async ({ ctx, input }) => {
46
+ if (!ctx.user) {
47
+ throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User must be authenticated' });
48
+ }
49
+
50
+ const worksheet = await prisma.worksheet.findUnique({
51
+ where: { id: input.id },
52
+ });
53
+
54
+ return worksheet ? true : false;
55
+ }),
56
+
41
57
  // List all worksheets for a class
42
58
  listWorksheets: protectedProcedure
43
59
  .input(z.object({
@@ -8,6 +8,21 @@ export async function clearDatabase() {
8
8
  logger.info('Clearing database');
9
9
  await prisma.notification.deleteMany();
10
10
 
11
+ // Delete worksheet-related records
12
+ await prisma.studentQuestionProgress.deleteMany();
13
+ await prisma.studentWorksheetResponse.deleteMany();
14
+ await prisma.worksheetQuestion.deleteMany();
15
+ await prisma.worksheet.deleteMany();
16
+
17
+ // Delete reactions (they reference announcements and comments)
18
+ await prisma.reaction.deleteMany();
19
+
20
+ // Delete comments (they reference announcements and users)
21
+ await prisma.comment.deleteMany();
22
+
23
+ // Delete NewtonChat (they reference submissions and conversations)
24
+ await prisma.newtonChat.deleteMany();
25
+
11
26
  // Delete chat-related records
12
27
  await prisma.mention.deleteMany();
13
28
  await prisma.message.deleteMany();
@@ -39,10 +54,19 @@ export async function clearDatabase() {
39
54
  // Delete schools (which reference files for logos) - this will cascade delete the file references
40
55
  await prisma.school.deleteMany();
41
56
 
57
+ // Delete marketing-related records
58
+ await prisma.schoolDevelopementProgram.deleteMany();
59
+ await prisma.earlyAccessRequest.deleteMany();
60
+
42
61
  // Finally delete all files
43
62
  await prisma.file.deleteMany();
44
63
  }
45
64
 
65
+ // Helper function to generate DiceBear avatar URL
66
+ function getDiceBearAvatar(seed: string): string {
67
+ return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(seed)}`;
68
+ }
69
+
46
70
  export async function createUser(email: string, password: string, username: string) {
47
71
  logger.debug("Creating user", { email, username, password });
48
72
 
@@ -109,7 +133,7 @@ export const seedDatabase = async () => {
109
133
  createUser('charlotte.walker@student.riverside.edu', 'student123', 'charlotte.walker'),
110
134
  ]);
111
135
 
112
- // 4. Create User Profiles
136
+ // 4. Create User Profiles with DiceBear avatars
113
137
  await Promise.all([
114
138
  prisma.userProfile.create({
115
139
  data: {
@@ -118,6 +142,7 @@ export const seedDatabase = async () => {
118
142
  bio: 'Biology teacher with 15 years of experience. Passionate about making science accessible to all students.',
119
143
  location: 'Riverside, CA',
120
144
  website: 'https://sarahjohnson-bio.com',
145
+ profilePicture: getDiceBearAvatar(teachers[0].username),
121
146
  }
122
147
  }),
123
148
  prisma.userProfile.create({
@@ -126,6 +151,7 @@ export const seedDatabase = async () => {
126
151
  displayName: 'Mr. Michael Chen',
127
152
  bio: 'Mathematics teacher and department head. Specializes in AP Calculus and Statistics.',
128
153
  location: 'Riverside, CA',
154
+ profilePicture: getDiceBearAvatar(teachers[1].username),
129
155
  }
130
156
  }),
131
157
  prisma.userProfile.create({
@@ -134,18 +160,24 @@ export const seedDatabase = async () => {
134
160
  displayName: 'Ms. Emma Davis',
135
161
  bio: 'English Literature teacher. Loves fostering creative writing and critical thinking.',
136
162
  location: 'Riverside, CA',
163
+ profilePicture: getDiceBearAvatar(teachers[2].username),
137
164
  }
138
165
  }),
139
166
  ]);
140
167
 
141
- // Add profiles for some students
142
- await Promise.all(students.slice(0, 6).map((student, index) => {
143
- const names = ['Alex Martinez', 'Sophia Williams', 'James Brown', 'Olivia Taylor', 'Ethan Anderson', 'Ava Thomas'];
168
+ // Add profiles for all students with DiceBear avatars
169
+ await Promise.all(students.map((student, index) => {
170
+ const names = [
171
+ 'Alex Martinez', 'Sophia Williams', 'James Brown', 'Olivia Taylor',
172
+ 'Ethan Anderson', 'Ava Thomas', 'Noah Jackson', 'Isabella White',
173
+ 'Liam Harris', 'Mia Clark', 'Lucas Lewis', 'Charlotte Walker'
174
+ ];
144
175
  return prisma.userProfile.create({
145
176
  data: {
146
177
  userId: student.id,
147
- displayName: names[index],
148
- bio: `Grade 11 student at Riverside High School.`,
178
+ displayName: names[index] || student.username,
179
+ bio: index < 6 ? `Grade 11 student at Riverside High School.` : undefined,
180
+ profilePicture: getDiceBearAvatar(student.username),
149
181
  }
150
182
  });
151
183
  }));