@learnpack/learnpack 5.0.256 → 5.0.260

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.
@@ -10,7 +10,7 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-DZq54NPa.js"></script>
13
+ <script type="module" crossorigin src="/creator/assets/index-BvHkfJm4.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/creator/assets/index-DmpsXknz.css">
15
15
  </head>
16
16
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.256",
4
+ "version": "5.0.260",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -12,6 +12,7 @@
12
12
  "@tailwindcss/vite": "^4.1.3",
13
13
  "axios": "^1.8.4",
14
14
  "framer-motion": "^12.9.2",
15
+ "franc": "^6.2.0",
15
16
  "front-matter": "^4.0.2",
16
17
  "html2canvas": "^1.4.1",
17
18
  "i18next": "^25.2.1",
@@ -2455,6 +2456,16 @@
2455
2456
  "url": "https://github.com/sponsors/wooorm"
2456
2457
  }
2457
2458
  },
2459
+ "node_modules/collapse-white-space": {
2460
+ "version": "2.1.0",
2461
+ "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
2462
+ "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==",
2463
+ "license": "MIT",
2464
+ "funding": {
2465
+ "type": "github",
2466
+ "url": "https://github.com/sponsors/wooorm"
2467
+ }
2468
+ },
2458
2469
  "node_modules/color-convert": {
2459
2470
  "version": "2.0.1",
2460
2471
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3287,6 +3298,19 @@
3287
3298
  }
3288
3299
  }
3289
3300
  },
3301
+ "node_modules/franc": {
3302
+ "version": "6.2.0",
3303
+ "resolved": "https://registry.npmjs.org/franc/-/franc-6.2.0.tgz",
3304
+ "integrity": "sha512-rcAewP7PSHvjq7Kgd7dhj82zE071kX5B4W1M4ewYMf/P+i6YsDQmj62Xz3VQm9zyUzUXwhIde/wHLGCMrM+yGg==",
3305
+ "license": "MIT",
3306
+ "dependencies": {
3307
+ "trigram-utils": "^2.0.0"
3308
+ },
3309
+ "funding": {
3310
+ "type": "github",
3311
+ "url": "https://github.com/sponsors/wooorm"
3312
+ }
3313
+ },
3290
3314
  "node_modules/front-matter": {
3291
3315
  "version": "4.0.2",
3292
3316
  "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz",
@@ -5058,6 +5082,16 @@
5058
5082
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
5059
5083
  "license": "MIT"
5060
5084
  },
5085
+ "node_modules/n-gram": {
5086
+ "version": "2.0.2",
5087
+ "resolved": "https://registry.npmjs.org/n-gram/-/n-gram-2.0.2.tgz",
5088
+ "integrity": "sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ==",
5089
+ "license": "MIT",
5090
+ "funding": {
5091
+ "type": "github",
5092
+ "url": "https://github.com/sponsors/wooorm"
5093
+ }
5094
+ },
5061
5095
  "node_modules/nanoid": {
5062
5096
  "version": "3.3.11",
5063
5097
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -6046,6 +6080,20 @@
6046
6080
  "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
6047
6081
  "license": "MIT"
6048
6082
  },
