@learnpack/learnpack 5.0.204 → 5.0.211
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 +13 -13
- package/lib/commands/init.js +1 -0
- package/lib/commands/serve.d.ts +1 -1
- package/lib/commands/serve.js +244 -85
- package/lib/creatorDist/assets/{index-hTGEtFj4.js → index-DxwqeFD3.js} +13540 -13534
- package/lib/creatorDist/index.html +1 -1
- package/lib/models/creator.d.ts +1 -1
- package/lib/utils/creatorUtilities.js +6 -5
- package/lib/utils/rigoActions.d.ts +9 -6
- package/lib/utils/rigoActions.js +14 -10
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/src/commands/init.ts +2 -2
- package/src/commands/serve.ts +460 -138
- package/src/creator/src/App.tsx +2 -3
- package/src/creator/src/components/Login.tsx +2 -1
- package/src/creator/src/components/syllabus/ContentIndex.tsx +2 -1
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +43 -22
- package/src/creator/src/locales/en.json +10 -7
- package/src/creator/src/locales/es.json +4 -1
- package/src/creator/src/utils/creatorUtils.ts +7 -5
- package/src/creatorDist/assets/{index-hTGEtFj4.js → index-DxwqeFD3.js} +13540 -13534
- package/src/creatorDist/index.html +1 -1
- package/src/models/creator.ts +2 -1
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +315 -315
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/creatorUtilities.ts +6 -5
- package/src/utils/rigoActions.ts +25 -15
package/src/commands/serve.ts
CHANGED
@@ -23,11 +23,11 @@ import { Bucket, Storage } from "@google-cloud/storage"
|
|
23
23
|
import { downloadEditor, decompress } from "../managers/file"
|
24
24
|
import * as fs from "fs"
|
25
25
|
import {
|
26
|
-
createCodeFile,
|
26
|
+
// createCodeFile,
|
27
27
|
translateExercise,
|
28
28
|
isValidRigoToken,
|
29
29
|
readmeCreator,
|
30
|
-
makeReadmeReadable,
|
30
|
+
// makeReadmeReadable,
|
31
31
|
generateImage,
|
32
32
|
isPackageAuthor,
|
33
33
|
} from "../utils/rigoActions"
|
@@ -50,28 +50,19 @@ const frontMatter = require("front-matter")
|
|
50
50
|
|
51
51
|
dotenv.config()
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
function findLast<T>(
|
54
|
+
array: T[],
|
55
|
+
predicate: (item: T) => boolean
|
56
|
+
): T | undefined {
|
57
|
+
for (let i = array.length - 1; i >= 0; i--) {
|
58
|
+
if (predicate(array[i])) return array[i]
|
56
59
|
}
|
57
60
|
|
58
|
-
return
|
59
|
-
}
|
60
|
-
|
61
|
-
function fixSlugLength(slug: string): string {
|
62
|
-
let clean = slug.toLowerCase()
|
63
|
-
clean = clean.replace(/[^\da-z-]+/g, "-")
|
64
|
-
clean = clean.replace(/-+/g, "-")
|
65
|
-
clean = clean.slice(0, 49)
|
66
|
-
clean = clean.replace(/^-+/, "").replace(/-+$/, "")
|
67
|
-
clean = clean.replace(/^[^\da-z]+/, "").replace(/[^\da-z]+$/, "")
|
68
|
-
if (!clean) throw new Error("Invalid slug after cleaning")
|
69
|
-
|
70
|
-
return clean
|
61
|
+
return undefined
|
71
62
|
}
|
72
63
|
|
73
64
|
export const createLearnJson = (courseInfo: FormState) => {
|
74
|
-
console.log("courseInfo to create learn json", courseInfo)
|
65
|
+
// console.log("courseInfo to create learn json", courseInfo)
|
75
66
|
|
76
67
|
const expectedPreviewUrl = `https://${slugify(
|
77
68
|
courseInfo.title as string
|
@@ -167,27 +158,118 @@ export const processImage = async (
|
|
167
158
|
}
|
168
159
|
}
|
169
160
|
|
170
|
-
const createInitialSidebar = async (
|
161
|
+
const createInitialSidebar = async (
|
162
|
+
slugs: string[],
|
163
|
+
initialLanguage = "en"
|
164
|
+
) => {
|
165
|
+
const language = initialLanguage === "en" ? "us" : initialLanguage
|
166
|
+
|
171
167
|
const sidebar: Record<string, Record<string, string>> = {}
|
172
168
|
for (const slug of slugs) {
|
173
169
|
sidebar[slug] = {
|
174
|
-
|
170
|
+
[language]: slug,
|
175
171
|
}
|
176
172
|
}
|
177
173
|
|
178
174
|
return sidebar
|
179
175
|
}
|
180
176
|
|
177
|
+
const uploadInitialReadme = async (
|
178
|
+
bucket: Bucket,
|
179
|
+
exSlug: string,
|
180
|
+
targetDir: string,
|
181
|
+
packageContext: FormState
|
182
|
+
) => {
|
183
|
+
const isGeneratingText = `
|
184
|
+
\`\`\`loader slug="${exSlug}"
|
185
|
+
:rigo
|
186
|
+
\`\`\`
|
187
|
+
`
|
188
|
+
const readmeFilename = `README.${
|
189
|
+
packageContext.language && packageContext.language !== "en" ?
|
190
|
+
`${packageContext.language}.` :
|
191
|
+
""
|
192
|
+
}md`
|
193
|
+
|
194
|
+
await uploadFileToBucket(
|
195
|
+
bucket,
|
196
|
+
isGeneratingText,
|
197
|
+
`${targetDir}/${readmeFilename}`
|
198
|
+
)
|
199
|
+
}
|
200
|
+
|
201
|
+
const cleanFormState = (formState: FormState) => {
|
202
|
+
// keysToDelete: description, technologies, purpose
|
203
|
+
const {
|
204
|
+
description,
|
205
|
+
technologies,
|
206
|
+
purpose,
|
207
|
+
hasContentIndex,
|
208
|
+
duration,
|
209
|
+
isCompleted,
|
210
|
+
variables,
|
211
|
+
currentStep,
|
212
|
+
language,
|
213
|
+
...rest
|
214
|
+
} = formState
|
215
|
+
return rest
|
216
|
+
}
|
217
|
+
|
218
|
+
async function startExerciseGeneration(
|
219
|
+
bucket: Bucket,
|
220
|
+
rigoToken: string,
|
221
|
+
steps: Lesson[],
|
222
|
+
packageContext: FormState,
|
223
|
+
exercise: Lesson,
|
224
|
+
tutorialDir: string,
|
225
|
+
courseSlug: string,
|
226
|
+
purposeSlug: string,
|
227
|
+
lastLesson = ""
|
228
|
+
): Promise<void> {
|
229
|
+
const exercisesDir = `${tutorialDir}/exercises`
|
230
|
+
|
231
|
+
const exSlug = slugify(exercise.id + "-" + exercise.title)
|
232
|
+
console.log("exSlug", exSlug)
|
233
|
+
|
234
|
+
const readmeFilename = `README.${
|
235
|
+
packageContext.language && packageContext.language !== "en" ?
|
236
|
+
`${packageContext.language}.` :
|
237
|
+
""
|
238
|
+
}md`
|
239
|
+
|
240
|
+
const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`
|
241
|
+
|
242
|
+
const res = await readmeCreator(
|
243
|
+
rigoToken,
|
244
|
+
{
|
245
|
+
title: `${exercise.id} - ${exercise.title}`,
|
246
|
+
output_lang: packageContext.language || "en",
|
247
|
+
list_of_exercises: JSON.stringify(
|
248
|
+
steps.map(step => step.id + "-" + step.title)
|
249
|
+
),
|
250
|
+
tutorial_description: JSON.stringify(cleanFormState(packageContext)),
|
251
|
+
lesson_description: exercise.description,
|
252
|
+
kind: exercise.type.toLowerCase(),
|
253
|
+
last_lesson: lastLesson,
|
254
|
+
},
|
255
|
+
purposeSlug,
|
256
|
+
webhookUrl
|
257
|
+
)
|
258
|
+
// console.log("res processing in background", res)
|
259
|
+
}
|
260
|
+
|
181
261
|
export async function processExercise(
|
182
262
|
bucket: Bucket,
|
183
263
|
rigoToken: string,
|
184
264
|
steps: Lesson[],
|
185
265
|
packageContext: FormState,
|
186
266
|
exercise: Lesson,
|
187
|
-
|
267
|
+
tutorialDir: string,
|
188
268
|
courseSlug: string,
|
189
|
-
purposeSlug: string
|
269
|
+
purposeSlug: string,
|
270
|
+
lastLesson = ""
|
190
271
|
): Promise<string> {
|
272
|
+
const exercisesDir = `${tutorialDir}/exercises`
|
191
273
|
// const tid = toast.loading("Generating lesson...")
|
192
274
|
const exSlug = slugify(exercise.id + "-" + exercise.title)
|
193
275
|
console.log("exSlug", exSlug)
|
@@ -201,99 +283,102 @@ export async function processExercise(
|
|
201
283
|
|
202
284
|
console.log("✍🏻 Generating lesson", exercise.id, exercise.title)
|
203
285
|
|
204
|
-
const isGeneratingText = `
|
205
|
-
\`\`\`loader slug="${exSlug}"
|
206
|
-
:rigo
|
207
|
-
\`\`\`
|
208
|
-
`
|
209
|
-
|
210
|
-
await uploadFileToBucket(
|
211
|
-
bucket,
|
212
|
-
isGeneratingText,
|
213
|
-
`${targetDir}/${readmeFilename}`
|
214
|
-
)
|
215
|
-
|
216
286
|
const readme = await readmeCreator(
|
217
287
|
rigoToken,
|
218
288
|
{
|
219
289
|
title: `${exercise.id} - ${exercise.title}`,
|
220
290
|
output_lang: packageContext.language || "en",
|
221
|
-
list_of_exercises: JSON.stringify(
|
222
|
-
|
291
|
+
list_of_exercises: JSON.stringify(
|
292
|
+
steps.map(step => step.id + "-" + step.title)
|
293
|
+
),
|
294
|
+
tutorial_description: JSON.stringify(cleanFormState(packageContext)),
|
223
295
|
lesson_description: exercise.description,
|
224
296
|
kind: exercise.type.toLowerCase(),
|
297
|
+
last_lesson: lastLesson,
|
225
298
|
},
|
226
299
|
purposeSlug
|
227
300
|
)
|
228
301
|
|
229
302
|
const duration = exercise.duration
|
230
|
-
let attempts = 0
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
) {
|
237
|
-
emitToCourse(courseSlug, "course-creation", {
|
238
|
-
lesson: exSlug,
|
239
|
-
status: "generating",
|
240
|
-
log: `🔄 The lesson ${exercise.id} - ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
|
241
|
-
})
|
242
|
-
|
243
|
-
// eslint-disable-next-line
|
244
|
-
const reducedReadme = await makeReadmeReadable(
|
245
|
-
rigoToken,
|
246
|
-
{
|
247
|
-
lesson: readability.body,
|
248
|
-
number_of_words: readability.minutes.toString(),
|
249
|
-
expected_number_words: PARAMS.max_words.toString(),
|
250
|
-
fkgl_results: JSON.stringify(readability.fkglResult),
|
251
|
-
expected_grade_level: PARAMS.expected_grade_level,
|
252
|
-
},
|
253
|
-
purposeSlug
|
254
|
-
)
|
255
|
-
|
256
|
-
if (!reducedReadme) break
|
257
|
-
|
258
|
-
readability = checkReadability(
|
259
|
-
reducedReadme.parsed.content,
|
260
|
-
PARAMS.max_words,
|
261
|
-
duration || 2
|
262
|
-
)
|
303
|
+
// let attempts = 0
|
304
|
+
const readability = checkReadability(
|
305
|
+
readme.parsed.content,
|
306
|
+
PARAMS.max_words,
|
307
|
+
duration || 3
|
308
|
+
)
|
263
309
|
|
264
|
-
|
265
|
-
|
310
|
+
emitToCourse(courseSlug, "course-creation", {
|
311
|
+
lesson: exSlug,
|
312
|
+
status: "generating",
|
313
|
+
log: `🔄 The lesson ${exercise.id} - ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
|
314
|
+
})
|
315
|
+
// while (
|
316
|
+
// readability.fkglResult.fkgl > PARAMS.max_fkgl &&
|
317
|
+
// attempts < PARAMS.max_rewrite_attempts
|
318
|
+
// ) {
|
319
|
+
|
320
|
+
// // eslint-disable-next-line
|
321
|
+
// const reducedReadme = await makeReadmeReadable(
|
322
|
+
// rigoToken,
|
323
|
+
// {
|
324
|
+
// lesson: readability.body,
|
325
|
+
// number_of_words: readability.minutes.toString(),
|
326
|
+
// expected_number_words: PARAMS.max_words.toString(),
|
327
|
+
// fkgl_results: JSON.stringify(readability.fkglResult),
|
328
|
+
// expected_grade_level: PARAMS.expected_grade_level,
|
329
|
+
// },
|
330
|
+
// purposeSlug
|
331
|
+
// )
|
332
|
+
|
333
|
+
// if (!reducedReadme) break
|
334
|
+
|
335
|
+
// readability = checkReadability(
|
336
|
+
// reducedReadme.parsed.content,
|
337
|
+
// PARAMS.max_words,
|
338
|
+
// duration || 3
|
339
|
+
// )
|
340
|
+
|
341
|
+
// attempts++
|
342
|
+
// }
|
266
343
|
|
267
344
|
await uploadFileToBucket(
|
268
345
|
bucket,
|
269
346
|
readability.newMarkdown,
|
270
347
|
`${targetDir}/${readmeFilename}`
|
271
348
|
)
|
272
|
-
emitToCourse(courseSlug, "course-creation", {
|
273
|
-
lesson: exSlug,
|
274
|
-
status: "done",
|
275
|
-
log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully!`,
|
276
|
-
})
|
277
349
|
|
278
|
-
if (
|
350
|
+
if (
|
351
|
+
exercise.type.toLowerCase() === "code" &&
|
352
|
+
readme.parsed.codefile_content
|
353
|
+
) {
|
279
354
|
console.log("🔍 Creating code file for", exercise.title)
|
280
355
|
|
281
|
-
const codeFile = await createCodeFile(rigoToken, {
|
282
|
-
readme: readability.newMarkdown,
|
283
|
-
tutorial_info: JSON.stringify(packageContext),
|
284
|
-
})
|
285
356
|
await uploadFileToBucket(
|
286
357
|
bucket,
|
287
|
-
|
288
|
-
`${targetDir}
|
358
|
+
readme.parsed.codefile_content,
|
359
|
+
`${targetDir}/${readme.parsed.codefile_name.toLowerCase().trim()}`
|
289
360
|
)
|
361
|
+
}
|
362
|
+
|
363
|
+
const imagesArray: any[] = extractImagesFromMarkdown(readability.newMarkdown)
|
364
|
+
|
365
|
+
if (imagesArray.length > 0) {
|
290
366
|
emitToCourse(courseSlug, "course-creation", {
|
291
367
|
lesson: exSlug,
|
292
368
|
status: "done",
|
293
|
-
log:
|
369
|
+
log: `🔄 Generating images for ${exercise.title}`,
|
294
370
|
})
|
371
|
+
for (const image of imagesArray) {
|
372
|
+
// eslint-disable-next-line no-await-in-loop
|
373
|
+
await processImage(bucket, tutorialDir, image.url, image.alt, rigoToken)
|
374
|
+
}
|
295
375
|
}
|
296
376
|
|
377
|
+
emitToCourse(courseSlug, "course-creation", {
|
378
|
+
lesson: exSlug,
|
379
|
+
status: "done",
|
380
|
+
log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully!`,
|
381
|
+
})
|
297
382
|
return readability.newMarkdown
|
298
383
|
}
|
299
384
|
|
@@ -352,6 +437,15 @@ export default class ServeCommand extends SessionCommand {
|
|
352
437
|
process.env.GCP_BUCKET_NAME || "learnpack-packages"
|
353
438
|
)
|
354
439
|
|
440
|
+
const host = process.env.HOST
|
441
|
+
|
442
|
+
if (!host) {
|
443
|
+
console.log("HOST is not set")
|
444
|
+
process.exit(1)
|
445
|
+
} else {
|
446
|
+
console.log("HOST is set to", host)
|
447
|
+
}
|
448
|
+
|
355
449
|
// async function listFilesWithPrefix(prefix: string) {
|
356
450
|
// const [files] = await bucket.getFiles({ prefix })
|
357
451
|
// return files
|
@@ -555,6 +649,154 @@ export default class ServeCommand extends SessionCommand {
|
|
555
649
|
res.json({ id, status: "SUCCESS" })
|
556
650
|
})
|
557
651
|
|
652
|
+
app.post(
|
653
|
+
"/webhooks/:courseSlug/exercise-processor/:lessonID/:rigoToken",
|
654
|
+
async (req, res) => {
|
655
|
+
// console.log("Receiving a webhook to exercise processor")
|
656
|
+
const { courseSlug, lessonID, rigoToken } = req.params
|
657
|
+
const readme = req.body
|
658
|
+
|
659
|
+
const syllabus = await bucket.file(
|
660
|
+
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
661
|
+
)
|
662
|
+
const [content] = await syllabus.download()
|
663
|
+
const syllabusJson: Syllabus = JSON.parse(content.toString())
|
664
|
+
const exerciseIndex = syllabusJson.lessons.findIndex(
|
665
|
+
lesson => lesson.id === lessonID
|
666
|
+
)
|
667
|
+
if (exerciseIndex === -1) {
|
668
|
+
console.log(
|
669
|
+
"Exercise not found receiving webhook, this should not happen",
|
670
|
+
lessonID
|
671
|
+
)
|
672
|
+
return res.json({ status: "ERROR", error: "Exercise not found" })
|
673
|
+
}
|
674
|
+
|
675
|
+
const exercise = syllabusJson.lessons[exerciseIndex]
|
676
|
+
|
677
|
+
const nextExercise = syllabusJson.lessons[exerciseIndex + 1] || null
|
678
|
+
|
679
|
+
if (!exercise) {
|
680
|
+
console.log(
|
681
|
+
"Exercise not found receiving webhook, this should not happen",
|
682
|
+
lessonID
|
683
|
+
)
|
684
|
+
return res.json({ status: "SUCCESS" })
|
685
|
+
}
|
686
|
+
|
687
|
+
const exSlug = slugify(exercise.id + "-" + exercise.title)
|
688
|
+
|
689
|
+
const readability = checkReadability(
|
690
|
+
readme.parsed.content,
|
691
|
+
PARAMS.max_words,
|
692
|
+
3
|
693
|
+
)
|
694
|
+
|
695
|
+
emitToCourse(courseSlug, "course-creation", {
|
696
|
+
lesson: exSlug,
|
697
|
+
status: "generating",
|
698
|
+
log: `🔄 The lesson ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
|
699
|
+
})
|
700
|
+
|
701
|
+
const exercisesDir = `courses/${courseSlug}/exercises`
|
702
|
+
const targetDir = `${exercisesDir}/${exSlug}`
|
703
|
+
|
704
|
+
const readmeFilename = `README.${
|
705
|
+
readme.parsed.language_code && readme.parsed.language_code !== "en" ?
|
706
|
+
`${readme.parsed.language_code}.` :
|
707
|
+
""
|
708
|
+
}md`
|
709
|
+
|
710
|
+
await uploadFileToBucket(
|
711
|
+
bucket,
|
712
|
+
readability.newMarkdown,
|
713
|
+
`${targetDir}/${readmeFilename}`
|
714
|
+
)
|
715
|
+
|
716
|
+
if (
|
717
|
+
exercise.type.toLowerCase() === "code" &&
|
718
|
+
readme.parsed.codefile_content
|
719
|
+
) {
|
720
|
+
emitToCourse(courseSlug, "course-creation", {
|
721
|
+
lesson: exSlug,
|
722
|
+
status: "generating",
|
723
|
+
log: `🔄 Creating code file for ${exercise.title}`,
|
724
|
+
})
|
725
|
+
|
726
|
+
await uploadFileToBucket(
|
727
|
+
bucket,
|
728
|
+
readme.parsed.codefile_content,
|
729
|
+
`${targetDir}/${readme.parsed.codefile_name.toLowerCase().trim()}`
|
730
|
+
)
|
731
|
+
}
|
732
|
+
|
733
|
+
if (nextExercise) {
|
734
|
+
startExerciseGeneration(
|
735
|
+
bucket,
|
736
|
+
rigoToken,
|
737
|
+
syllabusJson.lessons,
|
738
|
+
syllabusJson.courseInfo,
|
739
|
+
nextExercise,
|
740
|
+
`courses/${courseSlug}`,
|
741
|
+
courseSlug,
|
742
|
+
syllabusJson.courseInfo.purpose,
|
743
|
+
readme.parsed.content
|
744
|
+
)
|
745
|
+
}
|
746
|
+
|
747
|
+
const imagesArray: any[] = extractImagesFromMarkdown(
|
748
|
+
readability.newMarkdown
|
749
|
+
)
|
750
|
+
|
751
|
+
if (imagesArray.length > 0) {
|
752
|
+
console.log(
|
753
|
+
"This course requires images and I don't have the token :)"
|
754
|
+
)
|
755
|
+
|
756
|
+
emitToCourse(courseSlug, "course-creation", {
|
757
|
+
lesson: exSlug,
|
758
|
+
status: "pending",
|
759
|
+
log: `🔄 Generating images for ${exercise.title}`,
|
760
|
+
})
|
761
|
+
for (const image of imagesArray) {
|
762
|
+
// eslint-disable-next-line no-await-in-loop
|
763
|
+
await processImage(
|
764
|
+
bucket,
|
765
|
+
`courses/${courseSlug}`,
|
766
|
+
image.url,
|
767
|
+
image.alt,
|
768
|
+
rigoToken
|
769
|
+
)
|
770
|
+
}
|
771
|
+
}
|
772
|
+
|
773
|
+
emitToCourse(courseSlug, "course-creation", {
|
774
|
+
lesson: exSlug,
|
775
|
+
status: "done",
|
776
|
+
log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully!`,
|
777
|
+
})
|
778
|
+
|
779
|
+
const newSyllabus = {
|
780
|
+
...syllabusJson,
|
781
|
+
lessons: syllabusJson.lessons.map(lesson => {
|
782
|
+
if (lesson.id === exercise.id) {
|
783
|
+
return { ...lesson, generated: true }
|
784
|
+
}
|
785
|
+
|
786
|
+
return lesson
|
787
|
+
}),
|
788
|
+
}
|
789
|
+
|
790
|
+
await uploadFileToBucket(
|
791
|
+
bucket,
|
792
|
+
JSON.stringify(newSyllabus),
|
793
|
+
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
794
|
+
)
|
795
|
+
|
796
|
+
res.json({ status: "SUCCESS" })
|
797
|
+
}
|
798
|
+
)
|
799
|
+
|
558
800
|
app.get("/check-preview-image/:slug", async (req, res) => {
|
559
801
|
const { slug } = req.params
|
560
802
|
const file = bucket.file(`courses/${slug}/preview.png`)
|
@@ -931,76 +1173,95 @@ export default class ServeCommand extends SessionCommand {
|
|
931
1173
|
return res.status(400).json({ error: "Missing tokens" })
|
932
1174
|
}
|
933
1175
|
|
934
|
-
const
|
1176
|
+
const courseSlug = slugify(syllabus.courseInfo.title)
|
935
1177
|
|
936
|
-
const tutorialDir = `courses/${
|
1178
|
+
const tutorialDir = `courses/${courseSlug}`
|
937
1179
|
const learnJson = createLearnJson(syllabus.courseInfo)
|
938
1180
|
|
939
1181
|
try {
|
940
|
-
await api.createRigoPackage(rigoToken,
|
1182
|
+
await api.createRigoPackage(rigoToken, courseSlug, learnJson)
|
941
1183
|
} catch (error) {
|
942
1184
|
console.error("Failed to create Rigo package:", error)
|
943
1185
|
return res.status(400).json({ error: "Failed to create Rigo package" })
|
944
1186
|
}
|
945
1187
|
|
946
|
-
|
947
|
-
|
948
|
-
JSON.stringify(learnJson),
|
949
|
-
`${tutorialDir}/learn.json`
|
950
|
-
)
|
951
|
-
console.log("🔄 Learn.json uploaded to bucket to", tutorialDir)
|
952
|
-
|
953
|
-
console.log(
|
954
|
-
"🔄 Processing lessons with purpose",
|
955
|
-
syllabus.courseInfo.purpose
|
956
|
-
)
|
957
|
-
const lessonsPromises = syllabus.lessons.map(lesson =>
|
958
|
-
processExercise(
|
1188
|
+
try {
|
1189
|
+
await uploadFileToBucket(
|
959
1190
|
bucket,
|
960
|
-
|
961
|
-
|
962
|
-
syllabus.courseInfo,
|
963
|
-
lesson,
|
964
|
-
tutorialDir + "/exercises",
|
965
|
-
slugify(syllabus.courseInfo.title),
|
966
|
-
syllabus.courseInfo.purpose
|
1191
|
+
JSON.stringify(learnJson),
|
1192
|
+
`${tutorialDir}/learn.json`
|
967
1193
|
)
|
968
|
-
)
|
969
|
-
|
1194
|
+
} catch (error) {
|
1195
|
+
console.error("Failed to upload learn.json:", error)
|
1196
|
+
return res.status(400).json({ error: "Failed to upload learn.json" })
|
1197
|
+
}
|
970
1198
|
|
971
|
-
|
1199
|
+
console.log(
|
1200
|
+
"🔄 Learn.json and initialSyllabus.json uploaded to bucket to",
|
1201
|
+
tutorialDir
|
1202
|
+
)
|
972
1203
|
|
973
|
-
for (
|
974
|
-
|
975
|
-
|
1204
|
+
for (let i = 0; i < syllabus.lessons.length; i++) {
|
1205
|
+
const lesson = syllabus.lessons[i]
|
1206
|
+
const exSlug = slugify(lesson.id + "-" + lesson.title)
|
976
1207
|
|
977
|
-
|
1208
|
+
const targetDir = `${tutorialDir}/exercises/${exSlug}`
|
978
1209
|
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
)
|
988
|
-
}
|
989
|
-
)
|
990
|
-
await Promise.all(imagePromises)
|
1210
|
+
// eslint-disable-next-line no-await-in-loop
|
1211
|
+
await uploadInitialReadme(
|
1212
|
+
bucket,
|
1213
|
+
exSlug,
|
1214
|
+
targetDir,
|
1215
|
+
syllabus.courseInfo
|
1216
|
+
)
|
1217
|
+
}
|
991
1218
|
|
992
1219
|
const sidebar = await createInitialSidebar(
|
993
1220
|
syllabus.lessons.map(lesson =>
|
994
1221
|
slugify(lesson.id + "-" + lesson.title)
|
995
|
-
)
|
1222
|
+
),
|
1223
|
+
syllabus.courseInfo.language
|
996
1224
|
)
|
1225
|
+
|
1226
|
+
const initialSyllabus = {
|
1227
|
+
...syllabus,
|
1228
|
+
lessons: syllabus.lessons.map((lesson, index) => {
|
1229
|
+
if (index < 1) {
|
1230
|
+
return { ...lesson, generated: true }
|
1231
|
+
}
|
1232
|
+
|
1233
|
+
return {
|
1234
|
+
...lesson,
|
1235
|
+
generated: false,
|
1236
|
+
}
|
1237
|
+
}),
|
1238
|
+
}
|
1239
|
+
await uploadFileToBucket(
|
1240
|
+
bucket,
|
1241
|
+
JSON.stringify(initialSyllabus),
|
1242
|
+
`${tutorialDir}/.learn/initialSyllabus.json`
|
1243
|
+
)
|
1244
|
+
|
997
1245
|
await uploadFileToBucket(
|
998
1246
|
bucket,
|
999
1247
|
JSON.stringify(sidebar),
|
1000
1248
|
`${tutorialDir}/.learn/sidebar.json`
|
1001
1249
|
)
|
1250
|
+
const firstLesson = syllabus.lessons[0]
|
1002
1251
|
|
1003
|
-
|
1252
|
+
const lastResult = "Nothing"
|
1253
|
+
|
1254
|
+
startExerciseGeneration(
|
1255
|
+
bucket,
|
1256
|
+
rigoToken,
|
1257
|
+
syllabus.lessons,
|
1258
|
+
syllabus.courseInfo,
|
1259
|
+
firstLesson,
|
1260
|
+
tutorialDir,
|
1261
|
+
courseSlug,
|
1262
|
+
syllabus.courseInfo.purpose,
|
1263
|
+
lastResult
|
1264
|
+
)
|
1004
1265
|
|
1005
1266
|
return res.json({
|
1006
1267
|
message: "Course created",
|
@@ -1008,6 +1269,71 @@ export default class ServeCommand extends SessionCommand {
|
|
1008
1269
|
})
|
1009
1270
|
})
|
1010
1271
|
|
1272
|
+
app.get("/courses/:courseSlug/syllabus", async (req, res) => {
|
1273
|
+
try {
|
1274
|
+
console.log("GET /courses/:courseSlug/syllabus")
|
1275
|
+
const { courseSlug } = req.params
|
1276
|
+
const syllabus = await bucket.file(
|
1277
|
+
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
1278
|
+
)
|
1279
|
+
const [content] = await syllabus.download()
|
1280
|
+
const syllabusJson: Syllabus = JSON.parse(content.toString())
|
1281
|
+
res.json(syllabusJson)
|
1282
|
+
} catch (error) {
|
1283
|
+
console.error("Error getting syllabus:", error)
|
1284
|
+
return res.status(500).json({ error: "Error getting syllabus" })
|
1285
|
+
}
|
1286
|
+
})
|
1287
|
+
|
1288
|
+
app.post("/actions/continue-course/:courseSlug", async (req, res) => {
|
1289
|
+
console.log("POST /actions/continue-course/:courseSlug")
|
1290
|
+
const { courseSlug } = req.params
|
1291
|
+
|
1292
|
+
const { feedback }: { feedback: string } = req.body
|
1293
|
+
|
1294
|
+
const rigoToken = req.header("x-rigo-token")
|
1295
|
+
const bcToken = req.header("x-breathecode-token")
|
1296
|
+
if (!rigoToken || !bcToken) {
|
1297
|
+
return res.status(400).json({ error: "Missing tokens" })
|
1298
|
+
}
|
1299
|
+
|
1300
|
+
const syllabus = await bucket.file(
|
1301
|
+
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
1302
|
+
)
|
1303
|
+
const [content] = await syllabus.download()
|
1304
|
+
const syllabusJson: Syllabus = JSON.parse(content.toString())
|
1305
|
+
const notGeneratedLessons = syllabusJson.lessons.filter(
|
1306
|
+
lesson => !lesson.generated
|
1307
|
+
)
|
1308
|
+
|
1309
|
+
const lastGeneratedLesson = findLast(
|
1310
|
+
syllabusJson.lessons,
|
1311
|
+
lesson => lesson.generated ?? false
|
1312
|
+
)
|
1313
|
+
|
1314
|
+
console.log("ABout to generate", notGeneratedLessons.length, "lessons")
|
1315
|
+
|
1316
|
+
const firstLessonToGenerate = notGeneratedLessons[0]
|
1317
|
+
|
1318
|
+
await startExerciseGeneration(
|
1319
|
+
bucket,
|
1320
|
+
rigoToken,
|
1321
|
+
syllabusJson.lessons,
|
1322
|
+
syllabusJson.courseInfo,
|
1323
|
+
firstLessonToGenerate,
|
1324
|
+
`courses/${courseSlug}`,
|
1325
|
+
courseSlug,
|
1326
|
+
syllabusJson.courseInfo.purpose,
|
1327
|
+
JSON.stringify(lastGeneratedLesson) +
|
1328
|
+
`\n\nThe user provided this feedback in relation to the course: ${feedback}`
|
1329
|
+
)
|
1330
|
+
|
1331
|
+
return res.json({
|
1332
|
+
message: "Course continued",
|
1333
|
+
slug: courseSlug,
|
1334
|
+
})
|
1335
|
+
})
|
1336
|
+
|
1011
1337
|
app.get(
|
1012
1338
|
"/courses/:courseSlug/exercises/:exerciseSlug/",
|
1013
1339
|
async (req, res) => {
|
@@ -1069,11 +1395,7 @@ export default class ServeCommand extends SessionCommand {
|
|
1069
1395
|
slug
|
1070
1396
|
)
|
1071
1397
|
|
1072
|
-
//
|
1073
|
-
|
1074
|
-
const fixedSlug = fixSlugLength(slug)
|
1075
|
-
|
1076
|
-
// console.log(fixedSlug, "FIXED SLUG")
|
1398
|
+
// const fixedSlug = fixSlugLength(slug)
|
1077
1399
|
|
1078
1400
|
const prefix = `courses/${slug}/`
|
1079
1401
|
const tmpRoot = path.join(os.tmpdir(), `learnpack-${slug}`)
|
@@ -1106,7 +1428,7 @@ export default class ServeCommand extends SessionCommand {
|
|
1106
1428
|
.replace(/{{description}}/g, config.description.us)
|
1107
1429
|
.replace(
|
1108
1430
|
/{{preview}}/g,
|
1109
|
-
fixPreviewUrl(
|
1431
|
+
fixPreviewUrl(slug, "") ||
|
1110
1432
|
"https://raw.githubusercontent.com/learnpack/ide/master/public/learnpack.svg"
|
1111
1433
|
)
|
1112
1434
|
.replace(/{{slug}}/g, slug)
|
@@ -1136,9 +1458,9 @@ export default class ServeCommand extends SessionCommand {
|
|
1136
1458
|
|
1137
1459
|
// 8) Crear el config.json en buildRoot con lo que retorna buildConfig
|
1138
1460
|
const fullConfig = { config, exercises }
|
1139
|
-
fullConfig.config.slug =
|
1461
|
+
fullConfig.config.slug = slug
|
1140
1462
|
fullConfig.config.preview =
|
1141
|
-
fixPreviewUrl(
|
1463
|
+
fixPreviewUrl(slug, "") ||
|
1142
1464
|
"https://raw.githubusercontent.com/learnpack/ide/master/public/learnpack.svg"
|
1143
1465
|
|
1144
1466
|
fs.writeFileSync(
|
@@ -1148,7 +1470,7 @@ export default class ServeCommand extends SessionCommand {
|
|
1148
1470
|
)
|
1149
1471
|
|
1150
1472
|
// 9) Empaquetar en ZIP (solo contenido de buildRoot)
|
1151
|
-
const zipName = `${
|
1473
|
+
const zipName = `${slug}.zip`
|
1152
1474
|
const zipPath = path.join(tmpRoot, zipName)
|
1153
1475
|
const output = fs.createWriteStream(zipPath)
|
1154
1476
|
const archive = archiver("zip", { zlib: { level: 9 } })
|