dsh-plugin-lookatstudy 0.8.1 → 0.9.1
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 +3 -3
- package/lib/adoc-parser-1UxcYHid.mjs +47 -0
- package/lib/client.js +157 -28
- package/lib/client.js.map +1 -1
- package/lib/code-parser-BOOk9IWV.mjs +0 -2
- package/lib/index.d.mts +12 -5
- package/lib/index.mjs +967 -565
- package/lib/notebook-parser-ChbZBIKJ.mjs +0 -2
- package/lib/org-parser-BT5yvx9h.mjs +53 -0
- package/lib/rmd-parser-EgaMaffn.mjs +17 -0
- package/lib/rst-parser-CV93w3Sq.mjs +99 -0
- package/package.json +7 -2
- package/lib/code-parser-BOOk9IWV.mjs.map +0 -1
- package/lib/index.d.mts.map +0 -1
- package/lib/index.mjs.map +0 -1
- package/lib/notebook-parser-ChbZBIKJ.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-plugin-lookatstudy
|
|
2
2
|
|
|
3
|
-
Turn any markdown document, local folder, or GitHub learning repository into a guided course inside [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) — your dsh agent becomes a full AI tutor with the interaction design of [LookatStudy](https://github.com/
|
|
3
|
+
Turn any markdown document, local folder, or GitHub learning repository into a guided course inside [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) — your dsh agent becomes a full AI tutor with the interaction design of [LookatStudy](https://github.com/Kaiji-Z/LookatStudy): per-concept knowledge tracking, mastery-driven progression, spaced repetition, mastery proposals, friction awareness, learner memory, a Cornell notebook, and an in-chat proposal card. Learning engine modules are vendored from LookatStudy (MIT).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -65,8 +65,8 @@ LookatStudy's Electron-native experiences have no host surface in dsh: persisten
|
|
|
65
65
|
pnpm exec tsdown # build lib/ (host + client entries, peers external)
|
|
66
66
|
pnpm test # 48 node:test cases over the real source (no key needed)
|
|
67
67
|
|
|
68
|
-
# iterate against a live dsh (
|
|
69
|
-
pnpm dsh web --patch
|
|
68
|
+
# iterate against a live dsh (this repo lives beside a deepseek-harness checkout):
|
|
69
|
+
pnpm dsh web --patch ../dsh-plugin-lookatstudy/cordis.dev.yml # run from the harness checkout
|
|
70
70
|
# then open http://127.0.0.1:3080/ and switch to the 学习 tab
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/vendor/adoc-parser.ts
|
|
2
|
+
function parseAdoc(adocText) {
|
|
3
|
+
const lines = adocText.split("\n");
|
|
4
|
+
const output = [];
|
|
5
|
+
let inSourceBlock = false;
|
|
6
|
+
let pendingLang = "";
|
|
7
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8
|
+
const line = lines[i] ?? "";
|
|
9
|
+
const sourceMatch = line.match(/^\[source,\s*(\w*)\s*\]$/);
|
|
10
|
+
if (sourceMatch && !inSourceBlock) {
|
|
11
|
+
pendingLang = sourceMatch[1] ?? "";
|
|
12
|
+
if ((lines[i + 1] ?? "").trim() === "----") {
|
|
13
|
+
inSourceBlock = true;
|
|
14
|
+
i++;
|
|
15
|
+
output.push(`\`\`\`${pendingLang}`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (inSourceBlock && line.trim() === "----") {
|
|
20
|
+
inSourceBlock = false;
|
|
21
|
+
output.push("```");
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (inSourceBlock) {
|
|
25
|
+
output.push(line);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let processed = line;
|
|
29
|
+
processed = processed.replace(/^(=+)\s+/, (_match, eqs) => {
|
|
30
|
+
if (eqs.length > 6) return "#".repeat(6) + " ";
|
|
31
|
+
return "#".repeat(eqs.length) + " ";
|
|
32
|
+
});
|
|
33
|
+
processed = processed.replace(/image::(\S+?)\[([^\]]*)\]/g, (_m, path, alt) => {
|
|
34
|
+
return alt ? `` : ``;
|
|
35
|
+
});
|
|
36
|
+
processed = processed.replace(/image:(\S+?)\[([^\]]*)\]/g, (_m, path, alt) => {
|
|
37
|
+
return alt ? `` : ``;
|
|
38
|
+
});
|
|
39
|
+
processed = processed.replace(/link:(\S+?)\[([^\]]*)\]/g, "[$2]($1)");
|
|
40
|
+
processed = processed.replace(/(\s|^)\*([^\s*][^*]*?)\*(?=\s|[.,;:!?)])/gm, "$1**$2**");
|
|
41
|
+
processed = processed.replace(/(\s|^)_([^\s_][^_]*?)_(?=\s|[.,;:!?)])/gm, "$1*$2*");
|
|
42
|
+
output.push(processed);
|
|
43
|
+
}
|
|
44
|
+
return { markdown: output.join("\n") };
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { parseAdoc };
|
package/lib/client.js
CHANGED
|
@@ -41,9 +41,15 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
.lks-body{flex:1;min-height:0;min-width:0;display:flex;gap:16px}
|
|
42
42
|
.lks-col{display:flex;flex-direction:column;min-width:0}
|
|
43
43
|
.lks-colhead{flex:none;font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary);letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px}
|
|
44
|
-
.lks-col-rail{flex:
|
|
44
|
+
.lks-col-rail{flex:0 1 260px;min-width:180px;overflow-y:auto;padding:2px 6px}
|
|
45
45
|
.lks-col-tutor{flex:0 0 auto;width:min(100%,var(--dsh-composer-card-max-width,780px))}
|
|
46
46
|
.lks-col-bb{flex:1 1 0;min-width:230px;overflow-y:auto;padding:2px 6px}
|
|
47
|
+
/* While the study view is mounted in wide mode, the host composer card is
|
|
48
|
+
shifted under the tutor column (the transcript's width axis) instead of the
|
|
49
|
+
scroll body's center — views.tsx measures the offset, sets the variable and
|
|
50
|
+
toggles the class, and removes both whenever the shift collapses (narrow
|
|
51
|
+
pane, hidden view) so the host's natural centering comes back. */
|
|
52
|
+
[data-composer-card].lks-composer-follow{align-self:flex-start;margin-left:var(--lks-composer-shift,0px)}
|
|
47
53
|
/* narrow (<1220px): one composer-width pane at a time, chosen by a centered
|
|
48
54
|
segmented pill group (joined buttons in one capsule — button language, not
|
|
49
55
|
tab language, against the host's view tabs right above). */
|
|
@@ -72,7 +78,7 @@ window.__ModuleLoader__.load({
|
|
|
72
78
|
.lks-masterybar i{display:block;height:100%;background:var(--dsw-alias-state-business-primary);transition:width .3s ease}
|
|
73
79
|
.lks-masterybar.gold i{background:var(--dsw-alias-state-warn-primary)}
|
|
74
80
|
.lks-search{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13.5px;padding:5px 9px;margin-bottom:4px}
|
|
75
|
-
.lks-search:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
|
|
81
|
+
.lks-search:focus{outline:none;border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
|
|
76
82
|
.lks-sec-num{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;border-radius:50%;background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);font-size:11px;font-weight:700;margin-right:4px}
|
|
77
83
|
.lks-tag.due{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}
|
|
78
84
|
.lks-import{margin-top:8px}
|
|
@@ -87,13 +93,25 @@ window.__ModuleLoader__.load({
|
|
|
87
93
|
.lks-duebox .lks-over{color:var(--dsw-alias-state-warn-label);flex:none}
|
|
88
94
|
|
|
89
95
|
.lks-sec{font-size:12px;color:var(--dsw-alias-label-tertiary);text-transform:uppercase;letter-spacing:.05em;margin:14px 0 4px}
|
|
90
|
-
|
|
96
|
+
/* Collapsible section head: a real button (keyboard can toggle it), sticky so
|
|
97
|
+
the current chapter stays identified while 4000px of nodes scroll under it. */
|
|
98
|
+
.lks-sechead{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary);text-transform:uppercase;letter-spacing:.05em;text-align:left;padding:6px 4px;margin:14px 0 4px;border-radius:6px;position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-base)}
|
|
99
|
+
.lks-sechead:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
100
|
+
.lks-sechead:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}
|
|
101
|
+
.lks-sechead-t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
102
|
+
.lks-sechead-c{flex:none;font-size:10px}
|
|
103
|
+
.lks-sechead-n{flex:none;font-size:11px;text-transform:none;letter-spacing:0}
|
|
104
|
+
.lks-node{display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:6px 9px;border-radius:8px;margin:1px 0;cursor:pointer}
|
|
91
105
|
.lks-node:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
106
|
+
.lks-node:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}
|
|
92
107
|
.lks-node.focus{background:var(--dsw-alias-bg-layer-3);outline:1px solid var(--dsw-alias-border-l2)}
|
|
93
108
|
.lks-node .lks-g{width:18px;text-align:center;flex:none}
|
|
94
109
|
.lks-node .lks-t{flex:1;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
/* aria-disabled (not the disabled attribute): the row stays focusable so the
|
|
111
|
+
status tooltip can explain WHY it is locked — Firefox drops titles on truly
|
|
112
|
+
disabled form controls. */
|
|
113
|
+
.lks-node[aria-disabled='true']{opacity:.45;cursor:not-allowed}
|
|
114
|
+
.lks-node[aria-disabled='true']:hover{background:none}
|
|
97
115
|
.lks-tag{font-size:11px;border-radius:4px;padding:0 4px;flex:none}
|
|
98
116
|
.lks-tag.weak{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}
|
|
99
117
|
.lks-tag.fric{background:var(--dsw-alias-state-error-primary);color:#fff}
|
|
@@ -102,6 +120,7 @@ window.__ModuleLoader__.load({
|
|
|
102
120
|
.lks-pct{font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none;width:32px;text-align:right}
|
|
103
121
|
|
|
104
122
|
.lks-empty{color:var(--dsw-alias-label-tertiary);text-align:center;padding:48px 8px;font-size:14px;line-height:2}
|
|
123
|
+
.lks-actbar{display:flex;flex-direction:row;justify-content:flex-end;flex:none;padding-bottom:8px}
|
|
105
124
|
|
|
106
125
|
/* ── middle column: the tutor ── */
|
|
107
126
|
.lks-transcript{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding:4px 2px}
|
|
@@ -140,7 +159,9 @@ window.__ModuleLoader__.load({
|
|
|
140
159
|
.lks-starter:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}
|
|
141
160
|
|
|
142
161
|
/* proposal banner + shared buttons */
|
|
143
|
-
|
|
162
|
+
/* A mastery proposal is a positive, informational moment — business tint, not
|
|
163
|
+
the warn tint it previously wore (a reviewer flagged the semantic mismatch). */
|
|
164
|
+
.lks-banner{display:flex;align-items:center;gap:10px;background:var(--dsw-alias-state-business-tertiary);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 14px;margin:8px 0;font-size:14px;flex:none}
|
|
144
165
|
.lks-banner .lks-why{flex:1;color:var(--dsw-alias-label-secondary)}
|
|
145
166
|
.lks-btn{display:inline-flex;align-items:center;gap:6px;border-radius:8px;padding:6px 14px;font-size:13.5px;font-weight:600;flex:none}
|
|
146
167
|
.lks-btn.primary{background:var(--dsw-alias-state-business-primary);color:#fff}
|
|
@@ -175,7 +196,7 @@ window.__ModuleLoader__.load({
|
|
|
175
196
|
.lks-zone{margin-bottom:14px}
|
|
176
197
|
.lks-zone h4{font-size:14px;color:var(--dsw-alias-label-secondary);margin:0 0 8px;font-weight:600}
|
|
177
198
|
.lks-note{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 14px;margin-bottom:8px}
|
|
178
|
-
.lks-note .lks-note-src{float:right;font-size:
|
|
199
|
+
.lks-note .lks-note-src{float:right;font-size:11.5px;color:var(--dsw-alias-label-secondary)}
|
|
179
200
|
.lks-note .lks-note-title{font-weight:600;font-size:13px}
|
|
180
201
|
.lks-note .lks-note-text{margin-top:4px;white-space:pre-wrap;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
181
202
|
.lks-note .lks-note-q{margin-top:6px;color:var(--dsw-alias-label-tertiary);font-size:12px;border-left:2px solid var(--dsw-alias-state-warn-primary);padding-left:8px}
|
|
@@ -325,6 +346,18 @@ window.__ModuleLoader__.load({
|
|
|
325
346
|
});
|
|
326
347
|
this.refresh();
|
|
327
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Flip learning mode. The host syncs its tool registry before responding,
|
|
351
|
+
* so awaiting this guarantees the `study_*` tools exist for the next prompt.
|
|
352
|
+
*/
|
|
353
|
+
async activate(active) {
|
|
354
|
+
await fetchJson("/lookatstudy/api/active", {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: { "content-type": "application/json" },
|
|
357
|
+
body: JSON.stringify({ active })
|
|
358
|
+
});
|
|
359
|
+
this.refresh();
|
|
360
|
+
}
|
|
328
361
|
/** Point the tutor's focus at one lesson. */
|
|
329
362
|
async setFocus(lessonId) {
|
|
330
363
|
await fetchJson("/lookatstudy/api/focus", {
|
|
@@ -362,6 +395,7 @@ window.__ModuleLoader__.load({
|
|
|
362
395
|
function useStudy() {
|
|
363
396
|
return {
|
|
364
397
|
data: (0, react.useSyncExternalStore)(studyStore.subscribe, studyStore.getSnapshot, studyStore.getSnapshot),
|
|
398
|
+
activate: studyStore.activate.bind(studyStore),
|
|
365
399
|
setMode: studyStore.setMode.bind(studyStore),
|
|
366
400
|
setFocus: studyStore.setFocus.bind(studyStore),
|
|
367
401
|
deleteCourse: studyStore.deleteCourse.bind(studyStore),
|
|
@@ -559,6 +593,17 @@ window.__ModuleLoader__.load({
|
|
|
559
593
|
function titleMatches(title, query) {
|
|
560
594
|
return query.trim().toLowerCase().split(/\s+/).filter(Boolean).every((k) => title.toLowerCase().includes(k));
|
|
561
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Default expansion for one rail section: collapsed when every study lesson is
|
|
598
|
+
* done (mastered) or not yet reachable (locked) — long courses otherwise scroll
|
|
599
|
+
* 5× their viewport. The active frontier and the focus lesson's section stay
|
|
600
|
+
* open; exam nodes never force a section open (they are gated separately).
|
|
601
|
+
* Pure.
|
|
602
|
+
* @param section - one course section projection with lesson kinds/statuses.
|
|
603
|
+
*/
|
|
604
|
+
function sectionDefaultOpen(section) {
|
|
605
|
+
return section.lessons.some((l) => l.focus || l.kind !== "exam" && l.status !== "mastered" && l.status !== "locked");
|
|
606
|
+
}
|
|
562
607
|
/** The rail's import row: a GitHub URL input plus the paste/folder hints. */
|
|
563
608
|
function ImportRow({ send }) {
|
|
564
609
|
const [url, setUrl] = (0, react.useState)("");
|
|
@@ -707,14 +752,11 @@ window.__ModuleLoader__.load({
|
|
|
707
752
|
call: node.call
|
|
708
753
|
});
|
|
709
754
|
break;
|
|
710
|
-
case "turn-error":
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
});
|
|
716
|
-
break;
|
|
717
|
-
default: break;
|
|
755
|
+
case "turn-error": rows.push({
|
|
756
|
+
key: `e${node.seq}`,
|
|
757
|
+
role: "error",
|
|
758
|
+
text: node.message
|
|
759
|
+
});
|
|
718
760
|
}
|
|
719
761
|
if (partial !== null) {
|
|
720
762
|
const text = assistantText(partial.blocks);
|
|
@@ -762,18 +804,64 @@ window.__ModuleLoader__.load({
|
|
|
762
804
|
}
|
|
763
805
|
/** Tab body: the factory-bound ctx carries workspaces/sessions for the per-lesson session jumps. */
|
|
764
806
|
function StudyTab({ useSession, inputActions, ctx }) {
|
|
765
|
-
const { data, setMode, setFocus, deleteCourse, bindLessonSession } = useStudy();
|
|
807
|
+
const { data, activate, setMode, setFocus, deleteCourse, bindLessonSession } = useStudy();
|
|
766
808
|
const snapshot = useSession((s) => s);
|
|
767
809
|
const [pane, setPane] = (0, react.useState)(storedPane);
|
|
810
|
+
const rootRef = (0, react.useRef)(null);
|
|
811
|
+
(0, react.useEffect)(() => {
|
|
812
|
+
const root = rootRef.current;
|
|
813
|
+
const card = document.querySelector("[data-composer-card]");
|
|
814
|
+
const seat = card?.parentElement ?? null;
|
|
815
|
+
if (root === null || card === null || seat === null) return;
|
|
816
|
+
const clear = () => {
|
|
817
|
+
card.classList.remove("lks-composer-follow");
|
|
818
|
+
card.style.removeProperty("--lks-composer-shift");
|
|
819
|
+
};
|
|
820
|
+
const apply = () => {
|
|
821
|
+
const tutor = root.querySelector(".lks-col-tutor");
|
|
822
|
+
const rail = root.querySelector(".lks-col-rail");
|
|
823
|
+
if (tutor === null || rail === null || getComputedStyle(rail).display === "none") return clear();
|
|
824
|
+
const seatRect = seat.getBoundingClientRect();
|
|
825
|
+
const tutorRect = tutor.getBoundingClientRect();
|
|
826
|
+
if (seatRect.width === 0 || tutorRect.width === 0) return clear();
|
|
827
|
+
const pad = parseFloat(getComputedStyle(seat).paddingLeft) || 0;
|
|
828
|
+
const cardW = card.getBoundingClientRect().width;
|
|
829
|
+
const max = seatRect.width - 2 * pad - cardW;
|
|
830
|
+
const shift = Math.round(Math.max(0, Math.min(tutorRect.left - (seatRect.left + pad), max)));
|
|
831
|
+
if (shift < 2) return clear();
|
|
832
|
+
card.style.setProperty("--lks-composer-shift", `${shift}px`);
|
|
833
|
+
card.classList.add("lks-composer-follow");
|
|
834
|
+
};
|
|
835
|
+
apply();
|
|
836
|
+
const ro = new ResizeObserver(apply);
|
|
837
|
+
ro.observe(root);
|
|
838
|
+
ro.observe(seat);
|
|
839
|
+
window.addEventListener("resize", apply);
|
|
840
|
+
return () => {
|
|
841
|
+
ro.disconnect();
|
|
842
|
+
window.removeEventListener("resize", apply);
|
|
843
|
+
clear();
|
|
844
|
+
};
|
|
845
|
+
}, []);
|
|
768
846
|
const send = (text) => {
|
|
769
|
-
|
|
770
|
-
|
|
847
|
+
(async () => {
|
|
848
|
+
if (data?.active !== true) await activate(true);
|
|
849
|
+
inputActions.setDraft(text);
|
|
850
|
+
inputActions.submit();
|
|
851
|
+
})();
|
|
771
852
|
};
|
|
772
853
|
return (0, react.createElement)("div", {
|
|
854
|
+
ref: rootRef,
|
|
773
855
|
className: "lks-root lks-study",
|
|
774
856
|
"data-conversation-composer-overlay": "",
|
|
775
857
|
"data-pane": pane
|
|
776
|
-
}, (0, react.createElement)("div", { className: "lks-
|
|
858
|
+
}, (0, react.createElement)("div", { className: "lks-actbar" }, data === null ? null : (0, react.createElement)("button", {
|
|
859
|
+
className: `lks-btn ${data.active ? "ghost" : "primary"}`,
|
|
860
|
+
title: data.active ? "注销 study 工具与导师人格;学习进度保留,可随时重新开启" : "注册 study 工具并载入导师人格,之后普通对话和现在一样",
|
|
861
|
+
onClick: () => {
|
|
862
|
+
activate(!data.active);
|
|
863
|
+
}
|
|
864
|
+
}, data.active ? "⏻ 退出学习模式" : "▶ 开始学习")), (0, react.createElement)("div", { className: "lks-body" }, (0, react.createElement)("div", { className: "lks-switch" }, ...[
|
|
777
865
|
{
|
|
778
866
|
id: "rail",
|
|
779
867
|
label: "课程"
|
|
@@ -797,6 +885,7 @@ window.__ModuleLoader__.load({
|
|
|
797
885
|
}
|
|
798
886
|
}, p.label))), (0, react.createElement)(CourseRail, {
|
|
799
887
|
data,
|
|
888
|
+
activate,
|
|
800
889
|
setFocus,
|
|
801
890
|
deleteCourse,
|
|
802
891
|
bindLessonSession,
|
|
@@ -811,7 +900,7 @@ window.__ModuleLoader__.load({
|
|
|
811
900
|
}), (0, react.createElement)(BlackboardColumn, { data })));
|
|
812
901
|
}
|
|
813
902
|
/** Left column: course management (pick/delete/search/import), lesson tree, due box. */
|
|
814
|
-
function CourseRail({ data, setFocus, deleteCourse, bindLessonSession, send, ctx, currentSessionId }) {
|
|
903
|
+
function CourseRail({ data, activate, setFocus, deleteCourse, bindLessonSession, send, ctx, currentSessionId }) {
|
|
815
904
|
const [jumping, setJumping] = (0, react.useState)(null);
|
|
816
905
|
/** Open (or mint) the lesson's own session — the simplified thread system. */
|
|
817
906
|
const openLessonThread = (lesson) => {
|
|
@@ -828,6 +917,7 @@ window.__ModuleLoader__.load({
|
|
|
828
917
|
}
|
|
829
918
|
setJumping(lesson.id);
|
|
830
919
|
setFocus(lesson.id).then(() => (async () => {
|
|
920
|
+
if (data?.active !== true) await activate(true);
|
|
831
921
|
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
832
922
|
if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
|
|
833
923
|
const { path } = await area.json();
|
|
@@ -854,6 +944,8 @@ window.__ModuleLoader__.load({
|
|
|
854
944
|
const [query, setQuery] = (0, react.useState)("");
|
|
855
945
|
const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
|
|
856
946
|
const [showImport, setShowImport] = (0, react.useState)(false);
|
|
947
|
+
/** Per-section open overrides (courseId/sectionTitle → open?), on top of the frontier default. */
|
|
948
|
+
const [secOpen, setSecOpen] = (0, react.useState)({});
|
|
857
949
|
const [error, setError] = (0, react.useState)(null);
|
|
858
950
|
const reportError = (err) => {
|
|
859
951
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -911,7 +1003,29 @@ window.__ModuleLoader__.load({
|
|
|
911
1003
|
}
|
|
912
1004
|
}
|
|
913
1005
|
}
|
|
914
|
-
}),
|
|
1006
|
+
}), course.sections.some((s) => s.lessons.some((l) => l.focus)) ? (0, react.createElement)("button", {
|
|
1007
|
+
className: "lks-btn ghost",
|
|
1008
|
+
style: {
|
|
1009
|
+
margin: "0 0 6px",
|
|
1010
|
+
padding: "3px 8px",
|
|
1011
|
+
fontSize: "13px"
|
|
1012
|
+
},
|
|
1013
|
+
title: "在课程树中定位当前焦点课时(自动展开所在章节)",
|
|
1014
|
+
onClick: () => {
|
|
1015
|
+
const focusSection = course.sections.find((s) => s.lessons.some((l) => l.focus));
|
|
1016
|
+
if (focusSection === void 0) return;
|
|
1017
|
+
setSecOpen((m) => ({
|
|
1018
|
+
...m,
|
|
1019
|
+
[`${courseId}/${focusSection.title}`]: true
|
|
1020
|
+
}));
|
|
1021
|
+
window.setTimeout(() => {
|
|
1022
|
+
document.querySelector(".lks-col-rail .lks-node.focus")?.scrollIntoView({
|
|
1023
|
+
block: "center",
|
|
1024
|
+
behavior: "smooth"
|
|
1025
|
+
});
|
|
1026
|
+
}, 60);
|
|
1027
|
+
}
|
|
1028
|
+
}, "📍 回到当前课时") : null, data.dueCount > 0 ? (0, react.createElement)("div", { className: "lks-duebox" }, `🔁 待复习 ${data.dueCount}`, ...data.due.map((d) => (0, react.createElement)("div", {
|
|
915
1029
|
key: d.lessonId,
|
|
916
1030
|
className: "lks-due-item"
|
|
917
1031
|
}, (0, react.createElement)("span", null, d.lessonTitle), d.overdueDays > 0 ? (0, react.createElement)("span", { className: "lks-over" }, `超${d.overdueDays}天`) : null)), (0, react.createElement)("button", {
|
|
@@ -924,14 +1038,27 @@ window.__ModuleLoader__.load({
|
|
|
924
1038
|
const examAllowed = examOpen(section.lessons);
|
|
925
1039
|
const lessons = section.lessons.filter((l) => query.trim() === "" || titleMatches(l.title, query) || l.focus);
|
|
926
1040
|
if (lessons.length === 0) return [];
|
|
927
|
-
|
|
1041
|
+
const secKey = `${courseId}/${section.title}`;
|
|
1042
|
+
const open = query.trim() !== "" || (secOpen[secKey] ?? sectionDefaultOpen(section));
|
|
1043
|
+
return [(0, react.createElement)("button", {
|
|
928
1044
|
key: section.title,
|
|
929
|
-
|
|
930
|
-
|
|
1045
|
+
type: "button",
|
|
1046
|
+
className: "lks-sechead",
|
|
1047
|
+
"aria-expanded": String(open),
|
|
1048
|
+
title: open ? "折叠本章节" : `展开本章节(${section.lessons.length} 课时)`,
|
|
1049
|
+
onClick: () => {
|
|
1050
|
+
setSecOpen((m) => ({
|
|
1051
|
+
...m,
|
|
1052
|
+
[secKey]: !open
|
|
1053
|
+
}));
|
|
1054
|
+
}
|
|
1055
|
+
}, (0, react.createElement)("span", { className: "lks-sec-num" }, String(section.index + 1)), (0, react.createElement)("span", { className: "lks-sechead-t" }, section.title), open ? null : (0, react.createElement)("span", { className: "lks-sechead-n" }, `${section.lessons.length} 课`), (0, react.createElement)("span", { className: "lks-sechead-c" }, open ? "▾" : "▸")), ...open ? lessons.map((lesson) => {
|
|
931
1056
|
const locked = lesson.status === "locked" || lesson.kind === "exam" && !examAllowed;
|
|
932
|
-
return (0, react.createElement)("
|
|
1057
|
+
return (0, react.createElement)("button", {
|
|
933
1058
|
key: lesson.id,
|
|
934
|
-
|
|
1059
|
+
type: "button",
|
|
1060
|
+
className: `lks-node${lesson.focus ? " focus" : ""}`,
|
|
1061
|
+
"aria-disabled": locked || void 0,
|
|
935
1062
|
title: jumping === lesson.id ? "正在打开课时会话…" : statusTitle(lesson.kind, locked ? "locked" : lesson.status),
|
|
936
1063
|
onClick: () => {
|
|
937
1064
|
if (locked) return;
|
|
@@ -958,7 +1085,7 @@ window.__ModuleLoader__.load({
|
|
|
958
1085
|
className: "lks-pct",
|
|
959
1086
|
title: "课时掌握度 = 最薄弱知识点的掌握度"
|
|
960
1087
|
}, `${lesson.masteryPct}%`) : null);
|
|
961
|
-
})];
|
|
1088
|
+
}) : []];
|
|
962
1089
|
}), (0, react.createElement)("button", {
|
|
963
1090
|
className: "lks-btn ghost",
|
|
964
1091
|
style: { marginTop: "10px" },
|
|
@@ -1129,6 +1256,7 @@ window.__ModuleLoader__.load({
|
|
|
1129
1256
|
}
|
|
1130
1257
|
/** Button body in its own component so hook order stays constant. */
|
|
1131
1258
|
function Inner({ ctx }) {
|
|
1259
|
+
const { data, activate } = useStudy();
|
|
1132
1260
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1133
1261
|
const [error, setError] = (0, react.useState)(null);
|
|
1134
1262
|
const start = () => {
|
|
@@ -1137,6 +1265,7 @@ window.__ModuleLoader__.load({
|
|
|
1137
1265
|
setError(null);
|
|
1138
1266
|
(async () => {
|
|
1139
1267
|
try {
|
|
1268
|
+
if (data?.active !== true) await activate(true);
|
|
1140
1269
|
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
1141
1270
|
if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
|
|
1142
1271
|
const { path } = await area.json();
|
|
@@ -1169,7 +1298,7 @@ window.__ModuleLoader__.load({
|
|
|
1169
1298
|
title: "一键准备学习:建立学习工作区、开启会话并让导师就位",
|
|
1170
1299
|
disabled: busy,
|
|
1171
1300
|
onClick: start
|
|
1172
|
-
}, busy ? (0, react.createElement)(IconLoadingOutline16, { className: "lks-spin" }) : (0, react.createElement)(IconThinkOutline16, null), busy ? "正在准备学习区…" : "开始学习"), error !== null ? (0, react.createElement)("span", {
|
|
1301
|
+
}, busy ? (0, react.createElement)(IconLoadingOutline16, { className: "lks-spin" }) : (0, react.createElement)(IconThinkOutline16, null), busy ? "正在准备学习区…" : data?.active === true ? "进入学习" : "开始学习"), error !== null ? (0, react.createElement)("span", {
|
|
1173
1302
|
className: "lks-propcard-err",
|
|
1174
1303
|
style: { marginTop: 0 }
|
|
1175
1304
|
}, error) : null);
|