dsh-plugin-lookatstudy 0.22.2 → 0.24.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 +389 -33
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +429 -60
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -156,6 +156,7 @@ window.__ModuleLoader__.load({
|
|
|
156
156
|
.lks-ui .lks14-viewtab:hover{color:var(--ink)}
|
|
157
157
|
.lks-ui .lks14-viewtab.on{background:rgb(var(--brand-rgb)/0.16);color:var(--brand)}
|
|
158
158
|
.lks-ui .lks14-prose{color:var(--ink);font-size:.9375rem;line-height:1.65}
|
|
159
|
+
.lks-ui .lks14-prose img{max-width:100%;height:auto}
|
|
159
160
|
.lks-ui .lks14-prose h1,.lks-ui .lks-note-text h1{font-size:1.25rem;font-weight:800;color:var(--ink-strong)}
|
|
160
161
|
.lks-ui .lks14-prose h2,.lks-ui .lks-note-text h2{font-size:1.1rem;font-weight:700;color:var(--ink-strong);border-bottom:1px solid var(--border);padding-bottom:.3em}
|
|
161
162
|
.lks-ui .lks14-prose h3,.lks-ui .lks-note-text h3{font-size:1rem;font-weight:700;color:var(--ink)}
|
|
@@ -434,9 +435,27 @@ window.__ModuleLoader__.load({
|
|
|
434
435
|
|
|
435
436
|
/* ══════════ P11b (C-track closure): chat parts, notes closure, tooltip, confirm ══════════ */
|
|
436
437
|
/* C14: the collapsible reasoning block */
|
|
438
|
+
/* 0.24.0: the host-style thinking disclosure (ReasoningRow port) — running
|
|
439
|
+
rows stream the latest line follow-end with the sweep, settled rows lead
|
|
440
|
+
with the first line, the expanded body reads the tertiary tier */
|
|
437
441
|
.lks-ui .lks14-reasoning{border:1px solid var(--border-faint);border-radius:10px;padding:4px 10px;font-size:12px;color:var(--ink-faint)}
|
|
438
442
|
.lks-ui .lks14-reasoning summary{cursor:pointer;font-weight:600;color:var(--ink-muted);user-select:none}
|
|
439
443
|
.lks-ui .lks14-reasoning[open] summary{margin-bottom:4px}
|
|
444
|
+
.lks-ui .lks14-reasoning-row{display:flex;align-items:center;gap:6px;min-width:0;position:relative;overflow:hidden}
|
|
445
|
+
.lks-ui .lks14-reasoning-title{flex:none;font-weight:400;color:var(--ink-muted)}
|
|
446
|
+
.lks-ui .lks14-reasoning-sep{flex:none;width:2px;height:2px;margin:0 4px;border-radius:1px;background:var(--ink-faint)}
|
|
447
|
+
.lks-ui .lks14-reasoning-summary{min-width:0;overflow:hidden;flex:1 1 auto;color:var(--ink-faint);white-space:nowrap}
|
|
448
|
+
.lks-ui .lks14-reasoning-sumtext{display:block;overflow:hidden;text-overflow:ellipsis}
|
|
449
|
+
.lks-ui .lks14-reasoning-summary[data-follow-end]{display:flex;justify-content:flex-end}
|
|
450
|
+
.lks-ui .lks14-reasoning-summary[data-follow-end] .lks14-reasoning-sumtext{flex:0 0 auto;width:max-content;min-width:100%;overflow:visible;text-align:start;text-overflow:clip}
|
|
451
|
+
.lks-ui .lks14-reasoning[data-state='running'] .lks14-reasoning-row::after{content:'';position:absolute;inset-block:0;left:0;width:180px;background:linear-gradient(90deg,transparent 0%,rgb(var(--surface-2-rgb)/0.9) 55%,transparent 100%);animation:lks-reasoning-sweep 2.6s ease-out infinite;pointer-events:none}
|
|
452
|
+
@keyframes lks-reasoning-sweep{0%{left:-180px}90%,100%{left:100%}}
|
|
453
|
+
@media (prefers-reduced-motion: reduce){.lks-ui .lks14-reasoning[data-state='running'] .lks14-reasoning-row::after{animation:none}}
|
|
454
|
+
.lks-ui .lks14-reasoning.open .lks14-reasoning-body,.lks-ui .lks14-reasoning[open] .lks14-reasoning-body{padding:4px 0 4px 20px;color:var(--ink-faint);white-space:pre-wrap;word-break:break-word}
|
|
455
|
+
/* 0.24.0: the teach-tab 原文/对照/译文 segmented control */
|
|
456
|
+
.lks-ui .lks-langview{display:inline-flex;gap:2px;padding:2px;border:1px solid var(--border-faint);border-radius:8px;background:rgb(var(--surface-2-rgb)/0.6)}
|
|
457
|
+
.lks-ui .lks-langview button{border:0;background:transparent;color:var(--ink-muted);font-size:12px;line-height:1;padding:4px 8px;border-radius:6px;cursor:pointer}
|
|
458
|
+
.lks-ui .lks-langview button.on{background:rgb(var(--brand-rgb)/0.16);color:var(--brand)}
|
|
440
459
|
/* C14: the tool-call chip states */
|
|
441
460
|
.lks-ui .lks14-toolchip{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;font-size:12px;font-weight:600;
|
|
442
461
|
padding:3px 10px;border-radius:999px;background:rgb(var(--ink-rgb)/0.05);color:var(--ink-muted)}
|
|
@@ -787,7 +806,31 @@ window.__ModuleLoader__.load({
|
|
|
787
806
|
.lks-ui .lks14-threadmenu-row.on{background:rgb(var(--brand-rgb)/0.1)}
|
|
788
807
|
.lks-ui .lks14-threadmenu-row.on .lks14-threadmenu-title{color:var(--brand);font-weight:700}
|
|
789
808
|
.lks-ui .lks14-threadmenu-dot{flex:none;width:6px;height:6px;border-radius:999px;background:var(--ink-faint)}
|
|
790
|
-
|
|
809
|
+
/* 0.23.0 course scope: the coverage line under a course thread's title, and
|
|
810
|
+
the granularity toggle row pinned above the +新建 row */
|
|
811
|
+
.lks-ui .lks14-threadmenu-title{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
|
|
812
|
+
.lks-ui .lks14-threadmenu-cover{font-size:10.5px;color:var(--ink-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
813
|
+
.lks-ui .lks14-threadmenu-row.scope{margin-top:3px;border-top:1px solid var(--border-faint);border-radius:0;padding-top:7px}
|
|
814
|
+
.lks-ui .lks14-threadmenu-scope-icon{flex:none;font-size:12px;line-height:1}
|
|
815
|
+
.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)}
|
|
816
|
+
.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)}
|
|
817
|
+
.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}
|
|
818
|
+
/* 0.23.0: the ask_user_question answer row — the panel's own answer UI for
|
|
819
|
+
the host's pending clarifying question (the hidden host column used to be
|
|
820
|
+
the only place its options lived) */
|
|
821
|
+
.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)}
|
|
822
|
+
.lks-ui .lks14-askrow.answered{opacity:.62;border-color:var(--border-faint);background:none}
|
|
823
|
+
.lks-ui .lks14-askhead{font-size:12px;font-weight:800;color:var(--brand)}
|
|
824
|
+
.lks-ui .lks14-askrow.answered .lks14-askhead{color:var(--ink-muted)}
|
|
825
|
+
.lks-ui .lks14-askq{display:flex;flex-direction:column;gap:6px}
|
|
826
|
+
.lks-ui .lks14-askq-h{font-size:13px;font-weight:700;color:var(--ink)}
|
|
827
|
+
.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}
|
|
828
|
+
.lks-ui .lks14-askopt:hover:not(:disabled){border-color:rgb(var(--brand-rgb)/0.55);background:rgb(var(--brand-rgb)/0.07)}
|
|
829
|
+
.lks-ui .lks14-askopt:disabled{cursor:default;opacity:.75}
|
|
830
|
+
.lks-ui .lks14-askopt-label{font-size:12.5px;font-weight:600;color:var(--ink)}
|
|
831
|
+
.lks-ui .lks14-askopt-desc{font-size:11px;color:var(--ink-muted);line-height:1.5}
|
|
832
|
+
.lks-ui .lks14-askfree{font-size:12px;color:var(--ink-muted)}
|
|
833
|
+
.lks-ui .lks14-askdone{font-size:11px;color:var(--ink-faint)}
|
|
791
834
|
.lks-ui .lks14-threadmenu-row{position:relative}
|
|
792
835
|
.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
836
|
.lks-ui .lks14-threadmenu-row:hover .lks14-threadmenu-gear,.lks-ui .lks14-threadmenu-gear:focus-visible{opacity:1}
|
|
@@ -1778,6 +1821,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1778
1821
|
});
|
|
1779
1822
|
this.refresh();
|
|
1780
1823
|
}
|
|
1824
|
+
/** 0.24.0: push the host interface language (the panel reads <html lang>,
|
|
1825
|
+
* which the host keeps in sync with the active locale) — the apply-side
|
|
1826
|
+
* translation matching consumes it. */
|
|
1827
|
+
async setInterfaceLang(lang) {
|
|
1828
|
+
await fetchJson("/lookatstudy/api/prefs", {
|
|
1829
|
+
method: "POST",
|
|
1830
|
+
headers: { "content-type": "application/json" },
|
|
1831
|
+
body: JSON.stringify({ interfaceLang: lang })
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1781
1834
|
/** E3: land one attachment verbatim in the study workspace; returns the workspace-relative path. */
|
|
1782
1835
|
async uploadAttachment(name, dataBase64) {
|
|
1783
1836
|
const body = await fetchJson("/lookatstudy/api/attachment", {
|
|
@@ -1850,6 +1903,19 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1850
1903
|
this.refresh();
|
|
1851
1904
|
return res;
|
|
1852
1905
|
}
|
|
1906
|
+
/** Flip a course's thread granularity (0.23.0, issue #11 follow-up). Never
|
|
1907
|
+
* touches sediment — both sides' groups stay and are resumed on flip. */
|
|
1908
|
+
async setCourseScope(courseId, scope) {
|
|
1909
|
+
await fetchJson("/lookatstudy/api/course-scope", {
|
|
1910
|
+
method: "POST",
|
|
1911
|
+
headers: { "content-type": "application/json" },
|
|
1912
|
+
body: JSON.stringify({
|
|
1913
|
+
courseId,
|
|
1914
|
+
scope
|
|
1915
|
+
})
|
|
1916
|
+
});
|
|
1917
|
+
this.refresh();
|
|
1918
|
+
}
|
|
1853
1919
|
/** Delete one course (and its proposals) from the host state. */
|
|
1854
1920
|
async deleteCourse(courseId) {
|
|
1855
1921
|
await fetchJson("/lookatstudy/api/course/delete", {
|
|
@@ -2047,10 +2113,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2047
2113
|
activate: studyStore.activate.bind(studyStore),
|
|
2048
2114
|
setMode: studyStore.setMode.bind(studyStore),
|
|
2049
2115
|
setHistoryBudget: studyStore.setHistoryBudget.bind(studyStore),
|
|
2116
|
+
setInterfaceLang: studyStore.setInterfaceLang.bind(studyStore),
|
|
2050
2117
|
uploadAttachment: studyStore.uploadAttachment.bind(studyStore),
|
|
2051
2118
|
setFocus: studyStore.setFocus.bind(studyStore),
|
|
2052
2119
|
searchLessons: studyStore.searchLessons.bind(studyStore),
|
|
2053
2120
|
tts: studyStore.tts.bind(studyStore),
|
|
2121
|
+
setCourseScope: studyStore.setCourseScope.bind(studyStore),
|
|
2054
2122
|
deleteCourse: studyStore.deleteCourse.bind(studyStore),
|
|
2055
2123
|
deleteNote: studyStore.deleteNote.bind(studyStore),
|
|
2056
2124
|
editNote: studyStore.editNote.bind(studyStore),
|
|
@@ -2255,6 +2323,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2255
2323
|
"review.banner": "正在复习:{lesson}",
|
|
2256
2324
|
"review.threadTitle": "复习:{lesson}",
|
|
2257
2325
|
"threads.label": "课时线程",
|
|
2326
|
+
"threads.scope": "跨课时连续对话",
|
|
2327
|
+
"threads.scope.on": "已开启",
|
|
2328
|
+
"threads.scope.off": "未开启",
|
|
2329
|
+
"threads.coverage": "覆盖",
|
|
2330
|
+
"ask.title": "导师想先确认一下",
|
|
2331
|
+
"ask.free": "在下方输入框直接回复即可",
|
|
2332
|
+
"ask.answered": "已回答",
|
|
2258
2333
|
"meter.label": "上下文用量",
|
|
2259
2334
|
"meter.aria": "上下文已用 {pct}%",
|
|
2260
2335
|
"meter.brk.system": "系统提示",
|
|
@@ -2283,6 +2358,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2283
2358
|
"rail.due.start": "开始复习",
|
|
2284
2359
|
"rail.due.none": "暂时没有到期的复习,保持节奏!",
|
|
2285
2360
|
"chat.reasoning": "思考过程({n} 字)",
|
|
2361
|
+
"chat.think": "思考",
|
|
2362
|
+
"langview.original": "原文",
|
|
2363
|
+
"langview.bilingual": "对照",
|
|
2364
|
+
"langview.translation": "译文",
|
|
2286
2365
|
"msg.speak": "朗读",
|
|
2287
2366
|
"note.pin": "置顶笔记",
|
|
2288
2367
|
"note.unpin": "取消置顶",
|
|
@@ -2603,6 +2682,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2603
2682
|
"palette.kind.action": "Action",
|
|
2604
2683
|
"palette.empty": "No matches",
|
|
2605
2684
|
"threads.label": "Lesson threads",
|
|
2685
|
+
"threads.scope": "Continuous course thread",
|
|
2686
|
+
"threads.scope.on": "on",
|
|
2687
|
+
"threads.scope.off": "off",
|
|
2688
|
+
"threads.coverage": "covers",
|
|
2689
|
+
"ask.title": "The tutor wants a quick confirm",
|
|
2690
|
+
"ask.free": "Answer in the composer below",
|
|
2691
|
+
"ask.answered": "answered",
|
|
2606
2692
|
"threads.open": "{n} threads",
|
|
2607
2693
|
"threads.now": "current",
|
|
2608
2694
|
"threads.new": "New thread (your next message starts it)",
|
|
@@ -2643,6 +2729,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2643
2729
|
"rail.due.start": "Start reviewing",
|
|
2644
2730
|
"rail.due.none": "Nothing due right now — keep the streak!",
|
|
2645
2731
|
"chat.reasoning": "Thinking ({n} chars)",
|
|
2732
|
+
"chat.think": "Think",
|
|
2733
|
+
"langview.original": "Original",
|
|
2734
|
+
"langview.bilingual": "Both",
|
|
2735
|
+
"langview.translation": "Translation",
|
|
2646
2736
|
"msg.speak": "Speak",
|
|
2647
2737
|
"note.pin": "Pin note",
|
|
2648
2738
|
"note.unpin": "Unpin",
|
|
@@ -2998,6 +3088,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2998
3088
|
return out.join("\n");
|
|
2999
3089
|
}
|
|
3000
3090
|
//#endregion
|
|
3091
|
+
//#region src/thread-key.ts
|
|
3092
|
+
/** Resolve the thread-group map key for a send from `lessonId`. Absent scope
|
|
3093
|
+
* is legacy lesson behavior — old files never move. */
|
|
3094
|
+
function threadOwnerKey(scope, courseId, lessonId) {
|
|
3095
|
+
return scope === "course" ? `course:${courseId}` : lessonId;
|
|
3096
|
+
}
|
|
3097
|
+
//#endregion
|
|
3001
3098
|
//#region src/vendor/mermaid-elk-rewrite.ts
|
|
3002
3099
|
/**
|
|
3003
3100
|
* mermaid-elk-rewrite —— flowchart → flowchart-elk 前缀改写(v0.21 纯函数)。
|
|
@@ -3634,6 +3731,22 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3634
3731
|
return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
|
|
3635
3732
|
}
|
|
3636
3733
|
/**
|
|
3734
|
+
* The thinking disclosure's collapsed one-line summary (0.24.0, the host
|
|
3735
|
+
* ReasoningRow contract verbatim): while RUNNING the LAST line rides (the
|
|
3736
|
+
* summary visibly follows the stream's tail), settled the FIRST line leads;
|
|
3737
|
+
* double-asterisk markers are omitted either way. Pure.
|
|
3738
|
+
*/
|
|
3739
|
+
function reasoningSummaryLine(text, running) {
|
|
3740
|
+
return (running ? (() => {
|
|
3741
|
+
const v = text.trimEnd();
|
|
3742
|
+
const nl = v.lastIndexOf("\n");
|
|
3743
|
+
return nl === -1 ? v : v.slice(nl + 1);
|
|
3744
|
+
})() : (() => {
|
|
3745
|
+
const nl = text.indexOf("\n");
|
|
3746
|
+
return nl === -1 ? text : text.slice(0, nl);
|
|
3747
|
+
})()).replaceAll("**", "");
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3637
3750
|
* Tooltip text for one course-tree status glyph. Pure (the translator is an
|
|
3638
3751
|
* optional parameter defaulting to the active one, evaluated per call).
|
|
3639
3752
|
*/
|
|
@@ -3751,10 +3864,26 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3751
3864
|
return group.threads.filter((t) => t.status !== "archived").slice().sort((a, b) => a.lastAt < b.lastAt ? 1 : -1).map((t) => ({
|
|
3752
3865
|
id: t.id,
|
|
3753
3866
|
title: t.title,
|
|
3754
|
-
current: t.id === group.active
|
|
3867
|
+
current: t.id === group.active,
|
|
3868
|
+
touchedLessons: t.touchedLessons
|
|
3755
3869
|
}));
|
|
3756
3870
|
}
|
|
3757
3871
|
/**
|
|
3872
|
+
* 0.23.0 course scope: the coverage label under a course-scoped thread — the
|
|
3873
|
+
* lessons it has been sent from, by section order, capped at three titles
|
|
3874
|
+
* with an 等 N 课时 tail. Empty/undefined coverage renders '' (lesson-scoped
|
|
3875
|
+
* threads show nothing). Pure.
|
|
3876
|
+
*/
|
|
3877
|
+
function threadCoverageLabel(touched, courses) {
|
|
3878
|
+
if (touched === void 0 || touched.length === 0) return "";
|
|
3879
|
+
const titleOf = /* @__PURE__ */ new Map();
|
|
3880
|
+
for (const c of courses) for (const s of c.sections) for (const l of s.lessons) titleOf.set(l.id, l.title);
|
|
3881
|
+
const titles = touched.map((id) => titleOf.get(id)).filter((t) => t !== void 0);
|
|
3882
|
+
if (titles.length === 0) return "";
|
|
3883
|
+
const head = titles.slice(0, 3).join(" · ");
|
|
3884
|
+
return titles.length > 3 ? `${head} 等 ${String(titles.length)} 课时` : head;
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3758
3887
|
* Default expansion for one rail section: collapsed when every study lesson is
|
|
3759
3888
|
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
3760
3889
|
* stays open; exam nodes never force a section open. Pure.
|
|
@@ -4680,6 +4809,27 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4680
4809
|
*/
|
|
4681
4810
|
const TURN_STALL_MS = 24e4;
|
|
4682
4811
|
/**
|
|
4812
|
+
* 0.23.1 issue 3: how long an import funnel may sit with NO live turn before
|
|
4813
|
+
* the panel reads the import as failed. The old failure exit armed on the
|
|
4814
|
+
* turnActive prop having rendered true once — but a turn that dies inside one
|
|
4815
|
+
* event-window notification batch (turn/start + turn/end between two reads,
|
|
4816
|
+
* the keyless-provider fast fail; journal-verified: attempt → step/end →
|
|
4817
|
+
* turn/end within seconds) never flips the prop, so the exit never armed and
|
|
4818
|
+
* the funnel spun forever. This deadline needs no observation: past grace
|
|
4819
|
+
* with the turn not active = failed. The grace only covers prompt queue
|
|
4820
|
+
* latency (turn/start journals within seconds of the queue-time resolve); a
|
|
4821
|
+
* live turn holds the funnel open for its whole life however long the import
|
|
4822
|
+
* tools run.
|
|
4823
|
+
*/
|
|
4824
|
+
const IMPORT_FAIL_GRACE_MS = 3e4;
|
|
4825
|
+
/**
|
|
4826
|
+
* The funnel's failure decision (pure — the wall clock lives at the poll call
|
|
4827
|
+
* site, mirroring {@link turnStalled}).
|
|
4828
|
+
*/
|
|
4829
|
+
function importJobDead(job, turnActive, now, grace = IMPORT_FAIL_GRACE_MS) {
|
|
4830
|
+
return job !== null && !turnActive && now - job.startedAt > grace;
|
|
4831
|
+
}
|
|
4832
|
+
/**
|
|
4683
4833
|
* The stall decision (pure — the wall clock lives at the poll call site):
|
|
4684
4834
|
* an ACTIVE fold stays active while the window's top seq keeps advancing;
|
|
4685
4835
|
* once it has been frozen for longer than stallMs the panel treats the turn
|
|
@@ -4873,8 +5023,20 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4873
5023
|
});
|
|
4874
5024
|
} else if (event.type === "tool/call") {
|
|
4875
5025
|
const name = event.data?.tool?.name ?? event.data?.name ?? "tool";
|
|
5026
|
+
const callId = event.data?.callId ?? event.data?.toolCallId;
|
|
5027
|
+
if (name === "ask_user_question") {
|
|
5028
|
+
const ask = parseAskArguments(event.data?.arguments, callId ?? String(event.seq));
|
|
5029
|
+
rows.push({
|
|
5030
|
+
key: `t${String(callId ?? event.seq)}`,
|
|
5031
|
+
role: "ask",
|
|
5032
|
+
text: ask.summary,
|
|
5033
|
+
toolState: "loading",
|
|
5034
|
+
ask
|
|
5035
|
+
});
|
|
5036
|
+
continue;
|
|
5037
|
+
}
|
|
4876
5038
|
rows.push({
|
|
4877
|
-
key: `t${
|
|
5039
|
+
key: `t${String(callId ?? event.seq)}`,
|
|
4878
5040
|
role: "tool",
|
|
4879
5041
|
text: name,
|
|
4880
5042
|
toolState: "loading"
|
|
@@ -4882,13 +5044,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4882
5044
|
} else if (event.type === "tool/result") {
|
|
4883
5045
|
const rid = event.data?.message?.source?.callId ?? event.data?.callId ?? event.data?.toolCallId;
|
|
4884
5046
|
if (rid !== void 0) {
|
|
4885
|
-
const idx = rows.findIndex((r) => r.key === `t${rid}` && r.role === "tool");
|
|
5047
|
+
const idx = rows.findIndex((r) => r.key === `t${rid}` && (r.role === "tool" || r.role === "ask"));
|
|
4886
5048
|
if (idx >= 0) {
|
|
4887
5049
|
const failed = event.data?.message?.isError === true || event.data?.error !== void 0;
|
|
4888
5050
|
rows[idx] = {
|
|
4889
5051
|
...rows[idx],
|
|
4890
5052
|
toolState: failed ? "error" : "done",
|
|
4891
|
-
resultText: failed ? void 0 : resultRenderedText(event)
|
|
5053
|
+
resultText: failed ? void 0 : resultRenderedText(event),
|
|
5054
|
+
...rows[idx].role === "ask" && rows[idx].ask !== void 0 && !failed ? { ask: {
|
|
5055
|
+
...rows[idx].ask,
|
|
5056
|
+
answered: true
|
|
5057
|
+
} } : {}
|
|
4892
5058
|
};
|
|
4893
5059
|
}
|
|
4894
5060
|
}
|
|
@@ -4897,17 +5063,32 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4897
5063
|
continue;
|
|
4898
5064
|
}
|
|
4899
5065
|
const chunk = event.data?.chunk;
|
|
4900
|
-
if (chunk === void 0 || chunk.type !== "text-delta") continue;
|
|
5066
|
+
if (chunk === void 0 || chunk.type !== "text-delta" && chunk.type !== "reasoning-delta") continue;
|
|
4901
5067
|
const attemptId = event.data?.attemptId ?? String(event.seq);
|
|
5068
|
+
const delta = chunk.text ?? "";
|
|
4902
5069
|
const stream = streams.get(attemptId);
|
|
4903
5070
|
if (stream === void 0) streams.set(attemptId, {
|
|
4904
|
-
text: chunk.text
|
|
5071
|
+
text: chunk.type === "text-delta" ? delta : "",
|
|
5072
|
+
reasoning: chunk.type === "reasoning-delta" ? delta : "",
|
|
4905
5073
|
anchor: rows.length,
|
|
4906
5074
|
settled: false
|
|
4907
5075
|
});
|
|
4908
|
-
else
|
|
5076
|
+
else if (chunk.type === "text-delta") stream.text += delta;
|
|
5077
|
+
else stream.reasoning += delta;
|
|
4909
5078
|
latest = attemptId;
|
|
4910
5079
|
}
|
|
5080
|
+
let askOpen = false;
|
|
5081
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
5082
|
+
const r = rows[i];
|
|
5083
|
+
if (r.role === "user") askOpen = true;
|
|
5084
|
+
else if (r.role === "ask" && r.ask !== void 0 && askOpen && !r.ask.answered) rows[i] = {
|
|
5085
|
+
...r,
|
|
5086
|
+
ask: {
|
|
5087
|
+
...r.ask,
|
|
5088
|
+
answered: true
|
|
5089
|
+
}
|
|
5090
|
+
};
|
|
5091
|
+
}
|
|
4911
5092
|
const stream = latest !== void 0 ? streams.get(latest) : void 0;
|
|
4912
5093
|
if (stream !== void 0 && !stream.settled) {
|
|
4913
5094
|
const text = stream.text.trim();
|
|
@@ -4916,9 +5097,70 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4916
5097
|
role: "streaming",
|
|
4917
5098
|
text
|
|
4918
5099
|
});
|
|
5100
|
+
const reasoning = stream.reasoning.trim();
|
|
5101
|
+
if (reasoning !== "") rows.splice(stream.anchor, 0, {
|
|
5102
|
+
key: "streaming-reasoning",
|
|
5103
|
+
role: "reasoning",
|
|
5104
|
+
text: reasoning,
|
|
5105
|
+
running: true
|
|
5106
|
+
});
|
|
4919
5107
|
}
|
|
4920
5108
|
return rows;
|
|
4921
5109
|
}
|
|
5110
|
+
/**
|
|
5111
|
+
* Parse an ask_user_question call's arguments into the answerable row payload
|
|
5112
|
+
* (0.23.0). The live journal carries a JSON STRING (verified against real
|
|
5113
|
+
* journals 2026-09-14: {questions:[{header,id,multi_select,options:[{label,
|
|
5114
|
+
* description}]}]}); tolerate an already-parsed object arm for robustness.
|
|
5115
|
+
* Unparseable shapes degrade to a header-only row (the composer still answers
|
|
5116
|
+
* in free text). Pure.
|
|
5117
|
+
*/
|
|
5118
|
+
function parseAskArguments(raw, callId) {
|
|
5119
|
+
const empty = {
|
|
5120
|
+
callId,
|
|
5121
|
+
summary: "",
|
|
5122
|
+
questions: [],
|
|
5123
|
+
answered: false
|
|
5124
|
+
};
|
|
5125
|
+
if (raw === void 0 || raw === "") return empty;
|
|
5126
|
+
let parsed;
|
|
5127
|
+
try {
|
|
5128
|
+
parsed = JSON.parse(raw);
|
|
5129
|
+
} catch {
|
|
5130
|
+
return empty;
|
|
5131
|
+
}
|
|
5132
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.questions)) return empty;
|
|
5133
|
+
const questions = [];
|
|
5134
|
+
for (const q of parsed.questions) {
|
|
5135
|
+
if (typeof q !== "object" || q === null) continue;
|
|
5136
|
+
const rec = q;
|
|
5137
|
+
const id = typeof rec.id === "string" ? rec.id : String(questions.length);
|
|
5138
|
+
const header = typeof rec.header === "string" && rec.header !== "" ? rec.header : typeof rec.question === "string" ? rec.question : "";
|
|
5139
|
+
const multiSelect = rec.multi_select === true;
|
|
5140
|
+
const options = [];
|
|
5141
|
+
if (Array.isArray(rec.options)) for (const o of rec.options) {
|
|
5142
|
+
if (typeof o !== "object" || o === null) continue;
|
|
5143
|
+
const orec = o;
|
|
5144
|
+
if (typeof orec.label === "string" && orec.label !== "") options.push({
|
|
5145
|
+
label: orec.label,
|
|
5146
|
+
description: typeof orec.description === "string" ? orec.description : ""
|
|
5147
|
+
});
|
|
5148
|
+
}
|
|
5149
|
+
questions.push({
|
|
5150
|
+
id,
|
|
5151
|
+
header,
|
|
5152
|
+
multiSelect,
|
|
5153
|
+
options
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
5156
|
+
if (questions.length === 0) return empty;
|
|
5157
|
+
return {
|
|
5158
|
+
callId,
|
|
5159
|
+
summary: questions.map((q) => q.header).filter((h) => h !== "").join(" / "),
|
|
5160
|
+
questions,
|
|
5161
|
+
answered: false
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
4922
5164
|
//#endregion
|
|
4923
5165
|
//#region src/client/error-boundary.tsx
|
|
4924
5166
|
/**
|
|
@@ -7384,14 +7626,44 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7384
7626
|
key: row.key,
|
|
7385
7627
|
className: "lks14-msg lks14-msg-user"
|
|
7386
7628
|
}, row.text);
|
|
7387
|
-
if (row.role === "reasoning")
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7629
|
+
if (row.role === "reasoning") {
|
|
7630
|
+
const running = row.running === true;
|
|
7631
|
+
return (0, react.createElement)("details", {
|
|
7632
|
+
key: row.key,
|
|
7633
|
+
className: "lks14-reasoning",
|
|
7634
|
+
"data-state": running ? "running" : "ok",
|
|
7635
|
+
"data-testid": "reasoning-row"
|
|
7636
|
+
}, (0, react.createElement)("summary", { className: "lks14-reasoning-row" }, (0, react.createElement)(IconThinkOutline16, { size: 14 }), (0, react.createElement)("span", { className: "lks14-reasoning-title" }, tr("chat.think")), (0, react.createElement)("span", {
|
|
7637
|
+
className: "lks14-reasoning-sep",
|
|
7638
|
+
"aria-hidden": true
|
|
7639
|
+
}), (0, react.createElement)("span", {
|
|
7640
|
+
className: "lks14-reasoning-summary",
|
|
7641
|
+
"data-follow-end": running || void 0
|
|
7642
|
+
}, (0, react.createElement)("span", { className: "lks14-reasoning-sumtext" }, reasoningSummaryLine(row.text, running)))), (0, react.createElement)("div", { className: "lks14-reasoning-body" }, row.text));
|
|
7643
|
+
}
|
|
7391
7644
|
if (row.role === "tool") return (0, react.createElement)("div", {
|
|
7392
7645
|
key: row.key,
|
|
7393
7646
|
className: "lks14-toolchip " + String(row.toolState ?? "loading")
|
|
7394
7647
|
}, row.toolState === "done" ? "✓" : row.toolState === "error" ? "×" : (0, react.createElement)("i", null), (0, react.createElement)("span", null, row.text));
|
|
7648
|
+
if (row.role === "ask" && row.ask !== void 0) {
|
|
7649
|
+
const answered = row.ask.answered;
|
|
7650
|
+
return (0, react.createElement)("div", {
|
|
7651
|
+
key: row.key,
|
|
7652
|
+
className: `lks14-askrow${answered ? " answered" : ""}`,
|
|
7653
|
+
"data-testid": "ask-row"
|
|
7654
|
+
}, (0, react.createElement)("div", { className: "lks14-askhead" }, tr("ask.title")), ...row.ask.questions.map((q) => (0, react.createElement)("div", {
|
|
7655
|
+
key: q.id,
|
|
7656
|
+
className: "lks14-askq"
|
|
7657
|
+
}, q.header !== "" ? (0, react.createElement)("div", { className: "lks14-askq-h" }, q.header) : null, ...q.options.map((opt) => (0, react.createElement)("button", {
|
|
7658
|
+
key: opt.label,
|
|
7659
|
+
className: "lks14-askopt",
|
|
7660
|
+
disabled: answered || interactive?.answerAsk === void 0,
|
|
7661
|
+
"data-testid": "ask-option",
|
|
7662
|
+
onClick: () => {
|
|
7663
|
+
interactive?.answerAsk?.(opt.label);
|
|
7664
|
+
}
|
|
7665
|
+
}, (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);
|
|
7666
|
+
}
|
|
7395
7667
|
const cls = row.role === "streaming" ? "lks14-msg lks14-msg-assistant streaming" : "lks14-msg lks14-msg-assistant";
|
|
7396
7668
|
const body = (0, react.createElement)(ErrorBoundary, { key: `b-${row.key}` }, (0, react.createElement)("div", {
|
|
7397
7669
|
className: cls,
|
|
@@ -7444,7 +7716,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7444
7716
|
};
|
|
7445
7717
|
}
|
|
7446
7718
|
function StudyPanelBody({ ctx }) {
|
|
7447
|
-
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, uploadAttachment } = useStudy();
|
|
7719
|
+
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, setCourseScope, uploadAttachment, setInterfaceLang } = useStudy();
|
|
7720
|
+
(0, react.useEffect)(() => {
|
|
7721
|
+
const read = () => {
|
|
7722
|
+
const lang = document.documentElement.lang;
|
|
7723
|
+
if (/^[a-z]{2,3}(-[A-Za-z0-9]+)*$/.test(lang)) setInterfaceLang(lang).catch(() => {});
|
|
7724
|
+
};
|
|
7725
|
+
read();
|
|
7726
|
+
return ctx.locale?.subscribe(read);
|
|
7727
|
+
}, []);
|
|
7448
7728
|
(0, react.useEffect)(() => {
|
|
7449
7729
|
wireCodeBlockCopy();
|
|
7450
7730
|
}, []);
|
|
@@ -7592,6 +7872,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7592
7872
|
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
7873
|
const localBound = (0, react.useRef)(null);
|
|
7594
7874
|
const boundSessionOf = (lessonId) => lessonId !== null && localBound.current?.lessonId === lessonId ? localBound.current.sessionId : null;
|
|
7875
|
+
const threadKeyOf = (lessonId) => {
|
|
7876
|
+
if (lessonId === null || data === null) return null;
|
|
7877
|
+
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);
|
|
7878
|
+
return null;
|
|
7879
|
+
};
|
|
7880
|
+
const threadGroupOf = (lessonId) => {
|
|
7881
|
+
const key = threadKeyOf(lessonId);
|
|
7882
|
+
return key !== null ? data?.lessonThreads[key] ?? null : null;
|
|
7883
|
+
};
|
|
7595
7884
|
const streamEl = (0, react.useRef)(null);
|
|
7596
7885
|
const touchStart = (0, react.useRef)(null);
|
|
7597
7886
|
const activeFace = (0, react.useRef)(null);
|
|
@@ -7600,7 +7889,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7600
7889
|
try {
|
|
7601
7890
|
let face = activeFace.current;
|
|
7602
7891
|
if (face === null) {
|
|
7603
|
-
const sessionId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? (lesson !== null ?
|
|
7892
|
+
const sessionId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? threadGroupOf(lesson !== null ? lesson.lessonId : null)?.active ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7604
7893
|
if (sessionId !== null && sessionKnown(ctx, sessionId)) {
|
|
7605
7894
|
const actx = ctx.sessions.scope(sessionId);
|
|
7606
7895
|
face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx) ?? null;
|
|
@@ -7636,7 +7925,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7636
7925
|
} else celebrate(ev.kind);
|
|
7637
7926
|
prevCelebrationRef.current = snapshot;
|
|
7638
7927
|
}, [data]);
|
|
7639
|
-
const boundId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? (lesson !== null ?
|
|
7928
|
+
const boundId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? threadGroupOf(lesson !== null ? lesson.lessonId : null)?.active ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7640
7929
|
const nudged = (0, react.useRef)(false);
|
|
7641
7930
|
(0, react.useEffect)(() => {
|
|
7642
7931
|
if (nudged.current || (data?.dueCount ?? 0) === 0) return;
|
|
@@ -7823,7 +8112,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7823
8112
|
if (data?.active !== true) await activate(true);
|
|
7824
8113
|
if (targetLessonId !== void 0 && targetLessonId !== lesson.lessonId) await setFocus(targetLessonId).catch(() => {});
|
|
7825
8114
|
const bindId = targetLessonId ?? lesson.lessonId;
|
|
7826
|
-
let sessionId = boundSessionOf(bindId) ??
|
|
8115
|
+
let sessionId = boundSessionOf(bindId) ?? threadGroupOf(bindId)?.active ?? data?.lessonSessions[bindId] ?? null;
|
|
7827
8116
|
if (sessionId !== null && !sessionKnown(ctx, sessionId)) sessionId = null;
|
|
7828
8117
|
if (sessionId === null) {
|
|
7829
8118
|
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
@@ -7836,10 +8125,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7836
8125
|
sessionId
|
|
7837
8126
|
};
|
|
7838
8127
|
await bindLessonSession(bindId, sessionId, threadTitle ?? threadAutoTitle(text));
|
|
7839
|
-
} else
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
8128
|
+
} else {
|
|
8129
|
+
localBound.current = {
|
|
8130
|
+
lessonId: bindId,
|
|
8131
|
+
sessionId
|
|
8132
|
+
};
|
|
8133
|
+
await bindLessonSession(bindId, sessionId);
|
|
8134
|
+
}
|
|
7843
8135
|
const shell = PANEL_SHELL.current;
|
|
7844
8136
|
if (shell !== null) shell.suppressHandBack(() => {
|
|
7845
8137
|
ctx.sessions.open(sessionId);
|
|
@@ -8018,6 +8310,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8018
8310
|
onThreadRename: renameLessonThreadUi,
|
|
8019
8311
|
onThreadArchive: archiveLessonThreadUi,
|
|
8020
8312
|
onThreadDelete: deleteLessonThreadUi,
|
|
8313
|
+
courseScope: data !== null && lesson !== null ? data.courses.find((c) => c.sections.some((sec) => sec.lessons.some((l) => l.id === lesson.lessonId)))?.threadScope ?? "lesson" : "lesson",
|
|
8314
|
+
onScopeToggle: (next) => {
|
|
8315
|
+
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;
|
|
8316
|
+
if (cid !== void 0) setCourseScope(cid, next);
|
|
8317
|
+
},
|
|
8021
8318
|
contextMeter,
|
|
8022
8319
|
contextBreakdown: breakdown,
|
|
8023
8320
|
uploadAttachment
|
|
@@ -8390,11 +8687,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8390
8687
|
};
|
|
8391
8688
|
const [importJob, setImportJob] = (0, react.useState)(null);
|
|
8392
8689
|
const [importResult, setImportResult] = (0, react.useState)(null);
|
|
8393
|
-
const sawTurnRef = (0, react.useRef)(false);
|
|
8394
|
-
if (importJob !== null && turnActive) sawTurnRef.current = true;
|
|
8395
8690
|
const startImport = (label, prompt) => {
|
|
8396
8691
|
setImportResult(null);
|
|
8397
|
-
sawTurnRef.current = false;
|
|
8398
8692
|
setImportJob({
|
|
8399
8693
|
label,
|
|
8400
8694
|
startedAt: Date.now(),
|
|
@@ -8415,7 +8709,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8415
8709
|
setPanel("map");
|
|
8416
8710
|
return;
|
|
8417
8711
|
}
|
|
8418
|
-
if (
|
|
8712
|
+
if (importJobDead(importJob, turnActive, Date.now())) {
|
|
8419
8713
|
setImportJob(null);
|
|
8420
8714
|
setImportResult({
|
|
8421
8715
|
ok: false,
|
|
@@ -8981,7 +9275,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8981
9275
|
return cut <= 0 ? path : norm.slice(0, cut);
|
|
8982
9276
|
}
|
|
8983
9277
|
/** 中栏: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 }) {
|
|
9278
|
+
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
9279
|
const stuck = (0, react.useRef)(true);
|
|
8986
9280
|
const [showFab, setShowFab] = (0, react.useState)(false);
|
|
8987
9281
|
const [threadsOpen, setThreadsOpen] = (0, react.useState)(false);
|
|
@@ -9207,7 +9501,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9207
9501
|
revealed
|
|
9208
9502
|
}));
|
|
9209
9503
|
};
|
|
9210
|
-
const
|
|
9504
|
+
const threadKeyOfHere = (lessonId) => {
|
|
9505
|
+
if (lessonId === null || data === null) return null;
|
|
9506
|
+
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);
|
|
9507
|
+
return null;
|
|
9508
|
+
};
|
|
9509
|
+
const keyHere = threadKeyOfHere(lesson !== null ? lesson.lessonId : null);
|
|
9510
|
+
const threadPills = threadGroupPills(keyHere !== null ? data?.lessonThreads[keyHere] : void 0);
|
|
9211
9511
|
const lastAssistant = rowsView.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
9212
9512
|
const [error, setError] = (0, react.useState)(null);
|
|
9213
9513
|
const fire = (action) => {
|
|
@@ -9298,14 +9598,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9298
9598
|
}));
|
|
9299
9599
|
send(tr("proposal.decline.msg", { lesson: proposal.lessonTitle }));
|
|
9300
9600
|
}
|
|
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", {
|
|
9601
|
+
}, 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
9602
|
className: "lks14-threadchip",
|
|
9303
9603
|
"aria-expanded": String(threadsOpen),
|
|
9304
9604
|
"data-tooltip": tr("threads.label"),
|
|
9305
9605
|
onClick: () => {
|
|
9306
9606
|
setThreadsOpen((v) => !v);
|
|
9307
9607
|
}
|
|
9308
|
-
}, tr("threads.open", { n: String(threadPills.length) }), (0, react.createElement)("span", { className: "lks14-threadchip-caret" }, threadsOpen ? "▾" : "▸")), threadsOpen ? (0, react.createElement)("div", {
|
|
9608
|
+
}, 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
9609
|
className: "lks14-threadmenu",
|
|
9310
9610
|
role: "menu"
|
|
9311
9611
|
}, ...threadPills.map((p) => (0, react.createElement)("div", {
|
|
@@ -9350,7 +9650,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9350
9650
|
if (v !== "" && v !== p.title) onThreadRename(p.id, v);
|
|
9351
9651
|
setRenaming(null);
|
|
9352
9652
|
}
|
|
9353
|
-
}) : (0, react.createElement)("span", { className: "lks14-threadmenu-title" }, p.title
|
|
9653
|
+
}) : (0, react.createElement)("span", { className: "lks14-threadmenu-title" }, p.title, courseScope === "course" && threadCoverageLabel(p.touchedLessons, data?.courses ?? []) !== "" ? (0, react.createElement)("span", {
|
|
9654
|
+
className: "lks14-threadmenu-cover",
|
|
9655
|
+
"data-testid": "thread-cover"
|
|
9656
|
+
}, `${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
9657
|
className: "lks14-threadmenu-gear",
|
|
9355
9658
|
"aria-label": tr("threads.actions"),
|
|
9356
9659
|
"data-tooltip": tr("threads.actions"),
|
|
@@ -9393,6 +9696,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9393
9696
|
setGearFor(null);
|
|
9394
9697
|
}
|
|
9395
9698
|
}, tr("threads.delete"))) : null)), (0, react.createElement)("button", {
|
|
9699
|
+
key: "__scope__",
|
|
9700
|
+
className: "lks14-threadmenu-row scope",
|
|
9701
|
+
role: "menuitemcheckbox",
|
|
9702
|
+
"aria-checked": String(courseScope === "course"),
|
|
9703
|
+
"data-testid": "thread-scope-toggle",
|
|
9704
|
+
onClick: () => {
|
|
9705
|
+
onScopeToggle(courseScope === "course" ? "lesson" : "course");
|
|
9706
|
+
}
|
|
9707
|
+
}, (0, react.createElement)("span", {
|
|
9708
|
+
className: "lks14-threadmenu-scope-icon",
|
|
9709
|
+
"aria-hidden": "true"
|
|
9710
|
+
}, 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
9711
|
key: "__new__",
|
|
9397
9712
|
className: "lks14-threadmenu-row new",
|
|
9398
9713
|
role: "menuitem",
|
|
@@ -9427,7 +9742,20 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9427
9742
|
onClick: () => {
|
|
9428
9743
|
send(starters[0]?.message ?? tr("prompt.start"));
|
|
9429
9744
|
}
|
|
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
|
|
9745
|
+
}, 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) ? {
|
|
9746
|
+
send,
|
|
9747
|
+
answerAsk: (label) => {
|
|
9748
|
+
(async () => {
|
|
9749
|
+
if (busy) {
|
|
9750
|
+
stop();
|
|
9751
|
+
await new Promise((r) => {
|
|
9752
|
+
setTimeout(r, 1800);
|
|
9753
|
+
});
|
|
9754
|
+
}
|
|
9755
|
+
send(label);
|
|
9756
|
+
})();
|
|
9757
|
+
}
|
|
9758
|
+
} : void 0, row.role === "assistant" ? {
|
|
9431
9759
|
playing: msgAudio?.key === row.key,
|
|
9432
9760
|
index: msgAudio?.index ?? 0,
|
|
9433
9761
|
total: msgAudio?.total ?? 0,
|
|
@@ -9618,6 +9946,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9618
9946
|
const [read, setRead] = (0, react.useState)(null);
|
|
9619
9947
|
const readCtl = (0, react.useRef)(null);
|
|
9620
9948
|
const [readError, setReadError] = (0, react.useState)(null);
|
|
9949
|
+
const [langView, setLangView] = (0, react.useState)("bilingual");
|
|
9950
|
+
(0, react.useEffect)(() => {
|
|
9951
|
+
if (lesson === null) return;
|
|
9952
|
+
let next = "bilingual";
|
|
9953
|
+
try {
|
|
9954
|
+
const stored = localStorage.getItem(`lks-langview:${lesson.lessonId}`);
|
|
9955
|
+
if (stored === "original" || stored === "bilingual" || stored === "translation") next = stored;
|
|
9956
|
+
} catch {}
|
|
9957
|
+
setLangView(next);
|
|
9958
|
+
}, [lesson?.lessonId]);
|
|
9959
|
+
const teachHtml = lesson === null ? "" : langView === "translation" && lesson.translationHtml !== void 0 ? lesson.translationHtml : langView === "bilingual" && lesson.bilingualHtml !== void 0 ? lesson.bilingualHtml : lesson.html;
|
|
9621
9960
|
const { tts, addUserNote, recordReview, editNote, pinNote } = useStudy();
|
|
9622
9961
|
const proseRef = (0, react.useRef)(null);
|
|
9623
9962
|
const diagRef = (0, react.useRef)(null);
|
|
@@ -9819,7 +10158,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9819
10158
|
if (proseRef.current !== null) applyHighlights(proseRef.current, recordQuotes);
|
|
9820
10159
|
setEnhanceTick(Date.now());
|
|
9821
10160
|
});
|
|
9822
|
-
}, [tab,
|
|
10161
|
+
}, [tab, teachHtml]);
|
|
9823
10162
|
(0, react.useEffect)(() => {
|
|
9824
10163
|
if (pendingLocate === null || tab !== "teach" || proseRef.current === null) return;
|
|
9825
10164
|
locateNoteQuote(pendingLocate, proseRef.current);
|
|
@@ -9882,7 +10221,24 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9882
10221
|
setRateBusy(false);
|
|
9883
10222
|
});
|
|
9884
10223
|
}
|
|
9885
|
-
}, tr(key))))) : null, (0, react.createElement)("div", { className: "lks-readbar" }, (0, react.createElement)("
|
|
10224
|
+
}, tr(key))))) : null, (0, react.createElement)("div", { className: "lks-readbar" }, lesson.bilingualHtml !== void 0 ? (0, react.createElement)("div", {
|
|
10225
|
+
className: "lks-langview",
|
|
10226
|
+
"data-testid": "langview"
|
|
10227
|
+
}, [
|
|
10228
|
+
"original",
|
|
10229
|
+
"bilingual",
|
|
10230
|
+
"translation"
|
|
10231
|
+
].map((v) => (0, react.createElement)("button", {
|
|
10232
|
+
key: v,
|
|
10233
|
+
className: langView === v ? "on" : "",
|
|
10234
|
+
"data-testid": `langview-${v}`,
|
|
10235
|
+
onClick: () => {
|
|
10236
|
+
setLangView(v);
|
|
10237
|
+
try {
|
|
10238
|
+
localStorage.setItem(`lks-langview:${lesson.lessonId}`, v);
|
|
10239
|
+
} catch {}
|
|
10240
|
+
}
|
|
10241
|
+
}, tr(`langview.${v}`)))) : null, (0, react.createElement)("button", {
|
|
9886
10242
|
className: "lks-btn ghost",
|
|
9887
10243
|
style: {
|
|
9888
10244
|
padding: "3px 8px",
|
|
@@ -9916,7 +10272,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9916
10272
|
}, (0, react.createElement)("div", {
|
|
9917
10273
|
className: "lks14-prose",
|
|
9918
10274
|
ref: proseRef,
|
|
9919
|
-
dangerouslySetInnerHTML: { __html:
|
|
10275
|
+
dangerouslySetInnerHTML: { __html: teachHtml }
|
|
9920
10276
|
})), quoteBtn !== null ? (0, react.createElement)("div", {
|
|
9921
10277
|
className: "lks-quote-btn",
|
|
9922
10278
|
"data-lks-quote-btn": "",
|