dsh-plugin-lookatstudy 0.22.2 → 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 +246 -19
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +155 -21
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -787,7 +787,31 @@ window.__ModuleLoader__.load({
|
|
|
787
787
|
.lks-ui .lks14-threadmenu-row.on{background:rgb(var(--brand-rgb)/0.1)}
|
|
788
788
|
.lks-ui .lks14-threadmenu-row.on .lks14-threadmenu-title{color:var(--brand);font-weight:700}
|
|
789
789
|
.lks-ui .lks14-threadmenu-dot{flex:none;width:6px;height:6px;border-radius:999px;background:var(--ink-faint)}
|
|
790
|
-
|
|
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)}
|
|
791
815
|
.lks-ui .lks14-threadmenu-row{position:relative}
|
|
792
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}
|
|
793
817
|
.lks-ui .lks14-threadmenu-row:hover .lks14-threadmenu-gear,.lks-ui .lks14-threadmenu-gear:focus-visible{opacity:1}
|
|
@@ -1850,6 +1874,19 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1850
1874
|
this.refresh();
|
|
1851
1875
|
return res;
|
|
1852
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
|
+
}
|
|
1853
1890
|
/** Delete one course (and its proposals) from the host state. */
|
|
1854
1891
|
async deleteCourse(courseId) {
|
|
1855
1892
|
await fetchJson("/lookatstudy/api/course/delete", {
|
|
@@ -2051,6 +2088,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2051
2088
|
setFocus: studyStore.setFocus.bind(studyStore),
|
|
2052
2089
|
searchLessons: studyStore.searchLessons.bind(studyStore),
|
|
2053
2090
|
tts: studyStore.tts.bind(studyStore),
|
|
2091
|
+
setCourseScope: studyStore.setCourseScope.bind(studyStore),
|
|
2054
2092
|
deleteCourse: studyStore.deleteCourse.bind(studyStore),
|
|
2055
2093
|
deleteNote: studyStore.deleteNote.bind(studyStore),
|
|
2056
2094
|
editNote: studyStore.editNote.bind(studyStore),
|
|
@@ -2255,6 +2293,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2255
2293
|
"review.banner": "正在复习:{lesson}",
|
|
2256
2294
|
"review.threadTitle": "复习:{lesson}",
|
|
2257
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": "已回答",
|
|
2258
2303
|
"meter.label": "上下文用量",
|
|
2259
2304
|
"meter.aria": "上下文已用 {pct}%",
|
|
2260
2305
|
"meter.brk.system": "系统提示",
|
|
@@ -2603,6 +2648,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2603
2648
|
"palette.kind.action": "Action",
|
|
2604
2649
|
"palette.empty": "No matches",
|
|
2605
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",
|
|
2606
2658
|
"threads.open": "{n} threads",
|
|
2607
2659
|
"threads.now": "current",
|
|
2608
2660
|
"threads.new": "New thread (your next message starts it)",
|
|
@@ -2998,6 +3050,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2998
3050
|
return out.join("\n");
|
|
2999
3051
|
}
|
|
3000
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
|
|
3001
3060
|
//#region src/vendor/mermaid-elk-rewrite.ts
|
|
3002
3061
|
/**
|
|
3003
3062
|
* mermaid-elk-rewrite —— flowchart → flowchart-elk 前缀改写(v0.21 纯函数)。
|
|
@@ -3751,10 +3810,26 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3751
3810
|
return group.threads.filter((t) => t.status !== "archived").slice().sort((a, b) => a.lastAt < b.lastAt ? 1 : -1).map((t) => ({
|
|
3752
3811
|
id: t.id,
|
|
3753
3812
|
title: t.title,
|
|
3754
|
-
current: t.id === group.active
|
|
3813
|
+
current: t.id === group.active,
|
|
3814
|
+
touchedLessons: t.touchedLessons
|
|
3755
3815
|
}));
|
|
3756
3816
|
}
|
|
3757
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
|
+
/**
|
|
3758
3833
|
* Default expansion for one rail section: collapsed when every study lesson is
|
|
3759
3834
|
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
3760
3835
|
* stays open; exam nodes never force a section open. Pure.
|
|
@@ -4873,8 +4948,20 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4873
4948
|
});
|
|
4874
4949
|
} else if (event.type === "tool/call") {
|
|
4875
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
|
+
}
|
|
4876
4963
|
rows.push({
|
|
4877
|
-
key: `t${
|
|
4964
|
+
key: `t${String(callId ?? event.seq)}`,
|
|
4878
4965
|
role: "tool",
|
|
4879
4966
|
text: name,
|
|
4880
4967
|
toolState: "loading"
|
|
@@ -4882,13 +4969,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4882
4969
|
} else if (event.type === "tool/result") {
|
|
4883
4970
|
const rid = event.data?.message?.source?.callId ?? event.data?.callId ?? event.data?.toolCallId;
|
|
4884
4971
|
if (rid !== void 0) {
|
|
4885
|
-
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"));
|
|
4886
4973
|
if (idx >= 0) {
|
|
4887
4974
|
const failed = event.data?.message?.isError === true || event.data?.error !== void 0;
|
|
4888
4975
|
rows[idx] = {
|
|
4889
4976
|
...rows[idx],
|
|
4890
4977
|
toolState: failed ? "error" : "done",
|
|
4891
|
-
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
|
+
} } : {}
|
|
4892
4983
|
};
|
|
4893
4984
|
}
|
|
4894
4985
|
}
|
|
@@ -4908,6 +4999,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4908
4999
|
else stream.text += chunk.text ?? "";
|
|
4909
5000
|
latest = attemptId;
|
|
4910
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
|
+
}
|
|
4911
5014
|
const stream = latest !== void 0 ? streams.get(latest) : void 0;
|
|
4912
5015
|
if (stream !== void 0 && !stream.settled) {
|
|
4913
5016
|
const text = stream.text.trim();
|
|
@@ -4919,6 +5022,60 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4919
5022
|
}
|
|
4920
5023
|
return rows;
|
|
4921
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
|
+
}
|
|
4922
5079
|
//#endregion
|
|
4923
5080
|
//#region src/client/error-boundary.tsx
|
|
4924
5081
|
/**
|
|
@@ -7392,6 +7549,25 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7392
7549
|
key: row.key,
|
|
7393
7550
|
className: "lks14-toolchip " + String(row.toolState ?? "loading")
|
|
7394
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
|
+
}
|
|
7395
7571
|
const cls = row.role === "streaming" ? "lks14-msg lks14-msg-assistant streaming" : "lks14-msg lks14-msg-assistant";
|
|
7396
7572
|
const body = (0, react.createElement)(ErrorBoundary, { key: `b-${row.key}` }, (0, react.createElement)("div", {
|
|
7397
7573
|
className: cls,
|
|
@@ -7444,7 +7620,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7444
7620
|
};
|
|
7445
7621
|
}
|
|
7446
7622
|
function StudyPanelBody({ ctx }) {
|
|
7447
|
-
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();
|
|
7448
7624
|
(0, react.useEffect)(() => {
|
|
7449
7625
|
wireCodeBlockCopy();
|
|
7450
7626
|
}, []);
|
|
@@ -7592,6 +7768,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7592
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 ?? []);
|
|
7593
7769
|
const localBound = (0, react.useRef)(null);
|
|
7594
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
|
+
};
|
|
7595
7780
|
const streamEl = (0, react.useRef)(null);
|
|
7596
7781
|
const touchStart = (0, react.useRef)(null);
|
|
7597
7782
|
const activeFace = (0, react.useRef)(null);
|
|
@@ -7600,7 +7785,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7600
7785
|
try {
|
|
7601
7786
|
let face = activeFace.current;
|
|
7602
7787
|
if (face === null) {
|
|
7603
|
-
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);
|
|
7604
7789
|
if (sessionId !== null && sessionKnown(ctx, sessionId)) {
|
|
7605
7790
|
const actx = ctx.sessions.scope(sessionId);
|
|
7606
7791
|
face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx) ?? null;
|
|
@@ -7636,7 +7821,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7636
7821
|
} else celebrate(ev.kind);
|
|
7637
7822
|
prevCelebrationRef.current = snapshot;
|
|
7638
7823
|
}, [data]);
|
|
7639
|
-
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);
|
|
7640
7825
|
const nudged = (0, react.useRef)(false);
|
|
7641
7826
|
(0, react.useEffect)(() => {
|
|
7642
7827
|
if (nudged.current || (data?.dueCount ?? 0) === 0) return;
|
|
@@ -7823,7 +8008,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7823
8008
|
if (data?.active !== true) await activate(true);
|
|
7824
8009
|
if (targetLessonId !== void 0 && targetLessonId !== lesson.lessonId) await setFocus(targetLessonId).catch(() => {});
|
|
7825
8010
|
const bindId = targetLessonId ?? lesson.lessonId;
|
|
7826
|
-
let sessionId = boundSessionOf(bindId) ??
|
|
8011
|
+
let sessionId = boundSessionOf(bindId) ?? threadGroupOf(bindId)?.active ?? data?.lessonSessions[bindId] ?? null;
|
|
7827
8012
|
if (sessionId !== null && !sessionKnown(ctx, sessionId)) sessionId = null;
|
|
7828
8013
|
if (sessionId === null) {
|
|
7829
8014
|
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
@@ -7836,10 +8021,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7836
8021
|
sessionId
|
|
7837
8022
|
};
|
|
7838
8023
|
await bindLessonSession(bindId, sessionId, threadTitle ?? threadAutoTitle(text));
|
|
7839
|
-
} else
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
8024
|
+
} else {
|
|
8025
|
+
localBound.current = {
|
|
8026
|
+
lessonId: bindId,
|
|
8027
|
+
sessionId
|
|
8028
|
+
};
|
|
8029
|
+
await bindLessonSession(bindId, sessionId);
|
|
8030
|
+
}
|
|
7843
8031
|
const shell = PANEL_SHELL.current;
|
|
7844
8032
|
if (shell !== null) shell.suppressHandBack(() => {
|
|
7845
8033
|
ctx.sessions.open(sessionId);
|
|
@@ -8018,6 +8206,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8018
8206
|
onThreadRename: renameLessonThreadUi,
|
|
8019
8207
|
onThreadArchive: archiveLessonThreadUi,
|
|
8020
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
|
+
},
|
|
8021
8214
|
contextMeter,
|
|
8022
8215
|
contextBreakdown: breakdown,
|
|
8023
8216
|
uploadAttachment
|
|
@@ -8981,7 +9174,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8981
9174
|
return cut <= 0 ? path : norm.slice(0, cut);
|
|
8982
9175
|
}
|
|
8983
9176
|
/** 中栏:the tutor chat stream with its own composer (upstream ChatStream + ChatComposer). */
|
|
8984
|
-
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 }) {
|
|
8985
9178
|
const stuck = (0, react.useRef)(true);
|
|
8986
9179
|
const [showFab, setShowFab] = (0, react.useState)(false);
|
|
8987
9180
|
const [threadsOpen, setThreadsOpen] = (0, react.useState)(false);
|
|
@@ -9207,7 +9400,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9207
9400
|
revealed
|
|
9208
9401
|
}));
|
|
9209
9402
|
};
|
|
9210
|
-
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);
|
|
9211
9410
|
const lastAssistant = rowsView.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
9212
9411
|
const [error, setError] = (0, react.useState)(null);
|
|
9213
9412
|
const fire = (action) => {
|
|
@@ -9298,14 +9497,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9298
9497
|
}));
|
|
9299
9498
|
send(tr("proposal.decline.msg", { lesson: proposal.lessonTitle }));
|
|
9300
9499
|
}
|
|
9301
|
-
}, 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", {
|
|
9302
9501
|
className: "lks14-threadchip",
|
|
9303
9502
|
"aria-expanded": String(threadsOpen),
|
|
9304
9503
|
"data-tooltip": tr("threads.label"),
|
|
9305
9504
|
onClick: () => {
|
|
9306
9505
|
setThreadsOpen((v) => !v);
|
|
9307
9506
|
}
|
|
9308
|
-
}, 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", {
|
|
9309
9508
|
className: "lks14-threadmenu",
|
|
9310
9509
|
role: "menu"
|
|
9311
9510
|
}, ...threadPills.map((p) => (0, react.createElement)("div", {
|
|
@@ -9350,7 +9549,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9350
9549
|
if (v !== "" && v !== p.title) onThreadRename(p.id, v);
|
|
9351
9550
|
setRenaming(null);
|
|
9352
9551
|
}
|
|
9353
|
-
}) : (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", {
|
|
9354
9556
|
className: "lks14-threadmenu-gear",
|
|
9355
9557
|
"aria-label": tr("threads.actions"),
|
|
9356
9558
|
"data-tooltip": tr("threads.actions"),
|
|
@@ -9393,6 +9595,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9393
9595
|
setGearFor(null);
|
|
9394
9596
|
}
|
|
9395
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", {
|
|
9396
9610
|
key: "__new__",
|
|
9397
9611
|
className: "lks14-threadmenu-row new",
|
|
9398
9612
|
role: "menuitem",
|
|
@@ -9427,7 +9641,20 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9427
9641
|
onClick: () => {
|
|
9428
9642
|
send(starters[0]?.message ?? tr("prompt.start"));
|
|
9429
9643
|
}
|
|
9430
|
-
}, 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" ? {
|
|
9431
9658
|
playing: msgAudio?.key === row.key,
|
|
9432
9659
|
index: msgAudio?.index ?? 0,
|
|
9433
9660
|
total: msgAudio?.total ?? 0,
|