@learnpack/learnpack 5.0.364 → 5.0.365

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.
@@ -25,6 +25,7 @@ const dotenv = require("dotenv");
25
25
  const creatorUtilities_1 = require("../utils/creatorUtilities");
26
26
  const lessonRecovery_1 = require("../utils/lessonRecovery");
27
27
  const generationRetry_1 = require("../utils/generationRetry");
28
+ const continueGeneration_1 = require("../utils/continueGeneration");
28
29
  // import { handleAssetCreation } from "./publish"
29
30
  const axios_1 = require("axios");
30
31
  const FormData = require("form-data");
@@ -1518,6 +1519,28 @@ class ServeCommand extends SessionCommand_1.default {
1518
1519
  if (!exercise.id || !exercise.title) {
1519
1520
  throw new errorHandler_1.ValidationError(`Lesson "${lessonUid}" is missing required properties (id or title)`);
1520
1521
  }
1522
+ const language = syllabus.courseInfo.language || "en";
1523
+ const previousStatus = exercise.status;
1524
+ // A second click, or a retry while the job still runs, would start
1525
+ // another paid job for the same lesson.
1526
+ if (previousStatus === "GENERATING") {
1527
+ const jobId = (0, continueGeneration_1.currentJobId)(exercise, language);
1528
+ let job = null;
1529
+ if (jobId) {
1530
+ try {
1531
+ job = await (0, rigoActions_1.getCompletionJob)(rigoToken, jobId, {
1532
+ timeoutMs: 5000,
1533
+ });
1534
+ }
1535
+ catch (error) {
1536
+ console.error(`Could not check job ${jobId}:`, error);
1537
+ }
1538
+ }
1539
+ if ((0, continueGeneration_1.isGenerationRunning)(exercise, language, job, Date.now())) {
1540
+ console.log(`Lesson ${lessonUid} is already being generated (job ${jobId}), not starting another one`);
1541
+ throw new errorHandler_1.ConflictError(`Lesson "${exercise.title}" is already being generated`, { code: "LESSON_ALREADY_GENERATING", completionId: jobId });
1542
+ }
1543
+ }
1521
1544
  // previous exercise
1522
1545
  let previousReadme = "---";
1523
1546
  const previousExercise = syllabus.lessons[exerciseIndex - 1];
@@ -1543,7 +1566,7 @@ class ServeCommand extends SessionCommand_1.default {
1543
1566
  // A new job by the user, so the automatic retries start over.
1544
1567
  syllabus.lessons[exerciseIndex].status = "GENERATING";
1545
1568
  syllabus.lessons[exerciseIndex].translations = {
1546
- [syllabus.courseInfo.language || "en"]: startedJob(completionId),
1569
+ [language]: startedJob(completionId),
1547
1570
  };
1548
1571
  console.log("Lesson", syllabus.lessons[exerciseIndex]);
1549
1572
  if (syllabus.feedback && typeof syllabus.feedback === "string") {
@@ -1552,7 +1575,7 @@ class ServeCommand extends SessionCommand_1.default {
1552
1575
  else {
1553
1576
  syllabus.feedback = feedback;
1554
1577
  }
1555
- syllabus.generationMode = mode;
1578
+ syllabus.generationMode = (0, continueGeneration_1.resolveGenerationMode)(previousStatus, syllabus.generationMode, mode);
1556
1579
  await saveSyllabus(courseSlug, syllabus, bucket);
1557
1580
  });
1558
1581
  res.json({ status: "SUCCESS" });
@@ -3763,9 +3786,12 @@ class ServeCommand extends SessionCommand_1.default {
3763
3786
  await uploadInitialReadme(bucket, exSlug, targetDir, syllabus.courseInfo);
3764
3787
  }
3765
3788
  const sidebar = await (0, sidebar_1.createInitialSidebar)(syllabus.lessons.map(lesson => (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title)), syllabus.courseInfo.language);
3789
+ const language = syllabus.courseInfo.language || "en";
3766
3790
  const initialSyllabus = Object.assign(Object.assign({}, syllabus), { lessons: syllabus.lessons.map((lesson, index) => {
3767
3791
  if (index < 1) {
3768
- return Object.assign(Object.assign({}, lesson), { generated: false, status: "GENERATING" });
3792
+ // Its start time, so a retry right away is refused even before
3793
+ // the job id is saved below.
3794
+ return Object.assign(Object.assign({}, lesson), { generated: false, status: "GENERATING", translations: { [language]: startedJob(0) } });
3769
3795
  }
3770
3796
  return Object.assign(Object.assign({}, lesson), { generated: false, status: "PENDING" });
3771
3797
  }) });
@@ -3773,17 +3799,16 @@ class ServeCommand extends SessionCommand_1.default {
3773
3799
  await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(sidebar), `${tutorialDir}/.learn/sidebar.json`);
3774
3800
  const firstLesson = syllabus.lessons[0];
3775
3801
  const lastResult = "---";
3776
- // Use new two-phase generation workflow
3777
- const completionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, firstLesson, courseSlug, syllabus.courseInfo.purpose, lastResult);
3778
- if (firstLesson) {
3779
- firstLesson.translations = {
3780
- [syllabus.courseInfo.language || "en"]: {
3781
- completionId,
3782
- startedAt: Date.now(),
3783
- completedAt: 0,
3784
- },
3785
- };
3786
- }
3802
+ // In the course queue, so the webhook of this job waits until its id
3803
+ // is saved. The id was never saved before, so the first lesson had no
3804
+ // protection against a second job.
3805
+ await serializeByCourse(courseSlug, async () => {
3806
+ // Use new two-phase generation workflow
3807
+ const completionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, firstLesson, courseSlug, syllabus.courseInfo.purpose, lastResult);
3808
+ await patchLesson(courseSlug, firstLesson.uid, bucket, lesson => {
3809
+ lesson.translations = { [language]: startedJob(completionId) };
3810
+ });
3811
+ });
3787
3812
  await createInitialReadme(JSON.stringify(syllabus.courseInfo), courseSlug, rigoToken);
