@learnpack/learnpack 5.0.290 → 5.0.292
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/lib/commands/init.js +42 -42
- package/lib/commands/serve.js +505 -36
- package/lib/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
- package/lib/creatorDist/assets/{index-7zTdUX04.js → index-DOEfLGDQ.js} +1426 -1374
- package/lib/creatorDist/index.html +2 -2
- package/lib/models/creator.d.ts +4 -0
- package/lib/utils/api.d.ts +1 -1
- package/lib/utils/api.js +2 -1
- package/lib/utils/configBuilder.js +20 -1
- package/lib/utils/rigoActions.d.ts +14 -0
- package/lib/utils/rigoActions.js +43 -1
- package/package.json +1 -1
- package/src/commands/init.ts +655 -650
- package/src/commands/serve.ts +865 -106
- package/src/creator/src/App.tsx +13 -14
- package/src/creator/src/components/FileUploader.tsx +2 -68
- package/src/creator/src/components/LessonItem.tsx +47 -8
- package/src/creator/src/components/Login.tsx +0 -6
- package/src/creator/src/components/syllabus/ContentIndex.tsx +11 -0
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -1
- package/src/creator/src/index.css +227 -217
- package/src/creator/src/locales/en.json +2 -2
- package/src/creator/src/locales/es.json +2 -2
- package/src/creator/src/utils/lib.ts +470 -468
- package/src/creator/src/utils/rigo.ts +85 -85
- package/src/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
- package/src/creatorDist/assets/{index-7zTdUX04.js → index-DOEfLGDQ.js} +1426 -1374
- package/src/creatorDist/index.html +2 -2
- package/src/models/creator.ts +2 -0
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +420 -418
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +2 -1
- package/src/utils/configBuilder.ts +100 -82
- package/src/utils/rigoActions.ts +73 -0
package/src/commands/serve.ts
CHANGED
@@ -34,6 +34,8 @@ import {
|
|
34
34
|
createStructuredPreviewReadme,
|
35
35
|
translateCourseMetadata,
|
36
36
|
getLanguageCodes,
|
37
|
+
initialContentGenerator,
|
38
|
+
addInteractivity,
|
37
39
|
} from "../utils/rigoActions"
|
38
40
|
import * as dotenv from "dotenv"
|
39
41
|
|
@@ -119,6 +121,38 @@ const PARAMS = {
|
|
119
121
|
max_title_length: 50,
|
120
122
|
}
|
121
123
|
|
124
|
+
async function fetchComponentsYml(): Promise<string> {
|
125
|
+
try {
|
126
|
+
const [assessmentResponse, explanatoryResponse] = await Promise.all([
|
127
|
+
axios.get(
|
128
|
+
"https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/assessment_components.yml"
|
129
|
+
),
|
130
|
+
axios.get(
|
131
|
+
"https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/explanatory_components.yml"
|
132
|
+
),
|
133
|
+
])
|
134
|
+
|
135
|
+
const combinedContent = `
|
136
|
+
# ASSESSMENT COMPONENTS
|
137
|
+
These components are designed for evaluation and knowledge assessment:
|
138
|
+
|
139
|
+
${assessmentResponse.data}
|
140
|
+
|
141
|
+
---
|
142
|
+
|
143
|
+
# EXPLANATORY COMPONENTS
|
144
|
+
These components are designed for explanation and learning support:
|
145
|
+
|
146
|
+
${explanatoryResponse.data}
|
147
|
+
`
|
148
|
+
|
149
|
+
return combinedContent
|
150
|
+
} catch (error) {
|
151
|
+
console.error("Failed to fetch components YAML files:", error)
|
152
|
+
return ""
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
122
156
|
export const processImage = async (
|
123
157
|
url: string,
|
124
158
|
description: string,
|
@@ -193,6 +227,21 @@ const cleanFormState = (formState: FormState) => {
|
|
193
227
|
return rest
|
194
228
|
}
|
195
229
|
|
230
|
+
const cleanFormStateForSyllabus = (formState: FormState) => {
|
231
|
+
return {
|
232
|
+
...formState,
|
233
|
+
description: formState.description,
|
234
|
+
technologies: formState.technologies,
|
235
|
+
purposse: undefined,
|
236
|
+
duration: undefined,
|
237
|
+
hasContentIndex: undefined,
|
238
|
+
variables: undefined,
|
239
|
+
currentStep: undefined,
|
240
|
+
language: undefined,
|
241
|
+
isCompleted: undefined,
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
196
245
|
const createMultiLangAsset = async (
|
197
246
|
bucket: Bucket,
|
198
247
|
rigoToken: string,
|
@@ -211,9 +260,17 @@ const createMultiLangAsset = async (
|
|
211
260
|
const indexReadme = await bucket.file(
|
212
261
|
`courses/${courseSlug}/README${getReadmeExtension(lang)}`
|
213
262
|
)
|
214
|
-
|
215
|
-
|
216
|
-
|
263
|
+
let indexReadmeString = ""
|
264
|
+
try {
|
265
|
+
// eslint-disable-next-line no-await-in-loop
|
266
|
+
const [indexReadmeContent] = await indexReadme.download()
|
267
|
+
indexReadmeString = indexReadmeContent.toString()
|
268
|
+
} catch (error) {
|
269
|
+
console.error("Error downloading index readme", error)
|
270
|
+
// TODO: Trigger generation of the index readme
|
271
|
+
indexReadmeString = ""
|
272
|
+
}
|
273
|
+
|
217
274
|
const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
|
218
275
|
|
219
276
|
// eslint-disable-next-line no-await-in-loop
|
@@ -249,8 +306,6 @@ async function startExerciseGeneration(
|
|
249
306
|
|
250
307
|
const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`
|
251
308
|
|
252
|
-
console.log("WEBHOOK URL", webhookUrl)
|
253
|
-
|
254
309
|
const res = await readmeCreator(
|
255
310
|
rigoToken.trim(),
|
256
311
|
{
|
@@ -272,6 +327,118 @@ async function startExerciseGeneration(
|
|
272
327
|
return res.id
|
273
328
|
}
|
274
329
|
|
330
|
+
const lessonCleaner = (lesson: Lesson) => {
|
331
|
+
return {
|
332
|
+
...lesson,
|
333
|
+
description: lesson.description,
|
334
|
+
duration: undefined,
|
335
|
+
generated: undefined,
|
336
|
+
status: undefined,
|
337
|
+
translations: undefined,
|
338
|
+
uid: undefined,
|
339
|
+
}
|
340
|
+
}
|
341
|
+
|
342
|
+
async function startInitialContentGeneration(
|
343
|
+
rigoToken: string,
|
344
|
+
steps: Lesson[],
|
345
|
+
packageContext: FormState,
|
346
|
+
exercise: Lesson,
|
347
|
+
courseSlug: string,
|
348
|
+
purposeSlug: string,
|
349
|
+
lastLesson = ""
|
350
|
+
): Promise<number> {
|
351
|
+
const exSlug = slugify(exercise.id + "-" + exercise.title)
|
352
|
+
console.log("Starting initial content generation for", exSlug)
|
353
|
+
|
354
|
+
const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.id}/${rigoToken}`
|
355
|
+
|
356
|
+
// Emit notification that initial content generation is starting
|
357
|
+
emitToCourse(courseSlug, "course-creation", {
|
358
|
+
lesson: exSlug,
|
359
|
+
status: "generating",
|
360
|
+
log: `🔄 Starting initial content generation for lesson: ${exercise.title}`,
|
361
|
+
})
|
362
|
+
|
363
|
+
const fullSyllabus = {
|
364
|
+
steps: [steps.map(lessonCleaner)],
|
365
|
+
courseInfo: cleanFormStateForSyllabus(packageContext),
|
366
|
+
purpose: purposeSlug,
|
367
|
+
}
|
368
|
+
|
369
|
+
// Add random 6-digit number to avoid cache issues
|
370
|
+
// eslint-disable-next-line no-mixed-operators
|
371
|
+
const randomCacheEvict = Math.floor(100_000 + Math.random() * 900_000)
|
372
|
+
|
373
|
+
const res = await initialContentGenerator(
|
374
|
+
rigoToken.trim(),
|
375
|
+
{
|
376
|
+
// prev_lesson: lastLesson,
|
377
|
+
output_language: packageContext.language || "en",
|
378
|
+
current_syllabus: JSON.stringify(fullSyllabus),
|
379
|
+
lesson_description:
|
380
|
+
JSON.stringify(lessonCleaner(exercise)) + `-${randomCacheEvict}`,
|
381
|
+
},
|
382
|
+
webhookUrl
|
383
|
+
)
|
384
|
+
|
385
|
+
console.log("INITIAL CONTENT GENERATOR RES", res)
|
386
|
+
return res.id
|
387
|
+
}
|
388
|
+
|
389
|
+
async function startInteractivityGeneration(
|
390
|
+
rigoToken: string,
|
391
|
+
steps: Lesson[],
|
392
|
+
packageContext: FormState,
|
393
|
+
exercise: Lesson,
|
394
|
+
courseSlug: string,
|
395
|
+
purposeSlug: string,
|
396
|
+
bucket: Bucket,
|
397
|
+
lastLesson = ""
|
398
|
+
): Promise<number> {
|
399
|
+
const exSlug = slugify(exercise.id + "-" + exercise.title)
|
400
|
+
console.log("Starting interactivity generation for", exSlug)
|
401
|
+
|
402
|
+
const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/interactivity-processor/${exercise.id}/${rigoToken}`
|
403
|
+
|
404
|
+
// Emit notification that interactivity generation is starting
|
405
|
+
emitToCourse(courseSlug, "course-creation", {
|
406
|
+
lesson: exSlug,
|
407
|
+
status: "generating",
|
408
|
+
log: `🔄 Starting interactivity generation for lesson: ${exercise.title}`,
|
409
|
+
})
|
410
|
+
|
411
|
+
const componentsYml = await fetchComponentsYml()
|
412
|
+
|
413
|
+
// Get current syllabus to include used_components
|
414
|
+
const currentSyllabus = await getSyllabus(courseSlug, bucket)
|
415
|
+
|
416
|
+
const fullSyllabus = {
|
417
|
+
steps: steps.map(lessonCleaner),
|
418
|
+
courseInfo: cleanFormStateForSyllabus(packageContext),
|
419
|
+
used_components: currentSyllabus.used_components || {},
|
420
|
+
}
|
421
|
+
|
422
|
+
// Add random 6-digit number to avoid cache issues
|
423
|
+
// eslint-disable-next-line no-mixed-operators
|
424
|
+
const randomCacheEvict = Math.floor(100_000 + Math.random() * 900_000)
|
425
|
+
|
426
|
+
const res = await addInteractivity(
|
427
|
+
rigoToken.trim(),
|
428
|
+
{
|
429
|
+
components: componentsYml,
|
430
|
+
prev_lesson: lastLesson,
|
431
|
+
initial_lesson: exercise.initialContent + `-${randomCacheEvict}`,
|
432
|
+
output_language: packageContext.language || "en",
|
433
|
+
current_syllabus: JSON.stringify(fullSyllabus),
|
434
|
+
},
|
435
|
+
webhookUrl
|
436
|
+
)
|
437
|
+
|
438
|
+
console.log("INTERACTIVITY GENERATOR RES", res)
|
439
|
+
return res.id
|
440
|
+
}
|
441
|
+
|
275
442
|
async function createInitialReadme(
|
276
443
|
tutorialInfo: string,
|
277
444
|
tutorialSlug: string,
|
@@ -293,6 +460,219 @@ async function createInitialReadme(
|
|
293
460
|
}
|
294
461
|
}
|
295
462
|
|
463
|
+
async function getSyllabus(
|
464
|
+
courseSlug: string,
|
465
|
+
bucket: Bucket
|
466
|
+
): Promise<Syllabus> {
|
467
|
+
const syllabus = await bucket.file(
|
468
|
+
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
469
|
+
)
|
470
|
+
const [content] = await syllabus.download()
|
471
|
+
return JSON.parse(content.toString())
|
472
|
+
}
|
473
|
+
|
474
|
+
async function saveSyllabus(
|
475
|
+
courseSlug: string,
|
476
|
+
syllabus: Syllabus,
|
477
|
+
bucket: Bucket
|
478
|
+
): Promise<void> {
|
479
|
+
await uploadFileToBucket(
|
480
|
+
bucket,
|
481
|
+
JSON.stringify(syllabus),
|
482
|
+
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
483
|
+
)
|
484
|
+
}
|
485
|
+
|
486
|
+
async function updateUsedComponents(
|
487
|
+
courseSlug: string,
|
488
|
+
usedComponents: string[],
|
489
|
+
bucket: Bucket
|
490
|
+
): Promise<void> {
|
491
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
492
|
+
|
493
|
+
// Initialize used_components if undefined
|
494
|
+
if (!syllabus.used_components) {
|
495
|
+
syllabus.used_components = {}
|
496
|
+
}
|
497
|
+
|
498
|
+
// Update component counts
|
499
|
+
for (const componentName of usedComponents) {
|
500
|
+
if (syllabus.used_components[componentName]) {
|
501
|
+
syllabus.used_components[componentName] += 1
|
502
|
+
} else {
|
503
|
+
syllabus.used_components[componentName] = 1
|
504
|
+
}
|
505
|
+
}
|
506
|
+
|
507
|
+
console.log("Updated component usage:", syllabus.used_components)
|
508
|
+
await saveSyllabus(courseSlug, syllabus, bucket)
|
509
|
+
}
|
510
|
+
|
511
|
+
async function updateLessonWithInitialContent(
|
512
|
+
courseSlug: string,
|
513
|
+
lessonID: string,
|
514
|
+
initialResponse: any,
|
515
|
+
bucket: Bucket
|
516
|
+
): Promise<void> {
|
517
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
518
|
+
const lessonIndex = syllabus.lessons.findIndex(
|
519
|
+
lesson => lesson.id === lessonID
|
520
|
+
)
|
521
|
+
|
522
|
+
if (lessonIndex === -1) {
|
523
|
+
console.error(`Lesson ${lessonID} not found in syllabus`)
|
524
|
+
return
|
525
|
+
}
|
526
|
+
|
527
|
+
const lesson = syllabus.lessons[lessonIndex]
|
528
|
+
|
529
|
+
// Update initial content
|
530
|
+
lesson.initialContent = initialResponse.lesson_content
|
531
|
+
|
532
|
+
await saveSyllabus(courseSlug, syllabus, bucket)
|
533
|
+
}
|
534
|
+
|
535
|
+
async function updateLessonStatusToError(
|
536
|
+
courseSlug: string,
|
537
|
+
lessonID: string,
|
538
|
+
bucket: Bucket
|
539
|
+
): Promise<void> {
|
540
|
+
try {
|
541
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
542
|
+
const lessonIndex = syllabus.lessons.findIndex(
|
543
|
+
lesson => lesson.id === lessonID
|
544
|
+
)
|
545
|
+
|
546
|
+
if (lessonIndex === -1) {
|
547
|
+
console.error(
|
548
|
+
`Lesson ${lessonID} not found in syllabus when updating status to error`
|
549
|
+
)
|
550
|
+
return
|
551
|
+
}
|
552
|
+
|
553
|
+
const lesson = syllabus.lessons[lessonIndex]
|
554
|
+
|
555
|
+
// Update lesson status to ERROR
|
556
|
+
lesson.status = "ERROR"
|
557
|
+
|
558
|
+
// Update translations to mark as failed
|
559
|
+
const currentTranslations = lesson.translations || {}
|
560
|
+
const language = syllabus.courseInfo.language || "en"
|
561
|
+
|
562
|
+
if (currentTranslations[language]) {
|
563
|
+
currentTranslations[language].completedAt = Date.now()
|
564
|
+
} else {
|
565
|
+
currentTranslations[language] = {
|
566
|
+
completionId: 0,
|
567
|
+
startedAt: Date.now(),
|
568
|
+
completedAt: Date.now(),
|
569
|
+
}
|
570
|
+
}
|
571
|
+
|
572
|
+
lesson.translations = currentTranslations
|
573
|
+
|
574
|
+
await saveSyllabus(courseSlug, syllabus, bucket)
|
575
|
+
console.log(`Updated lesson ${lessonID} status to ERROR in syllabus`)
|
576
|
+
} catch (error) {
|
577
|
+
console.error(`Error updating lesson ${lessonID} status to ERROR:`, error)
|
578
|
+
}
|
579
|
+
}
|
580
|
+
|
581
|
+
async function continueWithNextLesson(
|
582
|
+
courseSlug: string,
|
583
|
+
currentExerciseIndex: number,
|
584
|
+
rigoToken: string,
|
585
|
+
finalContent: string,
|
586
|
+
bucket: Bucket
|
587
|
+
): Promise<void> {
|
588
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
589
|
+
const nextExercise = syllabus.lessons[currentExerciseIndex + 1] || null
|
590
|
+
|
591
|
+
let nextCompletionId: number | null = null
|
592
|
+
if (
|
593
|
+
nextExercise &&
|
594
|
+
(currentExerciseIndex === 0 ||
|
595
|
+
!(currentExerciseIndex % 3 === 0) ||
|
596
|
+
syllabus.generationMode === "continue-with-all")
|
597
|
+
) {
|
598
|
+
let feedback = ""
|
599
|
+
if (syllabus.feedback) {
|
600
|
+
feedback = `\n\nThe user added the following feedback with relation to the previous generations: ${syllabus.feedback}`
|
601
|
+
}
|
602
|
+
|
603
|
+
nextCompletionId = await startInitialContentGeneration(
|
604
|
+
rigoToken,
|
605
|
+
syllabus.lessons,
|
606
|
+
syllabus.courseInfo,
|
607
|
+
nextExercise,
|
608
|
+
courseSlug,
|
609
|
+
syllabus.courseInfo.purpose,
|
610
|
+
finalContent + "\n\n" + feedback
|
611
|
+
)
|
612
|
+
} else {
|
613
|
+
console.log(
|
614
|
+
"Stopping generation process at",
|
615
|
+
currentExerciseIndex,
|
616
|
+
syllabus.lessons[currentExerciseIndex].title,
|
617
|
+
"because it's a multiple of 3"
|
618
|
+
)
|
619
|
+
}
|
620
|
+
|
621
|
+
// Update syllabus with next lesson status
|
622
|
+
const newSyllabus = {
|
623
|
+
...syllabus,
|
624
|
+
lessons: syllabus.lessons.map((lesson, index) => {
|
625
|
+
if (index === currentExerciseIndex) {
|
626
|
+
const currentTranslations = lesson.translations || {}
|
627
|
+
let currentTranslation =
|
628
|
+
currentTranslations[syllabus.courseInfo.language || "en"]
|
629
|
+
if (currentTranslation) {
|
630
|
+
currentTranslation.completedAt = Date.now()
|
631
|
+
} else {
|
632
|
+
currentTranslation = {
|
633
|
+
completionId: nextCompletionId || 0,
|
634
|
+
startedAt: Date.now(),
|
635
|
+
completedAt: Date.now(),
|
636
|
+
}
|
637
|
+
}
|
638
|
+
|
639
|
+
currentTranslations[syllabus.courseInfo.language || "en"] =
|
640
|
+
currentTranslation
|
641
|
+
return {
|
642
|
+
...lesson,
|
643
|
+
generated: true,
|
644
|
+
status: "DONE" as const,
|
645
|
+
translations: {
|
646
|
+
[syllabus.courseInfo.language || "en"]: {
|
647
|
+
completionId: nextCompletionId || 0,
|
648
|
+
startedAt: currentTranslation.startedAt,
|
649
|
+
completedAt: Date.now(),
|
650
|
+
},
|
651
|
+
},
|
652
|
+
}
|
653
|
+
}
|
654
|
+
|
655
|
+
if (nextExercise && nextExercise.id === lesson.id && nextCompletionId) {
|
656
|
+
return {
|
657
|
+
...lesson,
|
658
|
+
generated: false,
|
659
|
+
status: "GENERATING" as const,
|
660
|
+
translations: {
|
661
|
+
[syllabus.courseInfo.language || "en"]: {
|
662
|
+
completionId: nextCompletionId,
|
663
|
+
startedAt: Date.now(),
|
664
|
+
},
|
665
|
+
},
|
666
|
+
}
|
667
|
+
}
|
668
|
+
|
669
|
+
return { ...lesson }
|
670
|
+
}),
|
671
|
+
}
|
672
|
+
|
673
|
+
await saveSyllabus(courseSlug, newSyllabus, bucket)
|
674
|
+
}
|
675
|
+
|
296
676
|
const fixPreviewUrl = (slug: string, previewUrl: string) => {
|
297
677
|
if (!previewUrl) {
|
298
678
|
return null
|
@@ -627,17 +1007,13 @@ export default class ServeCommand extends SessionCommand {
|
|
627
1007
|
})
|
628
1008
|
}
|
629
1009
|
|
630
|
-
const syllabus = await bucket
|
631
|
-
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
632
|
-
)
|
633
|
-
const [content] = await syllabus.download()
|
634
|
-
const syllabusJson: Syllabus = JSON.parse(content.toString())
|
1010
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
635
1011
|
|
636
|
-
const exercise =
|
1012
|
+
const exercise = syllabus.lessons[parseInt(position)]
|
637
1013
|
|
638
1014
|
// previous exercise
|
639
1015
|
let previousReadme = "---"
|
640
|
-
const previousExercise =
|
1016
|
+
const previousExercise = syllabus.lessons[parseInt(position) - 1]
|
641
1017
|
if (previousExercise) {
|
642
1018
|
// Get the readme of the previous exercise
|
643
1019
|
const exSlug = slugify(
|
@@ -656,43 +1032,37 @@ export default class ServeCommand extends SessionCommand {
|
|
656
1032
|
}
|
657
1033
|
}
|
658
1034
|
|
659
|
-
|
1035
|
+
// Use new two-phase generation workflow
|
1036
|
+
const completionId = await startInitialContentGeneration(
|
660
1037
|
rigoToken,
|
661
|
-
|
662
|
-
|
1038
|
+
syllabus.lessons,
|
1039
|
+
syllabus.courseInfo,
|
663
1040
|
exercise,
|
664
1041
|
courseSlug,
|
665
|
-
|
1042
|
+
syllabus.courseInfo.purpose,
|
666
1043
|
previousReadme +
|
667
1044
|
"\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
|
668
1045
|
feedback
|
669
1046
|
)
|
670
1047
|
|
671
|
-
|
672
|
-
|
673
|
-
[
|
1048
|
+
syllabus.lessons[parseInt(position)].status = "GENERATING"
|
1049
|
+
syllabus.lessons[parseInt(position)].translations = {
|
1050
|
+
[syllabus.courseInfo.language || "en"]: {
|
674
1051
|
completionId,
|
675
1052
|
startedAt: Date.now(),
|
676
1053
|
completedAt: 0,
|
677
1054
|
},
|
678
1055
|
}
|
679
|
-
console.log("Lesson",
|
680
|
-
if (
|
681
|
-
|
682
|
-
typeof syllabusJson.feedback === "string"
|
683
|
-
) {
|
684
|
-
syllabusJson.feedback += "\n\n" + feedback
|
1056
|
+
console.log("Lesson", syllabus.lessons[parseInt(position)])
|
1057
|
+
if (syllabus.feedback && typeof syllabus.feedback === "string") {
|
1058
|
+
syllabus.feedback += "\n\n" + feedback
|
685
1059
|
} else {
|
686
|
-
|
1060
|
+
syllabus.feedback = feedback
|
687
1061
|
}
|
688
1062
|
|
689
|
-
|
1063
|
+
syllabus.generationMode = mode
|
690
1064
|
|
691
|
-
await
|
692
|
-
bucket,
|
693
|
-
JSON.stringify(syllabusJson),
|
694
|
-
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
695
|
-
)
|
1065
|
+
await saveSyllabus(courseSlug, syllabus, bucket)
|
696
1066
|
|
697
1067
|
res.json({ status: "SUCCESS" })
|
698
1068
|
}
|
@@ -927,6 +1297,400 @@ export default class ServeCommand extends SessionCommand {
|
|
927
1297
|
}
|
928
1298
|
)
|
929
1299
|
|
1300
|
+
// Phase 1: Initial content generation webhook
|
1301
|
+
app.post(
|
1302
|
+
"/webhooks/:courseSlug/initial-content-processor/:lessonID/:rigoToken",
|
1303
|
+
async (req, res) => {
|
1304
|
+
const { courseSlug, lessonID, rigoToken } = req.params
|
1305
|
+
const response = req.body
|
1306
|
+
|
1307
|
+
console.log("RECEIVING INITIAL CONTENT WEBHOOK", response)
|
1308
|
+
|
1309
|
+
// Handle errors
|
1310
|
+
if (response.status === "ERROR") {
|
1311
|
+
await updateLessonStatusToError(courseSlug, lessonID, bucket)
|
1312
|
+
emitToCourse(courseSlug, "course-creation", {
|
1313
|
+
lesson: lessonID,
|
1314
|
+
status: "error",
|
1315
|
+
log: `❌ Error generating initial content for lesson ${lessonID}`,
|
1316
|
+
})
|
1317
|
+
|
1318
|
+
// Retry initial content generation
|
1319
|
+
try {
|
1320
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
1321
|
+
const lessonIndex = syllabus.lessons.findIndex(
|
1322
|
+
lesson => lesson.id === lessonID
|
1323
|
+
)
|
1324
|
+
const exercise = syllabus.lessons[lessonIndex]
|
1325
|
+
|
1326
|
+
if (exercise) {
|
1327
|
+
emitToCourse(courseSlug, "course-creation", {
|
1328
|
+
lesson: lessonID,
|
1329
|
+
status: "generating",
|
1330
|
+
log: `🔄 Retrying initial content generation for lesson ${lessonID}`,
|
1331
|
+
})
|
1332
|
+
|
1333
|
+
const retryCompletionId = await startInitialContentGeneration(
|
1334
|
+
rigoToken,
|
1335
|
+
syllabus.lessons,
|
1336
|
+
syllabus.courseInfo,
|
1337
|
+
exercise,
|
1338
|
+
courseSlug,
|
1339
|
+
syllabus.courseInfo.purpose,
|
1340
|
+
""
|
1341
|
+
)
|
1342
|
+
|
1343
|
+
// Update lesson status to show it's retrying
|
1344
|
+
exercise.status = "GENERATING"
|
1345
|
+
exercise.translations = {
|
1346
|
+
[syllabus.courseInfo.language || "en"]: {
|
1347
|
+
completionId: retryCompletionId,
|
1348
|
+
startedAt: Date.now(),
|
1349
|
+
completedAt: 0,
|
1350
|
+
},
|
1351
|
+
}
|
1352
|
+
await saveSyllabus(courseSlug, syllabus, bucket)
|
1353
|
+
}
|
1354
|
+
} catch (retryError) {
|
1355
|
+
console.error(
|
1356
|
+
"Error retrying initial content generation:",
|
1357
|
+
retryError
|
1358
|
+
)
|
1359
|
+
emitToCourse(courseSlug, "course-creation", {
|
1360
|
+
lesson: lessonID,
|
1361
|
+
status: "error",
|
1362
|
+
log: `❌ Failed to retry initial content generation for lesson ${lessonID}`,
|
1363
|
+
})
|
1364
|
+
}
|
1365
|
+
|
1366
|
+
return res.json({ status: "ERROR" })
|
1367
|
+
}
|
1368
|
+
|
1369
|
+
try {
|
1370
|
+
emitToCourse(courseSlug, "course-creation", {
|
1371
|
+
lesson: lessonID,
|
1372
|
+
status: "generating",
|
1373
|
+
log: `✅ Initial content generated for lesson ${lessonID}`,
|
1374
|
+
})
|
1375
|
+
|
1376
|
+
// Update lesson with initial content
|
1377
|
+
await updateLessonWithInitialContent(
|
1378
|
+
courseSlug,
|
1379
|
+
lessonID,
|
1380
|
+
response.parsed,
|
1381
|
+
bucket
|
1382
|
+
)
|
1383
|
+
|
1384
|
+
// Start Phase 2: Add interactivity
|
1385
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
1386
|
+
const lessonIndex = syllabus.lessons.findIndex(
|
1387
|
+
lesson => lesson.id === lessonID
|
1388
|
+
)
|
1389
|
+
const exercise = syllabus.lessons[lessonIndex]
|
1390
|
+
|
1391
|
+
let lastLesson = ""
|
1392
|
+
|
1393
|
+
const prevLessonIndex = lessonIndex - 1
|
1394
|
+
if (prevLessonIndex >= 0) {
|
1395
|
+
try {
|
1396
|
+
const prevLesson = syllabus.lessons[prevLessonIndex]
|
1397
|
+
// search the lesson content in the bucket
|
1398
|
+
const prevLessonSlug = slugify(
|
1399
|
+
prevLesson.id + "-" + prevLesson.title
|
1400
|
+
)
|
1401
|
+
const file = bucket.file(
|
1402
|
+
`courses/${courseSlug}/exercises/${prevLessonSlug}/README${getReadmeExtension(
|
1403
|
+
syllabus.courseInfo.language || "en"
|
1404
|
+
)}`
|
1405
|
+
)
|
1406
|
+
const [content] = await file.download()
|
1407
|
+
lastLesson = content.toString()
|
1408
|
+
} catch (error) {
|
1409
|
+
console.error("Error searching previous lesson content:", error)
|
1410
|
+
}
|
1411
|
+
}
|
1412
|
+
|
1413
|
+
if (exercise) {
|
1414
|
+
const completionId = await startInteractivityGeneration(
|
1415
|
+
rigoToken,
|
1416
|
+
syllabus.lessons,
|
1417
|
+
syllabus.courseInfo,
|
1418
|
+
exercise,
|
1419
|
+
courseSlug,
|
1420
|
+
syllabus.courseInfo.purpose,
|
1421
|
+
bucket,
|
1422
|
+
lastLesson
|
1423
|
+
)
|
1424
|
+
|
1425
|
+
// Update lesson status to show it's in Phase 2
|
1426
|
+
exercise.status = "GENERATING"
|
1427
|
+
exercise.translations = {
|
1428
|
+
[syllabus.courseInfo.language || "en"]: {
|
1429
|
+
completionId,
|
1430
|
+
startedAt: Date.now(),
|
1431
|
+
completedAt: 0,
|
1432
|
+
},
|
1433
|
+
}
|
1434
|
+
await saveSyllabus(courseSlug, syllabus, bucket)
|
1435
|
+
|
1436
|
+
emitToCourse(courseSlug, "course-creation", {
|
1437
|
+
lesson: lessonID,
|
1438
|
+
status: "generating",
|
1439
|
+
log: `🔄 Starting interactivity phase for lesson ${exercise.title}`,
|
1440
|
+
})
|
1441
|
+
}
|
1442
|
+
|
1443
|
+
emitToCourse(courseSlug, "course-creation", {
|
1444
|
+
lesson: lessonID,
|
1445
|
+
status: "initial-content-complete",
|
1446
|
+
log: `✅ Initial content generated for lesson ${lessonID}, starting interactivity phase`,
|
1447
|
+
})
|
1448
|
+
|
1449
|
+
res.json({ status: "SUCCESS" })
|
1450
|
+
} catch (error) {
|
1451
|
+
console.error("Error processing initial content webhook:", error)
|
1452
|
+
await updateLessonStatusToError(courseSlug, lessonID, bucket)
|
1453
|
+
emitToCourse(courseSlug, "course-creation", {
|
1454
|
+
lesson: lessonID,
|
1455
|
+
status: "error",
|
1456
|
+
log: `❌ Error processing initial content for lesson ${lessonID}`,
|
1457
|
+
})
|
1458
|
+
res
|
1459
|
+
.status(500)
|
1460
|
+
.json({ status: "ERROR", error: (error as Error).message })
|
1461
|
+
}
|
1462
|
+
}
|
1463
|
+
)
|
1464
|
+
|
1465
|
+
// Phase 2: Interactivity generation webhook (replaces exercise-processor logic)
|
1466
|
+
app.post(
|
1467
|
+
"/webhooks/:courseSlug/interactivity-processor/:lessonID/:rigoToken",
|
1468
|
+
async (req, res) => {
|
1469
|
+
const { courseSlug, lessonID, rigoToken } = req.params
|
1470
|
+
const response = req.body
|
1471
|
+
|
1472
|
+
console.log("RECEIVING INTERACTIVITY WEBHOOK", response)
|
1473
|
+
|
1474
|
+
// Handle errors
|
1475
|
+
if (response.status === "ERROR") {
|
1476
|
+
await updateLessonStatusToError(courseSlug, lessonID, bucket)
|
1477
|
+
emitToCourse(courseSlug, "course-creation", {
|
1478
|
+
lesson: lessonID,
|
1479
|
+
status: "error",
|
1480
|
+
log: `❌ Error adding interactivity to lesson ${lessonID}`,
|
1481
|
+
})
|
1482
|
+
|
1483
|
+
// Retry interactivity generation
|
1484
|
+
try {
|
1485
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
1486
|
+
const lessonIndex = syllabus.lessons.findIndex(
|
1487
|
+
lesson => lesson.id === lessonID
|
1488
|
+
)
|
1489
|
+
const exercise = syllabus.lessons[lessonIndex]
|
1490
|
+
|
1491
|
+
if (exercise && exercise.initialContent) {
|
1492
|
+
emitToCourse(courseSlug, "course-creation", {
|
1493
|
+
lesson: lessonID,
|
1494
|
+
status: "generating",
|
1495
|
+
log: `🔄 Retrying interactivity generation for lesson ${lessonID}`,
|
1496
|
+
})
|
1497
|
+
|
1498
|
+
// Get previous lesson content for context
|
1499
|
+
let lastLesson = ""
|
1500
|
+
const prevLessonIndex = lessonIndex - 1
|
1501
|
+
if (prevLessonIndex >= 0) {
|
1502
|
+
try {
|
1503
|
+
const prevLesson = syllabus.lessons[prevLessonIndex]
|
1504
|
+
const prevLessonSlug = slugify(
|
1505
|
+
prevLesson.id + "-" + prevLesson.title
|
1506
|
+
)
|
1507
|
+
const file = bucket.file(
|
1508
|
+
`courses/${courseSlug}/exercises/${prevLessonSlug}/README${getReadmeExtension(
|
1509
|
+
syllabus.courseInfo.language || "en"
|
1510
|
+
)}`
|
1511
|
+
)
|
1512
|
+
const [content] = await file.download()
|
1513
|
+
lastLesson = content.toString()
|
1514
|
+
} catch (error) {
|
1515
|
+
console.error(
|
1516
|
+
"Error getting previous lesson content for retry:",
|
1517
|
+
error
|
1518
|
+
)
|
1519
|
+
}
|
1520
|
+
}
|
1521
|
+
|
1522
|
+
const retryCompletionId = await startInteractivityGeneration(
|
1523
|
+
rigoToken,
|
1524
|
+
syllabus.lessons,
|
1525
|
+
syllabus.courseInfo,
|
1526
|
+
exercise,
|
1527
|
+
courseSlug,
|
1528
|
+
syllabus.courseInfo.purpose,
|
1529
|
+
bucket,
|
1530
|
+
lastLesson
|
1531
|
+
)
|
1532
|
+
|
1533
|
+
// Update lesson status to show it's retrying
|
1534
|
+
exercise.status = "GENERATING"
|
1535
|
+
exercise.translations = {
|
1536
|
+
[syllabus.courseInfo.language || "en"]: {
|
1537
|
+
completionId: retryCompletionId,
|
1538
|
+
startedAt: Date.now(),
|
1539
|
+
completedAt: 0,
|
1540
|
+
},
|
1541
|
+
}
|
1542
|
+
await saveSyllabus(courseSlug, syllabus, bucket)
|
1543
|
+
}
|
1544
|
+
} catch (retryError) {
|
1545
|
+
console.error(
|
1546
|
+
"Error retrying interactivity generation:",
|
1547
|
+
retryError
|
1548
|
+
)
|
1549
|
+
emitToCourse(courseSlug, "course-creation", {
|
1550
|
+
lesson: lessonID,
|
1551
|
+
status: "error",
|
1552
|
+
log: `❌ Failed to retry interactivity generation for lesson ${lessonID}`,
|
1553
|
+
})
|
1554
|
+
}
|
1555
|
+
|
1556
|
+
return res.json({ status: "ERROR" })
|
1557
|
+
}
|
1558
|
+
|
1559
|
+
try {
|
1560
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
1561
|
+
const exerciseIndex = syllabus.lessons.findIndex(
|
1562
|
+
lesson => lesson.id === lessonID
|
1563
|
+
)
|
1564
|
+
|
1565
|
+
if (exerciseIndex === -1) {
|
1566
|
+
console.error("Exercise not found receiving webhook:", lessonID)
|
1567
|
+
return res.json({ status: "ERROR", error: "Exercise not found" })
|
1568
|
+
}
|
1569
|
+
|
1570
|
+
const exercise = syllabus.lessons[exerciseIndex]
|
1571
|
+
const exSlug = slugify(exercise.id + "-" + exercise.title)
|
1572
|
+
|
1573
|
+
// Process readability of final content
|
1574
|
+
const readability = checkReadability(
|
1575
|
+
response.parsed.final_content,
|
1576
|
+
PARAMS.max_words,
|
1577
|
+
3
|
1578
|
+
)
|
1579
|
+
|
1580
|
+
emitToCourse(courseSlug, "course-creation", {
|
1581
|
+
lesson: exSlug,
|
1582
|
+
status: "generating",
|
1583
|
+
log: `🔄 The lesson ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
|
1584
|
+
})
|
1585
|
+
|
1586
|
+
// Upload README with final content
|
1587
|
+
const exercisesDir = `courses/${courseSlug}/exercises`
|
1588
|
+
const targetDir = `${exercisesDir}/${exSlug}`
|
1589
|
+
const readmeFilename = `README${getReadmeExtension(
|
1590
|
+
response.parsed.output_language ||
|
1591
|
+
syllabus.courseInfo.language ||
|
1592
|
+
"en"
|
1593
|
+
)}`
|
1594
|
+
|
1595
|
+
await uploadFileToBucket(
|
1596
|
+
bucket,
|
1597
|
+
readability.newMarkdown,
|
1598
|
+
`${targetDir}/${readmeFilename}`
|
1599
|
+
)
|
1600
|
+
|
1601
|
+
// Handle code files if it's a coding exercise
|
1602
|
+
if (
|
1603
|
+
exercise.type.toLowerCase() === "code" &&
|
1604
|
+
response.parsed.codefile_content
|
1605
|
+
) {
|
1606
|
+
emitToCourse(courseSlug, "course-creation", {
|
1607
|
+
lesson: exSlug,
|
1608
|
+
status: "generating",
|
1609
|
+
log: `🔄 Creating code file for ${exercise.title}`,
|
1610
|
+
})
|
1611
|
+
|
1612
|
+
await uploadFileToBucket(
|
1613
|
+
bucket,
|
1614
|
+
response.parsed.codefile_content,
|
1615
|
+
`${targetDir}/${response.parsed.codefile_name
|
1616
|
+
.toLowerCase()
|
1617
|
+
.trim()}`
|
1618
|
+
)
|
1619
|
+
|
1620
|
+
emitToCourse(courseSlug, "course-creation", {
|
1621
|
+
lesson: exSlug,
|
1622
|
+
status: "generating",
|
1623
|
+
log: `✅ Code file created for ${exercise.title}`,
|
1624
|
+
})
|
1625
|
+
|
1626
|
+
if (response.parsed.solution_content) {
|
1627
|
+
const codeFileName = response.parsed.codefile_name
|
1628
|
+
.toLowerCase()
|
1629
|
+
.trim()
|
1630
|
+
const solutionFileName = "solution.hide." + codeFileName
|
1631
|
+
await uploadFileToBucket(
|
1632
|
+
bucket,
|
1633
|
+
response.parsed.solution_content,
|
1634
|
+
`${targetDir}/${solutionFileName}`
|
1635
|
+
)
|
1636
|
+
emitToCourse(courseSlug, "course-creation", {
|
1637
|
+
lesson: exSlug,
|
1638
|
+
status: "generating",
|
1639
|
+
log: `✅ Solution file created for ${exercise.title}`,
|
1640
|
+
})
|
1641
|
+
}
|
1642
|
+
}
|
1643
|
+
|
1644
|
+
// Update used components if provided by the AI
|
1645
|
+
if (
|
1646
|
+
response.parsed.used_components &&
|
1647
|
+
Array.isArray(response.parsed.used_components)
|
1648
|
+
) {
|
1649
|
+
await updateUsedComponents(
|
1650
|
+
courseSlug,
|
1651
|
+
response.parsed.used_components,
|
1652
|
+
bucket
|
1653
|
+
)
|
1654
|
+
emitToCourse(courseSlug, "course-creation", {
|
1655
|
+
lesson: exSlug,
|
1656
|
+
status: "generating",
|
1657
|
+
log: `📊 Updated component usage tracking: ${response.parsed.used_components.join(
|
1658
|
+
", "
|
1659
|
+
)}`,
|
1660
|
+
})
|
1661
|
+
}
|
1662
|
+
|
1663
|
+
// Continue with next lesson
|
1664
|
+
await continueWithNextLesson(
|
1665
|
+
courseSlug,
|
1666
|
+
exerciseIndex,
|
1667
|
+
rigoToken,
|
1668
|
+
response.parsed.final_content,
|
1669
|
+
bucket
|
1670
|
+
)
|
1671
|
+
|
1672
|
+
emitToCourse(courseSlug, "course-creation", {
|
1673
|
+
lesson: exSlug,
|
1674
|
+
status: "done",
|
1675
|
+
log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully with interactivity!`,
|
1676
|
+
})
|
1677
|
+
|
1678
|
+
res.json({ status: "SUCCESS" })
|
1679
|
+
} catch (error) {
|
1680
|
+
console.error("Error processing interactivity webhook:", error)
|
1681
|
+
await updateLessonStatusToError(courseSlug, lessonID, bucket)
|
1682
|
+
emitToCourse(courseSlug, "course-creation", {
|
1683
|
+
lesson: lessonID,
|
1684
|
+
status: "error",
|
1685
|
+
log: `❌ Error processing interactivity for lesson ${lessonID}`,
|
1686
|
+
})
|
1687
|
+
res
|
1688
|
+
.status(500)
|
1689
|
+
.json({ status: "ERROR", error: (error as Error).message })
|
1690
|
+
}
|
1691
|
+
}
|
1692
|
+
)
|
1693
|
+
|
930
1694
|
// The following endpoint is used to store an incoming translation where it supposed to be
|
931
1695
|
app.post(
|
932
1696
|
"/webhooks/:courseSlug/:exSlug/save-translation",
|
@@ -1016,8 +1780,11 @@ export default class ServeCommand extends SessionCommand {
|
|
1016
1780
|
}
|
1017
1781
|
|
1018
1782
|
try {
|
1019
|
-
console.log("GET CONFIG, COURSE SLUG", courseSlug)
|
1020
1783
|
const { config, exercises } = await buildConfig(bucket, courseSlug)
|
1784
|
+
|
1785
|
+
console.log("CONFIG", config)
|
1786
|
+
console.log("EXERCISES", exercises)
|
1787
|
+
|
1021
1788
|
res.set("X-Creator-Web", "true")
|
1022
1789
|
res.set("Access-Control-Expose-Headers", "X-Creator-Web")
|
1023
1790
|
|
@@ -1526,7 +2293,8 @@ export default class ServeCommand extends SessionCommand {
|
|
1526
2293
|
|
1527
2294
|
const lastResult = "---"
|
1528
2295
|
|
1529
|
-
|
2296
|
+
// Use new two-phase generation workflow
|
2297
|
+
const completionId = await startInitialContentGeneration(
|
1530
2298
|
rigoToken,
|
1531
2299
|
syllabus.lessons,
|
1532
2300
|
syllabus.courseInfo,
|
@@ -1804,32 +2572,21 @@ export default class ServeCommand extends SessionCommand {
|
|
1804
2572
|
|
1805
2573
|
app.get("/actions/fetch/:link", async (req, res) => {
|
1806
2574
|
const { link } = req.params
|
2575
|
+
console.log("[DEBUG] FETCHING LINK", link)
|
1807
2576
|
try {
|
1808
2577
|
// 1) Decode the URL
|
1809
2578
|
const decoded = Buffer.from(link, "base64url").toString("utf-8")
|
1810
2579
|
const ytMatch = decoded.match(YT_REGEX)
|
1811
|
-
|
2580
|
+
console.log("[DEBUG] YT MATCH", ytMatch)
|
1812
2581
|
if (ytMatch) {
|
1813
2582
|
const videoId = ytMatch[1]
|
1814
2583
|
const resFromRigo = await axios.get(
|
1815
2584
|
`${RIGOBOT_REALTIME_HOST}/actions/youtube-transcript/${videoId}`
|
1816
2585
|
)
|
1817
2586
|
|
1818
|
-
|
2587
|
+
console.log("[DEBUG] RES FROM RIGO", resFromRigo.data)
|
1819
2588
|
|
1820
|
-
|
1821
|
-
// try {
|
1822
|
-
// const { data: meta } = await axios.get(
|
1823
|
-
// "https://www.youtube.com/oembed",
|
1824
|
-
// {
|
1825
|
-
// params: { url: decoded, format: "json" },
|
1826
|
-
// }
|
1827
|
-
// )
|
1828
|
-
// console.log("META", meta)
|
1829
|
-
// } catch (error) {
|
1830
|
-
// console.error("ERROR FETCHING META", error)
|
1831
|
-
// meta = null
|
1832
|
-
// }
|
2589
|
+
const transcript = resFromRigo.data.transcript
|
1833
2590
|
|
1834
2591
|
return res.json({
|
1835
2592
|
url: decoded,
|
@@ -1839,14 +2596,13 @@ export default class ServeCommand extends SessionCommand {
|
|
1839
2596
|
})
|
1840
2597
|
}
|
1841
2598
|
|
1842
|
-
console.log("NOT A YOUTUBE LINK", decoded)
|
1843
|
-
|
1844
2599
|
const response = await axios.get(decoded, { responseType: "text" })
|
1845
2600
|
const html = response.data as string
|
1846
2601
|
const title = getTitleFromHTML(html)
|
1847
2602
|
console.log("TITLE", title)
|
1848
2603
|
|
1849
2604
|
const text = convert(html)
|
2605
|
+
console.log("[DEBUG] TEXT", text)
|
1850
2606
|
return res.json({
|
1851
2607
|
url: decoded,
|
1852
2608
|
text,
|
@@ -2001,68 +2757,71 @@ export default class ServeCommand extends SessionCommand {
|
|
2001
2757
|
let filename: string
|
2002
2758
|
|
2003
2759
|
switch (format) {
|
2004
|
-
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
2010
|
-
})
|
2011
|
-
filename = `${course_slug}-scorm.zip`
|
2012
|
-
|
2013
|
-
break
|
2014
|
-
}
|
2015
|
-
|
2016
|
-
case "zip": {
|
2017
|
-
outputPath = await exportToZip({
|
2018
|
-
courseSlug: course_slug,
|
2019
|
-
format: "zip",
|
2020
|
-
bucket,
|
2021
|
-
outDir: path.join(__dirname, "../output/directory"),
|
2022
|
-
})
|
2023
|
-
filename = `${course_slug}.zip`
|
2024
|
-
|
2025
|
-
break
|
2026
|
-
}
|
2027
|
-
|
2028
|
-
case "epub": {
|
2029
|
-
console.log("EPUB export", metadata)
|
2030
|
-
// Validate required metadata for EPUB
|
2031
|
-
if (
|
2032
|
-
!metadata ||
|
2033
|
-
!metadata.creator ||
|
2034
|
-
!metadata.publisher ||
|
2035
|
-
!metadata.title ||
|
2036
|
-
!metadata.rights ||
|
2037
|
-
!metadata.lang
|
2038
|
-
) {
|
2039
|
-
console.log("Missing required metadata for EPUB export", metadata)
|
2040
|
-
return res.status(400).json({
|
2041
|
-
error: "Missing required metadata for EPUB export",
|
2042
|
-
required: ["creator", "publisher", "title", "rights", "lang"],
|
2760
|
+
case "scorm": {
|
2761
|
+
outputPath = await exportToScorm({
|
2762
|
+
courseSlug: course_slug,
|
2763
|
+
format: "scorm",
|
2764
|
+
bucket,
|
2765
|
+
outDir: path.join(__dirname, "../output/directory"),
|
2043
2766
|
})
|
2767
|
+
filename = `${course_slug}-scorm.zip`
|
2768
|
+
|
2769
|
+
break
|
2044
2770
|
}
|
2045
2771
|
|
2046
|
-
|
2047
|
-
{
|
2772
|
+
case "zip": {
|
2773
|
+
outputPath = await exportToZip({
|
2048
2774
|
courseSlug: course_slug,
|
2049
|
-
format: "
|
2775
|
+
format: "zip",
|
2050
2776
|
bucket,
|
2051
2777
|
outDir: path.join(__dirname, "../output/directory"),
|
2052
|
-
|
2053
|
-
}
|
2054
|
-
metadata
|
2055
|
-
)
|
2056
|
-
filename = `${course_slug}.epub`
|
2057
|
-
|
2058
|
-
break
|
2059
|
-
}
|
2778
|
+
})
|
2779
|
+
filename = `${course_slug}.zip`
|
2060
2780
|
|
2061
|
-
|
2062
|
-
|
2063
|
-
|
2064
|
-
|
2065
|
-
|
2781
|
+
break
|
2782
|
+
}
|
2783
|
+
|
2784
|
+
case "epub": {
|
2785
|
+
console.log("EPUB export", metadata)
|
2786
|
+
// Validate required metadata for EPUB
|
2787
|
+
if (
|
2788
|
+
!metadata ||
|
2789
|
+
!metadata.creator ||
|
2790
|
+
!metadata.publisher ||
|
2791
|
+
!metadata.title ||
|
2792
|
+
!metadata.rights ||
|
2793
|
+
!metadata.lang
|
2794
|
+
) {
|
2795
|
+
console.log(
|
2796
|
+
"Missing required metadata for EPUB export",
|
2797
|
+
metadata
|
2798
|
+
)
|
2799
|
+
return res.status(400).json({
|
2800
|
+
error: "Missing required metadata for EPUB export",
|
2801
|
+
required: ["creator", "publisher", "title", "rights", "lang"],
|
2802
|
+
})
|
2803
|
+
}
|
2804
|
+
|
2805
|
+
outputPath = await exportToEpub(
|
2806
|
+
{
|
2807
|
+
courseSlug: course_slug,
|
2808
|
+
format: "epub",
|
2809
|
+
bucket,
|
2810
|
+
outDir: path.join(__dirname, "../output/directory"),
|
2811
|
+
language: language,
|
2812
|
+
},
|
2813
|
+
metadata
|
2814
|
+
)
|
2815
|
+
filename = `${course_slug}.epub`
|
2816
|
+
|
2817
|
+
break
|
2818
|
+
}
|
2819
|
+
|
2820
|
+
default: {
|
2821
|
+
return res.status(400).json({
|
2822
|
+
error: "Invalid format. Supported formats: scorm, epub, zip",
|
2823
|
+
})
|
2824
|
+
}
|
2066
2825
|
}
|
2067
2826
|
|
2068
2827
|
// Send the file and clean up
|