@speakableio/core 1.0.20 → 1.0.22

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.
@@ -30,12 +30,116 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/entry-points/index.native.ts
31
31
  var index_native_exports = {};
32
32
  __export(index_native_exports, {
33
- createFsClient: () => createFsClientNative
33
+ ActivityPageType: () => ActivityPageType,
34
+ BASE_MULTIPLE_CHOICE_FIELD_VALUES: () => BASE_MULTIPLE_CHOICE_FIELD_VALUES,
35
+ BASE_REPEAT_FIELD_VALUES: () => BASE_REPEAT_FIELD_VALUES,
36
+ BASE_RESPOND_FIELD_VALUES: () => BASE_RESPOND_FIELD_VALUES,
37
+ FeedbackTypesCard: () => FeedbackTypesCard,
38
+ FsCtx: () => FsCtx,
39
+ LENIENCY_OPTIONS: () => LENIENCY_OPTIONS,
40
+ LeniencyCard: () => LeniencyCard,
41
+ MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES: () => MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES,
42
+ REPEAT_PAGE_ACTIVITY_TYPES: () => REPEAT_PAGE_ACTIVITY_TYPES,
43
+ RESPOND_AUDIO_PAGE_ACTIVITY_TYPES: () => RESPOND_AUDIO_PAGE_ACTIVITY_TYPES,
44
+ RESPOND_PAGE_ACTIVITY_TYPES: () => RESPOND_PAGE_ACTIVITY_TYPES,
45
+ RESPOND_WRITE_PAGE_ACTIVITY_TYPES: () => RESPOND_WRITE_PAGE_ACTIVITY_TYPES,
46
+ SPEAKABLE_NOTIFICATIONS: () => SPEAKABLE_NOTIFICATIONS,
47
+ STUDENT_LEVELS_OPTIONS: () => STUDENT_LEVELS_OPTIONS,
48
+ SpeakableNotificationTypes: () => SpeakableNotificationTypes,
49
+ SpeakableProvider: () => SpeakableProvider,
50
+ VerificationCardStatus: () => VerificationCardStatus,
51
+ assignmentQueryKeys: () => assignmentQueryKeys,
52
+ cardsQueryKeys: () => cardsQueryKeys,
53
+ checkIsMCPage: () => checkIsMCPage,
54
+ checkIsMediaPage: () => checkIsMediaPage,
55
+ checkIsRepeatPage: () => checkIsRepeatPage,
56
+ checkIsRespondAudioPage: () => checkIsRespondAudioPage,
57
+ checkIsRespondPage: () => checkIsRespondPage,
58
+ checkIsRespondWrittenPage: () => checkIsRespondWrittenPage,
59
+ checkIsShortAnswerPage: () => checkIsShortAnswerPage,
60
+ checkTypePageActivity: () => checkTypePageActivity,
61
+ cleanString: () => cleanString,
62
+ createAssignmentRepo: () => createAssignmentRepo,
63
+ createCardRepo: () => createCardRepo,
64
+ createFsClient: () => createFsClientNative,
65
+ createSetRepo: () => createSetRepo,
66
+ creditQueryKeys: () => creditQueryKeys,
67
+ debounce: () => debounce,
68
+ getCardFromCache: () => getCardFromCache,
69
+ getLabelPage: () => getLabelPage,
70
+ getPagePrompt: () => getPagePrompt,
71
+ getPhraseLength: () => getPhraseLength,
72
+ getRespondCardTool: () => getRespondCardTool,
73
+ getSetFromCache: () => getSetFromCache,
74
+ getTotalCompletedCards: () => getTotalCompletedCards,
75
+ getTranscript: () => getTranscript,
76
+ getWordHash: () => getWordHash,
77
+ purify: () => purify,
78
+ refsCardsFiresotre: () => refsCardsFiresotre,
79
+ refsSetsFirestore: () => refsSetsFirestore,
80
+ scoreQueryKeys: () => scoreQueryKeys,
81
+ setsQueryKeys: () => setsQueryKeys,
82
+ updateCardInCache: () => updateCardInCache,
83
+ updateSetInCache: () => updateSetInCache,
84
+ useActivity: () => useActivity,
85
+ useActivityFeedbackAccess: () => useActivityFeedbackAccess,
86
+ useAssignment: () => useAssignment,
87
+ useBaseOpenAI: () => useBaseOpenAI,
88
+ useCards: () => useCards,
89
+ useClearScore: () => useClearScore,
90
+ useClearScoreV2: () => useClearScoreV2,
91
+ useCreateCard: () => useCreateCard,
92
+ useCreateCards: () => useCreateCards,
93
+ useCreateNotification: () => useCreateNotification,
94
+ useGetCard: () => useGetCard,
95
+ useOrganizationAccess: () => useOrganizationAccess,
96
+ useScore: () => useScore,
97
+ useSet: () => useSet,
98
+ useSpeakableApi: () => useSpeakableApi,
99
+ useSpeakableTranscript: () => useSpeakableTranscript,
100
+ useSubmitAssignmentScore: () => useSubmitAssignmentScore,
101
+ useSubmitPracticeScore: () => useSubmitPracticeScore,
102
+ useUpdateCardScore: () => useUpdateCardScore,
103
+ useUpdateScore: () => useUpdateScore,
104
+ useUpdateStudentVocab: () => useUpdateStudentVocab,
105
+ useUserCredits: () => useUserCredits
34
106
  });
35
107
  module.exports = __toCommonJS(index_native_exports);
36
108
 
37
- // src/lib/create-firebase-client-native.ts
38
- var import_firestore = require("@react-native-firebase/firestore");
109
+ // src/providers/SpeakableProvider.tsx
110
+ var import_react = require("react");
111
+ var import_jsx_runtime = require("react/jsx-runtime");
112
+ var FsCtx = (0, import_react.createContext)(null);
113
+ function SpeakableProvider({
114
+ user,
115
+ children,
116
+ queryClient,
117
+ permissions,
118
+ fsClient
119
+ }) {
120
+ const [speakableApi, setSpeakableApi] = (0, import_react.useState)(null);
121
+ (0, import_react.useEffect)(() => {
122
+ setSpeakableApi(fsClient);
123
+ }, [fsClient]);
124
+ if (!speakableApi) return null;
125
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
126
+ FsCtx.Provider,
127
+ {
128
+ value: {
129
+ speakableApi,
130
+ queryClient,
131
+ user,
132
+ permissions
133
+ },
134
+ children
135
+ }
136
+ );
137
+ }
138
+ function useSpeakableApi() {
139
+ const ctx = (0, import_react.useContext)(FsCtx);
140
+ if (!ctx) throw new Error("useSpeakableApi must be used within a SpeakableProvider");
141
+ return ctx;
142
+ }
39
143
 
40
144
  // src/utils/error-handler.ts
41
145
  var ServiceError = class extends Error {
@@ -345,13 +449,30 @@ var createAssignmentRepo = () => {
345
449
  };
346
450
  };
347
451
 
348
- // src/providers/SpeakableProvider.tsx
349
- var import_react = require("react");
350
- var import_jsx_runtime = require("react/jsx-runtime");
351
- var FsCtx = (0, import_react.createContext)(null);
352
-
353
452
  // src/domains/assignment/hooks/assignment.hooks.ts
354
453
  var import_react_query = require("@tanstack/react-query");
454
+ var assignmentQueryKeys = {
455
+ all: ["assignments"],
456
+ byId: (id) => [...assignmentQueryKeys.all, id],
457
+ list: () => [...assignmentQueryKeys.all, "list"]
458
+ };
459
+ function useAssignment({
460
+ assignmentId,
461
+ enabled = true,
462
+ analyticType,
463
+ userId
464
+ }) {
465
+ const { speakableApi } = useSpeakableApi();
466
+ return (0, import_react_query.useQuery)({
467
+ queryKey: assignmentQueryKeys.byId(assignmentId),
468
+ queryFn: () => speakableApi.assignmentRepo.getAssignment({
469
+ assignmentId,
470
+ analyticType,
471
+ currentUserId: userId
472
+ }),
473
+ enabled
474
+ });
475
+ }
355
476
 
356
477
  // src/domains/assignment/hooks/score-hooks.ts
357
478
  var import_react_query2 = require("@tanstack/react-query");
@@ -374,6 +495,24 @@ function debounce(func, waitFor) {
374
495
  });
375
496
  }
376
497
 
498
+ // src/lib/tanstack/handle-optimistic-update-query.ts
499
+ var handleOptimisticUpdate = async ({
500
+ queryClient,
501
+ queryKey,
502
+ newData
503
+ }) => {
504
+ await queryClient.cancelQueries({
505
+ queryKey
506
+ });
507
+ const previousData = queryClient.getQueryData(queryKey);
508
+ if (previousData === void 0) {
509
+ queryClient.setQueryData(queryKey, newData);
510
+ } else {
511
+ queryClient.setQueryData(queryKey, { ...previousData, ...newData });
512
+ }
513
+ return { previousData };
514
+ };
515
+
377
516
  // src/constants/speakable-plans.ts
