@speakableio/core 1.0.59 → 1.0.60

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.
@@ -0,0 +1,3888 @@
1
+ // src/providers/SpeakableProvider.tsx
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var FsCtx = createContext(null);
5
+ function SpeakableProvider({
6
+ user,
7
+ children,
8
+ queryClient,
9
+ permissions,
10
+ fsClient
11
+ }) {
12
+ const [speakableApi, setSpeakableApi] = useState(null);
13
+ useEffect(() => {
14
+ setSpeakableApi(fsClient);
15
+ }, [fsClient]);
16
+ if (!speakableApi) return null;
17
+ return /* @__PURE__ */ jsx(
18
+ FsCtx.Provider,
19
+ {
20
+ value: {
21
+ speakableApi,
22
+ queryClient,
23
+ user,
24
+ permissions
25
+ },
26
+ children
27
+ }
28
+ );
29
+ }
30
+ function useSpeakableApi() {
31
+ const ctx = useContext(FsCtx);
32
+ if (!ctx) throw new Error("useSpeakableApi must be used within a SpeakableProvider");
33
+ return ctx;
34
+ }
35
+
36
+ // src/utils/error-handler.ts
37
+ var ServiceError = class extends Error {
38
+ constructor(message, originalError, code) {
39
+ super(message);
40
+ this.originalError = originalError;
41
+ this.code = code;
42
+ this.name = "ServiceError";
43
+ }
44
+ };
45
+ function withErrorHandler(fn, serviceName) {
46
+ return async (...args) => {
47
+ try {
48
+ return await fn(...args);
49
+ } catch (error) {
50
+ console.error(`Service ${serviceName} failed with args:`, args);
51
+ console.error("Error details:", error);
52
+ if (error instanceof Error && "code" in error) {
53
+ const firebaseError = error;
54
+ throw new ServiceError(
55
+ `Error in ${serviceName}: ${firebaseError.message}`,
56
+ error,
57
+ firebaseError.code
58
+ );
59
+ }
60
+ if (error instanceof Error) {
61
+ throw new ServiceError(`Error in ${serviceName}: ${error.message}`, error);
62
+ }
63
+ throw new ServiceError(`Unknown error in ${serviceName}`, error);
64
+ }
65
+ };
66
+ }
67
+
68
+ // src/lib/firebase/api.ts
69
+ var FirebaseAPI = class _FirebaseAPI {
70
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
71
+ constructor() {
72
+ this.config = null;
73
+ }
74
+ static getInstance() {
75
+ if (!_FirebaseAPI.instance) {
76
+ _FirebaseAPI.instance = new _FirebaseAPI();
77
+ }
78
+ return _FirebaseAPI.instance;
79
+ }
80
+ initialize(config) {
81
+ this.config = config;
82
+ }
83
+ get db() {
84
+ if (!this.config) throw new Error("Firebase API not initialized");
85
+ return this.config.db;
86
+ }
87
+ get helpers() {
88
+ if (!this.config) throw new Error("Firebase API not initialized");
89
+ return this.config.helpers;
90
+ }
91
+ get httpsCallable() {
92
+ var _a;
93
+ return (_a = this.config) == null ? void 0 : _a.httpsCallable;
94
+ }
95
+ logEvent(name, data) {
96
+ var _a;
97
+ (_a = this.config) == null ? void 0 : _a.logEvent(name, data);
98
+ }
99
+ accessQueryConstraints() {
100
+ const { query: query2, orderBy: orderBy2, limit: limit2, startAt: startAt2, startAfter: startAfter2, endAt: endAt2, endBefore: endBefore2, where: where2, increment: increment2 } = this.helpers;
101
+ return {
102
+ query: query2,
103
+ orderBy: orderBy2,
104
+ limit: limit2,
105
+ startAt: startAt2,
106
+ startAfter: startAfter2,
107
+ endAt: endAt2,
108
+ endBefore: endBefore2,
109
+ where: where2,
110
+ increment: increment2
111
+ };
112
+ }
113
+ accessHelpers() {
114
+ const { doc: doc2, collection: collection2, writeBatch: writeBatch2, serverTimestamp: serverTimestamp2, setDoc: setDoc2 } = this.helpers;
115
+ return {
116
+ doc: (path) => doc2(this.db, path),
117
+ collection: (path) => collection2(this.db, path),
118
+ writeBatch: () => writeBatch2(this.db),
119
+ serverTimestamp: serverTimestamp2,
120
+ setDoc: setDoc2
121
+ };
122
+ }
123
+ async getDoc(path) {
124
+ const { getDoc: getDoc2, doc: doc2 } = this.helpers;
125
+ const docRef = doc2(this.db, path);
126
+ const docSnap = await getDoc2(docRef);
127
+ const data = docSnap.exists() ? {
128
+ ...docSnap.data(),
129
+ id: docSnap.id
130
+ } : null;
131
+ return {
132
+ id: docSnap.id,
133
+ data
134
+ };
135
+ }
136
+ async getDocs(path, ...queryConstraints) {
137
+ const { getDocs: getDocs2, query: query2, collection: collection2 } = this.helpers;
138
+ const collectionRef = collection2(this.db, path);
139
+ const q = queryConstraints.length > 0 ? query2(collectionRef, ...queryConstraints) : collectionRef;
140
+ const querySnapshot = await getDocs2(q);
141
+ const data = querySnapshot.docs.map((doc2) => ({
142
+ data: doc2.data(),
143
+ id: doc2.id
144
+ }));
145
+ return {
146
+ data,
147
+ querySnapshot,
148
+ empty: querySnapshot.empty
149
+ };
150
+ }
151
+ async addDoc(path, data) {
152
+ const { addDoc: addDoc2, collection: collection2 } = this.helpers;
153
+ const collectionRef = collection2(this.db, path);
154
+ const docRef = await addDoc2(collectionRef, data);
155
+ return {
156
+ ...data,
157
+ id: docRef.id
158
+ };
159
+ }
160
+ async setDoc(path, data, options = {}) {
161
+ const { setDoc: setDoc2, doc: doc2 } = this.helpers;
162
+ const docRef = doc2(this.db, path);
163
+ await setDoc2(docRef, data, options);
164
+ }
165
+ async updateDoc(path, data) {
166
+ const { updateDoc: updateDoc2, doc: doc2 } = this.helpers;
167
+ const docRef = doc2(this.db, path);
168
+ await updateDoc2(docRef, data);
169
+ }
170
+ async deleteDoc(path) {
171
+ const { deleteDoc: deleteDoc2, doc: doc2 } = this.helpers;
172
+ const docRef = doc2(this.db, path);
173
+ await deleteDoc2(docRef);
174
+ }
175
+ async runTransaction(updateFunction) {
176
+ const { runTransaction: runTransaction2 } = this.helpers;
177
+ return runTransaction2(this.db, updateFunction);
178
+ }
179
+ async runBatch(operations) {
180
+ const { writeBatch: writeBatch2 } = this.helpers;
181
+ const batch = writeBatch2(this.db);
182
+ await Promise.all(operations.map((op) => op()));
183
+ await batch.commit();
184
+ }
185
+ writeBatch() {
186
+ const { writeBatch: writeBatch2 } = this.helpers;
187
+ const batch = writeBatch2(this.db);
188
+ return batch;
189
+ }
190
+ };
191
+ var api = FirebaseAPI.getInstance();
192
+
193
+ // src/domains/assignment/assignment.constants.ts
194
+ var ASSIGNMENT_ANALYTICS_TYPES = [
195
+ "macro" /* Macro */,
196
+ "gradebook" /* Gradebook */,
197
+ "cards" /* Cards */,
198
+ "student" /* Student */,
199
+ "student_summary" /* StudentSummary */
200
+ ];
201
+ var ASSIGNMENTS_COLLECTION = "assignments";
202
+ var ANALYTICS_SUBCOLLECTION = "analytics";
203
+ var SCORES_SUBCOLLECTION = "scores";
204
+ var refsAssignmentFiresotre = {
205
+ allAssignments: () => ASSIGNMENTS_COLLECTION,
206
+ assignment: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}`,
207
+ assignmentAllAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}`,
208
+ assignmentAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}/${params.type}`,
209
+ assignmentScores: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${SCORES_SUBCOLLECTION}/${params.userId}`
210
+ };
211
+
212
+ // src/domains/assignment/services/get-assignments-score.service.ts
213
+ var _getAssignmentScores = async ({
214
+ assignmentId,
215
+ analyticType = "macro" /* Macro */,
216
+ studentId,
217
+ currentUserId
218
+ }) => {
219
+ if (analyticType === "student" /* Student */) {
220
+ const path = refsAssignmentFiresotre.assignmentScores({
221
+ id: assignmentId,
222
+ userId: currentUserId
223
+ });
224
+ const response = await api.getDoc(path);
225
+ return { scores: response.data, id: assignmentId };
226
+ }
227
+ if (analyticType === "student_summary" /* StudentSummary */ && studentId) {
228
+ const path = refsAssignmentFiresotre.assignmentScores({
229
+ id: assignmentId,
230
+ userId: studentId
231
+ });
232
+ const response = await api.getDoc(path);
233
+ return { scores: response.data, id: assignmentId };
234
+ }
235
+ if (analyticType !== "all" /* All */ && ASSIGNMENT_ANALYTICS_TYPES.includes(analyticType)) {
236
+ const ref = refsAssignmentFiresotre.assignmentAnalytics({
237
+ id: assignmentId,
238
+ type: analyticType
239
+ });
240
+ const docData = await api.getDoc(ref);
241
+ return { scores: docData.data, id: assignmentId };
242
+ } else if (analyticType === "all" /* All */) {
243
+ const ref = refsAssignmentFiresotre.assignmentAllAnalytics({ id: assignmentId });
244
+ const response = await api.getDocs(ref);
245
+ const data = response.data.reduce((acc, curr) => {
246
+ acc[curr.id] = curr;
247
+ return acc;
248
+ }, {});
249
+ return { scores: data, id: assignmentId };
250
+ }
251
+ };
252
+ var getAssignmentScores = withErrorHandler(_getAssignmentScores, "getAssignmentScores");
253
+
254
+ // src/domains/assignment/services/attach-score-assignment.service.ts
255
+ var _attachScoresAssignment = async ({
256
+ assignments,
257
+ analyticType,
258
+ studentId,
259
+ currentUserId
260
+ }) => {
261
+ const scoresPromises = assignments.map((a) => {
262
+ return getAssignmentScores({
263
+ assignmentId: a.id,
264
+ analyticType,
265
+ studentId,
266
+ currentUserId
267
+ });
268
+ });
269
+ const scores = await Promise.all(scoresPromises);
270
+ const scoresObject = scores.reduce((acc, curr) => {
271
+ acc[curr.id] = curr.scores;
272
+ return acc;
273
+ }, {});
274
+ const assignmentsWithScores = assignments.map((a) => {
275
+ var _a;
276
+ return {
277
+ ...a,
278
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/ban-ts-comment
279
+ // @ts-ignore
280
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
281
+ scores: (_a = scoresObject[a.id]) != null ? _a : null
282
+ };
283
+ });
284
+ return assignmentsWithScores;
285
+ };
286
+ var attachScoresAssignment = withErrorHandler(
287
+ _attachScoresAssignment,
288
+ "attachScoresAssignment"
289
+ );
290
+
291
+ // src/domains/assignment/services/get-all-assignment.service.ts
292
+ async function _getAllAssignments() {
293
+ const path = refsAssignmentFiresotre.allAssignments();
294
+ const response = await api.getDocs(path);
295
+ return response.data;
296
+ }
297
+ var getAllAssignments = withErrorHandler(_getAllAssignments, "getAllAssignments");
298
+
299
+ // src/domains/assignment/utils/check-assignment-availability.ts
300
+ import dayjs from "dayjs";
301
+ var checkAssignmentAvailability = (scheduledTime) => {
302
+ if (!scheduledTime) return true;
303
+ const scheduledDate = typeof scheduledTime === "string" ? dayjs(scheduledTime) : dayjs(scheduledTime.toDate());
304
+ if (!scheduledDate.isValid()) return true;
305
+ return dayjs().isAfter(scheduledDate);
306
+ };
307
+
308
+ // src/domains/assignment/services/get-assignment.service.ts
309
+ async function _getAssignment(params) {
310
+ var _a;
311
+ const path = refsAssignmentFiresotre.assignment({ id: params.assignmentId });
312
+ const response = await api.getDoc(path);
313
+ if (!response.data) return null;
314
+ const assignment = response.data;
315
+ const isAvailable = checkAssignmentAvailability(assignment.scheduledTime);
316
+ const assignmentWithId = {
317
+ ...assignment,
318
+ isAvailable,
319
+ id: params.assignmentId,
320
+ scheduledTime: (_a = assignment.scheduledTime) != null ? _a : null
321
+ };
322
+ if (params.analyticType) {
323
+ const assignmentsWithScores = await attachScoresAssignment({
324
+ assignments: [assignmentWithId],
325
+ analyticType: params.analyticType,
326
+ currentUserId: params.currentUserId
327
+ });
328
+ return assignmentsWithScores.length > 0 ? assignmentsWithScores[0] : assignmentWithId;
329
+ }
330
+ return assignmentWithId;
331
+ }
332
+ var getAssignment = withErrorHandler(_getAssignment, "getAssignment");
333
+
334
+ // src/domains/assignment/assignment.repo.ts
335
+ var createAssignmentRepo = () => {
336
+ return {
337
+ getAssignment,
338
+ attachScoresAssignment,
339
+ getAssignmentScores,
340
+ getAllAssignments
341
+ };
342
+ };
343
+
344
+ // src/domains/assignment/hooks/assignment.hooks.ts
345
+ import { useQuery } from "@tanstack/react-query";
346
+ var assignmentQueryKeys = {
347
+ all: ["assignments"],
348
+ byId: (id) => [...assignmentQueryKeys.all, id],
349
+ list: () => [...assignmentQueryKeys.all, "list"]
350
+ };
351
+ function useAssignment({
352
+ assignmentId,
353
+ enabled = true,
354
+ analyticType,
355
+ userId
356
+ }) {
357
+ const { speakableApi } = useSpeakableApi();
358
+ return useQuery({
359
+ queryKey: assignmentQueryKeys.byId(assignmentId),
360
+ queryFn: () => speakableApi.assignmentRepo.getAssignment({
361
+ assignmentId,
362
+ analyticType,
363
+ currentUserId: userId
364
+ }),
365
+ enabled
366
+ });
367
+ }
368
+
369
+ // src/domains/assignment/hooks/score-hooks.ts
370
+ import { useMutation, useQuery as useQuery2 } from "@tanstack/react-query";
371
+
372
+ // src/utils/debounce.utils.ts
373
+ function debounce(func, waitFor) {
374
+ let timeoutId;
375
+ return (...args) => new Promise((resolve, reject) => {
376
+ if (timeoutId) {
377
+ clearTimeout(timeoutId);
378
+ }
379
+ timeoutId = setTimeout(async () => {
380
+ try {
381
+ const result = await func(...args);
382
+ resolve(result);
383
+ } catch (error) {
384
+ reject(error);
385
+ }
386
+ }, waitFor);
387
+ });
388
+ }
389
+
390
+ // src/lib/tanstack/handle-optimistic-update-query.ts
391
+ var handleOptimisticUpdate = async ({
392
+ queryClient,
393
+ queryKey,
394
+ newData
395
+ }) => {
396
+ await queryClient.cancelQueries({
397
+ queryKey
398
+ });
399
+ const previousData = queryClient.getQueryData(queryKey);
400
+ if (previousData === void 0) {
401
+ queryClient.setQueryData(queryKey, newData);
402
+ } else {
403
+ queryClient.setQueryData(queryKey, { ...previousData, ...newData });
404
+ }
405
+ return { previousData };
406
+ };
407
+
408
+ // src/constants/speakable-plans.ts
409
+ var FEEDBACK_PLANS = {
410
+ FEEDBACK_TRANSCRIPT: "FEEDBACK_TRANSCRIPT",
411
+ // Transcript from the audio
412
+ FEEDBACK_SUMMARY: "FEEDBACK_SUMMARY",
413
+ // Chatty summary (Free plan)
414
+ FEEDBACK_GRAMMAR_INSIGHTS: "FEEDBACK_GRAMMAR_INSIGHTS",
415
+ // Grammar insights
416
+ FEEDBACK_SUGGESTED_RESPONSE: "FEEDBACK_SUGGESTED_RESPONSE",
417
+ // Suggested Response
418
+ FEEDBACK_RUBRIC: "FEEDBACK_RUBRIC",
419
+ // Suggested Response
420
+ FEEDBACK_GRADING_STANDARDS: "FEEDBACK_GRADING_STANDARDS",
421
+ // ACTFL / WIDA Estimate
422
+ FEEDBACK_TARGET_LANGUAGE: "FEEDBACK_TARGET_LANGUAGE",
423
+ // Ability to set the feedback language to the target language of the student
424
+ FEEDBACK_DISABLE_ALLOW_RETRIES: "FEEDBACK_DISABLE_ALLOW_RETRIES"
425
+ // Turn of allow retries
426
+ };
427
+ var AUTO_GRADING_PLANS = {
428
+ AUTO_GRADING_PASS_FAIL: "AUTO_GRADING_PASS_FAIL",
429
+ // Pass / fail grading
430
+ AUTO_GRADING_RUBRICS: "AUTO_GRADING_RUBRICS",
431
+ // Autograded rubrics
432
+ AUTO_GRADING_STANDARDS_BASED: "AUTO_GRADING_STANDARDS_BASED"
433
+ // Standards based grading
434
+ };
435
+ var AI_ASSISTANT_PLANS = {
436
+ AI_ASSISTANT_DOCUMENT_UPLOADS: "AI_ASSISTANT_DOCUMENT_UPLOADS",
437
+ // Allow document uploading
438
+ AI_ASSISTANT_UNLIMITED_USE: "AI_ASSISTANT_UNLIMITED_USE"
439
+ // Allow unlimited use of AI assistant. Otherwise, limits are used.
440
+ };
441
+ var ASSIGNMENT_SETTINGS_PLANS = {
442
+ ASSESSMENTS: "ASSESSMENTS",
443
+ // Ability to create assessment assignment types
444
+ GOOGLE_CLASSROOM_GRADE_PASSBACK: "GOOGLE_CLASSROOM_GRADE_PASSBACK"
445
+ // Assignment scores can sync with classroom
446
+ };
447
+ var ANALYTICS_PLANS = {
448
+ ANALYTICS_GRADEBOOK: "ANALYTICS_GRADEBOOK",
449
+ // Access to the gradebook page
450
+ ANALYTICS_CLASSROOM_ANALYTICS: "ANALYTICS_CLASSROOM_ANALYTICS",
451
+ // Access to the classroom analytics page
452
+ ANALYTICS_STUDENT_PROGRESS_REPORTS: "ANALYTICS_STUDENT_PROGRESS_REPORTS",
453
+ // Access to the panel that shows an individual student's progress and assignments
454
+ ANALYTICS_ASSIGNMENT_RESULTS: "ANALYTICS_ASSIGNMENT_RESULTS",
455
+ // Access to the assigment RESULTS page
456
+ ANALYTICS_ORGANIZATION: "ANALYTICS_ORGANIZATION"
457
+ // Access to the organization analytics panel (for permitted admins)
458
+ };
459
+ var SPACES_PLANS = {
460
+ SPACES_CREATE_SPACE: "SPACES_CREATE_SPACE",
461
+ // Ability to create spaces
462
+ SPACES_CHECK_POINTS: "SPACES_CHECK_POINTS"
463
+ // Feature not available yet. Ability to create checkpoints for spaces for data aggregation
464
+ };
465
+ var DISCOVER_PLANS = {
466
+ DISCOVER_ORGANIZATION_LIBRARY: "DISCOVER_ORGANIZATION_LIBRARY"
467
+ // Access to the organizations shared library
468
+ };
469
+ var MEDIA_AREA_PLANS = {
470
+ MEDIA_AREA_DOCUMENT_UPLOAD: "MEDIA_AREA_DOCUMENT_UPLOAD",
471
+ MEDIA_AREA_AUDIO_FILES: "MEDIA_AREA_AUDIO_FILES"
472
+ };
473
+ var FREE_PLAN = [];
474
+ var TEACHER_PRO_PLAN = [
475
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
476
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
477
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
478
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
479
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
480
+ SPACES_PLANS.SPACES_CREATE_SPACE
481
+ // AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
482
+ ];
483
+ var SCHOOL_STARTER = [
484
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
485
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
486
+ FEEDBACK_PLANS.FEEDBACK_GRAMMAR_INSIGHTS,
487
+ FEEDBACK_PLANS.FEEDBACK_SUGGESTED_RESPONSE,
488
+ FEEDBACK_PLANS.FEEDBACK_RUBRIC,
489
+ FEEDBACK_PLANS.FEEDBACK_GRADING_STANDARDS,
490
+ FEEDBACK_PLANS.FEEDBACK_DISABLE_ALLOW_RETRIES,
491
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
492
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
493
+ AUTO_GRADING_PLANS.AUTO_GRADING_RUBRICS,
494
+ AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
495
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_DOCUMENT_UPLOADS,
496
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_UNLIMITED_USE,
497
+ // ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS,
498
+ ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK,
499
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
500
+ ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS,
501
+ ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS,
502
+ // ANALYTICS_PLANS.ANALYTICS_ORGANIZATION,
503
+ SPACES_PLANS.SPACES_CREATE_SPACE,
504
+ SPACES_PLANS.SPACES_CHECK_POINTS,
505
+ // DISCOVER_PLANS.DISCOVER_ORGANIZATION_LIBRARY,
506
+ MEDIA_AREA_PLANS.MEDIA_AREA_DOCUMENT_UPLOAD,
507
+ MEDIA_AREA_PLANS.MEDIA_AREA_AUDIO_FILES
508
+ ];
509
+ var ORGANIZATION_PLAN = [
510
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
511
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
512
+ FEEDBACK_PLANS.FEEDBACK_GRAMMAR_INSIGHTS,
513
+ FEEDBACK_PLANS.FEEDBACK_SUGGESTED_RESPONSE,
514
+ FEEDBACK_PLANS.FEEDBACK_RUBRIC,
515
+ FEEDBACK_PLANS.FEEDBACK_GRADING_STANDARDS,
516
+ FEEDBACK_PLANS.FEEDBACK_DISABLE_ALLOW_RETRIES,
517
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
518
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
519
+ AUTO_GRADING_PLANS.AUTO_GRADING_RUBRICS,
520
+ AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
521
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_DOCUMENT_UPLOADS,
522
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_UNLIMITED_USE,
523
+ ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS,
524
+ ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK,
525
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
526
+ ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS,
527
+ ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS,
528
+ ANALYTICS_PLANS.ANALYTICS_ORGANIZATION,
529
+ SPACES_PLANS.SPACES_CREATE_SPACE,
530
+ SPACES_PLANS.SPACES_CHECK_POINTS,
531
+ DISCOVER_PLANS.DISCOVER_ORGANIZATION_LIBRARY,
532
+ MEDIA_AREA_PLANS.MEDIA_AREA_DOCUMENT_UPLOAD,
533
+ MEDIA_AREA_PLANS.MEDIA_AREA_AUDIO_FILES
534
+ ];
535
+ var SpeakablePlanTypes = {
536
+ basic: "basic",
537
+ teacher_pro: "teacher_pro",
538
+ school_starter: "school_starter",
539
+ organization: "organization",
540
+ // OLD PLANS
541
+ starter: "starter",
542
+ growth: "growth",
543
+ professional: "professional"
544
+ };
545
+ var SpeakablePermissionsMap = {
546
+ [SpeakablePlanTypes.basic]: FREE_PLAN,
547
+ [SpeakablePlanTypes.starter]: TEACHER_PRO_PLAN,
548
+ [SpeakablePlanTypes.teacher_pro]: TEACHER_PRO_PLAN,
549
+ [SpeakablePlanTypes.growth]: ORGANIZATION_PLAN,
550
+ [SpeakablePlanTypes.professional]: ORGANIZATION_PLAN,
551
+ [SpeakablePlanTypes.organization]: ORGANIZATION_PLAN,
552
+ [SpeakablePlanTypes.school_starter]: SCHOOL_STARTER
553
+ };
554
+ var SpeakablePlanHierarchy = [
555
+ SpeakablePlanTypes.basic,
556
+ SpeakablePlanTypes.starter,
557
+ SpeakablePlanTypes.teacher_pro,
558
+ SpeakablePlanTypes.growth,
559
+ SpeakablePlanTypes.professional,
560
+ SpeakablePlanTypes.school_starter,
561
+ SpeakablePlanTypes.organization
562
+ ];
563
+
564
+ // src/hooks/usePermissions.ts
565
+ var usePermissions = () => {
566
+ const { permissions } = useSpeakableApi();
567
+ const has = (permission) => {
568
+ var _a;
569
+ return (_a = permissions.permissions) == null ? void 0 : _a.includes(permission);
570
+ };
571
+ return {
572
+ plan: permissions.plan,
573
+ permissionsLoaded: permissions.loaded,
574
+ isStripePlan: permissions.isStripePlan,
575
+ refreshDate: permissions.refreshDate,
576
+ isInstitutionPlan: permissions.isInstitutionPlan,
577
+ subscriptionId: permissions.subscriptionId,
578
+ contact: permissions.contact,
579
+ hasGradebook: has(ANALYTICS_PLANS.ANALYTICS_GRADEBOOK),
580
+ hasGoogleClassroomGradePassback: has(ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK),
581
+ hasAssessments: has(ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS),
582
+ hasSectionAnalytics: has(ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS),
583
+ hasStudentReports: has(ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS),
584
+ permissions: permissions || [],
585
+ hasStudentPortfolios: permissions.hasStudentPortfolios,
586
+ isFreeOrgTrial: permissions.type === "free_org_trial",
587
+ freeOrgTrialExpired: permissions.freeOrgTrialExpired
588
+ };
589
+ };
590
+ var usePermissions_default = usePermissions;
591
+
592
+ // src/domains/notification/notification.constants.ts
593
+ var SPEAKABLE_NOTIFICATIONS = {
594
+ NEW_ASSIGNMENT: "new_assignment",
595
+ ASSESSMENT_SUBMITTED: "assessment_submitted",
596
+ ASSESSMENT_SCORED: "assessment_scored",
597
+ NEW_COMMENT: "NEW_COMMENT"
598
+ };
599
+ var SpeakableNotificationTypes = {
600
+ NEW_ASSIGNMENT: "NEW_ASSIGNMENT",
601
+ FEEDBACK_FROM_TEACHER: "FEEDBACK_FROM_TEACHER",
602
+ MESSAGE_FROM_STUDENT: "MESSAGE_FROM_STUDENT",
603
+ PHRASE_MARKED_CORRECT: "PHRASE_MARKED_CORRECT",
604
+ STUDENT_PROGRESS: "STUDENT_PROGRESS",
605
+ PLAYLIST_FOLLOWERS: "PLAYLIST_FOLLOWERS",
606
+ PLAYLIST_PLAYS: "PLAYLIST_PLAYS",
607
+ // New notifications
608
+ ASSESSMENT_SUBMITTED: "ASSESSMENT_SUBMITTED",
609
+ // Notification FOR TEACHER when student submits assessment
610
+ ASSESSMENT_SCORED: "ASSESSMENT_SCORED",
611
+ // Notification FOR STUDENT when teacher scores assessment
612
+ // Comment
613
+ NEW_COMMENT: "NEW_COMMENT"
614
+ };
615
+
616
+ // src/domains/notification/services/create-notification.service.ts
617
+ import dayjs2 from "dayjs";
618
+
619
+ // src/constants/web.constants.ts
620
+ var WEB_BASE_URL = "https://app.speakable.io";
621
+
622
+ // src/domains/notification/services/send-notification.service.ts
623
+ var _sendNotification = async (sendTo, notification) => {
624
+ var _a, _b, _c;
625
+ const results = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "createNotificationV2")) == null ? void 0 : _c({
626
+ sendTo,
627
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
628
+ notification
629
+ }));
630
+ return results;
631
+ };
632
+ var sendNotification = withErrorHandler(_sendNotification, "sendNotification");
633
+
634
+ // src/domains/notification/services/create-notification.service.ts
635
+ var createNotification = async ({
636
+ data,
637
+ type,
638
+ userId,
639
+ profile
640
+ }) => {
641
+ let result;
642
+ switch (type) {
643
+ case SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT:
644
+ result = await handleAssignNotifPromise({ data, profile });
645
+ break;
646
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED:
647
+ result = await createAssessmentSubmissionNotification({
648
+ data,
649
+ profile,
650
+ userId
651
+ });
652
+ break;
653
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED:
654
+ result = await createAssessmentScoredNotification({
655
+ data,
656
+ profile
657
+ });
658
+ break;
659
+ default:
660
+ result = null;
661
+ break;
662
+ }
663
+ return result;
664
+ };
665
+ var handleAssignNotifPromise = async ({
666
+ data: assignments,
667
+ profile
668
+ }) => {
669
+ if (!assignments.length) return;
670
+ try {
671
+ const notifsPromises = assignments.map(async (assignment) => {
672
+ const {
673
+ section,
674
+ section: { members },
675
+ ...rest
676
+ } = assignment;
677
+ if (!section || !members) throw new Error("Invalid assignment data");
678
+ const data = { section, sendTo: members, assignment: { ...rest } };
679
+ return createNewAssignmentNotification({ data, profile });
680
+ });
681
+ await Promise.all(notifsPromises);
682
+ return {
683
+ success: true,
684
+ message: "Assignment notifications sent successfully"
685
+ };
686
+ } catch (error) {
687
+ console.error("Error in handleAssignNotifPromise:", error);
688
+ throw error;
689
+ }
690
+ };
691
+ var createNewAssignmentNotification = async ({
692
+ data,
693
+ profile
694
+ }) => {
695
+ var _a;
696
+ const { assignment, sendTo } = data;
697
+ const teacherName = profile.displayName || "Your teacher";
698
+ const dueDate = assignment.dueDateTimestamp ? dayjs2(assignment.dueDateTimestamp.toDate()).format("MMM Do") : null;
699
+ const results = await sendNotification(sendTo, {
700
+ courseId: assignment.courseId,
701
+ type: SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT,
702
+ senderName: teacherName,
703
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
704
+ messagePreview: `A new assignment "${assignment.name}" is now available. ${dueDate ? `Due ${dueDate}` : ""}`,
705
+ title: "New Assignment Available!",
706
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url
707
+ });
708
+ return results;
709
+ };
710
+ var createAssessmentSubmissionNotification = async ({
711
+ data: assignment,
712
+ profile,
713
+ userId
714
+ }) => {
715
+ var _a;
716
+ const studentName = profile.displayName || "Your student";
717
+ const results = await sendNotification(assignment.owners, {
718
+ courseId: assignment.courseId,
719
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED,
720
+ link: `${WEB_BASE_URL}/a/${assignment.id}?studentId=${userId}`,
721
+ title: `Assessment Submitted!`,
722
+ senderName: studentName,
723
+ messagePreview: `${studentName} has submitted the assessment "${assignment.name}"`,
724
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url
725
+ });
726
+ return results;
727
+ };
728
+ var createAssessmentScoredNotification = async ({
729
+ data,
730
+ profile
731
+ }) => {
732
+ var _a, _b, _c, _d, _e;
733
+ const { assignment, sendTo } = data;
734
+ const teacherName = profile.displayName || "Your teacher";
735
+ const title = `${assignment.isAssessment ? "Assessment" : "Assignment"} Reviewed!`;
736
+ const messagePreview = `Your ${assignment.isAssessment ? "assessment" : "assignment"} has been reviewed by your teacher. Click to view the feedback.`;
737
+ const results = await sendNotification(sendTo, {
738
+ courseId: assignment.courseId,
739
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED,
740
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
741
+ title,
742
+ messagePreview,
743
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url,
744
+ senderName: teacherName
745
+ });
746
+ await ((_e = (_c = (_b = api).httpsCallable) == null ? void 0 : _c.call(_b, "sendAssessmentScoredEmail")) == null ? void 0 : _e({
747
+ assessmentTitle: assignment.name,
748
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
749
+ senderImage: ((_d = profile.image) == null ? void 0 : _d.url) || "",
750
+ studentId: sendTo[0],
751
+ teacherName: profile.displayName
752
+ }));
753
+ return results;
754
+ };
755
+
756
+ // src/domains/notification/hooks/notification.hooks.ts
757
+ var notificationQueryKeys = {
758
+ all: ["notifications"],
759
+ byId: (id) => [...notificationQueryKeys.all, id]
760
+ };
761
+ var useCreateNotification = () => {
762
+ const { user, queryClient } = useSpeakableApi();
763
+ const handleCreateNotifications = async (type, data) => {
764
+ var _a, _b;
765
+ const result = await createNotification({
766
+ type,
767
+ userId: user.auth.uid,
768
+ profile: (_a = user == null ? void 0 : user.profile) != null ? _a : {},
769
+ data
770
+ });
771
+ queryClient.invalidateQueries({
772
+ queryKey: notificationQueryKeys.byId((_b = user == null ? void 0 : user.auth.uid) != null ? _b : "")
773
+ });
774
+ return result;
775
+ };
776
+ return {
777
+ createNotification: handleCreateNotifications
778
+ };
779
+ };
780
+
781
+ // src/hooks/useGoogleClassroom.ts
782
+ var useGoogleClassroom = () => {
783
+ const submitAssignmentToGoogleClassroom = async ({
784
+ assignment,
785
+ scores,
786
+ googleUserId = null
787
+ // optional to override the user's googleUserId
788
+ }) => {
789
+ var _a, _b, _c;
790
+ try {
791
+ const { googleClassroomUserId = null } = scores;
792
+ const googleId = googleUserId || googleClassroomUserId;
793
+ if (!googleId)
794
+ return {
795
+ error: true,
796
+ message: "No Google Classroom ID found"
797
+ };
798
+ const { courseWorkId, maxPoints, owners, courseId } = assignment;
799
+ const draftGrade = (scores == null ? void 0 : scores.score) ? (scores == null ? void 0 : scores.score) / 100 * maxPoints : 0;
800
+ const result = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssignmentToGoogleClassroomV2")) == null ? void 0 : _c({
801
+ teacherId: owners[0],
802
+ courseId,
803
+ courseWorkId,
804
+ userId: googleId,
805
+ draftGrade
806
+ }));
807
+ return result;
808
+ } catch (error) {
809
+ return { error: true, message: error.message };
810
+ }
811
+ };
812
+ return {
813
+ submitAssignmentToGoogleClassroom
814
+ };
815
+ };
816
+
817
+ // src/lib/firebase/firebase-analytics/grading-standard.ts
818
+ var logGradingStandardLog = (data) => {
819
+ var _a, _b, _c;
820
+ if (data.courseId && data.type && data.level) {
821
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
822
+ eventType: data.type || "custom",
823
+ level: data.level,
824
+ courseId: data.courseId
825
+ });
826
+ }
827
+ api.logEvent("logGradingStandard", data);
828
+ };
829
+
830
+ // src/constants/analytics.constants.ts
831
+ var SPEAKABLE_ANALYTICS = {
832
+ VOICE_SUCCESS: "voice_success",
833
+ VOICE_FALLBACK_SUCCESS: "voice_fallback_success",
834
+ VOICE_FALLBACK_FAIL: "voice_fallback_fail",
835
+ VOICE_FAIL: "voice_fail",
836
+ RESPOND_CARD_SUCCESS: "respond_card_success",
837
+ RESPOND_CARD_FAIL: "respond_card_fail",
838
+ RESPOND_WRITE_CARD_SUCCESS: "respond_write_card_success",
839
+ RESPOND_WRITE_CARD_FAIL: "respond_write_card_fail",
840
+ RESPOND_WRITE_CARD_SUBMITTED: "respond_write_card_submitted",
841
+ RESPOND_WRITE_CARD_ERROR: "respond_write_card_error",
842
+ RESPOND_CARD_ERROR: "respond_card_error",
843
+ RESPOND_CARD_SUBMITTED: "respond_card_submitted",
844
+ RESPOND_FREE_PLAN: "respond_free_plan",
845
+ RESPOND_WRITE_FREE_PLAN: "respond_write_free_plan",
846
+ SUBMISSION: "assignment_submitted",
847
+ ASSIGNMENT_STARTED: "assignment_started",
848
+ CREATE_ASSIGNMENT: "create_assignment",
849
+ MC_SUCCESS: "multiple_choice_success",
850
+ MC_FAIL: "multiple_choice_fail",
851
+ MC_ERROR: "multiple_choice_error",
852
+ ACTFL_LEVEL: "actfl_level",
853
+ WIDA_LEVEL: "wida_level",
854
+ // New events
855
+ VIEW_SCORES_MODAL: "view_scores_modal",
856
+ SHORT_ANSWER_SUCCESS: "short_answer_success",
857
+ SHORT_ANSWER_FAIL: "short_answer_fail",
858
+ SHORT_ANSWER_ERROR: "short_answer_error",
859
+ RETRY: "retry",
860
+ MESSAGE_SENT: "message_sent",
861
+ MESSAGE_ERROR: "message_error",
862
+ VIEW_DETAILS_CLICK: "view_details_click",
863
+ TABS_CLICK: "tabs_click",
864
+ VIEW_MEDIA: "view_media",
865
+ VIEW_SCORES: "view_scores",
866
+ VIEW_GRADING_METHOD: "view_grading_method",
867
+ VIEW_FEEDBACK: "view_feedback"
868
+ };
869
+
870
+ // src/lib/firebase/firebase-analytics/assignment.ts
871
+ var logOpenAssignment = (data = {}) => {
872
+ api.logEvent("open_assignment", data);
873
+ };
874
+ var logOpenActivityPreview = (data = {}) => {
875
+ api.logEvent("open_activity_preview", data);
876
+ };
877
+ var logSubmitAssignment = (data = {}) => {
878
+ var _a, _b, _c;
879
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
880
+ eventType: SPEAKABLE_ANALYTICS.SUBMISSION,
881
+ ...data
882
+ });
883
+ api.logEvent(SPEAKABLE_ANALYTICS.SUBMISSION, data);
884
+ };
885
+ var logStartAssignment = (data = {}) => {
886
+ var _a, _b, _c;
887
+ if (data.courseId) {
888
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
889
+ eventType: SPEAKABLE_ANALYTICS.ASSIGNMENT_STARTED,
890
+ ...data
891
+ });
892
+ }
893
+ api.logEvent(SPEAKABLE_ANALYTICS.ASSIGNMENT_STARTED, data);
894
+ };
895
+
896
+ // src/domains/assignment/utils/create-default-score.ts
897
+ var defaultScore = (props) => {
898
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
899
+ const score = {
900
+ progress: 0,
901
+ score: 0,
902
+ startDate: serverTimestamp2(),
903
+ status: "IN_PROGRESS",
904
+ submitted: false,
905
+ cards: {},
906
+ lastPlayed: serverTimestamp2(),
907
+ owners: props.owners,
908
+ userId: props.userId
909
+ };
910
+ if (props.googleClassroomUserId) {
911
+ score.googleClassroomUserId = props.googleClassroomUserId;
912
+ }
913
+ if (props.courseId) {
914
+ score.courseId = props.courseId;
915
+ }
916
+ return score;
917
+ };
918
+
919
+ // src/domains/assignment/score-practice.constants.ts
920
+ var SCORES_PRACTICE_COLLECTION = "users";
921
+ var SCORES_PRACTICE_SUBCOLLECTION = "practice";
922
+ var refsScoresPractice = {
923
+ practiceScores: (params) => `${SCORES_PRACTICE_COLLECTION}/${params.userId}/${SCORES_PRACTICE_SUBCOLLECTION}/${params.setId}`,
924
+ practiceScoreHistoryRefDoc: (params) => `${SCORES_PRACTICE_COLLECTION}/${params.userId}/${SCORES_PRACTICE_SUBCOLLECTION}/${params.setId}/attempts/${params.date}`
925
+ };
926
+
927
+ // src/domains/assignment/services/create-score.service.ts
928
+ var _updateAssignmentGradebookStatus = async ({
929
+ assignmentId,
930
+ userId,
931
+ status,
932
+ score
933
+ }) => {
934
+ var _a, _b, _c;
935
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "updateAssignmentGradebookStatus")) == null ? void 0 : _c({
936
+ assignmentId,
937
+ userId,
938
+ status,
939
+ score
940
+ }));
941
+ };
942
+ var updateAssignmentGradebookStatus = withErrorHandler(
943
+ _updateAssignmentGradebookStatus,
944
+ "updateAssignmentGradebookStatus"
945
+ );
946
+ async function _createScore(params) {
947
+ var _a, _b, _c;
948
+ if (params.isAssignment) {
949
+ const ref = refsAssignmentFiresotre.assignmentScores({
950
+ id: params.activityId,
951
+ userId: params.userId
952
+ });
953
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "updateAssignmentGradebookStatus")) == null ? void 0 : _c({
954
+ assignmentId: params.activityId,
955
+ userId: params.userId,
956
+ status: "IN_PROGRESS",
957
+ score: null
958
+ }));
959
+ await api.setDoc(ref, params.scoreData, { merge: true });
960
+ await updateAssignmentGradebookStatus({
961
+ assignmentId: params.activityId,
962
+ userId: params.userId,
963
+ status: "IN_PROGRESS",
964
+ score: null
965
+ });
966
+ return {
967
+ id: params.userId
968
+ };
969
+ } else {
970
+ const ref = refsScoresPractice.practiceScores({
971
+ userId: params.userId,
972
+ setId: params.activityId
973
+ });
974
+ await api.setDoc(ref, params.scoreData);
975
+ return {
976
+ id: params.userId
977
+ };
978
+ }
979
+ }
980
+ var createScore = withErrorHandler(_createScore, "createScore");
981
+
982
+ // src/domains/assignment/services/get-score.service.ts
983
+ async function getAssignmentScore({
984
+ userId,
985
+ assignment,
986
+ googleClassroomUserId
987
+ }) {
988
+ const path = refsAssignmentFiresotre.assignmentScores({
989
+ id: assignment.id,
990
+ userId
991
+ });
992
+ const response = await api.getDoc(path);
993
+ if (response.data == null) {
994
+ const newScore = {
995
+ ...defaultScore({
996
+ owners: [userId],
997
+ userId,
998
+ courseId: assignment.courseId,
999
+ googleClassroomUserId
1000
+ }),
1001
+ assignmentId: assignment.id
1002
+ };
1003
+ logStartAssignment({
1004
+ courseId: assignment.courseId
1005
+ });
1006
+ const result = await createScore({
1007
+ activityId: assignment.id,
1008
+ userId,
1009
+ isAssignment: true,
1010
+ scoreData: newScore
1011
+ });
1012
+ return {
1013
+ ...newScore,
1014
+ id: result.id
1015
+ };
1016
+ }
1017
+ if (response.data.cards == null) {
1018
+ try {
1019
+ await api.updateDoc(path, { cards: {} });
1020
+ } catch (error) {
1021
+ console.error("Error initializing score cards:", error);
1022
+ }
1023
+ return {
1024
+ ...response.data,
1025
+ cards: {}
1026
+ };
1027
+ }
1028
+ return response.data;
1029
+ }
1030
+ async function getPracticeScore({ userId, setId }) {
1031
+ const path = refsScoresPractice.practiceScores({ userId, setId });
1032
+ const response = await api.getDoc(path);
1033
+ if (response.data == null) {
1034
+ const newScore = {
1035
+ ...defaultScore({
1036
+ owners: [userId],
1037
+ userId
1038
+ }),
1039
+ setId
1040
+ };
1041
+ const result = await createScore({
1042
+ activityId: setId,
1043
+ userId,
1044
+ isAssignment: false,
1045
+ scoreData: newScore
1046
+ });
1047
+ return {
1048
+ ...newScore,
1049
+ id: result.id
1050
+ };
1051
+ }
1052
+ return response.data;
1053
+ }
1054
+ async function _getScore(params) {
1055
+ if (params.isAssignment) {
1056
+ return await getAssignmentScore({
1057
+ userId: params.userId,
1058
+ assignment: {
1059
+ id: params.activityId,
1060
+ courseId: params.courseId
1061
+ },
1062
+ googleClassroomUserId: params.googleClassroomUserId
1063
+ });
1064
+ } else {
1065
+ return await getPracticeScore({
1066
+ userId: params.userId,
1067
+ setId: params.activityId
1068
+ });
1069
+ }
1070
+ }
1071
+ var getScore = withErrorHandler(_getScore, "getScore");
1072
+
1073
+ // src/domains/assignment/utils/calculateScoreAndProgress.ts
1074
+ var calculateScoreAndProgress = (scores, cardsList, weights) => {
1075
+ const filteredScoreCards = Object.keys((scores == null ? void 0 : scores.cards) || {}).reduce(
1076
+ (acc, cardId) => {
1077
+ var _a;
1078
+ const cardScores = (_a = scores == null ? void 0 : scores.cards) == null ? void 0 : _a[cardId];
1079
+ if (cardScores && !cardScores.media_area_opened) {
1080
+ acc[cardId] = cardScores;
1081
+ }
1082
+ return acc;
1083
+ },
1084
+ {}
1085
+ );
1086
+ const totalSetPoints = cardsList.reduce((acc, cardId) => {
1087
+ acc += (weights == null ? void 0 : weights[cardId]) || 1;
1088
+ return acc;
1089
+ }, 0);
1090
+ const totalPointsAwarded = Object.keys(filteredScoreCards).reduce((acc, cardId) => {
1091
+ var _a;
1092
+ const cardScores = filteredScoreCards[cardId];
1093
+ if ((cardScores == null ? void 0 : cardScores.completed) || (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0) {
1094
+ const score2 = (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0 ? Number((_a = cardScores == null ? void 0 : cardScores.score) != null ? _a : 0) : null;
1095
+ const weight = (weights == null ? void 0 : weights[cardId]) || 1;
1096
+ const fraction = (score2 != null ? score2 : 0) / 100;
1097
+ if (score2 || score2 === 0) {
1098
+ acc += weight * fraction;
1099
+ } else {
1100
+ acc += weight;
1101
+ }
1102
+ }
1103
+ return acc;
1104
+ }, 0);
1105
+ const totalCompletedCards = Object.keys(filteredScoreCards).reduce((acc, cardId) => {
1106
+ const cardScores = filteredScoreCards[cardId];
1107
+ if ((cardScores == null ? void 0 : cardScores.completed) || (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0) {
1108
+ acc += 1;
1109
+ }
1110
+ return acc;
1111
+ }, 0);
1112
+ const percent = totalPointsAwarded / totalSetPoints;
1113
+ const score = Math.round(percent * 100);
1114
+ const progress = Math.round(totalCompletedCards / (cardsList.length || 1) * 100);
1115
+ return { score, progress };
1116
+ };
1117
+ var calculateScoreAndProgress_default = calculateScoreAndProgress;
1118
+
1119
+ // src/domains/assignment/services/update-score.service.ts
1120
+ async function _updateScore(params) {
1121
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1122
+ id: params.activityId,
1123
+ userId: params.userId
1124
+ }) : refsScoresPractice.practiceScores({
1125
+ setId: params.activityId,
1126
+ userId: params.userId
1127
+ });
1128
+ await api.updateDoc(path, {
1129
+ ...params.data
1130
+ });
1131
+ }
1132
+ var updateScore = withErrorHandler(_updateScore, "updateScore");
1133
+ async function _updateCardScore(params) {
1134
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1135
+ id: params.activityId,
1136
+ userId: params.userId
1137
+ }) : refsScoresPractice.practiceScores({
1138
+ setId: params.activityId,
1139
+ userId: params.userId
1140
+ });
1141
+ const updates = Object.keys(params.updates.cardScore).reduce(
1142
+ (acc, key) => {
1143
+ acc[`cards.${params.cardId}.${key}`] = params.updates.cardScore[key];
1144
+ return acc;
1145
+ },
1146
+ {}
1147
+ );
1148
+ if (params.updates.progress) {
1149
+ updates.progress = params.updates.progress;
1150
+ }
1151
+ if (params.updates.score) {
1152
+ updates.score = params.updates.score;
1153
+ }
1154
+ await api.updateDoc(path, {
1155
+ ...updates
1156
+ });
1157
+ }
1158
+ var updateCardScore = withErrorHandler(_updateCardScore, "updateCardScore");
1159
+
1160
+ // src/domains/assignment/services/clear-score.service.ts
1161
+ import dayjs3 from "dayjs";
1162
+ async function clearScore(params) {
1163
+ var _a, _b, _c, _d, _e;
1164
+ const update = {
1165
+ [`cards.${params.cardId}`]: {
1166
+ attempts: ((_a = params.cardScores.attempts) != null ? _a : 1) + 1,
1167
+ correct: (_b = params.cardScores.correct) != null ? _b : 0,
1168
+ // save old score history
1169
+ history: [
1170
+ {
1171
+ ...params.cardScores,
1172
+ attempts: (_c = params.cardScores.attempts) != null ? _c : 1,
1173
+ correct: (_d = params.cardScores.correct) != null ? _d : 0,
1174
+ retryTime: dayjs3().format("YYYY-MM-DD HH:mm:ss"),
1175
+ history: null
1176
+ },
1177
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1178
+ ...(_e = params.cardScores.history) != null ? _e : []
1179
+ ]
1180
+ }
1181
+ };
1182
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1183
+ id: params.activityId,
1184
+ userId: params.userId
1185
+ }) : refsScoresPractice.practiceScores({
1186
+ setId: params.activityId,
1187
+ userId: params.userId
1188
+ });
1189
+ await api.updateDoc(path, update);
1190
+ return {
1191
+ update,
1192
+ activityId: params.activityId
1193
+ };
1194
+ }
1195
+ async function clearScoreV2(params) {
1196
+ var _a, _b, _c, _d, _e;
1197
+ const update = {
1198
+ [`cards.${params.cardId}`]: {
1199
+ ...params.cardScores,
1200
+ attempts: ((_a = params.cardScores.attempts) != null ? _a : 1) + 1,
1201
+ correct: (_b = params.cardScores.correct) != null ? _b : 0,
1202
+ history: [
1203
+ {
1204
+ ...params.cardScores,
1205
+ attempts: (_c = params.cardScores.attempts) != null ? _c : 1,
1206
+ correct: (_d = params.cardScores.correct) != null ? _d : 0,
1207
+ retryTime: dayjs3().format("YYYY-MM-DD HH:mm:ss"),
1208
+ history: null
1209
+ },
1210
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1211
+ ...(_e = params.cardScores.history) != null ? _e : []
1212
+ ]
1213
+ }
1214
+ };
1215
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
1216
+ id: params.activityId,
1217
+ userId: params.userId
1218
+ }) : refsScoresPractice.practiceScores({
1219
+ setId: params.activityId,
1220
+ userId: params.userId
1221
+ });
1222
+ await api.updateDoc(path, update);
1223
+ return {
1224
+ update,
1225
+ activityId: params.activityId
1226
+ };
1227
+ }
1228
+
1229
+ // src/domains/assignment/services/submit-assignment-score.service.ts
1230
+ import dayjs4 from "dayjs";
1231
+ async function _submitAssignmentScore({
1232
+ assignment,
1233
+ userId,
1234
+ status,
1235
+ studentName
1236
+ }) {
1237
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
1238
+ const path = refsAssignmentFiresotre.assignmentScores({ id: assignment.id, userId });
1239
+ const fieldsUpdated = {
1240
+ submitted: true,
1241
+ progress: 100,
1242
+ submissionDate: serverTimestamp2(),
1243
+ status
1244
+ };
1245
+ if (assignment.isAssessment) {
1246
+ const result = await handleAssessment(assignment, userId, fieldsUpdated, studentName);
1247
+ return result;
1248
+ } else if (assignment.courseId) {
1249
+ await handleCourseAssignment(assignment, userId);
1250
+ }
1251
+ await api.updateDoc(path, { ...fieldsUpdated });
1252
+ return { success: true, fieldsUpdated };
1253
+ }
1254
+ var submitAssignmentScore = withErrorHandler(
1255
+ _submitAssignmentScore,
1256
+ "submitAssignmentScore"
1257
+ );
1258
+ async function handleAssessment(assignment, userId, fieldsUpdated, studentName) {
1259
+ var _a, _b, _c;
1260
+ const path = refsAssignmentFiresotre.assignmentScores({ id: assignment.id, userId });
1261
+ const response = await api.getDoc(path);
1262
+ if (!response.data) {
1263
+ throw new Error("Score not found");
1264
+ }
1265
+ await api.updateDoc(path, { status: "PENDING_REVIEW" });
1266
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssessment")) == null ? void 0 : _c({
1267
+ assignmentId: assignment.id,
1268
+ assignmentTitle: assignment.name,
1269
+ userId,
1270
+ teacherId: assignment.owners[0],
1271
+ studentName
1272
+ }));
1273
+ fieldsUpdated.status = "PENDING_REVIEW";
1274
+ return { success: true, fieldsUpdated };
1275
+ }
1276
+ async function handleCourseAssignment(assignment, userId) {
1277
+ var _a, _b, _c;
1278
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssignmentV2")) == null ? void 0 : _c({
1279
+ assignmentId: assignment.id,
1280
+ userId
1281
+ }));
1282
+ }
1283
+ async function submitPracticeScore({
1284
+ setId,
1285
+ userId,
1286
+ scores
1287
+ }) {
1288
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
1289
+ const date = dayjs4().format("YYYY-MM-DD-HH-mm");
1290
+ const ref = refsScoresPractice.practiceScoreHistoryRefDoc({ setId, userId, date });
1291
+ const fieldsUpdated = {
1292
+ ...scores,
1293
+ submitted: true,
1294
+ progress: 100,
1295
+ submissionDate: serverTimestamp2(),
1296
+ status: "SUBMITTED"
1297
+ };
1298
+ await api.setDoc(ref, { ...fieldsUpdated });
1299
+ const refScores = refsScoresPractice.practiceScores({ userId, setId });
1300
+ await api.deleteDoc(refScores);
1301
+ return { success: true, fieldsUpdated };
1302
+ }
1303
+
1304
+ // src/domains/assignment/hooks/score-hooks.ts
1305
+ var scoreQueryKeys = {
1306
+ all: ["scores"],
1307
+ byId: (id) => [...scoreQueryKeys.all, id],
1308
+ list: () => [...scoreQueryKeys.all, "list"]
1309
+ };
1310
+ function useScore({
1311
+ isAssignment,
1312
+ activityId,
1313
+ userId = "",
1314
+ courseId,
1315
+ enabled = true,
1316
+ googleClassroomUserId
1317
+ }) {
1318
+ return useQuery2({
1319
+ queryFn: () => getScore({
1320
+ userId,
1321
+ courseId,
1322
+ activityId,
1323
+ googleClassroomUserId,
1324
+ isAssignment
1325
+ }),
1326
+ queryKey: scoreQueryKeys.byId(activityId),
1327
+ enabled
1328
+ });
1329
+ }
1330
+ var debounceUpdateScore = debounce(updateScore, 500);
1331
+ function useUpdateScore() {
1332
+ const { queryClient } = useSpeakableApi();
1333
+ const mutation = useMutation({
1334
+ mutationFn: debounceUpdateScore,
1335
+ onMutate: (variables) => {
1336
+ return handleOptimisticUpdate({
1337
+ queryClient,
1338
+ queryKey: scoreQueryKeys.byId(variables.activityId),
1339
+ newData: variables.data
1340
+ });
1341
+ },
1342
+ onError: (_, variables, context) => {
1343
+ if (context == null ? void 0 : context.previousData)
1344
+ queryClient.setQueryData(scoreQueryKeys.byId(variables.activityId), context.previousData);
1345
+ },
1346
+ onSettled: (_, err, variables) => {
1347
+ queryClient.invalidateQueries({
1348
+ queryKey: scoreQueryKeys.byId(variables.activityId)
1349
+ });
1350
+ }
1351
+ });
1352
+ return {
1353
+ mutationUpdateScore: mutation
1354
+ };
1355
+ }
1356
+ function useUpdateCardScore({
1357
+ isAssignment,
1358
+ activityId,
1359
+ userId,
1360
+ cardIds,
1361
+ weights
1362
+ }) {
1363
+ const { queryClient } = useSpeakableApi();
1364
+ const queryKey = scoreQueryKeys.byId(activityId);
1365
+ const mutation = useMutation({
1366
+ mutationFn: async ({ cardId, cardScore }) => {
1367
+ const previousScores = queryClient.getQueryData(queryKey);
1368
+ const { progress, score, newScoreUpdated, updatedCardScore } = getScoreUpdated({
1369
+ previousScores: previousScores != null ? previousScores : {},
1370
+ cardId,
1371
+ cardScore,
1372
+ cardIds,
1373
+ weights
1374
+ });
1375
+ await updateCardScore({
1376
+ userId,
1377
+ cardId,
1378
+ isAssignment,
1379
+ activityId,
1380
+ updates: {
1381
+ cardScore: updatedCardScore,
1382
+ progress,
1383
+ score
1384
+ }
1385
+ });
1386
+ return { cardId, scoresUpdated: newScoreUpdated };
1387
+ },
1388
+ onMutate: ({ cardId, cardScore }) => {
1389
+ const previousData = queryClient.getQueryData(queryKey);
1390
+ queryClient.setQueryData(queryKey, (previousScore) => {
1391
+ const updates = handleOptimisticScore({
1392
+ score: previousScore,
1393
+ cardId,
1394
+ cardScore,
1395
+ cardIds,
1396
+ weights
1397
+ });
1398
+ return {
1399
+ ...previousScore,
1400
+ ...updates
1401
+ };
1402
+ });
1403
+ return { previousData };
1404
+ },
1405
+ onError: (error, variables, context) => {
1406
+ console.log("Error updating card score:", error.message);
1407
+ if (context == null ? void 0 : context.previousData) {
1408
+ queryClient.setQueryData(queryKey, context.previousData);
1409
+ }
1410
+ },
1411
+ onSettled: () => {
1412
+ queryClient.invalidateQueries({
1413
+ queryKey
1414
+ });
1415
+ }
1416
+ });
1417
+ return {
1418
+ mutationUpdateCardScore: mutation
1419
+ };
1420
+ }
1421
+ var getScoreUpdated = ({
1422
+ cardId,
1423
+ cardScore,
1424
+ previousScores,
1425
+ cardIds,
1426
+ weights
1427
+ }) => {
1428
+ var _a, _b;
1429
+ const previousCard = (_a = previousScores.cards) == null ? void 0 : _a[cardId];
1430
+ const newCardScore = {
1431
+ ...previousCard != null ? previousCard : {},
1432
+ ...cardScore
1433
+ };
1434
+ const newScores = {
1435
+ ...previousScores,
1436
+ cards: {
1437
+ ...(_b = previousScores.cards) != null ? _b : {},
1438
+ [cardId]: newCardScore
1439
+ }
1440
+ };
1441
+ const { score, progress } = calculateScoreAndProgress_default(newScores, cardIds, weights);
1442
+ return {
1443
+ newScoreUpdated: newScores,
1444
+ updatedCardScore: cardScore,
1445
+ score,
1446
+ progress
1447
+ };
1448
+ };
1449
+ var handleOptimisticScore = ({
1450
+ score,
1451
+ cardId,
1452
+ cardScore,
1453
+ cardIds,
1454
+ weights
1455
+ }) => {
1456
+ var _a;
1457
+ let cards = { ...(_a = score == null ? void 0 : score.cards) != null ? _a : {} };
1458
+ cards = {
1459
+ ...cards,
1460
+ [cardId]: {
1461
+ ...cards[cardId],
1462
+ ...cardScore
1463
+ }
1464
+ };
1465
+ const { score: scoreValue, progress } = calculateScoreAndProgress_default(
1466
+ // @ts-ignore
1467
+ {
1468
+ ...score != null ? score : {},
1469
+ cards
1470
+ },
1471
+ cardIds,
1472
+ weights
1473
+ );
1474
+ return { cards, score: scoreValue, progress };
1475
+ };
1476
+ function useClearScore() {
1477
+ const { queryClient } = useSpeakableApi();
1478
+ const mutation = useMutation({
1479
+ mutationFn: clearScore,
1480
+ onError: (error) => {
1481
+ console.log("Error clearing score:", error.message);
1482
+ },
1483
+ onSettled: (result) => {
1484
+ var _a;
1485
+ queryClient.invalidateQueries({
1486
+ queryKey: scoreQueryKeys.byId((_a = result == null ? void 0 : result.activityId) != null ? _a : "")
1487
+ });
1488
+ }
1489
+ });
1490
+ return {
1491
+ mutationClearScore: mutation
1492
+ };
1493
+ }
1494
+ function useClearScoreV2() {
1495
+ const { queryClient } = useSpeakableApi();
1496
+ const mutation = useMutation({
1497
+ mutationFn: clearScoreV2,
1498
+ onError: (error) => {
1499
+ console.log("Error clearing score V2:", error.message);
1500
+ },
1501
+ onSettled: (result) => {
1502
+ var _a;
1503
+ queryClient.invalidateQueries({
1504
+ queryKey: scoreQueryKeys.byId((_a = result == null ? void 0 : result.activityId) != null ? _a : "")
1505
+ });
1506
+ }
1507
+ });
1508
+ return {
1509
+ mutationClearScore: mutation
1510
+ };
1511
+ }
1512
+ function useSubmitAssignmentScore({
1513
+ onAssignmentSubmitted,
1514
+ studentName
1515
+ }) {
1516
+ const { queryClient } = useSpeakableApi();
1517
+ const { hasGoogleClassroomGradePassback } = usePermissions_default();
1518
+ const { submitAssignmentToGoogleClassroom } = useGoogleClassroom();
1519
+ const { createNotification: createNotification2 } = useCreateNotification();
1520
+ const mutation = useMutation({
1521
+ mutationFn: async ({
1522
+ assignment,
1523
+ userId,
1524
+ scores,
1525
+ status
1526
+ }) => {
1527
+ try {
1528
+ const scoreUpdated = await submitAssignmentScore({
1529
+ assignment,
1530
+ userId,
1531
+ status,
1532
+ studentName
1533
+ });
1534
+ if (assignment.courseWorkId != null && !assignment.isAssessment && hasGoogleClassroomGradePassback) {
1535
+ await submitAssignmentToGoogleClassroom({
1536
+ assignment,
1537
+ scores
1538
+ });
1539
+ }
1540
+ if (assignment.isAssessment) {
1541
+ createNotification2(SpeakableNotificationTypes.ASSESSMENT_SUBMITTED, assignment);
1542
+ }
1543
+ if (assignment == null ? void 0 : assignment.id) {
1544
+ logSubmitAssignment({
1545
+ courseId: assignment == null ? void 0 : assignment.courseId
1546
+ });
1547
+ }
1548
+ onAssignmentSubmitted(assignment.id);
1549
+ queryClient.setQueryData(scoreQueryKeys.byId(assignment.id), {
1550
+ ...scores,
1551
+ ...scoreUpdated.fieldsUpdated
1552
+ });
1553
+ return {
1554
+ success: true,
1555
+ message: "Score submitted successfully"
1556
+ };
1557
+ } catch (error) {
1558
+ return {
1559
+ success: false,
1560
+ error
1561
+ };
1562
+ }
1563
+ }
1564
+ });
1565
+ return {
1566
+ submitAssignmentScore: mutation.mutateAsync,
1567
+ isLoading: mutation.isPending
1568
+ };
1569
+ }
1570
+ function useSubmitPracticeScore() {
1571
+ const { queryClient } = useSpeakableApi();
1572
+ const mutation = useMutation({
1573
+ mutationFn: async ({
1574
+ setId,
1575
+ userId,
1576
+ scores
1577
+ }) => {
1578
+ try {
1579
+ await submitPracticeScore({
1580
+ setId,
1581
+ userId,
1582
+ scores
1583
+ });
1584
+ queryClient.invalidateQueries({
1585
+ queryKey: scoreQueryKeys.byId(setId)
1586
+ });
1587
+ return {
1588
+ success: true,
1589
+ message: "Score submitted successfully"
1590
+ };
1591
+ } catch (error) {
1592
+ return {
1593
+ success: false,
1594
+ error
1595
+ };
1596
+ }
1597
+ }
1598
+ });
1599
+ return {
1600
+ submitPracticeScore: mutation.mutateAsync,
1601
+ isLoading: mutation.isPending
1602
+ };
1603
+ }
1604
+
1605
+ // src/domains/cards/card.hooks.ts
1606
+ import { useMutation as useMutation2, useQueries, useQuery as useQuery3 } from "@tanstack/react-query";
1607
+ import { useMemo } from "react";
1608
+
1609
+ // src/domains/cards/card.model.ts
1610
+ var ConversationPageMode = /* @__PURE__ */ ((ConversationPageMode2) => {
1611
+ ConversationPageMode2["VoiceAndText"] = "voice_and_text";
1612
+ ConversationPageMode2["PhoneCall"] = "phone_call";
1613
+ return ConversationPageMode2;
1614
+ })(ConversationPageMode || {});
1615
+ var ActivityPageType = /* @__PURE__ */ ((ActivityPageType2) => {
1616
+ ActivityPageType2["READ_REPEAT"] = "READ_REPEAT";
1617
+ ActivityPageType2["READ_RESPOND"] = "READ_RESPOND";
1618
+ ActivityPageType2["FREE_RESPONSE"] = "FREE_RESPONSE";
1619
+ ActivityPageType2["REPEAT"] = "REPEAT";
1620
+ ActivityPageType2["RESPOND"] = "RESPOND";
1621
+ ActivityPageType2["RESPOND_WRITE"] = "RESPOND_WRITE";
1622
+ ActivityPageType2["MULTIPLE_CHOICE"] = "MULTIPLE_CHOICE";
1623
+ ActivityPageType2["MEDIA_PAGE"] = "MEDIA_PAGE";
1624
+ ActivityPageType2["SHORT_ANSWER"] = "SHORT_ANSWER";
1625
+ ActivityPageType2["CONVERSATION"] = "CONVERSATION";
1626
+ return ActivityPageType2;
1627
+ })(ActivityPageType || {});
1628
+ var RESPOND_PAGE_ACTIVITY_TYPES = [
1629
+ "READ_RESPOND" /* READ_RESPOND */,
1630
+ "RESPOND" /* RESPOND */,
1631
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
1632
+ "FREE_RESPONSE" /* FREE_RESPONSE */
1633
+ ];
1634
+ var MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES = ["MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */];
1635
+ var REPEAT_PAGE_ACTIVITY_TYPES = ["READ_REPEAT" /* READ_REPEAT */, "REPEAT" /* REPEAT */];
1636
+ var RESPOND_WRITE_PAGE_ACTIVITY_TYPES = [
1637
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
1638
+ "FREE_RESPONSE" /* FREE_RESPONSE */
1639
+ ];
1640
+ var RESPOND_AUDIO_PAGE_ACTIVITY_TYPES = [
1641
+ "RESPOND" /* RESPOND */,
1642
+ "READ_RESPOND" /* READ_RESPOND */
1643
+ ];
1644
+ var CONVERSATION_PAGE_ACTIVITY_TYPES = ["CONVERSATION" /* CONVERSATION */];
1645
+
1646
+ // src/domains/cards/card.constants.ts
1647
+ var FeedbackTypesCard = /* @__PURE__ */ ((FeedbackTypesCard2) => {
1648
+ FeedbackTypesCard2["SuggestedResponse"] = "suggested_response";
1649
+ FeedbackTypesCard2["Wida"] = "wida";
1650
+ FeedbackTypesCard2["GrammarInsights"] = "grammar_insights";
1651
+ FeedbackTypesCard2["Actfl"] = "actfl";
1652
+ FeedbackTypesCard2["ProficiencyLevel"] = "proficiency_level";
1653
+ return FeedbackTypesCard2;
1654
+ })(FeedbackTypesCard || {});
1655
+ var LeniencyCard = /* @__PURE__ */ ((LeniencyCard2) => {
1656
+ LeniencyCard2["CONFIDENCE"] = "confidence";
1657
+ LeniencyCard2["EASY"] = "easy";
1658
+ LeniencyCard2["NORMAL"] = "normal";
1659
+ LeniencyCard2["HARD"] = "hard";
1660
+ return LeniencyCard2;
1661
+ })(LeniencyCard || {});
1662
+ var LENIENCY_OPTIONS = [
1663
+ {
1664
+ label: "Build Confidence - most lenient",
1665
+ value: "confidence" /* CONFIDENCE */
1666
+ },
1667
+ {
1668
+ label: "Very Lenient",
1669
+ value: "easy" /* EASY */
1670
+ },
1671
+ {
1672
+ label: "Normal",
1673
+ value: "normal" /* NORMAL */
1674
+ },
1675
+ {
1676
+ label: "No leniency - most strict",
1677
+ value: "hard" /* HARD */
1678
+ }
1679
+ ];
1680
+ var STUDENT_LEVELS_OPTIONS = [
1681
+ {
1682
+ label: "Beginner",
1683
+ description: "Beginner Level: Just starting out. Can say a few basic words and phrases.",
1684
+ value: "beginner"
1685
+ },
1686
+ {
1687
+ label: "Elementary",
1688
+ description: "Elementary Level: Can understand simple sentences and have very basic conversations.",
1689
+ value: "elementary"
1690
+ },
1691
+ {
1692
+ label: "Intermediate",
1693
+ description: "Intermediate Level: Can talk about everyday topics and handle common situations.",
1694
+ value: "intermediate"
1695
+ },
1696
+ {
1697
+ label: "Advanced",
1698
+ description: "Advanced Level: Can speak and understand with ease, and explain ideas clearly.",
1699
+ value: "advanced"
1700
+ },
1701
+ {
1702
+ label: "Fluent",
1703
+ description: "Fluent Level: Speaks naturally and easily. Can use the language in work or school settings.",
1704
+ value: "fluent"
1705
+ },
1706
+ {
1707
+ label: "Native-like",
1708
+ description: "Native-like Level: Understands and speaks like a native. Can discuss complex ideas accurately.",
1709
+ value: "nativeLike"
1710
+ }
1711
+ ];
1712
+ var BASE_RESPOND_FIELD_VALUES = {
1713
+ title: "",
1714
+ allowRetries: true,
1715
+ respondTime: 180,
1716
+ maxCharacters: 1e4
1717
+ };
1718
+ var BASE_REPEAT_FIELD_VALUES = {
1719
+ repeat: 1
1720
+ };
1721
+ var BASE_MULTIPLE_CHOICE_FIELD_VALUES = {
1722
+ MCQType: "single",
1723
+ answer: ["A"],
1724
+ choices: [
1725
+ { option: "A", value: "Option A" },
1726
+ { option: "B", value: "Option B" },
1727
+ { option: "C", value: "Option C" }
1728
+ ]
1729
+ };
1730
+ var VerificationCardStatus = /* @__PURE__ */ ((VerificationCardStatus2) => {
1731
+ VerificationCardStatus2["VERIFIED"] = "VERIFIED";
1732
+ VerificationCardStatus2["WARNING"] = "WARNING";
1733
+ VerificationCardStatus2["NOT_RECOMMENDED"] = "NOT_RECOMMENDED";
1734
+ VerificationCardStatus2["NOT_WORKING"] = "NOT_WORKING";
1735
+ VerificationCardStatus2["NOT_CHECKED"] = "NOT_CHECKED";
1736
+ return VerificationCardStatus2;
1737
+ })(VerificationCardStatus || {});
1738
+ var CARDS_COLLECTION = "flashcards";
1739
+ var refsCardsFiresotre = {
1740
+ allCards: CARDS_COLLECTION,
1741
+ card: (id) => `${CARDS_COLLECTION}/${id}`
1742
+ };
1743
+
1744
+ // src/domains/cards/services/get-card.service.ts
1745
+ async function _getCard(params) {
1746
+ const ref = refsCardsFiresotre.card(params.cardId);
1747
+ const response = await api.getDoc(ref);
1748
+ if (!response.data) return null;
1749
+ const type = response.data.type || "READ_REPEAT" /* READ_REPEAT */;
1750
+ const cardsMapped = {
1751
+ ...response.data,
1752
+ type
1753
+ };
1754
+ return cardsMapped;
1755
+ }
1756
+ var getCard = withErrorHandler(_getCard, "getCard");
1757
+
1758
+ // src/domains/cards/services/create-card.service.ts
1759
+ import { v4 } from "uuid";
1760
+
1761
+ // src/utils/text-utils.ts
1762
+ import { sha1 } from "js-sha1";
1763
+ var purify = (word) => {
1764
+ 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();
1765
+ };
1766
+ var cleanString = (words) => {
1767
+ const splitWords = words == null ? void 0 : words.split("+");
1768
+ if (splitWords && splitWords.length === 1) {
1769
+ const newWord = purify(words);
1770
+ return newWord;
1771
+ } else if (splitWords && splitWords.length > 1) {
1772
+ const split = splitWords.map((w) => purify(w));
1773
+ return split;
1774
+ } else {
1775
+ return "";
1776
+ }
1777
+ };
1778
+ var getWordHash = (word, language) => {
1779
+ const cleanedWord = cleanString(word);
1780
+ const wordHash = sha1(`${language}-${cleanedWord}`);
1781
+ console.log("wordHash core library", wordHash);
1782
+ return wordHash;
1783
+ };
1784
+ function getPhraseLength(phrase, input) {
1785
+ if (Array.isArray(phrase) && phrase.includes(input)) {
1786
+ return phrase[phrase.indexOf(input)].split(" ").length;
1787
+ } else {
1788
+ return phrase ? phrase.split(" ").length : 0;
1789
+ }
1790
+ }
1791
+
1792
+ // src/domains/cards/services/get-card-verification-status.service.ts
1793
+ var charactarLanguages = ["zh", "ja", "ko"];
1794
+ var getVerificationStatus = async (target_text, language) => {
1795
+ if ((target_text == null ? void 0 : target_text.length) < 3 && !charactarLanguages.includes(language)) {
1796
+ return "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
1797
+ }
1798
+ const hash = getWordHash(target_text, language);
1799
+ const response = await api.getDoc(`checked-pronunciations/${hash}`);
1800
+ try {
1801
+ if (response.data) {
1802
+ return processRecord(response.data);
1803
+ } else {
1804
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1805
+ }
1806
+ } catch (e) {
1807
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1808
+ }
1809
+ };
1810
+ var processRecord = (data) => {
1811
+ const { pronunciations = 0, fails = 0 } = data;
1812
+ const attempts = pronunciations + fails;
1813
+ const successRate = attempts > 0 ? pronunciations / attempts * 100 : 0;
1814
+ let newStatus = null;
1815
+ if (attempts < 6) {
1816
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1817
+ }
1818
+ if (successRate > 25) {
1819
+ newStatus = "VERIFIED" /* VERIFIED */;
1820
+ } else if (successRate > 10) {
1821
+ newStatus = "WARNING" /* WARNING */;
1822
+ } else if (fails > 20 && successRate < 10 && pronunciations > 1) {
1823
+ newStatus = "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
1824
+ } else if (pronunciations === 0 && fails > 20) {
1825
+ newStatus = "NOT_WORKING" /* NOT_WORKING */;
1826
+ } else {
1827
+ newStatus = "NOT_CHECKED" /* NOT_CHECKED */;
1828
+ }
1829
+ return newStatus;
1830
+ };
1831
+
1832
+ // src/domains/cards/services/create-card.service.ts
1833
+ async function _createCard({ data }) {
1834
+ const response = await api.addDoc(refsCardsFiresotre.allCards, data);
1835
+ return response;
1836
+ }
1837
+ var createCard = withErrorHandler(_createCard, "createCard");
1838
+ async function _createCards({ cards }) {
1839
+ const { writeBatch: writeBatch2, doc: doc2 } = api.accessHelpers();
1840
+ const batch = writeBatch2();
1841
+ const cardsWithId = [];
1842
+ for (const card of cards) {
1843
+ const cardId = v4();
1844
+ const ref = doc2(refsCardsFiresotre.card(cardId));
1845
+ const newCardObject = {
1846
+ ...card,
1847
+ id: cardId
1848
+ };
1849
+ if (card.type === "READ_REPEAT" /* READ_REPEAT */ && card.target_text && card.language) {
1850
+ const verificationStatus = await getVerificationStatus(card.target_text, card.language);
1851
+ newCardObject.verificationStatus = verificationStatus || null;
1852
+ }
1853
+ cardsWithId.push(newCardObject);
1854
+ batch.set(ref, newCardObject);
1855
+ }
1856
+ await batch.commit();
1857
+ return cardsWithId;
1858
+ }
1859
+ var createCards = withErrorHandler(_createCards, "createCards");
1860
+
1861
+ // src/domains/cards/card.hooks.ts
1862
+ var cardsQueryKeys = {
1863
+ all: ["cards"],
1864
+ one: (params) => [...cardsQueryKeys.all, params.cardId]
1865
+ };
1866
+ function useCards({
1867
+ cardIds,
1868
+ enabled = true,
1869
+ asObject
1870
+ }) {
1871
+ const queries = useQueries({
1872
+ queries: cardIds.map((cardId) => ({
1873
+ enabled: enabled && cardIds.length > 0,
1874
+ queryKey: cardsQueryKeys.one({
1875
+ cardId
1876
+ }),
1877
+ queryFn: () => getCard({ cardId })
1878
+ }))
1879
+ });
1880
+ const cards = queries.map((query2) => query2.data).filter(Boolean);
1881
+ const cardsObject = useMemo(() => {
1882
+ if (!asObject) return null;
1883
+ return cards.reduce((acc, card) => {
1884
+ acc[card.id] = card;
1885
+ return acc;
1886
+ }, {});
1887
+ }, [asObject, cards]);
1888
+ return {
1889
+ cards,
1890
+ cardsObject,
1891
+ cardsQueries: queries
1892
+ };
1893
+ }
1894
+ function useCreateCard() {
1895
+ const { queryClient } = useSpeakableApi();
1896
+ const mutationCreateCard = useMutation2({
1897
+ mutationFn: createCard,
1898
+ onSuccess: (cardCreated) => {
1899
+ queryClient.invalidateQueries({ queryKey: cardsQueryKeys.one({ cardId: cardCreated.id }) });
1900
+ }
1901
+ });
1902
+ return {
1903
+ mutationCreateCard
1904
+ };
1905
+ }
1906
+ function useCreateCards() {
1907
+ const mutationCreateCards = useMutation2({
1908
+ mutationFn: createCards
1909
+ });
1910
+ return {
1911
+ mutationCreateCards
1912
+ };
1913
+ }
1914
+ function getCardFromCache({
1915
+ cardId,
1916
+ queryClient
1917
+ }) {
1918
+ return queryClient.getQueryData(cardsQueryKeys.one({ cardId }));
1919
+ }
1920
+ function updateCardInCache({
1921
+ cardId,
1922
+ card,
1923
+ queryClient
1924
+ }) {
1925
+ queryClient.setQueryData(cardsQueryKeys.one({ cardId }), card);
1926
+ }
1927
+ function useGetCard({ cardId, enabled = true }) {
1928
+ const query2 = useQuery3({
1929
+ queryKey: cardsQueryKeys.one({ cardId }),
1930
+ queryFn: () => getCard({ cardId }),
1931
+ enabled: enabled && !!cardId
1932
+ });
1933
+ return query2;
1934
+ }
1935
+
1936
+ // src/domains/cards/card.repo.ts
1937
+ var createCardRepo = () => {
1938
+ return {
1939
+ createCard,
1940
+ createCards,
1941
+ getCard
1942
+ };
1943
+ };
1944
+
1945
+ // src/domains/cards/utils/check-page-type.ts
1946
+ function checkIsRepeatPage(cardType) {
1947
+ if (cardType === void 0) return false;
1948
+ return REPEAT_PAGE_ACTIVITY_TYPES.includes(cardType);
1949
+ }
1950
+ function checkIsMCPage(cardType) {
1951
+ if (cardType === void 0) return false;
1952
+ return MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES.includes(cardType);
1953
+ }
1954
+ function checkIsRespondPage(cardType) {
1955
+ if (cardType === void 0) return false;
1956
+ return RESPOND_PAGE_ACTIVITY_TYPES.includes(cardType);
1957
+ }
1958
+ function checkIsRespondWrittenPage(cardType) {
1959
+ if (cardType === void 0) return false;
1960
+ return RESPOND_WRITE_PAGE_ACTIVITY_TYPES.includes(cardType);
1961
+ }
1962
+ function checkIsRespondAudioPage(cardType) {
1963
+ if (cardType === void 0) return false;
1964
+ return RESPOND_AUDIO_PAGE_ACTIVITY_TYPES.includes(cardType);
1965
+ }
1966
+ var checkIsMediaPage = (cardType) => {
1967
+ if (cardType === void 0) return false;
1968
+ return cardType === "MEDIA_PAGE" /* MEDIA_PAGE */;
1969
+ };
1970
+ var checkIsShortAnswerPage = (cardType) => {
1971
+ if (cardType === void 0) return false;
1972
+ return cardType === "SHORT_ANSWER" /* SHORT_ANSWER */;
1973
+ };
1974
+ var checkIsConversationPage = (cardType) => {
1975
+ if (cardType === void 0) return false;
1976
+ return cardType === "CONVERSATION" /* CONVERSATION */;
1977
+ };
1978
+ var checkTypePageActivity = (cardType) => {
1979
+ const isRespondAudio = checkIsRespondAudioPage(cardType);
1980
+ const isRespondWritten = checkIsRespondWrittenPage(cardType);
1981
+ const isRespond = checkIsRespondPage(cardType);
1982
+ const isMC = checkIsMCPage(cardType);
1983
+ const isRepeat = checkIsRepeatPage(cardType);
1984
+ const isMediaPage = checkIsMediaPage(cardType);
1985
+ const isShortAnswer = checkIsShortAnswerPage(cardType);
1986
+ const isConversation = checkIsConversationPage(cardType);
1987
+ const isNoOneOfThem = !isRespond && !isMC && !isRepeat && !isMediaPage && !isShortAnswer && !isConversation;
1988
+ if (isNoOneOfThem) {
1989
+ return {
1990
+ isRespondAudio: false,
1991
+ isRespondWritten: false,
1992
+ isRespond: false,
1993
+ isMC: false,
1994
+ isRepeat: true,
1995
+ isMediaPage: false,
1996
+ isShortAnswer: false,
1997
+ hasSomeType: false,
1998
+ isConversation: false
1999
+ };
2000
+ }
2001
+ return {
2002
+ isRespondAudio,
2003
+ isRespondWritten,
2004
+ isRespond,
2005
+ isMC,
2006
+ isRepeat,
2007
+ isMediaPage,
2008
+ isShortAnswer,
2009
+ hasSomeType: true,
2010
+ isConversation
2011
+ };
2012
+ };
2013
+
2014
+ // src/domains/cards/utils/get-page-prompt.ts
2015
+ function extractTextFromRichText(richText) {
2016
+ if (!richText) return "";
2017
+ return richText.replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
2018
+ }
2019
+ function getPagePrompt(card) {
2020
+ if (!card) return { has: false, text: "", rich_text: "", isTextEqualToRichText: false };
2021
+ const { isMC, isRepeat, isRespond, isShortAnswer, isConversation } = checkTypePageActivity(
2022
+ card == null ? void 0 : card.type
2023
+ );
2024
+ const hidePrompt = (card == null ? void 0 : card.hidePrompt) === true;
2025
+ const createReturnObject = (text, richText, extraText) => {
2026
+ const plainText = text || "";
2027
+ const richTextPlain = extractTextFromRichText(richText);
2028
+ return {
2029
+ has: true,
2030
+ text: plainText,
2031
+ rich_text: richText || "",
2032
+ isTextEqualToRichText: plainText.trim() === richTextPlain.trim(),
2033
+ extraText: extraText || ""
2034
+ };
2035
+ };
2036
+ if (isRepeat) {
2037
+ return createReturnObject(card == null ? void 0 : card.target_text, card == null ? void 0 : card.rich_text);
2038
+ }
2039
+ if (isConversation) {
2040
+ return createReturnObject(card == null ? void 0 : card.prompt, card == null ? void 0 : card.rich_text, card == null ? void 0 : card.goal);
2041
+ }
2042
+ if (isRespond && !hidePrompt) {
2043
+ return createReturnObject(card == null ? void 0 : card.prompt, card == null ? void 0 : card.rich_text);
2044
+ }
2045
+ if (isMC) {
2046
+ return createReturnObject(card == null ? void 0 : card.question, card == null ? void 0 : card.rich_text);
2047
+ }
2048
+ if (isShortAnswer && !hidePrompt) {
2049
+ return createReturnObject(card == null ? void 0 : card.prompt, card == null ? void 0 : card.rich_text);
2050
+ }
2051
+ return {
2052
+ has: false,
2053
+ text: "",
2054
+ rich_text: "",
2055
+ isTextEqualToRichText: false,
2056
+ extraText: ""
2057
+ };
2058
+ }
2059
+
2060
+ // src/domains/cards/utils/get-completed-pages.ts
2061
+ var getTotalCompletedCards = (pageScores) => {
2062
+ return Object.values(pageScores != null ? pageScores : {}).reduce((acc, cardScore) => {
2063
+ var _a, _b;
2064
+ if (((_b = (_a = cardScore.completed) != null ? _a : cardScore.score) != null ? _b : cardScore.score === 0) && !cardScore.media_area_opened) {
2065
+ acc++;
2066
+ }
2067
+ return acc;
2068
+ }, 0);
2069
+ };
2070
+
2071
+ // src/domains/cards/utils/get-label-page.ts
2072
+ var labels = {
2073
+ repeat: {
2074
+ short: "Repeat",
2075
+ long: "Listen & Repeat"
2076
+ },
2077
+ mc: {
2078
+ short: "Multiple Choice",
2079
+ long: "Multiple Choice"
2080
+ },
2081
+ mediaPage: {
2082
+ short: "Media Page",
2083
+ long: "Media Page"
2084
+ },
2085
+ shortAnswer: {
2086
+ short: "Short Answer",
2087
+ long: "Short Answer"
2088
+ },
2089
+ respondWritten: {
2090
+ short: "Open Response",
2091
+ long: "Written Open Response"
2092
+ },
2093
+ respondAudio: {
2094
+ short: "Open Response",
2095
+ long: "Spoken Open Response"
2096
+ }
2097
+ };
2098
+ var getLabelPage = (pageType) => {
2099
+ if (!pageType) {
2100
+ return {
2101
+ short: "",
2102
+ long: ""
2103
+ };
2104
+ }
2105
+ const { isRepeat, isMC, isMediaPage, isShortAnswer, isRespondWritten, isRespondAudio } = checkTypePageActivity(pageType);
2106
+ if (isRepeat) {
2107
+ return labels.repeat;
2108
+ }
2109
+ if (isMC) {
2110
+ return labels.mc;
2111
+ }
2112
+ if (isMediaPage) {
2113
+ return labels.mediaPage;
2114
+ }
2115
+ if (isShortAnswer) {
2116
+ return labels.shortAnswer;
2117
+ }
2118
+ if (isRespondWritten) {
2119
+ return labels.respondWritten;
2120
+ }
2121
+ if (isRespondAudio) {
2122
+ return labels.respondAudio;
2123
+ }
2124
+ return {
2125
+ short: "",
2126
+ long: ""
2127
+ };
2128
+ };
2129
+
2130
+ // src/domains/cards/utils/get-page-media-data.ts
2131
+ function getPageMediaData(page) {
2132
+ var _a, _b, _c, _d;
2133
+ const direction = (_a = page.media_area_layout) != null ? _a : "left";
2134
+ const { isMediaPage } = checkTypePageActivity(page.type);
2135
+ const singleMedia = getSingleMediaPageData(page);
2136
+ const resource = {
2137
+ id: (_c = (_b = page.media_area_id) != null ? _b : page.media_area_context_ref) != null ? _c : void 0,
2138
+ shouldRender: ((_d = page.media_area_id) != null ? _d : page.media_area_context_ref) != null ? true : false
2139
+ };
2140
+ const promptMedia = {
2141
+ direction,
2142
+ content: singleMedia,
2143
+ shouldRender: singleMedia !== void 0
2144
+ };
2145
+ return {
2146
+ isMediaPage,
2147
+ resource,
2148
+ promptMedia
2149
+ };
2150
+ }
2151
+ function getSingleMediaPageData(page) {
2152
+ if (!page.media) return void 0;
2153
+ const media = {
2154
+ type: page.media.type,
2155
+ content: page.media.url
2156
+ };
2157
+ return {
2158
+ ...media,
2159
+ rawObject: page.media
2160
+ };
2161
+ }
2162
+
2163
+ // src/domains/sets/set.hooks.ts
2164
+ import { useQuery as useQuery4 } from "@tanstack/react-query";
2165
+
2166
+ // src/domains/sets/set.constants.ts
2167
+ var SETS_COLLECTION = "sets";
2168
+ var refsSetsFirestore = {
2169
+ allSets: SETS_COLLECTION,
2170
+ set: (id) => `${SETS_COLLECTION}/${id}`
2171
+ };
2172
+
2173
+ // src/domains/sets/services/get-set.service.ts
2174
+ async function _getSet({ setId }) {
2175
+ const response = await api.getDoc(refsSetsFirestore.set(setId));
2176
+ return response.data;
2177
+ }
2178
+ var getSet = withErrorHandler(_getSet, "getSet");
2179
+
2180
+ // src/domains/sets/set.hooks.ts
2181
+ var setsQueryKeys = {
2182
+ all: ["sets"],
2183
+ one: (params) => [...setsQueryKeys.all, params.setId]
2184
+ };
2185
+ var useSet = ({ setId, enabled }) => {
2186
+ return useQuery4({
2187
+ queryKey: setsQueryKeys.one({ setId }),
2188
+ queryFn: () => getSet({ setId }),
2189
+ enabled: setId !== void 0 && setId !== "" && enabled
2190
+ });
2191
+ };
2192
+ function getSetFromCache({
2193
+ setId,
2194
+ queryClient
2195
+ }) {
2196
+ if (!setId) return null;
2197
+ return queryClient.getQueryData(setsQueryKeys.one({ setId }));
2198
+ }
2199
+ function updateSetInCache({
2200
+ set,
2201
+ queryClient
2202
+ }) {
2203
+ const { id, ...setData } = set;
2204
+ queryClient.setQueryData(setsQueryKeys.one({ setId: id }), setData);
2205
+ }
2206
+
2207
+ // src/domains/sets/set.repo.ts
2208
+ var createSetRepo = () => {
2209
+ return {
2210
+ getSet
2211
+ };
2212
+ };
2213
+
2214
+ // src/utils/ai/detect-transcript-hallucionation.ts
2215
+ var HALLUCINATION_THRESHOLDS = {
2216
+ // Short repeats
2217
+ MIN_CONSECUTIVE_REPEATS: 5,
2218
+ // Increased from 3 to allow phrases like "pio pio pio" or "no no no no"
2219
+ MIN_WORDS_FOR_RATIO_CHECK: 15,
2220
+ // Increased from 10 to require longer text for ratio check
2221
+ MAX_UNIQUE_WORDS_FOR_RATIO: 3,
2222
+ MIN_REPETITION_RATIO: 4,
2223
+ // Increased from 3 to be more permissive
2224
+ // Phrase repeats
2225
+ MIN_SENTENCE_LENGTH: 15,
2226
+ // Increased from 10 to avoid flagging short natural sentences
2227
+ MIN_CONSECUTIVE_SIMILAR_SENTENCES: 3,
2228
+ // Increased from 2 to allow some natural repetition
2229
+ MIN_SENTENCES_FOR_DUPLICATE_CHECK: 4,
2230
+ // Increased from 3
2231
+ // Cyclic patterns
2232
+ MIN_CYCLE_LENGTH: 30,
2233
+ // Increased from 20 to focus on longer patterns
2234
+ MIN_CYCLE_REPEATS: 3,
2235
+ // Entropy detection
2236
+ MIN_LENGTH_FOR_ENTROPY_CHECK: 60,
2237
+ // Increased from 50
2238
+ MAX_ENTROPY_THRESHOLD: 2.2,
2239
+ // Decreased from 2.5 to be more strict on entropy (lower = more repetitive needed)
2240
+ // Similarity
2241
+ SENTENCE_SIMILARITY_THRESHOLD: 0.85,
2242
+ // Increased from 0.8 to require more similarity
2243
+ SEGMENT_SIMILARITY_THRESHOLD: 0.9
2244
+ // Increased from 0.85
2245
+ };
2246
+ function detectTranscriptHallucinationWithDetails(transcript) {
2247
+ if (!transcript || transcript.trim().length === 0) {
2248
+ return { isHallucination: false };
2249
+ }
2250
+ const text = transcript.trim();
2251
+ if (text.length < 10) {
2252
+ return { isHallucination: false };
2253
+ }
2254
+ const shortRepeats = detectShortRepeats(text);
2255
+ if (shortRepeats) {
2256
+ return {
2257
+ isHallucination: true,
2258
+ reason: "Detected repeated short words or phrases",
2259
+ confidence: 0.9
2260
+ };
2261
+ }
2262
+ const phraseRepeats = detectPhraseRepeats(text);
2263
+ if (phraseRepeats) {
2264
+ return {
2265
+ isHallucination: true,
2266
+ reason: "Detected repeated sentences or phrases",
2267
+ confidence: 0.85
2268
+ };
2269
+ }
2270
+ const cyclicRepeats = detectCyclicPattern(text);
2271
+ if (cyclicRepeats) {
2272
+ return {
2273
+ isHallucination: true,
2274
+ reason: "Detected cyclic repetition pattern",
2275
+ confidence: 0.8
2276
+ };
2277
+ }
2278
+ if (text.length >= HALLUCINATION_THRESHOLDS.MIN_LENGTH_FOR_ENTROPY_CHECK) {
2279
+ const entropy = calculateEntropy(text);
2280
+ if (entropy < HALLUCINATION_THRESHOLDS.MAX_ENTROPY_THRESHOLD) {
2281
+ return {
2282
+ isHallucination: true,
2283
+ reason: "Detected low entropy (likely gibberish or excessive repetition)",
2284
+ confidence: 0.75
2285
+ };
2286
+ }
2287
+ }
2288
+ return { isHallucination: false };
2289
+ }
2290
+ function detectShortRepeats(text) {
2291
+ const words = text.toLowerCase().split(/[\s,;.!?]+/).filter((w) => w.length > 0);
2292
+ if (words.length < 4) return false;
2293
+ let repeatCount = 1;
2294
+ for (let i = 1; i < words.length; i++) {
2295
+ if (words[i] === words[i - 1]) {
2296
+ repeatCount++;
2297
+ if (repeatCount >= HALLUCINATION_THRESHOLDS.MIN_CONSECUTIVE_REPEATS) {
2298
+ return true;
2299
+ }
2300
+ } else {
2301
+ repeatCount = 1;
2302
+ }
2303
+ }
2304
+ const uniqueWords = new Set(words);
2305
+ const repetitionRatio = words.length / uniqueWords.size;
2306
+ if (words.length >= HALLUCINATION_THRESHOLDS.MIN_WORDS_FOR_RATIO_CHECK && uniqueWords.size <= HALLUCINATION_THRESHOLDS.MAX_UNIQUE_WORDS_FOR_RATIO && repetitionRatio >= HALLUCINATION_THRESHOLDS.MIN_REPETITION_RATIO) {
2307
+ return true;
2308
+ }
2309
+ return false;
2310
+ }
2311
+ function detectPhraseRepeats(text) {
2312
+ const sentences = text.split(/[.!?]+/).map((s) => s.trim().toLowerCase()).filter((s) => s.length > HALLUCINATION_THRESHOLDS.MIN_SENTENCE_LENGTH);
2313
+ if (sentences.length < 2) return false;
2314
+ for (let i = 0; i < sentences.length - 1; i++) {
2315
+ let consecutiveRepeats = 1;
2316
+ for (let j = i + 1; j < sentences.length; j++) {
2317
+ if (isSimilarSentence(sentences[i], sentences[j])) {
2318
+ consecutiveRepeats++;
2319
+ } else {
2320
+ break;
2321
+ }
2322
+ }
2323
+ if (consecutiveRepeats >= HALLUCINATION_THRESHOLDS.MIN_CONSECUTIVE_SIMILAR_SENTENCES) {
2324
+ return true;
2325
+ }
2326
+ }
2327
+ const uniqueSentences = new Set(sentences);
2328
+ if (sentences.length >= HALLUCINATION_THRESHOLDS.MIN_SENTENCES_FOR_DUPLICATE_CHECK && uniqueSentences.size === 1) {
2329
+ return true;
2330
+ }
2331
+ return false;
2332
+ }
2333
+ function isSimilarSentence(s1, s2, threshold = HALLUCINATION_THRESHOLDS.SENTENCE_SIMILARITY_THRESHOLD) {
2334
+ if (s1 === s2) return true;
2335
+ const normalized1 = s1.replace(/\s+/g, " ").trim();
2336
+ const normalized2 = s2.replace(/\s+/g, " ").trim();
2337
+ if (normalized1 === normalized2) return true;
2338
+ const words1 = normalized1.split(/\s+/);
2339
+ const words2 = normalized2.split(/\s+/);
2340
+ if (Math.abs(words1.length - words2.length) > 2) return false;
2341
+ const set1 = new Set(words1);
2342
+ const set2 = new Set(words2);
2343
+ const intersection = new Set([...set1].filter((w) => set2.has(w)));
2344
+ const similarity = intersection.size * 2 / (set1.size + set2.size);
2345
+ return similarity >= threshold;
2346
+ }
2347
+ function detectCyclicPattern(text) {
2348
+ const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
2349
+ const length = normalized.length;
2350
+ const minCycleLength = HALLUCINATION_THRESHOLDS.MIN_CYCLE_LENGTH;
2351
+ const maxCycleLength = Math.floor(length / 2);
2352
+ if (maxCycleLength < minCycleLength) return false;
2353
+ const step = 5;
2354
+ for (let cycleLen = minCycleLength; cycleLen <= maxCycleLength; cycleLen += step) {
2355
+ const pattern = normalized.substring(0, cycleLen);
2356
+ let matchCount = 0;
2357
+ let pos = 0;
2358
+ while (pos < length) {
2359
+ const segment = normalized.substring(pos, pos + cycleLen);
2360
+ if (segment.length < cycleLen) {
2361
+ const partialMatch = pattern.startsWith(segment);
2362
+ if (partialMatch && matchCount > 0) {
2363
+ matchCount++;
2364
+ }
2365
+ break;
2366
+ }
2367
+ if (segment === pattern || isSegmentSimilar(segment, pattern)) {
2368
+ matchCount++;
2369
+ pos += cycleLen;
2370
+ } else {
2371
+ break;
2372
+ }
2373
+ }
2374
+ if (matchCount >= HALLUCINATION_THRESHOLDS.MIN_CYCLE_REPEATS) {
2375
+ return true;
2376
+ }
2377
+ }
2378
+ return false;
2379
+ }
2380
+ function isSegmentSimilar(s1, s2) {
2381
+ if (s1 === s2) return true;
2382
+ if (s1.length !== s2.length) return false;
2383
+ let matches = 0;
2384
+ const minLength = Math.min(s1.length, s2.length);
2385
+ for (let i = 0; i < minLength; i++) {
2386
+ if (s1[i] === s2[i]) {
2387
+ matches++;
2388
+ }
2389
+ }
2390
+ const similarity = matches / minLength;
2391
+ return similarity >= HALLUCINATION_THRESHOLDS.SEGMENT_SIMILARITY_THRESHOLD;
2392
+ }
2393
+ function calculateEntropy(text) {
2394
+ if (!text || text.length === 0) {
2395
+ return 0;
2396
+ }
2397
+ const frequencies = /* @__PURE__ */ new Map();
2398
+ for (const char of text.toLowerCase()) {
2399
+ frequencies.set(char, (frequencies.get(char) || 0) + 1);
2400
+ }
2401
+ let entropy = 0;
2402
+ const length = text.length;
2403
+ for (const count of frequencies.values()) {
2404
+ const probability = count / length;
2405
+ entropy -= probability * Math.log2(probability);
2406
+ }
2407
+ return entropy;
2408
+ }
2409
+ function cleanHallucinatedTranscript(transcript) {
2410
+ var _a, _b;
2411
+ const result = detectTranscriptHallucinationWithDetails(transcript);
2412
+ if (result.isHallucination) {
2413
+ console.warn(
2414
+ "Hallucinated transcript detected and removed:",
2415
+ transcript.substring(0, 100),
2416
+ `
2417
+ Reason: ${(_a = result.reason) != null ? _a : "Unknown"}`,
2418
+ `Confidence: ${String((_b = result.confidence) != null ? _b : "Unknown")}`
2419
+ );
2420
+ return "";
2421
+ }
2422
+ return transcript;
2423
+ }
2424
+
2425
+ // src/utils/ai/get-transcript.ts
2426
+ async function getTranscript(model, args, cleanHallucinations = true) {
2427
+ var _a, _b, _c, _d, _e, _f;
2428
+ const getGeminiTranscript = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "getGeminiTranscript");
2429
+ const getAssemblyAITranscript = (_d = (_c = api).httpsCallable) == null ? void 0 : _d.call(_c, "transcribeAssemblyAIAudio");
2430
+ const getWhisperTranscript = (_f = (_e = api).httpsCallable) == null ? void 0 : _f.call(_e, "generateGroqTranscript");
2431
+ if (model === "whisper") {
2432
+ try {
2433
+ const { data } = await (getWhisperTranscript == null ? void 0 : getWhisperTranscript({
2434
+ audioUrl: args.audioUrl,
2435
+ language: args.language
2436
+ }));
2437
+ return cleanHallucinations ? cleanHallucinatedTranscript(data) : data;
2438
+ } catch (error) {
2439
+ console.error("Error getting transcript from Whisper:", error);
2440
+ throw error;
2441
+ }
2442
+ }
2443
+ if (model === "gemini") {
2444
+ try {
2445
+ const { data } = await (getGeminiTranscript == null ? void 0 : getGeminiTranscript({
2446
+ audioUrl: args.audioUrl,
2447
+ targetLanguage: args.language,
2448
+ prompt: args.prompt
2449
+ }));
2450
+ return cleanHallucinations ? cleanHallucinatedTranscript(data.transcript) : data.transcript;
2451
+ } catch (error) {
2452
+ console.error("Error getting transcript from Gemini:", error);
2453
+ throw error;
2454
+ }
2455
+ }
2456
+ if (model === "assemblyai") {
2457
+ try {
2458
+ const response = await (getAssemblyAITranscript == null ? void 0 : getAssemblyAITranscript({
2459
+ audioUrl: args.audioUrl,
2460
+ language: args.language
2461
+ }));
2462
+ return cleanHallucinations ? cleanHallucinatedTranscript(response.data) : response.data;
2463
+ } catch (error) {
2464
+ console.error("Error getting transcript from AssemblyAI:", error);
2465
+ throw error;
2466
+ }
2467
+ }
2468
+ return null;
2469
+ }
2470
+ async function getTranscriptCycle(args) {
2471
+ const models = ["whisper", "gemini", "assemblyai"];
2472
+ let transcript = "";
2473
+ let lastError = null;
2474
+ for (const model of models) {
2475
+ try {
2476
+ console.log("Getting transcript from", model);
2477
+ const transcriptResult = await getTranscript(model, args, false);
2478
+ const rawTranscript = transcriptResult || "";
2479
+ transcript = cleanHallucinatedTranscript(rawTranscript);
2480
+ if (transcript !== "") {
2481
+ console.log(`Successfully got transcript from ${model}`);
2482
+ break;
2483
+ }
2484
+ console.warn(`${model} returned empty transcript, trying next model`);
2485
+ } catch (e) {
2486
+ console.error(`Error with ${model} transcript:`, e);
2487
+ lastError = e;
2488
+ }
2489
+ }
2490
+ if (transcript === "") {
2491
+ console.error("All transcript models failed or returned empty", lastError);
2492
+ return {
2493
+ transcript: "",
2494
+ success: false
2495
+ };
2496
+ }
2497
+ return {
2498
+ transcript,
2499
+ success: true
2500
+ };
2501
+ }
2502
+
2503
+ // src/constants/all-langs.json
2504
+ var all_langs_default = {
2505
+ af: "Afrikaans",
2506
+ sq: "Albanian",
2507
+ am: "Amharic",
2508
+ ar: "Arabic",
2509
+ hy: "Armenian",
2510
+ az: "Azerbaijani",
2511
+ eu: "Basque",
2512
+ be: "Belarusian",
2513
+ bn: "Bengali",
2514
+ bs: "Bosnian",
2515
+ bg: "Bulgarian",
2516
+ ca: "Catalan",
2517
+ ceb: "Cebuano",
2518
+ zh: "Chinese",
2519
+ co: "Corsican",
2520
+ hr: "Croatian",
2521
+ cs: "Czech",
2522
+ da: "Danish",
2523
+ nl: "Dutch",
2524
+ en: "English",
2525
+ eo: "Esperanto",
2526
+ et: "Estonian",
2527
+ fi: "Finnish",
2528
+ fr: "French",
2529
+ fy: "Frisian",
2530
+ gl: "Galician",
2531
+ ka: "Georgian",
2532
+ de: "German",
2533
+ el: "Greek",
2534
+ gu: "Gujarati",
2535
+ ht: "Haitian Creole",
2536
+ ha: "Hausa",
2537
+ haw: "Hawaiian",
2538
+ he: "Hebrew",
2539
+ hi: "Hindi",
2540
+ hmn: "Hmong",
2541
+ hu: "Hungarian",
2542
+ is: "Icelandic",
2543
+ ig: "Igbo",
2544
+ id: "Indonesian",
2545
+ ga: "Irish",
2546
+ it: "Italian",
2547
+ ja: "Japanese",
2548
+ jv: "Javanese",
2549
+ kn: "Kannada",
2550
+ kk: "Kazakh",
2551
+ km: "Khmer",
2552
+ ko: "Korean",
2553
+ ku: "Kurdish",
2554
+ ky: "Kyrgyz",
2555
+ lo: "Lao",
2556
+ la: "Latin",
2557
+ lv: "Latvian",
2558
+ lt: "Lithuanian",
2559
+ lb: "Luxembourgish",
2560
+ mk: "Macedonian",
2561
+ mg: "Malagasy",
2562
+ ms: "Malay",
2563
+ ml: "Malayalam",
2564
+ mt: "Maltese",
2565
+ mi: "Maori",
2566
+ mr: "Marathi",
2567
+ mn: "Mongolian",
2568
+ my: "Myanmar (Burmese)",
2569
+ ne: "Nepali",
2570
+ no: "Norwegian",
2571
+ ny: "Nyanja (Chichewa)",
2572
+ ps: "Pashto",
2573
+ fa: "Persian",
2574
+ pl: "Polish",
2575
+ pt: "Portuguese",
2576
+ pa: "Punjabi",
2577
+ ro: "Romanian",
2578
+ ru: "Russian",
2579
+ sm: "Samoan",
2580
+ gd: "Scots Gaelic",
2581
+ sr: "Serbian",
2582
+ st: "Sesotho",
2583
+ sn: "Shona",
2584
+ sd: "Sindhi",
2585
+ si: "Sinhala (Sinhalese)",
2586
+ sk: "Slovak",
2587
+ sl: "Slovenian",
2588
+ so: "Somali",
2589
+ es: "Spanish",
2590
+ su: "Sundanese",
2591
+ sw: "Swahili",
2592
+ sv: "Swedish",
2593
+ tl: "Tagalog (Filipino)",
2594
+ tg: "Tajik",
2595
+ ta: "Tamil",
2596
+ te: "Telugu",
2597
+ th: "Thai",
2598
+ tr: "Turkish",
2599
+ uk: "Ukrainian",
2600
+ ur: "Urdu",
2601
+ uz: "Uzbek",
2602
+ vi: "Vietnamese",
2603
+ cy: "Welsh",
2604
+ xh: "Xhosa",
2605
+ yi: "Yiddish",
2606
+ yo: "Yoruba",
2607
+ zu: "Zulu"
2608
+ };
2609
+
2610
+ // src/utils/ai/get-respond-card-tool.ts
2611
+ var getRespondCardTool = ({
2612
+ language,
2613
+ standard = "actfl"
2614
+ }) => {
2615
+ const lang = all_langs_default[language] || "English";
2616
+ const tool = {
2617
+ tool_choice: {
2618
+ type: "function",
2619
+ function: { name: "get_feedback" }
2620
+ },
2621
+ tools: [
2622
+ {
2623
+ type: "function",
2624
+ function: {
2625
+ name: "get_feedback",
2626
+ description: "Get feedback on a student's response",
2627
+ parameters: {
2628
+ type: "object",
2629
+ required: [
2630
+ "success",
2631
+ "score",
2632
+ "score_justification",
2633
+ "errors",
2634
+ "improvedResponse",
2635
+ "compliments"
2636
+ ],
2637
+ properties: {
2638
+ success: {
2639
+ type: "boolean",
2640
+ description: "Mark true if the student's response was on-topic and generally demonstrated understanding. A few grammar mistakes are acceptable. Mark false if the student's response was off-topic or did not demonstrate understanding."
2641
+ },
2642
+ errors: {
2643
+ type: "array",
2644
+ items: {
2645
+ type: "object",
2646
+ required: ["error", "grammar_error_type", "correction", "justification"],
2647
+ properties: {
2648
+ error: {
2649
+ type: "string",
2650
+ description: "The grammatical error in the student's response."
2651
+ },
2652
+ correction: {
2653
+ type: "string",
2654
+ description: "The suggested correction to the error"
2655
+ },
2656
+ justification: {
2657
+ type: "string",
2658
+ description: `An explanation of the rationale behind the suggested correction. WRITE THIS IN ${lang}!`
2659
+ },
2660
+ grammar_error_type: {
2661
+ type: "string",
2662
+ enum: [
2663
+ "subjVerbAgree",
2664
+ "tenseErrors",
2665
+ "articleMisuse",
2666
+ "prepositionErrors",
2667
+ "adjNounAgree",
2668
+ "pronounErrors",
2669
+ "wordOrder",
2670
+ "verbConjugation",
2671
+ "pluralization",
2672
+ "negationErrors",
2673
+ "modalVerbMisuse",
2674
+ "relativeClause",
2675
+ "auxiliaryVerb",
2676
+ "complexSentenceAgreement",
2677
+ "idiomaticExpression",
2678
+ "registerInconsistency",
2679
+ "voiceMisuse"
2680
+ ],
2681
+ description: "The type of grammatical error found. It should be one of the following categories: subject-verb agreement, tense errors, article misuse, preposition errors, adjective-noun agreement, pronoun errors, word order, verb conjugation, pluralization errors, negation errors, modal verb misuse, relative clause errors, auxiliary verb misuse, complex sentence agreement, idiomatic expression, register inconsistency, or voice misuse"
2682
+ }
2683
+ }
2684
+ },
2685
+ description: "An array of objects, each representing a grammatical error in the student's response. Each object should have the following properties: error, grammar_error_type, correction, and justification. If there were no errors, return an empty array."
2686
+ },
2687
+ compliments: {
2688
+ type: "array",
2689
+ items: {
2690
+ type: "string"
2691
+ },
2692
+ description: `An array of strings, each representing something the student did well. Each string should be WRITTEN IN ${lang}!`
2693
+ },
2694
+ improvedResponse: {
2695
+ type: "string",
2696
+ description: "An improved response with proper grammar and more detail, if applicable."
2697
+ },
2698
+ score: {
2699
+ type: "number",
2700
+ description: "A score between 0 and 100, reflecting the overall quality of the response"
2701
+ },
2702
+ score_justification: {
2703
+ type: "string",
2704
+ description: "An explanation of the rationale behind the assigned score, considering both accuracy and fluency"
2705
+ }
2706
+ }
2707
+ }
2708
+ }
2709
+ }
2710
+ ]
2711
+ };
2712
+ if (standard === "wida") {
2713
+ const wida_level = {
2714
+ type: "number",
2715
+ enum: [1, 2, 3, 4, 5, 6],
2716
+ description: `The student's WIDA (World-Class Instructional Design and Assessment) proficiency level. Choose one of the following options: 1, 2, 3, 4, 5, 6 which corresponds to
2717
+
2718
+ 1 - Entering
2719
+ 2 - Emerging
2720
+ 3 - Developing
2721
+ 4 - Expanding
2722
+ 5 - Bridging
2723
+ 6 - Reaching
2724
+
2725
+ This is an estimate based on the level of the student's response. Use the descriptions of the WIDA speaking standards to guide your decision.
2726
+ `
2727
+ };
2728
+ const wida_justification = {
2729
+ type: "string",
2730
+ description: `An explanation of the rationale behind the assigned WIDA level of the response, considering both accuracy and fluency. WRITE THIS IN ENGLISH!`
2731
+ };
2732
+ tool.tools[0].function.parameters.required.push("wida_level");
2733
+ tool.tools[0].function.parameters.required.push("wida_justification");
2734
+ tool.tools[0].function.parameters.properties.wida_level = wida_level;
2735
+ tool.tools[0].function.parameters.properties.wida_justification = wida_justification;
2736
+ } else {
2737
+ const actfl_level = {
2738
+ type: "string",
2739
+ enum: ["NL", "NM", "NH", "IL", "IM", "IH", "AL", "AM", "AH", "S", "D"],
2740
+ description: "The student's ACTFL (American Council on the Teaching of Foreign Languages) proficiency level. Choose one of the following options: NL, NM, NH, IL, IM, IH, AL, AM, AH, S, or D"
2741
+ };
2742
+ const actfl_justification = {
2743
+ type: "string",
2744
+ description: "An explanation of the rationale behind the assigned ACTFL level, considering both accuracy and fluency"
2745
+ };
2746
+ tool.tools[0].function.parameters.required.push("actfl_level");
2747
+ tool.tools[0].function.parameters.required.push("actfl_justification");
2748
+ tool.tools[0].function.parameters.properties.actfl_level = actfl_level;
2749
+ tool.tools[0].function.parameters.properties.actfl_justification = actfl_justification;
2750
+ }
2751
+ return tool;
2752
+ };
2753
+
2754
+ // src/hooks/useActivity.ts
2755
+ import { useEffect as useEffect2 } from "react";
2756
+
2757
+ // src/services/add-grading-standard.ts
2758
+ var addGradingStandardLog = async (gradingStandard, userId) => {
2759
+ logGradingStandardLog(gradingStandard);
2760
+ const path = `users/${userId}/grading_standard_logs`;
2761
+ await api.addDoc(path, gradingStandard);
2762
+ };
2763
+
2764
+ // src/hooks/useActivityTracker.ts
2765
+ import { v4 as v42 } from "uuid";
2766
+ function useActivityTracker({ userId }) {
2767
+ const trackActivity = async ({
2768
+ activityName,
2769
+ activityType,
2770
+ id = v42(),
2771
+ language = ""
2772
+ }) => {
2773
+ if (userId) {
2774
+ const { doc: doc2, serverTimestamp: serverTimestamp2, setDoc: setDoc2 } = api.accessHelpers();
2775
+ const activityRef = doc2(`users/${userId}/activity/${id}`);
2776
+ const timestamp = serverTimestamp2();
2777
+ await setDoc2(activityRef, {
2778
+ name: activityName,
2779
+ type: activityType,
2780
+ lastSeen: timestamp,
2781
+ id,
2782
+ language
2783
+ });
2784
+ }
2785
+ };
2786
+ return {
2787
+ trackActivity
2788
+ };
2789
+ }
2790
+
2791
+ // src/hooks/useActivity.ts
2792
+ function useActivity({
2793
+ id,
2794
+ isAssignment,
2795
+ onAssignmentSubmitted,
2796
+ ltiData
2797
+ }) {
2798
+ var _a, _b;
2799
+ const { queryClient, user } = useSpeakableApi();
2800
+ const userId = user.auth.uid;
2801
+ const assignmentQuery = useAssignment({
2802
+ assignmentId: id,
2803
+ userId,
2804
+ enabled: isAssignment
2805
+ });
2806
+ const activeAssignment = assignmentQuery.data;
2807
+ const setId = isAssignment ? (_a = activeAssignment == null ? void 0 : activeAssignment.setId) != null ? _a : "" : id;
2808
+ const querySet = useSet({ setId });
2809
+ const setData = querySet.data;
2810
+ const assignmentContent = activeAssignment == null ? void 0 : activeAssignment.content;
2811
+ const assignmentWeights = activeAssignment == null ? void 0 : activeAssignment.weights;
2812
+ const setContent = setData == null ? void 0 : setData.content;
2813
+ const setWeights = setData == null ? void 0 : setData.weights;
2814
+ const contentCardsToUse = isAssignment ? assignmentContent != null ? assignmentContent : setContent : setContent;
2815
+ const weightsToUse = isAssignment ? assignmentWeights != null ? assignmentWeights : setWeights : setWeights;
2816
+ const activityId = isAssignment ? (_b = activeAssignment == null ? void 0 : activeAssignment.id) != null ? _b : "" : setId;
2817
+ const { cardsObject, cardsQueries, cards } = useCards({
2818
+ cardIds: contentCardsToUse != null ? contentCardsToUse : [],
2819
+ enabled: querySet.isSuccess,
2820
+ asObject: true
2821
+ });
2822
+ const scorableCardIds = (contentCardsToUse != null ? contentCardsToUse : []).filter((cardId) => {
2823
+ const card = cardsObject == null ? void 0 : cardsObject[cardId];
2824
+ return (card == null ? void 0 : card.type) !== "MEDIA_PAGE" /* MEDIA_PAGE */;
2825
+ });
2826
+ const scoreQuery = useScore({
2827
+ isAssignment,
2828
+ activityId: id,
2829
+ userId,
2830
+ courseId: activeAssignment == null ? void 0 : activeAssignment.courseId,
2831
+ googleClassroomUserId: user.profile.googleClassroomUserId,
2832
+ enabled: isAssignment ? assignmentQuery.isSuccess : querySet.isSuccess
2833
+ });
2834
+ const { mutationUpdateScore } = useUpdateScore();
2835
+ const { mutationUpdateCardScore } = useUpdateCardScore({
2836
+ activityId,
2837
+ isAssignment,
2838
+ userId,
2839
+ cardIds: scorableCardIds,
2840
+ weights: weightsToUse != null ? weightsToUse : {}
2841
+ });
2842
+ const { mutationClearScore } = useClearScore();
2843
+ const { submitAssignmentScore: submitAssignmentScore2 } = useSubmitAssignmentScore({
2844
+ onAssignmentSubmitted,
2845
+ studentName: user.profile.displayName
2846
+ });
2847
+ const { submitPracticeScore: submitPracticeScore2 } = useSubmitPracticeScore();
2848
+ const handleUpdateScore = (data) => {
2849
+ mutationUpdateScore.mutate({
2850
+ data,
2851
+ isAssignment,
2852
+ activityId,
2853
+ userId
2854
+ });
2855
+ };
2856
+ const handleUpdateCardScore = (cardId, cardScore) => {
2857
+ mutationUpdateCardScore.mutate({ cardId, cardScore });
2858
+ if (cardScore.proficiency_level) {
2859
+ logGradingStandardEntry({
2860
+ type: cardScore.proficiency_level.standardId,
2861
+ cardId,
2862
+ gradingStandard: cardScore.proficiency_level
2863
+ });
2864
+ } else if (cardScore.wida || cardScore.actfl) {
2865
+ logGradingStandardEntry({
2866
+ type: cardScore.wida ? "wida" : "actfl",
2867
+ cardId,
2868
+ gradingStandard: cardScore.wida || cardScore.actfl || { level: "", justification: "" }
2869
+ });
2870
+ }
2871
+ };
2872
+ const onClearScore = ({
2873
+ cardId,
2874
+ wasCompleted = true
2875
+ }) => {
2876
+ var _a2, _b2;
2877
+ const currentCard = cardsObject == null ? void 0 : cardsObject[cardId];
2878
+ if ((currentCard == null ? void 0 : currentCard.type) === "MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */ || (currentCard == null ? void 0 : currentCard.type) === "READ_REPEAT" /* READ_REPEAT */) {
2879
+ return;
2880
+ }
2881
+ const queryKeys = scoreQueryKeys.byId(activityId);
2882
+ const activeCardScores = (_b2 = (_a2 = queryClient.getQueryData(queryKeys)) == null ? void 0 : _a2.cards) == null ? void 0 : _b2[cardId];
2883
+ if (activeCardScores === void 0) return;
2884
+ mutationClearScore.mutate({
2885
+ isAssignment,
2886
+ activityId,
2887
+ cardScores: activeCardScores,
2888
+ cardId,
2889
+ userId
2890
+ });
2891
+ };
2892
+ const onSubmitScore = async () => {
2893
+ var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
2894
+ try {
2895
+ let results;
2896
+ if (isAssignment) {
2897
+ const someCardIsManualGraded = cards.some((page) => page.grading_method === "manual");
2898
+ results = await submitAssignmentScore2({
2899
+ assignment: {
2900
+ id: (_b2 = (_a2 = assignmentQuery.data) == null ? void 0 : _a2.id) != null ? _b2 : "",
2901
+ name: (_d = (_c = assignmentQuery.data) == null ? void 0 : _c.name) != null ? _d : "",
2902
+ owners: (_f = (_e = assignmentQuery.data) == null ? void 0 : _e.owners) != null ? _f : [],
2903
+ courseId: (_h = (_g = assignmentQuery.data) == null ? void 0 : _g.courseId) != null ? _h : "",
2904
+ courseWorkId: (_j = (_i = assignmentQuery.data) == null ? void 0 : _i.courseWorkId) != null ? _j : "",
2905
+ isAssessment: (_l = (_k = assignmentQuery.data) == null ? void 0 : _k.isAssessment) != null ? _l : false,
2906
+ maxPoints: (_n = (_m = assignmentQuery.data) == null ? void 0 : _m.maxPoints) != null ? _n : 0
2907
+ },
2908
+ userId,
2909
+ scores: scoreQuery.data,
2910
+ status: someCardIsManualGraded ? "PENDING_REVIEW" : "SUBMITTED"
2911
+ });
2912
+ if ((_o = assignmentQuery.data) == null ? void 0 : _o.ltiDeeplink) {
2913
+ submitLTIScore({
2914
+ maxPoints: (_p = assignmentQuery.data) == null ? void 0 : _p.maxPoints,
2915
+ score: (_r = (_q = scoreQuery.data) == null ? void 0 : _q.score) != null ? _r : 0,
2916
+ SERVICE_KEY: (_s = ltiData == null ? void 0 : ltiData.serviceKey) != null ? _s : "",
2917
+ lineItemId: (_t = ltiData == null ? void 0 : ltiData.lineItemId) != null ? _t : "",
2918
+ lti_id: (_u = ltiData == null ? void 0 : ltiData.lti_id) != null ? _u : ""
2919
+ });
2920
+ }
2921
+ } else {
2922
+ results = await submitPracticeScore2({
2923
+ setId: (_w = (_v = querySet.data) == null ? void 0 : _v.id) != null ? _w : "",
2924
+ userId,
2925
+ scores: scoreQuery.data
2926
+ });
2927
+ }
2928
+ return results;
2929
+ } catch (error) {
2930
+ return {
2931
+ success: false,
2932
+ error
2933
+ };
2934
+ }
2935
+ };
2936
+ const logGradingStandardEntry = ({
2937
+ cardId,
2938
+ gradingStandard,
2939
+ type
2940
+ }) => {
2941
+ var _a2, _b2, _c, _d, _e, _f, _g, _h, _i;
2942
+ const card = cardsObject == null ? void 0 : cardsObject[cardId];
2943
+ const scoresObject = queryClient.getQueryData(scoreQueryKeys.byId(activityId));
2944
+ const cardScore = (_a2 = scoresObject == null ? void 0 : scoresObject.cards) == null ? void 0 : _a2[cardId];
2945
+ const serverTimestamp2 = api.helpers.serverTimestamp;
2946
+ addGradingStandardLog(
2947
+ {
2948
+ assignmentId: (_b2 = activeAssignment == null ? void 0 : activeAssignment.id) != null ? _b2 : "",
2949
+ courseId: (_c = activeAssignment == null ? void 0 : activeAssignment.courseId) != null ? _c : "",
2950
+ teacherId: (_d = activeAssignment == null ? void 0 : activeAssignment.owners[0]) != null ? _d : "",
2951
+ setId: (_e = setData == null ? void 0 : setData.id) != null ? _e : "",
2952
+ cardId,
2953
+ level: gradingStandard.level,
2954
+ justification: gradingStandard.justification,
2955
+ transcript: (_f = cardScore == null ? void 0 : cardScore.transcript) != null ? _f : "",
2956
+ audioUrl: (_g = cardScore == null ? void 0 : cardScore.audio) != null ? _g : "",
2957
+ prompt: (_h = card == null ? void 0 : card.prompt) != null ? _h : "",
2958
+ responseType: (card == null ? void 0 : card.type) === "RESPOND_WRITE" /* RESPOND_WRITE */ ? "written" : "spoken",
2959
+ type,
2960
+ dateMade: serverTimestamp2(),
2961
+ language: (_i = card == null ? void 0 : card.language) != null ? _i : ""
2962
+ },
2963
+ userId
2964
+ );
2965
+ };
2966
+ useEffect2(() => {
2967
+ if (isAssignment) {
2968
+ logOpenAssignment({ assignmentId: id });
2969
+ } else {
2970
+ logOpenActivityPreview({ setId: id });
2971
+ }
2972
+ }, []);
2973
+ useInitActivity({
2974
+ assignment: activeAssignment != null ? activeAssignment : void 0,
2975
+ set: setData != null ? setData : void 0,
2976
+ enabled: !!setData,
2977
+ userId
2978
+ });
2979
+ return {
2980
+ set: {
2981
+ data: setData,
2982
+ query: querySet
2983
+ },
2984
+ cards: {
2985
+ data: cardsObject,
2986
+ query: cardsQueries,
2987
+ cardsArray: cards
2988
+ },
2989
+ assignment: {
2990
+ data: isAssignment ? activeAssignment : void 0,
2991
+ query: assignmentQuery
2992
+ },
2993
+ scores: {
2994
+ data: scoreQuery.data,
2995
+ query: scoreQuery,
2996
+ actions: {
2997
+ update: handleUpdateScore,
2998
+ clear: onClearScore,
2999
+ submit: onSubmitScore,
3000
+ updateCard: handleUpdateCardScore,
3001
+ logGradingStandardEntry
3002
+ }
3003
+ }
3004
+ };
3005
+ }
3006
+ var useInitActivity = ({
3007
+ assignment,
3008
+ set,
3009
+ enabled,
3010
+ userId
3011
+ }) => {
3012
+ const { trackActivity } = useActivityTracker({ userId });
3013
+ const init = () => {
3014
+ var _a, _b, _c, _d, _e, _f, _g;
3015
+ if (!enabled) return;
3016
+ if (!assignment) {
3017
+ trackActivity({
3018
+ activityName: (_a = set == null ? void 0 : set.name) != null ? _a : "",
3019
+ activityType: "set",
3020
+ id: set == null ? void 0 : set.id,
3021
+ language: set == null ? void 0 : set.language
3022
+ });
3023
+ } else if (assignment.name) {
3024
+ trackActivity({
3025
+ activityName: assignment.name,
3026
+ activityType: assignment.isAssessment ? "assessment" : "assignment",
3027
+ id: assignment.id,
3028
+ language: set == null ? void 0 : set.language
3029
+ });
3030
+ }
3031
+ if (set == null ? void 0 : set.public) {
3032
+ (_d = (_c = (_b = api).httpsCallable) == null ? void 0 : _c.call(_b, "onSetOpened")) == null ? void 0 : _d({
3033
+ setId: set.id,
3034
+ language: set.language
3035
+ });
3036
+ }
3037
+ (_g = (_f = (_e = api).httpsCallable) == null ? void 0 : _f.call(_e, "updateAlgoliaIndex")) == null ? void 0 : _g({
3038
+ updatePlays: true,
3039
+ objectID: set == null ? void 0 : set.id
3040
+ });
3041
+ };
3042
+ useEffect2(() => {
3043
+ init();
3044
+ }, [set]);
3045
+ };
3046
+ var submitLTIScore = async ({
3047
+ maxPoints,
3048
+ score,
3049
+ SERVICE_KEY,
3050
+ lineItemId,
3051
+ lti_id
3052
+ }) => {
3053
+ var _a, _b, _c;
3054
+ try {
3055
+ if (!SERVICE_KEY || !lineItemId || !lti_id) {
3056
+ throw new Error("Missing required LTI credentials");
3057
+ }
3058
+ const earnedPoints = score ? score / 100 * maxPoints : 0;
3059
+ const { data } = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitLTIAssignmentScore")) == null ? void 0 : _c({
3060
+ SERVICE_KEY,
3061
+ scoreData: {
3062
+ lineItemId,
3063
+ userId: lti_id,
3064
+ maxPoints,
3065
+ earnedPoints
3066
+ }
3067
+ }));
3068
+ return { success: true, data };
3069
+ } catch (error) {
3070
+ console.error("Failed to submit LTI score:", error);
3071
+ return {
3072
+ success: false,
3073
+ error: error instanceof Error ? error : new Error("Unknown error occurred")
3074
+ };
3075
+ }
3076
+ };
3077
+
3078
+ // src/hooks/useCredits.ts
3079
+ import { useQuery as useQuery5 } from "@tanstack/react-query";
3080
+ var creditQueryKeys = {
3081
+ userCredits: (uid) => ["userCredits", uid]
3082
+ };
3083
+ var useUserCredits = () => {
3084
+ const { user } = useSpeakableApi();
3085
+ const email = user.auth.email;
3086
+ const uid = user.auth.uid;
3087
+ const query2 = useQuery5({
3088
+ queryKey: creditQueryKeys.userCredits(uid),
3089
+ queryFn: () => fetchUserCredits({ uid, email }),
3090
+ enabled: !!uid,
3091
+ refetchInterval: 1e3 * 60 * 5
3092
+ });
3093
+ return {
3094
+ ...query2
3095
+ };
3096
+ };
3097
+ var fetchUserCredits = async ({ uid, email }) => {
3098
+ if (!uid) {
3099
+ throw new Error("User ID is required");
3100
+ }
3101
+ const contractSnap = await api.getDoc(`creditContracts/${uid}`);
3102
+ if (contractSnap.data == null) {
3103
+ return {
3104
+ id: uid,
3105
+ userId: uid,
3106
+ email,
3107
+ effectivePlanId: "free_tier",
3108
+ status: "inactive",
3109
+ isUnlimited: false,
3110
+ creditsAvailable: 100,
3111
+ creditsAllocatedThisPeriod: 100,
3112
+ topOffCreditsAvailable: 0,
3113
+ topOffCreditsTotal: 0,
3114
+ allocationSource: "free_tier",
3115
+ sourceDetails: {},
3116
+ periodStart: null,
3117
+ periodEnd: null,
3118
+ planTermEndTimestamp: null,
3119
+ ownerType: "individual",
3120
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3121
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
3122
+ };
3123
+ }
3124
+ const contractData = contractSnap.data;
3125
+ const monthlyCredits = (contractData == null ? void 0 : contractData.creditsAvailable) || 0;
3126
+ const topOffCredits = (contractData == null ? void 0 : contractData.topOffCreditsAvailable) || 0;
3127
+ const totalCredits = monthlyCredits + topOffCredits;
3128
+ return {
3129
+ id: contractSnap.id,
3130
+ ...contractData,
3131
+ // Add computed total for convenience
3132
+ totalCreditsAvailable: totalCredits
3133
+ };
3134
+ };
3135
+
3136
+ // src/hooks/useOrganizationAccess.ts
3137
+ import { useQuery as useQuery6 } from "@tanstack/react-query";
3138
+ var useOrganizationAccess = () => {
3139
+ const { user } = useSpeakableApi();
3140
+ const email = user.auth.email;
3141
+ const query2 = useQuery6({
3142
+ queryKey: ["organizationAccess", email],
3143
+ queryFn: async () => {
3144
+ if (!email) {
3145
+ return {
3146
+ hasUnlimitedAccess: false,
3147
+ subscriptionId: null,
3148
+ organizationId: null,
3149
+ organizationName: null,
3150
+ subscriptionEndDate: null,
3151
+ accessType: "individual"
3152
+ };
3153
+ }
3154
+ return getOrganizationAccess(email);
3155
+ },
3156
+ enabled: !!email,
3157
+ // Only run query if we have a user email
3158
+ staleTime: 5 * 60 * 1e3,
3159
+ // Consider data fresh for 5 minutes
3160
+ gcTime: 10 * 60 * 1e3,
3161
+ // Keep in cache for 10 minutes
3162
+ retry: 2
3163
+ // Retry failed requests twice
3164
+ });
3165
+ return {
3166
+ ...query2
3167
+ };
3168
+ };
3169
+ var getOrganizationAccess = async (email) => {
3170
+ const { limit: limit2, where: where2 } = api.accessQueryConstraints();
3171
+ try {
3172
+ const organizationSnapshot = await api.getDocs(
3173
+ "organizations",
3174
+ where2("members", "array-contains", email),
3175
+ where2("masterSubscriptionStatus", "==", "active"),
3176
+ limit2(1)
3177
+ );
3178
+ if (!organizationSnapshot.empty) {
3179
+ const orgData = organizationSnapshot.data[0];
3180
+ return {
3181
+ hasUnlimitedAccess: true,
3182
+ subscriptionId: orgData == null ? void 0 : orgData.masterSubscriptionId,
3183
+ organizationId: orgData.id,
3184
+ organizationName: orgData.name || "Unknown Organization",
3185
+ subscriptionEndDate: orgData.masterSubscriptionEndDate || null,
3186
+ accessType: "organization"
3187
+ };
3188
+ }
3189
+ const institutionSnapshot = await api.getDocs(
3190
+ "institution_subscriptions",
3191
+ where2("users", "array-contains", email),
3192
+ where2("active", "==", true),
3193
+ limit2(1)
3194
+ );
3195
+ if (!institutionSnapshot.empty) {
3196
+ const institutionData = institutionSnapshot.data[0];
3197
+ const isUnlimited = (institutionData == null ? void 0 : institutionData.plan) === "organization" || (institutionData == null ? void 0 : institutionData.plan) === "school_starter";
3198
+ return {
3199
+ hasUnlimitedAccess: isUnlimited,
3200
+ subscriptionId: institutionData.id,
3201
+ organizationId: institutionData == null ? void 0 : institutionData.institutionId,
3202
+ organizationName: institutionData.name || institutionData.institutionId || "Legacy Institution",
3203
+ subscriptionEndDate: institutionData.endDate || null,
3204
+ accessType: "institution_subscriptions"
3205
+ };
3206
+ }
3207
+ return {
3208
+ hasUnlimitedAccess: false,
3209
+ subscriptionId: null,
3210
+ organizationId: null,
3211
+ organizationName: null,
3212
+ subscriptionEndDate: null,
3213
+ accessType: "individual"
3214
+ };
3215
+ } catch (error) {
3216
+ console.error("Error checking organization access:", error);
3217
+ return {
3218
+ hasUnlimitedAccess: false,
3219
+ subscriptionId: null,
3220
+ organizationId: null,
3221
+ organizationName: null,
3222
+ subscriptionEndDate: null,
3223
+ accessType: "individual"
3224
+ };
3225
+ }
3226
+ };
3227
+
3228
+ // src/hooks/useSpeakableTranscript.ts
3229
+ import { useMutation as useMutation3 } from "@tanstack/react-query";
3230
+ function useSpeakableTranscript() {
3231
+ const mutation = useMutation3({
3232
+ mutationFn: async ({
3233
+ model,
3234
+ audioUrl,
3235
+ language,
3236
+ prompt
3237
+ }) => {
3238
+ return getTranscript(model, { audioUrl, language, prompt });
3239
+ },
3240
+ retry: false
3241
+ });
3242
+ return {
3243
+ mutation
3244
+ };
3245
+ }
3246
+ function useSpeakableTranscriptCycle() {
3247
+ const mutation = useMutation3({
3248
+ mutationFn: async (args) => {
3249
+ return getTranscriptCycle(args);
3250
+ },
3251
+ retry: false
3252
+ });
3253
+ return {
3254
+ mutationTranscriptCycle: mutation
3255
+ };
3256
+ }
3257
+
3258
+ // src/hooks/useUpdateStudentVoc.ts
3259
+ var useUpdateStudentVocab = (page) => {
3260
+ const { user } = useSpeakableApi();
3261
+ const currentUserId = user == null ? void 0 : user.auth.uid;
3262
+ if (!page || !currentUserId || !page.target_text || !page.language) {
3263
+ return {
3264
+ studentVocabMarkVoiceSuccess: void 0,
3265
+ studentVocabMarkVoiceFail: void 0
3266
+ };
3267
+ }
3268
+ const getDataObject = () => {
3269
+ var _a, _b;
3270
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
3271
+ const language = (_a = page.language) != null ? _a : "en";
3272
+ const word = (_b = page.target_text) != null ? _b : "";
3273
+ const phrase_length = getPhraseLength(word);
3274
+ const wordHash = getWordHash(word, language);
3275
+ const docPath = `users/${currentUserId}/vocab/${wordHash}`;
3276
+ const communityPath = `checked-pronunciations/${wordHash}`;
3277
+ const id = `${language}-${cleanString(word)}`;
3278
+ const data = {
3279
+ id,
3280
+ word,
3281
+ words: (word == null ? void 0 : word.split(" ")) || [],
3282
+ wordHash,
3283
+ language,
3284
+ lastSeen: serverTimestamp2(),
3285
+ phrase_length
3286
+ };
3287
+ return {
3288
+ docPath,
3289
+ communityPath,
3290
+ data
3291
+ };
3292
+ };
3293
+ const markVoiceSuccess = async () => {
3294
+ const { docPath, communityPath, data } = getDataObject();
3295
+ const { increment: increment2 } = api.accessQueryConstraints();
3296
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
3297
+ data.voiceSuccess = increment2(1);
3298
+ try {
3299
+ await api.updateDoc(docPath, data);
3300
+ } catch (error) {
3301
+ if (error instanceof Error && "code" in error) {
3302
+ const firebaseError = error;
3303
+ if (firebaseError.code === "not-found") {
3304
+ data.firstSeen = serverTimestamp2();
3305
+ await api.setDoc(docPath, data, { merge: true });
3306
+ } else {
3307
+ console.error("Error actualizando vocabulario:", error);
3308
+ }
3309
+ } else {
3310
+ console.error("Error desconocido:", error);
3311
+ }
3312
+ }
3313
+ try {
3314
+ data.pronunciations = increment2(1);
3315
+ await api.setDoc(communityPath, data, { merge: true });
3316
+ } catch (error) {
3317
+ console.log(error);
3318
+ }
3319
+ };
3320
+ const markVoiceFail = async () => {
3321
+ const { docPath, communityPath, data } = getDataObject();
3322
+ const { increment: increment2 } = api.accessQueryConstraints();
3323
+ const { serverTimestamp: serverTimestamp2 } = api.accessHelpers();
3324
+ data.voiceFail = increment2(1);
3325
+ try {
3326
+ await api.updateDoc(docPath, data);
3327
+ } catch (error) {
3328
+ if (error instanceof Error && error.message === "not-found") {
3329
+ data.firstSeen = serverTimestamp2();
3330
+ await api.setDoc(docPath, data, { merge: true });
3331
+ } else {
3332
+ console.log(error);
3333
+ }
3334
+ }
3335
+ try {
3336
+ data.fails = increment2(1);
3337
+ await api.setDoc(communityPath, data, { merge: true });
3338
+ } catch (error) {
3339
+ console.log(error);
3340
+ }
3341
+ };
3342
+ return {
3343
+ studentVocabMarkVoiceSuccess: markVoiceSuccess,
3344
+ studentVocabMarkVoiceFail: markVoiceFail
3345
+ };
3346
+ };
3347
+
3348
+ // src/hooks/useActivityFeedbackAccess.ts
3349
+ import { useQuery as useQuery7 } from "@tanstack/react-query";
3350
+ var activityFeedbackAccessQueryKeys = {
3351
+ activityFeedbackAccess: (args) => ["activityFeedbackAccess", ...Object.values(args)]
3352
+ };
3353
+ var useActivityFeedbackAccess = ({
3354
+ aiEnabled = false,
3355
+ isActivityRoute = false
3356
+ }) => {
3357
+ var _a, _b, _c;
3358
+ const { user } = useSpeakableApi();
3359
+ const uid = user.auth.uid;
3360
+ const isTeacher = (_a = user.profile) == null ? void 0 : _a.isTeacher;
3361
+ const isStudent = (_b = user.profile) == null ? void 0 : _b.isStudent;
3362
+ const userRoles = ((_c = user.profile) == null ? void 0 : _c.roles) || [];
3363
+ const query2 = useQuery7({
3364
+ queryKey: activityFeedbackAccessQueryKeys.activityFeedbackAccess({
3365
+ aiEnabled,
3366
+ isActivityRoute
3367
+ }),
3368
+ queryFn: async () => {
3369
+ var _a2, _b2, _c2;
3370
+ if (!uid) {
3371
+ return {
3372
+ canAccessFeedback: false,
3373
+ reason: "Missing user ID",
3374
+ isUnlimited: false,
3375
+ accessType: "none"
3376
+ };
3377
+ }
3378
+ try {
3379
+ if (aiEnabled) {
3380
+ return {
3381
+ canAccessFeedback: true,
3382
+ reason: "AI feedback enabled",
3383
+ isUnlimited: true,
3384
+ accessType: "ai_enabled"
3385
+ };
3386
+ }
3387
+ if (isTeacher || userRoles.includes("ADMIN")) {
3388
+ return {
3389
+ canAccessFeedback: true,
3390
+ reason: "Teacher preview access",
3391
+ isUnlimited: true,
3392
+ accessType: "teacher_preview"
3393
+ };
3394
+ }
3395
+ if (isStudent && isActivityRoute) {
3396
+ try {
3397
+ const result = await ((_c2 = (_b2 = (_a2 = api).httpsCallable) == null ? void 0 : _b2.call(_a2, "checkStudentTeacherPlan")) == null ? void 0 : _c2({
3398
+ studentId: uid
3399
+ }));
3400
+ const planCheckResult = result.data;
3401
+ if (planCheckResult.canAccessFeedback) {
3402
+ return {
3403
+ canAccessFeedback: true,
3404
+ reason: planCheckResult.reason || "Student access via teacher with active plan",
3405
+ isUnlimited: planCheckResult.hasTeacherWithUnlimitedAccess,
3406
+ accessType: "student_with_teacher_plan"
3407
+ };
3408
+ } else {
3409
+ return {
3410
+ canAccessFeedback: false,
3411
+ reason: planCheckResult.reason || "No teacher with active plan found",
3412
+ isUnlimited: false,
3413
+ accessType: "none"
3414
+ };
3415
+ }
3416
+ } catch (error) {
3417
+ console.error("Error checking student teacher plan:", error);
3418
+ return {
3419
+ canAccessFeedback: false,
3420
+ reason: "Error checking teacher plans",
3421
+ isUnlimited: false,
3422
+ accessType: "none"
3423
+ };
3424
+ }
3425
+ }
3426
+ return {
3427
+ canAccessFeedback: false,
3428
+ reason: "No access permissions found for current context",
3429
+ isUnlimited: false,
3430
+ accessType: "none"
3431
+ };
3432
+ } catch (error) {
3433
+ console.error("Error checking activity feedback access:", error);
3434
+ return {
3435
+ canAccessFeedback: false,
3436
+ reason: "Error checking access permissions",
3437
+ isUnlimited: false,
3438
+ accessType: "none"
3439
+ };
3440
+ }
3441
+ },
3442
+ enabled: !!uid,
3443
+ staleTime: 5 * 60 * 1e3,
3444
+ // 5 minutes
3445
+ gcTime: 10 * 60 * 1e3
3446
+ // 10 minutes
3447
+ });
3448
+ return {
3449
+ ...query2
3450
+ };
3451
+ };
3452
+
3453
+ // src/hooks/useOpenAI.ts
3454
+ var useBaseOpenAI = ({
3455
+ onTranscriptSuccess,
3456
+ onTranscriptError,
3457
+ onCompletionSuccess,
3458
+ onCompletionError,
3459
+ aiEnabled,
3460
+ submitAudioResponse,
3461
+ uploadAudioAndGetTranscript,
3462
+ onGetAudioUrlAndTranscript
3463
+ }) => {
3464
+ const { user, queryClient } = useSpeakableApi();
3465
+ const currentUserId = user.auth.uid;
3466
+ const { data: feedbackAccess } = useActivityFeedbackAccess({
3467
+ aiEnabled
3468
+ });
3469
+ const getTranscript2 = async (audioUrl, language, prompt) => {
3470
+ var _a, _b, _c, _d, _e, _f;
3471
+ const getGeminiTranscript = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "getGeminiTranscript");
3472
+ const getAssemblyAITranscript = (_d = (_c = api).httpsCallable) == null ? void 0 : _d.call(_c, "transcribeAssemblyAIAudio");
3473
+ const getWhisperTranscript = (_f = (_e = api).httpsCallable) == null ? void 0 : _f.call(_e, "transcribeAudio");
3474
+ try {
3475
+ const { data } = await (getWhisperTranscript == null ? void 0 : getWhisperTranscript({
3476
+ audioUrl,
3477
+ language
3478
+ }));
3479
+ const transcript = data;
3480
+ if (transcript) {
3481
+ return transcript;
3482
+ }
3483
+ } catch (error) {
3484
+ console.log("Whisper transcript failed, trying Gemini fallback:", error);
3485
+ }
3486
+ try {
3487
+ const { data } = await (getGeminiTranscript == null ? void 0 : getGeminiTranscript({
3488
+ audioUrl,
3489
+ targetLanguage: language,
3490
+ prompt: prompt || ""
3491
+ }));
3492
+ const transcript = data.transcript;
3493
+ if (transcript) {
3494
+ return transcript;
3495
+ }
3496
+ } catch (error) {
3497
+ console.log("Gemini transcript failed, trying AssemblyAI fallback:", error);
3498
+ }
3499
+ try {
3500
+ const response = await (getAssemblyAITranscript == null ? void 0 : getAssemblyAITranscript({
3501
+ audioUrl,
3502
+ language
3503
+ }));
3504
+ const transcript = response == null ? void 0 : response.data;
3505
+ if (transcript) {
3506
+ return transcript;
3507
+ }
3508
+ throw new Error("Both transcript services failed");
3509
+ } catch (error) {
3510
+ console.log("AssemblyAI transcript also failed:", error);
3511
+ onTranscriptError({
3512
+ type: "TRANSCRIPT",
3513
+ message: (error == null ? void 0 : error.message) || "Error getting transcript from both services"
3514
+ });
3515
+ throw new Error(error);
3516
+ }
3517
+ };
3518
+ const getFreeResponseCompletion = async (messages, isFreeResponse, feedbackLanguage, gradingStandard = "actfl") => {
3519
+ var _a, _b, _c, _d, _e;
3520
+ const responseTool = getRespondCardTool({
3521
+ language: feedbackLanguage,
3522
+ standard: gradingStandard
3523
+ });
3524
+ try {
3525
+ const createChatCompletion = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "createChatCompletion");
3526
+ const {
3527
+ data: {
3528
+ response,
3529
+ prompt_tokens = 0,
3530
+ completion_tokens = 0,
3531
+ success: aiSuccess = false
3532
+ // the AI was able to generate a response
3533
+ }
3534
+ } = await (createChatCompletion == null ? void 0 : createChatCompletion({
3535
+ chat: {
3536
+ model: isFreeResponse ? "gpt-4-1106-preview" : "gpt-3.5-turbo-1106",
3537
+ messages,
3538
+ temperature: 0.7,
3539
+ ...responseTool
3540
+ },
3541
+ type: isFreeResponse ? "LONG_RESPONSE" : "SHORT_RESPONSE"
3542
+ }));
3543
+ const functionArguments = JSON.parse(((_e = (_d = (_c = response == null ? void 0 : response.tool_calls) == null ? void 0 : _c[0]) == null ? void 0 : _d.function) == null ? void 0 : _e.arguments) || "{}");
3544
+ const result = {
3545
+ ...functionArguments,
3546
+ prompt_tokens,
3547
+ completion_tokens,
3548
+ aiSuccess
3549
+ };
3550
+ onCompletionSuccess(result);
3551
+ return result;
3552
+ } catch (error) {
3553
+ onCompletionError({
3554
+ type: "COMPLETION",
3555
+ message: (error == null ? void 0 : error.message) || "Error getting completion"
3556
+ });
3557
+ throw new Error(error);
3558
+ }
3559
+ };
3560
+ const getFeedback = async ({
3561
+ cardId,
3562
+ language = "en",
3563
+ // required
3564
+ writtenResponse = null,
3565
+ // if the type = RESPOND_WRITE
3566
+ audio = null,
3567
+ autoGrade = true,
3568
+ file = null,
3569
+ pagePrompt = null
3570
+ }) => {
3571
+ try {
3572
+ if (!(feedbackAccess == null ? void 0 : feedbackAccess.canAccessFeedback)) {
3573
+ const result = {
3574
+ noFeedbackAvailable: true,
3575
+ success: true,
3576
+ reason: (feedbackAccess == null ? void 0 : feedbackAccess.reason) || "No feedback access",
3577
+ accessType: (feedbackAccess == null ? void 0 : feedbackAccess.accessType) || "none"
3578
+ };
3579
+ onCompletionSuccess(result);
3580
+ return result;
3581
+ }
3582
+ let transcript;
3583
+ let audioUrl = void 0;
3584
+ if (writtenResponse) {
3585
+ transcript = writtenResponse;
3586
+ onTranscriptSuccess(writtenResponse);
3587
+ } else if (typeof audio === "string" && file) {
3588
+ if (feedbackAccess == null ? void 0 : feedbackAccess.canAccessFeedback) {
3589
+ transcript = await getTranscript2(audio, language, pagePrompt != null ? pagePrompt : "");
3590
+ audioUrl = audio;
3591
+ onTranscriptSuccess(transcript);
3592
+ } else {
3593
+ console.info(
3594
+ `Transcript not available: ${(feedbackAccess == null ? void 0 : feedbackAccess.reason) || "No feedback access"}`
3595
+ );
3596
+ }
3597
+ } else {
3598
+ const response = await uploadAudioAndGetTranscript(audio || "", language, pagePrompt != null ? pagePrompt : "");
3599
+ transcript = response.transcript;
3600
+ audioUrl = response.audioUrl;
3601
+ }
3602
+ onGetAudioUrlAndTranscript == null ? void 0 : onGetAudioUrlAndTranscript({ transcript, audioUrl });
3603
+ if (feedbackAccess == null ? void 0 : feedbackAccess.canAccessFeedback) {
3604
+ const results = await getAIResponse({
3605
+ cardId,
3606
+ transcript: transcript || ""
3607
+ });
3608
+ let output = results;
3609
+ if (!autoGrade) {
3610
+ output = {
3611
+ ...output,
3612
+ noFeedbackAvailable: true,
3613
+ success: true
3614
+ };
3615
+ }
3616
+ onCompletionSuccess(output);
3617
+ return output;
3618
+ } else {
3619
+ const result = {
3620
+ noFeedbackAvailable: true,
3621
+ success: true,
3622
+ reason: (feedbackAccess == null ? void 0 : feedbackAccess.reason) || "No feedback access",
3623
+ accessType: (feedbackAccess == null ? void 0 : feedbackAccess.accessType) || "none"
3624
+ };
3625
+ onCompletionSuccess(result);
3626
+ return result;
3627
+ }
3628
+ } catch (error) {
3629
+ console.error("Error getting feedback:", error);
3630
+ throw new Error(error);
3631
+ }
3632
+ };
3633
+ const onGetGeminiFeedback = async ({
3634
+ cardId,
3635
+ studentId,
3636
+ studentResponse
3637
+ }) => {
3638
+ var _a, _b;
3639
+ try {
3640
+ const getGeminiFeedback = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "callGetFeedback");
3641
+ return await (getGeminiFeedback == null ? void 0 : getGeminiFeedback({
3642
+ cardId,
3643
+ studentId,
3644
+ studentResponse
3645
+ }));
3646
+ } catch (error) {
3647
+ console.error("Error getting Gemini feedback:", error);
3648
+ throw error;
3649
+ }
3650
+ };
3651
+ const onGetProficiencyEstimate = async ({
3652
+ cardId,
3653
+ studentId,
3654
+ studentResponse
3655
+ }) => {
3656
+ var _a, _b;
3657
+ try {
3658
+ const getProficiencyEstimate = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "getProficiencyEstimate");
3659
+ return await (getProficiencyEstimate == null ? void 0 : getProficiencyEstimate({
3660
+ cardId,
3661
+ studentId,
3662
+ studentResponse
3663
+ }));
3664
+ } catch (error) {
3665
+ console.error("Error getting proficiency estimate:", error);
3666
+ return {};
3667
+ }
3668
+ };
3669
+ const getAIResponse = async ({ cardId, transcript }) => {
3670
+ var _a;
3671
+ try {
3672
+ const card = getCardFromCache({
3673
+ cardId,
3674
+ queryClient
3675
+ });
3676
+ let feedbackData;
3677
+ let proficiencyData = {};
3678
+ if (card && card.grading_method === "manual") {
3679
+ } else if (card && card.grading_method !== "standards_based") {
3680
+ const [geminiResult, proficiencyResult] = await Promise.all([
3681
+ onGetGeminiFeedback == null ? void 0 : onGetGeminiFeedback({
3682
+ cardId,
3683
+ studentId: currentUserId,
3684
+ studentResponse: transcript
3685
+ }),
3686
+ onGetProficiencyEstimate == null ? void 0 : onGetProficiencyEstimate({
3687
+ cardId,
3688
+ studentId: currentUserId,
3689
+ studentResponse: transcript
3690
+ })
3691
+ ]);
3692
+ proficiencyData = (proficiencyResult == null ? void 0 : proficiencyResult.data) || {};
3693
+ feedbackData = {
3694
+ ...(_a = geminiResult == null ? void 0 : geminiResult.data) != null ? _a : {},
3695
+ // @ts-ignore
3696
+ proficiency_level: (proficiencyData == null ? void 0 : proficiencyData.proficiency_level) || null
3697
+ };
3698
+ } else {
3699
+ const geminiResult = await (onGetGeminiFeedback == null ? void 0 : onGetGeminiFeedback({
3700
+ cardId,
3701
+ studentId: currentUserId,
3702
+ studentResponse: transcript
3703
+ }));
3704
+ feedbackData = geminiResult == null ? void 0 : geminiResult.data;
3705
+ }
3706
+ const results = {
3707
+ ...proficiencyData,
3708
+ ...feedbackData,
3709
+ aiSuccess: true,
3710
+ promptSuccess: (feedbackData == null ? void 0 : feedbackData.success) || false,
3711
+ transcript
3712
+ };
3713
+ return results;
3714
+ } catch (error) {
3715
+ onCompletionError({
3716
+ type: "AI_FEEDBACK",
3717
+ message: (error == null ? void 0 : error.message) || "Error getting ai feedback"
3718
+ });
3719
+ throw new Error(error);
3720
+ }
3721
+ };
3722
+ return {
3723
+ submitAudioResponse,
3724
+ uploadAudioAndGetTranscript,
3725
+ getTranscript: getTranscript2,
3726
+ getFreeResponseCompletion,
3727
+ getFeedback
3728
+ };
3729
+ };
3730
+
3731
+ // src/lib/create-firebase-client-native.ts
3732
+ import {
3733
+ getDoc,
3734
+ getDocs,
3735
+ addDoc,
3736
+ setDoc,
3737
+ updateDoc,
3738
+ deleteDoc,
3739
+ runTransaction,
3740
+ writeBatch,
3741
+ doc,
3742
+ collection,
3743
+ query,
3744
+ serverTimestamp,
3745
+ orderBy,
3746
+ limit,
3747
+ startAt,
3748
+ startAfter,
3749
+ endAt,
3750
+ endBefore,
3751
+ where,
3752
+ increment
3753
+ } from "@react-native-firebase/firestore";
3754
+
3755
+ // src/lib/create-firebase-client.ts
3756
+ function createFsClientBase({
3757
+ db,
3758
+ helpers,
3759
+ httpsCallable,
3760
+ logEvent
3761
+ }) {
3762
+ const dbAsFirestore = db;
3763
+ api.initialize({
3764
+ db: dbAsFirestore,
3765
+ helpers,
3766
+ httpsCallable,
3767
+ logEvent
3768
+ });
3769
+ return {
3770
+ assignmentRepo: createAssignmentRepo(),
3771
+ cardRepo: createCardRepo()
3772
+ };
3773
+ }
3774
+
3775
+ // src/lib/create-firebase-client-native.ts
3776
+ var createFsClientNative = ({ db, httpsCallable, logEvent }) => {
3777
+ return createFsClientBase({
3778
+ db,
3779
+ httpsCallable,
3780
+ logEvent,
3781
+ helpers: {
3782
+ getDoc,
3783
+ getDocs,
3784
+ addDoc,
3785
+ setDoc,
3786
+ updateDoc,
3787
+ deleteDoc,
3788
+ runTransaction,
3789
+ writeBatch,
3790
+ doc,
3791
+ collection,
3792
+ query,
3793
+ serverTimestamp,
3794
+ orderBy,
3795
+ limit,
3796
+ startAt,
3797
+ startAfter,
3798
+ endAt,
3799
+ endBefore,
3800
+ where,
3801
+ increment
3802
+ }
3803
+ });
3804
+ };
3805
+ export {
3806
+ ActivityPageType,
3807
+ BASE_MULTIPLE_CHOICE_FIELD_VALUES,
3808
+ BASE_REPEAT_FIELD_VALUES,
3809
+ BASE_RESPOND_FIELD_VALUES,
3810
+ CONVERSATION_PAGE_ACTIVITY_TYPES,
3811
+ ConversationPageMode,
3812
+ FeedbackTypesCard,
3813
+ FsCtx,
3814
+ LENIENCY_OPTIONS,
3815
+ LeniencyCard,
3816
+ MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES,
3817
+ REPEAT_PAGE_ACTIVITY_TYPES,
3818
+ RESPOND_AUDIO_PAGE_ACTIVITY_TYPES,
3819
+ RESPOND_PAGE_ACTIVITY_TYPES,
3820
+ RESPOND_WRITE_PAGE_ACTIVITY_TYPES,
3821
+ SPEAKABLE_ANALYTICS,
3822
+ SPEAKABLE_NOTIFICATIONS,
3823
+ STUDENT_LEVELS_OPTIONS,
3824
+ SpeakableNotificationTypes,
3825
+ SpeakableProvider,
3826
+ VerificationCardStatus,
3827
+ assignmentQueryKeys,
3828
+ cardsQueryKeys,
3829
+ checkIsConversationPage,
3830
+ checkIsMCPage,
3831
+ checkIsMediaPage,
3832
+ checkIsRepeatPage,
3833
+ checkIsRespondAudioPage,
3834
+ checkIsRespondPage,
3835
+ checkIsRespondWrittenPage,
3836
+ checkIsShortAnswerPage,
3837
+ checkTypePageActivity,
3838
+ cleanString,
3839
+ createAssignmentRepo,
3840
+ createCardRepo,
3841
+ createFsClientNative as createFsClient,
3842
+ createSetRepo,
3843
+ creditQueryKeys,
3844
+ debounce,
3845
+ getCardFromCache,
3846
+ getLabelPage,
3847
+ getPageMediaData,
3848
+ getPagePrompt,
3849
+ getPhraseLength,
3850
+ getRespondCardTool,
3851
+ getSetFromCache,
3852
+ getSingleMediaPageData,
3853
+ getTotalCompletedCards,
3854
+ getTranscript,
3855
+ getTranscriptCycle,
3856
+ getWordHash,
3857
+ purify,
3858
+ refsCardsFiresotre,
3859
+ refsSetsFirestore,
3860
+ scoreQueryKeys,
3861
+ setsQueryKeys,
3862
+ updateCardInCache,
3863
+ updateSetInCache,
3864
+ useActivity,
3865
+ useActivityFeedbackAccess,
3866
+ useAssignment,
3867
+ useBaseOpenAI,
3868
+ useCards,
3869
+ useClearScore,
3870
+ useClearScoreV2,
3871
+ useCreateCard,
3872
+ useCreateCards,
3873
+ useCreateNotification,
3874
+ useGetCard,
3875
+ useOrganizationAccess,
3876
+ useScore,
3877
+ useSet,
3878
+ useSpeakableApi,
3879
+ useSpeakableTranscript,
3880
+ useSpeakableTranscriptCycle,
3881
+ useSubmitAssignmentScore,
3882
+ useSubmitPracticeScore,
3883
+ useUpdateCardScore,
3884
+ useUpdateScore,
3885
+ useUpdateStudentVocab,
3886
+ useUserCredits
3887
+ };
3888
+ //# sourceMappingURL=index.native.mjs.map