@learnpack/learnpack 5.0.291 → 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 +492 -33
- package/lib/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
- package/lib/creatorDist/assets/{index-wLKEQIG6.js → index-DOEfLGDQ.js} +1424 -1372
- 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/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 +794 -48
- package/src/creator/src/App.tsx +12 -12
- 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-wLKEQIG6.js → index-DOEfLGDQ.js} +1424 -1372
- 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 +363 -361
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +2 -1
- 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,
|
@@ -257,8 +306,6 @@ async function startExerciseGeneration(
|
|
257
306
|
|
258
307
|
const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`
|
259
308
|
|
260
|
-
console.log("WEBHOOK URL", webhookUrl)
|
261
|
-
|
262
309
|
const res = await readmeCreator(
|
263
310
|
rigoToken.trim(),
|
264
311
|
{
|
@@ -280,6 +327,118 @@ async function startExerciseGeneration(
|
|
280
327
|
return res.id
|
281
328
|
}
|
282
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
|
+
|
283
442
|
async function createInitialReadme(
|
284
443
|
tutorialInfo: string,
|
285
444
|
tutorialSlug: string,
|
@@ -301,6 +460,219 @@ async function createInitialReadme(
|
|
301
460
|
}
|
302
461
|
}
|
303
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
|
+
|
304
676
|
const fixPreviewUrl = (slug: string, previewUrl: string) => {
|
305
677
|
if (!previewUrl) {
|
306
678
|
return null
|
@@ -635,17 +1007,13 @@ export default class ServeCommand extends SessionCommand {
|
|
635
1007
|
})
|
636
1008
|
}
|
637
1009
|
|
638
|
-
const syllabus = await bucket
|
639
|
-
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
640
|
-
)
|
641
|
-
const [content] = await syllabus.download()
|
642
|
-
const syllabusJson: Syllabus = JSON.parse(content.toString())
|
1010
|
+
const syllabus = await getSyllabus(courseSlug, bucket)
|
643
1011
|
|
644
|
-
const exercise =
|
1012
|
+
const exercise = syllabus.lessons[parseInt(position)]
|
645
1013
|
|
646
1014
|
// previous exercise
|
647
1015
|
let previousReadme = "---"
|
648
|
-
const previousExercise =
|
1016
|
+
const previousExercise = syllabus.lessons[parseInt(position) - 1]
|
649
1017
|
if (previousExercise) {
|
650
1018
|
// Get the readme of the previous exercise
|
651
1019
|
const exSlug = slugify(
|
@@ -664,43 +1032,37 @@ export default class ServeCommand extends SessionCommand {
|
|
664
1032
|
}
|
665
1033
|
}
|
666
1034
|
|
667
|
-
|
1035
|
+
// Use new two-phase generation workflow
|
1036
|
+
const completionId = await startInitialContentGeneration(
|
668
1037
|
rigoToken,
|
669
|
-
|
670
|
-
|
1038
|
+
syllabus.lessons,
|
1039
|
+
syllabus.courseInfo,
|
671
1040
|
exercise,
|
672
1041
|
courseSlug,
|
673
|
-
|
1042
|
+
syllabus.courseInfo.purpose,
|
674
1043
|
previousReadme +
|
675
1044
|
"\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
|
676
1045
|
feedback
|
677
1046
|
)
|
678
1047
|
|
679
|
-
|
680
|
-
|
681
|
-
[
|
1048
|
+
syllabus.lessons[parseInt(position)].status = "GENERATING"
|
1049
|
+
syllabus.lessons[parseInt(position)].translations = {
|
1050
|
+
[syllabus.courseInfo.language || "en"]: {
|
682
1051
|
completionId,
|
683
1052
|
startedAt: Date.now(),
|
684
1053
|
completedAt: 0,
|
685
1054
|
},
|
686
1055
|
}
|
687
|
-
console.log("Lesson",
|
688
|
-
if (
|
689
|
-
|
690
|
-
typeof syllabusJson.feedback === "string"
|
691
|
-
) {
|
692
|
-
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
|
693
1059
|
} else {
|
694
|
-
|
1060
|
+
syllabus.feedback = feedback
|
695
1061
|
}
|
696
1062
|
|
697
|
-
|
1063
|
+
syllabus.generationMode = mode
|
698
1064
|
|
699
|
-
await
|
700
|
-
bucket,
|
701
|
-
JSON.stringify(syllabusJson),
|
702
|
-
`courses/${courseSlug}/.learn/initialSyllabus.json`
|
703
|
-
)
|
1065
|
+
await saveSyllabus(courseSlug, syllabus, bucket)
|
704
1066
|
|
705
1067
|
res.json({ status: "SUCCESS" })
|
706
1068
|
}
|
@@ -935,6 +1297,400 @@ export default class ServeCommand extends SessionCommand {
|
|
935
1297
|
}
|
936
1298
|
)
|
937
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
|
+
|
938
1694
|
// The following endpoint is used to store an incoming translation where it supposed to be
|
939
1695
|
app.post(
|
940
1696
|
"/webhooks/:courseSlug/:exSlug/save-translation",
|
@@ -1024,10 +1780,11 @@ export default class ServeCommand extends SessionCommand {
|
|
1024
1780
|
}
|
1025
1781
|
|
1026
1782
|
try {
|
1027
|
-
console.log("GET CONFIG, COURSE SLUG", courseSlug)
|
1028
1783
|
const { config, exercises } = await buildConfig(bucket, courseSlug)
|
1784
|
+
|
1029
1785
|
console.log("CONFIG", config)
|
1030
1786
|
console.log("EXERCISES", exercises)
|
1787
|
+
|
1031
1788
|
res.set("X-Creator-Web", "true")
|
1032
1789
|
res.set("Access-Control-Expose-Headers", "X-Creator-Web")
|
1033
1790
|
|
@@ -1536,7 +2293,8 @@ export default class ServeCommand extends SessionCommand {
|
|
1536
2293
|
|
1537
2294
|
const lastResult = "---"
|
1538
2295
|
|
1539
|
-
|
2296
|
+
// Use new two-phase generation workflow
|
2297
|
+
const completionId = await startInitialContentGeneration(
|
1540
2298
|
rigoToken,
|
1541
2299
|
syllabus.lessons,
|
1542
2300
|
syllabus.courseInfo,
|
@@ -1814,32 +2572,21 @@ export default class ServeCommand extends SessionCommand {
|
|
1814
2572
|
|
1815
2573
|
app.get("/actions/fetch/:link", async (req, res) => {
|
1816
2574
|
const { link } = req.params
|
2575
|
+
console.log("[DEBUG] FETCHING LINK", link)
|
1817
2576
|
try {
|
1818
2577
|
// 1) Decode the URL
|
1819
2578
|
const decoded = Buffer.from(link, "base64url").toString("utf-8")
|
1820
2579
|
const ytMatch = decoded.match(YT_REGEX)
|
1821
|
-
|
2580
|
+
console.log("[DEBUG] YT MATCH", ytMatch)
|
1822
2581
|
if (ytMatch) {
|
1823
2582
|
const videoId = ytMatch[1]
|
1824
2583
|
const resFromRigo = await axios.get(
|
1825
2584
|
`${RIGOBOT_REALTIME_HOST}/actions/youtube-transcript/${videoId}`
|
1826
2585
|
)
|
1827
2586
|
|
1828
|
-
|
2587
|
+
console.log("[DEBUG] RES FROM RIGO", resFromRigo.data)
|
1829
2588
|
|
1830
|
-
|
1831
|
-
// try {
|
1832
|
-
// const { data: meta } = await axios.get(
|
1833
|
-
// "https://www.youtube.com/oembed",
|
1834
|
-
// {
|
1835
|
-
// params: { url: decoded, format: "json" },
|
1836
|
-
// }
|
1837
|
-
// )
|
1838
|
-
// console.log("META", meta)
|
1839
|
-
// } catch (error) {
|
1840
|
-
// console.error("ERROR FETCHING META", error)
|
1841
|
-
// meta = null
|
1842
|
-
// }
|
2589
|
+
const transcript = resFromRigo.data.transcript
|
1843
2590
|
|
1844
2591
|
return res.json({
|
1845
2592
|
url: decoded,
|
@@ -1849,14 +2596,13 @@ export default class ServeCommand extends SessionCommand {
|
|
1849
2596
|
})
|
1850
2597
|
}
|
1851
2598
|
|
1852
|
-
console.log("NOT A YOUTUBE LINK", decoded)
|
1853
|
-
|
1854
2599
|
const response = await axios.get(decoded, { responseType: "text" })
|
1855
2600
|
const html = response.data as string
|
1856
2601
|
const title = getTitleFromHTML(html)
|
1857
2602
|
console.log("TITLE", title)
|
1858
2603
|
|
1859
2604
|
const text = convert(html)
|
2605
|
+
console.log("[DEBUG] TEXT", text)
|
1860
2606
|
return res.json({
|
1861
2607
|
url: decoded,
|
1862
2608
|
text,
|