@studious-lms/server 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +28 -0
- package/.env.example +5 -0
- package/.github/workflows/deploy.yml +123 -0
- package/Dockerfile +54 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +31 -11
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/config/env.d.ts +17 -0
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +8 -2
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts +10 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +22 -2
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/prisma.d.ts +104 -5
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +33 -2
- package/dist/lib/prisma.js.map +1 -1
- package/dist/models/agenda.d.ts +20 -20
- package/dist/models/announcement.d.ts +177 -69
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +551 -353
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +197 -71
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/auth.d.ts +348 -114
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +324 -188
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +9 -3
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +146 -56
- package/dist/models/cloudStorage.d.ts.map +1 -1
- package/dist/models/comment.d.ts +269 -110
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +3 -2
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +206 -63
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +13 -2
- package/dist/models/conversation.js.map +1 -1
- package/dist/models/department.d.ts +196 -70
- package/dist/models/department.d.ts.map +1 -1
- package/dist/models/event.d.ts +287 -125
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +389 -263
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +433 -181
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +209 -83
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +80 -26
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/message.d.ts +95 -41
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +83 -29
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/notification.d.ts +61 -25
- package/dist/models/notification.d.ts.map +1 -1
- package/dist/models/school.d.ts +349 -133
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/section.d.ts +216 -72
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +156 -48
- package/dist/models/tag.d.ts.map +1 -1
- package/dist/models/user.d.ts +82 -28
- package/dist/models/user.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +1023 -410
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +19 -7
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +0 -8
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +50 -67
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +393 -388
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +63 -69
- package/dist/pipelines/aiLabChatContract.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +41 -41
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +8 -8
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +59 -14
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +10 -8
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +47 -67
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +2 -2
- package/dist/routers/_app.d.ts +2268 -1898
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/admin.d.ts +13 -13
- package/dist/routers/agenda.d.ts +18 -18
- package/dist/routers/announcement.d.ts +32 -28
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +335 -327
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +4 -4
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +10 -10
- package/dist/routers/auth.d.ts +2 -2
- package/dist/routers/class.d.ts +170 -147
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +40 -16
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/comment.js +6 -3
- package/dist/routers/comment.js.map +1 -1
- package/dist/routers/conversation.d.ts +17 -17
- package/dist/routers/department.d.ts +34 -34
- package/dist/routers/discussion.d.ts +65 -53
- package/dist/routers/discussion.d.ts.map +1 -1
- package/dist/routers/discussion.js +6 -2
- package/dist/routers/discussion.js.map +1 -1
- package/dist/routers/embedding.d.ts +69 -0
- package/dist/routers/embedding.d.ts.map +1 -0
- package/dist/routers/embedding.js +143 -0
- package/dist/routers/embedding.js.map +1 -0
- package/dist/routers/event.d.ts +39 -39
- package/dist/routers/file.d.ts +12 -12
- package/dist/routers/folder.d.ts +38 -38
- package/dist/routers/labChat.d.ts +19 -19
- package/dist/routers/marketing.d.ts +2 -2
- package/dist/routers/message.d.ts +13 -13
- package/dist/routers/newtonChat.d.ts +4 -4
- package/dist/routers/notifications.d.ts +9 -9
- package/dist/routers/permissions.d.ts +14 -14
- package/dist/routers/school.d.ts +55 -55
- package/dist/routers/search.d.ts +22 -1
- package/dist/routers/search.d.ts.map +1 -1
- package/dist/routers/search.js +9 -3
- package/dist/routers/search.js.map +1 -1
- package/dist/routers/section.d.ts +4 -4
- package/dist/routers/studentProgress.d.ts +1 -1
- package/dist/routers/tag.d.ts +7 -7
- package/dist/routers/user.d.ts +10 -10
- package/dist/routers/worksheet.d.ts +97 -56
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +28 -2
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/routes/aiStream.d.ts +2 -0
- package/dist/routes/aiStream.d.ts.map +1 -0
- package/dist/routes/aiStream.js +696 -0
- package/dist/routes/aiStream.js.map +1 -0
- package/dist/seedDatabase.d.ts +2 -2
- package/dist/seedDatabase.js +6 -6
- package/dist/seedDatabase.js.map +1 -1
- package/dist/services/adminInstitution.d.ts +8 -8
- package/dist/services/adminRunJob.d.ts +4 -4
- package/dist/services/agenda.d.ts +18 -18
- package/dist/services/aiChatStream.d.ts +68 -0
- package/dist/services/aiChatStream.d.ts.map +1 -0
- package/dist/services/aiChatStream.js +157 -0
- package/dist/services/aiChatStream.js.map +1 -0
- package/dist/services/aiModifierTools.d.ts +32 -0
- package/dist/services/aiModifierTools.d.ts.map +1 -0
- package/dist/services/aiModifierTools.js +165 -0
- package/dist/services/aiModifierTools.js.map +1 -0
- package/dist/services/aiProposalTools.d.ts +32 -0
- package/dist/services/aiProposalTools.d.ts.map +1 -0
- package/dist/services/aiProposalTools.js +207 -0
- package/dist/services/aiProposalTools.js.map +1 -0
- package/dist/services/aiStream.d.ts +22 -0
- package/dist/services/aiStream.d.ts.map +1 -0
- package/dist/services/aiStream.js +142 -0
- package/dist/services/aiStream.js.map +1 -0
- package/dist/services/aiTools.d.ts +16 -0
- package/dist/services/aiTools.d.ts.map +1 -0
- package/dist/services/aiTools.js +271 -0
- package/dist/services/aiTools.js.map +1 -0
- package/dist/services/announcement.d.ts +12 -12
- package/dist/services/announcement.d.ts.map +1 -1
- package/dist/services/announcement.js +7 -2
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts +293 -293
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +36 -15
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +10 -10
- package/dist/services/auth.d.ts +2 -2
- package/dist/services/class.d.ts +161 -138
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +25 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +34 -16
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/comment.js +23 -2
- package/dist/services/comment.js.map +1 -1
- package/dist/services/conversation.d.ts +17 -17
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +9 -7
- package/dist/services/conversation.js.map +1 -1
- package/dist/services/department.d.ts +33 -33
- package/dist/services/discussion.d.ts +58 -58
- package/dist/services/discussion.d.ts.map +1 -1
- package/dist/services/discussion.js +8 -8
- package/dist/services/discussion.js.map +1 -1
- package/dist/services/discussionSummary.d.ts +21 -0
- package/dist/services/discussionSummary.d.ts.map +1 -0
- package/dist/services/discussionSummary.js +179 -0
- package/dist/services/discussionSummary.js.map +1 -0
- package/dist/services/embedding.d.ts +67 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +226 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/event.d.ts +37 -37
- package/dist/services/file.d.ts +24 -24
- package/dist/services/folder.d.ts +36 -36
- package/dist/services/labChat.d.ts +19 -19
- package/dist/services/marketing.d.ts +2 -2
- package/dist/services/message.d.ts +11 -11
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +2 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +4 -4
- package/dist/services/notification.d.ts +9 -9
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/permissions.d.ts +14 -14
- package/dist/services/school.d.ts +50 -50
- package/dist/services/search.d.ts +12 -0
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +120 -2
- package/dist/services/search.js.map +1 -1
- package/dist/services/section.d.ts +3 -3
- package/dist/services/tag.d.ts +4 -4
- package/dist/services/user.d.ts +4 -4
- package/dist/services/user.d.ts.map +1 -1
- package/dist/services/user.js +10 -15
- package/dist/services/user.js.map +1 -1
- package/dist/services/worksheet.d.ts +62 -47
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +11 -4
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/streamBroadcast.d.ts +11 -0
- package/dist/utils/streamBroadcast.d.ts.map +1 -0
- package/dist/utils/streamBroadcast.js +32 -0
- package/dist/utils/streamBroadcast.js.map +1 -0
- package/dist/workers/connection.d.ts +15 -0
- package/dist/workers/connection.d.ts.map +1 -1
- package/dist/workers/connection.js +20 -2
- package/dist/workers/connection.js.map +1 -1
- package/dist/workers/index.js +4 -2
- package/dist/workers/index.js.map +1 -1
- package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
- package/dist/workers/processors/createRunJobWorker.js +31 -8
- package/dist/workers/processors/createRunJobWorker.js.map +1 -1
- package/dist/workers/processors/embedContent.d.ts +8 -0
- package/dist/workers/processors/embedContent.d.ts.map +1 -0
- package/dist/workers/processors/embedContent.js +76 -0
- package/dist/workers/processors/embedContent.js.map +1 -0
- package/dist/workers/queues.d.ts +6 -0
- package/dist/workers/queues.d.ts.map +1 -1
- package/dist/workers/queues.js +5 -3
- package/dist/workers/queues.js.map +1 -1
- package/package.json +5 -1
- package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
- package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
- package/prisma/schema.prisma +39 -7
- package/src/index.ts +4 -0
- package/src/lib/config/cors.ts +32 -9
- package/src/lib/config/env.ts +6 -0
- package/src/lib/googleCloudStorage.ts +21 -0
- package/src/lib/prisma.ts +40 -1
- package/src/models/class.ts +10 -3
- package/src/models/comment.ts +1 -0
- package/src/models/conversation.ts +12 -0
- package/src/models/worksheet.ts +16 -7
- package/src/pipelines/aiLabChat.ts +54 -79
- package/src/pipelines/aiLabChatContract.ts +69 -69
- package/src/pipelines/aiNewtonChat.ts +46 -43
- package/src/pipelines/gradeWorksheet.ts +66 -15
- package/src/pipelines/labChatPrompt.ts +63 -88
- package/src/routers/_app.ts +2 -0
- package/src/routers/announcement.ts +4 -3
- package/src/routers/assignment.ts +2 -2
- package/src/routers/comment.ts +5 -0
- package/src/routers/discussion.ts +7 -0
- package/src/routers/embedding.ts +159 -0
- package/src/routers/search.ts +10 -1
- package/src/routers/worksheet.ts +29 -0
- package/src/routes/aiStream.ts +789 -0
- package/src/seedDatabase.ts +4 -4
- package/src/services/aiChatStream.ts +219 -0
- package/src/services/aiModifierTools.ts +188 -0
- package/src/services/aiProposalTools.ts +272 -0
- package/src/services/aiStream.ts +175 -0
- package/src/services/aiTools.ts +290 -0
- package/src/services/announcement.ts +7 -0
- package/src/services/assignment.ts +36 -14
- package/src/services/class.ts +30 -2
- package/src/services/comment.ts +22 -0
- package/src/services/conversation.ts +9 -4
- package/src/services/discussion.ts +9 -8
- package/src/services/discussionSummary.ts +226 -0
- package/src/services/embedding.ts +285 -0
- package/src/services/message.ts +2 -2
- package/src/services/search.ts +138 -0
- package/src/services/user.ts +9 -17
- package/src/services/worksheet.ts +10 -2
- package/src/utils/streamBroadcast.ts +48 -0
- package/src/workers/connection.ts +19 -0
- package/src/workers/index.ts +2 -0
- package/src/workers/processors/createRunJobWorker.ts +32 -6
- package/src/workers/processors/embedContent.ts +90 -0
- package/src/workers/queues.ts +5 -1
- package/tests/integration/routers/conversation.test.ts +9 -7
- package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
- package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
- package/prisma/migrations/20250807062924_init/migration.sql +0 -436
- package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
- package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
- package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
- package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
- package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
- package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
- package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
- package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
- package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
- package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
- package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
- package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
- package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
- package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
- package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
- package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
- package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
- package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
- /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Tool definitions and execution for agent tool calls.
|
|
3
|
+
* Provides RAG search, assignment lookup, worksheet creation, student progress,
|
|
4
|
+
* and web search tools.
|
|
5
|
+
*/
|
|
6
|
+
import { jsonSchema, type ToolSet } from 'ai';
|
|
7
|
+
import OpenAI from 'openai';
|
|
8
|
+
import { searchEmbeddings } from './embedding.js';
|
|
9
|
+
import { prisma } from '../lib/prisma.js';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
export interface ToolCallResult {
|
|
13
|
+
toolName: string;
|
|
14
|
+
result: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the tool set for a given class context.
|
|
19
|
+
* Tools are scoped to a specific class for security.
|
|
20
|
+
*/
|
|
21
|
+
export function aiTools(classId: string): ToolSet {
|
|
22
|
+
return {
|
|
23
|
+
search_materials: {
|
|
24
|
+
description: 'Search class materials, files, assignments, worksheets, and announcements using semantic similarity. Use this to find relevant context for answering questions.',
|
|
25
|
+
inputSchema: jsonSchema({
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
query: { type: 'string', description: 'The search query to find relevant materials' },
|
|
29
|
+
limit: { type: 'number', description: 'Maximum results (1-10)', default: 5 },
|
|
30
|
+
},
|
|
31
|
+
required: ['query'],
|
|
32
|
+
}),
|
|
33
|
+
execute: async ({ query, limit = 5 }: { query: string; limit?: number }) => {
|
|
34
|
+
try {
|
|
35
|
+
const results = await searchEmbeddings({ classId, query, limit });
|
|
36
|
+
return results.map((r) => ({
|
|
37
|
+
sourceType: r.sourceType,
|
|
38
|
+
sourceId: r.sourceId,
|
|
39
|
+
content: r.content.slice(0, 500),
|
|
40
|
+
similarity: Math.round(r.similarity * 100) / 100,
|
|
41
|
+
metadata: r.metadata,
|
|
42
|
+
}));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.error('search_materials tool failed', { error, classId, query });
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
search_assignments: {
|
|
51
|
+
description: 'Search existing assignments by title, instructions, or type. Returns assignment details including due dates and submission types.',
|
|
52
|
+
inputSchema: jsonSchema({
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
query: { type: 'string', description: 'Search query for assignments' },
|
|
56
|
+
limit: { type: 'number', description: 'Maximum results (1-10)', default: 5 },
|
|
57
|
+
},
|
|
58
|
+
required: ['query'],
|
|
59
|
+
}),
|
|
60
|
+
execute: async ({ query, limit = 5 }: { query: string; limit?: number }) => {
|
|
61
|
+
try {
|
|
62
|
+
const results = await searchEmbeddings({
|
|
63
|
+
classId,
|
|
64
|
+
query,
|
|
65
|
+
limit,
|
|
66
|
+
sourceTypes: ['ASSIGNMENT'],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const assignmentIds = results.map((r) => r.sourceId);
|
|
70
|
+
const assignments = await prisma.assignment.findMany({
|
|
71
|
+
where: { id: { in: assignmentIds } },
|
|
72
|
+
select: {
|
|
73
|
+
id: true,
|
|
74
|
+
title: true,
|
|
75
|
+
instructions: true,
|
|
76
|
+
type: true,
|
|
77
|
+
dueDate: true,
|
|
78
|
+
maxGrade: true,
|
|
79
|
+
section: { select: { name: true } },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return assignments.map((a) => ({
|
|
84
|
+
id: a.id,
|
|
85
|
+
title: a.title,
|
|
86
|
+
instructions: a.instructions?.slice(0, 200) ?? null,
|
|
87
|
+
type: a.type,
|
|
88
|
+
dueDate: a.dueDate.toISOString(),
|
|
89
|
+
maxGrade: a.maxGrade,
|
|
90
|
+
section: a.section?.name ?? null,
|
|
91
|
+
}));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error('search_assignments tool failed', { error, classId, query });
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
get_student_progress: {
|
|
100
|
+
description: 'Get progress summary for a specific student in this class, including grades and submission status.',
|
|
101
|
+
inputSchema: jsonSchema({
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
studentId: { type: 'string', description: 'The student user ID' },
|
|
105
|
+
},
|
|
106
|
+
required: ['studentId'],
|
|
107
|
+
}),
|
|
108
|
+
execute: async ({ studentId }: { studentId: string }) => {
|
|
109
|
+
try {
|
|
110
|
+
const submissions = await prisma.submission.findMany({
|
|
111
|
+
where: {
|
|
112
|
+
studentId,
|
|
113
|
+
assignment: { classId },
|
|
114
|
+
},
|
|
115
|
+
select: {
|
|
116
|
+
id: true,
|
|
117
|
+
gradeReceived: true,
|
|
118
|
+
submittedAt: true,
|
|
119
|
+
assignment: {
|
|
120
|
+
select: {
|
|
121
|
+
title: true,
|
|
122
|
+
maxGrade: true,
|
|
123
|
+
type: true,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
orderBy: { submittedAt: 'desc' },
|
|
128
|
+
take: 20,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const totalAssignments = await prisma.assignment.count({
|
|
132
|
+
where: { classId },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const submitted = submissions.filter((s) => s.submittedAt);
|
|
136
|
+
const graded = submissions.filter((s) => s.gradeReceived !== null);
|
|
137
|
+
const avgGrade = graded.length > 0
|
|
138
|
+
? graded.reduce((sum, s) => sum + (s.gradeReceived || 0), 0) / graded.length
|
|
139
|
+
: null;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
studentId,
|
|
143
|
+
totalAssignments,
|
|
144
|
+
submittedCount: submitted.length,
|
|
145
|
+
gradedCount: graded.length,
|
|
146
|
+
averageGrade: avgGrade ? Math.round(avgGrade * 10) / 10 : null,
|
|
147
|
+
recentSubmissions: submissions.slice(0, 5).map((s) => ({
|
|
148
|
+
assignment: s.assignment.title,
|
|
149
|
+
gradeReceived: s.gradeReceived,
|
|
150
|
+
maxGrade: s.assignment.maxGrade,
|
|
151
|
+
submittedAt: s.submittedAt?.toISOString() ?? null,
|
|
152
|
+
})),
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error('get_student_progress tool failed', { error, classId, studentId });
|
|
156
|
+
return { error: 'Failed to fetch student progress' };
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
get_worksheet_details: {
|
|
162
|
+
description: 'Get full details of a worksheet including all questions, answers, and mark schemes.',
|
|
163
|
+
inputSchema: jsonSchema({
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
worksheetId: { type: 'string', description: 'The worksheet ID to look up' },
|
|
167
|
+
},
|
|
168
|
+
required: ['worksheetId'],
|
|
169
|
+
}),
|
|
170
|
+
execute: async ({ worksheetId }: { worksheetId: string }) => {
|
|
171
|
+
try {
|
|
172
|
+
const worksheet = await prisma.worksheet.findUnique({
|
|
173
|
+
where: { id: worksheetId, classId },
|
|
174
|
+
include: {
|
|
175
|
+
questions: {
|
|
176
|
+
orderBy: { order: 'asc' },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!worksheet) {
|
|
182
|
+
return { error: 'Worksheet not found' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
id: worksheet.id,
|
|
187
|
+
name: worksheet.name,
|
|
188
|
+
questions: worksheet.questions.map((q) => ({
|
|
189
|
+
id: q.id,
|
|
190
|
+
type: q.type,
|
|
191
|
+
question: q.question,
|
|
192
|
+
answer: q.answer,
|
|
193
|
+
options: q.options,
|
|
194
|
+
points: q.points,
|
|
195
|
+
order: q.order,
|
|
196
|
+
})),
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.error('get_worksheet_details tool failed', { error, classId, worksheetId });
|
|
200
|
+
return { error: 'Failed to fetch worksheet' };
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
get_mark_scheme: {
|
|
206
|
+
description: 'Get a mark scheme (rubric) by ID, including criteria and point allocations.',
|
|
207
|
+
inputSchema: jsonSchema({
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
markSchemeId: { type: 'string', description: 'The mark scheme ID' },
|
|
211
|
+
},
|
|
212
|
+
required: ['markSchemeId'],
|
|
213
|
+
}),
|
|
214
|
+
execute: async ({ markSchemeId }: { markSchemeId: string }) => {
|
|
215
|
+
try {
|
|
216
|
+
const markScheme = await prisma.markScheme.findUnique({
|
|
217
|
+
where: { id: markSchemeId },
|
|
218
|
+
select: { id: true, name: true, structured: true },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!markScheme) {
|
|
222
|
+
return { error: 'Mark scheme not found' };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let parsed: unknown = {};
|
|
226
|
+
try { parsed = JSON.parse(markScheme.structured || '{}'); } catch { /* noop */ }
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
id: markScheme.id,
|
|
230
|
+
name: markScheme.name,
|
|
231
|
+
structured: parsed,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logger.error('get_mark_scheme tool failed', { error, classId, markSchemeId });
|
|
235
|
+
return { error: 'Failed to fetch mark scheme' };
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
web_search: {
|
|
241
|
+
description: 'Search the web for current information on a topic. Use this when you need up-to-date facts, definitions, explanations, or context beyond the class materials.',
|
|
242
|
+
inputSchema: jsonSchema({
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
query: { type: 'string', description: 'The search query to look up on the web' },
|
|
246
|
+
},
|
|
247
|
+
required: ['query'],
|
|
248
|
+
}),
|
|
249
|
+
execute: async ({ query }: { query: string }) => {
|
|
250
|
+
try {
|
|
251
|
+
const client = new OpenAI();
|
|
252
|
+
const response = await client.responses.create({
|
|
253
|
+
model: 'gpt-4o-mini',
|
|
254
|
+
tools: [{ type: 'web_search_preview' }],
|
|
255
|
+
input: query,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const messageOutput = response.output.find(
|
|
259
|
+
(item) => item.type === 'message',
|
|
260
|
+
);
|
|
261
|
+
if (!messageOutput || messageOutput.type !== 'message') {
|
|
262
|
+
return { summary: 'No results found.', sources: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const textContent = messageOutput.content.find(
|
|
266
|
+
(c) => c.type === 'output_text',
|
|
267
|
+
);
|
|
268
|
+
if (!textContent || textContent.type !== 'output_text') {
|
|
269
|
+
return { summary: 'No results found.', sources: [] };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const sources = textContent.annotations
|
|
273
|
+
.filter(
|
|
274
|
+
(a): a is Extract<typeof a, { type: 'url_citation' }> =>
|
|
275
|
+
a.type === 'url_citation',
|
|
276
|
+
)
|
|
277
|
+
.map((a) => ({ url: a.url, title: a.title }));
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
summary: textContent.text.slice(0, 2000),
|
|
281
|
+
sources: sources.slice(0, 5),
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
logger.error('web_search tool failed', { error, query });
|
|
285
|
+
return { summary: 'Web search failed. Please try again.', sources: [] };
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -16,6 +16,7 @@ import { createDirectUploadFiles, type DirectUploadFile } from "../lib/fileUploa
|
|
|
16
16
|
import { deleteFile } from "../lib/googleCloudStorage.js";
|
|
17
17
|
import { sendToMultiple } from "./notification.js";
|
|
18
18
|
import { logger } from "../utils/logger.js";
|
|
19
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
19
20
|
|
|
20
21
|
export async function getAllAnnouncements(classId: string) {
|
|
21
22
|
const announcements = await findAnnouncementsByClassId(classId);
|
|
@@ -127,6 +128,9 @@ export async function createAnnouncementRecord(
|
|
|
127
128
|
logger.error("Failed to send announcement notifications:", error);
|
|
128
129
|
});
|
|
129
130
|
|
|
131
|
+
// Auto-index announcement for RAG search
|
|
132
|
+
enqueueEmbedding(classId, 'ANNOUNCEMENT', announcement.id);
|
|
133
|
+
|
|
130
134
|
return {
|
|
131
135
|
announcement: announcementWithAttachments || announcement,
|
|
132
136
|
uploadFiles: directUploadFiles.length > 0 ? directUploadFiles : undefined,
|
|
@@ -263,6 +267,9 @@ export async function updateAnnouncementRecord(
|
|
|
263
267
|
},
|
|
264
268
|
});
|
|
265
269
|
|
|
270
|
+
// Re-index announcement for RAG search after update
|
|
271
|
+
enqueueEmbedding(input.classId, 'ANNOUNCEMENT', input.id);
|
|
272
|
+
|
|
266
273
|
return {
|
|
267
274
|
announcement: updatedAnnouncement,
|
|
268
275
|
uploadFiles: directUploadFiles.length > 0 ? directUploadFiles : undefined,
|
|
@@ -27,6 +27,7 @@ import { sendToMultiple } from "./notification.js";
|
|
|
27
27
|
import { enqueueRunJob } from "../workers/queues.js";
|
|
28
28
|
import { logger } from "../utils/logger.js";
|
|
29
29
|
import { invalidateStudentProgressInferenceCache } from "./studentProgressInferenceCache.js";
|
|
30
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
30
31
|
|
|
31
32
|
export async function assignmentExists(id: string) {
|
|
32
33
|
const assignment = await findAssignmentById(id);
|
|
@@ -311,13 +312,18 @@ export async function createAssignmentRecord(
|
|
|
311
312
|
}
|
|
312
313
|
await Promise.all(fileOperations);
|
|
313
314
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
if (!inProgress) {
|
|
316
|
+
sendToMultiple({
|
|
317
|
+
receiverIds: (studentIds && studentIds.length > 0) ? studentIds : classData.students.map((s) => s.id),
|
|
318
|
+
title: `New assignment in ${classData.name}`,
|
|
319
|
+
content: `"${title}" is due ${new Date(dueDate).toLocaleDateString()}.\n\n[View assignment](/class/${classId}/assignments/${assignment.id})`,
|
|
320
|
+
}).catch((error) => {
|
|
321
|
+
logger.error("Failed to send assignment notifications:", error);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Auto-index assignment for RAG search
|
|
326
|
+
enqueueEmbedding(classId, 'ASSIGNMENT', assignment.id);
|
|
321
327
|
|
|
322
328
|
return assignment;
|
|
323
329
|
}
|
|
@@ -425,12 +431,13 @@ export async function updateAssignmentRecord(
|
|
|
425
431
|
});
|
|
426
432
|
}
|
|
427
433
|
|
|
428
|
-
const submissionData =
|
|
429
|
-
input.studentIds
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
+
const submissionData = input.studentIds
|
|
435
|
+
? (input.studentIds.length > 0
|
|
436
|
+
? input.studentIds.map((studentId) => ({ student: { connect: { id: studentId } } }))
|
|
437
|
+
: classData?.students.map((student) => ({
|
|
438
|
+
student: { connect: { id: student.id } },
|
|
439
|
+
})))
|
|
440
|
+
: undefined;
|
|
434
441
|
|
|
435
442
|
if (input.removedAttachments?.length) {
|
|
436
443
|
const filesToDelete = assignment.attachments.filter((f) =>
|
|
@@ -481,7 +488,7 @@ export async function updateAssignmentRecord(
|
|
|
481
488
|
connect: input.studentIds.map((id) => ({ id })),
|
|
482
489
|
},
|
|
483
490
|
}),
|
|
484
|
-
...(submissionData && {
|
|
491
|
+
...(input.studentIds && submissionData && {
|
|
485
492
|
submissions: {
|
|
486
493
|
deleteMany: {},
|
|
487
494
|
create: submissionData,
|
|
@@ -590,6 +597,21 @@ export async function updateAssignmentRecord(
|
|
|
590
597
|
.catch((error) => logger.error("Failed to update max grade from rubric:", error));
|
|
591
598
|
}
|
|
592
599
|
|
|
600
|
+
// Re-index assignment for RAG search after update
|
|
601
|
+
enqueueEmbedding(input.classId, 'ASSIGNMENT', input.id);
|
|
602
|
+
if (assignment.inProgress && input.inProgress === false && classData) {
|
|
603
|
+
const receiverIds = input.studentIds?.length
|
|
604
|
+
? input.studentIds
|
|
605
|
+
: classData.students.map((s) => s.id);
|
|
606
|
+
sendToMultiple({
|
|
607
|
+
receiverIds,
|
|
608
|
+
title: `New assignment in ${classData.name}`,
|
|
609
|
+
content: `"${updatedAssignment.title}" is due ${new Date(updatedAssignment.dueDate).toLocaleDateString()}.\n\n[View assignment](/class/${classData.id}/assignments/${updatedAssignment.id})`,
|
|
610
|
+
}).catch((error) => {
|
|
611
|
+
logger.error("Failed to send assignment publish notifications:", error);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
593
615
|
return updatedAssignment;
|
|
594
616
|
}
|
|
595
617
|
|
package/src/services/class.ts
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
} from "../models/class.js";
|
|
28
28
|
import { Class, Folder } from "@prisma/client";
|
|
29
29
|
import { invalidateStudentProgressInferenceCache } from "./studentProgressInferenceCache.js";
|
|
30
|
+
import { sendToMultiple } from "./notification.js";
|
|
31
|
+
import { logger } from "../utils/logger.js";
|
|
30
32
|
|
|
31
33
|
async function requireActiveSchoolMembers(schoolId: string, userIds: string[]) {
|
|
32
34
|
const uniqueUserIds = [...new Set(userIds)];
|
|
@@ -85,7 +87,16 @@ export async function getAllClasses(userId: string) {
|
|
|
85
87
|
section: cls.section,
|
|
86
88
|
subject: cls.subject,
|
|
87
89
|
dueToday: cls.assignments,
|
|
88
|
-
assignments: cls.assignments
|
|
90
|
+
assignments: cls.assignments.map((assignment) => ({
|
|
91
|
+
...assignment,
|
|
92
|
+
late: new Date(assignment.dueDate) < new Date(),
|
|
93
|
+
submitted: assignment.submissions.find(
|
|
94
|
+
(s) => s.studentId === userId,
|
|
95
|
+
)?.submitted ?? false,
|
|
96
|
+
returned: assignment.submissions.find(
|
|
97
|
+
(s) => s.studentId === userId,
|
|
98
|
+
)?.returned ?? false,
|
|
99
|
+
})),
|
|
89
100
|
members: [...cls.students, ...cls.teachers],
|
|
90
101
|
color: cls.color,
|
|
91
102
|
schoolId: cls.schoolId ?? null,
|
|
@@ -739,7 +750,7 @@ export async function publishLabDraft(
|
|
|
739
750
|
});
|
|
740
751
|
}
|
|
741
752
|
|
|
742
|
-
|
|
753
|
+
const published = await prisma.assignment.update({
|
|
743
754
|
where: { id: draftId },
|
|
744
755
|
data: {
|
|
745
756
|
inProgress: false,
|
|
@@ -750,6 +761,23 @@ export async function publishLabDraft(
|
|
|
750
761
|
modifiedAt: new Date(),
|
|
751
762
|
},
|
|
752
763
|
});
|
|
764
|
+
|
|
765
|
+
const classData = await prisma.class.findUnique({
|
|
766
|
+
where: { id: classId },
|
|
767
|
+
select: { name: true, students: { select: { id: true } } },
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
if (classData) {
|
|
771
|
+
sendToMultiple({
|
|
772
|
+
receiverIds: classData.students.map((s) => s.id),
|
|
773
|
+
title: `New assignment in ${classData.name}`,
|
|
774
|
+
content: `"${published.title}" is due ${new Date(published.dueDate).toLocaleDateString()}.\n\n[View assignment](/class/${classId}/assignments/${published.id})`,
|
|
775
|
+
}).catch((error) => {
|
|
776
|
+
logger.error("Failed to send lab draft publish notifications:", error);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return published;
|
|
753
781
|
}
|
|
754
782
|
|
|
755
783
|
export async function getFiles(classId: string) {
|
package/src/services/comment.ts
CHANGED
|
@@ -158,6 +158,28 @@ export async function deleteComment(userId: string, id: string) {
|
|
|
158
158
|
return { success: true };
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/** Pin or unpin a comment. Only teachers can pin comments. */
|
|
162
|
+
export async function pinComment(userId: string, commentId: string, isPinned: boolean) {
|
|
163
|
+
const comment = await requireCommentAccess(commentId, userId);
|
|
164
|
+
if (comment.parentCommentId) {
|
|
165
|
+
throw new TRPCError({
|
|
166
|
+
code: "BAD_REQUEST",
|
|
167
|
+
message: "Only top-level comments can be pinned",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!isClassTeacher(comment, userId)) {
|
|
171
|
+
throw new TRPCError({
|
|
172
|
+
code: "FORBIDDEN",
|
|
173
|
+
message: "Only teachers can pin comments",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const updated = await prisma.comment.update({
|
|
177
|
+
where: { id: commentId },
|
|
178
|
+
data: { isPinned },
|
|
179
|
+
});
|
|
180
|
+
return updated;
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
/** Add or update a reaction (emoji) on a comment. */
|
|
162
184
|
export async function addReaction(
|
|
163
185
|
userId: string,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
findConversationByIdAndMember,
|
|
16
16
|
createConversationMember,
|
|
17
17
|
hideConversationMember,
|
|
18
|
+
unhideConversationMember,
|
|
18
19
|
deleteConversationMember,
|
|
19
20
|
} from "../models/conversation.js";
|
|
20
21
|
|
|
@@ -160,10 +161,14 @@ export async function createConversationRecord(
|
|
|
160
161
|
});
|
|
161
162
|
|
|
162
163
|
if (existingDM) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
const userMembership = existingDM.members.find(
|
|
165
|
+
(m) => m.userId === userId
|
|
166
|
+
);
|
|
167
|
+
if (userMembership) {
|
|
168
|
+
await unhideConversationMember(userId, existingDM.id);
|
|
169
|
+
}
|
|
170
|
+
const conversation = await findConversationByIdAndMember(existingDM.id, userId);
|
|
171
|
+
return conversation!;
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -225,7 +226,7 @@ async function getDiscussionForUser(discussionId: string, userId: string) {
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
async function claimDiscussionAttachments(
|
|
228
|
-
tx:
|
|
229
|
+
tx: AppTransactionClient,
|
|
229
230
|
userId: string,
|
|
230
231
|
discussionId: string,
|
|
231
232
|
attachmentIds: string[] = []
|
|
@@ -242,7 +243,7 @@ async function claimDiscussionAttachments(
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
async function claimReplyAttachments(
|
|
245
|
-
tx:
|
|
246
|
+
tx: AppTransactionClient,
|
|
246
247
|
userId: string,
|
|
247
248
|
replyId: string,
|
|
248
249
|
attachmentIds: string[] = []
|
|
@@ -674,28 +675,28 @@ export async function toggleDiscussionReaction(userId: string, input: { discussi
|
|
|
674
675
|
|
|
675
676
|
export async function listDiscussionTags(userId: string, classId: string) {
|
|
676
677
|
await getDiscussionClassAccess(classId, userId);
|
|
677
|
-
return prisma.
|
|
678
|
+
return prisma.classTag.findMany({ where: { classId }, orderBy: { name: "asc" } });
|
|
678
679
|
}
|
|
679
680
|
|
|
680
681
|
export async function createDiscussionTag(userId: string, input: { classId: string; name: string; color: string }) {
|
|
681
682
|
const isTeacher = await prisma.class.findFirst({ where: { id: input.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
|
|
682
683
|
if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
|
|
683
|
-
return prisma.
|
|
684
|
+
return prisma.classTag.create({ data: { classId: input.classId, name: input.name.trim(), color: input.color } });
|
|
684
685
|
}
|
|
685
686
|
|
|
686
687
|
export async function updateDiscussionTag(userId: string, input: { tagId: string; name?: string; color?: string }) {
|
|
687
|
-
const tag = await prisma.
|
|
688
|
+
const tag = await prisma.classTag.findUnique({ where: { id: input.tagId }, select: { classId: true } });
|
|
688
689
|
if (!tag) throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
|
|
689
690
|
const isTeacher = await prisma.class.findFirst({ where: { id: tag.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
|
|
690
691
|
if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
|
|
691
|
-
return prisma.
|
|
692
|
+
return prisma.classTag.update({ where: { id: input.tagId }, data: { name: input.name?.trim(), color: input.color } });
|
|
692
693
|
}
|
|
693
694
|
|
|
694
695
|
export async function deleteDiscussionTag(userId: string, tagId: string) {
|
|
695
|
-
const tag = await prisma.
|
|
696
|
+
const tag = await prisma.classTag.findUnique({ where: { id: tagId }, select: { classId: true } });
|
|
696
697
|
if (!tag) throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
|
|
697
698
|
const isTeacher = await prisma.class.findFirst({ where: { id: tag.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
|
|
698
699
|
if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
|
|
699
|
-
await prisma.
|
|
700
|
+
await prisma.classTag.delete({ where: { id: tagId } });
|
|
700
701
|
return { success: true };
|
|
701
702
|
}
|