@speakableio/core 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,46 +1,935 @@
1
1
  // src/providers/SpeakableProvider.tsx
2
2
  import { createContext, useContext, useEffect, useState } from "react";
3
3
 
4
+ // src/utils/error-handler.ts
5
+ var ServiceError = class extends Error {
6
+ constructor(message, originalError, code) {
7
+ super(message);
8
+ this.originalError = originalError;
9
+ this.code = code;
10
+ this.name = "ServiceError";
11
+ }
12
+ };
13
+ function withErrorHandler(fn, serviceName) {
14
+ return async (...args) => {
15
+ try {
16
+ return await fn(...args);
17
+ } catch (error) {
18
+ if (error instanceof Error && "code" in error) {
19
+ const firebaseError = error;
20
+ throw new ServiceError(
21
+ `Error in ${serviceName}: ${firebaseError.message}`,
22
+ error,
23
+ firebaseError.code
24
+ );
25
+ }
26
+ if (error instanceof Error) {
27
+ throw new ServiceError(`Error in ${serviceName}: ${error.message}`, error);
28
+ }
29
+ throw new ServiceError(`Unknown error in ${serviceName}`, error);
30
+ }
31
+ };
32
+ }
33
+
34
+ // src/lib/firebase/api.ts
35
+ var FirebaseAPI = class _FirebaseAPI {
36
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
37
+ constructor() {
38
+ this.config = null;
39
+ }
40
+ static getInstance() {
41
+ if (!_FirebaseAPI.instance) {
42
+ _FirebaseAPI.instance = new _FirebaseAPI();
43
+ }
44
+ return _FirebaseAPI.instance;
45
+ }
46
+ initialize(config) {
47
+ this.config = config;
48
+ }
49
+ get db() {
50
+ if (!this.config) throw new Error("Firebase API not initialized");
51
+ return this.config.db;
52
+ }
53
+ get helpers() {
54
+ if (!this.config) throw new Error("Firebase API not initialized");
55
+ return this.config.helpers;
56
+ }
57
+ get functions() {
58
+ if (!this.config) throw new Error("Firebase API not initialized");
59
+ return this.config.functions;
60
+ }
61
+ httpsCallable(functionName) {
62
+ if (!this.config) throw new Error("Firebase API httpsCallable not initialized");
63
+ return this.config.httpsCallable(this.functions, functionName);
64
+ }
65
+ accessQueryConstraints() {
66
+ const { query, orderBy, limit, startAt, startAfter, endAt, endBefore } = this.helpers;
67
+ return {
68
+ query,
69
+ orderBy,
70
+ limit,
71
+ startAt,
72
+ startAfter,
73
+ endAt,
74
+ endBefore
75
+ };
76
+ }
77
+ accessHelpers() {
78
+ const { doc, collection, writeBatch, serverTimestamp, setDoc } = this.helpers;
79
+ return {
80
+ doc: (path) => doc(this.db, path),
81
+ collection: (path) => collection(this.db, path),
82
+ writeBatch: () => writeBatch(this.db),
83
+ serverTimestamp,
84
+ setDoc
85
+ };
86
+ }
87
+ async getDoc(path) {
88
+ const { getDoc, doc } = this.helpers;
89
+ const docRef = doc(this.db, path);
90
+ const docSnap = await getDoc(docRef);
91
+ const data = docSnap.exists() ? {
92
+ id: docSnap.id,
93
+ ...docSnap.data()
94
+ } : null;
95
+ return {
96
+ id: docSnap.id,
97
+ data
98
+ };
99
+ }
100
+ async getDocs(path, ...queryConstraints) {
101
+ const { getDocs, query, collection } = this.helpers;
102
+ const collectionRef = collection(this.db, path);
103
+ const q = queryConstraints.length > 0 ? query(collectionRef, ...queryConstraints) : collectionRef;
104
+ const querySnapshot = await getDocs(q);
105
+ const data = querySnapshot.docs.map((doc) => ({
106
+ id: doc.id,
107
+ data: doc.data()
108
+ }));
109
+ return {
110
+ data,
111
+ querySnapshot
112
+ };
113
+ }
114
+ async addDoc(path, data) {
115
+ const { addDoc, collection } = this.helpers;
116
+ const collectionRef = collection(this.db, path);
117
+ const docRef = await addDoc(collectionRef, data);
118
+ return {
119
+ id: docRef.id,
120
+ ...data
121
+ };
122
+ }
123
+ async setDoc(path, data, options = {}) {
124
+ const { setDoc, doc } = this.helpers;
125
+ const docRef = doc(this.db, path);
126
+ await setDoc(docRef, data, options);
127
+ }
128
+ async updateDoc(path, data) {
129
+ const { updateDoc, doc } = this.helpers;
130
+ const docRef = doc(this.db, path);
131
+ await updateDoc(docRef, data);
132
+ }
133
+ async deleteDoc(path) {
134
+ const { deleteDoc, doc } = this.helpers;
135
+ const docRef = doc(this.db, path);
136
+ await deleteDoc(docRef);
137
+ }
138
+ async runTransaction(updateFunction) {
139
+ const { runTransaction } = this.helpers;
140
+ return runTransaction(this.db, updateFunction);
141
+ }
142
+ async runBatch(operations) {
143
+ const { writeBatch } = this.helpers;
144
+ const batch = writeBatch(this.db);
145
+ await Promise.all(operations.map((op) => op()));
146
+ await batch.commit();
147
+ }
148
+ writeBatch() {
149
+ const { writeBatch } = this.helpers;
150
+ const batch = writeBatch(this.db);
151
+ return batch;
152
+ }
153
+ };
154
+ var api = FirebaseAPI.getInstance();
155
+
156
+ // src/domains/assignment/assignment.constants.ts
157
+ var ASSIGNMENT_ANALYTICS_TYPES = [
158
+ "macro" /* Macro */,
159
+ "gradebook" /* Gradebook */,
160
+ "cards" /* Cards */,
161
+ "student" /* Student */,
162
+ "student_summary" /* StudentSummary */
163
+ ];
164
+ var ASSIGNMENTS_COLLECTION = "assignments";
165
+ var ANALYTICS_SUBCOLLECTION = "analytics";
166
+ var SCORES_SUBCOLLECTION = "scores";
167
+ var refsAssignmentFiresotre = {
168
+ allAssignments: () => ASSIGNMENTS_COLLECTION,
169
+ assignment: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}`,
170
+ assignmentAllAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}`,
171
+ assignmentAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}/${params.type}`,
172
+ assignmentScores: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${SCORES_SUBCOLLECTION}/${params.userId}`
173
+ };
174
+
175
+ // src/domains/assignment/services/get-assignments-score.service.ts
176
+ var _getAssignmentScores = async ({
177
+ assignmentId,
178
+ analyticType = "macro" /* Macro */,
179
+ studentId,
180
+ currentUserId
181
+ }) => {
182
+ if (analyticType === "student" /* Student */) {
183
+ const path = refsAssignmentFiresotre.assignmentScores({
184
+ id: assignmentId,
185
+ userId: currentUserId
186
+ });
187
+ const response = await api.getDoc(path);
188
+ return { scores: response.data, id: assignmentId };
189
+ }
190
+ if (analyticType === "student_summary" /* StudentSummary */ && studentId) {
191
+ const path = refsAssignmentFiresotre.assignmentScores({
192
+ id: assignmentId,
193
+ userId: studentId
194
+ });
195
+ const response = await api.getDoc(path);
196
+ return { scores: response.data, id: assignmentId };
197
+ }
198
+ if (analyticType !== "all" /* All */ && ASSIGNMENT_ANALYTICS_TYPES.includes(analyticType)) {
199
+ const ref = refsAssignmentFiresotre.assignmentAnalytics({
200
+ id: assignmentId,
201
+ type: analyticType
202
+ });
203
+ const docData = await api.getDoc(ref);
204
+ return { scores: docData.data, id: assignmentId };
205
+ } else if (analyticType === "all" /* All */) {
206
+ const ref = refsAssignmentFiresotre.assignmentAllAnalytics({ id: assignmentId });
207
+ const response = await api.getDocs(ref);
208
+ const data = response.data.reduce((acc, curr) => {
209
+ acc[curr.id] = curr;
210
+ return acc;
211
+ }, {});
212
+ return { scores: data, id: assignmentId };
213
+ }
214
+ };
215
+ var getAssignmentScores = withErrorHandler(_getAssignmentScores, "getAssignmentScores");
216
+
217
+ // src/domains/assignment/services/attach-score-assignment.service.ts
218
+ var _attachScoresAssignment = async ({
219
+ assignments,
220
+ analyticType,
221
+ studentId,
222
+ currentUserId
223
+ }) => {
224
+ const scoresPromises = assignments.map((a) => {
225
+ return getAssignmentScores({
226
+ assignmentId: a.id,
227
+ analyticType,
228
+ studentId,
229
+ currentUserId
230
+ });
231
+ });
232
+ const scores = await Promise.all(scoresPromises);
233
+ const scoresObject = scores.reduce((acc, curr) => {
234
+ acc[curr.id] = curr.scores;
235
+ return acc;
236
+ }, {});
237
+ const assignmentsWithScores = assignments.map((a) => {
238
+ return {
239
+ ...a,
240
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/ban-ts-comment
241
+ // @ts-ignore
242
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
243
+ scores: scoresObject[a.id] ?? null
244
+ };
245
+ });
246
+ return assignmentsWithScores;
247
+ };
248
+ var attachScoresAssignment = withErrorHandler(
249
+ _attachScoresAssignment,
250
+ "attachScoresAssignment"
251
+ );
252
+
253
+ // src/domains/assignment/services/get-all-assignment.service.ts
254
+ async function _getAllAssignments() {
255
+ const path = refsAssignmentFiresotre.allAssignments();
256
+ const response = await api.getDocs(path);
257
+ return response.data;
258
+ }
259
+ var getAllAssignments = withErrorHandler(_getAllAssignments, "getAllAssignments");
260
+
261
+ // src/domains/assignment/utils/check-assignment-availability.ts
262
+ import dayjs from "dayjs";
263
+ var checkAssignmentAvailability = (scheduledTime) => {
264
+ if (!scheduledTime) return true;
265
+ const scheduledDate = typeof scheduledTime === "string" ? dayjs(scheduledTime) : dayjs(scheduledTime.toDate());
266
+ if (!scheduledDate.isValid()) return true;
267
+ return dayjs().isAfter(scheduledDate);
268
+ };
269
+
270
+ // src/domains/assignment/services/get-assignment.service.ts
271
+ async function _getAssignment(params) {
272
+ const path = refsAssignmentFiresotre.assignment({ id: params.assignmentId });
273
+ const response = await api.getDoc(path);
274
+ if (!response.data) return null;
275
+ const assignment = response.data;
276
+ const isAvailable = checkAssignmentAvailability(assignment.scheduledTime);
277
+ const assignmentWithId = {
278
+ ...assignment,
279
+ isAvailable,
280
+ id: params.assignmentId,
281
+ scheduledTime: assignment.scheduledTime ?? null
282
+ };
283
+ if (params.analyticType) {
284
+ const assignmentsWithScores = await attachScoresAssignment({
285
+ assignments: [assignmentWithId],
286
+ analyticType: params.analyticType,
287
+ currentUserId: params.currentUserId
288
+ });
289
+ return assignmentsWithScores.length > 0 ? assignmentsWithScores[0] : assignmentWithId;
290
+ }
291
+ return assignmentWithId;
292
+ }
293
+ var getAssignment = withErrorHandler(_getAssignment, "getAssignment");
294
+
4
295
  // src/domains/assignment/assignment.repo.ts