378
517
  var FEEDBACK_PLANS = {
379
518
  FEEDBACK_TRANSCRIPT: "FEEDBACK_TRANSCRIPT",
@@ -530,9 +669,64 @@ var SpeakablePlanHierarchy = [
530
669
  SpeakablePlanTypes.organization
531
670
  ];
532
671
 
672
+ // src/hooks/usePermissions.ts
673
+ var usePermissions = () => {
674
+ const { permissions } = useSpeakableApi();
675
+ const has = (permission) => {
676
+ var _a;
677
+ return (_a = permissions.permissions) == null ? void 0 : _a.includes(permission);
678
+ };
679
+ return {
680
+ plan: permissions.plan,
681
+ permissionsLoaded: permissions.loaded,
682
+ isStripePlan: permissions.isStripePlan,
683
+ refreshDate: permissions.refreshDate,
684
+ isInstitutionPlan: permissions.isInstitutionPlan,
685
+ subscriptionId: permissions.subscriptionId,
686
+ contact: permissions.contact,
687
+ hasGradebook: has(ANALYTICS_PLANS.ANALYTICS_GRADEBOOK),
688
+ hasGoogleClassroomGradePassback: has(ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK),
689
+ hasAssessments: has(ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS),
690
+ hasSectionAnalytics: has(ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS),
691
+ hasStudentReports: has(ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS),
692
+ permissions: permissions || [],
693
+ hasStudentPortfolios: permissions.hasStudentPortfolios,
694
+ isFreeOrgTrial: permissions.type === "free_org_trial",
695
+ freeOrgTrialExpired: permissions.freeOrgTrialExpired
696
+ };
697
+ };
698
+ var usePermissions_default = usePermissions;
699
+
700
+ // src/domains/notification/notification.constants.ts
701
+ var SPEAKABLE_NOTIFICATIONS = {
702
+ NEW_ASSIGNMENT: "new_assignment",
703
+ ASSESSMENT_SUBMITTED: "assessment_submitted",
704
+ ASSESSMENT_SCORED: "assessment_scored",
705
+ NEW_COMMENT: "NEW_COMMENT"
706
+ };
707
+ var SpeakableNotificationTypes = {
708
+ NEW_ASSIGNMENT: "NEW_ASSIGNMENT",
709
+ FEEDBACK_FROM_TEACHER: "FEEDBACK_FROM_TEACHER",
710
+ MESSAGE_FROM_STUDENT: "MESSAGE_FROM_STUDENT",
711
+ PHRASE_MARKED_CORRECT: "PHRASE_MARKED_CORRECT",
712
+ STUDENT_PROGRESS: "STUDENT_PROGRESS",
713
+ PLAYLIST_FOLLOWERS: "PLAYLIST_FOLLOWERS",
714
+ PLAYLIST_PLAYS: "PLAYLIST_PLAYS",
715
+ // New notifications
716
+ ASSESSMENT_SUBMITTED: "ASSESSMENT_SUBMITTED",
717
+ // Notification FOR TEACHER when student submits assessment
718
+ ASSESSMENT_SCORED: "ASSESSMENT_SCORED",
719
+ // Notification FOR STUDENT when teacher scores assessment
720
+ // Comment
721
+ NEW_COMMENT: "NEW_COMMENT"
722
+ };
723
+
533
724
  // src/domains/notification/services/create-notification.service.ts
534
725
  var import_dayjs2 = __toESM(require("dayjs"));
535
726
 
727
+ // src/constants/web.constants.ts
728
+ var WEB_BASE_URL = "https://app.speakable.io";
729
+
536
730
  // src/domains/notification/services/send-notification.service.ts
537
731
  var _sendNotification = async (sendTo, notification) => {
538
732
  var _a, _b, _c;
@@ -545,6 +739,202 @@ var _sendNotification = async (sendTo, notification) => {
545
739
  };
546
740
  var sendNotification = withErrorHandler(_sendNotification, "sendNotification");
547
741
 
742
+ // src/domains/notification/services/create-notification.service.ts
743
+ var createNotification = async ({
744
+ data,
745
+ type,
746
+ userId,
747
+ profile
748
+ }) => {
749
+ let result;
750
+ switch (type) {
751
+ case SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT:
752
+ result = await handleAssignNotifPromise({ data, profile });
753
+ break;
754
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED:
755
+ result = await createAssessmentSubmissionNotification({
756
+ data,
757
+ profile,
758
+ userId
759
+ });
760
+ break;
761
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED:
762
+ result = await createAssessmentScoredNotification({
763
+ data,
764
+ profile
765
+ });
766
+ break;
767
+ default:
768
+ result = null;
769
+ break;
770
+ }
771
+ return result;
772
+ };
773
+ var handleAssignNotifPromise = async ({
774
+ data: assignments,
775
+ profile
776
+ }) => {
777
+ if (!assignments.length) return;
778
+ try {
779
+ const notifsPromises = assignments.map(async (assignment) => {
780
+ const {
781
+ section,
782
+ section: { members },
783
+ ...rest
784
+ } = assignment;
785
+ if (!section || !members) throw new Error("Invalid assignment data");
786
+ const data = { section, sendTo: members, assignment: { ...rest } };
787
+ return createNewAssignmentNotification({ data, profile });
788
+ });
789
+ await Promise.all(notifsPromises);
790
+ return {
791
+ success: true,
792
+ message: "Assignment notifications sent successfully"
793
+ };
794
+ } catch (error) {
795
+ console.error("Error in handleAssignNotifPromise:", error);
796
+ throw error;
797
+ }
798
+ };
799
+ var createNewAssignmentNotification = async ({
800
+ data,
801
+ profile
802
+ }) => {
803
+ var _a;
804
+ const { assignment, sendTo } = data;
805
+ const teacherName = profile.displayName || "Your teacher";
806
+ const dueDate = assignment.dueDateTimestamp ? (0, import_dayjs2.default)(assignment.dueDateTimestamp.toDate()).format("MMM Do") : null;
807
+ const results = await sendNotification(sendTo, {
808
+ courseId: assignment.courseId,
809
+ type: SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT,
810
+ senderName: teacherName,
811
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
812
+ messagePreview: `A new assignment "${assignment.name}" is now available. ${dueDate ? `Due ${dueDate}` : ""}`,
813
+ title: "New Assignment Available!",
814
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url
815
+ });
816
+ return results;
817
+ };
818
+ var createAssessmentSubmissionNotification = async ({
819
+ data: assignment,
820
+ profile,
821
+ userId
822
+ }) => {
823
+ var _a;
824
+ const studentName = profile.displayName || "Your student";
825
+ const results = await sendNotification(assignment.owners, {
826
+ courseId: assignment.courseId,
827
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED,
828
+ link: `${WEB_BASE_URL}/a/${assignment.id}?studentId=${userId}`,
829
+ title: `Assessment Submitted!`,
830
+ senderName: studentName,
831
+ messagePreview: `${studentName} has submitted the assessment "${assignment.name}"`,
832
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url
833
+ });
834
+ return results;
835
+ };
836
+ var createAssessmentScoredNotification = async ({
837
+ data,
838
+ profile
839
+ }) => {
840
+ var _a, _b, _c, _d, _e;
841
+ const { assignment, sendTo } = data;
842
+ const teacherName = profile.displayName || "Your teacher";
843
+ const title = `${assignment.isAssessment ? "Assessment" : "Assignment"} Reviewed!`;
844
+ const messagePreview = `Your ${assignment.isAssessment ? "assessment" : "assignment"} has been reviewed by your teacher. Click to view the feedback.`;
845
+ const results = await sendNotification(sendTo, {
846
+ courseId: assignment.courseId,
847
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED,
848
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
849
+ title,
850
+ messagePreview,
851
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url,
852
+ senderName: teacherName
853
+ });
854
+ await ((_e = (_c = (_b = api).httpsCallable) == null ? void 0 : _c.call(_b, "sendAssessmentScoredEmail")) == null ? void 0 : _e({
855
+ assessmentTitle: assignment.name,
856
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
857
+ senderImage: ((_d = profile.image) == null ? void 0 : _d.url) || "",
858
+ studentId: sendTo[0],
859
+ teacherName: profile.displayName
860
+ }));
861
+ return results;
862
+ };
863
+
864
+ // src/domains/notification/hooks/notification.hooks.ts
865
+ var notificationQueryKeys = {
866
+ all: ["notifications"],
867
+ byId: (id) => [...notificationQueryKeys.all, id]
868
+ };
869
+ var useCreateNotification = () => {
870
+ const { user, queryClient } = useSpeakableApi();
871
+ const handleCreateNotifications = async (type, data) => {
872
+ var _a, _b;
873
+ const result = await createNotification({
874
+ type,
875
+ userId: user.auth.uid,
876
+ profile: (_a = user == null ? void 0 : user.profile) != null ? _a : {},
877
+ data
878
+ });
879
+ queryClient.invalidateQueries({
880
+ queryKey: notificationQueryKeys.byId((_b = user == null ? void 0 : user.auth.uid) != null ? _b : "")
881
+ });
882
+ return result;
883
+ };
884
+ return {
885
+ createNotification: handleCreateNotifications
886
+ };
887
+ };
888
+
889
+ // src/hooks/useGoogleClassroom.ts
890
+ var useGoogleClassroom = () => {
891
+ const submitAssignmentToGoogleClassroom = async ({
892
+ assignment,
893
+ scores,
894
+ googleUserId = null
895
+ // optional to override the user's googleUserId
896
+ }) => {
897
+ var _a, _b, _c;
898
+ try {
899
+ const { googleClassroomUserId = null } = scores;
900
+ const googleId = googleUserId || googleClassroomUserId;
901
+ if (!googleId)
902
+ return {
903
+ error: true,
904
+ message: "No Google Classroom ID found"
905
+ };
906
+ const { courseWorkId, maxPoints, owners, courseId } = assignment;
907
+ const draftGrade = (scores == null ? void 0 : scores.score) ? (scores == null ? void 0 : scores.score) / 100 * maxPoints : 0;
908
+ const result = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssignmentToGoogleClassroomV2")) == null ? void 0 : _c({
909
+ teacherId: owners[0],
910
+ courseId,
911
+ courseWorkId,
912
+ userId: googleId,
913
+ draftGrade
914
+ }));
915
+ return result;
916
+ } catch (error) {
917
+ return { error: true, message: error.message };
918
+ }
919
+ };
920
+ return {
921
+ submitAssignmentToGoogleClassroom
922
+ };
923
+ };
924
+
925
+ // src/lib/firebase/firebase-analytics/grading-standard.ts
926
+ var logGradingStandardLog = (data) => {
927
+ var _a, _b, _c;
928
+ if (data.courseId && data.type && data.level) {
929
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
930
+ eventType: data.type || "custom",
931
+ level: data.level,
932
+ courseId: data.courseId
933
+ });
934
+ }
935
+ api.logEvent("logGradingStandard", data);
936
+ };
937
+
548
938
  // src/constants/analytics.constants.ts
549
939
  var ANALYTICS_EVENT_TYPES = {
550
940
  VOICE_SUCCESS: "voice_success",
@@ -584,6 +974,20 @@ var ANALYTICS_EVENT_TYPES = {
584
974
  };
585
975
 
586
976
  // src/lib/firebase/firebase-analytics/assignment.ts
977
+ var logOpenAssignment = (data = {}) => {
978
+ api.logEvent("open_assignment", data);
979
+ };
980
+ var logOpenActivityPreview = (data = {}) => {
981
+ api.logEvent("open_activity_preview", data);
982
+ };
983
+ var logSubmitAssignment = (data = {}) => {
984
+ var _a, _b, _c;
985
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
986
+ eventType: ANALYTICS_EVENT_TYPES.SUBMISSION,
987
+ ...data
988
+ });
989
+ api.logEvent(ANALYTICS_EVENT_TYPES.SUBMISSION, data);
990
+ };
587
991
  var logStartAssignment = (data = {}) => {
588
992
  var _a, _b, _c;
589
993
  if (data.courseId) {
@@ -826,6 +1230,72 @@ var updateCardScore = withErrorHandler(_updateCardScore, "updateCardScore");
826
1230
 
827
1231
  // src/domains/assignment/services/clear-score.service.ts
828
1232
  var import_dayjs3 = __toESM(require("dayjs"));
1233
+ async function clearScore(params) {
1234
+ var _a, _b, _c, _d, _e;
1235
+ const update = {
1236
+ [`cards.${params.cardId}`]: {
1237
+ attempts: ((_a = params.cardScores.attempts) != null ? _a : 1) + 1,
1238
+ correct: (_b = params.cardScores.correct) != null ? _b : 0,
1239
+ // save old score history
1240
+ history: [
1241
+ {
1242
+ ...params.cardScores,
1243
+ attempts: (_c = params.cardScores.attempts) != null ? _c : 1,
1244
+ correct: (_d = params.cardScores.correct) != null ? _d : 0,
1245
+ retryTime: (0, import_dayjs3.default)().format("YYYY-MM-DD HH:mm:ss"),
1246
+ history: null
1247
+ },
1248
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1249
+ ...(_e = params.cardScores.history) != null ? _e : []
1250
+ ]
1251
+ }
1252
+ };
1253
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1254
+ id: params.activityId,
1255
+ userId: params.userId
1256
+ }) : refsScoresPractice.practiceScores({
1257
+ setId: params.activityId,
1258
+ userId: params.userId
1259
+ });
1260
+ await api.updateDoc(path, update);
1261
+ return {
1262
+ update,
1263
+ activityId: params.activityId
1264
+ };
1265
+ }
1266
+ async function clearScoreV2(params) {
1267
+ var _a, _b, _c, _d, _e;
1268
+ const update = {
1269
+ [`cards.${params.cardId}`]: {
1270
+ ...params.cardScores,
1271
+ attempts: ((_a = params.cardScores.attempts) != null ? _a : 1) + 1,
1272
+ correct: (_b = params.cardScores.correct) != null ? _b : 0,
1273
+ history: [
1274
+ {
1275
+ ...params.cardScores,
1276
+ attempts: (_c = params.cardScores.attempts) != null ? _c : 1,
1277
+ correct: (_d = params.cardScores.correct) != null ? _d : 0,
1278
+ retryTime: (0, import_dayjs3.default)().format("YYYY-MM-DD HH:mm:ss"),
1279
+ history: null
1280
+ },
1281
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1282
+ ...(_e = params.cardScores.history) != null ? _e : []
1283
+ ]
1284
+ }
1285
+ };
1286
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1287
+ id: params.activityId,
1288
+ userId: params.userId
1289
+ }) : refsScoresPractice.practiceScores({
1290
+ setId: params.activityId,
1291
+ userId: params.userId
1292
+ });
1293
+ await api.updateDoc(path, update);
1294
+ return {
1295
+ update,
1296
+ activityId: params.activityId
1297
+ };
1298
+ }
829
1299
 
830
1300
  // src/domains/assignment/services/submit-assignment-score.service.ts
831
1301
  var import_dayjs4 = __toESM(require("dayjs"));
@@ -891,15 +1361,458 @@ async function handleCourseAssignment(assignment, userId) {
891
1361
  userId
892
1362
  }));
893
1363
  }
1364
+ async function submitPracticeScore({
1365
+ setId,
1366
+ userId,
1367
+ scores
1368
+ }) {
1369
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
1370
+ const date = (0, import_dayjs4.default)().format("YYYY-MM-DD-HH-mm");
1371
+ const ref = refsScoresPractice.practiceScoreHistoryRefDoc({ setId, userId, date });
1372
+ const fieldsUpdated = {
1373
+ ...scores,
1374
+ submitted: true,
1375
+ progress: 100,
1376
+ submissionDate: serverTimestamp2(),
1377
+ status: "SUBMITTED"
1378
+ };
1379
+ await api.setDoc(ref, { ...fieldsUpdated });
1380
+ const refScores = refsScoresPractice.practiceScores({ userId, setId });
1381
+ await api.deleteDoc(refScores);
1382
+ return { success: true, fieldsUpdated };
1383
+ }
894
1384
 
895
1385
  // src/domains/assignment/hooks/score-hooks.ts
1386
+ var scoreQueryKeys = {
1387
+ all: ["scores"],
1388
+ byId: (id) => [...scoreQueryKeys.all, id],
1389
+ list: () => [...scoreQueryKeys.all, "list"]
1390
+ };
1391
+ function useScore({
1392
+ isAssignment,
1393
+ activityId,
1394
+ userId = "",
1395
+ courseId,
1396
+ enabled = true,
1397
+ googleClassroomUserId
1398
+ }) {
1399
+ return (0, import_react_query2.useQuery)({
1400
+ queryFn: () => getScore({
1401
+ userId,
1402
+ courseId,
1403
+ activityId,
1404
+ googleClassroomUserId,
1405
+ isAssignment
1406
+ }),
1407
+ queryKey: scoreQueryKeys.byId(activityId),
1408
+ enabled
1409
+ });
1410
+ }
896
1411
  var debounceUpdateScore = debounce(updateScore, 500);
