dsh-aha-tutor 1.1.0 → 1.3.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/AGENTS.md +5 -3
- package/CHANGELOG.md +38 -0
- package/README.en.md +7 -7
- package/README.md +8 -8
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/assets/fonts/OFL-Caveat.txt +93 -0
- package/assets/fonts/OFL-LXGWWenKaiGB.txt +94 -0
- package/assets/fonts/caveat-latin-d0b7b931.woff2 +0 -0
- package/assets/fonts/wenkai-gb-0-88f1a875.woff2 +0 -0
- package/assets/fonts/wenkai-gb-1-78a38e72.woff2 +0 -0
- package/assets/fonts/wenkai-gb-2-2a649892.woff2 +0 -0
- package/assets/fonts/wenkai-gb-3-6949c050.woff2 +0 -0
- package/assets/fonts/wenkai-gb-4-375dc3b3.woff2 +0 -0
- package/assets/fonts/wenkai-gb-5-d0359bbc.woff2 +0 -0
- package/assets/fonts/wenkai-gb-6-a238b41c.woff2 +0 -0
- package/docs/guide.zh-CN.md +26 -14
- package/lib/agent.js +1 -1
- package/lib/client.js +109 -33
- package/lib/index.js +845 -143
- package/lib/{runtime-CiA2I7-X.js → runtime-BCGFUDlg.js} +345 -178
- package/package.json +2 -1
- package/samples/bayes-thinking.aha-course.json +51 -51
- package/samples/harmonic-motion.aha-course.json +56 -56
|
@@ -240,12 +240,34 @@ function normalizeBlueprint(raw, { language } = {}) {
|
|
|
240
240
|
projects: list(raw?.projects, LIMITS.projects, normalizeProject)
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
|
+
function blueprintOutline(raw) {
|
|
244
|
+
const units = list(raw?.units, LIMITS.units, (unit) => {
|
|
245
|
+
const title = text(unit?.title, 120);
|
|
246
|
+
if (!title) return null;
|
|
247
|
+
return {
|
|
248
|
+
title,
|
|
249
|
+
lectures: list(unit?.lectures, LIMITS.lecturesPerUnit, (lecture) => {
|
|
250
|
+
const lessons = strings(lecture?.lessons, LIMITS.lessonsPerLecture, 120);
|
|
251
|
+
const name = text(lecture?.title, 120);
|
|
252
|
+
return name || lessons.length ? {
|
|
253
|
+
title: name,
|
|
254
|
+
lessons
|
|
255
|
+
} : null;
|
|
256
|
+
})
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
title: text(raw?.title, 120),
|
|
261
|
+
units
|
|
262
|
+
};
|
|
263
|
+
}
|
|
243
264
|
const ITEM_KINDS = [
|
|
244
265
|
"choice",
|
|
245
266
|
"multi",
|
|
246
267
|
"blank",
|
|
247
268
|
"number",
|
|
248
269
|
"short",
|
|
270
|
+
"work",
|
|
249
271
|
"order"
|
|
250
272
|
];
|
|
251
273
|
function normalizeItem(raw, index = 0, conceptIds = []) {
|
|
@@ -261,6 +283,8 @@ function normalizeItem(raw, index = 0, conceptIds = []) {
|
|
|
261
283
|
difficulty: Math.min(3, Math.max(1, Math.round(Number(raw?.difficulty) || 2))),
|
|
262
284
|
conceptIds: (Array.isArray(raw?.conceptIds) ? raw.conceptIds : []).filter((id) => conceptIds.includes(id)).slice(0, 4)
|
|
263
285
|
};
|
|
286
|
+
const figure = text(raw?.svg ?? raw?.figure, 16e3);
|
|
287
|
+
if (/^<svg[\s>]/iu.test(figure)) item.svg = figure;
|
|
264
288
|
if (kind === "choice" || kind === "multi" || kind === "order") {
|
|
265
289
|
item.options = list(raw?.options, LIMITS.options4, (option, optionIndex) => {
|
|
266
290
|
const label = text(typeof option === "string" ? option : option?.text ?? option?.label, 400);
|
|
@@ -667,14 +691,28 @@ function normalizeOp(raw, index) {
|
|
|
667
691
|
default: return null;
|
|
668
692
|
}
|
|
669
693
|
}
|
|
694
|
+
function syncOps(ops, rawAts) {
|
|
695
|
+
if (!rawAts.some((value) => value !== null)) return;
|
|
696
|
+
let current = 0;
|
|
697
|
+
ops.forEach((op, index) => {
|
|
698
|
+
current = Math.max(current, rawAts[index] ?? current);
|
|
699
|
+
op.at = current;
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function sentenceIndex(value) {
|
|
703
|
+
const number = Number(value);
|
|
704
|
+
return value !== null && value !== "" && Number.isInteger(number) && number >= 0 ? Math.min(19, number) : null;
|
|
705
|
+
}
|
|
670
706
|
function normalizeBoard(raw, { conceptIds = [] } = {}) {
|
|
671
707
|
const sourceSteps = Array.isArray(raw) ? raw : Array.isArray(raw?.steps) ? raw.steps : [];
|
|
672
708
|
const steps = [];
|
|
673
709
|
let opCounter = 0;
|
|
710
|
+
let worked = false;
|
|
674
711
|
const idMap = /* @__PURE__ */ new Map();
|
|
675
712
|
for (const rawStep of sourceSteps.slice(0, BOARD_LIMITS.steps)) {
|
|
676
713
|
const say = text(rawStep?.say ?? rawStep?.narration ?? rawStep?.speech, BOARD_LIMITS.say);
|
|
677
714
|
const ops = [];
|
|
715
|
+
const ats = [];
|
|
678
716
|
for (const rawOp of (Array.isArray(rawStep?.ops ?? rawStep?.board) ? rawStep.ops ?? rawStep.board : []).slice(0, BOARD_LIMITS.opsPerStep)) {
|
|
679
717
|
const op = normalizeOp(rawOp, opCounter);
|
|
680
718
|
if (!op) continue;
|
|
@@ -682,13 +720,22 @@ function normalizeBoard(raw, { conceptIds = [] } = {}) {
|
|
|
682
720
|
op.id = `o${++opCounter}`;
|
|
683
721
|
if (originalId) idMap.set(originalId, op.id);
|
|
684
722
|
ops.push(op);
|
|
723
|
+
ats.push(sentenceIndex(rawOp?.at));
|
|
685
724
|
}
|
|
686
|
-
|
|
725
|
+
syncOps(ops, ats);
|
|
726
|
+
let check = rawStep?.check ? normalizeItem(rawStep.check, steps.length, conceptIds) : null;
|
|
727
|
+
if (check?.kind === "work") if (worked) check = {
|
|
728
|
+
...check,
|
|
729
|
+
kind: "short"
|
|
730
|
+
};
|
|
731
|
+
else worked = true;
|
|
687
732
|
if (!say && !ops.length && !check) continue;
|
|
733
|
+
const goal = sentenceIndex(rawStep?.goal);
|
|
688
734
|
steps.push({
|
|
689
735
|
id: `s${steps.length + 1}`,
|
|
690
736
|
say,
|
|
691
737
|
ops,
|
|
738
|
+
...goal !== null && goal < 12 ? { goal } : {},
|
|
692
739
|
...check && check.kind !== "order" ? { check: {
|
|
693
740
|
...check,
|
|
694
741
|
id: `k${steps.length + 1}`
|
|
@@ -723,6 +770,164 @@ function boardToPlainText(board, { maxChars = 8e3, uptoStep } = {}) {
|
|
|
723
770
|
return output.length > maxChars ? `${output.slice(0, maxChars)}\n…` : output;
|
|
724
771
|
}
|
|
725
772
|
//#endregion
|
|
773
|
+
//#region src/core/json-extract.js
|
|
774
|
+
var JsonExtractError = class extends Error {
|
|
775
|
+
constructor(message, { cause, text } = {}) {
|
|
776
|
+
super(message, cause ? { cause } : void 0);
|
|
777
|
+
this.name = "JsonExtractError";
|
|
778
|
+
this.preview = typeof text === "string" ? text.slice(0, 200) : void 0;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
const FENCE = /```(?:json|jsonl|javascript|js)?\s*\n?([\s\S]*?)```/giu;
|
|
782
|
+
function stripTrailingCommas(text) {
|
|
783
|
+
let out = "";
|
|
784
|
+
let inString = false;
|
|
785
|
+
let escaped = false;
|
|
786
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
787
|
+
const char = text[index];
|
|
788
|
+
if (inString) {
|
|
789
|
+
out += char;
|
|
790
|
+
if (escaped) escaped = false;
|
|
791
|
+
else if (char === "\\") escaped = true;
|
|
792
|
+
else if (char === "\"") inString = false;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (char === "\"") {
|
|
796
|
+
inString = true;
|
|
797
|
+
out += char;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (char === ",") {
|
|
801
|
+
let next = index + 1;
|
|
802
|
+
while (next < text.length && /\s/u.test(text[next])) next += 1;
|
|
803
|
+
if (text[next] === "}" || text[next] === "]") continue;
|
|
804
|
+
}
|
|
805
|
+
out += char;
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
809
|
+
function balancedSlice(text, from = 0) {
|
|
810
|
+
const start = text.slice(from).search(/[{[]/u);
|
|
811
|
+
if (start < 0) return void 0;
|
|
812
|
+
const begin = from + start;
|
|
813
|
+
const stack = [];
|
|
814
|
+
let inString = false;
|
|
815
|
+
let escaped = false;
|
|
816
|
+
for (let index = begin; index < text.length; index += 1) {
|
|
817
|
+
const char = text[index];
|
|
818
|
+
if (inString) {
|
|
819
|
+
if (escaped) escaped = false;
|
|
820
|
+
else if (char === "\\") escaped = true;
|
|
821
|
+
else if (char === "\"") inString = false;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (char === "\"") inString = true;
|
|
825
|
+
else if (char === "{" || char === "[") stack.push(char);
|
|
826
|
+
else if (char === "}" || char === "]") {
|
|
827
|
+
const open = stack.pop();
|
|
828
|
+
if (char === "}" && open !== "{" || char === "]" && open !== "[") return void 0;
|
|
829
|
+
if (stack.length === 0) return {
|
|
830
|
+
begin,
|
|
831
|
+
end: index + 1,
|
|
832
|
+
text: text.slice(begin, index + 1)
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
begin,
|
|
838
|
+
end: text.length,
|
|
839
|
+
text: text.slice(begin),
|
|
840
|
+
truncated: true,
|
|
841
|
+
stack
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function repairTruncated(text) {
|
|
845
|
+
let body = text;
|
|
846
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
847
|
+
const closed = closeOpen(body);
|
|
848
|
+
if (closed !== void 0) try {
|
|
849
|
+
return JSON.parse(stripTrailingCommas(closed));
|
|
850
|
+
} catch {}
|
|
851
|
+
const cut = Math.max(body.lastIndexOf(","), body.lastIndexOf("{"), body.lastIndexOf("["));
|
|
852
|
+
if (cut <= 0) break;
|
|
853
|
+
body = body.slice(0, body[cut] === "," ? cut : cut + 1);
|
|
854
|
+
}
|
|
855
|
+
throw new JsonExtractError("model output was truncated beyond repair", { text });
|
|
856
|
+
}
|
|
857
|
+
function closeOpen(text) {
|
|
858
|
+
const stack = [];
|
|
859
|
+
let inString = false;
|
|
860
|
+
let escaped = false;
|
|
861
|
+
for (const char of text) {
|
|
862
|
+
if (inString) {
|
|
863
|
+
if (escaped) escaped = false;
|
|
864
|
+
else if (char === "\\") escaped = true;
|
|
865
|
+
else if (char === "\"") inString = false;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (char === "\"") inString = true;
|
|
869
|
+
else if (char === "{" || char === "[") stack.push(char);
|
|
870
|
+
else if (char === "}" || char === "]") stack.pop();
|
|
871
|
+
}
|
|
872
|
+
if (inString) return void 0;
|
|
873
|
+
return text + stack.reverse().map((open) => open === "{" ? "}" : "]").join("");
|
|
874
|
+
}
|
|
875
|
+
function extractJson(raw, { allowTruncated = true } = {}) {
|
|
876
|
+
if (raw !== null && typeof raw === "object") return raw;
|
|
877
|
+
const text = String(raw ?? "").replace(/^/u, "").trim();
|
|
878
|
+
if (!text) throw new JsonExtractError("model returned an empty response", { text });
|
|
879
|
+
const candidates = [];
|
|
880
|
+
for (const match of text.matchAll(FENCE)) candidates.push(match[1].trim());
|
|
881
|
+
candidates.push(text);
|
|
882
|
+
let lastError;
|
|
883
|
+
for (const candidate of candidates) {
|
|
884
|
+
try {
|
|
885
|
+
return JSON.parse(candidate);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
lastError = error;
|
|
888
|
+
}
|
|
889
|
+
const slice = balancedSlice(candidate);
|
|
890
|
+
if (slice === void 0) continue;
|
|
891
|
+
if (!slice.truncated) try {
|
|
892
|
+
return JSON.parse(stripTrailingCommas(slice.text));
|
|
893
|
+
} catch (error) {
|
|
894
|
+
lastError = error;
|
|
895
|
+
}
|
|
896
|
+
else if (allowTruncated) try {
|
|
897
|
+
return repairTruncated(slice.text);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
lastError = error;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
throw new JsonExtractError("model output did not contain valid JSON", {
|
|
903
|
+
cause: lastError,
|
|
904
|
+
text
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
function parseJsonLines(raw, { final = true } = {}) {
|
|
908
|
+
const lines = String(raw ?? "").replace(/```(?:jsonl|json)?/giu, "").split(/\r?\n/u);
|
|
909
|
+
const rest = final ? "" : lines.pop() ?? "";
|
|
910
|
+
const items = [];
|
|
911
|
+
const errors = [];
|
|
912
|
+
for (const line of lines) {
|
|
913
|
+
const trimmed = line.trim();
|
|
914
|
+
if (!trimmed || !/^[{[]/u.test(trimmed)) continue;
|
|
915
|
+
try {
|
|
916
|
+
items.push(JSON.parse(stripTrailingCommas(trimmed)));
|
|
917
|
+
} catch (error) {
|
|
918
|
+
errors.push({
|
|
919
|
+
line: trimmed.slice(0, 120),
|
|
920
|
+
message: error.message
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
items,
|
|
926
|
+
errors,
|
|
927
|
+
rest
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
//#endregion
|
|
726
931
|
//#region src/core/mastery.js
|
|
727
932
|
const BKT_DEFAULTS = Object.freeze({
|
|
728
933
|
pInit: .2,
|
|
@@ -868,6 +1073,13 @@ function pushHistory(state, entry) {
|
|
|
868
1073
|
function masteryPercent(state) {
|
|
869
1074
|
return Math.round(clamp(Number(state?.pKnown) || 0, 0, 1) * 100);
|
|
870
1075
|
}
|
|
1076
|
+
Object.freeze([
|
|
1077
|
+
"none",
|
|
1078
|
+
"weak",
|
|
1079
|
+
"learning",
|
|
1080
|
+
"proficient",
|
|
1081
|
+
"mastered"
|
|
1082
|
+
]);
|
|
871
1083
|
function masteryLevel(state) {
|
|
872
1084
|
const p = clamp(Number(state?.pKnown) || 0, 0, 1);
|
|
873
1085
|
let level = MASTERY_LEVELS[0].id;
|
|
@@ -1014,10 +1226,53 @@ function selectPassages(chunks, query, { maxChars = 12e3, limit = 12 } = {}) {
|
|
|
1014
1226
|
}
|
|
1015
1227
|
return result;
|
|
1016
1228
|
}
|
|
1229
|
+
function spreadPassages(chunks, { maxChars = 12e3, samples = 16 } = {}) {
|
|
1230
|
+
if (!chunks?.length) return [];
|
|
1231
|
+
if (chunks.reduce((sum, chunk) => sum + chunk.text.length, 0) <= maxChars) return chunks.slice();
|
|
1232
|
+
const count = Math.min(chunks.length, Math.max(2, samples));
|
|
1233
|
+
const share = Math.max(200, Math.floor(maxChars / count));
|
|
1234
|
+
const picked = [];
|
|
1235
|
+
for (let index = 0; index < count; index += 1) {
|
|
1236
|
+
const at = Math.round(index * (chunks.length - 1) / (count - 1));
|
|
1237
|
+
if (picked.at(-1) !== at) picked.push(at);
|
|
1238
|
+
}
|
|
1239
|
+
return picked.map((at) => {
|
|
1240
|
+
const chunk = chunks[at];
|
|
1241
|
+
return chunk.text.length > share ? {
|
|
1242
|
+
...chunk,
|
|
1243
|
+
text: `${chunk.text.slice(0, share)}…`
|
|
1244
|
+
} : chunk;
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
function documentParts(chunks, { partChars = 2e4, maxParts = 24, maxPartChars = 4e4 } = {}) {
|
|
1248
|
+
if (!chunks?.length) return [];
|
|
1249
|
+
const total = Math.max(1, chunks.reduce((sum, chunk) => sum + chunk.text.length, 0));
|
|
1250
|
+
const count = Math.min(maxParts, Math.max(1, Math.ceil(total / partChars)));
|
|
1251
|
+
const parts = Array.from({ length: count }, () => []);
|
|
1252
|
+
let seen = 0;
|
|
1253
|
+
for (const chunk of chunks) {
|
|
1254
|
+
parts[Math.min(count - 1, Math.floor(seen / total * count))].push(chunk);
|
|
1255
|
+
seen += chunk.text.length;
|
|
1256
|
+
}
|
|
1257
|
+
return parts.filter((part) => part.length).map((part) => {
|
|
1258
|
+
return part.reduce((sum, chunk) => sum + chunk.text.length, 0) > maxPartChars ? spreadPassages(part, {
|
|
1259
|
+
maxChars: maxPartChars,
|
|
1260
|
+
samples: 24
|
|
1261
|
+
}) : part;
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
function locationOf(passage) {
|
|
1265
|
+
return passage?.page ? `p.${passage.page}` : passage?.slide ? `slide ${passage.slide}` : passage?.section ?? "";
|
|
1266
|
+
}
|
|
1267
|
+
function passageRange(passages) {
|
|
1268
|
+
const first = locationOf(passages?.[0]);
|
|
1269
|
+
const last = locationOf(passages?.at(-1));
|
|
1270
|
+
return first && last && first !== last ? `${first}–${last}` : first || last;
|
|
1271
|
+
}
|
|
1017
1272
|
function formatPassages(passages, sourceTitle) {
|
|
1018
1273
|
return passages.map((passage) => {
|
|
1019
|
-
const where = passage
|
|
1020
|
-
return `<<${sourceTitle ?? passage.source ?? "source"
|
|
1274
|
+
const where = locationOf(passage);
|
|
1275
|
+
return `<<${[sourceTitle ?? passage.source ?? "source", where].filter(Boolean).join(" · ") || "excerpt"}>>\n${passage.text}`;
|
|
1021
1276
|
}).join("\n\n");
|
|
1022
1277
|
}
|
|
1023
1278
|
//#endregion
|
|
@@ -1117,164 +1372,6 @@ async function materialImages(store, materials) {
|
|
|
1117
1372
|
return images;
|
|
1118
1373
|
}
|
|
1119
1374
|
//#endregion
|
|
1120
|
-
//#region src/core/json-extract.js
|
|
1121
|
-
var JsonExtractError = class extends Error {
|
|
1122
|
-
constructor(message, { cause, text } = {}) {
|
|
1123
|
-
super(message, cause ? { cause } : void 0);
|
|
1124
|
-
this.name = "JsonExtractError";
|
|
1125
|
-
this.preview = typeof text === "string" ? text.slice(0, 200) : void 0;
|
|
1126
|
-
}
|
|
1127
|
-
};
|
|
1128
|
-
const FENCE = /```(?:json|jsonl|javascript|js)?\s*\n?([\s\S]*?)```/giu;
|
|
1129
|
-
function stripTrailingCommas(text) {
|
|
1130
|
-
let out = "";
|
|
1131
|
-
let inString = false;
|
|
1132
|
-
let escaped = false;
|
|
1133
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
1134
|
-
const char = text[index];
|
|
1135
|
-
if (inString) {
|
|
1136
|
-
out += char;
|
|
1137
|
-
if (escaped) escaped = false;
|
|
1138
|
-
else if (char === "\\") escaped = true;
|
|
1139
|
-
else if (char === "\"") inString = false;
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
if (char === "\"") {
|
|
1143
|
-
inString = true;
|
|
1144
|
-
out += char;
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
if (char === ",") {
|
|
1148
|
-
let next = index + 1;
|
|
1149
|
-
while (next < text.length && /\s/u.test(text[next])) next += 1;
|
|
1150
|
-
if (text[next] === "}" || text[next] === "]") continue;
|
|
1151
|
-
}
|
|
1152
|
-
out += char;
|
|
1153
|
-
}
|
|
1154
|
-
return out;
|
|
1155
|
-
}
|
|
1156
|
-
function balancedSlice(text, from = 0) {
|
|
1157
|
-
const start = text.slice(from).search(/[{[]/u);
|
|
1158
|
-
if (start < 0) return void 0;
|
|
1159
|
-
const begin = from + start;
|
|
1160
|
-
const stack = [];
|
|
1161
|
-
let inString = false;
|
|
1162
|
-
let escaped = false;
|
|
1163
|
-
for (let index = begin; index < text.length; index += 1) {
|
|
1164
|
-
const char = text[index];
|
|
1165
|
-
if (inString) {
|
|
1166
|
-
if (escaped) escaped = false;
|
|
1167
|
-
else if (char === "\\") escaped = true;
|
|
1168
|
-
else if (char === "\"") inString = false;
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
if (char === "\"") inString = true;
|
|
1172
|
-
else if (char === "{" || char === "[") stack.push(char);
|
|
1173
|
-
else if (char === "}" || char === "]") {
|
|
1174
|
-
const open = stack.pop();
|
|
1175
|
-
if (char === "}" && open !== "{" || char === "]" && open !== "[") return void 0;
|
|
1176
|
-
if (stack.length === 0) return {
|
|
1177
|
-
begin,
|
|
1178
|
-
end: index + 1,
|
|
1179
|
-
text: text.slice(begin, index + 1)
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
return {
|
|
1184
|
-
begin,
|
|
1185
|
-
end: text.length,
|
|
1186
|
-
text: text.slice(begin),
|
|
1187
|
-
truncated: true,
|
|
1188
|
-
stack
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
function repairTruncated(text) {
|
|
1192
|
-
let body = text;
|
|
1193
|
-
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
1194
|
-
const closed = closeOpen(body);
|
|
1195
|
-
if (closed !== void 0) try {
|
|
1196
|
-
return JSON.parse(stripTrailingCommas(closed));
|
|
1197
|
-
} catch {}
|
|
1198
|
-
const cut = Math.max(body.lastIndexOf(","), body.lastIndexOf("{"), body.lastIndexOf("["));
|
|
1199
|
-
if (cut <= 0) break;
|
|
1200
|
-
body = body.slice(0, body[cut] === "," ? cut : cut + 1);
|
|
1201
|
-
}
|
|
1202
|
-
throw new JsonExtractError("model output was truncated beyond repair", { text });
|
|
1203
|
-
}
|
|
1204
|
-
function closeOpen(text) {
|
|
1205
|
-
const stack = [];
|
|
1206
|
-
let inString = false;
|
|
1207
|
-
let escaped = false;
|
|
1208
|
-
for (const char of text) {
|
|
1209
|
-
if (inString) {
|
|
1210
|
-
if (escaped) escaped = false;
|
|
1211
|
-
else if (char === "\\") escaped = true;
|
|
1212
|
-
else if (char === "\"") inString = false;
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1215
|
-
if (char === "\"") inString = true;
|
|
1216
|
-
else if (char === "{" || char === "[") stack.push(char);
|
|
1217
|
-
else if (char === "}" || char === "]") stack.pop();
|
|
1218
|
-
}
|
|
1219
|
-
if (inString) return void 0;
|
|
1220
|
-
return text + stack.reverse().map((open) => open === "{" ? "}" : "]").join("");
|
|
1221
|
-
}
|
|
1222
|
-
function extractJson(raw, { allowTruncated = true } = {}) {
|
|
1223
|
-
if (raw !== null && typeof raw === "object") return raw;
|
|
1224
|
-
const text = String(raw ?? "").replace(/^/u, "").trim();
|
|
1225
|
-
if (!text) throw new JsonExtractError("model returned an empty response", { text });
|
|
1226
|
-
const candidates = [];
|
|
1227
|
-
for (const match of text.matchAll(FENCE)) candidates.push(match[1].trim());
|
|
1228
|
-
candidates.push(text);
|
|
1229
|
-
let lastError;
|
|
1230
|
-
for (const candidate of candidates) {
|
|
1231
|
-
try {
|
|
1232
|
-
return JSON.parse(candidate);
|
|
1233
|
-
} catch (error) {
|
|
1234
|
-
lastError = error;
|
|
1235
|
-
}
|
|
1236
|
-
const slice = balancedSlice(candidate);
|
|
1237
|
-
if (slice === void 0) continue;
|
|
1238
|
-
if (!slice.truncated) try {
|
|
1239
|
-
return JSON.parse(stripTrailingCommas(slice.text));
|
|
1240
|
-
} catch (error) {
|
|
1241
|
-
lastError = error;
|
|
1242
|
-
}
|
|
1243
|
-
else if (allowTruncated) try {
|
|
1244
|
-
return repairTruncated(slice.text);
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
lastError = error;
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
throw new JsonExtractError("model output did not contain valid JSON", {
|
|
1250
|
-
cause: lastError,
|
|
1251
|
-
text
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
function parseJsonLines(raw, { final = true } = {}) {
|
|
1255
|
-
const lines = String(raw ?? "").replace(/```(?:jsonl|json)?/giu, "").split(/\r?\n/u);
|
|
1256
|
-
const rest = final ? "" : lines.pop() ?? "";
|
|
1257
|
-
const items = [];
|
|
1258
|
-
const errors = [];
|
|
1259
|
-
for (const line of lines) {
|
|
1260
|
-
const trimmed = line.trim();
|
|
1261
|
-
if (!trimmed || !/^[{[]/u.test(trimmed)) continue;
|
|
1262
|
-
try {
|
|
1263
|
-
items.push(JSON.parse(stripTrailingCommas(trimmed)));
|
|
1264
|
-
} catch (error) {
|
|
1265
|
-
errors.push({
|
|
1266
|
-
line: trimmed.slice(0, 120),
|
|
1267
|
-
message: error.message
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
return {
|
|
1272
|
-
items,
|
|
1273
|
-
errors,
|
|
1274
|
-
rest
|
|
1275
|
-
};
|
|
1276
|
-
}
|
|
1277
|
-
//#endregion
|
|
1278
1375
|
//#region src/host/generate.js
|
|
1279
1376
|
var TutorError = class extends Error {
|
|
1280
1377
|
constructor(code, message) {
|
|
@@ -1284,15 +1381,19 @@ var TutorError = class extends Error {
|
|
|
1284
1381
|
}
|
|
1285
1382
|
};
|
|
1286
1383
|
const REPAIR = "Your previous reply could not be parsed as JSON. Reply again with the complete answer as valid JSON only — no code fences, no commentary.";
|
|
1287
|
-
async function generateJson(llm, { purpose = "teach", prompt, maxTokens = 8e3, images, signal, normalize = (value) => value }) {
|
|
1288
|
-
const
|
|
1384
|
+
async function generateJson(llm, { purpose = "teach", prompt, maxTokens = 8e3, images, signal, normalize = (value) => value, onText }) {
|
|
1385
|
+
const request = {
|
|
1289
1386
|
purpose,
|
|
1290
1387
|
system: prompt.system,
|
|
1291
1388
|
user: prompt.user,
|
|
1292
1389
|
images,
|
|
1293
1390
|
maxTokens,
|
|
1294
1391
|
signal
|
|
1295
|
-
}
|
|
1392
|
+
};
|
|
1393
|
+
const first = onText ? await llm.stream({
|
|
1394
|
+
...request,
|
|
1395
|
+
onText
|
|
1396
|
+
}) : await llm.complete(request);
|
|
1296
1397
|
try {
|
|
1297
1398
|
return normalize(extractJson(first));
|
|
1298
1399
|
} catch (error) {
|
|
@@ -1313,7 +1414,7 @@ async function generateJson(llm, { purpose = "teach", prompt, maxTokens = 8e3, i
|
|
|
1313
1414
|
}
|
|
1314
1415
|
}
|
|
1315
1416
|
}
|
|
1316
|
-
async function generateSteps(llm, { purpose = "teach", prompt, maxTokens = 12e3, signal, conceptIds = [], onStep, minSteps = 1, images }) {
|
|
1417
|
+
async function generateSteps(llm, { purpose = "teach", effort, prompt, maxTokens = 12e3, signal, conceptIds = [], onStep, minSteps = 1, images }) {
|
|
1317
1418
|
const raw = [];
|
|
1318
1419
|
let buffer = "";
|
|
1319
1420
|
let emitted = 0;
|
|
@@ -1340,6 +1441,7 @@ async function generateSteps(llm, { purpose = "teach", prompt, maxTokens = 12e3,
|
|
|
1340
1441
|
};
|
|
1341
1442
|
const full = await llm.stream({
|
|
1342
1443
|
purpose,
|
|
1444
|
+
effort,
|
|
1343
1445
|
system: prompt.system,
|
|
1344
1446
|
user: prompt.user,
|
|
1345
1447
|
images,
|
|
@@ -1369,6 +1471,20 @@ async function generateSteps(llm, { purpose = "teach", prompt, maxTokens = 12e3,
|
|
|
1369
1471
|
}
|
|
1370
1472
|
return board;
|
|
1371
1473
|
}
|
|
1474
|
+
function streamCounter(marker, onCount) {
|
|
1475
|
+
let tail = "";
|
|
1476
|
+
let count = 0;
|
|
1477
|
+
return (delta) => {
|
|
1478
|
+
const textValue = tail + String(delta ?? "");
|
|
1479
|
+
let found = 0;
|
|
1480
|
+
for (let at = textValue.indexOf(marker); at !== -1; at = textValue.indexOf(marker, at + marker.length)) found += 1;
|
|
1481
|
+
tail = textValue.slice(-(marker.length - 1));
|
|
1482
|
+
if (found) {
|
|
1483
|
+
count += found;
|
|
1484
|
+
onCount(count);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1372
1488
|
async function generateText(llm, { purpose = "teach", prompt, maxTokens = 6e3, signal, onText }) {
|
|
1373
1489
|
const textValue = onText ? await llm.stream({
|
|
1374
1490
|
purpose,
|
|
@@ -1462,8 +1578,11 @@ Fill the left column, then {"t":"col"}, then the right column; start a new page
|
|
|
1462
1578
|
const STEP_SPEC = `Each output line is ONE teaching step:
|
|
1463
1579
|
{"say":"what you say while writing — natural spoken sentences, 1–4 sentences, no markdown","ops":[ …board ops written during this step… ],"check":{ …optional quick question… }}
|
|
1464
1580
|
|
|
1581
|
+
Write the board in time with your voice: give every op "at", the 0-based index of the sentence in "say" during which you write it, so each item appears while the sentence about it is spoken (sentences end at 。!?;.!?;). "at" never goes down within a step: {"say":"先看这个公式。它说明力与位移方向相反。","ops":[{"t":"math","tex":"F=-kx","at":0},{"t":"p","text":"负号 = 方向相反","at":1}]}
|
|
1582
|
+
|
|
1465
1583
|
A check is a short question asked right after the step, to see whether the idea landed:
|
|
1466
|
-
{"kind":"choice|multi|blank|number|short","prompt":"…","options":["…","…"],"answer":<option index (0-based) for choice, list of indexes for multi, accepted strings for blank, a number for number, a model answer for short>,"tolerance":<number, for number>,"explanation":"why the answer is right, in one or two sentences","conceptIds":["<ids from the concept list>"]}
|
|
1584
|
+
{"kind":"choice|multi|blank|number|short|work","prompt":"…","options":["…","…"],"answer":<option index (0-based) for choice, list of indexes for multi, accepted strings for blank, a number for number, a model answer for short, the full model working for work>,"tolerance":<number, for number>,"rubric":["…"],"explanation":"why the answer is right, in one or two sentences","conceptIds":["<ids from the concept list>"]}
|
|
1585
|
+
A "work" check has the learner write out a whole calculation or derivation: "answer" is the model working, one step per line with inline math $…$, and "rubric" lists the 2–4 steps a correct working must contain. Use "work" only where the instructions ask for it.
|
|
1467
1586
|
Make wrong options plausible (common mistakes), never trick questions.`;
|
|
1468
1587
|
function questionnairePrompt({ goal, language, materialsSummary, profile }) {
|
|
1469
1588
|
return {
|
|
@@ -1473,14 +1592,33 @@ function questionnairePrompt({ goal, language, materialsSummary, profile }) {
|
|
|
1473
1592
|
Return {"questions":[{"prompt":"…","topic":"purpose|level|timeline|style|focus|other","multi":false,"options":[{"label":"…","detail":"…"}]}]}`)
|
|
1474
1593
|
};
|
|
1475
1594
|
}
|
|
1476
|
-
|
|
1595
|
+
const SIZE_RANGES = Object.freeze({
|
|
1596
|
+
short: [
|
|
1597
|
+
3,
|
|
1598
|
+
8,
|
|
1599
|
+
"1–2"
|
|
1600
|
+
],
|
|
1601
|
+
standard: [
|
|
1602
|
+
10,
|
|
1603
|
+
20,
|
|
1604
|
+
"2–4"
|
|
1605
|
+
],
|
|
1606
|
+
full: [
|
|
1607
|
+
25,
|
|
1608
|
+
45,
|
|
1609
|
+
"4–8"
|
|
1610
|
+
]
|
|
1611
|
+
});
|
|
1612
|
+
function sizeGuidance(timeline, size) {
|
|
1613
|
+
const range = SIZE_RANGES[size];
|
|
1614
|
+
if (range) return `The learner chose the course size: ${range[0]}–${range[1]} lessons in the whole course, in ${range[2]} units. Count every lesson across all units before you answer: never more than ${range[1]}; if the subject seems to need more, merge closely related lessons. The pace only spreads the lessons over the weeks. Never pad with filler lessons.`;
|
|
1477
1615
|
return `Size the course to the goal first. When the goal is a single idea or names a time budget ("in 20 minutes", "one evening", "this weekend"), fit it: one unit with a single lecture of 1–4 lessons can be the whole course. Never pad with filler lessons. ${timeline === "intensive" ? "Intensive pace: for a broad subject, up to 4–6 units and 18–30 lessons, focused on what matters most for the goal." : timeline === "relaxed" ? "No fixed deadline: for a broad subject, up to 6–10 units and 35–70 lessons, with room for depth, connections and enrichment." : "Balanced pace: for a broad subject, up to 5–8 units and 26–45 lessons, covering foundations, practice and an applied project."}`;
|
|
1478
1616
|
}
|
|
1479
|
-
function blueprintPrompt({ goal, language, intake, materialsSummary, profile, timeline, weeklyMinutes }) {
|
|
1617
|
+
function blueprintPrompt({ goal, language, intake, materialsSummary, profile, timeline, size, weeklyMinutes }) {
|
|
1480
1618
|
return {
|
|
1481
1619
|
system: join(PERSONA, `You design personal courses with the care of an experienced curriculum designer: clear prerequisites first, one new idea per lesson, frequent practice, and applied projects that make the knowledge usable. ${languageRule(language)}`, JSON_ONLY),
|
|
1482
1620
|
user: join(block("learning_goal", goal), block("intake_answers", intake), block("materials", materialsSummary), learnerBlock({ profile }), `Design the course blueprint.
|
|
1483
|
-
- ${sizeGuidance(timeline)}${weeklyMinutes ? ` The learner can study about ${weeklyMinutes} minutes per week.` : ""}
|
|
1621
|
+
- ${sizeGuidance(timeline, size)}${weeklyMinutes ? ` The learner can study about ${weeklyMinutes} minutes per week.` : ""}
|
|
1484
1622
|
- Structure: units → lectures (1–5 per unit) → lessons (1–5 per lecture). Each lesson takes 10–25 minutes and teaches one coherent idea.
|
|
1485
1623
|
- Order lessons so every prerequisite comes first. Start from where the learner is, not from zero if they already know the basics.
|
|
1486
1624
|
- If materials were provided, follow their scope and order where sensible.
|
|
@@ -1497,13 +1635,20 @@ function revisePrompt({ blueprint, instruction, language }) {
|
|
|
1497
1635
|
user: join(block("current_blueprint", blueprint), block("change_request", instruction), "Apply the change request and return the complete revised blueprint in the same JSON shape. Keep the \"id\" field of every unit, lecture, lesson and concept you do not substantially change; new items get no id.")
|
|
1498
1636
|
};
|
|
1499
1637
|
}
|
|
1638
|
+
const ITEM_FIGURE = "When a figure makes a question clearer (a shape to measure, a graph to read, a circuit, a diagram to label), give the item \"svg\": a small diagram following the same rules as the board's svg op (viewBox at most 480×320). Most items need no figure.";
|
|
1639
|
+
function reworkPrompt({ outline, unitTitle, lectureTitle, lesson, mode, language }) {
|
|
1640
|
+
return {
|
|
1641
|
+
system: join(PERSONA, `You design personal courses with the care of an experienced curriculum designer: clear prerequisites first, one new idea per lesson. ${languageRule(language)}`, JSON_ONLY),
|
|
1642
|
+
user: join(block("course_outline", outline), block("position", `Unit: ${unitTitle}\nLecture: ${lectureTitle}`), block("lesson", lesson), mode === "split" ? "Split this lesson into two consecutive lessons that together cover everything it covers. Each teaches one coherent idea in 10–20 minutes, and the first prepares the second. Stay consistent with the rest of the course." : "Rewrite this lesson to go deeper. Keep its topic and its place in the course, but aim at why it works, its edge cases and its connections to the rest of the course; adjust the objectives, concepts and minutes (at most 25) to match.", "Return {\"lessons\":[{\"title\":\"…\",\"summary\":\"…\",\"objectives\":[\"…\"],\"concepts\":[\"…\"],\"estMinutes\":15}]}")
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1500
1645
|
function practicePrompt({ course, lecture, lessons, language, passages }) {
|
|
1501
1646
|
const lessonLines = lessons.map((lesson) => `- [${lesson.id}] ${lesson.title}: ${lesson.objectives.join("; ")} | concepts: ${lesson.concepts.map((concept) => `${concept.id}=${concept.name}`).join(", ")}`).join("\n");
|
|
1502
1647
|
return {
|
|
1503
1648
|
system: join(PERSONA, `You write practice that builds real understanding: retrieval, application, and common-mistake traps, from easy to harder. ${languageRule(language)}`, JSON_ONLY),
|
|
1504
|
-
user: join(block("course", `${course.title} — ${course.summary ?? ""}`), block("lecture", lecture.title), block("lessons", lessonLines), block("source_passages", passages), `For EACH lesson write 4–6 practice items. Mix kinds: "choice" (4 options), "multi", "blank" (short exact answer; list accepted spellings), "number" (with tolerance), "short" (a sentence or two, with a model answer and 2–3 rubric points). At least one item per lesson asks the learner to apply the idea to a new situation. Give each item 1–2 hints that nudge without giving the answer, an explanation, a difficulty 1–3, and the conceptIds it tests (use the ids given).
|
|
1649
|
+
user: join(block("course", `${course.title} — ${course.summary ?? ""}`), block("lecture", lecture.title), block("lessons", lessonLines), block("source_passages", passages), `For EACH lesson write 4–6 practice items. Mix kinds: "choice" (4 options), "multi", "blank" (short exact answer; list accepted spellings), "number" (with tolerance), "short" (a sentence or two, with a model answer and 2–3 rubric points). At least one item per lesson asks the learner to apply the idea to a new situation. Give each item 1–2 hints that nudge without giving the answer, an explanation, a difficulty 1–3, and the conceptIds it tests (use the ids given). ${ITEM_FIGURE}
|
|
1505
1650
|
|
|
1506
|
-
Return {"lessons":{"<lesson id>":{"items":[{"kind":"choice","prompt":"…","options":["…"],"answer":0,"hints":["…"],"explanation":"…","difficulty":1,"conceptIds":["…"]}]}}}`)
|
|
1651
|
+
Return {"lessons":{"<lesson id>":{"items":[{"kind":"choice","prompt":"…","options":["…"],"answer":0,"hints":["…"],"explanation":"…","difficulty":1,"conceptIds":["…"],"svg":"<svg …>…</svg> (optional)"}]}}}`)
|
|
1507
1652
|
};
|
|
1508
1653
|
}
|
|
1509
1654
|
function projectsPrompt({ course, projects, language }) {
|
|
@@ -1533,8 +1678,10 @@ function lessonPrompt({ course, unit, lecture, lesson, language, learner, passag
|
|
|
1533
1678
|
- Build intuition before formalism; then make it precise; then work at least one example fully on the board.
|
|
1534
1679
|
- Draw a diagram or chart whenever a picture makes the idea click.
|
|
1535
1680
|
- Put 2–4 checks after the key ideas, plus one at the end; every check must name its conceptIds.
|
|
1681
|
+
- If the lesson works through a calculation or a derivation, make exactly one check a "work" check, right after the key worked example: a similar problem the learner solves alone, writing every step. A lesson without a calculation or derivation gets no "work" check.
|
|
1536
1682
|
- If source passages are given, stay consistent with them and mention the page when you lean on one.
|
|
1537
1683
|
- Close with a recap step: a "key" note with the 2–3 takeaways, and say what comes next.
|
|
1684
|
+
- Give each step that works towards one of the objectives "goal": its 0-based index in the objectives above (${lesson.objectives.map((objective, index) => `${index} = ${objective}`).join("; ") || "none"}); leave it out for the hook and the recap. Cover the objectives in order.
|
|
1538
1685
|
Start with a step whose ops begin with {"t":"title","text":"${lesson.title.replace(/"/gu, "\\\"")}"}.`)
|
|
1539
1686
|
};
|
|
1540
1687
|
}
|
|
@@ -1557,10 +1704,24 @@ function reexplainPrompt({ lesson, language, step, boardSoFar, style }) {
|
|
|
1557
1704
|
user: join(block("lesson", lesson ? lesson.title : ""), block("board_so_far", boardSoFar), block("step_to_reexplain", step), `The learner did not get this step. Explain the same idea again ${REEXPLAIN_STYLES[style] ?? REEXPLAIN_STYLES.example}, in 2–4 steps, starting with {"t":"page","title":"…"}. End with one quick check.`)
|
|
1558
1705
|
};
|
|
1559
1706
|
}
|
|
1707
|
+
const GRADE_FIELDS = "\"score\":<0 to 1>,\"verdict\":\"correct|partial|incorrect\",\"feedback\":\"2–3 encouraging sentences: what is right, what is missing, how to fix it\",\"errorTag\":\"<3–6 word label of the main mistake, or empty>\",\"misconception\":<true if the answer shows a confident wrong belief rather than a gap>";
|
|
1560
1708
|
function gradePrompt({ item, response, language }) {
|
|
1709
|
+
const work = item.kind === "work";
|
|
1561
1710
|
return {
|
|
1562
|
-
system: join(PERSONA, `You grade open answers fairly: credit correct ideas expressed in the learner's own words, ignore spelling and phrasing, and pinpoint the exact gap. ${languageRule(language)}`, JSON_ONLY),
|
|
1563
|
-
user: join(block("question", item.prompt), block("model_answer", item.answer), block("rubric", (item.rubric ?? []).join("\n")), block("learner_answer", response),
|
|
1711
|
+
system: join(PERSONA, `You grade open answers fairly: credit correct ideas expressed in the learner's own words, ignore spelling and phrasing, and pinpoint the exact gap.${work ? " For a written working, follow it line by line and find the first line that goes wrong; a different but valid route is not a mistake, and a slip carried forward counts once." : ""} ${languageRule(language)}`, JSON_ONLY),
|
|
1712
|
+
user: join(block("question", item.prompt), block(work ? "model_working" : "model_answer", item.answer), block("rubric", (item.rubric ?? []).join("\n")), block(work ? "learner_working" : "learner_answer", response), work ? `Return {${GRADE_FIELDS},"wrongStep":"the first wrong line of the learner's working, copied exactly, or empty when every line is right","why":"one sentence: what is wrong in that line","fix":"that line written correctly, with inline math $…$"}` : `Return {${GRADE_FIELDS}}`)
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function correctionPrompt({ course, lesson, language, boardSoFar, item, response, grade }) {
|
|
1716
|
+
const notes = [
|
|
1717
|
+
grade.wrongStep ? `First wrong line: ${grade.wrongStep}` : "",
|
|
1718
|
+
grade.why ? `What is wrong: ${grade.why}` : "",
|
|
1719
|
+
grade.fix ? `Corrected line: ${grade.fix}` : "",
|
|
1720
|
+
grade.feedback ? `Feedback already shown: ${grade.feedback}` : ""
|
|
1721
|
+
].filter(Boolean).join("\n");
|
|
1722
|
+
return {
|
|
1723
|
+
system: join(PERSONA, BOARD_SPEC, STEP_SPEC, languageRule(language), JSONL_ONLY),
|
|
1724
|
+
user: join(block("course", course ? course.title : ""), block("lesson", lesson ? `${lesson.title}: ${lesson.objectives?.join("; ") ?? ""}` : ""), block("board_so_far", boardSoFar), block("problem", item.prompt), block("model_working", item.answer), block("learner_working", response), block("grader_notes", notes), "The learner just wrote out their working for this problem and went wrong somewhere. Correct it at the board in 2–3 steps, continuing on the current board: (1) write the learner's wrong line as they wrote it, in an op with an \"id\", then {\"t\":\"mark\",\"ref\":\"<that id>\",\"style\":\"box\"}, and say kindly what went wrong there and why; (2) write the corrected line and carry the working on to the right answer; (3) end with a short {\"t\":\"note\",\"tone\":\"key\",\"text\":\"…\"} holding the rule to remember. Speak to the learner directly and keep it brief. Add no check, and do not continue the lesson itself.")
|
|
1564
1725
|
};
|
|
1565
1726
|
}
|
|
1566
1727
|
function projectReviewPrompt({ project, submission, language }) {
|
|
@@ -1593,7 +1754,7 @@ Return {"cards":[{"front":"…","back":"…","tag":"<topic>"}]}`)
|
|
|
1593
1754
|
function quizPrompt({ scopeTitle, outline, concepts, language, count = 10 }) {
|
|
1594
1755
|
return {
|
|
1595
1756
|
system: join(PERSONA, `You write fair, well-balanced quizzes. ${languageRule(language)}`, JSON_ONLY),
|
|
1596
|
-
user: join(block("scope", scopeTitle), block("outline", outline), block("concepts", concepts), `Write a ${count}-question quiz across the whole scope: about 30% recall, 50% application, 20% harder transfer questions. Mix choice, multi, blank, number and short items, each with explanation, difficulty and conceptIds (use the ids given).
|
|
1757
|
+
user: join(block("scope", scopeTitle), block("outline", outline), block("concepts", concepts), `Write a ${count}-question quiz across the whole scope: about 30% recall, 50% application, 20% harder transfer questions. Mix choice, multi, blank, number and short items, each with explanation, difficulty and conceptIds (use the ids given). ${ITEM_FIGURE}
|
|
1597
1758
|
|
|
1598
1759
|
Return {"items":[…same item shape as practice items…]}`)
|
|
1599
1760
|
};
|
|
@@ -1604,10 +1765,16 @@ function summaryPrompt({ scopeTitle, passages, outline, language }) {
|
|
|
1604
1765
|
user: join(block("scope", scopeTitle), block("outline", outline), block("source_passages", passages), "Write structured study notes in Markdown: a 3–5 sentence overview, then sections with headings, key points, formulas ($…$ / $$…$$), and short examples, then a \"Check yourself\" list of 5 questions. Cite pages as (p. N) where the source says so.")
|
|
1605
1766
|
};
|
|
1606
1767
|
}
|
|
1607
|
-
function digestPrompt({ title, passages, language }) {
|
|
1768
|
+
function digestPrompt({ title, passages, notes, language }) {
|
|
1608
1769
|
return {
|
|
1609
1770
|
system: join(PERSONA, `You help learners digest long readings: you surface the structure and the argument, not just a summary. ${languageRule(language)}`, JSON_ONLY),
|
|
1610
|
-
user: join(block("document", title), block("passages", passages), "Return {\"overview\":\"3–5 sentences\",\"structure\":[{\"heading\":\"…\",\"points\":[\"…\"],\"pages\":\"e.g. 3–7\"}],\"keyIdeas\":[\"…\"],\"arguments\":[{\"claim\":\"…\",\"support\":\"…\",\"page\":\"…\"}],\"glossary\":[{\"term\":\"…\",\"meaning\":\"…\"}],\"questions\":[\"5 questions worth discussing or testing\"]}")
|
|
1771
|
+
user: join(block("document", title), notes ? block("notes_on_each_part", notes) : block("passages", passages), notes ? "The notes above cover the whole document part by part, in order. Combine them into one digest of the whole document: follow its structure from beginning to end, merge points that repeat, and keep the page references from the notes." : "", "Return {\"overview\":\"3–5 sentences\",\"structure\":[{\"heading\":\"…\",\"points\":[\"…\"],\"pages\":\"e.g. 3–7\"}],\"keyIdeas\":[\"…\"],\"arguments\":[{\"claim\":\"…\",\"support\":\"…\",\"page\":\"…\"}],\"glossary\":[{\"term\":\"…\",\"meaning\":\"…\"}],\"questions\":[\"5 questions worth discussing or testing\"]}")
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function digestPartPrompt({ title, part, parts, range, passages, language }) {
|
|
1775
|
+
return {
|
|
1776
|
+
system: join(PERSONA, `You are reading a long document one part at a time for a learner. Take faithful notes on this part only; a later pass combines the notes of all parts. ${languageRule(language)}`, JSON_ONLY),
|
|
1777
|
+
user: join(block("document", title), block("part", `Part ${part} of ${parts}${range ? ` (${range})` : ""}`), block("passages", passages), "Take notes on this part only. Return {\"sections\":[{\"heading\":\"the chapter or section title as the text gives it\",\"pages\":\"e.g. 12–18\",\"points\":[\"…\"]}],\"keyIdeas\":[\"…\"],\"arguments\":[{\"claim\":\"…\",\"support\":\"…\",\"page\":\"…\"}],\"glossary\":[{\"term\":\"…\",\"meaning\":\"…\"}]}. Take page numbers from the passage labels; never invent them.")
|
|
1611
1778
|
};
|
|
1612
1779
|
}
|
|
1613
1780
|
const ASSIST_MODES = Object.freeze({
|
|
@@ -1630,7 +1797,7 @@ function shortPrompt({ topic, passages, language }) {
|
|
|
1630
1797
|
function materialBriefPrompt({ title, passages, language }) {
|
|
1631
1798
|
return {
|
|
1632
1799
|
system: join(PERSONA, languageRule(language), JSON_ONLY),
|
|
1633
|
-
user: join(block("document", title), block("passages", passages), "Describe this material for a course designer. Return {\"summary\":\"3–4 sentences\",\"topics\":[\"main topics in order\"],\"level\":\"beginner|intermediate|advanced\",\"suggestedGoal\":\"one sentence learning goal a student might have for it\"}")
|
|
1800
|
+
user: join(block("document", title), block("passages", passages), "The passages are excerpts spread across the whole document, each labelled with where it comes from. Describe this material as a whole for a course designer, not just its opening. Return {\"summary\":\"3–4 sentences\",\"topics\":[\"main topics in the order the document covers them\"],\"level\":\"beginner|intermediate|advanced\",\"suggestedGoal\":\"one sentence learning goal a student might have for it\"}")
|
|
1634
1801
|
};
|
|
1635
1802
|
}
|
|
1636
1803
|
function profilePrompt({ profile, events, language }) {
|
|
@@ -1654,4 +1821,4 @@ function getRuntime() {
|
|
|
1654
1821
|
return runtime;
|
|
1655
1822
|
}
|
|
1656
1823
|
//#endregion
|
|
1657
|
-
export {
|
|
1824
|
+
export { courseProgress as $, streamCounter as A, documentParts as B, reworkPrompt as C, generateJson as D, TutorError as E, materialsSummary as F, createConceptState as G, passageRange as H, passagesFor as I, extractJson as J, masteryPercent as K, readMaterials as L, emptyProgress as M, masteryFor as N, generateSteps as O, materialImages as P, conceptIndex as Q, readProfile as R, revisePrompt as S, summaryPrompt as T, spreadPassages as U, formatPassages as V, applyEvidence as W, normalizeBoard as X, boardToPlainText as Y, blueprintOutline as Z, projectReviewPrompt as _, newId as _t, blueprintPrompt as a, nextLesson as at, quizPrompt as b, detectLanguage as c, normalizeCheatsheet as ct, flashcardsPrompt as d, normalizeProject as dt, findLesson as et, gradePrompt as f, normalizeQuestionnaire as ft, profilePrompt as g, assertId as gt, practicePrompt as h, text as ht, assistPrompt as i, lessonAfter as it, EMPTY_PROFILE as j, generateText as k, digestPartPrompt as l, normalizeLesson as lt, materialBriefPrompt as m, safeRelativePath as mt, publishRuntime as n, insertLesson as nt, cheatsheetPrompt as o, normalizeBlueprint as ot, lessonPrompt as p, outlineForPrompt as pt, recommendNext as q, askPrompt as r, intakeSummary as rt, correctionPrompt as s, normalizeCards as st, getRuntime as t, flattenLessons as tt, digestPrompt as u, normalizePractice as ut, projectsPrompt as v, shortPrompt as w, reexplainPrompt as x, questionnairePrompt as y, readProgress as z };
|