6083
+ "node_modules/trigram-utils": {
6084
+ "version": "2.0.1",
6085
+ "resolved": "https://registry.npmjs.org/trigram-utils/-/trigram-utils-2.0.1.tgz",
6086
+ "integrity": "sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==",
6087
+ "license": "MIT",
6088
+ "dependencies": {
6089
+ "collapse-white-space": "^2.0.0",
6090
+ "n-gram": "^2.0.0"
6091
+ },
6092
+ "funding": {
6093
+ "type": "github",
6094
+ "url": "https://github.com/sponsors/wooorm"
6095
+ }
6096
+ },
6049
6097
  "node_modules/trim-lines": {
6050
6098
  "version": "3.0.1",
6051
6099
  "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -15,6 +15,7 @@
15
15
  "@tailwindcss/vite": "^4.1.3",
16
16
  "axios": "^1.8.4",
17
17
  "framer-motion": "^12.9.2",
18
+ "franc": "^6.2.0",
18
19
  "front-matter": "^4.0.2",
19
20
  "html2canvas": "^1.4.1",
20
21
  "i18next": "^25.2.1",
@@ -14,6 +14,8 @@ import {
14
14
  parseLesson,
15
15
  fixTitleLength,
16
16
  getTechnologies,
17
+ detectLanguage,
18
+ isValidPublicToken,
17
19
  } from "./utils/lib"
18
20
 
19
21
  import { Uploader } from "./components/Uploader"
@@ -27,6 +29,7 @@ import { possiblePurposes, PurposeSelector } from "./components/PurposeSelector"
27
29
  import { useTranslation } from "react-i18next"
28
30
  import NotificationListener from "./components/NotificationListener"
29
31
  import { slugify } from "./utils/creatorUtils"
32
+ import TurnstileModal from "./components/TurnstileModal"
30
33
 
31
34
  function App() {
32
35
  const navigate = useNavigate()
@@ -67,12 +70,13 @@ function App() {
67
70
  )
68
71
 
69
72
  const [notificationId, setNotificationId] = useState<string>("")
73
+ const [showTurnstileModal, setShowTurnstileModal] = useState(false)
70
74
 
71
75
  useEffect(() => {
72
76
  if (formState.isCompleted) {
73
77
  handleCreateTutorial()
74
78
  }
75
- }, [formState])
79
+ }, [formState.isCompleted])
76
80
 
