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 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 stream.text += chunk.text ?? "";
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") return (0, react.createElement)("details", {
7545
- key: row.key,
7546
- className: "lks14-reasoning"
7547
- }, (0, react.createElement)("summary", null, tr("chat.reasoning", { n: String(row.text.length) })), row.text);
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 boundId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? threadGroupOf(lesson !== null ? lesson.lessonId : null)?.active ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
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
- send(prompt);
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 (sawTurnRef.current && !turnActive) {
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
- send(tr("prompt.import", { url: "https://github.com/microsoft/AI-For-Beginners" }));
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, lesson?.html]);
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)("button", {
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: lesson.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": "",