@learnpack/learnpack 5.0.204 → 5.0.209

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.
@@ -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
- const validateLanguage = (language: string) => {
54
- if (!language || language.length !== 2) {
55
- return "en"
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 language
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 (slugs: string[]) => {
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
- us: slug,
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
- exercisesDir: string,
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(steps),
222
- tutorial_description: JSON.stringify(packageContext),
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
- let readability = checkReadability(readme.parsed.content, 250, duration || 3)
232
-
233
- while (
234
- readability.fkglResult.fkgl > PARAMS.max_fkgl &&
235
- attempts < PARAMS.max_rewrite_attempts
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
- attempts++
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 (exercise.type.toLowerCase() === "code") {
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
- codeFile.parsed.content,
288
- `${targetDir}/index.${codeFile.parsed.extension.replace(".", "")}`
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: `✅ The code file for ${exercise.title} has been generated successfully!`,
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 slug = slugify(syllabus.courseInfo.title)
1176
+ const courseSlug = slugify(syllabus.courseInfo.title)
935
1177
 
936
- const tutorialDir = `courses/${slug}`
1178
+ const tutorialDir = `courses/${courseSlug}`
937
1179
  const learnJson = createLearnJson(syllabus.courseInfo)
938
1180
 
939
1181
  try {
940
- await api.createRigoPackage(rigoToken, slug, learnJson)
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
- await uploadFileToBucket(
947
- bucket,
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
- rigoToken,
961
- syllabus.lessons,
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
- const readmeContents = await Promise.all(lessonsPromises)
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
- let imagesArray: any[] = []
1199
+ console.log(
1200
+ "🔄 Learn.json and initialSyllabus.json uploaded to bucket to",
1201
+ tutorialDir
1202
+ )
972
1203
 
973
- for (const content of readmeContents) {
974
- imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)]
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
- console.log("📷 Generating images...")
1208
+ const targetDir = `${tutorialDir}/exercises/${exSlug}`
978
1209
 
979
- const imagePromises = imagesArray.map(
980
- async (image: { alt: string; url: string }) => {
981
- return processImage(
982
- bucket,
983
- tutorialDir,
984
- image.url,
985
- image.alt,
986
- rigoToken
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
- console.log("SIDEBAR UPLOADED", sidebar)
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
- // console.log(slug, "SLUG")
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(fixedSlug, "") ||
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 = fixedSlug
1461
+ fullConfig.config.slug = slug
1140
1462
  fullConfig.config.preview =
1141
- fixPreviewUrl(fixedSlug, "") ||
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 = `${fixedSlug}.zip`
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 } })