dsh-plugin-lookatstudy 0.23.0 → 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 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)}
@@ -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),
@@ -2328,6 +2358,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2328
2358
  "rail.due.start": "开始复习",
2329
2359
  "rail.due.none": "暂时没有到期的复习,保持节奏!",
2330
2360
  "chat.reasoning": "思考过程({n} 字)",
2361
+ "chat.think": "思考",
2362
+ "langview.original": "原文",
2363
+ "langview.bilingual": "对照",
2364
+ "langview.translation": "译文",
2331
2365
  "msg.speak": "朗读",
2332
2366
  "note.pin": "置顶笔记",
2333
2367
  "note.unpin": "取消置顶",
@@ -2695,6 +2729,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2695
2729
  "rail.due.start": "Start reviewing",
2696
2730
  "rail.due.none": "Nothing due right now — keep the streak!",
2697
2731
  "chat.reasoning": "Thinking ({n} chars)",
2732
+ "chat.think": "Think",
2733
+ "langview.original": "Original",
2734
+ "langview.bilingual": "Both",
2735
+ "langview.translation": "Translation",
2698
2736
  "msg.speak": "Speak",
2699
2737
  "note.pin": "Pin note",
2700
2738
  "note.unpin": "Unpin",
@@ -3693,6 +3731,22 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
3693
3731
  return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
3694
3732
  }
3695
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
+ /**
3696
3750
  * Tooltip text for one course-tree status glyph. Pure (the translator is an
3697
3751
  * optional parameter defaulting to the active one, evaluated per call).
3698
3752
  */
@@ -4755,6 +4809,27 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
4755
4809
  */
4756
4810
  const TURN_STALL_MS = 24e4;
4757
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
+ /**
4758
4833
  * The stall decision (pure — the wall clock lives at the poll call site):
4759
4834
  * an ACTIVE fold stays active while the window's top seq keeps advancing;
4760
4835
  * once it has been frozen for longer than stallMs the panel treats the turn
@@ -4988,15 +5063,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
4988
5063
  continue;
4989
5064
  }
4990
5065
  const chunk = event.data?.chunk;
4991
- if (chunk === void 0 || chunk.type !== "text-delta") continue;
5066
+ if (chunk === void 0 || chunk.type !== "text-delta" && chunk.type !== "reasoning-delta") continue;
4992
5067
  const attemptId = event.data?.attemptId ?? String(event.seq);
5068
+ const delta = chunk.text ?? "";
4993
5069
  const stream = streams.get(attemptId);
4994
5070
  if (stream === void 0) streams.set(attemptId, {
4995
- text: chunk.text ?? "",
5071
+ text: chunk.type === "text-delta" ? delta : "",
5072
+ reasoning: chunk.type === "reasoning-delta" ? delta : "",
4996
5073
  anchor: rows.length,
4997
5074
  settled: false
4998
5075
  });
4999
- else stream.text += chunk.text ?? "";
5076
+ else if (chunk.type === "text-delta") stream.text += delta;
5077
+ else stream.reasoning += delta;
5000
5078
  latest = attemptId;
5001
5079
  }
5002
5080
  let askOpen = false;
@@ -5019,6 +5097,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
5019
5097
  role: "streaming",
5020
5098
  text
5021
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
+ });
5022
5107
  }
5023
5108
  return rows;
5024
5109
  }
@@ -7541,10 +7626,21 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
7541
7626
  key: row.key,
7542
7627
  className: "lks14-msg lks14-msg-user"
7543
7628
  }, 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);
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
+ }
7548
7644
  if (row.role === "tool") return (0, react.createElement)("div", {
7549
7645
  key: row.key,
7550
7646
  className: "lks14-toolchip " + String(row.toolState ?? "loading")
@@ -7620,7 +7716,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
7620
7716
  };
7621
7717
  }
7622
7718
  function StudyPanelBody({ ctx }) {
7623
- const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, setCourseScope, 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
+ }, []);
7624
7728
  (0, react.useEffect)(() => {
7625
7729
  wireCodeBlockCopy();
7626
7730
  }, []);
@@ -8583,11 +8687,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8583
8687
  };
8584
8688
  const [importJob, setImportJob] = (0, react.useState)(null);
8585
8689
  const [importResult, setImportResult] = (0, react.useState)(null);
8586
- const sawTurnRef = (0, react.useRef)(false);
8587
- if (importJob !== null && turnActive) sawTurnRef.current = true;
8588
8690
  const startImport = (label, prompt) => {
8589
8691
  setImportResult(null);
8590
- sawTurnRef.current = false;
8591
8692
  setImportJob({
8592
8693
  label,
8593
8694
  startedAt: Date.now(),
@@ -8608,7 +8709,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8608
8709
  setPanel("map");
8609
8710
  return;
8610
8711
  }
8611
- if (sawTurnRef.current && !turnActive) {
8712
+ if (importJobDead(importJob, turnActive, Date.now())) {
8612
8713
  setImportJob(null);
8613
8714
  setImportResult({
8614
8715
  ok: false,
@@ -9845,6 +9946,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
9845
9946
  const [read, setRead] = (0, react.useState)(null);
9846
9947
  const readCtl = (0, react.useRef)(null);
9847
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;
9848
9960
  const { tts, addUserNote, recordReview, editNote, pinNote } = useStudy();
9849
9961
  const proseRef = (0, react.useRef)(null);
9850
9962
  const diagRef = (0, react.useRef)(null);
@@ -10046,7 +10158,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
10046
10158
  if (proseRef.current !== null) applyHighlights(proseRef.current, recordQuotes);
10047
10159
  setEnhanceTick(Date.now());
10048
10160
  });
10049
- }, [tab, lesson?.html]);
10161
+ }, [tab, teachHtml]);
10050
10162
  (0, react.useEffect)(() => {
10051
10163
  if (pendingLocate === null || tab !== "teach" || proseRef.current === null) return;
10052
10164
  locateNoteQuote(pendingLocate, proseRef.current);
@@ -10109,7 +10221,24 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
10109
10221
  setRateBusy(false);
10110
10222
  });
10111
10223
  }
10112
- }, tr(key))))) : null, (0, react.createElement)("div", { className: "lks-readbar" }, (0, react.createElement)("button", {
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", {
10113
10242
  className: "lks-btn ghost",
10114
10243
  style: {
10115
10244
  padding: "3px 8px",
@@ -10143,7 +10272,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
10143
10272
  }, (0, react.createElement)("div", {
10144
10273
  className: "lks14-prose",
10145
10274
  ref: proseRef,
10146
- dangerouslySetInnerHTML: { __html: lesson.html }
10275
+ dangerouslySetInnerHTML: { __html: teachHtml }
10147
10276
  })), quoteBtn !== null ? (0, react.createElement)("div", {
10148
10277
  className: "lks-quote-btn",
10149
10278
  "data-lks-quote-btn": "",