dsh-plugin-lookatstudy 0.12.1 → 0.12.3

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
@@ -106,7 +106,7 @@ window.__ModuleLoader__.load({
106
106
  .lks-sec{font-size:12px;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em;margin:14px 0 4px}
107
107
  /* Collapsible section head: a real button (keyboard can toggle it), sticky so
108
108
  the current chapter stays identified while 4000px of nodes scroll under it. */
109
- .lks-sechead{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em;text-align:left;padding:6px 4px;margin:14px 0 4px;border-radius:6px;position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-base)}
109
+ .lks-sechead{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);text-align:left;padding:6px 4px;margin:14px 0 4px;border-radius:6px;position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-base)}
110
110
  .lks-sechead:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}
111
111
  .lks-sechead:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
112
112
  .lks-sechead-t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -261,6 +261,10 @@ window.__ModuleLoader__.load({
261
261
  .lks-dock-streak{color:var(--dsw-alias-state-business-primary)}
262
262
  .lks-dockseg.lks-muted{color:var(--dsw-alias-label-secondary)}
263
263
 
264
+ /* concept map legend (amber = weak) */
265
+ .lks-cmap-legend{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:4px}
266
+ .lks-cmap-legend i{width:10px;height:10px;border-radius:3px;background:var(--dsw-alias-state-warn-tertiary,#fef5e7);border:1px solid var(--dsw-alias-state-warn-primary,#dd8629)}
267
+
264
268
  /* keyed tool.call.toolview cards (conversation tab tool rows) */
265
269
  .lks-tv{display:flex;flex-direction:column;gap:4px;padding:6px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);font-size:13px}
266
270
  .lks-tv.err{border-color:var(--dsw-alias-state-error-primary)}
@@ -269,6 +273,11 @@ window.__ModuleLoader__.load({
269
273
  .lks-tv-chip.ok{background:var(--dsw-alias-state-success-tertiary);color:var(--lks-ok-ink)}
270
274
  .lks-tv-chip.bad{background:var(--dsw-alias-state-error-tertiary);color:var(--lks-err-ink)}
271
275
  .lks-tv-line{color:var(--dsw-alias-label-secondary);line-height:1.6;white-space:pre-wrap}
276
+ /* exam star card + the failure moment */
277
+ .lks-tv-stars{display:inline-flex;align-items:center;gap:5px;font-size:13px;font-weight:600;color:var(--lks-warn-ink)}
278
+ .lks-tv-stars .dim{opacity:.22}
279
+ .lks-tv-stars-label{font-weight:400;color:var(--dsw-alias-label-secondary)}
280
+ .lks-tv-failnote{margin-top:4px;font-size:12.5px;color:var(--dsw-alias-label-secondary);line-height:1.6}
272
281
  `;
273
282
  /**
274
283
  * Inject the study stylesheet into `document.head` (idempotent).
@@ -1512,77 +1521,6 @@ window.__ModuleLoader__.load({
1512
1521
  return hubId;
1513
1522
  }
1514
1523
  //#endregion
1515
- //#region src/client/diagrams.ts
1516
- /**
1517
- * Blackboard diagram views (SPEC Phase 2): the lesson concept map (ELK layered
1518
- * layout via the vendored cmap-elk-layout + CDN elkjs, draw.io-flavored SVG
1519
- * skin rendered plugin-side). Degrades honestly: any CDN/ELK failure leaves a
1520
- * one-line notice instead of a broken pane.
1521
- *
1522
- * The lesson mind map view was RETIRED with upstream v0.26.0 (2026-09-01
1523
- * port): body text that has headings already shows its structure, text without
1524
- * headings yields a first-line-truncated node graph nobody can read — both
1525
- * lose — and this concept map already covers the semantic-structure need at
1526
- * higher quality. Its three CDN imports went with it.
1527
- */
1528
- /** Lesson concept map: the lesson node + its concepts (weak ones accented),
1529
- * laid out by the vendored ELK pipeline, skinned draw.io-style (plugin-original,
1530
- * upstream's skin is coupled to its token pipeline). Mastery shades the nodes. */
1531
- async function renderLessonConceptMap(container, lessonTitle, concepts) {
1532
- const layout = await layoutConceptMap([{
1533
- id: "lesson",
1534
- label: lessonTitle
1535
- }, ...concepts.map((c) => ({
1536
- id: `c-${c.title}`,
1537
- label: c.title
1538
- }))], concepts.map((c) => ({
1539
- from: "lesson",
1540
- to: `c-${c.title}`
1541
- })));
1542
- const masteryBy = new Map(concepts.map((c) => [`c-${c.title}`, c.masteryPct]));
1543
- const ns = "http://www.w3.org/2000/svg";
1544
- const svg = document.createElementNS(ns, "svg");
1545
- svg.setAttribute("width", String(Math.max(320, layout.width)));
1546
- svg.setAttribute("height", String(Math.max(200, layout.height)));
1547
- svg.setAttribute("viewBox", `0 0 ${layout.width} ${layout.height}`);
1548
- svg.setAttribute("style", "max-width:100%;background:var(--dsw-surface-1, #fff);border:1px solid var(--dsw-border-1, #d0d7de);border-radius:8px");
1549
- for (const e of layout.edges) {
1550
- const path = document.createElementNS(ns, "path");
1551
- path.setAttribute("d", e.pts.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" "));
1552
- path.setAttribute("fill", "none");
1553
- path.setAttribute("stroke", "var(--dsw-border-2, #8b949e)");
1554
- path.setAttribute("stroke-width", "1.5");
1555
- svg.append(path);
1556
- }
1557
- for (const n of layout.nodes) {
1558
- const g = document.createElementNS(ns, "g");
1559
- g.setAttribute("transform", `translate(${n.x},${n.y})`);
1560
- const rect = document.createElementNS(ns, "rect");
1561
- const isLesson = n.id === "lesson";
1562
- const mastery = masteryBy.get(n.id);
1563
- rect.setAttribute("width", String(n.w));
1564
- rect.setAttribute("height", String(n.h));
1565
- rect.setAttribute("rx", "6");
1566
- rect.setAttribute("fill", mastery !== void 0 && mastery < 70 ? "#fff3d9" : "var(--dsw-surface-2, #f6f8fa)");
1567
- rect.setAttribute("stroke", isLesson ? "var(--dsw-accent-1, #0969da)" : "var(--dsw-border-2, #8b949e)");
1568
- rect.setAttribute("stroke-width", isLesson ? "2" : "1");
1569
- g.append(rect);
1570
- n.lines.forEach((line, li) => {
1571
- const label = document.createElementNS(ns, "text");
1572
- label.setAttribute("x", String(n.w / 2));
1573
- label.setAttribute("y", String(n.h / 2 - (n.lines.length - 1) * 7 + li * 14 + 4));
1574
- label.setAttribute("text-anchor", "middle");
1575
- label.setAttribute("font-size", isLesson ? "13" : "12");
1576
- label.setAttribute("font-weight", isLesson ? "600" : "400");
1577
- label.setAttribute("fill", "var(--dsw-ink-1, #1f2328)");
1578
- label.textContent = line;
1579
- g.append(label);
1580
- });
1581
- svg.append(g);
1582
- }
1583
- container.append(svg);
1584
- }
1585
- //#endregion
1586
1524
  //#region src/client/locale.ts
1587
1525
  /** Simplified Chinese dictionary (also the fallback locale). */
1588
1526
  const ZH = {
@@ -1661,11 +1599,15 @@ window.__ModuleLoader__.load({
1661
1599
  "bb.empty": "黑板还空着",
1662
1600
  "bb.empty.hint": "在左侧课程树选择一课",
1663
1601
  "bb.mastery": "掌握度 {pct}%",
1602
+ "cmap.legend": "琥珀色 = 薄弱知识点(掌握度 <70%)",
1603
+ "cmap.node.pct": "掌握度 {pct}%",
1664
1604
  "bb.strategy": "本课学法",
1665
1605
  "bb.fallback.cmap.empty": "概念图需要先由导师定义本课概念",
1666
1606
  "bb.fallback.cmap": "概念图渲染不可用(布局引擎加载失败) — 已回退讲解视图",
1667
1607
  "bb.notes": "笔记",
1668
1608
  "bb.notes.empty": "这一课还没有笔记",
1609
+ "tv.exam.stars": "本次 {n}★(历史最佳 {best}★)",
1610
+ "tv.exam.fail": "这轮没拿到星——错题就是下一课的地图,从每题的解析看起。",
1669
1611
  "active.off.title": "注册 study 工具并载入导师人格,之后普通对话和现在一样",
1670
1612
  "active.on.title": "注销 study 工具与导师人格;学习进度保留,可随时重新开启",
1671
1613
  "active.off": "▶ 开始学习",
@@ -1676,8 +1618,8 @@ window.__ModuleLoader__.load({
1676
1618
  "settings.mode.hint": "导师的讲解风格;也可在学习页随时切换",
1677
1619
  "settings.studyMode": "学习模式",
1678
1620
  "settings.studyMode.hint": "开启后注册 study_* 工具并载入导师人格;进度与笔记始终保留",
1679
- "settings.on": "已开启",
1680
- "settings.off": "已关闭",
1621
+ "settings.on": "状态:已开启",
1622
+ "settings.off": "状态:已关闭",
1681
1623
  "settings.turnOn": "开启",
1682
1624
  "settings.turnOff": "关闭",
1683
1625
  "settings.stats": "学习统计",
@@ -1777,11 +1719,15 @@ window.__ModuleLoader__.load({
1777
1719
  "bb.empty": "The blackboard is empty",
1778
1720
  "bb.empty.hint": "Pick a lesson in the course tree on the left",
1779
1721
  "bb.mastery": "mastery {pct}%",
1722
+ "cmap.legend": "Amber = weak concept (mastery < 70%)",
1723
+ "cmap.node.pct": "mastery {pct}%",
1780
1724
  "bb.strategy": "Approach",
1781
1725
  "bb.fallback.cmap.empty": "The concept map needs the tutor to define this lesson's concepts first",
1782
1726
  "bb.fallback.cmap": "Concept map unavailable (layout engine failed to load) — fell back to the teach view",
1783
1727
  "bb.notes": "Notes",
1784
1728
  "bb.notes.empty": "No notes for this lesson yet",
1729
+ "tv.exam.stars": "This attempt {n}★ (best {best}★)",
1730
+ "tv.exam.fail": "No stars this round — the wrong answers are the map for the next one; start with their explanations.",
1785
1731
  "active.off.title": "Register the study tools and load the tutor persona; plain chats stay as they are",
1786
1732
  "active.on.title": "Retire the study tools and tutor persona; progress is kept and you can re-enable anytime",
1787
1733
  "active.off": "▶ Start learning",
@@ -1792,8 +1738,8 @@ window.__ModuleLoader__.load({
1792
1738
  "settings.mode.hint": "The tutor's explanation style; switchable anytime in the study tab too",
1793
1739
  "settings.studyMode": "Study mode",
1794
1740
  "settings.studyMode.hint": "On registers the study_* tools and the tutor persona; progress and notes are always kept",
1795
- "settings.on": "On",
1796
- "settings.off": "Off",
1741
+ "settings.on": "Status: on",
1742
+ "settings.off": "Status: off",
1797
1743
  "settings.turnOn": "Turn on",
1798
1744
  "settings.turnOff": "Turn off",
1799
1745
  "settings.stats": "Study stats",
@@ -1879,6 +1825,86 @@ window.__ModuleLoader__.load({
1879
1825
  return activeT(key, params);
1880
1826
  }
1881
1827
  //#endregion
1828
+ //#region src/client/diagrams.ts
1829
+ /**
1830
+ * Blackboard diagram views (SPEC Phase 2): the lesson concept map (ELK layered
1831
+ * layout via the vendored cmap-elk-layout + CDN elkjs, draw.io-flavored SVG
1832
+ * skin rendered plugin-side). Degrades honestly: any CDN/ELK failure leaves a
1833
+ * one-line notice instead of a broken pane.
1834
+ *
1835
+ * The lesson mind map view was RETIRED with upstream v0.26.0 (2026-09-01
1836
+ * port): body text that has headings already shows its structure, text without
1837
+ * headings yields a first-line-truncated node graph nobody can read — both
1838
+ * lose — and this concept map already covers the semantic-structure need at
1839
+ * higher quality. Its three CDN imports went with it.
1840
+ */
1841
+ /** Lesson concept map: the lesson node + its concepts (weak ones accented),
1842
+ * laid out by the vendored ELK pipeline, skinned draw.io-style (plugin-original,
1843
+ * upstream's skin is coupled to its token pipeline). Mastery shades the nodes. */
1844
+ async function renderLessonConceptMap(container, lessonTitle, concepts) {
1845
+ const layout = await layoutConceptMap([{
1846
+ id: "lesson",
1847
+ label: lessonTitle
1848
+ }, ...concepts.map((c) => ({
1849
+ id: `c-${c.title}`,
1850
+ label: c.title
1851
+ }))], concepts.map((c) => ({
1852
+ from: "lesson",
1853
+ to: `c-${c.title}`
1854
+ })));
1855
+ const masteryBy = new Map(concepts.map((c) => [`c-${c.title}`, c.masteryPct]));
1856
+ const ns = "http://www.w3.org/2000/svg";
1857
+ const svg = document.createElementNS(ns, "svg");
1858
+ svg.setAttribute("width", String(Math.max(320, layout.width)));
1859
+ svg.setAttribute("height", String(Math.max(200, layout.height)));
1860
+ svg.setAttribute("viewBox", `0 0 ${layout.width} ${layout.height}`);
1861
+ svg.setAttribute("style", "max-width:100%;background:var(--dsw-alias-bg-layer-1, #fff);border:1px solid var(--dsw-alias-border-l2, #d0d7de);border-radius:8px");
1862
+ for (const e of layout.edges) {
1863
+ const path = document.createElementNS(ns, "path");
1864
+ path.setAttribute("d", e.pts.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" "));
1865
+ path.setAttribute("fill", "none");
1866
+ path.setAttribute("stroke", "var(--dsw-alias-border-l2, #8b949e)");
1867
+ path.setAttribute("stroke-width", "1.5");
1868
+ svg.append(path);
1869
+ }
1870
+ for (const n of layout.nodes) {
1871
+ const g = document.createElementNS(ns, "g");
1872
+ g.setAttribute("transform", `translate(${n.x},${n.y})`);
1873
+ const rect = document.createElementNS(ns, "rect");
1874
+ const isLesson = n.id === "lesson";
1875
+ const mastery = masteryBy.get(n.id);
1876
+ rect.setAttribute("width", String(n.w));
1877
+ rect.setAttribute("height", String(n.h));
1878
+ rect.setAttribute("rx", "6");
1879
+ rect.setAttribute("fill", mastery !== void 0 && mastery < 70 ? "var(--dsw-alias-state-warn-tertiary, #fef5e7)" : "var(--dsw-alias-bg-layer-2, #f6f8fa)");
1880
+ rect.setAttribute("stroke", isLesson ? "var(--dsw-alias-state-business-primary, #0969da)" : "var(--dsw-alias-border-l2, #8b949e)");
1881
+ rect.setAttribute("stroke-width", isLesson ? "2" : "1");
1882
+ g.append(rect);
1883
+ n.lines.forEach((line, li) => {
1884
+ const label = document.createElementNS(ns, "text");
1885
+ label.setAttribute("x", String(n.w / 2));
1886
+ label.setAttribute("y", String(n.h / 2 - (n.lines.length - 1) * 7 + li * 14 + 4));
1887
+ label.setAttribute("text-anchor", "middle");
1888
+ label.setAttribute("font-size", isLesson ? "13" : "12");
1889
+ label.setAttribute("font-weight", isLesson ? "600" : "400");
1890
+ label.setAttribute("fill", "var(--dsw-alias-label-primary, #1f2328)");
1891
+ label.textContent = line;
1892
+ g.append(label);
1893
+ });
1894
+ const title = document.createElementNS(ns, "title");
1895
+ const label = n.lines.join(" ");
1896
+ title.textContent = isLesson || mastery === void 0 ? label : `${label} · ${tr("cmap.node.pct", { pct: mastery })}`;
1897
+ g.append(title);
1898
+ svg.append(g);
1899
+ }
1900
+ container.append(svg);
1901
+ const legend = document.createElement("span");
1902
+ legend.className = "lks-cmap-legend";
1903
+ const swatch = document.createElement("i");
1904
+ legend.append(swatch, document.createTextNode(tr("cmap.legend")));
1905
+ container.append(legend);
1906
+ }
1907
+ //#endregion
1882
1908
  //#region src/client/views.tsx
1883
1909
  /**
1884
1910
  * The study tab: one `conversation.view` entry rendering the whole plugin —
@@ -2029,6 +2055,9 @@ window.__ModuleLoader__.load({
2029
2055
  * Tool results are skipped: the host conversation renders the rich
2030
2056
  * `tool.call.toolview` cards, and the tutor column stays a pure persona
2031
2057
  * surface (a transcript mirror there grew a second, diverging record).
2058
+ * Both inputs tolerate undefined: newer hosts (DSH v0.1.2-alpha.4) mount
2059
+ * the conversation view before session data exists, and the fold must
2060
+ * degrade to an empty transcript instead of blanking the whole tab.
2032
2061
  * @param nodes - finalized conversation nodes from the snapshot.
2033
2062
  * @param partial - the in-flight assistant partial, or null.
2034
2063
  * @param t - translator (defaults to the active one, per call).
@@ -2036,7 +2065,7 @@ window.__ModuleLoader__.load({
2036
2065
  */
2037
2066
  function transcriptRows(nodes, partial, t = tr) {
2038
2067
  const rows = [];
2039
- for (const node of nodes) switch (node.kind) {
2068
+ for (const node of nodes ?? []) switch (node.kind) {
2040
2069
  case "user":
2041
2070
  case "steering": {
2042
2071
  const text = textOf(node.content);
@@ -2062,7 +2091,7 @@ window.__ModuleLoader__.load({
2062
2091
  text: node.message
2063
2092
  });
2064
2093
  }
2065
- if (partial !== null) {
2094
+ if (partial != null) {
2066
2095
  const text = assistantText(partial.blocks);
2067
2096
  if (text !== "") rows.push({
2068
2097
  key: "streaming",
@@ -2208,7 +2237,7 @@ window.__ModuleLoader__.load({
2208
2237
  bindLessonSession,
2209
2238
  send,
2210
2239
  ctx,
2211
- currentSessionId: snapshot.sessionId
2240
+ currentSessionId: snapshot?.sessionId ?? ""
2212
2241
  }), (0, react.createElement)(TutorColumn, {
2213
2242
  data,
2214
2243
  setMode,
@@ -2491,7 +2520,7 @@ window.__ModuleLoader__.load({
2491
2520
  /** Middle column: the tutor — transcript, proposal banner, pills, starters. Typing happens in the native composer below the tab. */
2492
2521
  function TutorColumn({ data, setMode, send, snapshot }) {
2493
2522
  const [error, setError] = (0, react.useState)(null);
2494
- const rows = transcriptRows(snapshot.nodes, snapshot.partial);
2523
+ const rows = transcriptRows(snapshot?.nodes, snapshot?.partial ?? null);
2495
2524
  const lastAssistant = rows.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
2496
2525
  const scrollRef = (0, react.useRef)(null);
2497
2526
  (0, react.useEffect)(() => {
@@ -2641,7 +2670,7 @@ window.__ModuleLoader__.load({
2641
2670
  */
2642
2671
  function studyStartButton(ctx) {
2643
2672
  return function StudyStartButton({ session }) {
2644
- if (session.blank !== true) return null;
2673
+ if (session?.blank !== true) return null;
2645
2674
  return (0, react.createElement)(Inner, {
2646
2675
  key: "inner",
2647
2676
  ctx
@@ -2884,9 +2913,34 @@ window.__ModuleLoader__.load({
2884
2913
  function StudyDueView({ toolName, block }) {
2885
2914
  return studyToolCard(toolName, block);
2886
2915
  }
2887
- /** study_exam_result star card. */
2916
+ /** Stars from the exam card's presentationMeta line (`N★`); null when absent. Pure. */
2917
+ function examStars(block) {
2918
+ const lines = metaLines(block);
2919
+ for (const line of lines) {
2920
+ const m = /^([0-3])★\s*\(?(?:best\s*([0-3])★)?/.exec(line.trim()) ?? /^(?:本次\s*)?([0-3])★/.exec(line.trim());
2921
+ if (m !== null) return {
2922
+ stars: Number(m[1]),
2923
+ best: Number(m[2] ?? m[1])
2924
+ };
2925
+ }
2926
+ return null;
2927
+ }
2928
+ /** study_exam_result star card — and the failure moment finally gets its line. */
2888
2929
  function StudyExamView({ toolName, block }) {
2889
- return studyToolCard(toolName, block);
2930
+ const r = examStars(block);
2931
+ const stars = r === null ? null : (0, react.createElement)("span", { className: "lks-tv-stars" }, [
2932
+ 0,
2933
+ 1,
2934
+ 2
2935
+ ].map((i) => (0, react.createElement)("span", {
2936
+ key: i,
2937
+ className: i < r.stars ? "full" : "dim"
2938
+ }, "★")), (0, react.createElement)("span", { className: "lks-tv-stars-label" }, tr("tv.exam.stars", {
2939
+ n: r.stars,
2940
+ best: r.best
2941
+ })));
2942
+ const fail = r !== null && r.stars === 0 ? (0, react.createElement)("div", { className: "lks-tv-failnote" }, tr("tv.exam.fail")) : null;
2943
+ return studyToolCard(toolName, block, (0, react.createElement)("span", null, stars, fail));
2890
2944
  }
2891
2945
  //#endregion
2892
2946
  //#region src/client/index.ts