@learnpack/learnpack 5.0.27 → 5.0.29

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.
@@ -19,8 +19,9 @@ import {
19
19
  createCodeFile,
20
20
  readmeCreator,
21
21
  createPreviewReadme,
22
+ reduceReadme,
22
23
  } from "../utils/rigoActions"
23
- import { getConsumables } from "../utils/api"
24
+ import { getConsumable } from "../utils/api"
24
25
  import {
25
26
  checkReadingTime,
26
27
  PackageInfo,
@@ -28,9 +29,18 @@ import {
28
29
  extractImagesFromMarkdown,
29
30
  getFilenameFromUrl,
30
31
  makePackageInfo,
32
+ estimateDuration,
33
+ getContentIndex,
34
+ createFileOnDesktop,
31
35
  } from "../utils/creatorUtilities"
32
36
  import SessionManager from "../managers/session"
33
37
 
38
+ const durationByKind: Record<string, number> = {
39
+ code: 3,
40
+ quiz: 2,
41
+ read: 1,
42
+ }
43
+
34
44
  const initializeInteractiveCreation = async (
35
45
  rigoToken: string,
36
46
  courseInfo: string
@@ -39,12 +49,16 @@ const initializeInteractiveCreation = async (
39
49
  title: string
40
50
  description: string
41
51
  interactions: string
52
+ difficulty: string
53
+ duration: number
42
54
  }> => {
43
55
  let prevInteractions = ""
44
56
  let isReady = false
45
57
  let currentSteps = []
46
58
  let currentTitle = ""
47
59
  let currentDescription = ""
60
+ let currentDifficulty = ""
61
+
48
62
  while (!isReady) {
49
63
  let wholeInfo = courseInfo
50
64
  wholeInfo += `
@@ -69,6 +83,10 @@ const initializeInteractiveCreation = async (
69
83
  currentDescription = res.parsed.description
70
84
  }
71
85
 
86
+ if (res.parsed.difficulty && currentDifficulty !== res.parsed.difficulty) {
87
+ currentDifficulty = res.parsed.difficulty
88
+ }
89
+
72
90
  if (!isReady) {
73
91
  console.log(currentSteps)
74
92
  Console.info(`AI: ${res.parsed.aiMessage}`)
@@ -85,12 +103,47 @@ const initializeInteractiveCreation = async (
85
103
  }
86
104
  }
87
105
 
106
+ const duration = estimateDuration(currentSteps)
88
107
  return {
89
108
  steps: currentSteps,
90
109
  title: currentTitle,
91
110
  description: currentDescription,
92
111
  interactions: prevInteractions,
112
+ difficulty: currentDifficulty,
113
+ duration,
114
+ }
115
+ }
116
+
117
+ const appendContentIndex = async () => {
118
+ const choices = await prompts([
119
+ {
120
+ type: "confirm",
121
+ name: "contentIndex",
122
+ message: "Do you have a content index for this tutorial?",
123
+ },
124
+ ])
125
+ if (choices.contentIndex) {
126
+ await createFileOnDesktop()
127
+ Console.info(
128
+ "Please make the necessary in the recently created file in your desktop, it should automatically open. Edit the file to match your expectations and save it. Keep the same name and structure as the example file. Continue when ready."
129
+ )
130
+ const isReady = await prompts([
131
+ {
132
+ type: "confirm",
133
+ name: "isReady",
134
+ message: "Are you ready to continue?",
135
+ },
136
+ ])
137
+ if (!isReady.isReady) {
138
+ Console.error("Please make the necessary changes and try again.")
139
+ process.exit(1)
140
+ }
141
+
142
+ const contentIndex = getContentIndex()
143
+ return contentIndex
93
144
  }
145
+
146
+ return null
94
147
  }
95
148
 
96
149
  const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
@@ -98,7 +151,11 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
98
151
 
99
152
  let sessionPayload = await SessionManager.getPayload()
100
153
 
101
- if (!sessionPayload || !sessionPayload.rigobot) {
154
+ if (
155
+ !sessionPayload ||
156
+ !sessionPayload.rigobot ||
157
+ (sessionPayload.token && !(await api.validateToken(sessionPayload.token)))
158
+ ) {
102
159
  Console.info(
103
160
  "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
161
  )
@@ -112,16 +169,13 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
112
169
 
113
170
  const rigoToken = sessionPayload.rigobot.key
114
171
 
115
- const consumables = await getConsumables(
116
- sessionPayload.token,
117
- "ai-generation"
118
- )
172
+ const consumable = await getConsumable(sessionPayload.token, "ai-generation")
119
173
 
120
- if (consumables.ai_generation === 0) {
174
+ if (consumable.count === 0) {
121
175
  Console.error(
122
- "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"
176
+ "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"
123
177
  )
124
- process.exit(1)
178
+ // process.exit(1)
125
179
  }
126
180
 
127
181
  const isCreator = await hasCreatorPermission(rigoToken)
@@ -134,26 +188,32 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
134
188
 
135
189
  Console.success("🎉 Let's begin this learning journey!")
136
190
 
191
+ const contentIndex = await appendContentIndex()
192
+
137
193
  let packageContext = `
138
194
  \n
139
195
  Title: ${packageInfo.title.us}
140
196
  Description: ${packageInfo.description.us}
141
- Difficulty: ${packageInfo.difficulty}
142
- Duration: ${packageInfo.duration}
197
+
198
+ ${
199
+ contentIndex ?
200
+ `Content Index submitted by the user, use this to guide your creation: ${contentIndex}` :
201
+ ""
202
+ }
203
+
143
204
  `
144
205
 
145
- const { steps, title, description } = await initializeInteractiveCreation(
146
- rigoToken,
147
- packageContext
148
- )
206
+ const { steps, title, description, duration, difficulty } =
207
+ await initializeInteractiveCreation(rigoToken, packageContext)
149
208
  packageInfo.title.us = title
150
209
  packageInfo.description.us = description
210
+ packageInfo.duration = duration
211
+ packageInfo.difficulty = difficulty
151
212
 
152
213
  packageContext = `
153
214
  Title: ${title}
154
215
  Description: ${description}
155
- Difficulty: ${packageInfo.difficulty}
156
- Estimated duration: ${packageInfo.duration}
216
+
157
217
  List of exercises: ${steps.join(", ")}
158
218
  `
159
219
  const exercisesDir = path.join(tutorialDir, "exercises")
@@ -179,21 +239,38 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
179
239
  kind: kind.toLowerCase(),
180
240
  })
181
241
 
182
- const { exceedsThreshold, newMarkdown } = checkReadingTime(
242
+ const duration = durationByKind[kind.toLowerCase()]
243
+ let readingTime = checkReadingTime(
183
244
  readme.parsed.content,
184
- 200
245
+ 200,
246
+ duration || 1
185
247
  )
186
248
 
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`
191
- )
249
+ if (readingTime.exceedsThreshold) {
250
+ // Console.info(
251
+ // `The reading time for the lesson ${exTitle} exceeds the threshold, reducing it...`
252
+ // )
253
+ const reducedReadme = await reduceReadme(rigoToken, {
254
+ lesson: readingTime.body,
255
+ number_of_words: readingTime.minutes.toString(),
256
+ expected_number_words: "200",
257
+ })
258
+
259
+ if (reducedReadme) {
260
+ readingTime = checkReadingTime(
261
+ reducedReadme.parsed.content,
262
+ 200,
263
+ duration || 1
264
+ )
265
+ }
192
266
  }
193
267
 
194
268
  const readmeFilename = "README.md"
195
269
 
196
- fs.writeFileSync(path.join(exerciseDir, readmeFilename), newMarkdown)
270
+ fs.writeFileSync(
271
+ path.join(exerciseDir, readmeFilename),
272
+ readingTime.newMarkdown
273
+ )
197
274
 
198
275
  if (kind.toLowerCase() === "code") {
199
276
  const codeFile = await createCodeFile(rigoToken, {
@@ -210,15 +287,14 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
210
287
  )
211
288
  }
212
289
 
213
- return readme.parsed.content
290
+ return readingTime.newMarkdown
214
291
  })
215
292
 
216
- let imagesArray: any[] = []
217
-
218
293
  const readmeContents = await Promise.all(exercisePromises)
219
-
220
294
  Console.success("Lessons created! 🎉")
295
+
221
296
  Console.info("Generating images for the lessons...")
297
+ let imagesArray: any[] = []
222
298
 
223
299
  for (const content of readmeContents) {
224
300
  imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)]
@@ -255,7 +331,7 @@ const getChoices = async (empty: boolean) => {
255
331
  title: "My Interactive Tutorial",
256
332
  description: "",
257
333
  difficulty: "beginner",
258
- duration: 1,
334
+ duration: 5,
259
335
  useAI: "no",
260
336
  grading: "isolated",
261
337
  }
@@ -293,30 +369,7 @@ const getChoices = async (empty: boolean) => {
293
369
  initial: "",
294
370
  message: "Description for your tutorial? Press enter to leave blank",
295
371
  },
296
- {
297
- type: "select",
298
- name: "difficulty",
299
- message: "How difficulty will be to complete the tutorial?",
300
- choices: [
301
- { title: "Begginer (no previous experience)", value: "beginner" },
302
- { title: "Easy (just a bit of experience required)", value: "easy" },
303
- {
304
- title: "Intermediate (you need experience)",
305
- value: "intermediate",
306
- },
307
- { title: "Hard (master the topic)", value: "hard" },
308
- ],
309
- },
310
- {
311
- type: "text",
312
- name: "duration",
313
- initial: "1",
314
- message: "How many hours avg it takes to complete (number)?",
315
- validate: (value: string) => {
316
- const n = Math.floor(Number(value))
317
- return n !== Number.POSITIVE_INFINITY && String(n) === value && n >= 0
318
- },
319
- },
372
+
320
373
  {
321
374
  type: "select",
322
375
  name: "useAI",
@@ -332,7 +385,12 @@ const getChoices = async (empty: boolean) => {
332
385
  },
333
386
  ])
