@learnpack/learnpack 5.0.130 → 5.0.134

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.
@@ -10,7 +10,7 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-wpTTgviz.js"></script>
13
+ <script type="module" crossorigin src="/creator/assets/index-D-toUNNO.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/creator/assets/index-1sKQbOKY.css">
15
15
  </head>
16
16
  <body>
@@ -25,6 +25,7 @@ export declare function checkReadability(markdown: string, wordsPerMinute?: numb
25
25
  body: string;
26
26
  fkglResult: TFKGLResult;
27
27
  };
28
+ export declare const slugify: (text: string) => string;
28
29
  export declare const getExInfo: (title: string) => {
29
30
  exNumber: string;
30
31
  kind: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appendAIRules = exports.appendContentIndex = exports.saveTranslatedReadme = exports.makePackageInfo = exports.getExInfo = exports.estimateReadingTime = void 0;
3
+ exports.appendAIRules = exports.appendContentIndex = exports.saveTranslatedReadme = exports.makePackageInfo = exports.getExInfo = exports.slugify = exports.estimateReadingTime = void 0;
4
4
  exports.checkReadability = checkReadability;
5
5
  exports.extractImagesFromMarkdown = extractImagesFromMarkdown;
6
6
  exports.getFilenameFromUrl = getFilenameFromUrl;
@@ -93,6 +93,7 @@ const slugify = (text) => {
93
93
  .replace(/\s+/g, "-")
94
94
  .replace(/[^\w-]+/g, "");
95
95
  };
96
+ exports.slugify = slugify;
96
97
  const getExInfo = (title) => {
97
98
  // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
98
99
  let [exNumber, exTitle] = title.split(" - ");
@@ -103,7 +104,7 @@ const getExInfo = (title) => {
103
104
  exNumber = exNumber.trim();
104
105
  // Clean title
105
106
  exTitle = exTitle.replace((kindMatch === null || kindMatch === void 0 ? void 0 : kindMatch[0]) || "", "").trim();
106
- exTitle = slugify(exTitle);
107
+ exTitle = (0, exports.slugify)(exTitle);
107
108
  return {
108
109
  exNumber,
109
110
  kind,
@@ -330,13 +331,13 @@ function countSentences(text) {
330
331
  function howManyDifficultParagraphs(paragraphs, maxFKGL) {
331
332
  return paragraphs.filter(paragraph => paragraph.fkgl > maxFKGL).length;
332
333
  }
333
- const example_content = `Write or paste your table of content below this line, each topic should be defined on a new line, here is an example:
334
-
335
- Introduction to AI: Explain what is AI and its applications
336
- Introduction to Machine Learning: Explain what is machine learning and its applications
337
- What is an AI Model: Explain what is an AI model and its applications
338
- How to use an AI Model: Different APIs, local models, etc.
339
- How to build an AI Model: Fine-tuning, data collection, cleaning and more.
334
+ const example_content = `Write or paste your table of content below this line, each topic should be defined on a new line, here is an example:
335
+
336
+ Introduction to AI: Explain what is AI and its applications
337
+ Introduction to Machine Learning: Explain what is machine learning and its applications
338
+ What is an AI Model: Explain what is an AI model and its applications
339
+ How to use an AI Model: Different APIs, local models, etc.
340
+ How to build an AI Model: Fine-tuning, data collection, cleaning and more.
340
341
  `;
341
342
  const appendContentIndex = async () => {
342
343
  const choices = await prompts([
@@ -366,13 +367,13 @@ const appendContentIndex = async () => {
366
367
  return null;
367
368
  };
368
369
  exports.appendContentIndex = appendContentIndex;
369
- const example_airules = `
370
- Write with an engaging tone, use simple words and avoid complex sentences.
371
- Write in first person, as if you are talking to the reader.
372
- Add mental maps to help the reader understand the content.
373
- Add diagrams to help the reader understand the content.
374
- No code exercises required
375
-
370
+ const example_airules = `
371
+ Write with an engaging tone, use simple words and avoid complex sentences.
372
+ Write in first person, as if you are talking to the reader.
373
+ Add mental maps to help the reader understand the content.
374
+ Add diagrams to help the reader understand the content.
375
+ No code exercises required
376
+
376
377
  `;
377
378
  const appendAIRules = async () => {
378
379
  const choices = await prompts([
@@ -1 +1 @@
1
- {"version":"5.0.130","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false}},"args":[]},"breakToken":{"id":"breakToken","description":"Break the token","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false},"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"serve":{"id":"serve","description":"Runs a small server to build tutorials","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
1
+ {"version":"5.0.134","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false}},"args":[]},"breakToken":{"id":"breakToken","description":"Break the token","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false},"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"serve":{"id":"serve","description":"Runs a small server to build tutorials","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.130",
4
+ "version": "5.0.134",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -10,21 +10,260 @@ import * as mkdirp from "mkdirp"
10
10
  import { convert } from "html-to-text"
11
11
  import * as rimraf from "rimraf"
12
12
  import SessionCommand from "../utils/SessionCommand"
13
- import { Storage } from "@google-cloud/storage"
13
+ import { Bucket, Storage } from "@google-cloud/storage"
14
14
  import { downloadEditor, decompress } from "../managers/file"
15
15
  import * as fs from "fs"
16
- import { translateExercise, isValidRigoToken } from "../utils/rigoActions"
16
+ import {
17
+ createCodeFile,
18
+ translateExercise,
19
+ isValidRigoToken,
20
+ readmeCreator,
21
+ makeReadmeReadable,
22
+ generateImage,
23
+ } from "../utils/rigoActions"
17
24
  import * as dotenv from "dotenv"
25
+ import {
26
+ extractImagesFromMarkdown,
27
+ getFilenameFromUrl,
28
+ } from "../utils/creatorUtilities"
18
29
  // import { handleAssetCreation } from "./publish"
19
30
  import axios from "axios"
20
31
  import * as FormData from "form-data"
21
32
  import { RIGOBOT_HOST } from "../utils/api"
22
33
  import { minutesToISO8601Duration } from "../utils/misc"
23
34
  import { buildConfig, ConfigResponse } from "../utils/configBuilder"
35
+ import { checkReadability, slugify } from "../utils/creatorUtilities"
36
+ const frontMatter = require("front-matter")
24
37
 
25
38
  dotenv.config()
26
39
 
27
- const frontMatter = require("front-matter")
40
+ export const createLearnJson = (courseInfo: FormState) => {
41
+ console.log("courseInfo to create learn json", courseInfo)
42
+
43
+ const learnJson = {
44
+ slug: slugify(courseInfo.title as string),
45
+ title: {
46
+ us: courseInfo.title,
47
+ },
48
+ technologies: courseInfo.technologies || [],
49
+ difficulty: "beginner",
50
+ description: {
51
+ us: courseInfo.description,
52
+ },
53
+ grading: "isolated",
54
+ telemetry: {
55
+ batch: "https://breathecode.herokuapp.com/v1/assignment/me/telemetry",
56
+ },
57
+ preview: "preview.png",
58
+ }
59
+ return learnJson
60
+ }
61
+
62
+ const uploadFileToBucket = async (
63
+ bucket: Bucket,
64
+ file: string,
65
+ path: string
66
+ ) => {
67
+ const fileRef = bucket.file(path)
68
+ await fileRef.save(file)
69
+ }
70
+
71
+ const uploadImageToBucket = async (
72
+ bucket: Bucket,
73
+ url: string,
74
+ path: string
75
+ ) => {
76
+ const response = await fetch(url)
77
+ if (!response.ok) {
78
+ throw new Error(`Failed to download image: ${response.statusText}`)
79
+ }
80
+
81
+ const contentType =
82
+ response.headers.get("content-type") || "application/octet-stream"
83
+ const buffer = await response.arrayBuffer()
84
+
85
+ const fileRef = bucket.file(path)
86
+ await fileRef.save(Buffer.from(buffer), {
87
+ resumable: false,
88
+ contentType,
89
+ })
90
+ }
91
+
92
+ const PARAMS = {
93
+ expected_grade_level: "7",
94
+ max_fkgl: 9,
95
+ max_words: 200,
96
+ max_rewrite_attempts: 2,
97
+ max_title_length: 50,
98
+ }
99
+
100
+ export const processImage = async (
101
+ bucket: Bucket,
102
+ tutorialDir: string,
103
+ url: string,
104
+ description: string,
105
+ rigoToken: string
106
+ ) => {
107
+ try {
108
+ const filename = getFilenameFromUrl(url)
109
+
110
+ const imagePath = `${tutorialDir}/.learn/assets/${filename}`
111
+
112
+ console.log("🖼️ Generating image", imagePath)
113
+
114
+ const res = await generateImage(rigoToken, { prompt: description })
115
+ await uploadImageToBucket(bucket, res.image_url, imagePath)
116
+
117
+ console.log("✅ Image", imagePath, "generated successfully!")
118
+ return true
119
+ } catch {
120
+ return false
121
+ }
122
+ }
123
+
124
+ export async function processExercise(
125
+ bucket: Bucket,
126
+ rigoToken: string,
127
+ steps: Lesson[],
128
+ packageContext: string,
129
+ exercise: Lesson,
130
+ exercisesDir: string
131
+ ): Promise<string> {
132
+ // const tid = toast.loading("Generating lesson...")
133
+ const exSlug = slugify(exercise.id + "-" + exercise.title)
134
+ const readmeFilename = "README.md"
135
+ const targetDir = `${exercisesDir}/${exSlug}`
136
+
137
+ console.log("✍🏻 Generating lesson", exercise.id, exercise.title)
138
+ const isGeneratingText = `The lesson ${exercise.id} - ${exercise.title} is being generated by Rigobot, wait a sec!
139
+
140
+ \`\`\`loader slug="${exSlug}"
141
+ # This lesson is being generated by Rigobot, wait a sec!
142
+ \`\`\`
143
+ `
144
+
145
+ await uploadFileToBucket(
146
+ bucket,
147
+ isGeneratingText,
148
+ `${targetDir}/${readmeFilename}`
149
+ )
150
+
151
+ const readme = await readmeCreator(rigoToken, {
152
+ title: `${exercise.id} - ${exercise.title}`,
153
+ output_lang: "en",
154
+ list_of_exercises: JSON.stringify(steps),
155
+ tutorial_description: packageContext,
156
+ lesson_description: exercise.description,
157
+ kind: exercise.type.toLowerCase(),
158
+ })
159
+
160
+ const duration = exercise.duration
161
+ let attempts = 0
162
+ let readability = checkReadability(readme.parsed.content, 200, duration || 1)
163
+
164
+ while (
165
+ readability.fkglResult.fkgl > PARAMS.max_fkgl &&
166
+ attempts < PARAMS.max_rewrite_attempts
167
+ ) {
168
+ console.log(
169
+ "🔄 The lesson",
170
+ exercise.id,
171
+ exercise.title,
172
+ "has a readability score of",
173
+ readability.fkglResult.fkgl
174
+ )
175
+
176
+ // eslint-disable-next-line
177
+ const reducedReadme = await makeReadmeReadable(rigoToken, {
178
+ lesson: readability.body,
179
+ number_of_words: readability.minutes.toString(),
180
+ expected_number_words: PARAMS.max_words.toString(),
181
+ fkgl_results: JSON.stringify(readability.fkglResult),
182
+ expected_grade_level: PARAMS.expected_grade_level,
183
+ })
184
+
185
+ if (!reducedReadme)
186
+ break
187
+
188
+ readability = checkReadability(
189
+ reducedReadme.parsed.content,
190
+ PARAMS.max_words,
191
+ duration || 1
192
+ )
193
+
194
+ attempts++
195
+ }
196
+
197
+ console.log(
198
+ `✅ After ${attempts} attempts, the lesson ${
199
+ exercise.title
200
+ } has a readability score of ${
201
+ readability.fkglResult.fkgl
202
+ } using FKGL. And it has ${readability.minutes.toFixed(
203
+ 2
204
+ )} minutes of reading time.`
205
+ )
206
+
207
+ await uploadFileToBucket(
208
+ bucket,
209
+ readability.newMarkdown,
210
+ `${targetDir}/${readmeFilename}`
211
+ )
212
+
213
+ if (exercise.type.toLowerCase() === "code") {
214
+ console.log("🔍 Creating code file for", exercise.title)
215
+
216
+ const codeFile = await createCodeFile(rigoToken, {
217
+ readme: readability.newMarkdown,
218
+ tutorial_info: packageContext,
219
+ })
220
+ await uploadFileToBucket(
221
+ bucket,
222
+ codeFile.parsed.content,
223
+ `${targetDir}/index.${codeFile.parsed.extension.replace(".", "")}`
224
+ )
225
+ }
226
+
227
+ return readability.newMarkdown
228
+ }
229
+
230
+ interface Lesson {
231
+ id: string
232
+ uid: string
233
+ title: string
234
+ type: "READ" | "CODE" | "QUIZ"
235
+ description: string
236
+ duration?: number
237
+ }
238
+
239
+ interface ParsedLink {
240
+ url: string
241
+ title?: string
242
+ text?: string
243
+ transcript?: string
244
+ description?: string
245
+ author?: string
246
+ duration?: number
247
+ thumbnail?: string
248
+ }
249
+ type FormState = {
250
+ description: string
251
+ duration: number
252
+ targetAudience: string
253
+ hasContentIndex: boolean
254
+ contentIndex: string
255
+ sources: ParsedLink[]
256
+ isCompleted: boolean
257
+ variables: string[]
258
+ currentStep: string
259
+ title: string
260
+ technologies?: string[]
261
+ }
262
+
263
+ type Syllabus = {
264
+ lessons: Lesson[]
265
+ courseInfo: FormState
266
+ }
28
267
 
29
268
  const fixPreviewUrl = (slug: string, previewUrl: string) => {
30
269
  if (!previewUrl) {
@@ -151,7 +390,7 @@ export default class ServeCommand extends SessionCommand {
151
390
 
152
391
  app.post(
153
392
  "/upload-image",
154
- express.json({ limit: "20mb" }),
393
+ express.json({ limit: 20_000_000 }),
155
394
  async (req, res) => {
156
395
  const { image_url, destination } = req.body
157
396
  if (!image_url || !destination) {
@@ -428,6 +667,64 @@ export default class ServeCommand extends SessionCommand {
428
667
  res.sendFile(file)
429
668
  })
430
669
 
670
+ app.post("/actions/create-course", async (req, res) => {
671
+ console.log("POST /actions/create-course")
672
+ const { syllabus }: { syllabus: Syllabus } = req.body
673
+ const rigoToken = req.header("x-rigo-token")
674
+ const bcToken = req.header("x-breathecode-token")
675
+
676
+ if (!rigoToken || !bcToken) {
677
+ return res.status(400).json({ error: "Missing tokens" })
678
+ }
679
+
680
+ const tutorialDir = "courses/" + slugify(syllabus.courseInfo.title)
681
+
682
+ const learnJson = createLearnJson(syllabus.courseInfo)
683
+ await uploadFileToBucket(
684
+ bucket,
685
+ JSON.stringify(learnJson),
686
+ `courses/${slugify(syllabus.courseInfo.title)}/learn.json`
687
+ )
688
+
689
+ const lessonsPromises = syllabus.lessons.map(lesson =>
690
+ processExercise(
691
+ bucket,
692
+ rigoToken,
693
+ syllabus.lessons,
694
+ JSON.stringify(syllabus.courseInfo),
695
+ lesson,
696
+ tutorialDir + "/exercises"
697
+ )
698
+ )
699
+ const readmeContents = await Promise.all(lessonsPromises)
700
+
701
+ let imagesArray: any[] = []
702
+
703
+ for (const content of readmeContents) {
704
+ imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)]
705
+ }
706
+
707
+ console.log("📷 Generating images...")
708
+
709
+ const imagePromises = imagesArray.map(
710
+ async (image: { alt: string; url: string }) => {
711
+ return processImage(
712
+ bucket,
713
+ tutorialDir,
714
+ image.url,
715
+ image.alt,
716
+ rigoToken
717
+ )
718
+ }
719
+ )
720
+ await Promise.all(imagePromises)
721
+
722
+ return res.json({
723
+ message: "Course created",
724
+ slug: slugify(syllabus.courseInfo.title),
725
+ })
726
+ })
727
+
431
728
  app.post("/actions/publish/:slug", async (req, res) => {
432
729
  try {
433
730
  const { slug } = req.params
@@ -4,19 +4,12 @@ import useStore from "../../utils/store"
4
4
  import { interactiveCreation } from "../../utils/rigo"
5
5
  import {
6
6
  parseLesson,
7
- uploadFileToBucket,
8
7
  useConsumableCall,
9
8
  validateTokens,
10
- extractImagesFromMarkdown,
11
9
  checkParams,
12
10
  loginWithToken,
11
+ createCourse,
13
12
  } from "../../utils/lib"
14
- import {
15
- createLearnJson,
16
- processExercise,
17
- slugify,
18
- processImage,
19
- } from "../../utils/creatorUtils"
20
13
 
21
14
  import Loader from "../Loader"
22
15
  import { TMessage } from "../Message"
@@ -26,10 +19,10 @@ import toast from "react-hot-toast"
26
19
  import { ContentIndex } from "./ContentIndex"
27
20
  import { Sidebar } from "./Sidebar"
28
21
  import Login from "../Login"
29
- import { eventBus } from "../../utils/eventBus"
30
22
  import { useNavigate } from "react-router"
31
23
  import PreviewGenerator from "../PreviewGenerator"
32
24
  import { ParamsChecker } from "../ParamsChecker"
25
+ import { slugify } from "../../utils/creatorUtils"
33
26
 
34
27
  const SyllabusEditor: React.FC = () => {
35
28
  const navigate = useNavigate()
@@ -138,7 +131,6 @@ const SyllabusEditor: React.FC = () => {
138
131
  toast.error("Please provide a title for the course")
139
132
  return
140
133
  }
141
- setIsGenerating(true)
142
134
 
143
135
  let tokenToUse = auth.rigoToken
144
136
  const onValidRigoToken = (rigotoken: string) => {
@@ -159,54 +151,19 @@ const SyllabusEditor: React.FC = () => {
159
151
  return
160
152
  }
161
153
  setIsGenerating(true)
162
-
163
- const tutorialDir = "courses/" + slugify(syllabus.courseInfo.title)
164
- const lessonsPromises = syllabus.lessons.map((lesson) =>
165
- processExercise(
166
- tokenToUse,
167
- syllabus.lessons,
168
- JSON.stringify(syllabus.courseInfo),
169
- lesson,
170
- tutorialDir + "/exercises"
171
- )
172
- )
173
- const readmeContents = await Promise.all(lessonsPromises)
174
-
175
- let imagesArray: any[] = []
176
-
177
- for (const content of readmeContents) {
178
- imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)]
179
- }
180
-
181
- eventBus.emit("course-generation", {
182
- message: "📷 Generating images...",
183
- })
184
-
185
- const imagePromises = imagesArray.map(
186
- async (image: { alt: string; url: string }) => {
187
- return processImage(tutorialDir, image.url, image.alt, tokenToUse)
188
- }
189
- )
190
- await Promise.all(imagePromises)
191
-
192
- const learnJson = createLearnJson(syllabus.courseInfo)
193
- await uploadFileToBucket(
194
- JSON.stringify(learnJson),
195
- "courses/" + slugify(syllabus.courseInfo.title) + "/learn.json"
196
- )
197
-
198
- cleanHistory()
199
-
200
- window.location.href = `/preview/${slugify(
201
- syllabus.courseInfo.title
202
- )}?token=${auth.bcToken}`
203
- // setIsGenerating(false)
154
+ createCourse(syllabus, tokenToUse, auth.bcToken)
155
+
156
+ setTimeout(() => {
157
+ cleanHistory()
158
+ setIsGenerating(false)
159
+ window.location.href = `/preview/${slugify(
160
+ syllabus.courseInfo.title || ""
161
+ )}?token=${auth.bcToken}`
162
+ }, 1000)
204
163
  }
205
164
 
206
165
  if (!syllabus) return null
207
166
 
208
- // console.log(auth.user)
209
-
210
167
  return isGenerating ? (
211
168
  <>
212
169
  <Loader
@@ -2,6 +2,7 @@ import axios from "axios"
2
2
  import { BREATHECODE_HOST, RIGOBOT_HOST } from "./constants"
3
3
  import { Lesson } from "../components/LessonItem"
4
4
  import { randomUUID } from "./creatorUtils"
5
+ import { Syllabus } from "./store"
5
6
 
6
7
  export function parseLesson(input: string, previous: Lesson[]): Lesson | null {
7
8
  const pattern = /^([\d.]+)\s*-\s*(.*?)\s*\[(\w+):\s*(.+)\]$/
@@ -289,3 +290,23 @@ export const generateImage = async (
289
290
  return null
290
291
  }
291
292
  }
293
+
294
+ export const createCourse = async (
295
+ syllabus: Syllabus,
296
+ token: string,
297
+ breathecodeToken: string
298
+ ) => {
299
+ const response = await axios.post(
300
+ `/actions/create-course`,
301
+ {
302
+ syllabus,
303
+ },
304
+ {
305
+ headers: {
306
+ "x-breathecode-token": breathecodeToken,
307
+ "x-rigo-token": token,
308
+ },
309
+ }
310
+ )
311
+ return response.data
312
+ }