education-playlists 0.1.0
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/README.md +86 -0
- package/dist/api/client.d.ts +10 -0
- package/dist/catalog/index.d.ts +18 -0
- package/dist/catalog/mit-ocw-seed.d.ts +41 -0
- package/dist/catalog/taxonomy.d.ts +8 -0
- package/dist/components/EducationPlaylists.d.ts +36 -0
- package/dist/components/PlaylistPlanner.d.ts +14 -0
- package/dist/components/PlaylistView.d.ts +23 -0
- package/dist/components/styles.d.ts +30 -0
- package/dist/index.cjs +1407 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1332 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/playlists.d.ts +31 -0
- package/dist/lib/progress.d.ts +17 -0
- package/dist/lib/sanitize.d.ts +19 -0
- package/dist/lib/search.d.ts +9 -0
- package/dist/lib/sharing.d.ts +31 -0
- package/dist/lib/time.d.ts +22 -0
- package/dist/planner/index.d.ts +72 -0
- package/dist/quiz.d.ts +36 -0
- package/dist/server/index.cjs +493 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.js +465 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types.d.ts +131 -0
- package/package.json +64 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PlannerDeps } from '../planner';
|
|
2
|
+
export type { PlannerDeps, WebSearchHit, PlanResult, FollowUpsResult, FollowUpQuestion, PlanRequest } from '../planner';
|
|
3
|
+
/** Request bodies larger than this are refused before parsing. */
|
|
4
|
+
export declare const MAX_PLAN_BODY_BYTES: number;
|
|
5
|
+
/**
|
|
6
|
+
* Answers one planner request. The host owns everything around it: auth, rate
|
|
7
|
+
* limiting (each POST can cost a model call and a few searches), and CORS.
|
|
8
|
+
*/
|
|
9
|
+
export declare function handleEducationPlaylistsRequest(request: Request, deps?: PlannerDeps): Promise<Response>;
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
// src/catalog/taxonomy.ts
|
|
2
|
+
var EDUCATION_CATEGORIES = [
|
|
3
|
+
{
|
|
4
|
+
id: "engineering",
|
|
5
|
+
name: "Engineering",
|
|
6
|
+
majors: [
|
|
7
|
+
{
|
|
8
|
+
id: "computer-science",
|
|
9
|
+
name: "Computer Science",
|
|
10
|
+
department: "6",
|
|
11
|
+
programs: [
|
|
12
|
+
{ id: "foundations", name: "Foundations", description: "Programming and discrete math" },
|
|
13
|
+
{ id: "algorithms-theory", name: "Algorithms & Theory" },
|
|
14
|
+
{ id: "ai-ml", name: "AI & Machine Learning" },
|
|
15
|
+
{ id: "systems", name: "Systems" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "science",
|
|
22
|
+
name: "Science",
|
|
23
|
+
majors: [
|
|
24
|
+
{
|
|
25
|
+
id: "mathematics",
|
|
26
|
+
name: "Mathematics",
|
|
27
|
+
department: "18",
|
|
28
|
+
programs: [
|
|
29
|
+
{ id: "calculus", name: "Calculus" },
|
|
30
|
+
{ id: "linear-algebra-probability", name: "Linear Algebra & Probability" }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "physics",
|
|
35
|
+
name: "Physics",
|
|
36
|
+
department: "8",
|
|
37
|
+
programs: [{ id: "core-physics", name: "Core Physics" }]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "biology-chemistry",
|
|
41
|
+
name: "Biology & Chemistry",
|
|
42
|
+
department: "7",
|
|
43
|
+
programs: [{ id: "life-sciences-core", name: "Life Sciences Core" }]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "brain-cognitive",
|
|
47
|
+
name: "Brain & Cognitive Sciences",
|
|
48
|
+
department: "9",
|
|
49
|
+
programs: [{ id: "brain-mind", name: "Brain & Mind" }]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "social-science",
|
|
55
|
+
name: "Social Science & Management",
|
|
56
|
+
majors: [
|
|
57
|
+
{
|
|
58
|
+
id: "economics",
|
|
59
|
+
name: "Economics",
|
|
60
|
+
department: "14",
|
|
61
|
+
programs: [{ id: "principles", name: "Principles" }]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "finance",
|
|
65
|
+
name: "Finance",
|
|
66
|
+
department: "15",
|
|
67
|
+
programs: [{ id: "finance-theory", name: "Finance Theory" }]
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// src/catalog/mit-ocw-seed.ts
|
|
74
|
+
var OCW = "https://ocw.mit.edu/courses/";
|
|
75
|
+
var OCW_YOUTUBE_SEARCH = "https://www.youtube.com/@mitocw/search?query=";
|
|
76
|
+
var seedProvenance = (note) => ({
|
|
77
|
+
provider: "mit_ocw",
|
|
78
|
+
method: "curated_seed",
|
|
79
|
+
verified: false,
|
|
80
|
+
note
|
|
81
|
+
});
|
|
82
|
+
function courseItems(course) {
|
|
83
|
+
const base = {
|
|
84
|
+
courseNumber: course.number,
|
|
85
|
+
institution: "MIT",
|
|
86
|
+
level: course.level
|
|
87
|
+
};
|
|
88
|
+
const id = course.slug;
|
|
89
|
+
const items = [];
|
|
90
|
+
if (course.lectures > 0) {
|
|
91
|
+
items.push({
|
|
92
|
+
...base,
|
|
93
|
+
id: `${id}#videos`,
|
|
94
|
+
title: `${course.number} ${course.title}: lecture videos`,
|
|
95
|
+
url: `${OCW_YOUTUBE_SEARCH}${encodeURIComponent(`${course.number} ${course.title}`)}`,
|
|
96
|
+
kind: "video_playlist",
|
|
97
|
+
minutes: course.lectures * course.lectureMinutes,
|
|
98
|
+
estimate: "rough",
|
|
99
|
+
estimateBasis: `about ${course.lectures} lectures \xD7 ${course.lectureMinutes} min`,
|
|
100
|
+
description: course.description,
|
|
101
|
+
provenance: seedProvenance(
|
|
102
|
+
"Searches MIT OCW's YouTube channel; replaced by per-lecture URLs once the OCW YouTube dataset is ingested."
|
|
103
|
+
)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
items.push({
|
|
107
|
+
...base,
|
|
108
|
+
id: `${id}#courseware`,
|
|
109
|
+
title: `${course.number} ${course.title}: notes, problem sets and exams`,
|
|
110
|
+
url: `${OCW}${course.slug}/`,
|
|
111
|
+
kind: "courseware",
|
|
112
|
+
minutes: course.studyHours * 60,
|
|
113
|
+
estimate: "rough",
|
|
114
|
+
estimateBasis: `about ${course.studyHours} h of problem sets and readings`,
|
|
115
|
+
description: course.description,
|
|
116
|
+
provenance: seedProvenance("Canonical OCW course page, curated by hand.")
|
|
117
|
+
});
|
|
118
|
+
return items;
|
|
119
|
+
}
|
|
120
|
+
var c = (number, title, slug, level, lectures, lectureMinutes, studyHours, description) => ({ number, title, slug, level, lectures, lectureMinutes, studyHours, description });
|
|
121
|
+
var MIT_OCW_SEED_COURSES = Object.fromEntries(
|
|
122
|
+
[
|
|
123
|
+
// Computer science (Course 6)
|
|
124
|
+
c("6.0001", "Introduction to Computer Science and Programming in Python", "6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016", "introductory", 12, 50, 30, "Programming in Python for people with little or no experience: computation, abstraction, recursion, objects, and algorithmic complexity."),
|
|
125
|
+
c("6.0002", "Introduction to Computational Thinking and Data Science", "6-0002-introduction-to-computational-thinking-and-data-science-fall-2016", "introductory", 15, 50, 30, "Using computation to solve problems: optimization, graphs, simulation, statistics and machine learning basics, in Python."),
|
|
126
|
+
c("6.042J", "Mathematics for Computer Science", "6-042j-mathematics-for-computer-science-spring-2015", "introductory", 25, 80, 60, "Discrete math for computer science: proofs, induction, number theory, graphs, counting and discrete probability."),
|
|
127
|
+
c("6.006", "Introduction to Algorithms", "6-006-introduction-to-algorithms-spring-2020", "intermediate", 21, 50, 60, "Data structures and algorithms: sorting, hashing, graph search, shortest paths and dynamic programming."),
|
|
128
|
+
c("6.046J", "Design and Analysis of Algorithms", "6-046j-design-and-analysis-of-algorithms-spring-2015", "advanced", 24, 80, 60, "Algorithm design techniques: divide and conquer, amortization, randomization, network flow, and approximation."),
|
|
129
|
+
c("18.404J", "Theory of Computation", "18-404j-theory-of-computation-fall-2020", "advanced", 25, 75, 50, "Automata, computability and complexity: what can be computed, and how efficiently."),
|
|
130
|
+
c("6.034", "Artificial Intelligence", "6-034-artificial-intelligence-fall-2010", "intermediate", 23, 50, 40, "Representations and methods of AI: search, constraints, learning, and knowledge representation."),
|
|
131
|
+
c("6.036", "Introduction to Machine Learning", "6-036-introduction-to-machine-learning-fall-2020", "intermediate", 13, 90, 40, "Principles and algorithms of machine learning: linear models, neural networks, and reinforcement learning."),
|
|
132
|
+
c("18.065", "Matrix Methods in Data Analysis, Signal Processing, and Machine Learning", "18-065-matrix-methods-in-data-analysis-signal-processing-and-machine-learning-spring-2018", "advanced", 36, 50, 40, "Linear algebra for deep learning and data analysis: SVD, low-rank approximation, optimization, and neural nets."),
|
|
133
|
+
c("6.004", "Computation Structures", "6-004-computation-structures-spring-2017", "intermediate", 25, 50, 50, "How computers work: digital logic, processor design, memory hierarchy, and operating-system support."),
|
|
134
|
+
c("6.033", "Computer System Engineering", "6-033-computer-system-engineering-spring-2018", "advanced", 26, 80, 50, "Designing large computer systems: networking, fault tolerance, distributed systems, and security."),
|
|
135
|
+
c("6.172", "Performance Engineering of Software Systems", "6-172-performance-engineering-of-software-systems-fall-2018", "advanced", 23, 80, 60, "Building fast, scalable software: work and span, caching, parallelism, and measurement."),
|
|
136
|
+
// Mathematics (Course 18)
|
|
137
|
+
c("18.01", "Single Variable Calculus", "18-01-single-variable-calculus-fall-2006", "introductory", 39, 50, 60, "Differentiation and integration of functions of one variable, with applications."),
|
|
138
|
+
c("18.02", "Multivariable Calculus", "18-02-multivariable-calculus-fall-2007", "introductory", 35, 50, 60, "Vectors, partial derivatives, multiple integrals, and vector calculus."),
|
|
139
|
+
c("18.03", "Differential Equations", "18-03-differential-equations-spring-2010", "intermediate", 33, 50, 50, "Ordinary differential equations: first-order and linear equations, Laplace transforms, and systems."),
|
|
140
|
+
c("18.06", "Linear Algebra", "18-06-linear-algebra-spring-2010", "introductory", 34, 45, 50, "Matrix theory and linear algebra: systems of equations, vector spaces, determinants, eigenvalues, and positive definite matrices."),
|
|
141
|
+
c("18.05", "Introduction to Probability and Statistics", "18-05-introduction-to-probability-and-statistics-spring-2014", "introductory", 0, 0, 60, "Probability, random variables, Bayesian and frequentist statistics, taught through readings and class problems."),
|
|
142
|
+
// Physics (Course 8)
|
|
143
|
+
c("8.01SC", "Classical Mechanics", "8-01sc-classical-mechanics-fall-2016", "introductory", 35, 45, 60, "Newtonian mechanics: kinematics, forces, energy, momentum, rotation, and gravitation."),
|
|
144
|
+
c("8.02", "Physics II: Electricity and Magnetism", "8-02-physics-ii-electricity-and-magnetism-spring-2007", "introductory", 36, 50, 50, "Electric and magnetic fields, circuits, induction, and electromagnetic waves."),
|
|
145
|
+
c("8.04", "Quantum Physics I", "8-04-quantum-physics-i-spring-2013", "advanced", 24, 80, 60, "Wave mechanics: the Schr\xF6dinger equation, one-dimensional potentials, and the hydrogen atom."),
|
|
146
|
+
// Biology (Course 7), chemistry (Course 5), brain and cognitive sciences (Course 9)
|
|
147
|
+
c("7.016", "Introductory Biology", "7-016-introductory-biology-fall-2018", "introductory", 35, 50, 40, "Biochemistry, genetics, molecular biology, and cell biology, with an emphasis on how they connect."),
|
|
148
|
+
c("7.012", "Introduction to Biology", "7-012-introduction-to-biology-fall-2004", "introductory", 35, 50, 30, "Fundamental principles of biochemistry, genetics, molecular biology, and cell biology."),
|
|
149
|
+
c("5.111SC", "Principles of Chemical Science", "5-111sc-principles-of-chemical-science-fall-2014", "introductory", 36, 50, 40, "Atomic structure, bonding, thermodynamics, kinetics, and chemical equilibrium."),
|
|
150
|
+
c("9.13", "The Human Brain", "9-13-the-human-brain-spring-2019", "introductory", 23, 80, 30, "How the brain gives rise to perception, language, and thought, and how we know."),
|
|
151
|
+
c("9.00SC", "Introduction to Psychology", "9-00sc-introduction-to-psychology-fall-2011", "introductory", 24, 50, 30, "Survey of the scientific study of human nature: perception, memory, emotion, and social behavior."),
|
|
152
|
+
// Economics (Course 14) and management (Course 15)
|
|
153
|
+
c("14.01SC", "Principles of Microeconomics", "14-01sc-principles-of-microeconomics-fall-2011", "introductory", 26, 50, 40, "How consumers and firms make choices, and how markets set prices."),
|
|
154
|
+
c("14.02", "Principles of Macroeconomics", "14-02-principles-of-macroeconomics-spring-2014", "introductory", 25, 80, 40, "Output, unemployment, inflation, interest rates, and the policies that move them."),
|
|
155
|
+
c("15.401", "Finance Theory I", "15-401-finance-theory-i-fall-2008", "intermediate", 24, 80, 40, "Present value, fixed income, equities, risk and return, and capital budgeting.")
|
|
156
|
+
].map((course) => [course.number, course])
|
|
157
|
+
);
|
|
158
|
+
function preset(id, title, categoryId, majorId, programId, level, description, courseNumbers) {
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
title,
|
|
162
|
+
description,
|
|
163
|
+
categoryId,
|
|
164
|
+
majorId,
|
|
165
|
+
programId,
|
|
166
|
+
level,
|
|
167
|
+
visibility: "preset",
|
|
168
|
+
items: courseNumbers.flatMap((number) => {
|
|
169
|
+
const course = MIT_OCW_SEED_COURSES[number];
|
|
170
|
+
if (!course) throw new Error(`Unknown seed course ${number}`);
|
|
171
|
+
return courseItems(course);
|
|
172
|
+
})
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
var MIT_OCW_PRESET_PLAYLISTS = [
|
|
176
|
+
preset("cs-foundations", "CS Foundations", "engineering", "computer-science", "foundations", "introductory", "Start programming and pick up the math computer science leans on.", ["6.0001", "6.0002", "6.042J"]),
|
|
177
|
+
preset("cs-algorithms-theory", "Algorithms and Theory", "engineering", "computer-science", "algorithms-theory", "intermediate", "From data structures to what can be computed at all.", ["6.006", "6.046J", "18.404J"]),
|
|
178
|
+
preset("cs-ai-ml", "AI and Machine Learning", "engineering", "computer-science", "ai-ml", "intermediate", "Classical AI, modern machine learning, and the linear algebra underneath.", ["6.034", "6.036", "18.065"]),
|
|
179
|
+
preset("cs-systems", "Computer Systems", "engineering", "computer-science", "systems", "intermediate", "How hardware runs software, and how to build systems that are fast and reliable.", ["6.004", "6.033", "6.172"]),
|
|
180
|
+
preset("math-calculus", "The Calculus Sequence", "science", "mathematics", "calculus", "introductory", "Single-variable, multivariable, and differential equations, in that order.", ["18.01", "18.02", "18.03"]),
|
|
181
|
+
preset("math-linear-algebra-probability", "Linear Algebra and Probability", "science", "mathematics", "linear-algebra-probability", "introductory", "The two subjects every quantitative field assumes.", ["18.06", "18.05"]),
|
|
182
|
+
preset("physics-core", "Core Physics", "science", "physics", "core-physics", "introductory", "Mechanics, electromagnetism, then a first look at quantum.", ["8.01SC", "8.02", "8.04"]),
|
|
183
|
+
preset("life-sciences-core", "Life Sciences Core", "science", "biology-chemistry", "life-sciences-core", "introductory", "Biology and the chemistry it is built on.", ["5.111SC", "7.016", "7.012"]),
|
|
184
|
+
preset("brain-mind", "Brain and Mind", "science", "brain-cognitive", "brain-mind", "introductory", "Psychology and neuroscience for the curious.", ["9.00SC", "9.13"]),
|
|
185
|
+
preset("econ-principles", "Economics Principles", "social-science", "economics", "principles", "introductory", "Micro then macro: how markets and economies work.", ["14.01SC", "14.02"]),
|
|
186
|
+
preset("finance-theory", "Finance Theory", "social-science", "finance", "finance-theory", "intermediate", "Valuation, risk, and return.", ["15.401"])
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// src/catalog/index.ts
|
|
190
|
+
function getDefaultCatalog() {
|
|
191
|
+
return { categories: EDUCATION_CATEGORIES, playlists: MIT_OCW_PRESET_PLAYLISTS };
|
|
192
|
+
}
|
|
193
|
+
function catalogItems(catalog) {
|
|
194
|
+
const seen = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const playlist of catalog.playlists) {
|
|
196
|
+
for (const item of playlist.items) if (!seen.has(item.id)) seen.set(item.id, item);
|
|
197
|
+
}
|
|
198
|
+
return [...seen.values()];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/lib/search.ts
|
|
202
|
+
var STOP_WORDS = new Set(
|
|
203
|
+
"a an and are as at be by for from how i in into is it learn learning me my of on or the to want what with about understand study become get better".split(" ")
|
|
204
|
+
);
|
|
205
|
+
function tokenize(text) {
|
|
206
|
+
return text.toLowerCase().split(/[^a-z0-9.]+/).map((token) => token.replace(/^\.+|\.+$/g, "")).filter((token) => token.length > 1 && !STOP_WORDS.has(token));
|
|
207
|
+
}
|
|
208
|
+
function searchCatalog(items, query, limit = 10) {
|
|
209
|
+
const terms = tokenize(query);
|
|
210
|
+
if (terms.length === 0) return [];
|
|
211
|
+
return items.map((item, order) => {
|
|
212
|
+
const title = item.title.toLowerCase();
|
|
213
|
+
const body = `${item.description ?? ""} ${item.courseNumber ?? ""}`.toLowerCase();
|
|
214
|
+
let score = 0;
|
|
215
|
+
for (const term of terms) {
|
|
216
|
+
if (title.includes(term)) score += 2;
|
|
217
|
+
else if (body.includes(term)) score += 1;
|
|
218
|
+
}
|
|
219
|
+
return { item, score, order };
|
|
220
|
+
}).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.order - b.order).slice(0, limit).map((entry) => entry.item);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/lib/sanitize.ts
|
|
224
|
+
var MAX_ITEM_MINUTES = 500 * 60;
|
|
225
|
+
function safeUrl(value) {
|
|
226
|
+
if (typeof value !== "string") return null;
|
|
227
|
+
try {
|
|
228
|
+
const url = new URL(value.trim());
|
|
229
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/lib/playlists.ts
|
|
236
|
+
var newId = () => typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `pl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
237
|
+
function createPlaylist(input) {
|
|
238
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
239
|
+
return {
|
|
240
|
+
id: newId(),
|
|
241
|
+
title: input.title.trim() || "My playlist",
|
|
242
|
+
description: input.description,
|
|
243
|
+
categoryId: "custom",
|
|
244
|
+
majorId: "custom",
|
|
245
|
+
visibility: input.visibility ?? "private",
|
|
246
|
+
ownerId: input.ownerId,
|
|
247
|
+
members: [],
|
|
248
|
+
items: input.items ?? [],
|
|
249
|
+
plannedFrom: input.plannedFrom,
|
|
250
|
+
createdAt: now,
|
|
251
|
+
updatedAt: now
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/planner/index.ts
|
|
256
|
+
var MAX_GOAL_LENGTH = 300;
|
|
257
|
+
var MAX_ANSWERS = 8;
|
|
258
|
+
var MAX_ANSWER_LENGTH = 300;
|
|
259
|
+
var MAX_PLAN_ITEMS = 12;
|
|
260
|
+
var MAX_SEARCHES = 5;
|
|
261
|
+
var WEB_RESULTS_PER_SEARCH = 3;
|
|
262
|
+
var WEB_ITEM_MINUTES = 60;
|
|
263
|
+
var DEFAULT_FOLLOW_UPS = [
|
|
264
|
+
{ id: "level", question: "Where are you starting from?", options: ["New to this", "Some background", "Comfortable, want depth"] },
|
|
265
|
+
{ id: "time", question: "How much time can you give it each week?", options: ["2 hours", "5 hours", "10 hours"] },
|
|
266
|
+
{ id: "format", question: "What do you learn best from?", options: ["Lecture videos", "Problem sets and notes", "A mix"] },
|
|
267
|
+
{ id: "outcome", question: "What should you be able to do at the end?", options: ["Pass a course or exam", "Build a project", "Switch careers", "Just curious"] }
|
|
268
|
+
];
|
|
269
|
+
function normalizePlanRequest(raw) {
|
|
270
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
271
|
+
const goal = typeof r.goal === "string" ? r.goal.trim().slice(0, MAX_GOAL_LENGTH) : "";
|
|
272
|
+
if (!goal) throw new Error("A learning goal is required");
|
|
273
|
+
const answers = Array.isArray(r.answers) ? r.answers.slice(0, MAX_ANSWERS).flatMap((entry) => {
|
|
274
|
+
const e = entry && typeof entry === "object" ? entry : {};
|
|
275
|
+
const question = typeof e.question === "string" ? e.question.trim().slice(0, MAX_ANSWER_LENGTH) : "";
|
|
276
|
+
const answer = typeof e.answer === "string" ? e.answer.trim().slice(0, MAX_ANSWER_LENGTH) : "";
|
|
277
|
+
return question && answer ? [{ question, answer }] : [];
|
|
278
|
+
}) : [];
|
|
279
|
+
return { goal, answers };
|
|
280
|
+
}
|
|
281
|
+
function extractJson(text) {
|
|
282
|
+
const start = text.indexOf("{");
|
|
283
|
+
const end = text.lastIndexOf("}");
|
|
284
|
+
if (start < 0 || end <= start) return null;
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
var FOLLOW_UP_SYSTEM = `You help a self-directed learner plan a study playlist of free university courses and lecture videos.
|
|
292
|
+
Ask 3 or 4 short follow-up questions that would most change which courses to recommend (their starting level, weekly time, preferred format, and what they want to be able to do are usually the most useful).
|
|
293
|
+
Give each question 2 to 4 short answer options. The learner can also type their own answer.
|
|
294
|
+
Reply with JSON only, in this shape: {"questions":[{"question":"...","options":["...","..."]}]}`;
|
|
295
|
+
function parseFollowUps(text) {
|
|
296
|
+
const json2 = extractJson(text);
|
|
297
|
+
if (!json2 || !Array.isArray(json2.questions)) return null;
|
|
298
|
+
const questions = json2.questions.slice(0, 4).flatMap((entry, index) => {
|
|
299
|
+
const e = entry && typeof entry === "object" ? entry : {};
|
|
300
|
+
const question = typeof e.question === "string" ? e.question.trim().slice(0, 200) : "";
|
|
301
|
+
if (!question) return [];
|
|
302
|
+
const options = Array.isArray(e.options) ? e.options.filter((o) => typeof o === "string" && o.trim() !== "").map((o) => o.trim().slice(0, 80)).slice(0, 4) : [];
|
|
303
|
+
return [{ id: `q${index + 1}`, question, options }];
|
|
304
|
+
});
|
|
305
|
+
return questions.length ? questions : null;
|
|
306
|
+
}
|
|
307
|
+
async function suggestFollowUps(goal, deps = {}) {
|
|
308
|
+
const { goal: cleanGoal } = normalizePlanRequest({ goal });
|
|
309
|
+
if (deps.generate) {
|
|
310
|
+
try {
|
|
311
|
+
const text = await deps.generate({ system: FOLLOW_UP_SYSTEM, prompt: `Learning goal: ${cleanGoal}` });
|
|
312
|
+
const questions = parseFollowUps(text);
|
|
313
|
+
if (questions) return { questions, mode: "llm" };
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { questions: DEFAULT_FOLLOW_UPS, mode: "offline" };
|
|
318
|
+
}
|
|
319
|
+
var PLAN_SYSTEM = `You plan a self-paced study playlist from free university courseware (MIT OpenCourseWare first) and lecture videos.
|
|
320
|
+
From the learner's goal and answers, write 3 to 5 short search queries (a few words each, the way you would search a course catalog) that together cover what they need, in study order.
|
|
321
|
+
If you know MIT course numbers that fit (for example 6.0001 or 18.06), list them too.
|
|
322
|
+
Reply with JSON only, in this shape: {"title":"...","searches":["..."],"courses":["6.0001"]}`;
|
|
323
|
+
function parseModelPlan(text) {
|
|
324
|
+
const json2 = extractJson(text);
|
|
325
|
+
if (!json2 || !Array.isArray(json2.searches)) return null;
|
|
326
|
+
const strings = (value, max) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "").map((v) => v.trim().slice(0, max)) : [];
|
|
327
|
+
const searches = strings(json2.searches, 120).slice(0, MAX_SEARCHES);
|
|
328
|
+
if (searches.length === 0) return null;
|
|
329
|
+
return {
|
|
330
|
+
title: typeof json2.title === "string" && json2.title.trim() ? json2.title.trim().slice(0, 120) : void 0,
|
|
331
|
+
searches,
|
|
332
|
+
courses: strings(json2.courses, 20).slice(0, 10)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function readPreferences(answers = []) {
|
|
336
|
+
const text = answers.map((a) => a.answer.toLowerCase()).join(" | ");
|
|
337
|
+
const beginner = /\b(new|beginner|none|no background|never|starting out|from scratch)\b/.test(text);
|
|
338
|
+
const advanced = /\b(comfortable|depth|advanced|expert|graduate|deep)\b/.test(text);
|
|
339
|
+
const wantsVideos = /\bvideo|lecture|watch/.test(text);
|
|
340
|
+
const wantsCourseware = /\bproblem|notes|reading|exercise|courseware|text/.test(text);
|
|
341
|
+
const hours = text.match(/(\d+(?:\.\d+)?)\s*(?:\+\s*)?(?:h|hr|hrs|hour|hours)\b/);
|
|
342
|
+
return {
|
|
343
|
+
maxLevel: beginner && !advanced ? "intermediate" : void 0,
|
|
344
|
+
preferAdvanced: advanced && !beginner,
|
|
345
|
+
format: wantsVideos && !wantsCourseware ? "videos" : wantsCourseware && !wantsVideos ? "courseware" : "mix",
|
|
346
|
+
minutesPerWeek: hours ? Math.round(Number(hours[1]) * 60) : void 0
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
var LEVEL_ORDER = { introductory: 0, intermediate: 1, advanced: 2 };
|
|
350
|
+
function webItem(hit, query, index) {
|
|
351
|
+
const url = safeUrl(hit.url);
|
|
352
|
+
const title = typeof hit.title === "string" ? hit.title.trim().slice(0, 200) : "";
|
|
353
|
+
if (!url || !title) return null;
|
|
354
|
+
const isVideo = /youtube\.com|youtu\.be|vimeo\.com/.test(url);
|
|
355
|
+
return {
|
|
356
|
+
id: `web:${url}`,
|
|
357
|
+
title,
|
|
358
|
+
url,
|
|
359
|
+
kind: isVideo ? "video" : "reading",
|
|
360
|
+
minutes: WEB_ITEM_MINUTES,
|
|
361
|
+
estimate: "rough",
|
|
362
|
+
estimateBasis: "length unknown; budgeted at 1 h",
|
|
363
|
+
description: hit.snippet?.slice(0, 300),
|
|
364
|
+
provenance: {
|
|
365
|
+
provider: isVideo ? "youtube" : "web",
|
|
366
|
+
method: "llm_search",
|
|
367
|
+
verified: false,
|
|
368
|
+
note: `Found by the planner's web search for "${query}" (result ${index + 1}).`
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async function planPlaylist(rawRequest, deps = {}) {
|
|
373
|
+
const { goal, answers = [] } = normalizePlanRequest(rawRequest);
|
|
374
|
+
const catalog = deps.catalog ?? getDefaultCatalog();
|
|
375
|
+
const items = catalogItems(catalog);
|
|
376
|
+
const prefs = readPreferences(answers);
|
|
377
|
+
let mode = "offline";
|
|
378
|
+
let plan = null;
|
|
379
|
+
if (deps.generate) {
|
|
380
|
+
try {
|
|
381
|
+
const prompt = [`Learning goal: ${goal}`, ...answers.map((a) => `Q: ${a.question}
|
|
382
|
+
A: ${a.answer}`)].join("\n\n");
|
|
383
|
+
plan = parseModelPlan(await deps.generate({ system: PLAN_SYSTEM, prompt }));
|
|
384
|
+
if (plan) mode = "llm";
|
|
385
|
+
} catch {
|
|
386
|
+
plan = null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const searches = plan?.searches ?? [
|
|
390
|
+
goal,
|
|
391
|
+
...answers.map((a) => a.answer).filter((answer) => tokenize(answer).length >= 2 && !DEFAULT_FOLLOW_UPS.some((q) => q.options.includes(answer)))
|
|
392
|
+
].slice(0, MAX_SEARCHES);
|
|
393
|
+
const picked = /* @__PURE__ */ new Map();
|
|
394
|
+
const take = (item) => {
|
|
395
|
+
if (!picked.has(item.id) && ![...picked.values()].some((p) => p.url === item.url)) picked.set(item.id, item);
|
|
396
|
+
};
|
|
397
|
+
for (const number of plan?.courses ?? []) {
|
|
398
|
+
for (const item of items) if (item.courseNumber?.toLowerCase() === number.toLowerCase()) take(item);
|
|
399
|
+
}
|
|
400
|
+
for (const query of searches) for (const item of searchCatalog(items, query, 6)) take(item);
|
|
401
|
+
if (deps.search) {
|
|
402
|
+
for (const query of searches.slice(0, 3)) {
|
|
403
|
+
try {
|
|
404
|
+
const hits = await deps.search(`${query} free course lecture videos`);
|
|
405
|
+
hits.slice(0, WEB_RESULTS_PER_SEARCH).forEach((hit, index) => {
|
|
406
|
+
const item = webItem(hit, query, index);
|
|
407
|
+
if (item) take(item);
|
|
408
|
+
});
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const chosen = [...picked.values()].filter((item) => !prefs.maxLevel || !item.level || LEVEL_ORDER[item.level] <= LEVEL_ORDER[prefs.maxLevel]).filter((item) => {
|
|
414
|
+
if (prefs.format === "videos") return item.kind === "video" || item.kind === "video_playlist";
|
|
415
|
+
if (prefs.format === "courseware") return item.kind !== "video" && item.kind !== "video_playlist";
|
|
416
|
+
return true;
|
|
417
|
+
}).map((item, order) => ({ item, order })).sort((a, b) => {
|
|
418
|
+
const la = a.item.level ? LEVEL_ORDER[a.item.level] : 1;
|
|
419
|
+
const lb = b.item.level ? LEVEL_ORDER[b.item.level] : 1;
|
|
420
|
+
return (prefs.preferAdvanced ? lb - la : la - lb) || a.order - b.order;
|
|
421
|
+
}).slice(0, MAX_PLAN_ITEMS).map((entry) => entry.item);
|
|
422
|
+
const playlist = createPlaylist({
|
|
423
|
+
title: plan?.title ?? `My path: ${goal.slice(0, 80)}`,
|
|
424
|
+
description: `Planned from: ${goal}`,
|
|
425
|
+
items: chosen,
|
|
426
|
+
plannedFrom: { goal, answers }
|
|
427
|
+
});
|
|
428
|
+
return { playlist, searches, mode, minutesPerWeek: prefs.minutesPerWeek };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/server/index.ts
|
|
432
|
+
var MAX_PLAN_BODY_BYTES = 8 * 1024;
|
|
433
|
+
var json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" } });
|
|
434
|
+
async function readBody(request) {
|
|
435
|
+
const text = await request.text();
|
|
436
|
+
if (text.length > MAX_PLAN_BODY_BYTES) throw new RangeError("Request body too large");
|
|
437
|
+
return text ? JSON.parse(text) : {};
|
|
438
|
+
}
|
|
439
|
+
async function handleEducationPlaylistsRequest(request, deps = {}) {
|
|
440
|
+
const path = new URL(request.url).pathname.replace(/\/+$/, "");
|
|
441
|
+
if (request.method === "GET") {
|
|
442
|
+
if (path.endsWith("/catalog")) return json(deps.catalog ?? getDefaultCatalog());
|
|
443
|
+
return json({ error: "Not found" }, 404);
|
|
444
|
+
}
|
|
445
|
+
if (request.method !== "POST") return json({ error: "Method not allowed" }, 405);
|
|
446
|
+
let body;
|
|
447
|
+
try {
|
|
448
|
+
body = await readBody(request);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
return json({ error: error instanceof RangeError ? error.message : "Body must be JSON" }, error instanceof RangeError ? 413 : 400);
|
|
451
|
+
}
|
|
452
|
+
let planRequest;
|
|
453
|
+
try {
|
|
454
|
+
planRequest = normalizePlanRequest(body);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return json({ error: error.message }, 400);
|
|
457
|
+
}
|
|
458
|
+
if (path.endsWith("/questions")) return json(await suggestFollowUps(planRequest.goal, deps));
|
|
459
|
+
return json(await planPlaylist(planRequest, deps));
|
|
460
|
+
}
|
|
461
|
+
export {
|
|
462
|
+
MAX_PLAN_BODY_BYTES,
|
|
463
|
+
handleEducationPlaylistsRequest
|
|
464
|
+
};
|
|
465
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/catalog/taxonomy.ts","../../src/catalog/mit-ocw-seed.ts","../../src/catalog/index.ts","../../src/lib/search.ts","../../src/lib/sanitize.ts","../../src/lib/playlists.ts","../../src/planner/index.ts","../../src/server/index.ts"],"sourcesContent":["/**\n * @fileoverview The catalog's shape, borrowed from a university: broad\n * categories (schools), majors (departments) and programs (tracks within a\n * major). Majors carry their MIT department number so ingested MIT records,\n * which are filed by department, land in the right place.\n */\nimport type { Category } from '../types';\n\nexport const EDUCATION_CATEGORIES: Category[] = [\n {\n id: 'engineering',\n name: 'Engineering',\n majors: [\n {\n id: 'computer-science',\n name: 'Computer Science',\n department: '6',\n programs: [\n { id: 'foundations', name: 'Foundations', description: 'Programming and discrete math' },\n { id: 'algorithms-theory', name: 'Algorithms & Theory' },\n { id: 'ai-ml', name: 'AI & Machine Learning' },\n { id: 'systems', name: 'Systems' },\n ],\n },\n ],\n },\n {\n id: 'science',\n name: 'Science',\n majors: [\n {\n id: 'mathematics',\n name: 'Mathematics',\n department: '18',\n programs: [\n { id: 'calculus', name: 'Calculus' },\n { id: 'linear-algebra-probability', name: 'Linear Algebra & Probability' },\n ],\n },\n {\n id: 'physics',\n name: 'Physics',\n department: '8',\n programs: [{ id: 'core-physics', name: 'Core Physics' }],\n },\n {\n id: 'biology-chemistry',\n name: 'Biology & Chemistry',\n department: '7',\n programs: [{ id: 'life-sciences-core', name: 'Life Sciences Core' }],\n },\n {\n id: 'brain-cognitive',\n name: 'Brain & Cognitive Sciences',\n department: '9',\n programs: [{ id: 'brain-mind', name: 'Brain & Mind' }],\n },\n ],\n },\n {\n id: 'social-science',\n name: 'Social Science & Management',\n majors: [\n {\n id: 'economics',\n name: 'Economics',\n department: '14',\n programs: [{ id: 'principles', name: 'Principles' }],\n },\n {\n id: 'finance',\n name: 'Finance',\n department: '15',\n programs: [{ id: 'finance-theory', name: 'Finance Theory' }],\n },\n ],\n },\n];\n","/**\n * @fileoverview The bundled seed catalog: MIT OpenCourseWare courses, grouped\n * the way MIT groups them (by department), into one preset playlist per\n * program.\n *\n * This is a hand-curated starting point, not an ingestion. Each course page\n * URL is the canonical `ocw.mit.edu/courses/<slug>/` address, and every item\n * says so in its provenance (`curated_seed`, `verified: false`) until an\n * ingestion run replaces it. The intended sources, in order of authority:\n *\n * 1. `mitodl/ocw_oer_export` — MIT's own exporter over the MIT Learn API:\n * title, canonical URL, level, description, topics, instructors, term.\n * 2. The MIT OCW YouTube course dataset (Kaggle, `jorgoose`) — course playlist\n * and per-lecture video URLs, for real `video` items with exact durations.\n * 3. Community awesome-lists, kept as `community_list` items in their own\n * playlists rather than merged into the official records.\n *\n * Until (2) is ingested, a course's \"lecture videos\" item searches MIT OCW's\n * own YouTube channel for the course instead of guessing a playlist id.\n *\n * Time estimates are rough on purpose: lecture count × typical session length\n * for videos, and the term's problem-set/reading load for courseware.\n */\nimport type { CourseLevel, Playlist, PlaylistItem, Provenance } from '../types';\n\nconst OCW = 'https://ocw.mit.edu/courses/';\nconst OCW_YOUTUBE_SEARCH = 'https://www.youtube.com/@mitocw/search?query=';\n\n/** One OCW course and the numbers its time estimates are derived from. */\nexport interface SeedCourse {\n number: string;\n title: string;\n /** The path segment after `/courses/`. */\n slug: string;\n level: CourseLevel;\n /** Recorded lectures; 0 for a course that publishes no lecture video. */\n lectures: number;\n lectureMinutes: number;\n /** Hours of problem sets, readings and exams over the term. */\n studyHours: number;\n description: string;\n}\n\nconst seedProvenance = (note: string): Provenance => ({\n provider: 'mit_ocw',\n method: 'curated_seed',\n verified: false,\n note,\n});\n\n/** The items a course contributes to a playlist: its lectures, then its courseware. */\nfunction courseItems(course: SeedCourse): PlaylistItem[] {\n const base = {\n courseNumber: course.number,\n institution: 'MIT',\n level: course.level,\n };\n const id = course.slug;\n const items: PlaylistItem[] = [];\n\n if (course.lectures > 0) {\n items.push({\n ...base,\n id: `${id}#videos`,\n title: `${course.number} ${course.title}: lecture videos`,\n url: `${OCW_YOUTUBE_SEARCH}${encodeURIComponent(`${course.number} ${course.title}`)}`,\n kind: 'video_playlist',\n minutes: course.lectures * course.lectureMinutes,\n estimate: 'rough',\n estimateBasis: `about ${course.lectures} lectures × ${course.lectureMinutes} min`,\n description: course.description,\n provenance: seedProvenance(\n \"Searches MIT OCW's YouTube channel; replaced by per-lecture URLs once the OCW YouTube dataset is ingested.\",\n ),\n });\n }\n\n items.push({\n ...base,\n id: `${id}#courseware`,\n title: `${course.number} ${course.title}: notes, problem sets and exams`,\n url: `${OCW}${course.slug}/`,\n kind: 'courseware',\n minutes: course.studyHours * 60,\n estimate: 'rough',\n estimateBasis: `about ${course.studyHours} h of problem sets and readings`,\n description: course.description,\n provenance: seedProvenance('Canonical OCW course page, curated by hand.'),\n });\n\n return items;\n}\n\nconst c = (\n number: string,\n title: string,\n slug: string,\n level: CourseLevel,\n lectures: number,\n lectureMinutes: number,\n studyHours: number,\n description: string,\n): SeedCourse => ({ number, title, slug, level, lectures, lectureMinutes, studyHours, description });\n\n/** The seed courses, keyed by course number. */\nexport const MIT_OCW_SEED_COURSES: Record<string, SeedCourse> = Object.fromEntries(\n [\n // Computer science (Course 6)\n c('6.0001', 'Introduction to Computer Science and Programming in Python', '6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016', 'introductory', 12, 50, 30, 'Programming in Python for people with little or no experience: computation, abstraction, recursion, objects, and algorithmic complexity.'),\n c('6.0002', 'Introduction to Computational Thinking and Data Science', '6-0002-introduction-to-computational-thinking-and-data-science-fall-2016', 'introductory', 15, 50, 30, 'Using computation to solve problems: optimization, graphs, simulation, statistics and machine learning basics, in Python.'),\n c('6.042J', 'Mathematics for Computer Science', '6-042j-mathematics-for-computer-science-spring-2015', 'introductory', 25, 80, 60, 'Discrete math for computer science: proofs, induction, number theory, graphs, counting and discrete probability.'),\n c('6.006', 'Introduction to Algorithms', '6-006-introduction-to-algorithms-spring-2020', 'intermediate', 21, 50, 60, 'Data structures and algorithms: sorting, hashing, graph search, shortest paths and dynamic programming.'),\n c('6.046J', 'Design and Analysis of Algorithms', '6-046j-design-and-analysis-of-algorithms-spring-2015', 'advanced', 24, 80, 60, 'Algorithm design techniques: divide and conquer, amortization, randomization, network flow, and approximation.'),\n c('18.404J', 'Theory of Computation', '18-404j-theory-of-computation-fall-2020', 'advanced', 25, 75, 50, 'Automata, computability and complexity: what can be computed, and how efficiently.'),\n c('6.034', 'Artificial Intelligence', '6-034-artificial-intelligence-fall-2010', 'intermediate', 23, 50, 40, 'Representations and methods of AI: search, constraints, learning, and knowledge representation.'),\n c('6.036', 'Introduction to Machine Learning', '6-036-introduction-to-machine-learning-fall-2020', 'intermediate', 13, 90, 40, 'Principles and algorithms of machine learning: linear models, neural networks, and reinforcement learning.'),\n c('18.065', 'Matrix Methods in Data Analysis, Signal Processing, and Machine Learning', '18-065-matrix-methods-in-data-analysis-signal-processing-and-machine-learning-spring-2018', 'advanced', 36, 50, 40, 'Linear algebra for deep learning and data analysis: SVD, low-rank approximation, optimization, and neural nets.'),\n c('6.004', 'Computation Structures', '6-004-computation-structures-spring-2017', 'intermediate', 25, 50, 50, 'How computers work: digital logic, processor design, memory hierarchy, and operating-system support.'),\n c('6.033', 'Computer System Engineering', '6-033-computer-system-engineering-spring-2018', 'advanced', 26, 80, 50, 'Designing large computer systems: networking, fault tolerance, distributed systems, and security.'),\n c('6.172', 'Performance Engineering of Software Systems', '6-172-performance-engineering-of-software-systems-fall-2018', 'advanced', 23, 80, 60, 'Building fast, scalable software: work and span, caching, parallelism, and measurement.'),\n // Mathematics (Course 18)\n c('18.01', 'Single Variable Calculus', '18-01-single-variable-calculus-fall-2006', 'introductory', 39, 50, 60, 'Differentiation and integration of functions of one variable, with applications.'),\n c('18.02', 'Multivariable Calculus', '18-02-multivariable-calculus-fall-2007', 'introductory', 35, 50, 60, 'Vectors, partial derivatives, multiple integrals, and vector calculus.'),\n c('18.03', 'Differential Equations', '18-03-differential-equations-spring-2010', 'intermediate', 33, 50, 50, 'Ordinary differential equations: first-order and linear equations, Laplace transforms, and systems.'),\n c('18.06', 'Linear Algebra', '18-06-linear-algebra-spring-2010', 'introductory', 34, 45, 50, 'Matrix theory and linear algebra: systems of equations, vector spaces, determinants, eigenvalues, and positive definite matrices.'),\n c('18.05', 'Introduction to Probability and Statistics', '18-05-introduction-to-probability-and-statistics-spring-2014', 'introductory', 0, 0, 60, 'Probability, random variables, Bayesian and frequentist statistics, taught through readings and class problems.'),\n // Physics (Course 8)\n c('8.01SC', 'Classical Mechanics', '8-01sc-classical-mechanics-fall-2016', 'introductory', 35, 45, 60, 'Newtonian mechanics: kinematics, forces, energy, momentum, rotation, and gravitation.'),\n c('8.02', 'Physics II: Electricity and Magnetism', '8-02-physics-ii-electricity-and-magnetism-spring-2007', 'introductory', 36, 50, 50, 'Electric and magnetic fields, circuits, induction, and electromagnetic waves.'),\n c('8.04', 'Quantum Physics I', '8-04-quantum-physics-i-spring-2013', 'advanced', 24, 80, 60, 'Wave mechanics: the Schrödinger equation, one-dimensional potentials, and the hydrogen atom.'),\n // Biology (Course 7), chemistry (Course 5), brain and cognitive sciences (Course 9)\n c('7.016', 'Introductory Biology', '7-016-introductory-biology-fall-2018', 'introductory', 35, 50, 40, 'Biochemistry, genetics, molecular biology, and cell biology, with an emphasis on how they connect.'),\n c('7.012', 'Introduction to Biology', '7-012-introduction-to-biology-fall-2004', 'introductory', 35, 50, 30, 'Fundamental principles of biochemistry, genetics, molecular biology, and cell biology.'),\n c('5.111SC', 'Principles of Chemical Science', '5-111sc-principles-of-chemical-science-fall-2014', 'introductory', 36, 50, 40, 'Atomic structure, bonding, thermodynamics, kinetics, and chemical equilibrium.'),\n c('9.13', 'The Human Brain', '9-13-the-human-brain-spring-2019', 'introductory', 23, 80, 30, 'How the brain gives rise to perception, language, and thought, and how we know.'),\n c('9.00SC', 'Introduction to Psychology', '9-00sc-introduction-to-psychology-fall-2011', 'introductory', 24, 50, 30, 'Survey of the scientific study of human nature: perception, memory, emotion, and social behavior.'),\n // Economics (Course 14) and management (Course 15)\n c('14.01SC', 'Principles of Microeconomics', '14-01sc-principles-of-microeconomics-fall-2011', 'introductory', 26, 50, 40, 'How consumers and firms make choices, and how markets set prices.'),\n c('14.02', 'Principles of Macroeconomics', '14-02-principles-of-macroeconomics-spring-2014', 'introductory', 25, 80, 40, 'Output, unemployment, inflation, interest rates, and the policies that move them.'),\n c('15.401', 'Finance Theory I', '15-401-finance-theory-i-fall-2008', 'intermediate', 24, 80, 40, 'Present value, fixed income, equities, risk and return, and capital budgeting.'),\n ].map((course) => [course.number, course]),\n);\n\n/** A preset playlist built from seed courses, in study order. */\nfunction preset(\n id: string,\n title: string,\n categoryId: string,\n majorId: string,\n programId: string,\n level: CourseLevel,\n description: string,\n courseNumbers: string[],\n): Playlist {\n return {\n id,\n title,\n description,\n categoryId,\n majorId,\n programId,\n level,\n visibility: 'preset',\n items: courseNumbers.flatMap((number) => {\n const course = MIT_OCW_SEED_COURSES[number];\n if (!course) throw new Error(`Unknown seed course ${number}`);\n return courseItems(course);\n }),\n };\n}\n\nexport const MIT_OCW_PRESET_PLAYLISTS: Playlist[] = [\n preset('cs-foundations', 'CS Foundations', 'engineering', 'computer-science', 'foundations', 'introductory', 'Start programming and pick up the math computer science leans on.', ['6.0001', '6.0002', '6.042J']),\n preset('cs-algorithms-theory', 'Algorithms and Theory', 'engineering', 'computer-science', 'algorithms-theory', 'intermediate', 'From data structures to what can be computed at all.', ['6.006', '6.046J', '18.404J']),\n preset('cs-ai-ml', 'AI and Machine Learning', 'engineering', 'computer-science', 'ai-ml', 'intermediate', 'Classical AI, modern machine learning, and the linear algebra underneath.', ['6.034', '6.036', '18.065']),\n preset('cs-systems', 'Computer Systems', 'engineering', 'computer-science', 'systems', 'intermediate', 'How hardware runs software, and how to build systems that are fast and reliable.', ['6.004', '6.033', '6.172']),\n preset('math-calculus', 'The Calculus Sequence', 'science', 'mathematics', 'calculus', 'introductory', 'Single-variable, multivariable, and differential equations, in that order.', ['18.01', '18.02', '18.03']),\n preset('math-linear-algebra-probability', 'Linear Algebra and Probability', 'science', 'mathematics', 'linear-algebra-probability', 'introductory', 'The two subjects every quantitative field assumes.', ['18.06', '18.05']),\n preset('physics-core', 'Core Physics', 'science', 'physics', 'core-physics', 'introductory', 'Mechanics, electromagnetism, then a first look at quantum.', ['8.01SC', '8.02', '8.04']),\n preset('life-sciences-core', 'Life Sciences Core', 'science', 'biology-chemistry', 'life-sciences-core', 'introductory', 'Biology and the chemistry it is built on.', ['5.111SC', '7.016', '7.012']),\n preset('brain-mind', 'Brain and Mind', 'science', 'brain-cognitive', 'brain-mind', 'introductory', 'Psychology and neuroscience for the curious.', ['9.00SC', '9.13']),\n preset('econ-principles', 'Economics Principles', 'social-science', 'economics', 'principles', 'introductory', 'Micro then macro: how markets and economies work.', ['14.01SC', '14.02']),\n preset('finance-theory', 'Finance Theory', 'social-science', 'finance', 'finance-theory', 'intermediate', 'Valuation, risk, and return.', ['15.401']),\n];\n","/**\n * @fileoverview The catalog as the widget and the server read it: the\n * taxonomy plus the preset playlists, and the lookups over them.\n */\nimport type { EducationCatalog, Playlist, PlaylistItem } from '../types';\nimport { EDUCATION_CATEGORIES } from './taxonomy';\nimport { MIT_OCW_PRESET_PLAYLISTS } from './mit-ocw-seed';\n\nexport { EDUCATION_CATEGORIES } from './taxonomy';\nexport { MIT_OCW_PRESET_PLAYLISTS, MIT_OCW_SEED_COURSES } from './mit-ocw-seed';\nexport type { SeedCourse } from './mit-ocw-seed';\n\n/** The bundled catalog: every category, and every preset playlist. */\nexport function getDefaultCatalog(): EducationCatalog {\n return { categories: EDUCATION_CATEGORIES, playlists: MIT_OCW_PRESET_PLAYLISTS };\n}\n\n/** Narrows playlists to a category, major and program; any of them may be left out. */\nexport function filterPlaylists(\n playlists: Playlist[],\n filter: { categoryId?: string; majorId?: string; programId?: string },\n): Playlist[] {\n return playlists.filter(\n (playlist) =>\n (!filter.categoryId || playlist.categoryId === filter.categoryId) &&\n (!filter.majorId || playlist.majorId === filter.majorId) &&\n (!filter.programId || playlist.programId === filter.programId),\n );\n}\n\n/** Every distinct item in the catalog, first occurrence wins. */\nexport function catalogItems(catalog: EducationCatalog): PlaylistItem[] {\n const seen = new Map<string, PlaylistItem>();\n for (const playlist of catalog.playlists) {\n for (const item of playlist.items) if (!seen.has(item.id)) seen.set(item.id, item);\n }\n return [...seen.values()];\n}\n","/**\n * @fileoverview Keyword search over catalog items. It is what the planner\n * runs its searches against when no web search is wired in, and what keeps a\n * custom playlist anchored to known courses when one is.\n */\nimport type { PlaylistItem } from '../types';\n\nconst STOP_WORDS = new Set(\n 'a an and are as at be by for from how i in into is it learn learning me my of on or the to want what with about understand study become get better'.split(' '),\n);\n\nexport function tokenize(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^a-z0-9.]+/)\n .map((token) => token.replace(/^\\.+|\\.+$/g, ''))\n .filter((token) => token.length > 1 && !STOP_WORDS.has(token));\n}\n\n/** Items ranked by how many query terms they mention; title hits count double. */\nexport function searchCatalog(items: PlaylistItem[], query: string, limit = 10): PlaylistItem[] {\n const terms = tokenize(query);\n if (terms.length === 0) return [];\n return items\n .map((item, order) => {\n const title = item.title.toLowerCase();\n const body = `${item.description ?? ''} ${item.courseNumber ?? ''}`.toLowerCase();\n let score = 0;\n for (const term of terms) {\n if (title.includes(term)) score += 2;\n else if (body.includes(term)) score += 1;\n }\n return { item, score, order };\n })\n .filter((entry) => entry.score > 0)\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .slice(0, limit)\n .map((entry) => entry.item);\n}\n","/**\n * @fileoverview Turning untrusted data into a playlist: a share link someone\n * pasted, a record from an ingestion run, or an LLM's answer.\n *\n * Every link the widget renders comes through here first. Only `http(s)` URLs\n * survive, so a shared playlist cannot smuggle a `javascript:` href onto the\n * page, and every field is length-capped and type-checked rather than trusted.\n */\nimport type {\n Playlist,\n PlaylistItem,\n PlaylistItemKind,\n PlaylistVisibility,\n ProvenanceMethod,\n EducationProvider,\n CourseLevel,\n} from '../types';\n\nexport const MAX_PLAYLIST_ITEMS = 60;\nconst MAX_TITLE = 200;\nconst MAX_TEXT = 600;\n/** A single item may not claim more than 500 hours. */\nconst MAX_ITEM_MINUTES = 500 * 60;\n\nconst KINDS: PlaylistItemKind[] = ['video_playlist', 'video', 'courseware', 'lecture_notes', 'assignment', 'reading', 'textbook'];\nconst PROVIDERS: EducationProvider[] = ['mit_ocw', 'edx', 'coursera', 'khan', 'openstax', 'youtube', 'web'];\nconst METHODS: ProvenanceMethod[] = ['curated_seed', 'official_api', 'snapshot_dataset', 'community_list', 'llm_search', 'user'];\nconst LEVELS: CourseLevel[] = ['introductory', 'intermediate', 'advanced'];\nconst VISIBILITIES: PlaylistVisibility[] = ['preset', 'public', 'private'];\n\nconst text = (value: unknown, max: number): string | undefined =>\n typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : undefined;\n\nconst oneOf = <T extends string>(value: unknown, allowed: readonly T[], fallback: T): T =>\n allowed.includes(value as T) ? (value as T) : fallback;\n\n/** The URL if it is an absolute http(s) URL, else `null`. */\nexport function safeUrl(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n try {\n const url = new URL(value.trim());\n return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString() : null;\n } catch {\n return null;\n }\n}\n\nexport function sanitizeItem(raw: unknown, index = 0): PlaylistItem | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const url = safeUrl(r.url);\n const title = text(r.title, MAX_TITLE);\n if (!url || !title) return null;\n const minutes = Number(r.minutes);\n const p = (r.provenance && typeof r.provenance === 'object' ? r.provenance : {}) as Record<string, unknown>;\n return {\n id: text(r.id, MAX_TITLE) ?? `item-${index}-${url}`,\n title,\n url,\n kind: oneOf(r.kind, KINDS, 'reading'),\n minutes: Number.isFinite(minutes) ? Math.min(Math.max(0, Math.round(minutes)), MAX_ITEM_MINUTES) : 0,\n estimate: r.estimate === 'exact' ? 'exact' : 'rough',\n estimateBasis: text(r.estimateBasis, MAX_TITLE),\n courseNumber: text(r.courseNumber, 40),\n institution: text(r.institution, 120),\n level: LEVELS.includes(r.level as CourseLevel) ? (r.level as CourseLevel) : undefined,\n description: text(r.description, MAX_TEXT),\n provenance: {\n provider: oneOf(p.provider, PROVIDERS, 'web'),\n method: oneOf(p.method, METHODS, 'user'),\n sourceUrl: safeUrl(p.sourceUrl) ?? undefined,\n fetchedAt: text(p.fetchedAt, 40),\n // Nothing that arrives from outside gets to call itself verified.\n verified: false,\n note: text(p.note, MAX_TEXT),\n },\n };\n}\n\n/**\n * A playlist rebuilt from untrusted input, or `null` if nothing usable is\n * left. Members are dropped: who a playlist is shared with is the server's\n * record, never something a link can assert.\n */\nexport function sanitizePlaylist(raw: unknown): Playlist | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const title = text(r.title, MAX_TITLE);\n if (!title || !Array.isArray(r.items)) return null;\n const items = r.items\n .slice(0, MAX_PLAYLIST_ITEMS)\n .map((item, index) => sanitizeItem(item, index))\n .filter((item): item is PlaylistItem => item !== null);\n if (items.length === 0) return null;\n const visibility = oneOf(r.visibility, VISIBILITIES, 'public');\n return {\n id: text(r.id, 120) ?? `shared-${Date.now().toString(36)}`,\n title,\n description: text(r.description, MAX_TEXT),\n categoryId: text(r.categoryId, 80) ?? 'custom',\n majorId: text(r.majorId, 80) ?? 'custom',\n programId: text(r.programId, 80),\n level: LEVELS.includes(r.level as CourseLevel) ? (r.level as CourseLevel) : undefined,\n // A link can only ever carry a public copy.\n visibility: visibility === 'preset' ? 'public' : visibility,\n items,\n };\n}\n","/**\n * @fileoverview A learner's own playlists: creating and editing them, and\n * keeping them in the browser.\n *\n * Browser storage is this sketch's only backend. It is enough for private\n * playlists and progress on one device; sharing a private playlist with an\n * invitee needs the host app to store playlists server-side (see the\n * package README), which the `PlaylistStore` interface is shaped for.\n */\nimport type { Playlist, PlaylistItem, PlaylistVisibility } from '../types';\nimport { sanitizePlaylist } from './sanitize';\n\nexport const PLAYLISTS_STORAGE_KEY = 'educationPlaylists.mine';\n\nexport interface PlaylistStore {\n list(): Playlist[];\n save(playlist: Playlist): Playlist[];\n remove(id: string): Playlist[];\n}\n\nconst newId = (): string =>\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : `pl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n\nexport function createPlaylist(input: {\n title: string;\n ownerId?: string;\n visibility?: Exclude<PlaylistVisibility, 'preset'>;\n items?: PlaylistItem[];\n description?: string;\n plannedFrom?: Playlist['plannedFrom'];\n}): Playlist {\n const now = new Date().toISOString();\n return {\n id: newId(),\n title: input.title.trim() || 'My playlist',\n description: input.description,\n categoryId: 'custom',\n majorId: 'custom',\n visibility: input.visibility ?? 'private',\n ownerId: input.ownerId,\n members: [],\n items: input.items ?? [],\n plannedFrom: input.plannedFrom,\n createdAt: now,\n updatedAt: now,\n };\n}\n\n/** Appends `item` unless the playlist already links to the same URL. */\nexport function addItem(playlist: Playlist, item: PlaylistItem): Playlist {\n if (playlist.items.some((existing) => existing.id === item.id || existing.url === item.url)) return playlist;\n return { ...playlist, items: [...playlist.items, item], updatedAt: new Date().toISOString() };\n}\n\nexport function removeItem(playlist: Playlist, itemId: string): Playlist {\n return { ...playlist, items: playlist.items.filter((item) => item.id !== itemId), updatedAt: new Date().toISOString() };\n}\n\n/** Moves the item at `from` to `to`, clamped to the list. */\nexport function moveItem(playlist: Playlist, from: number, to: number): Playlist {\n const items = [...playlist.items];\n if (from < 0 || from >= items.length) return playlist;\n const [moved] = items.splice(from, 1);\n items.splice(Math.max(0, Math.min(to, items.length)), 0, moved);\n return { ...playlist, items, updatedAt: new Date().toISOString() };\n}\n\n/** A store over `localStorage`, or in memory where there is none. */\nexport function createLocalPlaylistStore(key = PLAYLISTS_STORAGE_KEY): PlaylistStore {\n let memory: Playlist[] = [];\n const read = (): Playlist[] => {\n try {\n const raw = typeof localStorage === 'undefined' ? null : localStorage.getItem(key);\n if (!raw) return memory;\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n // Stored playlists keep their visibility and members; only their\n // content is re-validated, since anything with page access can write here.\n return parsed.flatMap((entry) => {\n const clean = sanitizePlaylist(entry);\n if (!clean) return [];\n const original = entry as Playlist;\n return [{ ...clean, visibility: original.visibility === 'public' ? 'public' : 'private', ownerId: original.ownerId, members: original.members ?? [], plannedFrom: original.plannedFrom, createdAt: original.createdAt, updatedAt: original.updatedAt }];\n });\n } catch {\n return memory;\n }\n };\n const write = (playlists: Playlist[]): Playlist[] => {\n memory = playlists;\n try {\n if (typeof localStorage !== 'undefined') localStorage.setItem(key, JSON.stringify(playlists));\n } catch {\n // Blocked storage: this page keeps the in-memory copy.\n }\n return playlists;\n };\n return {\n list: read,\n save(playlist) {\n const others = read().filter((existing) => existing.id !== playlist.id);\n return write([playlist, ...others]);\n },\n remove(id) {\n return write(read().filter((existing) => existing.id !== id));\n },\n };\n}\n","/**\n * @fileoverview The interactive planner: from a learning goal, ask a few\n * follow-up questions, then turn the goal and the answers into searches and\n * the searches into a custom playlist.\n *\n * The language model and the web search are injected, never imported, so the\n * package stays provider-neutral and the host decides which model pays for\n * it. With neither, the planner still works (\"offline\" mode): fixed questions,\n * keyword searches over the bundled catalog. That is also the fallback when a\n * model call fails or answers with something unparseable, so a learner never\n * gets an error where a reasonable playlist was possible.\n */\nimport type { EducationCatalog, PlanAnswer, Playlist, PlaylistItem, CourseLevel } from '../types';\nimport { catalogItems, getDefaultCatalog } from '../catalog';\nimport { searchCatalog, tokenize } from '../lib/search';\nimport { createPlaylist } from '../lib/playlists';\nimport { safeUrl } from '../lib/sanitize';\n\nexport const MAX_GOAL_LENGTH = 300;\nexport const MAX_ANSWERS = 8;\nexport const MAX_ANSWER_LENGTH = 300;\nexport const MAX_PLAN_ITEMS = 12;\nconst MAX_SEARCHES = 5;\nconst WEB_RESULTS_PER_SEARCH = 3;\n/** A web result's length is unknown; this is what it is budgeted at. */\nconst WEB_ITEM_MINUTES = 60;\n\nexport interface FollowUpQuestion {\n id: string;\n question: string;\n /** Quick answers; the learner can always type their own instead. */\n options: string[];\n}\n\nexport interface WebSearchHit {\n title: string;\n url: string;\n snippet?: string;\n}\n\nexport interface PlannerDeps {\n /** Runs a language model: a system and a user prompt in, its text out. */\n generate?: (input: { system: string; prompt: string }) => Promise<string>;\n /** Web search for material outside the catalog. */\n search?: (query: string) => Promise<WebSearchHit[]>;\n catalog?: EducationCatalog;\n}\n\nexport interface PlanRequest {\n goal: string;\n answers?: PlanAnswer[];\n}\n\nexport type PlannerMode = 'llm' | 'offline';\n\nexport interface FollowUpsResult {\n questions: FollowUpQuestion[];\n mode: PlannerMode;\n}\n\nexport interface PlanResult {\n playlist: Playlist;\n /** The searches the playlist was assembled from, shown so the learner can see why. */\n searches: string[];\n mode: PlannerMode;\n /** Weekly time the answers implied, when they named one. */\n minutesPerWeek?: number;\n}\n\n/** The questions asked when there is no model, or the model's were unusable. */\nexport const DEFAULT_FOLLOW_UPS: FollowUpQuestion[] = [\n { id: 'level', question: 'Where are you starting from?', options: ['New to this', 'Some background', 'Comfortable, want depth'] },\n { id: 'time', question: 'How much time can you give it each week?', options: ['2 hours', '5 hours', '10 hours'] },\n { id: 'format', question: 'What do you learn best from?', options: ['Lecture videos', 'Problem sets and notes', 'A mix'] },\n { id: 'outcome', question: 'What should you be able to do at the end?', options: ['Pass a course or exam', 'Build a project', 'Switch careers', 'Just curious'] },\n];\n\n/** Trims and caps a request; throws when there is no goal to plan from. */\nexport function normalizePlanRequest(raw: unknown): PlanRequest {\n const r = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;\n const goal = typeof r.goal === 'string' ? r.goal.trim().slice(0, MAX_GOAL_LENGTH) : '';\n if (!goal) throw new Error('A learning goal is required');\n const answers = Array.isArray(r.answers)\n ? r.answers\n .slice(0, MAX_ANSWERS)\n .flatMap((entry) => {\n const e = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>;\n const question = typeof e.question === 'string' ? e.question.trim().slice(0, MAX_ANSWER_LENGTH) : '';\n const answer = typeof e.answer === 'string' ? e.answer.trim().slice(0, MAX_ANSWER_LENGTH) : '';\n return question && answer ? [{ question, answer }] : [];\n })\n : [];\n return { goal, answers };\n}\n\n/** The first JSON object in a model's reply, tolerating code fences and chatter around it. */\nexport function extractJson(text: string): unknown {\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start < 0 || end <= start) return null;\n try {\n return JSON.parse(text.slice(start, end + 1));\n } catch {\n return null;\n }\n}\n\nconst FOLLOW_UP_SYSTEM = `You help a self-directed learner plan a study playlist of free university courses and lecture videos.\nAsk 3 or 4 short follow-up questions that would most change which courses to recommend (their starting level, weekly time, preferred format, and what they want to be able to do are usually the most useful).\nGive each question 2 to 4 short answer options. The learner can also type their own answer.\nReply with JSON only, in this shape: {\"questions\":[{\"question\":\"...\",\"options\":[\"...\",\"...\"]}]}`;\n\nfunction parseFollowUps(text: string): FollowUpQuestion[] | null {\n const json = extractJson(text) as { questions?: unknown } | null;\n if (!json || !Array.isArray(json.questions)) return null;\n const questions = json.questions\n .slice(0, 4)\n .flatMap((entry, index) => {\n const e = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>;\n const question = typeof e.question === 'string' ? e.question.trim().slice(0, 200) : '';\n if (!question) return [];\n const options = Array.isArray(e.options)\n ? e.options.filter((o): o is string => typeof o === 'string' && o.trim() !== '').map((o) => o.trim().slice(0, 80)).slice(0, 4)\n : [];\n return [{ id: `q${index + 1}`, question, options }];\n });\n return questions.length ? questions : null;\n}\n\n/** Follow-up questions for a goal: the model's, or the defaults. */\nexport async function suggestFollowUps(goal: string, deps: PlannerDeps = {}): Promise<FollowUpsResult> {\n const { goal: cleanGoal } = normalizePlanRequest({ goal });\n if (deps.generate) {\n try {\n const text = await deps.generate({ system: FOLLOW_UP_SYSTEM, prompt: `Learning goal: ${cleanGoal}` });\n const questions = parseFollowUps(text);\n if (questions) return { questions, mode: 'llm' };\n } catch {\n // Fall through to the defaults below.\n }\n }\n return { questions: DEFAULT_FOLLOW_UPS, mode: 'offline' };\n}\n\nconst PLAN_SYSTEM = `You plan a self-paced study playlist from free university courseware (MIT OpenCourseWare first) and lecture videos.\nFrom the learner's goal and answers, write 3 to 5 short search queries (a few words each, the way you would search a course catalog) that together cover what they need, in study order.\nIf you know MIT course numbers that fit (for example 6.0001 or 18.06), list them too.\nReply with JSON only, in this shape: {\"title\":\"...\",\"searches\":[\"...\"],\"courses\":[\"6.0001\"]}`;\n\ninterface ModelPlan {\n title?: string;\n searches: string[];\n courses: string[];\n}\n\nfunction parseModelPlan(text: string): ModelPlan | null {\n const json = extractJson(text) as Record<string, unknown> | null;\n if (!json || !Array.isArray(json.searches)) return null;\n const strings = (value: unknown, max: number) =>\n Array.isArray(value)\n ? value.filter((v): v is string => typeof v === 'string' && v.trim() !== '').map((v) => v.trim().slice(0, max))\n : [];\n const searches = strings(json.searches, 120).slice(0, MAX_SEARCHES);\n if (searches.length === 0) return null;\n return {\n title: typeof json.title === 'string' && json.title.trim() ? json.title.trim().slice(0, 120) : undefined,\n searches,\n courses: strings(json.courses, 20).slice(0, 10),\n };\n}\n\n/** What the answers say about level, format and pace, read from their words. */\nexport function readPreferences(answers: PlanAnswer[] = []): {\n maxLevel?: CourseLevel;\n preferAdvanced: boolean;\n format: 'videos' | 'courseware' | 'mix';\n minutesPerWeek?: number;\n} {\n const text = answers.map((a) => a.answer.toLowerCase()).join(' | ');\n const beginner = /\\b(new|beginner|none|no background|never|starting out|from scratch)\\b/.test(text);\n const advanced = /\\b(comfortable|depth|advanced|expert|graduate|deep)\\b/.test(text);\n const wantsVideos = /\\bvideo|lecture|watch/.test(text);\n const wantsCourseware = /\\bproblem|notes|reading|exercise|courseware|text/.test(text);\n const hours = text.match(/(\\d+(?:\\.\\d+)?)\\s*(?:\\+\\s*)?(?:h|hr|hrs|hour|hours)\\b/);\n return {\n maxLevel: beginner && !advanced ? 'intermediate' : undefined,\n preferAdvanced: advanced && !beginner,\n format: wantsVideos && !wantsCourseware ? 'videos' : wantsCourseware && !wantsVideos ? 'courseware' : 'mix',\n minutesPerWeek: hours ? Math.round(Number(hours[1]) * 60) : undefined,\n };\n}\n\nconst LEVEL_ORDER: Record<CourseLevel, number> = { introductory: 0, intermediate: 1, advanced: 2 };\n\nfunction webItem(hit: WebSearchHit, query: string, index: number): PlaylistItem | null {\n const url = safeUrl(hit.url);\n const title = typeof hit.title === 'string' ? hit.title.trim().slice(0, 200) : '';\n if (!url || !title) return null;\n const isVideo = /youtube\\.com|youtu\\.be|vimeo\\.com/.test(url);\n return {\n id: `web:${url}`,\n title,\n url,\n kind: isVideo ? 'video' : 'reading',\n minutes: WEB_ITEM_MINUTES,\n estimate: 'rough',\n estimateBasis: 'length unknown; budgeted at 1 h',\n description: hit.snippet?.slice(0, 300),\n provenance: {\n provider: isVideo ? 'youtube' : 'web',\n method: 'llm_search',\n verified: false,\n note: `Found by the planner's web search for \"${query}\" (result ${index + 1}).`,\n },\n };\n}\n\n/** Builds a custom playlist from a goal and the learner's answers. */\nexport async function planPlaylist(rawRequest: PlanRequest, deps: PlannerDeps = {}): Promise<PlanResult> {\n const { goal, answers = [] } = normalizePlanRequest(rawRequest);\n const catalog = deps.catalog ?? getDefaultCatalog();\n const items = catalogItems(catalog);\n const prefs = readPreferences(answers);\n\n let mode: PlannerMode = 'offline';\n let plan: ModelPlan | null = null;\n if (deps.generate) {\n try {\n const prompt = [`Learning goal: ${goal}`, ...answers.map((a) => `Q: ${a.question}\\nA: ${a.answer}`)].join('\\n\\n');\n plan = parseModelPlan(await deps.generate({ system: PLAN_SYSTEM, prompt }));\n if (plan) mode = 'llm';\n } catch {\n plan = null;\n }\n }\n // Offline, the goal and anything the learner typed in their own words are the searches.\n const searches = plan?.searches ?? [\n goal,\n ...answers\n .map((a) => a.answer)\n .filter((answer) => tokenize(answer).length >= 2 && !DEFAULT_FOLLOW_UPS.some((q) => q.options.includes(answer))),\n ].slice(0, MAX_SEARCHES);\n\n const picked = new Map<string, PlaylistItem>();\n const take = (item: PlaylistItem) => {\n if (!picked.has(item.id) && ![...picked.values()].some((p) => p.url === item.url)) picked.set(item.id, item);\n };\n\n // Courses the model named, when the catalog has them, lead: they are its most specific advice.\n for (const number of plan?.courses ?? []) {\n for (const item of items) if (item.courseNumber?.toLowerCase() === number.toLowerCase()) take(item);\n }\n for (const query of searches) for (const item of searchCatalog(items, query, 6)) take(item);\n\n if (deps.search) {\n for (const query of searches.slice(0, 3)) {\n try {\n const hits = await deps.search(`${query} free course lecture videos`);\n hits.slice(0, WEB_RESULTS_PER_SEARCH).forEach((hit, index) => {\n const item = webItem(hit, query, index);\n if (item) take(item);\n });\n } catch {\n // One failed search costs its results, not the playlist.\n }\n }\n }\n\n const chosen = [...picked.values()]\n .filter((item) => !prefs.maxLevel || !item.level || LEVEL_ORDER[item.level] <= LEVEL_ORDER[prefs.maxLevel])\n .filter((item) => {\n if (prefs.format === 'videos') return item.kind === 'video' || item.kind === 'video_playlist';\n if (prefs.format === 'courseware') return item.kind !== 'video' && item.kind !== 'video_playlist';\n return true;\n })\n .map((item, order) => ({ item, order }))\n // Study order: easier material first, otherwise the order the searches found it in.\n .sort((a, b) => {\n const la = a.item.level ? LEVEL_ORDER[a.item.level] : 1;\n const lb = b.item.level ? LEVEL_ORDER[b.item.level] : 1;\n return (prefs.preferAdvanced ? lb - la : la - lb) || a.order - b.order;\n })\n .slice(0, MAX_PLAN_ITEMS)\n .map((entry) => entry.item);\n\n const playlist = createPlaylist({\n title: plan?.title ?? `My path: ${goal.slice(0, 80)}`,\n description: `Planned from: ${goal}`,\n items: chosen,\n plannedFrom: { goal, answers },\n });\n return { playlist, searches, mode, minutesPerWeek: prefs.minutesPerWeek };\n}\n","/**\n * @fileoverview Server half of the planner: one request handler a host mounts\n * on its own route, with the language model and web search passed in.\n *\n * - `POST <base>/questions` `{ goal }` → `{ questions, mode }`\n * - `POST <base>` `{ goal, answers }` → `{ playlist, searches, mode, minutesPerWeek? }`\n * - `GET <base>/catalog` → the bundled catalog\n *\n * Plain `Request`/`Response`, no DOM and no React, so it runs on Workers,\n * Node 18+, Bun and Deno. Bodies are capped and re-validated here; the\n * widget's own limits are a courtesy, not a check.\n */\nimport { getDefaultCatalog } from '../catalog';\nimport { normalizePlanRequest, planPlaylist, suggestFollowUps, type PlannerDeps } from '../planner';\n\nexport type { PlannerDeps, WebSearchHit, PlanResult, FollowUpsResult, FollowUpQuestion, PlanRequest } from '../planner';\n\n/** Request bodies larger than this are refused before parsing. */\nexport const MAX_PLAN_BODY_BYTES = 8 * 1024;\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json; charset=utf-8' } });\n\nasync function readBody(request: Request): Promise<unknown> {\n const text = await request.text();\n if (text.length > MAX_PLAN_BODY_BYTES) throw new RangeError('Request body too large');\n return text ? JSON.parse(text) : {};\n}\n\n/**\n * Answers one planner request. The host owns everything around it: auth, rate\n * limiting (each POST can cost a model call and a few searches), and CORS.\n */\nexport async function handleEducationPlaylistsRequest(request: Request, deps: PlannerDeps = {}): Promise<Response> {\n const path = new URL(request.url).pathname.replace(/\\/+$/, '');\n\n if (request.method === 'GET') {\n if (path.endsWith('/catalog')) return json(deps.catalog ?? getDefaultCatalog());\n return json({ error: 'Not found' }, 404);\n }\n if (request.method !== 'POST') return json({ error: 'Method not allowed' }, 405);\n\n let body: unknown;\n try {\n body = await readBody(request);\n } catch (error) {\n return json({ error: error instanceof RangeError ? error.message : 'Body must be JSON' }, error instanceof RangeError ? 413 : 400);\n }\n\n let planRequest;\n try {\n planRequest = normalizePlanRequest(body);\n } catch (error) {\n return json({ error: (error as Error).message }, 400);\n }\n\n if (path.endsWith('/questions')) return json(await suggestFollowUps(planRequest.goal, deps));\n return json(await planPlaylist(planRequest, deps));\n}\n"],"mappings":"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","names":["json"]}
|