@learnpack/learnpack 5.0.309 → 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 (87) 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/rigo.ts +1 -1
  48. package/src/creator/src/utils/socket.ts +61 -61
  49. package/src/creator/src/utils/store.ts +222 -222
  50. package/src/creator/src/vite-env.d.ts +1 -1
  51. package/src/creator/vite.config.ts +13 -13
  52. package/src/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  53. package/src/creatorDist/index.html +1 -1
  54. package/src/managers/config/defaults.ts +49 -49
  55. package/src/managers/config/exercise.ts +364 -364
  56. package/src/managers/config/index.ts +775 -775
  57. package/src/managers/file.ts +236 -236
  58. package/src/managers/server/routes.ts +554 -554
  59. package/src/managers/telemetry.ts +188 -188
  60. package/src/models/action.ts +13 -13
  61. package/src/models/config-manager.ts +28 -28
  62. package/src/models/config.ts +106 -106
  63. package/src/models/exercise-obj.ts +30 -30
  64. package/src/models/session.ts +39 -39
  65. package/src/models/socket.ts +61 -61
  66. package/src/models/status.ts +16 -16
  67. package/src/ui/_app/app.css +1 -1
  68. package/src/ui/_app/app.js +477 -407
  69. package/src/ui/app.tar.gz +0 -0
  70. package/src/utils/BaseCommand.ts +56 -56
  71. package/src/utils/api.ts +665 -665
  72. package/src/utils/audit.ts +392 -392
  73. package/src/utils/checkNotInstalled.ts +267 -267
  74. package/src/utils/convertCreds.js +34 -34
  75. package/src/utils/creatorUtilities.ts +504 -504
  76. package/src/utils/export/README.md +178 -178
  77. package/src/utils/incrementVersion.js +74 -74
  78. package/src/utils/misc.ts +58 -58
  79. package/src/utils/sidebarGenerator.ts +195 -195
  80. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  81. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  82. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  83. package/src/utils/templates/scorm/config/index.html +209 -209
  84. package/src/utils/templates/scorm/ims_xml.xsd +1 -1
  85. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  86. package/src/utils/templates/scorm/imsmanifest.xml +38 -38
  87. 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