77
81
  useEffect(() => {
78
82
  verifyToken()
@@ -119,7 +123,6 @@ function App() {
119
123
  console.log("description", description)
120
124
  setFormState({
121
125
  description: description,
122
- currentStep: "duration",
123
126
  })
124
127
  }
125
128
  if (duration && description && !isNaN(parseInt(duration))) {
@@ -128,7 +131,6 @@ function App() {
128
131
  console.log("duration", durationInt)
129
132
  setFormState({
130
133
  duration: durationInt,
131
- currentStep: "hasContentIndex",
132
134
  })
133
135
  } else {
134
136
  console.log("Invalid duration received in params", duration)
@@ -150,25 +152,55 @@ function App() {
150
152
  if (purpose && purpose.length > 0 && possiblePurposes.includes(purpose)) {
151
153
  setFormState({
152
154
  purpose: purpose,
155
+ })
156
+ }
157
+
158
+ if (description && duration && purpose) {
159
+ setFormState({
153
160
  currentStep: "hasContentIndex",
154
161
  })
155
162
  }
163
+ if (description && duration && !purpose) {
164
+ setFormState({
165
+ currentStep: "purpose",
166
+ })
167
+ }
156
168
  }
157
169
 
158
170
  const handleCreateTutorial = async () => {
159
171
  try {
160
172
  let isAuthenticated = false
161
- if (auth.publicToken) {
162
- isAuthenticated = await isValidRigoToken(auth.publicToken)
173
+ let tokenToUse = ""
174
+ let tokenType = ""
175
+ if (auth.rigoToken) {
176
+ console.log("auth.rigoToken", auth.rigoToken)
177
+ const isRigoTokenValid = await isValidRigoToken(auth.rigoToken)
178
+ if (isRigoTokenValid) {
179
+ tokenToUse = auth.rigoToken
180
+ isAuthenticated = true
181
+ tokenType = "rigo"
182
+ } else {
183
+ setAuth({
184
+ ...auth,
185
+ rigoToken: "",
186
+ bcToken: "",
187
+ userId: "",
188
+ user: null,
189
+ })
190
+ }
163
191
  }
164
- if (!isAuthenticated && auth.rigoToken) {
165
- setAuth({
166
- ...auth,
167
- rigoToken: "",
168
- bcToken: "",
169
- userId: "",
170
- user: null,
171
- })
192
+
193
+ if (auth.publicToken && !isAuthenticated) {
194
+ const isPublicTokenValid = await isValidPublicToken(auth.publicToken)
195
+ if (isPublicTokenValid) {
196
+ tokenToUse = auth.publicToken
197
+ isAuthenticated = true
198
+ tokenType = "public"
199
+ }
200
+ }
201
+ if (!isAuthenticated) {
202
+ setShowTurnstileModal(true)
203
+ return
172
204
  }
173
205
 
174
206
  let techs = technologies.filter((t) => t.lang === formState.language)
@@ -186,9 +218,9 @@ function App() {
186
218
  .join(", ")}</techs>`,
187
219
  prevInteractions: "USER: " + formState.description,
188
220
  },
189
- auth.rigoToken && isAuthenticated ? auth.rigoToken : auth.publicToken,
221
+ tokenToUse,
190
222
  formState.purpose || "learnpack-lesson-writer",
191
- auth.rigoToken && isAuthenticated ? false : true
223
+ tokenType === "rigo" ? false : true
192
224
  )
193
225
  console.log("RES", res)
194
226
 
@@ -218,6 +250,15 @@ function App() {
218
250
  slug: "description",
219
251
  isCompleted: formState.description.length > 0,
220
252
  required: true,
253
+ validator: (value: string) => {
254
+ const lang = detectLanguage(value)
255
+
256
+ if (lang) {
257
+ i18n.changeLanguage(lang)
258
+ }
259
+
260
+ return value.length > 0
261
+ },
221
262
  content: (
222
263
  <textarea
223
264
  required
@@ -322,6 +363,11 @@ function App() {
322
363
  })
323
364
  }
324
365
  }}
366
+ // onError={() => {
367
+ // setFormState({
368
+ // currentStep: "purpose",
369
+ // })
370
+ // }}
325
371
  />
326
372
  ),
327
373
  },
@@ -392,6 +438,14 @@ function App() {
392
438
  return (
393
439
  <>
394
440
  <ParamsChecker />
441
+ {showTurnstileModal && (
442
+ <TurnstileModal
443
+ onClose={() => {
444
+ setShowTurnstileModal(false)
445
+ handleCreateTutorial()
446
+ }}
447
+ />
448
+ )}
395
449
  {formState.isCompleted && history.length === 0 ? (
396
450
  <>
397
451
  <Loader
@@ -402,7 +456,6 @@ function App() {
402
456
  <NotificationListener
403
457
  onNotification={(res) => {
404
458
  console.log("Async response", res)
405
- toast.success("Initial course created successfully")
406
459
  const lessons = res.parsed.listOfSteps.map((lesson: any) => {
407
460
  return parseLesson(lesson, [])
408
461
  })
@@ -1,19 +1,19 @@
1
1
  import { useEffect } from "react"
2
2
  import CreatorSocket from "../utils/socket"
3
+ import { DEV_MODE } from "../utils/constants"
3
4
 
4
5
  interface NotificationListenerProps {
5
6
  notificationId: string
6
7
  onNotification: (data: any) => void
7
8
  }
8
9
 
9
- const socketClient = new CreatorSocket("")
10
+ const socketClient = new CreatorSocket(DEV_MODE ? "http://localhost:3000" : "")
10
11
 
11
12
  const NotificationListener: React.FC<NotificationListenerProps> = ({
12
13
  notificationId,
13
14
  onNotification,
14
15
  }) => {
15
16
  useEffect(() => {
16
-
17
17
  if (!notificationId) return
18
18
 
19
19
  socketClient.connect()
@@ -4,14 +4,14 @@ import useStore from "../utils/store"
4
4
  import { useTranslation } from "react-i18next"
5
5
 
6
6
  export const possiblePurposes = [
7
- "learnpack-lesson-writer",
7
+ // "learnpack-lesson-writer",
8
8
  "homework-and-exam-preparation-aid",
9
9
  "skill-building-facilitator",
10
10
  "certification-preparation-specialist",
11
11
  ]
12
12
 
13
13
  type PurposeSlug =
14
- | "learnpack-lesson-writer"
14
+ // | "learnpack-lesson-writer"
15
15
  | "homework-and-exam-preparation-aid"
16
16
  | "skill-building-facilitator"
17
17
  | "certification-preparation-specialist"
@@ -30,11 +30,11 @@ export const PurposeSelector: React.FC<PurposeSelectorProps> = ({
30
30
 
31
31
  const PURPOSES: { slug: PurposeSlug; label: string; description: string }[] =
32
32
  [
33
- {
34
- slug: "learnpack-lesson-writer",
35
- label: t("purposeSelector.learnpack-lesson-writer.label"),
36
- description: t("purposeSelector.learnpack-lesson-writer.description"),
37
- },
33
+ // {
34
+ // slug: "learnpack-lesson-writer",
35
+ // label: t("purposeSelector.learnpack-lesson-writer.label"),
36
+ // description: t("purposeSelector.learnpack-lesson-writer.description"),
37
+ // },
38
38
  {
39
39
  slug: "homework-and-exam-preparation-aid",
40
40
  label: t("purposeSelector.homework-and-exam-preparation-aid.label"),
@@ -11,6 +11,7 @@ export type Step = {
11
11
  content: React.ReactNode
12
12
  slug: string
13
13
  isCompleted: boolean
14
+ validator?: (value: any) => boolean
14
15
  }
15
16
 
16
17
  type Props = {
@@ -39,6 +40,13 @@ const StepWizard: React.FC<Props> = ({
39
40
  toast.error(t("stepWizard.requiredFields"))
40
41
  return
41
42
  }
43
+ if (
44
+ steps[index].validator &&
45
+ !steps[index].validator(formState[steps[index].slug])
46
+ ) {
47
+ toast.error(t("stepWizard.invalidFields"))
48
+ return
49
+ }
42
50
  if (index < totalSteps - 1)
43
51
  setFormState({
44
52
  ...formState,
@@ -0,0 +1,36 @@
1
+ import TurnstileChallenge from "./TurnstileChallenge"
2
+ import { DEV_MODE } from "../utils/constants"
3
+ import { isHuman } from "../utils/rigo"
4
+ import { toast } from "react-hot-toast"
5
+ import useStore from "../utils/store"
6
+ import { useTranslation } from "react-i18next"
7
+
8
+ export default function TurnstileModal({ onClose }: { onClose: () => void }) {
9
+ const auth = useStore((state) => state.auth)
10
+ const setAuth = useStore((state) => state.setAuth)
11
+ const { t } = useTranslation()
12
+
13
+ return (
14
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-1000">
15
+ <TurnstileChallenge
16
+ siteKey={
17
+ DEV_MODE ? "0x4AAAAAABeKMBYYinMU4Ib0" : "0x4AAAAAABeZ9tjEevGBsJFU"
18
+ }
19
+ onSuccess={async (token) => {
20
+ const { human, message, token: jwtToken } = await isHuman(token)
21
+ if (human) {
22
+ toast.success(t("stepWizard.humanSuccess"))
23
+ setAuth({
24
+ ...auth,
25
+ publicToken: jwtToken,
26
+ })
27
+ onClose()
28
+ } else {
29
+ toast.error(message)
30
+ onClose()
31
+ }
32
+ }}
33
+ />
34
+ </div>
35
+ )
36
+ }
@@ -13,6 +13,7 @@ import {
13
13
  // reWriteTitle,
14
14
  useConsumableCall,
15
15
  isValidRigoToken,
16
+ isValidPublicToken,
16
17
  } from "../../utils/lib"
17
18
 
18
19
  import Loader from "../Loader"
@@ -120,26 +121,46 @@ const SyllabusEditor: React.FC = () => {
120
121
  setIsThinking(true)
121
122
 
122
123
  try {
123
- setMessages([
124
- ...messages,
125
- { type: "user", content: prompt },
126
- { type: "assistant", content: "" },
127
- ])
128
124
  let isAuthenticated = false
125
+ let tokenType = ""
126
+ let tokenToUse = ""
129
127
  if (auth.rigoToken) {
130
- isAuthenticated = await isValidRigoToken(auth.rigoToken)
128
+ const isRigoTokenValid = await isValidRigoToken(auth.rigoToken)
129
+ if (isRigoTokenValid) {
130
+ isAuthenticated = true
131
+ tokenType = "rigo"
132
+ tokenToUse = auth.rigoToken
133
+ } else {
134
+ setAuth({
135
+ ...auth,
136
+ rigoToken: "",
137
+ bcToken: "",
138
+ userId: "",
139
+ user: null,
140
+ })
141
+ }
131
142
  }
132
143
 
133
- if (!isAuthenticated && auth.rigoToken) {
134
- setAuth({
135
- ...auth,
136
- rigoToken: "",
137
- bcToken: "",
138
- userId: "",
139
- user: null,
140
- })
144
+ if (auth.publicToken && !isAuthenticated) {
145
+ const isPublicTokenValid = await isValidPublicToken(auth.publicToken)
146
+ if (isPublicTokenValid) {
147
+ isAuthenticated = true
148
+ tokenType = "public"
149
+ tokenToUse = auth.publicToken
150
+ }
141
151
  }
142
152
 
153
+ if (!isAuthenticated) {
154
+ setShowLoginModal(true)
155
+ return
156
+ }
157
+
158
+ setMessages([
159
+ ...messages,
160
+ { type: "user", content: prompt },
161
+ { type: "assistant", content: "" },
162
+ ])
163
+
143
164
  const res = await publicInteractiveCreation(
144
165
  {
145
166
  courseInfo:
@@ -153,9 +174,9 @@ const SyllabusEditor: React.FC = () => {
153
174
  .map((message) => `${message.type}: ${message.content}`)
154
175
  .join("\n") + `\nUSER: ${prompt}`,
155
176
  },
156
- auth.rigoToken && isAuthenticated ? auth.rigoToken : auth.publicToken,
177
+ tokenToUse,
157
178
  syllabus?.courseInfo?.purpose || "learnpack-lesson-writer",
158
- auth.rigoToken && isAuthenticated ? false : true
179
+ tokenType === "rigo" ? false : true
159
180
  )
160
181
 
161
182
  setNotificationId(res.notificationId)
@@ -5,6 +5,7 @@
5
5
  },
6
6
  "stepWizard": {
7
7
  "requiredFields": "Please fill out all required fields!",
8
+ "invalidFields": "Please fill out all fields correctly!",
8
9
  "subtitle": "Setting up your tutorial",
9
10
  "back": "Back",
10
11
  "next": "Next",
@@ -5,6 +5,7 @@
5
5
  },
6
6
  "stepWizard": {
7
7
  "requiredFields": "Por favor, completa todos los campos requeridos.",
8
+ "invalidFields": "Por favor, completa todos los campos correctamente.",
8
9
  "subtitle": "Configurando tu tutorial",
9
10
  "back": "Atrás",
10
11
  "next": "Siguiente",
@@ -1,4 +1,5 @@
1
1
  import axios from "axios"
2
+ import { franc } from "franc"
2
3
  import { BREATHECODE_HOST, DEV_MODE, RIGOBOT_HOST } from "./constants"
3
4
  import { Lesson } from "../components/LessonItem"
4
5
  import { randomUUID } from "./creatorUtils"
@@ -434,6 +435,19 @@ export const isValidRigoToken = async (rigobotToken: string) => {
434
435
 
435
436
  return true
436
437
  }
438
+ export const isValidPublicToken = async (publicToken: string) => {
439
+ const headers = {
440
+ "Content-Type": "application/json",
441
+ Authorization: "Token " + publicToken,
442
+ }
443
+ const rigoUrl = `${RIGOBOT_HOST}/v1/auth/public/token/validate`
444
+ const rigoResp = await fetch(rigoUrl, { headers })
445
+ if (!rigoResp.ok) {
446
+ return false
447
+ }
448
+
449
+ return true
450
+ }
437
451
 
438
452
  export const fixTitleLength = (title: string) => {
439
453
  const MAX_LENGTH = 49
@@ -446,3 +460,9 @@ export const getTechnologies = async () => {
446
460
  const response = await axios.get(`/technologies`)
447
461
  return response.data
448
462
  }
463
+
464
+ export const detectLanguage = (text: string) => {
465
+ const lang = franc(text)
466
+ if (lang === "spa") return "es"
467
+ else return "en"
468
+ }