@learnpack/learnpack 5.0.308 → 5.0.310

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.
Files changed (88) hide show
  1. package/README.md +409 -409
  2. package/lib/commands/audit.js +15 -15
  3. package/lib/commands/breakToken.js +19 -19
  4. package/lib/commands/clean.js +3 -3
  5. package/lib/commands/logout.js +3 -3
  6. package/lib/commands/serve.js +16 -16
  7. package/lib/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  8. package/lib/creatorDist/index.html +1 -1
  9. package/lib/managers/config/index.js +77 -77
  10. package/lib/utils/creatorUtilities.js +14 -14
  11. package/lib/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  12. package/lib/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  13. package/lib/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  14. package/lib/utils/templates/scorm/config/index.html +209 -209
  15. package/lib/utils/templates/scorm/ims_xml.xsd +1 -1
  16. package/lib/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  17. package/lib/utils/templates/scorm/imsmanifest.xml +38 -38
  18. package/lib/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
  19. package/package.json +1 -1
  20. package/src/commands/audit.ts +487 -487
  21. package/src/commands/breakToken.ts +67 -67
  22. package/src/commands/clean.ts +30 -30
  23. package/src/commands/logout.ts +38 -38
  24. package/src/commands/publish.ts +517 -517
  25. package/src/commands/serve.ts +3179 -3179
  26. package/src/commands/start.ts +333 -333
  27. package/src/commands/translate.ts +123 -123
  28. package/src/creator/README.md +54 -54
  29. package/src/creator/package-lock.json +6621 -6621
  30. package/src/creator/package.json +55 -55
  31. package/src/creator/src/App.tsx +611 -608
  32. package/src/creator/src/components/FileUploader.tsx +340 -302
  33. package/src/creator/src/components/Icon.tsx +18 -18
  34. package/src/creator/src/components/LessonItem.tsx +152 -152
  35. package/src/creator/src/components/Login.tsx +259 -259
  36. package/src/creator/src/components/ParamsChecker.tsx +25 -25
  37. package/src/creator/src/components/Uploader.tsx +3 -6
  38. package/src/creator/src/components/syllabus/ContentIndex.tsx +323 -323
  39. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +341 -337
  40. package/src/creator/src/i18n.ts +28 -28
  41. package/src/creator/src/locales/en.json +139 -138
  42. package/src/creator/src/locales/es.json +139 -138
  43. package/src/creator/src/utils/configTypes.ts +122 -122
  44. package/src/creator/src/utils/constants.ts +13 -13
  45. package/src/creator/src/utils/creatorUtils.ts +46 -46
  46. package/src/creator/src/utils/eventBus.ts +2 -2
  47. package/src/creator/src/utils/lib.ts +172 -172
  48. package/src/creator/src/utils/rigo.ts +1 -1
  49. package/src/creator/src/utils/socket.ts +61 -61
  50. package/src/creator/src/utils/store.ts +222 -222
  51. package/src/creator/src/vite-env.d.ts +1 -1
  52. package/src/creator/vite.config.ts +13 -13
  53. package/src/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  54. package/src/creatorDist/index.html +1 -1
  55. package/src/managers/config/defaults.ts +49 -49
  56. package/src/managers/config/exercise.ts +364 -364
  57. package/src/managers/config/index.ts +775 -775
  58. package/src/managers/file.ts +236 -236
  59. package/src/managers/server/routes.ts +554 -554
  60. package/src/managers/telemetry.ts +188 -188
  61. package/src/models/action.ts +13 -13
  62. package/src/models/config-manager.ts +28 -28
  63. package/src/models/config.ts +106 -106
  64. package/src/models/exercise-obj.ts +30 -30
  65. package/src/models/session.ts +39 -39
  66. package/src/models/socket.ts +61 -61
  67. package/src/models/status.ts +16 -16
  68. package/src/ui/_app/app.css +1 -1
  69. package/src/ui/_app/app.js +477 -407
  70. package/src/ui/app.tar.gz +0 -0
  71. package/src/utils/BaseCommand.ts +56 -56
  72. package/src/utils/api.ts +665 -665
  73. package/src/utils/audit.ts +392 -392
  74. package/src/utils/checkNotInstalled.ts +267 -267
  75. package/src/utils/convertCreds.js +34 -34
  76. package/src/utils/creatorUtilities.ts +504 -504
  77. package/src/utils/export/README.md +178 -178
  78. package/src/utils/incrementVersion.js +74 -74
  79. package/src/utils/misc.ts +58 -58
  80. package/src/utils/sidebarGenerator.ts +195 -195
  81. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  82. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  83. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  84. package/src/utils/templates/scorm/config/index.html +209 -209
  85. package/src/utils/templates/scorm/ims_xml.xsd +1 -1
  86. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  87. package/src/utils/templates/scorm/imsmanifest.xml +38 -38
  88. package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
