@learnpack/learnpack 5.0.309 → 5.0.311

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/publish.js +16 -6
  7. package/lib/commands/serve.js +16 -16
  8. package/lib/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  9. package/lib/creatorDist/index.html +1 -1
  10. package/lib/managers/config/index.js +77 -77
  11. package/lib/utils/creatorUtilities.js +14 -14
  12. package/lib/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  13. package/lib/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  14. package/lib/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  15. package/lib/utils/templates/scorm/config/index.html +209 -209
  16. package/lib/utils/templates/scorm/ims_xml.xsd +1 -1
  17. package/lib/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  18. package/lib/utils/templates/scorm/imsmanifest.xml +38 -38
  19. package/lib/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
  20. package/package.json +1 -1
  21. package/src/commands/audit.ts +487 -487
  22. package/src/commands/breakToken.ts +67 -67
  23. package/src/commands/clean.ts +30 -30
  24. package/src/commands/logout.ts +38 -38
  25. package/src/commands/publish.ts +39 -23
  26. package/src/commands/serve.ts +3179 -3179
  27. package/src/commands/start.ts +333 -333
  28. package/src/commands/translate.ts +123 -123
  29. package/src/creator/README.md +54 -54
  30. package/src/creator/package-lock.json +6621 -6621
  31. package/src/creator/package.json +55 -55
  32. package/src/creator/src/App.tsx +611 -608
  33. package/src/creator/src/components/FileUploader.tsx +340 -302
  34. package/src/creator/src/components/Icon.tsx +18 -18
  35. package/src/creator/src/components/LessonItem.tsx +152 -152
  36. package/src/creator/src/components/Login.tsx +259 -259
  37. package/src/creator/src/components/ParamsChecker.tsx +25 -25
  38. package/src/creator/src/components/Uploader.tsx +3 -6
  39. package/src/creator/src/components/syllabus/ContentIndex.tsx +323 -323
  40. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +341 -337
  41. package/src/creator/src/i18n.ts +28 -28
  42. package/src/creator/src/locales/en.json +139 -138
  43. package/src/creator/src/locales/es.json +139 -138
  44. package/src/creator/src/utils/configTypes.ts +122 -122
  45. package/src/creator/src/utils/constants.ts +13 -13
  46. package/src/creator/src/utils/creatorUtils.ts +46 -46
  47. package/src/creator/src/utils/eventBus.ts +2 -2
  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