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/CHANGELOG.md +22 -0
- package/README.en.md +3 -2
- package/README.md +3 -2
- package/docs/guide.zh-CN.md +18 -5
- package/lib/agent.js +1 -1
- package/lib/client.js +103 -31
- package/lib/index.js +276 -13
- package/lib/{runtime-B6zUpJjA.js → runtime-DfoeKFPj.js} +245 -151
- package/package.json +1 -1
- package/samples/bayes-thinking.aha-course.json +62 -56
- package/samples/harmonic-motion.aha-course.json +70 -63
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
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
|
-
...
|
|
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
|
-
|
|
3752
|
-
|
|
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,
|