1412
+ function useUpdateScore() {
1413
+ const { queryClient } = useSpeakableApi();
1414
+ const mutation = (0, import_react_query2.useMutation)({
1415
+ mutationFn: debounceUpdateScore,
1416
+ onMutate: (variables) => {
1417
+ return handleOptimisticUpdate({
1418
+ queryClient,
1419
+ queryKey: scoreQueryKeys.byId(variables.activityId),
1420
+ newData: variables.data
1421
+ });
1422
+ },
1423
+ onError: (_, variables, context) => {
1424
+ if (context == null ? void 0 : context.previousData)
1425
+ queryClient.setQueryData(scoreQueryKeys.byId(variables.activityId), context.previousData);
1426
+ },
1427
+ onSettled: (_, err, variables) => {
1428
+ queryClient.invalidateQueries({
1429
+ queryKey: scoreQueryKeys.byId(variables.activityId)
1430
+ });
1431
+ }
1432
+ });
1433
+ return {
1434
+ mutationUpdateScore: mutation
1435
+ };
1436
+ }
1437
+ function useUpdateCardScore({
1438
+ isAssignment,
1439
+ activityId,
1440
+ userId,
1441
+ cardIds,
1442
+ weights
1443
+ }) {
1444
+ const { queryClient } = useSpeakableApi();
1445
+ const queryKey = scoreQueryKeys.byId(activityId);
1446
+ const mutation = (0, import_react_query2.useMutation)({
1447
+ mutationFn: async ({ cardId, cardScore }) => {
1448
+ const previousScores = queryClient.getQueryData(queryKey);
1449
+ const { progress, score, newScoreUpdated, updatedCardScore } = getScoreUpdated({
1450
+ previousScores: previousScores != null ? previousScores : {},
1451
+ cardId,
1452
+ cardScore,
1453
+ cardIds,
1454
+ weights
1455
+ });
1456
+ await updateCardScore({
1457
+ userId,
1458
+ cardId,
1459
+ isAssignment,
1460
+ activityId,
1461
+ updates: {
1462
+ cardScore: updatedCardScore,
1463
+ progress,
1464
+ score
1465
+ }
1466
+ });
1467
+ return { cardId, scoresUpdated: newScoreUpdated };
1468
+ },
1469
+ onMutate: ({ cardId, cardScore }) => {
1470
+ const previousData = queryClient.getQueryData(queryKey);
1471
+ queryClient.setQueryData(queryKey, (previousScore) => {
1472
+ const updates = handleOptimisticScore({
1473
+ score: previousScore,
1474
+ cardId,
1475
+ cardScore,
1476
+ cardIds,
1477
+ weights
1478
+ });
1479
+ return {
1480
+ ...previousScore,
1481
+ ...updates
1482
+ };
1483
+ });
1484
+ return { previousData };
1485
+ },
1486
+ onError: (error, variables, context) => {
1487
+ console.log("Error updating card score:", error.message);
1488
+ if (context == null ? void 0 : context.previousData) {
1489
+ queryClient.setQueryData(queryKey, context.previousData);
1490
+ }
1491
+ },
1492
+ onSettled: () => {
1493
+ queryClient.invalidateQueries({
1494
+ queryKey
1495
+ });
1496
+ }
1497
+ });
1498
+ return {
1499
+ mutationUpdateCardScore: mutation
1500
+ };
1501
+ }
1502
+ var getScoreUpdated = ({
1503
+ cardId,
1504
+ cardScore,
1505
+ previousScores,
1506
+ cardIds,
1507
+ weights
1508
+ }) => {
1509
+ var _a, _b;
1510
+ const previousCard = (_a = previousScores.cards) == null ? void 0 : _a[cardId];
1511
+ const newCardScore = {
1512
+ ...previousCard != null ? previousCard : {},
1513
+ ...cardScore
1514
+ };
1515
+ const newScores = {
1516
+ ...previousScores,
1517
+ cards: {
1518
+ ...(_b = previousScores.cards) != null ? _b : {},
1519
+ [cardId]: newCardScore
1520
+ }
1521
+ };
1522
+ const { score, progress } = calculateScoreAndProgress_default(newScores, cardIds, weights);
1523
+ return {
1524
+ newScoreUpdated: newScores,
1525
+ updatedCardScore: cardScore,
1526
+ score,
1527
+ progress
1528
+ };
1529
+ };
1530
+ var handleOptimisticScore = ({
1531
+ score,
1532
+ cardId,
1533
+ cardScore,
1534
+ cardIds,
1535
+ weights
1536
+ }) => {
1537
+ var _a;
1538
+ let cards = { ...(_a = score == null ? void 0 : score.cards) != null ? _a : {} };
1539
+ cards = {
1540
+ ...cards,
1541
+ [cardId]: {
1542
+ ...cards[cardId],
1543
+ ...cardScore
1544
+ }
1545
+ };
1546
+ const { score: scoreValue, progress } = calculateScoreAndProgress_default(
1547
+ // @ts-ignore
1548
+ {
1549
+ ...score != null ? score : {},
1550
+ cards
1551
+ },
1552
+ cardIds,
1553
+ weights
1554
+ );
1555
+ return { cards, score: scoreValue, progress };
1556
+ };
1557
+ function useClearScore() {
1558
+ const { queryClient } = useSpeakableApi();
1559
+ const mutation = (0, import_react_query2.useMutation)({
1560
+ mutationFn: clearScore,
1561
+ onError: (error) => {
1562
+ console.log("Error clearing score:", error.message);
1563
+ },
1564
+ onSettled: (result) => {
1565
+ var _a;
1566
+ queryClient.invalidateQueries({
1567
+ queryKey: scoreQueryKeys.byId((_a = result == null ? void 0 : result.activityId) != null ? _a : "")
1568
+ });
1569
+ }
1570
+ });
1571
+ return {
1572
+ mutationClearScore: mutation
1573
+ };
1574
+ }
1575
+ function useClearScoreV2() {
1576
+ const { queryClient } = useSpeakableApi();
1577
+ const mutation = (0, import_react_query2.useMutation)({
1578
+ mutationFn: clearScoreV2,
1579
+ onError: (error) => {
1580
+ console.log("Error clearing score V2:", error.message);
1581
+ },
1582
+ onSettled: (result) => {
1583
+ var _a;
1584
+ queryClient.invalidateQueries({
1585
+ queryKey: scoreQueryKeys.byId((_a = result == null ? void 0 : result.activityId) != null ? _a : "")
1586
+ });
1587
+ }
1588
+ });
1589
+ return {
1590
+ mutationClearScore: mutation
1591
+ };
1592
+ }
1593
+ function useSubmitAssignmentScore({
1594
+ onAssignmentSubmitted,
1595
+ studentName
1596
+ }) {
1597
+ const { queryClient } = useSpeakableApi();
1598
+ const { hasGoogleClassroomGradePassback } = usePermissions_default();
1599
+ const { submitAssignmentToGoogleClassroom } = useGoogleClassroom();
1600
+ const { createNotification: createNotification2 } = useCreateNotification();
1601
+ const mutation = (0, import_react_query2.useMutation)({
1602
+ mutationFn: async ({
1603
+ assignment,
1604
+ userId,
1605
+ cardIds,
1606
+ weights,
1607
+ scores,
1608
+ status
1609
+ }) => {
1610
+ try {
1611
+ const scoreUpdated = await submitAssignmentScore({
1612
+ assignment,
1613
+ userId,
1614
+ cardIds,
1615
+ weights,
1616
+ status,
1617
+ studentName
1618
+ });
1619
+ if (assignment.courseWorkId != null && !assignment.isAssessment && hasGoogleClassroomGradePassback) {
1620
+ await submitAssignmentToGoogleClassroom({
1621
+ assignment,
1622
+ scores
1623
+ });
1624
+ }
1625
+ if (assignment.isAssessment) {
1626
+ createNotification2(SpeakableNotificationTypes.ASSESSMENT_SUBMITTED, assignment);
1627
+ }
1628
+ if (assignment == null ? void 0 : assignment.id) {
1629
+ logSubmitAssignment({
1630
+ courseId: assignment == null ? void 0 : assignment.courseId
1631
+ });
1632
+ }
1633
+ onAssignmentSubmitted(assignment.id);
1634
+ queryClient.setQueryData(scoreQueryKeys.byId(assignment.id), {
1635
+ ...scores,
1636
+ ...scoreUpdated.fieldsUpdated
1637
+ });
1638
+ return {
1639
+ success: true,
1640
+ message: "Score submitted successfully"
1641
+ };
1642
+ } catch (error) {
1643
+ return {
1644
+ success: false,
1645
+ error
1646
+ };
1647
+ }
1648
+ }
1649
+ });
1650
+ return {
1651
+ submitAssignmentScore: mutation.mutateAsync,
1652
+ isLoading: mutation.isPending
1653
+ };
1654
+ }
1655
+ function useSubmitPracticeScore() {
1656
+ const { queryClient } = useSpeakableApi();
1657
+ const mutation = (0, import_react_query2.useMutation)({
1658
+ mutationFn: async ({
1659
+ setId,
1660
+ userId,
1661
+ scores
1662
+ }) => {
1663
+ try {
1664
+ await submitPracticeScore({
1665
+ setId,
1666
+ userId,
1667
+ scores
1668
+ });
1669
+ queryClient.invalidateQueries({
1670
+ queryKey: scoreQueryKeys.byId(setId)
1671
+ });
1672
+ return {
1673
+ success: true,
1674
+ message: "Score submitted successfully"
1675
+ };
1676
+ } catch (error) {
1677
+ return {
1678
+ success: false,
1679
+ error
1680
+ };
1681
+ }
1682
+ }
1683
+ });
1684
+ return {
1685
+ submitPracticeScore: mutation.mutateAsync,
1686
+ isLoading: mutation.isPending
1687
+ };
1688
+ }
897
1689
 
898
1690
  // src/domains/cards/card.hooks.ts
899
1691
  var import_react_query3 = require("@tanstack/react-query");
900
1692
  var import_react2 = require("react");
901
1693
 
