@learnpack/learnpack 5.0.272 → 5.0.274

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 (75) 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/serve.js +32 -17
  8. package/lib/creatorDist/assets/{index-C1pv1wUb.js → index-BfLyIQVh.js} +10351 -10227
  9. package/lib/creatorDist/assets/{index-B4khtb0r.css → index-C39zeF3W.css} +3 -3
  10. package/lib/creatorDist/index.html +2 -2
  11. package/lib/managers/config/index.js +77 -77
  12. package/lib/utils/api.js +1 -0
  13. package/lib/utils/creatorUtilities.js +14 -14
  14. package/package.json +1 -1
  15. package/src/commands/audit.ts +487 -487
  16. package/src/commands/breakToken.ts +67 -67
  17. package/src/commands/clean.ts +30 -30
  18. package/src/commands/init.ts +650 -650
  19. package/src/commands/logout.ts +38 -38
  20. package/src/commands/publish.ts +522 -522
  21. package/src/commands/serve.ts +38 -28
  22. package/src/commands/start.ts +333 -333
  23. package/src/commands/translate.ts +123 -123
  24. package/src/creator/README.md +54 -54
  25. package/src/creator/eslint.config.js +28 -28
  26. package/src/creator/src/components/syllabus/ContentIndex.tsx +1 -1
  27. package/src/creator/src/i18n.ts +28 -28
  28. package/src/creator/src/index.css +217 -217
  29. package/src/creator/src/locales/en.json +1 -0
  30. package/src/creator/src/locales/es.json +1 -0
  31. package/src/creator/src/utils/configTypes.ts +122 -122
  32. package/src/creator/src/utils/constants.ts +13 -13
  33. package/src/creator/src/utils/creatorUtils.ts +46 -46
  34. package/src/creator/src/utils/eventBus.ts +2 -2
  35. package/src/creator/src/utils/lib.ts +468 -468
  36. package/src/creator/src/utils/rigo.ts +26 -26
  37. package/src/creator/src/utils/socket.ts +61 -61
  38. package/src/creator/src/utils/store.ts +222 -222
  39. package/src/creator/src/vite-env.d.ts +1 -1
  40. package/src/creator/vite.config.ts +13 -13
  41. package/src/creatorDist/assets/{index-C1pv1wUb.js → index-BfLyIQVh.js} +10351 -10227
  42. package/src/creatorDist/assets/{index-B4khtb0r.css → index-C39zeF3W.css} +3 -3
  43. package/src/creatorDist/index.html +2 -2
  44. package/src/managers/config/defaults.ts +49 -49
  45. package/src/managers/config/exercise.ts +364 -364
  46. package/src/managers/config/index.ts +775 -775
  47. package/src/managers/file.ts +236 -236
  48. package/src/managers/server/routes.ts +554 -554
  49. package/src/managers/session.ts +182 -182
  50. package/src/managers/telemetry.ts +188 -188
  51. package/src/models/action.ts +13 -13
  52. package/src/models/config-manager.ts +28 -28
  53. package/src/models/config.ts +106 -106
  54. package/src/models/creator.ts +40 -40
  55. package/src/models/exercise-obj.ts +30 -30
  56. package/src/models/session.ts +39 -39
  57. package/src/models/socket.ts +61 -61
  58. package/src/models/status.ts +16 -16
  59. package/src/ui/_app/app.css +1 -1
  60. package/src/ui/_app/app.js +435 -414
  61. package/src/ui/_app/learnpack.svg +7 -7
  62. package/src/ui/app.tar.gz +0 -0
  63. package/src/utils/BaseCommand.ts +56 -56
  64. package/src/utils/api.ts +31 -30
  65. package/src/utils/audit.ts +392 -392
  66. package/src/utils/checkNotInstalled.ts +267 -267
  67. package/src/utils/configBuilder.ts +82 -82
  68. package/src/utils/convertCreds.js +34 -34
  69. package/src/utils/creatorUtilities.ts +504 -504
  70. package/src/utils/incrementVersion.js +74 -74
  71. package/src/utils/misc.ts +58 -58
  72. package/src/utils/rigoActions.ts +500 -500
  73. package/src/utils/sidebarGenerator.ts +195 -195
  74. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  75. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
@@ -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
+ }