@studious-lms/server 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +28 -0
- package/.env.example +5 -0
- package/.github/workflows/deploy.yml +123 -0
- package/Dockerfile +54 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/instrument.js +3 -3
- package/dist/instrument.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +31 -11
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/config/env.d.ts +17 -0
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +8 -2
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts +10 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +22 -2
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/inferDocumentFormat.d.ts +10 -0
- package/dist/lib/inferDocumentFormat.d.ts.map +1 -0
- package/dist/lib/inferDocumentFormat.js +48 -0
- package/dist/lib/inferDocumentFormat.js.map +1 -0
- package/dist/lib/legal.d.ts +3 -0
- package/dist/lib/legal.d.ts.map +1 -0
- package/dist/lib/legal.js +6 -0
- package/dist/lib/legal.js.map +1 -0
- package/dist/lib/prisma.d.ts +104 -5
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +33 -2
- package/dist/lib/prisma.js.map +1 -1
- package/dist/models/agenda.d.ts +20 -20
- package/dist/models/announcement.d.ts +211 -101
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +560 -362
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +197 -71
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/auth.d.ts +356 -114
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +4 -2
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +327 -201
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +2 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +146 -56
- package/dist/models/cloudStorage.d.ts.map +1 -1
- package/dist/models/comment.d.ts +262 -110
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +206 -80
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/department.d.ts +196 -70
- package/dist/models/department.d.ts.map +1 -1
- package/dist/models/event.d.ts +305 -141
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +422 -272
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +433 -181
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +205 -79
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +79 -38
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/marketing.js +12 -4
- package/dist/models/marketing.js.map +1 -1
- package/dist/models/message.d.ts +97 -43
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +84 -30
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/notification.d.ts +57 -21
- package/dist/models/notification.d.ts.map +1 -1
- package/dist/models/school.d.ts +349 -133
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/section.d.ts +216 -72
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +156 -48
- package/dist/models/tag.d.ts.map +1 -1
- package/dist/models/user.d.ts +82 -28
- package/dist/models/user.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +1032 -423
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +8 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +7 -9
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +194 -125
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +233 -389
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +54 -70
- package/dist/pipelines/aiLabChatContract.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +41 -41
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +8 -8
- package/dist/pipelines/labChatPrompt.d.ts +18 -11
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +93 -75
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
- package/dist/routers/_app.d.ts +2601 -2151
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/admin.d.ts +13 -13
- package/dist/routers/agenda.d.ts +18 -18
- package/dist/routers/announcement.d.ts +60 -60
- package/dist/routers/assignment.d.ts +393 -383
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/attendance.d.ts +10 -10
- package/dist/routers/auth.d.ts +4 -2
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +15 -2
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +157 -153
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +18 -18
- package/dist/routers/conversation.d.ts +11 -11
- package/dist/routers/department.d.ts +34 -34
- package/dist/routers/discussion.d.ts +68 -56
- package/dist/routers/discussion.d.ts.map +1 -1
- package/dist/routers/discussion.js +6 -2
- package/dist/routers/discussion.js.map +1 -1
- package/dist/routers/embedding.d.ts +73 -0
- package/dist/routers/embedding.d.ts.map +1 -0
- package/dist/routers/embedding.js +151 -0
- package/dist/routers/embedding.js.map +1 -0
- package/dist/routers/event.d.ts +57 -55
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +14 -14
- package/dist/routers/folder.d.ts +38 -38
- package/dist/routers/labChat.d.ts +16 -16
- package/dist/routers/marketing.d.ts +1 -14
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/message.d.ts +13 -13
- package/dist/routers/newtonChat.d.ts +4 -4
- package/dist/routers/notifications.d.ts +5 -5
- package/dist/routers/permissions.d.ts +14 -14
- package/dist/routers/school.d.ts +59 -55
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/search.d.ts +22 -1
- package/dist/routers/search.d.ts.map +1 -1
- package/dist/routers/search.js +9 -3
- package/dist/routers/search.js.map +1 -1
- package/dist/routers/section.d.ts +4 -4
- package/dist/routers/studentProgress.d.ts +1 -1
- package/dist/routers/tag.d.ts +7 -7
- package/dist/routers/user.d.ts +99 -10
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +19 -2
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +93 -55
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +26 -2
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/routes/aiStream.d.ts +2 -0
- package/dist/routes/aiStream.d.ts.map +1 -0
- package/dist/routes/aiStream.js +726 -0
- package/dist/routes/aiStream.js.map +1 -0
- package/dist/seedDatabase.d.ts +3 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +6 -6
- package/dist/seedDatabase.js.map +1 -1
- package/dist/services/adminInstitution.d.ts +8 -8
- package/dist/services/adminRunJob.d.ts +4 -4
- package/dist/services/agenda.d.ts +18 -18
- package/dist/services/aiChatStream.d.ts +88 -0
- package/dist/services/aiChatStream.d.ts.map +1 -0
- package/dist/services/aiChatStream.js +254 -0
- package/dist/services/aiChatStream.js.map +1 -0
- package/dist/services/aiModifierTools.d.ts +16 -0
- package/dist/services/aiModifierTools.d.ts.map +1 -0
- package/dist/services/aiModifierTools.js +274 -0
- package/dist/services/aiModifierTools.js.map +1 -0
- package/dist/services/aiProposalTools.d.ts +32 -0
- package/dist/services/aiProposalTools.d.ts.map +1 -0
- package/dist/services/aiProposalTools.js +207 -0
- package/dist/services/aiProposalTools.js.map +1 -0
- package/dist/services/aiStream.d.ts +22 -0
- package/dist/services/aiStream.d.ts.map +1 -0
- package/dist/services/aiStream.js +142 -0
- package/dist/services/aiStream.js.map +1 -0
- package/dist/services/aiTools.d.ts +16 -0
- package/dist/services/aiTools.d.ts.map +1 -0
- package/dist/services/aiTools.js +271 -0
- package/dist/services/aiTools.js.map +1 -0
- package/dist/services/announcement.d.ts +42 -42
- package/dist/services/announcement.d.ts.map +1 -1
- package/dist/services/announcement.js +7 -2
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts +355 -345
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +30 -9
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +10 -10
- package/dist/services/auth.d.ts +4 -2
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +13 -4
- package/dist/services/auth.js.map +1 -1
- package/dist/services/class.d.ts +146 -142
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +24 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +18 -18
- package/dist/services/conversation.d.ts +11 -11
- package/dist/services/department.d.ts +33 -33
- package/dist/services/discussion.d.ts +58 -58
- package/dist/services/discussion.d.ts.map +1 -1
- package/dist/services/discussion.js +15 -8
- package/dist/services/discussion.js.map +1 -1
- package/dist/services/discussionSummary.d.ts +21 -0
- package/dist/services/discussionSummary.d.ts.map +1 -0
- package/dist/services/discussionSummary.js +179 -0
- package/dist/services/discussionSummary.js.map +1 -0
- package/dist/services/embedding.d.ts +85 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +372 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/event.d.ts +55 -53
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +28 -28
- package/dist/services/folder.d.ts +36 -36
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +32 -2
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +16 -16
- package/dist/services/marketing.d.ts +2 -15
- package/dist/services/marketing.d.ts.map +1 -1
- package/dist/services/marketing.js +5 -5
- package/dist/services/marketing.js.map +1 -1
- package/dist/services/message.d.ts +11 -11
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +2 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +4 -4
- package/dist/services/notification.d.ts +5 -5
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/openaiDocumentGen.d.ts +37 -0
- package/dist/services/openaiDocumentGen.d.ts.map +1 -0
- package/dist/services/openaiDocumentGen.js +262 -0
- package/dist/services/openaiDocumentGen.js.map +1 -0
- package/dist/services/permissions.d.ts +14 -14
- package/dist/services/school.d.ts +54 -50
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/search.d.ts +13 -1
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +145 -2
- package/dist/services/search.js.map +1 -1
- package/dist/services/section.d.ts +3 -3
- package/dist/services/tag.d.ts +4 -4
- package/dist/services/user.d.ts +4 -4
- package/dist/services/user.d.ts.map +1 -1
- package/dist/services/user.js +10 -15
- package/dist/services/user.js.map +1 -1
- package/dist/services/userPrivacy.d.ts +86 -0
- package/dist/services/userPrivacy.d.ts.map +1 -0
- package/dist/services/userPrivacy.js +184 -0
- package/dist/services/userPrivacy.js.map +1 -0
- package/dist/services/worksheet.d.ts +86 -57
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +57 -5
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +7 -3
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/streamBroadcast.d.ts +11 -0
- package/dist/utils/streamBroadcast.d.ts.map +1 -0
- package/dist/utils/streamBroadcast.js +32 -0
- package/dist/utils/streamBroadcast.js.map +1 -0
- package/dist/workers/connection.d.ts +15 -0
- package/dist/workers/connection.d.ts.map +1 -1
- package/dist/workers/connection.js +20 -2
- package/dist/workers/connection.js.map +1 -1
- package/dist/workers/index.js +4 -2
- package/dist/workers/index.js.map +1 -1
- package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
- package/dist/workers/processors/createRunJobWorker.js +31 -8
- package/dist/workers/processors/createRunJobWorker.js.map +1 -1
- package/dist/workers/processors/embedContent.d.ts +8 -0
- package/dist/workers/processors/embedContent.d.ts.map +1 -0
- package/dist/workers/processors/embedContent.js +67 -0
- package/dist/workers/processors/embedContent.js.map +1 -0
- package/dist/workers/queues.d.ts +6 -0
- package/dist/workers/queues.d.ts.map +1 -1
- package/dist/workers/queues.js +5 -3
- package/dist/workers/queues.js.map +1 -1
- package/package.json +5 -1
- package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
- package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
- package/prisma/migrations/20260610140000_expand_embedding_source_types/migration.sql +4 -0
- package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
- package/prisma/schema.prisma +44 -7
- package/src/index.ts +4 -0
- package/src/instrument.ts +1 -1
- package/src/lib/config/cors.ts +32 -9
- package/src/lib/config/env.ts +6 -0
- package/src/lib/googleCloudStorage.ts +21 -0
- package/src/lib/inferDocumentFormat.ts +58 -0
- package/src/lib/legal.ts +2 -0
- package/src/lib/prisma.ts +40 -1
- package/src/models/auth.ts +4 -0
- package/src/models/class.ts +3 -2
- package/src/models/marketing.ts +10 -2
- package/src/models/worksheet.ts +5 -2
- package/src/pipelines/aiLabChat.ts +245 -146
- package/src/pipelines/aiLabChatContract.ts +59 -69
- package/src/pipelines/aiNewtonChat.ts +46 -43
- package/src/pipelines/labChatPrompt.ts +115 -90
- package/src/routers/_app.ts +2 -0
- package/src/routers/auth.ts +13 -0
- package/src/routers/discussion.ts +7 -0
- package/src/routers/embedding.ts +164 -0
- package/src/routers/search.ts +10 -1
- package/src/routers/user.ts +26 -0
- package/src/routers/worksheet.ts +27 -0
- package/src/routes/aiStream.ts +827 -0
- package/src/seedDatabase.ts +4 -4
- package/src/services/aiChatStream.ts +341 -0
- package/src/services/aiModifierTools.ts +341 -0
- package/src/services/aiProposalTools.ts +274 -0
- package/src/services/aiStream.ts +175 -0
- package/src/services/aiTools.ts +290 -0
- package/src/services/announcement.ts +7 -0
- package/src/services/assignment.ts +31 -7
- package/src/services/auth.ts +14 -2
- package/src/services/class.ts +27 -2
- package/src/services/discussion.ts +22 -8
- package/src/services/discussionSummary.ts +226 -0
- package/src/services/embedding.ts +446 -0
- package/src/services/folder.ts +31 -0
- package/src/services/marketing.ts +3 -3
- package/src/services/message.ts +2 -2
- package/src/services/openaiDocumentGen.ts +319 -0
- package/src/services/search.ts +163 -1
- package/src/services/user.ts +9 -17
- package/src/services/userPrivacy.ts +196 -0
- package/src/services/worksheet.ts +77 -3
- package/src/utils/email.ts +5 -1
- package/src/utils/streamBroadcast.ts +48 -0
- package/src/workers/connection.ts +19 -0
- package/src/workers/index.ts +2 -0
- package/src/workers/processors/createRunJobWorker.ts +32 -6
- package/src/workers/processors/embedContent.ts +86 -0
- package/src/workers/queues.ts +5 -1
- package/tests/helpers/registerInput.ts +20 -0
- package/tests/integration/globalSetup.ts +10 -18
- package/tests/integration/routers/auth.test.ts +13 -18
- package/tests/unit/lib/inferDocumentFormat.test.ts +33 -0
- package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
- package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
- package/tests/unit/services/aiChatStream.test.ts +92 -0
- package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
- package/prisma/migrations/20250807062924_init/migration.sql +0 -436
- package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
- package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
- package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
- package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
- package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
- package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
- package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
- package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
- package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
- package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
- package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
- package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
- package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
- package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
- package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
- package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
- package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
- package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
- /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate files via OpenAI Code Interpreter (Responses API).
|
|
3
|
+
* Supports every format the sandbox can produce (pdf, docx, xlsx, pptx, …).
|
|
4
|
+
*/
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
import type { Response } from 'openai/resources/responses/responses.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
const DOCUMENT_GEN_MODEL = 'gpt-4o-mini';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extensions OpenAI Code Interpreter can read/write in its sandbox.
|
|
13
|
+
* @see https://developers.openai.com/api/docs/guides/tools-code-interpreter
|
|
14
|
+
*/
|
|
15
|
+
export const SUPPORTED_DOCUMENT_FORMATS = [
|
|
16
|
+
'pdf',
|
|
17
|
+
'docx',
|
|
18
|
+
'doc',
|
|
19
|
+
'xlsx',
|
|
20
|
+
'pptx',
|
|
21
|
+
'csv',
|
|
22
|
+
'md',
|
|
23
|
+
'html',
|
|
24
|
+
'txt',
|
|
25
|
+
'json',
|
|
26
|
+
'xml',
|
|
27
|
+
'tex',
|
|
28
|
+
'png',
|
|
29
|
+
'jpeg',
|
|
30
|
+
'jpg',
|
|
31
|
+
'gif',
|
|
32
|
+
'zip',
|
|
33
|
+
'tar',
|
|
34
|
+
'py',
|
|
35
|
+
'js',
|
|
36
|
+
'ts',
|
|
37
|
+
'css',
|
|
38
|
+
'sh',
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export type DocumentFormat = (typeof SUPPORTED_DOCUMENT_FORMATS)[number];
|
|
42
|
+
|
|
43
|
+
const FORMAT_MIME: Record<DocumentFormat, string> = {
|
|
44
|
+
pdf: 'application/pdf',
|
|
45
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
46
|
+
doc: 'application/msword',
|
|
47
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
48
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
49
|
+
csv: 'text/csv',
|
|
50
|
+
md: 'text/markdown',
|
|
51
|
+
html: 'text/html',
|
|
52
|
+
txt: 'text/plain',
|
|
53
|
+
json: 'application/json',
|
|
54
|
+
xml: 'application/xml',
|
|
55
|
+
tex: 'text/x-tex',
|
|
56
|
+
png: 'image/png',
|
|
57
|
+
jpeg: 'image/jpeg',
|
|
58
|
+
jpg: 'image/jpeg',
|
|
59
|
+
gif: 'image/gif',
|
|
60
|
+
zip: 'application/zip',
|
|
61
|
+
tar: 'application/x-tar',
|
|
62
|
+
py: 'text/x-python',
|
|
63
|
+
js: 'text/javascript',
|
|
64
|
+
ts: 'application/typescript',
|
|
65
|
+
css: 'text/css',
|
|
66
|
+
sh: 'application/x-sh',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const FORMAT_HINTS: Record<DocumentFormat, string> = {
|
|
70
|
+
pdf: 'fpdf2 or reportlab — structured document with headings and body text',
|
|
71
|
+
docx: 'python-docx — Word document with headings and paragraphs',
|
|
72
|
+
doc: 'python-docx saving as .doc if supported, otherwise .docx',
|
|
73
|
+
xlsx: 'openpyxl or pandas — spreadsheet with labelled columns/rows',
|
|
74
|
+
pptx: 'python-pptx — slides with title + bullet content per slide',
|
|
75
|
+
csv: 'pandas or csv module — tabular data with headers',
|
|
76
|
+
md: 'write the markdown file directly',
|
|
77
|
+
html: 'write a complete, styled HTML page',
|
|
78
|
+
txt: 'write plain text with clear section breaks',
|
|
79
|
+
json: 'json.dumps with readable indentation',
|
|
80
|
+
xml: 'well-formed XML document',
|
|
81
|
+
tex: 'LaTeX source that compiles to the requested content',
|
|
82
|
+
png: 'matplotlib or Pillow — chart, diagram, or visual',
|
|
83
|
+
jpeg: 'matplotlib or Pillow — save as JPEG',
|
|
84
|
+
jpg: 'matplotlib or Pillow — save as JPEG',
|
|
85
|
+
gif: 'matplotlib or Pillow — save as GIF',
|
|
86
|
+
zip: 'zipfile — bundle multiple related files if needed',
|
|
87
|
+
tar: 'tarfile — archive if multiple files are required',
|
|
88
|
+
py: 'valid Python source code file',
|
|
89
|
+
js: 'valid JavaScript source file',
|
|
90
|
+
ts: 'valid TypeScript source file',
|
|
91
|
+
css: 'stylesheet file',
|
|
92
|
+
sh: 'shell script file',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export interface GenerateDocumentInput {
|
|
96
|
+
title: string;
|
|
97
|
+
/** Markdown or plain-text source material for the output file. */
|
|
98
|
+
content: string;
|
|
99
|
+
/** Output format. Defaults to pdf. */
|
|
100
|
+
format?: DocumentFormat | string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface GeneratedDocumentResult {
|
|
104
|
+
bytes: Buffer;
|
|
105
|
+
filename: string;
|
|
106
|
+
mimeType: string;
|
|
107
|
+
format: DocumentFormat;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ContainerGeneratedFile {
|
|
111
|
+
containerId: string;
|
|
112
|
+
fileId: string;
|
|
113
|
+
filename: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function normalizeDocumentFormat(
|
|
117
|
+
format?: string | null,
|
|
118
|
+
): DocumentFormat {
|
|
119
|
+
const raw = (format ?? 'pdf').toLowerCase().replace(/^\./, '');
|
|
120
|
+
if ((SUPPORTED_DOCUMENT_FORMATS as readonly string[]).includes(raw)) {
|
|
121
|
+
return raw as DocumentFormat;
|
|
122
|
+
}
|
|
123
|
+
return 'pdf';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function mimeTypeForFormat(format: DocumentFormat): string {
|
|
127
|
+
return FORMAT_MIME[format];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function extensionForFormat(format: DocumentFormat): string {
|
|
131
|
+
return format === 'jpg' ? 'jpg' : format;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Pull generated-file citations for a given extension out of a Responses output. */
|
|
135
|
+
export function extractContainerGeneratedFiles(
|
|
136
|
+
response: Response,
|
|
137
|
+
extension: DocumentFormat,
|
|
138
|
+
): ContainerGeneratedFile[] {
|
|
139
|
+
const seen = new Set<string>();
|
|
140
|
+
const files: ContainerGeneratedFile[] = [];
|
|
141
|
+
const suffix = `.${extension}`;
|
|
142
|
+
|
|
143
|
+
const add = (containerId: string, fileId: string, filename: string) => {
|
|
144
|
+
const key = `${containerId}:${fileId}`;
|
|
145
|
+
if (seen.has(key)) return;
|
|
146
|
+
seen.add(key);
|
|
147
|
+
files.push({ containerId, fileId, filename });
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
for (const item of response.output ?? []) {
|
|
151
|
+
if (item.type !== 'message') continue;
|
|
152
|
+
for (const block of item.content ?? []) {
|
|
153
|
+
if (block.type !== 'output_text') continue;
|
|
154
|
+
for (const ann of block.annotations ?? []) {
|
|
155
|
+
if (ann.type !== 'container_file_citation') continue;
|
|
156
|
+
const name = ann.filename?.toLowerCase() ?? '';
|
|
157
|
+
if (name.endsWith(suffix)) {
|
|
158
|
+
add(ann.container_id, ann.file_id, ann.filename);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return files;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function containerIdFromResponse(response: Response): string | undefined {
|
|
168
|
+
for (const item of response.output ?? []) {
|
|
169
|
+
if (item.type === 'code_interpreter_call' && item.container_id) {
|
|
170
|
+
return item.container_id;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function listContainerGeneratedFiles(
|
|
177
|
+
client: OpenAI,
|
|
178
|
+
containerId: string,
|
|
179
|
+
extension: DocumentFormat,
|
|
180
|
+
): Promise<ContainerGeneratedFile[]> {
|
|
181
|
+
const suffix = `.${extension}`;
|
|
182
|
+
const files: ContainerGeneratedFile[] = [];
|
|
183
|
+
for await (const file of client.containers.files.list(containerId)) {
|
|
184
|
+
const path = file.path?.toLowerCase() ?? '';
|
|
185
|
+
if (path.endsWith(suffix) && file.source === 'assistant') {
|
|
186
|
+
files.push({
|
|
187
|
+
containerId,
|
|
188
|
+
fileId: file.id,
|
|
189
|
+
filename: file.path.split('/').pop() ?? file.path,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return files;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function downloadContainerFile(
|
|
197
|
+
client: OpenAI,
|
|
198
|
+
file: ContainerGeneratedFile,
|
|
199
|
+
): Promise<Buffer | null> {
|
|
200
|
+
try {
|
|
201
|
+
const res = await client.containers.files.content.retrieve(file.fileId, {
|
|
202
|
+
container_id: file.containerId,
|
|
203
|
+
});
|
|
204
|
+
const bytes = await res.arrayBuffer();
|
|
205
|
+
return Buffer.from(bytes);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logger.error('Failed to download container file', { error, file });
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function safeBasename(title: string): string {
|
|
213
|
+
const base = title
|
|
214
|
+
.replace(/[^a-zA-Z0-9\s\-_]/g, '')
|
|
215
|
+
.replace(/\s+/g, '_')
|
|
216
|
+
.substring(0, 60);
|
|
217
|
+
return base || 'document';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildDocumentGenInstructions(format: DocumentFormat): string {
|
|
221
|
+
return [
|
|
222
|
+
'You are a document generator for K-12 and university teachers.',
|
|
223
|
+
'When given a title, output format, and source content:',
|
|
224
|
+
'1. Use the python tool to create exactly ONE output file of the requested type.',
|
|
225
|
+
`2. For this format use: ${FORMAT_HINTS[format]}.`,
|
|
226
|
+
`3. You MUST output a ${format.toUpperCase()} file — not any other format.`,
|
|
227
|
+
`4. The saved filename MUST end with .${extensionForFormat(format)}.`,
|
|
228
|
+
'5. Structure content clearly — headings, sections, lists, tables/charts as appropriate.',
|
|
229
|
+
'6. Include the document title prominently where the format allows.',
|
|
230
|
+
'7. Put ONLY the educational content in the file — no meta commentary.',
|
|
231
|
+
'8. You MUST save the file to disk in the working directory before you finish.',
|
|
232
|
+
...(format !== 'pdf'
|
|
233
|
+
? [`9. Do NOT create a PDF when ${format.toUpperCase()} was requested.`]
|
|
234
|
+
: []),
|
|
235
|
+
].join(' ');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Generate a file via OpenAI Code Interpreter. */
|
|
239
|
+
export async function generateDocumentWithOpenAI(
|
|
240
|
+
input: GenerateDocumentInput,
|
|
241
|
+
): Promise<GeneratedDocumentResult | null> {
|
|
242
|
+
const client = new OpenAI();
|
|
243
|
+
const format = normalizeDocumentFormat(input.format);
|
|
244
|
+
const ext = extensionForFormat(format);
|
|
245
|
+
const filename = `${safeBasename(input.title)}.${ext}`;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const response = await client.responses.create({
|
|
249
|
+
model: DOCUMENT_GEN_MODEL,
|
|
250
|
+
instructions: buildDocumentGenInstructions(format),
|
|
251
|
+
tools: [
|
|
252
|
+
{
|
|
253
|
+
type: 'code_interpreter',
|
|
254
|
+
container: { type: 'auto' },
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
tool_choice: 'required',
|
|
258
|
+
input: [
|
|
259
|
+
`Create a ${ext.toUpperCase()} file using the python tool.`,
|
|
260
|
+
`Output filename (exact): ${filename}`,
|
|
261
|
+
`Output format: ${ext}`,
|
|
262
|
+
`Document title: ${input.title}`,
|
|
263
|
+
'Source content (markdown or plain text — adapt to the output format):',
|
|
264
|
+
input.content,
|
|
265
|
+
].join('\n\n'),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
let matches = extractContainerGeneratedFiles(response, format);
|
|
269
|
+
|
|
270
|
+
if (matches.length === 0) {
|
|
271
|
+
const containerId = containerIdFromResponse(response);
|
|
272
|
+
if (containerId) {
|
|
273
|
+
matches = await listContainerGeneratedFiles(client, containerId, format);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (matches.length === 0) {
|
|
278
|
+
logger.error('OpenAI document gen produced no matching files', {
|
|
279
|
+
title: input.title,
|
|
280
|
+
format,
|
|
281
|
+
outputTypes: (response.output ?? []).map((o) => o.type),
|
|
282
|
+
});
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const preferred =
|
|
287
|
+
matches.find((f) => f.filename.toLowerCase() === filename.toLowerCase()) ??
|
|
288
|
+
matches[0];
|
|
289
|
+
|
|
290
|
+
const bytes = await downloadContainerFile(client, preferred);
|
|
291
|
+
if (!bytes) return null;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
bytes,
|
|
295
|
+
filename: preferred.filename,
|
|
296
|
+
mimeType: mimeTypeForFormat(format),
|
|
297
|
+
format,
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logger.error('OpenAI document generation failed', {
|
|
301
|
+
error,
|
|
302
|
+
title: input.title,
|
|
303
|
+
format,
|
|
304
|
+
});
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @deprecated Use {@link generateDocumentWithOpenAI} */
|
|
310
|
+
export async function generatePdfWithOpenAI(
|
|
311
|
+
input: Omit<GenerateDocumentInput, 'format'>,
|
|
312
|
+
): Promise<Buffer | null> {
|
|
313
|
+
const result = await generateDocumentWithOpenAI({ ...input, format: 'pdf' });
|
|
314
|
+
return result?.bytes ?? null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** @deprecated Use {@link extractContainerGeneratedFiles} */
|
|
318
|
+
export const extractContainerPdfFiles = (response: Response) =>
|
|
319
|
+
extractContainerGeneratedFiles(response, 'pdf');
|
package/src/services/search.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { prisma } from "../lib/prisma.js";
|
|
2
|
+
import { generateEmbedding } from "./embedding.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
import type { EmbeddingSourceType } from "@prisma/client";
|
|
2
5
|
|
|
3
6
|
type SearchScope =
|
|
4
7
|
| "class"
|
|
@@ -8,7 +11,9 @@ type SearchScope =
|
|
|
8
11
|
| "worksheet"
|
|
9
12
|
| "file"
|
|
10
13
|
| "announcement"
|
|
11
|
-
| "section"
|
|
14
|
+
| "section"
|
|
15
|
+
| "discussion"
|
|
16
|
+
| "submission";
|
|
12
17
|
|
|
13
18
|
type SearchResult = {
|
|
14
19
|
id: string;
|
|
@@ -58,6 +63,8 @@ const trimResults = (results: SearchResult[], limit: number) =>
|
|
|
58
63
|
file: 5,
|
|
59
64
|
announcement: 6,
|
|
60
65
|
section: 7,
|
|
66
|
+
discussion: 8,
|
|
67
|
+
submission: 9,
|
|
61
68
|
};
|
|
62
69
|
const priorityDiff = priority[a.type] - priority[b.type];
|
|
63
70
|
if (priorityDiff !== 0) return priorityDiff;
|
|
@@ -395,3 +402,158 @@ export async function searchGlobalClasses(
|
|
|
395
402
|
|
|
396
403
|
return { query, results: trimResults(results, limit) };
|
|
397
404
|
}
|
|
405
|
+
|
|
406
|
+
const SOURCE_TYPE_TO_SCOPE: Record<EmbeddingSourceType, SearchScope> = {
|
|
407
|
+
ASSIGNMENT: "assignment",
|
|
408
|
+
WORKSHEET: "worksheet",
|
|
409
|
+
ANNOUNCEMENT: "announcement",
|
|
410
|
+
FILE: "file",
|
|
411
|
+
SECTION: "section",
|
|
412
|
+
DISCUSSION: "discussion",
|
|
413
|
+
SUBMISSION: "submission",
|
|
414
|
+
SYLLABUS: "syllabus",
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const MIN_SIMILARITY = 0.25;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Cross-class semantic search via pgvector.
|
|
421
|
+
* Generates an embedding for `query`, searches across all classes the user
|
|
422
|
+
* belongs to, and maps hits back to SearchResult shape.
|
|
423
|
+
*/
|
|
424
|
+
export async function searchSemanticGlobal(
|
|
425
|
+
userId: string,
|
|
426
|
+
input: { query: string; limit?: number },
|
|
427
|
+
) {
|
|
428
|
+
const query = input.query.trim().slice(0, 200);
|
|
429
|
+
if (!query) {
|
|
430
|
+
return { query, results: [] as SearchResult[] };
|
|
431
|
+
}
|
|
432
|
+
const limit = Math.min(Math.max(input.limit ?? SEARCH_LIMIT, 1), 30);
|
|
433
|
+
|
|
434
|
+
const memberships = await prisma.class.findMany({
|
|
435
|
+
where: {
|
|
436
|
+
OR: [
|
|
437
|
+
{ teachers: { some: { id: userId } } },
|
|
438
|
+
{ students: { some: { id: userId } } },
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
select: { id: true, name: true },
|
|
442
|
+
});
|
|
443
|
+
const allClassIds = memberships.map((cls) => cls.id);
|
|
444
|
+
if (allClassIds.length === 0) {
|
|
445
|
+
return { query, results: [] as SearchResult[] };
|
|
446
|
+
}
|
|
447
|
+
const classById = new Map(memberships.map((cls) => [cls.id, cls]));
|
|
448
|
+
|
|
449
|
+
let queryVector: number[];
|
|
450
|
+
try {
|
|
451
|
+
queryVector = await generateEmbedding(query);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
logger.error("Semantic search embedding failed", { err });
|
|
454
|
+
return { query, results: [] as SearchResult[] };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const vectorStr = `[${queryVector.join(",")}]`;
|
|
458
|
+
|
|
459
|
+
type SemanticRow = {
|
|
460
|
+
sourceType: EmbeddingSourceType;
|
|
461
|
+
sourceId: string;
|
|
462
|
+
classId: string;
|
|
463
|
+
content: string;
|
|
464
|
+
similarity: number;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const hits = await prisma.$queryRaw<SemanticRow[]>`
|
|
468
|
+
SELECT DISTINCT ON ("sourceType", "sourceId")
|
|
469
|
+
"sourceType",
|
|
470
|
+
"sourceId",
|
|
471
|
+
"classId",
|
|
472
|
+
content,
|
|
473
|
+
1 - (vector <=> ${vectorStr}::vector) as similarity
|
|
474
|
+
FROM "Embedding"
|
|
475
|
+
WHERE "classId" = ANY(${allClassIds}::text[])
|
|
476
|
+
ORDER BY "sourceType", "sourceId", vector <=> ${vectorStr}::vector
|
|
477
|
+
LIMIT ${limit * 3}
|
|
478
|
+
`;
|
|
479
|
+
|
|
480
|
+
const filtered = hits
|
|
481
|
+
.filter((h) => h.similarity >= MIN_SIMILARITY)
|
|
482
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
483
|
+
.slice(0, limit);
|
|
484
|
+
|
|
485
|
+
const results: SearchResult[] = [];
|
|
486
|
+
for (const hit of filtered) {
|
|
487
|
+
const cls = classById.get(hit.classId);
|
|
488
|
+
const className = cls?.name ?? "Class";
|
|
489
|
+
const scope = SOURCE_TYPE_TO_SCOPE[hit.sourceType];
|
|
490
|
+
if (!scope) continue;
|
|
491
|
+
|
|
492
|
+
const snippet = hit.content.replace(/\s+/g, " ").trim().slice(0, 140);
|
|
493
|
+
|
|
494
|
+
let title = snippet.slice(0, 60);
|
|
495
|
+
let href = `/class/${hit.classId}`;
|
|
496
|
+
|
|
497
|
+
if (scope === "assignment") {
|
|
498
|
+
const assignment = await prisma.assignment.findUnique({
|
|
499
|
+
where: { id: hit.sourceId },
|
|
500
|
+
select: { title: true },
|
|
501
|
+
});
|
|
502
|
+
title = assignment?.title ?? title;
|
|
503
|
+
href = `/class/${hit.classId}/assignment/${hit.sourceId}`;
|
|
504
|
+
} else if (scope === "worksheet") {
|
|
505
|
+
const worksheet = await prisma.worksheet.findUnique({
|
|
506
|
+
where: { id: hit.sourceId },
|
|
507
|
+
select: { name: true },
|
|
508
|
+
});
|
|
509
|
+
title = worksheet?.name ?? title;
|
|
510
|
+
href = `/class/${hit.classId}/worksheets`;
|
|
511
|
+
} else if (scope === "announcement") {
|
|
512
|
+
href = `/class/${hit.classId}`;
|
|
513
|
+
} else if (scope === "file") {
|
|
514
|
+
const file = await prisma.file.findUnique({
|
|
515
|
+
where: { id: hit.sourceId },
|
|
516
|
+
select: { name: true },
|
|
517
|
+
});
|
|
518
|
+
title = file?.name ?? title;
|
|
519
|
+
href = `/class/${hit.classId}/files`;
|
|
520
|
+
} else if (scope === "section") {
|
|
521
|
+
const section = await prisma.section.findUnique({
|
|
522
|
+
where: { id: hit.sourceId },
|
|
523
|
+
select: { name: true },
|
|
524
|
+
});
|
|
525
|
+
title = section?.name ?? title;
|
|
526
|
+
href = `/class/${hit.classId}/assignments`;
|
|
527
|
+
} else if (scope === "discussion") {
|
|
528
|
+
const discussion = await prisma.discussion.findUnique({
|
|
529
|
+
where: { id: hit.sourceId },
|
|
530
|
+
select: { title: true },
|
|
531
|
+
});
|
|
532
|
+
title = discussion?.title ?? title;
|
|
533
|
+
href = `/class/${hit.classId}/discussions/${hit.sourceId}`;
|
|
534
|
+
} else if (scope === "submission") {
|
|
535
|
+
const submission = await prisma.submission.findUnique({
|
|
536
|
+
where: { id: hit.sourceId },
|
|
537
|
+
select: { assignment: { select: { title: true, id: true } } },
|
|
538
|
+
});
|
|
539
|
+
title = submission?.assignment.title ? `Submission: ${submission.assignment.title}` : title;
|
|
540
|
+
href = submission?.assignment.id ? `/class/${hit.classId}/assignment/${submission.assignment.id}` : href;
|
|
541
|
+
} else if (scope === "syllabus") {
|
|
542
|
+
title = `${className} Syllabus`;
|
|
543
|
+
href = `/class/${hit.classId}/syllabus`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
results.push({
|
|
547
|
+
id: `semantic:${hit.sourceId}`,
|
|
548
|
+
type: scope,
|
|
549
|
+
title,
|
|
550
|
+
subtitle: `${className} · ${scope.charAt(0).toUpperCase() + scope.slice(1)}`,
|
|
551
|
+
classId: hit.classId,
|
|
552
|
+
className,
|
|
553
|
+
href,
|
|
554
|
+
matchedText: snippet,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return { query, results };
|
|
559
|
+
}
|
package/src/services/user.ts
CHANGED
|
@@ -5,15 +5,7 @@ import { TRPCError } from "@trpc/server";
|
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
6
|
import { env } from "../lib/config/env.js";
|
|
7
7
|
import { findUserById, findUserProfileByUserId, upsertUserProfile } from "../models/user.js";
|
|
8
|
-
|
|
9
|
-
function getFileUrl(filePath: string | null): string | null {
|
|
10
|
-
if (!filePath) return null;
|
|
11
|
-
if (filePath.startsWith("http")) {
|
|
12
|
-
return filePath;
|
|
13
|
-
}
|
|
14
|
-
const backendUrl = env.BACKEND_URL || "http://localhost:3001";
|
|
15
|
-
return `${backendUrl}/api/files/${encodeURIComponent(filePath)}`;
|
|
16
|
-
}
|
|
8
|
+
import { makeFilePublic } from "../lib/googleCloudStorage.js";
|
|
17
9
|
|
|
18
10
|
/** Get user profile with display info and profile picture URLs. */
|
|
19
11
|
export async function getProfile(userId: string) {
|
|
@@ -36,10 +28,8 @@ export async function getProfile(userId: string) {
|
|
|
36
28
|
bio: (userProfile as any).bio || null,
|
|
37
29
|
location: (userProfile as any).location || null,
|
|
38
30
|
website: (userProfile as any).website || null,
|
|
39
|
-
profilePicture:
|
|
40
|
-
profilePictureThumbnail:
|
|
41
|
-
(userProfile as any).profilePictureThumbnail
|
|
42
|
-
),
|
|
31
|
+
profilePicture: (userProfile as any).profilePicture ?? null,
|
|
32
|
+
profilePictureThumbnail: (userProfile as any).profilePictureThumbnail ?? null,
|
|
43
33
|
}
|
|
44
34
|
: {
|
|
45
35
|
displayName: null,
|
|
@@ -78,6 +68,10 @@ export async function updateProfile(
|
|
|
78
68
|
try {
|
|
79
69
|
profilePictureUrl = input.profilePicture.filePath;
|
|
80
70
|
profilePictureThumbnail = null;
|
|
71
|
+
|
|
72
|
+
// Best-effort: make the uploaded file publicly readable so the public
|
|
73
|
+
// GCS URL resolved by the Prisma extension actually works.
|
|
74
|
+
await makeFilePublic(profilePictureUrl);
|
|
81
75
|
} catch (error) {
|
|
82
76
|
logger.error("Profile picture processing failed", {
|
|
83
77
|
userId,
|
|
@@ -125,10 +119,8 @@ export async function updateProfile(
|
|
|
125
119
|
bio: (updatedProfile as any).bio || null,
|
|
126
120
|
location: (updatedProfile as any).location || null,
|
|
127
121
|
website: (updatedProfile as any).website || null,
|
|
128
|
-
profilePicture:
|
|
129
|
-
profilePictureThumbnail:
|
|
130
|
-
(updatedProfile as any).profilePictureThumbnail
|
|
131
|
-
),
|
|
122
|
+
profilePicture: (updatedProfile as any).profilePicture ?? null,
|
|
123
|
+
profilePictureThumbnail: (updatedProfile as any).profilePictureThumbnail ?? null,
|
|
132
124
|
},
|
|
133
125
|
};
|
|
134
126
|
}
|