334
387
 
335
- return choices
388
+ const completeChoices = {
389
+ ...choices,
390
+ difficulty: "beginner",
391
+ duration: 30,
392
+ }
393
+ return completeChoices
336
394
  }
337
395
 
338
396
  class InitComand extends BaseCommand {
@@ -15,6 +15,8 @@ import {
15
15
  downloadEditor,
16
16
  checkIfDirectoryExists,
17
17
  } from "../managers/file"
18
+ import api, { TAcademy } from "../utils/api"
19
+ import * as prompts from "prompts"
18
20
 
19
21
  const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
20
22
  // const RIGOBOT_HOST =
@@ -45,6 +47,29 @@ const runAudit = () => {
45
47
  )
46
48
  }
47
49
 
50
+ const selectAcademy = async (academies: TAcademy[]) => {
51
+ if (academies.length === 0) {
52
+ return null
53
+ }
54
+
55
+ if (academies.length === 1) {
56
+ return academies[0]
57
+ }
58
+
59
+ // prompts the user to select an academy to upload the assets
60
+ Console.info("In which academy do you want to publish the asset?")
61
+ const response = await prompts({
62
+ type: "select",
63
+ name: "academy",
64
+ message: "Select an academy",
65
+ choices: academies.map((academy) => ({
66
+ title: academy.name,
67
+ value: academy,
68
+ })),
69
+ })
70
+ return response.academy
71
+ }
72
+
48
73
  export default class BuildCommand extends SessionCommand {
49
74
  static description =
50
75
  "Builds the project by copying necessary files and directories into a zip file"
@@ -65,7 +90,11 @@ export default class BuildCommand extends SessionCommand {
65
90
  const configObject = this.configManager?.get()
66
91
 
67
92
  let sessionPayload = await SessionManager.getPayload()
68
- if (!sessionPayload || !sessionPayload.rigobot) {
93
+ if (
94
+ !sessionPayload ||
95
+ !sessionPayload.rigobot ||
96
+ (sessionPayload.token && !(await api.validateToken(sessionPayload.token)))
97
+ ) {
69
98
  Console.error("You must be logged in to upload a LearnPack package")
70
99
  try {
71
100
  sessionPayload = await SessionManager.login()
@@ -87,7 +116,10 @@ export default class BuildCommand extends SessionCommand {
87
116
  this.configManager?.buildIndex()
88
117
  }
89
118
 
90
- // const rigoToken = "417d612d226a1606ad3a4e94b1881a9f0124b667"
119
+ // const academies = await api.listUserAcademies(sessionPayload.token)
120
+ // // console.log(academies, "academies")
121
+ // const academy = await selectAcademy(academies)
122
+ // console.log(academy, "academy")
91
123
 
92
124
  // Read learn.json to get the slug
93
125
  const learnJsonPath = path.join(process.cwd(), "learn.json")
@@ -176,10 +176,12 @@ export default class StartCommand extends SessionCommand {
176
176
 
177
177
  const files = prioritizeHTMLFile(data.files)
178
178
 
179
- if (config.editor.agent === "os") {
179
+ if (config.editor.agent !== "os") {
180
+ // Console.info("Opening files for vscode agent")
180
181
  eventManager.enqueue(dispatcher.events.OPEN_FILES, files)
181
182
  } else {
182
- dispatcher.enqueue(dispatcher.events.OPEN_FILES, files)
183
+ // dispatcher.enqueue(dispatcher.events.OPEN_FILES, files)
184
+ Console.debug("Ignoring files for os agent")
183
185
  }
184
186
 
185
187
  socket.ready("Ready to compile...")
@@ -187,10 +189,14 @@ export default class StartCommand extends SessionCommand {
187
189
 
188
190
  socket.on("open_window", (data: TOpenWindowData) => {
189
191
  Console.debug("Opening window: ", data)
192
+ console.log("config.os", config.os)
193
+
190
194
  // cli.open(data.url); This uses XDG under the ground
191
195
  if (config.os !== "linux" || (config.os === "linux" && hasXDG)) {
196
+ console.log("Opening window with XDG")
192
197
  eventManager.enqueue(dispatcher.events.OPEN_WINDOW, data)
193
198
  } else {
199
+ console.log("Opening window without XDG")
194
200
  dispatcher.enqueue(dispatcher.events.OPEN_WINDOW, data)
195
201
  }
196
202
 
@@ -249,16 +255,6 @@ export default class StartCommand extends SessionCommand {
249
255
  })
250
256
  })
251
257
 
252
- // socket.on("quiz_submission", (data: any) => {
253
- // const { stepPosition, event, eventData } = data
254
- // TelemetryManager.registerStepEvent(stepPosition, event, eventData)
255
- // })
256
-
257
- // socket.on("ai_interaction", (data: any) => {
258
- // const { stepPosition, event, eventData } = data
259
- // TelemetryManager.registerStepEvent(stepPosition, event, eventData)
260
- // })
261
-
262
258
  socket.on("telemetry_event", (data: any) => {
263
259
  const { stepPosition, event, eventData } = data
264
260
 
@@ -358,9 +354,9 @@ export default class StartCommand extends SessionCommand {
358
354
 
359
355
  // start watching for file changes
360
356
  if (StartCommand.flags.watch)
361
- this.configManager.watchIndex(_filename => {
357
+ this.configManager.watchIndex((_filename, _fileContent) => {
362
358
  // Instead of reloading with socket.reload(), I just notify the frontend for the file change
363
- socket.emit("file_change", "ready", _filename)
359
+ socket.emit("file_change", "ready", [_filename, _fileContent])
364
360
  })
365
361
  }
366
362
  }