dsh-plugin-lookatstudy 0.21.0 → 0.21.1

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
@@ -937,6 +937,12 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
937
937
  .lks14-composer{flex:none;display:flex;gap:6px;align-items:flex-end;padding:8px 12px 10px;border-top:1px solid var(--dsw-alias-border-l1)}
938
938
  .lks14-composertext{flex:1;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:7px 10px;resize:none;box-sizing:border-box}
939
939
  .lks14-composertext:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
940
+ /* issue #7: the composer's pending-attachment chip (thumbnail + name + remove) */
941
+ .lks14-attachchip{flex:none;display:flex;align-items:center;gap:8px;margin:0 0 6px;padding:5px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2);max-width:100%}
942
+ .lks14-attachchip-thumb{flex:none;width:44px;height:44px;object-fit:cover;border-radius:7px;border:1px solid var(--dsw-alias-border-l1)}
943
+ .lks14-attachchip-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;color:var(--dsw-alias-label-primary)}
944
+ .lks14-attachchip-x{flex:none;border:none;background:none;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;border-radius:6px;padding:2px 6px}
945
+ .lks14-attachchip-x:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
940
946
 
941
947
  /* ============================================================
942
948
  * v0.29 pane-resize 拖拽手柄(PaneResizeHandle)——
@@ -2179,6 +2185,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2179
2185
  "settings.budget.on": "开启历史预算",
2180
2186
  "settings.budget.off": "关闭历史预算",
2181
2187
  "attach.sent": "我发了一个文件:{name}(工作区路径 {path})——请读取它再继续。",
2188
+ "attach.remove": "移除待发附件",
2182
2189
  "attach.fail": "附件上传失败",
2183
2190
  "attach.label": "附件(≤20MiB,存入学习工作区)",
2184
2191
  "rail.mastered": "毕业 {mastered}/{total} 课",
@@ -2525,6 +2532,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2525
2532
  "settings.budget.on": "Enable history budget",
2526
2533
  "settings.budget.off": "Disable history budget",
2527
2534
  "attach.sent": "I attached a file: {name} (workspace path {path}) — read it before continuing.",
2535
+ "attach.remove": "Remove pending attachment",
2528
2536
  "attach.fail": "Attachment upload failed",
2529
2537
  "attach.label": "Attach (≤20MiB, lands in the study workspace)",
2530
2538
  "rail.mastered": "{mastered}/{total} lessons graduated",
@@ -2765,10 +2773,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2765
2773
  }
2766
2774
  /** Render inline markup (code, bold, italic, links, images) over escaped text.
2767
2775
  * Image src allowlist: https?:// or data:image/ — folder imports inline local
2768
- * images as data URLs (capped), GitHub imports reference jsDelivr; anything
2769
- * else stays literal text (no broken relative images, no exotic schemes). */
2776
+ * images as data URLs (capped), GitHub imports reference jsDelivr; the
2777
+ * plugin's own attachment route (/lookatstudy/api/attachment/<name>, issue #7
2778
+ * — same-origin, server-side validated) renders workspace attachments;
2779
+ * anything else stays literal text (no broken relative images, no exotic
2780
+ * schemes). */
2770
2781
  function inline(escaped) {
2771
- return escaped.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+|data:image\/[^)\s]+)\)/g, (_m, alt, src) => `<img src="${src}" alt="${alt}" loading="lazy">`).replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noreferrer\">$1</a>");
2782
+ return escaped.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+|data:image\/[^)\s]+|\/lookatstudy\/api\/attachment\/[^)\s]+)\)/g, (_m, alt, src) => `<img src="${src}" alt="${alt}" loading="lazy">`).replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noreferrer\">$1</a>");
2772
2783
  }
2773
2784
  /** True when the line opens a GFM table row (pipes with a delimiter row next). */
2774
2785
  function isTableRow(line) {
@@ -8504,6 +8515,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8504
8515
  const stuck = (0, react.useRef)(true);
8505
8516
  const [showFab, setShowFab] = (0, react.useState)(false);
8506
8517
  const [threadsOpen, setThreadsOpen] = (0, react.useState)(false);
8518
+ const [pendingAttach, setPendingAttach] = (0, react.useState)(null);
8507
8519
  const onStreamScroll = () => {
8508
8520
  const el = streamEl.current;
8509
8521
  if (el === null) return;
@@ -8755,18 +8767,31 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8755
8767
  for (let i = 0; i < bytes.length; i += CHUNK) bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
8756
8768
  return uploadAttachment(file.name, btoa(bin));
8757
8769
  }).then((path) => {
8758
- send(tr("attach.sent", {
8770
+ setPendingAttach({
8759
8771
  name: file.name,
8760
- path
8761
- }));
8772
+ path,
8773
+ image: /\.(?:png|jpe?g|gif|webp|bmp)$/i.test(file.name)
8774
+ });
8762
8775
  }, () => {
8763
8776
  showStudyToast(tr("attach.fail"), { severity: "error" });
8764
8777
  });
8765
8778
  };
8779
+ /** One send = draft + pending attachment merged into a single message (issue #7). */
8780
+ const submitComposer = () => {
8781
+ if (busy) return;
8782
+ const text = draft.trim();
8783
+ if (text === "" && pendingAttach === null) return;
8784
+ const attachLine = pendingAttach === null ? "" : tr("attach.sent", {
8785
+ name: pendingAttach.name,
8786
+ path: pendingAttach.path
8787
+ });
8788
+ setPendingAttach(null);
8789
+ send(attachLine === "" ? text : text === "" ? attachLine : `${text}\n\n${attachLine}`);
8790
+ };
8766
8791
  const onComposerKey = (e) => {
8767
8792
  if (e.key === "Enter" && !e.shiftKey) {
8768
8793
  e.preventDefault();
8769
- if (draft.trim() !== "" && !busy) send(draft);
8794
+ if ((draft.trim() !== "" || pendingAttach !== null) && !busy) submitComposer();
8770
8795
  }
8771
8796
  };
