@epic-web/workshop-utils 6.84.0 → 6.84.2

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,62 @@ 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
+ if (isProblemOrSolutionSubpage(last)) {
482
+ const slug = segments.at(-2) ?? null;
483
+ return {
484
+ lessonSlug: slug ? stripEpicAiSlugSuffix(slug) : null,
485
+ subpage: last,
486
+ };
487
+ }
488
+ if (last === 'embed') {
489
+ const slug = segments.at(-2) ?? null;
490
+ return {
491
+ lessonSlug: slug ? stripEpicAiSlugSuffix(slug) : null,
492
+ subpage: null,
493
+ };
494
+ }
495
+ return { lessonSlug: stripEpicAiSlugSuffix(last), subpage: null };
496
+ };
497
+ try {
498
+ const url = new URL(urlString);
499
+ const segments = url.pathname.split('/').filter(Boolean);
500
+ return parseSegments(segments);
501
+ }
502
+ catch {
503
+ // Fall back to naive parsing; this is best-effort and only used for
504
+ // mapping Epic lesson slugs -> local pages in admin UI/progress.
505
+ const withoutHash = urlString.split('#')[0] ?? urlString;
506
+ const withoutQuery = withoutHash.split('?')[0] ?? withoutHash;
507
+ const segments = withoutQuery.split('/').filter(Boolean);
508
+ return parseSegments(segments);
509
+ }
510
+ }
511
+ function embedMatchesLessonSlug(embedUrl, epicLessonSlug) {
512
+ const parsed = parseEpicLessonSlugFromEmbedUrl(embedUrl);
513
+ if (!parsed.lessonSlug)
514
+ return false;
515
+ if (parsed.lessonSlug === epicLessonSlug)
516
+ return true;
517
+ const normalizedLessonSlug = stripEpicAiSlugSuffix(epicLessonSlug);
518
+ return parsed.lessonSlug === normalizedLessonSlug;
519
+ }
520
+ export function resolveLocalProgressForEpicLesson(epicLessonSlug, { workshopInstructions, workshopFinished, exercises, }) {
521
+ const hasEmbed = (embeds) => embeds?.some((embedUrl) => embedMatchesLessonSlug(embedUrl, epicLessonSlug));
463
522
  if (workshopInstructions.compiled.status === 'success' &&
464
523
  hasEmbed(workshopInstructions.compiled.epicVideoEmbeds)) {
465
524
  return { type: 'workshop-instructions' };
@@ -487,6 +546,15 @@ function getProgressForLesson(epicLessonSlug, { workshopInstructions, workshopFi
487
546
  type: 'step',
488
547
  exerciseNumber: exercise.exerciseNumber,
489
548
  stepNumber: step.stepNumber,
549
+ stepType: 'problem',
550
+ };
551
+ }
552
+ if (hasEmbed(step.solution?.epicVideoEmbeds)) {
553
+ return {
554
+ type: 'step',
555
+ exerciseNumber: exercise.exerciseNumber,
556
+ stepNumber: step.stepNumber,
557
+ stepType: 'solution',
490
558
  };
491
559
  }
492
560
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-utils",
3
- "version": "6.84.0",
3
+ "version": "6.84.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },