@learnpack/learnpack 5.0.275 → 5.0.277

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 (95) 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/init.js +41 -41
  6. package/lib/commands/logout.js +3 -3
  7. package/lib/commands/publish.js +5 -10
  8. package/lib/commands/serve.js +55 -2
  9. package/lib/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  10. package/lib/managers/config/index.js +77 -77
  11. package/lib/utils/api.d.ts +1 -1
  12. package/lib/utils/api.js +12 -9
  13. package/lib/utils/creatorUtilities.js +14 -14
  14. package/lib/utils/export/epub.d.ts +2 -0
  15. package/lib/utils/export/epub.js +298 -0
  16. package/lib/utils/export/index.d.ts +3 -0
  17. package/lib/utils/export/index.js +7 -0
  18. package/lib/utils/export/scorm.d.ts +2 -0
  19. package/lib/utils/export/scorm.js +84 -0
  20. package/lib/utils/export/shared.d.ts +4 -0
  21. package/lib/utils/export/shared.js +61 -0
  22. package/lib/utils/export/types.d.ts +15 -0
  23. package/lib/utils/export/types.js +2 -0
  24. package/package.json +2 -1
  25. package/src/commands/audit.ts +487 -487
  26. package/src/commands/breakToken.ts +67 -67
  27. package/src/commands/clean.ts +30 -30
  28. package/src/commands/init.ts +650 -650
  29. package/src/commands/logout.ts +38 -38
  30. package/src/commands/publish.ts +20 -25
  31. package/src/commands/serve.ts +69 -4
  32. package/src/commands/start.ts +333 -333
  33. package/src/commands/translate.ts +123 -123
  34. package/src/creator/README.md +54 -54
  35. package/src/creator/eslint.config.js +7 -7
  36. package/src/creator/src/components/syllabus/ContentIndex.tsx +312 -312
  37. package/src/creator/src/i18n.ts +28 -28
  38. package/src/creator/src/index.css +217 -217
  39. package/src/creator/src/locales/en.json +126 -126
  40. package/src/creator/src/locales/es.json +126 -126
  41. package/src/creator/src/utils/configTypes.ts +122 -122
  42. package/src/creator/src/utils/constants.ts +13 -13
  43. package/src/creator/src/utils/creatorUtils.ts +46 -46
  44. package/src/creator/src/utils/eventBus.ts +2 -2
  45. package/src/creator/src/utils/lib.ts +468 -468
  46. package/src/creator/src/utils/socket.ts +61 -61
  47. package/src/creator/src/utils/store.ts +222 -222
  48. package/src/creator/src/vite-env.d.ts +1 -1
  49. package/src/creator/vite.config.ts +13 -13
  50. package/src/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  51. package/src/managers/config/defaults.ts +49 -49
  52. package/src/managers/config/exercise.ts +364 -364
  53. package/src/managers/config/index.ts +775 -775
  54. package/src/managers/file.ts +236 -236
  55. package/src/managers/server/routes.ts +554 -554
  56. package/src/managers/session.ts +182 -182
  57. package/src/managers/telemetry.ts +188 -188
  58. package/src/models/action.ts +13 -13
  59. package/src/models/config-manager.ts +28 -28
  60. package/src/models/config.ts +106 -106
  61. package/src/models/creator.ts +47 -47
  62. package/src/models/exercise-obj.ts +30 -30
  63. package/src/models/session.ts +39 -39
  64. package/src/models/socket.ts +61 -61
  65. package/src/models/status.ts +16 -16
  66. package/src/ui/_app/app.css +1 -1
  67. package/src/ui/_app/app.js +400 -397
  68. package/src/ui/app.tar.gz +0 -0
  69. package/src/utils/BaseCommand.ts +56 -56
  70. package/src/utils/api.ts +53 -39
  71. package/src/utils/audit.ts +392 -392
  72. package/src/utils/checkNotInstalled.ts +267 -267
  73. package/src/utils/configBuilder.ts +82 -82
  74. package/src/utils/convertCreds.js +34 -34
  75. package/src/utils/creatorUtilities.ts +504 -504
  76. package/src/utils/export/README.md +178 -0
  77. package/src/utils/export/epub.ts +400 -0
  78. package/src/utils/export/index.ts +3 -0
  79. package/src/utils/export/scorm.ts +121 -0
  80. package/src/utils/export/shared.ts +61 -0
  81. package/src/utils/export/types.ts +17 -0
  82. package/src/utils/incrementVersion.js +74 -74
  83. package/src/utils/misc.ts +58 -58
  84. package/src/utils/rigoActions.ts +500 -500
  85. package/src/utils/sidebarGenerator.ts +195 -195
  86. package/src/utils/templates/epub/epub.css +133 -0
  87. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  88. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  89. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -0
  90. package/src/utils/templates/scorm/config/api.js +175 -0
  91. package/src/utils/templates/scorm/config/index.html +210 -0
  92. package/src/utils/templates/scorm/ims_xml.xsd +1 -0
  93. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -0
  94. package/src/utils/templates/scorm/imsmanifest.xml +38 -0
  95. package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -0