1694
+ // src/domains/cards/card.model.ts
1695
+ var ActivityPageType = /* @__PURE__ */ ((ActivityPageType2) => {
1696
+ ActivityPageType2["READ_REPEAT"] = "READ_REPEAT";
1697
+ ActivityPageType2["READ_RESPOND"] = "READ_RESPOND";
1698
+ ActivityPageType2["FREE_RESPONSE"] = "FREE_RESPONSE";
1699
+ ActivityPageType2["REPEAT"] = "REPEAT";
1700
+ ActivityPageType2["RESPOND"] = "RESPOND";
1701
+ ActivityPageType2["RESPOND_WRITE"] = "RESPOND_WRITE";
1702
+ ActivityPageType2["MULTIPLE_CHOICE"] = "MULTIPLE_CHOICE";
1703
+ ActivityPageType2["MEDIA_PAGE"] = "MEDIA_PAGE";
1704
+ ActivityPageType2["SHORT_ANSWER"] = "SHORT_ANSWER";
1705
+ return ActivityPageType2;
1706
+ })(ActivityPageType || {});
1707
+ var RESPOND_PAGE_ACTIVITY_TYPES = [
1708
+ "READ_RESPOND" /* READ_RESPOND */,
1709
+ "RESPOND" /* RESPOND */,
1710
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
1711
+ "FREE_RESPONSE" /* FREE_RESPONSE */
1712
+ ];
1713
+ var MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES = ["MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */];
1714
+ var REPEAT_PAGE_ACTIVITY_TYPES = ["READ_REPEAT" /* READ_REPEAT */, "REPEAT" /* REPEAT */];
1715
+ var RESPOND_WRITE_PAGE_ACTIVITY_TYPES = [
1716
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
1717
+ "FREE_RESPONSE" /* FREE_RESPONSE */
1718
+ ];
1719
+ var RESPOND_AUDIO_PAGE_ACTIVITY_TYPES = [
1720
+ "RESPOND" /* RESPOND */,
1721
+ "READ_RESPOND" /* READ_RESPOND */
1722
+ ];
1723
+
902
1724
  // src/domains/cards/card.constants.ts
1725
+ var FeedbackTypesCard = /* @__PURE__ */ ((FeedbackTypesCard2) => {
1726
+ FeedbackTypesCard2["SuggestedResponse"] = "suggested_response";
1727
+ FeedbackTypesCard2["Wida"] = "wida";
1728
+ FeedbackTypesCard2["GrammarInsights"] = "grammar_insights";
1729
+ FeedbackTypesCard2["Actfl"] = "actfl";
1730
+ FeedbackTypesCard2["ProficiencyLevel"] = "proficiency_level";
1731
+ return FeedbackTypesCard2;
1732
+ })(FeedbackTypesCard || {});
1733
+ var LeniencyCard = /* @__PURE__ */ ((LeniencyCard2) => {
1734
+ LeniencyCard2["CONFIDENCE"] = "confidence";
1735
+ LeniencyCard2["EASY"] = "easy";
1736
+ LeniencyCard2["NORMAL"] = "normal";
1737
+ LeniencyCard2["HARD"] = "hard";
1738
+ return LeniencyCard2;
1739
+ })(LeniencyCard || {});
1740
+ var LENIENCY_OPTIONS = [
1741
+ {
1742
+ label: "Build Confidence - most lenient",
1743
+ value: "confidence" /* CONFIDENCE */
1744
+ },
1745
+ {
1746
+ label: "Very Lenient",
1747
+ value: "easy" /* EASY */
1748
+ },
1749
+ {
1750
+ label: "Normal",
1751
+ value: "normal" /* NORMAL */
1752
+ },
1753
+ {
1754
+ label: "No leniency - most strict",
1755
+ value: "hard" /* HARD */
1756
+ }
1757
+ ];
1758
+ var STUDENT_LEVELS_OPTIONS = [
1759
+ {
1760
+ label: "Beginner",
1761
+ description: "Beginner Level: Just starting out. Can say a few basic words and phrases.",
1762
+ value: "beginner"
1763
+ },
1764
+ {
1765
+ label: "Elementary",
1766
+ description: "Elementary Level: Can understand simple sentences and have very basic conversations.",
1767
+ value: "elementary"
1768
+ },
1769
+ {
1770
+ label: "Intermediate",
1771
+ description: "Intermediate Level: Can talk about everyday topics and handle common situations.",
1772
+ value: "intermediate"
1773
+ },
1774
+ {
1775
+ label: "Advanced",
1776
+ description: "Advanced Level: Can speak and understand with ease, and explain ideas clearly.",
1777
+ value: "advanced"
1778
+ },
1779
+ {
1780
+ label: "Fluent",
1781
+ description: "Fluent Level: Speaks naturally and easily. Can use the language in work or school settings.",
1782
+ value: "fluent"
1783
+ },
1784
+ {
1785
+ label: "Native-like",
1786
+ description: "Native-like Level: Understands and speaks like a native. Can discuss complex ideas accurately.",
1787
+ value: "nativeLike"
1788
+ }
1789
+ ];
1790
+ var BASE_RESPOND_FIELD_VALUES = {
1791
+ title: "",
1792
+ allowRetries: true,
1793
+ respondTime: 180,
1794
+ maxCharacters: 1e3
1795
+ };
1796
+ var BASE_REPEAT_FIELD_VALUES = {
1797
+ repeat: 1
1798
+ };
1799
+ var BASE_MULTIPLE_CHOICE_FIELD_VALUES = {
1800
+ MCQType: "single",
1801
+ answer: ["A"],
1802
+ choices: [
1803
+ { option: "A", value: "Option A" },
1804
+ { option: "B", value: "Option B" },
1805
+ { option: "C", value: "Option C" }
1806
+ ]
1807
+ };
1808
+ var VerificationCardStatus = /* @__PURE__ */ ((VerificationCardStatus2) => {
1809
+ VerificationCardStatus2["VERIFIED"] = "VERIFIED";
1810
+ VerificationCardStatus2["WARNING"] = "WARNING";
1811
+ VerificationCardStatus2["NOT_RECOMMENDED"] = "NOT_RECOMMENDED";
1812
+ VerificationCardStatus2["NOT_WORKING"] = "NOT_WORKING";
1813
+ VerificationCardStatus2["NOT_CHECKED"] = "NOT_CHECKED";
1814
+ return VerificationCardStatus2;
1815
+ })(VerificationCardStatus || {});
903
1816
  var CARDS_COLLECTION = "flashcards";
904
1817
  var refsCardsFiresotre = {
905
1818
  allCards: CARDS_COLLECTION,
@@ -946,6 +1859,13 @@ var getWordHash = (word, language) => {
946
1859
  console.log("wordHash core library", wordHash);
947
1860
  return wordHash;
948
1861
  };
1862
+ function getPhraseLength(phrase, input) {
1863
+ if (Array.isArray(phrase) && phrase.includes(input)) {
1864
+ return phrase[phrase.indexOf(input)].split(" ").length;
1865
+ } else {
1866
+ return phrase ? phrase.split(" ").length : 0;
1867
+ }
1868
+ }
949
1869
 
950
1870
  // src/domains/cards/services/get-card-verification-status.service.ts
951
1871
  var charactarLanguages = ["zh", "ja", "ko"];
@@ -1016,6 +1936,81 @@ async function _createCards({ cards }) {
1016
1936
  }
1017
1937
  var createCards = withErrorHandler(_createCards, "createCards");
1018
1938
 
1939
+ // src/domains/cards/card.hooks.ts
1940
+ var cardsQueryKeys = {
1941
+ all: ["cards"],
1942
+ one: (params) => [...cardsQueryKeys.all, params.cardId]
1943
+ };
1944
+ function useCards({
1945
+ cardIds,
1946
+ enabled = true,
1947
+ asObject
1948
+ }) {
1949
+ const queries = (0, import_react_query3.useQueries)({
1950
+ queries: cardIds.map((cardId) => ({
1951
+ enabled: enabled && cardIds.length > 0,
1952
+ queryKey: cardsQueryKeys.one({
1953
+ cardId
1954
+ }),
1955
+ queryFn: () => getCard({ cardId })
1956
+ }))
1957
+ });
1958
+ const cards = queries.map((query2) => query2.data).filter(Boolean);
1959
+ const cardsObject = (0, import_react2.useMemo)(() => {
1960
+ if (!asObject) return null;
1961
+ return cards.reduce((acc, card) => {
1962
+ acc[card.id] = card;
1963
+ return acc;
1964
+ }, {});
1965
+ }, [asObject, cards]);
1966
+ return {
1967
+ cards,
1968
+ cardsObject,
1969
+ cardsQueries: queries
1970
+ };
1971
+ }
1972
+ function useCreateCard() {
1973
+ const { queryClient } = useSpeakableApi();
1974
+ const mutationCreateCard = (0, import_react_query3.useMutation)({
1975
+ mutationFn: createCard,
1976
+ onSuccess: (cardCreated) => {
1977
+ queryClient.invalidateQueries({ queryKey: cardsQueryKeys.one({ cardId: cardCreated.id }) });
1978
+ }
1979
+ });
1980
+ return {
1981
+ mutationCreateCard
1982
+ };
1983
+ }
1984
+ function useCreateCards() {
1985
+ const mutationCreateCards = (0, import_react_query3.useMutation)({
1986
+ mutationFn: createCards
1987
+ });
1988
+ return {
1989
+ mutationCreateCards
1990
+ };
1991
+ }
1992
+ function getCardFromCache({
1993
+ cardId,
1994
+ queryClient
1995
+ }) {
1996
+ return queryClient.getQueryData(cardsQueryKeys.one({ cardId }));
1997
+ }
1998
+ function updateCardInCache({
1999
+ cardId,
2000
+ card,
2001
+ queryClient
2002
+ }) {
2003
+ queryClient.setQueryData(cardsQueryKeys.one({ cardId }), card);
2004
+ }
2005
+ function useGetCard({ cardId, enabled = true }) {
2006
+ const query2 = (0, import_react_query3.useQuery)({
2007
+ queryKey: cardsQueryKeys.one({ cardId }),
2008
+ queryFn: () => getCard({ cardId }),
2009
+ enabled: enabled && !!cardId
2010
+ });
2011
+ return query2;
2012
+ }
2013
+
1019
2014
  // src/domains/cards/card.repo.ts
1020
2015
  var createCardRepo = () => {
1021
2016
  return {
@@ -1025,10 +2020,181 @@ var createCardRepo = () => {
1025
2020
  };
1026
2021
  };
1027
2022
 
1028
- // src/domains/sets/set.hooks.ts
1029
- var import_react_query4 = require("@tanstack/react-query");
1030
-
1031
- // src/domains/sets/set.constants.ts
2023
+ // src/domains/cards/utils/check-page-type.ts
2024
+ function checkIsRepeatPage(cardType) {
2025
+ if (cardType === void 0) return false;
2026
+ return REPEAT_PAGE_ACTIVITY_TYPES.includes(cardType);
2027
+ }
2028
+ function checkIsMCPage(cardType) {
2029
+ if (cardType === void 0) return false;
2030
+ return MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES.includes(cardType);
2031
+ }
2032
+ function checkIsRespondPage(cardType) {
2033
+ if (cardType === void 0) return false;
2034
+ return RESPOND_PAGE_ACTIVITY_TYPES.includes(cardType);
2035
+ }
2036
+ function checkIsRespondWrittenPage(cardType) {
2037
+ if (cardType === void 0) return false;
2038
+ return RESPOND_WRITE_PAGE_ACTIVITY_TYPES.includes(cardType);
2039
+ }
2040
+ function checkIsRespondAudioPage(cardType) {
2041
+ if (cardType === void 0) return false;
2042
+ return RESPOND_AUDIO_PAGE_ACTIVITY_TYPES.includes(cardType);
2043
+ }
2044
+ var checkIsMediaPage = (cardType) => {
2045
+ if (cardType === void 0) return false;
2046
+ return cardType === "MEDIA_PAGE" /* MEDIA_PAGE */;
2047
+ };
2048
+ var checkIsShortAnswerPage = (cardType) => {
2049
+ if (cardType === void 0) return false;
2050
+ return cardType === "SHORT_ANSWER" /* SHORT_ANSWER */;
2051
+ };
2052
+ var checkTypePageActivity = (cardType) => {
2053
+ const isRespondAudio = checkIsRespondAudioPage(cardType);
2054
+ const isRespondWritten = checkIsRespondWrittenPage(cardType);
2055
+ const isRespond = checkIsRespondPage(cardType);
2056
+ const isMC = checkIsMCPage(cardType);
2057
+ const isRepeat = checkIsRepeatPage(cardType);
2058
+ const isMediaPage = checkIsMediaPage(cardType);
2059
+ const isShortAnswer = checkIsShortAnswerPage(cardType);
2060
+ const isNoOneOfThem = !isRespond && !isMC && !isRepeat && !isMediaPage && !isShortAnswer;
2061
+ if (isNoOneOfThem) {
2062
+ return {
2063
+ isRespondAudio: false,
2064
+ isRespondWritten: false,
2065
+ isRespond: false,
2066
+ isMC: false,
2067
+ isRepeat: true,
2068
+ isMediaPage: false,
2069
+ isShortAnswer: false,
2070
+ hasSomeType: false
2071
+ };
2072
+ }
2073
+ return {
2074
+ isRespondAudio,
2075
+ isRespondWritten,
2076
+ isRespond,
2077
+ isMC,
2078
+ isRepeat,
2079
+ isMediaPage,
2080
+ isShortAnswer,
2081
+ hasSomeType: true
2082
+ };
2083
+ };
2084
+
2085
+ // src/domains/cards/utils/get-page-prompt.ts
2086
+ function extractTextFromRichText(richText) {
2087
+ if (!richText) return "";
2088
+ return richText.replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
2089
+ }
2090
+ function getPagePrompt(card) {
2091
+ if (!card) return { has: false, text: "", rich_text: "", isTextEqualToRichText: false };
2092
+ const { isMC, isRepeat, isRespond, isShortAnswer } = checkTypePageActivity(card == null ? void 0 : card.type);
2093
+ const hidePrompt = (card == null ? void 0 : card.hidePrompt) === true;
2094
+ const createReturnObject = (text, richText) => {
2095
+ const plainText = text || "";
2096
+ const richTextPlain = extractTextFromRichText(richText);
2097
+ return {
2098
+ has: true,
2099
+ text: plainText,
2100
+ rich_text: richText || "",
2101
+ isTextEqualToRichText: plainText.trim() === richTextPlain.trim()
2102
+ };
2103
+ };
2104
+ if (isRepeat) {
2105
+ return createReturnObject(card == null ? void 0 : card.target_text, card == null ? void 0 : card.rich_text);
2106
+ }
2107
+ if (isRespond && !hidePrompt) {
2108
+ return createReturnObject(card == null ? void 0 : card.prompt, card == null ? void 0 : card.rich_text);
2109
+ }
2110
+ if (isMC) {
2111
+ return createReturnObject(card == null ? void 0 : card.question, card == null ? void 0 : card.rich_text);
2112
+ }
2113
+ if (isShortAnswer && !hidePrompt) {
2114
+ return createReturnObject(card == null ? void 0 : card.prompt, card == null ? void 0 : card.rich_text);
2115
+ }
2116
+ return {
2117
+ has: false,
2118
+ text: "",
2119
+ rich_text: "",
2120
+ isTextEqualToRichText: false
2121
+ };
2122
+ }
2123
+
2124
+ // src/domains/cards/utils/get-completed-pages.ts
2125
+ var getTotalCompletedCards = (pageScores) => {
2126
+ return Object.values(pageScores != null ? pageScores : {}).reduce((acc, cardScore) => {
2127
+ var _a, _b;
2128
+ if (((_b = (_a = cardScore.completed) != null ? _a : cardScore.score) != null ? _b : cardScore.score === 0) && !cardScore.media_area_opened) {
2129
+ acc++;
2130
+ }
2131
+ return acc;
2132
+ }, 0);
2133
+ };
2134
+
2135
+ // src/domains/cards/utils/get-label-page.ts
2136
+ var labels = {
2137
+ repeat: {
2138
+ short: "Repeat",
2139
+ long: "Listen & Repeat"
2140
+ },
2141
+ mc: {
2142
+ short: "Multiple Choice",
2143
+ long: "Multiple Choice"
2144
+ },
2145
+ mediaPage: {
2146
+ short: "Media Page",
2147
+ long: "Media Page"
2148
+ },
2149
+ shortAnswer: {
2150
+ short: "Short Answer",
2151
+ long: "Short Answer"
2152
+ },
2153
+ respondWritten: {
2154
+ short: "Open Response",
2155
+ long: "Written Open Response"
2156
+ },
2157
+ respondAudio: {
2158
+ short: "Open Response",
2159
+ long: "Spoken Open Response"
2160
+ }
2161
+ };
2162
+ var getLabelPage = (pageType) => {
2163
+ if (!pageType) {
2164
+ return {
2165
+ short: "",
2166
+ long: ""
2167
+ };
2168
+ }
2169
+ const { isRepeat, isMC, isMediaPage, isShortAnswer, isRespondWritten, isRespondAudio } = checkTypePageActivity(pageType);
2170
+ if (isRepeat) {
2171
+ return labels.repeat;
2172
+ }
2173
+ if (isMC) {
2174
+ return labels.mc;
2175
+ }
2176
+ if (isMediaPage) {
2177
+ return labels.mediaPage;
2178
+ }
2179
+ if (isShortAnswer) {
2180
+ return labels.shortAnswer;
2181
+ }
2182
+ if (isRespondWritten) {
2183
+ return labels.respondWritten;
2184
+ }
2185
+ if (isRespondAudio) {
2186
+ return labels.respondAudio;
2187
+ }
2188
+ return {
2189
+ short: "",
2190
+ long: ""
2191
+ };
2192
+ };
2193
+
2194
+ // src/domains/sets/set.hooks.ts
2195
+ var import_react_query4 = require("@tanstack/react-query");
2196
+
2197
+ // src/domains/sets/set.constants.ts
1032
2198
  var SETS_COLLECTION = "sets";
1033
2199
  var refsSetsFirestore = {
1034
2200
  allSets: SETS_COLLECTION,
@@ -1042,6 +2208,1243 @@ async function _getSet({ setId }) {
1042
2208
  }
1043
2209
  var getSet = withErrorHandler(_getSet, "getSet");
1044
2210
 
2211
+ // src/domains/sets/set.hooks.ts
2212
+ var setsQueryKeys = {
2213
+ all: ["sets"],
2214
+ one: (params) => [...setsQueryKeys.all, params.setId]
2215
+ };
2216
+ var useSet = ({ setId, enabled }) => {
2217
+ return (0, import_react_query4.useQuery)({
2218
+ queryKey: setsQueryKeys.one({ setId }),
2219
+ queryFn: () => getSet({ setId }),
2220
+ enabled: setId !== void 0 && setId !== "" && enabled
2221
+ });
2222
+ };
2223
+ function getSetFromCache({
2224
+ setId,
2225
+ queryClient
2226
+ }) {
2227
+ if (!setId) return null;
2228
+ return queryClient.getQueryData(setsQueryKeys.one({ setId }));
2229
+ }
2230
+ function updateSetInCache({
2231
+ set,
2232
+ queryClient
2233
+ }) {
2234
+ const { id, ...setData } = set;
2235
+ queryClient.setQueryData(setsQueryKeys.one({ setId: id }), setData);
2236
+ }
2237
+
2238
+ // src/domains/sets/set.repo.ts
2239
+ var createSetRepo = () => {
2240
+ return {
2241
+ getSet
2242
+ };
2243
+ };
2244
+
2245
+ // src/utils/ai/get-transcript.ts
2246
+ async function getTranscript(model, args) {
2247
+ var _a, _b, _c, _d;
2248
+ const getGeminiTranscript = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "getGeminiTranscript");
2249
+ const getAssemblyAITranscript = (_d = (_c = api).httpsCallable) == null ? void 0 : _d.call(_c, "transcribeAssemblyAIAudio");
2250
+ if (model === "gemini") {
2251
+ try {
2252
+ const { data } = await (getGeminiTranscript == null ? void 0 : getGeminiTranscript({
2253
+ audioUrl: args.audioUrl,
2254
+ targetLanguage: args.language,
2255
+ prompt: args.prompt
2256
+ }));
2257
+ return data.transcript;
2258
+ } catch (error) {
2259
+ console.error("Error getting transcript from Gemini:", error);
2260
+ throw error;
2261
+ }
2262
+ }
2263
+ if (model === "assemblyai") {
2264
+ try {
2265
+ const response = await (getAssemblyAITranscript == null ? void 0 : getAssemblyAITranscript({
2266
+ audioUrl: args.audioUrl,
2267
+ language: args.language
2268
+ }));
2269
+ return response.data;
2270
+ } catch (error) {
2271
+ console.error("Error getting transcript from AssemblyAI:", error);
2272
+ throw error;
2273
+ }
2274
+ }
2275
+ return null;
2276
+ }
2277
+
2278
+ // src/constants/all-langs.json
2279
+ var all_langs_default = {
2280
+ af: "Afrikaans",
2281
+ sq: "Albanian",
2282
+ am: "Amharic",
2283
+ ar: "Arabic",
2284
+ hy: "Armenian",
2285
+ az: "Azerbaijani",
2286
+ eu: "Basque",
2287
+ be: "Belarusian",
2288
+ bn: "Bengali",
2289
+ bs: "Bosnian",
2290
+ bg: "Bulgarian",
2291
+ ca: "Catalan",
2292
+ ceb: "Cebuano",
2293
+ zh: "Chinese",
2294
+ co: "Corsican",
2295
+ hr: "Croatian",
2296
+ cs: "Czech",
2297
+ da: "Danish",
2298
+ nl: "Dutch",
2299
+ en: "English",
2300
+ eo: "Esperanto",
2301
+ et: "Estonian",
2302
+ fi: "Finnish",
2303
+ fr: "French",
2304
+ fy: "Frisian",
2305
+ gl: "Galician",
2306
+ ka: "Georgian",
2307
+ de: "German",
2308
+ el: "Greek",
2309
+ gu: "Gujarati",
2310
+ ht: "Haitian Creole",
2311
+ ha: "Hausa",
2312
+ haw: "Hawaiian",
2313
+ he: "Hebrew",
2314
+ hi: "Hindi",
2315
+ hmn: "Hmong",
2316
+ hu: "Hungarian",
2317
+ is: "Icelandic",
2318
+ ig: "Igbo",
2319
+ id: "Indonesian",
2320
+ ga: "Irish",
2321
+ it: "Italian",
2322
+ ja: "Japanese",
2323
+ jv: "Javanese",
2324
+ kn: "Kannada",
2325
+ kk: "Kazakh",
2326
+ km: "Khmer",
2327
+ ko: "Korean",
2328
+ ku: "Kurdish",
2329
+ ky: "Kyrgyz",
2330
+ lo: "Lao",
2331
+ la: "Latin",
2332
+ lv: "Latvian",
2333
+ lt: "Lithuanian",
2334
+ lb: "Luxembourgish",
2335
+ mk: "Macedonian",
2336
+ mg: "Malagasy",
2337
+ ms: "Malay",
2338
+ ml: "Malayalam",
2339
+ mt: "Maltese",
2340
+ mi: "Maori",
2341
+ mr: "Marathi",
2342
+ mn: "Mongolian",
2343
+ my: "Myanmar (Burmese)",
2344
+ ne: "Nepali",
2345
+ no: "Norwegian",
2346
+ ny: "Nyanja (Chichewa)",
2347
+ ps: "Pashto",
2348
+ fa: "Persian",
2349
+ pl: "Polish",
2350
+ pt: "Portuguese",
2351
+ pa: "Punjabi",
2352
+ ro: "Romanian",
2353
+ ru: "Russian",
2354
+ sm: "Samoan",
2355
+ gd: "Scots Gaelic",
2356
+ sr: "Serbian",
2357
+ st: "Sesotho",
2358
+ sn: "Shona",
2359
+ sd: "Sindhi",
2360
+ si: "Sinhala (Sinhalese)",
2361
+ sk: "Slovak",
2362
+ sl: "Slovenian",
2363
+ so: "Somali",
2364
+ es: "Spanish",
2365
+ su: "Sundanese",
2366
+ sw: "Swahili",
2367
+ sv: "Swedish",
2368
+ tl: "Tagalog (Filipino)",
2369
+ tg: "Tajik",
2370
+ ta: "Tamil",
2371
+ te: "Telugu",
2372
+ th: "Thai",
2373
+ tr: "Turkish",
2374
+ uk: "Ukrainian",
2375
+ ur: "Urdu",
2376
+ uz: "Uzbek",
2377
+ vi: "Vietnamese",
2378
+ cy: "Welsh",
2379
+ xh: "Xhosa",
2380
+ yi: "Yiddish",
2381
+ yo: "Yoruba",
2382
+ zu: "Zulu"
2383
+ };
2384
+
2385
+ // src/utils/ai/get-respond-card-tool.ts
2386
+ var getRespondCardTool = ({
2387
+ language,
2388
+ standard = "actfl"
2389
+ }) => {
2390
+ const lang = all_langs_default[language] || "English";
2391
+ const tool = {
2392
+ tool_choice: {
2393
+ type: "function",
2394
+ function: { name: "get_feedback" }
2395
+ },
2396
+ tools: [
2397
+ {
2398
+ type: "function",
2399
+ function: {
2400
+ name: "get_feedback",
2401
+ description: "Get feedback on a student's response",
2402
+ parameters: {
2403
+ type: "object",
2404
+ required: [
2405
+ "success",
2406
+ "score",
2407
+ "score_justification",
2408
+ "errors",
2409
+ "improvedResponse",
2410
+ "compliments"
2411
+ ],
2412
+ properties: {
2413
+ success: {
2414
+ type: "boolean",
2415
+ description: "Mark true if the student's response was on-topic and generally demonstrated understanding. A few grammar mistakes are acceptable. Mark false if the student's response was off-topic or did not demonstrate understanding."
2416
+ },
2417
+ errors: {
2418
+ type: "array",
2419
+ items: {
2420
+ type: "object",
2421
+ required: ["error", "grammar_error_type", "correction", "justification"],
2422
+ properties: {
2423
+ error: {
2424
+ type: "string",
2425
+ description: "The grammatical error in the student's response."
2426
+ },
2427
+ correction: {
2428
+ type: "string",
2429
+ description: "The suggested correction to the error"
2430
+ },
2431
+ justification: {
2432
+ type: "string",
2433
+ description: `An explanation of the rationale behind the suggested correction. WRITE THIS IN ${lang}!`
2434
+ },
2435
+ grammar_error_type: {
2436
+ type: "string",
2437
+ enum: [
2438
+ "subjVerbAgree",
2439
+ "tenseErrors",
2440
+ "articleMisuse",
2441
+ "prepositionErrors",
2442
+ "adjNounAgree",
2443
+ "pronounErrors",
2444
+ "wordOrder",
2445
+ "verbConjugation",
2446
+ "pluralization",
2447
+ "negationErrors",
2448
+ "modalVerbMisuse",
2449
+ "relativeClause",
2450
+ "auxiliaryVerb",
2451
+ "complexSentenceAgreement",
2452
+ "idiomaticExpression",
2453
+ "registerInconsistency",
2454
+ "voiceMisuse"
2455
+ ],
2456
+ description: "The type of grammatical error found. It should be one of the following categories: subject-verb agreement, tense errors, article misuse, preposition errors, adjective-noun agreement, pronoun errors, word order, verb conjugation, pluralization errors, negation errors, modal verb misuse, relative clause errors, auxiliary verb misuse, complex sentence agreement, idiomatic expression, register inconsistency, or voice misuse"
2457
+ }
2458
+ }
2459
+ },
2460
+ description: "An array of objects, each representing a grammatical error in the student's response. Each object should have the following properties: error, grammar_error_type, correction, and justification. If there were no errors, return an empty array."
2461
+ },
2462
+ compliments: {
2463
+ type: "array",
2464
+ items: {
2465
+ type: "string"
2466
+ },
2467
+ description: `An array of strings, each representing something the student did well. Each string should be WRITTEN IN ${lang}!`
2468
+ },
2469
+ improvedResponse: {
2470
+ type: "string",
2471
+ description: "An improved response with proper grammar and more detail, if applicable."
2472
+ },
2473
+ score: {
2474
+ type: "number",
2475
+ description: "A score between 0 and 100, reflecting the overall quality of the response"
2476
+ },
2477
+ score_justification: {
2478
+ type: "string",
2479
+ description: "An explanation of the rationale behind the assigned score, considering both accuracy and fluency"
2480
+ }
2481
+ }
2482
+ }
2483
+ }
2484
+ }
2485
+ ]
2486
+ };
2487
+ if (standard === "wida") {
2488
+ const wida_level = {
2489
+ type: "number",
2490
+ enum: [1, 2, 3, 4, 5, 6],
2491
+ description: `The student's WIDA (World-Class Instructional Design and Assessment) proficiency level. Choose one of the following options: 1, 2, 3, 4, 5, 6 which corresponds to
2492
+
2493
+ 1 - Entering
2494
+ 2 - Emerging
2495
+ 3 - Developing
2496
+ 4 - Expanding
2497
+ 5 - Bridging
2498
+ 6 - Reaching
2499
+
2500
+ This is an estimate based on the level of the student's response. Use the descriptions of the WIDA speaking standards to guide your decision.
2501
+ `
2502
+ };
2503
+ const wida_justification = {
2504
+ type: "string",
2505
+ description: `An explanation of the rationale behind the assigned WIDA level of the response, considering both accuracy and fluency. WRITE THIS IN ENGLISH!`
2506
+ };
2507
+ tool.tools[0].function.parameters.required.push("wida_level");
2508
+ tool.tools[0].function.parameters.required.push("wida_justification");
2509
+ tool.tools[0].function.parameters.properties.wida_level = wida_level;
2510
+ tool.tools[0].function.parameters.properties.wida_justification = wida_justification;
2511
+ } else {
2512
+ const actfl_level = {
2513
+ type: "string",
2514
+ enum: ["NL", "NM", "NH", "IL", "IM", "IH", "AL", "AM", "AH", "S", "D"],
2515
+ description: "The student's ACTFL (American Council on the Teaching of Foreign Languages) proficiency level. Choose one of the following options: NL, NM, NH, IL, IM, IH, AL, AM, AH, S, or D"
2516
+ };
2517
+ const actfl_justification = {
2518
+ type: "string",
2519
+ description: "An explanation of the rationale behind the assigned ACTFL level, considering both accuracy and fluency"
2520
+ };
2521
+ tool.tools[0].function.parameters.required.push("actfl_level");
2522
+ tool.tools[0].function.parameters.required.push("actfl_justification");
2523
+ tool.tools[0].function.parameters.properties.actfl_level = actfl_level;
2524
+ tool.tools[0].function.parameters.properties.actfl_justification = actfl_justification;
2525
+ }
2526
+ return tool;
2527
+ };
2528
+
2529
+ // src/hooks/useActivity.ts
2530
+ var import_react3 = require("react");
2531
+
2532
+ // src/services/add-grading-standard.ts
2533
+ var addGradingStandardLog = async (gradingStandard, userId) => {
2534
+ logGradingStandardLog(gradingStandard);
2535
+ const path = `users/${userId}/grading_standard_logs`;
2536
+ await api.addDoc(path, gradingStandard);
2537
+ };
2538
+
2539
+ // src/hooks/useActivityTracker.ts
2540
+ var import_uuid2 = require("uuid");
2541
+ function useActivityTracker({ userId }) {
2542
+ const trackActivity = async ({
2543
+ activityName,
2544
+ activityType,
2545
+ id = (0, import_uuid2.v4)(),
2546
+ language = ""
2547
+ }) => {
2548
+ if (userId) {
2549
+ const { doc: doc2, serverTimestamp: serverTimestamp2, setDoc: setDoc2 } = api.accessHelpers();
2550
+ const activityRef = doc2(`users/${userId}/activity/${id}`);
2551
+ const timestamp = serverTimestamp2();
2552
+ await setDoc2(activityRef, {
2553
+ name: activityName,
2554
+ type: activityType,
2555
+ lastSeen: timestamp,
2556
+ id,
2557
+ language
2558
+ });
2559
+ }
2560
+ };
2561
+ return {
2562
+ trackActivity
2563
+ };
2564
+ }
2565
+
2566
+ // src/hooks/useActivity.ts
2567
+ function useActivity({
2568
+ id,
2569
+ isAssignment,
2570
+ onAssignmentSubmitted,
2571
+ ltiData
2572
+ }) {
2573
+ var _a, _b;
2574
+ const { queryClient, user } = useSpeakableApi();
2575
+ const userId = user.auth.uid;
2576
+ const assignmentQuery = useAssignment({
2577
+ assignmentId: id,
2578
+ userId,
2579
+ enabled: isAssignment
2580
+ });
2581
+ const activeAssignment = assignmentQuery.data;
2582
+ const setId = isAssignment ? (_a = activeAssignment == null ? void 0 : activeAssignment.setId) != null ? _a : "" : id;
2583
+ const querySet = useSet({ setId });
2584
+ const setData = querySet.data;
2585
+ const assignmentContent = activeAssignment == null ? void 0 : activeAssignment.content;
2586
+ const assignmentWeights = activeAssignment == null ? void 0 : activeAssignment.weights;
2587
+ const setContent = setData == null ? void 0 : setData.content;
2588
+ const setWeights = setData == null ? void 0 : setData.weights;
2589
+ const contentCardsToUse = isAssignment ? assignmentContent != null ? assignmentContent : setContent : setContent;
2590
+ const weightsToUse = isAssignment ? assignmentWeights != null ? assignmentWeights : setWeights : setWeights;
2591
+ const activityId = isAssignment ? (_b = activeAssignment == null ? void 0 : activeAssignment.id) != null ? _b : "" : setId;
2592
+ const { cardsObject, cardsQueries, cards } = useCards({
2593
+ cardIds: contentCardsToUse != null ? contentCardsToUse : [],
2594
+ enabled: querySet.isSuccess,
2595
+ asObject: true
2596
+ });
2597
+ const scorableCardIds = (contentCardsToUse != null ? contentCardsToUse : []).filter((cardId) => {
2598
+ const card = cardsObject == null ? void 0 : cardsObject[cardId];
2599
+ return (card == null ? void 0 : card.type) !== "MEDIA_PAGE" /* MEDIA_PAGE */;
2600
+ });
2601
+ const scoreQuery = useScore({
2602
+ isAssignment,
2603
+ activityId: id,
2604
+ userId,
2605
+ courseId: activeAssignment == null ? void 0 : activeAssignment.courseId,
2606
+ googleClassroomUserId: user.profile.googleClassroomUserId,
2607
+ enabled: isAssignment ? assignmentQuery.isSuccess : querySet.isSuccess
2608
+ });
2609
+ const { mutationUpdateScore } = useUpdateScore();
2610
+ const { mutationUpdateCardScore } = useUpdateCardScore({
2611
+ activityId,
2612
+ isAssignment,
2613
+ userId,
2614
+ cardIds: scorableCardIds,
2615
+ weights: weightsToUse != null ? weightsToUse : {}
2616
+ });
2617
+ const { mutationClearScore } = useClearScore();
2618
+ const { submitAssignmentScore: submitAssignmentScore2 } = useSubmitAssignmentScore({
2619
+ onAssignmentSubmitted,
2620
+ studentName: user.profile.displayName
2621
+ });
2622
+ const { submitPracticeScore: submitPracticeScore2 } = useSubmitPracticeScore();
2623
+ const handleUpdateScore = (data) => {
2624
+ mutationUpdateScore.mutate({
2625
+ data,
2626
+ isAssignment,
2627
+ activityId,
2628
+ userId
2629
+ });
2630
+ };
2631
+ const handleUpdateCardScore = (cardId, cardScore) => {
2632
+ mutationUpdateCardScore.mutate({ cardId, cardScore });
2633
+ if (cardScore.proficiency_level) {
2634
+ logGradingStandardEntry({
2635
+ type: cardScore.proficiency_level.standardId,
2636
+ cardId,
2637
+ gradingStandard: cardScore.proficiency_level
2638
+ });
2639
+ } else if (cardScore.wida || cardScore.actfl) {
2640
+ logGradingStandardEntry({
2641
+ type: cardScore.wida ? "wida" : "actfl",
2642
+ cardId,
2643
+ gradingStandard: cardScore.wida || cardScore.actfl || { level: "", justification: "" }
2644
+ });
2645
+ }
2646
+ };
2647
+ const onClearScore = ({
2648
+ cardId,
2649
+ wasCompleted = true
2650
+ }) => {
2651
+ var _a2, _b2;
2652
+ const currentCard = cardsObject == null ? void 0 : cardsObject[cardId];
2653
+ if ((currentCard == null ? void 0 : currentCard.type) === "MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */ || (currentCard == null ? void 0 : currentCard.type) === "READ_REPEAT" /* READ_REPEAT */) {
2654
+ return;
2655
+ }
2656
+ const queryKeys = scoreQueryKeys.byId(activityId);
2657
+ const activeCardScores = (_b2 = (_a2 = queryClient.getQueryData(queryKeys)) == null ? void 0 : _a2.cards) == null ? void 0 : _b2[cardId];
2658
+ if (activeCardScores === void 0) return;
2659
+ mutationClearScore.mutate({
2660
+ isAssignment,
2661
+ activityId,
2662
+ cardScores: activeCardScores,
2663
+ cardId,
2664
+ userId
2665
+ });
2666
+ };
2667
+ const onSubmitScore = async () => {
2668
+ var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
2669
+ try {
2670
+ let results;
2671
+ if (isAssignment) {
2672
+ const someCardIsManualGraded = cards.some((page) => page.grading_method === "manual");
2673
+ results = await submitAssignmentScore2({
2674
+ assignment: {
2675
+ id: (_b2 = (_a2 = assignmentQuery.data) == null ? void 0 : _a2.id) != null ? _b2 : "",
2676
+ name: (_d = (_c = assignmentQuery.data) == null ? void 0 : _c.name) != null ? _d : "",
2677
+ owners: (_f = (_e = assignmentQuery.data) == null ? void 0 : _e.owners) != null ? _f : [],
2678
+ courseId: (_h = (_g = assignmentQuery.data) == null ? void 0 : _g.courseId) != null ? _h : "",
2679
+ courseWorkId: (_j = (_i = assignmentQuery.data) == null ? void 0 : _i.courseWorkId) != null ? _j : "",
2680
+ isAssessment: (_l = (_k = assignmentQuery.data) == null ? void 0 : _k.isAssessment) != null ? _l : false,
2681
+ maxPoints: (_n = (_m = assignmentQuery.data) == null ? void 0 : _m.maxPoints) != null ? _n : 0
2682
+ },
2683
+ userId,
2684
+ cardIds: scorableCardIds,
2685
+ scores: scoreQuery.data,
2686
+ weights: weightsToUse != null ? weightsToUse : {},
2687
+ status: someCardIsManualGraded ? "PENDING_REVIEW" : "SUBMITTED"
2688
+ });
2689
+ if ((_o = assignmentQuery.data) == null ? void 0 : _o.ltiDeeplink) {
2690
+ submitLTIScore({
2691
+ maxPoints: (_p = assignmentQuery.data) == null ? void 0 : _p.maxPoints,
2692
+ score: (_r = (_q = scoreQuery.data) == null ? void 0 : _q.score) != null ? _r : 0,
2693
+ SERVICE_KEY: (_s = ltiData == null ? void 0 : ltiData.serviceKey) != null ? _s : "",
2694
+ lineItemId: (_t = ltiData == null ? void 0 : ltiData.lineItemId) != null ? _t : "",
2695
+ lti_id: (_u = ltiData == null ? void 0 : ltiData.lti_id) != null ? _u : ""
2696
+ });
2697
+ }
2698
+ } else {
2699
+ results = await submitPracticeScore2({
2700
+ setId: (_w = (_v = querySet.data) == null ? void 0 : _v.id) != null ? _w : "",
2701
+ userId,
2702
+ scores: scoreQuery.data
2703
+ });
2704
+ }
2705
+ return results;
2706
+ } catch (error) {
2707
+ return {
2708
+ success: false,
2709
+ error
2710
+ };
2711
+ }
2712
+ };
2713
+ const logGradingStandardEntry = ({
2714
+ cardId,
2715
+ gradingStandard,
2716
+ type
2717
+ }) => {
2718
+ var _a2, _b2, _c, _d, _e, _f, _g, _h, _i;
2719
+ const card = cardsObject == null ? void 0 : cardsObject[cardId];
2720
+ const scoresObject = queryClient.getQueryData(scoreQueryKeys.byId(activityId));
2721
+ const cardScore = (_a2 = scoresObject == null ? void 0 : scoresObject.cards) == null ? void 0 : _a2[cardId];
2722
+ const serverTimestamp2 = api.helpers.serverTimestamp;
2723
+ addGradingStandardLog(
2724
+ {
2725
+ assignmentId: (_b2 = activeAssignment == null ? void 0 : activeAssignment.id) != null ? _b2 : "",
2726
+ courseId: (_c = activeAssignment == null ? void 0 : activeAssignment.courseId) != null ? _c : "",
2727
+ teacherId: (_d = activeAssignment == null ? void 0 : activeAssignment.owners[0]) != null ? _d : "",
2728
+ setId: (_e = setData == null ? void 0 : setData.id) != null ? _e : "",
2729
+ cardId,
2730
+ level: gradingStandard.level,
2731
+ justification: gradingStandard.justification,
2732
+ transcript: (_f = cardScore == null ? void 0 : cardScore.transcript) != null ? _f : "",
2733
+ audioUrl: (_g = cardScore == null ? void 0 : cardScore.audio) != null ? _g : "",
2734
+ prompt: (_h = card == null ? void 0 : card.prompt) != null ? _h : "",
2735
+ responseType: (card == null ? void 0 : card.type) === "RESPOND_WRITE" /* RESPOND_WRITE */ ? "written" : "spoken",
2736
+ type,
2737
+ dateMade: serverTimestamp2(),
2738
+ language: (_i = card == null ? void 0 : card.language) != null ? _i : ""
2739
+ },
2740
+ userId
2741
+ );
2742
+ };
2743
+ (0, import_react3.useEffect)(() => {
2744
+ if (isAssignment) {
2745
+ logOpenAssignment({ assignmentId: id });
2746
+ } else {
2747
+ logOpenActivityPreview({ setId: id });
2748
+ }
2749
+ }, []);
2750
+ useInitActivity({
2751
+ assignment: activeAssignment != null ? activeAssignment : void 0,
2752
+ set: setData != null ? setData : void 0,
2753
+ enabled: !!setData,
2754
+ userId
2755
+ });
2756
+ return {
2757
+ set: {
2758
+ data: setData,
2759
+ query: querySet
2760
+ },
2761
+ cards: {
2762
+ data: cardsObject,
2763
+ query: cardsQueries,
2764
+ cardsArray: cards
2765
+ },
2766
+ assignment: {
2767
+ data: isAssignment ? activeAssignment : void 0,
2768
+ query: assignmentQuery
2769
+ },
2770
+ scores: {
2771
+ data: scoreQuery.data,
2772
+ query: scoreQuery,
2773
+ actions: {
2774
+ update: handleUpdateScore,
2775
+ clear: onClearScore,
2776
+ submit: onSubmitScore,
2777
+ updateCard: handleUpdateCardScore,
2778
+ logGradingStandardEntry
2779
+ }
2780
+ }
2781
+ };
2782
+ }
2783
+ var useInitActivity = ({
2784
+ assignment,
2785
+ set,
2786
+ enabled,
2787
+ userId
2788
+ }) => {
2789
+ const { trackActivity } = useActivityTracker({ userId });
2790
+ const init = () => {
2791
+ var _a, _b, _c, _d, _e, _f, _g;
2792
+ if (!enabled) return;
2793
+ if (!assignment) {
2794
+ trackActivity({
2795
+ activityName: (_a = set == null ? void 0 : set.name) != null ? _a : "",
2796
+ activityType: "set",
2797
+ id: set == null ? void 0 : set.id,
2798
+ language: set == null ? void 0 : set.language
2799
+ });
2800
+ } else if (assignment.name) {
2801
+ trackActivity({
2802
+ activityName: assignment.name,
2803
+ activityType: assignment.isAssessment ? "assessment" : "assignment",
2804
+ id: assignment.id,
2805
+ language: set == null ? void 0 : set.language
2806
+ });
2807
+ }
2808
+ if (set == null ? void 0 : set.public) {
2809
+ (_d = (_c = (_b = api).httpsCallable) == null ? void 0 : _c.call(_b, "onSetOpened")) == null ? void 0 : _d({
2810
+ setId: set.id,
2811
+ language: set.language
2812
+ });
2813
+ }
2814
+ (_g = (_f = (_e = api).httpsCallable) == null ? void 0 : _f.call(_e, "updateAlgoliaIndex")) == null ? void 0 : _g({
2815
+ updatePlays: true,
2816
+ objectID: set == null ? void 0 : set.id
2817
+ });
2818
+ };
2819
+ (0, import_react3.useEffect)(() => {
2820
+ init();
2821
+ }, [set]);
2822
+ };
2823
+ var submitLTIScore = async ({
2824
+ maxPoints,
2825
+ score,
2826
+ SERVICE_KEY,
2827
+ lineItemId,
2828
+ lti_id
2829
+ }) => {
2830
+ var _a, _b, _c;
2831
+ try {
2832
+ if (!SERVICE_KEY || !lineItemId || !lti_id) {
2833
+ throw new Error("Missing required LTI credentials");
2834
+ }
2835
+ const earnedPoints = score ? score / 100 * maxPoints : 0;
2836
+ const { data } = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitLTIAssignmentScore")) == null ? void 0 : _c({
2837
+ SERVICE_KEY,
2838
+ scoreData: {
2839
+ lineItemId,
2840
+ userId: lti_id,
2841
+ maxPoints,
2842
+ earnedPoints
2843
+ }
2844
+ }));
2845
+ return { success: true, data };
2846
+ } catch (error) {
2847
+ console.error("Failed to submit LTI score:", error);
2848
+ return {
2849
+ success: false,
2850
+ error: error instanceof Error ? error : new Error("Unknown error occurred")
2851
+ };
2852
+ }
2853
+ };
2854
+
2855
+ // src/hooks/useCredits.ts
2856
+ var import_react_query5 = require("@tanstack/react-query");
2857
+ var creditQueryKeys = {
2858
+ userCredits: (uid) => ["userCredits", uid]
2859
+ };
2860
+ var useUserCredits = () => {
2861
+ const { user } = useSpeakableApi();
2862
+ const email = user.auth.email;
2863
+ const uid = user.auth.uid;
2864
+ const query2 = (0, import_react_query5.useQuery)({
2865
+ queryKey: creditQueryKeys.userCredits(uid),
2866
+ queryFn: () => fetchUserCredits({ uid, email }),
2867
+ enabled: !!uid,
2868
+ refetchInterval: 1e3 * 60 * 5
2869
+ });
2870
+ return {
2871
+ ...query2
2872
+ };
2873
+ };
2874
+ var fetchUserCredits = async ({ uid, email }) => {
2875
+ if (!uid) {
2876
+ throw new Error("User ID is required");
2877
+ }
2878
+ const contractSnap = await api.getDoc(`creditContracts/${uid}`);
2879
+ if (contractSnap.data == null) {
2880
+ return {
2881
+ id: uid,
2882
+ userId: uid,
2883
+ email,
2884
+ effectivePlanId: "free_tier",
2885
+ status: "inactive",
2886
+ isUnlimited: false,
2887
+ creditsAvailable: 100,
2888
+ creditsAllocatedThisPeriod: 100,
2889
+ topOffCreditsAvailable: 0,
2890
+ topOffCreditsTotal: 0,
2891
+ allocationSource: "free_tier",
2892
+ sourceDetails: {},
2893
+ periodStart: null,
2894
+ periodEnd: null,
2895
+ planTermEndTimestamp: null,
2896
+ ownerType: "individual",
2897
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2898
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
2899
+ };
2900
+ }
2901
+ const contractData = contractSnap.data;
2902
+ const monthlyCredits = (contractData == null ? void 0 : contractData.creditsAvailable) || 0;
2903
+ const topOffCredits = (contractData == null ? void 0 : contractData.topOffCreditsAvailable) || 0;
2904
+ const totalCredits = monthlyCredits + topOffCredits;
2905
+ return {
2906
+ id: contractSnap.id,
2907
+ ...contractData,
2908
+ // Add computed total for convenience
2909
+ totalCreditsAvailable: totalCredits
2910
+ };
2911
+ };
2912
+
2913
+ // src/hooks/useOrganizationAccess.ts
2914
+ var import_react_query6 = require("@tanstack/react-query");
2915
+ var useOrganizationAccess = () => {
2916
+ const { user } = useSpeakableApi();
2917
+ const email = user.auth.email;
2918
+ const query2 = (0, import_react_query6.useQuery)({
2919
+ queryKey: ["organizationAccess", email],
2920
+ queryFn: async () => {
2921
+ if (!email) {
2922
+ return {
2923
+ hasUnlimitedAccess: false,
2924
+ subscriptionId: null,
2925
+ organizationId: null,
2926
+ organizationName: null,
2927
+ subscriptionEndDate: null,
2928
+ accessType: "individual"
2929
+ };
2930
+ }
2931
+ return getOrganizationAccess(email);
2932
+ },
2933
+ enabled: !!email,
2934
+ // Only run query if we have a user email
2935
+ staleTime: 5 * 60 * 1e3,
2936
+ // Consider data fresh for 5 minutes
2937
+ gcTime: 10 * 60 * 1e3,
2938
+ // Keep in cache for 10 minutes
2939
+ retry: 2
2940
+ // Retry failed requests twice
2941
+ });
2942
+ return {
2943
+ ...query2
2944
+ };
2945
+ };
2946
+ var getOrganizationAccess = async (email) => {
2947
+ const { limit: limit2, where: where2 } = api.accessQueryConstraints();
2948
+ try {
2949
+ const organizationSnapshot = await api.getDocs(
2950
+ "organizations",
2951
+ where2("members", "array-contains", email),
2952
+ where2("masterSubscriptionStatus", "==", "active"),
2953
+ limit2(1)
2954
+ );
2955
+ if (!organizationSnapshot.empty) {
2956
+ const orgData = organizationSnapshot.data[0];
2957
+ return {
2958
+ hasUnlimitedAccess: true,
2959
+ subscriptionId: orgData == null ? void 0 : orgData.masterSubscriptionId,
2960
+ organizationId: orgData.id,
2961
+ organizationName: orgData.name || "Unknown Organization",
2962
+ subscriptionEndDate: orgData.masterSubscriptionEndDate || null,
2963
+ accessType: "organization"
2964
+ };
2965
+ }
2966
+ const institutionSnapshot = await api.getDocs(
2967
+ "institution_subscriptions",
2968
+ where2("users", "array-contains", email),
2969
+ where2("active", "==", true),
2970
+ limit2(1)
2971
+ );
2972
+ if (!institutionSnapshot.empty) {
2973
+ const institutionData = institutionSnapshot.data[0];
2974
+ const isUnlimited = (institutionData == null ? void 0 : institutionData.plan) === "organization" || (institutionData == null ? void 0 : institutionData.plan) === "school_starter";
2975
+ return {
2976
+ hasUnlimitedAccess: isUnlimited,
2977
+ subscriptionId: institutionData.id,
2978
+ organizationId: institutionData == null ? void 0 : institutionData.institutionId,
2979
+ organizationName: institutionData.name || institutionData.institutionId || "Legacy Institution",
2980
+ subscriptionEndDate: institutionData.endDate || null,
2981
+ accessType: "institution_subscriptions"
2982
+ };
2983
+ }
2984
+ return {
2985
+ hasUnlimitedAccess: false,
2986
+ subscriptionId: null,
2987
+ organizationId: null,
2988
+ organizationName: null,
2989
+ subscriptionEndDate: null,
2990
+ accessType: "individual"
2991
+ };
2992
+ } catch (error) {
2993
+ console.error("Error checking organization access:", error);
2994
+ return {
2995
+ hasUnlimitedAccess: false,
2996
+ subscriptionId: null,
2997
+ organizationId: null,
2998
+ organizationName: null,
2999
+ subscriptionEndDate: null,
3000
+ accessType: "individual"
3001
+ };
3002
+ }
3003
+ };
3004
+
3005
+ // src/hooks/useSpeakableTranscript.ts
3006
+ var import_react_query7 = require("@tanstack/react-query");
3007
+ function useSpeakableTranscript() {
3008
+ const mutation = (0, import_react_query7.useMutation)({
3009
+ mutationFn: async ({
3010
+ model,
3011
+ audioUrl,
3012
+ language,
3013
+ prompt
3014
+ }) => {
3015
+ return getTranscript(model, { audioUrl, language, prompt });
3016
+ },
3017
+ retry: false
3018
+ });
3019
+ return {
3020
+ mutation
3021
+ };
3022
+ }
3023
+
3024
+ // src/hooks/useUpdateStudentVoc.ts
3025
+ var useUpdateStudentVocab = (page) => {
3026
+ const { user } = useSpeakableApi();
3027
+ const currentUserId = user == null ? void 0 : user.auth.uid;
3028
+ if (!page || !currentUserId || !page.target_text || !page.language) {
3029
+ return {
3030
+ studentVocabMarkVoiceSuccess: void 0,
3031
+ studentVocabMarkVoiceFail: void 0
3032
+ };
3033
+ }
3034
+ const getDataObject = () => {
3035
+ var _a, _b;
3036
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
3037
+ const language = (_a = page.language) != null ? _a : "en";
3038
+ const word = (_b = page.target_text) != null ? _b : "";
3039
+ const phrase_length = getPhraseLength(word);
3040
+ const wordHash = getWordHash(word, language);
3041
+ const docPath = `users/${currentUserId}/vocab/${wordHash}`;
3042
+ const communityPath = `checked-pronunciations/${wordHash}`;
3043
+ const id = `${language}-${cleanString(word)}`;
3044
+ const data = {
3045
+ id,
3046
+ word,
3047
+ words: (word == null ? void 0 : word.split(" ")) || [],
3048
+ wordHash,
3049
+ language,
3050
+ lastSeen: serverTimestamp2(),
3051
+ phrase_length
3052
+ };
3053
+ return {
3054
+ docPath,
3055
+ communityPath,
3056
+ data
3057
+ };
3058
+ };
3059
+ const markVoiceSuccess = async () => {
3060
+ const { docPath, communityPath, data } = getDataObject();
3061
+ const { increment: increment2 } = api.accessQueryConstraints();
3062
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
3063
+ data.voiceSuccess = increment2(1);
3064
+ try {
3065
+ await api.updateDoc(docPath, data);
3066
+ } catch (error) {
3067
+ if (error instanceof Error && error.message === "not-found") {
3068
+ data.firstSeen = serverTimestamp2();
3069
+ await api.setDoc(docPath, data, { merge: true });
3070
+ } else {
3071
+ console.log(error);
3072
+ }
3073
+ }
3074
+ try {
3075
+ data.pronunciations = increment2(1);
3076
+ await api.setDoc(communityPath, data, { merge: true });
3077
+ } catch (error) {
3078
+ console.log(error);
3079
+ }
3080
+ };
3081
+ const markVoiceFail = async () => {
3082
+ const { docPath, communityPath, data } = getDataObject();
3083
+ const { increment: increment2 } = api.accessQueryConstraints();
3084
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
3085
+ data.voiceFail = increment2(1);
3086
+ try {
3087
+ await api.updateDoc(docPath, data);
3088
+ } catch (error) {
3089
+ if (error instanceof Error && error.message === "not-found") {
3090
+ data.firstSeen = serverTimestamp2();
3091
+ await api.setDoc(docPath, data, { merge: true });
3092
+ } else {
3093
+ console.log(error);
3094
+ }
3095
+ }
3096
+ try {
3097
+ data.fails = increment2(1);
3098
+ await api.setDoc(communityPath, data, { merge: true });
3099
+ } catch (error) {
3100
+ console.log(error);
3101
+ }
3102
+ };
3103
+ return {
3104
+ studentVocabMarkVoiceSuccess: markVoiceSuccess,
3105
+ studentVocabMarkVoiceFail: markVoiceFail
3106
+ };
3107
+ };
3108
+
3109
+ // src/hooks/useActivityFeedbackAccess.ts
3110
+ var import_react_query8 = require("@tanstack/react-query");
3111
+ var activityFeedbackAccessQueryKeys = {
3112
+ activityFeedbackAccess: (args) => ["activityFeedbackAccess", ...Object.values(args)]
3113
+ };
3114
+ var useActivityFeedbackAccess = ({
3115
+ aiEnabled = false,
3116
+ isActivityRoute = false
3117
+ }) => {
3118
+ var _a, _b, _c;
3119
+ const { user } = useSpeakableApi();
3120
+ const uid = user.auth.uid;
3121
+ const isTeacher = (_a = user.profile) == null ? void 0 : _a.isTeacher;
3122
+ const isStudent = (_b = user.profile) == null ? void 0 : _b.isStudent;
3123
+ const userRoles = ((_c = user.profile) == null ? void 0 : _c.roles) || [];
3124
+ const query2 = (0, import_react_query8.useQuery)({
3125
+ queryKey: activityFeedbackAccessQueryKeys.activityFeedbackAccess({
3126
+ aiEnabled,
3127
+ isActivityRoute
3128
+ }),
3129
+ queryFn: async () => {
3130
+ var _a2, _b2, _c2;
3131
+ if (!uid) {
3132
+ return {
3133
+ canAccessFeedback: false,
3134
+ reason: "Missing user ID",
3135
+ isUnlimited: false,
3136
+ accessType: "none"
3137
+ };
3138
+ }
3139
+ try {
3140
+ if (aiEnabled) {
3141
+ return {
3142
+ canAccessFeedback: true,
3143
+ reason: "AI feedback enabled",
3144
+ isUnlimited: true,
3145
+ accessType: "ai_enabled"
3146
+ };
3147
+ }
3148
+ if (isTeacher || userRoles.includes("ADMIN")) {
3149
+ return {
3150
+ canAccessFeedback: true,
3151
+ reason: "Teacher preview access",
3152
+ isUnlimited: true,
3153
+ accessType: "teacher_preview"
3154
+ };
3155
+ }
3156
+ if (isStudent && isActivityRoute) {
3157
+ try {
3158
+ const result = await ((_c2 = (_b2 = (_a2 = api).httpsCallable) == null ? void 0 : _b2.call(_a2, "checkStudentTeacherPlan")) == null ? void 0 : _c2({
3159
+ studentId: uid
3160
+ }));
3161
+ const planCheckResult = result.data;
3162
+ if (planCheckResult.canAccessFeedback) {
3163
+ return {
3164
+ canAccessFeedback: true,
3165
+ reason: planCheckResult.reason || "Student access via teacher with active plan",
3166
+ isUnlimited: planCheckResult.hasTeacherWithUnlimitedAccess,
3167
+ accessType: "student_with_teacher_plan"
3168
+ };
3169
+ } else {
3170
+ return {
3171
+ canAccessFeedback: false,
3172
+ reason: planCheckResult.reason || "No teacher with active plan found",
3173
+ isUnlimited: false,
3174
+ accessType: "none"
3175
+ };
3176
+ }
3177
+ } catch (error) {
3178
+ console.error("Error checking student teacher plan:", error);
3179
+ return {
3180
+ canAccessFeedback: false,
3181
+ reason: "Error checking teacher plans",
3182
+ isUnlimited: false,
3183
+ accessType: "none"
3184
+ };
3185
+ }
3186
+ }
3187
+ return {
3188
+ canAccessFeedback: false,
3189
+ reason: "No access permissions found for current context",
3190
+ isUnlimited: false,
3191
+ accessType: "none"
3192
+ };
3193
+ } catch (error) {
3194
+ console.error("Error checking activity feedback access:", error);
3195
+ return {
3196
+ canAccessFeedback: false,
3197
+ reason: "Error checking access permissions",
3198
+ isUnlimited: false,
3199
+ accessType: "none"
3200
+ };
3201
+ }
3202
+ },
3203
+ enabled: !!uid,
3204
+ staleTime: 5 * 60 * 1e3,
3205
+ // 5 minutes
3206
+ gcTime: 10 * 60 * 1e3
3207
+ // 10 minutes
3208
+ });
3209
+ return {
3210
+ ...query2
3211
+ };
3212
+ };
3213
+
3214
+ // src/hooks/useOpenAI.ts
3215
+ var useBaseOpenAI = ({
3216
+ onTranscriptSuccess,
3217
+ onTranscriptError,
3218
+ onCompletionSuccess,
3219
+ onCompletionError,
3220
+ aiEnabled,
3221
+ submitAudioResponse,
3222
+ uploadAudioAndGetTranscript,
3223
+ onGetAudioUrlAndTranscript
3224
+ }) => {
3225
+ const { user, queryClient } = useSpeakableApi();
3226
+ const currentUserId = user.auth.uid;
3227
+ const { data: feedbackAccess } = useActivityFeedbackAccess({
3228
+ aiEnabled
3229
+ });
3230
+ const getTranscript2 = async (audioUrl, language, prompt) => {
3231
+ var _a, _b, _c, _d;
3232
+ const getGeminiTranscript = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "getGeminiTranscript");
3233
+ const getAssemblyAITranscript = (_d = (_c = api).httpsCallable) == null ? void 0 : _d.call(_c, "transcribeAssemblyAIAudio");
3234
+ try {
3235
+ const { data } = await (getGeminiTranscript == null ? void 0 : getGeminiTranscript({
3236
+ audioUrl,
3237
+ targetLanguage: language,
3238
+ prompt: prompt || ""
3239
+ }));
3240
+ const transcript = data.transcript;
3241
+ if (transcript) {
3242
+ return transcript;
3243
+ }
3244
+ } catch (error) {
3245
+ console.log("Gemini transcript failed, trying AssemblyAI fallback:", error);
3246
+ }
3247
+ try {
3248
+ const response = await (getAssemblyAITranscript == null ? void 0 : getAssemblyAITranscript({
3249
+ audioUrl,
3250
+ language
3251
+ }));
3252
+ const transcript = response == null ? void 0 : response.data;
3253
+ if (transcript) {
3254
+ return transcript;
3255
+ }
3256
+ throw new Error("Both transcript services failed");
3257
+ } catch (error) {
3258
+ console.log("AssemblyAI transcript also failed:", error);
3259
+ onTranscriptError({
3260
+ type: "TRANSCRIPT",
3261
+ message: (error == null ? void 0 : error.message) || "Error getting transcript from both services"
3262
+ });
3263
+ throw new Error(error);
3264
+ }
3265
+ };
3266
+ const getFreeResponseCompletion = async (messages, isFreeResponse, feedbackLanguage, gradingStandard = "actfl") => {
3267
+ var _a, _b, _c, _d, _e;
3268
+ const responseTool = getRespondCardTool({
3269
+ language: feedbackLanguage,
3270
+ standard: gradingStandard
3271
+ });
3272
+ try {
3273
+ const createChatCompletion = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "createChatCompletion");
3274
+ const {
3275
+ data: {
3276
+ response,
3277
+ prompt_tokens = 0,
3278
+ completion_tokens = 0,
3279
+ success: aiSuccess = false
3280
+ // the AI was able to generate a response
3281
+ }
3282
+ } = await (createChatCompletion == null ? void 0 : createChatCompletion({
3283
+ chat: {
3284
+ model: isFreeResponse ? "gpt-4-1106-preview" : "gpt-3.5-turbo-1106",
3285
+ messages,
3286
+ temperature: 0.7,
3287
+ ...responseTool
3288
+ },
3289
+ type: isFreeResponse ? "LONG_RESPONSE" : "SHORT_RESPONSE"
3290
+ }));
3291
+ const functionArguments = JSON.parse(((_e = (_d = (_c = response == null ? void 0 : response.tool_calls) == null ? void 0 : _c[0]) == null ? void 0 : _d.function) == null ? void 0 : _e.arguments) || "{}");
3292
+ const result = {
3293
+ ...functionArguments,
3294
+ prompt_tokens,
3295
+ completion_tokens,
3296
+ aiSuccess
3297
+ };
3298
+ onCompletionSuccess(result);
3299
+ return result;
3300
+ } catch (error) {
3301
+ onCompletionError({
3302
+ type: "COMPLETION",
3303
+ message: (error == null ? void 0 : error.message) || "Error getting completion"
3304
+ });
3305
+ throw new Error(error);
3306
+ }
3307
+ };
3308
+ const getFeedback = async ({
3309
+ cardId,
3310
+ language = "en",
3311
+ // required
3312
+ writtenResponse = null,
3313
+ // if the type = RESPOND_WRITE
3314
+ audio = null,
3315
+ autoGrade = true,
3316
+ file = null,
3317
+ pagePrompt = null
3318
+ }) => {
3319
+ try {
3320
+ if (!(feedbackAccess == null ? void 0 : feedbackAccess.canAccessFeedback)) {
3321
+ const result = {
3322
+ noFeedbackAvailable: true,
3323
+ success: true,
3324
+ reason: (feedbackAccess == null ? void 0 : feedbackAccess.reason) || "No feedback access",
3325
+ accessType: (feedbackAccess == null ? void 0 : feedbackAccess.accessType) || "none"
3326
+ };
3327
+ onCompletionSuccess(result);
3328
+ return result;
3329
+ }
3330
+ let transcript;
3331
+ let audioUrl = void 0;
3332
+ if (writtenResponse) {
3333
+ transcript = writtenResponse;
3334
+ onTranscriptSuccess(writtenResponse);
3335
+ } else if (typeof audio === "string" && file) {
3336
+ if (feedbackAccess == null ? void 0 : feedbackAccess.canAccessFeedback) {
3337
+ transcript = await getTranscript2(audio, language, pagePrompt != null ? pagePrompt : "");
3338
+ audioUrl = audio;
3339
+ onTranscriptSuccess(transcript);
3340
+ } else {
3341
+ console.info(
3342
+ `Transcript not available: ${(feedbackAccess == null ? void 0 : feedbackAccess.reason) || "No feedback access"}`
3343
+ );
3344
+ }
3345
+ } else {
3346
+ const response = await uploadAudioAndGetTranscript(audio || "", language, pagePrompt != null ? pagePrompt : "");
3347
+ transcript = response.transcript;
3348
+ audioUrl = response.audioUrl;
3349
+ }
3350
+ onGetAudioUrlAndTranscript == null ? void 0 : onGetAudioUrlAndTranscript({ transcript, audioUrl });
3351
+ if (feedbackAccess == null ? void 0 : feedbackAccess.canAccessFeedback) {
3352
+ const results = await getAIResponse({
3353
+ cardId,
3354
+ transcript: transcript || ""
3355
+ });
3356
+ let output = results;
3357
+ if (!autoGrade) {
3358
+ output = {
3359
+ ...output,
3360
+ noFeedbackAvailable: true,
3361
+ success: true
3362
+ };
3363
+ }
3364
+ onCompletionSuccess(output);
3365
+ return output;
3366
+ } else {
3367
+ const result = {
3368
+ noFeedbackAvailable: true,
3369
+ success: true,
3370
+ reason: (feedbackAccess == null ? void 0 : feedbackAccess.reason) || "No feedback access",
3371
+ accessType: (feedbackAccess == null ? void 0 : feedbackAccess.accessType) || "none"
3372
+ };
3373
+ onCompletionSuccess(result);
3374
+ return result;
3375
+ }
3376
+ } catch (error) {
3377
+ console.error("Error getting feedback:", error);
3378
+ throw new Error(error);
3379
+ }
3380
+ };
3381
+ const getAIResponse = async ({ cardId, transcript }) => {
3382
+ var _a, _b, _c, _d, _e;
3383
+ try {
3384
+ const getGeminiFeedback = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "callGetFeedback");
3385
+ const getProficiencyEstimate = (_d = (_c = api).httpsCallable) == null ? void 0 : _d.call(_c, "getProficiencyEstimate");
3386
+ const card = getCardFromCache({
3387
+ cardId,
3388
+ queryClient
3389
+ });
3390
+ let feedbackData;
3391
+ let proficiencyData = {};
3392
+ if (card && card.grading_method === "manual") {
3393
+ } else if (card && card.grading_method !== "standards_based") {
3394
+ const [geminiResult, proficiencyResult] = await Promise.all([
3395
+ getGeminiFeedback == null ? void 0 : getGeminiFeedback({
3396
+ cardId,
3397
+ studentId: currentUserId,
3398
+ studentResponse: transcript
3399
+ }),
3400
+ getProficiencyEstimate == null ? void 0 : getProficiencyEstimate({
3401
+ cardId,
3402
+ studentId: currentUserId,
3403
+ studentResponse: transcript
3404
+ })
3405
+ ]);
3406
+ proficiencyData = (proficiencyResult == null ? void 0 : proficiencyResult.data) || {};
3407
+ feedbackData = {
3408
+ ...(_e = geminiResult == null ? void 0 : geminiResult.data) != null ? _e : {},
3409
+ // @ts-ignore
3410
+ proficiency_level: (proficiencyData == null ? void 0 : proficiencyData.proficiency_level) || null
3411
+ };
3412
+ } else {
3413
+ const geminiResult = await (getGeminiFeedback == null ? void 0 : getGeminiFeedback({
3414
+ cardId,
3415
+ studentId: currentUserId,
3416
+ studentResponse: transcript
3417
+ }));
3418
+ feedbackData = geminiResult == null ? void 0 : geminiResult.data;
3419
+ }
3420
+ const results = {
3421
+ ...proficiencyData,
3422
+ ...feedbackData,
3423
+ aiSuccess: true,
3424
+ promptSuccess: (feedbackData == null ? void 0 : feedbackData.success) || false,
3425
+ transcript
3426
+ };
3427
+ return results;
3428
+ } catch (error) {
3429
+ onCompletionError({
3430
+ type: "AI_FEEDBACK",
3431
+ message: (error == null ? void 0 : error.message) || "Error getting ai feedback"
3432
+ });
3433
+ throw new Error(error);
3434
+ }
3435
+ };
3436
+ return {
3437
+ submitAudioResponse,
3438
+ uploadAudioAndGetTranscript,
3439
+ getTranscript: getTranscript2,
3440
+ getFreeResponseCompletion,
3441
+ getFeedback
3442
+ };
3443
+ };
3444
+
3445
+ // src/lib/create-firebase-client-native.ts
3446
+ var import_firestore = require("@react-native-firebase/firestore");
3447
+
1045
3448
  // src/lib/create-firebase-client.ts
1046
3449
  function createFsClientBase({
1047
3450
  db,