@@ -1,608 +1,611 @@
1
- import { useEffect, useState } from "react"
2
- import StepWizard from "./components/StepWizard"
3
- import SelectableCard from "./components/SelectableCard"
4
- import Loader from "./components/Loader"
5
- import { useNavigate } from "react-router"
6
- import { useShallow } from "zustand/react/shallow"
7
- import useStore, { TDifficulty } from "./utils/store"
8
-
9
- import { publicInteractiveCreation, isHuman } from "./utils/rigo"
10
- import {
11
- checkParams,
12
- isValidRigoToken,
13
- loginWithToken,
14
- parseLesson,
15
- fixTitleLength,
16
- getTechnologies,
17
- detectLanguage,
18
- isValidPublicToken,
19
- } from "./utils/lib"
20
-
21
- import { Uploader } from "./components/Uploader"
22
- import toast from "react-hot-toast"
23
- import { ParamsChecker } from "./components/ParamsChecker"
24
- import { DEV_MODE, RIGO_FLOAT_GIF } from "./utils/constants"
25
- import TurnstileChallenge from "./components/TurnstileChallenge"
26
- // import TurnstileChallenge from "./components/TurnstileChallenge"
27
- import ResumeCourseModal from "./components/ResumeCourseModal"
28
- import { possiblePurposes, PurposeSelector } from "./components/PurposeSelector"
29
- import { useTranslation } from "react-i18next"
30
- import NotificationListener from "./components/NotificationListener"
31
- import { slugify } from "./utils/creatorUtils"
32
- import TurnstileModal from "./components/TurnstileModal"
33
- import { TMessage } from "./components/Message"
34
- import LanguageDetectionModal from "./components/LanguageDetectionModal"
35
-
36
- function App() {
37
- const navigate = useNavigate()
38
- const { t, i18n } = useTranslation()
39
-
40
- const {
41
- formState,
42
- setFormState,
43
- setAuth,
44
- push,
45
- cleanHistory,
46
- setPlanToRedirect,
47
- history,
48
- uploadedFiles,
49
- auth,
50
- resetFormState,
51
- cleanAll,
52
- setMessages,
53
- technologies,
54
- setTechnologies,
55
- } = useStore(
56
- useShallow((state) => ({
57
- formState: state.formState,
58
- setFormState: state.setFormState,
59
- setAuth: state.setAuth,
60
- technologies: state.technologies,
61
- setTechnologies: state.setTechnologies,
62
- push: state.push,
63
- history: state.history,
64
- cleanHistory: state.cleanHistory,
65
- setPlanToRedirect: state.setPlanToRedirect,
66
- uploadedFiles: state.uploadedFiles,
67
- auth: state.auth,
68
- resetFormState: state.resetFormState,
69
- cleanAll: state.cleanAll,
70
- setMessages: state.setMessages,
71
- }))
72
- )
73
-
74
- const [notificationId, setNotificationId] = useState<string>("")
75
- const [showTurnstileModal, setShowTurnstileModal] = useState(false)
76
- const [showLanguageDetectionModal, setShowLanguageDetectionModal] = useState(false)
77
- const [detectedLanguage, setDetectedLanguage] = useState<string>("")
78
-
79
- useEffect(() => {
80
- if (formState.isCompleted) {
81
- handleCreateTutorial()
82
- }
83
- }, [formState.isCompleted])
84
-
85
- useEffect(() => {
86
- verifyToken()
87
- checkQueryParams()
88
- checkTechs()
89
- }, [])
90
-
91
- const verifyToken = async () => {
92
- const { token } = checkParams(["token"])
93
- if (token) {
94
- const user = await loginWithToken(token)
95
- setAuth({
96
- bcToken: token,
97
- userId: user.id,
98
- rigoToken: user.rigobot.key,
99
- user: user,
100
- publicToken: "",
101
- })
102
- }
103
- }
104
-
105
- const checkQueryParams = () => {
106
- const {
107
- description,
108
- duration,
109
- plan,
110
- purpose,
111
- language,
112
- new: newParam,
113
- difficulty,
114
- } = checkParams([
115
- "description",
116
- "duration",
117
- "plan",
118
- "purpose",
119
- "language",
120
- "new",
121
- "difficulty",
122
- ])
123
-
124
- if (newParam && newParam.toLowerCase().trim() === "true") {
125
- cleanAll()
126
- }
127
-
128
- if (description) {
129
- setFormState({
130
- description: description,
131
- })
132
- }
133
- if (duration && !isNaN(parseInt(duration))) {
134
- if (["15", "30", "60"].includes(duration)) {
135
- const durationInt = parseInt(duration)
136
- setFormState({
137
- duration: durationInt,
138
- })
139
- } else {
140
- console.error("Invalid duration received in params", duration)
141
- }
142
- }
143
-
144
- if (plan) {
145
- setPlanToRedirect(plan)
146
- } else {
147
- console.debug("No plan received in params")
148
- }
149
-
150
- // Language detection logic
151
- let finalLanguage = language
152
-
153
- // Change if query param is provided
154
- if (finalLanguage && finalLanguage.length === 2) {
155
- console.log("LANGUAGE", finalLanguage)
156
- setFormState({
157
- language: finalLanguage,
158
- })
159
- if (i18n.language !== finalLanguage) {
160
- i18n.changeLanguage(finalLanguage)
161
- }
162
- }
163
-
164
- // If no language is provided, detect the language of the description
165
- if (!finalLanguage && description) {
166
- const detectedLang = detectLanguage(description)
167
- if (detectedLang && detectedLang !== i18n.language) {
168
- finalLanguage = detectedLang
169
- setDetectedLanguage(detectedLang)
170
- setShowLanguageDetectionModal(true)
171
- }
172
- }
173
-
174
-
175
- if (purpose && purpose.length > 0 && possiblePurposes.includes(purpose)) {
176
- setFormState({
177
- purpose: purpose,
178
- })
179
- }
180
-
181
- if (
182
- difficulty &&
183
- ["easy", "beginner", "intermediate", "hard"].includes(difficulty)
184
- ) {
185
- setFormState({
186
- difficulty: difficulty as TDifficulty,
187
- })
188
- }
189
-
190
- if (description && purpose) {
191
- setFormState({
192
- currentStep: "duration",
193
- })
194
- }
195
- if (description && !purpose) {
196
- setFormState({
197
- currentStep: "purpose",
198
- })
199
- }
200
- }
201
-
202
- const handleCreateTutorial = async () => {
203
- try {
204
- let isAuthenticated = false
205
- let tokenToUse = ""
206
- let tokenType = ""
207
- if (auth.rigoToken) {
208
- console.log("auth.rigoToken", auth.rigoToken)
209
- const isRigoTokenValid = await isValidRigoToken(auth.rigoToken)
210
- if (isRigoTokenValid) {
211
- tokenToUse = auth.rigoToken
212
- isAuthenticated = true
213
- tokenType = "rigo"
214
- } else {
215
- setAuth({
216
- ...auth,
217
- rigoToken: "",
218
- bcToken: "",
219
- userId: "",
220
- user: null,
221
- })
222
- }
223
- }
224
-
225
- if (auth.publicToken && !isAuthenticated) {
226
- const isPublicTokenValid = await isValidPublicToken(auth.publicToken)
227
- if (isPublicTokenValid) {
228
- tokenToUse = auth.publicToken
229
- isAuthenticated = true
230
- tokenType = "public"
231
- }
232
- }
233
- if (!isAuthenticated) {
234
- setShowTurnstileModal(true)
235
- return
236
- }
237
-
238
- let techs = technologies.filter((t) => t.lang === formState.language)
239
-
240
- if (techs.length === 0) {
241
- techs = technologies.filter((t) => t.lang === "en")
242
- }
243
-
244
- const res = await publicInteractiveCreation(
245
- {
246
- courseInfo: `${JSON.stringify(
247
- formState
248
- )}. The following technologies are available, choose up to 3 from the following list: <techs>${techs
249
- .map((t) => t.slug)
250
- .join(", ")}</techs>`,
251
- prevInteractions: "USER: " + formState.description,
252
- },
253
- tokenToUse,
254
- formState.purpose || "learnpack-lesson-writer",
255
- tokenType === "rigo" ? false : true
256
- )
257
- setNotificationId(res.notificationId)
258
- } catch (error) {
259
- console.error(error, "ERROR CREATING TUTORIAL")
260
- toast.error("Something went wrong. Please try again.")
261
- setFormState({
262
- isCompleted: false,
263
- currentStep: "hasContentIndex",
264
- })
265
- }
266
- }
267
-
268
- const checkTechs = async () => {
269
- if (technologies.length === 0) {
270
- const technologies = await getTechnologies()
271
- console.log("TECHNOLOGIES", technologies)
272
- setTechnologies(technologies)
273
- }
274
- }
275
-
276
- const handleLanguageSwitch = () => {
277
- setFormState({
278
- language: detectedLanguage,
279
- })
280
- i18n.changeLanguage(detectedLanguage)
281
- setShowLanguageDetectionModal(false)
282
- }
283
-
284
- const handleLanguageStay = () => {
285
- setShowLanguageDetectionModal(false)
286
- }
287
-
288
- const buildSteps = () => {
289
- const steps = [
290
- {
291
- title: t("stepWizard.description"),
292
- slug: "description",
293
- isCompleted: formState.description.length > 0,
294
- required: true,
295
- validator: (value: string) => {
296
- const lang = detectLanguage(value)
297
- console.log("Description validator language detection:", { lang, currentLang: i18n.language, value })
298
-
299
- if (lang && lang !== "en" && lang !== i18n.language) {
300
- setDetectedLanguage(lang)
301
- setShowLanguageDetectionModal(true)
302
- }
303
-
304
- return value.length > 0
305
- },
306
- content: (
307
- <textarea
308
- required
309
- placeholder={t("stepWizard.descriptionPlaceholder")}
310
- className="w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white"
311
- value={formState.description}
312
- onChange={(e) => {
313
- setFormState({
314
- description: e.target.value,
315
- })
316
- }}
317
- />
318
- ),
319
- },
320
- {
321
- title: t("stepWizard.purpose"),
322
- slug: "purpose",
323
- isCompleted: formState?.purpose?.length > 0,
324
- required: true,
325
- content: (
326
- <PurposeSelector
327
- onFinish={(purpose) => {
328
- setFormState({
329
- purpose: purpose,
330
- currentStep: "duration",
331
- })
332
- }}
333
- />
334
- ),
335
- },
336
- {
337
- title: t("stepWizard.duration"),
338
- slug: "duration",
339
- isCompleted: formState.duration > 0,
340
- required: true,
341
- content: (
342
- <div className="flex flex-col md:flex-row gap-2">
343
- <SelectableCard
344
- title={t("stepWizard.durationCard.15")}
345
- // subtitle="This is a tutorial that will take 15 minutes to complete"
346
- onClick={() => {
347
- setFormState({
348
- duration: 15,
349
- currentStep: "verifyHuman",
350
- })
351
- }}
352
- selected={formState.duration === 15}
353
- />
354
- <SelectableCard
355
- title={t("stepWizard.durationCard.30")}
356
- // subtitle="This is a tutorial that will take 30 minutes to complete"
357
- onClick={() => {
358
- setFormState({
359
- duration: 30,
360
- currentStep: "verifyHuman",
361
- })
362
- }}
363
- selected={formState.duration === 30}
364
- />
365
- <SelectableCard
366
- title={t("stepWizard.durationCard.60")}
367
- // subtitle="This is a tutorial that will take 1 hour to complete"
368
- onClick={() => {
369
- setFormState({
370
- duration: 60,
371
- currentStep: "verifyHuman",
372
- })
373
- }}
374
- selected={formState.duration === 60}
375
- />
376
- </div>
377
- ),
378
- },
379
- {
380
- title: t("stepWizard.verifyHuman"),
381
- slug: "verifyHuman",
382
- isCompleted: false,
383
- required: true,
384
- content: (
385
- <TurnstileChallenge
386
- siteKey={
387
- DEV_MODE ? "0x4AAAAAABeKMBYYinMU4Ib0" : "0x4AAAAAABeZ9tjEevGBsJFU"
388
- }
389
- onSuccess={async (token) => {
390
- const { human, message, token: jwtToken } = await isHuman(token)
391
- if (human) {
392
- toast.success(t("stepWizard.humanSuccess"))
393
-
394
- console.log("JWT TOKEN received", jwtToken)
395
- setAuth({
396
- ...auth,
397
- publicToken: jwtToken,
398
- })
399
- setFormState({
400
- currentStep: "hasContentIndex",
401
- })
402
- } else {
403
- toast.error(message)
404
- setFormState({
405
- currentStep: "duration",
406
- })
407
- }
408
- }}
409
- onError={() => {
410
- toast.error(t("turnstileModal.error"), {
411
- duration: 10000,
412
- })
413
- setFormState({
414
- currentStep: "duration",
415
- })
416
- }}
417
- />
418
- ),
419
- },
420
- {
421
- title: t("stepWizard.hasContentIndex"),
422
- slug: "hasContentIndex",
423
- isCompleted: false,
424
- content: (
425
- <>
426
- <div className="flex flex-col md:flex-row gap-2 justify-center">
427
- <SelectableCard
428
- title={t("stepWizard.hasContentIndexCard.no")}
429
- onClick={() => {
430
- setFormState({
431
- hasContentIndex: false,
432
- variables: [
433
- ...formState.variables.filter(
434
- (v) => v !== "contentIndex"
435
- ),
436
- ],
437
- isCompleted: true,
438
- })
439
- }}
440
- // selected={formState.hasContentIndex === false}
441
- />
442
- <SelectableCard
443
- title={t("stepWizard.hasContentIndexCard.yes")}
444
- onClick={() => {
445
- setFormState({
446
- hasContentIndex: true,
447
- currentStep: "contentIndex",
448
- variables: [...formState.variables, "contentIndex"],
449
- })
450
- }}
451
- // selected={formState.hasContentIndex === true}
452
- />
453
- </div>
454
- </>
455
- ),
456
- },
457
- {
458
- title: t("stepWizard.contentIndex"),
459
- slug: "contentIndex",
460
- helpText: t("stepWizard.contentIndexHelpText"),
461
- isCompleted:
462
- formState.contentIndex.length > 0 || uploadedFiles.length > 0,
463
- required: true,
464
- content: (
465
- <Uploader
466
- onFinish={(text) => {
467
- setFormState({
468
- contentIndex: text,
469
- isCompleted: true,
470
- })
471
- }}
472
- />
473
- ),
474
- },
475
- ]
476
-
477
- return steps.filter(
478
- (step) =>
479
- formState.variables.includes(step.slug) ||
480
- step.slug === formState.currentStep
481
- )
482
- }
483
-
484
- return (
485
- <>
486
- <ParamsChecker />
487
- {showLanguageDetectionModal && (
488
- <LanguageDetectionModal
489
- detectedLanguage={detectedLanguage}
490
- onSwitch={handleLanguageSwitch}
491
- onStay={handleLanguageStay}
492
- />
493
- )}
494
- {showTurnstileModal && (
495
- <TurnstileModal
496
- onClose={() => {
497
- setShowTurnstileModal(false)
498
- handleCreateTutorial()
499
- }}
500
- onError={() => {
501
- toast.error(t("turnstileModal.error"), {
502
- duration: 10000,
503
- })
504
- setShowTurnstileModal(false)
505
- setFormState({
506
- currentStep: "purpose",
507
- })
508
- }}
509
- />
510
- )}
511
- {formState.isCompleted && history.length === 0 ? (
512
- <>
513
- <Loader
514
- text={t("loader.text")}
515
- icon={<img src={RIGO_FLOAT_GIF} alt="rigo" className="w-20 h-20" />}
516
- />
517
- {notificationId && (
518
- <NotificationListener
519
- onNotification={(res) => {
520
- const lessons = res.parsed.listOfSteps.map((lesson: any) => {
521
- return parseLesson(lesson, [])
522
- })
523
-
524
- push({
525
- lessons,
526
- courseInfo: {
527
- ...formState,
528
- title: res.parsed.title,
529
- slug: slugify(fixTitleLength(res.parsed.title)),
530
- description: res.parsed.description,
531
- language:
532
- res.parsed.languageCode || formState.language || "en",
533
- technologies:
534
- res.parsed.technologies.length > 0
535
- ? res.parsed.technologies
536
- : ["education", "quizzes"],
537
- },
538
- })
539
-
540
- if (res.parsed.languageCode) {
541
- i18n.changeLanguage(res.parsed.languageCode)
542
- }
543
-
544
- let initialMessages: TMessage[] = [
545
- {
546
- type: "user",
547
- content: formState.description,
548
- },
549
- {
550
- type: "assistant",
551
- content: res.parsed.aiMessage,
552
- },
553
- ]
554
-
555
- if (lessons.length > 0) {
556
- initialMessages.push({
557
- type: "assistant",
558
- content: t("contentIndex.okMessage"),
559
- })
560
- initialMessages.push({
561
- type: "assistant",
562
- content: t("contentIndex.instructionsMessage"),
563
- })
564
- }
565
-
566
- setMessages(initialMessages)
567
- navigate("/creator/syllabus")
568
- setFormState({
569
- isCompleted: false,
570
- currentStep: "description",
571
- })
572
- }}
573
- notificationId={notificationId}
574
- />
575
- )}
576
- </>
577
- ) : (
578
- <>
579
- {history.length > 0 && (
580
- <ResumeCourseModal
581
- onContinue={() => {
582
- navigate("/creator/syllabus")
583
- }}
584
- onStartOver={() => {
585
- resetFormState()
586
- cleanAll()
587
- cleanHistory()
588
- }}
589
- />
590
- )}
591
- <StepWizard
592
- hideLastButton={true}
593
- formState={formState}
594
- steps={buildSteps()}
595
- setFormState={setFormState}
596
- onFinish={() => {
597
- setFormState({
598
- isCompleted: true,
599
- })
600
- }}
601
- />
602
- </>
603
- )}
604
- </>
605
- )
606
- }
607
-
608
- export default App
1
+ import { useEffect, useState } from "react"
2
+ import StepWizard from "./components/StepWizard"
3
+ import SelectableCard from "./components/SelectableCard"
4
+ import Loader from "./components/Loader"
5
+ import { useNavigate } from "react-router"
6
+ import { useShallow } from "zustand/react/shallow"
7
+ import useStore, { TDifficulty } from "./utils/store"
8
+
9
+ import { publicInteractiveCreation, isHuman } from "./utils/rigo"
10
+ import {
11
+ checkParams,
12
+ isValidRigoToken,
13
+ loginWithToken,
14
+ parseLesson,
15
+ fixTitleLength,
16
+ getTechnologies,
17
+ detectLanguage,
18
+ isValidPublicToken,
19
+ } from "./utils/lib"
20
+
21
+ import { Uploader } from "./components/Uploader"
22
+ import toast from "react-hot-toast"
23
+ import { ParamsChecker } from "./components/ParamsChecker"
24
+ import { DEV_MODE, RIGO_FLOAT_GIF } from "./utils/constants"
25
+ import TurnstileChallenge from "./components/TurnstileChallenge"
26
+ // import TurnstileChallenge from "./components/TurnstileChallenge"
27
+ import ResumeCourseModal from "./components/ResumeCourseModal"
28
+ import { possiblePurposes, PurposeSelector } from "./components/PurposeSelector"
29
+ import { useTranslation } from "react-i18next"
30
+ import NotificationListener from "./components/NotificationListener"
31
+ import { slugify } from "./utils/creatorUtils"
32
+ import TurnstileModal from "./components/TurnstileModal"
33
+ import { TMessage } from "./components/Message"
34
+ import LanguageDetectionModal from "./components/LanguageDetectionModal"
35
+
36
+ function App() {
37
+ const navigate = useNavigate()
38
+ const { t, i18n } = useTranslation()
39
+
40
+ const {
41
+ formState,
42
+ setFormState,
43
+ setAuth,
44
+ push,
45
+ cleanHistory,
46
+ setPlanToRedirect,
47
+ history,
48
+ uploadedFiles,
49
+ auth,
50
+ resetFormState,
51
+ cleanAll,
52
+ setMessages,
53
+ technologies,
54
+ setTechnologies,
55
+ } = useStore(
56
+ useShallow((state) => ({
57
+ formState: state.formState,
58
+ setFormState: state.setFormState,
59
+ setAuth: state.setAuth,
60
+ technologies: state.technologies,
61
+ setTechnologies: state.setTechnologies,
62
+ push: state.push,
63
+ history: state.history,
64
+ cleanHistory: state.cleanHistory,
65
+ setPlanToRedirect: state.setPlanToRedirect,
66
+ uploadedFiles: state.uploadedFiles,
67
+ auth: state.auth,
68
+ resetFormState: state.resetFormState,
69
+ cleanAll: state.cleanAll,
70
+ setMessages: state.setMessages,
71
+ }))
72
+ )
73
+
74
+ const [notificationId, setNotificationId] = useState<string>("")
75
+ const [showTurnstileModal, setShowTurnstileModal] = useState(false)
76
+ const [showLanguageDetectionModal, setShowLanguageDetectionModal] = useState(false)
77
+ const [detectedLanguage, setDetectedLanguage] = useState<string>("")
78
+
79
+ useEffect(() => {
80
+ if (formState.isCompleted) {
81
+ handleCreateTutorial()
82
+ }
83
+ }, [formState.isCompleted])
84
+
85
+ useEffect(() => {
86
+ verifyToken()
87
+ checkQueryParams()
88
+ checkTechs()
89
+ }, [])
90
+
91
+ const verifyToken = async () => {
92
+ const { token } = checkParams(["token"])
93
+ if (token) {
94
+ const user = await loginWithToken(token)
95
+ setAuth({
96
+ bcToken: token,
97
+ userId: user.id,
98
+ rigoToken: user.rigobot.key,
99
+ user: user,
100
+ publicToken: "",
101
+ })
102
+ }
103
+ }
104
+
105
+ const checkQueryParams = () => {
106
+ const {
107
+ description,
108
+ duration,
109
+ plan,
110
+ purpose,
111
+ language,
112
+ new: newParam,
113
+ difficulty,
114
+ } = checkParams([
115
+ "description",
116
+ "duration",
117
+ "plan",
118
+ "purpose",
119
+ "language",
120
+ "new",
121
+ "difficulty",
122
+ ])
123
+
124
+ if (newParam && newParam.toLowerCase().trim() === "true") {
125
+ cleanAll()
126
+ }
127
+
128
+ if (description) {
129
+ setFormState({
130
+ description: description,
131
+ })
132
+ }
133
+ if (duration && !isNaN(parseInt(duration))) {
134
+ if (["15", "30", "60"].includes(duration)) {
135
+ const durationInt = parseInt(duration)
136
+ setFormState({
137
+ duration: durationInt,
138
+ })
139
+ } else {
140
+ console.error("Invalid duration received in params", duration)
141
+ }
142
+ }
143
+
144
+ if (plan) {
145
+ setPlanToRedirect(plan)
146
+ } else {
147
+ console.debug("No plan received in params")
148
+ }
149
+
150
+ // Language detection logic
151
+ let finalLanguage = language
152
+
153
+ // Change if query param is provided
154
+ if (finalLanguage && finalLanguage.length === 2) {
155
+ console.log("LANGUAGE", finalLanguage)
156
+ setFormState({
157
+ language: finalLanguage,
158
+ })
159
+ if (i18n.language !== finalLanguage) {
160
+ i18n.changeLanguage(finalLanguage)
161
+ }
162
+ }
163
+
164
+ // If no language is provided, detect the language of the description
165
+ if (!finalLanguage && description) {
166
+ const detectedLang = detectLanguage(description)
167
+ if (detectedLang && detectedLang !== i18n.language) {
168
+ finalLanguage = detectedLang
169
+ setDetectedLanguage(detectedLang)
170
+ setShowLanguageDetectionModal(true)
171
+ }
172
+ }
173
+
174
+
175
+ if (purpose && purpose.length > 0 && possiblePurposes.includes(purpose)) {
176
+ setFormState({
177
+ purpose: purpose,
178
+ })
179
+ }
180
+
181
+ if (
182
+ difficulty &&
183
+ ["easy", "beginner", "intermediate", "hard"].includes(difficulty)
184
+ ) {
185
+ setFormState({
186
+ difficulty: difficulty as TDifficulty,
187
+ })
188
+ }
189
+
190
+ if (description && purpose) {
191
+ setFormState({
192
+ currentStep: "duration",
193
+ })
194
+ }
195
+ if (description && !purpose) {
196
+ setFormState({
197
+ currentStep: "purpose",
198
+ })
199
+ }
200
+ }
201
+
202
+ const handleCreateTutorial = async () => {
203
+ try {
204
+ let isAuthenticated = false
205
+ let tokenToUse = ""
206
+ let tokenType = ""
207
+ if (auth.rigoToken) {
208
+ console.log("auth.rigoToken", auth.rigoToken)
209
+ const isRigoTokenValid = await isValidRigoToken(auth.rigoToken)
210
+ if (isRigoTokenValid) {
211
+ tokenToUse = auth.rigoToken
212
+ isAuthenticated = true
213
+ tokenType = "rigo"
214
+ } else {
215
+ setAuth({
216
+ ...auth,
217
+ rigoToken: "",
218
+ bcToken: "",
219
+ userId: "",
220
+ user: null,
221
+ })
222
+ }
223
+ }
224
+
225
+ if (auth.publicToken && !isAuthenticated) {
226
+ const isPublicTokenValid = await isValidPublicToken(auth.publicToken)
227
+ if (isPublicTokenValid) {
228
+ tokenToUse = auth.publicToken
229
+ isAuthenticated = true
230
+ tokenType = "public"
231
+ }
232
+ }
233
+ if (!isAuthenticated) {
234
+ setShowTurnstileModal(true)
235
+ return
236
+ }
237
+
238
+ let techs = technologies.filter((t) => t.lang === formState.language)
239
+
240
+ if (techs.length === 0) {
241
+ techs = technologies.filter((t) => t.lang === "en")
242
+ }
243
+
244
+ const res = await publicInteractiveCreation(
245
+ {
246
+ courseInfo: `${JSON.stringify(
247
+ formState
248
+ )}. The following technologies are available, choose up to 3 from the following list: <techs>${techs
249
+ .map((t) => t.slug)
250
+ .join(", ")}</techs>
251
+
252
+ The following files have been uploaded by the user: ${uploadedFiles.map((f) => `<FILE name="${f.name}">${f.text}</FILE>`).join("\n")}
253
+ `,
254
+ prevInteractions: "USER: " + formState.description,
255
+ },
256
+ tokenToUse,
257
+ formState.purpose || "learnpack-lesson-writer",
258
+ tokenType === "rigo" ? false : true
259
+ )
260
+ setNotificationId(res.notificationId)
261
+ } catch (error) {
262
+ console.error(error, "ERROR CREATING TUTORIAL")
263
+ toast.error("Something went wrong. Please try again.")
264
+ setFormState({
265
+ isCompleted: false,
266
+ currentStep: "hasContentIndex",
267
+ })
268
+ }
269
+ }
270
+
271
+ const checkTechs = async () => {
272
+ if (technologies.length === 0) {
273
+ const technologies = await getTechnologies()
274
+ console.log("TECHNOLOGIES", technologies)
275
+ setTechnologies(technologies)
276
+ }
277
+ }
278
+
279
+ const handleLanguageSwitch = () => {
280
+ setFormState({
281
+ language: detectedLanguage,
282
+ })
283
+ i18n.changeLanguage(detectedLanguage)
284
+ setShowLanguageDetectionModal(false)
285
+ }
286
+
287
+ const handleLanguageStay = () => {
288
+ setShowLanguageDetectionModal(false)
289
+ }
290
+
291
+ const buildSteps = () => {
292
+ const steps = [
293
+ {
294
+ title: t("stepWizard.description"),
295
+ slug: "description",
296
+ isCompleted: formState.description.length > 0,
297
+ required: true,
298
+ validator: (value: string) => {
299
+ const lang = detectLanguage(value)
300
+ console.log("Description validator language detection:", { lang, currentLang: i18n.language, value })
301
+
302
+ if (lang && lang !== "en" && lang !== i18n.language) {
303
+ setDetectedLanguage(lang)
304
+ setShowLanguageDetectionModal(true)
305
+ }
306
+
307
+ return value.length > 0
308
+ },
309
+ content: (
310
+ <textarea
311
+ required
312
+ placeholder={t("stepWizard.descriptionPlaceholder")}
313
+ className="w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white"
314
+ value={formState.description}
315
+ onChange={(e) => {
316
+ setFormState({
317
+ description: e.target.value,
318
+ })
319
+ }}
320
+ />
321
+ ),
322
+ },
323
+ {
324
+ title: t("stepWizard.purpose"),
325
+ slug: "purpose",
326
+ isCompleted: formState?.purpose?.length > 0,
327
+ required: true,
328
+ content: (
329
+ <PurposeSelector
330
+ onFinish={(purpose) => {
331
+ setFormState({
332
+ purpose: purpose,
333
+ currentStep: "duration",
334
+ })
335
+ }}
336
+ />
337
+ ),
338
+ },
339
+ {
340
+ title: t("stepWizard.duration"),
341
+ slug: "duration",
342
+ isCompleted: formState.duration > 0,
343
+ required: true,
344
+ content: (
345
+ <div className="flex flex-col md:flex-row gap-2">
346
+ <SelectableCard
347
+ title={t("stepWizard.durationCard.15")}
348
+ // subtitle="This is a tutorial that will take 15 minutes to complete"
349
+ onClick={() => {
350
+ setFormState({
351
+ duration: 15,
352
+ currentStep: "verifyHuman",
353
+ })
354
+ }}
355
+ selected={formState.duration === 15}
356
+ />
357
+ <SelectableCard
358
+ title={t("stepWizard.durationCard.30")}
359
+ // subtitle="This is a tutorial that will take 30 minutes to complete"
360
+ onClick={() => {
361
+ setFormState({
362
+ duration: 30,
363
+ currentStep: "verifyHuman",
364
+ })
365
+ }}
366
+ selected={formState.duration === 30}
367
+ />
368
+ <SelectableCard
369
+ title={t("stepWizard.durationCard.60")}
370
+ // subtitle="This is a tutorial that will take 1 hour to complete"
371
+ onClick={() => {
372
+ setFormState({
373
+ duration: 60,
374
+ currentStep: "verifyHuman",
375
+ })
376
+ }}
377
+ selected={formState.duration === 60}
378
+ />
379
+ </div>
380
+ ),
381
+ },
382
+ {
383
+ title: t("stepWizard.verifyHuman"),
384
+ slug: "verifyHuman",
385
+ isCompleted: false,
386
+ required: true,
387
+ content: (
388
+ <TurnstileChallenge
389
+ siteKey={
390
+ DEV_MODE ? "0x4AAAAAABeKMBYYinMU4Ib0" : "0x4AAAAAABeZ9tjEevGBsJFU"
391
+ }
392
+ onSuccess={async (token) => {
393
+ const { human, message, token: jwtToken } = await isHuman(token)
394
+ if (human) {
395
+ toast.success(t("stepWizard.humanSuccess"))
396
+
397
+ console.log("JWT TOKEN received", jwtToken)
398
+ setAuth({
399
+ ...auth,
400
+ publicToken: jwtToken,
401
+ })
402
+ setFormState({
403
+ currentStep: "hasContentIndex",
404
+ })
405
+ } else {
406
+ toast.error(message)
407
+ setFormState({
408
+ currentStep: "duration",
409
+ })
410
+ }
411
+ }}
412
+ onError={() => {
413
+ toast.error(t("turnstileModal.error"), {
414
+ duration: 10000,
415
+ })
416
+ setFormState({
417
+ currentStep: "duration",
418
+ })
419
+ }}
420
+ />
421
+ ),
422
+ },
423
+ {
424
+ title: t("stepWizard.hasContentIndex"),
425
+ slug: "hasContentIndex",
426
+ isCompleted: false,
427
+ content: (
428
+ <>
429
+ <div className="flex flex-col md:flex-row gap-2 justify-center">
430
+ <SelectableCard
431
+ title={t("stepWizard.hasContentIndexCard.no")}
432
+ onClick={() => {
433
+ setFormState({
434
+ hasContentIndex: false,
435
+ variables: [
436
+ ...formState.variables.filter(
437
+ (v) => v !== "contentIndex"
438
+ ),
439
+ ],
440
+ isCompleted: true,
441
+ })
442
+ }}
443
+ // selected={formState.hasContentIndex === false}
444
+ />
445
+ <SelectableCard
446
+ title={t("stepWizard.hasContentIndexCard.yes")}
447
+ onClick={() => {
448
+ setFormState({
449
+ hasContentIndex: true,
450
+ currentStep: "contentIndex",
451
+ variables: [...formState.variables, "contentIndex"],
452
+ })
453
+ }}
454
+ // selected={formState.hasContentIndex === true}
455
+ />
456
+ </div>
457
+ </>
458
+ ),
459
+ },
460
+ {
461
+ title: t("stepWizard.contentIndex"),
462
+ slug: "contentIndex",
463
+ helpText: t("stepWizard.contentIndexHelpText"),
464
+ isCompleted:
465
+ formState.contentIndex.length > 0 || uploadedFiles.length > 0,
466
+ required: true,
467
+ content: (
468
+ <Uploader
469
+ onFinish={(text) => {
470
+ setFormState({
471
+ contentIndex: text,
472
+ isCompleted: true,
473
+ })
474
+ }}
475
+ />
476
+ ),
477
+ },
478
+ ]
479
+
480
+ return steps.filter(
481
+ (step) =>
482
+ formState.variables.includes(step.slug) ||
483
+ step.slug === formState.currentStep
484
+ )
485
+ }
486
+
487
+ return (
488
+ <>
489
+ <ParamsChecker />
490
+ {showLanguageDetectionModal && (
491
+ <LanguageDetectionModal
492
+ detectedLanguage={detectedLanguage}
493
+ onSwitch={handleLanguageSwitch}
494
+ onStay={handleLanguageStay}
495
+ />
496
+ )}
497
+ {showTurnstileModal && (
498
+ <TurnstileModal
499
+ onClose={() => {
500
+ setShowTurnstileModal(false)
501
+ handleCreateTutorial()
502
+ }}
503
+ onError={() => {
504
+ toast.error(t("turnstileModal.error"), {
505
+ duration: 10000,
506
+ })
507
+ setShowTurnstileModal(false)
508
+ setFormState({
509
+ currentStep: "purpose",
510
+ })
511
+ }}
512
+ />
513
+ )}
514
+ {formState.isCompleted && history.length === 0 ? (
515
+ <>
516
+ <Loader
517
+ text={t("loader.text")}
518
+ icon={<img src={RIGO_FLOAT_GIF} alt="rigo" className="w-20 h-20" />}
519
+ />
520
+ {notificationId && (
521
+ <NotificationListener
522
+ onNotification={(res) => {
523
+ const lessons = res.parsed.listOfSteps.map((lesson: any) => {
524
+ return parseLesson(lesson, [])
525
+ })
526
+
527
+ push({
528
+ lessons,
529
+ courseInfo: {
530
+ ...formState,
531
+ title: res.parsed.title,
532
+ slug: slugify(fixTitleLength(res.parsed.title)),
533
+ description: res.parsed.description,
534
+ language:
535
+ res.parsed.languageCode || formState.language || "en",
536
+ technologies:
537
+ res.parsed.technologies.length > 0
538
+ ? res.parsed.technologies
539
+ : ["education", "quizzes"],
540
+ },
541
+ })
542
+
543
+ if (res.parsed.languageCode) {
544
+ i18n.changeLanguage(res.parsed.languageCode)
545
+ }
546
+
547
+ let initialMessages: TMessage[] = [
548
+ {
549
+ type: "user",
550
+ content: formState.description,
551
+ },
552
+ {
553
+ type: "assistant",
554
+ content: res.parsed.aiMessage,
555
+ },
556
+ ]
557
+
558
+ if (lessons.length > 0) {
559
+ initialMessages.push({
560
+ type: "assistant",
561
+ content: t("contentIndex.okMessage"),
562
+ })
563
+ initialMessages.push({
564
+ type: "assistant",
565
+ content: t("contentIndex.instructionsMessage"),
566
+ })
567
+ }
568
+
569
+ setMessages(initialMessages)
570
+ navigate("/creator/syllabus")
571
+ setFormState({
572
+ isCompleted: false,
573
+ currentStep: "description",
574
+ })
575
+ }}
576
+ notificationId={notificationId}
577
+ />
578
+ )}
579
+ </>
580
+ ) : (
581
+ <>
582
+ {history.length > 0 && (
583
+ <ResumeCourseModal
584
+ onContinue={() => {
585
+ navigate("/creator/syllabus")
586
+ }}
587
+ onStartOver={() => {
588
+ resetFormState()
589
+ cleanAll()
590
+ cleanHistory()
591
+ }}
592
+ />
593
+ )}
594
+ <StepWizard
595
+ hideLastButton={true}
596
+ formState={formState}
597
+ steps={buildSteps()}
598
+ setFormState={setFormState}
599
+ onFinish={() => {
600
+ setFormState({
601
+ isCompleted: true,
602
+ })
603
+ }}
604
+ />
605
+ </>
606
+ )}
607
+ </>
608
+ )
609
+ }
610
+
611
+ export default App