@learnpack/learnpack 5.0.26 → 5.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/lib/commands/init.js +60 -117
- package/lib/utils/creatorUtilities.d.ts +44 -0
- package/lib/utils/creatorUtilities.js +111 -0
- package/lib/utils/rigoActions.d.ts +3 -0
- package/lib/utils/rigoActions.js +11 -0
- package/oclif.manifest.json +1 -1
- package/package.json +3 -1
- package/src/commands/init.ts +99 -171
- package/src/utils/creatorUtilities.ts +147 -0
- package/src/utils/rigoActions.ts +22 -4
package/README.md
CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @learnpack/learnpack
|
|
21
21
|
$ learnpack COMMAND
|
22
22
|
running command...
|
23
23
|
$ learnpack (-v|--version|version)
|
24
|
-
@learnpack/learnpack/5.0.
|
24
|
+
@learnpack/learnpack/5.0.27 win32-x64 node-v20.16.0
|
25
25
|
$ learnpack --help [COMMAND]
|
26
26
|
USAGE
|
27
27
|
$ learnpack COMMAND
|
@@ -75,7 +75,7 @@ DESCRIPTION
|
|
75
75
|
12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
|
76
76
|
```
|
77
77
|
|
78
|
-
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
78
|
+
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\audit.ts)_
|
79
79
|
|
80
80
|
## `learnpack clean`
|
81
81
|
|
@@ -90,7 +90,7 @@ DESCRIPTION
|
|
90
90
|
Extra documentation goes here
|
91
91
|
```
|
92
92
|
|
93
|
-
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
93
|
+
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\clean.ts)_
|
94
94
|
|
95
95
|
## `learnpack download [PACKAGE]`
|
96
96
|
|
@@ -108,7 +108,7 @@ DESCRIPTION
|
|
108
108
|
Extra documentation goes here
|
109
109
|
```
|
110
110
|
|
111
|
-
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
111
|
+
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\download.ts)_
|
112
112
|
|
113
113
|
## `learnpack help [COMMAND]`
|
114
114
|
|
@@ -140,7 +140,7 @@ OPTIONS
|
|
140
140
|
-y, --yes Skip all prompts and initialize an empty project
|
141
141
|
```
|
142
142
|
|
143
|
-
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
143
|
+
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\init.ts)_
|
144
144
|
|
145
145
|
## `learnpack login [PACKAGE]`
|
146
146
|
|
@@ -158,7 +158,7 @@ DESCRIPTION
|
|
158
158
|
Extra documentation goes here
|
159
159
|
```
|
160
160
|
|
161
|
-
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
161
|
+
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\login.ts)_
|
162
162
|
|
163
163
|
## `learnpack logout [PACKAGE]`
|
164
164
|
|
@@ -176,7 +176,7 @@ DESCRIPTION
|
|
176
176
|
Extra documentation goes here
|
177
177
|
```
|
178
178
|
|
179
|
-
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
179
|
+
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\logout.ts)_
|
180
180
|
|
181
181
|
## `learnpack plugins`
|
182
182
|
|
@@ -307,7 +307,7 @@ OPTIONS
|
|
307
307
|
-h, --help show CLI help
|
308
308
|
```
|
309
309
|
|
310
|
-
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
310
|
+
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\publish.ts)_
|
311
311
|
|
312
312
|
## `learnpack start`
|
313
313
|
|
@@ -329,7 +329,7 @@ OPTIONS
|
|
329
329
|
-y, --yes Skip all prompts and initialize an empty project
|
330
330
|
```
|
331
331
|
|
332
|
-
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
332
|
+
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\start.ts)_
|
333
333
|
|
334
334
|
## `learnpack test [EXERCISESLUG]`
|
335
335
|
|
@@ -346,7 +346,7 @@ OPTIONS
|
|
346
346
|
-y, --yes Skip all prompts and initialize an empty project
|
347
347
|
```
|
348
348
|
|
349
|
-
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
349
|
+
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\test.ts)_
|
350
350
|
|
351
351
|
## `learnpack translate`
|
352
352
|
|
@@ -360,7 +360,7 @@ OPTIONS
|
|
360
360
|
-y, --yes Skip all prompts and initialize an empty project
|
361
361
|
```
|
362
362
|
|
363
|
-
_See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
363
|
+
_See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\translate.ts)_
|
364
364
|
<!-- commandsstop -->
|
365
365
|
|
366
366
|
> > > > > > > 0cb3e56d84c197f9d008836bb573eade212b7e57
|
package/lib/commands/init.js
CHANGED
@@ -7,92 +7,39 @@ const fs = require("fs-extra");
|
|
7
7
|
const prompts = require("prompts");
|
8
8
|
const cli_ux_1 = require("cli-ux");
|
9
9
|
const eta = require("eta");
|
10
|
-
const api_1 = require("../utils/api");
|
11
10
|
const console_1 = require("../utils/console");
|
12
11
|
const errors_1 = require("../utils/errors");
|
13
12
|
const path = require("path");
|
14
13
|
const rigoActions_1 = require("../utils/rigoActions");
|
15
|
-
const
|
16
|
-
const
|
17
|
-
|
18
|
-
.toString()
|
19
|
-
.normalize("NFD")
|
20
|
-
.replace(/[\u0300-\u036F]/g, "")
|
21
|
-
.toLowerCase()
|
22
|
-
.trim()
|
23
|
-
.replace(/\s+/g, "-")
|
24
|
-
.replace(/[^\w-]+/g, "");
|
25
|
-
};
|
26
|
-
const getExInfo = (title) => {
|
27
|
-
// Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
|
28
|
-
let [exNumber, exTitle] = title.split(" - ");
|
29
|
-
// Extract kind and description
|
30
|
-
const kindMatch = exTitle.match(/\[(.*?):(.*?)]/);
|
31
|
-
const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read";
|
32
|
-
const description = kindMatch ? kindMatch[2].trim() : "";
|
33
|
-
exNumber = exNumber.trim();
|
34
|
-
// Clean title
|
35
|
-
exTitle = exTitle.replace((kindMatch === null || kindMatch === void 0 ? void 0 : kindMatch[0]) || "", "").trim();
|
36
|
-
exTitle = slugify(exTitle);
|
37
|
-
return {
|
38
|
-
exNumber,
|
39
|
-
kind,
|
40
|
-
description,
|
41
|
-
exTitle,
|
42
|
-
};
|
43
|
-
};
|
44
|
-
function extractImagesFromMarkdown(markdown) {
|
45
|
-
const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g;
|
46
|
-
const images = [];
|
47
|
-
let match;
|
48
|
-
while ((match = imageRegex.exec(markdown)) !== null) {
|
49
|
-
const altText = match[1];
|
50
|
-
const url = match[2];
|
51
|
-
images.push({ alt: altText, url: url });
|
52
|
-
}
|
53
|
-
return images;
|
54
|
-
}
|
55
|
-
function getFilenameFromUrl(url) {
|
56
|
-
return path.basename(url);
|
57
|
-
}
|
58
|
-
const makePackageInfo = (choices) => {
|
59
|
-
const packageInfo = {
|
60
|
-
grading: choices.grading,
|
61
|
-
difficulty: choices.difficulty,
|
62
|
-
duration: parseInt(choices.duration),
|
63
|
-
description: {
|
64
|
-
us: choices.description,
|
65
|
-
},
|
66
|
-
title: {
|
67
|
-
us: choices.title,
|
68
|
-
},
|
69
|
-
slug: choices.title
|
70
|
-
.toLowerCase()
|
71
|
-
.replace(/ /g, "-")
|
72
|
-
.replace(/[^\w-]+/g, ""),
|
73
|
-
};
|
74
|
-
return packageInfo;
|
75
|
-
};
|
76
|
-
async function createPreviewReadme(tutorialDir, packageInfo, rigoToken, readmeContents) {
|
77
|
-
const readmeFilename = `README.md`;
|
78
|
-
const readmeContent = await (0, rigoActions_1.generateCourseIntroduction)(rigoToken, {
|
79
|
-
course_title: packageInfo.title.us,
|
80
|
-
lessons_context: readmeContents.join("\n"),
|
81
|
-
});
|
82
|
-
fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer);
|
83
|
-
}
|
14
|
+
const api_1 = require("../utils/api");
|
15
|
+
const creatorUtilities_1 = require("../utils/creatorUtilities");
|
16
|
+
const session_1 = require("../managers/session");
|
84
17
|
const initializeInteractiveCreation = async (rigoToken, courseInfo) => {
|
85
18
|
let prevInteractions = "";
|
86
19
|
let isReady = false;
|
87
20
|
let currentSteps = [];
|
21
|
+
let currentTitle = "";
|
22
|
+
let currentDescription = "";
|
88
23
|
while (!isReady) {
|
24
|
+
let wholeInfo = courseInfo;
|
25
|
+
wholeInfo += `
|
26
|
+
Current title: ${currentTitle}
|
27
|
+
Current description: ${currentDescription}
|
28
|
+
`;
|
89
29
|
// eslint-disable-next-line
|
90
30
|
const res = await (0, rigoActions_1.interactiveCreation)(rigoToken, {
|
91
|
-
courseInfo:
|
31
|
+
courseInfo: wholeInfo,
|
92
32
|
prevInteractions: prevInteractions,
|
93
33
|
});
|
94
34
|
currentSteps = res.parsed.listOfSteps;
|
95
35
|
isReady = res.parsed.ready;
|
36
|
+
if (res.parsed.title && currentTitle !== res.parsed.title) {
|
37
|
+
currentTitle = res.parsed.title;
|
38
|
+
}
|
39
|
+
if (res.parsed.description &&
|
40
|
+
currentDescription !== res.parsed.description) {
|
41
|
+
currentDescription = res.parsed.description;
|
42
|
+
}
|
96
43
|
if (!isReady) {
|
97
44
|
console.log(currentSteps);
|
98
45
|
console_1.default.info(`AI: ${res.parsed.aiMessage}`);
|
@@ -109,42 +56,27 @@ const initializeInteractiveCreation = async (rigoToken, courseInfo) => {
|
|
109
56
|
}
|
110
57
|
}
|
111
58
|
return {
|
112
|
-
currentSteps,
|
113
|
-
|
59
|
+
steps: currentSteps,
|
60
|
+
title: currentTitle,
|
61
|
+
description: currentDescription,
|
62
|
+
interactions: prevInteractions,
|
114
63
|
};
|
115
64
|
};
|
116
65
|
const handleAILogic = async (tutorialDir, packageInfo) => {
|
117
|
-
console_1.default.info("Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/creators");
|
118
66
|
fs.removeSync(path.join(tutorialDir, "exercises", "01-hello-world"));
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
type: "password",
|
130
|
-
name: "password",
|
131
|
-
message: "What's your password?",
|
132
|
-
validate: (value) => {
|
133
|
-
return value.length > 0;
|
134
|
-
},
|
135
|
-
},
|
136
|
-
]);
|
137
|
-
let sessionPayload;
|
138
|
-
try {
|
139
|
-
sessionPayload = await api_1.default.login(loginPrompts.email, loginPrompts.password);
|
140
|
-
}
|
141
|
-
catch (error) {
|
142
|
-
console_1.default.error("Error trying to authenticate");
|
143
|
-
console_1.default.error(error.message || error);
|
144
|
-
return;
|
67
|
+
let sessionPayload = await session_1.default.getPayload();
|
68
|
+
if (!sessionPayload || !sessionPayload.rigobot) {
|
69
|
+
console_1.default.info("Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/creators");
|
70
|
+
try {
|
71
|
+
sessionPayload = await session_1.default.login();
|
72
|
+
}
|
73
|
+
catch (error) {
|
74
|
+
console_1.default.error("Error trying to authenticate");
|
75
|
+
console_1.default.error(error.message || error);
|
76
|
+
}
|
145
77
|
}
|
146
78
|
const rigoToken = sessionPayload.rigobot.key;
|
147
|
-
const consumables = await (0,
|
79
|
+
const consumables = await (0, api_1.getConsumables)(sessionPayload.token, "ai-generation");
|
148
80
|
if (consumables.ai_generation === 0) {
|
149
81
|
console_1.default.error("It seems you cannot generate tutorials with AI. Make sure you creator subscription is up to date here: https://4geeks.com/profile/subscriptions? . If you believe there is an issue you can always contact support@4geeks.com");
|
150
82
|
process.exit(1);
|
@@ -155,42 +87,53 @@ const handleAILogic = async (tutorialDir, packageInfo) => {
|
|
155
87
|
process.exit(1);
|
156
88
|
}
|
157
89
|
console_1.default.success("🎉 Let's begin this learning journey!");
|
158
|
-
|
90
|
+
let packageContext = `
|
159
91
|
\n
|
160
|
-
The following information comes from user inputs
|
161
92
|
Title: ${packageInfo.title.us}
|
162
93
|
Description: ${packageInfo.description.us}
|
163
|
-
Grading: ${packageInfo.grading}
|
164
94
|
Difficulty: ${packageInfo.difficulty}
|
165
95
|
Duration: ${packageInfo.duration}
|
166
|
-
|
167
|
-
Use it to generate more relevant exercises
|
168
96
|
`;
|
169
|
-
const {
|
97
|
+
const { steps, title, description } = await initializeInteractiveCreation(rigoToken, packageContext);
|
98
|
+
packageInfo.title.us = title;
|
99
|
+
packageInfo.description.us = description;
|
100
|
+
packageContext = `
|
101
|
+
Title: ${title}
|
102
|
+
Description: ${description}
|
103
|
+
Difficulty: ${packageInfo.difficulty}
|
104
|
+
Estimated duration: ${packageInfo.duration}
|
105
|
+
List of exercises: ${steps.join(", ")}
|
106
|
+
`;
|
170
107
|
const exercisesDir = path.join(tutorialDir, "exercises");
|
171
108
|
fs.ensureDirSync(exercisesDir);
|
172
109
|
console_1.default.info("Creating lessons...");
|
173
|
-
for (const [index, exercise] of
|
174
|
-
const { exNumber, exTitle } = getExInfo(exercise);
|
110
|
+
for (const [index, exercise] of steps.entries()) {
|
111
|
+
const { exNumber, exTitle } = (0, creatorUtilities_1.getExInfo)(exercise);
|
175
112
|
const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`);
|
176
113
|
fs.ensureDirSync(exerciseDir);
|
177
114
|
}
|
178
|
-
const exercisePromises =
|
179
|
-
const { exNumber, exTitle, kind, description } = getExInfo(exercise);
|
115
|
+
const exercisePromises = steps.map(async (exercise, index) => {
|
116
|
+
const { exNumber, exTitle, kind, description } = (0, creatorUtilities_1.getExInfo)(exercise);
|
180
117
|
const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`);
|
181
118
|
const readme = await (0, rigoActions_1.readmeCreator)(rigoToken, {
|
182
119
|
title: `${exNumber} - ${exTitle}`,
|
183
120
|
output_lang: "en",
|
184
|
-
list_of_exercises:
|
121
|
+
list_of_exercises: steps.join(","),
|
185
122
|
tutorial_description: packageContext,
|
186
123
|
lesson_description: description,
|
187
124
|
kind: kind.toLowerCase(),
|
188
125
|
});
|
126
|
+
const { exceedsThreshold, newMarkdown } = (0, creatorUtilities_1.checkReadingTime)(readme.parsed.content, 200);
|
127
|
+
if (exceedsThreshold) {
|
128
|
+
console_1.default.error("The reading time exceeds the threshold");
|
129
|
+
console_1.default.info(`Please reduce the reading time of the lesson, current reading time is ${exceedsThreshold} minutes`);
|
130
|
+
}
|
189
131
|
const readmeFilename = "README.md";
|
190
|
-
fs.writeFileSync(path.join(exerciseDir, readmeFilename),
|
132
|
+
fs.writeFileSync(path.join(exerciseDir, readmeFilename), newMarkdown);
|
191
133
|
if (kind.toLowerCase() === "code") {
|
192
134
|
const codeFile = await (0, rigoActions_1.createCodeFile)(rigoToken, {
|
193
135
|
readme: readme.parsed.content,
|
136
|
+
tutorial_info: packageContext,
|
194
137
|
});
|
195
138
|
fs.writeFileSync(path.join(exerciseDir, `app.${codeFile.parsed.extension.replace(".", "")}`), codeFile.parsed.content);
|
196
139
|
}
|
@@ -201,11 +144,11 @@ const handleAILogic = async (tutorialDir, packageInfo) => {
|
|
201
144
|
console_1.default.success("Lessons created! 🎉");
|
202
145
|
console_1.default.info("Generating images for the lessons...");
|
203
146
|
for (const content of readmeContents) {
|
204
|
-
imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)];
|
147
|
+
imagesArray = [...imagesArray, ...(0, creatorUtilities_1.extractImagesFromMarkdown)(content)];
|
205
148
|
}
|
206
149
|
const imagePromises = imagesArray.map(async (image) => {
|
207
150
|
try {
|
208
|
-
const filename = getFilenameFromUrl(image.url);
|
151
|
+
const filename = (0, creatorUtilities_1.getFilenameFromUrl)(image.url);
|
209
152
|
const imagePath = path.join(tutorialDir, ".learn", "assets", filename);
|
210
153
|
const res = await (0, rigoActions_1.generateImage)(rigoToken, { prompt: image.alt });
|
211
154
|
await (0, rigoActions_1.downloadImage)(res.image_url, imagePath);
|
@@ -219,7 +162,7 @@ const handleAILogic = async (tutorialDir, packageInfo) => {
|
|
219
162
|
await Promise.all(imagePromises);
|
220
163
|
console_1.default.info("Images generated successfully! 🎉 Your tutorial will be ready soon!");
|
221
164
|
console_1.default.info("Creating preview readme...");
|
222
|
-
await createPreviewReadme(tutorialDir, packageInfo, rigoToken, readmeContents);
|
165
|
+
await (0, rigoActions_1.createPreviewReadme)(tutorialDir, packageInfo, rigoToken, readmeContents);
|
223
166
|
return true;
|
224
167
|
};
|
225
168
|
const getChoices = async (empty) => {
|
@@ -307,7 +250,7 @@ class InitComand extends BaseCommand_1.default {
|
|
307
250
|
const { flags } = this.parse(InitComand);
|
308
251
|
await alreadyInitialized();
|
309
252
|
const choices = await getChoices(flags.yes);
|
310
|
-
const packageInfo = makePackageInfo(choices);
|
253
|
+
const packageInfo = (0, creatorUtilities_1.makePackageInfo)(choices);
|
311
254
|
const tutorialDir = `./${packageInfo.slug}`;
|
312
255
|
fs.ensureDirSync(tutorialDir);
|
313
256
|
const templatesDir = path.resolve(__dirname, "../../src/utils/templates/" + (choices.grading || "no-grading"));
|
@@ -0,0 +1,44 @@
|
|
1
|
+
type TEstimateReadingTimeReturns = {
|
2
|
+
minutes: number;
|
3
|
+
words: number;
|
4
|
+
};
|
5
|
+
export declare const estimateReadingTime: (text: string, wordsPerMinute?: number) => TEstimateReadingTimeReturns;
|
6
|
+
export type PackageInfo = {
|
7
|
+
grading: string;
|
8
|
+
difficulty: string;
|
9
|
+
duration: number;
|
10
|
+
description: {
|
11
|
+
us: string;
|
12
|
+
};
|
13
|
+
title: {
|
14
|
+
us: string;
|
15
|
+
};
|
16
|
+
};
|
17
|
+
export declare function checkReadingTime(markdown: string, wordsPerMinute?: number): {
|
18
|
+
newMarkdown: string;
|
19
|
+
exceedsThreshold: boolean;
|
20
|
+
};
|
21
|
+
export declare const getExInfo: (title: string) => {
|
22
|
+
exNumber: string;
|
23
|
+
kind: string;
|
24
|
+
description: string;
|
25
|
+
exTitle: string;
|
26
|
+
};
|
27
|
+
export declare function extractImagesFromMarkdown(markdown: string): {
|
28
|
+
alt: string;
|
29
|
+
url: string;
|
30
|
+
}[];
|
31
|
+
export declare function getFilenameFromUrl(url: string): string;
|
32
|
+
export declare const makePackageInfo: (choices: any) => {
|
33
|
+
grading: any;
|
34
|
+
difficulty: any;
|
35
|
+
duration: number;
|
36
|
+
description: {
|
37
|
+
us: any;
|
38
|
+
};
|
39
|
+
title: {
|
40
|
+
us: any;
|
41
|
+
};
|
42
|
+
slug: any;
|
43
|
+
};
|
44
|
+
export {};
|
@@ -0,0 +1,111 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.makePackageInfo = exports.getExInfo = exports.estimateReadingTime = void 0;
|
4
|
+
exports.checkReadingTime = checkReadingTime;
|
5
|
+
exports.extractImagesFromMarkdown = extractImagesFromMarkdown;
|
6
|
+
exports.getFilenameFromUrl = getFilenameFromUrl;
|
7
|
+
// eslint-disable-next-line
|
8
|
+
const frontMatter = require("front-matter");
|
9
|
+
const path = require("path");
|
10
|
+
const yaml = require("js-yaml");
|
11
|
+
const estimateReadingTime = (text, wordsPerMinute = 150) => {
|
12
|
+
const words = text.trim().split(/\s+/).length;
|
13
|
+
const minutes = words / wordsPerMinute;
|
14
|
+
if (minutes < 1) {
|
15
|
+
if (words === 0)
|
16
|
+
return {
|
17
|
+
minutes: 1,
|
18
|
+
words,
|
19
|
+
};
|
20
|
+
}
|
21
|
+
else {
|
22
|
+
return {
|
23
|
+
minutes,
|
24
|
+
words,
|
25
|
+
};
|
26
|
+
}
|
27
|
+
return {
|
28
|
+
minutes: 1,
|
29
|
+
words,
|
30
|
+
};
|
31
|
+
};
|
32
|
+
exports.estimateReadingTime = estimateReadingTime;
|
33
|
+
function checkReadingTime(markdown, wordsPerMinute = 150) {
|
34
|
+
const parsed = frontMatter(markdown);
|
35
|
+
const readingTime = (0, exports.estimateReadingTime)(parsed.body, wordsPerMinute);
|
36
|
+
let attributes = parsed.attributes ? parsed.attributes : {};
|
37
|
+
if (typeof parsed.attributes !== "object") {
|
38
|
+
attributes = {};
|
39
|
+
}
|
40
|
+
const updatedAttributes = Object.assign(Object.assign({}, attributes), { readingTime });
|
41
|
+
// Convert the front matter back to a proper YAML string
|
42
|
+
const yamlFrontMatter = yaml.dump(updatedAttributes).trim();
|
43
|
+
// Reconstruct the markdown with the front matter
|
44
|
+
const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`;
|
45
|
+
return {
|
46
|
+
newMarkdown,
|
47
|
+
exceedsThreshold: readingTime.minutes > wordsPerMinute,
|
48
|
+
};
|
49
|
+
}
|
50
|
+
const slugify = (text) => {
|
51
|
+
return text
|
52
|
+
.toString()
|
53
|
+
.normalize("NFD")
|
54
|
+
.replace(/[\u0300-\u036F]/g, "")
|
55
|
+
.toLowerCase()
|
56
|
+
.trim()
|
57
|
+
.replace(/\s+/g, "-")
|
58
|
+
.replace(/[^\w-]+/g, "");
|
59
|
+
};
|
60
|
+
const getExInfo = (title) => {
|
61
|
+
// Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
|
62
|
+
let [exNumber, exTitle] = title.split(" - ");
|
63
|
+
// Extract kind and description
|
64
|
+
const kindMatch = exTitle.match(/\[(.*?):(.*?)]/);
|
65
|
+
const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read";
|
66
|
+
const description = kindMatch ? kindMatch[2].trim() : "";
|
67
|
+
exNumber = exNumber.trim();
|
68
|
+
// Clean title
|
69
|
+
exTitle = exTitle.replace((kindMatch === null || kindMatch === void 0 ? void 0 : kindMatch[0]) || "", "").trim();
|
70
|
+
exTitle = slugify(exTitle);
|
71
|
+
return {
|
72
|
+
exNumber,
|
73
|
+
kind,
|
74
|
+
description,
|
75
|
+
exTitle,
|
76
|
+
};
|
77
|
+
};
|
78
|
+
exports.getExInfo = getExInfo;
|
79
|
+
function extractImagesFromMarkdown(markdown) {
|
80
|
+
const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g;
|
81
|
+
const images = [];
|
82
|
+
let match;
|
83
|
+
while ((match = imageRegex.exec(markdown)) !== null) {
|
84
|
+
const altText = match[1];
|
85
|
+
const url = match[2];
|
86
|
+
images.push({ alt: altText, url: url });
|
87
|
+
}
|
88
|
+
return images;
|
89
|
+
}
|
90
|
+
function getFilenameFromUrl(url) {
|
91
|
+
return path.basename(url);
|
92
|
+
}
|
93
|
+
const makePackageInfo = (choices) => {
|
94
|
+
const packageInfo = {
|
95
|
+
grading: choices.grading,
|
96
|
+
difficulty: choices.difficulty,
|
97
|
+
duration: parseInt(choices.duration),
|
98
|
+
description: {
|
99
|
+
us: choices.description,
|
100
|
+
},
|
101
|
+
title: {
|
102
|
+
us: choices.title,
|
103
|
+
},
|
104
|
+
slug: choices.title
|
105
|
+
.toLowerCase()
|
106
|
+
.replace(/ /g, "-")
|
107
|
+
.replace(/[^\w-]+/g, ""),
|
108
|
+
};
|
109
|
+
return packageInfo;
|
110
|
+
};
|
111
|
+
exports.makePackageInfo = makePackageInfo;
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { PackageInfo } from "./creatorUtilities";
|
1
2
|
type TCreateReadmeInputs = {
|
2
3
|
title: string;
|
3
4
|
output_lang: string;
|
@@ -30,6 +31,7 @@ type TInteractiveCreationInputs = {
|
|
30
31
|
export declare const interactiveCreation: (token: string, inputs: TInteractiveCreationInputs) => Promise<any>;
|
31
32
|
type TCreateCodeFileInputs = {
|
32
33
|
readme: string;
|
34
|
+
tutorial_info: string;
|
33
35
|
};
|
34
36
|
export declare const createCodeFile: (token: string, inputs: TCreateCodeFileInputs) => Promise<any>;
|
35
37
|
type TCreateCodingReadmeInputs = {
|
@@ -49,4 +51,5 @@ type TReadmeCreatorInputs = {
|
|
49
51
|
kind: string;
|
50
52
|
};
|
51
53
|
export declare const readmeCreator: (token: string, inputs: TReadmeCreatorInputs) => Promise<any>;
|
54
|
+
export declare function createPreviewReadme(tutorialDir: string, packageInfo: PackageInfo, rigoToken: string, readmeContents: string[]): Promise<void>;
|
52
55
|
export {};
|
package/lib/utils/rigoActions.js
CHANGED
@@ -2,9 +2,12 @@
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
|
4
4
|
exports.downloadImage = downloadImage;
|
5
|
+
exports.createPreviewReadme = createPreviewReadme;
|
5
6
|
const axios_1 = require("axios");
|
6
7
|
const fs_1 = require("fs");
|
7
8
|
const console_1 = require("../utils/console");
|
9
|
+
const fs = require("fs");
|
10
|
+
const path = require("path");
|
8
11
|
const RIGOBOT_HOST = "https://rigobot.herokuapp.com";
|
9
12
|
const createReadme = async (token, inputs) => {
|
10
13
|
try {
|
@@ -164,3 +167,11 @@ const readmeCreator = async (token, inputs) => {
|
|
164
167
|
throw new Error("Invalid kind of lesson");
|
165
168
|
};
|
166
169
|
exports.readmeCreator = readmeCreator;
|
170
|
+
async function createPreviewReadme(tutorialDir, packageInfo, rigoToken, readmeContents) {
|
171
|
+
const readmeFilename = `README.md`;
|
172
|
+
const readmeContent = await (0, exports.generateCourseIntroduction)(rigoToken, {
|
173
|
+
course_title: packageInfo.title.us,
|
174
|
+
lessons_context: readmeContents.join("\n"),
|
175
|
+
});
|
176
|
+
fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer);
|
177
|
+
}
|
package/oclif.manifest.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":"5.0.
|
1
|
+
{"version":"5.0.27","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":{},"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":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","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.
|
4
|
+
"version": "5.0.27",
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
6
6
|
"contributors": [
|
7
7
|
{
|
@@ -41,6 +41,7 @@
|
|
41
41
|
"eta": "^1.2.0",
|
42
42
|
"express": "^4.17.1",
|
43
43
|
"front-matter": "^4.0.2",
|
44
|
+
"js-yaml": "^4.1.0",
|
44
45
|
"moment": "^2.27.0",
|
45
46
|
"node-emoji": "^1.10.0",
|
46
47
|
"node-fetch": "^2.7.0",
|
@@ -60,6 +61,7 @@
|
|
60
61
|
"@types/debounce": "^1.2.1",
|
61
62
|
"@types/express": "^4.17.13",
|
62
63
|
"@types/fs-extra": "^8.1.0",
|
64
|
+
"@types/js-yaml": "^4.0.9",
|
63
65
|
"@types/mocha": "^5",
|
64
66
|
"@types/mock-fs": "^4.13.1",
|
65
67
|
"@types/node": "^10.17.60",
|
package/src/commands/init.ts
CHANGED
@@ -15,128 +15,59 @@ import {
|
|
15
15
|
hasCreatorPermission,
|
16
16
|
generateImage,
|
17
17
|
downloadImage,
|
18
|
-
generateCourseIntroduction,
|
19
18
|
interactiveCreation,
|
20
19
|
createCodeFile,
|
21
20
|
readmeCreator,
|
21
|
+
createPreviewReadme,
|
22
22
|
} from "../utils/rigoActions"
|
23
23
|
import { getConsumables } from "../utils/api"
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
.replace(/[^\w-]+/g, "")
|
34
|
-
}
|
35
|
-
|
36
|
-
const getExInfo = (title: string) => {
|
37
|
-
// Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
|
38
|
-
let [exNumber, exTitle] = title.split(" - ")
|
39
|
-
|
40
|
-
// Extract kind and description
|
41
|
-
const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
|
42
|
-
const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
|
43
|
-
const description = kindMatch ? kindMatch[2].trim() : ""
|
44
|
-
|
45
|
-
exNumber = exNumber.trim()
|
46
|
-
// Clean title
|
47
|
-
exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
|
48
|
-
exTitle = slugify(exTitle)
|
49
|
-
|
50
|
-
return {
|
51
|
-
exNumber,
|
52
|
-
kind,
|
53
|
-
description,
|
54
|
-
exTitle,
|
55
|
-
}
|
56
|
-
}
|
57
|
-
|
58
|
-
function extractImagesFromMarkdown(markdown: string) {
|
59
|
-
const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
|
60
|
-
const images = []
|
61
|
-
let match
|
62
|
-
|
63
|
-
while ((match = imageRegex.exec(markdown)) !== null) {
|
64
|
-
const altText = match[1]
|
65
|
-
const url = match[2]
|
66
|
-
images.push({ alt: altText, url: url })
|
67
|
-
}
|
68
|
-
|
69
|
-
return images
|
70
|
-
}
|
71
|
-
|
72
|
-
function getFilenameFromUrl(url: string) {
|
73
|
-
return path.basename(url)
|
74
|
-
}
|
75
|
-
|
76
|
-
const makePackageInfo = (choices: any) => {
|
77
|
-
const packageInfo = {
|
78
|
-
grading: choices.grading,
|
79
|
-
difficulty: choices.difficulty,
|
80
|
-
duration: parseInt(choices.duration),
|
81
|
-
description: {
|
82
|
-
us: choices.description,
|
83
|
-
},
|
84
|
-
title: {
|
85
|
-
us: choices.title,
|
86
|
-
},
|
87
|
-
slug: choices.title
|
88
|
-
.toLowerCase()
|
89
|
-
.replace(/ /g, "-")
|
90
|
-
.replace(/[^\w-]+/g, ""),
|
91
|
-
}
|
92
|
-
return packageInfo
|
93
|
-
}
|
94
|
-
|
95
|
-
async function createPreviewReadme(
|
96
|
-
tutorialDir: string,
|
97
|
-
packageInfo: PackageInfo,
|
98
|
-
rigoToken: string,
|
99
|
-
readmeContents: string[]
|
100
|
-
) {
|
101
|
-
const readmeFilename = `README.md`
|
102
|
-
|
103
|
-
const readmeContent = await generateCourseIntroduction(rigoToken, {
|
104
|
-
course_title: packageInfo.title.us,
|
105
|
-
lessons_context: readmeContents.join("\n"),
|
106
|
-
})
|
107
|
-
fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer)
|
108
|
-
}
|
109
|
-
|
110
|
-
type PackageInfo = {
|
111
|
-
grading: string
|
112
|
-
difficulty: string
|
113
|
-
duration: number
|
114
|
-
description: {
|
115
|
-
us: string
|
116
|
-
}
|
117
|
-
title: {
|
118
|
-
us: string
|
119
|
-
}
|
120
|
-
}
|
24
|
+
import {
|
25
|
+
checkReadingTime,
|
26
|
+
PackageInfo,
|
27
|
+
getExInfo,
|
28
|
+
extractImagesFromMarkdown,
|
29
|
+
getFilenameFromUrl,
|
30
|
+
makePackageInfo,
|
31
|
+
} from "../utils/creatorUtilities"
|
32
|
+
import SessionManager from "../managers/session"
|
121
33
|
|
122
34
|
const initializeInteractiveCreation = async (
|
123
35
|
rigoToken: string,
|
124
36
|
courseInfo: string
|
125
37
|
): Promise<{
|
126
|
-
|
127
|
-
|
38
|
+
steps: string[]
|
39
|
+
title: string
|
40
|
+
description: string
|
41
|
+
interactions: string
|
128
42
|
}> => {
|
129
43
|
let prevInteractions = ""
|
130
44
|
let isReady = false
|
131
45
|
let currentSteps = []
|
46
|
+
let currentTitle = ""
|
47
|
+
let currentDescription = ""
|
132
48
|
while (!isReady) {
|
49
|
+
let wholeInfo = courseInfo
|
50
|
+
wholeInfo += `
|
51
|
+
Current title: ${currentTitle}
|
52
|
+
Current description: ${currentDescription}
|
53
|
+
`
|
133
54
|
// eslint-disable-next-line
|
134
55
|
const res = await interactiveCreation(rigoToken, {
|
135
|
-
courseInfo:
|
56
|
+
courseInfo: wholeInfo,
|
136
57
|
prevInteractions: prevInteractions,
|
137
58
|
})
|
138
59
|
currentSteps = res.parsed.listOfSteps
|
139
60
|
isReady = res.parsed.ready
|
61
|
+
if (res.parsed.title && currentTitle !== res.parsed.title) {
|
62
|
+
currentTitle = res.parsed.title
|
63
|
+
}
|
64
|
+
|
65
|
+
if (
|
66
|
+
res.parsed.description &&
|
67
|
+
currentDescription !== res.parsed.description
|
68
|
+
) {
|
69
|
+
currentDescription = res.parsed.description
|
70
|
+
}
|
140
71
|
|
141
72
|
if (!isReady) {
|
142
73
|
console.log(currentSteps)
|
@@ -155,44 +86,28 @@ const initializeInteractiveCreation = async (
|
|
155
86
|
}
|
156
87
|
|
157
88
|
return {
|
158
|
-
currentSteps,
|
159
|
-
|
89
|
+
steps: currentSteps,
|
90
|
+
title: currentTitle,
|
91
|
+
description: currentDescription,
|
92
|
+
interactions: prevInteractions,
|
160
93
|
}
|
161
94
|
}
|
162
95
|
|
163
96
|
const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
|
164
|
-
Console.info(
|
165
|
-
"Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/creators"
|
166
|
-
)
|
167
|
-
|
168
97
|
fs.removeSync(path.join(tutorialDir, "exercises", "01-hello-world"))
|
169
98
|
|
170
|
-
|
171
|
-
{
|
172
|
-
type: "text",
|
173
|
-
name: "email",
|
174
|
-
message: "What's your email?",
|
175
|
-
validate: (value: string) => {
|
176
|
-
return value.length > 0 && value.includes("@")
|
177
|
-
},
|
178
|
-
},
|
179
|
-
{
|
180
|
-
type: "password",
|
181
|
-
name: "password",
|
182
|
-
message: "What's your password?",
|
183
|
-
validate: (value: string) => {
|
184
|
-
return value.length > 0
|
185
|
-
},
|
186
|
-
},
|
187
|
-
])
|
99
|
+
let sessionPayload = await SessionManager.getPayload()
|
188
100
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
101
|
+
if (!sessionPayload || !sessionPayload.rigobot) {
|
102
|
+
Console.info(
|
103
|
+
"Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/creators"
|
104
|
+
)
|
105
|
+
try {
|
106
|
+
sessionPayload = await SessionManager.login()
|
107
|
+
} catch (error) {
|
108
|
+
Console.error("Error trying to authenticate")
|
109
|
+
Console.error((error as TypeError).message || (error as string))
|
110
|
+
}
|
196
111
|
}
|
197
112
|
|
198
113
|
const rigoToken = sessionPayload.rigobot.key
|
@@ -219,71 +134,84 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
|
|
219
134
|
|
220
135
|
Console.success("🎉 Let's begin this learning journey!")
|
221
136
|
|
222
|
-
|
137
|
+
let packageContext = `
|
223
138
|
\n
|
224
|
-
The following information comes from user inputs
|
225
139
|
Title: ${packageInfo.title.us}
|
226
140
|
Description: ${packageInfo.description.us}
|
227
|
-
Grading: ${packageInfo.grading}
|
228
141
|
Difficulty: ${packageInfo.difficulty}
|
229
142
|
Duration: ${packageInfo.duration}
|
230
|
-
|
231
|
-
Use it to generate more relevant exercises
|
232
143
|
`
|
233
144
|
|
234
|
-
const {
|
145
|
+
const { steps, title, description } = await initializeInteractiveCreation(
|
235
146
|
rigoToken,
|
236
147
|
packageContext
|
237
148
|
)
|
149
|
+
packageInfo.title.us = title
|
150
|
+
packageInfo.description.us = description
|
238
151
|
|
152
|
+
packageContext = `
|
153
|
+
Title: ${title}
|
154
|
+
Description: ${description}
|
155
|
+
Difficulty: ${packageInfo.difficulty}
|
156
|
+
Estimated duration: ${packageInfo.duration}
|
157
|
+
List of exercises: ${steps.join(", ")}
|
158
|
+
`
|
239
159
|
const exercisesDir = path.join(tutorialDir, "exercises")
|
240
160
|
fs.ensureDirSync(exercisesDir)
|
241
161
|
|
242
162
|
Console.info("Creating lessons...")
|
243
|
-
for (const [index, exercise] of
|
163
|
+
for (const [index, exercise] of steps.entries()) {
|
244
164
|
const { exNumber, exTitle } = getExInfo(exercise)
|
245
165
|
const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`)
|
246
166
|
fs.ensureDirSync(exerciseDir)
|
247
167
|
}
|
248
168
|
|
249
|
-
const exercisePromises =
|
250
|
-
|
251
|
-
|
252
|
-
const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`)
|
253
|
-
|
254
|
-
const readme = await readmeCreator(rigoToken, {
|
255
|
-
title: `${exNumber} - ${exTitle}`,
|
256
|
-
output_lang: "en",
|
257
|
-
list_of_exercises: currentSteps.join(","),
|
258
|
-
tutorial_description: packageContext,
|
259
|
-
lesson_description: description,
|
260
|
-
kind: kind.toLowerCase(),
|
261
|
-
})
|
169
|
+
const exercisePromises = steps.map(async (exercise: any, index: number) => {
|
170
|
+
const { exNumber, exTitle, kind, description } = getExInfo(exercise)
|
171
|
+
const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`)
|
262
172
|
|
263
|
-
|
173
|
+
const readme = await readmeCreator(rigoToken, {
|
174
|
+
title: `${exNumber} - ${exTitle}`,
|
175
|
+
output_lang: "en",
|
176
|
+
list_of_exercises: steps.join(","),
|
177
|
+
tutorial_description: packageContext,
|
178
|
+
lesson_description: description,
|
179
|
+
kind: kind.toLowerCase(),
|
180
|
+
})
|
264
181
|
|
265
|
-
|
266
|
-
|
267
|
-
|
182
|
+
const { exceedsThreshold, newMarkdown } = checkReadingTime(
|
183
|
+
readme.parsed.content,
|
184
|
+
200
|
185
|
+
)
|
186
|
+
|
187
|
+
if (exceedsThreshold) {
|
188
|
+
Console.error("The reading time exceeds the threshold")
|
189
|
+
Console.info(
|
190
|
+
`Please reduce the reading time of the lesson, current reading time is ${exceedsThreshold} minutes`
|
268
191
|
)
|
192
|
+
}
|
269
193
|
|
270
|
-
|
271
|
-
const codeFile = await createCodeFile(rigoToken, {
|
272
|
-
readme: readme.parsed.content,
|
273
|
-
})
|
274
|
-
|
275
|
-
fs.writeFileSync(
|
276
|
-
path.join(
|
277
|
-
exerciseDir,
|
278
|
-
`app.${codeFile.parsed.extension.replace(".", "")}`
|
279
|
-
),
|
280
|
-
codeFile.parsed.content
|
281
|
-
)
|
282
|
-
}
|
194
|
+
const readmeFilename = "README.md"
|
283
195
|
|
284
|
-
|
196
|
+
fs.writeFileSync(path.join(exerciseDir, readmeFilename), newMarkdown)
|
197
|
+
|
198
|
+
if (kind.toLowerCase() === "code") {
|
199
|
+
const codeFile = await createCodeFile(rigoToken, {
|
200
|
+
readme: readme.parsed.content,
|
201
|
+
tutorial_info: packageContext,
|
202
|
+
})
|
203
|
+
|
204
|
+
fs.writeFileSync(
|
205
|
+
path.join(
|
206
|
+
exerciseDir,
|
207
|
+
`app.${codeFile.parsed.extension.replace(".", "")}`
|
208
|
+
),
|
209
|
+
codeFile.parsed.content
|
210
|
+
)
|
285
211
|
}
|
286
|
-
|
212
|
+
|
213
|
+
return readme.parsed.content
|
214
|
+
})
|
287
215
|
|
288
216
|
let imagesArray: any[] = []
|
289
217
|
|
@@ -0,0 +1,147 @@
|
|
1
|
+
// eslint-disable-next-line
|
2
|
+
const frontMatter = require("front-matter")
|
3
|
+
import * as path from "path"
|
4
|
+
|
5
|
+
import * as yaml from "js-yaml"
|
6
|
+
|
7
|
+
type TEstimateReadingTimeReturns = {
|
8
|
+
minutes: number
|
9
|
+
words: number
|
10
|
+
}
|
11
|
+
|
12
|
+
export const estimateReadingTime = (
|
13
|
+
text: string,
|
14
|
+
wordsPerMinute = 150
|
15
|
+
): TEstimateReadingTimeReturns => {
|
16
|
+
const words = text.trim().split(/\s+/).length
|
17
|
+
const minutes = words / wordsPerMinute
|
18
|
+
|
19
|
+
if (minutes < 1) {
|
20
|
+
if (words === 0)
|
21
|
+
return {
|
22
|
+
minutes: 1,
|
23
|
+
words,
|
24
|
+
}
|
25
|
+
} else {
|
26
|
+
return {
|
27
|
+
minutes,
|
28
|
+
words,
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
return {
|
33
|
+
minutes: 1,
|
34
|
+
words,
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
export type PackageInfo = {
|
39
|
+
grading: string
|
40
|
+
difficulty: string
|
41
|
+
duration: number
|
42
|
+
description: {
|
43
|
+
us: string
|
44
|
+
}
|
45
|
+
title: {
|
46
|
+
us: string
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
export function checkReadingTime(
|
51
|
+
markdown: string,
|
52
|
+
wordsPerMinute = 150
|
53
|
+
): { newMarkdown: string; exceedsThreshold: boolean } {
|
54
|
+
const parsed = frontMatter(markdown)
|
55
|
+
const readingTime = estimateReadingTime(parsed.body, wordsPerMinute)
|
56
|
+
let attributes = parsed.attributes ? parsed.attributes : {}
|
57
|
+
|
58
|
+
if (typeof parsed.attributes !== "object") {
|
59
|
+
attributes = {}
|
60
|
+
}
|
61
|
+
|
62
|
+
const updatedAttributes = {
|
63
|
+
...attributes,
|
64
|
+
readingTime,
|
65
|
+
}
|
66
|
+
|
67
|
+
// Convert the front matter back to a proper YAML string
|
68
|
+
const yamlFrontMatter = yaml.dump(updatedAttributes).trim()
|
69
|
+
|
70
|
+
// Reconstruct the markdown with the front matter
|
71
|
+
const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`
|
72
|
+
|
73
|
+
return {
|
74
|
+
newMarkdown,
|
75
|
+
exceedsThreshold: readingTime.minutes > wordsPerMinute,
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
const slugify = (text: string) => {
|
80
|
+
return text
|
81
|
+
.toString()
|
82
|
+
.normalize("NFD")
|
83
|
+
.replace(/[\u0300-\u036F]/g, "")
|
84
|
+
.toLowerCase()
|
85
|
+
.trim()
|
86
|
+
.replace(/\s+/g, "-")
|
87
|
+
.replace(/[^\w-]+/g, "")
|
88
|
+
}
|
89
|
+
|
90
|
+
export const getExInfo = (title: string) => {
|
91
|
+
// Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
|
92
|
+
let [exNumber, exTitle] = title.split(" - ")
|
93
|
+
|
94
|
+
// Extract kind and description
|
95
|
+
const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
|
96
|
+
const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
|
97
|
+
const description = kindMatch ? kindMatch[2].trim() : ""
|
98
|
+
|
99
|
+
exNumber = exNumber.trim()
|
100
|
+
// Clean title
|
101
|
+
exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
|
102
|
+
exTitle = slugify(exTitle)
|
103
|
+
|
104
|
+
return {
|
105
|
+
exNumber,
|
106
|
+
kind,
|
107
|
+
description,
|
108
|
+
exTitle,
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
export function extractImagesFromMarkdown(markdown: string) {
|
113
|
+
const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
|
114
|
+
const images = []
|
115
|
+
let match
|
116
|
+
|
117
|
+
while ((match = imageRegex.exec(markdown)) !== null) {
|
118
|
+
const altText = match[1]
|
119
|
+
const url = match[2]
|
120
|
+
images.push({ alt: altText, url: url })
|
121
|
+
}
|
122
|
+
|
123
|
+
return images
|
124
|
+
}
|
125
|
+
|
126
|
+
export function getFilenameFromUrl(url: string) {
|
127
|
+
return path.basename(url)
|
128
|
+
}
|
129
|
+
|
130
|
+
export const makePackageInfo = (choices: any) => {
|
131
|
+
const packageInfo = {
|
132
|
+
grading: choices.grading,
|
133
|
+
difficulty: choices.difficulty,
|
134
|
+
duration: parseInt(choices.duration),
|
135
|
+
description: {
|
136
|
+
us: choices.description,
|
137
|
+
},
|
138
|
+
title: {
|
139
|
+
us: choices.title,
|
140
|
+
},
|
141
|
+
slug: choices.title
|
142
|
+
.toLowerCase()
|
143
|
+
.replace(/ /g, "-")
|
144
|
+
.replace(/[^\w-]+/g, ""),
|
145
|
+
}
|
146
|
+
return packageInfo
|
147
|
+
}
|
package/src/utils/rigoActions.ts
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
import axios from "axios"
|
2
2
|
import { writeFile } from "fs"
|
3
3
|
import Console from "../utils/console"
|
4
|
+
import { PackageInfo } from "./creatorUtilities"
|
5
|
+
import * as fs from "fs"
|
6
|
+
import * as path from "path"
|
4
7
|
|
5
8
|
const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
|
6
9
|
|
@@ -188,6 +191,7 @@ export const interactiveCreation = async (
|
|
188
191
|
|
189
192
|
type TCreateCodeFileInputs = {
|
190
193
|
readme: string
|
194
|
+
tutorial_info: string
|
191
195
|
}
|
192
196
|
|
193
197
|
export const createCodeFile = async (
|
@@ -262,7 +266,7 @@ export const readmeCreator = async (
|
|
262
266
|
return createReadme(token, createReadmeInputs)
|
263
267
|
}
|
264
268
|
|
265
|
-
|
269
|
+
if (inputs.kind === "code") {
|
266
270
|
return createCodingReadme(token, {
|
267
271
|
title: inputs.title,
|
268
272
|
output_lang: inputs.output_lang,
|
@@ -271,7 +275,21 @@ export const readmeCreator = async (
|
|
271
275
|
lesson_description: inputs.lesson_description,
|
272
276
|
})
|
273
277
|
}
|
274
|
-
|
275
|
-
|
276
|
-
|
278
|
+
|
279
|
+
throw new Error("Invalid kind of lesson")
|
280
|
+
}
|
281
|
+
|
282
|
+
export async function createPreviewReadme(
|
283
|
+
tutorialDir: string,
|
284
|
+
packageInfo: PackageInfo,
|
285
|
+
rigoToken: string,
|
286
|
+
readmeContents: string[]
|
287
|
+
) {
|
288
|
+
const readmeFilename = `README.md`
|
289
|
+
|
290
|
+
const readmeContent = await generateCourseIntroduction(rigoToken, {
|
291
|
+
course_title: packageInfo.title.us,
|
292
|
+
lessons_context: readmeContents.join("\n"),
|
293
|
+
})
|
294
|
+
fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer)
|
277
295
|
}
|