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
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# education-playlists
|
|
2
|
+
|
|
3
|
+
Self-paced study playlists organized like a college catalog: pick a
|
|
4
|
+
**category**, a **major** and a **program**, then work through an ordered list
|
|
5
|
+
of lecture videos and courseware links, checking items off as you go. Every
|
|
6
|
+
item shows how long it should take and where its record came from.
|
|
7
|
+
|
|
8
|
+
An AI planner builds custom playlists: describe what you want to learn, answer
|
|
9
|
+
a few follow-up questions (tap an option or type your own answer), and it turns
|
|
10
|
+
the answers into searches and the searches into a playlist.
|
|
11
|
+
|
|
12
|
+
- **Preset, public and private playlists.** Presets ship with the catalog.
|
|
13
|
+
Save a copy to make it yours; keep it private and invite people by email, or
|
|
14
|
+
make it public and share a link.
|
|
15
|
+
- **Progress by time.** Checkmarks persist per browser; progress and "weeks
|
|
16
|
+
left at your pace" are measured in minutes, not item counts.
|
|
17
|
+
- **Provenance on every item.** Seed record, official API, dataset snapshot,
|
|
18
|
+
community list or AI search, and whether the URL has been verified.
|
|
19
|
+
- **Quiz hook.** Pass a `quizGenerator` (for example NotebookLM over the item's
|
|
20
|
+
material) and each item gets a "Quiz me" button.
|
|
21
|
+
|
|
22
|
+
The bundled catalog is a hand-curated seed of 28 MIT OpenCourseWare courses in
|
|
23
|
+
11 playlists. Course pages link to their canonical `ocw.mit.edu/courses/…`
|
|
24
|
+
address; until per-lecture video data is ingested, a course's "lecture videos"
|
|
25
|
+
item searches MIT OCW's YouTube channel.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { EducationPlaylists } from 'education-playlists';
|
|
31
|
+
|
|
32
|
+
// Homepage card: one row of playlists that expands in place.
|
|
33
|
+
<EducationPlaylists compact planEndpoint="/api/learn" openHref="/learn" />
|
|
34
|
+
|
|
35
|
+
// Full page, which also opens playlists shared by link (#playlist=…).
|
|
36
|
+
<EducationPlaylists planEndpoint="/api/learn" importFromHash />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Without `planEndpoint` the planner runs in the browser: standard follow-up
|
|
40
|
+
questions and keyword search over the bundled catalog.
|
|
41
|
+
|
|
42
|
+
## Server
|
|
43
|
+
|
|
44
|
+
`education-playlists/server` is a plain `Request → Response` handler (Workers,
|
|
45
|
+
Node 18+, Bun, Deno). Pass it a language model and, optionally, a web search:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { handleEducationPlaylistsRequest } from 'education-playlists/server';
|
|
49
|
+
|
|
50
|
+
export const POST = (request: Request) =>
|
|
51
|
+
handleEducationPlaylistsRequest(request, {
|
|
52
|
+
generate: async ({ system, prompt }) => callYourModel(system, prompt),
|
|
53
|
+
search: async (query) => searchVideos(query), // [{ title, url, snippet? }]
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `POST <base>/questions` `{ goal }` → `{ questions, mode }`
|
|
58
|
+
- `POST <base>` `{ goal, answers: [{ question, answer }] }` → `{ playlist, searches, mode, minutesPerWeek? }`
|
|
59
|
+
- `GET <base>/catalog` → the catalog
|
|
60
|
+
|
|
61
|
+
If the model fails or answers with something unusable, the handler plans
|
|
62
|
+
offline rather than erroring; `mode` says which happened. Rate limiting and
|
|
63
|
+
auth are the host's job.
|
|
64
|
+
|
|
65
|
+
## Data sources
|
|
66
|
+
|
|
67
|
+
Planned ingestion, in order of authority:
|
|
68
|
+
|
|
69
|
+
1. [`mitodl/ocw_oer_export`](https://github.com/mitodl/ocw_oer_export) over the
|
|
70
|
+
MIT Learn API, for the full MIT OCW catalog.
|
|
71
|
+
2. The MIT OpenCourseWare YouTube course dataset (Kaggle), for per-lecture
|
|
72
|
+
video URLs and exact durations.
|
|
73
|
+
3. Curated awesome-lists, kept as community-sourced playlists.
|
|
74
|
+
|
|
75
|
+
## Not built yet
|
|
76
|
+
|
|
77
|
+
- Server-side storage for playlists, so invites actually reach invitees and
|
|
78
|
+
private playlists follow a signed-in user across devices. The access rules
|
|
79
|
+
(`canView`, `canEdit`, `invite`, `acceptInvite`) are in place for it.
|
|
80
|
+
- The ingestion adapters above.
|
|
81
|
+
- A concrete quiz generator (the hook and prompt are here).
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bun run test
|
|
85
|
+
bun run build
|
|
86
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Browser side of the planner: call the host's endpoint when it
|
|
3
|
+
* has one, and plan locally when it does not or when the call fails.
|
|
4
|
+
*/
|
|
5
|
+
import type { PlanAnswer } from '../types';
|
|
6
|
+
import { type FollowUpsResult, type PlanResult } from '../planner';
|
|
7
|
+
/** Follow-up questions from `endpoint`, or the local defaults. */
|
|
8
|
+
export declare function fetchFollowUps(goal: string, endpoint?: string, signal?: AbortSignal): Promise<FollowUpsResult>;
|
|
9
|
+
/** A planned playlist from `endpoint`, or one planned locally from the catalog. */
|
|
10
|
+
export declare function fetchPlan(goal: string, answers: PlanAnswer[], endpoint?: string, signal?: AbortSignal): Promise<PlanResult>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The catalog as the widget and the server read it: the
|
|
3
|
+
* taxonomy plus the preset playlists, and the lookups over them.
|
|
4
|
+
*/
|
|
5
|
+
import type { EducationCatalog, Playlist, PlaylistItem } from '../types';
|
|
6
|
+
export { EDUCATION_CATEGORIES } from './taxonomy';
|
|
7
|
+
export { MIT_OCW_PRESET_PLAYLISTS, MIT_OCW_SEED_COURSES } from './mit-ocw-seed';
|
|
8
|
+
export type { SeedCourse } from './mit-ocw-seed';
|
|
9
|
+
/** The bundled catalog: every category, and every preset playlist. */
|
|
10
|
+
export declare function getDefaultCatalog(): EducationCatalog;
|
|
11
|
+
/** Narrows playlists to a category, major and program; any of them may be left out. */
|
|
12
|
+
export declare function filterPlaylists(playlists: Playlist[], filter: {
|
|
13
|
+
categoryId?: string;
|
|
14
|
+
majorId?: string;
|
|
15
|
+
programId?: string;
|
|
16
|
+
}): Playlist[];
|
|
17
|
+
/** Every distinct item in the catalog, first occurrence wins. */
|
|
18
|
+
export declare function catalogItems(catalog: EducationCatalog): PlaylistItem[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The bundled seed catalog: MIT OpenCourseWare courses, grouped
|
|
3
|
+
* the way MIT groups them (by department), into one preset playlist per
|
|
4
|
+
* program.
|
|
5
|
+
*
|
|
6
|
+
* This is a hand-curated starting point, not an ingestion. Each course page
|
|
7
|
+
* URL is the canonical `ocw.mit.edu/courses/<slug>/` address, and every item
|
|
8
|
+
* says so in its provenance (`curated_seed`, `verified: false`) until an
|
|
9
|
+
* ingestion run replaces it. The intended sources, in order of authority:
|
|
10
|
+
*
|
|
11
|
+
* 1. `mitodl/ocw_oer_export` — MIT's own exporter over the MIT Learn API:
|
|
12
|
+
* title, canonical URL, level, description, topics, instructors, term.
|
|
13
|
+
* 2. The MIT OCW YouTube course dataset (Kaggle, `jorgoose`) — course playlist
|
|
14
|
+
* and per-lecture video URLs, for real `video` items with exact durations.
|
|
15
|
+
* 3. Community awesome-lists, kept as `community_list` items in their own
|
|
16
|
+
* playlists rather than merged into the official records.
|
|
17
|
+
*
|
|
18
|
+
* Until (2) is ingested, a course's "lecture videos" item searches MIT OCW's
|
|
19
|
+
* own YouTube channel for the course instead of guessing a playlist id.
|
|
20
|
+
*
|
|
21
|
+
* Time estimates are rough on purpose: lecture count × typical session length
|
|
22
|
+
* for videos, and the term's problem-set/reading load for courseware.
|
|
23
|
+
*/
|
|
24
|
+
import type { CourseLevel, Playlist } from '../types';
|
|
25
|
+
/** One OCW course and the numbers its time estimates are derived from. */
|
|
26
|
+
export interface SeedCourse {
|
|
27
|
+
number: string;
|
|
28
|
+
title: string;
|
|
29
|
+
/** The path segment after `/courses/`. */
|
|
30
|
+
slug: string;
|
|
31
|
+
level: CourseLevel;
|
|
32
|
+
/** Recorded lectures; 0 for a course that publishes no lecture video. */
|
|
33
|
+
lectures: number;
|
|
34
|
+
lectureMinutes: number;
|
|
35
|
+
/** Hours of problem sets, readings and exams over the term. */
|
|
36
|
+
studyHours: number;
|
|
37
|
+
description: string;
|
|
38
|
+
}
|
|
39
|
+
/** The seed courses, keyed by course number. */
|
|
40
|
+
export declare const MIT_OCW_SEED_COURSES: Record<string, SeedCourse>;
|
|
41
|
+
export declare const MIT_OCW_PRESET_PLAYLISTS: Playlist[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The catalog's shape, borrowed from a university: broad
|
|
3
|
+
* categories (schools), majors (departments) and programs (tracks within a
|
|
4
|
+
* major). Majors carry their MIT department number so ingested MIT records,
|
|
5
|
+
* which are filed by department, land in the right place.
|
|
6
|
+
*/
|
|
7
|
+
import type { Category } from '../types';
|
|
8
|
+
export declare const EDUCATION_CATEGORIES: Category[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The Education Playlists widget: browse playlists by category,
|
|
3
|
+
* major and program; open one to work through it at your own pace; keep your
|
|
4
|
+
* own; or plan a custom one with the AI planner.
|
|
5
|
+
*
|
|
6
|
+
* `compact` is the homepage card: a single row of playlists that expands in
|
|
7
|
+
* place into the full widget, like the trending-news card beside it. Without
|
|
8
|
+
* `compact` it is the full-page view.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import type { EducationCatalog } from '../types';
|
|
12
|
+
import type { QuizGenerator } from '../quiz';
|
|
13
|
+
export interface EducationPlaylistsProps {
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: React.CSSProperties;
|
|
16
|
+
/** The homepage card: one row of playlists that expands in place. */
|
|
17
|
+
compact?: boolean;
|
|
18
|
+
/** Catalog to browse; defaults to the bundled MIT OpenCourseWare seed. */
|
|
19
|
+
catalog?: EducationCatalog;
|
|
20
|
+
/**
|
|
21
|
+
* The host's planner endpoint (see `education-playlists/server`). Without
|
|
22
|
+
* one the planner runs locally: standard questions, catalog keyword search.
|
|
23
|
+
*/
|
|
24
|
+
planEndpoint?: string;
|
|
25
|
+
/** Full-page view to link to from the compact card, e.g. `/learn`. */
|
|
26
|
+
openHref?: string;
|
|
27
|
+
/** Page a share link opens; defaults to `openHref`, then the current page. */
|
|
28
|
+
shareBaseHref?: string;
|
|
29
|
+
/** Open a playlist carried in the page's `#playlist=` fragment. */
|
|
30
|
+
importFromHash?: boolean;
|
|
31
|
+
/** Signed-in user, recorded as the owner of playlists they create. */
|
|
32
|
+
userId?: string;
|
|
33
|
+
/** Turns on "Quiz me" for each item (NotebookLM or another quiz source). */
|
|
34
|
+
quizGenerator?: QuizGenerator;
|
|
35
|
+
}
|
|
36
|
+
export declare function EducationPlaylists({ className, style, compact, catalog: catalogProp, planEndpoint, openHref, shareBaseHref, importFromHash, userId, quizGenerator, }: EducationPlaylistsProps): React.JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The "plan with AI" flow: a goal, then follow-up questions the
|
|
3
|
+
* learner answers by tapping an option or typing their own words, then the
|
|
4
|
+
* planned playlist.
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import type { PlanResult } from '../planner';
|
|
8
|
+
export interface PlaylistPlannerProps {
|
|
9
|
+
/** The host's planner endpoint; without one, planning runs locally over the catalog. */
|
|
10
|
+
planEndpoint?: string;
|
|
11
|
+
onPlanned: (result: PlanResult) => void;
|
|
12
|
+
initialGoal?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function PlaylistPlanner({ planEndpoint, onPlanned, initialGoal }: PlaylistPlannerProps): React.JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview One playlist, opened: its items with checkmarks and time
|
|
3
|
+
* estimates, overall progress, and — for a playlist the learner owns — the
|
|
4
|
+
* sharing controls.
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import type { Playlist } from '../types';
|
|
8
|
+
import type { QuizGenerator } from '../quiz';
|
|
9
|
+
export interface PlaylistViewProps {
|
|
10
|
+
playlist: Playlist;
|
|
11
|
+
done: ReadonlySet<string>;
|
|
12
|
+
onToggle: (itemId: string) => void;
|
|
13
|
+
/** Present when the learner may edit this playlist. */
|
|
14
|
+
onChange?: (playlist: Playlist) => void;
|
|
15
|
+
/** Saves a private copy into "My playlists" (for presets, shared links and fresh plans). */
|
|
16
|
+
onSaveCopy?: (playlist: Playlist) => void;
|
|
17
|
+
/** Weekly pace, for "about N weeks left". */
|
|
18
|
+
minutesPerWeek?: number;
|
|
19
|
+
/** Page a share link opens, e.g. `https://example.com/learn`. */
|
|
20
|
+
shareBaseHref?: string;
|
|
21
|
+
quizGenerator?: QuizGenerator;
|
|
22
|
+
}
|
|
23
|
+
export declare function PlaylistView({ playlist, done, onToggle, onChange, onSaveCopy, minutesPerWeek, shareBaseHref, quizGenerator }: PlaylistViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Inline styles for the widget. Colors are translucent greys
|
|
3
|
+
* over `currentColor`, so the widget takes on whatever light or dark surface
|
|
4
|
+
* the host puts it on, the way the news and weather widgets do.
|
|
5
|
+
*/
|
|
6
|
+
import type { CSSProperties } from 'react';
|
|
7
|
+
export declare const s: {
|
|
8
|
+
readonly root: CSSProperties;
|
|
9
|
+
readonly headerRow: CSSProperties;
|
|
10
|
+
readonly label: CSSProperties;
|
|
11
|
+
readonly muted: CSSProperties;
|
|
12
|
+
readonly row: CSSProperties;
|
|
13
|
+
readonly scrollRow: CSSProperties;
|
|
14
|
+
readonly chip: CSSProperties;
|
|
15
|
+
readonly chipActive: CSSProperties;
|
|
16
|
+
readonly button: CSSProperties;
|
|
17
|
+
readonly primary: CSSProperties;
|
|
18
|
+
readonly iconButton: CSSProperties;
|
|
19
|
+
readonly input: CSSProperties;
|
|
20
|
+
readonly card: CSSProperties;
|
|
21
|
+
readonly compactCard: CSSProperties;
|
|
22
|
+
readonly title: CSSProperties;
|
|
23
|
+
readonly bar: CSSProperties;
|
|
24
|
+
readonly barFill: CSSProperties;
|
|
25
|
+
readonly list: CSSProperties;
|
|
26
|
+
readonly item: CSSProperties;
|
|
27
|
+
readonly link: CSSProperties;
|
|
28
|
+
readonly badge: CSSProperties;
|
|
29
|
+
readonly section: CSSProperties;
|
|
30
|
+
};
|