dsh-plugin-lookatstudy 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/lib/client.js +2123 -100
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +1149 -283
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -625,6 +625,7 @@ function emptyState() {
|
|
|
625
625
|
focus: null,
|
|
626
626
|
memoryGlobal: null,
|
|
627
627
|
memoryPatterns: {},
|
|
628
|
+
artifacts: {},
|
|
628
629
|
proposals: [],
|
|
629
630
|
lessonSessions: {},
|
|
630
631
|
lastConsolidatedAt: null,
|
|
@@ -700,6 +701,7 @@ function loadState(path) {
|
|
|
700
701
|
memoryPatterns: raw.memoryPatterns ?? {},
|
|
701
702
|
proposals: raw.proposals ?? [],
|
|
702
703
|
lessonSessions: raw.lessonSessions ?? {},
|
|
704
|
+
artifacts: raw.artifacts ?? {},
|
|
703
705
|
lastConsolidatedAt: raw.lastConsolidatedAt ?? null,
|
|
704
706
|
xp: raw.xp ?? {
|
|
705
707
|
total: 0,
|
|
@@ -822,6 +824,7 @@ function deleteCourse(state, courseId) {
|
|
|
822
824
|
if (i < 0) throw new Error(`lookatstudy-plugin: unknown course id ${JSON.stringify(courseId)}`);
|
|
823
825
|
state.courses.splice(i, 1);
|
|
824
826
|
state.proposals = state.proposals.filter((p) => !p.lessonId.startsWith(`${courseId}:`));
|
|
827
|
+
for (const lessonId of Object.keys(state.artifacts)) if (lessonId.startsWith(`${courseId}:`)) delete state.artifacts[lessonId];
|
|
825
828
|
}
|
|
826
829
|
/**
|
|
827
830
|
* Locate a course; unknown ids fail loud.
|
|
@@ -1188,6 +1191,28 @@ function addNote(state, lessonId, zone, title, text, source, quote, now) {
|
|
|
1188
1191
|
* @param noteId - id of the note to remove.
|
|
1189
1192
|
* @throws when the lesson or the note id is unknown (fail loud, like every id lookup).
|
|
1190
1193
|
*/
|
|
1194
|
+
/**
|
|
1195
|
+
* Record one artifact on its lesson, idempotently: the key is the content
|
|
1196
|
+
* hash (type + stable-stringified data) — re-importing or re-generating the
|
|
1197
|
+
* same artifact returns the existing row instead of duplicating it (upstream
|
|
1198
|
+
* learned this the hard way with message-id keys).
|
|
1199
|
+
* @returns the stored artifact and whether this call created it.
|
|
1200
|
+
*/
|
|
1201
|
+
function recordArtifact(state, lessonId, artifact) {
|
|
1202
|
+
findLesson(state, lessonId);
|
|
1203
|
+
const existing = state.artifacts[lessonId] ?? [];
|
|
1204
|
+
const hit = existing.find((a) => a.hash === artifact.hash);
|
|
1205
|
+
if (hit !== void 0) return {
|
|
1206
|
+
artifact: hit,
|
|
1207
|
+
created: false
|
|
1208
|
+
};
|
|
1209
|
+
const next = [...existing, artifact];
|
|
1210
|
+
state.artifacts[lessonId] = next;
|
|
1211
|
+
return {
|
|
1212
|
+
artifact,
|
|
1213
|
+
created: true
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1191
1216
|
function deleteNote(state, lessonId, noteId) {
|
|
1192
1217
|
const ref = findLesson(state, lessonId);
|
|
1193
1218
|
const index = ref.lesson.notes.findIndex((n) => n.id === noteId);
|
|
@@ -1621,6 +1646,13 @@ function workbenchState(state, now) {
|
|
|
1621
1646
|
masteryPct: ref.lesson.mastery === null ? null : Math.round(ref.lesson.mastery * 100),
|
|
1622
1647
|
strategy: strategyBand(ref.lesson.mastery),
|
|
1623
1648
|
concepts: conceptViews(ref.lesson) ?? [],
|
|
1649
|
+
due: dueIds.has(ref.lesson.id),
|
|
1650
|
+
artifacts: (state.artifacts[ref.lesson.id] ?? []).map((a) => ({
|
|
1651
|
+
id: a.id,
|
|
1652
|
+
artifactType: a.artifactType,
|
|
1653
|
+
title: a.title,
|
|
1654
|
+
data: a.data
|
|
1655
|
+
})),
|
|
1624
1656
|
starters: starterPrompts(ref.lesson.title).map((s) => ({
|
|
1625
1657
|
label: s.label,
|
|
1626
1658
|
message: s.message
|
|
@@ -1858,6 +1890,60 @@ function registerDashboard(webServer, deps) {
|
|
|
1858
1890
|
}
|
|
1859
1891
|
return;
|
|
1860
1892
|
}
|
|
1893
|
+
if (req.method === "POST" && pathname === "/lookatstudy/api/note/user") {
|
|
1894
|
+
const body = await readJsonBodySafe(req, res);
|
|
1895
|
+
if (body === void 0) return;
|
|
1896
|
+
if (typeof body.lessonId !== "string" || typeof body.quote !== "string" || body.quote.trim().length < 2) {
|
|
1897
|
+
sendJson(res, 400, {
|
|
1898
|
+
ok: false,
|
|
1899
|
+
error: "lessonId and quote (a real selection) required"
|
|
1900
|
+
});
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
try {
|
|
1904
|
+
const quote = body.quote.trim();
|
|
1905
|
+
const text = typeof body.text === "string" && body.text.trim() !== "" ? body.text.trim() : quote;
|
|
1906
|
+
const note = addNote(deps.store.get(), body.lessonId, "record", quote.slice(0, 24), text, "content", quote, /* @__PURE__ */ new Date());
|
|
1907
|
+
deps.store.save();
|
|
1908
|
+
sendJson(res, 200, {
|
|
1909
|
+
ok: true,
|
|
1910
|
+
noteId: note.id
|
|
1911
|
+
});
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
sendJson(res, 404, {
|
|
1914
|
+
ok: false,
|
|
1915
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (req.method === "POST" && pathname === "/lookatstudy/api/review") {
|
|
1921
|
+
const body = await readJsonBodySafe(req, res);
|
|
1922
|
+
if (body === void 0) return;
|
|
1923
|
+
const quality = body.quality;
|
|
1924
|
+
if (typeof body.lessonId !== "string" || typeof quality !== "number" || ![
|
|
1925
|
+
1,
|
|
1926
|
+
4,
|
|
1927
|
+
5
|
|
1928
|
+
].includes(quality)) {
|
|
1929
|
+
sendJson(res, 400, {
|
|
1930
|
+
ok: false,
|
|
1931
|
+
error: "lessonId and quality (1 | 4 | 5) required"
|
|
1932
|
+
});
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
try {
|
|
1936
|
+
recordReview(deps.store.get(), body.lessonId, quality, /* @__PURE__ */ new Date());
|
|
1937
|
+
deps.store.save();
|
|
1938
|
+
sendJson(res, 200, { ok: true });
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
sendJson(res, 404, {
|
|
1941
|
+
ok: false,
|
|
1942
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1861
1947
|
if (req.method === "POST" && pathname === "/lookatstudy/api/lesson-session") {
|
|
1862
1948
|
const body = await readJsonBodySafe(req, res);
|
|
1863
1949
|
if (body === void 0) return;
|
|
@@ -1944,8 +2030,8 @@ const ZH = {
|
|
|
1944
2030
|
"col.tutor": "导师",
|
|
1945
2031
|
"col.bb": "黑板",
|
|
1946
2032
|
"pane.rail": "课程",
|
|
1947
|
-
"pane.
|
|
1948
|
-
"pane.
|
|
2033
|
+
"pane.chat": "对话",
|
|
2034
|
+
"pane.note": "黑板",
|
|
1949
2035
|
"viewtab.teach": "讲解",
|
|
1950
2036
|
"viewtab.cmap": "概念图",
|
|
1951
2037
|
"viewtab.cmap.title": "本课概念之间的关系图",
|
|
@@ -1969,6 +2055,71 @@ const ZH = {
|
|
|
1969
2055
|
"rail.empty.placeholder": "GitHub 仓库链接,如 microsoft/AI-For-Beginners",
|
|
1970
2056
|
"rail.empty.button": "导入",
|
|
1971
2057
|
"composer.send": "发送",
|
|
2058
|
+
"quiz.card.title": "练习",
|
|
2059
|
+
"quiz.progress": "{cur}/{total}",
|
|
2060
|
+
"quiz.next": "下一题",
|
|
2061
|
+
"quiz.finish": "看成绩单",
|
|
2062
|
+
"quiz.score": "成绩:{correct}/{total}",
|
|
2063
|
+
"quiz.right": "答对了",
|
|
2064
|
+
"quiz.wrong": "答错了",
|
|
2065
|
+
"quiz.youChose": "你选了",
|
|
2066
|
+
"quiz.answer": "正确答案",
|
|
2067
|
+
"quiz.hook": "练习卡完成:{correct}/{total} 正确。请简短点评,针对错题讲讲思路。",
|
|
2068
|
+
"quiz.action.explain-wrong": "讲讲错题",
|
|
2069
|
+
"quiz.action.retry": "再来一组",
|
|
2070
|
+
"quiz.action.go-deeper": "深入原理",
|
|
2071
|
+
"quiz.action.mark-mastered": "标记我掌握了",
|
|
2072
|
+
"quiz.action.markMastered.hint": "会让导师出综合题检验,通过即标记掌握",
|
|
2073
|
+
"quiz.action.next-topic": "下一个知识点",
|
|
2074
|
+
"artifact.lines": "第 {from}-{to} 行:",
|
|
2075
|
+
"artifact.expand": "放大查看",
|
|
2076
|
+
"artifact.guess.title": "猜一猜",
|
|
2077
|
+
"artifact.guess.pick": "我选「{label}」",
|
|
2078
|
+
"artifact.guess.wait": "已记下你的直觉——导师下回合揭晓,看看你想得对不对",
|
|
2079
|
+
"artifact.sedimented": "有新的学习产物沉淀进笔记",
|
|
2080
|
+
"zone.artifacts": "AI 产物",
|
|
2081
|
+
"note.quote.ask": "提问这段",
|
|
2082
|
+
"note.quote.save": "加到笔记",
|
|
2083
|
+
"note.quote.template": "请讲解这段:「{text}」",
|
|
2084
|
+
"note.saved": "已存入记录区笔记",
|
|
2085
|
+
"review.rate.title": "复习自评——这课现在记得多牢?",
|
|
2086
|
+
"review.again": "再来一次",
|
|
2087
|
+
"review.remembered": "记住了",
|
|
2088
|
+
"review.mastered": "完全掌握",
|
|
2089
|
+
"review.done.good": "已安排下次复习",
|
|
2090
|
+
"review.done.again": "明天再来一遍",
|
|
2091
|
+
"review.jump": "点开这一课去复习",
|
|
2092
|
+
"review.nudge": "有 {n} 课到了复习时间,先回来练一遍?",
|
|
2093
|
+
"review.nudge.go": "去复习",
|
|
2094
|
+
"rail.search.jump": "打开这一课",
|
|
2095
|
+
"proposal.banner": "《{lesson}》可以提前毕业了",
|
|
2096
|
+
"proposal.accept": "接受",
|
|
2097
|
+
"proposal.decline": "再练练",
|
|
2098
|
+
"proposal.accept.msg": "我接受《{lesson}》的掌握度提议,帮我标记为掌握。",
|
|
2099
|
+
"proposal.decline.msg": "《{lesson}》我先再练练,暂不接受提前毕业。",
|
|
2100
|
+
"tutor.thinking": "导师思考中…",
|
|
2101
|
+
"header.xp": "经验值 {xp}",
|
|
2102
|
+
"header.streak": "连续学习天数",
|
|
2103
|
+
"header.level": "等级",
|
|
2104
|
+
"map.node.locked": "锁定 — 先完成前面的课时",
|
|
2105
|
+
"map.node.due": "到了复习时间",
|
|
2106
|
+
"map.exam.locked": "考试未解锁 — 本章节全部课时掌握度 ≥50% 后开放",
|
|
2107
|
+
"companion.poke": "戳一戳伴学伙伴",
|
|
2108
|
+
"companion.poked": "伴学伙伴向你眨了眨眼",
|
|
2109
|
+
"companion.form.ember": "小焰",
|
|
2110
|
+
"companion.form.frost": "霜绒",
|
|
2111
|
+
"companion.form.moss": "苔芽",
|
|
2112
|
+
"companion.form.star": "星尘",
|
|
2113
|
+
"companion.form.ink": "墨墨",
|
|
2114
|
+
"settings.companion": "伴学伙伴",
|
|
2115
|
+
"settings.companion.hint": "选择学习面板右下角的伴学生物形态。",
|
|
2116
|
+
"quiz.msg.explain-wrong": "我刚才有题答错了,帮我讲讲为什么错、正确的思路是什么。",
|
|
2117
|
+
"quiz.msg.retry": "再来一组类似的题巩固一下。",
|
|
2118
|
+
"quiz.msg.go-deeper": "这组我答得不错,帮我深入讲讲背后的原理和容易混淆的地方。",
|
|
2119
|
+
"quiz.msg.mark-mastered": "这课我觉得掌握了,帮我确认一下——出个综合题检验,通过了就标记为掌握。",
|
|
2120
|
+
"quiz.msg.next-topic": "进入下一个知识点。",
|
|
2121
|
+
"toast.region": "通知",
|
|
2122
|
+
"toast.close": "关闭",
|
|
1972
2123
|
"composer.busy": "导师正在回复…",
|
|
1973
2124
|
"rail.empty.demo": "导入示例课程",
|
|
1974
2125
|
"rail.mastered": "毕业 {mastered}/{total} 课",
|
|
@@ -4247,6 +4398,196 @@ function completeLines(value) {
|
|
|
4247
4398
|
return lines;
|
|
4248
4399
|
}
|
|
4249
4400
|
//#endregion
|
|
4401
|
+
//#region src/artifacts.ts
|
|
4402
|
+
/**
|
|
4403
|
+
* The artifact channel — the upstream artifacts system port (0.15.0 P1):
|
|
4404
|
+
* tools that produce display-worthy learning objects return them tagged with
|
|
4405
|
+
* an `artifactType` (quiz / guess / compare_table / code_walkthrough /
|
|
4406
|
+
* concept_map / diagram), the state records them idempotently per lesson
|
|
4407
|
+
* (upstream's canvas_items equivalent — content-hashed, because the lesson
|
|
4408
|
+
* there was burned by message-id keys duplicating saves), and the panel
|
|
4409
|
+
* renders them as interactive cards. Host-side pure logic + node:crypto.
|
|
4410
|
+
* @module dsh-plugin-lookatstudy/artifacts
|
|
4411
|
+
*/
|
|
4412
|
+
/** Deterministic stringify (sorted object keys at every depth). */
|
|
4413
|
+
function stableStringify(value) {
|
|
4414
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
4415
|
+
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
4416
|
+
return JSON.stringify(value) ?? "null";
|
|
4417
|
+
}
|
|
4418
|
+
/** Short stable content hash (the dedup key — never a message id). */
|
|
4419
|
+
function contentHash(text) {
|
|
4420
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
4421
|
+
}
|
|
4422
|
+
const MAX_QUESTIONS = 20;
|
|
4423
|
+
/**
|
|
4424
|
+
* Sanitize a tutor-produced quiz: coerce strings, drop structurally invalid
|
|
4425
|
+
* questions (collecting a warning each), fail loud when nothing usable
|
|
4426
|
+
* remains. Mirrors upstream sanitizeArtifact('quiz') intent — the card never
|
|
4427
|
+
* renders a question it cannot judge.
|
|
4428
|
+
*/
|
|
4429
|
+
function sanitizeQuiz(raw) {
|
|
4430
|
+
const warnings = [];
|
|
4431
|
+
const input = raw ?? {};
|
|
4432
|
+
const title = typeof input.title === "string" && input.title.trim() !== "" ? input.title.trim() : "练习";
|
|
4433
|
+
const questions = [];
|
|
4434
|
+
const list = Array.isArray(input.questions) ? input.questions : [];
|
|
4435
|
+
list.slice(0, MAX_QUESTIONS).forEach((q, index) => {
|
|
4436
|
+
const item = q ?? {};
|
|
4437
|
+
const prompt = typeof item.prompt === "string" ? item.prompt.trim() : "";
|
|
4438
|
+
const options = Array.isArray(item.options) ? item.options.map((o) => typeof o === "string" ? o.trim() : "").filter((o) => o !== "") : [];
|
|
4439
|
+
const answer = typeof item.answer === "number" && Number.isInteger(item.answer) ? item.answer : -1;
|
|
4440
|
+
const explanation = typeof item.explanation === "string" ? item.explanation.trim() : "";
|
|
4441
|
+
if (prompt === "") {
|
|
4442
|
+
warnings.push(`question ${index + 1}: empty prompt dropped`);
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
if (options.length < 2) {
|
|
4446
|
+
warnings.push(`question ${index + 1}: fewer than 2 usable options dropped`);
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
if (answer < 0 || answer >= options.length) {
|
|
4450
|
+
warnings.push(`question ${index + 1}: answer index out of range dropped`);
|
|
4451
|
+
return;
|
|
4452
|
+
}
|
|
4453
|
+
questions.push({
|
|
4454
|
+
prompt,
|
|
4455
|
+
options,
|
|
4456
|
+
answer,
|
|
4457
|
+
explanation
|
|
4458
|
+
});
|
|
4459
|
+
});
|
|
4460
|
+
if (questions.length === 0) throw new Error(`lookatstudy-plugin: quiz artifact has no usable questions${warnings.length > 0 ? ` (${warnings.join("; ")})` : ""}`);
|
|
4461
|
+
if (list.length > MAX_QUESTIONS) warnings.push(`truncated to ${MAX_QUESTIONS} questions`);
|
|
4462
|
+
const data = {
|
|
4463
|
+
artifactType: "quiz",
|
|
4464
|
+
title,
|
|
4465
|
+
questions
|
|
4466
|
+
};
|
|
4467
|
+
if (warnings.length > 0) data.warnings = warnings;
|
|
4468
|
+
return {
|
|
4469
|
+
data,
|
|
4470
|
+
warnings
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
/** The artifact id derived from type + content (stable across re-imports). */
|
|
4474
|
+
function artifactId(artifactType, data) {
|
|
4475
|
+
return `${artifactType}-${contentHash(stableStringify(data))}`;
|
|
4476
|
+
}
|
|
4477
|
+
/** Coerce one string field, dropping empty. */
|
|
4478
|
+
function str(value) {
|
|
4479
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
4480
|
+
}
|
|
4481
|
+
/** Sanitize a guess (the opening two-option hook; unscored, revealed next turn). */
|
|
4482
|
+
function sanitizeGuess(raw) {
|
|
4483
|
+
const warnings = [];
|
|
4484
|
+
const input = raw ?? {};
|
|
4485
|
+
const prompt = str(input.prompt);
|
|
4486
|
+
if (prompt === null) throw new Error("lookatstudy-plugin: guess artifact needs a prompt");
|
|
4487
|
+
const list = Array.isArray(input.options) ? input.options : [];
|
|
4488
|
+
const options = [];
|
|
4489
|
+
list.slice(0, 2).forEach((o, index) => {
|
|
4490
|
+
const item = o ?? {};
|
|
4491
|
+
const id = str(item.id) ?? String.fromCharCode(97 + index);
|
|
4492
|
+
const label = str(item.label);
|
|
4493
|
+
if (label === null) {
|
|
4494
|
+
warnings.push(`option ${index + 1}: empty label dropped`);
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
options.push({
|
|
4498
|
+
id,
|
|
4499
|
+
label
|
|
4500
|
+
});
|
|
4501
|
+
});
|
|
4502
|
+
if (options.length !== 2) throw new Error(`lookatstudy-plugin: guess artifact needs exactly 2 options (${warnings.join("; ")})`);
|
|
4503
|
+
return {
|
|
4504
|
+
data: {
|
|
4505
|
+
artifactType: "guess",
|
|
4506
|
+
prompt,
|
|
4507
|
+
options
|
|
4508
|
+
},
|
|
4509
|
+
warnings
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
/** Sanitize a compare table (headers ≥2; every row matches the header width). */
|
|
4513
|
+
function sanitizeCompareTable(raw) {
|
|
4514
|
+
const warnings = [];
|
|
4515
|
+
const input = raw ?? {};
|
|
4516
|
+
const title = str(input.title) ?? "对比";
|
|
4517
|
+
const headers = (Array.isArray(input.headers) ? input.headers : []).map((h) => str(h)).filter((h) => h !== null);
|
|
4518
|
+
if (headers.length < 2) throw new Error("lookatstudy-plugin: compare_table needs at least 2 headers");
|
|
4519
|
+
const rows = [];
|
|
4520
|
+
for (const row of Array.isArray(input.rows) ? input.rows : []) {
|
|
4521
|
+
const cells = (Array.isArray(row) ? row : []).map((c) => typeof c === "string" ? c.trim() : "");
|
|
4522
|
+
if (cells.length === headers.length) rows.push(cells);
|
|
4523
|
+
else warnings.push(`row dropped (${cells.length} cells ≠ ${headers.length} headers)`);
|
|
4524
|
+
}
|
|
4525
|
+
if (rows.length === 0) throw new Error(`lookatstudy-plugin: compare_table has no usable rows (${warnings.join("; ")})`);
|
|
4526
|
+
return {
|
|
4527
|
+
data: {
|
|
4528
|
+
artifactType: "compare_table",
|
|
4529
|
+
title,
|
|
4530
|
+
headers,
|
|
4531
|
+
rows
|
|
4532
|
+
},
|
|
4533
|
+
warnings
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4536
|
+
/** Sanitize a mermaid diagram (type + non-empty code). */
|
|
4537
|
+
function sanitizeDiagram(raw) {
|
|
4538
|
+
const input = raw ?? {};
|
|
4539
|
+
const title = str(input.title) ?? "图示";
|
|
4540
|
+
const diagramType = input.diagramType === "sequence" || input.diagramType === "state" ? input.diagramType : "flowchart";
|
|
4541
|
+
const mermaid = str(input.mermaid);
|
|
4542
|
+
if (mermaid === null) throw new Error("lookatstudy-plugin: diagram artifact needs mermaid code");
|
|
4543
|
+
return {
|
|
4544
|
+
data: {
|
|
4545
|
+
artifactType: "diagram",
|
|
4546
|
+
title,
|
|
4547
|
+
diagramType,
|
|
4548
|
+
mermaid
|
|
4549
|
+
},
|
|
4550
|
+
warnings: []
|
|
4551
|
+
};
|
|
4552
|
+
}
|
|
4553
|
+
/** Sanitize a code walkthrough (annotations reference real 1-based line ranges). */
|
|
4554
|
+
function sanitizeCodeWalkthrough(raw) {
|
|
4555
|
+
const warnings = [];
|
|
4556
|
+
const input = raw ?? {};
|
|
4557
|
+
const title = str(input.title) ?? "代码讲解";
|
|
4558
|
+
const language = str(input.language) ?? "text";
|
|
4559
|
+
const code = str(input.code);
|
|
4560
|
+
if (code === null) throw new Error("lookatstudy-plugin: code_walkthrough artifact needs code");
|
|
4561
|
+
const lineCount = code.split("\n").length;
|
|
4562
|
+
const annotations = [];
|
|
4563
|
+
for (const a of Array.isArray(input.annotations) ? input.annotations : []) {
|
|
4564
|
+
const item = a ?? {};
|
|
4565
|
+
const note = str(item.note);
|
|
4566
|
+
const lineStart = typeof item.lineStart === "number" && Number.isInteger(item.lineStart) ? item.lineStart : -1;
|
|
4567
|
+
const lineEnd = typeof item.lineEnd === "number" && Number.isInteger(item.lineEnd) ? item.lineEnd : lineStart;
|
|
4568
|
+
if (note === null || lineStart < 1 || lineEnd < lineStart || lineEnd > lineCount) {
|
|
4569
|
+
warnings.push(`annotation dropped (line ${String(lineStart)}-${String(lineEnd)} of ${String(lineCount)})`);
|
|
4570
|
+
continue;
|
|
4571
|
+
}
|
|
4572
|
+
annotations.push({
|
|
4573
|
+
lineStart,
|
|
4574
|
+
lineEnd,
|
|
4575
|
+
note
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
if (annotations.length === 0) throw new Error(`lookatstudy-plugin: code_walkthrough has no usable annotations (${warnings.join("; ")})`);
|
|
4579
|
+
return {
|
|
4580
|
+
data: {
|
|
4581
|
+
artifactType: "code_walkthrough",
|
|
4582
|
+
title,
|
|
4583
|
+
language,
|
|
4584
|
+
code,
|
|
4585
|
+
annotations
|
|
4586
|
+
},
|
|
4587
|
+
warnings
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
4590
|
+
//#endregion
|
|
4250
4591
|
//#region src/tools.ts
|
|
4251
4592
|
/**
|
|
4252
4593
|
* The `study_*` tool surface, ported from LookatStudy's agent contract:
|
|
@@ -6407,211 +6748,555 @@ function studyTools(store, deps = {}) {
|
|
|
6407
6748
|
title: `Translate lesson: ${args.lessonId} → ${args.lang}`
|
|
6408
6749
|
})
|
|
6409
6750
|
});
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
resolveProposalTool,
|
|
6428
|
-
reportFrictionTool,
|
|
6429
|
-
rememberTool,
|
|
6430
|
-
defineTool({
|
|
6431
|
-
name: "study_consolidate",
|
|
6432
|
-
description: "Gather the consolidation window — friction entries and practice notes recorded since the last consolidation — and advance the watermark. You are the consolidation function (upstream runs an LLM call; here the tutor IS it): distill the window into 0–3 durable memory writes via study_remember (global style / per-course pattern / lesson-specific gaps), then tell the learner in one short line what you took away. Call when a session accumulates friction or after heavy quizzing — not every turn.",
|
|
6433
|
-
parameters: {},
|
|
6434
|
-
output: {
|
|
6435
|
-
schema: {
|
|
6436
|
-
type: "object",
|
|
6437
|
-
additionalProperties: false,
|
|
6438
|
-
properties: {
|
|
6439
|
-
since: {
|
|
6440
|
-
...nullableString,
|
|
6441
|
-
required: true
|
|
6442
|
-
},
|
|
6443
|
-
entries: {
|
|
6444
|
-
type: "array",
|
|
6445
|
-
required: true,
|
|
6446
|
-
items: {
|
|
6447
|
-
type: "object",
|
|
6448
|
-
additionalProperties: false,
|
|
6449
|
-
properties: {
|
|
6450
|
-
lessonId: {
|
|
6451
|
-
type: "string",
|
|
6452
|
-
required: true
|
|
6453
|
-
},
|
|
6454
|
-
lessonTitle: {
|
|
6455
|
-
type: "string",
|
|
6456
|
-
required: true
|
|
6457
|
-
},
|
|
6458
|
-
kind: {
|
|
6459
|
-
type: "string",
|
|
6460
|
-
required: true,
|
|
6461
|
-
enum: ["friction", "practice"]
|
|
6462
|
-
},
|
|
6463
|
-
category: { type: "string" },
|
|
6464
|
-
text: {
|
|
6465
|
-
type: "string",
|
|
6466
|
-
required: true
|
|
6467
|
-
},
|
|
6468
|
-
at: {
|
|
6469
|
-
type: "string",
|
|
6470
|
-
required: true
|
|
6471
|
-
}
|
|
6472
|
-
}
|
|
6473
|
-
}
|
|
6474
|
-
},
|
|
6475
|
-
counts: {
|
|
6751
|
+
const consolidateTool = defineTool({
|
|
6752
|
+
name: "study_consolidate",
|
|
6753
|
+
description: "Gather the consolidation window — friction entries and practice notes recorded since the last consolidation — and advance the watermark. You are the consolidation function (upstream runs an LLM call; here the tutor IS it): distill the window into 0–3 durable memory writes via study_remember (global style / per-course pattern / lesson-specific gaps), then tell the learner in one short line what you took away. Call when a session accumulates friction or after heavy quizzing — not every turn.",
|
|
6754
|
+
parameters: {},
|
|
6755
|
+
output: {
|
|
6756
|
+
schema: {
|
|
6757
|
+
type: "object",
|
|
6758
|
+
additionalProperties: false,
|
|
6759
|
+
properties: {
|
|
6760
|
+
since: {
|
|
6761
|
+
...nullableString,
|
|
6762
|
+
required: true
|
|
6763
|
+
},
|
|
6764
|
+
entries: {
|
|
6765
|
+
type: "array",
|
|
6766
|
+
required: true,
|
|
6767
|
+
items: {
|
|
6476
6768
|
type: "object",
|
|
6477
|
-
required: true,
|
|
6478
6769
|
additionalProperties: false,
|
|
6479
6770
|
properties: {
|
|
6480
|
-
|
|
6481
|
-
type: "
|
|
6771
|
+
lessonId: {
|
|
6772
|
+
type: "string",
|
|
6482
6773
|
required: true
|
|
6483
6774
|
},
|
|
6484
|
-
|
|
6485
|
-
type: "
|
|
6775
|
+
lessonTitle: {
|
|
6776
|
+
type: "string",
|
|
6777
|
+
required: true
|
|
6778
|
+
},
|
|
6779
|
+
kind: {
|
|
6780
|
+
type: "string",
|
|
6781
|
+
required: true,
|
|
6782
|
+
enum: ["friction", "practice"]
|
|
6783
|
+
},
|
|
6784
|
+
category: { type: "string" },
|
|
6785
|
+
text: {
|
|
6786
|
+
type: "string",
|
|
6787
|
+
required: true
|
|
6788
|
+
},
|
|
6789
|
+
at: {
|
|
6790
|
+
type: "string",
|
|
6486
6791
|
required: true
|
|
6487
6792
|
}
|
|
6488
6793
|
}
|
|
6489
|
-
},
|
|
6490
|
-
watermark: {
|
|
6491
|
-
type: "string",
|
|
6492
|
-
required: true
|
|
6493
6794
|
}
|
|
6494
|
-
}
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
entries: window.entries,
|
|
6509
|
-
counts: window.counts,
|
|
6510
|
-
watermark
|
|
6511
|
-
};
|
|
6512
|
-
});
|
|
6513
|
-
},
|
|
6514
|
-
presentCall: () => ({
|
|
6515
|
-
card: "generic",
|
|
6516
|
-
title: "Consolidate learner memory"
|
|
6517
|
-
})
|
|
6518
|
-
}),
|
|
6519
|
-
translateLessonTool,
|
|
6520
|
-
defineTool({
|
|
6521
|
-
name: "study_export",
|
|
6522
|
-
description: "Export one course as a single markdown learning pack (upstream pack-export, zero-LLM): sections and lesson bodies verbatim. The receiver imports it anywhere through study_import_markdown — same plugin, fresh machine, no network. Present the pack to the learner (a copyable block) or save it into the study workspace when they ask for a file.",
|
|
6523
|
-
parameters: { courseId: {
|
|
6524
|
-
type: "string",
|
|
6525
|
-
required: true,
|
|
6526
|
-
description: "Course id to export."
|
|
6527
|
-
} },
|
|
6528
|
-
output: {
|
|
6529
|
-
schema: {
|
|
6530
|
-
type: "object",
|
|
6531
|
-
additionalProperties: false,
|
|
6532
|
-
properties: {
|
|
6533
|
-
courseId: {
|
|
6534
|
-
type: "string",
|
|
6535
|
-
required: true
|
|
6536
|
-
},
|
|
6537
|
-
title: {
|
|
6538
|
-
type: "string",
|
|
6539
|
-
required: true
|
|
6540
|
-
},
|
|
6541
|
-
lessonCount: {
|
|
6542
|
-
type: "integer",
|
|
6543
|
-
required: true
|
|
6544
|
-
},
|
|
6545
|
-
chars: {
|
|
6546
|
-
type: "integer",
|
|
6547
|
-
required: true
|
|
6548
|
-
},
|
|
6549
|
-
markdown: {
|
|
6550
|
-
type: "string",
|
|
6551
|
-
required: true
|
|
6795
|
+
},
|
|
6796
|
+
counts: {
|
|
6797
|
+
type: "object",
|
|
6798
|
+
required: true,
|
|
6799
|
+
additionalProperties: false,
|
|
6800
|
+
properties: {
|
|
6801
|
+
friction: {
|
|
6802
|
+
type: "integer",
|
|
6803
|
+
required: true
|
|
6804
|
+
},
|
|
6805
|
+
practice: {
|
|
6806
|
+
type: "integer",
|
|
6807
|
+
required: true
|
|
6808
|
+
}
|
|
6552
6809
|
}
|
|
6810
|
+
},
|
|
6811
|
+
watermark: {
|
|
6812
|
+
type: "string",
|
|
6813
|
+
required: true
|
|
6553
6814
|
}
|
|
6554
|
-
}
|
|
6555
|
-
render: (_args, value) => [{
|
|
6556
|
-
type: "text",
|
|
6557
|
-
text: `Course pack “${value.title}” — ${value.lessonCount} lessons, ${value.chars} chars. Give the learner the markdown below (copyable); importing it goes through study_import_markdown.
|
|
6558
|
-
|
|
6559
|
-
` + value.markdown
|
|
6560
|
-
}]
|
|
6815
|
+
}
|
|
6561
6816
|
},
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6817
|
+
render: (_args, value) => [{
|
|
6818
|
+
type: "text",
|
|
6819
|
+
text: `Consolidation window since ${value.since ?? "(beginning)"}: ${value.counts.friction} friction, ${value.counts.practice} practice entries.` + (value.entries.length === 0 ? " Nothing to distill — tell the learner their memory is up to date." : " Distill these into 0–3 study_remember writes (global / pattern / lesson), then summarize in one line.")
|
|
6820
|
+
}]
|
|
6821
|
+
},
|
|
6822
|
+
execute() {
|
|
6823
|
+
return mutate((state) => {
|
|
6824
|
+
const window = gatherConsolidationWindow(state);
|
|
6825
|
+
const watermark = (/* @__PURE__ */ new Date()).toISOString();
|
|
6826
|
+
state.lastConsolidatedAt = watermark;
|
|
6566
6827
|
return {
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
markdown
|
|
6828
|
+
since: window.since,
|
|
6829
|
+
entries: window.entries,
|
|
6830
|
+
counts: window.counts,
|
|
6831
|
+
watermark
|
|
6572
6832
|
};
|
|
6573
|
-
}
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6833
|
+
});
|
|
6834
|
+
},
|
|
6835
|
+
presentCall: () => ({
|
|
6836
|
+
card: "generic",
|
|
6837
|
+
title: "Consolidate learner memory"
|
|
6838
|
+
})
|
|
6839
|
+
});
|
|
6840
|
+
const exportTool = defineTool({
|
|
6841
|
+
name: "study_export",
|
|
6842
|
+
description: "Export one course as a single markdown learning pack (upstream pack-export, zero-LLM): sections and lesson bodies verbatim. The receiver imports it anywhere through study_import_markdown — same plugin, fresh machine, no network. Present the pack to the learner (a copyable block) or save it into the study workspace when they ask for a file.",
|
|
6843
|
+
parameters: { courseId: {
|
|
6844
|
+
type: "string",
|
|
6845
|
+
required: true,
|
|
6846
|
+
description: "Course id to export."
|
|
6847
|
+
} },
|
|
6848
|
+
output: {
|
|
6849
|
+
schema: {
|
|
6850
|
+
type: "object",
|
|
6851
|
+
additionalProperties: false,
|
|
6852
|
+
properties: {
|
|
6853
|
+
courseId: {
|
|
6854
|
+
type: "string",
|
|
6855
|
+
required: true
|
|
6856
|
+
},
|
|
6857
|
+
title: {
|
|
6858
|
+
type: "string",
|
|
6859
|
+
required: true
|
|
6860
|
+
},
|
|
6861
|
+
lessonCount: {
|
|
6862
|
+
type: "integer",
|
|
6863
|
+
required: true
|
|
6864
|
+
},
|
|
6865
|
+
chars: {
|
|
6866
|
+
type: "integer",
|
|
6867
|
+
required: true
|
|
6868
|
+
},
|
|
6869
|
+
markdown: {
|
|
6870
|
+
type: "string",
|
|
6871
|
+
required: true
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
},
|
|
6875
|
+
render: (_args, value) => [{
|
|
6876
|
+
type: "text",
|
|
6877
|
+
text: `Course pack “${value.title}” — ${value.lessonCount} lessons, ${value.chars} chars. Give the learner the markdown below (copyable); importing it goes through study_import_markdown.
|
|
6878
|
+
|
|
6879
|
+
` + value.markdown
|
|
6880
|
+
}]
|
|
6881
|
+
},
|
|
6882
|
+
execute(args) {
|
|
6883
|
+
const course = findCourse(store.get(), args.courseId);
|
|
6884
|
+
const markdown = courseToPackMarkdown(course);
|
|
6885
|
+
const lessonCount = course.sections.reduce((n, sec) => n + sec.lessons.filter((l) => l.kind !== "exam").length, 0);
|
|
6886
|
+
return {
|
|
6887
|
+
courseId: course.id,
|
|
6888
|
+
title: course.title,
|
|
6889
|
+
lessonCount,
|
|
6890
|
+
chars: markdown.length,
|
|
6891
|
+
markdown
|
|
6892
|
+
};
|
|
6893
|
+
},
|
|
6894
|
+
isConcurrencySafe: () => true,
|
|
6895
|
+
presentCall: (args) => ({
|
|
6896
|
+
card: "generic",
|
|
6897
|
+
title: `Export course: ${args.courseId}`,
|
|
6898
|
+
kind: "read"
|
|
6899
|
+
})
|
|
6900
|
+
});
|
|
6901
|
+
const noteSaveTool = defineTool({
|
|
6902
|
+
name: "study_note_save",
|
|
6903
|
+
description: "Save an entry to the learner's Cornell notebook. Zones: `understand` (knowledge structures you generated — concept maps as mermaid, compare tables, diagrams; sediment your best structures here after showing them), `record` (the learner's own words — when they ask to take a note, or when they write something worth keeping, with the verbatim `quote`), `practice` (quiz log — normally written automatically by study_record_answer).",
|
|
6904
|
+
parameters: {
|
|
6905
|
+
lessonId: {
|
|
6906
|
+
type: "string",
|
|
6907
|
+
required: true,
|
|
6908
|
+
description: "Lesson the note belongs to."
|
|
6909
|
+
},
|
|
6910
|
+
zone: {
|
|
6911
|
+
type: "string",
|
|
6912
|
+
required: true,
|
|
6913
|
+
enum: [...NOTE_ZONES],
|
|
6914
|
+
description: "understand | record | practice."
|
|
6915
|
+
},
|
|
6916
|
+
title: {
|
|
6917
|
+
type: "string",
|
|
6918
|
+
required: true,
|
|
6919
|
+
description: "Short entry title."
|
|
6920
|
+
},
|
|
6921
|
+
text: {
|
|
6922
|
+
type: "string",
|
|
6923
|
+
required: true,
|
|
6924
|
+
description: "Entry body — markdown for the understand zone."
|
|
6925
|
+
},
|
|
6926
|
+
source: {
|
|
6927
|
+
type: "string",
|
|
6928
|
+
required: true,
|
|
6929
|
+
enum: [...NOTE_SOURCES],
|
|
6930
|
+
description: "ai (you generated) | content (quoted from lesson) | chat (quoted from conversation)."
|
|
6931
|
+
},
|
|
6932
|
+
quote: {
|
|
6933
|
+
type: "string",
|
|
6934
|
+
description: "Verbatim source quote, for record-zone notes."
|
|
6935
|
+
}
|
|
6936
|
+
},
|
|
6937
|
+
output: {
|
|
6938
|
+
schema: {
|
|
6939
|
+
type: "object",
|
|
6940
|
+
additionalProperties: false,
|
|
6941
|
+
properties: {
|
|
6942
|
+
noteId: {
|
|
6943
|
+
type: "string",
|
|
6944
|
+
required: true
|
|
6945
|
+
},
|
|
6946
|
+
zone: {
|
|
6947
|
+
type: "string",
|
|
6948
|
+
required: true
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
},
|
|
6952
|
+
render: (_args, value) => [{
|
|
6953
|
+
type: "text",
|
|
6954
|
+
text: `Saved ${value.zone}-zone note ${value.noteId}.`
|
|
6955
|
+
}]
|
|
6956
|
+
},
|
|
6957
|
+
async execute(args) {
|
|
6958
|
+
return mutate((state) => {
|
|
6959
|
+
const note = addNote(state, args.lessonId, args.zone, args.title, args.text, args.source, args.quote ?? null, /* @__PURE__ */ new Date());
|
|
6960
|
+
return {
|
|
6961
|
+
noteId: note.id,
|
|
6962
|
+
zone: note.zone
|
|
6963
|
+
};
|
|
6964
|
+
});
|
|
6965
|
+
},
|
|
6966
|
+
presentCall: (args) => ({
|
|
6967
|
+
card: "generic",
|
|
6968
|
+
title: `Save ${args.zone} note: ${args.title}`
|
|
6969
|
+
})
|
|
6970
|
+
});
|
|
6971
|
+
const notesTool = defineTool({
|
|
6972
|
+
name: "study_notes",
|
|
6973
|
+
description: "Read the learner's Cornell notebook: three zones per lesson (understand structures, learner records, practice log).",
|
|
6974
|
+
parameters: { lessonId: {
|
|
6975
|
+
type: "string",
|
|
6976
|
+
description: "One lesson's notes; omit for all lessons (most recent last)."
|
|
6977
|
+
} },
|
|
6978
|
+
output: {
|
|
6979
|
+
schema: {
|
|
6980
|
+
type: "object",
|
|
6981
|
+
additionalProperties: false,
|
|
6982
|
+
properties: {
|
|
6983
|
+
total: {
|
|
6984
|
+
type: "integer",
|
|
6985
|
+
required: true
|
|
6986
|
+
},
|
|
6987
|
+
notes: {
|
|
6988
|
+
type: "array",
|
|
6989
|
+
required: true,
|
|
6990
|
+
items: {
|
|
6991
|
+
type: "object",
|
|
6992
|
+
additionalProperties: false,
|
|
6993
|
+
properties: {
|
|
6994
|
+
id: {
|
|
6995
|
+
type: "string",
|
|
6996
|
+
required: true
|
|
6997
|
+
},
|
|
6998
|
+
lessonTitle: {
|
|
6999
|
+
type: "string",
|
|
7000
|
+
required: true
|
|
7001
|
+
},
|
|
7002
|
+
zone: {
|
|
7003
|
+
type: "string",
|
|
7004
|
+
required: true,
|
|
7005
|
+
enum: [...NOTE_ZONES]
|
|
7006
|
+
},
|
|
7007
|
+
title: {
|
|
7008
|
+
type: "string",
|
|
7009
|
+
required: true
|
|
7010
|
+
},
|
|
7011
|
+
text: {
|
|
7012
|
+
type: "string",
|
|
7013
|
+
required: true
|
|
7014
|
+
},
|
|
7015
|
+
source: {
|
|
7016
|
+
type: "string",
|
|
7017
|
+
required: true,
|
|
7018
|
+
enum: [...NOTE_SOURCES]
|
|
7019
|
+
},
|
|
7020
|
+
quote: {
|
|
7021
|
+
...nullableString,
|
|
7022
|
+
required: true
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
},
|
|
7029
|
+
render: (_args, value) => [{
|
|
7030
|
+
type: "text",
|
|
7031
|
+
text: value.total === 0 ? "Notebook is empty." : value.notes.map((n) => `[${n.zone}] ${n.lessonTitle} — ${n.title}${n.quote === null ? "" : ` (quote: “${n.quote.slice(0, 60)}”)`}`).join("\n")
|
|
7032
|
+
}]
|
|
7033
|
+
},
|
|
7034
|
+
async execute(args) {
|
|
7035
|
+
const state = store.get();
|
|
7036
|
+
const notes = (args.lessonId === void 0 ? state.courses.flatMap((c) => c.sections.flatMap((s) => s.lessons)) : [findLesson(state, args.lessonId).lesson]).flatMap((l) => l.notes.map((n) => ({
|
|
7037
|
+
id: n.id,
|
|
7038
|
+
lessonTitle: l.title,
|
|
7039
|
+
zone: n.zone,
|
|
7040
|
+
title: n.title,
|
|
7041
|
+
text: n.text,
|
|
7042
|
+
source: n.source,
|
|
7043
|
+
quote: n.quote
|
|
7044
|
+
})));
|
|
7045
|
+
return {
|
|
7046
|
+
total: notes.length,
|
|
7047
|
+
notes
|
|
7048
|
+
};
|
|
7049
|
+
},
|
|
7050
|
+
isConcurrencySafe: () => true,
|
|
7051
|
+
presentCall: () => ({
|
|
7052
|
+
card: "generic",
|
|
7053
|
+
title: "Read notebook",
|
|
7054
|
+
kind: "read"
|
|
7055
|
+
})
|
|
7056
|
+
});
|
|
7057
|
+
const setModeTool = defineTool({
|
|
7058
|
+
name: "study_set_mode",
|
|
7059
|
+
description: "Switch the tutoring soul when the learner asks for a different style: `direct` 精讲 (explain first, then verify), `guide` 引导 (questions first, hand over steps), `practice` 实战 (learn inside real, messy problems). Takes effect from the next reply.",
|
|
7060
|
+
parameters: { mode: {
|
|
7061
|
+
type: "string",
|
|
7062
|
+
required: true,
|
|
7063
|
+
enum: [...MODES],
|
|
7064
|
+
description: "direct | guide | practice."
|
|
7065
|
+
} },
|
|
7066
|
+
output: {
|
|
7067
|
+
schema: {
|
|
7068
|
+
type: "object",
|
|
7069
|
+
additionalProperties: false,
|
|
7070
|
+
properties: { mode: {
|
|
7071
|
+
type: "string",
|
|
7072
|
+
required: true,
|
|
7073
|
+
enum: [...MODES]
|
|
7074
|
+
} }
|
|
7075
|
+
},
|
|
7076
|
+
render: (_args, value) => [{
|
|
7077
|
+
type: "text",
|
|
7078
|
+
text: `Tutoring soul switched to ${value.mode} (effective next reply).`
|
|
7079
|
+
}]
|
|
7080
|
+
},
|
|
7081
|
+
async execute(args) {
|
|
7082
|
+
return mutate((state) => {
|
|
7083
|
+
state.mode = args.mode;
|
|
7084
|
+
return { mode: state.mode };
|
|
7085
|
+
});
|
|
7086
|
+
},
|
|
7087
|
+
presentCall: (args) => ({
|
|
7088
|
+
card: "generic",
|
|
7089
|
+
title: `Switch soul: ${args.mode}`
|
|
7090
|
+
})
|
|
7091
|
+
});
|
|
7092
|
+
const generateQuizTool = defineTool({
|
|
7093
|
+
name: "study_generate_quiz",
|
|
7094
|
+
description: "Generate an interactive practice card (quiz artifact) for the focus lesson: 3-4 questions (5 max), each with 2-6 options, the 0-based correct `answer` index, and an `explanation` of why it is right. The learner answers on the card itself (locally judged, progress kept); when they finish, a summary hook arrives in the conversation — acknowledge it, address wrong answers, do NOT re-grade these through study_record_answer. Use for practice blocks and review consolidation; single conversational questions stay as prose A-D options.",
|
|
7095
|
+
parameters: {
|
|
7096
|
+
lessonId: {
|
|
7097
|
+
type: "string",
|
|
7098
|
+
required: true,
|
|
7099
|
+
description: "Lesson the practice card belongs to."
|
|
7100
|
+
},
|
|
7101
|
+
title: {
|
|
7102
|
+
type: "string",
|
|
7103
|
+
description: "Card title (defaults to 练习)."
|
|
7104
|
+
},
|
|
7105
|
+
questions: {
|
|
7106
|
+
type: "array",
|
|
7107
|
+
required: true,
|
|
7108
|
+
description: "The questions, in answering order.",
|
|
7109
|
+
items: {
|
|
7110
|
+
type: "object",
|
|
7111
|
+
additionalProperties: false,
|
|
7112
|
+
properties: {
|
|
7113
|
+
prompt: {
|
|
7114
|
+
type: "string",
|
|
7115
|
+
required: true
|
|
7116
|
+
},
|
|
7117
|
+
options: {
|
|
7118
|
+
type: "array",
|
|
7119
|
+
required: true,
|
|
7120
|
+
items: { type: "string" }
|
|
7121
|
+
},
|
|
7122
|
+
answer: {
|
|
7123
|
+
type: "integer",
|
|
7124
|
+
required: true,
|
|
7125
|
+
description: "0-based index into options."
|
|
7126
|
+
},
|
|
7127
|
+
explanation: {
|
|
7128
|
+
type: "string",
|
|
7129
|
+
required: true,
|
|
7130
|
+
description: "Why the right answer is right."
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
},
|
|
7136
|
+
output: {
|
|
7137
|
+
schema: {
|
|
7138
|
+
type: "object",
|
|
7139
|
+
additionalProperties: false,
|
|
7140
|
+
properties: {
|
|
7141
|
+
artifactType: {
|
|
7142
|
+
type: "string",
|
|
7143
|
+
required: true,
|
|
7144
|
+
enum: ["quiz"]
|
|
7145
|
+
},
|
|
7146
|
+
artifactId: {
|
|
7147
|
+
type: "string",
|
|
7148
|
+
required: true
|
|
7149
|
+
},
|
|
7150
|
+
created: {
|
|
7151
|
+
type: "boolean",
|
|
7152
|
+
required: true
|
|
7153
|
+
},
|
|
7154
|
+
title: {
|
|
7155
|
+
type: "string",
|
|
7156
|
+
required: true
|
|
7157
|
+
},
|
|
7158
|
+
questions: {
|
|
7159
|
+
type: "array",
|
|
7160
|
+
required: true,
|
|
7161
|
+
items: {
|
|
7162
|
+
type: "object",
|
|
7163
|
+
additionalProperties: false,
|
|
7164
|
+
properties: {
|
|
7165
|
+
prompt: {
|
|
7166
|
+
type: "string",
|
|
7167
|
+
required: true
|
|
7168
|
+
},
|
|
7169
|
+
options: {
|
|
7170
|
+
type: "array",
|
|
7171
|
+
required: true,
|
|
7172
|
+
items: { type: "string" }
|
|
7173
|
+
},
|
|
7174
|
+
answer: {
|
|
7175
|
+
type: "integer",
|
|
7176
|
+
required: true
|
|
7177
|
+
},
|
|
7178
|
+
explanation: {
|
|
7179
|
+
type: "string",
|
|
7180
|
+
required: true
|
|
7181
|
+
}
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7184
|
+
},
|
|
7185
|
+
warnings: {
|
|
7186
|
+
type: "array",
|
|
7187
|
+
items: { type: "string" }
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
},
|
|
7191
|
+
render: (_args, value) => [{
|
|
7192
|
+
type: "text",
|
|
7193
|
+
text: `Practice card (${value.questions.length} questions): ${value.title}${value.created ? "" : " (already recorded)"}${value.warnings === void 0 || value.warnings.length === 0 ? "" : ` — warnings: ${value.warnings.join("; ")}`}`
|
|
7194
|
+
}]
|
|
7195
|
+
},
|
|
7196
|
+
async execute(args) {
|
|
7197
|
+
const sanitized = sanitizeQuiz(args);
|
|
7198
|
+
return mutate((state) => {
|
|
7199
|
+
const id = artifactId("quiz", sanitized.data);
|
|
7200
|
+
const artifact = {
|
|
7201
|
+
id,
|
|
7202
|
+
artifactType: "quiz",
|
|
7203
|
+
title: typeof sanitized.data.title === "string" ? sanitized.data.title : "练习",
|
|
7204
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7205
|
+
hash: id.slice(5),
|
|
7206
|
+
data: sanitized.data
|
|
7207
|
+
};
|
|
7208
|
+
const result = recordArtifact(state, args.lessonId, artifact);
|
|
7209
|
+
return {
|
|
7210
|
+
artifactType: "quiz",
|
|
7211
|
+
artifactId: result.artifact.id,
|
|
7212
|
+
created: result.created,
|
|
7213
|
+
title: result.artifact.title,
|
|
7214
|
+
questions: result.artifact.data.questions ?? [],
|
|
7215
|
+
warnings: result.artifact.data.warnings ?? []
|
|
7216
|
+
};
|
|
7217
|
+
});
|
|
7218
|
+
},
|
|
7219
|
+
presentCall: (args) => ({
|
|
7220
|
+
card: "generic",
|
|
7221
|
+
title: `Generate practice card: ${typeof args.title === "string" ? args.title : "练习"} (${args.questions?.length ?? 0} questions)`
|
|
7222
|
+
})
|
|
7223
|
+
});
|
|
7224
|
+
/** Record one sanitized artifact on its lesson (shared by the artifact tools). */
|
|
7225
|
+
const recordSanitized = (lessonId, result, type) => mutate((state) => {
|
|
7226
|
+
const id = artifactId(type, result.data);
|
|
7227
|
+
const stored = recordArtifact(state, lessonId, {
|
|
7228
|
+
id,
|
|
7229
|
+
artifactType: type,
|
|
7230
|
+
title: typeof result.data.title === "string" ? result.data.title : type,
|
|
7231
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7232
|
+
hash: id.slice(type.length + 1),
|
|
7233
|
+
data: result.data
|
|
7234
|
+
});
|
|
7235
|
+
return {
|
|
7236
|
+
artifactType: type,
|
|
7237
|
+
artifactId: stored.artifact.id,
|
|
7238
|
+
created: stored.created,
|
|
7239
|
+
...stored.artifact.data
|
|
7240
|
+
};
|
|
7241
|
+
});
|
|
7242
|
+
return [
|
|
7243
|
+
importMarkdown,
|
|
7244
|
+
importFolder,
|
|
7245
|
+
importGithub,
|
|
7246
|
+
importUrl,
|
|
7247
|
+
applyDesign,
|
|
7248
|
+
listCourses,
|
|
7249
|
+
courseMap,
|
|
7250
|
+
lessonContent,
|
|
7251
|
+
recordAnswerTool,
|
|
7252
|
+
examResultTool,
|
|
7253
|
+
completeLessonTool,
|
|
7254
|
+
dueReviewsTool,
|
|
7255
|
+
recordReviewTool,
|
|
7256
|
+
deleteCourseTool,
|
|
7257
|
+
defineConceptsTool,
|
|
7258
|
+
proposeMasteryTool,
|
|
7259
|
+
resolveProposalTool,
|
|
7260
|
+
reportFrictionTool,
|
|
7261
|
+
rememberTool,
|
|
7262
|
+
consolidateTool,
|
|
7263
|
+
translateLessonTool,
|
|
7264
|
+
exportTool,
|
|
7265
|
+
noteSaveTool,
|
|
7266
|
+
notesTool,
|
|
7267
|
+
generateQuizTool,
|
|
7268
|
+
defineTool({
|
|
7269
|
+
name: "study_pose_guess",
|
|
7270
|
+
description: "Pose the opening two-option guess (the curiosity hook): one or two sentences of prose first (counter-intuitive, everyday-related), then this tool with exactly 2 short options. The learner picks one; you reveal the answer NEXT turn and teach the lesson's core point. Iron rules: unscored, never touches mastery, never say 答对/答错 — this is a hook, not a quiz.",
|
|
7271
|
+
parameters: {
|
|
7272
|
+
lessonId: {
|
|
7273
|
+
type: "string",
|
|
6608
7274
|
required: true,
|
|
6609
|
-
|
|
6610
|
-
description: "ai (you generated) | content (quoted from lesson) | chat (quoted from conversation)."
|
|
7275
|
+
description: "Lesson the guess belongs to."
|
|
6611
7276
|
},
|
|
6612
|
-
|
|
7277
|
+
prompt: {
|
|
6613
7278
|
type: "string",
|
|
6614
|
-
|
|
7279
|
+
required: true,
|
|
7280
|
+
description: "e.g. 你觉得:递归算阶乘会比循环——更慢,还是差不多?"
|
|
7281
|
+
},
|
|
7282
|
+
options: {
|
|
7283
|
+
type: "array",
|
|
7284
|
+
required: true,
|
|
7285
|
+
description: "Exactly 2 options.",
|
|
7286
|
+
items: {
|
|
7287
|
+
type: "object",
|
|
7288
|
+
additionalProperties: false,
|
|
7289
|
+
properties: {
|
|
7290
|
+
id: {
|
|
7291
|
+
type: "string",
|
|
7292
|
+
required: true
|
|
7293
|
+
},
|
|
7294
|
+
label: {
|
|
7295
|
+
type: "string",
|
|
7296
|
+
required: true
|
|
7297
|
+
}
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
6615
7300
|
}
|
|
6616
7301
|
},
|
|
6617
7302
|
output: {
|
|
@@ -6619,156 +7304,331 @@ function studyTools(store, deps = {}) {
|
|
|
6619
7304
|
type: "object",
|
|
6620
7305
|
additionalProperties: false,
|
|
6621
7306
|
properties: {
|
|
6622
|
-
|
|
7307
|
+
artifactType: {
|
|
7308
|
+
type: "string",
|
|
7309
|
+
required: true,
|
|
7310
|
+
enum: ["guess"]
|
|
7311
|
+
},
|
|
7312
|
+
artifactId: {
|
|
6623
7313
|
type: "string",
|
|
6624
7314
|
required: true
|
|
6625
7315
|
},
|
|
6626
|
-
|
|
7316
|
+
created: {
|
|
7317
|
+
type: "boolean",
|
|
7318
|
+
required: true
|
|
7319
|
+
},
|
|
7320
|
+
prompt: {
|
|
6627
7321
|
type: "string",
|
|
6628
7322
|
required: true
|
|
7323
|
+
},
|
|
7324
|
+
options: {
|
|
7325
|
+
type: "array",
|
|
7326
|
+
required: true,
|
|
7327
|
+
items: {
|
|
7328
|
+
type: "object",
|
|
7329
|
+
additionalProperties: false,
|
|
7330
|
+
properties: {
|
|
7331
|
+
id: {
|
|
7332
|
+
type: "string",
|
|
7333
|
+
required: true
|
|
7334
|
+
},
|
|
7335
|
+
label: {
|
|
7336
|
+
type: "string",
|
|
7337
|
+
required: true
|
|
7338
|
+
}
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
6629
7341
|
}
|
|
6630
7342
|
}
|
|
6631
7343
|
},
|
|
6632
7344
|
render: (_args, value) => [{
|
|
6633
7345
|
type: "text",
|
|
6634
|
-
text: `
|
|
7346
|
+
text: `Guess posed: ${value.prompt}`
|
|
6635
7347
|
}]
|
|
6636
7348
|
},
|
|
6637
7349
|
async execute(args) {
|
|
6638
|
-
return
|
|
6639
|
-
const note = addNote(state, args.lessonId, args.zone, args.title, args.text, args.source, args.quote ?? null, /* @__PURE__ */ new Date());
|
|
6640
|
-
return {
|
|
6641
|
-
noteId: note.id,
|
|
6642
|
-
zone: note.zone
|
|
6643
|
-
};
|
|
6644
|
-
});
|
|
7350
|
+
return recordSanitized(args.lessonId, sanitizeGuess(args), "guess");
|
|
6645
7351
|
},
|
|
6646
7352
|
presentCall: (args) => ({
|
|
6647
7353
|
card: "generic",
|
|
6648
|
-
title: `
|
|
7354
|
+
title: `Pose guess: ${typeof args.prompt === "string" ? args.prompt.slice(0, 40) : ""}`
|
|
6649
7355
|
})
|
|
6650
7356
|
}),
|
|
6651
7357
|
defineTool({
|
|
6652
|
-
name: "
|
|
6653
|
-
description: "
|
|
6654
|
-
parameters: {
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
7358
|
+
name: "study_compare_table",
|
|
7359
|
+
description: "Generate a compare table for two or more concepts/solutions/technologies — when the learner asks A 和 B 有什么区别 or a horizontal comparison helps. Rendered as a table artifact card.",
|
|
7360
|
+
parameters: {
|
|
7361
|
+
lessonId: {
|
|
7362
|
+
type: "string",
|
|
7363
|
+
required: true,
|
|
7364
|
+
description: "Lesson the table belongs to."
|
|
7365
|
+
},
|
|
7366
|
+
title: {
|
|
7367
|
+
type: "string",
|
|
7368
|
+
required: true,
|
|
7369
|
+
description: "e.g. SQL vs NoSQL"
|
|
7370
|
+
},
|
|
7371
|
+
headers: {
|
|
7372
|
+
type: "array",
|
|
7373
|
+
required: true,
|
|
7374
|
+
items: { type: "string" },
|
|
7375
|
+
description: "Column names (first is usually the dimension)."
|
|
7376
|
+
},
|
|
7377
|
+
rows: {
|
|
7378
|
+
type: "array",
|
|
7379
|
+
required: true,
|
|
7380
|
+
items: {
|
|
7381
|
+
type: "array",
|
|
7382
|
+
items: { type: "string" }
|
|
7383
|
+
},
|
|
7384
|
+
description: "Rows; each row has headers.length cells."
|
|
7385
|
+
}
|
|
7386
|
+
},
|
|
6658
7387
|
output: {
|
|
6659
7388
|
schema: {
|
|
6660
7389
|
type: "object",
|
|
6661
7390
|
additionalProperties: false,
|
|
6662
7391
|
properties: {
|
|
6663
|
-
|
|
6664
|
-
type: "
|
|
7392
|
+
artifactType: {
|
|
7393
|
+
type: "string",
|
|
7394
|
+
required: true,
|
|
7395
|
+
enum: ["compare_table"]
|
|
7396
|
+
},
|
|
7397
|
+
artifactId: {
|
|
7398
|
+
type: "string",
|
|
7399
|
+
required: true
|
|
7400
|
+
},
|
|
7401
|
+
created: {
|
|
7402
|
+
type: "boolean",
|
|
7403
|
+
required: true
|
|
7404
|
+
},
|
|
7405
|
+
title: {
|
|
7406
|
+
type: "string",
|
|
6665
7407
|
required: true
|
|
6666
7408
|
},
|
|
6667
|
-
|
|
7409
|
+
headers: {
|
|
7410
|
+
type: "array",
|
|
7411
|
+
required: true,
|
|
7412
|
+
items: { type: "string" }
|
|
7413
|
+
},
|
|
7414
|
+
rows: {
|
|
6668
7415
|
type: "array",
|
|
6669
7416
|
required: true,
|
|
6670
7417
|
items: {
|
|
6671
|
-
type: "
|
|
6672
|
-
|
|
6673
|
-
properties: {
|
|
6674
|
-
id: {
|
|
6675
|
-
type: "string",
|
|
6676
|
-
required: true
|
|
6677
|
-
},
|
|
6678
|
-
lessonTitle: {
|
|
6679
|
-
type: "string",
|
|
6680
|
-
required: true
|
|
6681
|
-
},
|
|
6682
|
-
zone: {
|
|
6683
|
-
type: "string",
|
|
6684
|
-
required: true,
|
|
6685
|
-
enum: [...NOTE_ZONES]
|
|
6686
|
-
},
|
|
6687
|
-
title: {
|
|
6688
|
-
type: "string",
|
|
6689
|
-
required: true
|
|
6690
|
-
},
|
|
6691
|
-
text: {
|
|
6692
|
-
type: "string",
|
|
6693
|
-
required: true
|
|
6694
|
-
},
|
|
6695
|
-
source: {
|
|
6696
|
-
type: "string",
|
|
6697
|
-
required: true,
|
|
6698
|
-
enum: [...NOTE_SOURCES]
|
|
6699
|
-
},
|
|
6700
|
-
quote: {
|
|
6701
|
-
...nullableString,
|
|
6702
|
-
required: true
|
|
6703
|
-
}
|
|
6704
|
-
}
|
|
7418
|
+
type: "array",
|
|
7419
|
+
items: { type: "string" }
|
|
6705
7420
|
}
|
|
7421
|
+
},
|
|
7422
|
+
warnings: {
|
|
7423
|
+
type: "array",
|
|
7424
|
+
items: { type: "string" }
|
|
6706
7425
|
}
|
|
6707
7426
|
}
|
|
6708
7427
|
},
|
|
6709
7428
|
render: (_args, value) => [{
|
|
6710
7429
|
type: "text",
|
|
6711
|
-
text:
|
|
7430
|
+
text: `Compare table: ${value.title} (${value.rows.length} rows)`
|
|
6712
7431
|
}]
|
|
6713
7432
|
},
|
|
6714
7433
|
async execute(args) {
|
|
6715
|
-
|
|
6716
|
-
const notes = (args.lessonId === void 0 ? state.courses.flatMap((c) => c.sections.flatMap((s) => s.lessons)) : [findLesson(state, args.lessonId).lesson]).flatMap((l) => l.notes.map((n) => ({
|
|
6717
|
-
id: n.id,
|
|
6718
|
-
lessonTitle: l.title,
|
|
6719
|
-
zone: n.zone,
|
|
6720
|
-
title: n.title,
|
|
6721
|
-
text: n.text,
|
|
6722
|
-
source: n.source,
|
|
6723
|
-
quote: n.quote
|
|
6724
|
-
})));
|
|
6725
|
-
return {
|
|
6726
|
-
total: notes.length,
|
|
6727
|
-
notes
|
|
6728
|
-
};
|
|
7434
|
+
return recordSanitized(args.lessonId, sanitizeCompareTable(args), "compare_table");
|
|
6729
7435
|
},
|
|
6730
|
-
|
|
6731
|
-
presentCall: () => ({
|
|
7436
|
+
presentCall: (args) => ({
|
|
6732
7437
|
card: "generic",
|
|
6733
|
-
title: "
|
|
6734
|
-
kind: "read"
|
|
7438
|
+
title: `Compare table: ${typeof args.title === "string" ? args.title : ""}`
|
|
6735
7439
|
})
|
|
6736
7440
|
}),
|
|
6737
7441
|
defineTool({
|
|
6738
|
-
name: "
|
|
6739
|
-
description: "
|
|
6740
|
-
parameters: {
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
7442
|
+
name: "study_draw_diagram",
|
|
7443
|
+
description: "Draw a structured mermaid diagram: steps/decisions/causality → flowchart TD (or LR for short chains); multi-party interactions → sequenceDiagram; states/transitions → stateDiagram-v2. Return valid mermaid syntax only (no outer fences). Rendered as a diagram artifact card.",
|
|
7444
|
+
parameters: {
|
|
7445
|
+
lessonId: {
|
|
7446
|
+
type: "string",
|
|
7447
|
+
required: true,
|
|
7448
|
+
description: "Lesson the diagram belongs to."
|
|
7449
|
+
},
|
|
7450
|
+
title: {
|
|
7451
|
+
type: "string",
|
|
7452
|
+
required: true
|
|
7453
|
+
},
|
|
7454
|
+
diagramType: {
|
|
7455
|
+
type: "string",
|
|
7456
|
+
required: true,
|
|
7457
|
+
enum: [
|
|
7458
|
+
"flowchart",
|
|
7459
|
+
"sequence",
|
|
7460
|
+
"state"
|
|
7461
|
+
]
|
|
7462
|
+
},
|
|
7463
|
+
mermaid: {
|
|
7464
|
+
type: "string",
|
|
7465
|
+
required: true,
|
|
7466
|
+
description: "Mermaid code without fences."
|
|
7467
|
+
}
|
|
7468
|
+
},
|
|
6746
7469
|
output: {
|
|
6747
7470
|
schema: {
|
|
6748
7471
|
type: "object",
|
|
6749
7472
|
additionalProperties: false,
|
|
6750
|
-
properties: {
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
7473
|
+
properties: {
|
|
7474
|
+
artifactType: {
|
|
7475
|
+
type: "string",
|
|
7476
|
+
required: true,
|
|
7477
|
+
enum: ["diagram"]
|
|
7478
|
+
},
|
|
7479
|
+
artifactId: {
|
|
7480
|
+
type: "string",
|
|
7481
|
+
required: true
|
|
7482
|
+
},
|
|
7483
|
+
created: {
|
|
7484
|
+
type: "boolean",
|
|
7485
|
+
required: true
|
|
7486
|
+
},
|
|
7487
|
+
title: {
|
|
7488
|
+
type: "string",
|
|
7489
|
+
required: true
|
|
7490
|
+
},
|
|
7491
|
+
diagramType: {
|
|
7492
|
+
type: "string",
|
|
7493
|
+
required: true
|
|
7494
|
+
},
|
|
7495
|
+
mermaid: {
|
|
7496
|
+
type: "string",
|
|
7497
|
+
required: true
|
|
7498
|
+
}
|
|
7499
|
+
}
|
|
6755
7500
|
},
|
|
6756
7501
|
render: (_args, value) => [{
|
|
6757
7502
|
type: "text",
|
|
6758
|
-
text: `
|
|
7503
|
+
text: `Diagram: ${value.title} (${value.diagramType})`
|
|
6759
7504
|
}]
|
|
6760
7505
|
},
|
|
6761
7506
|
async execute(args) {
|
|
6762
|
-
return
|
|
6763
|
-
state.mode = args.mode;
|
|
6764
|
-
return { mode: state.mode };
|
|
6765
|
-
});
|
|
7507
|
+
return recordSanitized(args.lessonId, sanitizeDiagram(args), "diagram");
|
|
6766
7508
|
},
|
|
6767
7509
|
presentCall: (args) => ({
|
|
6768
7510
|
card: "generic",
|
|
6769
|
-
title: `
|
|
7511
|
+
title: `Draw diagram: ${typeof args.title === "string" ? args.title : ""}`
|
|
6770
7512
|
})
|
|
6771
|
-
})
|
|
7513
|
+
}),
|
|
7514
|
+
defineTool({
|
|
7515
|
+
name: "study_code_walkthrough",
|
|
7516
|
+
description: "Walk through a piece of code line-by-line / segment-by-segment — when the learner asks what the code means or the lesson contains code needing teardown. Rendered with line numbers + per-segment notes.",
|
|
7517
|
+
parameters: {
|
|
7518
|
+
lessonId: {
|
|
7519
|
+
type: "string",
|
|
7520
|
+
required: true,
|
|
7521
|
+
description: "Lesson the walkthrough belongs to."
|
|
7522
|
+
},
|
|
7523
|
+
title: {
|
|
7524
|
+
type: "string",
|
|
7525
|
+
required: true
|
|
7526
|
+
},
|
|
7527
|
+
language: {
|
|
7528
|
+
type: "string",
|
|
7529
|
+
required: true,
|
|
7530
|
+
description: "e.g. typescript / python"
|
|
7531
|
+
},
|
|
7532
|
+
code: {
|
|
7533
|
+
type: "string",
|
|
7534
|
+
required: true
|
|
7535
|
+
},
|
|
7536
|
+
annotations: {
|
|
7537
|
+
type: "array",
|
|
7538
|
+
required: true,
|
|
7539
|
+
description: "Per-segment notes.",
|
|
7540
|
+
items: {
|
|
7541
|
+
type: "object",
|
|
7542
|
+
additionalProperties: false,
|
|
7543
|
+
properties: {
|
|
7544
|
+
lineStart: {
|
|
7545
|
+
type: "integer",
|
|
7546
|
+
required: true
|
|
7547
|
+
},
|
|
7548
|
+
lineEnd: {
|
|
7549
|
+
type: "integer",
|
|
7550
|
+
required: true
|
|
7551
|
+
},
|
|
7552
|
+
note: {
|
|
7553
|
+
type: "string",
|
|
7554
|
+
required: true
|
|
7555
|
+
}
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
}
|
|
7559
|
+
},
|
|
7560
|
+
output: {
|
|
7561
|
+
schema: {
|
|
7562
|
+
type: "object",
|
|
7563
|
+
additionalProperties: false,
|
|
7564
|
+
properties: {
|
|
7565
|
+
artifactType: {
|
|
7566
|
+
type: "string",
|
|
7567
|
+
required: true,
|
|
7568
|
+
enum: ["code_walkthrough"]
|
|
7569
|
+
},
|
|
7570
|
+
artifactId: {
|
|
7571
|
+
type: "string",
|
|
7572
|
+
required: true
|
|
7573
|
+
},
|
|
7574
|
+
created: {
|
|
7575
|
+
type: "boolean",
|
|
7576
|
+
required: true
|
|
7577
|
+
},
|
|
7578
|
+
title: {
|
|
7579
|
+
type: "string",
|
|
7580
|
+
required: true
|
|
7581
|
+
},
|
|
7582
|
+
language: {
|
|
7583
|
+
type: "string",
|
|
7584
|
+
required: true
|
|
7585
|
+
},
|
|
7586
|
+
code: {
|
|
7587
|
+
type: "string",
|
|
7588
|
+
required: true
|
|
7589
|
+
},
|
|
7590
|
+
annotations: {
|
|
7591
|
+
type: "array",
|
|
7592
|
+
required: true,
|
|
7593
|
+
items: {
|
|
7594
|
+
type: "object",
|
|
7595
|
+
additionalProperties: false,
|
|
7596
|
+
properties: {
|
|
7597
|
+
lineStart: {
|
|
7598
|
+
type: "integer",
|
|
7599
|
+
required: true
|
|
7600
|
+
},
|
|
7601
|
+
lineEnd: {
|
|
7602
|
+
type: "integer",
|
|
7603
|
+
required: true
|
|
7604
|
+
},
|
|
7605
|
+
note: {
|
|
7606
|
+
type: "string",
|
|
7607
|
+
required: true
|
|
7608
|
+
}
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
},
|
|
7612
|
+
warnings: {
|
|
7613
|
+
type: "array",
|
|
7614
|
+
items: { type: "string" }
|
|
7615
|
+
}
|
|
7616
|
+
}
|
|
7617
|
+
},
|
|
7618
|
+
render: (_args, value) => [{
|
|
7619
|
+
type: "text",
|
|
7620
|
+
text: `Code walkthrough: ${value.title} (${value.annotations.length} segments)`
|
|
7621
|
+
}]
|
|
7622
|
+
},
|
|
7623
|
+
async execute(args) {
|
|
7624
|
+
return recordSanitized(args.lessonId, sanitizeCodeWalkthrough(args), "code_walkthrough");
|
|
7625
|
+
},
|
|
7626
|
+
presentCall: (args) => ({
|
|
7627
|
+
card: "generic",
|
|
7628
|
+
title: `Code walkthrough: ${typeof args.title === "string" ? args.title : ""}`
|
|
7629
|
+
})
|
|
7630
|
+
}),
|
|
7631
|
+
setModeTool
|
|
6772
7632
|
];
|
|
6773
7633
|
}
|
|
6774
7634
|
//#endregion
|
|
@@ -6816,6 +7676,12 @@ Never reveal the friction log or mastery mechanics as "being watched" — the nu
|
|
|
6816
7676
|
Course import design: when study_import_github or study_import_folder returns status "design_required", it renders a design brief (files with heading outlines and per-heading char counts). Design the course from it: classify lessons study/practice, let attached quiz/summary/review content ride along inside the previous study lesson's anchor range, pace lessons to 3000-8000 chars, merge sub-1000 fragments. Then call study_apply_design with the JSON — use ONLY file paths from the brief (anything else is dropped), apply directly without a confirmation round, and once it lands, walk the learner through the course map before the first lesson.
|
|
6817
7677
|
|
|
6818
7678
|
Quiz quality: 3–4 questions per quiz block is best (5 max), 4 options each. Distractors must come from real misconceptions, not absurd fillers. Test understanding, not recall: "in scenario Y, use X or Z?" rather than "define X". Every question carries an explanation of why the right answer is right. One scored block at a time.
|
|
7679
|
+
Artifact surfaces beyond the practice card (each renders as an interactive card and settles into the notebook):
|
|
7680
|
+
- Opening a brand-new lesson: study_pose_guess — one or two prose sentences of hook, then exactly 2 short options; reveal the answer next turn (unscored, never 答对/答错).
|
|
7681
|
+
- The learner asks A 和 B 有什么区别 or a comparison clarifies: study_compare_table.
|
|
7682
|
+
- Structure/flow/interaction/state needs a picture: study_draw_diagram (flowchart TD / sequenceDiagram / stateDiagram-v2 — valid mermaid only).
|
|
7683
|
+
- The lesson contains code needing teardown, or the learner asks what code means: study_code_walkthrough (line ranges + notes).
|
|
7684
|
+
Practice cards: for multi-question practice blocks (3-4 questions, review consolidation, learner asks to 练一练/出一组题), call study_generate_quiz — the learner answers on the interactive card (locally judged, progress kept). When the completion hook arrives ("练习卡完成:…"), acknowledge briefly and address wrong answers; do NOT re-grade card answers through study_record_answer. Single conversational questions stay as prose options.
|
|
6819
7685
|
Quiz option FORMAT (the study panel parses this to make options clickable): each option on its own plain line, exactly "A. <text>", "B. <text>", "C. <text>", "D. <text>" — letters consecutive from A; never tables, never nested lists, never bold/code wrapping the letter.
|
|
6820
7686
|
|
|
6821
7687
|
### 【Answer formatting · preferences】
|