3788
3813
  return res.json({
3789
3814
  message: "Course generation started",
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Rules for `POST /actions/continue-generating`, the request behind the
3
+ * creator's continue and retry buttons. Without them, every extra click starts
4
+ * another paid job for a lesson that is already being generated.
5
+ */
6
+ /**
7
+ * How long a PENDING job counts as running. Each phase of a lesson takes 1 to
8
+ * 3 minutes, so a job PENDING for longer is most likely stuck, for example
9
+ * after a worker restart. A stuck job holds the whole course, because the next
10
+ * lesson only starts after this one finishes. Replacing a slow job that was
11
+ * still alive only costs one extra job: serve ignores the older job's webhook.
12
+ */
13
+ export declare const RUNNING_JOB_MAX_AGE_MS: number;
14
+ /**
15
+ * How long a finished job counts as running. Rigobot saves the result before
16
+ * it sends the webhook, and serve may still be handling it.
17
+ */
18
+ export declare const WEBHOOK_GRACE_MS: number;
19
+ /**
20
+ * How long a lesson counts as running when its job cannot be checked: it has
21
+ * no job id yet, or Rigobot did not answer.
22
+ */
23
+ export declare const UNCHECKED_JOB_GRACE_MS: number;
24
+ export type GenerationMode = "next-three" | "continue-with-all";
25
+ type LessonState = {
26
+ status?: string;
27
+ translations?: {
28
+ [language: string]: {
29
+ completionId?: number;
30
+ startedAt?: number;
31
+ };
32
+ };
33
+ };
34
+ /** The fields of a Rigobot completion job these rules read. */
35
+ export type JobState = {
36
+ status?: string;
37
+ ended_at?: string | null;
38
+ };
39
+ /**
40
+ * The job to check before starting another one.
41
+ * @param lesson - The lesson as saved in the syllabus.
42
+ * @param language - The course language.
43
+ * @returns The id of the job the lesson waits for, or null if it has none.
44
+ */
45
+ export declare const currentJobId: (lesson: LessonState, language: string) => number | null;
46
+ /**
47
+ * Whether a lesson is still being generated, so a new job must not start.
48
+ * @param lesson - The lesson as saved in the syllabus.
49
+ * @param language - The course language.
50
+ * @param job - Its job as Rigobot reports it, or null if it was not checked.
51
+ * @param now - The current time in milliseconds.
52
+ * @returns True when the lesson must wait for its current job.
53
+ */
54
+ export declare const isGenerationRunning: (lesson: LessonState, language: string, job: JobState | null, now: number) => boolean;
55
+ /**
56
+ * The generation mode to save. The user only picks a mode on a PENDING
57
+ * lesson. The retry buttons offer no choice, so a retry keeps the mode of the
58
+ * course instead of resetting it.
59
+ * @param lessonStatus - The status of the lesson before this request.
60
+ * @param current - The mode saved in the syllabus.
61
+ * @param requested - The mode in the request.
62
+ * @returns The mode to save, or undefined when the course has none.
63
+ */
64
+ export declare const resolveGenerationMode: (lessonStatus: string | undefined, current: unknown, requested: unknown) => GenerationMode | undefined;
65
+ export {};
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /**
3
+ * Rules for `POST /actions/continue-generating`, the request behind the
4
+ * creator's continue and retry buttons. Without them, every extra click starts
5
+ * another paid job for a lesson that is already being generated.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.resolveGenerationMode = exports.isGenerationRunning = exports.currentJobId = exports.UNCHECKED_JOB_GRACE_MS = exports.WEBHOOK_GRACE_MS = exports.RUNNING_JOB_MAX_AGE_MS = void 0;
9
+ /**
10
+ * How long a PENDING job counts as running. Each phase of a lesson takes 1 to
11
+ * 3 minutes, so a job PENDING for longer is most likely stuck, for example
12
+ * after a worker restart. A stuck job holds the whole course, because the next
13
+ * lesson only starts after this one finishes. Replacing a slow job that was
14
+ * still alive only costs one extra job: serve ignores the older job's webhook.
15
+ */
16
+ exports.RUNNING_JOB_MAX_AGE_MS = 5 * 60 * 1000;
17
+ /**
18
+ * How long a finished job counts as running. Rigobot saves the result before
19
+ * it sends the webhook, and serve may still be handling it.
20
+ */
21
+ exports.WEBHOOK_GRACE_MS = 90 * 1000;
22
+ /**
23
+ * How long a lesson counts as running when its job cannot be checked: it has
24
+ * no job id yet, or Rigobot did not answer.
25
+ */
26
+ exports.UNCHECKED_JOB_GRACE_MS = 2 * 60 * 1000;
27
+ /**
28
+ * The job to check before starting another one.
29
+ * @param lesson - The lesson as saved in the syllabus.
30
+ * @param language - The course language.
31
+ * @returns The id of the job the lesson waits for, or null if it has none.
32
+ */
33
+ const currentJobId = (lesson, language) => {
34
+ var _a, _b;
35
+ const id = (_b = (_a = lesson.translations) === null || _a === void 0 ? void 0 : _a[language]) === null || _b === void 0 ? void 0 : _b.completionId;
36
+ return typeof id === "number" && id > 0 ? id : null;
37
+ };
38
+ exports.currentJobId = currentJobId;
39
+ /**
40
+ * Whether a lesson is still being generated, so a new job must not start.
41
+ * @param lesson - The lesson as saved in the syllabus.
42
+ * @param language - The course language.
43
+ * @param job - Its job as Rigobot reports it, or null if it was not checked.
44
+ * @param now - The current time in milliseconds.
45
+ * @returns True when the lesson must wait for its current job.
46
+ */
47
+ const isGenerationRunning = (lesson, language, job, now) => {
48
+ var _a, _b;
49
+ if (lesson.status !== "GENERATING")
50
+ return false;
51
+ const startedAt = (_b = (_a = lesson.translations) === null || _a === void 0 ? void 0 : _a[language]) === null || _b === void 0 ? void 0 : _b.startedAt;
52
+ const startedAgo = typeof startedAt === "number" && startedAt > 0 ?
53
+ now - startedAt :
54
+ Number.POSITIVE_INFINITY;
55
+ if (!job)
56
+ return startedAgo < exports.UNCHECKED_JOB_GRACE_MS;
57
+ if (job.status === "PENDING")
58
+ return startedAgo < exports.RUNNING_JOB_MAX_AGE_MS;
59
+ // A finished job. Its webhook may still be on its way to serve.
60
+ const endedAt = job.ended_at ? Date.parse(job.ended_at) : Number.NaN;
61
+ if (Number.isNaN(endedAt))
62
+ return startedAgo < exports.UNCHECKED_JOB_GRACE_MS;
63
+ return now - endedAt < exports.WEBHOOK_GRACE_MS;
64
+ };
65
+ exports.isGenerationRunning = isGenerationRunning;
66
+ /**
67
+ * The generation mode to save. The user only picks a mode on a PENDING
68
+ * lesson. The retry buttons offer no choice, so a retry keeps the mode of the
69
+ * course instead of resetting it.
70
+ * @param lessonStatus - The status of the lesson before this request.
71
+ * @param current - The mode saved in the syllabus.
72
+ * @param requested - The mode in the request.
73
+ * @returns The mode to save, or undefined when the course has none.
74
+ */
75
+ const resolveGenerationMode = (lessonStatus, current, requested) => {
76
+ const isMode = (mode) => mode === "next-three" || mode === "continue-with-all";
77
+ if (isMode(requested) && (lessonStatus === "PENDING" || !isMode(current))) {
78
+ return requested;
79
+ }
80
+ return isMode(current) ? current : undefined;
81
+ };
82
+ exports.resolveGenerationMode = resolveGenerationMode;
@@ -26,6 +26,7 @@ export type TCompletionJob = {
26
26
  status: "PENDING" | "SUCCESS" | "ERROR" | string;
27
27
  status_text?: string | null;
28
28
  parsed?: Record<string, any> | null;
29
+ ended_at?: string | null;
29
30
  };
30
31
  type TTranslateAndWaitOptions = {
31
32
  /** How often the job status is checked while it is still running. */
@@ -35,7 +36,9 @@ type TTranslateAndWaitOptions = {
35
36
  /** Called after every successful poll, useful to keep a heartbeat alive. */
36
37
  onPoll?: (job: TCompletionJob) => void | Promise<void>;
37
38
  };
38
- export declare const getCompletionJob: (token: string, completionId: number | string) => Promise<TCompletionJob>;
39
+ export declare const getCompletionJob: (token: string, completionId: number | string, options?: {
40
+ timeoutMs?: number;
41
+ }) => Promise<TCompletionJob>;
39
42
  export declare const translateExerciseAndWait: (token: string, inputs: TTranslateInputs, options?: TTranslateAndWaitOptions) => Promise<TCompletionJob>;
40
43
  type TGenerateCourseIntroductionInputs = {
41
44
  course_title: string;
@@ -114,12 +114,14 @@ const assertJobSucceeded = (job) => {
114
114
  }
115
115
  return job;
116
116
  };
117
- const getCompletionJob = async (token, completionId) => {
117
+ const getCompletionJob = async (token, completionId, options = {}) => {
118
118
  const response = await axios_1.default.get(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/${completionId}/`, {
119
119
  headers: {
120
120
  "Content-Type": "application/json",
121
121
  Authorization: "Token " + token,
122
122
  },
123
+ // No timeout by default: the translation polling waits as long as it needs.
124
+ timeout: options.timeoutMs,
123
125
  });
124
126
  return response.data;
125
127
  };
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.364",
4
+ "version": "5.0.365",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -39,6 +39,7 @@ import {
39
39
  addInteractivity,
40
40
  generateCodeChallenge,
41
41
  generateStepSlug,
42
+ getCompletionJob,
42
43
  } from "../utils/rigoActions"
43
44
  import * as dotenv from "dotenv"
44
45
 
@@ -55,6 +56,11 @@ import {
55
56
  planGenerationRetry,
56
57
  RetryPlan,
57
58
  } from "../utils/generationRetry"
59
+ import {
60
+ currentJobId,
61
+ isGenerationRunning,
62
+ resolveGenerationMode,
63
+ } from "../utils/continueGeneration"
58
64
  // import { handleAssetCreation } from "./publish"
59
65
  import axios from "axios"
60
66
  import * as FormData from "form-data"
@@ -2297,6 +2303,35 @@ class ServeCommand extends SessionCommand {
2297
2303
  )
2298
2304
  }
2299
2305
 
2306
+ const language = syllabus.courseInfo.language || "en"
2307
+ const previousStatus = exercise.status
2308
+
2309
+ // A second click, or a retry while the job still runs, would start
2310
+ // another paid job for the same lesson.
2311
+ if (previousStatus === "GENERATING") {
2312
+ const jobId = currentJobId(exercise, language)
2313
+ let job = null
2314
+ if (jobId) {
2315
+ try {
2316
+ job = await getCompletionJob(rigoToken, jobId, {
2317
+ timeoutMs: 5000,
2318
+ })
2319
+ } catch (error) {
2320
+ console.error(`Could not check job ${jobId}:`, error)
2321
+ }
2322
+ }
2323
+
2324
+ if (isGenerationRunning(exercise, language, job, Date.now())) {
2325
+ console.log(
2326
+ `Lesson ${lessonUid} is already being generated (job ${jobId}), not starting another one`
2327
+ )
2328
+ throw new ConflictError(
2329
+ `Lesson "${exercise.title}" is already being generated`,
2330
+ { code: "LESSON_ALREADY_GENERATING", completionId: jobId }
2331
+ )
2332
+ }
2333
+ }
2334
+
2300
2335
  // previous exercise
2301
2336
  let previousReadme = "---"
2302
2337
  const previousExercise = syllabus.lessons[exerciseIndex - 1]
@@ -2334,7 +2369,7 @@ class ServeCommand extends SessionCommand {
2334
2369
  // A new job by the user, so the automatic retries start over.
2335
2370
  syllabus.lessons[exerciseIndex].status = "GENERATING"
2336
2371
  syllabus.lessons[exerciseIndex].translations = {
2337
- [syllabus.courseInfo.language || "en"]: startedJob(completionId),
2372
+ [language]: startedJob(completionId),
2338
2373
  }
2339
2374
  console.log("Lesson", syllabus.lessons[exerciseIndex])
2340
2375
  if (syllabus.feedback && typeof syllabus.feedback === "string") {
@@ -2343,7 +2378,11 @@ class ServeCommand extends SessionCommand {
2343
2378
  syllabus.feedback = feedback
2344
2379
  }
2345
2380
 
2346
- syllabus.generationMode = mode
2381
+ syllabus.generationMode = resolveGenerationMode(
2382
+ previousStatus,
2383
+ syllabus.generationMode,
2384
+ mode
2385
+ )
2347
2386
 
2348
2387
  await saveSyllabus(courseSlug, syllabus, bucket)
2349
2388
  })
@@ -5479,11 +5518,19 @@ class ServeCommand extends SessionCommand {
5479
5518
  syllabus.courseInfo.language
5480
5519
  )
5481
5520
 
5521
+ const language = syllabus.courseInfo.language || "en"
5482
5522
  const initialSyllabus = {
5483
5523
  ...syllabus,
5484
5524
  lessons: syllabus.lessons.map((lesson, index) => {
5485
5525
  if (index < 1) {
5486
- return { ...lesson, generated: false, status: "GENERATING" }
5526
+ // Its start time, so a retry right away is refused even before
5527
+ // the job id is saved below.
5528
+ return {
5529
+ ...lesson,
5530
+ generated: false,
5531
+ status: "GENERATING",
5532
+ translations: { [language]: startedJob(0) },
5533
+ }
5487
5534
  }
5488
5535
 
5489
5536
  return {
@@ -5508,25 +5555,25 @@ class ServeCommand extends SessionCommand {
5508
5555
 
5509
5556
  const lastResult = "---"
5510
5557
 
5511
- // Use new two-phase generation workflow
5512
- const completionId = await startInitialContentGeneration(
5513
- rigoToken,
5514
- syllabus.lessons,
5515
- syllabus.courseInfo,
5516
- firstLesson,
5517
- courseSlug,
5518
- syllabus.courseInfo.purpose,
5519
- lastResult
5520
- )
5521
- if (firstLesson) {
5522
- firstLesson.translations = {
5523
- [syllabus.courseInfo.language || "en"]: {
5524
- completionId,
5525
- startedAt: Date.now(),
5526
- completedAt: 0,
5527
- },
5528
- }
5529
- }
5558
+ // In the course queue, so the webhook of this job waits until its id
5559
+ // is saved. The id was never saved before, so the first lesson had no
5560
+ // protection against a second job.
5561
+ await serializeByCourse(courseSlug, async () => {
5562
+ // Use new two-phase generation workflow
5563
+ const completionId = await startInitialContentGeneration(
5564
+ rigoToken,
5565
+ syllabus.lessons,
5566
+ syllabus.courseInfo,
5567
+ firstLesson,
5568
+ courseSlug,
5569
+ syllabus.courseInfo.purpose,
5570
+ lastResult
5571
+ )
5572
+
5573
+ await patchLesson(courseSlug, firstLesson.uid, bucket, lesson => {
5574
+ lesson.translations = { [language]: startedJob(completionId) }
5575
+ })
5576
+ })
5530
5577
 
5531
5578
  await createInitialReadme(
5532
5579
  JSON.stringify(syllabus.courseInfo),
@@ -131,23 +131,39 @@ Error generating stack: `+i.message+`
131
131
  components:\r
132
132
  - name: open_text_question\r
133
133
  description: >-\r
134
- The open text question is a type of interactive question where users can write down a text answer. The component consists in a markdown code block with the language set to \`question\` and has a metadata property eval in the metadata section of the code block with instructions on how to evaluate the answer. Inside the code block, you must write down 3-5 example answers with CORRECT: or INCORRECT: prefix. The actual question must be written before the code block so the user can see it.\r
134
+ The open text question is a type of interactive question where users can write down a text answer. The component consists in a markdown code block with the language set to \`question\` and has a metadata property eval in the metadata section of the code block with instructions on how to evaluate the answer. Inside the code block, you must write down 3-5 example answers with CORRECT: or INCORRECT: prefix. The question must be written before the code block, because the learner sees neither the eval nor the example answers. It must include everything needed to answer: the situation or the text to work on.\r
135
135
  avoid:\r
136
136
  - multiple choice\r
137
137
  - true/false\r
138
138
  - fill in the blank\r
139
139
  - this component is intended only for free form answers\r
140
140
  - never use for visual questions\r
141
- props:\r
141
+ goodFor:
142
+ - explaining a concept in the learner's own words
143
+ - applying a concept by producing a short text
144
+ - writing or rewriting a short piece, such as a prompt, a reply or a paraphrase
145
+ - justifying a decision against clear criteria
146
+ props:
142
147
  - eval: string (required)\r
143
- example: |\r
144
- ### What is console.log in your own words?\r
145
- \r
146
- \`\`\`question eval="The user needs to write using its own words what is console.log"\r
147
- CORRECT: The console.log is a function that logs a message to the console.\r
148
- INCORRECT: It is used to solve logarithms problems.\r
149
- CORRECT: It prints things to the console. \r
150
- \`\`\`\r
148
+ example: |
149
+ ### In your own words, what is a variable expense?
150
+
151
+ \`\`\`question eval="The answer must explain, in the learner's own words, that a variable expense changes from month to month"
152
+ CORRECT: It is money you spend that is not the same every month, like groceries.
153
+ CORRECT: An expense whose amount changes depending on what you use or buy.
154
+ INCORRECT: It is a payment that stays the same every month.
155
+ \`\`\`
156
+
157
+ --- Second Example ---
158
+
159
+ ### Write a prompt asking an AI assistant to summarize a customer's complaint email in three bullet points for your supervisor.
160
+
161
+ \`\`\`question eval="The prompt must state the task, the text to work on, the three bullet point format and the supervisor as the audience"
162
+ CORRECT: Summarize the customer complaint email below in three bullet points for my supervisor, covering the problem and what the customer asks for.
163
+ CORRECT: I need to brief my supervisor. Read this complaint email and write a summary of three bullet points.
164
+ INCORRECT: Summarize this.
165
+ INCORRECT: Write something about customer complaints.
166
+ \`\`\`
151
167
  rules:\r
152
168
  - MUST only require a textual answer that can be evaluated without images.\r
153
169
  - MUST NOT ask the learner to draw, sketch, diagram, circle, shade, underline, point, annotate, mark on paper, or \`visualize and show\`\r
@@ -156,6 +172,7 @@ components:\r
156
172
  - Answers can be as short as 1 word or as long as a full paragraph.\r
157
173
  - Only use for textual easy to evaluate questions\r
158
174
  - Never use this component for reflection questions, only for knowledge assessment questions.\r
175
+ - Every CORRECT example must meet the eval.\r
159
176
  \r
160
177
  - name: multiple_choice_question\r
161
178
  intendedUse:\r
@@ -182,47 +199,56 @@ components:\r
182
199
  rules:\r
183
200
  - Avoid questions with multiple correct answers. Each question must have an unique correct answer.\r
184
201
  - Avoid questions with too many options. Each question should have between 3 and 5 options.\r
185
- goodFor:\r
186
- - reflection\r
187
- - explanation\r
188
- - recall\r
189
- - knowledge assessment\r
202
+ - The question text must not contain the correct answer or repeat an option.\r
203
+ - Incorrect options must be plausible and as specific as the correct one.\r
204
+ - The question always goes in the numbered line, never in the heading above it. A heading is optional and is only a short title.\r
205
+ - Number only multiple choice questions. Headings and other components are never numbered, and the numbering restarts at 1 after any other component.\r
206
+ - Each option holds its full text on its own checkbox line. Never write the answers as numbered lines with checkboxes such as - [x] 1 below them.\r
207
+ goodFor:
208
+ - recall
209
+ - judging a situation\r
190
210
  \r
191
- example: |\r
192
- \r
193
- ### Put your knowledge to the test!\r
194
- \r
195
- Select the correct answer for each question.\r
196
- \r
197
- \r
198
- 1. Question number 1 -> this is the question (can be numbered or not), it is a list item that starts at the beginning of the line\r
199
- <!-- Each group of options should have an empty line between the question and the options -->\r
200
- - [ ] Incorrect answer -> this is one of the incorrect answers\r
201
- - [ ] Incorrect answer -> this is one of the incorrect answers\r
202
- - [x] Correct answer -> this is the correct answer (is the only one that can be marked with [x])\r
203
- <!-- Note that each answer should be a task list item indented by 3 to 6 spaces. -->\r
204
- \r
205
- 2. Question number 2\r
206
- <!-- Each group of options should have an empty line between the question and the options. Each option should be a task list item indented by 3 to 6 spaces. -->\r
207
- - [ ] Incorrect answer\r
208
- - [x] Correct answer\r
209
- - [ ] Incorrect answer\r
210
- \r
211
- \r
212
- --- Second Example ---\r
213
- \r
214
- - Question number 1\r
215
- \r
216
- - [x] Correct option\r
217
- - [ ] Incorrect option\r
218
- - [ ] Incorrect option\r
219
- \r
220
- - Question number 2\r
221
- \r
222
- - [ ] Incorrect option\r
223
- - [x] Correct option\r
224
- - [ ] Incorrect option\r
225
- \r
211
+ example: |
212
+ ### Put your knowledge to the test!
213
+
214
+ 1. Which expense is variable?
215
+
216
+ - [ ] Monthly rent
217
+ - [x] Groceries
218
+ - [ ] Phone plan
219
+
220
+ 2. What should you list first when building a budget?
221
+
222
+ - [ ] Your savings goal
223
+ - [ ] Your largest expense
224
+ - [x] Your monthly income
225
+
226
+ --- Second Example (a step that mixes components: each one has its own ### title, and only the multiple choice questions are numbered) ---
227
+
228
+ ### Check your basics
229
+
230
+ 1. Which expense is variable?
231
+
232
+ - [ ] Monthly rent
233
+ - [x] Groceries
234
+ - [ ] Phone plan
235
+
236
+ ### Complete the sentences
237
+
238
+ \`\`\`select_the_blank 1="fixed|variable|annual" 2="income|expense|saving" 3="budget|receipt|invoice"
239
+ Rent is a _1_ expense.
240
+ A monthly salary counts as _2_.
241
+ A plan for how money will be used is called a _3_.
242
+ \`\`\`
243
+
244
+ ### Choose the best option
245
+
246
+ 1. What should you list first when building a budget?
247
+
248
+ - [ ] Your largest expense
249
+ - [x] Your monthly income
250
+ - [ ] Your savings goal
251
+
226
252
  - name: select_the_blank\r
227
253
  measures:\r
228
254
  - rubric: content_mastery\r
@@ -232,30 +258,41 @@ components:\r
232
258
  - rubric: application_problem_solving\r
233
259
  weight: 0.15\r
234
260
  description: >-\r
235
- A select the blank is a component where the user completes a sentence by choosing the correct option from a dropdown for each blank, instead of typing free text. Because the user picks from a predefined list, grading is fully deterministic: the user can never fail due to typos, punctuation, casing or incomplete answers, and the available options give the learner guidance. It consists in a markdown code block with the language set to \`select_the_blank\`. Inside the code block, you must write the question with the blank (or blanks) to be filled. Each blank consists in _1_, _2_, _3_, etc. In the metadata section, for each blank you must provide the list of options separated by the pipe character \`|\`. IMPORTANT: the FIRST option of each blank is the correct one. You MUST provide at least 3 options per blank: 1 correct answer and at least 2 plausible distractors. You can also use select the blank for numerical questions.\r
261
+ Use it to complete sentences with key terms or categories of a few words. It is not for whole sentences or explanations: use open_text_question for those. A select the blank is a component where the user completes a sentence by choosing the correct option from a dropdown for each blank, instead of typing free text. Because the user picks from a predefined list, grading is fully deterministic: the user can never fail due to typos, punctuation, casing or incomplete answers, and the available options give the learner guidance. It consists in a markdown code block with the language set to \`select_the_blank\`. Inside the code block, you must write the question with the blank (or blanks) to be filled. Each blank consists in _1_, _2_, _3_, etc. In the metadata section, for each blank you must provide the list of options separated by the pipe character \`|\`. IMPORTANT: the FIRST option of each blank is the correct one. You MUST provide at least 3 options per blank: 1 correct answer and at least 2 plausible distractors. You can also use select the blank for numerical questions.\r
236
262
  rules:\r
237
263
  - The FIRST option of each blank is always the correct answer; the rest are distractors.\r
238
264
  - Each blank MUST have at least 3 options (1 correct + at least 2 distractors).\r
239
265
  - Options are separated by the pipe character \`|\`, NOT by commas (commas can be part of an option's text).\r
240
266
  - Distractors must be plausible and related to the topic, never obviously wrong.\r
241
- - Keep options reasonably short so they read well inside a dropdown.\r
267
+ - Each option is a term or short phrase of at most four words, never a whole sentence.\r
268
+ - The sentences go inside the code block and must be understandable on their own.\r
269
+ - Every number in the metadata must appear in the text as _1_, _2_, etc.; a block without blanks renders nothing.\r
270
+ - Use at least 3 blanks in total: with fewer, the exercise is too easy.\r
242
271
  - READ-BACK CHECK - When the correct option is inserted into its blank, the WHOLE sentence MUST be grammatically correct and factually true. Re-read each completed sentence to verify before finalizing.\r
243
272
  - UNIQUENESS - Each blank must have exactly ONE correct option given the surrounding text, and that text must contain enough information to identify it unambiguously. NEVER write two blanks whose surrounding text is identical or interchangeable (e.g. the same description reused for two different answers).\r
244
273
  - DISTRACTORS MUST BE WRONG IN CONTEXT - Each distractor, if placed in the blank, MUST make the sentence false or wrong in that specific context. If a distractor would also be a valid answer, the sentence is missing disambiguating information; add it.\r
245
274
  - CONSISTENT FRAMING - What the learner selects (the blank) must be exactly what the instruction asks them to identify. Do not place the blank where a different part of speech belongs (e.g. asking to pick a method but leaving the blank where a position noun like "end"/"beginning" should go).\r
275
+ goodFor:
276
+ - recalling key terms
277
+ - classifying something into categories
278
+ - completing a definition or a rule
279
+ - numerical answers
246
280
  avoid:\r
247
281
  - free text answers (use open_text_question for those)\r
248
282
  - blanks with fewer than 3 options\r
283
+ - a blank with only its correct answer, such as 1="fixed", because the learner has nothing to choose\r
249
284
  - never use for visual questions\r
250
285
  - blanks where two or more options would be equally valid given the surrounding text\r
251
286
  - sentences that read incorrectly once the correct option is inserted\r
252
287
  example: |\r
253
- ### Let's recall!\r
254
- \r
255
- \`\`\`select_the_blank 1="Paris|Madrid|Rome" 2="Berlin|Lisbon|Vienna"\r
256
- The capital of France is _1_, the capital of Germany is _2_.\r
257
- \`\`\`\r
258
- \r
288
+ ### Complete the sentences
289
+
290
+ \`\`\`select_the_blank 1="fixed|variable|annual" 2="variable|fixed|annual" 3="income|expense|saving" 4="budget|receipt|invoice"
291
+ Rent is a _1_ expense, while groceries are a _2_ expense.
292
+ A monthly salary counts as _3_.
293
+ A plan for how money will be used in a month is called a _4_.
294
+ \`\`\`
295
+
259
296
  --- Second Example ---\r
260
297
  \r
261
298
  ### Are you really learning? Let's check it!\r
@@ -287,15 +324,23 @@ components:\r
287
324
  - rubric: critical_thinking\r
288
325
  weight: 0.15\r
289
326
  description: >-\r
290
- An ordering component asks the user to arrange a set of items into the correct sequence. It is ideal for processes, step-by-step procedures, chronological events, or ranking things (for example: the lifecycle of an HTTP request, the steps of a git workflow, the steps of a installation process, etc). Grading is fully deterministic: the user's order is compared position by position with the correct one. It consists in a markdown code block with the language set to \`ordering\`. Inside the code block, write one item per line, IN THE CORRECT ORDER (the renderer shuffles them before showing them to the user, and the user reorders them with up/down arrows). You can optionally add a \`title\` in the metadata to describe what must be ordered.\r
327
+ An ordering component asks the user to arrange a set of items into the correct sequence. It is ideal for processes, step-by-step procedures or chronological events (for example: the lifecycle of an HTTP request, the steps of a git workflow, the steps of an installation process, the steps of returning a purchase, etc). Grading is fully deterministic: the user's order is compared position by position with the correct one. It consists in a markdown code block with the language set to \`ordering\`. Inside the code block, write one item per line, IN THE CORRECT ORDER (the renderer shuffles them before showing them to the user, and the user reorders them with up/down arrows). You can optionally add a \`title\` in the metadata to describe what must be ordered.\r
291
328
  rules:\r
292
329
  - Write the items in the CORRECT order inside the code block; the renderer shuffles them automatically.\r
293
330
  - Write exactly one item per line. Items may optionally start with a number or a list marker (it is stripped automatically).\r
294
331
  - Use between 3 and 6 items; fewer than 3 makes the exercise trivial, more than 6 becomes tedious.\r
295
- - There must be a single, unambiguous correct order. Do not use it when several orders could be valid.\r
332
+ - There must be a single, unambiguous correct order: swapping any two items must make the sequence wrong. Do not use it when several orders could be valid.\r
296
333
  - Keep each item short and self-contained.\r
334
+ - Use it only for a sequence the lesson teaches as steps, never for categories, priorities, opinions or tips.\r
335
+ - No item may reveal its position.\r
336
+ goodFor:
337
+ - the steps of a procedure
338
+ - the stages of a process
339
+ - events in time
297
340
  avoid:\r
298
341
  - sets where more than one order is acceptable\r
342
+ - actions or attitudes that happen at the same time\r
343
+ - lists of tips, qualities or good practices, even if the lesson names them one after another\r
299
344
  - fewer than 3 items\r
300
345
  - never use for visual questions\r
301
346
  example: |\r
@@ -317,6 +362,17 @@ components:\r
317
362
  git commit -m "Save changes"\r
318
363
  git push\r
319
364
  \`\`\`\r
365
+ \r
366
+ --- Third Example ---\r
367
+ \r
368
+ ### Let's get your money back!\r
369
+ \r
370
+ \`\`\`ordering title="Order the steps to return an online purchase"\r
371
+ Request the return on the website\r
372
+ Pack the item\r
373
+ Drop the box at the pickup point\r
374
+ Check that the refund arrived\r
375
+ \`\`\`\r
320
376
  \r
321
377
  - name: code_challenge_proposal\r
322
378
  intendedUse:\r
@@ -330,6 +386,9 @@ components:\r
330
386
  rules:\r
331
387
  - The main file must be always be named app.extension, where extension is the extension of the file, for example, app.js, app.html, app.css, etc.\r
332
388
  - Keep challenges simple and easy to understand for the student, it must be related to the lesson content and the exercise instructions.\r
389
+ goodFor:
390
+ - practicing a programming concept by writing code
391
+ - tasks that need a code editor
333
392
  avoid:\r
334
393
  - complex code that might not be inferred correctly by an AI model\r
335
394
  example: |\r
@@ -353,12 +412,12 @@ components:\r
353
412
  - process explanation\r
354
413
  - system architecture\r
355
414
  - data flow\r
356
- whenToUse: Use it only when the diagram\r
415
+ whenToUse: Use it for processes, sequences and decisions that read better as a diagram.\r
357
416
  avoid:\r
358
417
  - complex diagrams that might not render well\r
359
- - use for simple to medium complexity diagrams\r
360
418
  rules:\r
361
419
  - NEVER under ANY circumstances use or write parentheses inside [] because it will break the mermaid syntax and won't render properly\r
420
+ - Keep diagrams simple to medium in complexity.\r
362
421
  example: |\r
363
422
  \`\`\`mermaid\r
364
423
  graph TD\r
@@ -378,10 +437,10 @@ components:\r
378
437
  - providing hints\r
379
438
  - AI assistance\r
380
439
  - learning support\r
440
+ - exploring a concept or applying it to the learner's own situation\r
381
441
  avoid:\r
382
442
  - overuse\r
383
443
  - should be used sparingly to encourage independent thinking\r
384
- usage: Automatically appears in quiz and test components, no manual configuration needed\r
385
444
  example: |\r
386
445
  [Ask Rigo how to declare a variable in JavaScript](https://4geeks.com/ask?query=can-you-explain-me-in-simple-terms-how-to-declare-a-variable-in-javascript)\r
387
446
  \r
@@ -427,10 +486,10 @@ components:\r
427
486
  - name: conceptual_illustration\r
428
487
  intendedUse:\r
429
488
  - abstract concept visualization\r
430
- - metaphorical representation of programming ideas\r
489
+ - metaphorical representation of ideas\r
431
490
  - simplified educational illustrations\r
432
491
  description: >-\r
433
- Conceptual illustrations transform abstract programming concepts into visual metaphors\r
492
+ Conceptual illustrations transform abstract concepts into visual metaphors\r
434
493
  and simplified representations. They use friendly, approachable aesthetics with flat\r
435
494
  colors and minimal details to make complex ideas accessible. Focus on clarity and\r
436
495
  educational value through visual metaphors.\r
@@ -439,7 +498,7 @@ components:\r
439
498
  aesthetic, vector art style with smooth shapes, focus on clarity over realism,\r
440
499
  educational and welcoming visual language\r
441
500
  goodFor:\r
442
- - abstract programming concepts (closures, scope, inheritance)\r
501
+ - abstract concepts\r
443
502
  - memory models and heap/stack visualization\r
444
503
  - conceptual relationships between ideas\r
445
504
  - visual metaphors for complex topics\r
@@ -453,13 +512,11 @@ components:\r
453
512
  rules:\r
454
513
  - Must use standard markdown image syntax => ![alt text](image-url)\r
455
514
  - Alt text must contain at least 30 words describing the concept and visual metaphor\r
456
- - Must specify visual elements, e.g. "shown as", "represented by", "color-coded to differentiate"\r
457
515
  - Image URL must start with "./.learn/assets/"\r
458
516
  - Should simplify complex ideas into basic visual metaphors\r
459
- - Use color coding to differentiate concept categories\r
460
- - Include annotations that bridge the metaphor to the actual concept\r
517
+ - Do not include text unless the image cannot be understood without it: no titles, captions, quotes or dialogue.\r
461
518
  example: |\r
462
- ![Conceptual illustration of JavaScript closure using nested container metaphor: outer function shown as large blue box labeled "Outer Function Scope" containing variable boxes (x, y), with inner function as smaller orange box labeled "Inner Function" inside it, arrows pointing from inner to outer variables with label "maintains access to", simplified flat design with friendly colors and clear annotations explaining the closure relationship](./.learn/assets/closure-concept.png)\r
519
+ ![Conceptual illustration of a monthly budget shown as a large jar divided into three sections for rent, food and savings, with coins flowing into each section from a single paycheck envelope, using a friendly visual metaphor, simplified flat design, soft colors and clear shapes, with no text in the image](./.learn/assets/budget-jar.png)\r
463
520
  \r
464
521
  - name: realistic_photography\r
465
522
  intendedUse:\r
@@ -497,8 +554,9 @@ components:\r
497
554
  - Should include context that explains the educational relevance\r
498
555
  - If showing UI/screens, specify what elements to highlight or annotate\r
499
556
  - Maintain professional, clean aesthetic appropriate for learning\r
557
+ - Do not include text unless the image cannot be understood without it: no titles, captions, quotes or dialogue.\r
500
558
  example: |\r
501
- ![Professional photograph of a modern developer workspace showing a dual-monitor setup with code editor on left screen and browser developer tools on right screen, mechanical keyboard in foreground, with natural lighting from window, clean desk organization, demonstrating a typical full-stack development environment with annotations highlighting the debugging workflow between IDE and browser](./.learn/assets/developer-workspace.png)\r
559
+ ![Professional photograph of a modern developer workspace showing a dual-monitor setup with code editor on left screen and browser developer tools on right screen, mechanical keyboard in foreground, with natural lighting from window, clean desk organization, demonstrating a typical full-stack development environment](./.learn/assets/developer-workspace.png)\r
502
560
  `,Y5=!1,fa="";function Xp(e,t){return e==="localStorage"||e==="scorm"||e==="creatorWeb"&&t!=="creator"}function Ku(e){return e==="localStorage"||e==="scorm"||e==="creatorWeb"}const jvt="https://ai.4geeks.com";function i9e(e){Array.isArray(e)||(e=[e]);const t=new URLSearchParams(window.location.search);let r={};return e.forEach(a=>r[a]=t.get(a)),e.length==1?r[e[0]]:r}const qvt=location.protocol+"//"+location.hostname+(location.port?":"+location.port:""),Iv=function(){const e=window.location.pathname.endsWith("/config/index.html");let t=i9e("config");t&&t!==""&&(t=JSON.parse(atob(t)));let r=t?`${t.address}:${t.port}`:i9e("host")||qvt;return e&&(r=window.location.href.replace("/config/index.html","")),r};let Zp="localhost";const o9e=async()=>{var r;const e=Iv();if(window.location.pathname.endsWith("/config/index.html"))try{const n=await fetch(`${e}/.learn/config.json`);if(n.ok)return await n.json(),Zp="scorm",document.dispatchEvent(new CustomEvent("environment-change",{detail:{environment:"scorm"}})),"scorm"}catch(n){console.error("SCORM config fetch failed, falling back to legacy detection",n)}try{const n=Jr(),a=await fetch(`${e}/config?slug=${n}`),i=(r=a.headers.get("Content-Type"))==null?void 0:r.includes("application/json");if(a.ok&&i){let o="localhost";a.headers.get("X-Creator-Web")&&(o="creatorWeb"),Zp=o;const l=new CustomEvent("environment-change",{detail:{environment:o}});return document.dispatchEvent(l),o}else throw Error("The response was unsuccessful")}catch{Zp="localStorage";try{const a=await fetch(`${e}/config.json`);if(!a.ok)throw new Error("config.json not found");await a.json();const i=new CustomEvent("environment-change",{detail:{environment:"localStorage"}});return document.dispatchEvent(i),"localStorage"}catch{try{const i=await fetch(`${e}/.learn/config.json`);if(!i.ok)throw new Error(".learn/config.json not found");await i.json(),Zp="scorm";const o=new CustomEvent("environment-change",{detail:{environment:"scorm"}});return document.dispatchEvent(o),"scorm"}catch(i){return console.error("Error fetching scorm config, impossible to detect environment",yr(i)),"localStorage"}}}};o9e();const di="https://rigobot.herokuapp.com",s9e=3e4,Vvt=2e3,Gvt=3,Qvt=1e3,Wvt=3,Rv="https://breathecode.herokuapp.com",Yvt=()=>{const e=document.querySelector(".sidebar-component");e&&(window.getComputedStyle(e).left==="0px"?(e.classList.remove("sidebar-appear"),e.classList.add("sidebar-disappear")):(e==null||e.classList.remove("sidebar-disappear"),e==null||e.classList.add("sidebar-appear")),e==null||e.offsetWidth)},Kvt=async e=>await fetch(`${Iv()}/exercise/${e}`),Xvt=async(e,t)=>{const r=di+"/v1/conversation/?purpose="+e,n={method:"POST",headers:{"Content-Type":"application/json",Authorization:"Token "+t}};return await(await fetch(r,n)).json()},l9e=()=>{const e=document.querySelector("#socket-disconnected");e&&(e.style.display="block")},Zvt=()=>{const e=document.querySelector("#socket-disconnected");e&&(e.style.display="none")},Jvt=()=>{const e=new URLSearchParams(window.location.search);let t={};for(const[r,n]of e.entries())t[r]=n;return t},PC=()=>Jvt(),Ol=(e,t)=>{let r,n=null;const a=(...i)=>{n=i,clearTimeout(r),r=setTimeout(()=>{n=null,e(...i)},t)};return a.cancel=()=>{clearTimeout(r),n=null},a.flush=()=>{n!==null&&(clearTimeout(r),e(...n),n=null)},a},c9e=e=>e.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g,""),u9e=(e,t,r)=>{const n=new RegExp(t,"g");return e.replace(n,r)},d9e=(e,t,r)=>{let n=u9e(e,"{{publicUrl}}",t);if(r&&typeof r=="object")for(const[a,i]of Object.entries(r))n=u9e(n,`{{${a}}}`,i);return n},Z1=e=>{const t=new URLSearchParams;Object.entries(e).forEach(([a,i])=>{i!=null&&i!==""&&t.set(a,String(i))});const r=t.toString(),n=r?`${window.location.pathname}?${r}${window.location.hash}`:`${window.location.pathname}${window.location.hash}`;window.history.replaceState(null,"",n)};function fce(e,t){return Math.random()*(t-e)+e}class $C extends Error{constructor(t){super(t),this.name="MissingRigobotAccountError"}}class h9e extends Error{constructor(t){super(t),this.name="TokenExpiredError"}}const pce=(e,t="ai-conversation-message")=>{const r=e.voids.find(n=>n.slug===t);return r?r.balance.unit:0},K5=async e=>{const r=new TextEncoder().encode(e),n=await crypto.subtle.digest("SHA-256",r);return Array.from(new Uint8Array(n)).map(o=>o.toString(16).padStart(2,"0")).join("")};async function vh(e,t=2e3){const r=Date.now()-e;r<t&&await new Promise(n=>setTimeout(n,t-r))}const e4t=e=>{const t=new URL(window.location.href);t.searchParams.delete(e);const r=new URLSearchParams(t.hash.slice(1));r.delete(e),r.toString()?t.hash=`#${r.toString()}`:t.hash="",window.history.replaceState({},"",t.toString())},f9e=e=>{Xle.dataLayer(e)},Xu=(e,t)=>`---
503
561
  `+a9e(e)+`---
504
562
 
@@ -753,7 +811,7 @@ ${t}</body>`,n):`
753
811
  `+e.slice(o+1):l+=e.slice(a),l.slice(1)}function Rwt(e){for(var t="",r,n,a,i=0;i<e.length;i++){if(r=e.charCodeAt(i),r>=55296&&r<=56319&&(n=e.charCodeAt(i+1),n>=56320&&n<=57343)){t+=IEe((r-55296)*1024+n-56320+65536),i++;continue}a=jo[r],t+=!a&&jv(r)?e[i]:a||IEe(r)}return t}function Bwt(e,t,r){var n="",a=e.tag,i,o;for(i=0,o=r.length;i<o;i+=1)r0(e,t,r[i],!1,!1)&&(i!==0&&(n+=","+(e.condenseFlow?"":" ")),n+=e.dump);e.tag=a,e.dump="["+n+"]"}function Owt(e,t,r,n){var a="",i=e.tag,o,s;for(o=0,s=r.length;o<s;o+=1)r0(e,t+1,r[o],!0,!0)&&((!n||o!==0)&&(a+=qce(e,t)),e.dump&&dw===e.dump.charCodeAt(0)?a+="-":a+="- ",a+=e.dump);e.tag=i,e.dump=a||"[]"}function Dwt(e,t,r){var n="",a=e.tag,i=Object.keys(r),o,s,l,u,d;for(o=0,s=i.length;o<s;o+=1)d="",o!==0&&(d+=", "),e.condenseFlow&&(d+='"'),l=i[o],u=r[l],r0(e,t,l,!1,!1)&&(e.dump.length>1024&&(d+="? "),d+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),r0(e,t,u,!1,!1)&&(d+=e.dump,n+=d));e.tag=a,e.dump="{"+n+"}"}function Fwt(e,t,r,n){var a="",i=e.tag,o=Object.keys(r),s,l,u,d,h,f;if(e.sortKeys===!0)o.sort();else if(typeof e.sortKeys=="function")o.sort(e.sortKeys);else if(e.sortKeys)throw new uw("sortKeys must be a boolean or a function");for(s=0,l=o.length;s<l;s+=1)f="",(!n||s!==0)&&(f+=qce(e,t)),u=o[s],d=r[u],r0(e,t+1,u,!0,!0,!0)&&(h=e.tag!==null&&e.tag!=="?"||e.dump&&e.dump.length>1024,h&&(e.dump&&dw===e.dump.charCodeAt(0)?f+="?":f+="? "),f+=e.dump,h&&(f+=qce(e,t)),r0(e,t+1,d,!0,h)&&(e.dump&&dw===e.dump.charCodeAt(0)?f+=":":f+=": ",f+=e.dump,a+=f));e.tag=i,e.dump=a||"{}"}function jEe(e,t,r){var n,a,i,o,s,l;for(a=r?e.explicitTypes:e.implicitTypes,i=0,o=a.length;i<o;i+=1)if(s=a[i],(s.instanceOf||s.predicate)&&(!s.instanceOf||typeof t=="object"&&t instanceof s.instanceOf)&&(!s.predicate||s.predicate(t))){if(e.tag=r?s.tag:"?",s.represent){if(l=e.styleMap[s.tag]||s.defaultStyle,xEe.call(s.represent)==="[object Function]")n=s.represent(t,l);else if(_Ee.call(s.represent,l))n=s.represent[l](t,l);else throw new uw("!<"+s.tag+'> tag resolver accepts not "'+l+'" style');e.dump=n}return!0}return!1}function r0(e,t,r,n,a,i){e.tag=null,e.dump=r,jEe(e,r,!1)||jEe(e,r,!0);var o=xEe.call(e.dump);n&&(n=e.flowLevel<0||e.flowLevel>t);var s=o==="[object Object]"||o==="[object Array]",l,u;if(s&&(l=e.duplicates.indexOf(r),u=l!==-1),(e.tag!==null&&e.tag!=="?"||u||e.indent!==2&&t>0)&&(a=!1),u&&e.usedDuplicates[l])e.dump="*ref_"+l;else{if(s&&u&&!e.usedDuplicates[l]&&(e.usedDuplicates[l]=!0),o==="[object Object]")n&&Object.keys(e.dump).length!==0?(Fwt(e,t,e.dump,a),u&&(e.dump="&ref_"+l+e.dump)):(Dwt(e,t,e.dump),u&&(e.dump="&ref_"+l+" "+e.dump));else if(o==="[object Array]"){var d=e.noArrayIndent&&t>0?t-1:t;n&&e.dump.length!==0?(Owt(e,d,e.dump,a),u&&(e.dump="&ref_"+l+e.dump)):(Bwt(e,d,e.dump),u&&(e.dump="&ref_"+l+" "+e.dump))}else if(o==="[object String]")e.tag!=="?"&&Mwt(e,e.dump,t,i);else{if(e.skipInvalid)return!1;throw new uw("unacceptable kind of an object to dump "+o)}e.tag!==null&&e.tag!=="?"&&(e.dump="!<"+e.tag+"> "+e.dump)}return!0}function Pwt(e,t){var r=[],n=[],a,i;for(Gce(e,r,n),a=0,i=n.length;a<i;a+=1)t.duplicates.push(r[n[a]]);t.usedDuplicates=new Array(i)}function Gce(e,t,r){var n,a,i;if(e!==null&&typeof e=="object")if(a=t.indexOf(e),a!==-1)r.indexOf(a)===-1&&r.push(a);else if(t.push(e),Array.isArray(e))for(a=0,i=e.length;a<i;a+=1)Gce(e[a],t,r);else for(n=Object.keys(e),a=0,i=n.length;a<i;a+=1)Gce(e[n[a]],t,r)}function qEe(e,t){t=t||{};var r=new Ewt(t);return r.noRefs||Pwt(e,r),r0(r,0,e,!0,!0)?r.dump+`
754
812
  `:""}function $wt(e,t){return qEe(e,cw.extend({schema:swt},t))}jce.dump=qEe,jce.safeDump=$wt;var c9=iw,VEe=jce;function u9(e){return function(){throw new Error("Function "+e+" is deprecated and cannot be used.")}}ja.Type=to,ja.Schema=Pv,ja.FAILSAFE_SCHEMA=Pce,ja.JSON_SCHEMA=J9e,ja.CORE_SCHEMA=eEe,ja.DEFAULT_SAFE_SCHEMA=lw,ja.DEFAULT_FULL_SCHEMA=n9,ja.load=c9.load,ja.loadAll=c9.loadAll,ja.safeLoad=c9.safeLoad,ja.safeLoadAll=c9.safeLoadAll,ja.dump=VEe.dump,ja.safeDump=VEe.safeDump,ja.YAMLException=sw,ja.MINIMAL_SCHEMA=Pce,ja.SAFE_SCHEMA=lw,ja.DEFAULT_SCHEMA=n9,ja.scan=u9("scan"),ja.parse=u9("parse"),ja.compose=u9("compose"),ja.addConstructor=u9("addConstructor");var Uwt=ja,zwt=Uwt,GEe=zwt,Hwt="\\ufeff?",jwt=typeof process<"u"?process.platform:"",qwt="^("+Hwt+"(= yaml =|---)$([\\s\\S]*?)^(?:\\2|\\.\\.\\.)\\s*$"+(jwt==="win32"?"\\r?":"")+"(?:\\n)?)",QEe=new RegExp(qwt,"m");Oce.exports=Vwt,Oce.exports.test=Wwt;function Vwt(e,t){e=e||"";var r={allowUnsafe:!1};t=t instanceof Object?{...r,...t}:r,t.allowUnsafe=!!t.allowUnsafe;var n=e.split(/(\r?\n)/);return n[0]&&/= yaml =|---/.test(n[0])?Qwt(e,t.allowUnsafe):{attributes:{},body:e,bodyBegin:1}}function Gwt(e,t){for(var r=1,n=t.indexOf(`
755
813
  `),a=e.index+e[0].length;n!==-1;){if(n>=a)return r;r++,n=t.indexOf(`
756
- `,n+1)}return r}function Qwt(e,t){var r=QEe.exec(e);if(!r)return{attributes:{},body:e,bodyBegin:1};var n=t?GEe.load:GEe.safeLoad,a=r[r.length-1].replace(/^\s+|\s+$/g,""),i=n(a)||{},o=e.replace(r[0],""),s=Gwt(r,e);return{attributes:i,body:o,bodyBegin:s,frontmatter:a}}function Wwt(e){return e=e||"",QEe.test(e)}var Ywt=Oce.exports;const WEe=La(Ywt),Kwt=1;class Xwt{constructor(){hr(this,"name","engagement_indicator")}calculateStepIndicator(t,r){return r.status!=="skipped"&&r.status!=="unread"?Math.min(r.time_spent/3600*100,100):0}calculateGlobalIndicator(t,r){const n={total_time:.3,num_sessions:.2,completion_rate:.3,interactions:.2},a=Math.min(r.total_time_on_platform/36e3,1)*100,i=Math.min(r.num_sessions/5,1)*100,o=r.completion_rate,s=Math.min(r.total_interactions/100,1)*100;return(a*n.total_time+i*n.num_sessions+o*n.completion_rate+s*n.interactions)/100*100}}class Zwt{constructor(){hr(this,"name","frustration_indicator")}calculateStepIndicator(t,r){return r.status==="completed"?Math.min((r.comp_struggles+r.test_struggles)/20*100,100):r.status==="attempted"?Math.min(r.time_spent/1800*100,100):r.status==="skipped"?25:0}calculateGlobalIndicator(t,r){const n={steps_not_completed:.3,time_on_incomplete:.2,avg_comp_struggles:.2,avg_test_struggles:.2,skipped_steps:.1},a=r.total_steps>0?r.steps_not_completed/r.total_steps*100:0,i=Math.min(r.time_on_incomplete/18e3,1)*100,o=Math.min(r.avg_comp_struggles/10,1)*100,s=Math.min(r.avg_test_struggles/10,1)*100,l=r.total_steps>0?r.skipped_steps/r.total_steps*100:0;return(a*n.steps_not_completed+i*n.time_on_incomplete+o*n.avg_comp_struggles+s*n.avg_test_struggles+l*n.skipped_steps)/100*100}}function YEe(e){var x,w,C,S,N,L,M,B,F,I,O,P,D,R,U,$;let t=0;e.sessions&&e.sessions.length>0&&(t=e.sessions.reduce((z,q)=>z+q,0));let r=t;!t&&e.completed_at&&e.opened_at&&(r=e.completed_at-e.opened_at,t=r);let n=e.completed_at?"completed":e.opened_at?"attempted":"unread";const a=((x=e.compilations)==null?void 0:x.filter(z=>z.exit_code!==0).length)||0,i=((w=e.compilations)==null?void 0:w.filter(z=>z.exit_code===0).length)||0,o=((C=e.tests)==null?void 0:C.filter(z=>z.exit_code!==0).length)||0,s=((S=e.tests)==null?void 0:S.filter(z=>z.exit_code===0).length)||0,l=((N=e.quiz_submissions)==null?void 0:N.filter(z=>z.status==="ERROR").length)||0,u=((L=e.quiz_submissions)==null?void 0:L.filter(z=>z.status==="SUCCESS").length)||0;let d=0,h=0;for(let z=((M=e.compilations)==null?void 0:M.length)-1;z>=0&&e.compilations[z].exit_code!==0;z--)d++;for(let z=((B=e.compilations)==null?void 0:B.length)-1;z>=0&&e.compilations[z].exit_code===0;z--)h++;let f=0,p=0;for(let z=((F=e.tests)==null?void 0:F.length)-1;z>=0&&e.tests[z].exit_code!==0;z--)f++;for(let z=((I=e.tests)==null?void 0:I.length)-1;z>=0&&e.tests[z].exit_code===0;z--)p++;let g=0,A=0;for(let z=((O=e.quiz_submissions)==null?void 0:O.length)-1;z>=0&&e.quiz_submissions[z].status==="ERROR";z--)g++;for(let z=((P=e.quiz_submissions)==null?void 0:P.length)-1;z>=0&&e.quiz_submissions[z].status==="SUCCESS";z--)A++;const m=e.is_testeable||e.testeable_elements&&e.testeable_elements.length>0,y=(((D=e.quiz_submissions)==null?void 0:D.length)||0)===0&&(((R=e.tests)==null?void 0:R.length)||0)===0&&(((U=e.compilations)==null?void 0:U.length)||0)===0,v=!!m&&y,b=(($=e.ai_interactions)==null?void 0:$.length)||0,_=a+o+l>0&&i+s+u===0;return{status:n,time_spent:r,total_duration:t,comp_struggles:a,comp_success:i,total_comp_attempts:i+a,test_struggles:o,test_success:s,total_test_attempts:s+o,quiz_struggles:l,quiz_success:u,total_quiz_attempts:u+l,streak_comp_struggles:d,streak_comp_success:h,streak_test_struggle:f,streak_test_success:p,streak_quiz_struggles:g,streak_quiz_success:A,user_skipped:v,total_ai_interactions:b,ai_used:b>0,is_abandoned:_}}function Jwt(e){const t=e.length,r=e.map(YEe),n=r.filter(g=>g.status==="completed").length,a=r.filter(g=>g.status==="attempted").length,i=r.filter(g=>g.status==="skipped").length,o=r.filter(g=>g.status==="unread").length,s=t?n/t*100:0,l=e.reduce((g,A)=>{var m,y,v,b;return g+(((m=A.compilations)==null?void 0:m.length)||0)+(((y=A.tests)==null?void 0:y.length)||0)+(((v=A.quiz_submissions)==null?void 0:v.length)||0)+(((b=A.ai_interactions)==null?void 0:b.length)||0)},0),u=r.reduce((g,A)=>g+A.time_spent,0),d=a+i+o,h=t?r.reduce((g,A)=>g+A.comp_struggles,0)/t:0,f=t?r.reduce((g,A)=>g+A.test_struggles,0)/t:0;let p=0;for(const g of e){let A=0;for(const m of g.tests||[])m.exit_code===0?A++:A=0,p=Math.max(p,A)}return{total_time_on_platform:u,num_sessions:1,completion_rate:s,total_interactions:l,steps_not_completed:d,time_on_incomplete:u,avg_comp_struggles:h,avg_test_struggles:f,skipped_steps:i,total_steps:t,global_test_success_streak:p}}function qv(e){const t=[new Xwt,new Zwt],r=e.steps.map(o=>YEe(o)),n=Jwt(e.steps),a=e.steps.map((o,s)=>{const l=r[s],u={engagement_indicator:0,frustration_indicator:0};return t.forEach(d=>{u[d.name]=d.calculateStepIndicator(o,l)}),{slug:o.slug,metrics:l,indicators:u}}),i={engagement_indicator:0,frustration_indicator:0};return t.forEach(o=>{i[o.name]=o.calculateGlobalIndicator(a.map(s=>s.indicators[o.name]),n)}),{algo_version:Kwt,global:{metrics:n,indicators:i},steps:a}}function ekt(e,t){var d,h,f,p;if(!e.testeable_elements)return;const r=e.testeable_elements.find(g=>g.hash===t);if(!r)return;const n=r.type==="quiz"?((d=e.quiz_submissions.find(g=>g.quiz_hash===t))==null?void 0:d.ended_at)||e.opened_at||0:((h=e.tests.shift())==null?void 0:h.started_at)||e.opened_at||0,a=r.type==="quiz"?((f=e.quiz_submissions.findLast(g=>g.quiz_hash===t))==null?void 0:f.ended_at)||e.completed_at||1:((p=e.tests.shift())==null?void 0:p.started_at)||e.completed_at||1,i=Number((a-n)/1e3),o=e.ai_interactions.filter(g=>g.started_at>=n&&g.ended_at<=a).length,s=r.type==="quiz"?e.quiz_submissions.filter(g=>g.quiz_hash===t).length:e.tests.length,l=r.type==="quiz"?e.quiz_submissions.filter(g=>g.quiz_hash===t&&g.status==="ERROR").length:e.tests.filter(g=>g.exit_code!==1).length,u=r.type==="quiz"?e.quiz_submissions.filter(g=>g.quiz_hash===t&&g.status==="SUCCESS").length:e.tests.filter(g=>g.exit_code===1).length;return{started_at:n,ended_at:a,resolution_time_seconds:i,n_ai_interactions:o,n_tries:s,n_fail:l,n_success:u}}const KEe={name:"learnpack-ai",private:!0,version:"5.0.414",scripts:{dev:"node src/utils/development.js --turn on && vite",build:"node src/utils/development.js --turn off && tsc && vite build && node src/utils/uploadLearnpack.js && node bin/publish.js && node src/utils/createVersionsFile.js ","dev-build":"node src/utils/development.js --turn on && tsc && vite build",lint:"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",preview:"vite preview",watch:"vite build --watch","compile-command":"tsc src/utils/development.ts && node src/utils/development.js --turn on"},dependencies:{"@dnd-kit/core":"^6.3.1","@dnd-kit/sortable":"^10.0.0","@dnd-kit/utilities":"^3.2.2","@monaco-editor/react":"^4.6.0","@radix-ui/react-tooltip":"^1.2.8",axios:"^1.7.7","class-variance-authority":"^0.7.1",clsx:"^2.1.1",dompurify:"^3.3.0","front-matter":"^4.0.2","highlight.js":"^11.11.1",html2canvas:"^1.4.1",i18next:"^23.10.1","js-yaml":"^4.1.0","lucide-react":"^0.545.0",mermaid:"^11.6.0",mitt:"^3.0.1","monaco-editor":"^0.50.0","prompt-sync":"^4.2.0","pusher-js":"7.6.0","radix-ui":"^1.4.3",react:"^18.2.0","react-dom":"^18.2.0","react-draggable":"^4.4.6","react-gtm-module":"^2.0.11","react-hot-toast":"^2.4.1","react-i18next":"^14.1.0","react-markdown":"^9.0.1","react-monaco-editor":"^0.56.1","react-router-dom":"^6.27.0","react-syntax-highlighter":"^15.6.1","rehype-katex":"^7.0.1","remark-emoji":"^5.0.1","remark-gfm":"^4.0.0","remark-math":"^6.0.0","socket.io":"^4.7.2","socket.io-client":"^4.7.2","tailwind-merge":"^3.3.1","tailwindcss-animate":"^1.0.7",uuid:"^10.0.0",zustand:"^4.4.1"},devDependencies:{"@aws-sdk/client-cloudfront":"^3.840.0","@aws-sdk/client-s3":"^3.709.0","@types/dompurify":"^3.0.5","@types/js-yaml":"^4.0.9","@types/markdown-it":"^14.1.2","@types/node":"^24.9.1","@types/react":"^18.2.15","@types/react-dom":"^18.2.7","@types/react-gtm-module":"^2.0.3","@types/react-syntax-highlighter":"^15.5.13","@types/remarkable":"^2.0.3","@types/uuid":"^10.0.0","@typescript-eslint/eslint-plugin":"^6.0.0","@typescript-eslint/parser":"^6.0.0","@vitejs/plugin-react-swc":"^3.3.2",archiver:"^6.0.1",autoprefixer:"^10.4.21",dotenv:"^16.4.7",eslint:"^8.45.0","eslint-plugin-react-hooks":"^4.6.0","eslint-plugin-react-refresh":"^0.4.3",inquirer:"^9.2.12",postcss:"^8.5.6",tailwindcss:"^3.4.18",typescript:"^5.0.2",vite:"^4.5.13"}};function tkt(e){return{all:e=e||new Map,on:function(t,r){var n=e.get(t);n?n.push(r):e.set(t,[r])},off:function(t,r){var n=e.get(t);n&&(r?n.splice(n.indexOf(r)>>>0,1):e.set(t,[]))},emit:function(t,r){var n=e.get(t);n&&n.slice().map(function(a){a(r)}),(n=e.get("*"))&&n.slice().map(function(a){a(t,r)})}}}const _r=tkt(),Qce=async(e,t)=>{if(!t)throw new Error("Slug is required");const r=`${di}/v1/learnpack/session/?slug=${t}`;try{return(await Hr.get(r,{headers:{Authorization:`Token ${e}`}})).data}catch(n){throw console.error("Error fetching session:",yr(n)),n}},Wce=async(e,t,r,n,a)=>{const i=`${di}/v1/learnpack/session/${a}/`,o={tab_hash:t,config_json:n,package_slug:r};try{return(await Hr.put(i,o,{headers:{Authorization:`Token ${e}`,"Content-Type":"application/json"}})).data}catch(s){throw console.error("Error updating session:",yr(s)),s}},rkt=async(e,t,r,n)=>{const a=`${di}/v1/learnpack/session/`,i={tab_hash:t,config_json:n,package_slug:r};try{return(await Hr.post(a,i,{headers:{Authorization:`Token ${e}`,"Content-Type":"application/json"}})).data}catch(o){throw console.error("Error creating session:",yr(o)),o}};async function nkt(e){const t=`${Rv}/v1/payments/me/service/consumable?virtual=true`,r={Authorization:`Token ${e}`};try{return(await Hr.get(t,{headers:r})).data}catch(n){throw console.error("Error fetching consumables:",yr(n)),n}}async function akt(e,t="ai-conversation-message"){const r=`${Rv}/v1/payments/me/service/${t}/consumptionsession`,n={Authorization:`Token ${e}`};try{const a=await Hr.put(r,{},{headers:n});return a.status>=200&&a.status<300?(a.data,`${t}`,!0):(console.error(`Request failed with status code: ${a.status}`),console.error(`Response: ${a.data}`),!1)}catch(a){return console.error(`Error consuming ${t}:`,yr(a)),!1}}function ikt(e){if(!Array.isArray(e))return[];const t=[];for(const r of e){const n=typeof r=="number"?r:Number(r);Number.isFinite(n)&&t.push(n)}return t}async function Yce(e,t){var a;const r={id:null,assetIds:[]};if(!(e!=null&&e.trim())||!t)return r;const n=`${di}/v1/learnpack/package/${encodeURIComponent(t)}/assets/`;try{const o=(await Hr.get(n,{headers:{Authorization:`Token ${e.trim()}`}})).data;return{id:(o==null?void 0:o.id)!=null?String(o.id):null,assetIds:ikt(o==null?void 0:o.asset_ids)}}catch(i){return Hr.isAxiosError(i)&&((a=i.response)==null?void 0:a.status)===404||console.warn("fetchLearnpackPackageInfo failed:",yr(i)),r}}const okt=async(e,t)=>{var a;const r=`${di}/v1/learnpack/package/${t}/`,n={Authorization:`Token ${e}`};try{const i=await Hr.get(r,{headers:n});return{isAuthor:i.status===200,status:i.status}}catch(i){const o=((a=i==null?void 0:i.response)==null?void 0:a.status)||500;return console.error("Error fetching package:",yr(i)),{isAuthor:!1,status:o}}},skt=async(e,t,r=di)=>{const n=await Hr.get(`${r}/v1/prompting/completion/${t}/`,{headers:{Authorization:`Token ${e}`,"Content-Type":"application/json"}});return n.data,n.data};function lkt(e,t){if(t.length===0)return e;const r=e.includes("?")?"&":"?";return`${e}${r}asset_id=${t.join(",")}`}const ckt=async function(e,t,r,n){if(!e||!r){console.error("URL and token are required");return}r=r.trim();const a={"Content-Type":"application/json",Authorization:`Token ${r}`},i=lkt(e,n??[]);try{return(await Hr.post(i,t,{headers:a})).data}catch(o){throw console.error("Error while sending batch telemetry",yr(o)),o}},ukt=async function(e,t){const r=`${di}/v1/learnpack/telemetry`,n={"Content-Type":"application/json",Authorization:`Token ${t}`};try{return(await Hr.post(r,e,{headers:n})).data}catch(a){throw console.error("Error while sending batch telemetry",yr(a)),a}},dkt=5e3,hkt=6e4,fkt=5*60*1e3;async function XEe(e){var u;const{userId:t,packageSlug:r,rigoToken:n}=e;if(!t||!n||!r)return null;const a=n.trim(),i=new URLSearchParams;i.set("user_ids",String(t)),i.set("include_buffer","true"),i.set("include_steps","true"),i.set("package_slug",r);const o=`${di}/v1/learnpack/telemetry?${i.toString()}`,s=new AbortController,l=setTimeout(()=>s.abort(),dkt);try{const d=await fetch(o,{method:"GET",headers:{Authorization:`Token ${a}`,Accept:"application/json"},signal:s.signal});if(!d.ok)return console.warn("fetchTelemetryFromServer: non-OK response",d.status),null;const h=await d.json(),f=(u=h==null?void 0:h.results)==null?void 0:u[0];return!f||typeof f!="object"?null:f}catch(d){return d.name==="AbortError"?console.warn("fetchTelemetryFromServer: timeout or aborted"):console.warn("fetchTelemetryFromServer: failed",yr(d)),null}finally{clearTimeout(l)}}async function pkt(e){var u;const{userId:t,packageSlug:r,rigoToken:n}=e;if(!t||!n||!r)return{status:"empty"};const a=n.trim(),i=new URLSearchParams;i.set("user_ids",String(t)),i.set("include_buffer","true"),i.set("include_steps","true"),i.set("package_slug",r);const o=`${di}/v1/learnpack/telemetry?${i.toString()}`,s=new AbortController,l=setTimeout(()=>s.abort(),hkt);try{const d=await fetch(o,{method:"GET",headers:{Authorization:`Token ${a}`,Accept:"application/json"},signal:s.signal});if(!d.ok)return console.warn("fetchTelemetryWithStatus: non-OK response",d.status),{status:"server_error",code:d.status};const h=await d.json(),f=(u=h==null?void 0:h.results)==null?void 0:u[0];return!f||typeof f!="object"?{status:"empty"}:{status:"ok",data:f}}catch(d){return d.name==="AbortError"?(console.warn("fetchTelemetryWithStatus: timeout"),{status:"timeout"}):(console.warn("fetchTelemetryWithStatus: network error",yr(d)),{status:"timeout"})}finally{clearTimeout(l)}}function ZEe(e){return!e||typeof e.last_interaction_at!="number"?0:e.last_interaction_at}function d9(){return Math.random().toString(36).slice(2,10)+Math.random().toString(36).slice(2,10)}function JEe(e,t){const r=new Map;for(const n of t)n.slug&&r.set(n.slug,n);return e.map(n=>{var o,s,l,u,d,h,f,p,g,A;const a=r.get(n.slug);if(!a)return n;const i={...n};if(!i.completed_at&&a.completed_at&&(i.completed_at=a.completed_at),!i.is_completed&&a.is_completed&&(i.is_completed=!0),!i.opened_at&&a.opened_at&&(i.opened_at=a.opened_at),(o=a.testeable_elements)!=null&&o.length){const m=new Map((i.testeable_elements??[]).map(v=>[v.hash,v])),y=[...i.testeable_elements??[]];for(const v of a.testeable_elements){const b=m.get(v.hash);if(!b)y.push(v);else if(v.is_completed&&!b.is_completed){const _=y.findIndex(x=>x.hash===v.hash);_!==-1&&(y[_]={...b,is_completed:!0,metrics:b.metrics??v.metrics})}}i.testeable_elements=y}if((s=a.quiz_submissions)!=null&&s.length){const m=new Set((i.quiz_submissions??[]).map(v=>v.quiz_hash)),y=a.quiz_submissions.filter(v=>!m.has(v.quiz_hash));y.length>0&&(i.quiz_submissions=[...i.quiz_submissions??[],...y])}return!((l=i.tests)!=null&&l.length)&&((u=a.tests)!=null&&u.length)&&(i.tests=[...a.tests]),!((d=i.compilations)!=null&&d.length)&&((h=a.compilations)!=null&&h.length)&&(i.compilations=[...a.compilations]),!((f=i.ai_interactions)!=null&&f.length)&&((p=a.ai_interactions)!=null&&p.length)&&(i.ai_interactions=[...a.ai_interactions]),!((g=i.sessions)!=null&&g.length)&&((A=a.sessions)!=null&&A.length)&&(i.sessions=[...a.sessions]),i})}function gkt(e,t,r,n,a,i){const o=!!e,s=t&&t.slug===n;if(o&&!s)return{telemetry:n0({...e},r,n,a,i),source:"server"};if(!o&&s)return{telemetry:n0({...t},r,n,a,i),source:"local"};if(o&&s){const u=ZEe(e),d=ZEe(t),[h,f,p]=d>u?[t,e,"local"]:[e,t,"server"],g=n0({...h},r,n,a,i);return g.steps=JEe(g.steps,f.steps??[]),{telemetry:g,source:p}}const l=Date.now();return{telemetry:n0({telemetry_id:d9(),slug:n,version:`${i}`,agent:a,tutorial_started_at:l,last_interaction_at:l,steps:r,workout_session:[{started_at:l}],fullname:"",cohort_id:null,academy_id:null},r,n,a,i),source:"new"}}function Akt(e,t,r){if(!e||!r)return;const n=r.trim();fetch(e,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Token ${n}`},body:JSON.stringify(t),keepalive:!0}).catch(()=>{})}const mkt=async function(e,t,r){!e||!r||fetch(e,{method:"POST",body:JSON.stringify(t)}).then(n=>n).catch(n=>{console.error("Error while sending stream Telemetry",yr(n))})},ykt=async function(){try{return(await Hr.get(`${fa}/telemetry`)).data}catch(e){return console.error("Error while retrieving telemetry from CLI",yr(e)),null}},vkt=async function(e){try{const t=await Hr.post(`${fa}/telemetry`,e);return t.data,t.data}catch(t){return console.error("Error while saving telemetry in CLI",yr(t)),null}};function Kce(e){if(typeof e!="string")throw new TypeError("Input must be a string");return btoa(encodeURIComponent(e))}const bkt=(e,t)=>{let r={...t};return typeof r.starting_at<"u"&&typeof r.started_at>"u"&&(r.started_at=r.starting_at,delete r.starting_at),typeof r.ending_at<"u"&&typeof r.ended_at>"u"&&(r.ended_at=r.ending_at,delete r.ending_at),typeof r.started_at>"u"&&e!=="open_step"&&(r.started_at=Date.now()),typeof r.ended_at>"u"&&e!=="open_step"&&(r.ended_at=Date.now()),r.source_code&&(r.source_code=Kce(r.source_code)),r.stdout&&(r.stdout=Kce(r.stdout)),r.stderr&&(r.stderr=Kce(r.stderr)),Object.prototype.hasOwnProperty.call(r,"exitCode")&&(r.exit_code=r.exitCode,r.exitCode=void 0),r},wkt=["telemetry_id","user_id","fullname","slug","package_id","version","cohort_id","academy_id","agent","tutorial_started_at","last_interaction_at","steps","workout_session","global_metrics","global_indicators"];function eSe(e){return e==null||typeof e=="string"&&e.trim()===""?null:typeof e=="number"&&!Number.isNaN(e)?String(e):typeof e=="string"?e:null}function kkt(e,t){if(!Array.isArray(e))return[{started_at:t}];if(e.length===0)return[{started_at:t}];const r=e.filter(n=>!!n&&typeof n=="object"&&typeof n.started_at=="number");return r.length===0?[{started_at:t}]:r}function xkt(e,t){const r=new Map;for(const n of t)n.slug&&r.set(n.slug,n);return e.map(n=>{const a=r.get(n.slug);return a?{slug:n.slug,position:n.position,files:n.files,is_testeable:n.is_testeable,compilations:a.compilations??[],tests:a.tests??[],ai_interactions:a.ai_interactions??[],quiz_submissions:a.quiz_submissions??[],testeable_elements:a.testeable_elements??[],is_completed:a.is_completed??!1,completed_at:a.completed_at,opened_at:a.opened_at,sessions:a.sessions??[]}:n})}function n0(e,t,r,n,a){const i=e,o={};for(const g of wkt)Object.prototype.hasOwnProperty.call(i,g)&&(o[g]=i[g]);const s=Date.now(),l=r,u=typeof o.version=="string"&&o.version.trim()!==""?o.version:`${a}`,d=typeof o.tutorial_started_at=="number"?o.tutorial_started_at:s,h=typeof o.last_interaction_at=="number"?o.last_interaction_at:s,f=Array.isArray(o.steps)&&o.steps.length>0?xkt(t,o.steps):t,p=kkt(o.workout_session,s);return{telemetry_id:o.telemetry_id,user_id:o.user_id,fullname:o.fullname,slug:l,package_id:o.package_id,version:u,cohort_id:eSe(o.cohort_id),academy_id:eSe(o.academy_id),agent:typeof o.agent=="string"?o.agent:n,tutorial_started_at:d,last_interaction_at:h,steps:f,workout_session:p,global_metrics:o.global_metrics,global_indicators:o.global_indicators}}function tSe(e,t){e.telemetry_id||(e.telemetry_id=d9());const r=qv(e),n=e.steps.map((a,i)=>{const o=e.telemetry_id||d9();return{...a,telemetry_id:o,metrics:r.steps[i].metrics,indicators:r.steps[i].indicators}});return{...e,steps:n,global_metrics:r.global.metrics,global_indicators:r.global.indicators,user_id:t}}const cr={current:null,urls:{},telemetryKey:"",packageAssetIds:[],listeners:{},agent:"cloud",prevStep:void 0,prevStepStartedAt:void 0,activeHashes:new Map,currentReadyAt:void 0,prunedCompletions:new Set,nonTesteableSlugs:new Set,_lessonRenderedDebounce:null,user:{token:"",rigo_token:"",id:"",fullname:""},tutorialSlug:"",started:!1,reconciling:!1,version:`${KEe.version}`||"CLOUD:0.0.0",salute:e=>{},start:function(e,t,r,n,a,i){return this.packageAssetIds=[],this.telemetryKey=n,this.tutorialSlug=r,this.agent=e,this.user.id=a.user_id,this.user.token=a.token,this.user.rigo_token=a.rigo_token,this.user.fullname=a.fullname,this.current?Promise.resolve():(this.activeHashes=new Map,this.prunedCompletions=new Set,this.currentReadyAt=void 0,e==="cloud"?(this.reconciling=!0,(async()=>{var s;let o=!1;try{const l=jr.get(this.telemetryKey),u=String(l==null?void 0:l.user_id)===String(a==null?void 0:a.user_id),d=l&&l.slug===r&&u?l:null,h=!!((s=a.rigo_token)!=null&&s.trim()&&r),f=i!==void 0,[p,g]=await Promise.all([f?Promise.resolve(i):a.rigo_token&&a.user_id?XEe({userId:a.user_id,packageSlug:r,rigoToken:a.rigo_token}):Promise.resolve(null),h?Yce(a.rigo_token,r):Promise.resolve({id:null,assetIds:[]})]);this.packageAssetIds=g.assetIds;const{telemetry:A,source:m}=gkt(p,d,t,r,e,this.version);if(this.current=A,this.currentReadyAt=Date.now(),this.current.user_id=a.user_id,this.current.fullname=a.fullname,this.current.cohort_id=a.cohort_id||null,this.current.academy_id=a.academy_id||null,this.current.version||(this.current.version=`CLOUD:${this.version}`),g.id!=null&&this.mergePackageIdIfMissing(g.id),this.finishWorkoutSession(),this.save(),this.started=!0,!a.user_id){console.warn("No user ID found, impossible to submit telemetry at start");return}o=m==="local"}catch(l){throw console.error("There was a problem starting the Telemetry",yr(l)),new Error(`There was a problem starting, reload LearnPack
814
+ `,n+1)}return r}function Qwt(e,t){var r=QEe.exec(e);if(!r)return{attributes:{},body:e,bodyBegin:1};var n=t?GEe.load:GEe.safeLoad,a=r[r.length-1].replace(/^\s+|\s+$/g,""),i=n(a)||{},o=e.replace(r[0],""),s=Gwt(r,e);return{attributes:i,body:o,bodyBegin:s,frontmatter:a}}function Wwt(e){return e=e||"",QEe.test(e)}var Ywt=Oce.exports;const WEe=La(Ywt),Kwt=1;class Xwt{constructor(){hr(this,"name","engagement_indicator")}calculateStepIndicator(t,r){return r.status!=="skipped"&&r.status!=="unread"?Math.min(r.time_spent/3600*100,100):0}calculateGlobalIndicator(t,r){const n={total_time:.3,num_sessions:.2,completion_rate:.3,interactions:.2},a=Math.min(r.total_time_on_platform/36e3,1)*100,i=Math.min(r.num_sessions/5,1)*100,o=r.completion_rate,s=Math.min(r.total_interactions/100,1)*100;return(a*n.total_time+i*n.num_sessions+o*n.completion_rate+s*n.interactions)/100*100}}class Zwt{constructor(){hr(this,"name","frustration_indicator")}calculateStepIndicator(t,r){return r.status==="completed"?Math.min((r.comp_struggles+r.test_struggles)/20*100,100):r.status==="attempted"?Math.min(r.time_spent/1800*100,100):r.status==="skipped"?25:0}calculateGlobalIndicator(t,r){const n={steps_not_completed:.3,time_on_incomplete:.2,avg_comp_struggles:.2,avg_test_struggles:.2,skipped_steps:.1},a=r.total_steps>0?r.steps_not_completed/r.total_steps*100:0,i=Math.min(r.time_on_incomplete/18e3,1)*100,o=Math.min(r.avg_comp_struggles/10,1)*100,s=Math.min(r.avg_test_struggles/10,1)*100,l=r.total_steps>0?r.skipped_steps/r.total_steps*100:0;return(a*n.steps_not_completed+i*n.time_on_incomplete+o*n.avg_comp_struggles+s*n.avg_test_struggles+l*n.skipped_steps)/100*100}}function YEe(e){var x,w,C,S,N,L,M,B,F,I,O,P,D,R,U,$;let t=0;e.sessions&&e.sessions.length>0&&(t=e.sessions.reduce((z,q)=>z+q,0));let r=t;!t&&e.completed_at&&e.opened_at&&(r=e.completed_at-e.opened_at,t=r);let n=e.completed_at?"completed":e.opened_at?"attempted":"unread";const a=((x=e.compilations)==null?void 0:x.filter(z=>z.exit_code!==0).length)||0,i=((w=e.compilations)==null?void 0:w.filter(z=>z.exit_code===0).length)||0,o=((C=e.tests)==null?void 0:C.filter(z=>z.exit_code!==0).length)||0,s=((S=e.tests)==null?void 0:S.filter(z=>z.exit_code===0).length)||0,l=((N=e.quiz_submissions)==null?void 0:N.filter(z=>z.status==="ERROR").length)||0,u=((L=e.quiz_submissions)==null?void 0:L.filter(z=>z.status==="SUCCESS").length)||0;let d=0,h=0;for(let z=((M=e.compilations)==null?void 0:M.length)-1;z>=0&&e.compilations[z].exit_code!==0;z--)d++;for(let z=((B=e.compilations)==null?void 0:B.length)-1;z>=0&&e.compilations[z].exit_code===0;z--)h++;let f=0,p=0;for(let z=((F=e.tests)==null?void 0:F.length)-1;z>=0&&e.tests[z].exit_code!==0;z--)f++;for(let z=((I=e.tests)==null?void 0:I.length)-1;z>=0&&e.tests[z].exit_code===0;z--)p++;let g=0,A=0;for(let z=((O=e.quiz_submissions)==null?void 0:O.length)-1;z>=0&&e.quiz_submissions[z].status==="ERROR";z--)g++;for(let z=((P=e.quiz_submissions)==null?void 0:P.length)-1;z>=0&&e.quiz_submissions[z].status==="SUCCESS";z--)A++;const m=e.is_testeable||e.testeable_elements&&e.testeable_elements.length>0,y=(((D=e.quiz_submissions)==null?void 0:D.length)||0)===0&&(((R=e.tests)==null?void 0:R.length)||0)===0&&(((U=e.compilations)==null?void 0:U.length)||0)===0,v=!!m&&y,b=(($=e.ai_interactions)==null?void 0:$.length)||0,_=a+o+l>0&&i+s+u===0;return{status:n,time_spent:r,total_duration:t,comp_struggles:a,comp_success:i,total_comp_attempts:i+a,test_struggles:o,test_success:s,total_test_attempts:s+o,quiz_struggles:l,quiz_success:u,total_quiz_attempts:u+l,streak_comp_struggles:d,streak_comp_success:h,streak_test_struggle:f,streak_test_success:p,streak_quiz_struggles:g,streak_quiz_success:A,user_skipped:v,total_ai_interactions:b,ai_used:b>0,is_abandoned:_}}function Jwt(e){const t=e.length,r=e.map(YEe),n=r.filter(g=>g.status==="completed").length,a=r.filter(g=>g.status==="attempted").length,i=r.filter(g=>g.status==="skipped").length,o=r.filter(g=>g.status==="unread").length,s=t?n/t*100:0,l=e.reduce((g,A)=>{var m,y,v,b;return g+(((m=A.compilations)==null?void 0:m.length)||0)+(((y=A.tests)==null?void 0:y.length)||0)+(((v=A.quiz_submissions)==null?void 0:v.length)||0)+(((b=A.ai_interactions)==null?void 0:b.length)||0)},0),u=r.reduce((g,A)=>g+A.time_spent,0),d=a+i+o,h=t?r.reduce((g,A)=>g+A.comp_struggles,0)/t:0,f=t?r.reduce((g,A)=>g+A.test_struggles,0)/t:0;let p=0;for(const g of e){let A=0;for(const m of g.tests||[])m.exit_code===0?A++:A=0,p=Math.max(p,A)}return{total_time_on_platform:u,num_sessions:1,completion_rate:s,total_interactions:l,steps_not_completed:d,time_on_incomplete:u,avg_comp_struggles:h,avg_test_struggles:f,skipped_steps:i,total_steps:t,global_test_success_streak:p}}function qv(e){const t=[new Xwt,new Zwt],r=e.steps.map(o=>YEe(o)),n=Jwt(e.steps),a=e.steps.map((o,s)=>{const l=r[s],u={engagement_indicator:0,frustration_indicator:0};return t.forEach(d=>{u[d.name]=d.calculateStepIndicator(o,l)}),{slug:o.slug,metrics:l,indicators:u}}),i={engagement_indicator:0,frustration_indicator:0};return t.forEach(o=>{i[o.name]=o.calculateGlobalIndicator(a.map(s=>s.indicators[o.name]),n)}),{algo_version:Kwt,global:{metrics:n,indicators:i},steps:a}}function ekt(e,t){var d,h,f,p;if(!e.testeable_elements)return;const r=e.testeable_elements.find(g=>g.hash===t);if(!r)return;const n=r.type==="quiz"?((d=e.quiz_submissions.find(g=>g.quiz_hash===t))==null?void 0:d.ended_at)||e.opened_at||0:((h=e.tests.shift())==null?void 0:h.started_at)||e.opened_at||0,a=r.type==="quiz"?((f=e.quiz_submissions.findLast(g=>g.quiz_hash===t))==null?void 0:f.ended_at)||e.completed_at||1:((p=e.tests.shift())==null?void 0:p.started_at)||e.completed_at||1,i=Number((a-n)/1e3),o=e.ai_interactions.filter(g=>g.started_at>=n&&g.ended_at<=a).length,s=r.type==="quiz"?e.quiz_submissions.filter(g=>g.quiz_hash===t).length:e.tests.length,l=r.type==="quiz"?e.quiz_submissions.filter(g=>g.quiz_hash===t&&g.status==="ERROR").length:e.tests.filter(g=>g.exit_code!==1).length,u=r.type==="quiz"?e.quiz_submissions.filter(g=>g.quiz_hash===t&&g.status==="SUCCESS").length:e.tests.filter(g=>g.exit_code===1).length;return{started_at:n,ended_at:a,resolution_time_seconds:i,n_ai_interactions:o,n_tries:s,n_fail:l,n_success:u}}const KEe={name:"learnpack-ai",private:!0,version:"5.0.415",scripts:{dev:"node src/utils/development.js --turn on && vite",build:"node src/utils/development.js --turn off && tsc && vite build && node src/utils/uploadLearnpack.js && node bin/publish.js && node src/utils/createVersionsFile.js ","dev-build":"node src/utils/development.js --turn on && tsc && vite build",lint:"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",preview:"vite preview",watch:"vite build --watch","compile-command":"tsc src/utils/development.ts && node src/utils/development.js --turn on"},dependencies:{"@dnd-kit/core":"^6.3.1","@dnd-kit/sortable":"^10.0.0","@dnd-kit/utilities":"^3.2.2","@monaco-editor/react":"^4.6.0","@radix-ui/react-tooltip":"^1.2.8",axios:"^1.7.7","class-variance-authority":"^0.7.1",clsx:"^2.1.1",dompurify:"^3.3.0","front-matter":"^4.0.2","highlight.js":"^11.11.1",html2canvas:"^1.4.1",i18next:"^23.10.1","js-yaml":"^4.1.0","lucide-react":"^0.545.0",mermaid:"^11.6.0",mitt:"^3.0.1","monaco-editor":"^0.50.0","prompt-sync":"^4.2.0","pusher-js":"7.6.0","radix-ui":"^1.4.3",react:"^18.2.0","react-dom":"^18.2.0","react-draggable":"^4.4.6","react-gtm-module":"^2.0.11","react-hot-toast":"^2.4.1","react-i18next":"^14.1.0","react-markdown":"^9.0.1","react-monaco-editor":"^0.56.1","react-router-dom":"^6.27.0","react-syntax-highlighter":"^15.6.1","rehype-katex":"^7.0.1","remark-emoji":"^5.0.1","remark-gfm":"^4.0.0","remark-math":"^6.0.0","socket.io":"^4.7.2","socket.io-client":"^4.7.2","tailwind-merge":"^3.3.1","tailwindcss-animate":"^1.0.7",uuid:"^10.0.0",zustand:"^4.4.1"},devDependencies:{"@aws-sdk/client-cloudfront":"^3.840.0","@aws-sdk/client-s3":"^3.709.0","@types/dompurify":"^3.0.5","@types/js-yaml":"^4.0.9","@types/markdown-it":"^14.1.2","@types/node":"^24.9.1","@types/react":"^18.2.15","@types/react-dom":"^18.2.7","@types/react-gtm-module":"^2.0.3","@types/react-syntax-highlighter":"^15.5.13","@types/remarkable":"^2.0.3","@types/uuid":"^10.0.0","@typescript-eslint/eslint-plugin":"^6.0.0","@typescript-eslint/parser":"^6.0.0","@vitejs/plugin-react-swc":"^3.3.2",archiver:"^6.0.1",autoprefixer:"^10.4.21",dotenv:"^16.4.7",eslint:"^8.45.0","eslint-plugin-react-hooks":"^4.6.0","eslint-plugin-react-refresh":"^0.4.3",inquirer:"^9.2.12",postcss:"^8.5.6",tailwindcss:"^3.4.18",typescript:"^5.0.2",vite:"^4.5.13"}};function tkt(e){return{all:e=e||new Map,on:function(t,r){var n=e.get(t);n?n.push(r):e.set(t,[r])},off:function(t,r){var n=e.get(t);n&&(r?n.splice(n.indexOf(r)>>>0,1):e.set(t,[]))},emit:function(t,r){var n=e.get(t);n&&n.slice().map(function(a){a(r)}),(n=e.get("*"))&&n.slice().map(function(a){a(t,r)})}}}const _r=tkt(),Qce=async(e,t)=>{if(!t)throw new Error("Slug is required");const r=`${di}/v1/learnpack/session/?slug=${t}`;try{return(await Hr.get(r,{headers:{Authorization:`Token ${e}`}})).data}catch(n){throw console.error("Error fetching session:",yr(n)),n}},Wce=async(e,t,r,n,a)=>{const i=`${di}/v1/learnpack/session/${a}/`,o={tab_hash:t,config_json:n,package_slug:r};try{return(await Hr.put(i,o,{headers:{Authorization:`Token ${e}`,"Content-Type":"application/json"}})).data}catch(s){throw console.error("Error updating session:",yr(s)),s}},rkt=async(e,t,r,n)=>{const a=`${di}/v1/learnpack/session/`,i={tab_hash:t,config_json:n,package_slug:r};try{return(await Hr.post(a,i,{headers:{Authorization:`Token ${e}`,"Content-Type":"application/json"}})).data}catch(o){throw console.error("Error creating session:",yr(o)),o}};async function nkt(e){const t=`${Rv}/v1/payments/me/service/consumable?virtual=true`,r={Authorization:`Token ${e}`};try{return(await Hr.get(t,{headers:r})).data}catch(n){throw console.error("Error fetching consumables:",yr(n)),n}}async function akt(e,t="ai-conversation-message"){const r=`${Rv}/v1/payments/me/service/${t}/consumptionsession`,n={Authorization:`Token ${e}`};try{const a=await Hr.put(r,{},{headers:n});return a.status>=200&&a.status<300?(a.data,`${t}`,!0):(console.error(`Request failed with status code: ${a.status}`),console.error(`Response: ${a.data}`),!1)}catch(a){return console.error(`Error consuming ${t}:`,yr(a)),!1}}function ikt(e){if(!Array.isArray(e))return[];const t=[];for(const r of e){const n=typeof r=="number"?r:Number(r);Number.isFinite(n)&&t.push(n)}return t}async function Yce(e,t){var a;const r={id:null,assetIds:[]};if(!(e!=null&&e.trim())||!t)return r;const n=`${di}/v1/learnpack/package/${encodeURIComponent(t)}/assets/`;try{const o=(await Hr.get(n,{headers:{Authorization:`Token ${e.trim()}`}})).data;return{id:(o==null?void 0:o.id)!=null?String(o.id):null,assetIds:ikt(o==null?void 0:o.asset_ids)}}catch(i){return Hr.isAxiosError(i)&&((a=i.response)==null?void 0:a.status)===404||console.warn("fetchLearnpackPackageInfo failed:",yr(i)),r}}const okt=async(e,t)=>{var a;const r=`${di}/v1/learnpack/package/${t}/`,n={Authorization:`Token ${e}`};try{const i=await Hr.get(r,{headers:n});return{isAuthor:i.status===200,status:i.status}}catch(i){const o=((a=i==null?void 0:i.response)==null?void 0:a.status)||500;return console.error("Error fetching package:",yr(i)),{isAuthor:!1,status:o}}},skt=async(e,t,r=di)=>{const n=await Hr.get(`${r}/v1/prompting/completion/${t}/`,{headers:{Authorization:`Token ${e}`,"Content-Type":"application/json"}});return n.data,n.data};function lkt(e,t){if(t.length===0)return e;const r=e.includes("?")?"&":"?";return`${e}${r}asset_id=${t.join(",")}`}const ckt=async function(e,t,r,n){if(!e||!r){console.error("URL and token are required");return}r=r.trim();const a={"Content-Type":"application/json",Authorization:`Token ${r}`},i=lkt(e,n??[]);try{return(await Hr.post(i,t,{headers:a})).data}catch(o){throw console.error("Error while sending batch telemetry",yr(o)),o}},ukt=async function(e,t){const r=`${di}/v1/learnpack/telemetry`,n={"Content-Type":"application/json",Authorization:`Token ${t}`};try{return(await Hr.post(r,e,{headers:n})).data}catch(a){throw console.error("Error while sending batch telemetry",yr(a)),a}},dkt=5e3,hkt=6e4,fkt=5*60*1e3;async function XEe(e){var u;const{userId:t,packageSlug:r,rigoToken:n}=e;if(!t||!n||!r)return null;const a=n.trim(),i=new URLSearchParams;i.set("user_ids",String(t)),i.set("include_buffer","true"),i.set("include_steps","true"),i.set("package_slug",r);const o=`${di}/v1/learnpack/telemetry?${i.toString()}`,s=new AbortController,l=setTimeout(()=>s.abort(),dkt);try{const d=await fetch(o,{method:"GET",headers:{Authorization:`Token ${a}`,Accept:"application/json"},signal:s.signal});if(!d.ok)return console.warn("fetchTelemetryFromServer: non-OK response",d.status),null;const h=await d.json(),f=(u=h==null?void 0:h.results)==null?void 0:u[0];return!f||typeof f!="object"?null:f}catch(d){return d.name==="AbortError"?console.warn("fetchTelemetryFromServer: timeout or aborted"):console.warn("fetchTelemetryFromServer: failed",yr(d)),null}finally{clearTimeout(l)}}async function pkt(e){var u;const{userId:t,packageSlug:r,rigoToken:n}=e;if(!t||!n||!r)return{status:"empty"};const a=n.trim(),i=new URLSearchParams;i.set("user_ids",String(t)),i.set("include_buffer","true"),i.set("include_steps","true"),i.set("package_slug",r);const o=`${di}/v1/learnpack/telemetry?${i.toString()}`,s=new AbortController,l=setTimeout(()=>s.abort(),hkt);try{const d=await fetch(o,{method:"GET",headers:{Authorization:`Token ${a}`,Accept:"application/json"},signal:s.signal});if(!d.ok)return console.warn("fetchTelemetryWithStatus: non-OK response",d.status),{status:"server_error",code:d.status};const h=await d.json(),f=(u=h==null?void 0:h.results)==null?void 0:u[0];return!f||typeof f!="object"?{status:"empty"}:{status:"ok",data:f}}catch(d){return d.name==="AbortError"?(console.warn("fetchTelemetryWithStatus: timeout"),{status:"timeout"}):(console.warn("fetchTelemetryWithStatus: network error",yr(d)),{status:"timeout"})}finally{clearTimeout(l)}}function ZEe(e){return!e||typeof e.last_interaction_at!="number"?0:e.last_interaction_at}function d9(){return Math.random().toString(36).slice(2,10)+Math.random().toString(36).slice(2,10)}function JEe(e,t){const r=new Map;for(const n of t)n.slug&&r.set(n.slug,n);return e.map(n=>{var o,s,l,u,d,h,f,p,g,A;const a=r.get(n.slug);if(!a)return n;const i={...n};if(!i.completed_at&&a.completed_at&&(i.completed_at=a.completed_at),!i.is_completed&&a.is_completed&&(i.is_completed=!0),!i.opened_at&&a.opened_at&&(i.opened_at=a.opened_at),(o=a.testeable_elements)!=null&&o.length){const m=new Map((i.testeable_elements??[]).map(v=>[v.hash,v])),y=[...i.testeable_elements??[]];for(const v of a.testeable_elements){const b=m.get(v.hash);if(!b)y.push(v);else if(v.is_completed&&!b.is_completed){const _=y.findIndex(x=>x.hash===v.hash);_!==-1&&(y[_]={...b,is_completed:!0,metrics:b.metrics??v.metrics})}}i.testeable_elements=y}if((s=a.quiz_submissions)!=null&&s.length){const m=new Set((i.quiz_submissions??[]).map(v=>v.quiz_hash)),y=a.quiz_submissions.filter(v=>!m.has(v.quiz_hash));y.length>0&&(i.quiz_submissions=[...i.quiz_submissions??[],...y])}return!((l=i.tests)!=null&&l.length)&&((u=a.tests)!=null&&u.length)&&(i.tests=[...a.tests]),!((d=i.compilations)!=null&&d.length)&&((h=a.compilations)!=null&&h.length)&&(i.compilations=[...a.compilations]),!((f=i.ai_interactions)!=null&&f.length)&&((p=a.ai_interactions)!=null&&p.length)&&(i.ai_interactions=[...a.ai_interactions]),!((g=i.sessions)!=null&&g.length)&&((A=a.sessions)!=null&&A.length)&&(i.sessions=[...a.sessions]),i})}function gkt(e,t,r,n,a,i){const o=!!e,s=t&&t.slug===n;if(o&&!s)return{telemetry:n0({...e},r,n,a,i),source:"server"};if(!o&&s)return{telemetry:n0({...t},r,n,a,i),source:"local"};if(o&&s){const u=ZEe(e),d=ZEe(t),[h,f,p]=d>u?[t,e,"local"]:[e,t,"server"],g=n0({...h},r,n,a,i);return g.steps=JEe(g.steps,f.steps??[]),{telemetry:g,source:p}}const l=Date.now();return{telemetry:n0({telemetry_id:d9(),slug:n,version:`${i}`,agent:a,tutorial_started_at:l,last_interaction_at:l,steps:r,workout_session:[{started_at:l}],fullname:"",cohort_id:null,academy_id:null},r,n,a,i),source:"new"}}function Akt(e,t,r){if(!e||!r)return;const n=r.trim();fetch(e,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Token ${n}`},body:JSON.stringify(t),keepalive:!0}).catch(()=>{})}const mkt=async function(e,t,r){!e||!r||fetch(e,{method:"POST",body:JSON.stringify(t)}).then(n=>n).catch(n=>{console.error("Error while sending stream Telemetry",yr(n))})},ykt=async function(){try{return(await Hr.get(`${fa}/telemetry`)).data}catch(e){return console.error("Error while retrieving telemetry from CLI",yr(e)),null}},vkt=async function(e){try{const t=await Hr.post(`${fa}/telemetry`,e);return t.data,t.data}catch(t){return console.error("Error while saving telemetry in CLI",yr(t)),null}};function Kce(e){if(typeof e!="string")throw new TypeError("Input must be a string");return btoa(encodeURIComponent(e))}const bkt=(e,t)=>{let r={...t};return typeof r.starting_at<"u"&&typeof r.started_at>"u"&&(r.started_at=r.starting_at,delete r.starting_at),typeof r.ending_at<"u"&&typeof r.ended_at>"u"&&(r.ended_at=r.ending_at,delete r.ending_at),typeof r.started_at>"u"&&e!=="open_step"&&(r.started_at=Date.now()),typeof r.ended_at>"u"&&e!=="open_step"&&(r.ended_at=Date.now()),r.source_code&&(r.source_code=Kce(r.source_code)),r.stdout&&(r.stdout=Kce(r.stdout)),r.stderr&&(r.stderr=Kce(r.stderr)),Object.prototype.hasOwnProperty.call(r,"exitCode")&&(r.exit_code=r.exitCode,r.exitCode=void 0),r},wkt=["telemetry_id","user_id","fullname","slug","package_id","version","cohort_id","academy_id","agent","tutorial_started_at","last_interaction_at","steps","workout_session","global_metrics","global_indicators"];function eSe(e){return e==null||typeof e=="string"&&e.trim()===""?null:typeof e=="number"&&!Number.isNaN(e)?String(e):typeof e=="string"?e:null}function kkt(e,t){if(!Array.isArray(e))return[{started_at:t}];if(e.length===0)return[{started_at:t}];const r=e.filter(n=>!!n&&typeof n=="object"&&typeof n.started_at=="number");return r.length===0?[{started_at:t}]:r}function xkt(e,t){const r=new Map;for(const n of t)n.slug&&r.set(n.slug,n);return e.map(n=>{const a=r.get(n.slug);return a?{slug:n.slug,position:n.position,files:n.files,is_testeable:n.is_testeable,compilations:a.compilations??[],tests:a.tests??[],ai_interactions:a.ai_interactions??[],quiz_submissions:a.quiz_submissions??[],testeable_elements:a.testeable_elements??[],is_completed:a.is_completed??!1,completed_at:a.completed_at,opened_at:a.opened_at,sessions:a.sessions??[]}:n})}function n0(e,t,r,n,a){const i=e,o={};for(const g of wkt)Object.prototype.hasOwnProperty.call(i,g)&&(o[g]=i[g]);const s=Date.now(),l=r,u=typeof o.version=="string"&&o.version.trim()!==""?o.version:`${a}`,d=typeof o.tutorial_started_at=="number"?o.tutorial_started_at:s,h=typeof o.last_interaction_at=="number"?o.last_interaction_at:s,f=Array.isArray(o.steps)&&o.steps.length>0?xkt(t,o.steps):t,p=kkt(o.workout_session,s);return{telemetry_id:o.telemetry_id,user_id:o.user_id,fullname:o.fullname,slug:l,package_id:o.package_id,version:u,cohort_id:eSe(o.cohort_id),academy_id:eSe(o.academy_id),agent:typeof o.agent=="string"?o.agent:n,tutorial_started_at:d,last_interaction_at:h,steps:f,workout_session:p,global_metrics:o.global_metrics,global_indicators:o.global_indicators}}function tSe(e,t){e.telemetry_id||(e.telemetry_id=d9());const r=qv(e),n=e.steps.map((a,i)=>{const o=e.telemetry_id||d9();return{...a,telemetry_id:o,metrics:r.steps[i].metrics,indicators:r.steps[i].indicators}});return{...e,steps:n,global_metrics:r.global.metrics,global_indicators:r.global.indicators,user_id:t}}const cr={current:null,urls:{},telemetryKey:"",packageAssetIds:[],listeners:{},agent:"cloud",prevStep:void 0,prevStepStartedAt:void 0,activeHashes:new Map,currentReadyAt:void 0,prunedCompletions:new Set,nonTesteableSlugs:new Set,_lessonRenderedDebounce:null,user:{token:"",rigo_token:"",id:"",fullname:""},tutorialSlug:"",started:!1,reconciling:!1,version:`${KEe.version}`||"CLOUD:0.0.0",salute:e=>{},start:function(e,t,r,n,a,i){return this.packageAssetIds=[],this.telemetryKey=n,this.tutorialSlug=r,this.agent=e,this.user.id=a.user_id,this.user.token=a.token,this.user.rigo_token=a.rigo_token,this.user.fullname=a.fullname,this.current?Promise.resolve():(this.activeHashes=new Map,this.prunedCompletions=new Set,this.currentReadyAt=void 0,e==="cloud"?(this.reconciling=!0,(async()=>{var s;let o=!1;try{const l=jr.get(this.telemetryKey),u=String(l==null?void 0:l.user_id)===String(a==null?void 0:a.user_id),d=l&&l.slug===r&&u?l:null,h=!!((s=a.rigo_token)!=null&&s.trim()&&r),f=i!==void 0,[p,g]=await Promise.all([f?Promise.resolve(i):a.rigo_token&&a.user_id?XEe({userId:a.user_id,packageSlug:r,rigoToken:a.rigo_token}):Promise.resolve(null),h?Yce(a.rigo_token,r):Promise.resolve({id:null,assetIds:[]})]);this.packageAssetIds=g.assetIds;const{telemetry:A,source:m}=gkt(p,d,t,r,e,this.version);if(this.current=A,this.currentReadyAt=Date.now(),this.current.user_id=a.user_id,this.current.fullname=a.fullname,this.current.cohort_id=a.cohort_id||null,this.current.academy_id=a.academy_id||null,this.current.version||(this.current.version=`CLOUD:${this.version}`),g.id!=null&&this.mergePackageIdIfMissing(g.id),this.finishWorkoutSession(),this.save(),this.started=!0,!a.user_id){console.warn("No user ID found, impossible to submit telemetry at start");return}o=m==="local"}catch(l){throw console.error("There was a problem starting the Telemetry",yr(l)),new Error(`There was a problem starting, reload LearnPack
757
815
  Run
758
816
  $ learnpack start`)}finally{this.reconciling=!1}o&&a.user_id&&await this.submit()})()):this.retrieve().then(async o=>{var s;if(o?(this.current=n0(o,t,r,e,this.version),this.finishWorkoutSession()):this.current=n0({telemetry_id:d9(),slug:r,version:`${this.version}`,agent:e,tutorial_started_at:Date.now(),last_interaction_at:Date.now(),steps:t,workout_session:[{started_at:Date.now()}],fullname:this.user.fullname,cohort_id:null,academy_id:null},t,r,e,this.version),this.currentReadyAt=Date.now(),this.current.user_id=this.user.id,this.current.fullname=this.user.fullname,this.current.cohort_id=a.cohort_id||null,this.current.academy_id=a.academy_id||null,this.current.version||(this.current.version=`CLOUD:${this.version}`),this.save(),this.started=!0,!this.user.id){console.warn("No user ID found, impossible to submit telemetry at start");return}if((s=this.user.rigo_token)!=null&&s.trim()&&r){const l=await Yce(this.user.rigo_token,r);this.packageAssetIds=l.assetIds,l.id!=null&&this.mergePackageIdIfMissing(l.id)}await this.submit()}).catch(o=>{throw console.error("There was a problem starting the Telemetry",yr(o)),new Error(`There was a problem starting, reload LearnPack
759
817
  Run
package/src/ui/app.tar.gz CHANGED
Binary file
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Rules for `POST /actions/continue-generating`, the request behind the
3
+ * creator's continue and retry buttons. Without them, every extra click starts
4
+ * another paid job for a lesson that is already being generated.
5
+ */
6
+
7
+ /**
8
+ * How long a PENDING job counts as running. Each phase of a lesson takes 1 to
9
+ * 3 minutes, so a job PENDING for longer is most likely stuck, for example
10
+ * after a worker restart. A stuck job holds the whole course, because the next
11
+ * lesson only starts after this one finishes. Replacing a slow job that was
12
+ * still alive only costs one extra job: serve ignores the older job's webhook.
13
+ */
14
+ export const RUNNING_JOB_MAX_AGE_MS = 5 * 60 * 1000
15
+
16
+ /**
17
+ * How long a finished job counts as running. Rigobot saves the result before
18
+ * it sends the webhook, and serve may still be handling it.
19
+ */
20
+ export const WEBHOOK_GRACE_MS = 90 * 1000
21
+
22
+ /**
23
+ * How long a lesson counts as running when its job cannot be checked: it has
24
+ * no job id yet, or Rigobot did not answer.
25
+ */
26
+ export const UNCHECKED_JOB_GRACE_MS = 2 * 60 * 1000
27
+
28
+ export type GenerationMode = "next-three" | "continue-with-all";
29
+
30
+ type LessonState = {
31
+ status?: string;
32
+ translations?: {
33
+ [language: string]: { completionId?: number; startedAt?: number };
34
+ };
35
+ };
36
+
37
+ /** The fields of a Rigobot completion job these rules read. */
38
+ export type JobState = { status?: string; ended_at?: string | null };
39
+
40
+ /**
41
+ * The job to check before starting another one.
42
+ * @param lesson - The lesson as saved in the syllabus.
43
+ * @param language - The course language.
44
+ * @returns The id of the job the lesson waits for, or null if it has none.
45
+ */
46
+ export const currentJobId = (
47
+ lesson: LessonState,
48
+ language: string
49
+ ): number | null => {
50
+ const id = lesson.translations?.[language]?.completionId
51
+ return typeof id === "number" && id > 0 ? id : null
52
+ }
53
+
54
+ /**
55
+ * Whether a lesson is still being generated, so a new job must not start.
56
+ * @param lesson - The lesson as saved in the syllabus.
57
+ * @param language - The course language.
58
+ * @param job - Its job as Rigobot reports it, or null if it was not checked.
59
+ * @param now - The current time in milliseconds.
60
+ * @returns True when the lesson must wait for its current job.
61
+ */
62
+ export const isGenerationRunning = (
63
+ lesson: LessonState,
64
+ language: string,
65
+ job: JobState | null,
66
+ now: number
67
+ ): boolean => {
68
+ if (lesson.status !== "GENERATING") return false
69
+
70
+ const startedAt = lesson.translations?.[language]?.startedAt
71
+ const startedAgo =
72
+ typeof startedAt === "number" && startedAt > 0 ?
73
+ now - startedAt :
74
+ Number.POSITIVE_INFINITY
75
+
76
+ if (!job) return startedAgo < UNCHECKED_JOB_GRACE_MS
77
+ if (job.status === "PENDING") return startedAgo < RUNNING_JOB_MAX_AGE_MS
78
+
79
+ // A finished job. Its webhook may still be on its way to serve.
80
+ const endedAt = job.ended_at ? Date.parse(job.ended_at) : Number.NaN
81
+ if (Number.isNaN(endedAt)) return startedAgo < UNCHECKED_JOB_GRACE_MS
82
+
83
+ return now - endedAt < WEBHOOK_GRACE_MS
84
+ }
85
+
86
+ /**
87
+ * The generation mode to save. The user only picks a mode on a PENDING
88
+ * lesson. The retry buttons offer no choice, so a retry keeps the mode of the
89
+ * course instead of resetting it.
90
+ * @param lessonStatus - The status of the lesson before this request.
91
+ * @param current - The mode saved in the syllabus.
92
+ * @param requested - The mode in the request.
93
+ * @returns The mode to save, or undefined when the course has none.
94
+ */
95
+ export const resolveGenerationMode = (
96
+ lessonStatus: string | undefined,
97
+ current: unknown,
98
+ requested: unknown
99
+ ): GenerationMode | undefined => {
100
+ const isMode = (mode: unknown): mode is GenerationMode =>
101
+ mode === "next-three" || mode === "continue-with-all"
102
+
103
+ if (isMode(requested) && (lessonStatus === "PENDING" || !isMode(current))) {
104
+ return requested
105
+ }
106
+
107
+ return isMode(current) ? current : undefined
108
+ }
@@ -152,6 +152,7 @@ export type TCompletionJob = {
152
152
  status: "PENDING" | "SUCCESS" | "ERROR" | string;
153
153
  status_text?: string | null;
154
154
  parsed?: Record<string, any> | null;
155
+ ended_at?: string | null;
155
156
  };
156
157
 
157
158
  type TTranslateAndWaitOptions = {
@@ -192,7 +193,8 @@ const assertJobSucceeded = (job: TCompletionJob): TCompletionJob => {
192
193
 
193
194
  export const getCompletionJob = async (
194
195
  token: string,
195
- completionId: number | string
196
+ completionId: number | string,
197
+ options: { timeoutMs?: number } = {}
196
198
  ): Promise<TCompletionJob> => {
197
199
  const response = await axios.get(
198
200
  `${RIGOBOT_HOST}/v1/prompting/completion/${completionId}/`,
@@ -201,6 +203,8 @@ export const getCompletionJob = async (
201
203
  "Content-Type": "application/json",
202
204
  Authorization: "Token " + token,
203
205
  },
206
+ // No timeout by default: the translation polling waits as long as it needs.
207
+ timeout: options.timeoutMs,
204
208
  }
205
209
  )
206
210