dsh-aha-tutor 1.2.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/CHANGELOG.md +20 -0
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/docs/guide.zh-CN.md +15 -8
- package/lib/agent.js +1 -1
- package/lib/client.js +74 -31
- package/lib/index.js +237 -28
- package/lib/{runtime-ywaTyu56.js → runtime-BCGFUDlg.js} +250 -170
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -683,6 +707,7 @@ function normalizeBoard(raw, { conceptIds = [] } = {}) {
|
|
|
683
707
|
const sourceSteps = Array.isArray(raw) ? raw : Array.isArray(raw?.steps) ? raw.steps : [];
|
|
684
708
|
const steps = [];
|
|
685
709
|
let opCounter = 0;
|
|
710
|
+
let worked = false;
|
|
686
711
|
const idMap = /* @__PURE__ */ new Map();
|
|
687
712
|
for (const rawStep of sourceSteps.slice(0, BOARD_LIMITS.steps)) {
|
|
688
713
|
const say = text(rawStep?.say ?? rawStep?.narration ?? rawStep?.speech, BOARD_LIMITS.say);
|
|
@@ -698,7 +723,12 @@ function normalizeBoard(raw, { conceptIds = [] } = {}) {
|
|
|
698
723
|
ats.push(sentenceIndex(rawOp?.at));
|
|
699
724
|
}
|
|
700
725
|
syncOps(ops, ats);
|
|
701
|
-
|
|
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;
|
|
702
732
|
if (!say && !ops.length && !check) continue;
|
|
703
733
|
const goal = sentenceIndex(rawStep?.goal);
|
|
704
734
|
steps.push({
|
|
@@ -740,6 +770,164 @@ function boardToPlainText(board, { maxChars = 8e3, uptoStep } = {}) {
|
|
|
740
770
|
return output.length > maxChars ? `${output.slice(0, maxChars)}\n…` : output;
|
|
741
771
|
}
|
|
742
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
|
|
743
931
|
//#region src/core/mastery.js
|
|
744
932
|
const BKT_DEFAULTS = Object.freeze({
|
|
745
933
|
pInit: .2,
|
|
@@ -885,6 +1073,13 @@ function pushHistory(state, entry) {
|
|
|
885
1073
|
function masteryPercent(state) {
|
|
886
1074
|
return Math.round(clamp(Number(state?.pKnown) || 0, 0, 1) * 100);
|
|
887
1075
|
}
|
|
1076
|
+
Object.freeze([
|
|
1077
|
+
"none",
|
|
1078
|
+
"weak",
|
|
1079
|
+
"learning",
|
|
1080
|
+
"proficient",
|
|
1081
|
+
"mastered"
|
|
1082
|
+
]);
|
|
888
1083
|
function masteryLevel(state) {
|
|
889
1084
|
const p = clamp(Number(state?.pKnown) || 0, 0, 1);
|
|
890
1085
|
let level = MASTERY_LEVELS[0].id;
|
|
@@ -1177,164 +1372,6 @@ async function materialImages(store, materials) {
|
|
|
1177
1372
|
return images;
|
|
1178
1373
|
}
|
|
1179
1374
|
//#endregion
|
|
1180
|
-
//#region src/core/json-extract.js
|
|
1181
|
-
var JsonExtractError = class extends Error {
|
|
1182
|
-
constructor(message, { cause, text } = {}) {
|
|
1183
|
-
super(message, cause ? { cause } : void 0);
|
|
1184
|
-
this.name = "JsonExtractError";
|
|
1185
|
-
this.preview = typeof text === "string" ? text.slice(0, 200) : void 0;
|
|
1186
|
-
}
|
|
1187
|
-
};
|
|
1188
|
-
const FENCE = /```(?:json|jsonl|javascript|js)?\s*\n?([\s\S]*?)```/giu;
|
|
1189
|
-
function stripTrailingCommas(text) {
|
|
1190
|
-
let out = "";
|
|
1191
|
-
let inString = false;
|
|
1192
|
-
let escaped = false;
|
|
1193
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
1194
|
-
const char = text[index];
|
|
1195
|
-
if (inString) {
|
|
1196
|
-
out += char;
|
|
1197
|
-
if (escaped) escaped = false;
|
|
1198
|
-
else if (char === "\\") escaped = true;
|
|
1199
|
-
else if (char === "\"") inString = false;
|
|
1200
|
-
continue;
|
|
1201
|
-
}
|
|
1202
|
-
if (char === "\"") {
|
|
1203
|
-
inString = true;
|
|
1204
|
-
out += char;
|
|
1205
|
-
continue;
|
|
1206
|
-
}
|
|
1207
|
-
if (char === ",") {
|
|
1208
|
-
let next = index + 1;
|
|
1209
|
-
while (next < text.length && /\s/u.test(text[next])) next += 1;
|
|
1210
|
-
if (text[next] === "}" || text[next] === "]") continue;
|
|
1211
|
-
}
|
|
1212
|
-
out += char;
|
|
1213
|
-
}
|
|
1214
|
-
return out;
|
|
1215
|
-
}
|
|
1216
|
-
function balancedSlice(text, from = 0) {
|
|
1217
|
-
const start = text.slice(from).search(/[{[]/u);
|
|
1218
|
-
if (start < 0) return void 0;
|
|
1219
|
-
const begin = from + start;
|
|
1220
|
-
const stack = [];
|
|
1221
|
-
let inString = false;
|
|
1222
|
-
let escaped = false;
|
|
1223
|
-
for (let index = begin; index < text.length; index += 1) {
|
|
1224
|
-
const char = text[index];
|
|
1225
|
-
if (inString) {
|
|
1226
|
-
if (escaped) escaped = false;
|
|
1227
|
-
else if (char === "\\") escaped = true;
|
|
1228
|
-
else if (char === "\"") inString = false;
|
|
1229
|
-
continue;
|
|
1230
|
-
}
|
|
1231
|
-
if (char === "\"") inString = true;
|
|
1232
|
-
else if (char === "{" || char === "[") stack.push(char);
|
|
1233
|
-
else if (char === "}" || char === "]") {
|
|
1234
|
-
const open = stack.pop();
|
|
1235
|
-
if (char === "}" && open !== "{" || char === "]" && open !== "[") return void 0;
|
|
1236
|
-
if (stack.length === 0) return {
|
|
1237
|
-
begin,
|
|
1238
|
-
end: index + 1,
|
|
1239
|
-
text: text.slice(begin, index + 1)
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
return {
|
|
1244
|
-
begin,
|
|
1245
|
-
end: text.length,
|
|
1246
|
-
text: text.slice(begin),
|
|
1247
|
-
truncated: true,
|
|
1248
|
-
stack
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
function repairTruncated(text) {
|
|
1252
|
-
let body = text;
|
|
1253
|
-
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
1254
|
-
const closed = closeOpen(body);
|
|
1255
|
-
if (closed !== void 0) try {
|
|
1256
|
-
return JSON.parse(stripTrailingCommas(closed));
|
|
1257
|
-
} catch {}
|
|
1258
|
-
const cut = Math.max(body.lastIndexOf(","), body.lastIndexOf("{"), body.lastIndexOf("["));
|
|
1259
|
-
if (cut <= 0) break;
|
|
1260
|
-
body = body.slice(0, body[cut] === "," ? cut : cut + 1);
|
|
1261
|
-
}
|
|
1262
|
-
throw new JsonExtractError("model output was truncated beyond repair", { text });
|
|
1263
|
-
}
|
|
1264
|
-
function closeOpen(text) {
|
|
1265
|
-
const stack = [];
|
|
1266
|
-
let inString = false;
|
|
1267
|
-
let escaped = false;
|
|
1268
|
-
for (const char of text) {
|
|
1269
|
-
if (inString) {
|
|
1270
|
-
if (escaped) escaped = false;
|
|
1271
|
-
else if (char === "\\") escaped = true;
|
|
1272
|
-
else if (char === "\"") inString = false;
|
|
1273
|
-
continue;
|
|
1274
|
-
}
|
|
1275
|
-
if (char === "\"") inString = true;
|
|
1276
|
-
else if (char === "{" || char === "[") stack.push(char);
|
|
1277
|
-
else if (char === "}" || char === "]") stack.pop();
|
|
1278
|
-
}
|
|
1279
|
-
if (inString) return void 0;
|
|
1280
|
-
return text + stack.reverse().map((open) => open === "{" ? "}" : "]").join("");
|
|
1281
|
-
}
|
|
1282
|
-
function extractJson(raw, { allowTruncated = true } = {}) {
|
|
1283
|
-
if (raw !== null && typeof raw === "object") return raw;
|
|
1284
|
-
const text = String(raw ?? "").replace(/^/u, "").trim();
|
|
1285
|
-
if (!text) throw new JsonExtractError("model returned an empty response", { text });
|
|
1286
|
-
const candidates = [];
|
|
1287
|
-
for (const match of text.matchAll(FENCE)) candidates.push(match[1].trim());
|
|
1288
|
-
candidates.push(text);
|
|
1289
|
-
let lastError;
|
|
1290
|
-
for (const candidate of candidates) {
|
|
1291
|
-
try {
|
|
1292
|
-
return JSON.parse(candidate);
|
|
1293
|
-
} catch (error) {
|
|
1294
|
-
lastError = error;
|
|
1295
|
-
}
|
|
1296
|
-
const slice = balancedSlice(candidate);
|
|
1297
|
-
if (slice === void 0) continue;
|
|
1298
|
-
if (!slice.truncated) try {
|
|
1299
|
-
return JSON.parse(stripTrailingCommas(slice.text));
|
|
1300
|
-
} catch (error) {
|
|
1301
|
-
lastError = error;
|
|
1302
|
-
}
|
|
1303
|
-
else if (allowTruncated) try {
|
|
1304
|
-
return repairTruncated(slice.text);
|
|
1305
|
-
} catch (error) {
|
|
1306
|
-
lastError = error;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
throw new JsonExtractError("model output did not contain valid JSON", {
|
|
1310
|
-
cause: lastError,
|
|
1311
|
-
text
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
function parseJsonLines(raw, { final = true } = {}) {
|
|
1315
|
-
const lines = String(raw ?? "").replace(/```(?:jsonl|json)?/giu, "").split(/\r?\n/u);
|
|
1316
|
-
const rest = final ? "" : lines.pop() ?? "";
|
|
1317
|
-
const items = [];
|
|
1318
|
-
const errors = [];
|
|
1319
|
-
for (const line of lines) {
|
|
1320
|
-
const trimmed = line.trim();
|
|
1321
|
-
if (!trimmed || !/^[{[]/u.test(trimmed)) continue;
|
|
1322
|
-
try {
|
|
1323
|
-
items.push(JSON.parse(stripTrailingCommas(trimmed)));
|
|
1324
|
-
} catch (error) {
|
|
1325
|
-
errors.push({
|
|
1326
|
-
line: trimmed.slice(0, 120),
|
|
1327
|
-
message: error.message
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
return {
|
|
1332
|
-
items,
|
|
1333
|
-
errors,
|
|
1334
|
-
rest
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
//#endregion
|
|
1338
1375
|
//#region src/host/generate.js
|
|
1339
1376
|
var TutorError = class extends Error {
|
|
1340
1377
|
constructor(code, message) {
|
|
@@ -1377,7 +1414,7 @@ async function generateJson(llm, { purpose = "teach", prompt, maxTokens = 8e3, i
|
|
|
1377
1414
|
}
|
|
1378
1415
|
}
|
|
1379
1416
|
}
|
|
1380
|
-
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 }) {
|
|
1381
1418
|
const raw = [];
|
|
1382
1419
|
let buffer = "";
|
|
1383
1420
|
let emitted = 0;
|
|
@@ -1404,6 +1441,7 @@ async function generateSteps(llm, { purpose = "teach", prompt, maxTokens = 12e3,
|
|
|
1404
1441
|
};
|
|
1405
1442
|
const full = await llm.stream({
|
|
1406
1443
|
purpose,
|
|
1444
|
+
effort,
|
|
1407
1445
|
system: prompt.system,
|
|
1408
1446
|
user: prompt.user,
|
|
1409
1447
|
images,
|
|
@@ -1543,7 +1581,8 @@ const STEP_SPEC = `Each output line is ONE teaching step:
|
|
|
1543
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}]}
|
|
1544
1582
|
|
|
1545
1583
|
A check is a short question asked right after the step, to see whether the idea landed:
|
|
1546
|
-
{"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.
|
|
1547
1586
|
Make wrong options plausible (common mistakes), never trick questions.`;
|
|
1548
1587
|
function questionnairePrompt({ goal, language, materialsSummary, profile }) {
|
|
1549
1588
|
return {
|
|
@@ -1553,14 +1592,33 @@ function questionnairePrompt({ goal, language, materialsSummary, profile }) {
|
|
|
1553
1592
|
Return {"questions":[{"prompt":"…","topic":"purpose|level|timeline|style|focus|other","multi":false,"options":[{"label":"…","detail":"…"}]}]}`)
|
|
1554
1593
|
};
|
|
1555
1594
|
}
|
|
1556
|
-
|
|
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.`;
|
|
1557
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."}`;
|
|
1558
1616
|
}
|
|
1559
|
-
function blueprintPrompt({ goal, language, intake, materialsSummary, profile, timeline, weeklyMinutes }) {
|
|
1617
|
+
function blueprintPrompt({ goal, language, intake, materialsSummary, profile, timeline, size, weeklyMinutes }) {
|
|
1560
1618
|
return {
|
|
1561
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),
|
|
1562
1620
|
user: join(block("learning_goal", goal), block("intake_answers", intake), block("materials", materialsSummary), learnerBlock({ profile }), `Design the course blueprint.
|
|
1563
|
-
- ${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.` : ""}
|
|
1564
1622
|
- Structure: units → lectures (1–5 per unit) → lessons (1–5 per lecture). Each lesson takes 10–25 minutes and teaches one coherent idea.
|
|
1565
1623
|
- Order lessons so every prerequisite comes first. Start from where the learner is, not from zero if they already know the basics.
|
|
1566
1624
|
- If materials were provided, follow their scope and order where sensible.
|
|
@@ -1577,13 +1635,20 @@ function revisePrompt({ blueprint, instruction, language }) {
|
|
|
1577
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.")
|
|
1578
1636
|
};
|
|
1579
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
|
+
}
|
|
1580
1645
|
function practicePrompt({ course, lecture, lessons, language, passages }) {
|
|
1581
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");
|
|
1582
1647
|
return {
|
|
1583
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),
|
|
1584
|
-
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}
|
|
1585
1650
|
|
|
1586
|
-
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)"}]}}}`)
|
|
1587
1652
|
};
|
|
1588
1653
|
}
|
|
1589
1654
|
function projectsPrompt({ course, projects, language }) {
|
|
@@ -1613,6 +1678,7 @@ function lessonPrompt({ course, unit, lecture, lesson, language, learner, passag
|
|
|
1613
1678
|
- Build intuition before formalism; then make it precise; then work at least one example fully on the board.
|
|
1614
1679
|
- Draw a diagram or chart whenever a picture makes the idea click.
|
|
1615
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.
|
|
1616
1682
|
- If source passages are given, stay consistent with them and mention the page when you lean on one.
|
|
1617
1683
|
- Close with a recap step: a "key" note with the 2–3 takeaways, and say what comes next.
|
|
1618
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.
|
|
@@ -1638,10 +1704,24 @@ function reexplainPrompt({ lesson, language, step, boardSoFar, style }) {
|
|
|
1638
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.`)
|
|
1639
1705
|
};
|
|
1640
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>";
|
|
1641
1708
|
function gradePrompt({ item, response, language }) {
|
|
1709
|
+
const work = item.kind === "work";
|
|
1642
1710
|
return {
|
|
1643
|
-
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),
|
|
1644
|
-
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.")
|
|
1645
1725
|
};
|
|
1646
1726
|
}
|
|
1647
1727
|
function projectReviewPrompt({ project, submission, language }) {
|
|
@@ -1674,7 +1754,7 @@ Return {"cards":[{"front":"…","back":"…","tag":"<topic>"}]}`)
|
|
|
1674
1754
|
function quizPrompt({ scopeTitle, outline, concepts, language, count = 10 }) {
|
|
1675
1755
|
return {
|
|
1676
1756
|
system: join(PERSONA, `You write fair, well-balanced quizzes. ${languageRule(language)}`, JSON_ONLY),
|
|
1677
|
-
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}
|
|
1678
1758
|
|
|
1679
1759
|
Return {"items":[…same item shape as practice items…]}`)
|
|
1680
1760
|
};
|
|
@@ -1741,4 +1821,4 @@ function getRuntime() {
|
|
|
1741
1821
|
return runtime;
|
|
1742
1822
|
}
|
|
1743
1823
|
//#endregion
|
|
1744
|
-
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-aha-tutor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "开窍 Aha Tutor: a personal AI tutor for DeepSeek Harness that builds a course around you and teaches it on a narrated handwriting whiteboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|