@epic-web/workshop-utils 6.84.1 → 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.
- package/dist/epic-api.server.d.ts +35 -3
- package/dist/epic-api.server.js +71 -3
- package/package.json +1 -1
|
@@ -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: "
|
|
106
|
-
|
|
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;
|
package/dist/epic-api.server.js
CHANGED
|
@@ -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 =
|
|
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
|
|
462
|
-
|
|
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
|
}
|