dsh-plugin-lookatstudy 0.21.1 → 0.22.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 +464 -53
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +774 -107
- 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)}
|
|
@@ -1117,6 +1133,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1117
1133
|
.lks-acard-guess-opt.picked{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
|
|
1118
1134
|
.lks-acard-guess-wait{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
1119
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)}
|
|
1120
1145
|
.lks-acard-stage .lks-acard{margin:0}
|
|
1121
1146
|
.lks14-zone .lks-acard{margin:0 0 8px}
|
|
1122
1147
|
/* notebook tab badge (unseen artifacts) */
|
|
@@ -1770,17 +1795,39 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1770
1795
|
this.refresh();
|
|
1771
1796
|
}
|
|
1772
1797
|
/** Record the dsh session backing one lesson (the simplified thread system). */
|
|
1773
|
-
|
|
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) {
|
|
1774
1804
|
await fetchJson("/lookatstudy/api/lesson-session", {
|
|
1775
1805
|
method: "POST",
|
|
1776
1806
|
headers: { "content-type": "application/json" },
|
|
1777
1807
|
body: JSON.stringify({
|
|
1778
1808
|
lessonId,
|
|
1779
|
-
sessionId
|
|
1809
|
+
sessionId,
|
|
1810
|
+
title
|
|
1780
1811
|
})
|
|
1781
1812
|
});
|
|
1782
1813
|
this.refresh();
|
|
1783
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
|
|
1826
|
+
})
|
|
1827
|
+
});
|
|
1828
|
+
this.refresh();
|
|
1829
|
+
return res;
|
|
1830
|
+
}
|
|
1784
1831
|
/** Delete one course (and its proposals) from the host state. */
|
|
1785
1832
|
async deleteCourse(courseId) {
|
|
1786
1833
|
await fetchJson("/lookatstudy/api/course/delete", {
|
|
@@ -1993,7 +2040,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1993
2040
|
examRegenerate: studyStore.examRegenerate.bind(studyStore),
|
|
1994
2041
|
addUserNote: studyStore.addUserNote.bind(studyStore),
|
|
1995
2042
|
recordReview: studyStore.recordReview.bind(studyStore),
|
|
1996
|
-
bindLessonSession: studyStore.bindLessonSession.bind(studyStore)
|
|
2043
|
+
bindLessonSession: studyStore.bindLessonSession.bind(studyStore),
|
|
2044
|
+
lessonThreadOp: studyStore.lessonThreadOp.bind(studyStore)
|
|
1997
2045
|
};
|
|
1998
2046
|
}
|
|
1999
2047
|
//#endregion
|
|
@@ -2051,6 +2099,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2051
2099
|
"artifact.guess.title": "猜一猜",
|
|
2052
2100
|
"artifact.guess.pick": "我选「{label}」",
|
|
2053
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": "示意图",
|
|
2054
2108
|
"artifact.sedimented": "有新的学习产物沉淀进笔记",
|
|
2055
2109
|
"zone.artifacts": "AI 产物",
|
|
2056
2110
|
"note.quote.ask": "提问这段",
|
|
@@ -2170,6 +2224,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2170
2224
|
"palette.empty": "没有匹配项",
|
|
2171
2225
|
"threads.open": "{n} 条对话",
|
|
2172
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}",
|
|
2173
2237
|
"threads.label": "课时线程",
|
|
2174
2238
|
"meter.label": "上下文用量",
|
|
2175
2239
|
"meter.aria": "上下文已用 {pct}%",
|
|
@@ -2398,6 +2462,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2398
2462
|
"artifact.guess.title": "Take a guess",
|
|
2399
2463
|
"artifact.guess.pick": "I pick \"{label}\"",
|
|
2400
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",
|
|
2401
2471
|
"artifact.sedimented": "A new artifact settled into the notebook",
|
|
2402
2472
|
"zone.artifacts": "AI artifacts",
|
|
2403
2473
|
"note.quote.ask": "Ask about this",
|
|
@@ -2518,6 +2588,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2518
2588
|
"threads.label": "Lesson threads",
|
|
2519
2589
|
"threads.open": "{n} threads",
|
|
2520
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}",
|
|
2521
2601
|
"meter.label": "Context usage",
|
|
2522
2602
|
"meter.aria": "{pct}% of context used",
|
|
2523
2603
|
"meter.brk.system": "System prompt",
|
|
@@ -3627,6 +3707,28 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3627
3707
|
return stored === "rail" || stored === "chat" || stored === "note" ? stored : "chat";
|
|
3628
3708
|
}
|
|
3629
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
|
+
/**
|
|
3630
3732
|
* Default expansion for one rail section: collapsed when every study lesson is
|
|
3631
3733
|
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
3632
3734
|
* stays open; exam nodes never force a section open. Pure.
|
|
@@ -4537,6 +4639,27 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4537
4639
|
}
|
|
4538
4640
|
return depth > 0 || liveChunkOrphan;
|
|
4539
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
|
+
}
|
|
4540
4663
|
/** The window's highest event seq (the stop watermark's source). */
|
|
4541
4664
|
function feedLastSeq(window) {
|
|
4542
4665
|
if (window === void 0 || window.entries === void 0) return 0;
|
|
@@ -5854,6 +5977,54 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5854
5977
|
storage.setItem(seenArtifactsKey(lessonId), JSON.stringify([.../* @__PURE__ */ new Set([...seen, ...ids])]));
|
|
5855
5978
|
} catch {}
|
|
5856
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
|
+
}
|
|
5857
6028
|
/** The cards render mermaid through the shared CDN pipeline
|
|
5858
6029
|
* (enhanceDiagrams consumes pre > code.lang-mermaid blocks). */
|
|
5859
6030
|
async function renderMermaidInto(el, code, onError) {
|
|
@@ -5960,13 +6131,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5960
6131
|
})))) : null);
|
|
5961
6132
|
}
|
|
5962
6133
|
/** The opening guess: two big options, click = pick (sent to the tutor, who
|
|
5963
|
-
* reveals next turn); unscored — no right/wrong language anywhere.
|
|
5964
|
-
|
|
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 }) {
|
|
5965
6139
|
const prompt = typeof artifact.data.prompt === "string" ? artifact.data.prompt : "";
|
|
5966
6140
|
const options = artifact.data.options ?? [];
|
|
5967
|
-
const [picked, setPicked] = (0, react.useState)(
|
|
6141
|
+
const [picked, setPicked] = (0, react.useState)(() => guessPickedOf(artifact.id));
|
|
5968
6142
|
(0, react.useEffect)(() => {
|
|
5969
|
-
setPicked(
|
|
6143
|
+
setPicked(guessPickedOf(artifact.id));
|
|
5970
6144
|
}, [artifact.id]);
|
|
5971
6145
|
return (0, react.createElement)("div", {
|
|
5972
6146
|
className: "lks-acard lks-acard-guess",
|
|
@@ -5977,23 +6151,67 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5977
6151
|
disabled: picked !== null,
|
|
5978
6152
|
onClick: () => {
|
|
5979
6153
|
setPicked(opt.id);
|
|
6154
|
+
markGuessPicked(artifact.id, opt.id);
|
|
5980
6155
|
send(tr("artifact.guess.pick", { label: opt.label }));
|
|
5981
6156
|
}
|
|
5982
|
-
}, 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);
|
|
5983
6158
|
}
|
|
5984
6159
|
/** Dispatch one artifact row to its card. */
|
|
5985
|
-
function ArtifactCard({ artifact, send }) {
|
|
6160
|
+
function ArtifactCard({ artifact, send, revealed = false }) {
|
|
5986
6161
|
switch (artifact.artifactType) {
|
|
5987
6162
|
case "compare_table": return (0, react.createElement)(CompareTableCard, { artifact });
|
|
5988
6163
|
case "code_walkthrough": return (0, react.createElement)(CodeWalkthroughCard, { artifact });
|
|
5989
6164
|
case "diagram": return (0, react.createElement)(DiagramCard, { artifact });
|
|
5990
6165
|
case "guess": return (0, react.createElement)(GuessCard, {
|
|
5991
6166
|
artifact,
|
|
5992
|
-
send
|
|
6167
|
+
send,
|
|
6168
|
+
revealed
|
|
5993
6169
|
});
|
|
5994
6170
|
default: return null;
|
|
5995
6171
|
}
|
|
5996
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
|
+
}
|
|
5997
6215
|
//#endregion
|
|
5998
6216
|
//#region src/client/highlights.ts
|
|
5999
6217
|
/**
|
|
@@ -7106,7 +7324,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7106
7324
|
};
|
|
7107
7325
|
}
|
|
7108
7326
|
function StudyPanelBody({ ctx }) {
|
|
7109
|
-
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, uploadAttachment } = useStudy();
|
|
7327
|
+
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, uploadAttachment } = useStudy();
|
|
7110
7328
|
(0, react.useEffect)(() => {
|
|
7111
7329
|
wireCodeBlockCopy();
|
|
7112
7330
|
}, []);
|
|
@@ -7199,6 +7417,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7199
7417
|
const [feedGen, setFeedGen] = (0, react.useState)(false);
|
|
7200
7418
|
const lastSeqRef = (0, react.useRef)(0);
|
|
7201
7419
|
const stopWatermarkRef = (0, react.useRef)(null);
|
|
7420
|
+
const lastAdvanceAtRef = (0, react.useRef)(0);
|
|
7202
7421
|
const examSessionRef = (0, react.useRef)({
|
|
7203
7422
|
active: false,
|
|
7204
7423
|
terminate: null
|
|
@@ -7252,6 +7471,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7252
7471
|
const lesson = data?.lesson ?? null;
|
|
7253
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 ?? []);
|
|
7254
7473
|
const localBound = (0, react.useRef)(null);
|
|
7474
|
+
const boundSessionOf = (lessonId) => lessonId !== null && localBound.current?.lessonId === lessonId ? localBound.current.sessionId : null;
|
|
7255
7475
|
const streamEl = (0, react.useRef)(null);
|
|
7256
7476
|
const touchStart = (0, react.useRef)(null);
|
|
7257
7477
|
const activeFace = (0, react.useRef)(null);
|
|
@@ -7260,7 +7480,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7260
7480
|
try {
|
|
7261
7481
|
let face = activeFace.current;
|
|
7262
7482
|
if (face === null) {
|
|
7263
|
-
const sessionId =
|
|
7483
|
+
const sessionId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? (lesson !== null ? data?.lessonThreads[lesson.lessonId]?.active ?? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7264
7484
|
if (sessionId !== null && sessionKnown(ctx, sessionId)) {
|
|
7265
7485
|
const actx = ctx.sessions.scope(sessionId);
|
|
7266
7486
|
face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx) ?? null;
|
|
@@ -7296,7 +7516,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7296
7516
|
} else celebrate(ev.kind);
|
|
7297
7517
|
prevCelebrationRef.current = snapshot;
|
|
7298
7518
|
}, [data]);
|
|
7299
|
-
const boundId =
|
|
7519
|
+
const boundId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? (lesson !== null ? data?.lessonThreads[lesson.lessonId]?.active ?? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
7300
7520
|
const nudged = (0, react.useRef)(false);
|
|
7301
7521
|
(0, react.useEffect)(() => {
|
|
7302
7522
|
if (nudged.current || (data?.dueCount ?? 0) === 0) return;
|
|
@@ -7400,8 +7620,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7400
7620
|
const update = () => {
|
|
7401
7621
|
const snap = source.getSnapshot();
|
|
7402
7622
|
setRows(feedRows(snap));
|
|
7403
|
-
|
|
7404
|
-
|
|
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()));
|
|
7405
7629
|
};
|
|
7406
7630
|
update();
|
|
7407
7631
|
setFeedAttached(true);
|
|
@@ -7455,16 +7679,31 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7455
7679
|
const el = streamEl.current;
|
|
7456
7680
|
if (el !== null) el.scrollTop = el.scrollHeight;
|
|
7457
7681
|
}, [rows]);
|
|
7458
|
-
|
|
7459
|
-
|
|
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) => {
|
|
7460
7696
|
(async () => {
|
|
7461
7697
|
if (busy || lesson === null || text.trim() === "") return;
|
|
7462
7698
|
setBusy(true);
|
|
7463
7699
|
setSendError(null);
|
|
7464
7700
|
stopWatermarkRef.current = null;
|
|
7701
|
+
lastAdvanceAtRef.current = Date.now();
|
|
7465
7702
|
try {
|
|
7466
7703
|
if (data?.active !== true) await activate(true);
|
|
7467
|
-
|
|
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;
|
|
7468
7707
|
if (sessionId !== null && !sessionKnown(ctx, sessionId)) sessionId = null;
|
|
7469
7708
|
if (sessionId === null) {
|
|
7470
7709
|
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
@@ -7472,9 +7711,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7472
7711
|
const { path } = await area.json();
|
|
7473
7712
|
const workspace = await ctx.workspaces.create({ path });
|
|
7474
7713
|
sessionId = await ctx.sessions.create({ workspaceId: workspace.workspaceId });
|
|
7475
|
-
localBound.current =
|
|
7476
|
-
|
|
7477
|
-
|
|
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
|
+
};
|
|
7478
7723
|
const shell = PANEL_SHELL.current;
|
|
7479
7724
|
if (shell !== null) shell.suppressHandBack(() => {
|
|
7480
7725
|
ctx.sessions.open(sessionId);
|
|
@@ -7508,6 +7753,50 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7508
7753
|
window.removeEventListener("lookatstudy-exam-generate", onGenerate);
|
|
7509
7754
|
};
|
|
7510
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
|
+
};
|
|
7511
7800
|
const progress = data?.progress ?? null;
|
|
7512
7801
|
const body = (0, react.createElement)("div", {
|
|
7513
7802
|
className: "lks14-body",
|
|
@@ -7598,10 +7887,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7598
7887
|
stop,
|
|
7599
7888
|
setMode,
|
|
7600
7889
|
narrowPane,
|
|
7601
|
-
|
|
7890
|
+
onThreadSwitch: (sessionId) => {
|
|
7602
7891
|
setNarrowPane("chat");
|
|
7603
|
-
|
|
7892
|
+
switchLessonThread(sessionId);
|
|
7893
|
+
},
|
|
7894
|
+
onThreadNew: () => {
|
|
7895
|
+
setNarrowPane("chat");
|
|
7896
|
+
startNewThread();
|
|
7604
7897
|
},
|
|
7898
|
+
onThreadRename: renameLessonThreadUi,
|
|
7899
|
+
onThreadArchive: archiveLessonThreadUi,
|
|
7900
|
+
onThreadDelete: deleteLessonThreadUi,
|
|
7605
7901
|
contextMeter,
|
|
7606
7902
|
contextBreakdown: breakdown,
|
|
7607
7903
|
uploadAttachment
|
|
@@ -8067,7 +8363,30 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8067
8363
|
bottom: r.bottom
|
|
8068
8364
|
} });
|
|
8069
8365
|
}
|
|
8070
|
-
}, (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", {
|
|
8071
8390
|
className: `lks14-masteryhair${course.avgMasteryPct === 100 ? " gold" : ""}`,
|
|
8072
8391
|
title: course.avgMasteryPct === null ? tr("rail.avg.none") : tr("rail.avg", { pct: course.avgMasteryPct })
|
|
8073
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", {
|
|
@@ -8230,8 +8549,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8230
8549
|
}, (0, react.createElement)(IconRefreshOutline16, { size: 13 }), tr("review.random")), (0, react.createElement)("button", {
|
|
8231
8550
|
className: "lks-btn primary",
|
|
8232
8551
|
onClick: () => {
|
|
8552
|
+
const first = data.due[0];
|
|
8233
8553
|
setReviewOpen(false);
|
|
8234
|
-
|
|
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"));
|
|
8235
8558
|
}
|
|
8236
8559
|
}, tr("rail.due.start"))) : (0, react.createElement)("div", { className: "lks14-empty" }, tr("rail.due.none"))) : null;
|
|
8237
8560
|
const effectivePanel = data !== null && data.courses.length === 0 ? "import" : panel;
|
|
@@ -8511,10 +8834,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8511
8834
|
return cut <= 0 ? path : norm.slice(0, cut);
|
|
8512
8835
|
}
|
|
8513
8836
|
/** 中栏:the tutor chat stream with its own composer (upstream ChatStream + ChatComposer). */
|
|
8514
|
-
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 }) {
|
|
8515
8838
|
const stuck = (0, react.useRef)(true);
|
|
8516
8839
|
const [showFab, setShowFab] = (0, react.useState)(false);
|
|
8517
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);
|
|
8518
8844
|
const [pendingAttach, setPendingAttach] = (0, react.useState)(null);
|
|
8519
8845
|
const onStreamScroll = () => {
|
|
8520
8846
|
const el = streamEl.current;
|
|
@@ -8713,7 +9039,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8713
9039
|
return ids;
|
|
8714
9040
|
}, [rowsView]);
|
|
8715
9041
|
const backlog = (0, react.useMemo)(() => lesson !== null ? sedimentBacklog(lesson.artifacts, inlineIds, unseenArtifacts(lesson.lessonId, lesson.artifacts)) : [], [lesson, inlineIds]);
|
|
8716
|
-
const inlineArtifactCard = (row) => {
|
|
9042
|
+
const inlineArtifactCard = (row, revealed = false) => {
|
|
8717
9043
|
if (lesson === null || row.artifactId === void 0) return null;
|
|
8718
9044
|
const artifact = lesson.artifacts.find((a) => a.id === row.artifactId);
|
|
8719
9045
|
if (artifact === void 0) return chatRow({
|
|
@@ -8728,26 +9054,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8728
9054
|
masteryPct: lesson.masteryPct,
|
|
8729
9055
|
send,
|
|
8730
9056
|
onFinished: () => {}
|
|
8731
|
-
}) : (0, react.createElement)(
|
|
9057
|
+
}) : (0, react.createElement)(FoldableArtifactCard, {
|
|
8732
9058
|
artifact,
|
|
8733
|
-
send
|
|
9059
|
+
send,
|
|
9060
|
+
revealed
|
|
8734
9061
|
}));
|
|
8735
9062
|
};
|
|
8736
|
-
const threadPills = [];
|
|
8737
|
-
if (data !== null) {
|
|
8738
|
-
const titles = /* @__PURE__ */ new Map();
|
|
8739
|
-
for (const c of data.courses) for (const s of c.sections) for (const l of s.lessons) titles.set(l.id, l.title);
|
|
8740
|
-
for (const [lessonId, sessionId] of Object.entries(data.lessonSessions)) {
|
|
8741
|
-
if (sessionId === void 0) continue;
|
|
8742
|
-
const title = titles.get(lessonId);
|
|
8743
|
-
if (title === void 0) continue;
|
|
8744
|
-
threadPills.push({
|
|
8745
|
-
id: lessonId,
|
|
8746
|
-
title,
|
|
8747
|
-
current: lesson !== null && lesson.lessonId === lessonId
|
|
8748
|
-
});
|
|
8749
|
-
}
|
|
8750
|
-
}
|
|
9063
|
+
const threadPills = threadGroupPills(lesson !== null && data !== null ? data.lessonThreads[lesson.lessonId] : void 0);
|
|
8751
9064
|
const lastAssistant = rowsView.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
8752
9065
|
const [error, setError] = (0, react.useState)(null);
|
|
8753
9066
|
const fire = (action) => {
|
|
@@ -8848,19 +9161,115 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8848
9161
|
}, tr("threads.open", { n: String(threadPills.length) }), (0, react.createElement)("span", { className: "lks14-threadchip-caret" }, threadsOpen ? "▾" : "▸")), threadsOpen ? (0, react.createElement)("div", {
|
|
8849
9162
|
className: "lks14-threadmenu",
|
|
8850
9163
|
role: "menu"
|
|
8851
|
-
}, ...threadPills.map((p) => (0, react.createElement)("
|
|
9164
|
+
}, ...threadPills.map((p) => (0, react.createElement)("div", {
|
|
8852
9165
|
key: p.id,
|
|
8853
9166
|
className: `lks14-threadmenu-row${p.current ? " on" : ""}`,
|
|
8854
9167
|
role: "menuitem",
|
|
9168
|
+
tabIndex: 0,
|
|
8855
9169
|
"aria-current": p.current ? "true" : void 0,
|
|
8856
9170
|
onClick: () => {
|
|
9171
|
+
if (renaming !== null && renaming.id === p.id) return;
|
|
8857
9172
|
setThreadsOpen(false);
|
|
8858
|
-
|
|
9173
|
+
setGearFor(null);
|
|
9174
|
+
if (!p.current) onThreadSwitch(p.id);
|
|
8859
9175
|
}
|
|
8860
9176
|
}, (0, react.createElement)("span", {
|
|
8861
9177
|
className: "lks14-threadmenu-dot",
|
|
8862
9178
|
"aria-hidden": "true"
|
|
8863
|
-
}),
|
|
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", {
|
|
8864
9273
|
className: "lks14-stream",
|
|
8865
9274
|
onScroll: onStreamScroll,
|
|
8866
9275
|
ref: streamEl,
|
|
@@ -8871,7 +9280,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8871
9280
|
onClick: () => {
|
|
8872
9281
|
send(starters[0]?.message ?? tr("prompt.start"));
|
|
8873
9282
|
}
|
|
8874
|
-
}, 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" ? {
|
|
8875
9284
|
playing: msgAudio?.key === row.key,
|
|
8876
9285
|
index: msgAudio?.index ?? 0,
|
|
8877
9286
|
total: msgAudio?.total ?? 0,
|
|
@@ -8885,10 +9294,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8885
9294
|
masteryPct: lesson.masteryPct,
|
|
8886
9295
|
send,
|
|
8887
9296
|
onFinished: () => {}
|
|
8888
|
-
})), ...backlog.filter((a) => a.artifactType !== "quiz").map((a) => (0, react.createElement)(
|
|
9297
|
+
})), ...backlog.filter((a) => a.artifactType !== "quiz").map((a) => (0, react.createElement)(FoldableArtifactCard, {
|
|
8889
9298
|
key: a.id,
|
|
8890
9299
|
artifact: a,
|
|
8891
|
-
send
|
|
9300
|
+
send,
|
|
9301
|
+
defaultFolded: true
|
|
8892
9302
|
})), rows.length > 0 ? (0, react.createElement)("div", { className: "lks14-starters" }, ...starters.map((s) => (0, react.createElement)("button", {
|
|
8893
9303
|
key: s.label,
|
|
8894
9304
|
className: "lks14-starter",
|
|
@@ -9381,9 +9791,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9381
9791
|
}
|
|
9382
9792
|
setQuoteBtn(null);
|
|
9383
9793
|
window.getSelection()?.removeAllRanges();
|
|
9384
|
-
} }, 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, {
|
|
9385
9795
|
artifact: latestHeavy,
|
|
9386
|
-
send: () => {}
|
|
9796
|
+
send: () => {},
|
|
9797
|
+
defaultFolded: true
|
|
9387
9798
|
})) : null) : tab === "board" ? (() => {
|
|
9388
9799
|
const heavy = artifacts.filter((a) => a.artifactType === "compare_table" || a.artifactType === "diagram" || a.artifactType === "code_walkthrough");
|
|
9389
9800
|
const latest = heavy.length > 0 ? heavy[heavy.length - 1] : null;
|