5
- var createAssignmentRepo = (db, h) => {
6
- const { doc, collection, getDoc, getDocs } = h;
296
+ var createAssignmentRepo = () => {
7
297
  return {
8
- async get(id) {
9
- const snap = await getDoc(doc(db, `assignments/${id}`));
10
- if (!snap.exists()) return null;
11
- const data = snap.data();
12
- return { id, ...data };
13
- },
14
- async list() {
15
- const col = collection(db, "assignments");
16
- const snap = await getDocs(col);
17
- return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
18
- }
298
+ getAssignment,
299
+ attachScoresAssignment,
300
+ getAssignmentScores,
301
+ getAllAssignments
19
302
  };
20
303
  };
21
304
 
22
- // src/domains/assignment/assignment.hooks.ts
305
+ // src/domains/assignment/hooks/assignment.hooks.ts
23
306
  import { useQuery } from "@tanstack/react-query";
24
307
  var assignmentQueryKeys = {
25
308
  all: ["assignments"],
26
309
  byId: (id) => [...assignmentQueryKeys.all, id],
27
310
  list: () => [...assignmentQueryKeys.all, "list"]
28
311
  };
29
- function useAssignment({ id, enabled = true }) {
30
- const { fs } = useFs();
312
+ function useAssignment({
313
+ assignmentId,
314
+ enabled = true,
315
+ analyticType,
316
+ userId
317
+ }) {
318
+ const { speakableApi } = useSpeakableApi();
31
319
  return useQuery({
32
- queryKey: assignmentQueryKeys.byId(id),
33
- queryFn: () => fs.assignmentRepo.get(id),
320
+ queryKey: assignmentQueryKeys.byId(assignmentId),
321
+ queryFn: () => speakableApi.assignmentRepo.getAssignment({
322
+ assignmentId,
323
+ analyticType,
324
+ currentUserId: userId
325
+ }),
34
326
  enabled
35
327
  });
36
328
  }
37
329
 
330
+ // src/domains/cards/card.hooks.ts
331
+ import { useMutation, useQueries } from "@tanstack/react-query";
332
+ import { useMemo } from "react";
333
+
334
+ // src/domains/cards/card.constants.ts
335
+ var FeedbackTypesCard = /* @__PURE__ */ ((FeedbackTypesCard2) => {
336
+ FeedbackTypesCard2["SuggestedResponse"] = "suggested_response";
337
+ FeedbackTypesCard2["Wida"] = "wida";
338
+ FeedbackTypesCard2["GrammarInsights"] = "grammar_insights";
339
+ FeedbackTypesCard2["Actfl"] = "actfl";
340
+ FeedbackTypesCard2["ProficiencyLevel"] = "proficiency_level";
341
+ return FeedbackTypesCard2;
342
+ })(FeedbackTypesCard || {});
343
+ var LeniencyCard = /* @__PURE__ */ ((LeniencyCard2) => {
344
+ LeniencyCard2["CONFIDENCE"] = "confidence";
345
+ LeniencyCard2["EASY"] = "easy";
346
+ LeniencyCard2["NORMAL"] = "normal";
347
+ LeniencyCard2["HARD"] = "hard";
348
+ return LeniencyCard2;
349
+ })(LeniencyCard || {});
350
+ var LENIENCY_OPTIONS = [
351
+ {
352
+ label: "Build Confidence - most lenient",
353
+ value: "confidence" /* CONFIDENCE */
354
+ },
355
+ {
356
+ label: "Very Lenient",
357
+ value: "easy" /* EASY */
358
+ },
359
+ {
360
+ label: "Normal",
361
+ value: "normal" /* NORMAL */
362
+ },
363
+ {
364
+ label: "No leniency - most strict",
365
+ value: "hard" /* HARD */
366
+ }
367
+ ];
368
+ var STUDENT_LEVELS_OPTIONS = [
369
+ {
370
+ label: "Beginner",
371
+ description: "Beginner Level: Just starting out. Can say a few basic words and phrases.",
372
+ value: "beginner"
373
+ },
374
+ {
375
+ label: "Elementary",
376
+ description: "Elementary Level: Can understand simple sentences and have very basic conversations.",
377
+ value: "elementary"
378
+ },
379
+ {
380
+ label: "Intermediate",
381
+ description: "Intermediate Level: Can talk about everyday topics and handle common situations.",
382
+ value: "intermediate"
383
+ },
384
+ {
385
+ label: "Advanced",
386
+ description: "Advanced Level: Can speak and understand with ease, and explain ideas clearly.",
387
+ value: "advanced"
388
+ },
389
+ {
390
+ label: "Fluent",
391
+ description: "Fluent Level: Speaks naturally and easily. Can use the language in work or school settings.",
392
+ value: "fluent"
393
+ },
394
+ {
395
+ label: "Native-like",
396
+ description: "Native-like Level: Understands and speaks like a native. Can discuss complex ideas accurately.",
397
+ value: "nativeLike"
398
+ }
399
+ ];
400
+ var BASE_RESPOND_FIELD_VALUES = {
401
+ title: "",
402
+ allowRetries: true,
403
+ respondTime: 180,
404
+ maxCharacters: 1e3
405
+ };
406
+ var BASE_REPEAT_FIELD_VALUES = {
407
+ repeat: 1
408
+ };
409
+ var BASE_MULTIPLE_CHOICE_FIELD_VALUES = {
410
+ MCQType: "single",
411
+ answer: ["A"],
412
+ choices: [
413
+ { option: "A", value: "Option A" },
414
+ { option: "B", value: "Option B" },
415
+ { option: "C", value: "Option C" }
416
+ ]
417
+ };
418
+ var VerificationCardStatus = /* @__PURE__ */ ((VerificationCardStatus2) => {
419
+ VerificationCardStatus2["VERIFIED"] = "VERIFIED";
420
+ VerificationCardStatus2["WARNING"] = "WARNING";
421
+ VerificationCardStatus2["NOT_RECOMMENDED"] = "NOT_RECOMMENDED";
422
+ VerificationCardStatus2["NOT_WORKING"] = "NOT_WORKING";
423
+ VerificationCardStatus2["NOT_CHECKED"] = "NOT_CHECKED";
424
+ return VerificationCardStatus2;
425
+ })(VerificationCardStatus || {});
426
+ var CARDS_COLLECTION = "flashcards";
427
+ var refsCardsFiresotre = {
428
+ allCards: CARDS_COLLECTION,
429
+ card: (id) => `${CARDS_COLLECTION}/${id}`
430
+ };
431
+
432
+ // src/domains/cards/services/get-card.service.ts
433
+ async function _getCard(params) {
434
+ const ref = refsCardsFiresotre.card(params.cardId);
435
+ const response = await api.getDoc(ref);
436
+ if (!response.data) return null;
437
+ return response.data;
438
+ }
439
+ var getCard = withErrorHandler(_getCard, "getCard");
440
+
441
+ // src/domains/cards/services/create-card.service.ts
442
+ import { v4 } from "uuid";
443
+
444
+ // src/domains/cards/card.model.ts
445
+ var CardActivityType = /* @__PURE__ */ ((CardActivityType2) => {
446
+ CardActivityType2["READ_REPEAT"] = "READ_REPEAT";
447
+ CardActivityType2["VIDEO"] = "VIDEO";
448
+ CardActivityType2["TEXT"] = "TEXT";
449
+ CardActivityType2["READ_RESPOND"] = "READ_RESPOND";
450
+ CardActivityType2["FREE_RESPONSE"] = "FREE_RESPONSE";
451
+ CardActivityType2["REPEAT"] = "REPEAT";
452
+ CardActivityType2["RESPOND"] = "RESPOND";
453
+ CardActivityType2["RESPOND_WRITE"] = "RESPOND_WRITE";
454
+ CardActivityType2["TEXT_TO_SPEECH"] = "TEXT_TO_SPEECH";
455
+ CardActivityType2["MULTIPLE_CHOICE"] = "MULTIPLE_CHOICE";
456
+ CardActivityType2["PODCAST"] = "PODCAST";
457
+ CardActivityType2["MEDIA_PAGE"] = "MEDIA_PAGE";
458
+ CardActivityType2["WRITE"] = "WRITE";
459
+ CardActivityType2["SHORT_ANSWER"] = "SHORT_ANSWER";
460
+ CardActivityType2["SHORT_STORY"] = "SHORT_STORY";
461
+ CardActivityType2["SPEAK"] = "SPEAK";
462
+ CardActivityType2["CONVERSATION"] = "CONVERSATION";
463
+ CardActivityType2["CONVERSATION_WRITE"] = "CONVERSATION_WRITE";
464
+ CardActivityType2["DIALOGUE"] = "DIALOGUE";
465
+ CardActivityType2["INSTRUCTION"] = "INSTRUCTION";
466
+ CardActivityType2["LISTEN"] = "LISTEN";
467
+ CardActivityType2["READ"] = "READ";
468
+ CardActivityType2["ANSWER"] = "ANSWER";
469
+ return CardActivityType2;
470
+ })(CardActivityType || {});
471
+ var RESPOND_CARD_ACTIVITY_TYPES = [
472
+ "READ_RESPOND" /* READ_RESPOND */,
473
+ "RESPOND" /* RESPOND */,
474
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
475
+ "FREE_RESPONSE" /* FREE_RESPONSE */
476
+ ];
477
+ var MULTIPLE_CHOICE_CARD_ACTIVITY_TYPES = ["MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */];
478
+ var REPEAT_CARD_ACTIVITY_TYPES = ["READ_REPEAT" /* READ_REPEAT */, "REPEAT" /* REPEAT */];
479
+ var RESPOND_WRITE_CARD_ACTIVITY_TYPES = [
480
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
481
+ "FREE_RESPONSE" /* FREE_RESPONSE */
482
+ ];
483
+ var RESPOND_AUDIO_CARD_ACTIVITY_TYPES = [
484
+ "RESPOND" /* RESPOND */,
485
+ "READ_RESPOND" /* READ_RESPOND */
486
+ ];
487
+ var ALLOWED_CARD_ACTIVITY_TYPES_FOR_SUMMARY = [
488
+ "REPEAT" /* REPEAT */,
489
+ "RESPOND" /* RESPOND */,
490
+ "READ_REPEAT" /* READ_REPEAT */,
491
+ "READ_RESPOND" /* READ_RESPOND */,
492
+ "FREE_RESPONSE" /* FREE_RESPONSE */,
493
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
494
+ "MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */
495
+ ];
496
+
497
+ // src/utils/text-utils.ts
498
+ import sha1 from "js-sha1";
499
+ var purify = (word) => {
500
+ 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();
501
+ };
502
+ var cleanString = (words) => {
503
+ const splitWords = words?.split("+");
504
+ if (splitWords && splitWords.length === 1) {
505
+ const newWord = purify(words);
506
+ return newWord;
507
+ } else if (splitWords && splitWords.length > 1) {
508
+ const split = splitWords.map((w) => purify(w));
509
+ return split;
510
+ } else {
511
+ return "";
512
+ }
513
+ };
514
+ var getWordHash = (word, language) => {
515
+ const cleanedWord = cleanString(word);
516
+ const wordHash = sha1(`${language}-${cleanedWord}`);
517
+ return wordHash;
518
+ };
519
+
520
+ // src/domains/cards/services/get-card-verification-status.service.ts
521
+ var charactarLanguages = ["zh", "ja", "ko"];
522
+ var getVerificationStatus = async (target_text, language) => {
523
+ if (target_text?.length < 3 && !charactarLanguages.includes(language)) {
524
+ return "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
525
+ }
526
+ const hash = getWordHash(target_text, language);
527
+ const response = await api.getDoc(`checked-pronunciations/${hash}`);
528
+ try {
529
+ if (response.data) {
530
+ return processRecord(response.data);
531
+ } else {
532
+ return "NOT_CHECKED" /* NOT_CHECKED */;
533
+ }
534
+ } catch (e) {
535
+ return "NOT_CHECKED" /* NOT_CHECKED */;
536
+ }
537
+ };
538
+ var processRecord = (data) => {
539
+ const { pronunciations = 0, fails = 0 } = data;
540
+ const attempts = pronunciations + fails;
541
+ const successRate = attempts > 0 ? pronunciations / attempts * 100 : 0;
542
+ let newStatus = null;
543
+ if (attempts < 6) {
544
+ return "NOT_CHECKED" /* NOT_CHECKED */;
545
+ }
546
+ if (successRate > 25) {
547
+ newStatus = "VERIFIED" /* VERIFIED */;
548
+ } else if (successRate > 10) {
549
+ newStatus = "WARNING" /* WARNING */;
550
+ } else if (fails > 20 && successRate < 10 && pronunciations > 1) {
551
+ newStatus = "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
552
+ } else if (pronunciations === 0 && fails > 20) {
553
+ newStatus = "NOT_WORKING" /* NOT_WORKING */;
554
+ } else {
555
+ newStatus = "NOT_CHECKED" /* NOT_CHECKED */;
556
+ }
557
+ return newStatus;
558
+ };
559
+
560
+ // src/domains/cards/services/create-card.service.ts
561
+ async function _createCard({ data }) {
562
+ const response = await api.addDoc(refsCardsFiresotre.allCards, data);
563
+ return response;
564
+ }
565
+ var createCard = withErrorHandler(_createCard, "createCard");
566
+ async function _createCards({ cards }) {
567
+ const { writeBatch, doc } = api.accessHelpers();
568
+ const batch = writeBatch();
569
+ const cardsWithId = [];
570
+ for (const card of cards) {
571
+ const cardId = v4();
572
+ const ref = doc(refsCardsFiresotre.card(cardId));
573
+ const newCardObject = {
574
+ ...card,
575
+ id: cardId
576
+ };
577
+ if (card.type === "READ_REPEAT" /* READ_REPEAT */ && card.target_text && card.language) {
578
+ const verificationStatus = await getVerificationStatus(card.target_text, card.language);
579
+ newCardObject.verificationStatus = verificationStatus || null;
580
+ }
581
+ cardsWithId.push(newCardObject);
582
+ batch.set(ref, newCardObject);
583
+ }
584
+ await batch.commit();
585
+ return cardsWithId;
586
+ }
587
+ var createCards = withErrorHandler(_createCards, "createCards");
588
+
589
+ // src/domains/cards/card.hooks.ts
590
+ var cardsQueryKeys = {
591
+ all: ["cards"],
592
+ one: (params) => [...cardsQueryKeys.all, params.cardId]
593
+ };
594
+ function useCards({
595
+ cardIds,
596
+ enabled = true,
597
+ asObject
598
+ }) {
599
+ const queries = useQueries({
600
+ queries: cardIds.map((cardId) => ({
601
+ enabled: enabled && cardIds.length > 0,
602
+ queryKey: cardsQueryKeys.one({
603
+ cardId
604
+ }),
605
+ queryFn: () => getCard({ cardId })
606
+ }))
607
+ });
608
+ const cards = queries.map((query) => query.data).filter(Boolean);
609
+ const cardsObject = useMemo(() => {
610
+ if (!asObject) return null;
611
+ return cards.reduce((acc, card) => {
612
+ acc[card.id] = card;
613
+ return acc;
614
+ }, {});
615
+ }, [asObject, cards]);
616
+ return {
617
+ cards,
618
+ cardsObject,
619
+ cardsQueries: queries
620
+ };
621
+ }
622
+ function useCreateCard() {
623
+ const { queryClient } = useSpeakableApi();
624
+ const mutationCreateCard = useMutation({
625
+ mutationFn: createCard,
626
+ onSuccess: (cardCreated) => {
627
+ queryClient.invalidateQueries({ queryKey: cardsQueryKeys.one({ cardId: cardCreated.id }) });
628
+ }
629
+ });
630
+ return {
631
+ mutationCreateCard
632
+ };
633
+ }
634
+ function useCreateCards() {
635
+ const mutationCreateCards = useMutation({
636
+ mutationFn: createCards
637
+ });
638
+ return {
639
+ mutationCreateCards
640
+ };
641
+ }
642
+ function getCardFromCache({
643
+ cardId,
644
+ queryClient
645
+ }) {
646
+ return queryClient.getQueryData(cardsQueryKeys.one({ cardId }));
647
+ }
648
+ function updateCardInCache({
649
+ cardId,
650
+ card,
651
+ queryClient
652
+ }) {
653
+ queryClient.setQueryData(cardsQueryKeys.one({ cardId }), card);
654
+ }
655
+
656
+ // src/domains/cards/card.repo.ts
657
+ var createCardRepo = () => {
658
+ return {
659
+ createCard,
660
+ createCards,
661
+ getCard
662
+ };
663
+ };
664
+
665
+ // src/domains/sets/set.hooks.ts
666
+ import { useQuery as useQuery2 } from "@tanstack/react-query";
667
+
668
+ // src/domains/sets/set.constants.ts
669
+ var SETS_COLLECTION = "sets";
670
+ var refsSetsFirestore = {
671
+ allSets: SETS_COLLECTION,
672
+ set: (id) => `${SETS_COLLECTION}/${id}`
673
+ };
674
+
675
+ // src/domains/sets/services/get-set.service.ts
676
+ async function _getSet({ setId }) {
677
+ const response = await api.getDoc(refsSetsFirestore.set(setId));
678
+ return response.data;
679
+ }
680
+ var getSet = withErrorHandler(_getSet, "getSet");
681
+
682
+ // src/domains/sets/set.hooks.ts
683
+ var setsQueryKeys = {
684
+ all: ["sets"],
685
+ one: (params) => [...setsQueryKeys.all, params.setId]
686
+ };
687
+ var useSet = ({ setId, enabled }) => {
688
+ return useQuery2({
689
+ queryKey: setsQueryKeys.one({ setId }),
690
+ queryFn: () => getSet({ setId }),
691
+ enabled: setId !== void 0 && setId !== "" && enabled
692
+ });
693
+ };
694
+ function getSetFromCache({
695
+ setId,
696
+ queryClient
697
+ }) {
698
+ if (!setId) return null;
699
+ return queryClient.getQueryData(setsQueryKeys.one({ setId }));
700
+ }
701
+ function updateSetInCache({
702
+ set,
703
+ queryClient
704
+ }) {
705
+ const { id, ...setData } = set;
706
+ queryClient.setQueryData(setsQueryKeys.one({ setId: id }), setData);
707
+ }
708
+
709
+ // src/domains/sets/set.repo.ts
710
+ var createSetRepo = () => {
711
+ return {
712
+ getSet
713
+ };
714
+ };
715
+
716
+ // src/domains/notification/notification.constants.ts
717
+ var SPEAKABLE_NOTIFICATIONS = {
718
+ NEW_ASSIGNMENT: "new_assignment",
719
+ ASSESSMENT_SUBMITTED: "assessment_submitted",
720
+ ASSESSMENT_SCORED: "assessment_scored",
721
+ NEW_COMMENT: "NEW_COMMENT"
722
+ };
723
+ var SpeakableNotificationTypes = {
724
+ NEW_ASSIGNMENT: "NEW_ASSIGNMENT",
725
+ FEEDBACK_FROM_TEACHER: "FEEDBACK_FROM_TEACHER",
726
+ MESSAGE_FROM_STUDENT: "MESSAGE_FROM_STUDENT",
727
+ PHRASE_MARKED_CORRECT: "PHRASE_MARKED_CORRECT",
728
+ STUDENT_PROGRESS: "STUDENT_PROGRESS",
729
+ PLAYLIST_FOLLOWERS: "PLAYLIST_FOLLOWERS",
730
+ PLAYLIST_PLAYS: "PLAYLIST_PLAYS",
731
+ // New notifications
732
+ ASSESSMENT_SUBMITTED: "ASSESSMENT_SUBMITTED",
733
+ // Notification FOR TEACHER when student submits assessment
734
+ ASSESSMENT_SCORED: "ASSESSMENT_SCORED",
735
+ // Notification FOR STUDENT when teacher scores assessment
736
+ // Comment
737
+ NEW_COMMENT: "NEW_COMMENT"
738
+ };
739
+
740
+ // src/domains/notification/services/create-notification.service.ts
741
+ import dayjs2 from "dayjs";
742
+
743
+ // src/constants/web.constants.ts
744
+ var WEB_BASE_URL = "https://app.speakable.io";
745
+
746
+ // src/lib/firebase/firebase-functions.ts
747
+ var SpeakableFirebaseFunctions = {
748
+ updateAssignmentGradebookStatus: api.httpsCallable("updateAssignmentGradebookStatus"),
749
+ onSetOpened: api.httpsCallable("onSetOpened"),
750
+ updateAlgoliaIndex: api.httpsCallable("updateAlgoliaIndex"),
751
+ submitAssignmentToGoogleClassroomV2: api.httpsCallable("submitAssignmentToGoogleClassroomV2"),
752
+ submitLTIAssignmentScore: api.httpsCallable("submitLTIAssignmentScore"),
753
+ submitAssignmentV2: api.httpsCallable("submitLTIAssignmentScoreV2"),
754
+ submitAssessment: api.httpsCallable("submitAssessment"),
755
+ sendAssessmentScoredEmail: api.httpsCallable("sendAssessmentScoredEmail"),
756
+ createNotification: api.httpsCallable("createNotificationV2")
757
+ };
758
+
759
+ // src/domains/notification/services/send-notification.service.ts
760
+ var _sendNotification = async (sendTo, notification) => {
761
+ const results = await SpeakableFirebaseFunctions.createNotification({
762
+ sendTo,
763
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
764
+ notification
765
+ });
766
+ return results;
767
+ };
768
+ var sendNotification = withErrorHandler(_sendNotification, "sendNotification");
769
+
770
+ // src/domains/notification/services/create-notification.service.ts
771
+ var createNotification = async ({
772
+ data,
773
+ type,
774
+ userId,
775
+ profile
776
+ }) => {
777
+ let result;
778
+ switch (type) {
779
+ case SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT:
780
+ result = await handleAssignNotifPromise({ data, profile });
781
+ break;
782
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED:
783
+ result = await createAssessmentSubmissionNotification({
784
+ data,
785
+ profile,
786
+ userId
787
+ });
788
+ break;
789
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED:
790
+ result = await createAssessmentScoredNotification({
791
+ data,
792
+ profile
793
+ });
794
+ break;
795
+ default:
796
+ result = null;
797
+ break;
798
+ }
799
+ return result;
800
+ };
801
+ var handleAssignNotifPromise = async ({
802
+ data: assignments,
803
+ profile
804
+ }) => {
805
+ if (!assignments.length) return;
806
+ try {
807
+ const notifsPromises = assignments.map(async (assignment) => {
808
+ const {
809
+ section,
810
+ section: { members },
811
+ ...rest
812
+ } = assignment;
813
+ if (!section || !members) throw new Error("Invalid assignment data");
814
+ const data = { section, sendTo: members, assignment: { ...rest } };
815
+ return createNewAssignmentNotification({ data, profile });
816
+ });
817
+ await Promise.all(notifsPromises);
818
+ return {
819
+ success: true,
820
+ message: "Assignment notifications sent successfully"
821
+ };
822
+ } catch (error) {
823
+ console.error("Error in handleAssignNotifPromise:", error);
824
+ throw error;
825
+ }
826
+ };
827
+ var createNewAssignmentNotification = async ({
828
+ data,
829
+ profile
830
+ }) => {
831
+ const { assignment, sendTo } = data;
832
+ const teacherName = profile.displayName || "Your teacher";
833
+ const dueDate = assignment.dueDateTimestamp ? dayjs2(assignment.dueDateTimestamp.toDate()).format("MMM Do") : null;
834
+ const results = await sendNotification(sendTo, {
835
+ courseId: assignment.courseId,
836
+ type: SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT,
837
+ senderName: teacherName,
838
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
839
+ messagePreview: `A new assignment "${assignment.name}" is now available. ${dueDate ? `Due ${dueDate}` : ""}`,
840
+ title: "New Assignment Available!",
841
+ imageUrl: profile.image?.url
842
+ });
843
+ return results;
844
+ };
845
+ var createAssessmentSubmissionNotification = async ({
846
+ data: assignment,
847
+ profile,
848
+ userId
849
+ }) => {
850
+ const studentName = profile.displayName || "Your student";
851
+ const results = await sendNotification(assignment.owners, {
852
+ courseId: assignment.courseId,
853
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED,
854
+ link: `${WEB_BASE_URL}/a/${assignment.id}?studentId=${userId}`,
855
+ title: `Assessment Submitted!`,
856
+ senderName: studentName,
857
+ messagePreview: `${studentName} has submitted the assessment "${assignment.name}"`,
858
+ imageUrl: profile.image?.url
859
+ });
860
+ return results;
861
+ };
862
+ var createAssessmentScoredNotification = async ({
863
+ data,
864
+ profile
865
+ }) => {
866
+ const { assignment, sendTo } = data;
867
+ const teacherName = profile.displayName || "Your teacher";
868
+ const title = `${assignment.isAssessment ? "Assessment" : "Assignment"} Reviewed!`;
869
+ const messagePreview = `Your ${assignment.isAssessment ? "assessment" : "assignment"} has been reviewed by your teacher. Click to view the feedback.`;
870
+ const results = await sendNotification(sendTo, {
871
+ courseId: assignment.courseId,
872
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED,
873
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
874
+ title,
875
+ messagePreview,
876
+ imageUrl: profile.image?.url,
877
+ senderName: teacherName
878
+ });
879
+ await SpeakableFirebaseFunctions.sendAssessmentScoredEmail({
880
+ assessmentTitle: assignment.name,
881
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
882
+ senderImage: profile.image?.url || "",
883
+ studentId: sendTo[0],
884
+ teacherName: profile.displayName
885
+ });
886
+ return results;
887
+ };
888
+
889
+ // src/domains/notification/hooks/notification.hooks.ts
890
+ var notificationQueryKeys = {
891
+ all: ["notifications"],
892
+ byId: (id) => [...notificationQueryKeys.all, id]
893
+ };
894
+ var useCreateNotification = () => {
895
+ const { user, queryClient } = useSpeakableApi();
896
+ const handleCreateNotifications = async (type, data) => {
897
+ const result = await createNotification({
898
+ type,
899
+ userId: user?.auth.uid ?? "",
900
+ profile: user?.profile ?? {},
901
+ data
902
+ });
903
+ queryClient.invalidateQueries({
904
+ queryKey: notificationQueryKeys.byId(user?.auth.uid ?? "")
905
+ });
906
+ return result;
907
+ };
908
+ return {
909
+ createNotification: handleCreateNotifications
910
+ };
911
+ };
912
+
38
913
  // src/lib/create-firebase-client.ts
39
- async function createFsClient(db, platform) {
914
+ import { httpsCallable as webHttpsCallable } from "firebase/functions";
915
+ import { httpsCallable as nativeHttpsCallable } from "@react-native-firebase/functions";
916
+ async function createFsClient({
917
+ db,
918
+ platform,
919
+ functions
920
+ }) {
40
921
  const dbAsFirestore = db;
41
922
  const helpers = platform === "web" ? await import("firebase/firestore") : await import("@react-native-firebase/firestore");
923
+ const httpsCallable = platform === "web" ? webHttpsCallable : nativeHttpsCallable;
924
+ api.initialize({
925
+ db: dbAsFirestore,
926
+ helpers,
927
+ functions,
928
+ httpsCallable
929
+ });
42
930
  return {
43
- assignmentRepo: createAssignmentRepo(dbAsFirestore, helpers)
931
+ assignmentRepo: createAssignmentRepo(),
932
+ cardRepo: createCardRepo()
44
933
  };
45
934
  }
46
935
 
@@ -51,37 +940,81 @@ function SpeakableProvider({
51
940
  db,
52
941
  platform,
53
942
  children,
54
- queryClient
943
+ queryClient,
944
+ user,
945
+ permissions,
946
+ firebaseFunctions,
947
+ localStorage
55
948
  }) {
56
- const [fs, setFs] = useState(null);
949
+ const [speakableApi, setSpeakableApi] = useState(null);
57
950
  useEffect(() => {
58
- createFsClient(db, platform).then((repos) => {
59
- setFs(repos);
951
+ createFsClient({
952
+ db,
953
+ platform,
954
+ functions: firebaseFunctions
955
+ }).then((repos) => {
956
+ setSpeakableApi(repos);
60
957
  });
61
- }, [db, platform]);
62
- if (!fs) return null;
958
+ }, [db, firebaseFunctions, platform]);
959
+ if (!speakableApi) return null;
63
960
  return /* @__PURE__ */ jsx(
64
961
  FsCtx.Provider,
65
962
  {
66
963
  value: {
67
- fs,
68
- queryClient
964
+ speakableApi,
965
+ queryClient,
966
+ user,
967
+ permissions,
968
+ firebaseFunctions,
969
+ localStorage
69
970
  },
70
971
  children
71
972
  }
72
973
  );
73
974
  }
74
- function useFs() {
975
+ function useSpeakableApi() {
75
976
  const ctx = useContext(FsCtx);
76
- if (!ctx) throw new Error("useFs must be used within a SpeakableProvider");
977
+ if (!ctx) throw new Error("useSpeakableApi must be used within a SpeakableProvider");
77
978
  return ctx;
78
979
  }
79
980
  export {
981
+ ALLOWED_CARD_ACTIVITY_TYPES_FOR_SUMMARY,
982
+ BASE_MULTIPLE_CHOICE_FIELD_VALUES,
983
+ BASE_REPEAT_FIELD_VALUES,
984
+ BASE_RESPOND_FIELD_VALUES,
985
+ CardActivityType,
986
+ FeedbackTypesCard,
80
987
  FsCtx,
988
+ LENIENCY_OPTIONS,
989
+ LeniencyCard,
990
+ MULTIPLE_CHOICE_CARD_ACTIVITY_TYPES,
991
+ REPEAT_CARD_ACTIVITY_TYPES,
992
+ RESPOND_AUDIO_CARD_ACTIVITY_TYPES,
993
+ RESPOND_CARD_ACTIVITY_TYPES,
994
+ RESPOND_WRITE_CARD_ACTIVITY_TYPES,
995
+ SPEAKABLE_NOTIFICATIONS,
996
+ STUDENT_LEVELS_OPTIONS,
997
+ SpeakableNotificationTypes,
81
998
  SpeakableProvider,
999
+ VerificationCardStatus,
82
1000
  assignmentQueryKeys,
1001
+ cardsQueryKeys,
83
1002
  createAssignmentRepo,
1003
+ createCardRepo,
84
1004
  createFsClient,
1005
+ createSetRepo,
1006
+ getCardFromCache,
1007
+ getSetFromCache,
1008
+ refsCardsFiresotre,
1009
+ refsSetsFirestore,
1010
+ setsQueryKeys,
1011
+ updateCardInCache,
1012
+ updateSetInCache,
85
1013
  useAssignment,
86
- useFs
1014
+ useCards,
1015
+ useCreateCard,
1016
+ useCreateCards,
1017
+ useCreateNotification,
1018
+ useSet,
1019
+ useSpeakableApi
87
1020
  };