dsh-plugin-lookatstudy 0.22.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +475 -110
- package/lib/client.js.map +1 -1
- package/lib/{docx-parser-BhyqPImb.mjs → docx-parser-BB_sCd8F.mjs} +1 -1
- package/lib/{epub-parser-oH96guBW.mjs → epub-parser-B7JEfgSO.mjs} +2 -2
- package/lib/{html-article-Da8ksU0i.mjs → html-article-k4tDTqDh.mjs} +6 -2
- package/lib/index.mjs +883 -186
- package/lib/{inflate-DIKUrTRi.mjs → inflate-C-hhR5be.mjs} +19 -7
- package/lib/{pptx-parser-B9IDkiGM.mjs → pptx-parser-BooxpGV4.mjs} +1 -1
- package/lib/{zip-reader-KnRrq0av.mjs → zip-reader-BakpZwuD.mjs} +1 -1
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -391,6 +391,14 @@ window.__ModuleLoader__.load({
|
|
|
391
391
|
.lks-ui .lks14-emptycard-hint{font-size:.825rem;color:var(--ink-muted);line-height:1.6}
|
|
392
392
|
/* B5: notebook tab capsule + 960px reading column */
|
|
393
393
|
.lks-ui .lks14-notebody{margin:0 auto;max-width:960px;width:100%;padding:12px 20px 24px;box-sizing:border-box}
|
|
394
|
+
/* #10-followup: the reading column is height:auto (the note col scrolls), so
|
|
395
|
+
.lks14-board's height:100% resolved against a content-sized parent and the
|
|
396
|
+
stage collapsed to a ~90px sliver — the never-upscale contain fit then
|
|
397
|
+
scaled tall artifacts to tens of px. On the board tab the body fills the
|
|
398
|
+
col instead (a flex column); the notes/cmap tabs keep the scrolling
|
|
399
|
+
reading column. */
|
|
400
|
+
.lks-ui .lks14-notebody.lks14-notebody-fill{display:flex;flex-direction:column;flex:1 1 0;min-height:0;max-width:none;padding-bottom:8px}
|
|
401
|
+
.lks-ui .lks14-notebody-fill .lks14-board{flex:1 1 0}
|
|
394
402
|
.lks-ui .lks14-viewtabs{background:var(--surface-2);border-radius:10px;padding:3px;align-self:flex-start}
|
|
395
403
|
.lks-ui .lks14-readbar{position:sticky;top:0;z-index:20;background:var(--surface-2);border-radius:10px}
|
|
396
404
|
|
|
@@ -484,8 +492,12 @@ window.__ModuleLoader__.load({
|
|
|
484
492
|
.lks-ui .lks14-board-artifact{padding:20px;width:fit-content;max-width:1200px}
|
|
485
493
|
/* issue #10: the board stage handles oversize via zoom — the table lays out
|
|
486
494
|
normally inside a bounded card (display:block + overflow was made for the
|
|
487
|
-
narrow chat column; on the canvas it broke cells into overlap)
|
|
488
|
-
|
|
495
|
+
narrow chat column; on the canvas it broke cells into overlap).
|
|
496
|
+
#10-followup: width:100% made the table's width depend on the fit-content
|
|
497
|
+
wrapper WHILE the wrapper's width depended on the table — a circular
|
|
498
|
+
dependency that collapsed to near-zero in the wild; width:auto lets both
|
|
499
|
+
resolve from content (the wrapper hugs the table's max-content). */
|
|
500
|
+
.lks-ui .lks14-board-artifact .lks-acard-table{display:table;width:auto;max-width:100%;overflow:visible}
|
|
489
501
|
.lks-ui .lks14-board-artifact .lks-acard{min-width:560px;background:var(--surface-0);border:1px solid var(--border-faint);border-radius:14px}
|
|
490
502
|
.lks-ui .lks14-cmapwrap{position:relative}
|
|
491
503
|
.lks-ui .lks14-cmap-expand{position:absolute;top:10px;right:10px;z-index:5}
|
|
@@ -775,7 +787,31 @@ window.__ModuleLoader__.load({
|
|
|
775
787
|
.lks-ui .lks14-threadmenu-row.on{background:rgb(var(--brand-rgb)/0.1)}
|
|
776
788
|
.lks-ui .lks14-threadmenu-row.on .lks14-threadmenu-title{color:var(--brand);font-weight:700}
|
|
777
789
|
.lks-ui .lks14-threadmenu-dot{flex:none;width:6px;height:6px;border-radius:999px;background:var(--ink-faint)}
|
|
778
|
-
|
|
790
|
+
/* 0.23.0 course scope: the coverage line under a course thread's title, and
|
|
791
|
+
the granularity toggle row pinned above the +新建 row */
|
|
792
|
+
.lks-ui .lks14-threadmenu-title{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
|
|
793
|
+
.lks-ui .lks14-threadmenu-cover{font-size:10.5px;color:var(--ink-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
794
|
+
.lks-ui .lks14-threadmenu-row.scope{margin-top:3px;border-top:1px solid var(--border-faint);border-radius:0;padding-top:7px}
|
|
795
|
+
.lks-ui .lks14-threadmenu-scope-icon{flex:none;font-size:12px;line-height:1}
|
|
796
|
+
.lks-ui .lks14-threadmenu-scope-state{flex:none;margin-left:auto;font-size:10.5px;font-weight:700;color:var(--ink-faint);padding:1px 7px;border-radius:999px;border:1px solid var(--border)}
|
|
797
|
+
.lks-ui .lks14-threadmenu-scope-state.on{color:var(--brand);border-color:rgb(var(--brand-rgb)/0.45);background:rgb(var(--brand-rgb)/0.08)}
|
|
798
|
+
.lks-ui .lks14-threadmenu-row.new{margin-top:1px;border-top:1px solid var(--border-faint);border-radius:0 0 7px 7px;padding-top:7px}
|
|
799
|
+
/* 0.23.0: the ask_user_question answer row — the panel's own answer UI for
|
|
800
|
+
the host's pending clarifying question (the hidden host column used to be
|
|
801
|
+
the only place its options lived) */
|
|
802
|
+
.lks-ui .lks14-askrow{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border:1px solid rgb(var(--brand-rgb)/0.35);border-radius:12px;background:rgb(var(--brand-rgb)/0.05)}
|
|
803
|
+
.lks-ui .lks14-askrow.answered{opacity:.62;border-color:var(--border-faint);background:none}
|
|
804
|
+
.lks-ui .lks14-askhead{font-size:12px;font-weight:800;color:var(--brand)}
|
|
805
|
+
.lks-ui .lks14-askrow.answered .lks14-askhead{color:var(--ink-muted)}
|
|
806
|
+
.lks-ui .lks14-askq{display:flex;flex-direction:column;gap:6px}
|
|
807
|
+
.lks-ui .lks14-askq-h{font-size:13px;font-weight:700;color:var(--ink)}
|
|
808
|
+
.lks-ui .lks14-askopt{display:flex;flex-direction:column;gap:2px;align-items:flex-start;padding:8px 12px;border:1px solid var(--border);border-radius:9px;background:var(--surface-0);cursor:pointer;text-align:left;font:inherit}
|
|
809
|
+
.lks-ui .lks14-askopt:hover:not(:disabled){border-color:rgb(var(--brand-rgb)/0.55);background:rgb(var(--brand-rgb)/0.07)}
|
|
810
|
+
.lks-ui .lks14-askopt:disabled{cursor:default;opacity:.75}
|
|
811
|
+
.lks-ui .lks14-askopt-label{font-size:12.5px;font-weight:600;color:var(--ink)}
|
|
812
|
+
.lks-ui .lks14-askopt-desc{font-size:11px;color:var(--ink-muted);line-height:1.5}
|
|
813
|
+
.lks-ui .lks14-askfree{font-size:12px;color:var(--ink-muted)}
|
|
814
|
+
.lks-ui .lks14-askdone{font-size:11px;color:var(--ink-faint)}
|
|
779
815
|
.lks-ui .lks14-threadmenu-row{position:relative}
|
|
780
816
|
.lks-ui .lks14-threadmenu-gear{flex:none;margin-left:auto;border:none;background:none;color:var(--ink-faint);font-size:13px;line-height:1;cursor:pointer;padding:2px 5px;border-radius:5px;opacity:0;transition:opacity .12s,color .12s}
|
|
781
817
|
.lks-ui .lks14-threadmenu-row:hover .lks14-threadmenu-gear,.lks-ui .lks14-threadmenu-gear:focus-visible{opacity:1}
|
|
@@ -1689,8 +1725,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1689
1725
|
*/
|
|
1690
1726
|
/** Poll cadence for the shared store; one cycle serves every mounted seat. */
|
|
1691
1727
|
const POLL_MS = 3e3;
|
|
1728
|
+
/** Sentinel for 304 responses (the state poll's ETag path): keep the snapshot. */
|
|
1729
|
+
const NOT_MODIFIED = Symbol("not-modified");
|
|
1692
1730
|
async function fetchJson(url, init) {
|
|
1693
|
-
const
|
|
1731
|
+
const headers = new Headers(init?.headers);
|
|
1732
|
+
headers.set("x-lks-request", "1");
|
|
1733
|
+
const res = await fetch(url, {
|
|
1734
|
+
...init,
|
|
1735
|
+
headers
|
|
1736
|
+
});
|
|
1737
|
+
if (res.status === 304) return NOT_MODIFIED;
|
|
1694
1738
|
const body = await res.json().catch(() => null);
|
|
1695
1739
|
if (!res.ok) {
|
|
1696
1740
|
const error = body !== null && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `HTTP ${res.status}`;
|
|
@@ -1728,8 +1772,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1728
1772
|
this.inflight = true;
|
|
1729
1773
|
try {
|
|
1730
1774
|
const next = await fetchJson("/lookatstudy/api/state");
|
|
1731
|
-
|
|
1732
|
-
|
|
1775
|
+
if (next !== NOT_MODIFIED) {
|
|
1776
|
+
this.snapshot = next;
|
|
1777
|
+
this.emit();
|
|
1778
|
+
}
|
|
1733
1779
|
} catch {} finally {
|
|
1734
1780
|
this.inflight = false;
|
|
1735
1781
|
}
|
|
@@ -1828,6 +1874,19 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1828
1874
|
this.refresh();
|
|
1829
1875
|
return res;
|
|
1830
1876
|
}
|
|
1877
|
+
/** Flip a course's thread granularity (0.23.0, issue #11 follow-up). Never
|
|
1878
|
+
* touches sediment — both sides' groups stay and are resumed on flip. */
|
|
1879
|
+
async setCourseScope(courseId, scope) {
|
|
1880
|
+
await fetchJson("/lookatstudy/api/course-scope", {
|
|
1881
|
+
method: "POST",
|
|
1882
|
+
headers: { "content-type": "application/json" },
|
|
1883
|
+
body: JSON.stringify({
|
|
1884
|
+
courseId,
|
|
1885
|
+
scope
|
|
1886
|
+
})
|
|
1887
|
+
});
|
|
1888
|
+
this.refresh();
|
|
1889
|
+
}
|
|
1831
1890
|
/** Delete one course (and its proposals) from the host state. */
|
|
1832
1891
|
async deleteCourse(courseId) {
|
|
1833
1892
|
await fetchJson("/lookatstudy/api/course/delete", {
|
|
@@ -1968,7 +2027,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1968
2027
|
const bin = atob(raw);
|
|
1969
2028
|
const bytes = new Uint8Array(bin.length);
|
|
1970
2029
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1971
|
-
return bytes
|
|
2030
|
+
return bytes;
|
|
1972
2031
|
}
|
|
1973
2032
|
};
|
|
1974
2033
|
/** The voices the dashboard route accepts (same allowlist server-side). */
|
|
@@ -2028,6 +2087,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2028
2087
|
uploadAttachment: studyStore.uploadAttachment.bind(studyStore),
|
|
2029
2088
|
setFocus: studyStore.setFocus.bind(studyStore),
|
|
2030
2089
|
searchLessons: studyStore.searchLessons.bind(studyStore),
|
|
2090
|
+
tts: studyStore.tts.bind(studyStore),
|
|
2091
|
+
setCourseScope: studyStore.setCourseScope.bind(studyStore),
|
|
2031
2092
|
deleteCourse: studyStore.deleteCourse.bind(studyStore),
|
|
2032
2093
|
deleteNote: studyStore.deleteNote.bind(studyStore),
|
|
2033
2094
|
editNote: studyStore.editNote.bind(studyStore),
|
|
@@ -2086,7 +2147,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2086
2147
|
"quiz.right": "答对了",
|
|
2087
2148
|
"quiz.wrong": "答错了",
|
|
2088
2149
|
"quiz.youChose": "你选了",
|
|
2089
|
-
"quiz.answer": "正确答案",
|
|
2090
2150
|
"quiz.hook": "练习卡完成:{correct}/{total} 正确。请简短点评,针对错题讲讲思路。",
|
|
2091
2151
|
"quiz.action.explain-wrong": "讲讲错题",
|
|
2092
2152
|
"quiz.action.retry": "再来一组",
|
|
@@ -2122,8 +2182,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2122
2182
|
"review.nudge.go": "去复习",
|
|
2123
2183
|
"rail.search.jump": "打开这一课",
|
|
2124
2184
|
"proposal.banner": "《{lesson}》可以提前毕业了",
|
|
2125
|
-
"proposal.accept": "接受",
|
|
2126
|
-
"proposal.decline": "再练练",
|
|
2127
2185
|
"proposal.accept.msg": "我接受《{lesson}》的掌握度提议,帮我标记为掌握。",
|
|
2128
2186
|
"proposal.decline.msg": "《{lesson}》我先再练练,暂不接受提前毕业。",
|
|
2129
2187
|
"tutor.thinking": "导师思考中…",
|
|
@@ -2235,6 +2293,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2235
2293
|
"review.banner": "正在复习:{lesson}",
|
|
2236
2294
|
"review.threadTitle": "复习:{lesson}",
|
|
2237
2295
|
"threads.label": "课时线程",
|
|
2296
|
+
"threads.scope": "跨课时连续对话",
|
|
2297
|
+
"threads.scope.on": "已开启",
|
|
2298
|
+
"threads.scope.off": "未开启",
|
|
2299
|
+
"threads.coverage": "覆盖",
|
|
2300
|
+
"ask.title": "导师想先确认一下",
|
|
2301
|
+
"ask.free": "在下方输入框直接回复即可",
|
|
2302
|
+
"ask.answered": "已回答",
|
|
2238
2303
|
"meter.label": "上下文用量",
|
|
2239
2304
|
"meter.aria": "上下文已用 {pct}%",
|
|
2240
2305
|
"meter.brk.system": "系统提示",
|
|
@@ -2404,7 +2469,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2404
2469
|
"exam.leave.message": "考试进行中——现在离开,未答的题将按错计分。",
|
|
2405
2470
|
"exam.leave.confirm": "离开并结算",
|
|
2406
2471
|
"exam.leave.cancel": "继续答题",
|
|
2407
|
-
"prompt.exam.bank": "请为「{section}」的章节测验出题:先用
|
|
2472
|
+
"prompt.exam.bank": "请为「{section}」的章节测验出题:先用 study_lesson 读一遍本节课时,再按各课时的知识点用 study_exam_bank_apply 一次性提交整份题库(单选 5-15 题,kcTitle 必须用本节的概念标题,每题附一句话解析)。题库就绪后告诉我可以开考。",
|
|
2408
2473
|
"prompt.review": "开始今天的复习,从最到期的课时开始",
|
|
2409
2474
|
"prompt.proposal.accept": "接受提案 {id} —— 确认标记这课为已掌握",
|
|
2410
2475
|
"prompt.proposal.decline": "拒绝提案 {id} —— 我想再练练"
|
|
@@ -2449,7 +2514,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2449
2514
|
"quiz.right": "Correct",
|
|
2450
2515
|
"quiz.wrong": "Wrong",
|
|
2451
2516
|
"quiz.youChose": "You chose",
|
|
2452
|
-
"quiz.answer": "Answer",
|
|
2453
2517
|
"quiz.hook": "Practice card finished: {correct}/{total} correct. Please comment briefly and walk through the wrong ones.",
|
|
2454
2518
|
"quiz.action.explain-wrong": "Explain my wrong answers",
|
|
2455
2519
|
"quiz.action.retry": "Another set",
|
|
@@ -2485,8 +2549,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2485
2549
|
"review.nudge.go": "Review now",
|
|
2486
2550
|
"rail.search.jump": "Open this lesson",
|
|
2487
2551
|
"proposal.banner": "{lesson} is ready to graduate early",
|
|
2488
|
-
"proposal.accept": "Accept",
|
|
2489
|
-
"proposal.decline": "Keep practicing",
|
|
2490
2552
|
"proposal.accept.msg": "I accept the mastery proposal for \"{lesson}\" — mark it mastered.",
|
|
2491
2553
|
"proposal.decline.msg": "I want more practice on \"{lesson}\" before graduating.",
|
|
2492
2554
|
"tutor.thinking": "The tutor is thinking…",
|
|
@@ -2586,6 +2648,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2586
2648
|
"palette.kind.action": "Action",
|
|
2587
2649
|
"palette.empty": "No matches",
|
|
2588
2650
|
"threads.label": "Lesson threads",
|
|
2651
|
+
"threads.scope": "Continuous course thread",
|
|
2652
|
+
"threads.scope.on": "on",
|
|
2653
|
+
"threads.scope.off": "off",
|
|
2654
|
+
"threads.coverage": "covers",
|
|
2655
|
+
"ask.title": "The tutor wants a quick confirm",
|
|
2656
|
+
"ask.free": "Answer in the composer below",
|
|
2657
|
+
"ask.answered": "answered",
|
|
2589
2658
|
"threads.open": "{n} threads",
|
|
2590
2659
|
"threads.now": "current",
|
|
2591
2660
|
"threads.new": "New thread (your next message starts it)",
|
|
@@ -2767,7 +2836,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2767
2836
|
"exam.leave.message": "The exam is in progress — leaving now counts unanswered questions as wrong.",
|
|
2768
2837
|
"exam.leave.confirm": "Leave & settle",
|
|
2769
2838
|
"exam.leave.cancel": "Keep answering",
|
|
2770
|
-
"prompt.exam.bank": "Author the section exam bank for \"{section}\": read this section's lessons with
|
|
2839
|
+
"prompt.exam.bank": "Author the section exam bank for \"{section}\": read this section's lessons with study_lesson, then submit the full multiple-choice set in one study_exam_bank_apply call (5-15 single-choice questions, kcTitle must be one of the section's concept titles, one-line explanation each). Tell me when the bank is ready.",
|
|
2771
2840
|
"prompt.review": "Start today's review, from the most overdue lesson first",
|
|
2772
2841
|
"prompt.proposal.accept": "Accept proposal {id} — confirm marking this lesson as mastered",
|
|
2773
2842
|
"prompt.proposal.decline": "Reject proposal {id} — I want to practice more"
|
|
@@ -2981,6 +3050,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2981
3050
|
return out.join("\n");
|
|
2982
3051
|
}
|
|
2983
3052
|
//#endregion
|
|
3053
|
+
//#region src/thread-key.ts
|
|
3054
|
+
/** Resolve the thread-group map key for a send from `lessonId`. Absent scope
|
|
3055
|
+
* is legacy lesson behavior — old files never move. */
|
|
3056
|
+
function threadOwnerKey(scope, courseId, lessonId) {
|
|
3057
|
+
return scope === "course" ? `course:${courseId}` : lessonId;
|
|
3058
|
+
}
|
|
3059
|
+
//#endregion
|
|
2984
3060
|
//#region src/vendor/mermaid-elk-rewrite.ts
|
|
2985
3061
|
/**
|
|
2986
3062
|
* mermaid-elk-rewrite —— flowchart → flowchart-elk 前缀改写(v0.21 纯函数)。
|
|
@@ -3076,6 +3152,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3076
3152
|
//#endregion
|
|
3077
3153
|
//#region src/client/enhance.ts
|
|
3078
3154
|
var enhance_exports = /* @__PURE__ */ __exportAll({
|
|
3155
|
+
KATEX_SRI: () => KATEX_SRI,
|
|
3156
|
+
MERMAID_SECURITY_LEVEL: () => MERMAID_SECURITY_LEVEL,
|
|
3079
3157
|
attachCodeHeader: () => attachCodeHeader,
|
|
3080
3158
|
enhanceCode: () => enhanceCode,
|
|
3081
3159
|
enhanceDiagrams: () => enhanceDiagrams,
|
|
@@ -3127,10 +3205,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3127
3205
|
mermaid: "https://esm.sh/mermaid@11.4.1",
|
|
3128
3206
|
mermaidElk: "https://esm.sh/@mermaid-js/layout-elk@0.1.7"
|
|
3129
3207
|
};
|
|
3130
|
-
function loadScript(src) {
|
|
3208
|
+
function loadScript(src, integrity) {
|
|
3131
3209
|
return new Promise((resolve, reject) => {
|
|
3132
3210
|
const el = document.createElement("script");
|
|
3133
3211
|
el.src = src;
|
|
3212
|
+
if (integrity !== void 0) {
|
|
3213
|
+
el.integrity = integrity;
|
|
3214
|
+
el.crossOrigin = "anonymous";
|
|
3215
|
+
}
|
|
3134
3216
|
el.onload = () => {
|
|
3135
3217
|
resolve();
|
|
3136
3218
|
};
|
|
@@ -3140,6 +3222,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3140
3222
|
document.head.append(el);
|
|
3141
3223
|
});
|
|
3142
3224
|
}
|
|
3225
|
+
/** Audit D32: SRI pin for the KaTeX CDN script (0.16.22, sha384 over the exact bytes). */
|
|
3226
|
+
const KATEX_SRI = "sha384-cMkvdD8LoxVzGF/RPUKAcvmm49FQ0oxwDF3BGKtDXcEc+T1b2N+teh/OJfpU0jr6";
|
|
3143
3227
|
let katexPromise = null;
|
|
3144
3228
|
function defaultLoadKatex() {
|
|
3145
3229
|
katexPromise ??= (async () => {
|
|
@@ -3150,7 +3234,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3150
3234
|
link.setAttribute("data-lks-katex", "1");
|
|
3151
3235
|
document.head.append(link);
|
|
3152
3236
|
}
|
|
3153
|
-
if (window.katex === void 0) await loadScript(CDN.katexJs);
|
|
3237
|
+
if (window.katex === void 0) await loadScript(CDN.katexJs, KATEX_SRI);
|
|
3154
3238
|
const katex = window.katex;
|
|
3155
3239
|
if (katex === void 0) throw new Error("katex missing after load");
|
|
3156
3240
|
return katex;
|
|
@@ -3239,6 +3323,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3239
3323
|
noteTextColor: read("--ink-rgb", "rgb(245 245 250)")
|
|
3240
3324
|
};
|
|
3241
3325
|
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Audit A1 (2026-09-14): diagram sources arrive from imported (untrusted)
|
|
3328
|
+
* content — mermaid must run under 'strict'. 'loose' skips DOMPurify label
|
|
3329
|
+
* sanitization and enables click callbacks, which turns a hostile repo
|
|
3330
|
+
* README into stored XSS in the host page. Exported so the gate is testable.
|
|
3331
|
+
*/
|
|
3332
|
+
const MERMAID_SECURITY_LEVEL = "strict";
|
|
3242
3333
|
function defaultLoadMermaid() {
|
|
3243
3334
|
mermaidPromise ??= (async () => {
|
|
3244
3335
|
const mermaid = (await import(
|
|
@@ -3255,7 +3346,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3255
3346
|
mermaid.initialize({
|
|
3256
3347
|
startOnLoad: false,
|
|
3257
3348
|
theme: "base",
|
|
3258
|
-
securityLevel:
|
|
3349
|
+
securityLevel: MERMAID_SECURITY_LEVEL,
|
|
3259
3350
|
themeVariables: mermaidThemeVariables()
|
|
3260
3351
|
});
|
|
3261
3352
|
return mermaid;
|
|
@@ -3267,19 +3358,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3267
3358
|
const display = /\$\$([^$]+)\$\$/.exec(text);
|
|
3268
3359
|
const inline = /\$([^$\n]+)\$/.exec(text);
|
|
3269
3360
|
if (display === null && inline === null) return null;
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3272
|
-
display: true
|
|
3273
|
-
} : inline === null ? null : {
|
|
3274
|
-
...inline,
|
|
3275
|
-
display: false
|
|
3276
|
-
};
|
|
3361
|
+
const useDisplay = display !== null && (inline === null || display.index <= inline.index);
|
|
3362
|
+
const m = useDisplay ? display : inline;
|
|
3277
3363
|
if (m === null) return null;
|
|
3278
3364
|
return {
|
|
3279
3365
|
before: text.slice(0, m.index),
|
|
3280
3366
|
tex: m[1],
|
|
3281
3367
|
after: text.slice(m.index + m[0].length),
|
|
3282
|
-
display:
|
|
3368
|
+
display: useDisplay
|
|
3283
3369
|
};
|
|
3284
3370
|
}
|
|
3285
3371
|
/** KaTeX every $..$ / $$..$$ in the container; degrades to plain text on any failure. */
|
|
@@ -3294,7 +3380,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3294
3380
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
3295
3381
|
const targets = [];
|
|
3296
3382
|
let node;
|
|
3297
|
-
while ((node = walker.nextNode()) !== null)
|
|
3383
|
+
while ((node = walker.nextNode()) !== null) {
|
|
3384
|
+
const parent = node.parentElement;
|
|
3385
|
+
if (findMathSpan(node.data) !== null && parent?.tagName !== "CODE" && parent?.closest(".lks-shiki, .lks-mermaid") === null) targets.push(node);
|
|
3386
|
+
}
|
|
3298
3387
|
for (const t of targets) {
|
|
3299
3388
|
let guard = 0;
|
|
3300
3389
|
while (guard++ < 200) {
|
|
@@ -3474,7 +3563,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3474
3563
|
* no zoom; this is the one place our port must normalize).
|
|
3475
3564
|
*/
|
|
3476
3565
|
function panelZoomOf(el) {
|
|
3477
|
-
const z = el?.closest(".lks14")?.style.zoom;
|
|
3566
|
+
const z = (el?.closest(".lks14"))?.style.zoom;
|
|
3478
3567
|
const n = z === void 0 || z === "" ? NaN : Number(z);
|
|
3479
3568
|
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
3480
3569
|
}
|
|
@@ -3631,25 +3720,21 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3631
3720
|
for (const line of text.split("\n")) {
|
|
3632
3721
|
const plain = /^([A-D])[.、:)]\s*(.+)$/.exec(line.trim());
|
|
3633
3722
|
const table = /^\|\s*\*{0,2}([A-D])\*{0,2}\s*\|\s*(.+?)\s*\|$/.exec(line.trim());
|
|
3634
|
-
const match = plain
|
|
3635
|
-
table[0],
|
|
3636
|
-
table[1],
|
|
3637
|
-
table[2].replaceAll("**", "").replaceAll("`", "")
|
|
3638
|
-
] : null);
|
|
3723
|
+
const match = plain !== null ? [plain[1], plain[2]] : table !== null ? [table[1], table[2].replaceAll("**", "").replaceAll("`", "")] : null;
|
|
3639
3724
|
if (match === null) {
|
|
3640
3725
|
flush();
|
|
3641
3726
|
continue;
|
|
3642
3727
|
}
|
|
3643
3728
|
const nextLetter = run.length === 0 ? "A" : String.fromCharCode(run[run.length - 1].letter.charCodeAt(0) + 1);
|
|
3644
|
-
if (match[
|
|
3645
|
-
letter: match[
|
|
3646
|
-
text: match[
|
|
3729
|
+
if (match[0] === nextLetter) run.push({
|
|
3730
|
+
letter: match[0],
|
|
3731
|
+
text: match[1]
|
|
3647
3732
|
});
|
|
3648
3733
|
else {
|
|
3649
3734
|
flush();
|
|
3650
|
-
if (match[
|
|
3651
|
-
letter: match[
|
|
3652
|
-
text: match[
|
|
3735
|
+
if (match[0] === "A") run.push({
|
|
3736
|
+
letter: match[0],
|
|
3737
|
+
text: match[1]
|
|
3653
3738
|
});
|
|
3654
3739
|
}
|
|
3655
3740
|
}
|
|
@@ -3725,10 +3810,26 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3725
3810
|
return group.threads.filter((t) => t.status !== "archived").slice().sort((a, b) => a.lastAt < b.lastAt ? 1 : -1).map((t) => ({
|
|
3726
3811
|
id: t.id,
|
|
3727
3812
|
title: t.title,
|
|
3728
|
-
current: t.id === group.active
|
|
3813
|
+
current: t.id === group.active,
|
|
3814
|
+
touchedLessons: t.touchedLessons
|
|
3729
3815
|
}));
|
|
3730
3816
|
}
|
|
3731
3817
|
/**
|
|
3818
|
+
* 0.23.0 course scope: the coverage label under a course-scoped thread — the
|
|
3819
|
+
* lessons it has been sent from, by section order, capped at three titles
|
|
3820
|
+
* with an 等 N 课时 tail. Empty/undefined coverage renders '' (lesson-scoped
|
|
3821
|
+
* threads show nothing). Pure.
|
|
3822
|
+
*/
|
|
3823
|
+
function threadCoverageLabel(touched, courses) {
|
|
3824
|
+
if (touched === void 0 || touched.length === 0) return "";
|
|
3825
|
+
const titleOf = /* @__PURE__ */ new Map();
|
|
3826
|
+
for (const c of courses) for (const s of c.sections) for (const l of s.lessons) titleOf.set(l.id, l.title);
|
|
3827
|
+
const titles = touched.map((id) => titleOf.get(id)).filter((t) => t !== void 0);
|
|
3828
|
+
if (titles.length === 0) return "";
|
|
3829
|
+
const head = titles.slice(0, 3).join(" · ");
|
|
3830
|
+
return titles.length > 3 ? `${head} 等 ${String(titles.length)} 课时` : head;
|
|
3831
|
+
}
|
|
3832
|
+
/**
|
|
3732
3833
|
* Default expansion for one rail section: collapsed when every study lesson is
|
|
3733
3834
|
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
3734
3835
|
* stays open; exam nodes never force a section open. Pure.
|
|
@@ -3794,16 +3895,22 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3794
3895
|
*/
|
|
3795
3896
|
function ctxSegments(pct, breakdown) {
|
|
3796
3897
|
if (pct <= 0) return [];
|
|
3797
|
-
|
|
3898
|
+
if (breakdown === null) return [{
|
|
3899
|
+
key: "total",
|
|
3900
|
+
cls: "",
|
|
3901
|
+
width: pct
|
|
3902
|
+
}];
|
|
3903
|
+
const bd = breakdown;
|
|
3904
|
+
const total = bd.systemTokens + bd.toolsTokens + bd.messageTokens;
|
|
3798
3905
|
if (total <= 0) return [{
|
|
3799
3906
|
key: "total",
|
|
3800
3907
|
cls: "",
|
|
3801
3908
|
width: pct
|
|
3802
3909
|
}];
|
|
3803
3910
|
return [
|
|
3804
|
-
["sys",
|
|
3805
|
-
["tools",
|
|
3806
|
-
["msgs",
|
|
3911
|
+
["sys", bd.systemTokens],
|
|
3912
|
+
["tools", bd.toolsTokens],
|
|
3913
|
+
["msgs", bd.messageTokens]
|
|
3807
3914
|
].map(([key, tokens]) => ({
|
|
3808
3915
|
key,
|
|
3809
3916
|
cls: key,
|
|
@@ -4740,8 +4847,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4740
4847
|
const claimed = /* @__PURE__ */ new Set();
|
|
4741
4848
|
const out = [];
|
|
4742
4849
|
for (const row of rows) {
|
|
4743
|
-
const type = row.role === "tool" && row.toolState === "done" && row.resultText !== void 0 ? ARTIFACT_TOOLS[row.text] : void 0;
|
|
4744
|
-
if (type === void 0) {
|
|
4850
|
+
const type = row.role === "tool" && row.toolState === "done" && row.resultText !== void 0 && row.text !== void 0 ? ARTIFACT_TOOLS[row.text] : void 0;
|
|
4851
|
+
if (type === void 0 || row.resultText === void 0) {
|
|
4745
4852
|
out.push(row);
|
|
4746
4853
|
continue;
|
|
4747
4854
|
}
|
|
@@ -4841,8 +4948,20 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4841
4948
|
});
|
|
4842
4949
|
} else if (event.type === "tool/call") {
|
|
4843
4950
|
const name = event.data?.tool?.name ?? event.data?.name ?? "tool";
|
|
4951
|
+
const callId = event.data?.callId ?? event.data?.toolCallId;
|
|
4952
|
+
if (name === "ask_user_question") {
|
|
4953
|
+
const ask = parseAskArguments(event.data?.arguments, callId ?? String(event.seq));
|
|
4954
|
+
rows.push({
|
|
4955
|
+
key: `t${String(callId ?? event.seq)}`,
|
|
4956
|
+
role: "ask",
|
|
4957
|
+
text: ask.summary,
|
|
4958
|
+
toolState: "loading",
|
|
4959
|
+
ask
|
|
4960
|
+
});
|
|
4961
|
+
continue;
|
|
4962
|
+
}
|
|
4844
4963
|
rows.push({
|
|
4845
|
-
key: `t${
|
|
4964
|
+
key: `t${String(callId ?? event.seq)}`,
|
|
4846
4965
|
role: "tool",
|
|
4847
4966
|
text: name,
|
|
4848
4967
|
toolState: "loading"
|
|
@@ -4850,13 +4969,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4850
4969
|
} else if (event.type === "tool/result") {
|
|
4851
4970
|
const rid = event.data?.message?.source?.callId ?? event.data?.callId ?? event.data?.toolCallId;
|
|
4852
4971
|
if (rid !== void 0) {
|
|
4853
|
-
const idx = rows.findIndex((r) => r.key === `t${rid}` && r.role === "tool");
|
|
4972
|
+
const idx = rows.findIndex((r) => r.key === `t${rid}` && (r.role === "tool" || r.role === "ask"));
|
|
4854
4973
|
if (idx >= 0) {
|
|
4855
4974
|
const failed = event.data?.message?.isError === true || event.data?.error !== void 0;
|
|
4856
4975
|
rows[idx] = {
|
|
4857
4976
|
...rows[idx],
|
|
4858
4977
|
toolState: failed ? "error" : "done",
|
|
4859
|
-
resultText: failed ? void 0 : resultRenderedText(event)
|
|
4978
|
+
resultText: failed ? void 0 : resultRenderedText(event),
|
|
4979
|
+
...rows[idx].role === "ask" && rows[idx].ask !== void 0 && !failed ? { ask: {
|
|
4980
|
+
...rows[idx].ask,
|
|
4981
|
+
answered: true
|
|
4982
|
+
} } : {}
|
|
4860
4983
|
};
|
|
4861
4984
|
}
|
|
4862
4985
|
}
|
|
@@ -4876,6 +4999,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4876
4999
|
else stream.text += chunk.text ?? "";
|
|
4877
5000
|
latest = attemptId;
|
|
4878
5001
|
}
|
|
5002
|
+
let askOpen = false;
|
|
5003
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
5004
|
+
const r = rows[i];
|
|
5005
|
+
if (r.role === "user") askOpen = true;
|
|
5006
|
+
else if (r.role === "ask" && r.ask !== void 0 && askOpen && !r.ask.answered) rows[i] = {
|
|
5007
|
+
...r,
|
|
5008
|
+
ask: {
|
|
5009
|
+
...r.ask,
|
|
5010
|
+
answered: true
|
|
5011
|
+
}
|
|
5012
|
+
};
|
|
5013
|
+
}
|
|
4879
5014
|
const stream = latest !== void 0 ? streams.get(latest) : void 0;
|
|
4880
5015
|
if (stream !== void 0 && !stream.settled) {
|
|
4881
5016
|
const text = stream.text.trim();
|
|
@@ -4887,6 +5022,60 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4887
5022
|
}
|
|
4888
5023
|
return rows;
|
|
4889
5024
|
}
|
|
5025
|
+
/**
|
|
5026
|
+
* Parse an ask_user_question call's arguments into the answerable row payload
|
|
5027
|
+
* (0.23.0). The live journal carries a JSON STRING (verified against real
|
|
5028
|
+
* journals 2026-09-14: {questions:[{header,id,multi_select,options:[{label,
|
|
5029
|
+
* description}]}]}); tolerate an already-parsed object arm for robustness.
|
|
5030
|
+
* Unparseable shapes degrade to a header-only row (the composer still answers
|
|
5031
|
+
* in free text). Pure.
|
|
5032
|
+
*/
|
|
5033
|
+
function parseAskArguments(raw, callId) {
|
|
5034
|
+
const empty = {
|
|
5035
|
+
callId,
|
|
5036
|
+
summary: "",
|
|
5037
|
+
questions: [],
|
|
5038
|
+
answered: false
|
|
5039
|
+
};
|
|
5040
|
+
if (raw === void 0 || raw === "") return empty;
|
|
5041
|
+
let parsed;
|
|
5042
|
+
try {
|
|
5043
|
+
parsed = JSON.parse(raw);
|
|
5044
|
+
} catch {
|
|
5045
|
+
return empty;
|
|
5046
|
+
}
|
|
5047
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.questions)) return empty;
|
|
5048
|
+
const questions = [];
|
|
5049
|
+
for (const q of parsed.questions) {
|
|
5050
|
+
if (typeof q !== "object" || q === null) continue;
|
|
5051
|
+
const rec = q;
|
|
5052
|
+
const id = typeof rec.id === "string" ? rec.id : String(questions.length);
|
|
5053
|
+
const header = typeof rec.header === "string" && rec.header !== "" ? rec.header : typeof rec.question === "string" ? rec.question : "";
|
|
5054
|
+
const multiSelect = rec.multi_select === true;
|
|
5055
|
+
const options = [];
|
|
5056
|
+
if (Array.isArray(rec.options)) for (const o of rec.options) {
|
|
5057
|
+
if (typeof o !== "object" || o === null) continue;
|
|
5058
|
+
const orec = o;
|
|
5059
|
+
if (typeof orec.label === "string" && orec.label !== "") options.push({
|
|
5060
|
+
label: orec.label,
|
|
5061
|
+
description: typeof orec.description === "string" ? orec.description : ""
|
|
5062
|
+
});
|
|
5063
|
+
}
|
|
5064
|
+
questions.push({
|
|
5065
|
+
id,
|
|
5066
|
+
header,
|
|
5067
|
+
multiSelect,
|
|
5068
|
+
options
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
if (questions.length === 0) return empty;
|
|
5072
|
+
return {
|
|
5073
|
+
callId,
|
|
5074
|
+
summary: questions.map((q) => q.header).filter((h) => h !== "").join(" / "),
|
|
5075
|
+
questions,
|
|
5076
|
+
answered: false
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
4890
5079
|
//#endregion
|
|
4891
5080
|
//#region src/client/error-boundary.tsx
|
|
4892
5081
|
/**
|
|
@@ -5680,8 +5869,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5680
5869
|
* does everything. Gestures: single-pointer drag pan, two-pointer pinch
|
|
5681
5870
|
* anchored at the midpoint, wheel zoom anchored at the cursor, double
|
|
5682
5871
|
* click/tap toggles fit↔100%. ResizeObserver refits on content/stage size
|
|
5683
|
-
* changes (mermaid's async svg)
|
|
5684
|
-
*
|
|
5872
|
+
* changes (mermaid's async svg) — through the #12 gates: value-equality on
|
|
5873
|
+
* setSize, a bounds-differ short-circuit on refit, rAF coalescing, and a
|
|
5874
|
+
* recurrence-tripping oscillation breaker (see makeRefitBreaker). A floating
|
|
5875
|
+
* −/%/fit/+ toolbar stays mounted. Shared by the notebook's board tab and
|
|
5876
|
+
* the diagram modals.
|
|
5685
5877
|
* (lucide glyphs collapse to text/plugin icons; the client stays dependency-
|
|
5686
5878
|
* free.)
|
|
5687
5879
|
* @module dsh-plugin-lookatstudy/client/canvasstage
|
|
@@ -5690,6 +5882,43 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5690
5882
|
const MAX_SCALE = 4;
|
|
5691
5883
|
/** 工具条步进(手势/滚轮连续,按钮给干脆的一档)。 */
|
|
5692
5884
|
const STEP = 1.25;
|
|
5885
|
+
/** True when the two bounds readings differ in ANY field — the auto-refit
|
|
5886
|
+
* short-circuit. The pre-fix refit ran fitTransform on every RO callback with
|
|
5887
|
+
* zero hysteresis, so any feedback touching a size re-fired it forever. */
|
|
5888
|
+
function stageBoundsDiffer(a, b) {
|
|
5889
|
+
if (a === null) return true;
|
|
5890
|
+
return a.contentW !== b.contentW || a.contentH !== b.contentH || a.viewW !== b.viewW || a.viewH !== b.viewH;
|
|
5891
|
+
}
|
|
5892
|
+
function makeRefitBreaker(opts = {}) {
|
|
5893
|
+
const windowMs = opts.windowMs ?? 2e3;
|
|
5894
|
+
const now = opts.now ?? (() => performance.now());
|
|
5895
|
+
let seen = [];
|
|
5896
|
+
let tripped = false;
|
|
5897
|
+
return {
|
|
5898
|
+
allow(key) {
|
|
5899
|
+
const t = now();
|
|
5900
|
+
seen = seen.filter((s) => t - s.t < windowMs);
|
|
5901
|
+
if (tripped) return false;
|
|
5902
|
+
if (seen.some((s) => s.key === key)) {
|
|
5903
|
+
tripped = true;
|
|
5904
|
+
return false;
|
|
5905
|
+
}
|
|
5906
|
+
seen.push({
|
|
5907
|
+
t,
|
|
5908
|
+
key
|
|
5909
|
+
});
|
|
5910
|
+
return true;
|
|
5911
|
+
},
|
|
5912
|
+
reset() {
|
|
5913
|
+
seen = [];
|
|
5914
|
+
tripped = false;
|
|
5915
|
+
},
|
|
5916
|
+
tripped() {
|
|
5917
|
+
return tripped;
|
|
5918
|
+
}
|
|
5919
|
+
};
|
|
5920
|
+
}
|
|
5921
|
+
const boundsKey = (b) => `${String(b.contentW)}x${String(b.contentH)}@${String(b.viewW)}x${String(b.viewH)}`;
|
|
5693
5922
|
function CanvasStage({ children, grid = true, testid }) {
|
|
5694
5923
|
const stageRef = (0, react.useRef)(null);
|
|
5695
5924
|
const contentRef = (0, react.useRef)(null);
|
|
@@ -5711,6 +5940,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5711
5940
|
moved: false
|
|
5712
5941
|
});
|
|
5713
5942
|
const lastTapRef = (0, react.useRef)(null);
|
|
5943
|
+
const lastBoundsRef = (0, react.useRef)(null);
|
|
5944
|
+
const breakerRef = (0, react.useRef)(null);
|
|
5945
|
+
if (breakerRef.current === null) breakerRef.current = makeRefitBreaker();
|
|
5714
5946
|
const bounds = (0, react.useCallback)(() => {
|
|
5715
5947
|
const stage = stageRef.current;
|
|
5716
5948
|
const content = contentRef.current;
|
|
@@ -5723,32 +5955,62 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5723
5955
|
viewH: stage?.clientHeight ?? 0
|
|
5724
5956
|
};
|
|
5725
5957
|
}, []);
|
|
5726
|
-
const
|
|
5727
|
-
|
|
5728
|
-
|
|
5958
|
+
const currentBounds = (0, react.useCallback)(() => ({
|
|
5959
|
+
contentW: contentRef.current?.offsetWidth ?? 0,
|
|
5960
|
+
contentH: contentRef.current?.offsetHeight ?? 0,
|
|
5961
|
+
viewW: stageRef.current?.clientWidth ?? 0,
|
|
5962
|
+
viewH: stageRef.current?.clientHeight ?? 0
|
|
5963
|
+
}), []);
|
|
5964
|
+
const applyFit = (0, react.useCallback)((b) => {
|
|
5729
5965
|
setTf(fitTransform(b.contentW, b.contentH, b.viewW, b.viewH));
|
|
5730
5966
|
atFitRef.current = true;
|
|
5731
|
-
}, [
|
|
5967
|
+
}, []);
|
|
5968
|
+
const refit = (0, react.useCallback)(() => {
|
|
5969
|
+
breakerRef.current?.reset();
|
|
5970
|
+
const b = currentBounds();
|
|
5971
|
+
if (b.contentW === 0 || b.viewW === 0) return;
|
|
5972
|
+
lastBoundsRef.current = b;
|
|
5973
|
+
applyFit(b);
|
|
5974
|
+
}, [currentBounds, applyFit]);
|
|
5975
|
+
const measureNow = (0, react.useCallback)(() => {
|
|
5976
|
+
const content = contentRef.current;
|
|
5977
|
+
if (content === null) return;
|
|
5978
|
+
const w = content.offsetWidth;
|
|
5979
|
+
const h = content.offsetHeight;
|
|
5980
|
+
setSize((prev) => prev !== null && prev.w === w && prev.h === h ? prev : {
|
|
5981
|
+
w,
|
|
5982
|
+
h
|
|
5983
|
+
});
|
|
5984
|
+
const b = currentBounds();
|
|
5985
|
+
if (!stageBoundsDiffer(lastBoundsRef.current, b)) return;
|
|
5986
|
+
lastBoundsRef.current = b;
|
|
5987
|
+
if (b.contentW === 0 || b.viewW === 0) return;
|
|
5988
|
+
if (b.contentW < 120) return;
|
|
5989
|
+
if ((breakerRef.current ?? makeRefitBreaker()).allow(boundsKey(b))) applyFit(b);
|
|
5990
|
+
}, [currentBounds, applyFit]);
|
|
5732
5991
|
(0, react.useEffect)(() => {
|
|
5733
5992
|
const stage = stageRef.current;
|
|
5734
5993
|
const content = contentRef.current;
|
|
5735
5994
|
if (stage === null || content === null) return;
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5995
|
+
measureNow();
|
|
5996
|
+
let raf = 0;
|
|
5997
|
+
const onRo = () => {
|
|
5998
|
+
if (raf !== 0) return;
|
|
5999
|
+
raf = requestAnimationFrame(() => {
|
|
6000
|
+
raf = 0;
|
|
6001
|
+
measureNow();
|
|
5740
6002
|
});
|
|
5741
|
-
refit();
|
|
5742
6003
|
};
|
|
5743
|
-
|
|
5744
|
-
const ro = new ResizeObserver(measure);
|
|
6004
|
+
const ro = new ResizeObserver(onRo);
|
|
5745
6005
|
ro.observe(content);
|
|
5746
6006
|
ro.observe(stage);
|
|
5747
6007
|
return () => {
|
|
5748
6008
|
ro.disconnect();
|
|
6009
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
5749
6010
|
};
|
|
5750
|
-
}, [
|
|
6011
|
+
}, [measureNow]);
|
|
5751
6012
|
const zoomBy = (0, react.useCallback)((factor, ax, ay) => {
|
|
6013
|
+
breakerRef.current?.reset();
|
|
5752
6014
|
const b = bounds();
|
|
5753
6015
|
const cx = ax ?? b.viewW / 2;
|
|
5754
6016
|
const cy = ay ?? b.viewH / 2;
|
|
@@ -5960,7 +6222,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5960
6222
|
let seen;
|
|
5961
6223
|
try {
|
|
5962
6224
|
const raw = storage.getItem(seenArtifactsKey(lessonId));
|
|
5963
|
-
|
|
6225
|
+
const parsed = JSON.parse(raw ?? "[]");
|
|
6226
|
+
seen = new Set(Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : []);
|
|
5964
6227
|
} catch {
|
|
5965
6228
|
seen = /* @__PURE__ */ new Set();
|
|
5966
6229
|
}
|
|
@@ -6071,12 +6334,32 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6071
6334
|
to: a.lineEnd
|
|
6072
6335
|
})}`), a.note))));
|
|
6073
6336
|
}
|
|
6337
|
+
/** The 放大查看 modal's mermaid host — rendered ONCE per mermaid source. The
|
|
6338
|
+
* raw-ref-callback version re-ran renderMermaidInto on every parent render
|
|
6339
|
+
* (each 3s state poll), and its el.textContent='' clear + async re-render
|
|
6340
|
+
* re-mounted the svg twice per tick — the #12 flicker. A stable effect deps
|
|
6341
|
+
* chain (string mermaid + useCallback'd onFail) breaks the loop at the
|
|
6342
|
+
* source; the CanvasStage gates are the second line of defense. */
|
|
6343
|
+
function DiagramModalDiagram({ mermaid, onFail }) {
|
|
6344
|
+
const bodyRef = (0, react.useRef)(null);
|
|
6345
|
+
(0, react.useEffect)(() => {
|
|
6346
|
+
if (bodyRef.current === null) return;
|
|
6347
|
+
renderMermaidInto(bodyRef.current, mermaid, onFail);
|
|
6348
|
+
}, [mermaid, onFail]);
|
|
6349
|
+
return (0, react.createElement)("div", {
|
|
6350
|
+
className: "lks14-modal-diagram",
|
|
6351
|
+
ref: bodyRef
|
|
6352
|
+
});
|
|
6353
|
+
}
|
|
6074
6354
|
/** Mermaid diagram card with an expand modal (upstream DiagramViewerModal). */
|
|
6075
6355
|
function DiagramCard({ artifact }) {
|
|
6076
6356
|
const mermaid = typeof artifact.data.mermaid === "string" ? artifact.data.mermaid : "";
|
|
6077
6357
|
const bodyRef = (0, react.useRef)(null);
|
|
6078
6358
|
const [failed, setFailed] = (0, react.useState)(false);
|
|
6079
6359
|
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
6360
|
+
const onFail = (0, react.useCallback)(() => {
|
|
6361
|
+
setFailed(true);
|
|
6362
|
+
}, []);
|
|
6080
6363
|
(0, react.useEffect)(() => {
|
|
6081
6364
|
if (!expanded) return;
|
|
6082
6365
|
const onKey = (e) => {
|
|
@@ -6089,10 +6372,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6089
6372
|
}, [expanded]);
|
|
6090
6373
|
(0, react.useEffect)(() => {
|
|
6091
6374
|
if (bodyRef.current === null) return;
|
|
6092
|
-
renderMermaidInto(bodyRef.current, mermaid,
|
|
6093
|
-
|
|
6094
|
-
});
|
|
6095
|
-
}, [mermaid]);
|
|
6375
|
+
renderMermaidInto(bodyRef.current, mermaid, onFail);
|
|
6376
|
+
}, [mermaid, onFail]);
|
|
6096
6377
|
const body = failed ? (0, react.createElement)("pre", { className: "lks-acard-code" }, mermaid) : (0, react.createElement)("div", {
|
|
6097
6378
|
className: "lks-acard-diagram",
|
|
6098
6379
|
ref: bodyRef
|
|
@@ -6121,13 +6402,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6121
6402
|
onClick: () => {
|
|
6122
6403
|
setExpanded(false);
|
|
6123
6404
|
}
|
|
6124
|
-
}, (0, react.createElement)(IconCloseFill16, { size: 13 }))), failed ? (0, react.createElement)("pre", { className: "lks-acard-code" }, mermaid) : (0, react.createElement)(CanvasStage, { testid: "diagram-modal-stage" }, (0, react.createElement)(
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
if (el !== null) renderMermaidInto(el, mermaid, () => {
|
|
6128
|
-
setFailed(true);
|
|
6129
|
-
});
|
|
6130
|
-
}
|
|
6405
|
+
}, (0, react.createElement)(IconCloseFill16, { size: 13 }))), failed ? (0, react.createElement)("pre", { className: "lks-acard-code" }, mermaid) : (0, react.createElement)(CanvasStage, { testid: "diagram-modal-stage" }, (0, react.createElement)(DiagramModalDiagram, {
|
|
6406
|
+
mermaid,
|
|
6407
|
+
onFail
|
|
6131
6408
|
})))) : null);
|
|
6132
6409
|
}
|
|
6133
6410
|
/** The opening guess: two big options, click = pick (sent to the tutor, who
|
|
@@ -7272,6 +7549,25 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7272
7549
|
key: row.key,
|
|
7273
7550
|
className: "lks14-toolchip " + String(row.toolState ?? "loading")
|
|
7274
7551
|
}, row.toolState === "done" ? "✓" : row.toolState === "error" ? "×" : (0, react.createElement)("i", null), (0, react.createElement)("span", null, row.text));
|
|
7552
|
+
if (row.role === "ask" && row.ask !== void 0) {
|
|
7553
|
+
const answered = row.ask.answered;
|
|
7554
|
+
return (0, react.createElement)("div", {
|
|
7555
|
+
key: row.key,
|
|
7556
|
+
className: `lks14-askrow${answered ? " answered" : ""}`,
|
|
7557
|
+
"data-testid": "ask-row"
|
|
7558
|
+
}, (0, react.createElement)("div", { className: "lks14-askhead" }, tr("ask.title")), ...row.ask.questions.map((q) => (0, react.createElement)("div", {
|
|
7559
|
+
key: q.id,
|
|
7560
|
+
className: "lks14-askq"
|
|
7561
|
+
}, q.header !== "" ? (0, react.createElement)("div", { className: "lks14-askq-h" }, q.header) : null, ...q.options.map((opt) => (0, react.createElement)("button", {
|
|
7562
|
+
key: opt.label,
|
|
7563
|
+
className: "lks14-askopt",
|
|
7564
|
+
disabled: answered || interactive?.answerAsk === void 0,
|
|
7565
|
+
"data-testid": "ask-option",
|
|
7566
|
+
onClick: () => {
|
|
7567
|
+
interactive?.answerAsk?.(opt.label);
|
|
7568
|
+
}
|
|
7569
|
+
}, (0, react.createElement)("span", { className: "lks14-askopt-label" }, opt.label), opt.description !== "" ? (0, react.createElement)("span", { className: "lks14-askopt-desc" }, opt.description) : null)), q.options.length === 0 ? (0, react.createElement)("div", { className: "lks14-askfree" }, tr("ask.free")) : null)), answered ? (0, react.createElement)("div", { className: "lks14-askdone" }, tr("ask.answered")) : null);
|
|
7570
|
+
}
|
|
7275
7571
|
const cls = row.role === "streaming" ? "lks14-msg lks14-msg-assistant streaming" : "lks14-msg lks14-msg-assistant";
|
|
7276
7572
|
const body = (0, react.createElement)(ErrorBoundary, { key: `b-${row.key}` }, (0, react.createElement)("div", {
|
|
7277
7573
|
className: cls,
|
|
@@ -7307,7 +7603,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7307
7603
|
key: opt.letter,
|
|
7308
7604
|
className: "lks14-opt",
|
|
7309
7605
|
onClick: () => {
|
|
7310
|
-
interactive
|
|
7606
|
+
interactive?.send(tr("quiz.answer", {
|
|
7311
7607
|
letter: opt.letter,
|
|
7312
7608
|
text: opt.text
|
|
7313
7609
|
}));
|
|
@@ -7324,7 +7620,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7324
7620
|
};
|
|
7325
7621
|
}
|
|
7326
7622
|
function StudyPanelBody({ ctx }) {
|
|
7327
|
-
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, uploadAttachment } = useStudy();
|
|
7623
|
+
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, setCourseScope, uploadAttachment } = useStudy();
|
|
7328
7624
|
(0, react.useEffect)(() => {
|
|
7329
7625
|
wireCodeBlockCopy();
|
|
7330
7626
|
}, []);
|
|
@@ -7472,6 +7768,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7472
7768
|
const examLessonActive = lesson !== null && lesson.kind === "exam" && examOpen(data?.courses.flatMap((c) => c.sections).find((s) => s.lessons.some((l) => l.id === lesson.lessonId))?.lessons ?? []);
|
|
7473
7769
|
const localBound = (0, react.useRef)(null);
|
|
7474
7770
|
const boundSessionOf = (lessonId) => lessonId !== null && localBound.current?.lessonId === lessonId ? localBound.current.sessionId : null;
|
|
7771
|
+
const threadKeyOf = (lessonId) => {
|
|
7772
|
+
if (lessonId === null || data === null) return null;
|
|
7773
|
+
for (const c of data.courses) if (c.sections.some((s) => s.lessons.some((l) => l.id === lessonId))) return threadOwnerKey(c.threadScope, c.courseId, lessonId);
|
|
7774
|
+
return null;
|
|
7775
|
+
};
|
|
7776
|
+
const threadGroupOf = (lessonId) => {
|
|
7777
|
+
const key = threadKeyOf(lessonId);
|
|
7778
|
+
return key !== null ? data?.lessonThreads[key] ?? null : null;
|
|
7779
|
+
};
|
|
7475
7780
|
const streamEl = (0, react.useRef)(null);
|
|
7476
7781
|
const touchStart = (0, react.useRef)(null);
|
|
7477
7782
|
const activeFace = (0, react.useRef)(null);
|
|
@@ -7480,7 +7785,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7480
7785
|
try {
|
|
7481
7786
|
let face = activeFace.current;
|
|
7482
7787
|
if (face === null) {
|
|
7483
|
-
const sessionId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? (lesson !== null ?
|
|
7788
|
+
const sessionId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? threadGroupOf(lesson !== null ? lesson.lessonId : null)?.active ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7484
7789
|
if (sessionId !== null && sessionKnown(ctx, sessionId)) {
|
|
7485
7790
|
const actx = ctx.sessions.scope(sessionId);
|
|
7486
7791
|
face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx) ?? null;
|
|
@@ -7516,7 +7821,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7516
7821
|
} else celebrate(ev.kind);
|
|
7517
7822
|
prevCelebrationRef.current = snapshot;
|
|
7518
7823
|
}, [data]);
|
|
7519
|
-
const boundId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? (lesson !== null ?
|
|
7824
|
+
const boundId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? threadGroupOf(lesson !== null ? lesson.lessonId : null)?.active ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7520
7825
|
const nudged = (0, react.useRef)(false);
|
|
7521
7826
|
(0, react.useEffect)(() => {
|
|
7522
7827
|
if (nudged.current || (data?.dueCount ?? 0) === 0) return;
|
|
@@ -7703,7 +8008,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7703
8008
|
if (data?.active !== true) await activate(true);
|
|
7704
8009
|
if (targetLessonId !== void 0 && targetLessonId !== lesson.lessonId) await setFocus(targetLessonId).catch(() => {});
|
|
7705
8010
|
const bindId = targetLessonId ?? lesson.lessonId;
|
|
7706
|
-
let sessionId = boundSessionOf(bindId) ??
|
|
8011
|
+
let sessionId = boundSessionOf(bindId) ?? threadGroupOf(bindId)?.active ?? data?.lessonSessions[bindId] ?? null;
|
|
7707
8012
|
if (sessionId !== null && !sessionKnown(ctx, sessionId)) sessionId = null;
|
|
7708
8013
|
if (sessionId === null) {
|
|
7709
8014
|
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
@@ -7716,10 +8021,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7716
8021
|
sessionId
|
|
7717
8022
|
};
|
|
7718
8023
|
await bindLessonSession(bindId, sessionId, threadTitle ?? threadAutoTitle(text));
|
|
7719
|
-
} else
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
8024
|
+
} else {
|
|
8025
|
+
localBound.current = {
|
|
8026
|
+
lessonId: bindId,
|
|
8027
|
+
sessionId
|
|
8028
|
+
};
|
|
8029
|
+
await bindLessonSession(bindId, sessionId);
|
|
8030
|
+
}
|
|
7723
8031
|
const shell = PANEL_SHELL.current;
|
|
7724
8032
|
if (shell !== null) shell.suppressHandBack(() => {
|
|
7725
8033
|
ctx.sessions.open(sessionId);
|
|
@@ -7898,6 +8206,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7898
8206
|
onThreadRename: renameLessonThreadUi,
|
|
7899
8207
|
onThreadArchive: archiveLessonThreadUi,
|
|
7900
8208
|
onThreadDelete: deleteLessonThreadUi,
|
|
8209
|
+
courseScope: data !== null && lesson !== null ? data.courses.find((c) => c.sections.some((sec) => sec.lessons.some((l) => l.id === lesson.lessonId)))?.threadScope ?? "lesson" : "lesson",
|
|
8210
|
+
onScopeToggle: (next) => {
|
|
8211
|
+
const cid = data !== null && lesson !== null ? data.courses.find((c) => c.sections.some((sec) => sec.lessons.some((l) => l.id === lesson.lessonId)))?.courseId : void 0;
|
|
8212
|
+
if (cid !== void 0) setCourseScope(cid, next);
|
|
8213
|
+
},
|
|
7901
8214
|
contextMeter,
|
|
7902
8215
|
contextBreakdown: breakdown,
|
|
7903
8216
|
uploadAttachment
|
|
@@ -7980,6 +8293,33 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7980
8293
|
* search-and-jump (the dashboard search API), course switch, review, start
|
|
7981
8294
|
* studying — all funneling the panel's own actions.
|
|
7982
8295
|
*/
|
|
8296
|
+
/** The cmap modal's diagram host — rendered ONCE per (title, concepts) VALUE.
|
|
8297
|
+
* The raw-ref-callback version re-ran renderLessonConceptMap on every parent
|
|
8298
|
+
* render (each 3s state poll recreates the feed-derived concepts array, so
|
|
8299
|
+
* even a memo-by-identity effect would re-fire); the dataset key compares by
|
|
8300
|
+
* value — poll re-renders no-op, a real mastery change re-renders. Same #12
|
|
8301
|
+
* family as the diagram modal. */
|
|
8302
|
+
function CmapModalDiagram({ title, concepts }) {
|
|
8303
|
+
const ref = (0, react.useRef)(null);
|
|
8304
|
+
(0, react.useEffect)(() => {
|
|
8305
|
+
const el = ref.current;
|
|
8306
|
+
if (el === null) return;
|
|
8307
|
+
const key = `${title}\u0000${concepts.map((c) => `${c.title}:${String(c.masteryPct)}`).join("")}`;
|
|
8308
|
+
if (el.dataset.cmapKey === key) return;
|
|
8309
|
+
el.dataset.cmapKey = key;
|
|
8310
|
+
el.textContent = "";
|
|
8311
|
+
renderLessonConceptMap(el, title, concepts.map((c) => ({
|
|
8312
|
+
title: c.title,
|
|
8313
|
+
masteryPct: c.masteryPct
|
|
8314
|
+
}))).catch(() => {
|
|
8315
|
+
el.textContent = tr("bb.fallback.cmap");
|
|
8316
|
+
});
|
|
8317
|
+
});
|
|
8318
|
+
return (0, react.createElement)("div", {
|
|
8319
|
+
className: "lks14-modal-diagram",
|
|
8320
|
+
ref
|
|
8321
|
+
});
|
|
8322
|
+
}
|
|
7983
8323
|
function CommandPalette({ searchLessons, courses, onClose, onLesson, onCourse, onReview, onStudy }) {
|
|
7984
8324
|
const [query, setQuery] = (0, react.useState)("");
|
|
7985
8325
|
const [lessons, setLessons] = (0, react.useState)([]);
|
|
@@ -8834,7 +9174,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8834
9174
|
return cut <= 0 ? path : norm.slice(0, cut);
|
|
8835
9175
|
}
|
|
8836
9176
|
/** 中栏:the tutor chat stream with its own composer (upstream ChatStream + ChatComposer). */
|
|
8837
|
-
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, stop, setMode, narrowPane, onThreadSwitch, onThreadNew, onThreadRename, onThreadArchive, onThreadDelete, contextMeter, contextBreakdown, uploadAttachment }) {
|
|
9177
|
+
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, stop, setMode, narrowPane, onThreadSwitch, onThreadNew, onThreadRename, onThreadArchive, onThreadDelete, courseScope, onScopeToggle, contextMeter, contextBreakdown, uploadAttachment }) {
|
|
8838
9178
|
const stuck = (0, react.useRef)(true);
|
|
8839
9179
|
const [showFab, setShowFab] = (0, react.useState)(false);
|
|
8840
9180
|
const [threadsOpen, setThreadsOpen] = (0, react.useState)(false);
|
|
@@ -9060,7 +9400,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9060
9400
|
revealed
|
|
9061
9401
|
}));
|
|
9062
9402
|
};
|
|
9063
|
-
const
|
|
9403
|
+
const threadKeyOfHere = (lessonId) => {
|
|
9404
|
+
if (lessonId === null || data === null) return null;
|
|
9405
|
+
for (const c of data.courses) if (c.sections.some((sec) => sec.lessons.some((l) => l.id === lessonId))) return threadOwnerKey(c.threadScope, c.courseId, lessonId);
|
|
9406
|
+
return null;
|
|
9407
|
+
};
|
|
9408
|
+
const keyHere = threadKeyOfHere(lesson !== null ? lesson.lessonId : null);
|
|
9409
|
+
const threadPills = threadGroupPills(keyHere !== null ? data?.lessonThreads[keyHere] : void 0);
|
|
9064
9410
|
const lastAssistant = rowsView.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
9065
9411
|
const [error, setError] = (0, react.useState)(null);
|
|
9066
9412
|
const fire = (action) => {
|
|
@@ -9151,14 +9497,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9151
9497
|
}));
|
|
9152
9498
|
send(tr("proposal.decline.msg", { lesson: proposal.lessonTitle }));
|
|
9153
9499
|
}
|
|
9154
|
-
}, tr("proposal.decline"))) : decidedBadge !== null ? (0, react.createElement)("div", { className: `lks-propbanner decided ${decidedBadge.kind}` }, decidedBadge.kind === "accepted" ? (0, react.createElement)(IconCrownFill16, { size: 14 }) : (0, react.createElement)(IconBookFill16, { size: 14 }), (0, react.createElement)("span", { className: "lks-propbanner-why" }, tr(decidedBadge.kind === "accepted" ? "proposal.applied" : "proposal.rejected", { lesson: decidedBadge.title }))) : null, lesson !== null || !dormant ? (0, react.createElement)("div", { className: "lks14-lessonrow" }, lesson !== null ? (0, react.createElement)("span", null, lesson.title) : (0, react.createElement)("span", null, tr("col.tutor")), threadPills.length > 0 ? (0, react.createElement)("div", { className: "lks14-threadswitch" }, (0, react.createElement)("button", {
|
|
9500
|
+
}, tr("proposal.decline"))) : decidedBadge !== null ? (0, react.createElement)("div", { className: `lks-propbanner decided ${decidedBadge.kind}` }, decidedBadge.kind === "accepted" ? (0, react.createElement)(IconCrownFill16, { size: 14 }) : (0, react.createElement)(IconBookFill16, { size: 14 }), (0, react.createElement)("span", { className: "lks-propbanner-why" }, tr(decidedBadge.kind === "accepted" ? "proposal.applied" : "proposal.rejected", { lesson: decidedBadge.title }))) : null, lesson !== null || !dormant ? (0, react.createElement)("div", { className: "lks14-lessonrow" }, lesson !== null ? (0, react.createElement)("span", null, lesson.title) : (0, react.createElement)("span", null, tr("col.tutor")), threadPills.length > 0 || courseScope === "course" ? (0, react.createElement)("div", { className: "lks14-threadswitch" }, (0, react.createElement)("button", {
|
|
9155
9501
|
className: "lks14-threadchip",
|
|
9156
9502
|
"aria-expanded": String(threadsOpen),
|
|
9157
9503
|
"data-tooltip": tr("threads.label"),
|
|
9158
9504
|
onClick: () => {
|
|
9159
9505
|
setThreadsOpen((v) => !v);
|
|
9160
9506
|
}
|
|
9161
|
-
}, tr("threads.open", { n: String(threadPills.length) }), (0, react.createElement)("span", { className: "lks14-threadchip-caret" }, threadsOpen ? "▾" : "▸")), threadsOpen ? (0, react.createElement)("div", {
|
|
9507
|
+
}, threadPills.length > 0 ? tr("threads.open", { n: String(threadPills.length) }) : tr("threads.scope"), (0, react.createElement)("span", { className: "lks14-threadchip-caret" }, threadsOpen ? "▾" : "▸")), threadsOpen ? (0, react.createElement)("div", {
|
|
9162
9508
|
className: "lks14-threadmenu",
|
|
9163
9509
|
role: "menu"
|
|
9164
9510
|
}, ...threadPills.map((p) => (0, react.createElement)("div", {
|
|
@@ -9203,7 +9549,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9203
9549
|
if (v !== "" && v !== p.title) onThreadRename(p.id, v);
|
|
9204
9550
|
setRenaming(null);
|
|
9205
9551
|
}
|
|
9206
|
-
}) : (0, react.createElement)("span", { className: "lks14-threadmenu-title" }, p.title
|
|
9552
|
+
}) : (0, react.createElement)("span", { className: "lks14-threadmenu-title" }, p.title, courseScope === "course" && threadCoverageLabel(p.touchedLessons, data?.courses ?? []) !== "" ? (0, react.createElement)("span", {
|
|
9553
|
+
className: "lks14-threadmenu-cover",
|
|
9554
|
+
"data-testid": "thread-cover"
|
|
9555
|
+
}, `${tr("threads.coverage")}: ${threadCoverageLabel(p.touchedLessons, data?.courses ?? [])}`) : null), p.current ? (0, react.createElement)("span", { className: "lks14-threadmenu-now" }, tr("threads.now")) : null, (0, react.createElement)("button", {
|
|
9207
9556
|
className: "lks14-threadmenu-gear",
|
|
9208
9557
|
"aria-label": tr("threads.actions"),
|
|
9209
9558
|
"data-tooltip": tr("threads.actions"),
|
|
@@ -9246,6 +9595,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9246
9595
|
setGearFor(null);
|
|
9247
9596
|
}
|
|
9248
9597
|
}, tr("threads.delete"))) : null)), (0, react.createElement)("button", {
|
|
9598
|
+
key: "__scope__",
|
|
9599
|
+
className: "lks14-threadmenu-row scope",
|
|
9600
|
+
role: "menuitemcheckbox",
|
|
9601
|
+
"aria-checked": String(courseScope === "course"),
|
|
9602
|
+
"data-testid": "thread-scope-toggle",
|
|
9603
|
+
onClick: () => {
|
|
9604
|
+
onScopeToggle(courseScope === "course" ? "lesson" : "course");
|
|
9605
|
+
}
|
|
9606
|
+
}, (0, react.createElement)("span", {
|
|
9607
|
+
className: "lks14-threadmenu-scope-icon",
|
|
9608
|
+
"aria-hidden": "true"
|
|
9609
|
+
}, courseScope === "course" ? "🔗" : "⛓"), (0, react.createElement)("span", { className: "lks14-threadmenu-title" }, tr("threads.scope")), (0, react.createElement)("span", { className: `lks14-threadmenu-scope-state${courseScope === "course" ? " on" : ""}` }, courseScope === "course" ? tr("threads.scope.on") : tr("threads.scope.off"))), (0, react.createElement)("button", {
|
|
9249
9610
|
key: "__new__",
|
|
9250
9611
|
className: "lks14-threadmenu-row new",
|
|
9251
9612
|
role: "menuitem",
|
|
@@ -9280,7 +9641,20 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9280
9641
|
onClick: () => {
|
|
9281
9642
|
send(starters[0]?.message ?? tr("prompt.start"));
|
|
9282
9643
|
}
|
|
9283
|
-
}, tr("chat.start")) : null) : rowsView.map((row, i) => row.role === "artifact" ? inlineArtifactCard(row, rowsView.slice(i + 1).some((r) => r.role === "assistant")) : chatRow(row, !dormant && i === lastAssistant
|
|
9644
|
+
}, tr("chat.start")) : null) : rowsView.map((row, i) => row.role === "artifact" ? inlineArtifactCard(row, rowsView.slice(i + 1).some((r) => r.role === "assistant")) : chatRow(row, !dormant && (i === lastAssistant || row.role === "ask" && row.ask !== void 0 && !row.ask.answered) ? {
|
|
9645
|
+
send,
|
|
9646
|
+
answerAsk: (label) => {
|
|
9647
|
+
(async () => {
|
|
9648
|
+
if (busy) {
|
|
9649
|
+
stop();
|
|
9650
|
+
await new Promise((r) => {
|
|
9651
|
+
setTimeout(r, 1800);
|
|
9652
|
+
});
|
|
9653
|
+
}
|
|
9654
|
+
send(label);
|
|
9655
|
+
})();
|
|
9656
|
+
}
|
|
9657
|
+
} : void 0, row.role === "assistant" ? {
|
|
9284
9658
|
playing: msgAudio?.key === row.key,
|
|
9285
9659
|
index: msgAudio?.index ?? 0,
|
|
9286
9660
|
total: msgAudio?.total ?? 0,
|
|
@@ -9669,7 +10043,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9669
10043
|
(0, react.useEffect)(() => {
|
|
9670
10044
|
if (tab !== "teach" || proseRef.current === null || lesson === null) return;
|
|
9671
10045
|
enhanceRendered(proseRef.current).catch(() => {}).finally(() => {
|
|
9672
|
-
applyHighlights(proseRef.current, recordQuotes);
|
|
10046
|
+
if (proseRef.current !== null) applyHighlights(proseRef.current, recordQuotes);
|
|
9673
10047
|
setEnhanceTick(Date.now());
|
|
9674
10048
|
});
|
|
9675
10049
|
}, [tab, lesson?.html]);
|
|
@@ -9678,7 +10052,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9678
10052
|
locateNoteQuote(pendingLocate, proseRef.current);
|
|
9679
10053
|
setPendingLocate(null);
|
|
9680
10054
|
}, [pendingLocate, tab]);
|
|
9681
|
-
const body = lesson === null ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("bb.empty"), (0, react.createElement)("br"), tr("bb.empty.hint")) : (0, react.createElement)("div", { className: "lks14-notebody" }, (0, react.createElement)("div", { className: "lks14-viewtabs" }, (0, react.createElement)("button", {
|
|
10055
|
+
const body = lesson === null ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("bb.empty"), (0, react.createElement)("br"), tr("bb.empty.hint")) : (0, react.createElement)("div", { className: `lks14-notebody${tab === "board" ? " lks14-notebody-fill" : ""}` }, (0, react.createElement)("div", { className: "lks14-viewtabs" }, (0, react.createElement)("button", {
|
|
9682
10056
|
className: `lks14-viewtab${tab === "teach" ? " on" : ""}`,
|
|
9683
10057
|
"aria-pressed": String(tab === "teach"),
|
|
9684
10058
|
onClick: () => {
|
|
@@ -9722,7 +10096,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9722
10096
|
onClick: () => {
|
|
9723
10097
|
setRateBusy(true);
|
|
9724
10098
|
recordReview(lesson.lessonId, Number(q)).then(() => {
|
|
9725
|
-
const rateEl = document.querySelector(`[data-lks-rate="${lesson.lessonId}"]`);
|
|
10099
|
+
const rateEl = document.querySelector(`[data-lks-rate="${CSS.escape(lesson.lessonId)}"]`);
|
|
9726
10100
|
const rr = Number(q) >= 4 && rateEl !== null ? rateEl.getBoundingClientRect() : null;
|
|
9727
10101
|
celebrate(Number(q) >= 4 ? "correct" : "wrong", rr !== null ? { origin: {
|
|
9728
10102
|
x: rr.right - 48,
|
|
@@ -9830,19 +10204,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9830
10204
|
onClick: () => {
|
|
9831
10205
|
setCmapExpanded(false);
|
|
9832
10206
|
}
|
|
9833
|
-
}, (0, react.createElement)(IconCloseFill16, { size: 13 }))), (0, react.createElement)(CanvasStage, { testid: "cmap-modal-stage" }, (0, react.createElement)(
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
title: c.title,
|
|
9840
|
-
masteryPct: c.masteryPct
|
|
9841
|
-
}))).catch(() => {
|
|
9842
|
-
el.textContent = tr("bb.fallback.cmap");
|
|
9843
|
-
});
|
|
9844
|
-
}
|
|
9845
|
-
}
|
|
10207
|
+
}, (0, react.createElement)(IconCloseFill16, { size: 13 }))), (0, react.createElement)(CanvasStage, { testid: "cmap-modal-stage" }, (0, react.createElement)(CmapModalDiagram, {
|
|
10208
|
+
title: lesson.title,
|
|
10209
|
+
concepts: lesson.concepts.map((c) => ({
|
|
10210
|
+
title: c.title,
|
|
10211
|
+
masteryPct: c.masteryPct
|
|
10212
|
+
}))
|
|
9846
10213
|
})))) : null) : (0, react.createElement)("div", { className: "lks14-zones" }, (() => {
|
|
9847
10214
|
const understand = artifacts.filter((a) => a.artifactType === "compare_table" || a.artifactType === "diagram" || a.artifactType === "code_walkthrough");
|
|
9848
10215
|
if (understand.length === 0) return null;
|
|
@@ -9893,10 +10260,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9893
10260
|
"data-tooltip": tr("note.locate"),
|
|
9894
10261
|
"aria-label": tr("note.locate"),
|
|
9895
10262
|
onClick: () => {
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
setPendingLocate(n.quote ?? "");
|
|
9899
|
-
} else locateNoteQuote(n.quote ?? "", proseRef.current);
|
|
10263
|
+
setTab("teach");
|
|
10264
|
+
setPendingLocate(n.quote ?? "");
|
|
9900
10265
|
}
|
|
9901
10266
|
}, (0, react.createElement)(IconGoalOutline16, { size: 12 })) : null, (0, react.createElement)("button", {
|
|
9902
10267
|
className: "lks-note-act",
|