dsh-plugin-lookatstudy 0.23.0 → 0.25.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 +221 -21
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +288 -40
- 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)}
|
|
@@ -361,8 +362,8 @@ window.__ModuleLoader__.load({
|
|
|
361
362
|
.lks-ui .lks14-railbody{position:absolute;inset:0;overflow:hidden;z-index:10}
|
|
362
363
|
.lks-ui .lks14-railtrack{display:flex;height:100%;width:200%;transition:transform .3s var(--ease-out-expo)}
|
|
363
364
|
.lks-ui .lks14-railpane{width:50%;height:100%;position:relative}
|
|
364
|
-
.lks-ui .lks14-railscroll{height:100%;overflow-y:auto;overflow-x:hidden;padding:112px 8px 16px}
|
|
365
|
-
.lks-ui .lks14-railpane-import{height:100%;overflow-y:auto;padding:64px 12px 12px}
|
|
365
|
+
.lks-ui .lks14-railscroll{height:100%;overflow-y:auto;overflow-x:hidden;padding:112px 8px 16px;box-sizing:border-box}
|
|
366
|
+
.lks-ui .lks14-railpane-import{height:100%;overflow-y:auto;padding:64px 12px 12px;box-sizing:border-box}
|
|
366
367
|
.lks-ui .lks14-raillist{display:flex;flex-direction:column;gap:8px;margin-bottom:10px}
|
|
367
368
|
.lks-ui .lks14-railcourse{text-align:left;padding:10px 12px;border-radius:12px;background:rgb(255 255 255/0.05);display:flex;flex-direction:column;gap:2px}
|
|
368
369
|
.lks-ui .lks14-railcourse:hover{background:rgb(255 255 255/0.1)}
|
|
@@ -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)}
|
|
@@ -1802,6 +1821,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1802
1821
|
});
|
|
1803
1822
|
this.refresh();
|
|
1804
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
|
+
}
|
|
1805
1834
|
/** E3: land one attachment verbatim in the study workspace; returns the workspace-relative path. */
|
|
1806
1835
|
async uploadAttachment(name, dataBase64) {
|
|
1807
1836
|
const body = await fetchJson("/lookatstudy/api/attachment", {
|
|
@@ -2084,6 +2113,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2084
2113
|
activate: studyStore.activate.bind(studyStore),
|
|
2085
2114
|
setMode: studyStore.setMode.bind(studyStore),
|
|
2086
2115
|
setHistoryBudget: studyStore.setHistoryBudget.bind(studyStore),
|
|
2116
|
+
setInterfaceLang: studyStore.setInterfaceLang.bind(studyStore),
|
|
2087
2117
|
uploadAttachment: studyStore.uploadAttachment.bind(studyStore),
|
|
2088
2118
|
setFocus: studyStore.setFocus.bind(studyStore),
|
|
2089
2119
|
searchLessons: studyStore.searchLessons.bind(studyStore),
|
|
@@ -2228,6 +2258,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2228
2258
|
"toast.close": "关闭",
|
|
2229
2259
|
"composer.busy": "导师正在回复…",
|
|
2230
2260
|
"rail.empty.demo": "导入示例课程",
|
|
2261
|
+
"import.session.title": "课程导入",
|
|
2231
2262
|
"import.cta": "导入新课程",
|
|
2232
2263
|
"import.tab.url": "URL",
|
|
2233
2264
|
"import.tab.md": "MD",
|
|
@@ -2328,6 +2359,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2328
2359
|
"rail.due.start": "开始复习",
|
|
2329
2360
|
"rail.due.none": "暂时没有到期的复习,保持节奏!",
|
|
2330
2361
|
"chat.reasoning": "思考过程({n} 字)",
|
|
2362
|
+
"chat.think": "思考",
|
|
2363
|
+
"langview.original": "原文",
|
|
2364
|
+
"langview.bilingual": "对照",
|
|
2365
|
+
"langview.translation": "译文",
|
|
2331
2366
|
"msg.speak": "朗读",
|
|
2332
2367
|
"note.pin": "置顶笔记",
|
|
2333
2368
|
"note.unpin": "取消置顶",
|
|
@@ -2595,6 +2630,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2595
2630
|
"toast.close": "Close",
|
|
2596
2631
|
"composer.busy": "The tutor is replying…",
|
|
2597
2632
|
"rail.empty.demo": "Import the demo course",
|
|
2633
|
+
"import.session.title": "Course import",
|
|
2598
2634
|
"import.cta": "Import a new course",
|
|
2599
2635
|
"import.tab.url": "URL",
|
|
2600
2636
|
"import.tab.md": "MD",
|
|
@@ -2695,6 +2731,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2695
2731
|
"rail.due.start": "Start reviewing",
|
|
2696
2732
|
"rail.due.none": "Nothing due right now — keep the streak!",
|
|
2697
2733
|
"chat.reasoning": "Thinking ({n} chars)",
|
|
2734
|
+
"chat.think": "Think",
|
|
2735
|
+
"langview.original": "Original",
|
|
2736
|
+
"langview.bilingual": "Both",
|
|
2737
|
+
"langview.translation": "Translation",
|
|
2698
2738
|
"msg.speak": "Speak",
|
|
2699
2739
|
"note.pin": "Pin note",
|
|
2700
2740
|
"note.unpin": "Unpin",
|
|
@@ -3693,6 +3733,33 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3693
3733
|
return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
|
|
3694
3734
|
}
|
|
3695
3735
|
/**
|
|
3736
|
+
* The thinking disclosure's collapsed one-line summary (0.24.0, the host
|
|
3737
|
+
* ReasoningRow contract verbatim): while RUNNING the LAST line rides (the
|
|
3738
|
+
* summary visibly follows the stream's tail), settled the FIRST line leads;
|
|
3739
|
+
* double-asterisk markers are omitted either way. Pure.
|
|
3740
|
+
*/
|
|
3741
|
+
function reasoningSummaryLine(text, running) {
|
|
3742
|
+
return (running ? (() => {
|
|
3743
|
+
const v = text.trimEnd();
|
|
3744
|
+
const nl = v.lastIndexOf("\n");
|
|
3745
|
+
return nl === -1 ? v : v.slice(nl + 1);
|
|
3746
|
+
})() : (() => {
|
|
3747
|
+
const nl = text.indexOf("\n");
|
|
3748
|
+
return nl === -1 ? text : text.slice(0, nl);
|
|
3749
|
+
})()).replaceAll("**", "");
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* The dedicated import session's host-list title (0.24.1): imports mint a
|
|
3753
|
+
* course-less one-shot session that never rides a lesson thread — a uniform
|
|
3754
|
+
* label keeps the host session list legible instead of auto-title noise.
|
|
3755
|
+
* Label slugified to one line, whole title capped. Pure.
|
|
3756
|
+
*/
|
|
3757
|
+
function importSessionTitle(label, now, t = tr) {
|
|
3758
|
+
const flat = label.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 24);
|
|
3759
|
+
const date = `${String(now.getFullYear()).slice(2)}.${String(now.getMonth() + 1).padStart(2, "0")}.${String(now.getDate()).padStart(2, "0")}`;
|
|
3760
|
+
return `${flat === "" ? t("import.session.title") : flat} · ${date}`;
|
|
3761
|
+
}
|
|
3762
|
+
/**
|
|
3696
3763
|
* Tooltip text for one course-tree status glyph. Pure (the translator is an
|
|
3697
3764
|
* optional parameter defaulting to the active one, evaluated per call).
|
|
3698
3765
|
*/
|
|
@@ -4755,6 +4822,27 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4755
4822
|
*/
|
|
4756
4823
|
const TURN_STALL_MS = 24e4;
|
|
4757
4824
|
/**
|
|
4825
|
+
* 0.23.1 issue 3: how long an import funnel may sit with NO live turn before
|
|
4826
|
+
* the panel reads the import as failed. The old failure exit armed on the
|
|
4827
|
+
* turnActive prop having rendered true once — but a turn that dies inside one
|
|
4828
|
+
* event-window notification batch (turn/start + turn/end between two reads,
|
|
4829
|
+
* the keyless-provider fast fail; journal-verified: attempt → step/end →
|
|
4830
|
+
* turn/end within seconds) never flips the prop, so the exit never armed and
|
|
4831
|
+
* the funnel spun forever. This deadline needs no observation: past grace
|
|
4832
|
+
* with the turn not active = failed. The grace only covers prompt queue
|
|
4833
|
+
* latency (turn/start journals within seconds of the queue-time resolve); a
|
|
4834
|
+
* live turn holds the funnel open for its whole life however long the import
|
|
4835
|
+
* tools run.
|
|
4836
|
+
*/
|
|
4837
|
+
const IMPORT_FAIL_GRACE_MS = 3e4;
|
|
4838
|
+
/**
|
|
4839
|
+
* The funnel's failure decision (pure — the wall clock lives at the poll call
|
|
4840
|
+
* site, mirroring {@link turnStalled}).
|
|
4841
|
+
*/
|
|
4842
|
+
function importJobDead(job, turnActive, now, grace = IMPORT_FAIL_GRACE_MS) {
|
|
4843
|
+
return job !== null && !turnActive && now - job.startedAt > grace;
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4758
4846
|
* The stall decision (pure — the wall clock lives at the poll call site):
|
|
4759
4847
|
* an ACTIVE fold stays active while the window's top seq keeps advancing;
|
|
4760
4848
|
* once it has been frozen for longer than stallMs the panel treats the turn
|
|
@@ -4988,15 +5076,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4988
5076
|
continue;
|
|
4989
5077
|
}
|
|
4990
5078
|
const chunk = event.data?.chunk;
|
|
4991
|
-
if (chunk === void 0 || chunk.type !== "text-delta") continue;
|
|
5079
|
+
if (chunk === void 0 || chunk.type !== "text-delta" && chunk.type !== "reasoning-delta") continue;
|
|
4992
5080
|
const attemptId = event.data?.attemptId ?? String(event.seq);
|
|
5081
|
+
const delta = chunk.text ?? "";
|
|
4993
5082
|
const stream = streams.get(attemptId);
|
|
4994
5083
|
if (stream === void 0) streams.set(attemptId, {
|
|
4995
|
-
text: chunk.text
|
|
5084
|
+
text: chunk.type === "text-delta" ? delta : "",
|
|
5085
|
+
reasoning: chunk.type === "reasoning-delta" ? delta : "",
|
|
4996
5086
|
anchor: rows.length,
|
|
4997
5087
|
settled: false
|
|
4998
5088
|
});
|
|
4999
|
-
else
|
|
5089
|
+
else if (chunk.type === "text-delta") stream.text += delta;
|
|
5090
|
+
else stream.reasoning += delta;
|
|
5000
5091
|
latest = attemptId;
|
|
5001
5092
|
}
|
|
5002
5093
|
let askOpen = false;
|
|
@@ -5019,6 +5110,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5019
5110
|
role: "streaming",
|
|
5020
5111
|
text
|
|
5021
5112
|
});
|
|
5113
|
+
const reasoning = stream.reasoning.trim();
|
|
5114
|
+
if (reasoning !== "") rows.splice(stream.anchor, 0, {
|
|
5115
|
+
key: "streaming-reasoning",
|
|
5116
|
+
role: "reasoning",
|
|
5117
|
+
text: reasoning,
|
|
5118
|
+
running: true
|
|
5119
|
+
});
|
|
5022
5120
|
}
|
|
5023
5121
|
return rows;
|
|
5024
5122
|
}
|
|
@@ -7541,10 +7639,21 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7541
7639
|
key: row.key,
|
|
7542
7640
|
className: "lks14-msg lks14-msg-user"
|
|
7543
7641
|
}, row.text);
|
|
7544
|
-
if (row.role === "reasoning")
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7642
|
+
if (row.role === "reasoning") {
|
|
7643
|
+
const running = row.running === true;
|
|
7644
|
+
return (0, react.createElement)("details", {
|
|
7645
|
+
key: row.key,
|
|
7646
|
+
className: "lks14-reasoning",
|
|
7647
|
+
"data-state": running ? "running" : "ok",
|
|
7648
|
+
"data-testid": "reasoning-row"
|
|
7649
|
+
}, (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", {
|
|
7650
|
+
className: "lks14-reasoning-sep",
|
|
7651
|
+
"aria-hidden": true
|
|
7652
|
+
}), (0, react.createElement)("span", {
|
|
7653
|
+
className: "lks14-reasoning-summary",
|
|
7654
|
+
"data-follow-end": running || void 0
|
|
7655
|
+
}, (0, react.createElement)("span", { className: "lks14-reasoning-sumtext" }, reasoningSummaryLine(row.text, running)))), (0, react.createElement)("div", { className: "lks14-reasoning-body" }, row.text));
|
|
7656
|
+
}
|
|
7548
7657
|
if (row.role === "tool") return (0, react.createElement)("div", {
|
|
7549
7658
|
key: row.key,
|
|
7550
7659
|
className: "lks14-toolchip " + String(row.toolState ?? "loading")
|
|
@@ -7620,7 +7729,23 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7620
7729
|
};
|
|
7621
7730
|
}
|
|
7622
7731
|
function StudyPanelBody({ ctx }) {
|
|
7623
|
-
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, setCourseScope, uploadAttachment } = useStudy();
|
|
7732
|
+
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, setCourseScope, uploadAttachment, setInterfaceLang } = useStudy();
|
|
7733
|
+
(0, react.useEffect)(() => {
|
|
7734
|
+
const read = () => {
|
|
7735
|
+
let lang = document.documentElement.lang;
|
|
7736
|
+
if (!/^[a-z]{2,3}(-[A-Za-z0-9]+)*$/.test(lang)) lang = navigator.language;
|
|
7737
|
+
if (/^[a-z]{2,3}(-[A-Za-z0-9]+)*$/.test(lang)) setInterfaceLang(lang).catch(() => {});
|
|
7738
|
+
};
|
|
7739
|
+
read();
|
|
7740
|
+
const t1 = setTimeout(read, 600);
|
|
7741
|
+
const t2 = setTimeout(read, 2400);
|
|
7742
|
+
const off = ctx.locale?.subscribe(read);
|
|
7743
|
+
return () => {
|
|
7744
|
+
clearTimeout(t1);
|
|
7745
|
+
clearTimeout(t2);
|
|
7746
|
+
off?.();
|
|
7747
|
+
};
|
|
7748
|
+
}, []);
|
|
7624
7749
|
(0, react.useEffect)(() => {
|
|
7625
7750
|
wireCodeBlockCopy();
|
|
7626
7751
|
}, []);
|
|
@@ -7821,7 +7946,43 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7821
7946
|
} else celebrate(ev.kind);
|
|
7822
7947
|
prevCelebrationRef.current = snapshot;
|
|
7823
7948
|
}, [data]);
|
|
7824
|
-
const
|
|
7949
|
+
const [importSessionId, setImportSessionId] = (0, react.useState)(null);
|
|
7950
|
+
const sendImport = async (prompt, label) => {
|
|
7951
|
+
if (data?.active !== true) await activate(true);
|
|
7952
|
+
let sessionId = importSessionId;
|
|
7953
|
+
if (sessionId === null || !sessionKnown(ctx, sessionId)) {
|
|
7954
|
+
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
7955
|
+
if (!area.ok) throw new Error(`study area unavailable (HTTP ${String(area.status)})`);
|
|
7956
|
+
const { path } = await area.json();
|
|
7957
|
+
const workspace = await ctx.workspaces.create({ path });
|
|
7958
|
+
sessionId = await ctx.sessions.create({ workspaceId: workspace.workspaceId });
|
|
7959
|
+
setImportSessionId(sessionId);
|
|
7960
|
+
}
|
|
7961
|
+
const shell = PANEL_SHELL.current;
|
|
7962
|
+
if (shell !== null) shell.suppressHandBack(() => {
|
|
7963
|
+
ctx.sessions.open(sessionId);
|
|
7964
|
+
});
|
|
7965
|
+
else ctx.sessions.open(sessionId);
|
|
7966
|
+
const actx = ctx.sessions.scope(sessionId);
|
|
7967
|
+
const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
|
|
7968
|
+
if (face === void 0) throw new Error("import session is not addressable yet");
|
|
7969
|
+
activeFace.current = face;
|
|
7970
|
+
try {
|
|
7971
|
+
await face.rename?.(importSessionTitle(label, /* @__PURE__ */ new Date()));
|
|
7972
|
+
} catch {}
|
|
7973
|
+
const result = await face.prompt([{
|
|
7974
|
+
type: "text",
|
|
7975
|
+
text: prompt
|
|
7976
|
+
}], "queue");
|
|
7977
|
+
if (!result.ok) throw new Error(`prompt rejected: ${result.error.code}: ${result.error.message}`);
|
|
7978
|
+
};
|
|
7979
|
+
/** Forget the import session — called from every funnel exit. Unpinning
|
|
7980
|
+
* re-runs the binding effect, which restages the focused lesson's thread. */
|
|
7981
|
+
const endImportSession = (0, react.useCallback)(() => {
|
|
7982
|
+
setImportSessionId(null);
|
|
7983
|
+
activeFace.current = null;
|
|
7984
|
+
}, []);
|
|
7985
|
+
const boundId = importSessionId ?? boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? threadGroupOf(lesson !== null ? lesson.lessonId : null)?.active ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7825
7986
|
const nudged = (0, react.useRef)(false);
|
|
7826
7987
|
(0, react.useEffect)(() => {
|
|
7827
7988
|
if (nudged.current || (data?.dueCount ?? 0) === 0) return;
|
|
@@ -8063,6 +8224,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8063
8224
|
}, [send]);
|
|
8064
8225
|
const switchLessonThread = (sessionId) => {
|
|
8065
8226
|
if (lesson === null) return;
|
|
8227
|
+
if (importSessionId !== null) return;
|
|
8066
8228
|
if (localBound.current?.lessonId === lesson.lessonId && localBound.current.sessionId === sessionId) return;
|
|
8067
8229
|
localBound.current = {
|
|
8068
8230
|
lessonId: lesson.lessonId,
|
|
@@ -8077,6 +8239,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8077
8239
|
};
|
|
8078
8240
|
const startNewThread = () => {
|
|
8079
8241
|
if (lesson === null) return;
|
|
8242
|
+
if (importSessionId !== null) return;
|
|
8080
8243
|
localBound.current = null;
|
|
8081
8244
|
bindLessonSession(lesson.lessonId, null);
|
|
8082
8245
|
setRows([]);
|
|
@@ -8139,6 +8302,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8139
8302
|
turnActive: feedGen,
|
|
8140
8303
|
importProgress,
|
|
8141
8304
|
stop,
|
|
8305
|
+
sendImport,
|
|
8306
|
+
endImportSession,
|
|
8142
8307
|
paletteBus
|
|
8143
8308
|
}), (0, react.createElement)(PaneResizeHandle, {
|
|
8144
8309
|
side: "rail",
|
|
@@ -8550,7 +8715,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8550
8715
|
PANEL_SHELL.current = shell;
|
|
8551
8716
|
}
|
|
8552
8717
|
/** 左栏:course picker, tree, review box, import. */
|
|
8553
|
-
function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send, onJumped, streamingLessonId, turnActive, importProgress, stop, paletteBus }) {
|
|
8718
|
+
function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send, onJumped, streamingLessonId, turnActive, importProgress, stop, paletteBus, sendImport, endImportSession }) {
|
|
8554
8719
|
const [selectedCourse, setSelectedCourse] = (0, react.useState)("");
|
|
8555
8720
|
const [query, setQuery] = (0, react.useState)("");
|
|
8556
8721
|
const [searchRows, setSearchRows] = (0, react.useState)([]);
|
|
@@ -8583,23 +8748,28 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8583
8748
|
};
|
|
8584
8749
|
const [importJob, setImportJob] = (0, react.useState)(null);
|
|
8585
8750
|
const [importResult, setImportResult] = (0, react.useState)(null);
|
|
8586
|
-
const sawTurnRef = (0, react.useRef)(false);
|
|
8587
|
-
if (importJob !== null && turnActive) sawTurnRef.current = true;
|
|
8588
8751
|
const startImport = (label, prompt) => {
|
|
8589
8752
|
setImportResult(null);
|
|
8590
|
-
sawTurnRef.current = false;
|
|
8591
8753
|
setImportJob({
|
|
8592
8754
|
label,
|
|
8593
8755
|
startedAt: Date.now(),
|
|
8594
8756
|
baselineIds: new Set((data?.courses ?? []).map((c) => c.courseId))
|
|
8595
8757
|
});
|
|
8596
|
-
|
|
8758
|
+
sendImport(prompt, label).catch((err) => {
|
|
8759
|
+
setImportJob(null);
|
|
8760
|
+
endImportSession();
|
|
8761
|
+
setImportResult({
|
|
8762
|
+
ok: false,
|
|
8763
|
+
msg: err instanceof Error ? err.message : String(err)
|
|
8764
|
+
});
|
|
8765
|
+
});
|
|
8597
8766
|
};
|
|
8598
8767
|
(0, react.useEffect)(() => {
|
|
8599
8768
|
if (importJob === null || data === null) return;
|
|
8600
8769
|
const fresh = data.courses.find((c) => !importJob.baselineIds.has(c.courseId));
|
|
8601
8770
|
if (fresh !== void 0) {
|
|
8602
8771
|
setImportJob(null);
|
|
8772
|
+
endImportSession();
|
|
8603
8773
|
setImportResult({
|
|
8604
8774
|
ok: true,
|
|
8605
8775
|
msg: tr("import.success")
|
|
@@ -8608,8 +8778,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8608
8778
|
setPanel("map");
|
|
8609
8779
|
return;
|
|
8610
8780
|
}
|
|
8611
|
-
if (
|
|
8781
|
+
if (importJobDead(importJob, turnActive, Date.now())) {
|
|
8612
8782
|
setImportJob(null);
|
|
8783
|
+
endImportSession();
|
|
8613
8784
|
setImportResult({
|
|
8614
8785
|
ok: false,
|
|
8615
8786
|
msg: tr("import.error.turn")
|
|
@@ -8807,7 +8978,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8807
8978
|
}))))) : null, data.courses.length === 0 ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("rail.empty.title")) : null, (0, react.createElement)("button", {
|
|
8808
8979
|
className: "lks-btn ghost lks14-raildemo",
|
|
8809
8980
|
onClick: () => {
|
|
8810
|
-
|
|
8981
|
+
startImport(tr("import.tab.url"), tr("prompt.import", { url: "https://github.com/microsoft/AI-For-Beginners" }));
|
|
8811
8982
|
}
|
|
8812
8983
|
}, (0, react.createElement)(IconDownloadOutline16, null), tr("rail.empty.demo")), (0, react.createElement)(ImportPanel, {
|
|
8813
8984
|
job: importJob,
|
|
@@ -8818,6 +8989,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8818
8989
|
onCancel: () => {
|
|
8819
8990
|
stop();
|
|
8820
8991
|
setImportJob(null);
|
|
8992
|
+
endImportSession();
|
|
8821
8993
|
setImportResult({
|
|
8822
8994
|
ok: false,
|
|
8823
8995
|
msg: tr("import.cancelled")
|
|
@@ -9845,6 +10017,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9845
10017
|
const [read, setRead] = (0, react.useState)(null);
|
|
9846
10018
|
const readCtl = (0, react.useRef)(null);
|
|
9847
10019
|
const [readError, setReadError] = (0, react.useState)(null);
|
|
10020
|
+
const [langView, setLangView] = (0, react.useState)("bilingual");
|
|
10021
|
+
(0, react.useEffect)(() => {
|
|
10022
|
+
if (lesson === null) return;
|
|
10023
|
+
let next = "bilingual";
|
|
10024
|
+
try {
|
|
10025
|
+
const stored = localStorage.getItem(`lks-langview:${lesson.lessonId}`);
|
|
10026
|
+
if (stored === "original" || stored === "bilingual" || stored === "translation") next = stored;
|
|
10027
|
+
} catch {}
|
|
10028
|
+
setLangView(next);
|
|
10029
|
+
}, [lesson?.lessonId]);
|
|
10030
|
+
const teachHtml = lesson === null ? "" : langView === "translation" && lesson.translationHtml !== void 0 ? lesson.translationHtml : langView === "bilingual" && lesson.bilingualHtml !== void 0 ? lesson.bilingualHtml : lesson.html;
|
|
9848
10031
|
const { tts, addUserNote, recordReview, editNote, pinNote } = useStudy();
|
|
9849
10032
|
const proseRef = (0, react.useRef)(null);
|
|
9850
10033
|
const diagRef = (0, react.useRef)(null);
|
|
@@ -10046,7 +10229,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
10046
10229
|
if (proseRef.current !== null) applyHighlights(proseRef.current, recordQuotes);
|
|
10047
10230
|
setEnhanceTick(Date.now());
|
|
10048
10231
|
});
|
|
10049
|
-
}, [tab,
|
|
10232
|
+
}, [tab, teachHtml]);
|
|
10050
10233
|
(0, react.useEffect)(() => {
|
|
10051
10234
|
if (pendingLocate === null || tab !== "teach" || proseRef.current === null) return;
|
|
10052
10235
|
locateNoteQuote(pendingLocate, proseRef.current);
|
|
@@ -10109,7 +10292,24 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
10109
10292
|
setRateBusy(false);
|
|
10110
10293
|
});
|
|
10111
10294
|
}
|
|
10112
|
-
}, tr(key))))) : null, (0, react.createElement)("div", { className: "lks-readbar" }, (0, react.createElement)("
|
|
10295
|
+
}, tr(key))))) : null, (0, react.createElement)("div", { className: "lks-readbar" }, lesson.bilingualHtml !== void 0 ? (0, react.createElement)("div", {
|
|
10296
|
+
className: "lks-langview",
|
|
10297
|
+
"data-testid": "langview"
|
|
10298
|
+
}, [
|
|
10299
|
+
"original",
|
|
10300
|
+
"bilingual",
|
|
10301
|
+
"translation"
|
|
10302
|
+
].map((v) => (0, react.createElement)("button", {
|
|
10303
|
+
key: v,
|
|
10304
|
+
className: langView === v ? "on" : "",
|
|
10305
|
+
"data-testid": `langview-${v}`,
|
|
10306
|
+
onClick: () => {
|
|
10307
|
+
setLangView(v);
|
|
10308
|
+
try {
|
|
10309
|
+
localStorage.setItem(`lks-langview:${lesson.lessonId}`, v);
|
|
10310
|
+
} catch {}
|
|
10311
|
+
}
|
|
10312
|
+
}, tr(`langview.${v}`)))) : null, (0, react.createElement)("button", {
|
|
10113
10313
|
className: "lks-btn ghost",
|
|
10114
10314
|
style: {
|
|
10115
10315
|
padding: "3px 8px",
|
|
@@ -10143,7 +10343,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
10143
10343
|
}, (0, react.createElement)("div", {
|
|
10144
10344
|
className: "lks14-prose",
|
|
10145
10345
|
ref: proseRef,
|
|
10146
|
-
dangerouslySetInnerHTML: { __html:
|
|
10346
|
+
dangerouslySetInnerHTML: { __html: teachHtml }
|
|
10147
10347
|
})), quoteBtn !== null ? (0, react.createElement)("div", {
|
|
10148
10348
|
className: "lks-quote-btn",
|
|
10149
10349
|
"data-lks-quote-btn": "",
|