@learnpack/learnpack 5.0.358 → 5.0.360
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/lib/commands/publish.d.ts +0 -10
- package/lib/commands/publish.js +52 -168
- package/lib/commands/serve.js +52 -116
- package/lib/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
- package/lib/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
- package/lib/creatorDist/index.html +2 -2
- package/lib/utils/api.d.ts +62 -15
- package/lib/utils/api.js +124 -45
- package/lib/utils/assetResolver.d.ts +116 -0
- package/lib/utils/assetResolver.js +268 -0
- package/lib/utils/assetSync.d.ts +48 -0
- package/lib/utils/assetSync.js +267 -0
- package/lib/utils/creatorUtilities.d.ts +1 -0
- package/lib/utils/creatorUtilities.js +17 -1
- package/package.json +1 -1
- package/src/commands/publish.ts +61 -255
- package/src/commands/serve.ts +65 -140
- package/src/creator/src/App.test.tsx +338 -0
- package/src/creator/src/App.tsx +152 -26
- package/src/creator/src/components/CourseLanguagePicker.tsx +51 -0
- package/src/creator/src/components/LanguageDetectionModal.tsx +29 -9
- package/src/creator/src/components/StepWizard.tsx +11 -3
- package/src/creator/src/components/Uploader.test.tsx +65 -0
- package/src/creator/src/components/Uploader.tsx +9 -3
- package/src/creator/src/locales/en.json +7 -0
- package/src/creator/src/locales/es.json +7 -0
- package/src/creator/src/utils/courseLanguage.test.ts +215 -0
- package/src/creator/src/utils/courseLanguage.ts +173 -0
- package/src/creator/src/utils/creationRequest.test.ts +104 -0
- package/src/creator/src/utils/creationRequest.ts +63 -0
- package/src/creator/src/utils/store.ts +7 -0
- package/src/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
- package/src/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
- package/src/creatorDist/index.html +2 -2
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +2105 -2105
- package/src/ui/_app/sw.js +13 -0
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +157 -42
- package/src/utils/assetResolver.ts +368 -0
- package/src/utils/assetSync.ts +343 -0
- package/src/utils/awsCredentials.ts +57 -57
- package/src/utils/creatorUtilities.ts +16 -0
- package/src/utils/descriptions/publishStage.ts +394 -394
- package/src/utils/descriptions/resumePublication.ts +217 -217
- package/src/utils/descriptions/s3Storage.ts +214 -214
- package/src/utils/s3/packageManifestBackfill.ts +771 -771
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type { ParsedFile } from "../components/FileUploader";
|
|
3
|
+
import { buildCourseInfo, toCourseFormState } from "./creationRequest";
|
|
4
|
+
import useStore, { FormState } from "./store";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The courseInfo the wizard sent before the course language could be settled,
|
|
8
|
+
* copied byte for byte from App.tsx (upstream 68c0bf9). Whitespace inside the
|
|
9
|
+
* template is part of the prompt, so it must not be reformatted.
|
|
10
|
+
*/
|
|
11
|
+
const legacyCourseInfo = (
|
|
12
|
+
formState: FormState,
|
|
13
|
+
techs: { slug: string }[],
|
|
14
|
+
uploadedFiles: ParsedFile[]
|
|
15
|
+
) =>
|
|
16
|
+
// prettier-ignore
|
|
17
|
+
`${JSON.stringify(
|
|
18
|
+
formState
|
|
19
|
+
)}. The following technologies are available, choose up to 3 from the following list: <techs>${techs
|
|
20
|
+
.map((t) => t.slug)
|
|
21
|
+
.join(", ")}</techs>
|
|
22
|
+
|
|
23
|
+
The following files have been uploaded by the user: ${uploadedFiles.map((f) => `<FILE name="${f.name}">${f.text}</FILE>`).join("\n")}
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const defaultFormState = () => useStore.getInitialState().formState;
|
|
27
|
+
|
|
28
|
+
const techs = [{ slug: "python" }, { slug: "sql" }];
|
|
29
|
+
const uploadedFiles: ParsedFile[] = [
|
|
30
|
+
{
|
|
31
|
+
name: "notes.pdf",
|
|
32
|
+
text: "Relational databases",
|
|
33
|
+
status: "SUCCESS",
|
|
34
|
+
notificationId: "n1",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
describe("buildCourseInfo", () => {
|
|
39
|
+
it("sends exactly the previous prompt when no language is settled", () => {
|
|
40
|
+
const formState = {
|
|
41
|
+
...defaultFormState(),
|
|
42
|
+
description: "Learn SQL joins",
|
|
43
|
+
purpose: "learnpack-lesson-writer",
|
|
44
|
+
duration: 30,
|
|
45
|
+
hasContentIndex: true,
|
|
46
|
+
};
|
|
47
|
+
// The wizard state before this change had no languageChosen key.
|
|
48
|
+
const previousFormState = { ...formState };
|
|
49
|
+
delete previousFormState.languageChosen;
|
|
50
|
+
|
|
51
|
+
expect(
|
|
52
|
+
buildCourseInfo({ formState, techs, uploadedFiles, language: null })
|
|
53
|
+
).toBe(legacyCourseInfo(previousFormState, techs, uploadedFiles));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("asks for the settled language in plain words", () => {
|
|
57
|
+
const formState = { ...defaultFormState(), language: "en" };
|
|
58
|
+
const courseInfo = buildCourseInfo({
|
|
59
|
+
formState,
|
|
60
|
+
techs,
|
|
61
|
+
uploadedFiles,
|
|
62
|
+
language: "es",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(courseInfo).toContain('"language":"es"');
|
|
66
|
+
expect(courseInfo).not.toContain('"language":"en"');
|
|
67
|
+
expect(
|
|
68
|
+
courseInfo.endsWith(
|
|
69
|
+
"The user chose Spanish as the course language. This replaces the rule to write in the language of the user messages: write every field in Spanish, including the title, the description, and each step's title and description in listOfSteps, even if the user's messages or the uploaded files are in another language. Set languageCode to \"es\"."
|
|
70
|
+
)
|
|
71
|
+
).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("never sends languageChosen to the model", () => {
|
|
75
|
+
const formState = {
|
|
76
|
+
...defaultFormState(),
|
|
77
|
+
language: "es",
|
|
78
|
+
languageChosen: true,
|
|
79
|
+
};
|
|
80
|
+
for (const language of [null, "es"] as const) {
|
|
81
|
+
expect(
|
|
82
|
+
buildCourseInfo({ formState, techs, uploadedFiles, language })
|
|
83
|
+
).not.toContain("languageChosen");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("toCourseFormState", () => {
|
|
89
|
+
it("drops languageChosen and keeps every other key in order", () => {
|
|
90
|
+
const formState = { ...defaultFormState(), languageChosen: true };
|
|
91
|
+
const courseFormState = toCourseFormState(formState);
|
|
92
|
+
|
|
93
|
+
expect("languageChosen" in courseFormState).toBe(false);
|
|
94
|
+
expect(Object.keys(courseFormState)).toEqual(
|
|
95
|
+
Object.keys(formState).filter((key) => key !== "languageChosen")
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does not change the form state it was given", () => {
|
|
100
|
+
const formState = { ...defaultFormState(), languageChosen: true };
|
|
101
|
+
toCourseFormState(formState);
|
|
102
|
+
expect(formState.languageChosen).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ParsedFile } from "../components/FileUploader";
|
|
2
|
+
import type { TCourseLanguage } from "./courseLanguage";
|
|
3
|
+
import type { FormState } from "./store";
|
|
4
|
+
|
|
5
|
+
const LANGUAGE_NAMES: Record<TCourseLanguage, string> = {
|
|
6
|
+
en: "English",
|
|
7
|
+
es: "Spanish",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drops the wizard-only fields from the form state before it leaves the
|
|
12
|
+
* wizard, in the prompt or in the syllabus sent to the server.
|
|
13
|
+
* @param formState - The wizard's form state.
|
|
14
|
+
* @returns A copy without `languageChosen`, keys otherwise in the same order.
|
|
15
|
+
*/
|
|
16
|
+
export const toCourseFormState = (formState: FormState): FormState => {
|
|
17
|
+
const courseFormState = { ...formState };
|
|
18
|
+
delete courseFormState.languageChosen;
|
|
19
|
+
return courseFormState;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds the `courseInfo` text sent to the model to generate the syllabus.
|
|
24
|
+
*
|
|
25
|
+
* Without a settled language the text is exactly what the wizard sent before
|
|
26
|
+
* the course language could be settled, so the model keeps choosing.
|
|
27
|
+
* @param language - The course language settled before generation, or null.
|
|
28
|
+
*/
|
|
29
|
+
export const buildCourseInfo = ({
|
|
30
|
+
formState,
|
|
31
|
+
techs,
|
|
32
|
+
uploadedFiles,
|
|
33
|
+
language,
|
|
34
|
+
}: {
|
|
35
|
+
formState: FormState;
|
|
36
|
+
techs: { slug: string }[];
|
|
37
|
+
uploadedFiles: ParsedFile[];
|
|
38
|
+
language: TCourseLanguage | null;
|
|
39
|
+
}): string => {
|
|
40
|
+
const courseFormState = toCourseFormState(formState);
|
|
41
|
+
|
|
42
|
+
// Kept byte for byte from the original request, including the spaces on the
|
|
43
|
+
// blank line, which .editorconfig would trim. creationRequest.test.ts checks
|
|
44
|
+
// it against a copy of the original.
|
|
45
|
+
const courseInfo = `${JSON.stringify(
|
|
46
|
+
language ? { ...courseFormState, language } : courseFormState
|
|
47
|
+
)}. The following technologies are available, choose up to 3 from the following list: <techs>${techs
|
|
48
|
+
.map((t) => t.slug)
|
|
49
|
+
.join(", ")}</techs>
|
|
50
|
+
|
|
51
|
+
The following files have been uploaded by the user: ${uploadedFiles
|
|
52
|
+
.map((f) => `<FILE name="${f.name}">${f.text}</FILE>`)
|
|
53
|
+
.join("\n")}
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
if (!language) return courseInfo;
|
|
57
|
+
|
|
58
|
+
const name = LANGUAGE_NAMES[language];
|
|
59
|
+
// Prompt 390 keys on the first sentence to override its rule of writing in
|
|
60
|
+
// the language of the user messages, so keep its wording in sync.
|
|
61
|
+
return `${courseInfo}
|
|
62
|
+
The user chose ${name} as the course language. This replaces the rule to write in the language of the user messages: write every field in ${name}, including the title, the description, and each step's title and description in listOfSteps, even if the user's messages or the uploaded files are in another language. Set languageCode to "${language}".`;
|
|
63
|
+
};
|
|
@@ -15,6 +15,10 @@ export type FormState = {
|
|
|
15
15
|
difficulty: TDifficulty;
|
|
16
16
|
slug: string;
|
|
17
17
|
language?: string;
|
|
18
|
+
// True once the user picked the course language themselves (the language
|
|
19
|
+
// picker, the `language` URL param or a language modal). Never sent to the
|
|
20
|
+
// model or the server.
|
|
21
|
+
languageChosen?: boolean;
|
|
18
22
|
isCompleted: boolean;
|
|
19
23
|
variables: string[];
|
|
20
24
|
currentStep: string;
|
|
@@ -93,6 +97,7 @@ const useStore = create<Store>()(
|
|
|
93
97
|
contentIndex: "",
|
|
94
98
|
purpose: "",
|
|
95
99
|
language: "en",
|
|
100
|
+
languageChosen: false,
|
|
96
101
|
technologies: [],
|
|
97
102
|
difficulty: "beginner",
|
|
98
103
|
// sources: [],
|
|
@@ -129,6 +134,7 @@ const useStore = create<Store>()(
|
|
|
129
134
|
description: "",
|
|
130
135
|
duration: 0,
|
|
131
136
|
language: "en",
|
|
137
|
+
languageChosen: false,
|
|
132
138
|
difficulty: "beginner",
|
|
133
139
|
technologies: [],
|
|
134
140
|
hasContentIndex: false,
|
|
@@ -179,6 +185,7 @@ const useStore = create<Store>()(
|
|
|
179
185
|
slug: "",
|
|
180
186
|
difficulty: "beginner",
|
|
181
187
|
language: "en",
|
|
188
|
+
languageChosen: false,
|
|
182
189
|
technologies: [],
|
|
183
190
|
hasContentIndex: false,
|
|
184
191
|
contentIndex: "",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
--color-blue-50: oklch(97% 0.014 254.604);
|
|
66
66
|
--color-blue-100: oklch(93.2% 0.032 255.585);
|
|
67
67
|
--color-blue-200: oklch(88.2% 0.059 254.128);
|
|
68
|
+
--color-blue-300: oklch(80.9% 0.105 251.813);
|
|
68
69
|
--color-blue-400: oklch(70.7% 0.165 254.624);
|
|
69
70
|
--color-blue-500: oklch(62.3% 0.214 259.815);
|
|
70
71
|
--color-blue-600: oklch(54.6% 0.245 262.881);
|
|
@@ -439,6 +440,12 @@
|
|
|
439
440
|
.z-1000 {
|
|
440
441
|
z-index: 1000;
|
|
441
442
|
}
|
|
443
|
+
.col-span-2 {
|
|
444
|
+
grid-column: span 2 / span 2;
|
|
445
|
+
}
|
|
446
|
+
.row-start-2 {
|
|
447
|
+
grid-row-start: 2;
|
|
448
|
+
}
|
|
442
449
|
.container {
|
|
443
450
|
width: 100%;
|
|
444
451
|
}
|
|
@@ -536,6 +543,9 @@
|
|
|
536
543
|
.flex {
|
|
537
544
|
display: flex;
|
|
538
545
|
}
|
|
546
|
+
.grid {
|
|
547
|
+
display: grid;
|
|
548
|
+
}
|
|
539
549
|
.hidden {
|
|
540
550
|
display: none;
|
|
541
551
|
}
|
|
@@ -548,6 +558,9 @@
|
|
|
548
558
|
.table {
|
|
549
559
|
display: table;
|
|
550
560
|
}
|
|
561
|
+
.field-sizing-content {
|
|
562
|
+
field-sizing: content;
|
|
563
|
+
}
|
|
551
564
|
.h-2 {
|
|
552
565
|
height: calc(var(--spacing) * 2);
|
|
553
566
|
}
|
|
@@ -644,6 +657,9 @@
|
|
|
644
657
|
.w-full {
|
|
645
658
|
width: 100%;
|
|
646
659
|
}
|
|
660
|
+
.max-w-64 {
|
|
661
|
+
max-width: calc(var(--spacing) * 64);
|
|
662
|
+
}
|
|
647
663
|
.max-w-\[700px\] {
|
|
648
664
|
max-width: 700px;
|
|
649
665
|
}
|
|
@@ -705,6 +721,9 @@
|
|
|
705
721
|
.list-disc {
|
|
706
722
|
list-style-type: disc;
|
|
707
723
|
}
|
|
724
|
+
.grid-cols-2 {
|
|
725
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
726
|
+
}
|
|
708
727
|
.flex-col {
|
|
709
728
|
flex-direction: column;
|
|
710
729
|
}
|
|
@@ -780,6 +799,15 @@
|
|
|
780
799
|
calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse))
|
|
781
800
|
);
|
|
782
801
|
}
|
|
802
|
+
.justify-self-center {
|
|
803
|
+
justify-self: center;
|
|
804
|
+
}
|
|
805
|
+
.justify-self-end {
|
|
806
|
+
justify-self: flex-end;
|
|
807
|
+
}
|
|
808
|
+
.justify-self-start {
|
|
809
|
+
justify-self: flex-start;
|
|
810
|
+
}
|
|
783
811
|
.truncate {
|
|
784
812
|
text-overflow: ellipsis;
|
|
785
813
|
white-space: nowrap;
|
|
@@ -905,6 +933,9 @@
|
|
|
905
933
|
.bg-red-100 {
|
|
906
934
|
background-color: var(--color-red-100);
|
|
907
935
|
}
|
|
936
|
+
.bg-transparent {
|
|
937
|
+
background-color: #0000;
|
|
938
|
+
}
|
|
908
939
|
.bg-white {
|
|
909
940
|
background-color: var(--color-white);
|
|
910
941
|
}
|
|
@@ -1046,6 +1077,9 @@
|
|
|
1046
1077
|
.break-words {
|
|
1047
1078
|
overflow-wrap: break-word;
|
|
1048
1079
|
}
|
|
1080
|
+
.whitespace-nowrap {
|
|
1081
|
+
white-space: nowrap;
|
|
1082
|
+
}
|
|
1049
1083
|
.whitespace-pre-line {
|
|
1050
1084
|
white-space: pre-line;
|
|
1051
1085
|
}
|
|
@@ -1244,6 +1278,19 @@
|
|
|
1244
1278
|
.focus\:ring-blue-200:focus {
|
|
1245
1279
|
--tw-ring-color: var(--color-blue-200);
|
|
1246
1280
|
}
|
|
1281
|
+
.focus\:outline-none:focus {
|
|
1282
|
+
--tw-outline-style: none;
|
|
1283
|
+
outline-style: none;
|
|
1284
|
+
}
|
|
1285
|
+
.focus-visible\:ring-2:focus-visible {
|
|
1286
|
+
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0
|
|
1287
|
+
calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
|
1288
|
+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow),
|
|
1289
|
+
var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1290
|
+
}
|
|
1291
|
+
.focus-visible\:ring-blue-300:focus-visible {
|
|
1292
|
+
--tw-ring-color: var(--color-blue-300);
|
|
1293
|
+
}
|
|
1247
1294
|
.disabled\:cursor-not-allowed:disabled {
|
|
1248
1295
|
cursor: not-allowed;
|
|
1249
1296
|
}
|
|
@@ -1257,9 +1304,21 @@
|
|
|
1257
1304
|
opacity: 0.5;
|
|
1258
1305
|
}
|
|
1259
1306
|
@media (min-width: 40rem) {
|
|
1307
|
+
.sm\:col-span-1 {
|
|
1308
|
+
grid-column: span 1 / span 1;
|
|
1309
|
+
}
|
|
1310
|
+
.sm\:col-start-2 {
|
|
1311
|
+
grid-column-start: 2;
|
|
1312
|
+
}
|
|
1313
|
+
.sm\:row-start-1 {
|
|
1314
|
+
grid-row-start: 1;
|
|
1315
|
+
}
|
|
1260
1316
|
.sm\:w-auto {
|
|
1261
1317
|
width: auto;
|
|
1262
1318
|
}
|
|
1319
|
+
.sm\:grid-cols-\[1fr_auto_1fr\] {
|
|
1320
|
+
grid-template-columns: 1fr auto 1fr;
|
|
1321
|
+
}
|
|
1263
1322
|
.sm\:flex-row {
|
|
1264
1323
|
flex-direction: row;
|
|
1265
1324
|
}
|