@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.
Files changed (47) hide show
  1. package/lib/commands/publish.d.ts +0 -10
  2. package/lib/commands/publish.js +52 -168
  3. package/lib/commands/serve.js +52 -116
  4. package/lib/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
  5. package/lib/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
  6. package/lib/creatorDist/index.html +2 -2
  7. package/lib/utils/api.d.ts +62 -15
  8. package/lib/utils/api.js +124 -45
  9. package/lib/utils/assetResolver.d.ts +116 -0
  10. package/lib/utils/assetResolver.js +268 -0
  11. package/lib/utils/assetSync.d.ts +48 -0
  12. package/lib/utils/assetSync.js +267 -0
  13. package/lib/utils/creatorUtilities.d.ts +1 -0
  14. package/lib/utils/creatorUtilities.js +17 -1
  15. package/package.json +1 -1
  16. package/src/commands/publish.ts +61 -255
  17. package/src/commands/serve.ts +65 -140
  18. package/src/creator/src/App.test.tsx +338 -0
  19. package/src/creator/src/App.tsx +152 -26
  20. package/src/creator/src/components/CourseLanguagePicker.tsx +51 -0
  21. package/src/creator/src/components/LanguageDetectionModal.tsx +29 -9
  22. package/src/creator/src/components/StepWizard.tsx +11 -3
  23. package/src/creator/src/components/Uploader.test.tsx +65 -0
  24. package/src/creator/src/components/Uploader.tsx +9 -3
  25. package/src/creator/src/locales/en.json +7 -0
  26. package/src/creator/src/locales/es.json +7 -0
  27. package/src/creator/src/utils/courseLanguage.test.ts +215 -0
  28. package/src/creator/src/utils/courseLanguage.ts +173 -0
  29. package/src/creator/src/utils/creationRequest.test.ts +104 -0
  30. package/src/creator/src/utils/creationRequest.ts +63 -0
  31. package/src/creator/src/utils/store.ts +7 -0
  32. package/src/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
  33. package/src/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
  34. package/src/creatorDist/index.html +2 -2
  35. package/src/ui/_app/app.css +1 -1
  36. package/src/ui/_app/app.js +2105 -2105
  37. package/src/ui/_app/sw.js +13 -0
  38. package/src/ui/app.tar.gz +0 -0
  39. package/src/utils/api.ts +157 -42
  40. package/src/utils/assetResolver.ts +368 -0
  41. package/src/utils/assetSync.ts +343 -0
  42. package/src/utils/awsCredentials.ts +57 -57
  43. package/src/utils/creatorUtilities.ts +16 -0
  44. package/src/utils/descriptions/publishStage.ts +394 -394
  45. package/src/utils/descriptions/resumePublication.ts +217 -217
  46. package/src/utils/descriptions/s3Storage.ts +214 -214
  47. 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
  }