@@ -1,468 +1,468 @@
1
- import axios from "axios"
2
- import { franc } from "franc"
3
- import { BREATHECODE_HOST, DEV_MODE, RIGOBOT_HOST } from "./constants"
4
- import { Lesson } from "../components/LessonItem"
5
- import { randomUUID } from "./creatorUtils"
6
- import { Syllabus } from "./store"
7
-
8
- export function parseLesson(input: string, previous: Lesson[]): Lesson | null {
9
- const pattern = /^([\d.]+)\s*-\s*(.*?)\s*\[(\w+):\s*(.+)\]$/
10
- const match = input.match(pattern)
11
-
12
- if (!match) return null
13
-
14
- const [, index, title, type, description] = match
15
-
16
- const alreadyExistsIndex = previous.findIndex(
17
- (lesson) => lesson.id === index && lesson.title === title
18
- )
19
-
20
- if (alreadyExistsIndex !== -1) {
21
- return {
22
- id: index,
23
- uid: previous[alreadyExistsIndex].uid,
24
- title: title.trim(),
25
- type: type.trim().toUpperCase() as "READ" | "CODE" | "QUIZ",
26
- description: description.trim(),
27
- duration: previous[alreadyExistsIndex].duration,
28
- }
29
- }
30
-
31
- return {
32
- id: index,
33
- title: title.trim(),
34
- type: type.trim().toUpperCase() as "READ" | "CODE" | "QUIZ",
35
- description: description.trim(),
36
- duration: 2,
37
- uid: randomUUID(),
38
- }
39
- }
40
-
41
- export const uploadFileToBucket = async (content: string, path: string) => {
42
- const response = await axios.post(`/upload`, {
43
- content,
44
- destination: path,
45
- })
46
- return response.data
47
- }
48
- export const uploadImageToBucket = async (imageUrl: string, path: string) => {
49
- const response = await axios.post(`/upload-image`, {
50
- image_url: imageUrl,
51
- destination: path,
52
- })
53
- return response.data
54
- }
55
-
56
- export const checkParams = (paramsToCheck: string[]) => {
57
- const urlParams = new URLSearchParams(window.location.search)
58
- const result: Record<string, string> = {}
59
-
60
- paramsToCheck.forEach((param) => {
61
- const value = urlParams.get(param)
62
- if (value !== null) {
63
- result[param] = value
64
- }
65
- })
66
-
67
- return result
68
- }
69
-
70
- export async function getConsumables(token: string): Promise<any> {
71
- const url = `${BREATHECODE_HOST}/v1/payments/me/service/consumable?virtual=true`
72
-
73
- const headers = {
74
- Authorization: `Token ${token}`,
75
- }
76
-
77
- try {
78
- const response = await axios.get(url, { headers })
79
- return response.data
80
- } catch (error) {
81
- console.error("Error fetching consumables:", error)
82
- throw error
83
- }
84
- }
85
-
86
- type ConsumableSlug =
87
- | "ai-conversation-message"
88
- | "ai-compilation"
89
- | "ai-generation"
90
- | "ai-course-generation"
91
-
92
- export async function useConsumableCall(
93
- breathecodeToken: string,
94
- consumableSlug: ConsumableSlug = "ai-conversation-message"
95
- ): Promise<boolean> {
96
- const url = `${BREATHECODE_HOST}/v1/payments/me/service/${consumableSlug}/consumptionsession`
97
-
98
- const headers = {
99
- Authorization: `Token ${breathecodeToken}`,
100
- }
101
-
102
- try {
103
- const response = await axios.put(url, {}, { headers })
104
-
105
- if (response.status >= 200 && response.status < 300) {
106
- console.log(`Successfully consumed ${consumableSlug}`)
107
- return true
108
- } else {
109
- console.error(`Request failed with status code: ${response.status}`)
110
- console.error(`Response: ${response.data}`)
111
- return false
112
- }
113
- } catch (error) {
114
- console.error(`Error consuming ${consumableSlug}:`, error)
115
- return false
116
- }
117
- }
118
-
119
- type ConsumableItem = {
120
- id: number
121
- how_many: number
122
- unit_type: string
123
- valid_until: string | null
124
- }
125
-
126
- type VoidEntry = {
127
- id: number
128
- slug: string
129
- balance: { unit: number }
130
- items: ConsumableItem[]
131
- }
132
-
133
- export const parseConsumables = (
134
- voids: VoidEntry[]
135
- ): Record<string, number> => {
136
- const result: Record<string, number> = {}
137
-
138
- voids.forEach((entry) => {
139
- const maxHowMany = entry.items.length
140
- ? Math.max(...entry.items.map((item) => item.how_many))
141
- : 0
142
- result[entry.slug] = maxHowMany
143
- })
144
-
145
- return result
146
- }
147
-
148
- type LoginInfo = {
149
- email: string
150
- password: string
151
- }
152
-
153
- export const getRigobotJSON = async (breathecodeToken: string) => {
154
- const rigoUrl = `${RIGOBOT_HOST}/v1/auth/me/token?breathecode_token=${breathecodeToken}`
155
- const rigoResp = await fetch(rigoUrl)
156
- if (!rigoResp.ok) {
157
- throw new Error("Unable to obtain Rigobot token")
158
- }
159
- const rigobotJson = await rigoResp.json()
160
- return rigobotJson
161
- }
162
- export const validateUser = async (breathecodeToken: string) => {
163
- const config = {
164
- method: "GET",
165
- headers: {
166
- "Content-Type": "application/json",
167
- Authorization: `Token ${breathecodeToken}`,
168
- },
169
- }
170
-
171
- const res = await fetch(`${BREATHECODE_HOST}/v1/auth/user/me`, config)
172
- if (!res.ok) {
173
- console.log("ERROR", res)
174
- return null
175
- }
176
- const json = await res.json()
177
-
178
- if ("roles" in json) {
179
- delete json.roles
180
- }
181
- if ("permissions" in json) {
182
- delete json.permissions
183
- }
184
- if ("settings" in json) {
185
- delete json.settings
186
- }
187
-
188
- return json
189
- }
190
-
191
- export const login4Geeks = async (loginInfo: LoginInfo) => {
192
- const url = `${BREATHECODE_HOST}/v1/auth/login/`
193
-
194
- const res = await fetch(url, {
195
- body: JSON.stringify(loginInfo),
196
- method: "post",
197
- headers: {
198
- "Content-Type": "application/json",
199
- },
200
- })
201
-
202
- if (!res.ok) {
203
- throw Error("Unable to login with provided credentials")
204
- }
205
-
206
- const json = await res.json()
207
-
208
- const rigoJson = await getRigobotJSON(json.token)
209
-
210
- const user = await validateUser(json.token)
211
- const returns = { ...json, rigobot: { ...rigoJson }, user }
212
-
213
- return returns
214
- }
215
-
216
- export const loginWithToken = async (token: string) => {
217
- const rigoJson = await getRigobotJSON(token)
218
-
219
- const user = await validateUser(token)
220
-
221
- const returns = { rigobot: { ...rigoJson }, ...user }
222
-
223
- return returns
224
- }
225
-
226
- export const validateTokens = async (
227
- breathecodeToken: string,
228
- onValidRigoToken: (token: string) => void
229
- ) => {
230
- const user = await validateUser(breathecodeToken)
231
- console.log("USER", user)
232
- if (!user) {
233
- return false
234
- }
235
-
236
- const rigobotJson = await getRigobotJSON(breathecodeToken)
237
- if (!rigobotJson) {
238
- return false
239
- }
240
- onValidRigoToken(rigobotJson.key)
241
- return true
242
- }
243
-
244
- export function extractImagesFromMarkdown(markdown: string) {
245
- const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
246
- const images = []
247
- let match
248
-
249
- while ((match = imageRegex.exec(markdown)) !== null) {
250
- const altText = match[1]
251
- const url = match[2]
252
- images.push({ alt: altText, url: url })
253
- }
254
-
255
- return images
256
- }
257
-
258
- export function getFilenameFromUrl(url: string): string {
259
- try {
260
- // 1) Use the URL constructor to strip off protocol/host/search/hash
261
- const pathname = new URL(url, location.href).pathname
262
- // 2) Grab everything after the last “/”
263
- return pathname.substring(pathname.lastIndexOf("/") + 1)
264
- } catch {
265
- // Fallback for non-absolute URLs or invalid inputs
266
- const clean = url.split("?")[0].split("#")[0]
267
- return clean.substring(clean.lastIndexOf("/") + 1)
268
- }
269
- }
270
-
271
- export const makeCallbackUrl = (slug: string) => {
272
- if (DEV_MODE) {
273
- return `https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us120.gitpod.io/v1/learnpack/tools/images/callback?slug=${slug}`
274
- }
275
- return `${window.location.origin}/api/v1/webhooks/images`
276
- }
277
-
278
- type TGenerateImageParams = {
279
- prompt: string
280
- }
281
-
282
- export const generateImage = async (
283
- token: string,
284
- { prompt }: TGenerateImageParams
285
- ) => {
286
- try {
287
- const response = await axios.post(
288
- `${RIGOBOT_HOST}/v1/learnpack/tools/images`,
289
- {
290
- prompt,
291
- webhook_callback_url: "https://www.learnpack.co/api/v1/webhooks/images",
292
- },
293
- {
294
- headers: {
295
- "Content-Type": "application/json",
296
- Authorization: "Token " + token,
297
- },
298
- }
299
- )
300
-
301
- return response.data
302
- } catch (error) {
303
- console.error("Error generating image:", error)
304
- return null
305
- }
306
- }
307
-
308
- export const createCourse = async (
309
- syllabus: Syllabus,
310
- token: string,
311
- breathecodeToken: string
312
- ) => {
313
- const response = await axios.post(
314
- `/actions/create-course`,
315
- {
316
- syllabus,
317
- },
318
- {
319
- headers: {
320
- "x-breathecode-token": breathecodeToken,
321
- "x-rigo-token": token,
322
- },
323
- }
324
- )
325
- return response.data
326
- }
327
-
328
- interface SlugAvailabilityResponse {
329
- slug: string
330
- available: boolean
331
- }
332
-
333
- export const isSlugAvailable = async (slug: string): Promise<boolean> => {
334
- try {
335
- const url = `${RIGOBOT_HOST}/v1/learnpack/check-slug-availability?slug=${encodeURIComponent(
336
- slug
337
- )}`
338
- const response = await axios.get<SlugAvailabilityResponse>(url)
339
- return response.data.available
340
- } catch (error) {
341
- console.error("Error checking slug availability:", error)
342
- throw error
343
- }
344
- }
345
-
346
- export const reWriteTitle = async (title: string, token: string) => {
347
- // We hav in the parsed the newTitle
348
- try {
349
- const response = await axios.post(
350
- `${RIGOBOT_HOST}/v1/prompting/completion/1050/`,
351
- {
352
- inputs: {
353
- current_title: title,
354
- },
355
- include_purpose_objective: false,
356
- execute_async: false,
357
- },
358
- {
359
- headers: {
360
- "Content-Type": "application/json",
361
- Authorization: "Token " + token,
362
- },
363
- }
364
- )
365
- console.log("RESPONSE", response.data)
366
- return response.data.parsed.newTitle
367
- } catch (error) {
368
- console.error("Error rewriting title:", error)
369
- // Return the title as it is with a random number of 4 characters
370
- return `${title} ${Math.random().toString(36).substring(2, 6)}`
371
- }
372
- }
373
-
374
- export async function registerUserWithFormData(
375
- firstName: string,
376
- lastName: string,
377
- email: string
378
- ): Promise<any> {
379
- const formData = new FormData()
380
- formData.append("1_first_name", firstName)
381
- formData.append("1_last_name", lastName)
382
- formData.append("1_email", email)
383
-
384
- // Puedes modificar este objeto para agregar info real de tracking si la tienes.
385
- const conversionArray = [
386
- "$K1",
387
- {
388
- user_agent: navigator.userAgent,
389
- landing_url: "www.learnpack.co/my-tutorials",
390
- conversion_url: "app.learnpack.co/login",
391
- translations: "$undefined",
392
- utm_placement: "$undefined",
393
- utm_referrer: "$undefined",
394
- utm_medium: "$undefined",
395
- utm_source: "$undefined",
396
- utm_term: "$undefined",
397
- utm_content: "$undefined",
398
- utm_campaign: "$undefined",
399
- internal_cta_placement: "$undefined",
400
- internal_cta_content: "$undefined",
401
- internal_cta_campaign: "$undefined",
402
- },
403
- ]
404
-
405
- formData.append("0", JSON.stringify(conversionArray))
406
-
407
- try {
408
- const response = await axios.post(
409
- "https://www.learnpack.co/register",
410
- formData,
411
- {
412
- headers: {
413
- "Content-Type": "multipart/form-data",
414
- },
415
- }
416
- )
417
- return response.data
418
- } catch (error: any) {
419
- // Manejo de errores básico
420
- console.error(error, "ERROR REGISTERING IN LEARNPACK")
421
- return {
422
- success: false,
423
- message: error?.response?.data?.detail || "Registration error",
424
- data: error?.response?.data || null,
425
- }
426
- }
427
- }
428
-
429
- export const isValidRigoToken = async (rigobotToken: string) => {
430
- const rigoUrl = `${RIGOBOT_HOST}/v1/auth/token/${rigobotToken}`
431
- const rigoResp = await fetch(rigoUrl)
432
- if (!rigoResp.ok) {
433
- return false
434
- }
435
-
436
- return true
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
- }
451
-
452
- export const fixTitleLength = (title: string) => {
453
- const MAX_LENGTH = 49
454
- let fixed = title.slice(0, MAX_LENGTH)
455
- fixed = fixed.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "")
456
- return fixed
457
- }
458
-
459
- export const getTechnologies = async () => {
460
- const response = await axios.get(`/technologies`)
461
- return response.data
462
- }
463
-
464
- export const detectLanguage = (text: string) => {
465
- const lang = franc(text)
466
- if (lang === "spa") return "es"
467
- else return "en"
468
- }
1
+ import axios from "axios"
2
+ import { franc } from "franc"
3
+ import { BREATHECODE_HOST, DEV_MODE, RIGOBOT_HOST } from "./constants"
4
+ import { Lesson } from "../components/LessonItem"
5
+ import { randomUUID } from "./creatorUtils"
6
+ import { Syllabus } from "./store"
7
+
8
+ export function parseLesson(input: string, previous: Lesson[]): Lesson | null {
9
+ const pattern = /^([\d.]+)\s*-\s*(.*?)\s*\[(\w+):\s*(.+)\]$/
10
+ const match = input.match(pattern)
11
+
12
+ if (!match) return null
13
+
14
+ const [, index, title, type, description] = match
15
+
16
+ const alreadyExistsIndex = previous.findIndex(
17
+ (lesson) => lesson.id === index && lesson.title === title
18
+ )
19
+
20
+ if (alreadyExistsIndex !== -1) {
21
+ return {
22
+ id: index,
23
+ uid: previous[alreadyExistsIndex].uid,
24
+ title: title.trim(),
25
+ type: type.trim().toUpperCase() as "READ" | "CODE" | "QUIZ",
26
+ description: description.trim(),
27
+ duration: previous[alreadyExistsIndex].duration,
28
+ }
29
+ }
30
+
31
+ return {
32
+ id: index,
33
+ title: title.trim(),
34
+ type: type.trim().toUpperCase() as "READ" | "CODE" | "QUIZ",
35
+ description: description.trim(),
36
+ duration: 2,
37
+ uid: randomUUID(),
38
+ }
39
+ }
40
+
41
+ export const uploadFileToBucket = async (content: string, path: string) => {
42
+ const response = await axios.post(`/upload`, {
43
+ content,
44
+ destination: path,
45
+ })
46
+ return response.data
47
+ }
48
+ export const uploadImageToBucket = async (imageUrl: string, path: string) => {
49
+ const response = await axios.post(`/upload-image`, {
50
+ image_url: imageUrl,
51
+ destination: path,
52
+ })
53
+ return response.data
54
+ }
55
+
56
+ export const checkParams = (paramsToCheck: string[]) => {
57
+ const urlParams = new URLSearchParams(window.location.search)
58
+ const result: Record<string, string> = {}
59
+
60
+ paramsToCheck.forEach((param) => {
61
+ const value = urlParams.get(param)
62
+ if (value !== null) {
63
+ result[param] = value
64
+ }
65
+ })
66
+
67
+ return result
68
+ }
69
+
70
+ export async function getConsumables(token: string): Promise<any> {
71
+ const url = `${BREATHECODE_HOST}/v1/payments/me/service/consumable?virtual=true`
72
+
73
+ const headers = {
74
+ Authorization: `Token ${token}`,
75
+ }
76
+
77
+ try {
78
+ const response = await axios.get(url, { headers })
79
+ return response.data
80
+ } catch (error) {
81
+ console.error("Error fetching consumables:", error)
82
+ throw error
83
+ }
84
+ }
85
+
86
+ type ConsumableSlug =
87
+ | "ai-conversation-message"
88
+ | "ai-compilation"
89
+ | "ai-generation"
90
+ | "ai-course-generation"
91
+
92
+ export async function useConsumableCall(
93
+ breathecodeToken: string,
94
+ consumableSlug: ConsumableSlug = "ai-conversation-message"
95
+ ): Promise<boolean> {
96
+ const url = `${BREATHECODE_HOST}/v1/payments/me/service/${consumableSlug}/consumptionsession`
97
+
98
+ const headers = {
99
+ Authorization: `Token ${breathecodeToken}`,
100
+ }
101
+
102
+ try {
103
+ const response = await axios.put(url, {}, { headers })
104
+
105
+ if (response.status >= 200 && response.status < 300) {
106
+ console.log(`Successfully consumed ${consumableSlug}`)
107
+ return true
108
+ } else {
109
+ console.error(`Request failed with status code: ${response.status}`)
110
+ console.error(`Response: ${response.data}`)
111
+ return false
112
+ }
113
+ } catch (error) {
114
+ console.error(`Error consuming ${consumableSlug}:`, error)
115
+ return false
116
+ }
117
+ }
118
+
119
+ type ConsumableItem = {
120
+ id: number
121
+ how_many: number
122
+ unit_type: string
123
+ valid_until: string | null
124
+ }
125
+
126
+ type VoidEntry = {
127
+ id: number
128
+ slug: string
129
+ balance: { unit: number }
130
+ items: ConsumableItem[]
131
+ }
132
+
133
+ export const parseConsumables = (
134
+ voids: VoidEntry[]
135
+ ): Record<string, number> => {
136
+ const result: Record<string, number> = {}
137
+
138
+ voids.forEach((entry) => {
139
+ const maxHowMany = entry.items.length
140
+ ? Math.max(...entry.items.map((item) => item.how_many))
141
+ : 0
142
+ result[entry.slug] = maxHowMany
143
+ })
144
+
145
+ return result
146
+ }
147
+
148
+ type LoginInfo = {
149
+ email: string
150
+ password: string
151
+ }
152
+
153
+ export const getRigobotJSON = async (breathecodeToken: string) => {
154
+ const rigoUrl = `${RIGOBOT_HOST}/v1/auth/me/token?breathecode_token=${breathecodeToken}`
155
+ const rigoResp = await fetch(rigoUrl)
156
+ if (!rigoResp.ok) {
157
+ throw new Error("Unable to obtain Rigobot token")
158
+ }
159
+ const rigobotJson = await rigoResp.json()
160
+ return rigobotJson
161
+ }
162
+ export const validateUser = async (breathecodeToken: string) => {
163
+ const config = {
164
+ method: "GET",
165
+ headers: {
166
+ "Content-Type": "application/json",
167
+ Authorization: `Token ${breathecodeToken}`,
168
+ },
169
+ }
170
+
171
+ const res = await fetch(`${BREATHECODE_HOST}/v1/auth/user/me`, config)
172
+ if (!res.ok) {
173
+ console.log("ERROR", res)
174
+ return null
175
+ }
176
+ const json = await res.json()
177
+
178
+ if ("roles" in json) {
179
+ delete json.roles
180
+ }
181
+ if ("permissions" in json) {
182
+ delete json.permissions
183
+ }
184
+ if ("settings" in json) {
185
+ delete json.settings
186
+ }
187
+
188
+ return json
189
+ }
190
+
191
+ export const login4Geeks = async (loginInfo: LoginInfo) => {
192
+ const url = `${BREATHECODE_HOST}/v1/auth/login/`
193
+
194
+ const res = await fetch(url, {
195
+ body: JSON.stringify(loginInfo),
196
+ method: "post",
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ },
200
+ })
201
+
202
+ if (!res.ok) {
203
+ throw Error("Unable to login with provided credentials")
204
+ }
205
+
206
+ const json = await res.json()
207
+
208
+ const rigoJson = await getRigobotJSON(json.token)
209
+
210
+ const user = await validateUser(json.token)
211
+ const returns = { ...json, rigobot: { ...rigoJson }, user }
212
+
213
+ return returns
214
+ }
215
+
216
+ export const loginWithToken = async (token: string) => {
217
+ const rigoJson = await getRigobotJSON(token)
218
+
219
+ const user = await validateUser(token)
220
+
221
+ const returns = { rigobot: { ...rigoJson }, ...user }
222
+
223
+ return returns
224
+ }
225
+
226
+ export const validateTokens = async (
227
+ breathecodeToken: string,
228
+ onValidRigoToken: (token: string) => void
229
+ ) => {
230
+ const user = await validateUser(breathecodeToken)
231
+ console.log("USER", user)
232
+ if (!user) {
233
+ return false
234
+ }
235
+
236
+ const rigobotJson = await getRigobotJSON(breathecodeToken)
237
+ if (!rigobotJson) {
238
+ return false
239
+ }
240
+ onValidRigoToken(rigobotJson.key)
241
+ return true
242
+ }
243
+
244
+ export function extractImagesFromMarkdown(markdown: string) {
245
+ const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
246
+ const images = []
247
+ let match
248
+
249
+ while ((match = imageRegex.exec(markdown)) !== null) {
250
+ const altText = match[1]
251
+ const url = match[2]
252
+ images.push({ alt: altText, url: url })
253
+ }
254
+
255
+ return images
256
+ }
257
+
258
+ export function getFilenameFromUrl(url: string): string {
259
+ try {
260
+ // 1) Use the URL constructor to strip off protocol/host/search/hash
261
+ const pathname = new URL(url, location.href).pathname
262
+ // 2) Grab everything after the last “/”
263
+ return pathname.substring(pathname.lastIndexOf("/") + 1)
264
+ } catch {
265
+ // Fallback for non-absolute URLs or invalid inputs
266
+ const clean = url.split("?")[0].split("#")[0]
267
+ return clean.substring(clean.lastIndexOf("/") + 1)
268
+ }
269
+ }
270
+
271
+ export const makeCallbackUrl = (slug: string) => {
272
+ if (DEV_MODE) {
273
+ return `https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us120.gitpod.io/v1/learnpack/tools/images/callback?slug=${slug}`
274
+ }
275
+ return `${window.location.origin}/api/v1/webhooks/images`
276
+ }
277
+
278
+ type TGenerateImageParams = {
279
+ prompt: string
280
+ }
281
+
282
+ export const generateImage = async (
283
+ token: string,
284
+ { prompt }: TGenerateImageParams
285
+ ) => {
286
+ try {
287
+ const response = await axios.post(
288
+ `${RIGOBOT_HOST}/v1/learnpack/tools/images`,
289
+ {
290
+ prompt,
291
+ webhook_callback_url: "https://www.learnpack.co/api/v1/webhooks/images",
292
+ },
293
+ {
294
+ headers: {
295
+ "Content-Type": "application/json",
296
+ Authorization: "Token " + token,
297
+ },
298
+ }
299
+ )
300
+
301
+ return response.data
302
+ } catch (error) {
303
+ console.error("Error generating image:", error)
304
+ return null
305
+ }
306
+ }
307
+
308
+ export const createCourse = async (
309
+ syllabus: Syllabus,
310
+ token: string,
311
+ breathecodeToken: string
312
+ ) => {
313
+ const response = await axios.post(
314
+ `/actions/create-course`,
315
+ {
316
+ syllabus,
317
+ },
318
+ {
319
+ headers: {
320
+ "x-breathecode-token": breathecodeToken,
321
+ "x-rigo-token": token,
322
+ },
323
+ }
324
+ )
325
+ return response.data
326
+ }
327
+
328
+ interface SlugAvailabilityResponse {
329
+ slug: string
330
+ available: boolean
331
+ }
332
+
333
+ export const isSlugAvailable = async (slug: string): Promise<boolean> => {
334
+ try {
335
+ const url = `${RIGOBOT_HOST}/v1/learnpack/check-slug-availability?slug=${encodeURIComponent(
336
+ slug
337
+ )}`
338
+ const response = await axios.get<SlugAvailabilityResponse>(url)
339
+ return response.data.available
340
+ } catch (error) {
341
+ console.error("Error checking slug availability:", error)
342
+ throw error
343
+ }
344
+ }
345
+
346
+ export const reWriteTitle = async (title: string, token: string) => {
347
+ // We hav in the parsed the newTitle
348
+ try {
349
+ const response = await axios.post(
350
+ `${RIGOBOT_HOST}/v1/prompting/completion/1050/`,
351
+ {
352
+ inputs: {
353
+ current_title: title,
354
+ },
355
+ include_purpose_objective: false,
356
+ execute_async: false,
357
+ },
358
+ {
359
+ headers: {
360
+ "Content-Type": "application/json",
361
+ Authorization: "Token " + token,
362
+ },
363
+ }
364
+ )
365
+ console.log("RESPONSE", response.data)
366
+ return response.data.parsed.newTitle
367
+ } catch (error) {
368
+ console.error("Error rewriting title:", error)
369
+ // Return the title as it is with a random number of 4 characters
370
+ return `${title} ${Math.random().toString(36).substring(2, 6)}`
371
+ }
372
+ }
373
+
374
+ export async function registerUserWithFormData(
375
+ firstName: string,
376
+ lastName: string,
377
+ email: string
378
+ ): Promise<any> {
379
+ const formData = new FormData()
380
+ formData.append("1_first_name", firstName)
381
+ formData.append("1_last_name", lastName)
382
+ formData.append("1_email", email)
383
+
384
+ // Puedes modificar este objeto para agregar info real de tracking si la tienes.
385
+ const conversionArray = [
386
+ "$K1",
387
+ {
388
+ user_agent: navigator.userAgent,
389
+ landing_url: "www.learnpack.co/my-tutorials",
390
+ conversion_url: "app.learnpack.co/login",
391
+ translations: "$undefined",
392
+ utm_placement: "$undefined",
393
+ utm_referrer: "$undefined",
394
+ utm_medium: "$undefined",
395
+ utm_source: "$undefined",
396
+ utm_term: "$undefined",
397
+ utm_content: "$undefined",
398
+ utm_campaign: "$undefined",
399
+ internal_cta_placement: "$undefined",
400
+ internal_cta_content: "$undefined",
401
+ internal_cta_campaign: "$undefined",
402
+ },
403
+ ]
404
+
405
+ formData.append("0", JSON.stringify(conversionArray))
406
+
407
+ try {
408
+ const response = await axios.post(
409
+ "https://www.learnpack.co/register",
410
+ formData,
411
+ {
412
+ headers: {
413
+ "Content-Type": "multipart/form-data",
414
+ },
415
+ }
416
+ )
417
+ return response.data
418
+ } catch (error: any) {
419
+ // Manejo de errores básico
420
+ console.error(error, "ERROR REGISTERING IN LEARNPACK")
421
+ return {
422
+ success: false,
423
+ message: error?.response?.data?.detail || "Registration error",
424
+ data: error?.response?.data || null,
425
+ }
426
+ }
427
+ }
428
+
429
+ export const isValidRigoToken = async (rigobotToken: string) => {
430
+ const rigoUrl = `${RIGOBOT_HOST}/v1/auth/token/${rigobotToken}`
431
+ const rigoResp = await fetch(rigoUrl)
432
+ if (!rigoResp.ok) {
433
+ return false
434
+ }
435
+
436
+ return true
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
+ }
451
+
452
+ export const fixTitleLength = (title: string) => {
453
+ const MAX_LENGTH = 49
454
+ let fixed = title.slice(0, MAX_LENGTH)
455
+ fixed = fixed.replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "")
456
+ return fixed
457
+ }
458
+
459
+ export const getTechnologies = async () => {
460
+ const response = await axios.get(`/technologies`)
461
+ return response.data
462
+ }
463
+
464
+ export const detectLanguage = (text: string) => {
465
+ const lang = franc(text)
466
+ if (lang === "spa") return "es"
467
+ else return "en"
468
+ }