dsh-plugin-lookatstudy 0.12.3 → 0.12.5

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
@@ -235,6 +235,9 @@ window.__ModuleLoader__.load({
235
235
  .lks-zone-h{font-size:14px;color:var(--dsw-alias-label-secondary);margin:0 0 8px;font-weight:600}
236
236
  .lks-note{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 14px;margin-bottom:8px}
237
237
  .lks-note .lks-note-src{float:right;font-size:11.5px;color:var(--dsw-alias-label-secondary)}
238
+ .lks-note-del{float:right;clear:right;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;padding:2px;border-radius:5px;line-height:0}
239
+ .lks-note-del:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}
240
+ .lks-note-del.armed{color:#fff;background:var(--dsw-alias-state-error-primary)}
238
241
  .lks-note .lks-note-title{font-weight:600;font-size:13px}
239
242
  .lks-note .lks-note-text{margin-top:4px;font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.65}
240
243
  .lks-note .lks-note-text p{margin:4px 0}
@@ -298,10 +301,10 @@ window.__ModuleLoader__.load({
298
301
  * ic_ds_* glyphs vendored from @deepseek-ai/dsh-client-ui-primitives
299
302
  * (src/icons/index.tsx, same figma source as the host's icon set) so the
300
303
  * plugin's client bundle stays self-contained per the dsh client-bundle
301
- * contract — only react and dsh-client-runtime/client are runtime-external;
302
- * importing the host package root would drag its CSS modules into the
303
- * inlined bundle. Re-sync these three paths if the host icon set redesigns
304
- * them; scripts/verify.mjs pins the path data.
304
+ * contract — only react is runtime-external; importing the host package
305
+ * root would drag its CSS modules into the inlined bundle. Re-sync these
306
+ * three paths if the host icon set redesigns them; scripts/verify.mjs
307
+ * pins the path data.
305
308
  *
306
309
  * 2026-09-01 critique round: seven more ic_ds glyphs vendored (play/plus/
307
310
  * trash/refresh/globe/goal/warning) plus plugin-original learning-domain
@@ -715,6 +718,18 @@ window.__ModuleLoader__.load({
715
718
  });
716
719
  this.refresh();
717
720
  }
721
+ /** Delete one notebook entry from a lesson's Cornell zones. */
722
+ async deleteNote(lessonId, noteId) {
723
+ await fetchJson("/lookatstudy/api/note/delete", {
724
+ method: "POST",
725
+ headers: { "content-type": "application/json" },
726
+ body: JSON.stringify({
727
+ lessonId,
728
+ noteId
729
+ })
730
+ });
731
+ this.refresh();
732
+ }
718
733
  };
719
734
  /** The one shared store instance backing every study seat component. */
720
735
  const studyStore = new StudyStore();
@@ -727,6 +742,7 @@ window.__ModuleLoader__.load({
727
742
  setFocus: studyStore.setFocus.bind(studyStore),
728
743
  searchLessons: studyStore.searchLessons.bind(studyStore),
729
744
  deleteCourse: studyStore.deleteCourse.bind(studyStore),
745
+ deleteNote: studyStore.deleteNote.bind(studyStore),
730
746
  bindLessonSession: studyStore.bindLessonSession.bind(studyStore)
731
747
  };
732
748
  }
@@ -1567,6 +1583,8 @@ window.__ModuleLoader__.load({
1567
1583
  "rail.delete": "删除本课程",
1568
1584
  "rail.delete.confirm": "确认删除?",
1569
1585
  "rail.delete.title.confirm": "再点一次确认删除(含全部进度与笔记)。反悔前可先在设置页备份状态文件",
1586
+ "note.delete": "删除本条笔记",
1587
+ "note.delete.confirm": "确认删除?",
1570
1588
  "rail.section.collapse": "折叠本章节",
1571
1589
  "rail.section.expand": "展开本章节({count} 课时)",
1572
1590
  "rail.section.count": "{count} 课",
@@ -1687,6 +1705,8 @@ window.__ModuleLoader__.load({
1687
1705
  "rail.delete": "Delete this course",
1688
1706
  "rail.delete.confirm": "Delete?",
1689
1707
  "rail.delete.title.confirm": "Click again to confirm (all progress and notes included). Back up via the state file in Settings first",
1708
+ "note.delete": "Delete this note",
1709
+ "note.delete.confirm": "Delete?",
1690
1710
  "rail.section.collapse": "Collapse this section",
1691
1711
  "rail.section.expand": "Expand this section ({count} lessons)",
1692
1712
  "rail.section.count": "{count} lessons",
@@ -2045,10 +2065,10 @@ window.__ModuleLoader__.load({
2045
2065
  return runs.at(-1) ?? [];
2046
2066
  }
2047
2067
  function textOf(blocks) {
2048
- return blocks.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n\n").trim();
2068
+ return (blocks ?? []).filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n\n").trim();
2049
2069
  }
2050
2070
  function assistantText(blocks) {
2051
- return blocks.filter((b) => b.kind === "text").map((b) => b.text).join("\n\n").trim();
2071
+ return (blocks ?? []).filter((b) => b.kind === "text").map((b) => b.text ?? "").join("\n\n").trim();
2052
2072
  }
2053
2073
  /**
2054
2074
  * Fold conversation nodes (plus the streaming partial) into render rows.
@@ -2088,7 +2108,7 @@ window.__ModuleLoader__.load({
2088
2108
  case "turn-error": rows.push({
2089
2109
  key: `e${node.seq}`,
2090
2110
  role: "error",
2091
- text: node.message
2111
+ text: node.message ?? ""
2092
2112
  });
2093
2113
  }
2094
2114
  if (partial != null) {
@@ -2111,6 +2131,17 @@ window.__ModuleLoader__.load({
2111
2131
  if (error === null) return null;
2112
2132
  return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
2113
2133
  }
2134
+ /**
2135
+ * Resolve the transcript across host generations. 0.1.3+ hosts expose the
2136
+ * node array as the Chat target's `legacy` slice; pre-0.1.3 hosts hand the
2137
+ * whole conversation snapshot (nodes + partial) through `useSession`, whose
2138
+ * 0.1.3+ successor returns the session MACHINE (no node array). The array
2139
+ * check tells those two `useSession` shapes apart. Pure.
2140
+ */
2141
+ function pickTranscript(chatLegacy, sessionSnapshot) {
2142
+ if (chatLegacy !== void 0) return chatLegacy;
2143
+ return sessionSnapshot !== void 0 && Array.isArray(sessionSnapshot.nodes) ? sessionSnapshot : void 0;
2144
+ }
2114
2145
  /** sessionStorage key keeping the narrow-mode pane across view remounts (tab switches) and reloads. */
2115
2146
  const PANE_KEY = "dsh-plugin-lookatstudy:pane";
2116
2147
  /** Narrow the stored value back to a pane id; anything else (or nothing stored) falls back to 导师. Pure, unit-testable. */
@@ -2137,9 +2168,12 @@ window.__ModuleLoader__.load({
2137
2168
  };
2138
2169
  }
2139
2170
  /** Tab body: the factory-bound ctx carries workspaces/sessions for the per-lesson session jumps. */
2140
- function StudyTab({ useSession, inputActions, ctx }) {
2141
- const { data, activate, setMode, setFocus, searchLessons, deleteCourse, bindLessonSession } = useStudy();
2142
- const snapshot = useSession((s) => s);
2171
+ function StudyTab({ inputActions, ctx, ...standard }) {
2172
+ const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession } = useStudy();
2173
+ const chatLegacy = standard.useChat?.((s) => s.legacy);
2174
+ const sessionSnapshot = standard.useSession?.((s) => s);
2175
+ const snapshot = pickTranscript(chatLegacy, sessionSnapshot);
2176
+ const currentSessionId = standard.sessionId ?? snapshot?.sessionId ?? "";
2143
2177
  const [pane, setPane] = (0, react.useState)(storedPane);
2144
2178
  const rootRef = (0, react.useRef)(null);
2145
2179
  (0, react.useEffect)(() => {
@@ -2237,13 +2271,16 @@ window.__ModuleLoader__.load({
2237
2271
  bindLessonSession,
2238
2272
  send,
2239
2273
  ctx,
2240
- currentSessionId: snapshot?.sessionId ?? ""
2274
+ currentSessionId
2241
2275
  }), (0, react.createElement)(TutorColumn, {
2242
2276
  data,
2243
2277
  setMode,
2244
2278
  send,
2245
2279
  snapshot
2246
- }), (0, react.createElement)(BlackboardColumn, { data })));
2280
+ }), (0, react.createElement)(BlackboardColumn, {
2281
+ data,
2282
+ deleteNote
2283
+ })));
2247
2284
  }
2248
2285
  /** Left column: course management (pick/delete/search/import), lesson tree, due box. */
2249
2286
  function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, bindLessonSession, send, ctx, currentSessionId }) {
@@ -2268,7 +2305,7 @@ window.__ModuleLoader__.load({
2268
2305
  if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
2269
2306
  const { path } = await area.json();
2270
2307
  const workspace = await ctx.workspaces.create({ path });
2271
- const sessionId = await ctx.workspaces.connectWorkspace(workspace.workspaceId);
2308
+ const sessionId = ctx.workspaces.connectWorkspace !== void 0 ? await ctx.workspaces.connectWorkspace(workspace.workspaceId) : await ctx.sessions.create({ workspaceId: workspace.workspaceId });
2272
2309
  const actx = ctx.sessions.scope(sessionId);
2273
2310
  const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
2274
2311
  if (face === void 0) throw new Error("lesson session is not addressable yet");
@@ -2587,9 +2624,33 @@ window.__ModuleLoader__.load({
2587
2624
  }, s.label))) : null, (0, react.createElement)(ActionError, { error }));
2588
2625
  }
2589
2626
  /** Right column: the blackboard — focus-lesson 讲解/脑图/概念图 plus the Cornell 笔记. */
2590
- function BlackboardColumn({ data }) {
2627
+ function BlackboardColumn({ data, deleteNote }) {
2591
2628
  const lesson = data?.lesson ?? null;
2592
2629
  const [pane, setPane] = (0, react.useState)("teach");
2630
+ const [error, setError] = (0, react.useState)(null);
2631
+ const [armedNote, setArmedNote] = (0, react.useState)(null);
2632
+ (0, react.useEffect)(() => {
2633
+ if (armedNote === null) return;
2634
+ const disarm = () => {
2635
+ setArmedNote(null);
2636
+ };
2637
+ const onKey = (e) => {
2638
+ if (e.key === "Escape") disarm();
2639
+ };
2640
+ window.addEventListener("click", disarm);
2641
+ window.addEventListener("keydown", onKey);
2642
+ return () => {
2643
+ window.removeEventListener("click", disarm);
2644
+ window.removeEventListener("keydown", onKey);
2645
+ };
2646
+ }, [armedNote]);
2647
+ const fire = (action) => {
2648
+ action.then(() => {
2649
+ setError(null);
2650
+ }, (err) => {
2651
+ setError(err instanceof Error ? err.message : String(err));
2652
+ });
2653
+ };
2593
2654
  const proseRef = (0, react.useRef)(null);
2594
2655
  const diagRef = (0, react.useRef)(null);
2595
2656
  (0, react.useEffect)(() => {
@@ -2645,11 +2706,24 @@ window.__ModuleLoader__.load({
2645
2706
  }, (0, react.createElement)("div", { className: "lks-zone-h" }, tr(labelKey)), ...lesson.notes.filter((n) => n.zone === zone).map((n) => (0, react.createElement)("div", {
2646
2707
  key: n.id,
2647
2708
  className: "lks-note"
2648
- }, (0, react.createElement)("span", { className: "lks-note-src" }, n.source), (0, react.createElement)("div", { className: "lks-note-title" }, n.title), (0, react.createElement)("div", {
2709
+ }, (0, react.createElement)("span", { className: "lks-note-src" }, n.source), (0, react.createElement)("button", {
2710
+ className: `lks-note-del${armedNote === n.id ? " armed" : ""}`,
2711
+ title: armedNote === n.id ? tr("note.delete.confirm") : tr("note.delete"),
2712
+ "aria-label": armedNote === n.id ? tr("note.delete.confirm") : tr("note.delete"),
2713
+ onClick: (e) => {
2714
+ e.stopPropagation();
2715
+ if (armedNote !== n.id) {
2716
+ setArmedNote(n.id);
2717
+ return;
2718
+ }
2719
+ setArmedNote(null);
2720
+ fire(deleteNote(lesson.lessonId, n.id));
2721
+ }
2722
+ }, (0, react.createElement)(IconTrashOutline16, { size: 12 })), (0, react.createElement)("div", { className: "lks-note-title" }, n.title), (0, react.createElement)("div", {
2649
2723
  className: "lks-note-text",
2650
2724
  dangerouslySetInnerHTML: { __html: renderMarkdown(n.text) }
2651
2725
  }), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `“${n.quote}”`) : null))))));
2652
- return (0, react.createElement)("div", { className: "lks-col lks-col-bb" }, (0, react.createElement)("div", { className: "lks-colhead" }, tr("col.bb")), body);
2726
+ return (0, react.createElement)("div", { className: "lks-col lks-col-bb" }, (0, react.createElement)("div", { className: "lks-colhead" }, tr("col.bb")), body, (0, react.createElement)(ActionError, { error }));
2653
2727
  }
2654
2728
  //#endregion
2655
2729
  //#region src/client/starter.tsx
@@ -2669,8 +2743,9 @@ window.__ModuleLoader__.load({
2669
2743
  * @returns the component for `conversation.input.left`.
2670
2744
  */
2671
2745
  function studyStartButton(ctx) {
2672
- return function StudyStartButton({ session }) {
2673
- if (session?.blank !== true) return null;
2746
+ return function StudyStartButton(props) {
2747
+ const machine = props.useSession?.((s) => s);
2748
+ if ((props.session?.blank ?? machine?.blank) !== true) return null;
2674
2749
  return (0, react.createElement)(Inner, {
2675
2750
  key: "inner",
2676
2751
  ctx
@@ -2693,7 +2768,7 @@ window.__ModuleLoader__.load({
2693
2768
  if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
2694
2769
  const { path } = await area.json();
2695
2770
  const workspace = await ctx.workspaces.create({ path });
2696
- const sessionId = await ctx.workspaces.connectWorkspace(workspace.workspaceId);
2771
+ const sessionId = ctx.workspaces.connectWorkspace !== void 0 ? await ctx.workspaces.connectWorkspace(workspace.workspaceId) : await ctx.sessions.create({ workspaceId: workspace.workspaceId });
2697
2772
  const actx = ctx.sessions.scope(sessionId);
2698
2773
  const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
2699
2774
  if (face === void 0) throw new Error("study session is not addressable yet");