@studious-lms/server 1.16.0 → 1.17.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/chatAttachmentPrompt.d.ts +47 -0
- package/dist/lib/chatAttachmentPrompt.d.ts.map +1 -0
- package/dist/lib/chatAttachmentPrompt.js +190 -0
- package/dist/lib/chatAttachmentPrompt.js.map +1 -0
- package/dist/lib/inferDocumentFormat.js +3 -3
- package/dist/lib/inferDocumentFormat.js.map +1 -1
- 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 +2 -0
- package/dist/models/announcement.d.ts.map +1 -1
- 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/event.d.ts +2 -0
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +24 -0
- package/dist/models/file.d.ts.map +1 -1
- 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/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +39 -59
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +2 -2
- package/dist/pipelines/aiNewtonChat.d.ts +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +20 -19
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +1 -0
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +84 -13
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +4 -20
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +24 -64
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/routers/_app.d.ts +440 -26
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +10 -0
- 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 +4 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/event.d.ts +2 -0
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/marketing.d.ts +0 -13
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/school.d.ts +106 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +11 -3
- package/dist/routers/school.js.map +1 -1
- 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 +7 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +8 -3
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +2 -0
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/services/aiChatStream.d.ts +2 -0
- package/dist/services/aiChatStream.d.ts.map +1 -1
- package/dist/services/aiChatStream.js +31 -4
- package/dist/services/aiChatStream.js.map +1 -1
- package/dist/services/aiProposalTools.d.ts.map +1 -1
- package/dist/services/aiProposalTools.js +9 -9
- 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/assignment.d.ts +10 -0
- package/dist/services/assignment.d.ts.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 +4 -0
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/event.d.ts +2 -0
- package/dist/services/event.d.ts.map +1 -1
- 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/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +2 -8
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/openaiDocumentGen.d.ts +12 -0
- package/dist/services/openaiDocumentGen.d.ts.map +1 -1
- package/dist/services/openaiDocumentGen.js +173 -65
- package/dist/services/openaiDocumentGen.js.map +1 -1
- package/dist/services/school.d.ts +116 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +213 -2
- package/dist/services/school.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 +1 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +13 -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/aiNewtonChat.d.ts.map +1 -1
- package/dist/workers/processors/aiNewtonChat.js +3 -3
- package/dist/workers/processors/aiNewtonChat.js.map +1 -1
- package/package.json +2 -1
- package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
- package/prisma/schema.prisma +3 -0
- package/src/instrument.ts +1 -1
- package/src/lib/chatAttachmentPrompt.ts +270 -0
- package/src/lib/inferDocumentFormat.ts +1 -1
- 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 +46 -71
- package/src/pipelines/aiNewtonChat.ts +20 -23
- package/src/pipelines/gradeWorksheet.ts +87 -11
- package/src/pipelines/labChatPrompt.ts +29 -98
- package/src/routers/auth.ts +13 -0
- package/src/routers/school.ts +12 -0
- package/src/routers/user.ts +26 -0
- package/src/routers/worksheet.ts +11 -0
- package/src/services/aiChatStream.ts +30 -3
- package/src/services/aiProposalTools.ts +8 -7
- package/src/services/aiTools.ts +1 -1
- package/src/services/auth.ts +14 -2
- package/src/services/marketing.ts +3 -3
- package/src/services/newtonChat.ts +0 -7
- package/src/services/openaiDocumentGen.ts +190 -66
- package/src/services/school.ts +231 -0
- package/src/services/userPrivacy.ts +196 -0
- package/src/services/worksheet.ts +20 -1
- package/src/utils/email.ts +5 -1
- package/src/workers/processors/aiNewtonChat.ts +0 -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/integration/routers/newtonChat.test.ts +9 -8
- package/tests/unit/lib/chatAttachmentPrompt.test.ts +135 -0
- package/tests/unit/lib/inferDocumentFormat.test.ts +10 -0
- package/tests/unit/pipelines/labChatPrompt.test.ts +6 -6
- package/tests/unit/services/aiChatStream.test.ts +92 -0
- package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"newtonChat.js","sources":["services/newtonChat.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Newton chat service – AI tutor conversations for assignments. Get or create\n * tutor session, post messages, trigger AI responses. Broadcasts via Pusher.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { logger } from \"../utils/logger.js\";\nimport { chatChannel, pusher } from \"../lib/pusher.js\";\nimport { isAIUser } from \"../utils/aiUser.js\";\nimport { createPendingAIMessage } from \"../utils/inference.js\";\nimport { enqueueRunJob } from \"../workers/queues.js\";\nimport {\n claimMessageAttachments,\n ensureMessageHasContentOrAttachments,\n} from \"./message.js\";\nimport { messageFileSelect } from \"../models/message.js\";\nimport {\n findClassWithStudent,\n findSubmissionByAssignmentAndStudent,\n findNewtonChatByIdAndMember,\n findConversationMembersByUserIds,\n} from \"../models/newtonChat.js\";\nimport { prisma } from \"../lib/prisma.js\";\n\n/** Get or create Newton tutor conversation for a submission. Student-only. */\nexport async function getTutorConversation(\n userId: string,\n assignmentId: string,\n classId: string\n) {\n const classMembership = await findClassWithStudent(classId, userId);\n if (!classMembership) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Not a student in this class\",\n });\n }\n\n const submission = await findSubmissionByAssignmentAndStudent(\n assignmentId,\n userId\n );\n if (!submission) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Submission not found. Please create a submission first.\",\n });\n }\n\n const { chat, isNew } = await prisma.$transaction(async (tx) => {\n const existingNewtonChat = await tx.newtonChat.findFirst({\n where: { submissionId: submission.id },\n include: {\n conversation: {\n include: {\n members: {\n where: { userId },\n },\n },\n },\n },\n orderBy: { createdAt: \"desc\" },\n });\n\n if (existingNewtonChat && existingNewtonChat.conversation.members.length > 0) {\n return { chat: existingNewtonChat, isNew: false };\n }\n\n if (existingNewtonChat) {\n await tx.conversationMember.create({\n data: {\n userId,\n conversationId: existingNewtonChat.conversationId,\n role: \"MEMBER\",\n },\n });\n return { chat: existingNewtonChat, isNew: false };\n }\n\n const conversation = await tx.conversation.create({\n data: {\n type: \"DM\",\n name: \"Session with Newton Tutor\",\n displayInChat: false,\n },\n });\n\n await tx.conversationMember.create({\n data: {\n userId,\n conversationId: conversation.id,\n role: \"MEMBER\",\n },\n });\n\n const newtonChat = await tx.newtonChat.create({\n data: {\n submissionId: submission.id,\n conversationId: conversation.id,\n title: \"Session with Newton Tutor\",\n },\n });\n\n return { chat: newtonChat, isNew: true };\n });\n\n if (isNew) {\n // Create the AI introduction placeholder and enqueue OUTSIDE the\n // transaction — a tx rollback would otherwise orphan a job pointing at\n // a non-existent NewtonChat / Message row.\n try {\n const aiPlaceholder = await createPendingAIMessage(chat.conversationId);\n await enqueueRunJob(\n \"AI_NEWTON_CHAT\",\n { conversationId: chat.conversationId, aiMessageId: aiPlaceholder.id },\n { userId },\n );\n } catch (error) {\n logger.error(\"Failed to enqueue Newton introduction\", {\n error,\n newtonChatId: chat.id,\n });\n }\n }\n\n return {\n conversationId: chat.conversationId,\n newtonChatId: chat.id,\n };\n}\n\nexport async function postToNewtonChat(\n userId: string,\n newtonChatId: string,\n content: string,\n mentionedUserIds: string[] = [],\n fileIds: string[] = []\n) {\n const newtonChat = await findNewtonChatByIdAndMember(newtonChatId, userId);\n if (!newtonChat) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Newton chat not found or access denied\",\n });\n }\n\n if (mentionedUserIds.length > 0) {\n const mentionedMemberships = await findConversationMembersByUserIds(\n newtonChat.conversationId,\n mentionedUserIds\n );\n if (mentionedMemberships.length !== mentionedUserIds.length) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Some mentioned users are not members of this conversation\",\n });\n }\n }\n\n ensureMessageHasContentOrAttachments(content, fileIds);\n const normalizedContent = content.trim();\n\n if (!normalizedContent) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Newton chat messages must include text content\",\n });\n }\n\n const result = await prisma.$transaction(async (tx) => {\n const message = await tx.message.create({\n data: {\n content: normalizedContent,\n senderId: userId,\n conversationId: newtonChat.conversationId,\n },\n });\n\n await claimMessageAttachments(\n tx,\n userId,\n newtonChat.conversationId,\n message.id,\n fileIds\n );\n\n if (mentionedUserIds.length > 0) {\n await tx.mention.createMany({\n data: mentionedUserIds.map((mentionedUserId) => ({\n messageId: message.id,\n userId: mentionedUserId,\n })),\n });\n }\n\n await tx.newtonChat.update({\n where: { id: newtonChatId },\n data: { updatedAt: new Date() },\n });\n\n return tx.message.findUniqueOrThrow({\n where: { id: message.id },\n include: {\n sender: {\n select: {\n id: true,\n username: true,\n profile: {\n select: {\n displayName: true,\n profilePicture: true,\n },\n },\n },\n },\n attachments: {\n select: messageFileSelect,\n },\n },\n });\n });\n\n try {\n await pusher.trigger(\n chatChannel(newtonChat.conversationId),\n \"new-message\",\n {\n id: result.id,\n content: result.content,\n senderId: result.senderId,\n conversationId: result.conversationId,\n status: result.status,\n createdAt: result.createdAt,\n sender: result.sender,\n attachments: result.attachments ?? [],\n mentionedUserIds,\n meta: {},\n }\n );\n } catch (error) {\n console.error(\"Failed to broadcast newton chat message:\", error);\n }\n\n if (!isAIUser(userId)) {\n // Create the AI placeholder post-tx (so a rollback can't broadcast a\n // ghost bubble); the pipeline fills it in.\n try {\n const aiPlaceholder = await createPendingAIMessage(newtonChat.conversationId);\n await enqueueRunJob(\n \"AI_NEWTON_CHAT\",\n { conversationId: newtonChat.conversationId, aiMessageId: aiPlaceholder.id },\n { userId },\n );\n } catch (error) {\n logger.error(\"Failed to enqueue Newton response\", {\n error,\n newtonChatId,\n studentMessageId: result.id,\n conversationId: newtonChat.conversationId,\n });\n }\n }\n\n return {\n id: result.id,\n content: result.content,\n senderId: result.senderId,\n conversationId: result.conversationId,\n status: result.status,\n createdAt: result.createdAt,\n sender: result.sender,\n attachments: result.attachments ?? [],\n mentionedUserIds,\n };\n}\n"],"names":[],"mappings":"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","debug_id":"0575644e-e077-5f74-8b0c-a6feb71814e9"}
|
|
1
|
+
{"version":3,"file":"newtonChat.js","sources":["services/newtonChat.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Newton chat service – AI tutor conversations for assignments. Get or create\n * tutor session, post messages, trigger AI responses. Broadcasts via Pusher.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { logger } from \"../utils/logger.js\";\nimport { chatChannel, pusher } from \"../lib/pusher.js\";\nimport { isAIUser } from \"../utils/aiUser.js\";\nimport { createPendingAIMessage } from \"../utils/inference.js\";\nimport { enqueueRunJob } from \"../workers/queues.js\";\nimport {\n claimMessageAttachments,\n ensureMessageHasContentOrAttachments,\n} from \"./message.js\";\nimport { messageFileSelect } from \"../models/message.js\";\nimport {\n findClassWithStudent,\n findSubmissionByAssignmentAndStudent,\n findNewtonChatByIdAndMember,\n findConversationMembersByUserIds,\n} from \"../models/newtonChat.js\";\nimport { prisma } from \"../lib/prisma.js\";\n\n/** Get or create Newton tutor conversation for a submission. Student-only. */\nexport async function getTutorConversation(\n userId: string,\n assignmentId: string,\n classId: string\n) {\n const classMembership = await findClassWithStudent(classId, userId);\n if (!classMembership) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Not a student in this class\",\n });\n }\n\n const submission = await findSubmissionByAssignmentAndStudent(\n assignmentId,\n userId\n );\n if (!submission) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Submission not found. Please create a submission first.\",\n });\n }\n\n const { chat, isNew } = await prisma.$transaction(async (tx) => {\n const existingNewtonChat = await tx.newtonChat.findFirst({\n where: { submissionId: submission.id },\n include: {\n conversation: {\n include: {\n members: {\n where: { userId },\n },\n },\n },\n },\n orderBy: { createdAt: \"desc\" },\n });\n\n if (existingNewtonChat && existingNewtonChat.conversation.members.length > 0) {\n return { chat: existingNewtonChat, isNew: false };\n }\n\n if (existingNewtonChat) {\n await tx.conversationMember.create({\n data: {\n userId,\n conversationId: existingNewtonChat.conversationId,\n role: \"MEMBER\",\n },\n });\n return { chat: existingNewtonChat, isNew: false };\n }\n\n const conversation = await tx.conversation.create({\n data: {\n type: \"DM\",\n name: \"Session with Newton Tutor\",\n displayInChat: false,\n },\n });\n\n await tx.conversationMember.create({\n data: {\n userId,\n conversationId: conversation.id,\n role: \"MEMBER\",\n },\n });\n\n const newtonChat = await tx.newtonChat.create({\n data: {\n submissionId: submission.id,\n conversationId: conversation.id,\n title: \"Session with Newton Tutor\",\n },\n });\n\n return { chat: newtonChat, isNew: true };\n });\n\n if (isNew) {\n // Create the AI introduction placeholder and enqueue OUTSIDE the\n // transaction — a tx rollback would otherwise orphan a job pointing at\n // a non-existent NewtonChat / Message row.\n try {\n const aiPlaceholder = await createPendingAIMessage(chat.conversationId);\n await enqueueRunJob(\n \"AI_NEWTON_CHAT\",\n { conversationId: chat.conversationId, aiMessageId: aiPlaceholder.id },\n { userId },\n );\n } catch (error) {\n logger.error(\"Failed to enqueue Newton introduction\", {\n error,\n newtonChatId: chat.id,\n });\n }\n }\n\n return {\n conversationId: chat.conversationId,\n newtonChatId: chat.id,\n };\n}\n\nexport async function postToNewtonChat(\n userId: string,\n newtonChatId: string,\n content: string,\n mentionedUserIds: string[] = [],\n fileIds: string[] = []\n) {\n const newtonChat = await findNewtonChatByIdAndMember(newtonChatId, userId);\n if (!newtonChat) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Newton chat not found or access denied\",\n });\n }\n\n if (mentionedUserIds.length > 0) {\n const mentionedMemberships = await findConversationMembersByUserIds(\n newtonChat.conversationId,\n mentionedUserIds\n );\n if (mentionedMemberships.length !== mentionedUserIds.length) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Some mentioned users are not members of this conversation\",\n });\n }\n }\n\n ensureMessageHasContentOrAttachments(content, fileIds);\n const normalizedContent = content.trim();\n\n const result = await prisma.$transaction(async (tx) => {\n const message = await tx.message.create({\n data: {\n content: normalizedContent,\n senderId: userId,\n conversationId: newtonChat.conversationId,\n },\n });\n\n await claimMessageAttachments(\n tx,\n userId,\n newtonChat.conversationId,\n message.id,\n fileIds\n );\n\n if (mentionedUserIds.length > 0) {\n await tx.mention.createMany({\n data: mentionedUserIds.map((mentionedUserId) => ({\n messageId: message.id,\n userId: mentionedUserId,\n })),\n });\n }\n\n await tx.newtonChat.update({\n where: { id: newtonChatId },\n data: { updatedAt: new Date() },\n });\n\n return tx.message.findUniqueOrThrow({\n where: { id: message.id },\n include: {\n sender: {\n select: {\n id: true,\n username: true,\n profile: {\n select: {\n displayName: true,\n profilePicture: true,\n },\n },\n },\n },\n attachments: {\n select: messageFileSelect,\n },\n },\n });\n });\n\n try {\n await pusher.trigger(\n chatChannel(newtonChat.conversationId),\n \"new-message\",\n {\n id: result.id,\n content: result.content,\n senderId: result.senderId,\n conversationId: result.conversationId,\n status: result.status,\n createdAt: result.createdAt,\n sender: result.sender,\n attachments: result.attachments ?? [],\n mentionedUserIds,\n meta: {},\n }\n );\n } catch (error) {\n console.error(\"Failed to broadcast newton chat message:\", error);\n }\n\n if (!isAIUser(userId)) {\n // Create the AI placeholder post-tx (so a rollback can't broadcast a\n // ghost bubble); the pipeline fills it in.\n try {\n const aiPlaceholder = await createPendingAIMessage(newtonChat.conversationId);\n await enqueueRunJob(\n \"AI_NEWTON_CHAT\",\n { conversationId: newtonChat.conversationId, aiMessageId: aiPlaceholder.id },\n { userId },\n );\n } catch (error) {\n logger.error(\"Failed to enqueue Newton response\", {\n error,\n newtonChatId,\n studentMessageId: result.id,\n conversationId: newtonChat.conversationId,\n });\n }\n }\n\n return {\n id: result.id,\n content: result.content,\n senderId: result.senderId,\n conversationId: result.conversationId,\n status: result.status,\n createdAt: result.createdAt,\n sender: result.sender,\n attachments: result.attachments ?? [],\n mentionedUserIds,\n };\n}\n"],"names":[],"mappings":"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","debug_id":"39ab2fff-5212-508b-bb38-849fb5430153"}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { Response } from 'openai/resources/responses/responses.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extensions OpenAI Code Interpreter can read/write in its sandbox.
|
|
4
|
+
* @see https://developers.openai.com/api/docs/guides/tools-code-interpreter
|
|
5
|
+
*/
|
|
2
6
|
export declare const SUPPORTED_DOCUMENT_FORMATS: readonly ["pdf", "docx", "doc", "xlsx", "pptx", "csv", "md", "html", "txt", "json", "xml", "tex", "png", "jpeg", "jpg", "gif", "zip", "tar", "py", "js", "ts", "css", "sh"];
|
|
3
7
|
export type DocumentFormat = (typeof SUPPORTED_DOCUMENT_FORMATS)[number];
|
|
4
8
|
export interface GenerateDocumentInput {
|
|
5
9
|
title: string;
|
|
10
|
+
/** Markdown or plain-text source material for the output file. */
|
|
6
11
|
content: string;
|
|
12
|
+
/** Output format. Defaults to pdf. */
|
|
7
13
|
format?: DocumentFormat | string;
|
|
8
14
|
}
|
|
9
15
|
export interface GeneratedDocumentResult {
|
|
@@ -19,7 +25,13 @@ export interface ContainerGeneratedFile {
|
|
|
19
25
|
}
|
|
20
26
|
export declare function normalizeDocumentFormat(format?: string | null): DocumentFormat;
|
|
21
27
|
export declare function mimeTypeForFormat(format: DocumentFormat): string;
|
|
28
|
+
export declare function extensionForFormat(format: DocumentFormat): string;
|
|
29
|
+
/** Pull generated-file citations for a given extension out of a Responses output. */
|
|
22
30
|
export declare function extractContainerGeneratedFiles(response: Response, extension: DocumentFormat): ContainerGeneratedFile[];
|
|
23
31
|
/** Generate a file via OpenAI Code Interpreter. */
|
|
24
32
|
export declare function generateDocumentWithOpenAI(input: GenerateDocumentInput): Promise<GeneratedDocumentResult | null>;
|
|
33
|
+
/** @deprecated Use {@link generateDocumentWithOpenAI} */
|
|
34
|
+
export declare function generatePdfWithOpenAI(input: Omit<GenerateDocumentInput, 'format'>): Promise<Buffer | null>;
|
|
35
|
+
/** @deprecated Use {@link extractContainerGeneratedFiles} */
|
|
36
|
+
export declare const extractContainerPdfFiles: (response: Response) => ContainerGeneratedFile[];
|
|
25
37
|
//# sourceMappingURL=openaiDocumentGen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openaiDocumentGen.d.ts","sourceRoot":"/","sources":["services/openaiDocumentGen.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yCAAyC,CAAC;AAKxE,eAAO,MAAM,0BAA0B,
|
|
1
|
+
{"version":3,"file":"openaiDocumentGen.d.ts","sourceRoot":"/","sources":["services/openaiDocumentGen.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yCAAyC,CAAC;AAKxE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,6KAwB7B,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AA6FzE,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,uBAAuB,CACrC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,cAAc,CAMhB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAEhE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAEjE;AAED,qFAAqF;AACrF,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,cAAc,GACxB,sBAAsB,EAAE,CA2B1B;AA6ED,mDAAmD;AACnD,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAiEzC;AAED,yDAAyD;AACzD,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,QAAQ,CAAC,GAC3C,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAED,6DAA6D;AAC7D,eAAO,MAAM,wBAAwB,GAAI,UAAU,QAAQ,6BACV,CAAC"}
|
|
@@ -3,14 +3,38 @@
|
|
|
3
3
|
* Supports every format the sandbox can produce (pdf, docx, xlsx, pptx, …).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
6
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d086b281-e191-5157-a799-843675a76a37")}catch(e){}}();
|
|
7
7
|
import OpenAI from 'openai';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
|
-
const DOCUMENT_GEN_MODEL = 'gpt-
|
|
9
|
+
const DOCUMENT_GEN_MODEL = 'gpt-5.5';
|
|
10
|
+
/**
|
|
11
|
+
* Extensions OpenAI Code Interpreter can read/write in its sandbox.
|
|
12
|
+
* @see https://developers.openai.com/api/docs/guides/tools-code-interpreter
|
|
13
|
+
*/
|
|
10
14
|
export const SUPPORTED_DOCUMENT_FORMATS = [
|
|
11
|
-
'pdf',
|
|
12
|
-
'
|
|
13
|
-
'
|
|
15
|
+
'pdf',
|
|
16
|
+
'docx',
|
|
17
|
+
'doc',
|
|
18
|
+
'xlsx',
|
|
19
|
+
'pptx',
|
|
20
|
+
'csv',
|
|
21
|
+
'md',
|
|
22
|
+
'html',
|
|
23
|
+
'txt',
|
|
24
|
+
'json',
|
|
25
|
+
'xml',
|
|
26
|
+
'tex',
|
|
27
|
+
'png',
|
|
28
|
+
'jpeg',
|
|
29
|
+
'jpg',
|
|
30
|
+
'gif',
|
|
31
|
+
'zip',
|
|
32
|
+
'tar',
|
|
33
|
+
'py',
|
|
34
|
+
'js',
|
|
35
|
+
'ts',
|
|
36
|
+
'css',
|
|
37
|
+
'sh',
|
|
14
38
|
];
|
|
15
39
|
const FORMAT_MIME = {
|
|
16
40
|
pdf: 'application/pdf',
|
|
@@ -38,29 +62,67 @@ const FORMAT_MIME = {
|
|
|
38
62
|
sh: 'application/x-sh',
|
|
39
63
|
};
|
|
40
64
|
const FORMAT_HINTS = {
|
|
41
|
-
pdf: 'fpdf2 or reportlab',
|
|
42
|
-
docx: 'python-docx',
|
|
43
|
-
doc: 'python-docx',
|
|
44
|
-
xlsx: 'openpyxl or pandas',
|
|
45
|
-
pptx: 'python-pptx',
|
|
46
|
-
csv: 'pandas or csv module',
|
|
47
|
-
md: 'write markdown directly',
|
|
48
|
-
html: 'write a complete HTML page',
|
|
49
|
-
txt: 'write plain text',
|
|
50
|
-
json: 'json.dumps with indentation',
|
|
51
|
-
xml: 'well-formed XML',
|
|
52
|
-
tex: 'LaTeX source',
|
|
53
|
-
png: 'matplotlib or Pillow',
|
|
54
|
-
jpeg: 'matplotlib or Pillow',
|
|
55
|
-
jpg: 'matplotlib or Pillow',
|
|
56
|
-
gif: 'matplotlib or Pillow',
|
|
57
|
-
zip: 'zipfile',
|
|
58
|
-
tar: 'tarfile',
|
|
59
|
-
py: 'Python source',
|
|
60
|
-
js: 'JavaScript source',
|
|
61
|
-
ts: 'TypeScript source',
|
|
62
|
-
css: '
|
|
63
|
-
sh: 'shell script',
|
|
65
|
+
pdf: 'fpdf2 or reportlab — polished document with headings, body text, and tables',
|
|
66
|
+
docx: 'python-docx — Word document with heading styles, paragraphs, and lists',
|
|
67
|
+
doc: 'python-docx saving as .doc if supported, otherwise .docx',
|
|
68
|
+
xlsx: 'openpyxl or pandas — spreadsheet with labelled columns, formulas where useful',
|
|
69
|
+
pptx: 'python-pptx — full slide deck: title slide, section slides, bullets, diagrams on slides',
|
|
70
|
+
csv: 'pandas or csv module — tabular data with headers',
|
|
71
|
+
md: 'write the markdown file directly',
|
|
72
|
+
html: 'write a complete, styled HTML page',
|
|
73
|
+
txt: 'write plain text with clear section breaks',
|
|
74
|
+
json: 'json.dumps with readable indentation',
|
|
75
|
+
xml: 'well-formed XML document',
|
|
76
|
+
tex: 'LaTeX source that compiles to the requested content',
|
|
77
|
+
png: 'matplotlib, networkx, or Pillow — render the described chart/diagram as a finished image',
|
|
78
|
+
jpeg: 'matplotlib or Pillow — high-quality visual saved as JPEG',
|
|
79
|
+
jpg: 'matplotlib or Pillow — high-quality visual saved as JPEG',
|
|
80
|
+
gif: 'matplotlib or Pillow — save as GIF',
|
|
81
|
+
zip: 'zipfile — bundle multiple related files if needed',
|
|
82
|
+
tar: 'tarfile — archive if multiple files are required',
|
|
83
|
+
py: 'valid, runnable Python source with comments',
|
|
84
|
+
js: 'valid JavaScript source with comments',
|
|
85
|
+
ts: 'valid TypeScript source with comments',
|
|
86
|
+
css: 'stylesheet file',
|
|
87
|
+
sh: 'shell script file',
|
|
88
|
+
};
|
|
89
|
+
/** Extra quality rules appended for formats that need more than a one-line library hint. */
|
|
90
|
+
const FORMAT_QUALITY_RULES = {
|
|
91
|
+
png: [
|
|
92
|
+
'Interpret the source content as a visual spec — draw it, do not dump text into the image.',
|
|
93
|
+
'Charts: labelled axes, legend, title, readable font sizes, 150+ DPI.',
|
|
94
|
+
'Diagrams/flowcharts: use matplotlib patches, networkx, or Pillow drawing — boxes, arrows, labels.',
|
|
95
|
+
'Geometry/maps: accurate relative positions; label every important part.',
|
|
96
|
+
'Use color meaningfully; avoid blank or placeholder images.',
|
|
97
|
+
],
|
|
98
|
+
jpeg: [
|
|
99
|
+
'Same visual quality rules as PNG — polished chart or diagram, not a screenshot of text.',
|
|
100
|
+
],
|
|
101
|
+
jpg: [
|
|
102
|
+
'Same visual quality rules as PNG — polished chart or diagram, not a screenshot of text.',
|
|
103
|
+
],
|
|
104
|
+
gif: [
|
|
105
|
+
'Render a clear educational visual; static GIF is fine unless animation is described.',
|
|
106
|
+
],
|
|
107
|
+
pptx: [
|
|
108
|
+
'Split slides on "---" lines or "Slide N:" / "# " headings in the source content.',
|
|
109
|
+
'Title slide first, then content slides with concise bullets (not walls of text).',
|
|
110
|
+
'If the content describes a diagram, draw it on a slide (shapes/arrows) or embed a generated chart.',
|
|
111
|
+
'Use distinct slide layouts where appropriate (title, title+content).',
|
|
112
|
+
],
|
|
113
|
+
xlsx: [
|
|
114
|
+
'Parse markdown tables or tabular lists in the source into real rows/columns.',
|
|
115
|
+
'Bold header row; auto-fit column widths where possible.',
|
|
116
|
+
],
|
|
117
|
+
pdf: [
|
|
118
|
+
'If the source describes a figure/chart, generate and embed it — do not leave a "[diagram]" placeholder.',
|
|
119
|
+
],
|
|
120
|
+
docx: [
|
|
121
|
+
'Use Heading 1/2 styles for section titles; bullets and numbered lists for enumerations.',
|
|
122
|
+
],
|
|
123
|
+
html: [
|
|
124
|
+
'Include embedded CSS for readable typography; use tables or simple layout for structured content.',
|
|
125
|
+
],
|
|
64
126
|
};
|
|
65
127
|
export function normalizeDocumentFormat(format) {
|
|
66
128
|
const raw = (format ?? 'pdf').toLowerCase().replace(/^\./, '');
|
|
@@ -72,10 +134,21 @@ export function normalizeDocumentFormat(format) {
|
|
|
72
134
|
export function mimeTypeForFormat(format) {
|
|
73
135
|
return FORMAT_MIME[format];
|
|
74
136
|
}
|
|
137
|
+
export function extensionForFormat(format) {
|
|
138
|
+
return format === 'jpg' ? 'jpg' : format;
|
|
139
|
+
}
|
|
140
|
+
/** Pull generated-file citations for a given extension out of a Responses output. */
|
|
75
141
|
export function extractContainerGeneratedFiles(response, extension) {
|
|
76
|
-
const suffix = `.${extension}`;
|
|
77
142
|
const seen = new Set();
|
|
78
143
|
const files = [];
|
|
144
|
+
const suffix = `.${extension}`;
|
|
145
|
+
const add = (containerId, fileId, filename) => {
|
|
146
|
+
const key = `${containerId}:${fileId}`;
|
|
147
|
+
if (seen.has(key))
|
|
148
|
+
return;
|
|
149
|
+
seen.add(key);
|
|
150
|
+
files.push({ containerId, fileId, filename });
|
|
151
|
+
};
|
|
79
152
|
for (const item of response.output ?? []) {
|
|
80
153
|
if (item.type !== 'message')
|
|
81
154
|
continue;
|
|
@@ -86,17 +159,9 @@ export function extractContainerGeneratedFiles(response, extension) {
|
|
|
86
159
|
if (ann.type !== 'container_file_citation')
|
|
87
160
|
continue;
|
|
88
161
|
const name = ann.filename?.toLowerCase() ?? '';
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (seen.has(key))
|
|
93
|
-
continue;
|
|
94
|
-
seen.add(key);
|
|
95
|
-
files.push({
|
|
96
|
-
containerId: ann.container_id,
|
|
97
|
-
fileId: ann.file_id,
|
|
98
|
-
filename: ann.filename,
|
|
99
|
-
});
|
|
162
|
+
if (name.endsWith(suffix)) {
|
|
163
|
+
add(ann.container_id, ann.file_id, ann.filename);
|
|
164
|
+
}
|
|
100
165
|
}
|
|
101
166
|
}
|
|
102
167
|
}
|
|
@@ -125,38 +190,70 @@ async function listContainerGeneratedFiles(client, containerId, extension) {
|
|
|
125
190
|
}
|
|
126
191
|
return files;
|
|
127
192
|
}
|
|
193
|
+
async function downloadContainerFile(client, file) {
|
|
194
|
+
try {
|
|
195
|
+
const res = await client.containers.files.content.retrieve(file.fileId, {
|
|
196
|
+
container_id: file.containerId,
|
|
197
|
+
});
|
|
198
|
+
const bytes = await res.arrayBuffer();
|
|
199
|
+
return Buffer.from(bytes);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
logger.error('Failed to download container file', { error, file });
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
128
206
|
function safeBasename(title) {
|
|
129
|
-
|
|
207
|
+
const base = title
|
|
130
208
|
.replace(/[^a-zA-Z0-9\s\-_]/g, '')
|
|
131
209
|
.replace(/\s+/g, '_')
|
|
132
|
-
.substring(0, 60)
|
|
210
|
+
.substring(0, 60);
|
|
211
|
+
return base || 'document';
|
|
212
|
+
}
|
|
213
|
+
function buildDocumentGenInstructions(format) {
|
|
214
|
+
const qualityRules = FORMAT_QUALITY_RULES[format] ?? [];
|
|
215
|
+
return [
|
|
216
|
+
'You are a document generator for K-12 and university teachers.',
|
|
217
|
+
'When given a title, output format, and source content:',
|
|
218
|
+
'1. Use the python tool to create exactly ONE output file of the requested type.',
|
|
219
|
+
`2. For this format use: ${FORMAT_HINTS[format]}.`,
|
|
220
|
+
`3. You MUST output a ${format.toUpperCase()} file — not any other format.`,
|
|
221
|
+
`4. The saved filename MUST end with .${extensionForFormat(format)}.`,
|
|
222
|
+
'5. Faithfully adapt ALL source content to the output medium — do not summarize away detail.',
|
|
223
|
+
'6. Include the document title prominently where the format allows.',
|
|
224
|
+
'7. Put ONLY the educational content in the file — no meta commentary.',
|
|
225
|
+
'8. You MUST save the file to disk in the working directory before you finish.',
|
|
226
|
+
...(format !== 'pdf'
|
|
227
|
+
? [`9. Do NOT create a PDF when ${format.toUpperCase()} was requested.`]
|
|
228
|
+
: []),
|
|
229
|
+
...(qualityRules.length > 0
|
|
230
|
+
? ['', 'Format-specific quality:', ...qualityRules.map((r, i) => `${i + 1}. ${r}`)]
|
|
231
|
+
: []),
|
|
232
|
+
].join('\n');
|
|
133
233
|
}
|
|
134
234
|
/** Generate a file via OpenAI Code Interpreter. */
|
|
135
235
|
export async function generateDocumentWithOpenAI(input) {
|
|
136
236
|
const client = new OpenAI();
|
|
137
237
|
const format = normalizeDocumentFormat(input.format);
|
|
138
|
-
const ext = format
|
|
238
|
+
const ext = extensionForFormat(format);
|
|
139
239
|
const filename = `${safeBasename(input.title)}.${ext}`;
|
|
140
240
|
try {
|
|
141
241
|
const response = await client.responses.create({
|
|
142
242
|
model: DOCUMENT_GEN_MODEL,
|
|
143
|
-
instructions:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
...(format !== 'pdf' ? [`Do NOT create a PDF when ${ext.toUpperCase()} was requested.`] : []),
|
|
151
|
-
].join(' '),
|
|
152
|
-
tools: [{ type: 'code_interpreter', container: { type: 'auto' } }],
|
|
243
|
+
instructions: buildDocumentGenInstructions(format),
|
|
244
|
+
tools: [
|
|
245
|
+
{
|
|
246
|
+
type: 'code_interpreter',
|
|
247
|
+
container: { type: 'auto' },
|
|
248
|
+
},
|
|
249
|
+
],
|
|
153
250
|
tool_choice: 'required',
|
|
154
251
|
input: [
|
|
155
|
-
`Create a ${ext.toUpperCase()} file.`,
|
|
156
|
-
`
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
input.content,
|
|
252
|
+
`Create a ${ext.toUpperCase()} file using the python tool.`,
|
|
253
|
+
`Output filename (exact): ${filename}`,
|
|
254
|
+
`Output format: ${ext}`,
|
|
255
|
+
`Document title: ${input.title}`,
|
|
256
|
+
['Source content — adapt fully to the output medium (do not paste raw markdown into slides/images):', input.content].join('\n'),
|
|
160
257
|
].join('\n\n'),
|
|
161
258
|
});
|
|
162
259
|
let matches = extractContainerGeneratedFiles(response, format);
|
|
@@ -167,18 +264,18 @@ export async function generateDocumentWithOpenAI(input) {
|
|
|
167
264
|
}
|
|
168
265
|
}
|
|
169
266
|
if (matches.length === 0) {
|
|
170
|
-
logger.error('OpenAI document gen
|
|
267
|
+
logger.error('OpenAI document gen produced no matching files', {
|
|
171
268
|
title: input.title,
|
|
172
269
|
format,
|
|
270
|
+
outputTypes: (response.output ?? []).map((o) => o.type),
|
|
173
271
|
});
|
|
174
272
|
return null;
|
|
175
273
|
}
|
|
176
274
|
const preferred = matches.find((f) => f.filename.toLowerCase() === filename.toLowerCase()) ??
|
|
177
275
|
matches[0];
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
276
|
+
const bytes = await downloadContainerFile(client, preferred);
|
|
277
|
+
if (!bytes)
|
|
278
|
+
return null;
|
|
182
279
|
return {
|
|
183
280
|
bytes,
|
|
184
281
|
filename: preferred.filename,
|
|
@@ -187,9 +284,20 @@ export async function generateDocumentWithOpenAI(input) {
|
|
|
187
284
|
};
|
|
188
285
|
}
|
|
189
286
|
catch (error) {
|
|
190
|
-
logger.error('OpenAI document generation failed', {
|
|
287
|
+
logger.error('OpenAI document generation failed', {
|
|
288
|
+
error,
|
|
289
|
+
title: input.title,
|
|
290
|
+
format,
|
|
291
|
+
});
|
|
191
292
|
return null;
|
|
192
293
|
}
|
|
193
294
|
}
|
|
295
|
+
/** @deprecated Use {@link generateDocumentWithOpenAI} */
|
|
296
|
+
export async function generatePdfWithOpenAI(input) {
|
|
297
|
+
const result = await generateDocumentWithOpenAI({ ...input, format: 'pdf' });
|
|
298
|
+
return result?.bytes ?? null;
|
|
299
|
+
}
|
|
300
|
+
/** @deprecated Use {@link extractContainerGeneratedFiles} */
|
|
301
|
+
export const extractContainerPdfFiles = (response) => extractContainerGeneratedFiles(response, 'pdf');
|
|
194
302
|
//# sourceMappingURL=openaiDocumentGen.js.map
|
|
195
|
-
//# debugId=
|
|
303
|
+
//# debugId=d086b281-e191-5157-a799-843675a76a37
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openaiDocumentGen.js","sources":["services/openaiDocumentGen.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Generate files via OpenAI Code Interpreter (Responses API).\n * Supports every format the sandbox can produce (pdf, docx, xlsx, pptx, …).\n */\nimport OpenAI from 'openai';\nimport type { Response } from 'openai/resources/responses/responses.js';\nimport { logger } from '../utils/logger.js';\n\nconst DOCUMENT_GEN_MODEL = 'gpt-4o-mini';\n\nexport const SUPPORTED_DOCUMENT_FORMATS = [\n 'pdf', 'docx', 'doc', 'xlsx', 'pptx', 'csv', 'md', 'html', 'txt', 'json',\n 'xml', 'tex', 'png', 'jpeg', 'jpg', 'gif', 'zip', 'tar', 'py', 'js', 'ts',\n 'css', 'sh',\n] as const;\n\nexport type DocumentFormat = (typeof SUPPORTED_DOCUMENT_FORMATS)[number];\n\nconst FORMAT_MIME: Record<DocumentFormat, string> = {\n pdf: 'application/pdf',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n doc: 'application/msword',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n csv: 'text/csv',\n md: 'text/markdown',\n html: 'text/html',\n txt: 'text/plain',\n json: 'application/json',\n xml: 'application/xml',\n tex: 'text/x-tex',\n png: 'image/png',\n jpeg: 'image/jpeg',\n jpg: 'image/jpeg',\n gif: 'image/gif',\n zip: 'application/zip',\n tar: 'application/x-tar',\n py: 'text/x-python',\n js: 'text/javascript',\n ts: 'application/typescript',\n css: 'text/css',\n sh: 'application/x-sh',\n};\n\nconst FORMAT_HINTS: Record<DocumentFormat, string> = {\n pdf: 'fpdf2 or reportlab',\n docx: 'python-docx',\n doc: 'python-docx',\n xlsx: 'openpyxl or pandas',\n pptx: 'python-pptx',\n csv: 'pandas or csv module',\n md: 'write markdown directly',\n html: 'write a complete HTML page',\n txt: 'write plain text',\n json: 'json.dumps with indentation',\n xml: 'well-formed XML',\n tex: 'LaTeX source',\n png: 'matplotlib or Pillow',\n jpeg: 'matplotlib or Pillow',\n jpg: 'matplotlib or Pillow',\n gif: 'matplotlib or Pillow',\n zip: 'zipfile',\n tar: 'tarfile',\n py: 'Python source',\n js: 'JavaScript source',\n ts: 'TypeScript source',\n css: 'CSS stylesheet',\n sh: 'shell script',\n};\n\nexport interface GenerateDocumentInput {\n title: string;\n content: string;\n format?: DocumentFormat | string;\n}\n\nexport interface GeneratedDocumentResult {\n bytes: Buffer;\n filename: string;\n mimeType: string;\n format: DocumentFormat;\n}\n\nexport interface ContainerGeneratedFile {\n containerId: string;\n fileId: string;\n filename: string;\n}\n\nexport function normalizeDocumentFormat(format?: string | null): DocumentFormat {\n const raw = (format ?? 'pdf').toLowerCase().replace(/^\\./, '');\n if ((SUPPORTED_DOCUMENT_FORMATS as readonly string[]).includes(raw)) {\n return raw as DocumentFormat;\n }\n return 'pdf';\n}\n\nexport function mimeTypeForFormat(format: DocumentFormat): string {\n return FORMAT_MIME[format];\n}\n\nexport function extractContainerGeneratedFiles(\n response: Response,\n extension: DocumentFormat,\n): ContainerGeneratedFile[] {\n const suffix = `.${extension}`;\n const seen = new Set<string>();\n const files: ContainerGeneratedFile[] = [];\n\n for (const item of response.output ?? []) {\n if (item.type !== 'message') continue;\n for (const block of item.content ?? []) {\n if (block.type !== 'output_text') continue;\n for (const ann of block.annotations ?? []) {\n if (ann.type !== 'container_file_citation') continue;\n const name = ann.filename?.toLowerCase() ?? '';\n if (!name.endsWith(suffix)) continue;\n const key = `${ann.container_id}:${ann.file_id}`;\n if (seen.has(key)) continue;\n seen.add(key);\n files.push({\n containerId: ann.container_id,\n fileId: ann.file_id,\n filename: ann.filename,\n });\n }\n }\n }\n return files;\n}\n\nfunction containerIdFromResponse(response: Response): string | undefined {\n for (const item of response.output ?? []) {\n if (item.type === 'code_interpreter_call' && item.container_id) {\n return item.container_id;\n }\n }\n return undefined;\n}\n\nasync function listContainerGeneratedFiles(\n client: OpenAI,\n containerId: string,\n extension: DocumentFormat,\n): Promise<ContainerGeneratedFile[]> {\n const suffix = `.${extension}`;\n const files: ContainerGeneratedFile[] = [];\n for await (const file of client.containers.files.list(containerId)) {\n const path = file.path?.toLowerCase() ?? '';\n if (path.endsWith(suffix) && file.source === 'assistant') {\n files.push({\n containerId,\n fileId: file.id,\n filename: file.path.split('/').pop() ?? file.path,\n });\n }\n }\n return files;\n}\n\nfunction safeBasename(title: string): string {\n return (\n title\n .replace(/[^a-zA-Z0-9\\s\\-_]/g, '')\n .replace(/\\s+/g, '_')\n .substring(0, 60) || 'document'\n );\n}\n\n/** Generate a file via OpenAI Code Interpreter. */\nexport async function generateDocumentWithOpenAI(\n input: GenerateDocumentInput,\n): Promise<GeneratedDocumentResult | null> {\n const client = new OpenAI();\n const format = normalizeDocumentFormat(input.format);\n const ext = format === 'jpg' ? 'jpg' : format;\n const filename = `${safeBasename(input.title)}.${ext}`;\n\n try {\n const response = await client.responses.create({\n model: DOCUMENT_GEN_MODEL,\n instructions: [\n 'You generate classroom-ready files for teachers using the python tool.',\n `You MUST output a ${ext.toUpperCase()} file — not any other format.`,\n `Use: ${FORMAT_HINTS[format]}.`,\n `The saved filename MUST end with .${ext}.`,\n 'Save exactly one output file with the requested filename.',\n 'Include only educational content — no chat commentary.',\n ...(format !== 'pdf' ? [`Do NOT create a PDF when ${ext.toUpperCase()} was requested.`] : []),\n ].join(' '),\n tools: [{ type: 'code_interpreter', container: { type: 'auto' } }],\n tool_choice: 'required',\n input: [\n `Create a ${ext.toUpperCase()} file.`,\n `Filename (exact): ${filename}`,\n `Title: ${input.title}`,\n 'Content:',\n input.content,\n ].join('\\n\\n'),\n });\n\n let matches = extractContainerGeneratedFiles(response, format);\n if (matches.length === 0) {\n const containerId = containerIdFromResponse(response);\n if (containerId) {\n matches = await listContainerGeneratedFiles(client, containerId, format);\n }\n }\n\n if (matches.length === 0) {\n logger.error('OpenAI document gen: no output file', {\n title: input.title,\n format,\n });\n return null;\n }\n\n const preferred =\n matches.find((f) => f.filename.toLowerCase() === filename.toLowerCase()) ??\n matches[0];\n\n const res = await client.containers.files.content.retrieve(preferred.fileId, {\n container_id: preferred.containerId,\n });\n const bytes = Buffer.from(await res.arrayBuffer());\n\n return {\n bytes,\n filename: preferred.filename,\n mimeType: mimeTypeForFormat(format),\n format,\n };\n } catch (error) {\n logger.error('OpenAI document generation failed', { error, title: input.title, format });\n return null;\n }\n}\n"],"names":[],"mappings":"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","debug_id":"cd65973d-ca13-5498-8d6b-ff056b093631"}
|
|
1
|
+
{"version":3,"file":"openaiDocumentGen.js","sources":["services/openaiDocumentGen.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Generate files via OpenAI Code Interpreter (Responses API).\n * Supports every format the sandbox can produce (pdf, docx, xlsx, pptx, …).\n */\nimport OpenAI from 'openai';\nimport type { Response } from 'openai/resources/responses/responses.js';\nimport { logger } from '../utils/logger.js';\n\nconst DOCUMENT_GEN_MODEL = 'gpt-5.5';\n\n/**\n * Extensions OpenAI Code Interpreter can read/write in its sandbox.\n * @see https://developers.openai.com/api/docs/guides/tools-code-interpreter\n */\nexport const SUPPORTED_DOCUMENT_FORMATS = [\n 'pdf',\n 'docx',\n 'doc',\n 'xlsx',\n 'pptx',\n 'csv',\n 'md',\n 'html',\n 'txt',\n 'json',\n 'xml',\n 'tex',\n 'png',\n 'jpeg',\n 'jpg',\n 'gif',\n 'zip',\n 'tar',\n 'py',\n 'js',\n 'ts',\n 'css',\n 'sh',\n] as const;\n\nexport type DocumentFormat = (typeof SUPPORTED_DOCUMENT_FORMATS)[number];\n\nconst FORMAT_MIME: Record<DocumentFormat, string> = {\n pdf: 'application/pdf',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n doc: 'application/msword',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n csv: 'text/csv',\n md: 'text/markdown',\n html: 'text/html',\n txt: 'text/plain',\n json: 'application/json',\n xml: 'application/xml',\n tex: 'text/x-tex',\n png: 'image/png',\n jpeg: 'image/jpeg',\n jpg: 'image/jpeg',\n gif: 'image/gif',\n zip: 'application/zip',\n tar: 'application/x-tar',\n py: 'text/x-python',\n js: 'text/javascript',\n ts: 'application/typescript',\n css: 'text/css',\n sh: 'application/x-sh',\n};\n\nconst FORMAT_HINTS: Record<DocumentFormat, string> = {\n pdf: 'fpdf2 or reportlab — polished document with headings, body text, and tables',\n docx: 'python-docx — Word document with heading styles, paragraphs, and lists',\n doc: 'python-docx saving as .doc if supported, otherwise .docx',\n xlsx: 'openpyxl or pandas — spreadsheet with labelled columns, formulas where useful',\n pptx: 'python-pptx — full slide deck: title slide, section slides, bullets, diagrams on slides',\n csv: 'pandas or csv module — tabular data with headers',\n md: 'write the markdown file directly',\n html: 'write a complete, styled HTML page',\n txt: 'write plain text with clear section breaks',\n json: 'json.dumps with readable indentation',\n xml: 'well-formed XML document',\n tex: 'LaTeX source that compiles to the requested content',\n png: 'matplotlib, networkx, or Pillow — render the described chart/diagram as a finished image',\n jpeg: 'matplotlib or Pillow — high-quality visual saved as JPEG',\n jpg: 'matplotlib or Pillow — high-quality visual saved as JPEG',\n gif: 'matplotlib or Pillow — save as GIF',\n zip: 'zipfile — bundle multiple related files if needed',\n tar: 'tarfile — archive if multiple files are required',\n py: 'valid, runnable Python source with comments',\n js: 'valid JavaScript source with comments',\n ts: 'valid TypeScript source with comments',\n css: 'stylesheet file',\n sh: 'shell script file',\n};\n\n/** Extra quality rules appended for formats that need more than a one-line library hint. */\nconst FORMAT_QUALITY_RULES: Partial<Record<DocumentFormat, string[]>> = {\n png: [\n 'Interpret the source content as a visual spec — draw it, do not dump text into the image.',\n 'Charts: labelled axes, legend, title, readable font sizes, 150+ DPI.',\n 'Diagrams/flowcharts: use matplotlib patches, networkx, or Pillow drawing — boxes, arrows, labels.',\n 'Geometry/maps: accurate relative positions; label every important part.',\n 'Use color meaningfully; avoid blank or placeholder images.',\n ],\n jpeg: [\n 'Same visual quality rules as PNG — polished chart or diagram, not a screenshot of text.',\n ],\n jpg: [\n 'Same visual quality rules as PNG — polished chart or diagram, not a screenshot of text.',\n ],\n gif: [\n 'Render a clear educational visual; static GIF is fine unless animation is described.',\n ],\n pptx: [\n 'Split slides on \"---\" lines or \"Slide N:\" / \"# \" headings in the source content.',\n 'Title slide first, then content slides with concise bullets (not walls of text).',\n 'If the content describes a diagram, draw it on a slide (shapes/arrows) or embed a generated chart.',\n 'Use distinct slide layouts where appropriate (title, title+content).',\n ],\n xlsx: [\n 'Parse markdown tables or tabular lists in the source into real rows/columns.',\n 'Bold header row; auto-fit column widths where possible.',\n ],\n pdf: [\n 'If the source describes a figure/chart, generate and embed it — do not leave a \"[diagram]\" placeholder.',\n ],\n docx: [\n 'Use Heading 1/2 styles for section titles; bullets and numbered lists for enumerations.',\n ],\n html: [\n 'Include embedded CSS for readable typography; use tables or simple layout for structured content.',\n ],\n};\n\nexport interface GenerateDocumentInput {\n title: string;\n /** Markdown or plain-text source material for the output file. */\n content: string;\n /** Output format. Defaults to pdf. */\n format?: DocumentFormat | string;\n}\n\nexport interface GeneratedDocumentResult {\n bytes: Buffer;\n filename: string;\n mimeType: string;\n format: DocumentFormat;\n}\n\nexport interface ContainerGeneratedFile {\n containerId: string;\n fileId: string;\n filename: string;\n}\n\nexport function normalizeDocumentFormat(\n format?: string | null,\n): DocumentFormat {\n const raw = (format ?? 'pdf').toLowerCase().replace(/^\\./, '');\n if ((SUPPORTED_DOCUMENT_FORMATS as readonly string[]).includes(raw)) {\n return raw as DocumentFormat;\n }\n return 'pdf';\n}\n\nexport function mimeTypeForFormat(format: DocumentFormat): string {\n return FORMAT_MIME[format];\n}\n\nexport function extensionForFormat(format: DocumentFormat): string {\n return format === 'jpg' ? 'jpg' : format;\n}\n\n/** Pull generated-file citations for a given extension out of a Responses output. */\nexport function extractContainerGeneratedFiles(\n response: Response,\n extension: DocumentFormat,\n): ContainerGeneratedFile[] {\n const seen = new Set<string>();\n const files: ContainerGeneratedFile[] = [];\n const suffix = `.${extension}`;\n\n const add = (containerId: string, fileId: string, filename: string) => {\n const key = `${containerId}:${fileId}`;\n if (seen.has(key)) return;\n seen.add(key);\n files.push({ containerId, fileId, filename });\n };\n\n for (const item of response.output ?? []) {\n if (item.type !== 'message') continue;\n for (const block of item.content ?? []) {\n if (block.type !== 'output_text') continue;\n for (const ann of block.annotations ?? []) {\n if (ann.type !== 'container_file_citation') continue;\n const name = ann.filename?.toLowerCase() ?? '';\n if (name.endsWith(suffix)) {\n add(ann.container_id, ann.file_id, ann.filename);\n }\n }\n }\n }\n\n return files;\n}\n\nfunction containerIdFromResponse(response: Response): string | undefined {\n for (const item of response.output ?? []) {\n if (item.type === 'code_interpreter_call' && item.container_id) {\n return item.container_id;\n }\n }\n return undefined;\n}\n\nasync function listContainerGeneratedFiles(\n client: OpenAI,\n containerId: string,\n extension: DocumentFormat,\n): Promise<ContainerGeneratedFile[]> {\n const suffix = `.${extension}`;\n const files: ContainerGeneratedFile[] = [];\n for await (const file of client.containers.files.list(containerId)) {\n const path = file.path?.toLowerCase() ?? '';\n if (path.endsWith(suffix) && file.source === 'assistant') {\n files.push({\n containerId,\n fileId: file.id,\n filename: file.path.split('/').pop() ?? file.path,\n });\n }\n }\n return files;\n}\n\nasync function downloadContainerFile(\n client: OpenAI,\n file: ContainerGeneratedFile,\n): Promise<Buffer | null> {\n try {\n const res = await client.containers.files.content.retrieve(file.fileId, {\n container_id: file.containerId,\n });\n const bytes = await res.arrayBuffer();\n return Buffer.from(bytes);\n } catch (error) {\n logger.error('Failed to download container file', { error, file });\n return null;\n }\n}\n\nfunction safeBasename(title: string): string {\n const base = title\n .replace(/[^a-zA-Z0-9\\s\\-_]/g, '')\n .replace(/\\s+/g, '_')\n .substring(0, 60);\n return base || 'document';\n}\n\nfunction buildDocumentGenInstructions(format: DocumentFormat): string {\n const qualityRules = FORMAT_QUALITY_RULES[format] ?? [];\n return [\n 'You are a document generator for K-12 and university teachers.',\n 'When given a title, output format, and source content:',\n '1. Use the python tool to create exactly ONE output file of the requested type.',\n `2. For this format use: ${FORMAT_HINTS[format]}.`,\n `3. You MUST output a ${format.toUpperCase()} file — not any other format.`,\n `4. The saved filename MUST end with .${extensionForFormat(format)}.`,\n '5. Faithfully adapt ALL source content to the output medium — do not summarize away detail.',\n '6. Include the document title prominently where the format allows.',\n '7. Put ONLY the educational content in the file — no meta commentary.',\n '8. You MUST save the file to disk in the working directory before you finish.',\n ...(format !== 'pdf'\n ? [`9. Do NOT create a PDF when ${format.toUpperCase()} was requested.`]\n : []),\n ...(qualityRules.length > 0\n ? ['', 'Format-specific quality:', ...qualityRules.map((r, i) => `${i + 1}. ${r}`)]\n : []),\n ].join('\\n');\n}\n\n/** Generate a file via OpenAI Code Interpreter. */\nexport async function generateDocumentWithOpenAI(\n input: GenerateDocumentInput,\n): Promise<GeneratedDocumentResult | null> {\n const client = new OpenAI();\n const format = normalizeDocumentFormat(input.format);\n const ext = extensionForFormat(format);\n const filename = `${safeBasename(input.title)}.${ext}`;\n\n try {\n const response = await client.responses.create({\n model: DOCUMENT_GEN_MODEL,\n instructions: buildDocumentGenInstructions(format),\n tools: [\n {\n type: 'code_interpreter',\n container: { type: 'auto' },\n },\n ],\n tool_choice: 'required',\n input: [\n `Create a ${ext.toUpperCase()} file using the python tool.`,\n `Output filename (exact): ${filename}`,\n `Output format: ${ext}`,\n `Document title: ${input.title}`,\n ['Source content — adapt fully to the output medium (do not paste raw markdown into slides/images):', input.content].join('\\n'),\n ].join('\\n\\n'),\n });\n\n let matches = extractContainerGeneratedFiles(response, format);\n\n if (matches.length === 0) {\n const containerId = containerIdFromResponse(response);\n if (containerId) {\n matches = await listContainerGeneratedFiles(client, containerId, format);\n }\n }\n\n if (matches.length === 0) {\n logger.error('OpenAI document gen produced no matching files', {\n title: input.title,\n format,\n outputTypes: (response.output ?? []).map((o) => o.type),\n });\n return null;\n }\n\n const preferred =\n matches.find((f) => f.filename.toLowerCase() === filename.toLowerCase()) ??\n matches[0];\n\n const bytes = await downloadContainerFile(client, preferred);\n if (!bytes) return null;\n\n return {\n bytes,\n filename: preferred.filename,\n mimeType: mimeTypeForFormat(format),\n format,\n };\n } catch (error) {\n logger.error('OpenAI document generation failed', {\n error,\n title: input.title,\n format,\n });\n return null;\n }\n}\n\n/** @deprecated Use {@link generateDocumentWithOpenAI} */\nexport async function generatePdfWithOpenAI(\n input: Omit<GenerateDocumentInput, 'format'>,\n): Promise<Buffer | null> {\n const result = await generateDocumentWithOpenAI({ ...input, format: 'pdf' });\n return result?.bytes ?? null;\n}\n\n/** @deprecated Use {@link extractContainerGeneratedFiles} */\nexport const extractContainerPdfFiles = (response: Response) =>\n extractContainerGeneratedFiles(response, 'pdf');\n"],"names":[],"mappings":"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","debug_id":"d086b281-e191-5157-a799-843675a76a37"}
|
|
@@ -162,6 +162,8 @@ export declare function createSchoolClass(userId: string, input: {
|
|
|
162
162
|
email: string;
|
|
163
163
|
password: string;
|
|
164
164
|
verified: boolean;
|
|
165
|
+
termsAcceptedAt: Date | null;
|
|
166
|
+
termsVersion: string | null;
|
|
165
167
|
schoolId: string | null;
|
|
166
168
|
}[];
|
|
167
169
|
students: {
|
|
@@ -170,6 +172,8 @@ export declare function createSchoolClass(userId: string, input: {
|
|
|
170
172
|
email: string;
|
|
171
173
|
password: string;
|
|
172
174
|
verified: boolean;
|
|
175
|
+
termsAcceptedAt: Date | null;
|
|
176
|
+
termsVersion: string | null;
|
|
173
177
|
schoolId: string | null;
|
|
174
178
|
}[];
|
|
175
179
|
} & {
|
|
@@ -358,6 +362,118 @@ export declare function getStudentTimeline(userId: string, input: {
|
|
|
358
362
|
status: "PRESENT" | "LATE" | "ABSENT";
|
|
359
363
|
}[];
|
|
360
364
|
}>;
|
|
365
|
+
/**
|
|
366
|
+
* Role-aware overview used by `/view/[userId]`.
|
|
367
|
+
*
|
|
368
|
+
* - For STUDENT members it returns the same timeline as `getStudentTimeline`
|
|
369
|
+
* (events, assignments, attendance) so teachers/admins can audit progress.
|
|
370
|
+
* - For TEACHER/ADMIN members it returns their teaching agenda: every event in
|
|
371
|
+
* a class they teach plus their personal events, alongside the classes they
|
|
372
|
+
* run with enrolment counts.
|
|
373
|
+
*
|
|
374
|
+
* Permissions:
|
|
375
|
+
* - School admins can view anyone in the institution.
|
|
376
|
+
* - A teacher can view a STUDENT they share a class with.
|
|
377
|
+
* - A teacher/admin can always view their own overview.
|
|
378
|
+
*/
|
|
379
|
+
export declare function getMemberTimeline(userId: string, input: {
|
|
380
|
+
schoolId: string;
|
|
381
|
+
memberId: string;
|
|
382
|
+
from?: Date;
|
|
383
|
+
to?: Date;
|
|
384
|
+
}): Promise<{
|
|
385
|
+
role: "STUDENT";
|
|
386
|
+
member: {
|
|
387
|
+
id: string;
|
|
388
|
+
username: string;
|
|
389
|
+
profile: {
|
|
390
|
+
profilePicture: string | null;
|
|
391
|
+
displayName: string | null;
|
|
392
|
+
} | null;
|
|
393
|
+
};
|
|
394
|
+
events: {
|
|
395
|
+
class: {
|
|
396
|
+
id: string;
|
|
397
|
+
name: string;
|
|
398
|
+
color: string | null;
|
|
399
|
+
} | null;
|
|
400
|
+
id: string;
|
|
401
|
+
location: string | null;
|
|
402
|
+
name: string | null;
|
|
403
|
+
color: string | null;
|
|
404
|
+
startTime: Date;
|
|
405
|
+
endTime: Date;
|
|
406
|
+
remarks: string | null;
|
|
407
|
+
}[];
|
|
408
|
+
assignments: {
|
|
409
|
+
type: import(".prisma/client").$Enums.AssignmentType;
|
|
410
|
+
class: {
|
|
411
|
+
section: string;
|
|
412
|
+
id: string;
|
|
413
|
+
name: string;
|
|
414
|
+
color: string | null;
|
|
415
|
+
subject: string;
|
|
416
|
+
};
|
|
417
|
+
id: string;
|
|
418
|
+
submissions: {
|
|
419
|
+
id: string;
|
|
420
|
+
gradeReceived: number | null;
|
|
421
|
+
submittedAt: Date | null;
|
|
422
|
+
submitted: boolean | null;
|
|
423
|
+
returned: boolean | null;
|
|
424
|
+
}[];
|
|
425
|
+
title: string;
|
|
426
|
+
dueDate: Date;
|
|
427
|
+
maxGrade: number | null;
|
|
428
|
+
graded: boolean;
|
|
429
|
+
weight: number;
|
|
430
|
+
}[];
|
|
431
|
+
attendance: {
|
|
432
|
+
id: string;
|
|
433
|
+
date: Date;
|
|
434
|
+
class: {
|
|
435
|
+
id: string;
|
|
436
|
+
name: string;
|
|
437
|
+
color: string | null;
|
|
438
|
+
};
|
|
439
|
+
status: "PRESENT" | "LATE" | "ABSENT";
|
|
440
|
+
}[];
|
|
441
|
+
classes?: undefined;
|
|
442
|
+
} | {
|
|
443
|
+
role: "TEACHER" | "ADMIN";
|
|
444
|
+
member: {
|
|
445
|
+
id: string;
|
|
446
|
+
username: string;
|
|
447
|
+
profile: {
|
|
448
|
+
profilePicture: string | null;
|
|
449
|
+
displayName: string | null;
|
|
450
|
+
} | null;
|
|
451
|
+
};
|
|
452
|
+
events: {
|
|
453
|
+
class: {
|
|
454
|
+
id: string;
|
|
455
|
+
name: string;
|
|
456
|
+
color: string | null;
|
|
457
|
+
} | null;
|
|
458
|
+
id: string;
|
|
459
|
+
location: string | null;
|
|
460
|
+
name: string | null;
|
|
461
|
+
color: string | null;
|
|
462
|
+
startTime: Date;
|
|
463
|
+
endTime: Date;
|
|
464
|
+
remarks: string | null;
|
|
465
|
+
}[];
|
|
466
|
+
classes: {
|
|
467
|
+
id: string;
|
|
468
|
+
name: string;
|
|
469
|
+
section: string;
|
|
470
|
+
subject: string;
|
|
471
|
+
color: string | null;
|
|
472
|
+
studentCount: number;
|
|
473
|
+
}[];
|
|
474
|
+
assignments?: undefined;
|
|
475
|
+
attendance?: undefined;
|
|
476
|
+
}>;
|
|
361
477
|
export declare function createInvite(userId: string, input: {
|
|
362
478
|
schoolId: string;
|
|
363
479
|
email: string;
|