@goscribe/server 1.3.4 → 1.5.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/.env.example +12 -0
- package/.vscode/settings.json +3 -0
- package/REFACTOR_NOTES.md +60 -0
- package/TESTING_PROMPT.md +225 -0
- package/dist/controllers/admin.controller.d.ts +715 -0
- package/dist/controllers/admin.controller.js +9 -0
- package/dist/controllers/annotations.controller.d.ts +439 -0
- package/dist/controllers/annotations.controller.js +9 -0
- package/dist/controllers/app-router.controller.d.ts +3011 -0
- package/dist/controllers/app-router.controller.js +38 -0
- package/dist/controllers/app-router.controller.test.d.ts +1 -0
- package/dist/controllers/app-router.controller.test.js +36 -0
- package/dist/controllers/auth.controller.d.ts +323 -0
- package/dist/controllers/auth.controller.js +9 -0
- package/dist/controllers/base.controller.d.ts +4 -0
- package/dist/controllers/base.controller.js +5 -0
- package/dist/controllers/chat.controller.d.ts +341 -0
- package/dist/controllers/chat.controller.js +9 -0
- package/dist/controllers/copilot.controller.d.ts +397 -0
- package/dist/controllers/copilot.controller.js +9 -0
- package/dist/controllers/flashcards.controller.d.ts +651 -0
- package/dist/controllers/flashcards.controller.js +9 -0
- package/dist/controllers/members.controller.d.ts +339 -0
- package/dist/controllers/members.controller.js +9 -0
- package/dist/controllers/notifications.controller.d.ts +199 -0
- package/dist/controllers/notifications.controller.js +9 -0
- package/dist/controllers/payment.controller.d.ts +181 -0
- package/dist/controllers/payment.controller.js +9 -0
- package/dist/controllers/podcast.controller.d.ts +575 -0
- package/dist/controllers/podcast.controller.js +9 -0
- package/dist/controllers/router-module.controller.d.ts +5 -0
- package/dist/controllers/router-module.controller.js +6 -0
- package/dist/controllers/studyguide.controller.d.ts +73 -0
- package/dist/controllers/studyguide.controller.js +9 -0
- package/dist/controllers/worksheets.controller.d.ts +829 -0
- package/dist/controllers/worksheets.controller.js +9 -0
- package/dist/controllers/workspace.controller.d.ts +1207 -0
- package/dist/controllers/workspace.controller.js +9 -0
- package/dist/lib/activity_human_description.test.js +16 -15
- package/dist/lib/activity_log_service.test.js +28 -23
- package/dist/lib/ai/config.d.ts +20 -0
- package/dist/lib/ai/config.js +31 -0
- package/dist/lib/ai/embedding-client.d.ts +8 -0
- package/dist/lib/ai/embedding-client.js +30 -0
- package/dist/lib/ai/index.d.ts +47 -0
- package/dist/lib/ai/index.js +28 -0
- package/dist/lib/ai/inference-backend/client.d.ts +28 -0
- package/dist/lib/ai/inference-backend/client.js +301 -0
- package/dist/lib/ai/inference-backend/mocks.d.ts +12 -0
- package/dist/lib/ai/inference-backend/mocks.js +133 -0
- package/dist/lib/ai/inference-backend/types.d.ts +44 -0
- package/dist/lib/ai/inference-backend/types.js +1 -0
- package/dist/lib/ai/json-parse.d.ts +2 -0
- package/dist/lib/ai/json-parse.js +34 -0
- package/dist/lib/ai/llm-client.d.ts +6 -0
- package/dist/lib/ai/llm-client.js +19 -0
- package/dist/lib/ai/mock.d.ts +2 -0
- package/dist/lib/ai/mock.js +10 -0
- package/dist/lib/ai/types.d.ts +9 -0
- package/dist/lib/ai/types.js +1 -0
- package/dist/lib/chunking.d.ts +19 -0
- package/dist/lib/chunking.js +47 -0
- package/dist/lib/curated-kb-seed.d.ts +12 -0
- package/dist/lib/curated-kb-seed.js +155 -0
- package/dist/lib/email.js +67 -108
- package/dist/lib/embeddings.d.ts +2 -0
- package/dist/lib/embeddings.js +1 -0
- package/dist/lib/ensure-curated-kb-catalog.d.ts +6 -0
- package/dist/lib/ensure-curated-kb-catalog.js +53 -0
- package/dist/lib/env.d.ts +1 -5
- package/dist/lib/env.js +2 -7
- package/dist/lib/inference.d.ts +1 -8
- package/dist/lib/inference.js +1 -19
- package/dist/lib/kb-meta.d.ts +8 -0
- package/dist/lib/kb-meta.js +77 -0
- package/dist/lib/note-text.d.ts +1 -0
- package/dist/lib/note-text.js +47 -0
- package/dist/lib/notification-service.test.js +37 -36
- package/dist/lib/pdf.d.ts +11 -0
- package/dist/lib/pdf.js +11 -0
- package/dist/lib/usage_service.d.ts +2 -1
- package/dist/lib/usage_service.js +30 -12
- package/dist/lib/worksheet-generation.js +4 -4
- package/dist/lib/worksheet-generation.test.js +32 -17
- package/dist/lib/workspace-kb.d.ts +5 -0
- package/dist/lib/workspace-kb.js +7 -0
- package/dist/models/controller-context.model.d.ts +8 -0
- package/dist/models/controller-context.model.js +1 -0
- package/dist/repositories/artifact.repository.d.ts +60 -0
- package/dist/repositories/artifact.repository.js +40 -0
- package/dist/repositories/base.repository.d.ts +14 -0
- package/dist/repositories/base.repository.js +14 -0
- package/dist/repositories/invitation.repository.d.ts +94 -0
- package/dist/repositories/invitation.repository.js +44 -0
- package/dist/repositories/notification.repository.d.ts +72 -0
- package/dist/repositories/notification.repository.js +44 -0
- package/dist/repositories/router-module.repository.d.ts +10 -0
- package/dist/repositories/router-module.repository.js +14 -0
- package/dist/repositories/user.repository.d.ts +74 -0
- package/dist/repositories/user.repository.js +37 -0
- package/dist/repositories/workspace-member.repository.d.ts +31 -0
- package/dist/repositories/workspace-member.repository.js +31 -0
- package/dist/repositories/workspace.repository.d.ts +97 -0
- package/dist/repositories/workspace.repository.js +79 -0
- package/dist/routers/_app.d.ts +528 -33
- package/dist/routers/_app.js +4 -0
- package/dist/routers/admin.d.ts +0 -4
- package/dist/routers/admin.js +21 -549
- package/dist/routers/annotations.js +12 -170
- package/dist/routers/artifactVersions.d.ts +65 -0
- package/dist/routers/artifactVersions.js +14 -0
- package/dist/routers/auth.d.ts +0 -6
- package/dist/routers/auth.js +36 -421
- package/dist/routers/chat.js +15 -229
- package/dist/routers/copilot.d.ts +14 -13
- package/dist/routers/copilot.js +13 -532
- package/dist/routers/flashcards.d.ts +5 -5
- package/dist/routers/flashcards.js +23 -349
- package/dist/routers/knowledgeBase.d.ts +421 -0
- package/dist/routers/knowledgeBase.js +118 -0
- package/dist/routers/members.d.ts +0 -41
- package/dist/routers/members.js +22 -710
- package/dist/routers/notes.d.ts +94 -0
- package/dist/routers/notes.js +37 -0
- package/dist/routers/notifications.js +7 -109
- package/dist/routers/payment.d.ts +3 -2
- package/dist/routers/payment.js +11 -393
- package/dist/routers/podcast.d.ts +1 -1
- package/dist/routers/podcast.js +11 -784
- package/dist/routers/studyguide.js +3 -129
- package/dist/routers/worksheets.d.ts +29 -14
- package/dist/routers/worksheets.js +49 -628
- package/dist/routers/workspace.d.ts +0 -4
- package/dist/routers/workspace.js +28 -922
- package/dist/scripts/purge-deleted-users.js +2 -2
- package/dist/server.js +10 -3
- package/dist/services/activity/activity-human-description.service.d.ts +13 -0
- package/dist/services/activity/activity-human-description.service.js +221 -0
- package/dist/services/activity/activity-human-description.service.test.d.ts +1 -0
- package/dist/services/activity/activity-human-description.service.test.js +16 -0
- package/dist/services/activity/activity-log.service.d.ts +87 -0
- package/dist/services/activity/activity-log.service.js +276 -0
- package/dist/services/activity/activity-log.service.test.d.ts +1 -0
- package/dist/services/activity/activity-log.service.test.js +27 -0
- package/dist/services/activity-human-description.service.d.ts +13 -0
- package/dist/services/activity-human-description.service.js +221 -0
- package/dist/services/activity-human-description.service.test.d.ts +1 -0
- package/dist/services/activity-human-description.service.test.js +16 -0
- package/dist/services/activity-log.service.d.ts +87 -0
- package/dist/services/activity-log.service.js +276 -0
- package/dist/services/activity-log.service.test.d.ts +1 -0
- package/dist/services/activity-log.service.test.js +27 -0
- package/dist/services/admin/admin.service.d.ts +270 -0
- package/dist/services/admin/admin.service.js +476 -0
- package/dist/services/admin.service.d.ts +270 -0
- package/dist/services/admin.service.js +476 -0
- package/dist/services/ai/ai-session.service.d.ts +5 -0
- package/dist/services/ai/ai-session.service.js +4 -0
- package/dist/services/ai-session.service.d.ts +60 -0
- package/dist/services/ai-session.service.js +561 -0
- package/dist/services/annotation.service.d.ts +177 -0
- package/dist/services/annotation.service.js +154 -0
- package/dist/services/artifact-notification.service.d.ts +14 -0
- package/dist/services/artifact-notification.service.js +20 -0
- package/dist/services/artifact-version.service.d.ts +38 -0
- package/dist/services/artifact-version.service.js +129 -0
- package/dist/services/artifacts/annotation.service.d.ts +177 -0
- package/dist/services/artifacts/annotation.service.js +154 -0
- package/dist/services/artifacts/artifact-version.service.d.ts +38 -0
- package/dist/services/artifacts/artifact-version.service.js +129 -0
- package/dist/services/artifacts/chat.service.d.ts +127 -0
- package/dist/services/artifacts/chat.service.js +182 -0
- package/dist/services/artifacts/study-guide.service.d.ts +18 -0
- package/dist/services/artifacts/study-guide.service.js +65 -0
- package/dist/services/auth/auth.service.d.ts +94 -0
- package/dist/services/auth/auth.service.js +368 -0
- package/dist/services/auth.service.d.ts +94 -0
- package/dist/services/auth.service.js +368 -0
- package/dist/services/base.service.d.ts +14 -0
- package/dist/services/base.service.js +14 -0
- package/dist/services/billing/payment.service.d.ts +55 -0
- package/dist/services/billing/payment.service.js +368 -0
- package/dist/services/billing/subscription.service.d.ts +37 -0
- package/dist/services/billing/subscription.service.js +654 -0
- package/dist/services/billing/usage.service.d.ts +27 -0
- package/dist/services/billing/usage.service.js +77 -0
- package/dist/services/chat.service.d.ts +127 -0
- package/dist/services/chat.service.js +182 -0
- package/dist/services/content/copilot.service.d.ts +113 -0
- package/dist/services/content/copilot.service.js +453 -0
- package/dist/services/content/flashcard-progress.service.d.ts +159 -0
- package/dist/services/content/flashcard-progress.service.js +432 -0
- package/dist/services/content/flashcard.service.d.ts +140 -0
- package/dist/services/content/flashcard.service.js +326 -0
- package/dist/services/content/media-analysis.service.d.ts +23 -0
- package/dist/services/content/media-analysis.service.js +404 -0
- package/dist/services/content/podcast.service.d.ts +267 -0
- package/dist/services/content/podcast.service.js +653 -0
- package/dist/services/content/worksheet-content.service.d.ts +37 -0
- package/dist/services/content/worksheet-content.service.js +84 -0
- package/dist/services/content/worksheet-content.service.test.d.ts +1 -0
- package/dist/services/content/worksheet-content.service.test.js +69 -0
- package/dist/services/content/worksheet-generation.service.d.ts +91 -0
- package/dist/services/content/worksheet-generation.service.js +95 -0
- package/dist/services/content/worksheet-generation.service.test.d.ts +1 -0
- package/dist/services/content/worksheet-generation.service.test.js +20 -0
- package/dist/services/content/worksheet.service.d.ts +347 -0
- package/dist/services/content/worksheet.service.js +599 -0
- package/dist/services/copilot.service.d.ts +116 -0
- package/dist/services/copilot.service.js +447 -0
- package/dist/services/flashcard-progress.service.d.ts +2 -2
- package/dist/services/flashcard-progress.service.js +3 -2
- package/dist/services/flashcard.service.d.ts +140 -0
- package/dist/services/flashcard.service.js +325 -0
- package/dist/services/invitation.service.d.ts +66 -0
- package/dist/services/invitation.service.js +348 -0
- package/dist/services/knowledge/knowledge-base.service.d.ts +316 -0
- package/dist/services/knowledge/knowledge-base.service.js +544 -0
- package/dist/services/knowledge-base.service.d.ts +316 -0
- package/dist/services/knowledge-base.service.js +536 -0
- package/dist/services/media-analysis.service.d.ts +23 -0
- package/dist/services/media-analysis.service.js +384 -0
- package/dist/services/member.service.d.ts +36 -0
- package/dist/services/member.service.js +193 -0
- package/dist/services/members/invitation.service.d.ts +66 -0
- package/dist/services/members/invitation.service.js +348 -0
- package/dist/services/members/member.service.d.ts +36 -0
- package/dist/services/members/member.service.js +193 -0
- package/dist/services/note.service.d.ts +55 -0
- package/dist/services/note.service.js +111 -0
- package/dist/services/notification.service.d.ts +214 -0
- package/dist/services/notification.service.js +550 -0
- package/dist/services/notification.service.test.d.ts +1 -0
- package/dist/services/notification.service.test.js +87 -0
- package/dist/services/notifications/notification.service.d.ts +214 -0
- package/dist/services/notifications/notification.service.js +550 -0
- package/dist/services/notifications/notification.service.test.d.ts +1 -0
- package/dist/services/notifications/notification.service.test.js +87 -0
- package/dist/services/payment.service.d.ts +55 -0
- package/dist/services/payment.service.js +368 -0
- package/dist/services/podcast.service.d.ts +267 -0
- package/dist/services/podcast.service.js +654 -0
- package/dist/services/router-module.service.d.ts +7 -0
- package/dist/services/router-module.service.js +10 -0
- package/dist/services/study-guide.service.d.ts +18 -0
- package/dist/services/study-guide.service.js +65 -0
- package/dist/services/subscription.service.d.ts +37 -0
- package/dist/services/subscription.service.js +654 -0
- package/dist/services/usage-limit-policy.service.d.ts +12 -0
- package/dist/services/usage-limit-policy.service.js +22 -0
- package/dist/services/usage-limit-policy.service.test.d.ts +1 -0
- package/dist/services/usage-limit-policy.service.test.js +46 -0
- package/dist/services/usage.service.d.ts +27 -0
- package/dist/services/usage.service.js +77 -0
- package/dist/services/worksheet-content.service.d.ts +42 -0
- package/dist/services/worksheet-content.service.js +84 -0
- package/dist/services/worksheet-content.service.test.d.ts +1 -0
- package/dist/services/worksheet-content.service.test.js +69 -0
- package/dist/services/worksheet-generation.service.d.ts +91 -0
- package/dist/services/worksheet-generation.service.js +95 -0
- package/dist/services/worksheet-generation.service.test.d.ts +1 -0
- package/dist/services/worksheet-generation.service.test.js +20 -0
- package/dist/services/worksheet.service.d.ts +385 -0
- package/dist/services/worksheet.service.js +596 -0
- package/dist/services/workspace/workspace-analytics.service.d.ts +24 -0
- package/dist/services/workspace/workspace-analytics.service.js +95 -0
- package/dist/services/workspace/workspace-kb.service.d.ts +40 -0
- package/dist/services/workspace/workspace-kb.service.js +184 -0
- package/dist/services/workspace/workspace.service.d.ts +307 -0
- package/dist/services/workspace/workspace.service.js +394 -0
- package/dist/services/workspace-analytics.service.d.ts +24 -0
- package/dist/services/workspace-analytics.service.js +95 -0
- package/dist/services/workspace-kb.service.d.ts +40 -0
- package/dist/services/workspace-kb.service.js +184 -0
- package/dist/services/workspace-progress.service.d.ts +27 -0
- package/dist/services/workspace-progress.service.js +56 -0
- package/dist/services/workspace-progress.service.test.d.ts +1 -0
- package/dist/services/workspace-progress.service.test.js +49 -0
- package/dist/services/workspace.service.d.ts +307 -0
- package/dist/services/workspace.service.js +390 -0
- package/dist/trpc.js +2 -2
- package/package.json +5 -6
- package/prisma/migrations/20260509000001_add_knowledge_base/migration.sql +99 -0
- package/prisma/migrations/20260509000002_curate_knowledge_base/migration.sql +52 -0
- package/prisma/migrations/20260522000000_add_notes/migration.sql +27 -0
- package/prisma/migrations/20260524000000_remove_notes/migration.sql +3 -0
- package/prisma/schema.prisma +150 -48
- package/prisma/seed.mjs +67 -0
- package/scripts/debug/README.md +4 -0
- package/src/README.md +63 -0
- package/src/lib/ai/config.ts +34 -0
- package/src/lib/ai/embedding-client.ts +47 -0
- package/src/lib/ai/index.ts +62 -0
- package/src/lib/ai/inference-backend/client.ts +479 -0
- package/src/lib/ai/inference-backend/mocks.ts +171 -0
- package/src/lib/ai/inference-backend/types.ts +50 -0
- package/src/lib/ai/json-parse.ts +35 -0
- package/src/lib/ai/llm-client.ts +31 -0
- package/src/lib/ai/mock.ts +12 -0
- package/src/lib/ai/types.ts +11 -0
- package/src/lib/chunking.ts +81 -0
- package/src/lib/curated-kb-seed.ts +164 -0
- package/src/lib/email.ts +77 -115
- package/src/lib/embeddings.ts +9 -0
- package/src/lib/ensure-curated-kb-catalog.ts +60 -0
- package/src/lib/env.ts +2 -7
- package/src/lib/inference.ts +1 -21
- package/src/lib/kb-meta.ts +81 -0
- package/src/lib/pdf.ts +23 -0
- package/src/lib/workspace-kb.ts +7 -0
- package/src/repositories/artifact.repository.ts +55 -0
- package/src/repositories/base.repository.ts +19 -0
- package/src/repositories/invitation.repository.ts +53 -0
- package/src/repositories/notification.repository.ts +53 -0
- package/src/repositories/user.repository.ts +44 -0
- package/src/repositories/workspace-member.repository.ts +38 -0
- package/src/repositories/workspace.repository.ts +89 -0
- package/src/routers/_app.ts +4 -0
- package/src/routers/admin.ts +124 -692
- package/src/routers/annotations.ts +25 -203
- package/src/routers/artifactVersions.ts +32 -0
- package/src/routers/auth.ts +81 -519
- package/src/routers/chat.ts +42 -245
- package/src/routers/copilot.ts +41 -666
- package/src/routers/flashcards.ts +108 -404
- package/src/routers/knowledgeBase.ts +216 -0
- package/src/routers/members.ts +60 -782
- package/src/routers/notifications.ts +15 -117
- package/src/routers/payment.ts +37 -446
- package/src/routers/podcast.ts +36 -898
- package/src/routers/studyguide.ts +5 -144
- package/src/routers/worksheets.ts +171 -735
- package/src/routers/workspace.ts +138 -1109
- package/src/scripts/purge-deleted-users.ts +2 -2
- package/src/server.ts +10 -3
- package/src/{lib/activity_human_description.test.ts → services/activity/activity-human-description.service.test.ts} +1 -1
- package/src/{lib/activity_log_service.test.ts → services/activity/activity-log.service.test.ts} +1 -1
- package/src/{lib/activity_log_service.ts → services/activity/activity-log.service.ts} +2 -2
- package/src/services/admin/admin.service.ts +612 -0
- package/src/services/ai/ai-session.service.ts +5 -0
- package/src/services/artifacts/annotation.service.ts +189 -0
- package/src/services/artifacts/artifact-version.service.ts +151 -0
- package/src/services/artifacts/chat.service.ts +197 -0
- package/src/services/artifacts/study-guide.service.ts +72 -0
- package/src/services/auth/auth.service.ts +473 -0
- package/src/services/base.service.ts +19 -0
- package/src/services/billing/payment.service.ts +436 -0
- package/src/{lib/subscription_service.ts → services/billing/subscription.service.ts} +5 -5
- package/src/{lib/usage_service.ts → services/billing/usage.service.ts} +32 -12
- package/src/services/content/copilot.service.ts +596 -0
- package/src/services/{flashcard-progress.service.ts → content/flashcard-progress.service.ts} +6 -3
- package/src/services/content/flashcard.service.ts +394 -0
- package/src/services/content/media-analysis.service.ts +556 -0
- package/src/services/content/podcast.service.ts +777 -0
- package/src/services/content/worksheet-content.service.test.ts +83 -0
- package/src/services/content/worksheet-content.service.ts +117 -0
- package/src/{lib/worksheet-generation.test.ts → services/content/worksheet-generation.service.test.ts} +1 -1
- package/src/services/content/worksheet.service.ts +751 -0
- package/src/services/knowledge/knowledge-base.service.ts +705 -0
- package/src/services/members/invitation.service.ts +427 -0
- package/src/services/members/member.service.ts +241 -0
- package/src/{lib/notification-service.test.ts → services/notifications/notification.service.test.ts} +2 -2
- package/src/{lib/notification-service.ts → services/notifications/notification.service.ts} +102 -1
- package/src/services/workspace/workspace-analytics.service.ts +107 -0
- package/src/services/workspace/workspace-kb.service.ts +273 -0
- package/src/services/workspace/workspace.service.ts +481 -0
- package/src/trpc.ts +2 -2
- package/src/lib/ai-session.ts +0 -704
- package/src/lib/workspace-access.ts +0 -13
- /package/{check-difficulty.cjs → scripts/debug/check-difficulty.cjs} +0 -0
- /package/{check-questions.cjs → scripts/debug/check-questions.cjs} +0 -0
- /package/{db-summary.cjs → scripts/debug/db-summary.cjs} +0 -0
- /package/{mcq-test.cjs → scripts/debug/mcq-test.cjs} +0 -0
- /package/{test-generate.js → scripts/debug/test-generate.js} +0 -0
- /package/{test-ratio.cjs → scripts/debug/test-ratio.cjs} +0 -0
- /package/{zod-test.cjs → scripts/debug/zod-test.cjs} +0 -0
- /package/src/{lib/activity_human_description.ts → services/activity/activity-human-description.service.ts} +0 -0
- /package/src/{lib/worksheet-generation.ts → services/content/worksheet-generation.service.ts} +0 -0
package/src/routers/workspace.ts
CHANGED
|
@@ -1,1147 +1,176 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import { router, publicProcedure, authedProcedure, verifiedProcedure } from '../trpc.js';
|
|
4
|
-
import { supabaseClient } from '../lib/storage.js';
|
|
5
|
-
import { ArtifactType } from '../lib/constants.js';
|
|
6
|
-
import { aiSessionService } from '../lib/ai-session.js';
|
|
7
|
-
import PusherService from '../lib/pusher.js';
|
|
2
|
+
import { router, authedProcedure, verifiedProcedure } from '../trpc.js';
|
|
8
3
|
import { members } from './members.js';
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import { getUserUsage, getUserPlanLimits } from '../lib/usage_service.js';
|
|
13
|
-
import {
|
|
14
|
-
notifyArtifactFailed,
|
|
15
|
-
notifyArtifactReady,
|
|
16
|
-
notifyWorkspaceDeleted,
|
|
17
|
-
} from '../lib/notification-service.js';
|
|
18
|
-
|
|
19
|
-
// Helper function to update and emit analysis progress
|
|
20
|
-
async function updateAnalysisProgress(
|
|
21
|
-
db: PrismaClient,
|
|
22
|
-
workspaceId: string,
|
|
23
|
-
progress: any
|
|
24
|
-
) {
|
|
25
|
-
await db.workspace.update({
|
|
26
|
-
where: { id: workspaceId },
|
|
27
|
-
data: { analysisProgress: progress }
|
|
28
|
-
});
|
|
29
|
-
await PusherService.emitAnalysisProgress(workspaceId, progress);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// DRY helper to build progress steps for artifact generation pipeline
|
|
33
|
-
type StepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'error';
|
|
34
|
-
const PIPELINE_STEPS = ['fileUpload', 'fileAnalysis', 'studyGuide', 'flashcards'] as const;
|
|
35
|
-
|
|
36
|
-
function buildProgressSteps(
|
|
37
|
-
currentStep: typeof PIPELINE_STEPS[number],
|
|
38
|
-
currentStatus: StepStatus,
|
|
39
|
-
config: { generateStudyGuide: boolean; generateFlashcards: boolean },
|
|
40
|
-
overrides?: Partial<Record<typeof PIPELINE_STEPS[number], StepStatus>>
|
|
41
|
-
): Record<string, { order: number; status: StepStatus }> {
|
|
42
|
-
const stepIndex = PIPELINE_STEPS.indexOf(currentStep);
|
|
43
|
-
const steps: Record<string, { order: number; status: StepStatus }> = {};
|
|
44
|
-
|
|
45
|
-
for (let i = 0; i < PIPELINE_STEPS.length; i++) {
|
|
46
|
-
const step = PIPELINE_STEPS[i];
|
|
47
|
-
let status: StepStatus;
|
|
48
|
-
|
|
49
|
-
if (overrides?.[step]) {
|
|
50
|
-
status = overrides[step]!;
|
|
51
|
-
} else if (i < stepIndex) {
|
|
52
|
-
status = 'completed';
|
|
53
|
-
} else if (i === stepIndex) {
|
|
54
|
-
status = currentStatus;
|
|
55
|
-
} else {
|
|
56
|
-
// Future steps: check if they're configured
|
|
57
|
-
if (step === 'studyGuide' && !config.generateStudyGuide) {
|
|
58
|
-
status = 'skipped';
|
|
59
|
-
} else if (step === 'flashcards' && !config.generateFlashcards) {
|
|
60
|
-
status = 'skipped';
|
|
61
|
-
} else {
|
|
62
|
-
status = 'pending';
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
steps[step] = { order: i + 1, status };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return steps;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function buildProgress(
|
|
73
|
-
status: string,
|
|
74
|
-
filename: string,
|
|
75
|
-
fileType: string,
|
|
76
|
-
currentStep: typeof PIPELINE_STEPS[number],
|
|
77
|
-
currentStepStatus: StepStatus,
|
|
78
|
-
config: { generateStudyGuide: boolean; generateFlashcards: boolean },
|
|
79
|
-
extra?: Record<string, any>
|
|
80
|
-
) {
|
|
81
|
-
return {
|
|
82
|
-
status,
|
|
83
|
-
filename,
|
|
84
|
-
fileType,
|
|
85
|
-
startedAt: new Date().toISOString(),
|
|
86
|
-
steps: buildProgressSteps(currentStep, currentStepStatus, config, extra as any),
|
|
87
|
-
...extra,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Helper function to calculate search relevance score
|
|
92
|
-
function calculateRelevance(query: string, ...texts: (string | null | undefined)[]): number {
|
|
93
|
-
const queryLower = query.toLowerCase();
|
|
94
|
-
let score = 0;
|
|
95
|
-
|
|
96
|
-
for (const text of texts) {
|
|
97
|
-
if (!text) continue;
|
|
98
|
-
|
|
99
|
-
const textLower = text.toLowerCase();
|
|
100
|
-
|
|
101
|
-
// Exact match gets highest score
|
|
102
|
-
if (textLower.includes(queryLower)) {
|
|
103
|
-
score += 10;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Word boundary matches get good score
|
|
107
|
-
const words = queryLower.split(/\s+/);
|
|
108
|
-
for (const word of words) {
|
|
109
|
-
if (word.length > 2 && textLower.includes(word)) {
|
|
110
|
-
score += 5;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Partial matches get lower score
|
|
115
|
-
const queryChars = queryLower.split('');
|
|
116
|
-
let consecutiveMatches = 0;
|
|
117
|
-
for (const char of queryChars) {
|
|
118
|
-
if (textLower.includes(char)) {
|
|
119
|
-
consecutiveMatches++;
|
|
120
|
-
} else {
|
|
121
|
-
consecutiveMatches = 0;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
score += consecutiveMatches * 0.1;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return score;
|
|
128
|
-
}
|
|
4
|
+
import { WorkspaceService } from '../services/workspace/workspace.service.js';
|
|
5
|
+
import { WorkspaceAnalyticsService } from '../services/workspace/workspace-analytics.service.js';
|
|
6
|
+
import { MediaAnalysisService } from '../services/content/media-analysis.service.js';
|
|
129
7
|
|
|
130
8
|
export const workspace = router({
|
|
131
|
-
// List current user's workspaces
|
|
132
9
|
list: authedProcedure
|
|
133
|
-
.input(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const folders = await ctx.db.folder.findMany({
|
|
146
|
-
where: {
|
|
147
|
-
ownerId: ctx.session.user.id,
|
|
148
|
-
parentId: input.parentId ?? null,
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
return { workspaces, folders };
|
|
153
|
-
}),
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Fetches the entire directory tree for the user.
|
|
157
|
-
* Includes Folders, Workspaces (files), and Uploads (sub-files).
|
|
158
|
-
*/
|
|
159
|
-
getTree: authedProcedure
|
|
160
|
-
.query(async ({ ctx }) => {
|
|
161
|
-
const userId = ctx.session.user.id;
|
|
162
|
-
|
|
163
|
-
// 1. Fetch all folders
|
|
164
|
-
const allFolders = await ctx.db.folder.findMany({
|
|
165
|
-
where: { ownerId: userId },
|
|
166
|
-
orderBy: { updatedAt: 'desc' },
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// 2. Fetch all workspaces
|
|
170
|
-
const allWorkspaces = await ctx.db.workspace.findMany({
|
|
171
|
-
where: { ownerId: userId },
|
|
172
|
-
include: {
|
|
173
|
-
uploads: {
|
|
174
|
-
select: {
|
|
175
|
-
id: true,
|
|
176
|
-
name: true,
|
|
177
|
-
mimeType: true,
|
|
178
|
-
createdAt: true,
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
orderBy: { updatedAt: 'desc' },
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
folders: allFolders,
|
|
187
|
-
workspaces: allWorkspaces,
|
|
188
|
-
};
|
|
189
|
-
}),
|
|
10
|
+
.input(
|
|
11
|
+
z.object({
|
|
12
|
+
parentId: z.string().optional(),
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
.query(({ ctx, input }) =>
|
|
16
|
+
new WorkspaceService(ctx.db).list(ctx.session.user.id, input.parentId ?? null),
|
|
17
|
+
),
|
|
18
|
+
|
|
19
|
+
getTree: authedProcedure.query(({ ctx }) =>
|
|
20
|
+
new WorkspaceService(ctx.db).getTree(ctx.session.user.id),
|
|
21
|
+
),
|
|
190
22
|
|
|
191
23
|
create: authedProcedure
|
|
192
|
-
.input(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
ownerId: ctx.session.user.id,
|
|
204
|
-
folderId: input.parentId ?? null,
|
|
205
|
-
...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
|
|
206
|
-
artifacts: {
|
|
207
|
-
create: {
|
|
208
|
-
type: ArtifactType.FLASHCARD_SET,
|
|
209
|
-
title: "New Flashcard Set",
|
|
210
|
-
},
|
|
211
|
-
createMany: {
|
|
212
|
-
data: [
|
|
213
|
-
{ type: ArtifactType.WORKSHEET, title: "Worksheet 1" },
|
|
214
|
-
{ type: ArtifactType.WORKSHEET, title: "Worksheet 2" },
|
|
215
|
-
],
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
await aiSessionService.initSession(ws.id, ctx.session.user.id).catch((err) => {
|
|
222
|
-
logger.error('Failed to init AI session on workspace creation:', err);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
await PusherService.emitLibraryUpdate(ctx.session.user.id);
|
|
24
|
+
.input(
|
|
25
|
+
z.object({
|
|
26
|
+
name: z.string().min(1).max(100),
|
|
27
|
+
description: z.string().max(500).optional(),
|
|
28
|
+
parentId: z.string().optional(),
|
|
29
|
+
markerColor: z.string().nullable().optional(),
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
.mutation(({ ctx, input }) =>
|
|
33
|
+
new WorkspaceService(ctx.db).create(ctx.session.user.id, input),
|
|
34
|
+
),
|
|
226
35
|
|
|
227
|
-
return ws;
|
|
228
|
-
}),
|
|
229
36
|
createFolder: authedProcedure
|
|
230
|
-
.input(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
color: input.color ?? '#9D00FF',
|
|
241
|
-
parentId: input.parentId ?? null,
|
|
242
|
-
},
|
|
243
|
-
});
|
|
37
|
+
.input(
|
|
38
|
+
z.object({
|
|
39
|
+
name: z.string().min(1).max(100),
|
|
40
|
+
color: z.string().optional(),
|
|
41
|
+
parentId: z.string().optional(),
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
.mutation(({ ctx, input }) =>
|
|
45
|
+
new WorkspaceService(ctx.db).createFolder(ctx.session.user.id, input),
|
|
46
|
+
),
|
|
244
47
|
|
|
245
|
-
await PusherService.emitLibraryUpdate(ctx.session.user.id);
|
|
246
|
-
|
|
247
|
-
return folder;
|
|
248
|
-
}),
|
|
249
48
|
updateFolder: authedProcedure
|
|
250
|
-
.input(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
markerColor: input.markerColor
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
await PusherService.emitLibraryUpdate(ctx.session.user.id);
|
|
49
|
+
.input(
|
|
50
|
+
z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
name: z.string().min(1).max(100).optional(),
|
|
53
|
+
markerColor: z.string().nullable().optional(),
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
.mutation(({ ctx, input }) =>
|
|
57
|
+
new WorkspaceService(ctx.db).updateFolder(ctx.session.user.id, input),
|
|
58
|
+
),
|
|
265
59
|
|
|
266
|
-
return folder;
|
|
267
|
-
}),
|
|
268
60
|
deleteFolder: authedProcedure
|
|
269
|
-
.input(z.object({
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const folder = await ctx.db.folder.delete({ where: { id: input.id } });
|
|
61
|
+
.input(z.object({ id: z.string() }))
|
|
62
|
+
.mutation(({ ctx, input }) =>
|
|
63
|
+
new WorkspaceService(ctx.db).deleteFolder(ctx.session.user.id, input.id),
|
|
64
|
+
),
|
|
274
65
|
|
|
275
|
-
await PusherService.emitLibraryUpdate(ctx.session.user.id);
|
|
276
|
-
|
|
277
|
-
return folder;
|
|
278
|
-
}),
|
|
279
66
|
get: authedProcedure
|
|
280
|
-
.input(z.object({
|
|
281
|
-
|
|
282
|
-
}))
|
|
283
|
-
.query(async ({ ctx, input }) => {
|
|
284
|
-
const ws = await ctx.db.workspace.findFirst({
|
|
285
|
-
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
286
|
-
include: {
|
|
287
|
-
artifacts: true,
|
|
288
|
-
folder: true,
|
|
289
|
-
uploads: true,
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
293
|
-
return ws;
|
|
294
|
-
}),
|
|
295
|
-
getStats: authedProcedure
|
|
296
|
-
.query(async ({ ctx }) => {
|
|
297
|
-
const workspaces = await ctx.db.workspace.findMany({
|
|
298
|
-
where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
|
|
299
|
-
});
|
|
300
|
-
const folders = await ctx.db.folder.findMany({
|
|
301
|
-
where: { OR: [{ ownerId: ctx.session.user.id }] },
|
|
302
|
-
});
|
|
303
|
-
const lastUpdated = await ctx.db.workspace.findFirst({
|
|
304
|
-
where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
|
|
305
|
-
orderBy: { updatedAt: 'desc' },
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const spaceLeft = await ctx.db.fileAsset.aggregate({
|
|
309
|
-
where: { workspaceId: { in: workspaces.map((ws: any) => ws.id) }, userId: ctx.session.user.id },
|
|
310
|
-
_sum: { size: true },
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
const storageLimit = await getUserStorageLimit(ctx.session.user.id);
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
workspaces: workspaces.length,
|
|
317
|
-
folders: folders.length,
|
|
318
|
-
lastUpdated: lastUpdated?.updatedAt,
|
|
319
|
-
spaceUsed: spaceLeft._sum?.size ?? 0,
|
|
320
|
-
spaceTotal: storageLimit,
|
|
321
|
-
};
|
|
322
|
-
}),
|
|
323
|
-
|
|
324
|
-
// Study analytics: streaks, flashcard mastery, worksheet accuracy
|
|
325
|
-
getStudyAnalytics: authedProcedure
|
|
326
|
-
.query(async ({ ctx }) => {
|
|
327
|
-
const userId = ctx.session.user.id;
|
|
328
|
-
|
|
329
|
-
// Gather all study activity dates
|
|
330
|
-
const flashcardProgress = await ctx.db.flashcardProgress.findMany({
|
|
331
|
-
where: { userId },
|
|
332
|
-
select: { lastStudiedAt: true },
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
const worksheetProgress = await ctx.db.worksheetQuestionProgress.findMany({
|
|
336
|
-
where: { userId },
|
|
337
|
-
select: { updatedAt: true, completedAt: true },
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Build a set of unique study days (YYYY-MM-DD)
|
|
341
|
-
const studyDays = new Set<string>();
|
|
342
|
-
for (const fp of flashcardProgress) {
|
|
343
|
-
if (fp.lastStudiedAt) {
|
|
344
|
-
studyDays.add(fp.lastStudiedAt.toISOString().split('T')[0]);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
for (const wp of worksheetProgress) {
|
|
348
|
-
if (wp.completedAt) {
|
|
349
|
-
studyDays.add(wp.completedAt.toISOString().split('T')[0]);
|
|
350
|
-
} else {
|
|
351
|
-
studyDays.add(wp.updatedAt.toISOString().split('T')[0]);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Calculate streak (consecutive days ending today or yesterday)
|
|
356
|
-
const sortedDays = [...studyDays].sort().reverse();
|
|
357
|
-
let streak = 0;
|
|
358
|
-
|
|
359
|
-
if (sortedDays.length > 0) {
|
|
360
|
-
const today = new Date();
|
|
361
|
-
today.setHours(0, 0, 0, 0);
|
|
362
|
-
const yesterday = new Date(today);
|
|
363
|
-
yesterday.setDate(yesterday.getDate() - 1);
|
|
364
|
-
|
|
365
|
-
const todayStr = today.toISOString().split('T')[0];
|
|
366
|
-
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
|
367
|
-
|
|
368
|
-
// Streak only counts if the most recent study day is today or yesterday
|
|
369
|
-
if (sortedDays[0] === todayStr || sortedDays[0] === yesterdayStr) {
|
|
370
|
-
streak = 1;
|
|
371
|
-
for (let i = 1; i < sortedDays.length; i++) {
|
|
372
|
-
const current = new Date(sortedDays[i - 1]);
|
|
373
|
-
const prev = new Date(sortedDays[i]);
|
|
374
|
-
const diffDays = (current.getTime() - prev.getTime()) / (1000 * 60 * 60 * 24);
|
|
375
|
-
if (diffDays === 1) {
|
|
376
|
-
streak++;
|
|
377
|
-
} else {
|
|
378
|
-
break;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Weekly activity (last 7 days)
|
|
385
|
-
const weeklyActivity: boolean[] = [];
|
|
386
|
-
const today = new Date();
|
|
387
|
-
today.setHours(0, 0, 0, 0);
|
|
388
|
-
for (let i = 6; i >= 0; i--) {
|
|
389
|
-
const d = new Date(today);
|
|
390
|
-
d.setDate(d.getDate() - i);
|
|
391
|
-
const dayStr = d.toISOString().split('T')[0];
|
|
392
|
-
weeklyActivity.push(studyDays.has(dayStr));
|
|
393
|
-
}
|
|
67
|
+
.input(z.object({ id: z.string() }))
|
|
68
|
+
.query(({ ctx, input }) => new WorkspaceService(ctx.db).get(ctx.session.user.id, input.id)),
|
|
394
69
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
where: { userId, masteryLevel: { gte: 80 } },
|
|
399
|
-
});
|
|
400
|
-
const dueCards = await ctx.db.flashcardProgress.count({
|
|
401
|
-
where: { userId, nextReviewAt: { lte: new Date() } },
|
|
402
|
-
});
|
|
70
|
+
getStats: authedProcedure.query(({ ctx }) =>
|
|
71
|
+
new WorkspaceService(ctx.db).getStats(ctx.session.user.id),
|
|
72
|
+
),
|
|
403
73
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
});
|
|
408
|
-
const correctQuestions = await ctx.db.worksheetQuestionProgress.count({
|
|
409
|
-
where: { userId, correct: true },
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
return {
|
|
413
|
-
streak,
|
|
414
|
-
totalStudyDays: studyDays.size,
|
|
415
|
-
weeklyActivity,
|
|
416
|
-
flashcards: {
|
|
417
|
-
total: totalCards,
|
|
418
|
-
mastered: masteredCards,
|
|
419
|
-
dueForReview: dueCards,
|
|
420
|
-
},
|
|
421
|
-
worksheets: {
|
|
422
|
-
completed: completedQuestions,
|
|
423
|
-
correct: correctQuestions,
|
|
424
|
-
accuracy: completedQuestions > 0 ? Math.round((correctQuestions / completedQuestions) * 100) : 0,
|
|
425
|
-
},
|
|
426
|
-
};
|
|
427
|
-
}),
|
|
74
|
+
getStudyAnalytics: authedProcedure.query(({ ctx }) =>
|
|
75
|
+
new WorkspaceAnalyticsService(ctx.db).getStudyAnalytics(ctx.session.user.id),
|
|
76
|
+
),
|
|
428
77
|
|
|
429
78
|
update: authedProcedure
|
|
430
|
-
.input(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const updated = await ctx.db.workspace.update({
|
|
443
|
-
where: { id: input.id },
|
|
444
|
-
data: {
|
|
445
|
-
title: input.name ?? existed.title,
|
|
446
|
-
description: input.description,
|
|
447
|
-
// Preserve explicit null ("None color") instead of falling back.
|
|
448
|
-
markerColor: input.markerColor !== undefined ? input.markerColor : existed.markerColor,
|
|
449
|
-
icon: input.icon ?? existed.icon,
|
|
450
|
-
},
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
await PusherService.emitLibraryUpdate(ctx.session.user.id);
|
|
79
|
+
.input(
|
|
80
|
+
z.object({
|
|
81
|
+
id: z.string(),
|
|
82
|
+
name: z.string().min(1).max(100).optional(),
|
|
83
|
+
description: z.string().max(500).optional(),
|
|
84
|
+
markerColor: z.string().nullable().optional(),
|
|
85
|
+
icon: z.string().optional(),
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
.mutation(({ ctx, input }) =>
|
|
89
|
+
new WorkspaceService(ctx.db).update(ctx.session.user.id, input),
|
|
90
|
+
),
|
|
454
91
|
|
|
455
|
-
return updated;
|
|
456
|
-
}),
|
|
457
92
|
delete: authedProcedure
|
|
458
|
-
.input(z.object({
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const workspaceToDelete = await ctx.db.workspace.findFirst({
|
|
463
|
-
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
464
|
-
select: {
|
|
465
|
-
id: true,
|
|
466
|
-
title: true,
|
|
467
|
-
ownerId: true,
|
|
468
|
-
members: {
|
|
469
|
-
select: { userId: true },
|
|
470
|
-
},
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
if (!workspaceToDelete) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
475
|
-
|
|
476
|
-
const actor = await ctx.db.user.findUnique({
|
|
477
|
-
where: { id: ctx.session.user.id },
|
|
478
|
-
select: { name: true, email: true },
|
|
479
|
-
});
|
|
480
|
-
const actorName = actor?.name || actor?.email || 'A user';
|
|
481
|
-
|
|
482
|
-
await notifyWorkspaceDeleted(ctx.db, {
|
|
483
|
-
recipientUserIds: workspaceToDelete.members.map((m) => m.userId),
|
|
484
|
-
actorUserId: ctx.session.user.id,
|
|
485
|
-
actorName,
|
|
486
|
-
workspaceId: workspaceToDelete.id,
|
|
487
|
-
workspaceTitle: workspaceToDelete.title,
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
const deleted = await ctx.db.workspace.deleteMany({
|
|
491
|
-
where: { id: input.id, ownerId: ctx.session.user.id },
|
|
492
|
-
});
|
|
493
|
-
if (deleted.count === 0) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
93
|
+
.input(z.object({ id: z.string() }))
|
|
94
|
+
.mutation(({ ctx, input }) =>
|
|
95
|
+
new WorkspaceService(ctx.db).delete(ctx.session.user.id, input.id),
|
|
96
|
+
),
|
|
494
97
|
|
|
495
|
-
await PusherService.emitLibraryUpdate(ctx.session.user.id);
|
|
496
|
-
|
|
497
|
-
return true;
|
|
498
|
-
}),
|
|
499
98
|
getFolderInformation: authedProcedure
|
|
500
|
-
.input(z.object({
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
|
|
505
|
-
// find all of its parents
|
|
506
|
-
if (!folder) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
507
|
-
|
|
508
|
-
const parents = [];
|
|
509
|
-
let current = folder;
|
|
510
|
-
|
|
511
|
-
while (current.parentId) {
|
|
512
|
-
const parent = await ctx.db.folder.findFirst({ where: { id: current.parentId, ownerId: ctx.session.user.id } });
|
|
513
|
-
if (!parent) break;
|
|
514
|
-
parents.push(parent);
|
|
515
|
-
current = parent;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return { folder, parents };
|
|
519
|
-
}),
|
|
99
|
+
.input(z.object({ id: z.string() }))
|
|
100
|
+
.query(({ ctx, input }) =>
|
|
101
|
+
new WorkspaceService(ctx.db).getFolderInformation(ctx.session.user.id, input.id),
|
|
102
|
+
),
|
|
520
103
|
|
|
521
104
|
getSharedWith: authedProcedure
|
|
522
|
-
.input(z.object({
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
|
|
528
|
-
if (!user || !user.email) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
529
|
-
const sharedWith = await ctx.db.workspace.findMany({ where: { members: { some: { userId: ctx.session.user.id } } } });
|
|
530
|
-
const invitations = await ctx.db.workspaceInvitation.findMany({
|
|
531
|
-
where: { email: user.email, acceptedAt: null }, include: {
|
|
532
|
-
workspace: true,
|
|
533
|
-
}
|
|
534
|
-
});
|
|
105
|
+
.input(z.object({ id: z.string() }))
|
|
106
|
+
.query(({ ctx, input }) =>
|
|
107
|
+
new WorkspaceService(ctx.db).getSharedWith(ctx.session.user.id, input.id),
|
|
108
|
+
),
|
|
535
109
|
|
|
536
|
-
return { shared: sharedWith, invitations };
|
|
537
|
-
}),
|
|
538
110
|
uploadFiles: authedProcedure
|
|
539
|
-
.input(
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
z.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// Check storage limit
|
|
555
|
-
const workspaces = await ctx.db.workspace.findMany({
|
|
556
|
-
where: { OR: [{ ownerId: ctx.session.user.id }, { sharedWith: { some: { id: ctx.session.user.id } } }] },
|
|
557
|
-
});
|
|
558
|
-
const spaceUsed = await ctx.db.fileAsset.aggregate({
|
|
559
|
-
where: { workspaceId: { in: workspaces.map((w: any) => w.id) }, userId: ctx.session.user.id },
|
|
560
|
-
_sum: { size: true },
|
|
561
|
-
});
|
|
562
|
-
const storageLimit = await getUserStorageLimit(ctx.session.user.id);
|
|
563
|
-
const totalSize = input.files.reduce((acc, file) => acc + file.size, 0);
|
|
564
|
-
if ((spaceUsed._sum?.size ?? 0) + totalSize > storageLimit) {
|
|
565
|
-
logger.warn(`Storage limit exceeded for user ${ctx.session.user.id}. Used: ${spaceUsed._sum?.size}, Tried to upload: ${totalSize}, Limit: ${storageLimit}`);
|
|
566
|
-
throw new TRPCError({
|
|
567
|
-
code: 'FORBIDDEN',
|
|
568
|
-
message: `Storage limit exceeded. Maximum allowed storage is ${(storageLimit / (1024 * 1024 * 1024)).toFixed(1)}GB.`
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
const results = [];
|
|
572
|
-
|
|
573
|
-
for (const file of input.files) {
|
|
574
|
-
// 1. Insert into DB
|
|
575
|
-
const record = await ctx.db.fileAsset.create({
|
|
576
|
-
data: {
|
|
577
|
-
userId: ctx.session.user.id,
|
|
578
|
-
name: file.filename,
|
|
579
|
-
mimeType: file.contentType,
|
|
580
|
-
size: file.size,
|
|
581
|
-
workspaceId: input.id,
|
|
582
|
-
},
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
// 2. Generate signed URL for direct upload
|
|
586
|
-
const objectKey = `${ctx.session.user.id}/${record.id}-${file.filename}`;
|
|
587
|
-
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
588
|
-
.from('media')
|
|
589
|
-
.createSignedUploadUrl(objectKey); // 5 minutes
|
|
590
|
-
|
|
591
|
-
if (signedUrlError) {
|
|
592
|
-
throw new TRPCError({
|
|
593
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
594
|
-
message: `Failed to upload file`
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// 3. Update record with bucket info
|
|
599
|
-
await ctx.db.fileAsset.update({
|
|
600
|
-
where: { id: record.id },
|
|
601
|
-
data: {
|
|
602
|
-
bucket: 'media',
|
|
603
|
-
objectKey: objectKey,
|
|
604
|
-
},
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
results.push({
|
|
608
|
-
fileId: record.id,
|
|
609
|
-
uploadUrl: signedUrlData.signedUrl,
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return results;
|
|
111
|
+
.input(
|
|
112
|
+
z.object({
|
|
113
|
+
id: z.string(),
|
|
114
|
+
files: z.array(
|
|
115
|
+
z.object({
|
|
116
|
+
filename: z.string().min(1).max(255),
|
|
117
|
+
contentType: z.string().min(1).max(100),
|
|
118
|
+
size: z.number().min(1),
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
.mutation(({ ctx, input }) =>
|
|
124
|
+
new WorkspaceService(ctx.db).uploadFiles(ctx.session.user.id, input),
|
|
125
|
+
),
|
|
614
126
|
|
|
615
|
-
}),
|
|
616
127
|
deleteFiles: authedProcedure
|
|
617
|
-
.input(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
workspaceId: input.id,
|
|
627
|
-
userId: ctx.session.user.id,
|
|
628
|
-
},
|
|
629
|
-
});
|
|
630
|
-
// Delete from Supabase Storage (best-effort)
|
|
631
|
-
for (const file of files) {
|
|
632
|
-
if (file.bucket && file.objectKey) {
|
|
633
|
-
supabaseClient.storage
|
|
634
|
-
.from(file.bucket)
|
|
635
|
-
.remove([file.objectKey])
|
|
636
|
-
.catch((err: unknown) => {
|
|
637
|
-
logger.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
}
|
|
128
|
+
.input(
|
|
129
|
+
z.object({
|
|
130
|
+
fileId: z.array(z.string()),
|
|
131
|
+
id: z.string(),
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
.mutation(({ ctx, input }) =>
|
|
135
|
+
new WorkspaceService(ctx.db).deleteFiles(ctx.session.user.id, input),
|
|
136
|
+
),
|
|
641
137
|
|
|
642
|
-
await ctx.db.fileAsset.deleteMany({
|
|
643
|
-
where: {
|
|
644
|
-
id: { in: input.fileId },
|
|
645
|
-
workspaceId: input.id,
|
|
646
|
-
userId: ctx.session.user.id,
|
|
647
|
-
},
|
|
648
|
-
});
|
|
649
|
-
return true;
|
|
650
|
-
}),
|
|
651
138
|
getFileUploadUrl: authedProcedure
|
|
652
|
-
.input(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const spaceUsed = await ctx.db.fileAsset.aggregate({
|
|
664
|
-
where: { workspaceId: { in: workspaces.map((w: any) => w.id) }, userId: ctx.session.user.id },
|
|
665
|
-
_sum: { size: true },
|
|
666
|
-
});
|
|
667
|
-
const storageLimit = await getUserStorageLimit(ctx.session.user.id);
|
|
668
|
-
if ((spaceUsed._sum?.size ?? 0) + input.size > storageLimit) {
|
|
669
|
-
logger.warn(`Storage limit exceeded for user ${ctx.session.user.id}. Used: ${spaceUsed._sum?.size}, Tried to upload: ${input.size}, Limit: ${storageLimit}`);
|
|
670
|
-
throw new TRPCError({
|
|
671
|
-
code: 'FORBIDDEN',
|
|
672
|
-
message: `Storage limit exceeded. Maximum allowed storage is ${(storageLimit / (1024 * 1024 * 1024)).toFixed(1)}GB.`
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const objectKey = `workspace_${ctx.session.user.id}/${input.workspaceId}-file_${input.filename}`;
|
|
677
|
-
const fileAsset = await ctx.db.fileAsset.create({
|
|
678
|
-
data: {
|
|
679
|
-
workspaceId: input.workspaceId,
|
|
680
|
-
name: input.filename,
|
|
681
|
-
mimeType: input.contentType,
|
|
682
|
-
size: input.size,
|
|
683
|
-
userId: ctx.session.user.id,
|
|
684
|
-
bucket: 'media',
|
|
685
|
-
objectKey: objectKey,
|
|
686
|
-
},
|
|
687
|
-
});
|
|
688
|
-
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
689
|
-
.from('media')
|
|
690
|
-
.createSignedUploadUrl(objectKey, { upsert: true });
|
|
691
|
-
if (signedUrlError) {
|
|
692
|
-
logger.error('Signed upload URL error:', signedUrlError);
|
|
693
|
-
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to create upload URL: ${signedUrlError.message}` });
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
await ctx.db.workspace.update({
|
|
697
|
-
where: { id: input.workspaceId },
|
|
698
|
-
data: { needsAnalysis: true },
|
|
699
|
-
});
|
|
139
|
+
.input(
|
|
140
|
+
z.object({
|
|
141
|
+
workspaceId: z.string(),
|
|
142
|
+
filename: z.string(),
|
|
143
|
+
contentType: z.string(),
|
|
144
|
+
size: z.number(),
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
.query(({ ctx, input }) =>
|
|
148
|
+
new WorkspaceService(ctx.db).getFileUploadUrl(ctx.session.user.id, input),
|
|
149
|
+
),
|
|
700
150
|
|
|
701
|
-
return {
|
|
702
|
-
fileId: fileAsset.id,
|
|
703
|
-
uploadUrl: signedUrlData.signedUrl,
|
|
704
|
-
};
|
|
705
|
-
}),
|
|
706
151
|
uploadAndAnalyzeMedia: verifiedProcedure
|
|
707
|
-
.input(
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
id: z.string(),
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
.mutation(
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
where: { id: input.workspaceId, ownerId: ctx.session.user.id }
|
|
720
|
-
});
|
|
721
|
-
if (!workspace) {
|
|
722
|
-
logger.error('Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
|
|
723
|
-
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Check if analysis is already in progress
|
|
727
|
-
if (workspace.fileBeingAnalyzed) {
|
|
728
|
-
throw new TRPCError({
|
|
729
|
-
code: 'CONFLICT',
|
|
730
|
-
message: 'File analysis is already in progress for this workspace. Please wait for it to complete.'
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Fetch files from database
|
|
735
|
-
const files = await ctx.db.fileAsset.findMany({
|
|
736
|
-
where: {
|
|
737
|
-
id: { in: input.files.map(file => file.id) },
|
|
738
|
-
workspaceId: input.workspaceId,
|
|
739
|
-
userId: ctx.session.user.id,
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
if (files.length === 0) {
|
|
744
|
-
throw new TRPCError({
|
|
745
|
-
code: 'NOT_FOUND',
|
|
746
|
-
message: 'No files found with the provided IDs'
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Validate all files have bucket and objectKey
|
|
751
|
-
for (const file of files) {
|
|
752
|
-
if (!file.bucket || !file.objectKey) {
|
|
753
|
-
throw new TRPCError({
|
|
754
|
-
code: 'BAD_REQUEST',
|
|
755
|
-
message: `File ${file.id} does not have bucket or objectKey set`
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Use the first file for progress tracking and artifact naming
|
|
761
|
-
const primaryFile = files[0];
|
|
762
|
-
const fileType = primaryFile.mimeType.startsWith('image/') ? 'image' : 'pdf';
|
|
763
|
-
try {
|
|
764
|
-
// Set analysis in progress flag
|
|
765
|
-
await ctx.db.workspace.update({
|
|
766
|
-
where: { id: input.workspaceId },
|
|
767
|
-
data: { fileBeingAnalyzed: true },
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
const genConfig = { generateStudyGuide: input.generateStudyGuide, generateFlashcards: input.generateFlashcards };
|
|
771
|
-
|
|
772
|
-
PusherService.emitAnalysisProgress(input.workspaceId,
|
|
773
|
-
buildProgress('starting', primaryFile.name, fileType, 'fileUpload', 'pending', genConfig)
|
|
774
|
-
);
|
|
775
|
-
|
|
776
|
-
try {
|
|
777
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
778
|
-
buildProgress('starting', primaryFile.name, fileType, 'fileUpload', 'pending', genConfig)
|
|
779
|
-
);
|
|
780
|
-
} catch (error) {
|
|
781
|
-
logger.error('Failed to update analysis progress:', error);
|
|
782
|
-
await ctx.db.workspace.update({
|
|
783
|
-
where: { id: input.workspaceId },
|
|
784
|
-
data: { fileBeingAnalyzed: false },
|
|
785
|
-
});
|
|
786
|
-
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
787
|
-
throw error;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
791
|
-
buildProgress('uploading', primaryFile.name, fileType, 'fileUpload', 'in_progress', genConfig)
|
|
792
|
-
);
|
|
793
|
-
|
|
794
|
-
// Process all files using the new process_file endpoint
|
|
795
|
-
for (const file of files) {
|
|
796
|
-
// TypeScript: We already validated bucket and objectKey exist above
|
|
797
|
-
if (!file.bucket || !file.objectKey) {
|
|
798
|
-
continue; // Skip if somehow missing (shouldn't happen due to validation above)
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
802
|
-
.from(file.bucket)
|
|
803
|
-
.createSignedUrl(file.objectKey, 24 * 60 * 60); // 24 hours expiry
|
|
804
|
-
|
|
805
|
-
if (signedUrlError) {
|
|
806
|
-
await ctx.db.workspace.update({
|
|
807
|
-
where: { id: input.workspaceId },
|
|
808
|
-
data: { fileBeingAnalyzed: false },
|
|
809
|
-
});
|
|
810
|
-
throw new TRPCError({
|
|
811
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
812
|
-
message: `Failed to upload file`
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const fileUrl = signedUrlData.signedUrl;
|
|
817
|
-
const currentFileType = file.mimeType.startsWith('image/') ? 'image' : 'pdf';
|
|
818
|
-
|
|
819
|
-
// Use maxPages for large PDFs (>50 pages) to limit processing
|
|
820
|
-
const maxPages = currentFileType === 'pdf' && file.size && file.size > 50 ? 50 : undefined;
|
|
821
|
-
|
|
822
|
-
const processResult = await aiSessionService.processFile(
|
|
823
|
-
input.workspaceId,
|
|
824
|
-
ctx.session.user.id,
|
|
825
|
-
fileUrl,
|
|
826
|
-
currentFileType,
|
|
827
|
-
maxPages
|
|
828
|
-
);
|
|
152
|
+
.input(
|
|
153
|
+
z.object({
|
|
154
|
+
workspaceId: z.string(),
|
|
155
|
+
files: z.array(z.object({ id: z.string() })),
|
|
156
|
+
generateStudyGuide: z.boolean().default(true),
|
|
157
|
+
generateFlashcards: z.boolean().default(true),
|
|
158
|
+
generateWorksheet: z.boolean().default(true),
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
.mutation(({ ctx, input }) =>
|
|
162
|
+
new MediaAnalysisService(ctx.db).uploadAndAnalyzeMedia(input, ctx.session.user.id),
|
|
163
|
+
),
|
|
829
164
|
|
|
830
|
-
if (processResult.status === 'error') {
|
|
831
|
-
logger.error(`Failed to process file ${file.name}:`, processResult.error);
|
|
832
|
-
// Continue processing other files even if one fails
|
|
833
|
-
// Optionally, you could throw an error or mark this file as failed
|
|
834
|
-
} else {
|
|
835
|
-
logger.info(`Successfully processed file ${file.name}: ${processResult.pageCount} pages`);
|
|
836
|
-
|
|
837
|
-
// Store the comprehensive description in aiTranscription field
|
|
838
|
-
await ctx.db.fileAsset.update({
|
|
839
|
-
where: { id: file.id },
|
|
840
|
-
data: {
|
|
841
|
-
aiTranscription: {
|
|
842
|
-
comprehensiveDescription: processResult.comprehensiveDescription,
|
|
843
|
-
textContent: processResult.textContent,
|
|
844
|
-
imageDescriptions: processResult.imageDescriptions,
|
|
845
|
-
},
|
|
846
|
-
}
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
852
|
-
buildProgress('analyzing', primaryFile.name, fileType, 'fileAnalysis', 'in_progress', genConfig)
|
|
853
|
-
);
|
|
854
|
-
|
|
855
|
-
try {
|
|
856
|
-
// Analyze all files - use PDF analysis if any file is a PDF, otherwise use image analysis
|
|
857
|
-
// const hasPDF = files.some(f => !f.mimeType.startsWith('image/'));
|
|
858
|
-
// if (hasPDF) {
|
|
859
|
-
// await aiSessionService.analysePDF(input.workspaceId, ctx.session.user.id, file.id);
|
|
860
|
-
// } else {
|
|
861
|
-
// // If all files are images, analyze them
|
|
862
|
-
// for (const file of files) {
|
|
863
|
-
// await aiSessionService.analyseImage(input.workspaceId, ctx.session.user.id, file.id);
|
|
864
|
-
// }
|
|
865
|
-
// }
|
|
866
|
-
|
|
867
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
868
|
-
buildProgress('generating_artifacts', primaryFile.name, fileType, 'studyGuide', 'pending', genConfig)
|
|
869
|
-
);
|
|
870
|
-
} catch (error) {
|
|
871
|
-
logger.error('Failed to analyze files:', error);
|
|
872
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
873
|
-
buildProgress('error', primaryFile.name, fileType, 'fileAnalysis', 'error', genConfig, {
|
|
874
|
-
error: `Failed to analyze ${fileType}: ${error}`,
|
|
875
|
-
studyGuide: 'skipped', flashcards: 'skipped',
|
|
876
|
-
})
|
|
877
|
-
);
|
|
878
|
-
await ctx.db.workspace.update({
|
|
879
|
-
where: { id: input.workspaceId },
|
|
880
|
-
data: { fileBeingAnalyzed: false },
|
|
881
|
-
});
|
|
882
|
-
throw error;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const results: {
|
|
886
|
-
filename: string;
|
|
887
|
-
artifacts: {
|
|
888
|
-
studyGuide: any | null;
|
|
889
|
-
flashcards: any | null;
|
|
890
|
-
worksheet: any | null;
|
|
891
|
-
};
|
|
892
|
-
} = {
|
|
893
|
-
filename: primaryFile.name,
|
|
894
|
-
artifacts: {
|
|
895
|
-
studyGuide: null,
|
|
896
|
-
flashcards: null,
|
|
897
|
-
worksheet: null,
|
|
898
|
-
}
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
// Ensure AI session is initialized before generating artifacts
|
|
902
|
-
try {
|
|
903
|
-
await aiSessionService.initSession(input.workspaceId, ctx.session.user.id);
|
|
904
|
-
} catch (initError) {
|
|
905
|
-
logger.error('Failed to init AI session (continuing with workspace context):', initError);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Fetch current usage and limits to enforce plan restrictions for auto-generation
|
|
909
|
-
const [usage, limits] = await Promise.all([
|
|
910
|
-
getUserUsage(ctx.session.user.id),
|
|
911
|
-
getUserPlanLimits(ctx.session.user.id)
|
|
912
|
-
]);
|
|
913
|
-
|
|
914
|
-
// Generate artifacts - each step is isolated so failures don't block subsequent steps
|
|
915
|
-
if (input.generateStudyGuide) {
|
|
916
|
-
// Enforcement: Skip if limit reached
|
|
917
|
-
if (limits && usage.studyGuides >= limits.maxStudyGuides) {
|
|
918
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
919
|
-
buildProgress('skipped', primaryFile.name, fileType, 'studyGuide', 'skipped', genConfig)
|
|
920
|
-
);
|
|
921
|
-
await PusherService.emitError(input.workspaceId, 'Study guide skipped: Limit reached.', 'study_guide');
|
|
922
|
-
await notifyArtifactFailed(ctx.db, {
|
|
923
|
-
userId: ctx.session.user.id,
|
|
924
|
-
workspaceId: input.workspaceId,
|
|
925
|
-
artifactType: ArtifactType.STUDY_GUIDE,
|
|
926
|
-
message: 'Study guide was skipped because your plan limit was reached.',
|
|
927
|
-
}).catch(() => {});
|
|
928
|
-
} else {
|
|
929
|
-
try {
|
|
930
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
931
|
-
buildProgress('generating_study_guide', primaryFile.name, fileType, 'studyGuide', 'in_progress', genConfig)
|
|
932
|
-
);
|
|
933
|
-
|
|
934
|
-
const content = await aiSessionService.generateStudyGuide(input.workspaceId, ctx.session.user.id);
|
|
935
|
-
|
|
936
|
-
let artifact = await ctx.db.artifact.findFirst({
|
|
937
|
-
where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
|
|
938
|
-
});
|
|
939
|
-
if (!artifact) {
|
|
940
|
-
artifact = await ctx.db.artifact.create({
|
|
941
|
-
data: {
|
|
942
|
-
workspaceId: input.workspaceId,
|
|
943
|
-
type: ArtifactType.STUDY_GUIDE,
|
|
944
|
-
title: files.length === 1 ? `Study Guide - ${primaryFile.name}` : `Study Guide - ${files.length} files`,
|
|
945
|
-
createdById: ctx.session.user.id,
|
|
946
|
-
},
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
const lastVersion = await ctx.db.artifactVersion.findFirst({
|
|
951
|
-
where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
|
|
952
|
-
orderBy: { version: 'desc' },
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
await ctx.db.artifactVersion.create({
|
|
956
|
-
data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
results.artifacts.studyGuide = artifact;
|
|
960
|
-
await PusherService.emitStudyGuideComplete(input.workspaceId, artifact);
|
|
961
|
-
await notifyArtifactReady(ctx.db, {
|
|
962
|
-
userId: ctx.session.user.id,
|
|
963
|
-
workspaceId: input.workspaceId,
|
|
964
|
-
artifactId: artifact.id,
|
|
965
|
-
artifactType: ArtifactType.STUDY_GUIDE,
|
|
966
|
-
title: artifact.title,
|
|
967
|
-
}).catch(() => {});
|
|
968
|
-
} catch (sgError) {
|
|
969
|
-
logger.error('Study guide generation failed after retries:', sgError);
|
|
970
|
-
await PusherService.emitError(input.workspaceId, 'Study guide generation failed. Please try regenerating later.', 'study_guide');
|
|
971
|
-
await notifyArtifactFailed(ctx.db, {
|
|
972
|
-
userId: ctx.session.user.id,
|
|
973
|
-
workspaceId: input.workspaceId,
|
|
974
|
-
artifactType: ArtifactType.STUDY_GUIDE,
|
|
975
|
-
message: 'Study guide generation failed. Please try regenerating later.',
|
|
976
|
-
}).catch(() => {});
|
|
977
|
-
// Continue to flashcards - don't abort the whole pipeline
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (input.generateFlashcards) {
|
|
983
|
-
// Enforcement: Skip if limit reached
|
|
984
|
-
if (limits && usage.flashcards >= limits.maxFlashcards) {
|
|
985
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
986
|
-
buildProgress('skipped', primaryFile.name, fileType, 'flashcards', 'skipped', genConfig)
|
|
987
|
-
);
|
|
988
|
-
await PusherService.emitError(input.workspaceId, 'Flashcards skipped: Limit reached.', 'flashcards');
|
|
989
|
-
await notifyArtifactFailed(ctx.db, {
|
|
990
|
-
userId: ctx.session.user.id,
|
|
991
|
-
workspaceId: input.workspaceId,
|
|
992
|
-
artifactType: ArtifactType.FLASHCARD_SET,
|
|
993
|
-
message: 'Flashcards were skipped because your plan limit was reached.',
|
|
994
|
-
}).catch(() => {});
|
|
995
|
-
} else {
|
|
996
|
-
try {
|
|
997
|
-
const sgStatus = input.generateStudyGuide ? (results.artifacts.studyGuide ? 'completed' : 'error') : 'skipped';
|
|
998
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId,
|
|
999
|
-
buildProgress('generating_flashcards', primaryFile.name, fileType, 'flashcards', 'in_progress', genConfig,
|
|
1000
|
-
{ studyGuide: sgStatus } as any)
|
|
1001
|
-
);
|
|
1002
|
-
|
|
1003
|
-
const content = await aiSessionService.generateFlashcardQuestions(input.workspaceId, ctx.session.user.id, 10, 'medium');
|
|
1004
|
-
|
|
1005
|
-
const artifact = await ctx.db.artifact.create({
|
|
1006
|
-
data: {
|
|
1007
|
-
workspaceId: input.workspaceId,
|
|
1008
|
-
type: ArtifactType.FLASHCARD_SET,
|
|
1009
|
-
title: files.length === 1 ? `Flashcards - ${primaryFile.name}` : `Flashcards - ${files.length} files`,
|
|
1010
|
-
createdById: ctx.session.user.id,
|
|
1011
|
-
},
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
// Parse JSON flashcard content
|
|
1015
|
-
try {
|
|
1016
|
-
const parsed = typeof content === 'string' ? JSON.parse(content) : content;
|
|
1017
|
-
const flashcardData = Array.isArray(parsed) ? parsed : (parsed.flashcards || []);
|
|
1018
|
-
|
|
1019
|
-
for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
|
|
1020
|
-
const card = flashcardData[i];
|
|
1021
|
-
const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
|
|
1022
|
-
const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
|
|
1023
|
-
|
|
1024
|
-
await ctx.db.flashcard.create({
|
|
1025
|
-
data: {
|
|
1026
|
-
artifactId: artifact.id,
|
|
1027
|
-
front: front,
|
|
1028
|
-
back: back,
|
|
1029
|
-
order: i,
|
|
1030
|
-
tags: ['ai-generated', 'medium'],
|
|
1031
|
-
},
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
} catch (parseError) {
|
|
1035
|
-
console.error("Failed to parse flashcard JSON or create cards in workspace router:", parseError);
|
|
1036
|
-
// Fallback to text parsing if JSON fails
|
|
1037
|
-
const lines = content.split('\n').filter((line: string) => line.trim());
|
|
1038
|
-
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
1039
|
-
const line = lines[i];
|
|
1040
|
-
if (line.includes(' - ')) {
|
|
1041
|
-
const [front, back] = line.split(' - ');
|
|
1042
|
-
await ctx.db.flashcard.create({
|
|
1043
|
-
data: {
|
|
1044
|
-
artifactId: artifact.id,
|
|
1045
|
-
front: front.trim(),
|
|
1046
|
-
back: back.trim(),
|
|
1047
|
-
order: i,
|
|
1048
|
-
tags: ['ai-generated', 'medium'],
|
|
1049
|
-
},
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
results.artifacts.flashcards = artifact;
|
|
1056
|
-
await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
|
|
1057
|
-
await notifyArtifactReady(ctx.db, {
|
|
1058
|
-
userId: ctx.session.user.id,
|
|
1059
|
-
workspaceId: input.workspaceId,
|
|
1060
|
-
artifactId: artifact.id,
|
|
1061
|
-
artifactType: ArtifactType.FLASHCARD_SET,
|
|
1062
|
-
title: artifact.title,
|
|
1063
|
-
}).catch(() => {});
|
|
1064
|
-
} catch (fcError) {
|
|
1065
|
-
logger.error('Flashcard generation failed after retries:', fcError);
|
|
1066
|
-
await PusherService.emitError(input.workspaceId, 'Flashcard generation failed. Please try regenerating later.', 'flashcards');
|
|
1067
|
-
await notifyArtifactFailed(ctx.db, {
|
|
1068
|
-
userId: ctx.session.user.id,
|
|
1069
|
-
workspaceId: input.workspaceId,
|
|
1070
|
-
artifactType: ArtifactType.FLASHCARD_SET,
|
|
1071
|
-
message: 'Flashcard generation failed. Please try regenerating later.',
|
|
1072
|
-
}).catch(() => {});
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
await ctx.db.workspace.update({
|
|
1078
|
-
where: { id: input.workspaceId },
|
|
1079
|
-
data: { fileBeingAnalyzed: false },
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
await updateAnalysisProgress(ctx.db, input.workspaceId, {
|
|
1083
|
-
...buildProgress('completed', primaryFile.name, fileType, 'flashcards', 'completed', genConfig),
|
|
1084
|
-
completedAt: new Date().toISOString(),
|
|
1085
|
-
});
|
|
1086
|
-
return results;
|
|
1087
|
-
} catch (error) {
|
|
1088
|
-
logger.error('Failed to update analysis progress:', error);
|
|
1089
|
-
await ctx.db.workspace.update({
|
|
1090
|
-
where: { id: input.workspaceId },
|
|
1091
|
-
data: { fileBeingAnalyzed: false },
|
|
1092
|
-
});
|
|
1093
|
-
await PusherService.emitError(input.workspaceId, `Failed to update analysis progress: ${error}`, 'file_analysis');
|
|
1094
|
-
throw error;
|
|
1095
|
-
}
|
|
1096
|
-
}),
|
|
1097
165
|
search: authedProcedure
|
|
1098
|
-
.input(
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
// 1. Search Workspaces
|
|
1107
|
-
const workspaces = await ctx.db.workspace.findMany({
|
|
1108
|
-
where: {
|
|
1109
|
-
ownerId: ctx.session.user.id,
|
|
1110
|
-
markerColor: color || undefined,
|
|
1111
|
-
...(query ? {
|
|
1112
|
-
OR: [
|
|
1113
|
-
{ title: { contains: query, mode: 'insensitive' } },
|
|
1114
|
-
{ description: { contains: query, mode: 'insensitive' } },
|
|
1115
|
-
],
|
|
1116
|
-
} : {}),
|
|
1117
|
-
},
|
|
1118
|
-
orderBy: { updatedAt: 'desc' },
|
|
1119
|
-
take: input.limit,
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
// 2. Search Folders
|
|
1123
|
-
const folders = await ctx.db.folder.findMany({
|
|
1124
|
-
where: {
|
|
1125
|
-
ownerId: ctx.session.user.id,
|
|
1126
|
-
markerColor: color || undefined,
|
|
1127
|
-
...(query ? {
|
|
1128
|
-
name: { contains: query, mode: 'insensitive' },
|
|
1129
|
-
} : {}),
|
|
1130
|
-
},
|
|
1131
|
-
orderBy: { updatedAt: 'desc' },
|
|
1132
|
-
take: input.limit,
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
// Combined results with type discriminator
|
|
1136
|
-
const results = [
|
|
1137
|
-
...workspaces.map((w: any) => ({ ...w, type: 'workspace' as const })),
|
|
1138
|
-
...folders.map((f: any) => ({ ...f, type: 'folder' as const, title: f.name })), // normalize name to title
|
|
1139
|
-
].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
|
1140
|
-
.slice(0, input.limit);
|
|
1141
|
-
|
|
1142
|
-
return results;
|
|
1143
|
-
}),
|
|
166
|
+
.input(
|
|
167
|
+
z.object({
|
|
168
|
+
query: z.string(),
|
|
169
|
+
color: z.string().optional(),
|
|
170
|
+
limit: z.number().min(1).max(100).default(20),
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
.query(({ ctx, input }) => new WorkspaceService(ctx.db).search(ctx.session.user.id, input)),
|
|
1144
174
|
|
|
1145
|
-
// Members sub-router
|
|
1146
175
|
members,
|
|
1147
176
|
});
|