dsh-aha-tutor 1.6.0 → 1.7.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/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as BOARD_LIMITS, A as TutorError, B as passagesFor, C as researchQueriesPrompt, Ct as text, D as summaryPrompt, E as shortPrompt, Et as newId, F as EMPTY_PROFILE, G as formatPassages, H as readProfile, I as emptyProgress, J as applyEvidence, K as passageRange, L as masteryFor, M as generateSteps, N as generateText, O as verifyItemPrompt, P as streamCounter, Q as extractJson, R as materialImages, S as reexplainPrompt, St as safeRelativePath, T as reworkPrompt, Tt as assertId, U as readProgress, V as readMaterials, W as documentParts, X as masteryPercent, Y as createConceptState, Z as recommendNext, _ as profilePrompt, _t as normalizeLesson, a as blueprintPrompt, at as conceptIndex, b as questionnairePrompt, bt as normalizeQuestionnaire, c as correctionPrompt, ct as flattenLessons, d as digestPrompt, dt as lessonAfter, et as boardToPlainText, f as flashcardsPrompt, ft as nextLesson, g as practicePrompt, gt as normalizeItem, h as materialBriefPrompt, ht as normalizeCheatsheet, i as assistPrompt, it as blueprintOutline, j as generateJson, k as verifyStepPrompt, l as detectLanguage, lt as insertLesson, m as lessonPrompt, mt as normalizeCards, n as publishRuntime, nt as normalizeBoard, o as challengePrompt, ot as courseProgress, p as gradePrompt, pt as normalizeBlueprint, q as spreadPassages, r as askPrompt, rt as normalizeOp, s as cheatsheetPrompt, st as findLesson, tt as estimateSpeechMs, u as digestPartPrompt, ut as intakeSummary, v as projectReviewPrompt, vt as normalizePractice, w as revisePrompt, wt as plotToText, x as quizPrompt, xt as outlineForPrompt, y as projectsPrompt, yt as normalizeProject, z as materialsSummary } from "./runtime-B6zUpJjA.js";
1
+ import { $ as passageRange, A as verifyItemPrompt, At as plotToText, B as materialImages, C as quizPrompt, Ct as normalizeLesson, D as reworkPrompt, Dt as outlineForPrompt, E as revisePrompt, Et as normalizeQuestionnaire, F as generateText, G as readProgress, H as passagesFor, I as streamCounter, J as masteryPercent, K as applyEvidence, L as EMPTY_PROFILE, M as TutorError, Mt as newId, N as generateJson, O as shortPrompt, Ot as safeRelativePath, P as generateSteps, Q as formatPassages, R as emptyProgress, S as questionnairePrompt, St as normalizeItem, T as researchQueriesPrompt, Tt as normalizeProject, U as readMaterials, V as materialsSummary, W as readProfile, X as buildIndex, Y as recommendNext, Z as documentParts, _ as materialsRevisePrompt, _t as nextLesson, a as blueprintPrompt, at as boardToPlainText, b as projectReviewPrompt, bt as normalizeCheatsheet, c as correctionPrompt, ct as normalizeOp, d as digestPrompt, dt as courseProgress, et as search, f as flashcardsPrompt, ft as findLesson, g as materialBriefPrompt, gt as lessonAfter, h as lessonPrompt, ht as intakeSummary, i as assistPrompt, it as BOARD_LIMITS, j as verifyStepPrompt, jt as assertId, k as summaryPrompt, kt as text, l as detectLanguage, lt as blueprintOutline, m as handoutPrompt, mt as insertLesson, n as publishRuntime, nt as spreadPassages, o as challengePrompt, ot as estimateSpeechMs, p as gradePrompt, pt as flattenLessons, q as createConceptState, r as askPrompt, rt as extractJson, s as cheatsheetPrompt, st as normalizeBoard, tt as snippetOf, u as digestPartPrompt, ut as conceptIndex, v as practicePrompt, vt as normalizeBlueprint, w as reexplainPrompt, wt as normalizePractice, x as projectsPrompt, xt as normalizeHandout, y as profilePrompt, yt as normalizeCards, z as masteryFor } from "./runtime-DfoeKFPj.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join, relative, sep } from "node:path";
4
4
  import { credentialKey } from "@deepseek-ai/dsh-credentials";
@@ -490,15 +490,76 @@ function createCourses({ store, llm, jobs, research, now = () => /* @__PURE__ */
490
490
  meta: { courseId }
491
491
  });
