dsh-plugin-lookatstudy 0.21.0 → 0.22.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 +517 -65
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +734 -102
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -481,7 +481,11 @@ window.__ModuleLoader__.load({
|
|
|
481
481
|
.lks-ui .lks14-board-head svg{color:var(--ink-muted);flex:none}
|
|
482
482
|
.lks-ui .lks14-board-stage{flex:1;min-height:0;padding:0 8px 8px}
|
|
483
483
|
.lks-ui .lks14-board-stage> .lks14-stage{border-radius:14px;border:1px solid var(--border-faint);background:rgb(var(--surface0-rgb,17 17 20)/0.6);overflow:hidden}
|
|
484
|
-
.lks-ui .lks14-board-artifact{padding:20px;width:
|
|
484
|
+
.lks-ui .lks14-board-artifact{padding:20px;width:fit-content;max-width:1200px}
|
|
485
|
+
/* issue #10: the board stage handles oversize via zoom — the table lays out
|
|
486
|
+
normally inside a bounded card (display:block + overflow was made for the
|
|
487
|
+
narrow chat column; on the canvas it broke cells into overlap) */
|
|
488
|
+
.lks-ui .lks14-board-artifact .lks-acard-table{display:table;width:100%;max-width:100%;overflow:visible}
|
|
485
489
|
.lks-ui .lks14-board-artifact .lks-acard{min-width:560px;background:var(--surface-0);border:1px solid var(--border-faint);border-radius:14px}
|
|
486
490
|
.lks-ui .lks14-cmapwrap{position:relative}
|
|
487
491
|
.lks-ui .lks14-cmap-expand{position:absolute;top:10px;right:10px;z-index:5}
|
|
@@ -771,6 +775,18 @@ window.__ModuleLoader__.load({
|
|
|
771
775
|
.lks-ui .lks14-threadmenu-row.on{background:rgb(var(--brand-rgb)/0.1)}
|
|
772
776
|
.lks-ui .lks14-threadmenu-row.on .lks14-threadmenu-title{color:var(--brand);font-weight:700}
|
|
773
777
|
.lks-ui .lks14-threadmenu-dot{flex:none;width:6px;height:6px;border-radius:999px;background:var(--ink-faint)}
|
|
778
|
+
.lks-ui .lks14-threadmenu-row.new{margin-top:3px;border-top:1px solid var(--border-faint);border-radius:0 0 7px 7px;padding-top:7px}
|
|
779
|
+
.lks-ui .lks14-threadmenu-row{position:relative}
|
|
780
|
+
.lks-ui .lks14-threadmenu-gear{flex:none;margin-left:auto;border:none;background:none;color:var(--ink-faint);font-size:13px;line-height:1;cursor:pointer;padding:2px 5px;border-radius:5px;opacity:0;transition:opacity .12s,color .12s}
|
|
781
|
+
.lks-ui .lks14-threadmenu-row:hover .lks14-threadmenu-gear,.lks-ui .lks14-threadmenu-gear:focus-visible{opacity:1}
|
|
782
|
+
.lks-ui .lks14-threadmenu-gear:hover{background:rgb(var(--ink-rgb)/0.08);color:var(--ink)}
|
|
783
|
+
.lks-ui .lks14-threadmenu-actions{position:absolute;right:4px;top:calc(100% - 2px);z-index:70;min-width:118px;display:flex;flex-direction:column;padding:4px;border-radius:9px;background:var(--surface-0);border:1px solid var(--border);box-shadow:0 12px 32px -8px rgb(var(--shadow-rgb)/0.5)}
|
|
784
|
+
.lks-ui .lks14-threadmenu-actions button{border:none;background:none;font:inherit;font-size:12px;color:var(--ink);text-align:left;padding:6px 8px;border-radius:6px;cursor:pointer}
|
|
785
|
+
.lks-ui .lks14-threadmenu-actions button:hover{background:rgb(var(--ink-rgb)/0.06)}
|
|
786
|
+
.lks-ui .lks14-threadmenu-actions button.danger{color:#d2483c}
|
|
787
|
+
.lks-ui .lks14-threadmenu-actions button.danger:hover{background:rgb(210 72 60/0.1)}
|
|
788
|
+
.lks-ui .lks14-threadmenu-rename{flex:1;min-width:0;font:inherit;font-size:12px;padding:3px 6px;border:1px solid var(--brand);border-radius:6px;background:var(--surface-0);color:var(--ink);outline:none}
|
|
789
|
+
.lks-ui .lks14-threadmenu-plus{flex:none;width:13px;height:13px;display:inline-flex;align-items:center;justify-content:center;border-radius:999px;border:1px solid var(--brand);color:var(--brand);font-size:10px;line-height:1}
|
|
774
790
|
.lks-ui .lks14-threadmenu-row.on .lks14-threadmenu-dot{background:var(--brand)}
|
|
775
791
|
.lks-ui .lks14-threadmenu-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
776
792
|
.lks-ui .lks14-threadmenu-now{flex:none;font-size:10px;font-weight:800;color:var(--brand)}
|
|
@@ -937,6 +953,12 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
937
953
|
.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
954
|
.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
955
|
.lks14-composertext:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
|
|
956
|
+
/* issue #7: the composer's pending-attachment chip (thumbnail + name + remove) */
|
|
957
|
+
.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%}
|
|
958
|
+
.lks14-attachchip-thumb{flex:none;width:44px;height:44px;object-fit:cover;border-radius:7px;border:1px solid var(--dsw-alias-border-l1)}
|
|
959
|
+
.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)}
|
|
960
|
+
.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}
|
|
961
|
+
.lks14-attachchip-x:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
940
962
|
|
|
941
963
|
/* ============================================================
|
|
942
964
|
* v0.29 pane-resize 拖拽手柄(PaneResizeHandle)——
|
|
@@ -1111,6 +1133,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1111
1133
|
.lks-acard-guess-opt.picked{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
|
|
1112
1134
|
.lks-acard-guess-wait{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
1113
1135
|
.lks-acard-stage{margin-top:14px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
1136
|
+
/* issue #8: every artifact card wears a fold bar — folded = one thin line */
|
|
1137
|
+
.lks-acard-foldwrap{margin:0}
|
|
1138
|
+
.lks-acard-fold{display:flex;align-items:center;gap:7px;width:100%;text-align:left;font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:9px;padding:4px 10px;cursor:pointer}
|
|
1139
|
+
.lks-acard-fold:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
1140
|
+
.lks-acard-fold-caret{flex:none;color:var(--dsw-alias-label-tertiary)}
|
|
1141
|
+
.lks-acard-fold-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1142
|
+
.lks-acard-foldwrap:not(.folded) .lks-acard-fold{border-radius:9px 9px 0 0;border-bottom:none}
|
|
1143
|
+
.lks-acard-foldwrap:not(.folded) .lks-acard{border-radius:0 0 9px 9px}
|
|
1144
|
+
.lks-acard-guess-wait.revealed{color:var(--dsw-alias-state-success-primary)}
|
|
1114
1145
|
.lks-acard-stage .lks-acard{margin:0}
|
|
1115
1146
|
.lks14-zone .lks-acard{margin:0 0 8px}
|
|
1116
1147
|
/* notebook tab badge (unseen artifacts) */
|
|
@@ -1764,16 +1795,38 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1764
1795
|
this.refresh();
|
|
1765
1796
|
}
|
|
1766
1797
|
/** Record the dsh session backing one lesson (the simplified thread system). */
|
|
1767
|
-
|
|
1798
|
+
/**
|
|
1799
|
+
* Bind a session into the lesson's thread group (issue #11): a new session
|
|
1800
|
+
* id appends a thread (titled by `title` — first-message auto-naming);
|
|
1801
|
+
* `null` clears the active pointer (+新建 — the next send mints fresh).
|
|
1802
|
+
*/
|
|
1803
|
+
async bindLessonSession(lessonId, sessionId, title) {
|
|
1768
1804
|
await fetchJson("/lookatstudy/api/lesson-session", {
|
|
1769
1805
|
method: "POST",
|
|
1770
1806
|
headers: { "content-type": "application/json" },
|
|
1771
1807
|
body: JSON.stringify({
|
|
1772
1808
|
lessonId,
|
|
1773
|
-
sessionId
|
|
1809
|
+
sessionId,
|
|
1810
|
+
title
|
|
1811
|
+
})
|
|
1812
|
+
});
|
|
1813
|
+
this.refresh();
|
|
1814
|
+
}
|
|
1815
|
+
/** Issue #11 management (upstream gear menu): rename/archive/delete one
|
|
1816
|
+
* thread. Returns the route's post-op snapshot (active + thread count). */
|
|
1817
|
+
async lessonThreadOp(lessonId, sessionId, op, extra) {
|
|
1818
|
+
const res = await fetchJson("/lookatstudy/api/lesson-thread", {
|
|
1819
|
+
method: "POST",
|
|
1820
|
+
headers: { "content-type": "application/json" },
|
|
1821
|
+
body: JSON.stringify({
|
|
1822
|
+
lessonId,
|
|
1823
|
+
sessionId,
|
|
1824
|
+
op,
|
|
1825
|
+
...extra
|
|
1774
1826
|
})
|
|
1775
1827
|
});
|
|
1776
1828
|
this.refresh();
|
|
1829
|
+
return res;
|
|
1777
1830
|
}
|
|
1778
1831
|
/** Delete one course (and its proposals) from the host state. */
|
|
1779
1832
|
async deleteCourse(courseId) {
|
|
@@ -1987,7 +2040,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1987
2040
|
examRegenerate: studyStore.examRegenerate.bind(studyStore),
|
|
1988
2041
|
addUserNote: studyStore.addUserNote.bind(studyStore),
|
|
1989
2042
|
recordReview: studyStore.recordReview.bind(studyStore),
|
|
1990
|
-
bindLessonSession: studyStore.bindLessonSession.bind(studyStore)
|
|
2043
|
+
bindLessonSession: studyStore.bindLessonSession.bind(studyStore),
|
|
2044
|
+
lessonThreadOp: studyStore.lessonThreadOp.bind(studyStore)
|
|
1991
2045
|
};
|
|
1992
2046
|
}
|
|
1993
2047
|
//#endregion
|
|
@@ -2045,6 +2099,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2045
2099
|
"artifact.guess.title": "猜一猜",
|
|
2046
2100
|
"artifact.guess.pick": "我选「{label}」",
|
|
2047
2101
|
"artifact.guess.wait": "已记下你的直觉——导师下回合揭晓,看看你想得对不对",
|
|
2102
|
+
"artifact.guess.revealed": "已揭晓——结论见导师的下一条回复",
|
|
2103
|
+
"artifact.fold": "收起卡片(笔记区仍保留)",
|
|
2104
|
+
"artifact.unfold": "展开卡片",
|
|
2105
|
+
"artifact.kind.compare_table": "对照表",
|
|
2106
|
+
"artifact.kind.code_walkthrough": "代码走查",
|
|
2107
|
+
"artifact.kind.diagram": "示意图",
|
|
2048
2108
|
"artifact.sedimented": "有新的学习产物沉淀进笔记",
|
|
2049
2109
|
"zone.artifacts": "AI 产物",
|
|
2050
2110
|
"note.quote.ask": "提问这段",
|
|
@@ -2164,6 +2224,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2164
2224
|
"palette.empty": "没有匹配项",
|
|
2165
2225
|
"threads.open": "{n} 条对话",
|
|
2166
2226
|
"threads.now": "当前",
|
|
2227
|
+
"threads.new": "新建对话(下一条消息开新线)",
|
|
2228
|
+
"threads.actions": "线程操作",
|
|
2229
|
+
"rail.export.pack": "导出课程包(可再导入/分享的 Markdown)",
|
|
2230
|
+
"rail.export.done": "课程包已下载:{file}",
|
|
2231
|
+
"threads.rename": "重命名",
|
|
2232
|
+
"threads.archive": "归档",
|
|
2233
|
+
"threads.delete": "删除",
|
|
2234
|
+
"threads.delete.confirm": "删除「{name}」这条对话线?插件侧记录会移除;宿主会话列表中的原会话保留。",
|
|
2235
|
+
"review.banner": "正在复习:{lesson}",
|
|
2236
|
+
"review.threadTitle": "复习:{lesson}",
|
|
2167
2237
|
"threads.label": "课时线程",
|
|
2168
2238
|
"meter.label": "上下文用量",
|
|
2169
2239
|
"meter.aria": "上下文已用 {pct}%",
|
|
@@ -2179,6 +2249,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2179
2249
|
"settings.budget.on": "开启历史预算",
|
|
2180
2250
|
"settings.budget.off": "关闭历史预算",
|
|
2181
2251
|
"attach.sent": "我发了一个文件:{name}(工作区路径 {path})——请读取它再继续。",
|
|
2252
|
+
"attach.remove": "移除待发附件",
|
|
2182
2253
|
"attach.fail": "附件上传失败",
|
|
2183
2254
|
"attach.label": "附件(≤20MiB,存入学习工作区)",
|
|
2184
2255
|
"rail.mastered": "毕业 {mastered}/{total} 课",
|
|
@@ -2391,6 +2462,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2391
2462
|
"artifact.guess.title": "Take a guess",
|
|
2392
2463
|
"artifact.guess.pick": "I pick \"{label}\"",
|
|
2393
2464
|
"artifact.guess.wait": "Your instinct is noted — the tutor reveals next turn",
|
|
2465
|
+
"artifact.guess.revealed": "Revealed — see the tutor reply below for the answer",
|
|
2466
|
+
"artifact.fold": "Fold card (kept in the notes zone)",
|
|
2467
|
+
"artifact.unfold": "Unfold card",
|
|
2468
|
+
"artifact.kind.compare_table": "Compare table",
|
|
2469
|
+
"artifact.kind.code_walkthrough": "Code walkthrough",
|
|
2470
|
+
"artifact.kind.diagram": "Diagram",
|
|
2394
2471
|
"artifact.sedimented": "A new artifact settled into the notebook",
|
|
2395
2472
|
"zone.artifacts": "AI artifacts",
|
|
2396
2473
|
"note.quote.ask": "Ask about this",
|
|
@@ -2511,6 +2588,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2511
2588
|
"threads.label": "Lesson threads",
|
|
2512
2589
|
"threads.open": "{n} threads",
|
|
2513
2590
|
"threads.now": "current",
|
|
2591
|
+
"threads.new": "New thread (your next message starts it)",
|
|
2592
|
+
"threads.actions": "Thread actions",
|
|
2593
|
+
"rail.export.pack": "Export course pack (re-importable, shareable Markdown)",
|
|
2594
|
+
"rail.export.done": "Course pack downloaded: {file}",
|
|
2595
|
+
"threads.rename": "Rename",
|
|
2596
|
+
"threads.archive": "Archive",
|
|
2597
|
+
"threads.delete": "Delete",
|
|
2598
|
+
"threads.delete.confirm": "Delete the thread \"{name}\"? The plugin-side record is removed; the original session stays in the host session list.",
|
|
2599
|
+
"review.banner": "Reviewing: {lesson}",
|
|
2600
|
+
"review.threadTitle": "Review: {lesson}",
|
|
2514
2601
|
"meter.label": "Context usage",
|
|
2515
2602
|
"meter.aria": "{pct}% of context used",
|
|
2516
2603
|
"meter.brk.system": "System prompt",
|
|
@@ -2525,6 +2612,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2525
2612
|
"settings.budget.on": "Enable history budget",
|
|
2526
2613
|
"settings.budget.off": "Disable history budget",
|
|
2527
2614
|
"attach.sent": "I attached a file: {name} (workspace path {path}) — read it before continuing.",
|
|
2615
|
+
"attach.remove": "Remove pending attachment",
|
|
2528
2616
|
"attach.fail": "Attachment upload failed",
|
|
2529
2617
|
"attach.label": "Attach (≤20MiB, lands in the study workspace)",
|
|
2530
2618
|
"rail.mastered": "{mastered}/{total} lessons graduated",
|
|
@@ -2765,10 +2853,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2765
2853
|
}
|
|
2766
2854
|
/** Render inline markup (code, bold, italic, links, images) over escaped text.
|
|
2767
2855
|
* Image src allowlist: https?:// or data:image/ — folder imports inline local
|
|
2768
|
-
* images as data URLs (capped), GitHub imports reference jsDelivr;
|
|
2769
|
-
*
|
|
2856
|
+
* images as data URLs (capped), GitHub imports reference jsDelivr; the
|
|
2857
|
+
* plugin's own attachment route (/lookatstudy/api/attachment/<name>, issue #7
|
|
2858
|
+
* — same-origin, server-side validated) renders workspace attachments;
|
|
2859
|
+
* anything else stays literal text (no broken relative images, no exotic
|
|
2860
|
+
* schemes). */
|
|
2770
2861
|
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>");
|
|
2862
|
+
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
2863
|
}
|
|
2773
2864
|
/** True when the line opens a GFM table row (pipes with a delimiter row next). */
|
|
2774
2865
|
function isTableRow(line) {
|
|
@@ -3616,6 +3707,28 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3616
3707
|
return stored === "rail" || stored === "chat" || stored === "note" ? stored : "chat";
|
|
3617
3708
|
}
|
|
3618
3709
|
/**
|
|
3710
|
+
* Issue #11 (upstream first-message auto-naming): a fresh thread's title is
|
|
3711
|
+
* the sending text, single-lined and length-capped — the switcher chip
|
|
3712
|
+
* truncates via CSS, storage keeps a sane whole title. Pure.
|
|
3713
|
+
*/
|
|
3714
|
+
function threadAutoTitle(text, max = 80) {
|
|
3715
|
+
const oneLine = text.trim().replace(/\s+/g, " ");
|
|
3716
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
3717
|
+
}
|
|
3718
|
+
/**
|
|
3719
|
+
* Issue #11: the switcher's rows = the CURRENT lesson's thread group (upstream
|
|
3720
|
+
* v0.5 节点 = 会话组) — every thread, freshest first, the active one marked.
|
|
3721
|
+
* Pure.
|
|
3722
|
+
*/
|
|
3723
|
+
function threadGroupPills(group) {
|
|
3724
|
+
if (group === void 0) return [];
|
|
3725
|
+
return group.threads.filter((t) => t.status !== "archived").slice().sort((a, b) => a.lastAt < b.lastAt ? 1 : -1).map((t) => ({
|
|
3726
|
+
id: t.id,
|
|
3727
|
+
title: t.title,
|
|
3728
|
+
current: t.id === group.active
|
|
3729
|
+
}));
|
|
3730
|
+
}
|
|
3731
|
+
/**
|
|
3619
3732
|
* Default expansion for one rail section: collapsed when every study lesson is
|
|
3620
3733
|
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
3621
3734
|
* stays open; exam nodes never force a section open. Pure.
|
|
@@ -4526,6 +4639,27 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4526
4639
|
}
|
|
4527
4640
|
return depth > 0 || liveChunkOrphan;
|
|
4528
4641
|
}
|
|
4642
|
+
/**
|
|
4643
|
+
* Issue #9: how long a turn-liveness window may sit WITHOUT its top seq
|
|
4644
|
+
* advancing before the panel reads it as stalled and unlocks the composer.
|
|
4645
|
+
* Generous on purpose: a legit in-flight tool call journals nothing for up to
|
|
4646
|
+
* its own timeout (the import tools run 180 s), so anything past 240 s with a
|
|
4647
|
+
* motionless window is a dead turn (cancel paths never journal turn/end).
|
|
4648
|
+
*/
|
|
4649
|
+
const TURN_STALL_MS = 24e4;
|
|
4650
|
+
/**
|
|
4651
|
+
* The stall decision (pure — the wall clock lives at the poll call site):
|
|
4652
|
+
* an ACTIVE fold stays active while the window's top seq keeps advancing;
|
|
4653
|
+
* once it has been frozen for longer than stallMs the panel treats the turn
|
|
4654
|
+
* as over. `lastAdvanceAt === 0` means "first observation" — the clock starts
|
|
4655
|
+
* now, so a freshly mounted window with an old unpaired turn/start does not
|
|
4656
|
+
* insta-clear (it clears one stall window later if truly dead).
|
|
4657
|
+
*/
|
|
4658
|
+
function turnStalled(active, lastSeq, prevLastSeq, lastAdvanceAt, now, stallMs = TURN_STALL_MS) {
|
|
4659
|
+
if (!active) return false;
|
|
4660
|
+
if (lastSeq !== prevLastSeq || lastAdvanceAt === 0) return false;
|
|
4661
|
+
return now - lastAdvanceAt > stallMs;
|
|
4662
|
+
}
|
|
4529
4663
|
/** The window's highest event seq (the stop watermark's source). */
|
|
4530
4664
|
function feedLastSeq(window) {
|
|
4531
4665
|
if (window === void 0 || window.entries === void 0) return 0;
|
|
@@ -5843,6 +5977,54 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5843
5977
|
storage.setItem(seenArtifactsKey(lessonId), JSON.stringify([.../* @__PURE__ */ new Set([...seen, ...ids])]));
|
|
5844
5978
|
} catch {}
|
|
5845
5979
|
}
|
|
5980
|
+
/** localStorage: artifact id → the picked guess option id. The pick outlives
|
|
5981
|
+
* the component — a remount must not re-enable the buttons (and re-send). */
|
|
5982
|
+
const GUESS_PICKED_KEY = "dsh-plugin-lookatstudy:guess-picked";
|
|
5983
|
+
function guessPickedOf(artifactId, storage = localStorage) {
|
|
5984
|
+
try {
|
|
5985
|
+
const parsed = JSON.parse(storage.getItem(GUESS_PICKED_KEY) ?? "{}");
|
|
5986
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
5987
|
+
const v = parsed[artifactId];
|
|
5988
|
+
if (typeof v === "string") return v;
|
|
5989
|
+
}
|
|
5990
|
+
} catch {}
|
|
5991
|
+
return null;
|
|
5992
|
+
}
|
|
5993
|
+
function markGuessPicked(artifactId, optionId, storage = localStorage) {
|
|
5994
|
+
try {
|
|
5995
|
+
let map = {};
|
|
5996
|
+
try {
|
|
5997
|
+
const parsed = JSON.parse(storage.getItem(GUESS_PICKED_KEY) ?? "{}");
|
|
5998
|
+
if (parsed !== null && typeof parsed === "object") map = parsed;
|
|
5999
|
+
} catch {}
|
|
6000
|
+
map[artifactId] = optionId;
|
|
6001
|
+
storage.setItem(GUESS_PICKED_KEY, JSON.stringify(map));
|
|
6002
|
+
} catch {}
|
|
6003
|
+
}
|
|
6004
|
+
/** localStorage: artifact id → explicitly toggled fold state. Absent = the
|
|
6005
|
+
* mount's defaultFolded (feed cards open, sedimented surfaces folded). */
|
|
6006
|
+
const ACARD_FOLD_KEY = "dsh-plugin-lookatstudy:acard-folded";
|
|
6007
|
+
function acardFoldStoredOf(artifactId, storage = localStorage) {
|
|
6008
|
+
try {
|
|
6009
|
+
const parsed = JSON.parse(storage.getItem(ACARD_FOLD_KEY) ?? "{}");
|
|
6010
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
6011
|
+
const v = parsed[artifactId];
|
|
6012
|
+
if (typeof v === "boolean") return v;
|
|
6013
|
+
}
|
|
6014
|
+
} catch {}
|
|
6015
|
+
return null;
|
|
6016
|
+
}
|
|
6017
|
+
function markAcardFoldStored(artifactId, folded, storage = localStorage) {
|
|
6018
|
+
try {
|
|
6019
|
+
let map = {};
|
|
6020
|
+
try {
|
|
6021
|
+
const parsed = JSON.parse(storage.getItem(ACARD_FOLD_KEY) ?? "{}");
|
|
6022
|
+
if (parsed !== null && typeof parsed === "object") map = parsed;
|
|
6023
|
+
} catch {}
|
|
6024
|
+
map[artifactId] = folded;
|
|
6025
|
+
storage.setItem(ACARD_FOLD_KEY, JSON.stringify(map));
|
|
6026
|
+
} catch {}
|
|
6027
|
+
}
|
|
5846
6028
|
/** The cards render mermaid through the shared CDN pipeline
|
|
5847
6029
|
* (enhanceDiagrams consumes pre > code.lang-mermaid blocks). */
|
|
5848
6030
|
async function renderMermaidInto(el, code, onError) {
|
|
@@ -5949,13 +6131,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5949
6131
|
})))) : null);
|
|
5950
6132
|
}
|
|
5951
6133
|
/** The opening guess: two big options, click = pick (sent to the tutor, who
|
|
5952
|
-
* reveals next turn); unscored — no right/wrong language anywhere.
|
|
5953
|
-
|
|
6134
|
+
* reveals next turn); unscored — no right/wrong language anywhere.
|
|
6135
|
+
* Issue #8 lifecycle: the pick persists per artifact (a remount keeps the
|
|
6136
|
+
* buttons disabled — re-clicking used to re-send a message); once a later
|
|
6137
|
+
* assistant reply exists (`revealed`), the wait line becomes the reveal note. */
|
|
6138
|
+
function GuessCard({ artifact, send, revealed = false }) {
|
|
5954
6139
|
const prompt = typeof artifact.data.prompt === "string" ? artifact.data.prompt : "";
|
|
5955
6140
|
const options = artifact.data.options ?? [];
|
|
5956
|
-
const [picked, setPicked] = (0, react.useState)(
|
|
6141
|
+
const [picked, setPicked] = (0, react.useState)(() => guessPickedOf(artifact.id));
|
|
5957
6142
|
(0, react.useEffect)(() => {
|
|
5958
|
-
setPicked(
|
|
6143
|
+
setPicked(guessPickedOf(artifact.id));
|
|
5959
6144
|
}, [artifact.id]);
|
|
5960
6145
|
return (0, react.createElement)("div", {
|
|
5961
6146
|
className: "lks-acard lks-acard-guess",
|
|
@@ -5966,23 +6151,67 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5966
6151
|
disabled: picked !== null,
|
|
5967
6152
|
onClick: () => {
|
|
5968
6153
|
setPicked(opt.id);
|
|
6154
|
+
markGuessPicked(artifact.id, opt.id);
|
|
5969
6155
|
send(tr("artifact.guess.pick", { label: opt.label }));
|
|
5970
6156
|
}
|
|
5971
|
-
}, opt.label))), picked !== null ? (0, react.createElement)("div", { className:
|
|
6157
|
+
}, opt.label))), picked !== null ? (0, react.createElement)("div", { className: `lks-acard-guess-wait${revealed ? " revealed" : ""}` }, revealed ? tr("artifact.guess.revealed") : tr("artifact.guess.wait")) : null);
|
|
5972
6158
|
}
|
|
5973
6159
|
/** Dispatch one artifact row to its card. */
|
|
5974
|
-
function ArtifactCard({ artifact, send }) {
|
|
6160
|
+
function ArtifactCard({ artifact, send, revealed = false }) {
|
|
5975
6161
|
switch (artifact.artifactType) {
|
|
5976
6162
|
case "compare_table": return (0, react.createElement)(CompareTableCard, { artifact });
|
|
5977
6163
|
case "code_walkthrough": return (0, react.createElement)(CodeWalkthroughCard, { artifact });
|
|
5978
6164
|
case "diagram": return (0, react.createElement)(DiagramCard, { artifact });
|
|
5979
6165
|
case "guess": return (0, react.createElement)(GuessCard, {
|
|
5980
6166
|
artifact,
|
|
5981
|
-
send
|
|
6167
|
+
send,
|
|
6168
|
+
revealed
|
|
5982
6169
|
});
|
|
5983
6170
|
default: return null;
|
|
5984
6171
|
}
|
|
5985
6172
|
}
|
|
6173
|
+
/** The fold bar's kind label (fallback when the artifact carries no title). */
|
|
6174
|
+
const KIND_LABEL_KEY = {
|
|
6175
|
+
compare_table: "artifact.kind.compare_table",
|
|
6176
|
+
code_walkthrough: "artifact.kind.code_walkthrough",
|
|
6177
|
+
diagram: "artifact.kind.diagram",
|
|
6178
|
+
guess: "artifact.guess.title"
|
|
6179
|
+
};
|
|
6180
|
+
/**
|
|
6181
|
+
* Issue #8: every artifact card wears a fold bar — cards sediment into the
|
|
6182
|
+
* notebook by design, so nothing is deleted; folding keeps long conversations
|
|
6183
|
+
* readable (a folded card is one thin line). The explicit toggle persists per
|
|
6184
|
+
* artifact; absent a stored choice, `defaultFolded` decides (feed cards open,
|
|
6185
|
+
* the teach-stage and backlog surfaces start folded — the lesson prose owns
|
|
6186
|
+
* the first screen, issue #10).
|
|
6187
|
+
*/
|
|
6188
|
+
function FoldableArtifactCard({ artifact, send, defaultFolded = false, revealed = false }) {
|
|
6189
|
+
const [folded, setFolded] = (0, react.useState)(() => acardFoldStoredOf(artifact.id) ?? defaultFolded);
|
|
6190
|
+
const toggle = () => {
|
|
6191
|
+
setFolded((cur) => {
|
|
6192
|
+
const next = !cur;
|
|
6193
|
+
markAcardFoldStored(artifact.id, next);
|
|
6194
|
+
return next;
|
|
6195
|
+
});
|
|
6196
|
+
};
|
|
6197
|
+
const title = artifact.title.trim() !== "" ? artifact.title : tr(KIND_LABEL_KEY[artifact.artifactType] ?? "artifact.fold");
|
|
6198
|
+
return (0, react.createElement)("div", {
|
|
6199
|
+
className: `lks-acard-foldwrap${folded ? " folded" : ""}`,
|
|
6200
|
+
"data-lks-artifact": artifact.id
|
|
6201
|
+
}, (0, react.createElement)("button", {
|
|
6202
|
+
className: "lks-acard-fold",
|
|
6203
|
+
"aria-expanded": String(!folded),
|
|
6204
|
+
"data-tooltip": folded ? tr("artifact.unfold") : tr("artifact.fold"),
|
|
6205
|
+
onClick: toggle
|
|
6206
|
+
}, (0, react.createElement)("span", {
|
|
6207
|
+
className: "lks-acard-fold-caret",
|
|
6208
|
+
"aria-hidden": "true"
|
|
6209
|
+
}, folded ? "▸" : "▾"), (0, react.createElement)("span", { className: "lks-acard-fold-title" }, title)), folded ? null : (0, react.createElement)(ArtifactCard, {
|
|
6210
|
+
artifact,
|
|
6211
|
+
send,
|
|
6212
|
+
revealed
|
|
6213
|
+
}));
|
|
6214
|
+
}
|
|
5986
6215
|
//#endregion
|
|
5987
6216
|
//#region src/client/highlights.ts
|
|
5988
6217
|
/**
|
|
@@ -7095,7 +7324,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7095
7324
|
};
|
|
7096
7325
|
}
|
|
7097
7326
|
function StudyPanelBody({ ctx }) {
|
|
7098
|
-
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, uploadAttachment } = useStudy();
|
|
7327
|
+
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, uploadAttachment } = useStudy();
|
|
7099
7328
|
(0, react.useEffect)(() => {
|
|
7100
7329
|
wireCodeBlockCopy();
|
|
7101
7330
|
}, []);
|
|
@@ -7188,6 +7417,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7188
7417
|
const [feedGen, setFeedGen] = (0, react.useState)(false);
|
|
7189
7418
|
const lastSeqRef = (0, react.useRef)(0);
|
|
7190
7419
|
const stopWatermarkRef = (0, react.useRef)(null);
|
|
7420
|
+
const lastAdvanceAtRef = (0, react.useRef)(0);
|
|
7191
7421
|
const examSessionRef = (0, react.useRef)({
|
|
7192
7422
|
active: false,
|
|
7193
7423
|
terminate: null
|
|
@@ -7241,6 +7471,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7241
7471
|
const lesson = data?.lesson ?? null;
|
|
7242
7472
|
const examLessonActive = lesson !== null && lesson.kind === "exam" && examOpen(data?.courses.flatMap((c) => c.sections).find((s) => s.lessons.some((l) => l.id === lesson.lessonId))?.lessons ?? []);
|
|
7243
7473
|
const localBound = (0, react.useRef)(null);
|
|
7474
|
+
const boundSessionOf = (lessonId) => lessonId !== null && localBound.current?.lessonId === lessonId ? localBound.current.sessionId : null;
|
|
7244
7475
|
const streamEl = (0, react.useRef)(null);
|
|
7245
7476
|
const touchStart = (0, react.useRef)(null);
|
|
7246
7477
|
const activeFace = (0, react.useRef)(null);
|
|
@@ -7249,7 +7480,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7249
7480
|
try {
|
|
7250
7481
|
let face = activeFace.current;
|
|
7251
7482
|
if (face === null) {
|
|
7252
|
-
const sessionId =
|
|
7483
|
+
const sessionId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? (lesson !== null ? data?.lessonThreads[lesson.lessonId]?.active ?? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7253
7484
|
if (sessionId !== null && sessionKnown(ctx, sessionId)) {
|
|
7254
7485
|
const actx = ctx.sessions.scope(sessionId);
|
|
7255
7486
|
face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx) ?? null;
|
|
@@ -7285,7 +7516,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7285
7516
|
} else celebrate(ev.kind);
|
|
7286
7517
|
prevCelebrationRef.current = snapshot;
|
|
7287
7518
|
}, [data]);
|
|
7288
|
-
const boundId =
|
|
7519
|
+
const boundId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? (lesson !== null ? data?.lessonThreads[lesson.lessonId]?.active ?? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7289
7520
|
const nudged = (0, react.useRef)(false);
|
|
7290
7521
|
(0, react.useEffect)(() => {
|
|
7291
7522
|
if (nudged.current || (data?.dueCount ?? 0) === 0) return;
|
|
@@ -7389,8 +7620,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7389
7620
|
const update = () => {
|
|
7390
7621
|
const snap = source.getSnapshot();
|
|
7391
7622
|
setRows(feedRows(snap));
|
|
7392
|
-
|
|
7393
|
-
|
|
7623
|
+
const lastSeq = feedLastSeq(snap);
|
|
7624
|
+
const prevSeq = lastSeqRef.current;
|
|
7625
|
+
if (lastSeq !== prevSeq || lastAdvanceAtRef.current === 0) lastAdvanceAtRef.current = Date.now();
|
|
7626
|
+
lastSeqRef.current = lastSeq;
|
|
7627
|
+
const active = feedTurnActive(snap, stopWatermarkRef.current);
|
|
7628
|
+
setFeedGen(active && !turnStalled(active, lastSeq, prevSeq, lastAdvanceAtRef.current, Date.now()));
|
|
7394
7629
|
};
|
|
7395
7630
|
update();
|
|
7396
7631
|
setFeedAttached(true);
|
|
@@ -7444,16 +7679,31 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7444
7679
|
const el = streamEl.current;
|
|
7445
7680
|
if (el !== null) el.scrollTop = el.scrollHeight;
|
|
7446
7681
|
}, [rows]);
|
|
7447
|
-
|
|
7448
|
-
|
|
7682
|
+
(0, react.useEffect)(() => {
|
|
7683
|
+
if (!feedGen) return;
|
|
7684
|
+
const iv = setInterval(() => {
|
|
7685
|
+
if (Date.now() - lastAdvanceAtRef.current > 24e4) setFeedGen(false);
|
|
7686
|
+
}, 5e3);
|
|
7687
|
+
return () => {
|
|
7688
|
+
clearInterval(iv);
|
|
7689
|
+
};
|
|
7690
|
+
}, [feedGen]);
|
|
7691
|
+
/** The one send path: lazily stage the lesson thread, then prompt.
|
|
7692
|
+
* `targetLessonId` (issue #11 review routing) sends into ANOTHER lesson's
|
|
7693
|
+
* group: focus moves there first (the tutor's snapshot follows), then the
|
|
7694
|
+
* target's active thread — or a fresh titled one — carries the prompt. */
|
|
7695
|
+
const send = (text, threadTitle, targetLessonId) => {
|
|
7449
7696
|
(async () => {
|
|
7450
7697
|
if (busy || lesson === null || text.trim() === "") return;
|
|
7451
7698
|
setBusy(true);
|
|
7452
7699
|
setSendError(null);
|
|
7453
7700
|
stopWatermarkRef.current = null;
|
|
7701
|
+
lastAdvanceAtRef.current = Date.now();
|
|
7454
7702
|
try {
|
|
7455
7703
|
if (data?.active !== true) await activate(true);
|
|
7456
|
-
|
|
7704
|
+
if (targetLessonId !== void 0 && targetLessonId !== lesson.lessonId) await setFocus(targetLessonId).catch(() => {});
|
|
7705
|
+
const bindId = targetLessonId ?? lesson.lessonId;
|
|
7706
|
+
let sessionId = boundSessionOf(bindId) ?? data?.lessonThreads[bindId]?.active ?? data?.lessonSessions[bindId] ?? null;
|
|
7457
7707
|
if (sessionId !== null && !sessionKnown(ctx, sessionId)) sessionId = null;
|
|
7458
7708
|
if (sessionId === null) {
|
|
7459
7709
|
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
@@ -7461,9 +7711,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7461
7711
|
const { path } = await area.json();
|
|
7462
7712
|
const workspace = await ctx.workspaces.create({ path });
|
|
7463
7713
|
sessionId = await ctx.sessions.create({ workspaceId: workspace.workspaceId });
|
|
7464
|
-
localBound.current =
|
|
7465
|
-
|
|
7466
|
-
|
|
7714
|
+
localBound.current = {
|
|
7715
|
+
lessonId: bindId,
|
|
7716
|
+
sessionId
|
|
7717
|
+
};
|
|
7718
|
+
await bindLessonSession(bindId, sessionId, threadTitle ?? threadAutoTitle(text));
|
|
7719
|
+
} else localBound.current = {
|
|
7720
|
+
lessonId: bindId,
|
|
7721
|
+
sessionId
|
|
7722
|
+
};
|
|
7467
7723
|
const shell = PANEL_SHELL.current;
|
|
7468
7724
|
if (shell !== null) shell.suppressHandBack(() => {
|
|
7469
7725
|
ctx.sessions.open(sessionId);
|
|
@@ -7497,6 +7753,50 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7497
7753
|
window.removeEventListener("lookatstudy-exam-generate", onGenerate);
|
|
7498
7754
|
};
|
|
7499
7755
|
}, [send]);
|
|
7756
|
+
const switchLessonThread = (sessionId) => {
|
|
7757
|
+
if (lesson === null) return;
|
|
7758
|
+
if (localBound.current?.lessonId === lesson.lessonId && localBound.current.sessionId === sessionId) return;
|
|
7759
|
+
localBound.current = {
|
|
7760
|
+
lessonId: lesson.lessonId,
|
|
7761
|
+
sessionId
|
|
7762
|
+
};
|
|
7763
|
+
bindLessonSession(lesson.lessonId, sessionId);
|
|
7764
|
+
const shell = PANEL_SHELL.current;
|
|
7765
|
+
if (shell !== null) shell.suppressHandBack(() => {
|
|
7766
|
+
ctx.sessions.open(sessionId);
|
|
7767
|
+
});
|
|
7768
|
+
else ctx.sessions.open(sessionId);
|
|
7769
|
+
};
|
|
7770
|
+
const startNewThread = () => {
|
|
7771
|
+
if (lesson === null) return;
|
|
7772
|
+
localBound.current = null;
|
|
7773
|
+
bindLessonSession(lesson.lessonId, null);
|
|
7774
|
+
setRows([]);
|
|
7775
|
+
setFeedGen(false);
|
|
7776
|
+
};
|
|
7777
|
+
const renameLessonThreadUi = (sessionId, title) => {
|
|
7778
|
+
if (lesson === null) return;
|
|
7779
|
+
lessonThreadOp(lesson.lessonId, sessionId, "rename", { title }).then(() => {
|
|
7780
|
+
const actx = ctx.sessions.scope(sessionId);
|
|
7781
|
+
(actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx))?.rename?.(title).catch(() => {});
|
|
7782
|
+
}, (err) => {
|
|
7783
|
+
setSendError(friendlyError(err, "action"));
|
|
7784
|
+
});
|
|
7785
|
+
};
|
|
7786
|
+
const archiveLessonThreadUi = (sessionId) => {
|
|
7787
|
+
if (lesson === null) return;
|
|
7788
|
+
if (localBound.current?.lessonId === lesson.lessonId && localBound.current.sessionId === sessionId) localBound.current = null;
|
|
7789
|
+
lessonThreadOp(lesson.lessonId, sessionId, "archive", { archived: true }).catch((err) => {
|
|
7790
|
+
setSendError(friendlyError(err, "action"));
|
|
7791
|
+
});
|
|
7792
|
+
};
|
|
7793
|
+
const deleteLessonThreadUi = (sessionId) => {
|
|
7794
|
+
if (lesson === null) return;
|
|
7795
|
+
if (localBound.current?.lessonId === lesson.lessonId && localBound.current.sessionId === sessionId) localBound.current = null;
|
|
7796
|
+
lessonThreadOp(lesson.lessonId, sessionId, "delete").catch((err) => {
|
|
7797
|
+
setSendError(friendlyError(err, "action"));
|
|
7798
|
+
});
|
|
7799
|
+
};
|
|
7500
7800
|
const progress = data?.progress ?? null;
|
|
7501
7801
|
const body = (0, react.createElement)("div", {
|
|
7502
7802
|
className: "lks14-body",
|
|
@@ -7587,10 +7887,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7587
7887
|
stop,
|
|
7588
7888
|
setMode,
|
|
7589
7889
|
narrowPane,
|
|
7590
|
-
|
|
7890
|
+
onThreadSwitch: (sessionId) => {
|
|
7591
7891
|
setNarrowPane("chat");
|
|
7592
|
-
|
|
7892
|
+
switchLessonThread(sessionId);
|
|
7893
|
+
},
|
|
7894
|
+
onThreadNew: () => {
|
|
7895
|
+
setNarrowPane("chat");
|
|
7896
|
+
startNewThread();
|
|
7593
7897
|
},
|
|
7898
|
+
onThreadRename: renameLessonThreadUi,
|
|
7899
|
+
onThreadArchive: archiveLessonThreadUi,
|
|
7900
|
+
onThreadDelete: deleteLessonThreadUi,
|
|
7594
7901
|
contextMeter,
|
|
7595
7902
|
contextBreakdown: breakdown,
|
|
7596
7903
|
uploadAttachment
|
|
@@ -8056,7 +8363,30 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8056
8363
|
bottom: r.bottom
|
|
8057
8364
|
} });
|
|
8058
8365
|
}
|
|
8059
|
-
}, (0, react.createElement)(IconTrashOutline16, { size: 14 }))
|
|
8366
|
+
}, (0, react.createElement)(IconTrashOutline16, { size: 14 })), (0, react.createElement)("button", {
|
|
8367
|
+
className: "lks-btn ghost",
|
|
8368
|
+
"data-tooltip": tr("rail.export.pack"),
|
|
8369
|
+
"data-testid": "course-export",
|
|
8370
|
+
onClick: () => {
|
|
8371
|
+
if (courseId === null) return;
|
|
8372
|
+
(async () => {
|
|
8373
|
+
try {
|
|
8374
|
+
const r = await fetch(`/lookatstudy/api/course-pack?courseId=${encodeURIComponent(courseId)}`);
|
|
8375
|
+
const body = await r.json();
|
|
8376
|
+
if (!r.ok || body.ok !== true || body.markdown === void 0) throw new Error(`HTTP ${String(r.status)}`);
|
|
8377
|
+
const blob = new Blob([body.markdown], { type: "text/markdown;charset=utf-8" });
|
|
8378
|
+
const a = document.createElement("a");
|
|
8379
|
+
a.href = URL.createObjectURL(blob);
|
|
8380
|
+
a.download = body.fileName ?? "course-pack.md";
|
|
8381
|
+
a.click();
|
|
8382
|
+
URL.revokeObjectURL(a.href);
|
|
8383
|
+
showStudyToast(tr("rail.export.done", { file: a.download }), { severity: "success" });
|
|
8384
|
+
} catch (err) {
|
|
8385
|
+
showStudyToast(friendlyError(err, "action"), { severity: "error" });
|
|
8386
|
+
}
|
|
8387
|
+
})();
|
|
8388
|
+
}
|
|
8389
|
+
}, (0, react.createElement)(IconDownloadOutline16, { size: 14 }))), (0, react.createElement)("div", {
|
|
8060
8390
|
className: `lks14-masteryhair${course.avgMasteryPct === 100 ? " gold" : ""}`,
|
|
8061
8391
|
title: course.avgMasteryPct === null ? tr("rail.avg.none") : tr("rail.avg", { pct: course.avgMasteryPct })
|
|
8062
8392
|
}, (0, react.createElement)("i", { style: { transform: `scaleX(${(course.avgMasteryPct ?? 0) / 100})` } })), (0, react.createElement)("div", { className: "lks14-railcard-row tools" }, (0, react.createElement)("button", {
|
|
@@ -8219,8 +8549,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8219
8549
|
}, (0, react.createElement)(IconRefreshOutline16, { size: 13 }), tr("review.random")), (0, react.createElement)("button", {
|
|
8220
8550
|
className: "lks-btn primary",
|
|
8221
8551
|
onClick: () => {
|
|
8552
|
+
const first = data.due[0];
|
|
8222
8553
|
setReviewOpen(false);
|
|
8223
|
-
|
|
8554
|
+
if (first !== void 0) {
|
|
8555
|
+
showStudyToast(tr("review.banner", { lesson: first.lessonTitle }), { severity: "info" });
|
|
8556
|
+
send(tr("prompt.review"), tr("review.threadTitle", { lesson: first.lessonTitle }), first.lessonId);
|
|
8557
|
+
} else send(tr("prompt.review"));
|
|
8224
8558
|
}
|
|
8225
8559
|
}, tr("rail.due.start"))) : (0, react.createElement)("div", { className: "lks14-empty" }, tr("rail.due.none"))) : null;
|
|
8226
8560
|
const effectivePanel = data !== null && data.courses.length === 0 ? "import" : panel;
|
|
@@ -8500,10 +8834,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8500
8834
|
return cut <= 0 ? path : norm.slice(0, cut);
|
|
8501
8835
|
}
|
|
8502
8836
|
/** 中栏:the tutor chat stream with its own composer (upstream ChatStream + ChatComposer). */
|
|
8503
|
-
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, stop, setMode, narrowPane,
|
|
8837
|
+
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, stop, setMode, narrowPane, onThreadSwitch, onThreadNew, onThreadRename, onThreadArchive, onThreadDelete, contextMeter, contextBreakdown, uploadAttachment }) {
|
|
8504
8838
|
const stuck = (0, react.useRef)(true);
|
|
8505
8839
|
const [showFab, setShowFab] = (0, react.useState)(false);
|
|
8506
8840
|
const [threadsOpen, setThreadsOpen] = (0, react.useState)(false);
|
|
8841
|
+
const [gearFor, setGearFor] = (0, react.useState)(null);
|
|
8842
|
+
const [renaming, setRenaming] = (0, react.useState)(null);
|
|
8843
|
+
const [confirmThreadDel, setConfirmThreadDel] = (0, react.useState)(null);
|
|
8844
|
+
const [pendingAttach, setPendingAttach] = (0, react.useState)(null);
|
|
8507
8845
|
const onStreamScroll = () => {
|
|
8508
8846
|
const el = streamEl.current;
|
|
8509
8847
|
if (el === null) return;
|
|
@@ -8701,7 +9039,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8701
9039
|
return ids;
|
|
8702
9040
|
}, [rowsView]);
|
|
8703
9041
|
const backlog = (0, react.useMemo)(() => lesson !== null ? sedimentBacklog(lesson.artifacts, inlineIds, unseenArtifacts(lesson.lessonId, lesson.artifacts)) : [], [lesson, inlineIds]);
|
|
8704
|
-
const inlineArtifactCard = (row) => {
|
|
9042
|
+
const inlineArtifactCard = (row, revealed = false) => {
|
|
8705
9043
|
if (lesson === null || row.artifactId === void 0) return null;
|
|
8706
9044
|
const artifact = lesson.artifacts.find((a) => a.id === row.artifactId);
|
|
8707
9045
|
if (artifact === void 0) return chatRow({
|
|
@@ -8716,26 +9054,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8716
9054
|
masteryPct: lesson.masteryPct,
|
|
8717
9055
|
send,
|
|
8718
9056
|
onFinished: () => {}
|
|
8719
|
-
}) : (0, react.createElement)(
|
|
9057
|
+
}) : (0, react.createElement)(FoldableArtifactCard, {
|
|
8720
9058
|
artifact,
|
|
8721
|
-
send
|
|
9059
|
+
send,
|
|
9060
|
+
revealed
|
|
8722
9061
|
}));
|
|
8723
9062
|
};
|
|
8724
|
-
const threadPills = [];
|
|
8725
|
-
if (data !== null) {
|
|
8726
|
-
const titles = /* @__PURE__ */ new Map();
|
|
8727
|
-
for (const c of data.courses) for (const s of c.sections) for (const l of s.lessons) titles.set(l.id, l.title);
|
|
8728
|
-
for (const [lessonId, sessionId] of Object.entries(data.lessonSessions)) {
|
|
8729
|
-
if (sessionId === void 0) continue;
|
|
8730
|
-
const title = titles.get(lessonId);
|
|
8731
|
-
if (title === void 0) continue;
|
|
8732
|
-
threadPills.push({
|
|
8733
|
-
id: lessonId,
|
|
8734
|
-
title,
|
|
8735
|
-
current: lesson !== null && lesson.lessonId === lessonId
|
|
8736
|
-
});
|
|
8737
|
-
}
|
|
8738
|
-
}
|
|
9063
|
+
const threadPills = threadGroupPills(lesson !== null && data !== null ? data.lessonThreads[lesson.lessonId] : void 0);
|
|
8739
9064
|
const lastAssistant = rowsView.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
8740
9065
|
const [error, setError] = (0, react.useState)(null);
|
|
8741
9066
|
const fire = (action) => {
|
|
@@ -8755,18 +9080,31 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8755
9080
|
for (let i = 0; i < bytes.length; i += CHUNK) bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
8756
9081
|
return uploadAttachment(file.name, btoa(bin));
|
|
8757
9082
|
}).then((path) => {
|
|
8758
|
-
|
|
9083
|
+
setPendingAttach({
|
|
8759
9084
|
name: file.name,
|
|
8760
|
-
path
|
|
8761
|
-
|
|
9085
|
+
path,
|
|
9086
|
+
image: /\.(?:png|jpe?g|gif|webp|bmp)$/i.test(file.name)
|
|
9087
|
+
});
|
|
8762
9088
|
}, () => {
|
|
8763
9089
|
showStudyToast(tr("attach.fail"), { severity: "error" });
|
|
8764
9090
|
});
|
|
8765
9091
|
};
|
|
9092
|
+
/** One send = draft + pending attachment merged into a single message (issue #7). */
|
|
9093
|
+
const submitComposer = () => {
|
|
9094
|
+
if (busy) return;
|
|
9095
|
+
const text = draft.trim();
|
|
9096
|
+
if (text === "" && pendingAttach === null) return;
|
|
9097
|
+
const attachLine = pendingAttach === null ? "" : tr("attach.sent", {
|
|
9098
|
+
name: pendingAttach.name,
|
|
9099
|
+
path: pendingAttach.path
|
|
9100
|
+
});
|
|
9101
|
+
setPendingAttach(null);
|
|
9102
|
+
send(attachLine === "" ? text : text === "" ? attachLine : `${text}\n\n${attachLine}`);
|
|
9103
|
+
};
|
|
8766
9104
|
const onComposerKey = (e) => {
|
|
8767
9105
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
8768
9106
|
e.preventDefault();
|
|
8769
|
-
if (draft.trim() !== "" && !busy)
|
|
9107
|
+
if ((draft.trim() !== "" || pendingAttach !== null) && !busy) submitComposer();
|
|
8770
9108
|
}
|
|
8771
9109
|
};
|
|
8772
9110
|
const proposal = data?.pendingProposals[0] ?? null;
|
|
@@ -8823,19 +9161,115 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8823
9161
|
}, tr("threads.open", { n: String(threadPills.length) }), (0, react.createElement)("span", { className: "lks14-threadchip-caret" }, threadsOpen ? "▾" : "▸")), threadsOpen ? (0, react.createElement)("div", {
|
|
8824
9162
|
className: "lks14-threadmenu",
|
|
8825
9163
|
role: "menu"
|
|
8826
|
-
}, ...threadPills.map((p) => (0, react.createElement)("
|
|
9164
|
+
}, ...threadPills.map((p) => (0, react.createElement)("div", {
|
|
8827
9165
|
key: p.id,
|
|
8828
9166
|
className: `lks14-threadmenu-row${p.current ? " on" : ""}`,
|
|
8829
9167
|
role: "menuitem",
|
|
9168
|
+
tabIndex: 0,
|
|
8830
9169
|
"aria-current": p.current ? "true" : void 0,
|
|
8831
9170
|
onClick: () => {
|
|
9171
|
+
if (renaming !== null && renaming.id === p.id) return;
|
|
8832
9172
|
setThreadsOpen(false);
|
|
8833
|
-
|
|
9173
|
+
setGearFor(null);
|
|
9174
|
+
if (!p.current) onThreadSwitch(p.id);
|
|
8834
9175
|
}
|
|
8835
9176
|
}, (0, react.createElement)("span", {
|
|
8836
9177
|
className: "lks14-threadmenu-dot",
|
|
8837
9178
|
"aria-hidden": "true"
|
|
8838
|
-
}),
|
|
9179
|
+
}), renaming !== null && renaming.id === p.id ? (0, react.createElement)("input", {
|
|
9180
|
+
className: "lks14-threadmenu-rename",
|
|
9181
|
+
value: renaming.value,
|
|
9182
|
+
autoFocus: true,
|
|
9183
|
+
"data-testid": "thread-rename-input",
|
|
9184
|
+
onChange: (e) => {
|
|
9185
|
+
setRenaming({
|
|
9186
|
+
id: p.id,
|
|
9187
|
+
value: e.target.value
|
|
9188
|
+
});
|
|
9189
|
+
},
|
|
9190
|
+
onClick: (e) => {
|
|
9191
|
+
e.stopPropagation();
|
|
9192
|
+
},
|
|
9193
|
+
onKeyDown: (e) => {
|
|
9194
|
+
if (e.key === "Enter") {
|
|
9195
|
+
const v = renaming.value.trim();
|
|
9196
|
+
if (v !== "") onThreadRename(p.id, v);
|
|
9197
|
+
setRenaming(null);
|
|
9198
|
+
}
|
|
9199
|
+
if (e.key === "Escape") setRenaming(null);
|
|
9200
|
+
},
|
|
9201
|
+
onBlur: () => {
|
|
9202
|
+
const v = renaming.value.trim();
|
|
9203
|
+
if (v !== "" && v !== p.title) onThreadRename(p.id, v);
|
|
9204
|
+
setRenaming(null);
|
|
9205
|
+
}
|
|
9206
|
+
}) : (0, react.createElement)("span", { className: "lks14-threadmenu-title" }, p.title), p.current ? (0, react.createElement)("span", { className: "lks14-threadmenu-now" }, tr("threads.now")) : null, (0, react.createElement)("button", {
|
|
9207
|
+
className: "lks14-threadmenu-gear",
|
|
9208
|
+
"aria-label": tr("threads.actions"),
|
|
9209
|
+
"data-tooltip": tr("threads.actions"),
|
|
9210
|
+
"data-testid": `thread-gear`,
|
|
9211
|
+
onClick: (e) => {
|
|
9212
|
+
e.stopPropagation();
|
|
9213
|
+
setGearFor(gearFor === p.id ? null : p.id);
|
|
9214
|
+
}
|
|
9215
|
+
}, "⋯"), gearFor === p.id ? (0, react.createElement)("div", {
|
|
9216
|
+
className: "lks14-threadmenu-actions",
|
|
9217
|
+
role: "menu",
|
|
9218
|
+
"data-testid": "thread-actions",
|
|
9219
|
+
onClick: (e) => {
|
|
9220
|
+
e.stopPropagation();
|
|
9221
|
+
}
|
|
9222
|
+
}, (0, react.createElement)("button", { onClick: () => {
|
|
9223
|
+
setGearFor(null);
|
|
9224
|
+
setRenaming({
|
|
9225
|
+
id: p.id,
|
|
9226
|
+
value: p.title
|
|
9227
|
+
});
|
|
9228
|
+
} }, tr("threads.rename")), (0, react.createElement)("button", { onClick: () => {
|
|
9229
|
+
setGearFor(null);
|
|
9230
|
+
onThreadArchive(p.id);
|
|
9231
|
+
} }, tr("threads.archive")), (0, react.createElement)("button", {
|
|
9232
|
+
className: "danger",
|
|
9233
|
+
"data-testid": "thread-delete",
|
|
9234
|
+
onClick: (e) => {
|
|
9235
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
9236
|
+
setConfirmThreadDel({
|
|
9237
|
+
id: p.id,
|
|
9238
|
+
title: p.title,
|
|
9239
|
+
rect: {
|
|
9240
|
+
left: r.left,
|
|
9241
|
+
top: r.top,
|
|
9242
|
+
right: r.right,
|
|
9243
|
+
bottom: r.bottom
|
|
9244
|
+
}
|
|
9245
|
+
});
|
|
9246
|
+
setGearFor(null);
|
|
9247
|
+
}
|
|
9248
|
+
}, tr("threads.delete"))) : null)), (0, react.createElement)("button", {
|
|
9249
|
+
key: "__new__",
|
|
9250
|
+
className: "lks14-threadmenu-row new",
|
|
9251
|
+
role: "menuitem",
|
|
9252
|
+
onClick: () => {
|
|
9253
|
+
setThreadsOpen(false);
|
|
9254
|
+
onThreadNew();
|
|
9255
|
+
}
|
|
9256
|
+
}, (0, react.createElement)("span", {
|
|
9257
|
+
className: "lks14-threadmenu-plus",
|
|
9258
|
+
"aria-hidden": "true"
|
|
9259
|
+
}, "+"), (0, react.createElement)("span", { className: "lks14-threadmenu-title" }, tr("threads.new"))), confirmThreadDel !== null ? (0, react.createElement)(ConfirmCard, {
|
|
9260
|
+
anchor: confirmThreadDel.rect,
|
|
9261
|
+
message: tr("threads.delete.confirm", { name: confirmThreadDel.title }),
|
|
9262
|
+
danger: true,
|
|
9263
|
+
confirmLabel: tr("threads.delete"),
|
|
9264
|
+
onConfirm: () => {
|
|
9265
|
+
const id = confirmThreadDel.id;
|
|
9266
|
+
setConfirmThreadDel(null);
|
|
9267
|
+
onThreadDelete(id);
|
|
9268
|
+
},
|
|
9269
|
+
onCancel: () => {
|
|
9270
|
+
setConfirmThreadDel(null);
|
|
9271
|
+
}
|
|
9272
|
+
}) : null) : null) : null) : null, (0, react.createElement)("div", {
|
|
8839
9273
|
className: "lks14-stream",
|
|
8840
9274
|
onScroll: onStreamScroll,
|
|
8841
9275
|
ref: streamEl,
|
|
@@ -8846,7 +9280,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8846
9280
|
onClick: () => {
|
|
8847
9281
|
send(starters[0]?.message ?? tr("prompt.start"));
|
|
8848
9282
|
}
|
|
8849
|
-
}, tr("chat.start")) : null) : rowsView.map((row, i) => row.role === "artifact" ? inlineArtifactCard(row) : chatRow(row, !dormant && i === lastAssistant ? { send } : void 0, row.role === "assistant" ? {
|
|
9283
|
+
}, tr("chat.start")) : null) : rowsView.map((row, i) => row.role === "artifact" ? inlineArtifactCard(row, rowsView.slice(i + 1).some((r) => r.role === "assistant")) : chatRow(row, !dormant && i === lastAssistant ? { send } : void 0, row.role === "assistant" ? {
|
|
8850
9284
|
playing: msgAudio?.key === row.key,
|
|
8851
9285
|
index: msgAudio?.index ?? 0,
|
|
8852
9286
|
total: msgAudio?.total ?? 0,
|
|
@@ -8860,10 +9294,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8860
9294
|
masteryPct: lesson.masteryPct,
|
|
8861
9295
|
send,
|
|
8862
9296
|
onFinished: () => {}
|
|
8863
|
-
})), ...backlog.filter((a) => a.artifactType !== "quiz").map((a) => (0, react.createElement)(
|
|
9297
|
+
})), ...backlog.filter((a) => a.artifactType !== "quiz").map((a) => (0, react.createElement)(FoldableArtifactCard, {
|
|
8864
9298
|
key: a.id,
|
|
8865
9299
|
artifact: a,
|
|
8866
|
-
send
|
|
9300
|
+
send,
|
|
9301
|
+
defaultFolded: true
|
|
8867
9302
|
})), rows.length > 0 ? (0, react.createElement)("div", { className: "lks14-starters" }, ...starters.map((s) => (0, react.createElement)("button", {
|
|
8868
9303
|
key: s.label,
|
|
8869
9304
|
className: "lks14-starter",
|
|
@@ -8893,7 +9328,18 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8893
9328
|
onClick: () => {
|
|
8894
9329
|
if (!dormant) fire(setMode(mode.id));
|
|
8895
9330
|
}
|
|
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)("
|
|
9331
|
+
}, 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", {
|
|
9332
|
+
className: "lks14-attachchip-thumb",
|
|
9333
|
+
src: `/lookatstudy/api/attachment/${encodeURIComponent(pendingAttach.path.split("/").pop() ?? "")}`,
|
|
9334
|
+
alt: pendingAttach.name
|
|
9335
|
+
}) : null, (0, react.createElement)("span", { className: "lks14-attachchip-name" }, pendingAttach.name), (0, react.createElement)("button", {
|
|
9336
|
+
className: "lks14-attachchip-x",
|
|
9337
|
+
"aria-label": tr("attach.remove"),
|
|
9338
|
+
"data-tooltip": tr("attach.remove"),
|
|
9339
|
+
onClick: () => {
|
|
9340
|
+
setPendingAttach(null);
|
|
9341
|
+
}
|
|
9342
|
+
}, "✕")) : null, (0, react.createElement)("textarea", {
|
|
8897
9343
|
className: "lks14-composertext",
|
|
8898
9344
|
placeholder: busy ? tr("composer.busy") : tr("tutor.empty.hint"),
|
|
8899
9345
|
value: draft,
|
|
@@ -8983,9 +9429,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8983
9429
|
}, (0, react.createElement)(IconCloseFill16, { size: 14 })) : (0, react.createElement)("button", {
|
|
8984
9430
|
className: "lks-btn-send",
|
|
8985
9431
|
"aria-label": tr("composer.send"),
|
|
8986
|
-
disabled: dormant || draft.trim() === "",
|
|
9432
|
+
disabled: dormant || draft.trim() === "" && pendingAttach === null,
|
|
8987
9433
|
onClick: () => {
|
|
8988
|
-
|
|
9434
|
+
submitComposer();
|
|
8989
9435
|
}
|
|
8990
9436
|
}, (0, react.createElement)(IconArrowUpFill16, { size: 18 }))), showFab ? (0, react.createElement)("button", {
|
|
8991
9437
|
className: `lks14-scrollfab${busy ? " streaming" : ""}`,
|
|
@@ -9008,6 +9454,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9008
9454
|
const [zoneOverrides, setZoneOverrides] = (0, react.useState)({});
|
|
9009
9455
|
const zoneCounts = (0, react.useRef)({});
|
|
9010
9456
|
const [editingNote, setEditingNote] = (0, react.useState)(null);
|
|
9457
|
+
const [noteRev, setNoteRev] = (0, react.useState)(0);
|
|
9011
9458
|
const [editDraft, setEditDraft] = (0, react.useState)("");
|
|
9012
9459
|
const [pendingLocate, setPendingLocate] = (0, react.useState)(null);
|
|
9013
9460
|
const [cmapExpanded, setCmapExpanded] = (0, react.useState)(false);
|
|
@@ -9344,9 +9791,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9344
9791
|
}
|
|
9345
9792
|
setQuoteBtn(null);
|
|
9346
9793
|
window.getSelection()?.removeAllRanges();
|
|
9347
|
-
} }, tr("note.quote.save"))) : null), latestHeavy !== null ? (0, react.createElement)("div", { className: "lks-acard-stage" }, (0, react.createElement)(
|
|
9794
|
+
} }, tr("note.quote.save"))) : null), latestHeavy !== null ? (0, react.createElement)("div", { className: "lks-acard-stage" }, (0, react.createElement)(FoldableArtifactCard, {
|
|
9348
9795
|
artifact: latestHeavy,
|
|
9349
|
-
send: () => {}
|
|
9796
|
+
send: () => {},
|
|
9797
|
+
defaultFolded: true
|
|
9350
9798
|
})) : null) : tab === "board" ? (() => {
|
|
9351
9799
|
const heavy = artifacts.filter((a) => a.artifactType === "compare_table" || a.artifactType === "diagram" || a.artifactType === "code_walkthrough");
|
|
9352
9800
|
const latest = heavy.length > 0 ? heavy[heavy.length - 1] : null;
|
|
@@ -9431,7 +9879,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9431
9879
|
}));
|
|
9432
9880
|
}
|
|
9433
9881
|
}, 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
|
|
9882
|
+
key: `${n.id}:${String(noteRev)}`,
|
|
9435
9883
|
className: `lks-note${n.pinned ? " pinned" : ""}`
|
|
9436
9884
|
}, (0, react.createElement)("span", { className: "lks-note-src" }, tr("note.src." + n.source)), (0, react.createElement)("button", {
|
|
9437
9885
|
className: "lks-note-act",
|
|
@@ -9492,6 +9940,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9492
9940
|
disabled: editDraft.trim() === "",
|
|
9493
9941
|
onClick: () => {
|
|
9494
9942
|
setEditingNote(null);
|
|
9943
|
+
setNoteRev((r) => r + 1);
|
|
9495
9944
|
fire(editNote(lesson.lessonId, n.id, editDraft));
|
|
9496
9945
|
}
|
|
9497
9946
|
}, tr("note.save")))) : (0, react.createElement)(ContentBoundary, {
|
|
@@ -9499,7 +9948,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9499
9948
|
boundaryKey: `note-${n.id}`
|
|
9500
9949
|
}, (0, react.createElement)("div", {
|
|
9501
9950
|
className: "lks-note-text",
|
|
9502
|
-
|
|
9951
|
+
ref: (el) => {
|
|
9952
|
+
if (el !== null) enhanceRendered(el).catch(() => {});
|
|
9953
|
+
},
|
|
9954
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(normalizeMathNotation(n.text)) }
|
|
9503
9955
|
})), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `'${n.quote}'`) : null)) : []);
|
|
9504
9956
|
})));
|
|
9505
9957
|
return (0, react.createElement)("div", { className: "lks14-col lks14-note" }, body, confirmNoteDel !== null ? (0, react.createElement)(ConfirmCard, {
|