dsh-plugin-lookatstudy 0.20.1 → 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 +289 -15
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +172 -49
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -817,6 +817,11 @@ window.__ModuleLoader__.load({
|
|
|
817
817
|
.lks-ui[data-lks-theme='light'] .lks14-threadmenu{background:rgb(255 255 255/0.98);border-color:var(--border-faint);box-shadow:0 12px 32px -8px rgb(0 0 0/0.18)}
|
|
818
818
|
.lks-ui[data-lks-theme='light'] .lks14-ctxpanel{background:rgb(255 255 255/0.98);box-shadow:0 8px 24px rgb(0 0 0/0.12)}
|
|
819
819
|
/* ambient glyph motion sleeps under reduced-motion (A7 never had a guard) */
|
|
820
|
+
/* v0.29 pane-resize: the grip line lights the upstream accent under the skin
|
|
821
|
+
(base sheet's host-token color is the no-skin fallback; the handle itself
|
|
822
|
+
is structural lks14- and lives in styles.ts) */
|
|
823
|
+
.lks-ui .lks14-panehandle:hover::after,.lks-ui .lks14-panehandle[data-dragging='true']::after{background:color-mix(in srgb,var(--accent) 55%,transparent)}
|
|
824
|
+
|
|
820
825
|
@media (prefers-reduced-motion: reduce){
|
|
821
826
|
.lks-ui .lks-lessorow.st-mastered .lks-lessorow-glyph svg,.lks-ui .lks-hdr-xp .lks-hdr-glyph,.lks-ui .lks-hdr-streak .lks-hdr-glyph{animation:none}
|
|
822
827
|
}
|
|
@@ -887,7 +892,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
887
892
|
.lks14-col{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
|
|
888
893
|
|
|
889
894
|
/* 左 rail: course picker, tree, review, import */
|
|
890
|
-
.lks14-rail{flex:0 0 300px;border-right:none;padding:0;overflow:hidden;position:relative}
|
|
895
|
+
.lks14-rail{flex:0 0 var(--lks-rail-w,300px);border-right:none;padding:0;overflow:hidden;position:relative;transition:flex-basis 150ms ease}
|
|
891
896
|
.lks14-railhead{display:flex;align-items:center;gap:6px;margin-top:10px}
|
|
892
897
|
.lks14-railtitle{font-weight:600;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
893
898
|
.lks14-railsub{font-size:12px;color:var(--dsw-alias-label-secondary);margin:6px 0}
|
|
@@ -902,7 +907,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
902
907
|
.lks14-hint{font-size:11.5px;color:var(--dsw-alias-label-tertiary);margin-top:6px;line-height:1.6}
|
|
903
908
|
|
|
904
909
|
/* 中 chat: the tutor stream + its own composer (upstream ChatStream/ChatComposer) */
|
|
905
|
-
.lks14-chat{flex:0 0 auto;width:clamp(480px,45%,800px);min-width:0;border-right:none}
|
|
910
|
+
.lks14-chat{flex:0 0 auto;width:var(--lks-chat-w,clamp(480px,45%,800px));min-width:0;border-right:none;transition:width 150ms ease}
|
|
906
911
|
.lks14-chatlesson{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;color:var(--dsw-alias-label-tertiary)}
|
|
907
912
|
.lks14-pills{display:inline-flex;gap:2px}
|
|
908
913
|
.lks14-pill{border:none;background:none;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;padding:2px 8px;border-radius:7px;cursor:pointer}
|
|
@@ -932,6 +937,28 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
932
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)}
|
|
933
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}
|
|
934
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)}
|
|
946
|
+
|
|
947
|
+
/* ============================================================
|
|
948
|
+
* v0.29 pane-resize 拖拽手柄(PaneResizeHandle)——
|
|
949
|
+
* 栏间 6px 竖条,视觉握线默认隐形、hover/拖拽亮 accent;
|
|
950
|
+
* 命中区触屏 44px(触屏密度红线),细指针收窄 12px(不吃栏内容点击)。
|
|
951
|
+
* pane 分隔语汇 = surface 色阶不加描边:握线是 hover 才现身的交互
|
|
952
|
+
* affordance,不是常驻分隔线,不违反 v0.6「pane 间无 border」。
|
|
953
|
+
* ============================================================ */
|
|
954
|
+
.lks14-panehandle{position:relative;flex:none;width:6px;cursor:col-resize;touch-action:none;z-index:5}
|
|
955
|
+
.lks14-panehandle::before{content:"";position:absolute;top:0;bottom:0;left:50%;width:44px;transform:translateX(-50%)}
|
|
956
|
+
@media (pointer:fine){.lks14-panehandle::before{width:12px}}
|
|
957
|
+
.lks14-panehandle::after{content:"";position:absolute;top:0;bottom:0;left:50%;width:2px;transform:translateX(-50%);border-radius:1px;background:transparent;transition:background 120ms}
|
|
958
|
+
.lks14-panehandle:hover::after,.lks14-panehandle[data-dragging='true']::after{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent)}
|
|
959
|
+
/* 拖拽中:全局列宽光标 + 禁文本选择(宽度直写不经过 React) */
|
|
960
|
+
body.pane-dragging{cursor:col-resize;user-select:none}
|
|
961
|
+
body.pane-dragging *{cursor:col-resize}
|
|
935
962
|
|
|
936
963
|
/* 右 notebook: 讲解/概念图/笔记 (upstream NotebookPanel) */
|
|
937
964
|
.lks14-note{flex:1 1 auto;min-width:440px;padding:0;overflow-y:auto}
|
|
@@ -1001,6 +1028,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
1001
1028
|
.lks14-body[data-pane='rail'] .lks14-righthalf{display:none}
|
|
1002
1029
|
.lks14-body[data-pane='chat'] .lks14-rail,.lks14-body[data-pane='chat'] .lks14-note,
|
|
1003
1030
|
.lks14-body[data-pane='note'] .lks14-rail,.lks14-body[data-pane='note'] .lks14-chat{display:none}
|
|
1031
|
+
.lks14-panehandle{display:none}
|
|
1004
1032
|
.lks14-body[data-pane='rail'] .lks14-rail{display:flex;max-height:none;flex:1 1 auto}
|
|
1005
1033
|
.lks14-body[data-pane='note'] .lks14-note{display:flex}
|
|
1006
1034
|
}
|
|
@@ -2129,6 +2157,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2129
2157
|
"note.retryRender": "重试渲染",
|
|
2130
2158
|
"zoom.in": "放大面板字号",
|
|
2131
2159
|
"zoom.out": "缩小面板字号",
|
|
2160
|
+
"pane.rail.aria": "调整课程栏宽度",
|
|
2161
|
+
"pane.chat.aria": "调整对话栏宽度",
|
|
2162
|
+
"pane.tip": "拖拽调宽,双击重置",
|
|
2132
2163
|
"palette.label": "命令面板",
|
|
2133
2164
|
"palette.placeholder": "搜索课时/课程,或选择动作…",
|
|
2134
2165
|
"palette.lessons": "课时",
|
|
@@ -2154,6 +2185,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2154
2185
|
"settings.budget.on": "开启历史预算",
|
|
2155
2186
|
"settings.budget.off": "关闭历史预算",
|
|
2156
2187
|
"attach.sent": "我发了一个文件:{name}(工作区路径 {path})——请读取它再继续。",
|
|
2188
|
+
"attach.remove": "移除待发附件",
|
|
2157
2189
|
"attach.fail": "附件上传失败",
|
|
2158
2190
|
"attach.label": "附件(≤20MiB,存入学习工作区)",
|
|
2159
2191
|
"rail.mastered": "毕业 {mastered}/{total} 课",
|
|
@@ -2472,6 +2504,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2472
2504
|
"note.retryRender": "Retry render",
|
|
2473
2505
|
"zoom.in": "Increase panel font size",
|
|
2474
2506
|
"zoom.out": "Decrease panel font size",
|
|
2507
|
+
"pane.rail.aria": "Resize the course pane width",
|
|
2508
|
+
"pane.chat.aria": "Resize the chat pane width",
|
|
2509
|
+
"pane.tip": "Drag to resize, double-click to reset",
|
|
2475
2510
|
"palette.label": "Command palette",
|
|
2476
2511
|
"palette.placeholder": "Search lessons/courses or pick an action…",
|
|
2477
2512
|
"palette.lessons": "Lesson",
|
|
@@ -2497,6 +2532,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2497
2532
|
"settings.budget.on": "Enable history budget",
|
|
2498
2533
|
"settings.budget.off": "Disable history budget",
|
|
2499
2534
|
"attach.sent": "I attached a file: {name} (workspace path {path}) — read it before continuing.",
|
|
2535
|
+
"attach.remove": "Remove pending attachment",
|
|
2500
2536
|
"attach.fail": "Attachment upload failed",
|
|
2501
2537
|
"attach.label": "Attach (≤20MiB, lands in the study workspace)",
|
|
2502
2538
|
"rail.mastered": "{mastered}/{total} lessons graduated",
|
|
@@ -2737,10 +2773,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2737
2773
|
}
|
|
2738
2774
|
/** Render inline markup (code, bold, italic, links, images) over escaped text.
|
|
2739
2775
|
* Image src allowlist: https?:// or data:image/ — folder imports inline local
|
|
2740
|
-
* images as data URLs (capped), GitHub imports reference jsDelivr;
|
|
2741
|
-
*
|
|
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). */
|
|
2742
2781
|
function inline(escaped) {
|
|
2743
|
-
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>");
|
|
2744
2783
|
}
|
|
2745
2784
|
/** True when the line opens a GFM table row (pipes with a delimiter row next). */
|
|
2746
2785
|
function isTableRow(line) {
|
|
@@ -3327,6 +3366,149 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3327
3366
|
}
|
|
3328
3367
|
return out.join("\n");
|
|
3329
3368
|
}
|
|
3369
|
+
const CHAT_MAX = 1100;
|
|
3370
|
+
const clampNum = (v, min, max) => Math.min(max, Math.max(min, v));
|
|
3371
|
+
/** 对话栏未定制时的默认宽数值形式(= clamp(480px,45%,800px) 在 rowW 下;rowW = 容器宽 − rail 有效宽)。 */
|
|
3372
|
+
function defaultChatWidth(rowW) {
|
|
3373
|
+
return clampNum(rowW * .45, 480, 800);
|
|
3374
|
+
}
|
|
3375
|
+
/** 课程栏拖拽候选钳制:reserved = 对话栏有效宽 + NOTE_MIN + 手柄。 */
|
|
3376
|
+
function clampRailCandidate(next, containerW, reserved) {
|
|
3377
|
+
return clampNum(next, 240, Math.min(480, Math.max(240, containerW - reserved)));
|
|
3378
|
+
}
|
|
3379
|
+
/** 对话栏拖拽候选钳制:reserved = 课程栏有效宽 + NOTE_MIN;极端挤压允许压到硬底。 */
|
|
3380
|
+
function clampChatCandidate(next, rowW, reserved) {
|
|
3381
|
+
return clampNum(next, 480, Math.min(CHAT_MAX, Math.max(320, rowW - reserved)));
|
|
3382
|
+
}
|
|
3383
|
+
/** 存储值解析:空/非数/超区间 → null(回默认;历史脏值不硬吃)。 */
|
|
3384
|
+
function parseStoredWidth(v, min, max) {
|
|
3385
|
+
if (v == null || v.trim() === "") return null;
|
|
3386
|
+
const n = Number(v);
|
|
3387
|
+
if (!Number.isFinite(n) || n < min || n > max) return null;
|
|
3388
|
+
return Math.round(n);
|
|
3389
|
+
}
|
|
3390
|
+
/**
|
|
3391
|
+
* The panel root's font-zoom factor (E7 zoom scales the px-fixed layout) —
|
|
3392
|
+
* getBoundingClientRect returns VISUAL px under zoom while flex-basis/width
|
|
3393
|
+
* apply in LAYOUT px, so every drag measurement divides by this (upstream had
|
|
3394
|
+
* no zoom; this is the one place our port must normalize).
|
|
3395
|
+
*/
|
|
3396
|
+
function panelZoomOf(el) {
|
|
3397
|
+
const z = el?.closest(".lks14")?.style.zoom;
|
|
3398
|
+
const n = z === void 0 || z === "" ? NaN : Number(z);
|
|
3399
|
+
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* 渲染时有效宽度求解(持久化值 × 当前容器宽):拖拽钳制保证提交时刻不溢出,
|
|
3403
|
+
* 但宿主列后来变窄会破坏旧组合 —— 存储值按当前容器重新压回预算。
|
|
3404
|
+
* 返回 null = 未定制(挂 CSS 默认)。求解序 acyclic:rail 基准(默认 300 是
|
|
3405
|
+
* 常数)→ row 宽 → chat 默认(% 对 row)→ rail 预算按 chat 有效宽。
|
|
3406
|
+
*/
|
|
3407
|
+
function solvePaneWidths(railStored, chatStored, containerW) {
|
|
3408
|
+
const rowW = Math.max(0, containerW - (railStored ?? 300) - 12);
|
|
3409
|
+
const chat = chatStored == null ? null : clampChatCandidate(chatStored, rowW, 440);
|
|
3410
|
+
const chatBasis = chat ?? defaultChatWidth(rowW);
|
|
3411
|
+
return {
|
|
3412
|
+
rail: railStored == null ? null : clampRailCandidate(railStored, containerW, chatBasis + 440 + 12),
|
|
3413
|
+
chat
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
//#endregion
|
|
3417
|
+
//#region src/client/paneresize.tsx
|
|
3418
|
+
/**
|
|
3419
|
+
* PaneResizeHandle —— 栏间拖拽手柄(ported from upstream LookatStudy v0.29
|
|
3420
|
+
* issue #14, src/renderer/components/PaneResizeHandle.tsx @ v0.33.2, MIT —
|
|
3421
|
+
* adapted 2026-09-12; structural deviations recorded here):
|
|
3422
|
+
* - the drag target is the handle's OWN previousElementSibling (the column it
|
|
3423
|
+
* follows in the DOM), not a document.querySelector testid anchor;
|
|
3424
|
+
* - rail widths apply through flex-basis (its class is flex:0 0 basis), chat
|
|
3425
|
+
* widths through width (flex-basis:auto) — the side prop picks the channel;
|
|
3426
|
+
* - on commit the parent's layout effect clears the inline width once the
|
|
3427
|
+
* container-level CSS vars carry the truth (same paint, no flash), so this
|
|
3428
|
+
* component only writes during the drag and hands over on finish.
|
|
3429
|
+
*
|
|
3430
|
+
* 命中区触屏 44px / 细指针 12px(styles.ts ::before);拖拽期宽度 imperative
|
|
3431
|
+
* 直写目标元素(对话栏是消息流整树,逐帧 setState 不可接受),100ms 节流,
|
|
3432
|
+
* 松手终值必达;双击 = 重置回响应式默认(清 inline + onCommit(null),类/clamp
|
|
3433
|
+
* 接管)。a11y:role=separator + aria-label;键盘调整不在本期范围。
|
|
3434
|
+
*/
|
|
3435
|
+
/** 拖拽中宽度应用节流(松手必应用终值,不受节流影响)。 */
|
|
3436
|
+
const DRAG_APPLY_MIN_MS = 100;
|
|
3437
|
+
/** Inline width ← px;null clears it (class/var/CSS default takes over). */
|
|
3438
|
+
function applyWidth(el, side, px) {
|
|
3439
|
+
if (side === "rail") el.style.flexBasis = px === null ? "" : `${String(px)}px`;
|
|
3440
|
+
else el.style.width = px === null ? "" : `${String(px)}px`;
|
|
3441
|
+
}
|
|
3442
|
+
function PaneResizeHandle(props) {
|
|
3443
|
+
const [dragging, setDragging] = (0, react.useState)(false);
|
|
3444
|
+
const dragRef = (0, react.useRef)(null);
|
|
3445
|
+
/** props 进 effect 闭包会滞后:拖拽全程用 ref 取最新。 */
|
|
3446
|
+
const propsRef = (0, react.useRef)(props);
|
|
3447
|
+
propsRef.current = props;
|
|
3448
|
+
(0, react.useEffect)(() => {
|
|
3449
|
+
if (!dragging) return;
|
|
3450
|
+
document.body.classList.add("pane-dragging");
|
|
3451
|
+
const onMove = (e) => {
|
|
3452
|
+
const d = dragRef.current;
|
|
3453
|
+
if (d == null) return;
|
|
3454
|
+
const now = performance.now();
|
|
3455
|
+
if (now - d.lastAppliedAt < DRAG_APPLY_MIN_MS) return;
|
|
3456
|
+
d.lastAppliedAt = now;
|
|
3457
|
+
applyWidth(d.targetEl, propsRef.current.side, propsRef.current.clampLive(d.startW + (e.clientX - d.startX)));
|
|
3458
|
+
};
|
|
3459
|
+
const finish = (e) => {
|
|
3460
|
+
const d = dragRef.current;
|
|
3461
|
+
dragRef.current = null;
|
|
3462
|
+
setDragging(false);
|
|
3463
|
+
if (d == null) return;
|
|
3464
|
+
d.targetEl.style.transition = "";
|
|
3465
|
+
const finalW = propsRef.current.clampLive(d.startW + (e.clientX - d.startX));
|
|
3466
|
+
applyWidth(d.targetEl, propsRef.current.side, finalW);
|
|
3467
|
+
propsRef.current.onCommit(finalW);
|
|
3468
|
+
};
|
|
3469
|
+
window.addEventListener("pointermove", onMove);
|
|
3470
|
+
window.addEventListener("pointerup", finish);
|
|
3471
|
+
window.addEventListener("pointercancel", finish);
|
|
3472
|
+
return () => {
|
|
3473
|
+
document.body.classList.remove("pane-dragging");
|
|
3474
|
+
window.removeEventListener("pointermove", onMove);
|
|
3475
|
+
window.removeEventListener("pointerup", finish);
|
|
3476
|
+
window.removeEventListener("pointercancel", finish);
|
|
3477
|
+
};
|
|
3478
|
+
}, [dragging]);
|
|
3479
|
+
const startDrag = (e) => {
|
|
3480
|
+
if (e.button !== 0) return;
|
|
3481
|
+
const target = e.currentTarget.previousElementSibling;
|
|
3482
|
+
if (!(target instanceof HTMLElement)) return;
|
|
3483
|
+
e.preventDefault();
|
|
3484
|
+
dragRef.current = {
|
|
3485
|
+
startX: e.clientX,
|
|
3486
|
+
startW: target.getBoundingClientRect().width / panelZoomOf(target),
|
|
3487
|
+
lastAppliedAt: 0,
|
|
3488
|
+
targetEl: target
|
|
3489
|
+
};
|
|
3490
|
+
target.style.transition = "none";
|
|
3491
|
+
setDragging(true);
|
|
3492
|
+
try {
|
|
3493
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
3494
|
+
} catch {}
|
|
3495
|
+
};
|
|
3496
|
+
return (0, react.createElement)("div", {
|
|
3497
|
+
role: "separator",
|
|
3498
|
+
"aria-orientation": "vertical",
|
|
3499
|
+
"aria-label": props.side === "rail" ? tr("pane.rail.aria") : tr("pane.chat.aria"),
|
|
3500
|
+
"data-tooltip": tr("pane.tip"),
|
|
3501
|
+
"data-testid": `pane-handle-${props.side}`,
|
|
3502
|
+
"data-dragging": dragging ? "true" : void 0,
|
|
3503
|
+
className: "lks14-panehandle",
|
|
3504
|
+
onPointerDown: startDrag,
|
|
3505
|
+
onDoubleClick: (e) => {
|
|
3506
|
+
const target = e.currentTarget.previousElementSibling;
|
|
3507
|
+
if (target instanceof HTMLElement) applyWidth(target, props.side, null);
|
|
3508
|
+
props.onCommit(null);
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3330
3512
|
//#endregion
|
|
3331
3513
|
//#region src/client/views.tsx
|
|
3332
3514
|
/**
|
|
@@ -6948,6 +7130,56 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6948
7130
|
localStorage.setItem("dsh-plugin-lookatstudy:zoom", String(clamped));
|
|
6949
7131
|
} catch {}
|
|
6950
7132
|
};
|
|
7133
|
+
const [railW, setRailW] = (0, react.useState)(() => parseStoredWidth(localStorage.getItem("dsh-plugin-lookatstudy:pane-rail"), 240, 480));
|
|
7134
|
+
const [chatW, setChatW] = (0, react.useState)(() => parseStoredWidth(localStorage.getItem("dsh-plugin-lookatstudy:pane-chat"), 480, CHAT_MAX));
|
|
7135
|
+
const [paneBoxW, setPaneBoxW] = (0, react.useState)(0);
|
|
7136
|
+
const bodyEl = (0, react.useRef)(null);
|
|
7137
|
+
const rowEl = (0, react.useRef)(null);
|
|
7138
|
+
const panesCustom = railW !== null || chatW !== null;
|
|
7139
|
+
(0, react.useEffect)(() => {
|
|
7140
|
+
const el = bodyEl.current;
|
|
7141
|
+
if (el === null || !panesCustom) return;
|
|
7142
|
+
const ro = new ResizeObserver((entries) => {
|
|
7143
|
+
for (const entry of entries) setPaneBoxW(Math.round(entry.contentRect.width));
|
|
7144
|
+
});
|
|
7145
|
+
ro.observe(el);
|
|
7146
|
+
return () => {
|
|
7147
|
+
ro.disconnect();
|
|
7148
|
+
};
|
|
7149
|
+
}, [panesCustom]);
|
|
7150
|
+
const solvedPanes = solvePaneWidths(railW, chatW, paneBoxW > 0 ? paneBoxW : bodyEl.current?.clientWidth ?? 0);
|
|
7151
|
+
(0, react.useLayoutEffect)(() => {
|
|
7152
|
+
const el = bodyEl.current;
|
|
7153
|
+
if (el === null) return;
|
|
7154
|
+
if (solvedPanes.rail !== null) el.style.setProperty("--lks-rail-w", `${String(solvedPanes.rail)}px`);
|
|
7155
|
+
else el.style.removeProperty("--lks-rail-w");
|
|
7156
|
+
if (solvedPanes.chat !== null) el.style.setProperty("--lks-chat-w", `${String(solvedPanes.chat)}px`);
|
|
7157
|
+
else el.style.removeProperty("--lks-chat-w");
|
|
7158
|
+
for (const col of el.querySelectorAll(".lks14-rail, .lks14-chat")) {
|
|
7159
|
+
col.style.flexBasis = "";
|
|
7160
|
+
col.style.width = "";
|
|
7161
|
+
}
|
|
7162
|
+
}, [solvedPanes.rail, solvedPanes.chat]);
|
|
7163
|
+
const commitPaneWidth = (which, px) => {
|
|
7164
|
+
const key = which === "rail" ? "dsh-plugin-lookatstudy:pane-rail" : "dsh-plugin-lookatstudy:pane-chat";
|
|
7165
|
+
const value = px === null ? null : Math.round(px);
|
|
7166
|
+
if (which === "rail") setRailW(value);
|
|
7167
|
+
else setChatW(value);
|
|
7168
|
+
try {
|
|
7169
|
+
if (value === null) localStorage.removeItem(key);
|
|
7170
|
+
else localStorage.setItem(key, String(value));
|
|
7171
|
+
} catch {}
|
|
7172
|
+
};
|
|
7173
|
+
const railClampLive = (px) => {
|
|
7174
|
+
const el = bodyEl.current;
|
|
7175
|
+
const containerW = (el?.getBoundingClientRect().width ?? 1280) / panelZoomOf(el);
|
|
7176
|
+
const rowW = Math.max(0, containerW - 300 - 12);
|
|
7177
|
+
return clampRailCandidate(px, containerW, (solvedPanes.chat ?? defaultChatWidth(rowW)) + 440 + 12);
|
|
7178
|
+
};
|
|
7179
|
+
const chatClampLive = (px) => {
|
|
7180
|
+
const el = rowEl.current;
|
|
7181
|
+
return clampChatCandidate(px, (el?.getBoundingClientRect().width ?? 900) / panelZoomOf(el), 440);
|
|
7182
|
+
};
|
|
6951
7183
|
const paletteBus = (0, react.useRef)({});
|
|
6952
7184
|
const [paletteOpen, setPaletteOpen] = (0, react.useState)(false);
|
|
6953
7185
|
(0, react.useEffect)(() => {
|
|
@@ -7279,6 +7511,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7279
7511
|
const progress = data?.progress ?? null;
|
|
7280
7512
|
const body = (0, react.createElement)("div", {
|
|
7281
7513
|
className: "lks14-body",
|
|
7514
|
+
ref: bodyEl,
|
|
7282
7515
|
"data-pane": narrowPane,
|
|
7283
7516
|
onTouchStart: (e) => {
|
|
7284
7517
|
const t = e.touches[0];
|
|
@@ -7310,6 +7543,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7310
7543
|
importProgress,
|
|
7311
7544
|
stop,
|
|
7312
7545
|
paletteBus
|
|
7546
|
+
}), (0, react.createElement)(PaneResizeHandle, {
|
|
7547
|
+
side: "rail",
|
|
7548
|
+
clampLive: railClampLive,
|
|
7549
|
+
onCommit: (px) => commitPaneWidth("rail", px)
|
|
7313
7550
|
}), (0, react.createElement)("div", { className: "lks14-righthalf" }, (0, react.createElement)("div", { className: "lks14-appheader" }, (0, react.createElement)("span", { className: "lks-hdr-title" }, lesson?.courseTitle ?? tr("tab.label")), (0, react.createElement)("span", {
|
|
7314
7551
|
className: "lks-hdr-xp",
|
|
7315
7552
|
"data-tooltip": tr("header.xp", { xp: progress?.totalXp ?? 0 })
|
|
@@ -7339,7 +7576,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7339
7576
|
onClick: () => {
|
|
7340
7577
|
setZoom(zoomTier + .1);
|
|
7341
7578
|
}
|
|
7342
|
-
}, "A+"))), (0, react.createElement)("div", {
|
|
7579
|
+
}, "A+"))), (0, react.createElement)("div", {
|
|
7580
|
+
className: "lks14-row",
|
|
7581
|
+
ref: rowEl
|
|
7582
|
+
}, examLessonActive ? (0, react.createElement)("div", { className: "lks14-col lks14-chat" }, (0, react.createElement)(ExamView, {
|
|
7343
7583
|
lessonId: lesson.lessonId,
|
|
7344
7584
|
sectionTitle: lesson.sectionTitle,
|
|
7345
7585
|
paused: examLeave !== null,
|
|
@@ -7365,6 +7605,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7365
7605
|
contextMeter,
|
|
7366
7606
|
contextBreakdown: breakdown,
|
|
7367
7607
|
uploadAttachment
|
|
7608
|
+
}), (0, react.createElement)(PaneResizeHandle, {
|
|
7609
|
+
side: "chat",
|
|
7610
|
+
clampLive: chatClampLive,
|
|
7611
|
+
onCommit: (px) => commitPaneWidth("chat", px)
|
|
7368
7612
|
}), (0, react.createElement)(NotebookPane, {
|
|
7369
7613
|
data,
|
|
7370
7614
|
deleteNote,
|
|
@@ -8271,6 +8515,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8271
8515
|
const stuck = (0, react.useRef)(true);
|
|
8272
8516
|
const [showFab, setShowFab] = (0, react.useState)(false);
|
|
8273
8517
|
const [threadsOpen, setThreadsOpen] = (0, react.useState)(false);
|
|
8518
|
+
const [pendingAttach, setPendingAttach] = (0, react.useState)(null);
|
|
8274
8519
|
const onStreamScroll = () => {
|
|
8275
8520
|
const el = streamEl.current;
|
|
8276
8521
|
if (el === null) return;
|
|
@@ -8522,18 +8767,31 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8522
8767
|
for (let i = 0; i < bytes.length; i += CHUNK) bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
8523
8768
|
return uploadAttachment(file.name, btoa(bin));
|
|
8524
8769
|
}).then((path) => {
|
|
8525
|
-
|
|
8770
|
+
setPendingAttach({
|
|
8526
8771
|
name: file.name,
|
|
8527
|
-
path
|
|
8528
|
-
|
|
8772
|
+
path,
|
|
8773
|
+
image: /\.(?:png|jpe?g|gif|webp|bmp)$/i.test(file.name)
|
|
8774
|
+
});
|
|
8529
8775
|
}, () => {
|
|
8530
8776
|
showStudyToast(tr("attach.fail"), { severity: "error" });
|
|
8531
8777
|
});
|
|
8532
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
|
+
};
|
|
8533
8791
|
const onComposerKey = (e) => {
|
|
8534
8792
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
8535
8793
|
e.preventDefault();
|
|
8536
|
-
if (draft.trim() !== "" && !busy)
|
|
8794
|
+
if ((draft.trim() !== "" || pendingAttach !== null) && !busy) submitComposer();
|
|
8537
8795
|
}
|
|
8538
8796
|
};
|
|
8539
8797
|
const proposal = data?.pendingProposals[0] ?? null;
|
|
@@ -8660,7 +8918,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8660
8918
|
onClick: () => {
|
|
8661
8919
|
if (!dormant) fire(setMode(mode.id));
|
|
8662
8920
|
}
|
|
8663
|
-
}, 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)("
|
|
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", {
|
|
8664
8933
|
className: "lks14-composertext",
|
|
8665
8934
|
placeholder: busy ? tr("composer.busy") : tr("tutor.empty.hint"),
|
|
8666
8935
|
value: draft,
|
|
@@ -8750,9 +9019,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8750
9019
|
}, (0, react.createElement)(IconCloseFill16, { size: 14 })) : (0, react.createElement)("button", {
|
|
8751
9020
|
className: "lks-btn-send",
|
|
8752
9021
|
"aria-label": tr("composer.send"),
|
|
8753
|
-
disabled: dormant || draft.trim() === "",
|
|
9022
|
+
disabled: dormant || draft.trim() === "" && pendingAttach === null,
|
|
8754
9023
|
onClick: () => {
|
|
8755
|
-
|
|
9024
|
+
submitComposer();
|
|
8756
9025
|
}
|
|
8757
9026
|
}, (0, react.createElement)(IconArrowUpFill16, { size: 18 }))), showFab ? (0, react.createElement)("button", {
|
|
8758
9027
|
className: `lks14-scrollfab${busy ? " streaming" : ""}`,
|
|
@@ -8775,6 +9044,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8775
9044
|
const [zoneOverrides, setZoneOverrides] = (0, react.useState)({});
|
|
8776
9045
|
const zoneCounts = (0, react.useRef)({});
|
|
8777
9046
|
const [editingNote, setEditingNote] = (0, react.useState)(null);
|
|
9047
|
+
const [noteRev, setNoteRev] = (0, react.useState)(0);
|
|
8778
9048
|
const [editDraft, setEditDraft] = (0, react.useState)("");
|
|
8779
9049
|
const [pendingLocate, setPendingLocate] = (0, react.useState)(null);
|
|
8780
9050
|
const [cmapExpanded, setCmapExpanded] = (0, react.useState)(false);
|
|
@@ -9198,7 +9468,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9198
9468
|
}));
|
|
9199
9469
|
}
|
|
9200
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", {
|
|
9201
|
-
key: n.id
|
|
9471
|
+
key: `${n.id}:${String(noteRev)}`,
|
|
9202
9472
|
className: `lks-note${n.pinned ? " pinned" : ""}`
|
|
9203
9473
|
}, (0, react.createElement)("span", { className: "lks-note-src" }, tr("note.src." + n.source)), (0, react.createElement)("button", {
|
|
9204
9474
|
className: "lks-note-act",
|
|
@@ -9259,6 +9529,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9259
9529
|
disabled: editDraft.trim() === "",
|
|
9260
9530
|
onClick: () => {
|
|
9261
9531
|
setEditingNote(null);
|
|
9532
|
+
setNoteRev((r) => r + 1);
|
|
9262
9533
|
fire(editNote(lesson.lessonId, n.id, editDraft));
|
|
9263
9534
|
}
|
|
9264
9535
|
}, tr("note.save")))) : (0, react.createElement)(ContentBoundary, {
|
|
@@ -9266,7 +9537,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9266
9537
|
boundaryKey: `note-${n.id}`
|
|
9267
9538
|
}, (0, react.createElement)("div", {
|
|
9268
9539
|
className: "lks-note-text",
|
|
9269
|
-
|
|
9540
|
+
ref: (el) => {
|
|
9541
|
+
if (el !== null) enhanceRendered(el).catch(() => {});
|
|
9542
|
+
},
|
|
9543
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(normalizeMathNotation(n.text)) }
|
|
9270
9544
|
})), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `'${n.quote}'`) : null)) : []);
|
|
9271
9545
|
})));
|
|
9272
9546
|
return (0, react.createElement)("div", { className: "lks14-col lks14-note" }, body, confirmNoteDel !== null ? (0, react.createElement)(ConfirmCard, {
|