8772
8797
  const proposal = data?.pendingProposals[0] ?? null;
@@ -8893,7 +8918,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8893
8918
  onClick: () => {
8894
8919
  if (!dormant) fire(setMode(mode.id));
8895
8920
  }
8896
- }, mode.id === "direct" ? (0, react.createElement)(IconBoltFill16, { size: 12 }) : mode.id === "guide" ? (0, react.createElement)(IconGoalOutline16, { size: 12 }) : (0, react.createElement)(IconBookFill16, { size: 12 }), tr(mode.labelKey))))), (0, react.createElement)("textarea", {
8921
+ }, mode.id === "direct" ? (0, react.createElement)(IconBoltFill16, { size: 12 }) : mode.id === "guide" ? (0, react.createElement)(IconGoalOutline16, { size: 12 }) : (0, react.createElement)(IconBookFill16, { size: 12 }), tr(mode.labelKey))))), pendingAttach !== null ? (0, react.createElement)("div", { className: "lks14-attachchip" }, pendingAttach.image ? (0, react.createElement)("img", {
8922
+ className: "lks14-attachchip-thumb",
8923
+ src: `/lookatstudy/api/attachment/${encodeURIComponent(pendingAttach.path.split("/").pop() ?? "")}`,
8924
+ alt: pendingAttach.name
8925
+ }) : null, (0, react.createElement)("span", { className: "lks14-attachchip-name" }, pendingAttach.name), (0, react.createElement)("button", {
8926
+ className: "lks14-attachchip-x",
8927
+ "aria-label": tr("attach.remove"),
8928
+ "data-tooltip": tr("attach.remove"),
8929
+ onClick: () => {
8930
+ setPendingAttach(null);
8931
+ }
8932
+ }, "✕")) : null, (0, react.createElement)("textarea", {
8897
8933
  className: "lks14-composertext",
8898
8934
  placeholder: busy ? tr("composer.busy") : tr("tutor.empty.hint"),
8899
8935
  value: draft,
@@ -8983,9 +9019,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8983
9019
  }, (0, react.createElement)(IconCloseFill16, { size: 14 })) : (0, react.createElement)("button", {
8984
9020
  className: "lks-btn-send",
8985
9021
  "aria-label": tr("composer.send"),
8986
- disabled: dormant || draft.trim() === "",
9022
+ disabled: dormant || draft.trim() === "" && pendingAttach === null,
8987
9023
  onClick: () => {
8988
- send(draft);
9024
+ submitComposer();
8989
9025
  }
8990
9026
  }, (0, react.createElement)(IconArrowUpFill16, { size: 18 }))), showFab ? (0, react.createElement)("button", {
8991
9027
  className: `lks14-scrollfab${busy ? " streaming" : ""}`,
@@ -9008,6 +9044,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
9008
9044
  const [zoneOverrides, setZoneOverrides] = (0, react.useState)({});
9009
9045
  const zoneCounts = (0, react.useRef)({});
9010
9046
  const [editingNote, setEditingNote] = (0, react.useState)(null);
9047
+ const [noteRev, setNoteRev] = (0, react.useState)(0);
9011
9048
  const [editDraft, setEditDraft] = (0, react.useState)("");
9012
9049
  const [pendingLocate, setPendingLocate] = (0, react.useState)(null);
9013
9050
  const [cmapExpanded, setCmapExpanded] = (0, react.useState)(false);
@@ -9431,7 +9468,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
9431
9468
  }));
9432
9469
  }
9433
9470
  }, tr(labelKey), (0, react.createElement)("span", { className: "lks14-zonecount" }, String(zoneNotes.length)), (0, react.createElement)("span", { className: "lks14-zonecaret" }, open ? "▾" : "▸")), ...open ? zoneNotes.map((n) => (0, react.createElement)("div", {
9434
- key: n.id,
9471
+ key: `${n.id}:${String(noteRev)}`,
9435
9472
  className: `lks-note${n.pinned ? " pinned" : ""}`
9436
9473
  }, (0, react.createElement)("span", { className: "lks-note-src" }, tr("note.src." + n.source)), (0, react.createElement)("button", {
9437
9474
  className: "lks-note-act",
@@ -9492,6 +9529,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
9492
9529
  disabled: editDraft.trim() === "",
9493
9530
  onClick: () => {
9494
9531
  setEditingNote(null);
9532
+ setNoteRev((r) => r + 1);
9495
9533
  fire(editNote(lesson.lessonId, n.id, editDraft));
9496
9534
  }
9497
9535
  }, tr("note.save")))) : (0, react.createElement)(ContentBoundary, {
@@ -9499,7 +9537,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
9499
9537
  boundaryKey: `note-${n.id}`
9500
9538
  }, (0, react.createElement)("div", {
9501
9539
  className: "lks-note-text",
9502
- dangerouslySetInnerHTML: { __html: renderMarkdown(n.text) }
9540
+ ref: (el) => {
9541
+ if (el !== null) enhanceRendered(el).catch(() => {});
9542
+ },
9543
+ dangerouslySetInnerHTML: { __html: renderMarkdown(normalizeMathNotation(n.text)) }
9503
9544
  })), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `'${n.quote}'`) : null)) : []);
9504
9545
  })));
9505
9546
  return (0, react.createElement)("div", { className: "lks14-col lks14-note" }, body, confirmNoteDel !== null ? (0, react.createElement)(ConfirmCard, {