@speakableio/core 0.1.11 → 0.1.13

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/dist/index.mjs CHANGED
@@ -753,7 +753,7 @@ var SpeakableFirebaseFunctions = {
753
753
 
754
754
  // src/domains/notification/services/send-notification.service.ts
755
755
  var _sendNotification = async (sendTo, notification) => {
756
- const results = await SpeakableFirebaseFunctions.createNotification({
756
+ const results = await SpeakableFirebaseFunctions.createNotification?.({
757
757
  sendTo,
758
758
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
759
759
  notification
@@ -871,7 +871,7 @@ var createAssessmentScoredNotification = async ({
871
871
  imageUrl: profile.image?.url,
872
872
  senderName: teacherName
873
873
  });
874
- await SpeakableFirebaseFunctions.sendAssessmentScoredEmail({
874
+ await SpeakableFirebaseFunctions.sendAssessmentScoredEmail?.({
875
875
  assessmentTitle: assignment.name,
876
876
  link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
877
877
  senderImage: profile.image?.url || "",
@@ -965,6 +965,1067 @@ function useSpeakableApi() {
965
965
  if (!ctx) throw new Error("useSpeakableApi must be used within a SpeakableProvider");
966
966
  return ctx;
967
967
  }
968
+
969
+ // src/domains/assignment/hooks/score-hooks.ts
970
+ import { useMutation as useMutation2, useQuery as useQuery3 } from "@tanstack/react-query";
971
+
972
+ // src/utils/debounce.utils.ts
973
+ function debounce(func, waitFor) {
974
+ let timeoutId;
975
+ return (...args) => new Promise((resolve, reject) => {
976
+ if (timeoutId) {
977
+ clearTimeout(timeoutId);
978
+ }
979
+ timeoutId = setTimeout(async () => {
980
+ try {
981
+ const result = await func(...args);
982
+ resolve(result);
983
+ } catch (error) {
984
+ reject(error);
985
+ }
986
+ }, waitFor);
987
+ });
988
+ }
989
+
990
+ // src/lib/tanstack/handle-optimistic-update-query.ts
991
+ var handleOptimisticUpdate = async ({
992
+ queryClient,
993
+ queryKey,
994
+ newData
995
+ }) => {
996
+ await queryClient.cancelQueries({
997
+ queryKey
998
+ });
999
+ const previousData = queryClient.getQueryData(queryKey);
1000
+ if (previousData === void 0) {
1001
+ queryClient.setQueryData(queryKey, newData);
1002
+ } else {
1003
+ queryClient.setQueryData(queryKey, { ...previousData, ...newData });
1004
+ }
1005
+ return { previousData };
1006
+ };
1007
+
1008
+ // src/constants/speakable-plans.ts
1009
+ var FEEDBACK_PLANS = {
1010
+ FEEDBACK_TRANSCRIPT: "FEEDBACK_TRANSCRIPT",
1011
+ // Transcript from the audio
1012
+ FEEDBACK_SUMMARY: "FEEDBACK_SUMMARY",
1013
+ // Chatty summary (Free plan)
1014
+ FEEDBACK_GRAMMAR_INSIGHTS: "FEEDBACK_GRAMMAR_INSIGHTS",
1015
+ // Grammar insights
1016
+ FEEDBACK_SUGGESTED_RESPONSE: "FEEDBACK_SUGGESTED_RESPONSE",
1017
+ // Suggested Response
1018
+ FEEDBACK_RUBRIC: "FEEDBACK_RUBRIC",
1019
+ // Suggested Response
1020
+ FEEDBACK_GRADING_STANDARDS: "FEEDBACK_GRADING_STANDARDS",
1021
+ // ACTFL / WIDA Estimate
1022
+ FEEDBACK_TARGET_LANGUAGE: "FEEDBACK_TARGET_LANGUAGE",
1023
+ // Ability to set the feedback language to the target language of the student
1024
+ FEEDBACK_DISABLE_ALLOW_RETRIES: "FEEDBACK_DISABLE_ALLOW_RETRIES"
1025
+ // Turn of allow retries
1026
+ };
1027
+ var AUTO_GRADING_PLANS = {
1028
+ AUTO_GRADING_PASS_FAIL: "AUTO_GRADING_PASS_FAIL",
1029
+ // Pass / fail grading
1030
+ AUTO_GRADING_RUBRICS: "AUTO_GRADING_RUBRICS",
1031
+ // Autograded rubrics
1032
+ AUTO_GRADING_STANDARDS_BASED: "AUTO_GRADING_STANDARDS_BASED"
1033
+ // Standards based grading
1034
+ };
1035
+ var AI_ASSISTANT_PLANS = {
1036
+ AI_ASSISTANT_DOCUMENT_UPLOADS: "AI_ASSISTANT_DOCUMENT_UPLOADS",
1037
+ // Allow document uploading
1038
+ AI_ASSISTANT_UNLIMITED_USE: "AI_ASSISTANT_UNLIMITED_USE"
1039
+ // Allow unlimited use of AI assistant. Otherwise, limits are used.
1040
+ };
1041
+ var ASSIGNMENT_SETTINGS_PLANS = {
1042
+ ASSESSMENTS: "ASSESSMENTS",
1043
+ // Ability to create assessment assignment types
1044
+ GOOGLE_CLASSROOM_GRADE_PASSBACK: "GOOGLE_CLASSROOM_GRADE_PASSBACK"
1045
+ // Assignment scores can sync with classroom
1046
+ };
1047
+ var ANALYTICS_PLANS = {
1048
+ ANALYTICS_GRADEBOOK: "ANALYTICS_GRADEBOOK",
1049
+ // Access to the gradebook page
1050
+ ANALYTICS_CLASSROOM_ANALYTICS: "ANALYTICS_CLASSROOM_ANALYTICS",
1051
+ // Access to the classroom analytics page
1052
+ ANALYTICS_STUDENT_PROGRESS_REPORTS: "ANALYTICS_STUDENT_PROGRESS_REPORTS",
1053
+ // Access to the panel that shows an individual student's progress and assignments
1054
+ ANALYTICS_ASSIGNMENT_RESULTS: "ANALYTICS_ASSIGNMENT_RESULTS",
1055
+ // Access to the assigment RESULTS page
1056
+ ANALYTICS_ORGANIZATION: "ANALYTICS_ORGANIZATION"
1057
+ // Access to the organization analytics panel (for permitted admins)
1058
+ };
1059
+ var SPACES_PLANS = {
1060
+ SPACES_CREATE_SPACE: "SPACES_CREATE_SPACE",
1061
+ // Ability to create spaces
1062
+ SPACES_CHECK_POINTS: "SPACES_CHECK_POINTS"
1063
+ // Feature not available yet. Ability to create checkpoints for spaces for data aggregation
1064
+ };
1065
+ var DISCOVER_PLANS = {
1066
+ DISCOVER_ORGANIZATION_LIBRARY: "DISCOVER_ORGANIZATION_LIBRARY"
1067
+ // Access to the organizations shared library
1068
+ };
1069
+ var MEDIA_AREA_PLANS = {
1070
+ MEDIA_AREA_DOCUMENT_UPLOAD: "MEDIA_AREA_DOCUMENT_UPLOAD",
1071
+ MEDIA_AREA_AUDIO_FILES: "MEDIA_AREA_AUDIO_FILES"
1072
+ };
1073
+ var FREE_PLAN = [];
1074
+ var TEACHER_PRO_PLAN = [
1075
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
1076
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
1077
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
1078
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
1079
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
1080
+ SPACES_PLANS.SPACES_CREATE_SPACE
1081
+ // AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
1082
+ ];
1083
+ var SCHOOL_STARTER = [
1084
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
1085
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
1086
+ FEEDBACK_PLANS.FEEDBACK_GRAMMAR_INSIGHTS,
1087
+ FEEDBACK_PLANS.FEEDBACK_SUGGESTED_RESPONSE,
1088
+ FEEDBACK_PLANS.FEEDBACK_RUBRIC,
1089
+ FEEDBACK_PLANS.FEEDBACK_GRADING_STANDARDS,
1090
+ FEEDBACK_PLANS.FEEDBACK_DISABLE_ALLOW_RETRIES,
1091
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
1092
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
1093
+ AUTO_GRADING_PLANS.AUTO_GRADING_RUBRICS,
1094
+ AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
1095
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_DOCUMENT_UPLOADS,
1096
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_UNLIMITED_USE,
1097
+ // ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS,
1098
+ ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK,
1099
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
1100
+ ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS,
1101
+ ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS,
1102
+ // ANALYTICS_PLANS.ANALYTICS_ORGANIZATION,
1103
+ SPACES_PLANS.SPACES_CREATE_SPACE,
1104
+ SPACES_PLANS.SPACES_CHECK_POINTS,
1105
+ // DISCOVER_PLANS.DISCOVER_ORGANIZATION_LIBRARY,
1106
+ MEDIA_AREA_PLANS.MEDIA_AREA_DOCUMENT_UPLOAD,
1107
+ MEDIA_AREA_PLANS.MEDIA_AREA_AUDIO_FILES
1108
+ ];
1109
+ var ORGANIZATION_PLAN = [
1110
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
1111
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
1112
+ FEEDBACK_PLANS.FEEDBACK_GRAMMAR_INSIGHTS,
1113
+ FEEDBACK_PLANS.FEEDBACK_SUGGESTED_RESPONSE,
1114
+ FEEDBACK_PLANS.FEEDBACK_RUBRIC,
1115
+ FEEDBACK_PLANS.FEEDBACK_GRADING_STANDARDS,
1116
+ FEEDBACK_PLANS.FEEDBACK_DISABLE_ALLOW_RETRIES,
1117
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
1118
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
1119
+ AUTO_GRADING_PLANS.AUTO_GRADING_RUBRICS,
1120
+ AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
1121
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_DOCUMENT_UPLOADS,
1122
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_UNLIMITED_USE,
1123
+ ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS,
1124
+ ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK,
1125
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
1126
+ ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS,
1127
+ ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS,
1128
+ ANALYTICS_PLANS.ANALYTICS_ORGANIZATION,
1129
+ SPACES_PLANS.SPACES_CREATE_SPACE,
1130
+ SPACES_PLANS.SPACES_CHECK_POINTS,
1131
+ DISCOVER_PLANS.DISCOVER_ORGANIZATION_LIBRARY,
1132
+ MEDIA_AREA_PLANS.MEDIA_AREA_DOCUMENT_UPLOAD,
1133
+ MEDIA_AREA_PLANS.MEDIA_AREA_AUDIO_FILES
1134
+ ];
1135
+ var SpeakablePlanTypes = {
1136
+ basic: "basic",
1137
+ teacher_pro: "teacher_pro",
1138
+ school_starter: "school_starter",
1139
+ organization: "organization",
1140
+ // OLD PLANS
1141
+ starter: "starter",
1142
+ growth: "growth",
1143
+ professional: "professional"
1144
+ };
1145
+ var SpeakablePermissionsMap = {
1146
+ [SpeakablePlanTypes.basic]: FREE_PLAN,
1147
+ [SpeakablePlanTypes.starter]: TEACHER_PRO_PLAN,
1148
+ [SpeakablePlanTypes.teacher_pro]: TEACHER_PRO_PLAN,
1149
+ [SpeakablePlanTypes.growth]: ORGANIZATION_PLAN,
1150
+ [SpeakablePlanTypes.professional]: ORGANIZATION_PLAN,
1151
+ [SpeakablePlanTypes.organization]: ORGANIZATION_PLAN,
1152
+ [SpeakablePlanTypes.school_starter]: SCHOOL_STARTER
1153
+ };
1154
+ var SpeakablePlanHierarchy = [
1155
+ SpeakablePlanTypes.basic,
1156
+ SpeakablePlanTypes.starter,
1157
+ SpeakablePlanTypes.teacher_pro,
1158
+ SpeakablePlanTypes.growth,
1159
+ SpeakablePlanTypes.professional,
1160
+ SpeakablePlanTypes.school_starter,
1161
+ SpeakablePlanTypes.organization
1162
+ ];
1163
+
1164
+ // src/hooks/usePermissions.ts
1165
+ var usePermissions = () => {
1166
+ const { permissions } = useSpeakableApi();
1167
+ const has = (permission) => permissions.permissions?.includes(permission);
1168
+ return {
1169
+ plan: permissions.plan,
1170
+ permissionsLoaded: permissions.loaded,
1171
+ isStripePlan: permissions.isStripePlan,
1172
+ refreshDate: permissions.refreshDate,
1173
+ isInstitutionPlan: permissions.isInstitutionPlan,
1174
+ subscriptionId: permissions.subscriptionId,
1175
+ contact: permissions.contact,
1176
+ hasGradebook: has(ANALYTICS_PLANS.ANALYTICS_GRADEBOOK),
1177
+ hasGoogleClassroomGradePassback: has(ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK),
1178
+ hasAssessments: has(ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS),
1179
+ hasSectionAnalytics: has(ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS),
1180
+ hasStudentReports: has(ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS),
1181
+ permissions: permissions || [],
1182
+ hasStudentPortfolios: permissions.hasStudentPortfolios,
1183
+ isFreeOrgTrial: permissions.type === "free_org_trial",
1184
+ freeOrgTrialExpired: permissions.freeOrgTrialExpired
1185
+ };
1186
+ };
1187
+ var usePermissions_default = usePermissions;
1188
+
1189
+ // src/hooks/useGoogleClassroom.ts
1190
+ var useGoogleClassroom = () => {
1191
+ const submitAssignmentToGoogleClassroom = async ({
1192
+ assignment,
1193
+ scores,
1194
+ googleUserId = null
1195
+ // optional to override the user's googleUserId
1196
+ }) => {
1197
+ try {
1198
+ const { googleClassroomUserId = null } = scores;
1199
+ const googleId = googleUserId || googleClassroomUserId;
1200
+ if (!googleId)
1201
+ return {
1202
+ error: true,
1203
+ message: "No Google Classroom ID found"
1204
+ };
1205
+ const { courseWorkId, maxPoints, owners, courseId } = assignment;
1206
+ const draftGrade = scores?.score ? scores?.score / 100 * maxPoints : 0;
1207
+ const result = await SpeakableFirebaseFunctions.submitAssignmentToGoogleClassroomV2?.({
1208
+ teacherId: owners[0],
1209
+ courseId,
1210
+ courseWorkId,
1211
+ userId: googleId,
1212
+ draftGrade
1213
+ });
1214
+ return result;
1215
+ } catch (error) {
1216
+ return { error: true, message: error.message };
1217
+ }
1218
+ };
1219
+ return {
1220
+ submitAssignmentToGoogleClassroom
1221
+ };
1222
+ };
1223
+
1224
+ // src/domains/assignment/utils/create-default-score.ts
1225
+ var defaultScore = (props) => {
1226
+ const { serverTimestamp } = api.accessHelpers();
1227
+ const score = {
1228
+ progress: 0,
1229
+ score: 0,
1230
+ startDate: serverTimestamp(),
1231
+ status: "IN_PROGRESS",
1232
+ submitted: false,
1233
+ cards: {},
1234
+ lastPlayed: serverTimestamp(),
1235
+ owners: props.owners,
1236
+ userId: props.userId
1237
+ };
1238
+ if (props.googleClassroomUserId) {
1239
+ score.googleClassroomUserId = props.googleClassroomUserId;
1240
+ }
1241
+ if (props.courseId) {
1242
+ score.courseId = props.courseId;
1243
+ }
1244
+ return score;
1245
+ };
1246
+
1247
+ // src/domains/assignment/score-practice.constants.ts
1248
+ var SCORES_PRACTICE_COLLECTION = "users";
1249
+ var SCORES_PRACTICE_SUBCOLLECTION = "practice";
1250
+ var refsScoresPractice = {
1251
+ practiceScores: (params) => `${SCORES_PRACTICE_COLLECTION}/${params.userId}/${SCORES_PRACTICE_SUBCOLLECTION}/${params.setId}`,
1252
+ practiceScoreHistoryRefDoc: (params) => `${SCORES_PRACTICE_COLLECTION}/${params.userId}/${SCORES_PRACTICE_SUBCOLLECTION}/${params.setId}/attempts/${params.date}`
1253
+ };
1254
+
1255
+ // src/domains/assignment/services/create-score.service.ts
1256
+ async function _createScore(params) {
1257
+ if (params.isAssignment) {
1258
+ const ref = refsAssignmentFiresotre.assignmentScores({
1259
+ id: params.activityId,
1260
+ userId: params.userId
1261
+ });
1262
+ await SpeakableFirebaseFunctions.updateAssignmentGradebookStatus?.({
1263
+ assignmentId: params.activityId,
1264
+ userId: params.userId,
1265
+ status: "IN_PROGRESS",
1266
+ score: null
1267
+ });
1268
+ await api.setDoc(ref, params.scoreData, { merge: true });
1269
+ return {
1270
+ id: params.userId
1271
+ };
1272
+ } else {
1273
+ const ref = refsScoresPractice.practiceScores({
1274
+ userId: params.userId,
1275
+ setId: params.activityId
1276
+ });
1277
+ await api.setDoc(ref, params.scoreData);
1278
+ return {
1279
+ id: params.userId
1280
+ };
1281
+ }
1282
+ }
1283
+ var createScore = withErrorHandler(_createScore, "createScore");
1284
+
1285
+ // src/domains/assignment/services/get-score.service.ts
1286
+ async function getAssignmentScore({
1287
+ userId,
1288
+ assignment,
1289
+ googleClassroomUserId
1290
+ }) {
1291
+ const path = refsAssignmentFiresotre.assignmentScores({
1292
+ id: assignment.id,
1293
+ userId
1294
+ });
1295
+ const response = await api.getDoc(path);
1296
+ if (response.data == null) {
1297
+ const newScore = {
1298
+ ...defaultScore({
1299
+ owners: [userId],
1300
+ userId,
1301
+ courseId: assignment.courseId,
1302
+ googleClassroomUserId
1303
+ }),
1304
+ assignmentId: assignment.id
1305
+ };
1306
+ const result = await createScore({
1307
+ activityId: assignment.id,
1308
+ userId,
1309
+ isAssignment: true,
1310
+ scoreData: newScore
1311
+ });
1312
+ return {
1313
+ ...newScore,
1314
+ id: result.id
1315
+ };
1316
+ }
1317
+ return response.data;
1318
+ }
1319
+ async function getPracticeScore({ userId, setId }) {
1320
+ const path = refsScoresPractice.practiceScores({ userId, setId });
1321
+ const response = await api.getDoc(path);
1322
+ if (response.data == null) {
1323
+ const newScore = {
1324
+ ...defaultScore({
1325
+ owners: [userId],
1326
+ userId
1327
+ }),
1328
+ setId
1329
+ };
1330
+ const result = await createScore({
1331
+ activityId: setId,
1332
+ userId,
1333
+ isAssignment: false,
1334
+ scoreData: newScore
1335
+ });
1336
+ return {
1337
+ ...newScore,
1338
+ id: result.id
1339
+ };
1340
+ }
1341
+ return response.data;
1342
+ }
1343
+ async function _getScore(params) {
1344
+ if (params.isAssignment) {
1345
+ return await getAssignmentScore({
1346
+ userId: params.userId,
1347
+ assignment: {
1348
+ id: params.activityId,
1349
+ courseId: params.courseId
1350
+ },
1351
+ googleClassroomUserId: params.googleClassroomUserId
1352
+ });
1353
+ } else {
1354
+ return await getPracticeScore({
1355
+ userId: params.userId,
1356
+ setId: params.activityId
1357
+ });
1358
+ }
1359
+ }
1360
+ var getScore = withErrorHandler(_getScore, "getScore");
1361
+
1362
+ // src/domains/assignment/utils/calculateScoreAndProgress.ts
1363
+ var calculateScoreAndProgress = (scores, cardsList, weights) => {
1364
+ const totalSetPoints = cardsList.reduce((acc, cardId) => {
1365
+ acc += weights?.[cardId] || 1;
1366
+ return acc;
1367
+ }, 0);
1368
+ const totalPointsAwarded = Object.keys(scores?.cards || {}).reduce((acc, cardId) => {
1369
+ const cardScores = scores?.cards?.[cardId];
1370
+ if (cardScores?.completed || cardScores?.score || cardScores?.score === 0) {
1371
+ const score2 = cardScores?.score || cardScores?.score === 0 ? Number(cardScores?.score ?? 0) : null;
1372
+ const weight = weights?.[cardId] || 1;
1373
+ const fraction = (score2 ?? 0) / 100;
1374
+ if (score2 || score2 === 0) {
1375
+ acc += weight * fraction;
1376
+ } else {
1377
+ acc += weight;
1378
+ }
1379
+ }
1380
+ return acc;
1381
+ }, 0);
1382
+ const totalCompletedCards = Object.keys(scores?.cards || {}).reduce((acc, cardId) => {
1383
+ const cardScores = scores?.cards?.[cardId];
1384
+ if (cardScores?.completed || cardScores?.score || cardScores?.score === 0) {
1385
+ acc += 1;
1386
+ }
1387
+ return acc;
1388
+ }, 0);
1389
+ const percent = totalPointsAwarded / totalSetPoints;
1390
+ const score = Math.round(percent * 100);
1391
+ const progress = Math.round(totalCompletedCards / (cardsList.length || 1) * 100);
1392
+ return { score, progress };
1393
+ };
1394
+ var calculateScoreAndProgress_default = calculateScoreAndProgress;
1395
+
1396
+ // src/domains/assignment/services/update-score.service.ts
1397
+ async function _updateScore(params) {
1398
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1399
+ id: params.activityId,
1400
+ userId: params.userId
1401
+ }) : refsScoresPractice.practiceScores({
1402
+ setId: params.activityId,
1403
+ userId: params.userId
1404
+ });
1405
+ await api.updateDoc(path, {
1406
+ ...params.data
1407
+ });
1408
+ }
1409
+ var updateScore = withErrorHandler(_updateScore, "updateScore");
1410
+ async function _updateCardScore(params) {
1411
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1412
+ id: params.activityId,
1413
+ userId: params.userId
1414
+ }) : refsScoresPractice.practiceScores({
1415
+ setId: params.activityId,
1416
+ userId: params.userId
1417
+ });
1418
+ const updates = Object.keys(params.updates.cardScore).reduce(
1419
+ (acc, key) => {
1420
+ acc[`cards.${params.cardId}.${key}`] = params.updates.cardScore[key];
1421
+ return acc;
1422
+ },
1423
+ {}
1424
+ );
1425
+ if (params.updates.progress) {
1426
+ updates.progress = params.updates.progress;
1427
+ }
1428
+ if (params.updates.score) {
1429
+ updates.score = params.updates.score;
1430
+ }
1431
+ await api.updateDoc(path, {
1432
+ ...updates
1433
+ });
1434
+ }
1435
+ var updateCardScore = withErrorHandler(_updateCardScore, "updateCardScore");
1436
+
1437
+ // src/domains/assignment/services/clear-score.service.ts
1438
+ import dayjs3 from "dayjs";
1439
+ async function clearScore(params) {
1440
+ const update = {
1441
+ [`cards.${params.cardId}`]: {
1442
+ attempts: params.cardScores.attempts ?? 1,
1443
+ correct: params.cardScores.correct ?? 0,
1444
+ // save old score history
1445
+ history: [
1446
+ {
1447
+ ...params.cardScores,
1448
+ attempts: params.cardScores.attempts ?? 1,
1449
+ correct: params.cardScores.correct ?? 0,
1450
+ retryTime: dayjs3().format("YYYY-MM-DD HH:mm:ss"),
1451
+ history: null
1452
+ },
1453
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1454
+ ...params.cardScores.history ?? []
1455
+ ]
1456
+ }
1457
+ };
1458
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1459
+ id: params.activityId,
1460
+ userId: params.userId
1461
+ }) : refsScoresPractice.practiceScores({
1462
+ setId: params.activityId,
1463
+ userId: params.userId
1464
+ });
1465
+ await api.updateDoc(path, update);
1466
+ return {
1467
+ update,
1468
+ activityId: params.activityId
1469
+ };
1470
+ }
1471
+
1472
+ // src/domains/assignment/services/submit-assignment-score.service.ts
1473
+ import dayjs4 from "dayjs";
1474
+ async function submitAssignmentScore({
1475
+ cardIds,
1476
+ assignment,
1477
+ setWeights,
1478
+ userId,
1479
+ status,
1480
+ studentName
1481
+ }) {
1482
+ const { serverTimestamp } = api.accessHelpers();
1483
+ const path = refsAssignmentFiresotre.assignmentScores({ id: assignment.id, userId });
1484
+ const fieldsUpdated = {
1485
+ submitted: true,
1486
+ progress: 100,
1487
+ submissionDate: serverTimestamp(),
1488
+ status
1489
+ };
1490
+ if (assignment.isAssessment) {
1491
+ const result = await handleAssessment(
1492
+ assignment,
1493
+ userId,
1494
+ cardIds,
1495
+ setWeights,
1496
+ fieldsUpdated,
1497
+ studentName
1498
+ );
1499
+ return result;
1500
+ } else if (assignment.courseId) {
1501
+ await handleCourseAssignment(assignment, userId);
1502
+ }
1503
+ await api.updateDoc(path, { ...fieldsUpdated });
1504
+ return { success: true, fieldsUpdated };
1505
+ }
1506
+ async function handleAssessment(assignment, userId, cardIds, setWeights, fieldsUpdated, studentName) {
1507
+ const path = refsAssignmentFiresotre.assignmentScores({ id: assignment.id, userId });
1508
+ const response = await api.getDoc(path);
1509
+ if (!response.data) {
1510
+ throw new Error("Score not found");
1511
+ }
1512
+ const { score: scoreCalculated } = calculateScoreAndProgress_default(response.data, cardIds, setWeights);
1513
+ await api.updateDoc(path, { score: scoreCalculated, status: "PENDING_REVIEW" });
1514
+ await SpeakableFirebaseFunctions.submitAssessment?.({
1515
+ assignmentId: assignment.id,
1516
+ assignmentTitle: assignment.name,
1517
+ userId,
1518
+ teacherId: assignment.owners[0],
1519
+ studentName
1520
+ });
1521
+ fieldsUpdated.status = "PENDING_REVIEW";
1522
+ return { success: true, fieldsUpdated };
1523
+ }
1524
+ async function handleCourseAssignment(assignment, userId) {
1525
+ const { data } = await SpeakableFirebaseFunctions?.submitAssignmentV2?.({
1526
+ assignmentId: assignment.id,
1527
+ userId
1528
+ });
1529
+ }
1530
+ async function submitPracticeScore({
1531
+ setId,
1532
+ userId,
1533
+ scores
1534
+ }) {
1535
+ const { serverTimestamp } = api.accessHelpers();
1536
+ const date = dayjs4().format("YYYY-MM-DD-HH-mm");
1537
+ const ref = refsScoresPractice.practiceScoreHistoryRefDoc({ setId, userId, date });
1538
+ const fieldsUpdated = {
1539
+ ...scores,
1540
+ submitted: true,
1541
+ progress: 100,
1542
+ submissionDate: serverTimestamp(),
1543
+ status: "SUBMITTED"
1544
+ };
1545
+ await api.setDoc(ref, { ...fieldsUpdated });
1546
+ const refScores = refsScoresPractice.practiceScores({ userId, setId });
1547
+ await api.deleteDoc(refScores);
1548
+ return { success: true, fieldsUpdated };
1549
+ }
1550
+
1551
+ // src/domains/assignment/hooks/score-hooks.ts
1552
+ var scoreQueryKeys = {
1553
+ all: ["scores"],
1554
+ byId: (id) => [...scoreQueryKeys.all, id],
1555
+ list: () => [...scoreQueryKeys.all, "list"]
1556
+ };
1557
+ function useScore({
1558
+ isAssignment,
1559
+ activityId,
1560
+ userId = "",
1561
+ courseId,
1562
+ enabled = true,
1563
+ googleClassroomUserId
1564
+ }) {
1565
+ return useQuery3({
1566
+ queryFn: () => getScore({
1567
+ userId,
1568
+ courseId,
1569
+ activityId,
1570
+ googleClassroomUserId,
1571
+ isAssignment
1572
+ }),
1573
+ queryKey: scoreQueryKeys.byId(activityId),
1574
+ enabled
1575
+ });
1576
+ }
1577
+ var debounceUpdateScore = debounce(updateScore, 1e3);
1578
+ function useUpdateScore() {
1579
+ const { queryClient } = useSpeakableApi();
1580
+ const mutation = useMutation2({
1581
+ mutationFn: debounceUpdateScore,
1582
+ onMutate: (variables) => {
1583
+ return handleOptimisticUpdate({
1584
+ queryClient,
1585
+ queryKey: scoreQueryKeys.byId(variables.activityId),
1586
+ newData: variables.data
1587
+ });
1588
+ },
1589
+ onError: (_, variables, context) => {
1590
+ if (context?.previousData)
1591
+ queryClient.setQueryData(scoreQueryKeys.byId(variables.activityId), context.previousData);
1592
+ },
1593
+ onSettled: (_, err, variables) => {
1594
+ queryClient.invalidateQueries({
1595
+ queryKey: scoreQueryKeys.byId(variables.activityId)
1596
+ });
1597
+ }
1598
+ });
1599
+ return {
1600
+ mutationUpdateScore: mutation
1601
+ };
1602
+ }
1603
+ function useUpdateCardScore({
1604
+ isAssignment,
1605
+ activityId,
1606
+ userId
1607
+ }) {
1608
+ const { queryClient } = useSpeakableApi();
1609
+ const querySet = useSet({ setId: activityId });
1610
+ const dataCurrentSet = querySet.data;
1611
+ const queryKey = scoreQueryKeys.byId(activityId);
1612
+ const mutation = useMutation2({
1613
+ mutationFn: async ({ cardId, cardScore }) => {
1614
+ const previousScores = queryClient.getQueryData(queryKey);
1615
+ const { progress, score, newScoreUpdated, updatedCardScore } = getScoreUpdated({
1616
+ previousScores: previousScores ?? {},
1617
+ cardId,
1618
+ set: dataCurrentSet ?? void 0,
1619
+ cardScore
1620
+ });
1621
+ await updateCardScore({
1622
+ userId,
1623
+ cardId,
1624
+ isAssignment,
1625
+ activityId,
1626
+ updates: {
1627
+ cardScore: updatedCardScore,
1628
+ progress,
1629
+ score
1630
+ }
1631
+ });
1632
+ return { cardId, scoresUpdated: newScoreUpdated };
1633
+ },
1634
+ onMutate: ({ cardId, cardScore }) => {
1635
+ queryClient.setQueryData(queryKey, (previousScore) => {
1636
+ const updates = handleOptimisticScore({
1637
+ score: previousScore,
1638
+ cardId,
1639
+ cardScore,
1640
+ set: dataCurrentSet ?? void 0
1641
+ });
1642
+ return {
1643
+ ...previousScore,
1644
+ ...updates
1645
+ };
1646
+ });
1647
+ },
1648
+ // eslint-disable-next-line no-unused-vars
1649
+ onError: () => {
1650
+ const previousData = queryClient.getQueryData(queryKey);
1651
+ if (previousData) {
1652
+ queryClient.setQueryData(queryKey, previousData);
1653
+ }
1654
+ }
1655
+ });
1656
+ return {
1657
+ mutationUpdateCardScore: mutation
1658
+ };
1659
+ }
1660
+ var getScoreUpdated = ({
1661
+ cardId,
1662
+ cardScore,
1663
+ previousScores,
1664
+ set
1665
+ }) => {
1666
+ const previousCard = previousScores.cards?.[cardId];
1667
+ const newCardScore = {
1668
+ ...previousCard ?? {},
1669
+ ...cardScore
1670
+ };
1671
+ const newScores = {
1672
+ ...previousScores,
1673
+ cards: {
1674
+ ...previousScores.cards ?? {},
1675
+ [cardId]: newCardScore
1676
+ }
1677
+ };
1678
+ const { score, progress } = calculateScoreAndProgress_default(
1679
+ newScores,
1680
+ set?.content ?? [],
1681
+ set?.weights ?? {}
1682
+ );
1683
+ return {
1684
+ newScoreUpdated: newScores,
1685
+ updatedCardScore: cardScore,
1686
+ score,
1687
+ progress
1688
+ };
1689
+ };
1690
+ var handleOptimisticScore = ({
1691
+ score,
1692
+ cardId,
1693
+ cardScore,
1694
+ set
1695
+ }) => {
1696
+ let cards = { ...score?.cards ?? {} };
1697
+ cards = {
1698
+ ...cards,
1699
+ [cardId]: {
1700
+ ...cards[cardId],
1701
+ ...cardScore
1702
+ }
1703
+ };
1704
+ const { score: scoreValue, progress } = calculateScoreAndProgress_default(
1705
+ // @ts-ignore
1706
+ {
1707
+ ...score ?? {},
1708
+ cards
1709
+ },
1710
+ set?.content ?? [],
1711
+ set?.weights ?? {}
1712
+ );
1713
+ return { cards, score: scoreValue, progress };
1714
+ };
1715
+ function useClearScore() {
1716
+ const { queryClient } = useSpeakableApi();
1717
+ const mutation = useMutation2({
1718
+ mutationFn: clearScore,
1719
+ onSettled: (result) => {
1720
+ queryClient.invalidateQueries({
1721
+ queryKey: scoreQueryKeys.byId(result?.activityId ?? "")
1722
+ });
1723
+ }
1724
+ });
1725
+ return {
1726
+ mutationClearScore: mutation
1727
+ };
1728
+ }
1729
+ function useSubmitAssignmentScore({
1730
+ onAssignmentSubmitted,
1731
+ studentName
1732
+ }) {
1733
+ const { queryClient } = useSpeakableApi();
1734
+ const { hasGoogleClassroomGradePassback } = usePermissions_default();
1735
+ const { submitAssignmentToGoogleClassroom } = useGoogleClassroom();
1736
+ const { createNotification: createNotification2 } = useCreateNotification();
1737
+ const mutation = useMutation2({
1738
+ mutationFn: async ({
1739
+ assignment,
1740
+ userId,
1741
+ cardIds,
1742
+ setWeights,
1743
+ scores,
1744
+ status
1745
+ }) => {
1746
+ try {
1747
+ const scoreUpdated = await submitAssignmentScore({
1748
+ assignment,
1749
+ userId,
1750
+ cardIds,
1751
+ setWeights,
1752
+ status,
1753
+ studentName
1754
+ });
1755
+ if (assignment.courseWorkId != null && !assignment.isAssessment && hasGoogleClassroomGradePassback) {
1756
+ await submitAssignmentToGoogleClassroom({
1757
+ assignment,
1758
+ scores
1759
+ });
1760
+ }
1761
+ if (assignment.isAssessment) {
1762
+ createNotification2(SpeakableNotificationTypes.ASSESSMENT_SUBMITTED, assignment);
1763
+ }
1764
+ onAssignmentSubmitted(assignment.id);
1765
+ queryClient.setQueryData(scoreQueryKeys.byId(assignment.id), {
1766
+ ...scores,
1767
+ ...scoreUpdated.fieldsUpdated
1768
+ });
1769
+ return {
1770
+ success: true,
1771
+ message: "Score submitted successfully"
1772
+ };
1773
+ } catch (error) {
1774
+ return {
1775
+ success: false,
1776
+ error
1777
+ };
1778
+ }
1779
+ }
1780
+ });
1781
+ return {
1782
+ submitAssignmentScore: mutation.mutateAsync,
1783
+ isLoading: mutation.isPending
1784
+ };
1785
+ }
1786
+ function useSubmitPracticeScore() {
1787
+ const { queryClient } = useSpeakableApi();
1788
+ const mutation = useMutation2({
1789
+ mutationFn: async ({
1790
+ setId,
1791
+ userId,
1792
+ scores
1793
+ }) => {
1794
+ try {
1795
+ await submitPracticeScore({
1796
+ setId,
1797
+ userId,
1798
+ scores
1799
+ });
1800
+ queryClient.invalidateQueries({
1801
+ queryKey: scoreQueryKeys.byId(setId)
1802
+ });
1803
+ return {
1804
+ success: true,
1805
+ message: "Score submitted successfully"
1806
+ };
1807
+ } catch (error) {
1808
+ return {
1809
+ success: false,
1810
+ error
1811
+ };
1812
+ }
1813
+ }
1814
+ });
1815
+ return {
1816
+ submitPracticeScore: mutation.mutateAsync,
1817
+ isLoading: mutation.isPending
1818
+ };
1819
+ }
1820
+
1821
+ // src/hooks/useActivity.ts
1822
+ import { useEffect as useEffect2 } from "react";
1823
+
1824
+ // src/hooks/useActivityTracker.ts
1825
+ import { v4 as v42 } from "uuid";
1826
+ function useActivityTracker({ userId }) {
1827
+ const trackActivity = async ({
1828
+ activityName,
1829
+ activityType,
1830
+ id = v42(),
1831
+ language = ""
1832
+ }) => {
1833
+ if (userId) {
1834
+ const { doc, serverTimestamp, setDoc } = api.accessHelpers();
1835
+ const activityRef = doc(`users/${userId}/activity${id}`);
1836
+ const timestamp = serverTimestamp();
1837
+ await setDoc(activityRef, {
1838
+ name: activityName,
1839
+ type: activityType,
1840
+ lastSeen: timestamp,
1841
+ id,
1842
+ language
1843
+ });
1844
+ }
1845
+ };
1846
+ return {
1847
+ trackActivity
1848
+ };
1849
+ }
1850
+
1851
+ // src/hooks/useActivity.ts
1852
+ function useActivity({
1853
+ id,
1854
+ isAssignment,
1855
+ onAssignmentSubmitted
1856
+ }) {
1857
+ const { queryClient, user } = useSpeakableApi();
1858
+ const userId = user.auth.uid;
1859
+ const assignmentQuery = useAssignment({
1860
+ assignmentId: id,
1861
+ userId,
1862
+ enabled: isAssignment
1863
+ });
1864
+ const activeAssignment = assignmentQuery.data;
1865
+ const activityId = isAssignment ? activeAssignment?.setId ?? "" : id;
1866
+ const querySet = useSet({ setId: activityId ?? "" });
1867
+ const setData = querySet.data;
1868
+ const { cardsObject, cardsQueries } = useCards({
1869
+ cardIds: setData?.content ?? [],
1870
+ enabled: querySet.isSuccess,
1871
+ asObject: true
1872
+ });
1873
+ const scoreQuery = useScore({
1874
+ isAssignment,
1875
+ activityId: id,
1876
+ userId,
1877
+ courseId: activeAssignment?.courseId,
1878
+ googleClassroomUserId: "",
1879
+ // TODO
1880
+ enabled: isAssignment ? assignmentQuery.isSuccess : querySet.isSuccess
1881
+ });
1882
+ const { mutationUpdateScore } = useUpdateScore();
1883
+ const { mutationUpdateCardScore } = useUpdateCardScore({
1884
+ activityId,
1885
+ isAssignment,
1886
+ userId
1887
+ });
1888
+ const { mutationClearScore } = useClearScore();
1889
+ const { submitAssignmentScore: submitAssignmentScore2 } = useSubmitAssignmentScore({
1890
+ onAssignmentSubmitted,
1891
+ studentName: user.profile.displayName
1892
+ });
1893
+ const { submitPracticeScore: submitPracticeScore2 } = useSubmitPracticeScore();
1894
+ const handleUpdateScore = (data) => {
1895
+ mutationUpdateScore.mutate({
1896
+ data,
1897
+ isAssignment,
1898
+ activityId,
1899
+ userId
1900
+ });
1901
+ };
1902
+ const handleUpdateCardScore = (cardId, cardScore) => {
1903
+ mutationUpdateCardScore.mutate({ cardId, cardScore });
1904
+ };
1905
+ const onClearScore = ({
1906
+ cardId,
1907
+ wasCompleted = true
1908
+ }) => {
1909
+ const currentCard = cardsObject?.[cardId];
1910
+ if (currentCard?.type === "MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */ || currentCard?.type === "READ_REPEAT" /* READ_REPEAT */) {
1911
+ return;
1912
+ }
1913
+ const queryKeys = scoreQueryKeys.byId(activityId);
1914
+ const activeCardScores = queryClient.getQueryData(queryKeys)?.cards?.[cardId];
1915
+ if (activeCardScores === void 0) return;
1916
+ mutationClearScore.mutate({
1917
+ isAssignment,
1918
+ activityId,
1919
+ cardScores: activeCardScores,
1920
+ cardId,
1921
+ userId
1922
+ });
1923
+ };
1924
+ const onSubmitScore = async () => {
1925
+ try {
1926
+ let results;
1927
+ if (isAssignment) {
1928
+ const cardScores = scoreQuery.data?.cards || {};
1929
+ const hasPendingReview = Object.values(cardScores).some(
1930
+ (cardScore) => cardScore.status === "pending_review"
1931
+ );
1932
+ results = await submitAssignmentScore2({
1933
+ assignment: assignmentQuery.data,
1934
+ userId,
1935
+ cardIds: setData?.content ?? [],
1936
+ scores: scoreQuery.data,
1937
+ setWeights: setData?.weights ?? {},
1938
+ status: hasPendingReview ? "PENDING_REVIEW" : "FINALIZED"
1939
+ });
1940
+ } else {
1941
+ results = await submitPracticeScore2({
1942
+ setId: querySet.data?.id ?? "",
1943
+ userId,
1944
+ scores: scoreQuery.data
1945
+ });
1946
+ }
1947
+ return results;
1948
+ } catch (error) {
1949
+ return {
1950
+ success: false,
1951
+ error
1952
+ };
1953
+ }
1954
+ };
1955
+ useInitActivity({
1956
+ assignment: activeAssignment ?? void 0,
1957
+ set: setData ?? void 0,
1958
+ enabled: !!setData,
1959
+ userId
1960
+ });
1961
+ return {
1962
+ set: {
1963
+ data: setData,
1964
+ query: querySet
1965
+ },
1966
+ cards: {
1967
+ data: cardsObject,
1968
+ query: cardsQueries
1969
+ // activeCardData,
1970
+ },
1971
+ assignment: {
1972
+ data: isAssignment ? activeAssignment : void 0,
1973
+ query: assignmentQuery
1974
+ },
1975
+ scores: {
1976
+ data: scoreQuery.data,
1977
+ query: scoreQuery,
1978
+ // activeCardScores,
1979
+ actions: {
1980
+ update: handleUpdateScore,
1981
+ clear: onClearScore,
1982
+ submit: onSubmitScore,
1983
+ updateCard: handleUpdateCardScore,
1984
+ logGradingStandardEntry: () => {
1985
+ }
1986
+ }
1987
+ }
1988
+ };
1989
+ }
1990
+ var useInitActivity = ({
1991
+ assignment,
1992
+ set,
1993
+ enabled,
1994
+ userId
1995
+ }) => {
1996
+ const { trackActivity } = useActivityTracker({ userId });
1997
+ const init = () => {
1998
+ if (!enabled) return;
1999
+ if (!assignment) {
2000
+ trackActivity({
2001
+ activityName: set?.name ?? "",
2002
+ activityType: "set",
2003
+ id: set?.id,
2004
+ language: set?.language
2005
+ });
2006
+ } else if (assignment.name) {
2007
+ trackActivity({
2008
+ activityName: assignment.name,
2009
+ activityType: assignment.isAssessment ? "assessment" : "assignment",
2010
+ id: assignment.id,
2011
+ language: set?.language
2012
+ });
2013
+ }
2014
+ if (set?.public) {
2015
+ SpeakableFirebaseFunctions?.onSetOpened?.({
2016
+ setId: set.id,
2017
+ language: set.language
2018
+ });
2019
+ }
2020
+ SpeakableFirebaseFunctions?.updateAlgoliaIndex?.({
2021
+ updatePlays: true,
2022
+ objectID: set?.id
2023
+ });
2024
+ };
2025
+ useEffect2(() => {
2026
+ init();
2027
+ }, [set]);
2028
+ };
968
2029
  export {
969
2030
  ALLOWED_CARD_ACTIVITY_TYPES_FOR_SUMMARY,
970
2031
  BASE_MULTIPLE_CHOICE_FIELD_VALUES,
@@ -998,6 +2059,7 @@ export {
998
2059
  setsQueryKeys,
999
2060
  updateCardInCache,
1000
2061
  updateSetInCache,
2062
+ useActivity,
1001
2063
  useAssignment,
1002
2064
  useCards,
1003
2065
  useCreateCard,