@epic-web/workshop-utils 6.84.1 → 6.84.3

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.
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { getExercises, getWorkshopFinished, getWorkshopInstructions } from "./apps.server.js";
2
3
  import { type Timings } from "./timing.server.js";
3
4
  declare const EpicVideoMetadataSchema: z.ZodObject<{
4
5
  playbackId: z.ZodString;
@@ -81,29 +82,60 @@ export declare function getProgress({ timings, request, }?: {
81
82
  epicLessonUrl: string;
82
83
  epicLessonSlug: string;
83
84
  epicCompletedAt: string | null;
84
- } & ({
85
+ } & (LocalProgressForEpicLesson | {
86
+ type: "unknown";
87
+ }))[]>;
88
+ export type LocalProgressForEpicLesson = {
89
+ type: 'workshop-instructions';
90
+ } | {
91
+ type: 'workshop-finished';
92
+ } | {
93
+ type: 'instructions';
94
+ exerciseNumber: number;
95
+ } | {
96
+ type: 'finished';
97
+ exerciseNumber: number;
98
+ } | {
99
+ type: 'step';
100
+ exerciseNumber: number;
101
+ stepNumber: number;
102
+ stepType: 'problem' | 'solution';
103
+ };
104
+ export declare function resolveLocalProgressForEpicLesson(epicLessonSlug: string, { workshopInstructions, workshopFinished, exercises, }: {
105
+ workshopInstructions: Awaited<ReturnType<typeof getWorkshopInstructions>>;
106
+ workshopFinished: Awaited<ReturnType<typeof getWorkshopFinished>>;
107
+ exercises: Awaited<ReturnType<typeof getExercises>>;
108
+ }): {
85
109
  readonly type: "workshop-instructions";
86
110
  readonly exerciseNumber?: undefined;
87
111
  readonly stepNumber?: undefined;
112
+ readonly stepType?: undefined;
88
113
  } | {
89
114
  readonly type: "workshop-finished";
90
115
  readonly exerciseNumber?: undefined;
91
116
  readonly stepNumber?: undefined;
117
+ readonly stepType?: undefined;
92
118
  } | {
93
119
  readonly type: "instructions";
94
120
  readonly exerciseNumber: number;
95
121
  readonly stepNumber?: undefined;
122
+ readonly stepType?: undefined;
96
123
  } | {
97
124
  readonly type: "finished";
98
125
  readonly exerciseNumber: number;
99
126
  readonly stepNumber?: undefined;
127
+ readonly stepType?: undefined;
100
128
  } | {
101
129
  readonly type: "step";
102
130
  readonly exerciseNumber: number;
103
131
  readonly stepNumber: number;
132
+ readonly stepType: "problem";
104
133
  } | {
105
- type: "unknown";
106
- } | undefined))[]>;
134
+ readonly type: "step";
135
+ readonly exerciseNumber: number;
136
+ readonly stepNumber: number;
137
+ readonly stepType: "solution";
138
+ } | undefined;
107
139
  export declare function updateProgress({ lessonSlug, complete }: {
108
140
  lessonSlug: string;
109
141
  complete?: boolean;
@@ -216,6 +216,11 @@ async function getEpicVideoInfo({ epicVideoEmbed, accessToken, request, timings,
216
216
  if (epicUrl.pathname.startsWith('/tutorials/')) {
217
217
  epicUrl.pathname = epicUrl.pathname.replace(/^\/tutorials\//, '/workshops/');
218
218
  }
219
+ const epicPathSegments = epicUrl.pathname.split('/').filter(Boolean);
220
+ if (epicPathSegments.at(-1) === 'embed') {
221
+ epicPathSegments.pop();
222
+ epicUrl.pathname = `/${epicPathSegments.join('/')}`;
223
+ }
219
224
  // special case for epicai.pro videos
220
225
  const apiUrl = epicUrl.host === 'www.epicai.pro'
221
226
  ? getEpicAIVideoAPIUrl(epicVideoEmbed)
@@ -431,7 +436,7 @@ export async function getProgress({ timings, request, } = {}) {
431
436
  const epicLessonSlug = lesson.slug;
432
437
  const lessonProgress = epicProgress.find(({ lessonId }) => lessonId === lesson._id);
433
438
  const epicCompletedAt = lessonProgress ? lessonProgress.completedAt : null;
434
- const progressForLesson = getProgressForLesson(epicLessonSlug, {
439
+ const progressForLesson = resolveLocalProgressForEpicLesson(epicLessonSlug, {
435
440
  workshopInstructions,
436
441
  workshopFinished,
437
442
  exercises,
@@ -458,8 +463,60 @@ export async function getProgress({ timings, request, } = {}) {
458
463
  log(`processed ${progress.length} progress entries for workshop: ${slug}`);
459
464
  return progress;
460
465
  }
461
- function getProgressForLesson(epicLessonSlug, { workshopInstructions, workshopFinished, exercises, }) {
462
- const hasEmbed = (embed) => embed?.some((e) => e.split('/').at(-1) === epicLessonSlug);
466
+ function stripEpicAiSlugSuffix(value) {
467
+ // EpicAI embeds sometimes include a `~...` suffix in the slug segment.
468
+ // Keep the comparison tolerant by stripping it.
469
+ return value.replace(/~[^ ]*$/, '');
470
+ }
471
+ function isProblemOrSolutionSubpage(value) {
472
+ return value === 'problem' || value === 'solution';
473
+ }
474
+ function parseEpicLessonSlugFromEmbedUrl(urlString) {
475
+ const parseSegments = (segments) => {
476
+ if (segments.length === 0)
477
+ return { lessonSlug: null, subpage: null };
478
+ const last = segments.at(-1) ?? null;
479
+ if (!last)
480
+ return { lessonSlug: null, subpage: null };
481
+ // `/embed` can be appended to either the lesson page itself or a
482
+ // `/problem`/`/solution` subpage (e.g. `/lesson/solution/embed`). Strip it
483
+ // first so we can apply the subpage logic consistently.
484
+ if (last === 'embed')
485
+ return parseSegments(segments.slice(0, -1));
486
+ if (isProblemOrSolutionSubpage(last)) {
487
+ const slug = segments.at(-2) ?? null;
488
+ return {
489
+ lessonSlug: slug ? stripEpicAiSlugSuffix(slug) : null,
490
+ subpage: last,
491
+ };
492
+ }
493
+ return { lessonSlug: stripEpicAiSlugSuffix(last), subpage: null };
494
+ };
495
+ try {
496
+ const url = new URL(urlString);
497
+ const segments = url.pathname.split('/').filter(Boolean);
498
+ return parseSegments(segments);
499
+ }
500
+ catch {
501
+ // Fall back to naive parsing; this is best-effort and only used for
502
+ // mapping Epic lesson slugs -> local pages in admin UI/progress.
503
+ const withoutHash = urlString.split('#')[0] ?? urlString;
504
+ const withoutQuery = withoutHash.split('?')[0] ?? withoutHash;
505
+ const segments = withoutQuery.split('/').filter(Boolean);
506
+ return parseSegments(segments);
507
+ }
508
+ }
509
+ function embedMatchesLessonSlug(embedUrl, epicLessonSlug) {
510
+ const parsed = parseEpicLessonSlugFromEmbedUrl(embedUrl);
511
+ if (!parsed.lessonSlug)
512
+ return false;
513
+ if (parsed.lessonSlug === epicLessonSlug)
514
+ return true;
515
+ const normalizedLessonSlug = stripEpicAiSlugSuffix(epicLessonSlug);
516
+ return parsed.lessonSlug === normalizedLessonSlug;
517
+ }
518
+ export function resolveLocalProgressForEpicLesson(epicLessonSlug, { workshopInstructions, workshopFinished, exercises, }) {
519
+ const hasEmbed = (embeds) => embeds?.some((embedUrl) => embedMatchesLessonSlug(embedUrl, epicLessonSlug));
463
520
  if (workshopInstructions.compiled.status === 'success' &&
464
521
  hasEmbed(workshopInstructions.compiled.epicVideoEmbeds)) {
465
522
  return { type: 'workshop-instructions' };
@@ -487,6 +544,15 @@ function getProgressForLesson(epicLessonSlug, { workshopInstructions, workshopFi
487
544
  type: 'step',
488
545
  exerciseNumber: exercise.exerciseNumber,
489
546
  stepNumber: step.stepNumber,
547
+ stepType: 'problem',
548
+ };
549
+ }
550
+ if (hasEmbed(step.solution?.epicVideoEmbeds)) {
551
+ return {
552
+ type: 'step',
553
+ exerciseNumber: exercise.exerciseNumber,
554
+ stepNumber: step.stepNumber,
555
+ stepType: 'solution',
490
556
  };
491
557
  }
492
558
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-utils",
3
- "version": "6.84.1",
3
+ "version": "6.84.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },