@speakableio/core 0.1.103 → 0.1.105

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/hooks.cjs ADDED
@@ -0,0 +1,1604 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/entry-points/hooks.ts
31
+ var hooks_exports = {};
32
+ __export(hooks_exports, {
33
+ assignmentQueryKeys: () => assignmentQueryKeys,
34
+ cardsQueryKeys: () => cardsQueryKeys,
35
+ getCardFromCache: () => getCardFromCache,
36
+ getSetFromCache: () => getSetFromCache,
37
+ scoreQueryKeys: () => scoreQueryKeys,
38
+ setsQueryKeys: () => setsQueryKeys,
39
+ updateCardInCache: () => updateCardInCache,
40
+ updateSetInCache: () => updateSetInCache,
41
+ useAssignment: () => useAssignment,
42
+ useCards: () => useCards,
43
+ useClearScore: () => useClearScore,
44
+ useCreateCard: () => useCreateCard,
45
+ useCreateCards: () => useCreateCards,
46
+ useCreateNotification: () => useCreateNotification,
47
+ useGetCard: () => useGetCard,
48
+ useScore: () => useScore,
49
+ useSet: () => useSet,
50
+ useSubmitAssignmentScore: () => useSubmitAssignmentScore,
51
+ useSubmitPracticeScore: () => useSubmitPracticeScore,
52
+ useUpdateCardScore: () => useUpdateCardScore,
53
+ useUpdateScore: () => useUpdateScore
54
+ });
55
+ module.exports = __toCommonJS(hooks_exports);
56
+
57
+ // src/providers/SpeakableProvider.tsx
58
+ var import_react = require("react");
59
+ var import_jsx_runtime = require("react/jsx-runtime");
60
+ var FsCtx = (0, import_react.createContext)(null);
61
+ function useSpeakableApi() {
62
+ const ctx = (0, import_react.useContext)(FsCtx);
63
+ if (!ctx) throw new Error("useSpeakableApi must be used within a SpeakableProvider");
64
+ return ctx;
65
+ }
66
+
67
+ // src/domains/assignment/hooks/assignment.hooks.ts
68
+ var import_react_query = require("@tanstack/react-query");
69
+ var assignmentQueryKeys = {
70
+ all: ["assignments"],
71
+ byId: (id) => [...assignmentQueryKeys.all, id],
72
+ list: () => [...assignmentQueryKeys.all, "list"]
73
+ };
74
+ function useAssignment({
75
+ assignmentId,
76
+ enabled = true,
77
+ analyticType,
78
+ userId
79
+ }) {
80
+ const { speakableApi } = useSpeakableApi();
81
+ return (0, import_react_query.useQuery)({
82
+ queryKey: assignmentQueryKeys.byId(assignmentId),
83
+ queryFn: () => speakableApi.assignmentRepo.getAssignment({
84
+ assignmentId,
85
+ analyticType,
86
+ currentUserId: userId
87
+ }),
88
+ enabled
89
+ });
90
+ }
91
+
92
+ // src/domains/assignment/hooks/score-hooks.ts
93
+ var import_react_query2 = require("@tanstack/react-query");
94
+
95
+ // src/utils/debounce.utils.ts
96
+ function debounce(func, waitFor) {
97
+ let timeoutId;
98
+ return (...args) => new Promise((resolve, reject) => {
99
+ if (timeoutId) {
100
+ clearTimeout(timeoutId);
101
+ }
102
+ timeoutId = setTimeout(async () => {
103
+ try {
104
+ const result = await func(...args);
105
+ resolve(result);
106
+ } catch (error) {
107
+ reject(error);
108
+ }
109
+ }, waitFor);
110
+ });
111
+ }
112
+
113
+ // src/lib/tanstack/handle-optimistic-update-query.ts
114
+ var handleOptimisticUpdate = async ({
115
+ queryClient,
116
+ queryKey,
117
+ newData
118
+ }) => {
119
+ await queryClient.cancelQueries({
120
+ queryKey
121
+ });
122
+ const previousData = queryClient.getQueryData(queryKey);
123
+ if (previousData === void 0) {
124
+ queryClient.setQueryData(queryKey, newData);
125
+ } else {
126
+ queryClient.setQueryData(queryKey, { ...previousData, ...newData });
127
+ }
128
+ return { previousData };
129
+ };
130
+
131
+ // src/constants/speakable-plans.ts
132
+ var FEEDBACK_PLANS = {
133
+ FEEDBACK_TRANSCRIPT: "FEEDBACK_TRANSCRIPT",
134
+ // Transcript from the audio
135
+ FEEDBACK_SUMMARY: "FEEDBACK_SUMMARY",
136
+ // Chatty summary (Free plan)
137
+ FEEDBACK_GRAMMAR_INSIGHTS: "FEEDBACK_GRAMMAR_INSIGHTS",
138
+ // Grammar insights
139
+ FEEDBACK_SUGGESTED_RESPONSE: "FEEDBACK_SUGGESTED_RESPONSE",
140
+ // Suggested Response
141
+ FEEDBACK_RUBRIC: "FEEDBACK_RUBRIC",
142
+ // Suggested Response
143
+ FEEDBACK_GRADING_STANDARDS: "FEEDBACK_GRADING_STANDARDS",
144
+ // ACTFL / WIDA Estimate
145
+ FEEDBACK_TARGET_LANGUAGE: "FEEDBACK_TARGET_LANGUAGE",
146
+ // Ability to set the feedback language to the target language of the student
147
+ FEEDBACK_DISABLE_ALLOW_RETRIES: "FEEDBACK_DISABLE_ALLOW_RETRIES"
148
+ // Turn of allow retries
149
+ };
150
+ var AUTO_GRADING_PLANS = {
151
+ AUTO_GRADING_PASS_FAIL: "AUTO_GRADING_PASS_FAIL",
152
+ // Pass / fail grading
153
+ AUTO_GRADING_RUBRICS: "AUTO_GRADING_RUBRICS",
154
+ // Autograded rubrics
155
+ AUTO_GRADING_STANDARDS_BASED: "AUTO_GRADING_STANDARDS_BASED"
156
+ // Standards based grading
157
+ };
158
+ var AI_ASSISTANT_PLANS = {
159
+ AI_ASSISTANT_DOCUMENT_UPLOADS: "AI_ASSISTANT_DOCUMENT_UPLOADS",
160
+ // Allow document uploading
161
+ AI_ASSISTANT_UNLIMITED_USE: "AI_ASSISTANT_UNLIMITED_USE"
162
+ // Allow unlimited use of AI assistant. Otherwise, limits are used.
163
+ };
164
+ var ASSIGNMENT_SETTINGS_PLANS = {
165
+ ASSESSMENTS: "ASSESSMENTS",
166
+ // Ability to create assessment assignment types
167
+ GOOGLE_CLASSROOM_GRADE_PASSBACK: "GOOGLE_CLASSROOM_GRADE_PASSBACK"
168
+ // Assignment scores can sync with classroom
169
+ };
170
+ var ANALYTICS_PLANS = {
171
+ ANALYTICS_GRADEBOOK: "ANALYTICS_GRADEBOOK",
172
+ // Access to the gradebook page
173
+ ANALYTICS_CLASSROOM_ANALYTICS: "ANALYTICS_CLASSROOM_ANALYTICS",
174
+ // Access to the classroom analytics page
175
+ ANALYTICS_STUDENT_PROGRESS_REPORTS: "ANALYTICS_STUDENT_PROGRESS_REPORTS",
176
+ // Access to the panel that shows an individual student's progress and assignments
177
+ ANALYTICS_ASSIGNMENT_RESULTS: "ANALYTICS_ASSIGNMENT_RESULTS",
178
+ // Access to the assigment RESULTS page
179
+ ANALYTICS_ORGANIZATION: "ANALYTICS_ORGANIZATION"
180
+ // Access to the organization analytics panel (for permitted admins)
181
+ };
182
+ var SPACES_PLANS = {
183
+ SPACES_CREATE_SPACE: "SPACES_CREATE_SPACE",
184
+ // Ability to create spaces
185
+ SPACES_CHECK_POINTS: "SPACES_CHECK_POINTS"
186
+ // Feature not available yet. Ability to create checkpoints for spaces for data aggregation
187
+ };
188
+ var DISCOVER_PLANS = {
189
+ DISCOVER_ORGANIZATION_LIBRARY: "DISCOVER_ORGANIZATION_LIBRARY"
190
+ // Access to the organizations shared library
191
+ };
192
+ var MEDIA_AREA_PLANS = {
193
+ MEDIA_AREA_DOCUMENT_UPLOAD: "MEDIA_AREA_DOCUMENT_UPLOAD",
194
+ MEDIA_AREA_AUDIO_FILES: "MEDIA_AREA_AUDIO_FILES"
195
+ };
196
+ var FREE_PLAN = [];
197
+ var TEACHER_PRO_PLAN = [
198
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
199
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
200
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
201
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
202
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
203
+ SPACES_PLANS.SPACES_CREATE_SPACE
204
+ // AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
205
+ ];
206
+ var SCHOOL_STARTER = [
207
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
208
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
209
+ FEEDBACK_PLANS.FEEDBACK_GRAMMAR_INSIGHTS,
210
+ FEEDBACK_PLANS.FEEDBACK_SUGGESTED_RESPONSE,
211
+ FEEDBACK_PLANS.FEEDBACK_RUBRIC,
212
+ FEEDBACK_PLANS.FEEDBACK_GRADING_STANDARDS,
213
+ FEEDBACK_PLANS.FEEDBACK_DISABLE_ALLOW_RETRIES,
214
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
215
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
216
+ AUTO_GRADING_PLANS.AUTO_GRADING_RUBRICS,
217
+ AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
218
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_DOCUMENT_UPLOADS,
219
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_UNLIMITED_USE,
220
+ // ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS,
221
+ ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK,
222
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
223
+ ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS,
224
+ ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS,
225
+ // ANALYTICS_PLANS.ANALYTICS_ORGANIZATION,
226
+ SPACES_PLANS.SPACES_CREATE_SPACE,
227
+ SPACES_PLANS.SPACES_CHECK_POINTS,
228
+ // DISCOVER_PLANS.DISCOVER_ORGANIZATION_LIBRARY,
229
+ MEDIA_AREA_PLANS.MEDIA_AREA_DOCUMENT_UPLOAD,
230
+ MEDIA_AREA_PLANS.MEDIA_AREA_AUDIO_FILES
231
+ ];
232
+ var ORGANIZATION_PLAN = [
233
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
234
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
235
+ FEEDBACK_PLANS.FEEDBACK_GRAMMAR_INSIGHTS,
236
+ FEEDBACK_PLANS.FEEDBACK_SUGGESTED_RESPONSE,
237
+ FEEDBACK_PLANS.FEEDBACK_RUBRIC,
238
+ FEEDBACK_PLANS.FEEDBACK_GRADING_STANDARDS,
239
+ FEEDBACK_PLANS.FEEDBACK_DISABLE_ALLOW_RETRIES,
240
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
241
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
242
+ AUTO_GRADING_PLANS.AUTO_GRADING_RUBRICS,
243
+ AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
244
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_DOCUMENT_UPLOADS,
245
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_UNLIMITED_USE,
246
+ ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS,
247
+ ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK,
248
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
249
+ ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS,
250
+ ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS,
251
+ ANALYTICS_PLANS.ANALYTICS_ORGANIZATION,
252
+ SPACES_PLANS.SPACES_CREATE_SPACE,
253
+ SPACES_PLANS.SPACES_CHECK_POINTS,
254
+ DISCOVER_PLANS.DISCOVER_ORGANIZATION_LIBRARY,
255
+ MEDIA_AREA_PLANS.MEDIA_AREA_DOCUMENT_UPLOAD,
256
+ MEDIA_AREA_PLANS.MEDIA_AREA_AUDIO_FILES
257
+ ];
258
+ var SpeakablePlanTypes = {
259
+ basic: "basic",
260
+ teacher_pro: "teacher_pro",
261
+ school_starter: "school_starter",
262
+ organization: "organization",
263
+ // OLD PLANS
264
+ starter: "starter",
265
+ growth: "growth",
266
+ professional: "professional"
267
+ };
268
+ var SpeakablePermissionsMap = {
269
+ [SpeakablePlanTypes.basic]: FREE_PLAN,
270
+ [SpeakablePlanTypes.starter]: TEACHER_PRO_PLAN,
271
+ [SpeakablePlanTypes.teacher_pro]: TEACHER_PRO_PLAN,
272
+ [SpeakablePlanTypes.growth]: ORGANIZATION_PLAN,
273
+ [SpeakablePlanTypes.professional]: ORGANIZATION_PLAN,
274
+ [SpeakablePlanTypes.organization]: ORGANIZATION_PLAN,
275
+ [SpeakablePlanTypes.school_starter]: SCHOOL_STARTER
276
+ };
277
+ var SpeakablePlanHierarchy = [
278
+ SpeakablePlanTypes.basic,
279
+ SpeakablePlanTypes.starter,
280
+ SpeakablePlanTypes.teacher_pro,
281
+ SpeakablePlanTypes.growth,
282
+ SpeakablePlanTypes.professional,
283
+ SpeakablePlanTypes.school_starter,
284
+ SpeakablePlanTypes.organization
285
+ ];
286
+
287
+ // src/hooks/usePermissions.ts
288
+ var usePermissions = () => {
289
+ const { permissions } = useSpeakableApi();
290
+ const has = (permission) => {
291
+ var _a;
292
+ return (_a = permissions.permissions) == null ? void 0 : _a.includes(permission);
293
+ };
294
+ return {
295
+ plan: permissions.plan,
296
+ permissionsLoaded: permissions.loaded,
297
+ isStripePlan: permissions.isStripePlan,
298
+ refreshDate: permissions.refreshDate,
299
+ isInstitutionPlan: permissions.isInstitutionPlan,
300
+ subscriptionId: permissions.subscriptionId,
301
+ contact: permissions.contact,
302
+ hasGradebook: has(ANALYTICS_PLANS.ANALYTICS_GRADEBOOK),
303
+ hasGoogleClassroomGradePassback: has(ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK),
304
+ hasAssessments: has(ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS),
305
+ hasSectionAnalytics: has(ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS),
306
+ hasStudentReports: has(ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS),
307
+ permissions: permissions || [],
308
+ hasStudentPortfolios: permissions.hasStudentPortfolios,
309
+ isFreeOrgTrial: permissions.type === "free_org_trial",
310
+ freeOrgTrialExpired: permissions.freeOrgTrialExpired
311
+ };
312
+ };
313
+ var usePermissions_default = usePermissions;
314
+
315
+ // src/lib/firebase/api.ts
316
+ var FirebaseAPI = class _FirebaseAPI {
317
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
318
+ constructor() {
319
+ this.config = null;
320
+ }
321
+ static getInstance() {
322
+ if (!_FirebaseAPI.instance) {
323
+ _FirebaseAPI.instance = new _FirebaseAPI();
324
+ }
325
+ return _FirebaseAPI.instance;
326
+ }
327
+ initialize(config) {
328
+ this.config = config;
329
+ }
330
+ get db() {
331
+ if (!this.config) throw new Error("Firebase API not initialized");
332
+ return this.config.db;
333
+ }
334
+ get helpers() {
335
+ if (!this.config) throw new Error("Firebase API not initialized");
336
+ return this.config.helpers;
337
+ }
338
+ get httpsCallable() {
339
+ var _a;
340
+ return (_a = this.config) == null ? void 0 : _a.httpsCallable;
341
+ }
342
+ logEvent(name, data) {
343
+ var _a;
344
+ (_a = this.config) == null ? void 0 : _a.logEvent(name, data);
345
+ }
346
+ accessQueryConstraints() {
347
+ const { query, orderBy, limit, startAt, startAfter, endAt, endBefore, where, increment } = this.helpers;
348
+ return {
349
+ query,
350
+ orderBy,
351
+ limit,
352
+ startAt,
353
+ startAfter,
354
+ endAt,
355
+ endBefore,
356
+ where,
357
+ increment
358
+ };
359
+ }
360
+ accessHelpers() {
361
+ const { doc, collection, writeBatch, serverTimestamp, setDoc } = this.helpers;
362
+ return {
363
+ doc: (path) => doc(this.db, path),
364
+ collection: (path) => collection(this.db, path),
365
+ writeBatch: () => writeBatch(this.db),
366
+ serverTimestamp,
367
+ setDoc
368
+ };
369
+ }
370
+ async getDoc(path) {
371
+ const { getDoc, doc } = this.helpers;
372
+ const docRef = doc(this.db, path);
373
+ const docSnap = await getDoc(docRef);
374
+ const data = docSnap.exists() ? {
375
+ ...docSnap.data(),
376
+ id: docSnap.id
377
+ } : null;
378
+ return {
379
+ id: docSnap.id,
380
+ data
381
+ };
382
+ }
383
+ async getDocs(path, ...queryConstraints) {
384
+ const { getDocs, query, collection } = this.helpers;
385
+ const collectionRef = collection(this.db, path);
386
+ const q = queryConstraints.length > 0 ? query(collectionRef, ...queryConstraints) : collectionRef;
387
+ const querySnapshot = await getDocs(q);
388
+ const data = querySnapshot.docs.map((doc) => ({
389
+ data: doc.data(),
390
+ id: doc.id
391
+ }));
392
+ return {
393
+ data,
394
+ querySnapshot,
395
+ empty: querySnapshot.empty
396
+ };
397
+ }
398
+ async addDoc(path, data) {
399
+ const { addDoc, collection } = this.helpers;
400
+ const collectionRef = collection(this.db, path);
401
+ const docRef = await addDoc(collectionRef, data);
402
+ return {
403
+ ...data,
404
+ id: docRef.id
405
+ };
406
+ }
407
+ async setDoc(path, data, options = {}) {
408
+ const { setDoc, doc } = this.helpers;
409
+ const docRef = doc(this.db, path);
410
+ await setDoc(docRef, data, options);
411
+ }
412
+ async updateDoc(path, data) {
413
+ const { updateDoc, doc } = this.helpers;
414
+ const docRef = doc(this.db, path);
415
+ await updateDoc(docRef, data);
416
+ }
417
+ async deleteDoc(path) {
418
+ const { deleteDoc, doc } = this.helpers;
419
+ const docRef = doc(this.db, path);
420
+ await deleteDoc(docRef);
421
+ }
422
+ async runTransaction(updateFunction) {
423
+ const { runTransaction } = this.helpers;
424
+ return runTransaction(this.db, updateFunction);
425
+ }
426
+ async runBatch(operations) {
427
+ const { writeBatch } = this.helpers;
428
+ const batch = writeBatch(this.db);
429
+ await Promise.all(operations.map((op) => op()));
430
+ await batch.commit();
431
+ }
432
+ writeBatch() {
433
+ const { writeBatch } = this.helpers;
434
+ const batch = writeBatch(this.db);
435
+ return batch;
436
+ }
437
+ };
438
+ var api = FirebaseAPI.getInstance();
439
+
440
+ // src/hooks/useGoogleClassroom.ts
441
+ var useGoogleClassroom = () => {
442
+ const submitAssignmentToGoogleClassroom = async ({
443
+ assignment,
444
+ scores,
445
+ googleUserId = null
446
+ // optional to override the user's googleUserId
447
+ }) => {
448
+ var _a, _b, _c;
449
+ try {
450
+ const { googleClassroomUserId = null } = scores;
451
+ const googleId = googleUserId || googleClassroomUserId;
452
+ if (!googleId)
453
+ return {
454
+ error: true,
455
+ message: "No Google Classroom ID found"
456
+ };
457
+ const { courseWorkId, maxPoints, owners, courseId } = assignment;
458
+ const draftGrade = (scores == null ? void 0 : scores.score) ? (scores == null ? void 0 : scores.score) / 100 * maxPoints : 0;
459
+ const result = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssignmentToGoogleClassroomV2")) == null ? void 0 : _c({
460
+ teacherId: owners[0],
461
+ courseId,
462
+ courseWorkId,
463
+ userId: googleId,
464
+ draftGrade
465
+ }));
466
+ return result;
467
+ } catch (error) {
468
+ return { error: true, message: error.message };
469
+ }
470
+ };
471
+ return {
472
+ submitAssignmentToGoogleClassroom
473
+ };
474
+ };
475
+
476
+ // src/constants/analytics.constants.ts
477
+ var ANALYTICS_EVENT_TYPES = {
478
+ VOICE_SUCCESS: "voice_success",
479
+ VOICE_FAIL: "voice_fail",
480
+ RESPOND_CARD_SUCCESS: "respond_card_success",
481
+ RESPOND_CARD_FAIL: "respond_card_fail",
482
+ RESPOND_WRITE_CARD_SUCCESS: "respond_write_card_success",
483
+ RESPOND_WRITE_CARD_FAIL: "respond_write_card_fail",
484
+ RESPOND_WRITE_CARD_SUBMITTED: "respond_write_card_submitted",
485
+ RESPOND_WRITE_CARD_ERROR: "respond_write_card_error",
486
+ RESPOND_CARD_ERROR: "respond_card_error",
487
+ RESPOND_CARD_SUBMITTED: "respond_card_submitted",
488
+ RESPOND_FREE_PLAN: "respond_free_plan",
489
+ RESPOND_WRITE_FREE_PLAN: "respond_write_free_plan",
490
+ SUBMISSION: "assignment_submitted",
491
+ ASSIGNMENT_STARTED: "assignment_started",
492
+ CREATE_ASSIGNMENT: "create_assignment",
493
+ MC_SUCCESS: "multiple_choice_success",
494
+ MC_FAIL: "multiple_choice_fail",
495
+ MC_ERROR: "multiple_choice_error",
496
+ ACTFL_LEVEL: "actfl_level",
497
+ WIDA_LEVEL: "wida_level"
498
+ };
499
+
500
+ // src/lib/firebase/firebase-analytics/assignment.ts
501
+ var logSubmitAssignment = (data = {}) => {
502
+ var _a, _b, _c;
503
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
504
+ eventType: ANALYTICS_EVENT_TYPES.SUBMISSION,
505
+ ...data
506
+ });
507
+ api.logEvent(ANALYTICS_EVENT_TYPES.SUBMISSION, data);
508
+ };
509
+ var logStartAssignment = (data = {}) => {
510
+ var _a, _b, _c;
511
+ if (data.courseId) {
512
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
513
+ eventType: ANALYTICS_EVENT_TYPES.ASSIGNMENT_STARTED,
514
+ ...data
515
+ });
516
+ }
517
+ api.logEvent(ANALYTICS_EVENT_TYPES.ASSIGNMENT_STARTED, data);
518
+ };
519
+
520
+ // src/domains/notification/services/create-notification.service.ts
521
+ var import_dayjs = __toESM(require("dayjs"));
522
+
523
+ // src/constants/web.constants.ts
524
+ var WEB_BASE_URL = "https://app.speakable.io";
525
+
526
+ // src/domains/notification/notification.constants.ts
527
+ var SPEAKABLE_NOTIFICATIONS = {
528
+ NEW_ASSIGNMENT: "new_assignment",
529
+ ASSESSMENT_SUBMITTED: "assessment_submitted",
530
+ ASSESSMENT_SCORED: "assessment_scored",
531
+ NEW_COMMENT: "NEW_COMMENT"
532
+ };
533
+ var SpeakableNotificationTypes = {
534
+ NEW_ASSIGNMENT: "NEW_ASSIGNMENT",
535
+ FEEDBACK_FROM_TEACHER: "FEEDBACK_FROM_TEACHER",
536
+ MESSAGE_FROM_STUDENT: "MESSAGE_FROM_STUDENT",
537
+ PHRASE_MARKED_CORRECT: "PHRASE_MARKED_CORRECT",
538
+ STUDENT_PROGRESS: "STUDENT_PROGRESS",
539
+ PLAYLIST_FOLLOWERS: "PLAYLIST_FOLLOWERS",
540
+ PLAYLIST_PLAYS: "PLAYLIST_PLAYS",
541
+ // New notifications
542
+ ASSESSMENT_SUBMITTED: "ASSESSMENT_SUBMITTED",
543
+ // Notification FOR TEACHER when student submits assessment
544
+ ASSESSMENT_SCORED: "ASSESSMENT_SCORED",
545
+ // Notification FOR STUDENT when teacher scores assessment
546
+ // Comment
547
+ NEW_COMMENT: "NEW_COMMENT"
548
+ };
549
+
550
+ // src/utils/error-handler.ts
551
+ var ServiceError = class extends Error {
552
+ constructor(message, originalError, code) {
553
+ super(message);
554
+ this.originalError = originalError;
555
+ this.code = code;
556
+ this.name = "ServiceError";
557
+ }
558
+ };
559
+ function withErrorHandler(fn, serviceName) {
560
+ return async (...args) => {
561
+ try {
562
+ return await fn(...args);
563
+ } catch (error) {
564
+ if (error instanceof Error && "code" in error) {
565
+ const firebaseError = error;
566
+ throw new ServiceError(
567
+ `Error in ${serviceName}: ${firebaseError.message}`,
568
+ error,
569
+ firebaseError.code
570
+ );
571
+ }
572
+ if (error instanceof Error) {
573
+ throw new ServiceError(`Error in ${serviceName}: ${error.message}`, error);
574
+ }
575
+ throw new ServiceError(`Unknown error in ${serviceName}`, error);
576
+ }
577
+ };
578
+ }
579
+
580
+ // src/domains/notification/services/send-notification.service.ts
581
+ var _sendNotification = async (sendTo, notification) => {
582
+ var _a, _b, _c;
583
+ const results = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "createNotificationV2")) == null ? void 0 : _c({
584
+ sendTo,
585
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
586
+ notification
587
+ }));
588
+ return results;
589
+ };
590
+ var sendNotification = withErrorHandler(_sendNotification, "sendNotification");
591
+
592
+ // src/domains/notification/services/create-notification.service.ts
593
+ var createNotification = async ({
594
+ data,
595
+ type,
596
+ userId,
597
+ profile
598
+ }) => {
599
+ let result;
600
+ switch (type) {
601
+ case SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT:
602
+ result = await handleAssignNotifPromise({ data, profile });
603
+ break;
604
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED:
605
+ result = await createAssessmentSubmissionNotification({
606
+ data,
607
+ profile,
608
+ userId
609
+ });
610
+ break;
611
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED:
612
+ result = await createAssessmentScoredNotification({
613
+ data,
614
+ profile
615
+ });
616
+ break;
617
+ default:
618
+ result = null;
619
+ break;
620
+ }
621
+ return result;
622
+ };
623
+ var handleAssignNotifPromise = async ({
624
+ data: assignments,
625
+ profile
626
+ }) => {
627
+ if (!assignments.length) return;
628
+ try {
629
+ const notifsPromises = assignments.map(async (assignment) => {
630
+ const {
631
+ section,
632
+ section: { members },
633
+ ...rest
634
+ } = assignment;
635
+ if (!section || !members) throw new Error("Invalid assignment data");
636
+ const data = { section, sendTo: members, assignment: { ...rest } };
637
+ return createNewAssignmentNotification({ data, profile });
638
+ });
639
+ await Promise.all(notifsPromises);
640
+ return {
641
+ success: true,
642
+ message: "Assignment notifications sent successfully"
643
+ };
644
+ } catch (error) {
645
+ console.error("Error in handleAssignNotifPromise:", error);
646
+ throw error;
647
+ }
648
+ };
649
+ var createNewAssignmentNotification = async ({
650
+ data,
651
+ profile
652
+ }) => {
653
+ var _a;
654
+ const { assignment, sendTo } = data;
655
+ const teacherName = profile.displayName || "Your teacher";
656
+ const dueDate = assignment.dueDateTimestamp ? (0, import_dayjs.default)(assignment.dueDateTimestamp.toDate()).format("MMM Do") : null;
657
+ const results = await sendNotification(sendTo, {
658
+ courseId: assignment.courseId,
659
+ type: SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT,
660
+ senderName: teacherName,
661
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
662
+ messagePreview: `A new assignment "${assignment.name}" is now available. ${dueDate ? `Due ${dueDate}` : ""}`,
663
+ title: "New Assignment Available!",
664
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url
665
+ });
666
+ return results;
667
+ };
668
+ var createAssessmentSubmissionNotification = async ({
669
+ data: assignment,
670
+ profile,
671
+ userId
672
+ }) => {
673
+ var _a;
674
+ const studentName = profile.displayName || "Your student";
675
+ const results = await sendNotification(assignment.owners, {
676
+ courseId: assignment.courseId,
677
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED,
678
+ link: `${WEB_BASE_URL}/a/${assignment.id}?studentId=${userId}`,
679
+ title: `Assessment Submitted!`,
680
+ senderName: studentName,
681
+ messagePreview: `${studentName} has submitted the assessment "${assignment.name}"`,
682
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url
683
+ });
684
+ return results;
685
+ };
686
+ var createAssessmentScoredNotification = async ({
687
+ data,
688
+ profile
689
+ }) => {
690
+ var _a, _b, _c, _d, _e;
691
+ const { assignment, sendTo } = data;
692
+ const teacherName = profile.displayName || "Your teacher";
693
+ const title = `${assignment.isAssessment ? "Assessment" : "Assignment"} Reviewed!`;
694
+ const messagePreview = `Your ${assignment.isAssessment ? "assessment" : "assignment"} has been reviewed by your teacher. Click to view the feedback.`;
695
+ const results = await sendNotification(sendTo, {
696
+ courseId: assignment.courseId,
697
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED,
698
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
699
+ title,
700
+ messagePreview,
701
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url,
702
+ senderName: teacherName
703
+ });
704
+ await ((_e = (_c = (_b = api).httpsCallable) == null ? void 0 : _c.call(_b, "sendAssessmentScoredEmail")) == null ? void 0 : _e({
705
+ assessmentTitle: assignment.name,
706
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
707
+ senderImage: ((_d = profile.image) == null ? void 0 : _d.url) || "",
708
+ studentId: sendTo[0],
709
+ teacherName: profile.displayName
710
+ }));
711
+ return results;
712
+ };
713
+
714
+ // src/domains/notification/hooks/notification.hooks.ts
715
+ var notificationQueryKeys = {
716
+ all: ["notifications"],
717
+ byId: (id) => [...notificationQueryKeys.all, id]
718
+ };
719
+ var useCreateNotification = () => {
720
+ const { user, queryClient } = useSpeakableApi();
721
+ const handleCreateNotifications = async (type, data) => {
722
+ var _a, _b;
723
+ const result = await createNotification({
724
+ type,
725
+ userId: user.auth.uid,
726
+ profile: (_a = user == null ? void 0 : user.profile) != null ? _a : {},
727
+ data
728
+ });
729
+ queryClient.invalidateQueries({
730
+ queryKey: notificationQueryKeys.byId((_b = user == null ? void 0 : user.auth.uid) != null ? _b : "")
731
+ });
732
+ return result;
733
+ };
734
+ return {
735
+ createNotification: handleCreateNotifications
736
+ };
737
+ };
738
+
739
+ // src/domains/assignment/assignment.constants.ts
740
+ var ASSIGNMENTS_COLLECTION = "assignments";
741
+ var ANALYTICS_SUBCOLLECTION = "analytics";
742
+ var SCORES_SUBCOLLECTION = "scores";
743
+ var refsAssignmentFiresotre = {
744
+ allAssignments: () => ASSIGNMENTS_COLLECTION,
745
+ assignment: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}`,
746
+ assignmentAllAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}`,
747
+ assignmentAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}/${params.type}`,
748
+ assignmentScores: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${SCORES_SUBCOLLECTION}/${params.userId}`
749
+ };
750
+
751
+ // src/domains/assignment/utils/create-default-score.ts
752
+ var defaultScore = (props) => {
753
+ const { serverTimestamp } = api.accessHelpers();
754
+ const score = {
755
+ progress: 0,
756
+ score: 0,
757
+ startDate: serverTimestamp(),
758
+ status: "IN_PROGRESS",
759
+ submitted: false,
760
+ cards: {},
761
+ lastPlayed: serverTimestamp(),
762
+ owners: props.owners,
763
+ userId: props.userId
764
+ };
765
+ if (props.googleClassroomUserId) {
766
+ score.googleClassroomUserId = props.googleClassroomUserId;
767
+ }
768
+ if (props.courseId) {
769
+ score.courseId = props.courseId;
770
+ }
771
+ return score;
772
+ };
773
+
774
+ // src/domains/assignment/score-practice.constants.ts
775
+ var SCORES_PRACTICE_COLLECTION = "users";
776
+ var SCORES_PRACTICE_SUBCOLLECTION = "practice";
777
+ var refsScoresPractice = {
778
+ practiceScores: (params) => `${SCORES_PRACTICE_COLLECTION}/${params.userId}/${SCORES_PRACTICE_SUBCOLLECTION}/${params.setId}`,
779
+ practiceScoreHistoryRefDoc: (params) => `${SCORES_PRACTICE_COLLECTION}/${params.userId}/${SCORES_PRACTICE_SUBCOLLECTION}/${params.setId}/attempts/${params.date}`
780
+ };
781
+
782
+ // src/domains/assignment/services/create-score.service.ts
783
+ async function _createScore(params) {
784
+ var _a, _b, _c;
785
+ if (params.isAssignment) {
786
+ const ref = refsAssignmentFiresotre.assignmentScores({
787
+ id: params.activityId,
788
+ userId: params.userId
789
+ });
790
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "updateAssignmentGradebookStatus")) == null ? void 0 : _c({
791
+ assignmentId: params.activityId,
792
+ userId: params.userId,
793
+ status: "IN_PROGRESS",
794
+ score: null
795
+ }));
796
+ await api.setDoc(ref, params.scoreData, { merge: true });
797
+ return {
798
+ id: params.userId
799
+ };
800
+ } else {
801
+ const ref = refsScoresPractice.practiceScores({
802
+ userId: params.userId,
803
+ setId: params.activityId
804
+ });
805
+ await api.setDoc(ref, params.scoreData);
806
+ return {
807
+ id: params.userId
808
+ };
809
+ }
810
+ }
811
+ var createScore = withErrorHandler(_createScore, "createScore");
812
+
813
+ // src/domains/assignment/services/get-score.service.ts
814
+ async function getAssignmentScore({
815
+ userId,
816
+ assignment,
817
+ googleClassroomUserId
818
+ }) {
819
+ const path = refsAssignmentFiresotre.assignmentScores({
820
+ id: assignment.id,
821
+ userId
822
+ });
823
+ const response = await api.getDoc(path);
824
+ if (response.data == null) {
825
+ const newScore = {
826
+ ...defaultScore({
827
+ owners: [userId],
828
+ userId,
829
+ courseId: assignment.courseId,
830
+ googleClassroomUserId
831
+ }),
832
+ assignmentId: assignment.id
833
+ };
834
+ logStartAssignment({
835
+ courseId: assignment.courseId
836
+ });
837
+ const result = await createScore({
838
+ activityId: assignment.id,
839
+ userId,
840
+ isAssignment: true,
841
+ scoreData: newScore
842
+ });
843
+ return {
844
+ ...newScore,
845
+ id: result.id
846
+ };
847
+ }
848
+ return response.data;
849
+ }
850
+ async function getPracticeScore({ userId, setId }) {
851
+ const path = refsScoresPractice.practiceScores({ userId, setId });
852
+ const response = await api.getDoc(path);
853
+ if (response.data == null) {
854
+ const newScore = {
855
+ ...defaultScore({
856
+ owners: [userId],
857
+ userId
858
+ }),
859
+ setId
860
+ };
861
+ const result = await createScore({
862
+ activityId: setId,
863
+ userId,
864
+ isAssignment: false,
865
+ scoreData: newScore
866
+ });
867
+ return {
868
+ ...newScore,
869
+ id: result.id
870
+ };
871
+ }
872
+ return response.data;
873
+ }
874
+ async function _getScore(params) {
875
+ if (params.isAssignment) {
876
+ return await getAssignmentScore({
877
+ userId: params.userId,
878
+ assignment: {
879
+ id: params.activityId,
880
+ courseId: params.courseId
881
+ },
882
+ googleClassroomUserId: params.googleClassroomUserId
883
+ });
884
+ } else {
885
+ return await getPracticeScore({
886
+ userId: params.userId,
887
+ setId: params.activityId
888
+ });
889
+ }
890
+ }
891
+ var getScore = withErrorHandler(_getScore, "getScore");
892
+
893
+ // src/domains/assignment/utils/calculateScoreAndProgress.ts
894
+ var calculateScoreAndProgress = (scores, cardsList, weights) => {
895
+ const totalSetPoints = cardsList.reduce((acc, cardId) => {
896
+ acc += (weights == null ? void 0 : weights[cardId]) || 1;
897
+ return acc;
898
+ }, 0);
899
+ const totalPointsAwarded = Object.keys((scores == null ? void 0 : scores.cards) || {}).reduce((acc, cardId) => {
900
+ var _a, _b;
901
+ const cardScores = (_a = scores == null ? void 0 : scores.cards) == null ? void 0 : _a[cardId];
902
+ if ((cardScores == null ? void 0 : cardScores.completed) || (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0) {
903
+ const score2 = (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0 ? Number((_b = cardScores == null ? void 0 : cardScores.score) != null ? _b : 0) : null;
904
+ const weight = (weights == null ? void 0 : weights[cardId]) || 1;
905
+ const fraction = (score2 != null ? score2 : 0) / 100;
906
+ if (score2 || score2 === 0) {
907
+ acc += weight * fraction;
908
+ } else {
909
+ acc += weight;
910
+ }
911
+ }
912
+ return acc;
913
+ }, 0);
914
+ const totalCompletedCards = Object.keys((scores == null ? void 0 : scores.cards) || {}).reduce((acc, cardId) => {
915
+ var _a;
916
+ const cardScores = (_a = scores == null ? void 0 : scores.cards) == null ? void 0 : _a[cardId];
917
+ if ((cardScores == null ? void 0 : cardScores.completed) || (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0) {
918
+ acc += 1;
919
+ }
920
+ return acc;
921
+ }, 0);
922
+ const percent = totalPointsAwarded / totalSetPoints;
923
+ const score = Math.round(percent * 100);
924
+ const progress = Math.round(totalCompletedCards / (cardsList.length || 1) * 100);
925
+ return { score, progress };
926
+ };
927
+ var calculateScoreAndProgress_default = calculateScoreAndProgress;
928
+
929
+ // src/domains/assignment/services/update-score.service.ts
930
+ async function _updateScore(params) {
931
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
932
+ id: params.activityId,
933
+ userId: params.userId
934
+ }) : refsScoresPractice.practiceScores({
935
+ setId: params.activityId,
936
+ userId: params.userId
937
+ });
938
+ await api.updateDoc(path, {
939
+ ...params.data
940
+ });
941
+ }
942
+ var updateScore = withErrorHandler(_updateScore, "updateScore");
943
+ async function _updateCardScore(params) {
944
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
945
+ id: params.activityId,
946
+ userId: params.userId
947
+ }) : refsScoresPractice.practiceScores({
948
+ setId: params.activityId,
949
+ userId: params.userId
950
+ });
951
+ const updates = Object.keys(params.updates.cardScore).reduce(
952
+ (acc, key) => {
953
+ acc[`cards.${params.cardId}.${key}`] = params.updates.cardScore[key];
954
+ return acc;
955
+ },
956
+ {}
957
+ );
958
+ if (params.updates.progress) {
959
+ updates.progress = params.updates.progress;
960
+ }
961
+ if (params.updates.score) {
962
+ updates.score = params.updates.score;
963
+ }
964
+ await api.updateDoc(path, {
965
+ ...updates
966
+ });
967
+ }
968
+ var updateCardScore = withErrorHandler(_updateCardScore, "updateCardScore");
969
+
970
+ // src/domains/assignment/services/clear-score.service.ts
971
+ var import_dayjs2 = __toESM(require("dayjs"));
972
+ async function clearScore(params) {
973
+ var _a, _b, _c, _d, _e;
974
+ const update = {
975
+ [`cards.${params.cardId}`]: {
976
+ attempts: ((_a = params.cardScores.attempts) != null ? _a : 1) + 1,
977
+ correct: (_b = params.cardScores.correct) != null ? _b : 0,
978
+ // save old score history
979
+ history: [
980
+ {
981
+ ...params.cardScores,
982
+ attempts: (_c = params.cardScores.attempts) != null ? _c : 1,
983
+ correct: (_d = params.cardScores.correct) != null ? _d : 0,
984
+ retryTime: (0, import_dayjs2.default)().format("YYYY-MM-DD HH:mm:ss"),
985
+ history: null
986
+ },
987
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
988
+ ...(_e = params.cardScores.history) != null ? _e : []
989
+ ]
990
+ }
991
+ };
992
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
993
+ id: params.activityId,
994
+ userId: params.userId
995
+ }) : refsScoresPractice.practiceScores({
996
+ setId: params.activityId,
997
+ userId: params.userId
998
+ });
999
+ await api.updateDoc(path, update);
1000
+ return {
1001
+ update,
1002
+ activityId: params.activityId
1003
+ };
1004
+ }
1005
+
1006
+ // src/domains/assignment/services/submit-assignment-score.service.ts
1007
+ var import_dayjs3 = __toESM(require("dayjs"));
1008
+ async function _submitAssignmentScore({
1009
+ cardIds,
1010
+ assignment,
1011
+ weights,
1012
+ userId,
1013
+ status,
1014
+ studentName
1015
+ }) {
1016
+ const { serverTimestamp } = api.accessHelpers();
1017
+ const path = refsAssignmentFiresotre.assignmentScores({ id: assignment.id, userId });
1018
+ const fieldsUpdated = {
1019
+ submitted: true,
1020
+ progress: 100,
1021
+ submissionDate: serverTimestamp(),
1022
+ status
1023
+ };
1024
+ if (assignment.isAssessment) {
1025
+ const result = await handleAssessment(
1026
+ assignment,
1027
+ userId,
1028
+ cardIds,
1029
+ weights,
1030
+ fieldsUpdated,
1031
+ studentName
1032
+ );
1033
+ return result;
1034
+ } else if (assignment.courseId) {
1035
+ await handleCourseAssignment(assignment, userId);
1036
+ }
1037
+ await api.updateDoc(path, { ...fieldsUpdated });
1038
+ return { success: true, fieldsUpdated };
1039
+ }
1040
+ var submitAssignmentScore = withErrorHandler(
1041
+ _submitAssignmentScore,
1042
+ "submitAssignmentScore"
1043
+ );
1044
+ async function handleAssessment(assignment, userId, cardIds, weights, fieldsUpdated, studentName) {
1045
+ var _a, _b, _c;
1046
+ const path = refsAssignmentFiresotre.assignmentScores({ id: assignment.id, userId });
1047
+ const response = await api.getDoc(path);
1048
+ if (!response.data) {
1049
+ throw new Error("Score not found");
1050
+ }
1051
+ const { score: scoreCalculated } = calculateScoreAndProgress_default(response.data, cardIds, weights);
1052
+ await api.updateDoc(path, { score: scoreCalculated, status: "PENDING_REVIEW" });
1053
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssessment")) == null ? void 0 : _c({
1054
+ assignmentId: assignment.id,
1055
+ assignmentTitle: assignment.name,
1056
+ userId,
1057
+ teacherId: assignment.owners[0],
1058
+ studentName
1059
+ }));
1060
+ fieldsUpdated.status = "PENDING_REVIEW";
1061
+ return { success: true, fieldsUpdated };
1062
+ }
1063
+ async function handleCourseAssignment(assignment, userId) {
1064
+ var _a, _b, _c;
1065
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssignmentV2")) == null ? void 0 : _c({
1066
+ assignmentId: assignment.id,
1067
+ userId
1068
+ }));
1069
+ }
1070
+ async function submitPracticeScore({
1071
+ setId,
1072
+ userId,
1073
+ scores
1074
+ }) {
1075
+ const { serverTimestamp } = api.accessHelpers();
1076
+ const date = (0, import_dayjs3.default)().format("YYYY-MM-DD-HH-mm");
1077
+ const ref = refsScoresPractice.practiceScoreHistoryRefDoc({ setId, userId, date });
1078
+ const fieldsUpdated = {
1079
+ ...scores,
1080
+ submitted: true,
1081
+ progress: 100,
1082
+ submissionDate: serverTimestamp(),
1083
+ status: "SUBMITTED"
1084
+ };
1085
+ await api.setDoc(ref, { ...fieldsUpdated });
1086
+ const refScores = refsScoresPractice.practiceScores({ userId, setId });
1087
+ await api.deleteDoc(refScores);
1088
+ return { success: true, fieldsUpdated };
1089
+ }
1090
+
1091
+ // src/domains/assignment/hooks/score-hooks.ts
1092
+ var scoreQueryKeys = {
1093
+ all: ["scores"],
1094
+ byId: (id) => [...scoreQueryKeys.all, id],
1095
+ list: () => [...scoreQueryKeys.all, "list"]
1096
+ };
1097
+ function useScore({
1098
+ isAssignment,
1099
+ activityId,
1100
+ userId = "",
1101
+ courseId,
1102
+ enabled = true,
1103
+ googleClassroomUserId
1104
+ }) {
1105
+ return (0, import_react_query2.useQuery)({
1106
+ queryFn: () => getScore({
1107
+ userId,
1108
+ courseId,
1109
+ activityId,
1110
+ googleClassroomUserId,
1111
+ isAssignment
1112
+ }),
1113
+ queryKey: scoreQueryKeys.byId(activityId),
1114
+ enabled
1115
+ });
1116
+ }
1117
+ var debounceUpdateScore = debounce(updateScore, 1e3);
1118
+ function useUpdateScore() {
1119
+ const { queryClient } = useSpeakableApi();
1120
+ const mutation = (0, import_react_query2.useMutation)({
1121
+ mutationFn: debounceUpdateScore,
1122
+ onMutate: (variables) => {
1123
+ return handleOptimisticUpdate({
1124
+ queryClient,
1125
+ queryKey: scoreQueryKeys.byId(variables.activityId),
1126
+ newData: variables.data
1127
+ });
1128
+ },
1129
+ onError: (_, variables, context) => {
1130
+ if (context == null ? void 0 : context.previousData)
1131
+ queryClient.setQueryData(scoreQueryKeys.byId(variables.activityId), context.previousData);
1132
+ },
1133
+ onSettled: (_, err, variables) => {
1134
+ queryClient.invalidateQueries({
1135
+ queryKey: scoreQueryKeys.byId(variables.activityId)
1136
+ });
1137
+ }
1138
+ });
1139
+ return {
1140
+ mutationUpdateScore: mutation
1141
+ };
1142
+ }
1143
+ function useUpdateCardScore({
1144
+ isAssignment,
1145
+ activityId,
1146
+ userId,
1147
+ cardIds,
1148
+ weights
1149
+ }) {
1150
+ const { queryClient } = useSpeakableApi();
1151
+ const queryKey = scoreQueryKeys.byId(activityId);
1152
+ const mutation = (0, import_react_query2.useMutation)({
1153
+ mutationFn: async ({ cardId, cardScore }) => {
1154
+ const previousScores = queryClient.getQueryData(queryKey);
1155
+ const { progress, score, newScoreUpdated, updatedCardScore } = getScoreUpdated({
1156
+ previousScores: previousScores != null ? previousScores : {},
1157
+ cardId,
1158
+ cardScore,
1159
+ cardIds,
1160
+ weights
1161
+ });
1162
+ await updateCardScore({
1163
+ userId,
1164
+ cardId,
1165
+ isAssignment,
1166
+ activityId,
1167
+ updates: {
1168
+ cardScore: updatedCardScore,
1169
+ progress,
1170
+ score
1171
+ }
1172
+ });
1173
+ return { cardId, scoresUpdated: newScoreUpdated };
1174
+ },
1175
+ onMutate: ({ cardId, cardScore }) => {
1176
+ queryClient.setQueryData(queryKey, (previousScore) => {
1177
+ const updates = handleOptimisticScore({
1178
+ score: previousScore,
1179
+ cardId,
1180
+ cardScore,
1181
+ cardIds,
1182
+ weights
1183
+ });
1184
+ return {
1185
+ ...previousScore,
1186
+ ...updates
1187
+ };
1188
+ });
1189
+ },
1190
+ // eslint-disable-next-line no-unused-vars
1191
+ onError: (error) => {
1192
+ console.log(error.message);
1193
+ const previousData = queryClient.getQueryData(queryKey);
1194
+ if (previousData) {
1195
+ queryClient.setQueryData(queryKey, previousData);
1196
+ }
1197
+ }
1198
+ });
1199
+ return {
1200
+ mutationUpdateCardScore: mutation
1201
+ };
1202
+ }
1203
+ var getScoreUpdated = ({
1204
+ cardId,
1205
+ cardScore,
1206
+ previousScores,
1207
+ cardIds,
1208
+ weights
1209
+ }) => {
1210
+ var _a, _b;
1211
+ const previousCard = (_a = previousScores.cards) == null ? void 0 : _a[cardId];
1212
+ const newCardScore = {
1213
+ ...previousCard != null ? previousCard : {},
1214
+ ...cardScore
1215
+ };
1216
+ const newScores = {
1217
+ ...previousScores,
1218
+ cards: {
1219
+ ...(_b = previousScores.cards) != null ? _b : {},
1220
+ [cardId]: newCardScore
1221
+ }
1222
+ };
1223
+ const { score, progress } = calculateScoreAndProgress_default(newScores, cardIds, weights);
1224
+ return {
1225
+ newScoreUpdated: newScores,
1226
+ updatedCardScore: cardScore,
1227
+ score,
1228
+ progress
1229
+ };
1230
+ };
1231
+ var handleOptimisticScore = ({
1232
+ score,
1233
+ cardId,
1234
+ cardScore,
1235
+ cardIds,
1236
+ weights
1237
+ }) => {
1238
+ var _a;
1239
+ let cards = { ...(_a = score == null ? void 0 : score.cards) != null ? _a : {} };
1240
+ cards = {
1241
+ ...cards,
1242
+ [cardId]: {
1243
+ ...cards[cardId],
1244
+ ...cardScore
1245
+ }
1246
+ };
1247
+ const { score: scoreValue, progress } = calculateScoreAndProgress_default(
1248
+ // @ts-ignore
1249
+ {
1250
+ ...score != null ? score : {},
1251
+ cards
1252
+ },
1253
+ cardIds,
1254
+ weights
1255
+ );
1256
+ return { cards, score: scoreValue, progress };
1257
+ };
1258
+ function useClearScore() {
1259
+ const { queryClient } = useSpeakableApi();
1260
+ const mutation = (0, import_react_query2.useMutation)({
1261
+ mutationFn: clearScore,
1262
+ onSettled: (result) => {
1263
+ var _a;
1264
+ queryClient.invalidateQueries({
1265
+ queryKey: scoreQueryKeys.byId((_a = result == null ? void 0 : result.activityId) != null ? _a : "")
1266
+ });
1267
+ }
1268
+ });
1269
+ return {
1270
+ mutationClearScore: mutation
1271
+ };
1272
+ }
1273
+ function useSubmitAssignmentScore({
1274
+ onAssignmentSubmitted,
1275
+ studentName
1276
+ }) {
1277
+ const { queryClient } = useSpeakableApi();
1278
+ const { hasGoogleClassroomGradePassback } = usePermissions_default();
1279
+ const { submitAssignmentToGoogleClassroom } = useGoogleClassroom();
1280
+ const { createNotification: createNotification2 } = useCreateNotification();
1281
+ const mutation = (0, import_react_query2.useMutation)({
1282
+ mutationFn: async ({
1283
+ assignment,
1284
+ userId,
1285
+ cardIds,
1286
+ weights,
1287
+ scores,
1288
+ status
1289
+ }) => {
1290
+ try {
1291
+ const scoreUpdated = await submitAssignmentScore({
1292
+ assignment,
1293
+ userId,
1294
+ cardIds,
1295
+ weights,
1296
+ status,
1297
+ studentName
1298
+ });
1299
+ if (assignment.courseWorkId != null && !assignment.isAssessment && hasGoogleClassroomGradePassback) {
1300
+ await submitAssignmentToGoogleClassroom({
1301
+ assignment,
1302
+ scores
1303
+ });
1304
+ }
1305
+ if (assignment.isAssessment) {
1306
+ createNotification2(SpeakableNotificationTypes.ASSESSMENT_SUBMITTED, assignment);
1307
+ }
1308
+ if (assignment == null ? void 0 : assignment.id) {
1309
+ logSubmitAssignment({
1310
+ courseId: assignment == null ? void 0 : assignment.courseId
1311
+ });
1312
+ }
1313
+ onAssignmentSubmitted(assignment.id);
1314
+ queryClient.setQueryData(scoreQueryKeys.byId(assignment.id), {
1315
+ ...scores,
1316
+ ...scoreUpdated.fieldsUpdated
1317
+ });
1318
+ return {
1319
+ success: true,
1320
+ message: "Score submitted successfully"
1321
+ };
1322
+ } catch (error) {
1323
+ return {
1324
+ success: false,
1325
+ error
1326
+ };
1327
+ }
1328
+ }
1329
+ });
1330
+ return {
1331
+ submitAssignmentScore: mutation.mutateAsync,
1332
+ isLoading: mutation.isPending
1333
+ };
1334
+ }
1335
+ function useSubmitPracticeScore() {
1336
+ const { queryClient } = useSpeakableApi();
1337
+ const mutation = (0, import_react_query2.useMutation)({
1338
+ mutationFn: async ({
1339
+ setId,
1340
+ userId,
1341
+ scores
1342
+ }) => {
1343
+ try {
1344
+ await submitPracticeScore({
1345
+ setId,
1346
+ userId,
1347
+ scores
1348
+ });
1349
+ queryClient.invalidateQueries({
1350
+ queryKey: scoreQueryKeys.byId(setId)
1351
+ });
1352
+ return {
1353
+ success: true,
1354
+ message: "Score submitted successfully"
1355
+ };
1356
+ } catch (error) {
1357
+ return {
1358
+ success: false,
1359
+ error
1360
+ };
1361
+ }
1362
+ }
1363
+ });
1364
+ return {
1365
+ submitPracticeScore: mutation.mutateAsync,
1366
+ isLoading: mutation.isPending
1367
+ };
1368
+ }
1369
+
1370
+ // src/domains/cards/card.hooks.ts
1371
+ var import_react_query3 = require("@tanstack/react-query");
1372
+ var import_react2 = require("react");
1373
+
1374
+ // src/domains/cards/card.constants.ts
1375
+ var CARDS_COLLECTION = "flashcards";
1376
+ var refsCardsFiresotre = {
1377
+ allCards: CARDS_COLLECTION,
1378
+ card: (id) => `${CARDS_COLLECTION}/${id}`
1379
+ };
1380
+
1381
+ // src/domains/cards/services/get-card.service.ts
1382
+ async function _getCard(params) {
1383
+ const ref = refsCardsFiresotre.card(params.cardId);
1384
+ const response = await api.getDoc(ref);
1385
+ if (!response.data) return null;
1386
+ return response.data;
1387
+ }
1388
+ var getCard = withErrorHandler(_getCard, "getCard");
1389
+
1390
+ // src/domains/cards/services/create-card.service.ts
1391
+ var import_uuid = require("uuid");
1392
+
1393
+ // src/utils/text-utils.ts
1394
+ var import_js_sha1 = __toESM(require("js-sha1"));
1395
+ var purify = (word) => {
1396
+ return word.normalize("NFD").replace(/\/([^" "]*)/g, "").replace(/\([^()]*\)/g, "").replace(/([^()]*)/g, "").replace(/[\u0300-\u036f]/g, "").replace(/[-]/g, " ").replace(/[.,/#!¡¿?؟。,.?$%^&*;:{}=\-_`~()’'…\s]/g, "").replace(/\s\s+/g, " ").toLowerCase().trim();
1397
+ };
1398
+ var cleanString = (words) => {
1399
+ const splitWords = words == null ? void 0 : words.split("+");
1400
+ if (splitWords && splitWords.length === 1) {
1401
+ const newWord = purify(words);
1402
+ return newWord;
1403
+ } else if (splitWords && splitWords.length > 1) {
1404
+ const split = splitWords.map((w) => purify(w));
1405
+ return split;
1406
+ } else {
1407
+ return "";
1408
+ }
1409
+ };
1410
+ var getWordHash = (word, language) => {
1411
+ const cleanedWord = cleanString(word);
1412
+ const wordHash = (0, import_js_sha1.default)(`${language}-${cleanedWord}`);
1413
+ console.log("wordHash core library", wordHash);
1414
+ return wordHash;
1415
+ };
1416
+
1417
+ // src/domains/cards/services/get-card-verification-status.service.ts
1418
+ var charactarLanguages = ["zh", "ja", "ko"];
1419
+ var getVerificationStatus = async (target_text, language) => {
1420
+ if ((target_text == null ? void 0 : target_text.length) < 3 && !charactarLanguages.includes(language)) {
1421
+ return "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
1422
+ }
1423
+ const hash = getWordHash(target_text, language);
1424
+ const response = await api.getDoc(`checked-pronunciations/${hash}`);
1425
+ try {
1426
+ if (response.data) {
1427
+ return processRecord(response.data);
1428
+ } else {
1429
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1430
+ }
1431
+ } catch (e) {
1432
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1433
+ }
1434
+ };
1435
+ var processRecord = (data) => {
1436
+ const { pronunciations = 0, fails = 0 } = data;
1437
+ const attempts = pronunciations + fails;
1438
+ const successRate = attempts > 0 ? pronunciations / attempts * 100 : 0;
1439
+ let newStatus = null;
1440
+ if (attempts < 6) {
1441
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1442
+ }
1443
+ if (successRate > 25) {
1444
+ newStatus = "VERIFIED" /* VERIFIED */;
1445
+ } else if (successRate > 10) {
1446
+ newStatus = "WARNING" /* WARNING */;
1447
+ } else if (fails > 20 && successRate < 10 && pronunciations > 1) {
1448
+ newStatus = "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
1449
+ } else if (pronunciations === 0 && fails > 20) {
1450
+ newStatus = "NOT_WORKING" /* NOT_WORKING */;
1451
+ } else {
1452
+ newStatus = "NOT_CHECKED" /* NOT_CHECKED */;
1453
+ }
1454
+ return newStatus;
1455
+ };
1456
+
1457
+ // src/domains/cards/services/create-card.service.ts
1458
+ async function _createCard({ data }) {
1459
+ const response = await api.addDoc(refsCardsFiresotre.allCards, data);
1460
+ return response;
1461
+ }
1462
+ var createCard = withErrorHandler(_createCard, "createCard");
1463
+ async function _createCards({ cards }) {
1464
+ const { writeBatch, doc } = api.accessHelpers();
1465
+ const batch = writeBatch();
1466
+ const cardsWithId = [];
1467
+ for (const card of cards) {
1468
+ const cardId = (0, import_uuid.v4)();
1469
+ const ref = doc(refsCardsFiresotre.card(cardId));
1470
+ const newCardObject = {
1471
+ ...card,
1472
+ id: cardId
1473
+ };
1474
+ if (card.type === "READ_REPEAT" /* READ_REPEAT */ && card.target_text && card.language) {
1475
+ const verificationStatus = await getVerificationStatus(card.target_text, card.language);
1476
+ newCardObject.verificationStatus = verificationStatus || null;
1477
+ }
1478
+ cardsWithId.push(newCardObject);
1479
+ batch.set(ref, newCardObject);
1480
+ }
1481
+ await batch.commit();
1482
+ return cardsWithId;
1483
+ }
1484
+ var createCards = withErrorHandler(_createCards, "createCards");
1485
+
1486
+ // src/domains/cards/card.hooks.ts
1487
+ var cardsQueryKeys = {
1488
+ all: ["cards"],
1489
+ one: (params) => [...cardsQueryKeys.all, params.cardId]
1490
+ };
1491
+ function useCards({
1492
+ cardIds,
1493
+ enabled = true,
1494
+ asObject
1495
+ }) {
1496
+ const queries = (0, import_react_query3.useQueries)({
1497
+ queries: cardIds.map((cardId) => ({
1498
+ enabled: enabled && cardIds.length > 0,
1499
+ queryKey: cardsQueryKeys.one({
1500
+ cardId
1501
+ }),
1502
+ queryFn: () => getCard({ cardId })
1503
+ }))
1504
+ });
1505
+ const cards = queries.map((query) => query.data).filter(Boolean);
1506
+ const cardsObject = (0, import_react2.useMemo)(() => {
1507
+ if (!asObject) return null;
1508
+ return cards.reduce((acc, card) => {
1509
+ acc[card.id] = card;
1510
+ return acc;
1511
+ }, {});
1512
+ }, [asObject, cards]);
1513
+ return {
1514
+ cards,
1515
+ cardsObject,
1516
+ cardsQueries: queries
1517
+ };
1518
+ }
1519
+ function useCreateCard() {
1520
+ const { queryClient } = useSpeakableApi();
1521
+ const mutationCreateCard = (0, import_react_query3.useMutation)({
1522
+ mutationFn: createCard,
1523
+ onSuccess: (cardCreated) => {
1524
+ queryClient.invalidateQueries({ queryKey: cardsQueryKeys.one({ cardId: cardCreated.id }) });
1525
+ }
1526
+ });
1527
+ return {
1528
+ mutationCreateCard
1529
+ };
1530
+ }
1531
+ function useCreateCards() {
1532
+ const mutationCreateCards = (0, import_react_query3.useMutation)({
1533
+ mutationFn: createCards
1534
+ });
1535
+ return {
1536
+ mutationCreateCards
1537
+ };
1538
+ }
1539
+ function getCardFromCache({
1540
+ cardId,
1541
+ queryClient
1542
+ }) {
1543
+ return queryClient.getQueryData(cardsQueryKeys.one({ cardId }));
1544
+ }
1545
+ function updateCardInCache({
1546
+ cardId,
1547
+ card,
1548
+ queryClient
1549
+ }) {
1550
+ queryClient.setQueryData(cardsQueryKeys.one({ cardId }), card);
1551
+ }
1552
+ function useGetCard({ cardId, enabled = true }) {
1553
+ const query = (0, import_react_query3.useQuery)({
1554
+ queryKey: cardsQueryKeys.one({ cardId }),
1555
+ queryFn: () => getCard({ cardId }),
1556
+ enabled: enabled && !!cardId
1557
+ });
1558
+ return query;
1559
+ }
1560
+
1561
+ // src/domains/sets/set.hooks.ts
1562
+ var import_react_query4 = require("@tanstack/react-query");
1563
+
1564
+ // src/domains/sets/set.constants.ts
1565
+ var SETS_COLLECTION = "sets";
1566
+ var refsSetsFirestore = {
1567
+ allSets: SETS_COLLECTION,
1568
+ set: (id) => `${SETS_COLLECTION}/${id}`
1569
+ };
1570
+
1571
+ // src/domains/sets/services/get-set.service.ts
1572
+ async function _getSet({ setId }) {
1573
+ const response = await api.getDoc(refsSetsFirestore.set(setId));
1574
+ return response.data;
1575
+ }
1576
+ var getSet = withErrorHandler(_getSet, "getSet");
1577
+
1578
+ // src/domains/sets/set.hooks.ts
1579
+ var setsQueryKeys = {
1580
+ all: ["sets"],
1581
+ one: (params) => [...setsQueryKeys.all, params.setId]
1582
+ };
1583
+ var useSet = ({ setId, enabled }) => {
1584
+ return (0, import_react_query4.useQuery)({
1585
+ queryKey: setsQueryKeys.one({ setId }),
1586
+ queryFn: () => getSet({ setId }),
1587
+ enabled: setId !== void 0 && setId !== "" && enabled
1588
+ });
1589
+ };
1590
+ function getSetFromCache({
1591
+ setId,
1592
+ queryClient
1593
+ }) {
1594
+ if (!setId) return null;
1595
+ return queryClient.getQueryData(setsQueryKeys.one({ setId }));
1596
+ }
1597
+ function updateSetInCache({
1598
+ set,
1599
+ queryClient
1600
+ }) {
1601
+ const { id, ...setData } = set;
1602
+ queryClient.setQueryData(setsQueryKeys.one({ setId: id }), setData);
1603
+ }
1604
+ //# sourceMappingURL=hooks.cjs.map