492
492
  }
493
+ function addMaterials({ courseId, materialIds = [], instruction }) {
494
+ const wanted = [...new Set(materialIds)].filter((id) => /^mat_[a-z0-9]+$/u.test(id)).slice(0, 12);
495
+ if (!wanted.length) throw new TutorError("no-materials", "Choose at least one material");
496
+ const request = text(instruction, 600);
497
+ return jobs.start("craft/revise", async (api) => {
498
+ const course = await readCourse(courseId);
499
+ if (!course.units) throw new TutorError("no-blueprint", "There is no blueprint to revise yet");
500
+ const known = course.materialIds ?? [];
501
+ const added = (await readMaterials(store, wanted.filter((id) => !known.includes(id)))).slice(0, Math.max(0, 12 - known.length));
502
+ if (!added.length) throw new TutorError(wanted.every((id) => known.includes(id)) ? "materials-known" : known.length >= 12 ? "materials-full" : "not-found", "These materials cannot be added");
503
+ const current = {
504
+ title: course.title,
505
+ subtitle: course.subtitle,
506
+ emoji: course.emoji,
507
+ summary: course.summary,
508
+ level: course.level,
509
+ units: course.units,
510
+ projects: course.projects
511
+ };
512
+ const share = Math.floor(9e3 / added.length);
513
+ const passages = added.filter((item) => item.chunks.length).map((item) => formatPassages(spreadPassages(item.chunks, {
514
+ maxChars: share,
515
+ samples: 8
516
+ }), item.meta.title)).join("\n\n");
517
+ api.progress(0, 0, "blueprint");
518
+ const revised = await generateJson(llm, {
519
+ purpose: "teach",
520
+ prompt: materialsRevisePrompt({
521
+ blueprint: current,
522
+ materials: materialsSummary(added),
523
+ passages,
524
+ instruction: request,
525
+ language: course.language
526
+ }),
527
+ images: llm.supportsImages?.("teach") ? await materialImages(store, added) : void 0,
528
+ maxTokens: 16e3,
529
+ signal: api.signal,
530
+ onText: streamCounter("\"estMinutes\"", (count) => api.progress(count, 0, "blueprint")),
531
+ normalize: (raw) => attachProjectUnits(raw, normalizeBlueprint(raw, { language: course.language }))
532
+ });
533
+ const proposal = {
534
+ instruction: request,
535
+ materials: added.map((item) => ({
536
+ id: item.meta.id,
537
+ title: item.meta.title
538
+ })),
539
+ at: stamp(),
540
+ blueprint: revised
541
+ };
542
+ await store.writeJson(store.paths.proposal(courseId), proposal);
543
+ return {
544
+ course,
545
+ proposal
546
+ };
547
+ }, {
548
+ key: `blueprint:${courseId}`,
549
+ meta: { courseId }
550
+ });
551
+ }
493
552
  async function applyProposal({ courseId }) {
494
553
  const course = await readCourse(courseId);
495
554
  const proposal = await store.readJson(store.paths.proposal(courseId), null);
496
555
  if (!proposal?.blueprint?.units) throw new TutorError("no-proposal", "There is no proposed change");
556
+ const materialIds = [.../* @__PURE__ */ new Set([...course.materialIds ?? [], ...(proposal.materials ?? []).map((item) => item.id)])].slice(0, 12);
497
557
  const next = {
498
558
  ...course,
499
559
  ...proposal.blueprint,
500
560
  language: course.language,
501
- status: course.status === "ready" ? "ready" : "blueprint"
561
+ status: course.status === "ready" ? "ready" : "blueprint",
562
+ ...materialIds.length ? { materialIds } : {}
502
563
  };
503
564
  await saveCourse(next);
504
565
  await store.removeDir(store.paths.proposal(courseId));
@@ -766,6 +827,7 @@ function createCourses({ store, llm, jobs, research, now = () => /* @__PURE__ */
766
827
  startCraft,
767
828
  submitIntake,
768
829
  revise,
830
+ addMaterials,
769
831
  applyProposal,
770
832
  discardProposal,
771
833
  reworkLesson,
@@ -2044,10 +2106,10 @@ function neededWeeklyMinutes(course, start, deadline, studyDays) {
2044
2106
  const perDay = remaining / studyDayCount / (1 - REVIEW_SHARE);
2045
2107
  return Math.ceil(perDay * studyDays.length / 10) * 10;
2046
2108
  }
2047
- const itemKey = (item) => `${item.kind}:${item.refId}`;
2109
+ const itemKey$1 = (item) => `${item.kind}:${item.refId}`;
2048
2110
  function mergeAgenda(saved = [], planned = []) {
2049
- const seen = new Set(saved.map(itemKey));
2050
- return [...saved, ...planned.filter((item) => !seen.has(itemKey(item)))];
2111
+ const seen = new Set(saved.map(itemKey$1));
2112
+ return [...saved, ...planned.filter((item) => !seen.has(itemKey$1(item)))];
2051
2113
  }
2052
2114
  function planCalendar({ today, agenda = {}, plan, done = {}, checks = [], ahead = 120 }) {
2053
2115
  const lessonsDone = done.lessons ?? /* @__PURE__ */ new Map();
@@ -2056,7 +2118,7 @@ function planCalendar({ today, agenda = {}, plan, done = {}, checks = [], ahead
2056
2118
  const days = /* @__PURE__ */ new Map();
2057
2119
  const put = (day, item) => {
2058
2120
  const list = days.get(day) ?? [];
2059
- if (!list.some((entry) => itemKey(entry) === itemKey(item))) list.push(item);
2121
+ if (!list.some((entry) => itemKey$1(entry) === itemKey$1(item))) list.push(item);
2060
2122
  days.set(day, list);
2061
2123
  };
2062
2124
  for (const [day, items] of Object.entries(agenda)) if (day <= today) for (const item of items) put(day, item);
@@ -2106,9 +2168,10 @@ function createLearner({ store, llm, now = () => /* @__PURE__ */ new Date(), log
2106
2168
  return readProfile(store);
2107
2169
  }
2108
2170
  async function saveProfile(patch = {}) {
2171
+ const given = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== void 0));
2109
2172
  return store.updateJson(store.paths.profile(), { ...EMPTY_PROFILE }, (current) => cleanProfile({
2110
2173
  ...current,
2111
- ...patch,
2174
+ ...given,
2112
2175
  updatedAt: now().toISOString()
2113
2176
  }));
2114
2177
  }
@@ -2428,6 +2491,7 @@ function list(value) {
2428
2491
  }
2429
2492
  function cleanProfile(profile) {
2430
2493
  return {
2494
+ name: text(profile.name, 20).replace(/[\r\n]+/gu, " "),
2431
2495
  background: text(profile.background, 600),
2432
2496
  goals: list(profile.goals) ?? [],
2433
2497
  strengths: list(profile.strengths) ?? [],
@@ -2751,6 +2815,16 @@ function stateBefore(state, card) {
2751
2815
  function logEvidence(progress, entry) {
2752
2816
  progress.evidenceLog = [...Array.isArray(progress.evidenceLog) ? progress.evidenceLog : [], entry].slice(-50);
2753
2817
  }
2818
+ function itemKey(source) {
2819
+ if (source?.kind === "practice") return `practice:${source.lessonId}:${source.itemId}`;
2820
+ if (source?.kind === "quiz") return `quiz:${source.unitId}:${source.itemId}`;
2821
+ if (source?.kind === "check") return `check:${source.lessonId}:${source.stepId}`;
2822
+ return null;
2823
+ }
2824
+ function answersTo(progress, key, except = null) {
2825
+ if (!key) return [];
2826
+ return (Array.isArray(progress?.evidenceLog) ? progress.evidenceLog : []).filter((entry) => entry.item?.key === key && entry.id !== except && scored(entry.evidence));
2827
+ }
2754
2828
  function correctEvidence(progress, id, patch, { known = (conceptId) => conceptId, at = (/* @__PURE__ */ new Date()).toISOString() } = {}) {
2755
2829
  const log = Array.isArray(progress.evidenceLog) ? progress.evidenceLog : [];
2756
2830
  const index = log.findIndex((entry) => entry.id === id);
@@ -2917,6 +2991,11 @@ function gradeObjective(item, response) {
2917
2991
  gradable: true
2918
2992
  };
2919
2993
  }
2994
+ function wrongLabel(item, response) {
2995
+ if (item.kind === "choice") return `chose ${String(response).toUpperCase()}: ${(item.options?.find((option) => option.id === response)?.text ?? "").slice(0, 50)}`;
2996
+ if (item.kind === "number" || item.kind === "blank") return `answered ${String(response).slice(0, 40)}`;
2997
+ return "wrong";
2998
+ }
2920
2999
  function round2(value) {
2921
3000
  return Math.round(value * 100) / 100;
2922
3001
  }
@@ -3088,6 +3167,32 @@ function regrade(item, response, checked) {
3088
3167
  partial: verdict === "partial"
3089
3168
  };
3090
3169
  }
3170
+ function earlierGrade(item, response, { verdict, score, grade = null }) {
3171
+ if (verdict !== "wrong" && verdict !== "unclear") return null;
3172
+ let next;
3173
+ if (OBJECTIVE_KINDS.includes(item.kind)) {
3174
+ const answer = cleanResponse(item, response);
3175
+ const graded = gradeObjective(item, answer);
3176
+ if (!graded.gradable) return null;
3177
+ next = {
3178
+ score: graded.score,
3179
+ correct: graded.correct,
3180
+ errorTag: graded.correct ? "" : wrongLabel(item, answer)
3181
+ };
3182
+ } else {
3183
+ if (!grade || !Number.isFinite(Number(grade.score))) return null;
3184
+ const value = Math.min(1, Math.max(0, Number(grade.score)));
3185
+ const correct = grade.verdict ? grade.verdict === "correct" : value >= .8;
3186
+ next = {
3187
+ score: value,
3188
+ correct,
3189
+ errorTag: correct ? "" : text(grade.errorTag, 80)
3190
+ };
3191
+ }
3192
+ const before = Math.min(1, Math.max(0, Number(score) || 0));
3193
+ if (verdict === "unclear" && next.score <= before || Math.abs(next.score - before) < 1e-6) return null;
3194
+ return next;
3195
+ }
3091
3196
  //#endregion
3092
3197
  //#region src/host/lessons.js
3093
3198
  const EVENTS_KEPT = 200;
@@ -3537,12 +3642,13 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
3537
3642
  };
3538
3643
  });
3539
3644
  }
3540
- async function record({ courseId, lessonId, conceptIds = [], evidence = {} }) {
3645
+ async function record({ courseId, lessonId, conceptIds = [], evidence = {}, item = null }) {
3541
3646
  const course = await courses.readCourse(courseId);
3542
3647
  const index = conceptIndex(course);
3543
3648
  const ids = (Array.isArray(conceptIds) && conceptIds.length ? conceptIds : findLesson(course, lessonId)?.lesson.concepts.map((concept) => concept.id) ?? []).filter((id) => index.has(id)).slice(0, 8);
3544
3649
  const at = now().getTime();
3545
3650
  const evidenceId = newId("evd");
3651
+ const key = item?.source?.courseId === courseId ? itemKey(item.source) : null;
3546
3652
  const next = await store.updateJson(store.paths.progress(courseId), null, (progress) => {
3547
3653
  const value = progress ?? {
3548
3654
  courseId,
@@ -3568,7 +3674,11 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
3568
3674
  lessonId: lessonId ?? null,
3569
3675
  conceptIds: ids,
3570
3676
  evidence: JSON.parse(JSON.stringify(evidence)),
3571
- before
3677
+ before,
3678
+ ...key ? { item: {
3679
+ key,
3680
+ response: item.response ?? ""
3681
+ } } : {}
3572
3682
  });
3573
3683
  pushEvent(value, {
3574
3684
  type: "evidence",
@@ -3701,6 +3811,46 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
3701
3811
  save: rewrite(path, (value) => value?.items)
3702
3812
  };
3703
3813
  }
3814
+ async function regradeEarlier({ course, key, item, verdict, except, language, signal }) {
3815
+ const answers = answersTo(await store.readJson(store.paths.progress(course.id), null), key, except);
3816
+ const open = !OBJECTIVE_KINDS.includes(item.kind);
3817
+ const graded = await Promise.all((open ? answers.slice(-3) : answers).map(async (entry) => {
3818
+ const again = open ? await grade({
3819
+ item,
3820
+ response: entry.item.response,
3821
+ language,
3822
+ signal
3823
+ }).catch((error) => {
3824
+ if (signal?.aborted) throw error;
3825
+ return null;
3826
+ }) : null;
3827
+ if (open && !again) return null;
3828
+ return {
3829
+ id: entry.id,
3830
+ next: earlierGrade(item, entry.item.response, {
3831
+ verdict,
3832
+ score: entry.evidence.score,
3833
+ grade: again
3834
+ })
3835
+ };
3836
+ }));
3837
+ let found = null;
3838
+ for (const { id, next } of graded.filter((entry) => entry?.next)) {
3839
+ const corrected = await regradeEvidence(course, id, {
3840
+ score: next.score,
3841
+ errorTag: next.errorTag
3842
+ });
3843
+ if (corrected) found = {
3844
+ count: (found?.count ?? 0) + 1,
3845
+ mastery: {
3846
+ ...found?.mastery,
3847
+ ...corrected.mastery
3848
+ },
3849
+ recommendation: corrected.recommendation
3850
+ };
3851
+ }
3852
+ return found;
3853
+ }
3704
3854
  function challengeItem({ source, response, shown = {}, evidenceId, note }) {
3705
3855
  const doubt = text(note, 800);
3706
3856
  return jobs.start("item/challenge", async (api) => {
@@ -3736,24 +3886,39 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
3736
3886
  score: result.score,
3737
3887
  ...result.correct ? { errorTag: "" } : result.errorTag ? { errorTag: result.errorTag } : {}
3738
3888
  }) : null;
3889
+ const earlier = checked.item && place.course ? await regradeEarlier({
3890
+ course: place.course,
3891
+ key: itemKey(source),
3892
+ item,
3893
+ verdict: checked.verdict,
3894
+ except: evidenceId,
3895
+ language: place.language,
3896
+ signal: api.signal
3897
+ }) : null;
3739
3898
  if (place.course) await logEvent(place.course.id, {
3740
3899
  type: "item-challenge",
3741
3900
  lessonId: place.lessonId,
3742
3901
  verdict: checked.verdict,
3743
- changed
3902
+ changed,
3903
+ earlier: earlier?.count ?? 0
3744
3904
  });
3905
+ const mastery = corrected || earlier ? {
3906
+ ...corrected?.mastery,
3907
+ ...earlier?.mastery
3908
+ } : null;
3745
3909
  return {
3746
3910
  verdict: checked.verdict,
3747
3911
  summary: checked.summary,
3748
3912
  explanation: checked.explanation,
3749
3913
  item: checked.item ? item : null,
3750
3914
  result: changed ? result : null,
3751
- mastery: corrected?.mastery ?? null,
3752
- recommendation: corrected?.recommendation ?? null
3915
+ earlier: earlier?.count ?? 0,
3916
+ mastery,
3917
+ recommendation: earlier?.recommendation ?? corrected?.recommendation ?? null
3753
3918
  };
3754
3919
  });
3755
3920
  }
3756
- async function grade({ item, response, language = "zh-CN" }) {
3921
+ async function grade({ item, response, language = "zh-CN", signal }) {
3757
3922
  if (!item?.prompt) throw new TutorError("invalid", "Missing question");
3758
3923
  const work = item.kind === "work";
3759
3924
  const answer = text(response, 4e3);
@@ -3782,6 +3947,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
3782
3947
  language
3783
3948
  }),
3784
3949
  maxTokens: work ? 1800 : 1200,
3950
+ signal,
3785
3951
  normalize: (raw) => ({
3786
3952
  score: Math.min(1, Math.max(0, Number(raw?.score) || 0)),
3787
3953
  verdict: [
@@ -3961,6 +4127,44 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
3961
4127
  if (!exists && !jobs.running(`practice:${course.id}:${row.lecture.id}`)) lecturePractice(course, row);
3962
4128
  }).catch(() => void 0);
3963
4129
  }
4130
+ async function handout({ courseId, lessonId, force = false }) {
4131
+ const { course, row } = await context(courseId, lessonId);
4132
+ if (!force) {
4133
+ const existing = await store.readJson(store.paths.handout(courseId, lessonId), null);
4134
+ if (existing) return { handout: existing };
4135
+ }
4136
+ const board = await store.readJson(store.paths.board(courseId, lessonId), null);
4137
+ if (!board?.steps?.length) throw new TutorError("no-board", "The board is not written yet. Try again once its first step is up.");
4138
+ return { job: jobs.start("lesson/handout", async (api) => {
4139
+ const made = await generateJson(llm, {
4140
+ purpose: "teach",
4141
+ effort: "lowest",
4142
+ prompt: handoutPrompt({
4143
+ course,
4144
+ lesson: row.lesson,
4145
+ language: course.language,
4146
+ board: boardToPlainText(board, { maxChars: 12e3 })
4147
+ }),
4148
+ maxTokens: 5e3,
4149
+ signal: api.signal,
4150
+ normalize: normalizeHandout
4151
+ });
4152
+ const saved = {
4153
+ ...made,
4154
+ title: made.title || row.lesson.title,
4155
+ lessonId,
4156
+ createdAt: stamp()
4157
+ };
4158
+ await store.writeJson(store.paths.handout(courseId, lessonId), saved);
4159
+ return { handout: saved };
4160
+ }, {
4161
+ key: `handout:${courseId}:${lessonId}`,
4162
+ meta: {
4163
+ courseId,
4164
+ lessonId
4165
+ }
4166
+ }) };
4167
+ }
3964
4168
  async function unitSheet({ courseId, unitId, force = false }) {
3965
4169
  const course = await courses.readCourse(courseId);
3966
4170
  const unit = course.units?.find((item) => item.id === assertId(unitId, "unt"));
@@ -4117,6 +4321,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
4117
4321
  reexplain,
4118
4322
  challenge,
4119
4323
  challengeItem,
4324
+ handout,
4120
4325
  record,
4121
4326
  grade,
4122
4327
  correct,
@@ -4459,6 +4664,7 @@ function createStore({ root }) {
4459
4664
  practice: (id, lessonId) => join(paths.courseDir(id), "practice", `${assertId(lessonId, "les")}.json`),
4460
4665
  asides: (id, lessonId) => join(paths.courseDir(id), "asides", `${assertId(lessonId, "les")}.json`),
4461
4666
  sheet: (id, unitId) => join(paths.courseDir(id), "sheets", `${assertId(unitId, "unt")}.json`),
4667
+ handout: (id, lessonId) => join(paths.courseDir(id), "handouts", `${assertId(lessonId, "les")}.json`),
4462
4668
  unitQuiz: (id, unitId) => join(paths.courseDir(id), "quizzes", `${assertId(unitId, "unt")}.json`),
4463
4669
  materials: () => join(root, "materials"),
4464
4670
  materialDir: (id) => join(root, "materials", assertId(id, "mat")),
@@ -5271,6 +5477,18 @@ function answerOf(value) {
5271
5477
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
5272
5478
  return Array.isArray(value) ? value.filter((id) => typeof id === "string" && /^[a-z]$/u.test(id)).slice(0, 12) : "";
5273
5479
  }
5480
+ function answeredItem(payload, courseId) {
5481
+ if (!payload?.source || payload.source.kind === "deck") return null;
5482
+ try {
5483
+ const source = itemSource(payload);
5484
+ return source.courseId === courseId ? {
5485
+ source,
5486
+ response: answerOf(payload.response)
5487
+ } : null;
5488
+ } catch {
5489
+ return null;
5490
+ }
5491
+ }
5274
5492
  function shownGrade(value) {
5275
5493
  const grade = value && typeof value === "object" ? value : {};
5276
5494
  const words = (key, max) => typeof grade[key] === "string" ? grade[key].slice(0, max) : "";
@@ -5337,6 +5555,11 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
5337
5555
  courseId: need(payload, "courseId", "crs"),
5338
5556
  instruction: payload.instruction
5339
5557
  }) }),
5558
+ "course/add-materials": (payload) => ({ job: courses.addMaterials({
5559
+ courseId: need(payload, "courseId", "crs"),
5560
+ materialIds: idList(payload.materialIds, "mat", 12),
5561
+ instruction: payload.instruction
5562
+ }) }),
5340
5563
  "course/save-blueprint": (payload) => courses.saveBlueprint({
5341
5564
  courseId: need(payload, "courseId", "crs"),
5342
5565
  blueprint: payload.blueprint
@@ -5402,6 +5625,7 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
5402
5625
  "lesson/record": (payload) => lessons.record({
5403
5626
  courseId: need(payload, "courseId", "crs"),
5404
5627
  lessonId: optional(payload, "lessonId", "les"),
5628
+ item: answeredItem(payload, need(payload, "courseId", "crs")),
5405
5629
  conceptIds: (Array.isArray(payload.conceptIds) ? payload.conceptIds : []).filter((id) => /^cpt_[a-z0-9]{6,40}$/u.test(String(id))).slice(0, 8),
5406
5630
  evidence: {
5407
5631
  kind: payload.evidence?.kind,
@@ -5444,6 +5668,11 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
5444
5668
  lessonId: need(payload, "lessonId", "les"),
5445
5669
  selfRating: Number(payload.selfRating) || void 0
5446
5670
  }),
5671
+ "lesson/handout": (payload) => lessons.handout({
5672
+ courseId: need(payload, "courseId", "crs"),
5673
+ lessonId: need(payload, "lessonId", "les"),
5674
+ force: payload.force === true
5675
+ }),
5447
5676
  "lesson/practice": (payload) => lessons.practice({
5448
5677
  courseId: need(payload, "courseId", "crs"),
5449
5678
  lessonId: need(payload, "lessonId", "les")
@@ -5465,6 +5694,7 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
5465
5694
  }),
5466
5695
  "profile/get": () => learner.getProfile(),
5467
5696
  "profile/save": (payload) => learner.saveProfile({
5697
+ name: payload.name,
5468
5698
  background: payload.background,
5469
5699
  goals: payload.goals,
5470
5700
  strengths: payload.strengths,
@@ -5502,6 +5732,7 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
5502
5732
  }) }),
5503
5733
  "material/url": (payload) => ({ job: studio.addUrl({ url: payload.url }) }),
5504
5734
  "material/list": () => studio.listMaterials(),
5735
+ "material/search": (payload) => studio.searchMaterials({ query: typeof payload.query === "string" ? payload.query : "" }),
5505
5736
  "material/get": (payload) => studio.getMaterial({ id: need(payload, "id", "mat") }),
5506
5737
  "material/rename": (payload) => studio.renameMaterial({
5507
5738
  id: need(payload, "id", "mat"),
@@ -6366,6 +6597,37 @@ function createStudio({ store, llm, jobs, now = () => /* @__PURE__ */ new Date()
6366
6597
  const ids = await store.listIds(store.paths.materials(), "mat");
6367
6598
  return (await Promise.all(ids.map((id) => store.readJson(store.paths.material(id), null)))).filter(Boolean).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
6368
6599
  }
6600
+ let libraryIndex = null;
6601
+ const SEARCH_CHUNKS = 3e4;
6602
+ async function searchMaterials({ query }) {
6603
+ const wanted = text(query, 200);
6604
+ if (!wanted) return { results: [] };
6605
+ const metas = (await listMaterials()).filter((meta) => meta.kind !== "image");
6606
+ const key = metas.map((meta) => `${meta.id}:${meta.title}:${meta.chars ?? meta.pages ?? ""}`).join("|");
6607
+ if (libraryIndex?.key !== key) {
6608
+ const chunks = [];
6609
+ for (const meta of metas) {
6610
+ if (chunks.length >= SEARCH_CHUNKS) break;
6611
+ const own = await store.readJson(store.paths.materialChunks(meta.id), []) ?? [];
6612
+ for (const chunk of own.slice(0, SEARCH_CHUNKS - chunks.length)) chunks.push({
6613
+ ...chunk,
6614
+ materialId: meta.id,
6615
+ materialTitle: meta.title
6616
+ });
6617
+ }
6618
+ libraryIndex = {
6619
+ key,
6620
+ index: buildIndex(chunks)
6621
+ };
6622
+ }
6623
+ return { results: search(libraryIndex.index, wanted, { limit: 12 }).map(({ chunk }) => ({
6624
+ materialId: chunk.materialId,
6625
+ title: chunk.materialTitle,
6626
+ page: chunk.page ?? null,
6627
+ slide: chunk.slide ?? null,
6628
+ snippet: snippetOf(chunk.text, wanted)
6629
+ })) };
6630
+ }
6369
6631
  async function getMaterial({ id }) {
6370
6632
  const meta = await store.readJson(store.paths.material(assertId(id, "mat")), null);
6371
6633
  if (!meta) throw new TutorError("not-found", "Material not found");
@@ -6806,6 +7068,7 @@ function createStudio({ store, llm, jobs, now = () => /* @__PURE__ */ new Date()
6806
7068
  upload,
6807
7069
  addUrl,
6808
7070
  listMaterials,
7071
+ searchMaterials,
6809
7072
  getMaterial,
6810
7073
  renameMaterial,
6811
7074
  removeMaterial,