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 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/catalog/taxonomy.ts","../src/catalog/mit-ocw-seed.ts","../src/catalog/index.ts","../src/lib/time.ts","../src/lib/progress.ts","../src/lib/sanitize.ts","../src/lib/playlists.ts","../src/lib/sharing.ts","../src/lib/search.ts","../src/planner/index.ts","../src/quiz.ts","../src/api/client.ts","../src/components/EducationPlaylists.tsx","../src/components/PlaylistView.tsx","../src/components/styles.ts","../src/components/PlaylistPlanner.tsx"],"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 Time arithmetic for self-paced playlists: totals, what is\n * left once some items are checked off, and how it reads on screen.\n */\nimport type { Playlist } from '../types';\n\n/**\n * \"45 min\", \"2 h 30 min\", \"176 h\". From ten hours up the minutes are dropped:\n * most estimates are rough, and \"175 h 50 min\" claims a precision they lack.\n */\nexport function formatMinutes(minutes: number): string {\n const total = Math.max(0, Math.round(minutes));\n if (total < 60) return `${total} min`;\n if (total >= 600) return `${Math.round(total / 60)} h`;\n const hours = Math.floor(total / 60);\n const rest = total % 60;\n return rest ? `${hours} h ${rest} min` : `${hours} h`;\n}\n\nexport interface PlaylistTime {\n totalMinutes: number;\n doneMinutes: number;\n remainingMinutes: number;\n /** 0–100, by time rather than by item count, since items vary by tens of hours. */\n percent: number;\n doneCount: number;\n itemCount: number;\n}\n\nexport function playlistTime(playlist: Playlist, done: ReadonlySet<string>): PlaylistTime {\n let totalMinutes = 0;\n let doneMinutes = 0;\n let doneCount = 0;\n for (const item of playlist.items) {\n totalMinutes += item.minutes;\n if (done.has(item.id)) {\n doneMinutes += item.minutes;\n doneCount += 1;\n }\n }\n return {\n totalMinutes,\n doneMinutes,\n remainingMinutes: totalMinutes - doneMinutes,\n percent: totalMinutes ? Math.round((doneMinutes / totalMinutes) * 100) : 0,\n doneCount,\n itemCount: playlist.items.length,\n };\n}\n\n/** Weeks to finish what is left at a weekly pace; `null` with no pace. */\nexport function weeksToFinish(remainingMinutes: number, minutesPerWeek?: number): number | null {\n if (!minutesPerWeek || minutesPerWeek <= 0) return null;\n return Math.ceil(remainingMinutes / minutesPerWeek);\n}\n","/**\n * @fileoverview Which items a learner has checked off, kept per browser.\n *\n * Progress is keyed by item id, not by playlist, so finishing 18.06 in the\n * \"Linear Algebra and Probability\" playlist also ticks it in a custom playlist\n * that includes it. Storage access is wrapped: a private window or blocked\n * site data must leave a working (if forgetful) widget, not a thrown error.\n */\n\nexport const PROGRESS_STORAGE_KEY = 'educationPlaylists.progress';\n\nexport interface ProgressStore {\n load(): Set<string>;\n save(done: ReadonlySet<string>): void;\n}\n\n/** A store over `localStorage`, or in memory where there is none. */\nexport function createProgressStore(key = PROGRESS_STORAGE_KEY): ProgressStore {\n let memory = new Set<string>();\n const storage = (): Storage | null => {\n try {\n return typeof localStorage === 'undefined' ? null : localStorage;\n } catch {\n return null;\n }\n };\n return {\n load() {\n try {\n const raw = storage()?.getItem(key);\n if (!raw) return new Set(memory);\n const parsed: unknown = JSON.parse(raw);\n return Array.isArray(parsed) ? new Set(parsed.filter((id): id is string => typeof id === 'string')) : new Set();\n } catch {\n return new Set(memory);\n }\n },\n save(done) {\n memory = new Set(done);\n try {\n storage()?.setItem(key, JSON.stringify([...done]));\n } catch {\n // Quota or blocked storage: the in-memory copy still serves this page.\n }\n },\n };\n}\n\n/** The set with `id` toggled; never mutates the input. */\nexport function toggleDone(done: ReadonlySet<string>, id: string): Set<string> {\n const next = new Set(done);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\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 Who can see and change a playlist, invitations, and share\n * links.\n *\n * Access rules are pure functions so the server and the widget agree on them.\n * The widget's own checks are for display only; a server that stores\n * playlists must run `canView`/`canEdit` itself before answering.\n */\nimport type { Playlist, PlaylistMember, PlaylistRole } from '../types';\nimport { sanitizePlaylist } from './sanitize';\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nfunction activeMember(playlist: Playlist, userId: string | undefined): PlaylistMember | undefined {\n if (!userId) return undefined;\n return playlist.members?.find((member) => member.status === 'active' && member.userId === userId);\n}\n\nexport function canView(playlist: Playlist, userId?: string): boolean {\n if (playlist.visibility !== 'private') return true;\n if (userId && playlist.ownerId === userId) return true;\n return Boolean(activeMember(playlist, userId));\n}\n\nexport function canEdit(playlist: Playlist, userId?: string): boolean {\n if (playlist.visibility === 'preset') return false;\n if (userId && playlist.ownerId === userId) return true;\n const member = activeMember(playlist, userId);\n return member?.role === 'owner' || member?.role === 'editor';\n}\n\nconst defaultToken = (): string =>\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n\n/**\n * The playlist with `email` invited at `role`. Re-inviting an address updates\n * its role rather than adding a second row. Throws on a malformed address or\n * an attempt to invite a second owner.\n */\nexport function invite(\n playlist: Playlist,\n email: string,\n role: Exclude<PlaylistRole, 'owner'>,\n makeToken: () => string = defaultToken,\n): Playlist {\n const address = email.trim().toLowerCase();\n if (!EMAIL.test(address)) throw new Error(`Not an email address: ${email}`);\n if ((role as PlaylistRole) === 'owner') throw new Error('A playlist has exactly one owner');\n const members = [...(playlist.members ?? [])];\n const existing = members.findIndex((member) => member.email === address);\n if (existing >= 0) {\n members[existing] = { ...members[existing], role };\n } else {\n members.push({ email: address, role, status: 'invited', inviteToken: makeToken() });\n }\n return { ...playlist, members };\n}\n\n/** The playlist with the invite behind `token` accepted by `userId`, or `null` for an unknown token. */\nexport function acceptInvite(playlist: Playlist, token: string, userId: string): Playlist | null {\n const members = playlist.members ?? [];\n const index = members.findIndex((member) => member.inviteToken === token && member.status === 'invited');\n if (index < 0) return null;\n const next = [...members];\n next[index] = { ...next[index], userId, status: 'active', inviteToken: undefined };\n return { ...playlist, members: next };\n}\n\nexport function removeMember(playlist: Playlist, email: string): Playlist {\n const address = email.trim().toLowerCase();\n return { ...playlist, members: (playlist.members ?? []).filter((member) => member.email !== address) };\n}\n\nconst toBase64Url = (value: string): string => {\n const bytes = new TextEncoder().encode(value);\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n};\n\nconst fromBase64Url = (value: string): string => {\n const padded = value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=');\n const binary = atob(padded);\n return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));\n};\n\n/** Hash parameter a share link carries its playlist in. */\nexport const SHARE_HASH_PARAM = 'playlist';\n\n/**\n * A share fragment (`playlist=<base64url>`) carrying a public copy of the\n * playlist. The whole playlist rides in the URL fragment, which browsers never\n * send to a server, so sharing works with no storage behind it. Private\n * playlists are refused: those are shared by invitation, not by link.\n */\nexport function encodeShareFragment(playlist: Playlist): string {\n if (playlist.visibility === 'private') throw new Error('Private playlists are shared by invitation');\n const { members: _members, ownerId: _ownerId, plannedFrom: _plannedFrom, ...shareable } = playlist;\n return `${SHARE_HASH_PARAM}=${toBase64Url(JSON.stringify({ ...shareable, visibility: 'public' }))}`;\n}\n\n/** The playlist in a share fragment (with or without the leading `#`), or `null`. */\nexport function decodeShareFragment(fragment: string): Playlist | null {\n const params = new URLSearchParams(fragment.replace(/^#/, ''));\n const encoded = params.get(SHARE_HASH_PARAM);\n if (!encoded) return null;\n try {\n return sanitizePlaylist(JSON.parse(fromBase64Url(encoded)));\n } catch {\n return null;\n }\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 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 The quiz hook: the shape of a NotebookLM-style quiz over a\n * playlist item, and the prompt that asks for one.\n *\n * This is a stub on purpose. Generating a grounded quiz needs the item's\n * material loaded as a source (a NotebookLM notebook, or an extracted\n * transcript/PDF), and that belongs to the host — this repo's\n * `notebooklm-api-client` or the web app's `/api/notebooklm` routes. The\n * widget shows a Quiz button only when the host passes a `QuizGenerator`.\n */\nimport type { PlaylistItem } from './types';\n\nexport interface QuizQuestion {\n prompt: string;\n choices: string[];\n /** Index into `choices`. */\n answerIndex: number;\n explanation?: string;\n}\n\nexport interface Quiz {\n itemId: string;\n title: string;\n questions: QuizQuestion[];\n /** Which engine wrote it, so a learner can weigh it. */\n source: 'notebooklm' | 'llm';\n}\n\n/** Supplied by the host; resolves to a quiz grounded in the item's material. */\nexport type QuizGenerator = (item: PlaylistItem, options?: { questionCount?: number }) => Promise<Quiz>;\n\n/**\n * The instruction to send alongside the item's material — to NotebookLM's\n * \"ask\" with the item added as a source, or to a model with the extracted text.\n */\nexport function buildQuizPrompt(item: PlaylistItem, questionCount = 5): string {\n return [\n `Write a ${questionCount}-question multiple-choice quiz on \"${item.title}\"${item.courseNumber ? ` (${[item.institution, item.courseNumber].filter(Boolean).join(' ')})` : ''}.`,\n 'Use only the attached source material. Each question has 4 choices and exactly one correct answer, with a one-sentence explanation citing the material.',\n 'Reply with JSON only: {\"questions\":[{\"prompt\":\"...\",\"choices\":[\"...\",\"...\",\"...\",\"...\"],\"answerIndex\":0,\"explanation\":\"...\"}]}',\n ].join('\\n');\n}\n\n/** Validates a quiz a generator produced; drops malformed questions. */\nexport function parseQuiz(item: PlaylistItem, raw: unknown, source: Quiz['source']): Quiz | null {\n const r = (raw && typeof raw === 'object' ? raw : {}) as { questions?: unknown };\n if (!Array.isArray(r.questions)) return null;\n const questions = r.questions.flatMap((entry) => {\n const q = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>;\n const choices = Array.isArray(q.choices) ? q.choices.filter((c): c is string => typeof c === 'string') : [];\n const answerIndex = Number(q.answerIndex);\n if (typeof q.prompt !== 'string' || choices.length < 2 || !Number.isInteger(answerIndex) || answerIndex < 0 || answerIndex >= choices.length) return [];\n return [{ prompt: q.prompt, choices, answerIndex, explanation: typeof q.explanation === 'string' ? q.explanation : undefined }];\n });\n return questions.length ? { itemId: item.id, title: `Quiz: ${item.title}`, questions, source } : null;\n}\n","/**\n * @fileoverview Browser side of the planner: call the host's endpoint when it\n * has one, and plan locally when it does not or when the call fails.\n */\nimport type { PlanAnswer } from '../types';\nimport { planPlaylist, suggestFollowUps, type FollowUpsResult, type PlanResult } from '../planner';\nimport { sanitizePlaylist } from '../lib/sanitize';\n\nasync function post<T>(url: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n signal,\n });\n if (!res.ok) throw new Error(`Planner request failed (${res.status})`);\n return (await res.json()) as T;\n}\n\n/** Follow-up questions from `endpoint`, or the local defaults. */\nexport async function fetchFollowUps(goal: string, endpoint?: string, signal?: AbortSignal): Promise<FollowUpsResult> {\n if (endpoint) {\n try {\n const result = await post<FollowUpsResult>(`${endpoint.replace(/\\/+$/, '')}/questions`, { goal }, signal);\n if (Array.isArray(result?.questions) && result.questions.length) return result;\n } catch (error) {\n if ((error as Error).name === 'AbortError') throw error;\n }\n }\n return suggestFollowUps(goal);\n}\n\n/** A planned playlist from `endpoint`, or one planned locally from the catalog. */\nexport async function fetchPlan(\n goal: string,\n answers: PlanAnswer[],\n endpoint?: string,\n signal?: AbortSignal,\n): Promise<PlanResult> {\n if (endpoint) {\n try {\n const result = await post<PlanResult>(endpoint.replace(/\\/+$/, ''), { goal, answers }, signal);\n // The server's playlist is re-validated like any other outside data.\n const playlist = sanitizePlaylist({ ...result?.playlist, visibility: 'private' });\n if (playlist) {\n return {\n ...result,\n playlist: { ...playlist, visibility: 'private', members: [], plannedFrom: { goal, answers } },\n };\n }\n } catch (error) {\n if ((error as Error).name === 'AbortError') throw error;\n }\n }\n return planPlaylist({ goal, answers });\n}\n","/**\n * @fileoverview The Education Playlists widget: browse playlists by category,\n * major and program; open one to work through it at your own pace; keep your\n * own; or plan a custom one with the AI planner.\n *\n * `compact` is the homepage card: a single row of playlists that expands in\n * place into the full widget, like the trending-news card beside it. Without\n * `compact` it is the full-page view.\n */\nimport React, { useEffect, useMemo, useState } from 'react';\nimport type { EducationCatalog, Playlist } from '../types';\nimport { filterPlaylists, getDefaultCatalog } from '../catalog';\nimport { createProgressStore, toggleDone } from '../lib/progress';\nimport { createLocalPlaylistStore, createPlaylist } from '../lib/playlists';\nimport { decodeShareFragment } from '../lib/sharing';\nimport { formatMinutes, playlistTime } from '../lib/time';\nimport type { PlanResult } from '../planner';\nimport type { QuizGenerator } from '../quiz';\nimport { PlaylistView } from './PlaylistView';\nimport { PlaylistPlanner } from './PlaylistPlanner';\nimport { s } from './styles';\n\nexport interface EducationPlaylistsProps {\n className?: string;\n style?: React.CSSProperties;\n /** The homepage card: one row of playlists that expands in place. */\n compact?: boolean;\n /** Catalog to browse; defaults to the bundled MIT OpenCourseWare seed. */\n catalog?: EducationCatalog;\n /**\n * The host's planner endpoint (see `education-playlists/server`). Without\n * one the planner runs locally: standard questions, catalog keyword search.\n */\n planEndpoint?: string;\n /** Full-page view to link to from the compact card, e.g. `/learn`. */\n openHref?: string;\n /** Page a share link opens; defaults to `openHref`, then the current page. */\n shareBaseHref?: string;\n /** Open a playlist carried in the page's `#playlist=` fragment. */\n importFromHash?: boolean;\n /** Signed-in user, recorded as the owner of playlists they create. */\n userId?: string;\n /** Turns on \"Quiz me\" for each item (NotebookLM or another quiz source). */\n quizGenerator?: QuizGenerator;\n}\n\ntype Tab = 'browse' | 'mine' | 'plan';\ntype Source = 'preset' | 'mine' | 'shared' | 'planned';\n\ninterface OpenPlaylist {\n playlist: Playlist;\n source: Source;\n plan?: Pick<PlanResult, 'searches' | 'mode' | 'minutesPerWeek'>;\n}\n\nfunction ChevronIcon({ up }: { up?: boolean }) {\n return (\n <svg width={14} height={14} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" style={{ transform: up ? 'rotate(180deg)' : undefined, transition: 'transform 150ms ease' }}>\n <polyline points=\"6 9 12 15 18 9\" />\n </svg>\n );\n}\n\nfunction PlaylistCard({ playlist, done, onOpen, compact }: { playlist: Playlist; done: ReadonlySet<string>; onOpen: () => void; compact?: boolean }) {\n const time = playlistTime(playlist, done);\n return (\n <button type=\"button\" style={compact ? s.compactCard : s.card} onClick={onOpen}>\n <span style={{ ...s.title, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: compact ? 'nowrap' : undefined }}>{playlist.title}</span>\n {!compact && playlist.description && <span style={s.muted}>{playlist.description}</span>}\n <span style={s.muted}>\n {time.itemCount} items · ~{formatMinutes(time.totalMinutes)}\n {time.percent > 0 ? ` · ${time.percent}% done` : ''}\n </span>\n {time.percent > 0 && (\n <span style={s.bar}>\n <span style={{ ...s.barFill, display: 'block', width: `${time.percent}%` }} />\n </span>\n )}\n </button>\n );\n}\n\nexport function EducationPlaylists({\n className,\n style,\n compact = false,\n catalog: catalogProp,\n planEndpoint,\n openHref,\n shareBaseHref,\n importFromHash = false,\n userId,\n quizGenerator,\n}: EducationPlaylistsProps) {\n const catalog = useMemo(() => catalogProp ?? getDefaultCatalog(), [catalogProp]);\n const progressStore = useMemo(() => createProgressStore(), []);\n const playlistStore = useMemo(() => createLocalPlaylistStore(), []);\n\n const [expanded, setExpanded] = useState(!compact);\n const [tab, setTab] = useState<Tab>('browse');\n const [done, setDone] = useState<Set<string>>(() => new Set());\n const [mine, setMine] = useState<Playlist[]>([]);\n const [categoryId, setCategoryId] = useState<string | undefined>();\n const [majorId, setMajorId] = useState<string | undefined>();\n const [programId, setProgramId] = useState<string | undefined>();\n const [open, setOpen] = useState<OpenPlaylist | null>(null);\n\n // Storage is read after mount so the server render and the first client\n // render agree; progress then fills in.\n useEffect(() => {\n setDone(progressStore.load());\n setMine(playlistStore.list());\n }, [progressStore, playlistStore]);\n\n useEffect(() => {\n if (!importFromHash || typeof window === 'undefined') return;\n const shared = decodeShareFragment(window.location.hash);\n if (shared) {\n setExpanded(true);\n setOpen({ playlist: shared, source: 'shared' });\n }\n }, [importFromHash]);\n\n const category = catalog.categories.find((c) => c.id === categoryId);\n const major = category?.majors.find((m) => m.id === majorId);\n const visible = filterPlaylists(catalog.playlists, { categoryId, majorId, programId });\n\n const toggle = (itemId: string) => {\n setDone((prev) => {\n const next = toggleDone(prev, itemId);\n progressStore.save(next);\n return next;\n });\n };\n\n const saveMine = (playlist: Playlist) => {\n setMine(playlistStore.save({ ...playlist, updatedAt: new Date().toISOString() }));\n setOpen((current) => (current?.playlist.id === playlist.id ? { ...current, playlist } : current));\n };\n\n const saveCopy = (playlist: Playlist) => {\n const copy = createPlaylist({\n title: playlist.title,\n description: playlist.description,\n items: playlist.items,\n ownerId: userId,\n plannedFrom: playlist.plannedFrom,\n });\n setMine(playlistStore.save(copy));\n setOpen({ playlist: copy, source: 'mine' });\n setTab('mine');\n };\n\n const deleteMine = (id: string) => {\n setMine(playlistStore.remove(id));\n setOpen(null);\n };\n\n const openPlaylist = (playlist: Playlist, source: Source) => {\n setExpanded(true);\n setOpen({ playlist, source });\n };\n\n const shareBase = shareBaseHref ?? (openHref && typeof window !== 'undefined' ? new URL(openHref, window.location.href).toString() : undefined);\n\n const header = (\n <div style={s.headerRow}>\n <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, minWidth: 0 }}>\n <span style={s.label}>Learn</span>\n <span style={{ ...s.muted, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>\n {catalog.playlists.length} playlists · MIT OpenCourseWare\n </span>\n </div>\n <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>\n {compact && !expanded && (\n <button\n type=\"button\"\n style={s.chip}\n onClick={() => {\n setExpanded(true);\n setTab('plan');\n setOpen(null);\n }}\n >\n Plan my own\n </button>\n )}\n {compact && openHref && expanded && (\n <a href={openHref} style={{ ...s.chip, textDecoration: 'none' }}>\n Open full page\n </a>\n )}\n {compact && (\n <button type=\"button\" style={s.iconButton} onClick={() => setExpanded((v) => !v)} aria-expanded={expanded} aria-label={expanded ? 'Collapse education playlists' : 'Expand education playlists'}>\n <ChevronIcon up={expanded} />\n </button>\n )}\n </div>\n </div>\n );\n\n if (compact && !expanded) {\n return (\n <section className={className} style={{ ...s.root, ...style }} aria-label=\"Education playlists\">\n {header}\n <div style={s.scrollRow}>\n {[...mine, ...catalog.playlists].map((playlist) => (\n <PlaylistCard key={playlist.id} playlist={playlist} done={done} compact onOpen={() => openPlaylist(playlist, playlist.visibility === 'preset' ? 'preset' : 'mine')} />\n ))}\n </div>\n </section>\n );\n }\n\n const tabs: { id: Tab; label: string }[] = [\n { id: 'browse', label: 'Browse' },\n { id: 'mine', label: `My playlists${mine.length ? ` (${mine.length})` : ''}` },\n { id: 'plan', label: 'Plan with AI' },\n ];\n\n let body: React.ReactNode;\n if (open) {\n const editable = open.source === 'mine';\n body = (\n <div style={s.section}>\n <div style={s.row}>\n <button type=\"button\" style={s.button} onClick={() => setOpen(null)}>\n ← Back\n </button>\n {open.source === 'shared' && <span style={s.muted}>Shared with you by link</span>}\n {open.source === 'planned' && open.plan && (\n <span style={s.muted}>\n {open.plan.mode === 'llm' ? 'Planned by AI' : 'Planned from the catalog (AI not connected)'} from: {open.plan.searches.join(' · ')}\n </span>\n )}\n {editable && (\n <button type=\"button\" style={s.button} onClick={() => deleteMine(open.playlist.id)}>\n Delete\n </button>\n )}\n </div>\n <PlaylistView\n playlist={open.playlist}\n done={done}\n onToggle={toggle}\n onChange={editable ? saveMine : undefined}\n onSaveCopy={editable ? undefined : saveCopy}\n minutesPerWeek={open.plan?.minutesPerWeek}\n shareBaseHref={shareBase}\n quizGenerator={quizGenerator}\n />\n </div>\n );\n } else if (tab === 'browse') {\n body = (\n <div style={s.section}>\n <div style={s.row}>\n <button type=\"button\" style={{ ...s.chip, ...(!categoryId ? s.chipActive : null) }} onClick={() => { setCategoryId(undefined); setMajorId(undefined); setProgramId(undefined); }}>\n All\n </button>\n {catalog.categories.map((c) => (\n <button key={c.id} type=\"button\" style={{ ...s.chip, ...(categoryId === c.id ? s.chipActive : null) }} aria-pressed={categoryId === c.id} onClick={() => { setCategoryId(c.id); setMajorId(undefined); setProgramId(undefined); }}>\n {c.name}\n </button>\n ))}\n </div>\n {category && (\n <div style={s.row}>\n <span style={s.muted}>Major:</span>\n {category.majors.map((m) => (\n <button key={m.id} type=\"button\" style={{ ...s.chip, ...(majorId === m.id ? s.chipActive : null) }} aria-pressed={majorId === m.id} onClick={() => { setMajorId(majorId === m.id ? undefined : m.id); setProgramId(undefined); }}>\n {m.name}\n </button>\n ))}\n </div>\n )}\n {major && major.programs.length > 1 && (\n <div style={s.row}>\n <span style={s.muted}>Program:</span>\n {major.programs.map((p) => (\n <button key={p.id} type=\"button\" style={{ ...s.chip, ...(programId === p.id ? s.chipActive : null) }} aria-pressed={programId === p.id} onClick={() => setProgramId(programId === p.id ? undefined : p.id)}>\n {p.name}\n </button>\n ))}\n </div>\n )}\n <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 6 }}>\n {visible.map((playlist) => (\n <PlaylistCard key={playlist.id} playlist={playlist} done={done} onOpen={() => openPlaylist(playlist, 'preset')} />\n ))}\n </div>\n </div>\n );\n } else if (tab === 'mine') {\n body = mine.length ? (\n <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 6 }}>\n {mine.map((playlist) => (\n <PlaylistCard key={playlist.id} playlist={playlist} done={done} onOpen={() => openPlaylist(playlist, 'mine')} />\n ))}\n </div>\n ) : (\n <div style={s.muted}>Nothing saved yet. Save a preset playlist, or plan your own with AI.</div>\n );\n } else {\n body = <PlaylistPlanner planEndpoint={planEndpoint} onPlanned={(result) => setOpen({ playlist: result.playlist, source: 'planned', plan: result })} />;\n }\n\n return (\n <section className={className} style={{ ...s.root, ...style }} aria-label=\"Education playlists\">\n {header}\n {!open && (\n <div style={s.row} role=\"tablist\">\n {tabs.map((t) => (\n <button key={t.id} type=\"button\" role=\"tab\" aria-selected={tab === t.id} style={{ ...s.chip, ...(tab === t.id ? s.chipActive : null) }} onClick={() => setTab(t.id)}>\n {t.label}\n </button>\n ))}\n </div>\n )}\n {body}\n </section>\n );\n}\n","/**\n * @fileoverview One playlist, opened: its items with checkmarks and time\n * estimates, overall progress, and — for a playlist the learner owns — the\n * sharing controls.\n */\nimport React, { useState } from 'react';\nimport type { Playlist, PlaylistItem, PlaylistItemKind } from '../types';\nimport { formatMinutes, playlistTime, weeksToFinish } from '../lib/time';\nimport { encodeShareFragment, invite, removeMember } from '../lib/sharing';\nimport { removeItem } from '../lib/playlists';\nimport type { Quiz, QuizGenerator } from '../quiz';\nimport { s } from './styles';\n\nconst KIND_LABEL: Record<PlaylistItemKind, string> = {\n video_playlist: 'Videos',\n video: 'Video',\n courseware: 'Courseware',\n lecture_notes: 'Notes',\n assignment: 'Assignment',\n reading: 'Reading',\n textbook: 'Textbook',\n};\n\nconst PROVIDER_LABEL: Record<string, string> = {\n mit_ocw: 'MIT OCW',\n youtube: 'YouTube',\n edx: 'edX',\n coursera: 'Coursera',\n khan: 'Khan Academy',\n openstax: 'OpenStax',\n web: 'Web',\n};\n\nconst METHOD_LABEL: Record<string, string> = {\n curated_seed: 'curated',\n official_api: 'official',\n snapshot_dataset: 'dataset',\n community_list: 'community',\n llm_search: 'AI search',\n user: 'added',\n};\n\nexport interface PlaylistViewProps {\n playlist: Playlist;\n done: ReadonlySet<string>;\n onToggle: (itemId: string) => void;\n /** Present when the learner may edit this playlist. */\n onChange?: (playlist: Playlist) => void;\n /** Saves a private copy into \"My playlists\" (for presets, shared links and fresh plans). */\n onSaveCopy?: (playlist: Playlist) => void;\n /** Weekly pace, for \"about N weeks left\". */\n minutesPerWeek?: number;\n /** Page a share link opens, e.g. `https://example.com/learn`. */\n shareBaseHref?: string;\n quizGenerator?: QuizGenerator;\n}\n\nfunction ItemRow({\n item,\n checked,\n onToggle,\n onRemove,\n quizGenerator,\n}: {\n item: PlaylistItem;\n checked: boolean;\n onToggle: () => void;\n onRemove?: () => void;\n quizGenerator?: QuizGenerator;\n}) {\n const [quiz, setQuiz] = useState<Quiz | null>(null);\n const [quizState, setQuizState] = useState<'idle' | 'loading' | 'error'>('idle');\n const generateQuiz = async () => {\n if (!quizGenerator) return;\n setQuizState('loading');\n try {\n setQuiz(await quizGenerator(item));\n setQuizState('idle');\n } catch {\n setQuizState('error');\n }\n };\n\n return (\n <li style={s.item}>\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={onToggle}\n aria-label={`Mark \"${item.title}\" ${checked ? 'not done' : 'done'}`}\n style={{ marginTop: 3, cursor: 'pointer' }}\n />\n <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 2 }}>\n <a\n href={item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={{ ...s.link, textDecoration: checked ? 'line-through' : 'none', opacity: checked ? 0.6 : 1 }}\n >\n {item.title}\n </a>\n <div style={{ ...s.row, ...s.muted }}>\n <span style={s.badge}>{KIND_LABEL[item.kind]}</span>\n <span title={item.estimateBasis}>\n {item.estimate === 'rough' ? '~' : ''}\n {formatMinutes(item.minutes)}\n </span>\n <span style={s.badge} title={item.provenance.note}>\n {PROVIDER_LABEL[item.provenance.provider] ?? item.provenance.provider} · {METHOD_LABEL[item.provenance.method] ?? item.provenance.method}\n </span>\n {quizGenerator && (\n <button type=\"button\" style={{ ...s.chip, padding: '0 8px' }} onClick={generateQuiz} disabled={quizState === 'loading'}>\n {quizState === 'loading' ? 'Writing quiz…' : quizState === 'error' ? 'Quiz failed, retry' : 'Quiz me'}\n </button>\n )}\n {onRemove && (\n <button type=\"button\" style={{ ...s.chip, padding: '0 8px' }} onClick={onRemove} aria-label={`Remove \"${item.title}\"`}>\n Remove\n </button>\n )}\n </div>\n {quiz && (\n <ol style={{ margin: '4px 0 0', paddingLeft: 18, fontSize: 12 }}>\n {quiz.questions.map((question, index) => (\n <li key={index}>\n {question.prompt}\n <details>\n <summary style={{ cursor: 'pointer', opacity: 0.7 }}>Answer</summary>\n {question.choices[question.answerIndex]}\n {question.explanation ? ` — ${question.explanation}` : ''}\n </details>\n </li>\n ))}\n </ol>\n )}\n </div>\n </li>\n );\n}\n\nfunction SharingControls({ playlist, onChange, shareBaseHref }: { playlist: Playlist; onChange: (p: Playlist) => void; shareBaseHref?: string }) {\n const [email, setEmail] = useState('');\n const [message, setMessage] = useState<string | null>(null);\n\n const copyShareLink = async () => {\n const base = shareBaseHref ?? (typeof window !== 'undefined' ? `${window.location.origin}${window.location.pathname}` : '');\n const link = `${base}#${encodeShareFragment(playlist)}`;\n try {\n await navigator.clipboard.writeText(link);\n setMessage('Share link copied.');\n } catch {\n setMessage(link);\n }\n };\n\n const addInvite = () => {\n try {\n onChange(invite(playlist, email, 'viewer'));\n setEmail('');\n setMessage(null);\n } catch (error) {\n setMessage((error as Error).message);\n }\n };\n\n return (\n <div style={s.section}>\n <div style={s.row}>\n <span style={s.muted}>Visibility:</span>\n {(['private', 'public'] as const).map((visibility) => (\n <button\n key={visibility}\n type=\"button\"\n style={{ ...s.chip, ...(playlist.visibility === visibility ? s.chipActive : null) }}\n onClick={() => onChange({ ...playlist, visibility })}\n aria-pressed={playlist.visibility === visibility}\n >\n {visibility === 'private' ? 'Private' : 'Public'}\n </button>\n ))}\n {playlist.visibility === 'public' && (\n <button type=\"button\" style={s.button} onClick={copyShareLink}>\n Copy share link\n </button>\n )}\n </div>\n {playlist.visibility === 'private' && (\n <>\n <form\n style={s.row}\n onSubmit={(event) => {\n event.preventDefault();\n addInvite();\n }}\n >\n <input style={s.input} type=\"email\" placeholder=\"Invite by email\" value={email} onChange={(event) => setEmail(event.target.value)} aria-label=\"Invite by email\" />\n <button type=\"submit\" style={s.button} disabled={!email.trim()}>\n Invite\n </button>\n </form>\n {(playlist.members ?? []).map((member) => (\n <div key={member.email ?? member.userId} style={{ ...s.row, ...s.muted }}>\n <span>{member.email}</span>\n <span style={s.badge}>{member.status === 'invited' ? 'invite pending' : member.role}</span>\n {member.email && (\n <button type=\"button\" style={{ ...s.chip, padding: '0 8px' }} onClick={() => onChange(removeMember(playlist, member.email!))}>\n Remove\n </button>\n )}\n </div>\n ))}\n </>\n )}\n {message && <div style={{ ...s.muted, wordBreak: 'break-all' }}>{message}</div>}\n </div>\n );\n}\n\nexport function PlaylistView({ playlist, done, onToggle, onChange, onSaveCopy, minutesPerWeek, shareBaseHref, quizGenerator }: PlaylistViewProps) {\n const time = playlistTime(playlist, done);\n const weeks = weeksToFinish(time.remainingMinutes, minutesPerWeek);\n\n return (\n <div style={s.section}>\n <div>\n <div style={s.title}>{playlist.title}</div>\n {playlist.description && <div style={s.muted}>{playlist.description}</div>}\n </div>\n <div style={{ ...s.row, ...s.muted }}>\n <span>\n {time.doneCount}/{time.itemCount} done\n </span>\n <span>· ~{formatMinutes(time.remainingMinutes)} left of ~{formatMinutes(time.totalMinutes)}</span>\n {weeks !== null && <span>· about {weeks} week{weeks === 1 ? '' : 's'} at your pace</span>}\n </div>\n <div style={s.bar} role=\"progressbar\" aria-valuenow={time.percent} aria-valuemin={0} aria-valuemax={100} aria-label=\"Progress by time\">\n <div style={{ ...s.barFill, width: `${time.percent}%` }} />\n </div>\n {playlist.items.length === 0 ? (\n <div style={s.muted}>Nothing matched yet. Try different wording, or answer the questions in your own words.</div>\n ) : (\n <ul style={s.list}>\n {playlist.items.map((item) => (\n <ItemRow\n key={item.id}\n item={item}\n checked={done.has(item.id)}\n onToggle={() => onToggle(item.id)}\n onRemove={onChange ? () => onChange(removeItem(playlist, item.id)) : undefined}\n quizGenerator={quizGenerator}\n />\n ))}\n </ul>\n )}\n <div style={s.row}>\n {onSaveCopy && (\n <button type=\"button\" style={s.primary} onClick={() => onSaveCopy(playlist)}>\n Save to my playlists\n </button>\n )}\n </div>\n {onChange && <SharingControls playlist={playlist} onChange={onChange} shareBaseHref={shareBaseHref} />}\n </div>\n );\n}\n","/**\n * @fileoverview Inline styles for the widget. Colors are translucent greys\n * over `currentColor`, so the widget takes on whatever light or dark surface\n * the host puts it on, the way the news and weather widgets do.\n */\nimport type { CSSProperties } from 'react';\n\nconst line = '1px solid rgba(127,127,127,0.3)';\n\nexport const s = {\n root: { fontFamily: 'system-ui, sans-serif', display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 14px', borderRadius: 8, fontSize: 13, lineHeight: 1.4, minWidth: 0 } as CSSProperties,\n headerRow: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } as CSSProperties,\n label: { fontSize: 11, fontWeight: 600, opacity: 0.75, letterSpacing: 0.3, textTransform: 'uppercase' } as CSSProperties,\n muted: { opacity: 0.7, fontSize: 12 } as CSSProperties,\n row: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } as CSSProperties,\n scrollRow: { display: 'flex', gap: 6, overflowX: 'auto', paddingBottom: 2, scrollbarWidth: 'thin' } as CSSProperties,\n chip: { border: line, borderRadius: 999, padding: '2px 10px', fontSize: 12, background: 'transparent', color: 'inherit', cursor: 'pointer', whiteSpace: 'nowrap' } as CSSProperties,\n chipActive: { background: 'rgba(127,127,127,0.22)', borderColor: 'rgba(127,127,127,0.6)' } as CSSProperties,\n button: { border: line, borderRadius: 6, padding: '4px 10px', fontSize: 12, background: 'rgba(127,127,127,0.12)', color: 'inherit', cursor: 'pointer' } as CSSProperties,\n primary: { border: '1px solid rgba(99,102,241,0.7)', borderRadius: 6, padding: '5px 12px', fontSize: 12, fontWeight: 600, background: 'rgba(99,102,241,0.25)', color: 'inherit', cursor: 'pointer' } as CSSProperties,\n iconButton: { border: 'none', background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 2, opacity: 0.75, display: 'flex' } as CSSProperties,\n input: { flex: 1, minWidth: 0, border: line, borderRadius: 6, padding: '5px 8px', fontSize: 13, background: 'rgba(127,127,127,0.08)', color: 'inherit' } as CSSProperties,\n card: { border: line, borderRadius: 8, padding: '8px 10px', background: 'rgba(127,127,127,0.06)', textAlign: 'left', color: 'inherit', cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 } as CSSProperties,\n compactCard: { border: line, borderRadius: 8, padding: '6px 10px', background: 'rgba(127,127,127,0.06)', textAlign: 'left', color: 'inherit', cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 2, minWidth: 150, maxWidth: 190, flex: '0 0 auto' } as CSSProperties,\n title: { fontWeight: 600, fontSize: 13 } as CSSProperties,\n bar: { height: 4, borderRadius: 2, background: 'rgba(127,127,127,0.25)', overflow: 'hidden' } as CSSProperties,\n barFill: { height: '100%', background: 'rgba(34,197,94,0.85)' } as CSSProperties,\n list: { listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 4 } as CSSProperties,\n item: { display: 'flex', alignItems: 'flex-start', gap: 8, padding: '6px 4px', borderBottom: '1px solid rgba(127,127,127,0.15)' } as CSSProperties,\n link: { color: 'inherit', textDecoration: 'none', fontWeight: 500 } as CSSProperties,\n badge: { fontSize: 10, border: line, borderRadius: 4, padding: '0 5px', opacity: 0.8, whiteSpace: 'nowrap' } as CSSProperties,\n section: { display: 'flex', flexDirection: 'column', gap: 6 } as CSSProperties,\n} as const;\n","/**\n * @fileoverview The \"plan with AI\" flow: a goal, then follow-up questions the\n * learner answers by tapping an option or typing their own words, then the\n * planned playlist.\n */\nimport React, { useRef, useState } from 'react';\nimport type { PlanAnswer } from '../types';\nimport { fetchFollowUps, fetchPlan } from '../api/client';\nimport type { FollowUpQuestion, PlanResult, PlannerMode } from '../planner';\nimport { MAX_GOAL_LENGTH, MAX_ANSWER_LENGTH } from '../planner';\nimport { s } from './styles';\n\nexport interface PlaylistPlannerProps {\n /** The host's planner endpoint; without one, planning runs locally over the catalog. */\n planEndpoint?: string;\n onPlanned: (result: PlanResult) => void;\n initialGoal?: string;\n}\n\nconst MODE_NOTE: Record<PlannerMode, string> = {\n llm: 'Questions written by AI for your goal.',\n offline: 'AI is not connected here, so these are the standard questions.',\n};\n\nexport function PlaylistPlanner({ planEndpoint, onPlanned, initialGoal = '' }: PlaylistPlannerProps) {\n const [goal, setGoal] = useState(initialGoal);\n const [questions, setQuestions] = useState<FollowUpQuestion[] | null>(null);\n const [mode, setMode] = useState<PlannerMode | null>(null);\n const [answers, setAnswers] = useState<Record<string, string>>({});\n const [busy, setBusy] = useState<'questions' | 'plan' | null>(null);\n const [error, setError] = useState<string | null>(null);\n const abort = useRef<AbortController | null>(null);\n\n const run = async <T,>(kind: 'questions' | 'plan', task: (signal: AbortSignal) => Promise<T>): Promise<T | null> => {\n abort.current?.abort();\n const controller = new AbortController();\n abort.current = controller;\n setBusy(kind);\n setError(null);\n try {\n return await task(controller.signal);\n } catch (err) {\n if ((err as Error).name !== 'AbortError') setError((err as Error).message);\n return null;\n } finally {\n if (abort.current === controller) setBusy(null);\n }\n };\n\n const askQuestions = async () => {\n const result = await run('questions', (signal) => fetchFollowUps(goal, planEndpoint, signal));\n if (!result) return;\n setQuestions(result.questions);\n setMode(result.mode);\n setAnswers({});\n };\n\n const buildPlan = async () => {\n const answered: PlanAnswer[] = (questions ?? [])\n .map((question) => ({ question: question.question, answer: (answers[question.id] ?? '').trim() }))\n .filter((entry) => entry.answer);\n const result = await run('plan', (signal) => fetchPlan(goal, answered, planEndpoint, signal));\n if (result) onPlanned(result);\n };\n\n return (\n <div style={s.section}>\n <form\n style={s.row}\n onSubmit={(event) => {\n event.preventDefault();\n if (goal.trim()) void askQuestions();\n }}\n >\n <input\n style={s.input}\n value={goal}\n maxLength={MAX_GOAL_LENGTH}\n onChange={(event) => setGoal(event.target.value)}\n placeholder=\"What do you want to learn? e.g. machine learning from scratch\"\n aria-label=\"What do you want to learn?\"\n />\n <button type=\"submit\" style={s.primary} disabled={!goal.trim() || busy !== null}>\n {busy === 'questions' ? 'Thinking…' : questions ? 'Ask again' : 'Start'}\n </button>\n </form>\n\n {questions && (\n <div style={s.section}>\n {mode && <div style={s.muted}>{MODE_NOTE[mode]}</div>}\n {questions.map((question) => (\n <fieldset key={question.id} style={{ border: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 4 }}>\n <legend style={{ fontWeight: 500, padding: 0, marginBottom: 4 }}>{question.question}</legend>\n <div style={s.row}>\n {question.options.map((option) => (\n <button\n key={option}\n type=\"button\"\n style={{ ...s.chip, ...(answers[question.id] === option ? s.chipActive : null) }}\n aria-pressed={answers[question.id] === option}\n onClick={() => setAnswers((prev) => ({ ...prev, [question.id]: prev[question.id] === option ? '' : option }))}\n >\n {option}\n </button>\n ))}\n </div>\n <input\n style={s.input}\n value={answers[question.id] ?? ''}\n maxLength={MAX_ANSWER_LENGTH}\n onChange={(event) => setAnswers((prev) => ({ ...prev, [question.id]: event.target.value }))}\n placeholder=\"Or say it in your own words\"\n aria-label={`Your answer: ${question.question}`}\n />\n </fieldset>\n ))}\n <div style={s.row}>\n <button type=\"button\" style={s.primary} onClick={() => void buildPlan()} disabled={busy !== null}>\n {busy === 'plan' ? 'Planning…' : 'Build my playlist'}\n </button>\n </div>\n </div>\n )}\n {error && <div style={{ ...s.muted, color: '#ef4444' }}>{error}</div>}\n </div>\n );\n}\n"],"mappings":"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","names":["text","text","c","useState","useState","jsx","jsxs","useState","jsx","jsxs","useState","c"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview A learner's own playlists: creating and editing them, and
|
|
3
|
+
* keeping them in the browser.
|
|
4
|
+
*
|
|
5
|
+
* Browser storage is this sketch's only backend. It is enough for private
|
|
6
|
+
* playlists and progress on one device; sharing a private playlist with an
|
|
7
|
+
* invitee needs the host app to store playlists server-side (see the
|
|
8
|
+
* package README), which the `PlaylistStore` interface is shaped for.
|
|
9
|
+
*/
|
|
10
|
+
import type { Playlist, PlaylistItem, PlaylistVisibility } from '../types';
|
|
11
|
+
export declare const PLAYLISTS_STORAGE_KEY = "educationPlaylists.mine";
|
|
12
|
+
export interface PlaylistStore {
|
|
13
|
+
list(): Playlist[];
|
|
14
|
+
save(playlist: Playlist): Playlist[];
|
|
15
|
+
remove(id: string): Playlist[];
|
|
16
|
+
}
|
|
17
|
+
export declare function createPlaylist(input: {
|
|
18
|
+
title: string;
|
|
19
|
+
ownerId?: string;
|
|
20
|
+
visibility?: Exclude<PlaylistVisibility, 'preset'>;
|
|
21
|
+
items?: PlaylistItem[];
|
|
22
|
+
description?: string;
|
|
23
|
+
plannedFrom?: Playlist['plannedFrom'];
|
|
24
|
+
}): Playlist;
|
|
25
|
+
/** Appends `item` unless the playlist already links to the same URL. */
|
|
26
|
+
export declare function addItem(playlist: Playlist, item: PlaylistItem): Playlist;
|
|
27
|
+
export declare function removeItem(playlist: Playlist, itemId: string): Playlist;
|
|
28
|
+
/** Moves the item at `from` to `to`, clamped to the list. */
|
|
29
|
+
export declare function moveItem(playlist: Playlist, from: number, to: number): Playlist;
|
|
30
|
+
/** A store over `localStorage`, or in memory where there is none. */
|
|
31
|
+
export declare function createLocalPlaylistStore(key?: string): PlaylistStore;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Which items a learner has checked off, kept per browser.
|
|
3
|
+
*
|
|
4
|
+
* Progress is keyed by item id, not by playlist, so finishing 18.06 in the
|
|
5
|
+
* "Linear Algebra and Probability" playlist also ticks it in a custom playlist
|
|
6
|
+
* that includes it. Storage access is wrapped: a private window or blocked
|
|
7
|
+
* site data must leave a working (if forgetful) widget, not a thrown error.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PROGRESS_STORAGE_KEY = "educationPlaylists.progress";
|
|
10
|
+
export interface ProgressStore {
|
|
11
|
+
load(): Set<string>;
|
|
12
|
+
save(done: ReadonlySet<string>): void;
|
|
13
|
+
}
|
|
14
|
+
/** A store over `localStorage`, or in memory where there is none. */
|
|
15
|
+
export declare function createProgressStore(key?: string): ProgressStore;
|
|
16
|
+
/** The set with `id` toggled; never mutates the input. */
|
|
17
|
+
export declare function toggleDone(done: ReadonlySet<string>, id: string): Set<string>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Turning untrusted data into a playlist: a share link someone
|
|
3
|
+
* pasted, a record from an ingestion run, or an LLM's answer.
|
|
4
|
+
*
|
|
5
|
+
* Every link the widget renders comes through here first. Only `http(s)` URLs
|
|
6
|
+
* survive, so a shared playlist cannot smuggle a `javascript:` href onto the
|
|
7
|
+
* page, and every field is length-capped and type-checked rather than trusted.
|
|
8
|
+
*/
|
|
9
|
+
import type { Playlist, PlaylistItem } from '../types';
|
|
10
|
+
export declare const MAX_PLAYLIST_ITEMS = 60;
|
|
11
|
+
/** The URL if it is an absolute http(s) URL, else `null`. */
|
|
12
|
+
export declare function safeUrl(value: unknown): string | null;
|
|
13
|
+
export declare function sanitizeItem(raw: unknown, index?: number): PlaylistItem | null;
|
|
14
|
+
/**
|
|
15
|
+
* A playlist rebuilt from untrusted input, or `null` if nothing usable is
|
|
16
|
+
* left. Members are dropped: who a playlist is shared with is the server's
|
|
17
|
+
* record, never something a link can assert.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sanitizePlaylist(raw: unknown): Playlist | null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Keyword search over catalog items. It is what the planner
|
|
3
|
+
* runs its searches against when no web search is wired in, and what keeps a
|
|
4
|
+
* custom playlist anchored to known courses when one is.
|
|
5
|
+
*/
|
|
6
|
+
import type { PlaylistItem } from '../types';
|
|
7
|
+
export declare function tokenize(text: string): string[];
|
|
8
|
+
/** Items ranked by how many query terms they mention; title hits count double. */
|
|
9
|
+
export declare function searchCatalog(items: PlaylistItem[], query: string, limit?: number): PlaylistItem[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Who can see and change a playlist, invitations, and share
|
|
3
|
+
* links.
|
|
4
|
+
*
|
|
5
|
+
* Access rules are pure functions so the server and the widget agree on them.
|
|
6
|
+
* The widget's own checks are for display only; a server that stores
|
|
7
|
+
* playlists must run `canView`/`canEdit` itself before answering.
|
|
8
|
+
*/
|
|
9
|
+
import type { Playlist, PlaylistRole } from '../types';
|
|
10
|
+
export declare function canView(playlist: Playlist, userId?: string): boolean;
|
|
11
|
+
export declare function canEdit(playlist: Playlist, userId?: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The playlist with `email` invited at `role`. Re-inviting an address updates
|
|
14
|
+
* its role rather than adding a second row. Throws on a malformed address or
|
|
15
|
+
* an attempt to invite a second owner.
|
|
16
|
+
*/
|
|
17
|
+
export declare function invite(playlist: Playlist, email: string, role: Exclude<PlaylistRole, 'owner'>, makeToken?: () => string): Playlist;
|
|
18
|
+
/** The playlist with the invite behind `token` accepted by `userId`, or `null` for an unknown token. */
|
|
19
|
+
export declare function acceptInvite(playlist: Playlist, token: string, userId: string): Playlist | null;
|
|
20
|
+
export declare function removeMember(playlist: Playlist, email: string): Playlist;
|
|
21
|
+
/** Hash parameter a share link carries its playlist in. */
|
|
22
|
+
export declare const SHARE_HASH_PARAM = "playlist";
|
|
23
|
+
/**
|
|
24
|
+
* A share fragment (`playlist=<base64url>`) carrying a public copy of the
|
|
25
|
+
* playlist. The whole playlist rides in the URL fragment, which browsers never
|
|
26
|
+
* send to a server, so sharing works with no storage behind it. Private
|
|
27
|
+
* playlists are refused: those are shared by invitation, not by link.
|
|
28
|
+
*/
|
|
29
|
+
export declare function encodeShareFragment(playlist: Playlist): string;
|
|
30
|
+
/** The playlist in a share fragment (with or without the leading `#`), or `null`. */
|
|
31
|
+
export declare function decodeShareFragment(fragment: string): Playlist | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Time arithmetic for self-paced playlists: totals, what is
|
|
3
|
+
* left once some items are checked off, and how it reads on screen.
|
|
4
|
+
*/
|
|
5
|
+
import type { Playlist } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* "45 min", "2 h 30 min", "176 h". From ten hours up the minutes are dropped:
|
|
8
|
+
* most estimates are rough, and "175 h 50 min" claims a precision they lack.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatMinutes(minutes: number): string;
|
|
11
|
+
export interface PlaylistTime {
|
|
12
|
+
totalMinutes: number;
|
|
13
|
+
doneMinutes: number;
|
|
14
|
+
remainingMinutes: number;
|
|
15
|
+
/** 0–100, by time rather than by item count, since items vary by tens of hours. */
|
|
16
|
+
percent: number;
|
|
17
|
+
doneCount: number;
|
|
18
|
+
itemCount: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function playlistTime(playlist: Playlist, done: ReadonlySet<string>): PlaylistTime;
|
|
21
|
+
/** Weeks to finish what is left at a weekly pace; `null` with no pace. */
|
|
22
|
+
export declare function weeksToFinish(remainingMinutes: number, minutesPerWeek?: number): number | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The interactive planner: from a learning goal, ask a few
|
|
3
|
+
* follow-up questions, then turn the goal and the answers into searches and
|
|
4
|
+
* the searches into a custom playlist.
|
|
5
|
+
*
|
|
6
|
+
* The language model and the web search are injected, never imported, so the
|
|
7
|
+
* package stays provider-neutral and the host decides which model pays for
|
|
8
|
+
* it. With neither, the planner still works ("offline" mode): fixed questions,
|
|
9
|
+
* keyword searches over the bundled catalog. That is also the fallback when a
|
|
10
|
+
* model call fails or answers with something unparseable, so a learner never
|
|
11
|
+
* gets an error where a reasonable playlist was possible.
|
|
12
|
+
*/
|
|
13
|
+
import type { EducationCatalog, PlanAnswer, Playlist, CourseLevel } from '../types';
|
|
14
|
+
export declare const MAX_GOAL_LENGTH = 300;
|
|
15
|
+
export declare const MAX_ANSWERS = 8;
|
|
16
|
+
export declare const MAX_ANSWER_LENGTH = 300;
|
|
17
|
+
export declare const MAX_PLAN_ITEMS = 12;
|
|
18
|
+
export interface FollowUpQuestion {
|
|
19
|
+
id: string;
|
|
20
|
+
question: string;
|
|
21
|
+
/** Quick answers; the learner can always type their own instead. */
|
|
22
|
+
options: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface WebSearchHit {
|
|
25
|
+
title: string;
|
|
26
|
+
url: string;
|
|
27
|
+
snippet?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PlannerDeps {
|
|
30
|
+
/** Runs a language model: a system and a user prompt in, its text out. */
|
|
31
|
+
generate?: (input: {
|
|
32
|
+
system: string;
|
|
33
|
+
prompt: string;
|
|
34
|
+
}) => Promise<string>;
|
|
35
|
+
/** Web search for material outside the catalog. */
|
|
36
|
+
search?: (query: string) => Promise<WebSearchHit[]>;
|
|
37
|
+
catalog?: EducationCatalog;
|
|
38
|
+
}
|
|
39
|
+
export interface PlanRequest {
|
|
40
|
+
goal: string;
|
|
41
|
+
answers?: PlanAnswer[];
|
|
42
|
+
}
|
|
43
|
+
export type PlannerMode = 'llm' | 'offline';
|
|
44
|
+
export interface FollowUpsResult {
|
|
45
|
+
questions: FollowUpQuestion[];
|
|
46
|
+
mode: PlannerMode;
|
|
47
|
+
}
|
|
48
|
+
export interface PlanResult {
|
|
49
|
+
playlist: Playlist;
|
|
50
|
+
/** The searches the playlist was assembled from, shown so the learner can see why. */
|
|
51
|
+
searches: string[];
|
|
52
|
+
mode: PlannerMode;
|
|
53
|
+
/** Weekly time the answers implied, when they named one. */
|
|
54
|
+
minutesPerWeek?: number;
|
|
55
|
+
}
|
|
56
|
+
/** The questions asked when there is no model, or the model's were unusable. */
|
|
57
|
+
export declare const DEFAULT_FOLLOW_UPS: FollowUpQuestion[];
|
|
58
|
+
/** Trims and caps a request; throws when there is no goal to plan from. */
|
|
59
|
+
export declare function normalizePlanRequest(raw: unknown): PlanRequest;
|
|
60
|
+
/** The first JSON object in a model's reply, tolerating code fences and chatter around it. */
|
|
61
|
+
export declare function extractJson(text: string): unknown;
|
|
62
|
+
/** Follow-up questions for a goal: the model's, or the defaults. */
|
|
63
|
+
export declare function suggestFollowUps(goal: string, deps?: PlannerDeps): Promise<FollowUpsResult>;
|
|
64
|
+
/** What the answers say about level, format and pace, read from their words. */
|
|
65
|
+
export declare function readPreferences(answers?: PlanAnswer[]): {
|
|
66
|
+
maxLevel?: CourseLevel;
|
|
67
|
+
preferAdvanced: boolean;
|
|
68
|
+
format: 'videos' | 'courseware' | 'mix';
|
|
69
|
+
minutesPerWeek?: number;
|
|
70
|
+
};
|
|
71
|
+
/** Builds a custom playlist from a goal and the learner's answers. */
|
|
72
|
+
export declare function planPlaylist(rawRequest: PlanRequest, deps?: PlannerDeps): Promise<PlanResult>;
|
package/dist/quiz.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The quiz hook: the shape of a NotebookLM-style quiz over a
|
|
3
|
+
* playlist item, and the prompt that asks for one.
|
|
4
|
+
*
|
|
5
|
+
* This is a stub on purpose. Generating a grounded quiz needs the item's
|
|
6
|
+
* material loaded as a source (a NotebookLM notebook, or an extracted
|
|
7
|
+
* transcript/PDF), and that belongs to the host — this repo's
|
|
8
|
+
* `notebooklm-api-client` or the web app's `/api/notebooklm` routes. The
|
|
9
|
+
* widget shows a Quiz button only when the host passes a `QuizGenerator`.
|
|
10
|
+
*/
|
|
11
|
+
import type { PlaylistItem } from './types';
|
|
12
|
+
export interface QuizQuestion {
|
|
13
|
+
prompt: string;
|
|
14
|
+
choices: string[];
|
|
15
|
+
/** Index into `choices`. */
|
|
16
|
+
answerIndex: number;
|
|
17
|
+
explanation?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Quiz {
|
|
20
|
+
itemId: string;
|
|
21
|
+
title: string;
|
|
22
|
+
questions: QuizQuestion[];
|
|
23
|
+
/** Which engine wrote it, so a learner can weigh it. */
|
|
24
|
+
source: 'notebooklm' | 'llm';
|
|
25
|
+
}
|
|
26
|
+
/** Supplied by the host; resolves to a quiz grounded in the item's material. */
|
|
27
|
+
export type QuizGenerator = (item: PlaylistItem, options?: {
|
|
28
|
+
questionCount?: number;
|
|
29
|
+
}) => Promise<Quiz>;
|
|
30
|
+
/**
|
|
31
|
+
* The instruction to send alongside the item's material — to NotebookLM's
|
|
32
|
+
* "ask" with the item added as a source, or to a model with the extracted text.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildQuizPrompt(item: PlaylistItem, questionCount?: number): string;
|
|
35
|
+
/** Validates a quiz a generator produced; drops malformed questions. */
|
|
36
|
+
export declare function parseQuiz(item: PlaylistItem, raw: unknown, source: Quiz['source']): Quiz | null;
|