@learnpack/learnpack 5.0.275 → 5.0.276
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.
- package/README.md +409 -409
- package/lib/commands/audit.js +15 -15
- package/lib/commands/breakToken.js +19 -19
- package/lib/commands/clean.js +3 -3
- package/lib/commands/init.js +41 -41
- package/lib/commands/logout.js +3 -3
- package/lib/commands/publish.js +5 -10
- package/lib/commands/serve.js +3 -2
- package/lib/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
- package/lib/managers/config/index.js +77 -77
- package/lib/utils/api.d.ts +1 -1
- package/lib/utils/api.js +12 -9
- package/lib/utils/creatorUtilities.js +14 -14
- package/package.json +1 -1
- package/src/commands/audit.ts +487 -487
- package/src/commands/breakToken.ts +67 -67
- package/src/commands/clean.ts +30 -30
- package/src/commands/init.ts +650 -650
- package/src/commands/logout.ts +38 -38
- package/src/commands/publish.ts +20 -25
- package/src/commands/serve.ts +8 -3
- package/src/commands/start.ts +333 -333
- package/src/commands/translate.ts +123 -123
- package/src/creator/README.md +54 -54
- package/src/creator/eslint.config.js +28 -28
- package/src/creator/src/components/syllabus/ContentIndex.tsx +312 -312
- package/src/creator/src/i18n.ts +28 -28
- package/src/creator/src/index.css +217 -217
- package/src/creator/src/locales/en.json +126 -126
- package/src/creator/src/locales/es.json +126 -126
- package/src/creator/src/utils/configTypes.ts +122 -122
- package/src/creator/src/utils/constants.ts +13 -13
- package/src/creator/src/utils/creatorUtils.ts +46 -46
- package/src/creator/src/utils/eventBus.ts +2 -2
- package/src/creator/src/utils/lib.ts +468 -468
- package/src/creator/src/utils/socket.ts +61 -61
- package/src/creator/src/utils/store.ts +222 -222
- package/src/creator/src/vite-env.d.ts +1 -1
- package/src/creator/vite.config.ts +13 -13
- package/src/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
- package/src/managers/config/defaults.ts +49 -49
- package/src/managers/config/exercise.ts +364 -364
- package/src/managers/config/index.ts +775 -775
- package/src/managers/file.ts +236 -236
- package/src/managers/server/routes.ts +554 -554
- package/src/managers/session.ts +182 -182
- package/src/managers/telemetry.ts +188 -188
- package/src/models/action.ts +13 -13
- package/src/models/config-manager.ts +28 -28
- package/src/models/config.ts +106 -106
- package/src/models/creator.ts +47 -47
- package/src/models/exercise-obj.ts +30 -30
- package/src/models/session.ts +39 -39
- package/src/models/socket.ts +61 -61
- package/src/models/status.ts +16 -16
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +366 -363
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/BaseCommand.ts +56 -56
- package/src/utils/api.ts +53 -39
- package/src/utils/audit.ts +392 -392
- package/src/utils/checkNotInstalled.ts +267 -267
- package/src/utils/configBuilder.ts +82 -82
- package/src/utils/convertCreds.js +34 -34
- package/src/utils/creatorUtilities.ts +504 -504
- package/src/utils/incrementVersion.js +74 -74
- package/src/utils/misc.ts +58 -58
- package/src/utils/rigoActions.ts +500 -500
- package/src/utils/sidebarGenerator.ts +195 -195
- package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
- 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
|
+
}
|