dsh-plugin-lookatstudy 0.19.0 → 0.20.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/README.md +28 -10
- package/README.zh-CN.md +121 -0
- package/lib/client.js +528 -577
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +1 -1
- package/lib/index.mjs +56 -32
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -152,8 +152,9 @@ window.__ModuleLoader__.load({
|
|
|
152
152
|
.lks-ui .lks14-pill.on{background:rgb(var(--accent-rgb)/0.12);color:var(--accent-light)}
|
|
153
153
|
|
|
154
154
|
/* ── notebook: teach prose + view tabs ── */
|
|
155
|
-
.lks-ui .lks14-viewtab{color:var(--ink-muted);border-radius:
|
|
156
|
-
.lks-ui .lks14-viewtab
|
|
155
|
+
.lks-ui .lks14-viewtab{color:var(--ink-muted);border-radius:8px;transition:color .15s,background .15s}
|
|
156
|
+
.lks-ui .lks14-viewtab:hover{color:var(--ink)}
|
|
157
|
+
.lks-ui .lks14-viewtab.on{background:rgb(var(--brand-rgb)/0.16);color:var(--brand)}
|
|
157
158
|
.lks-ui .lks14-prose{color:var(--ink);font-size:.9375rem;line-height:1.65}
|
|
158
159
|
.lks-ui .lks14-prose h1,.lks-ui .lks-note-text h1{font-size:1.25rem;font-weight:800;color:var(--ink-strong)}
|
|
159
160
|
.lks-ui .lks14-prose h2,.lks-ui .lks-note-text h2{font-size:1.1rem;font-weight:700;color:var(--ink-strong);border-bottom:1px solid var(--border);padding-bottom:.3em}
|
|
@@ -390,7 +391,7 @@ window.__ModuleLoader__.load({
|
|
|
390
391
|
.lks-ui .lks14-emptycard-hint{font-size:.825rem;color:var(--ink-muted);line-height:1.6}
|
|
391
392
|
/* B5: notebook tab capsule + 960px reading column */
|
|
392
393
|
.lks-ui .lks14-notebody{margin:0 auto;max-width:960px;width:100%;padding:12px 20px 24px;box-sizing:border-box}
|
|
393
|
-
.lks-ui .lks14-viewtabs{background:
|
|
394
|
+
.lks-ui .lks14-viewtabs{background:var(--surface-2);border-radius:10px;padding:3px;align-self:flex-start}
|
|
394
395
|
.lks-ui .lks14-readbar{position:sticky;top:0;z-index:20;background:var(--surface-2);border-radius:10px}
|
|
395
396
|
|
|
396
397
|
/* ══════════ C-track (P11a): interaction chrome ══════════ */
|
|
@@ -445,7 +446,7 @@ window.__ModuleLoader__.load({
|
|
|
445
446
|
/* C6: zone head becomes the collapse toggle */
|
|
446
447
|
.lks-ui .lks14-zoneh{display:flex;align-items:center;gap:8px;background:none;border:none;font:inherit;cursor:pointer;text-align:left;padding:0}
|
|
447
448
|
.lks-ui .lks14-zonecount{font-size:10.5px;font-weight:700;color:var(--brand);background:rgb(var(--brand-rgb)/0.15);border-radius:999px;padding:1px 7px}
|
|
448
|
-
.lks-ui .lks14-zonecaret{color:var(--ink-
|
|
449
|
+
.lks-ui .lks14-zonecaret{color:var(--ink-muted);font-size:11px;line-height:1}
|
|
449
450
|
/* C6: note actions + pinned + edit */
|
|
450
451
|
.lks-ui .lks-note-act{background:none;border:none;color:var(--ink-faint);cursor:pointer;padding:2px;display:inline-flex}
|
|
451
452
|
.lks-ui .lks-note-act:hover{color:var(--ink)}
|
|
@@ -690,14 +691,9 @@ window.__ModuleLoader__.load({
|
|
|
690
691
|
.lks-ui .lks14-palette-text{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
691
692
|
.lks-ui .lks14-palette-sub{font-size:10.5px;color:var(--ink-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
692
693
|
.lks-ui .lks14-palette-empty{padding:18px;text-align:center;color:var(--ink-faint);font-size:12px}
|
|
693
|
-
/* E5: the
|
|
694
|
+
/* E5: the lesson row carries the labeled thread chip (menu opens on click) */
|
|
694
695
|
.lks-ui .lks14-lessonrow{display:flex;align-items:center;gap:8px;min-width:0}
|
|
695
696
|
.lks-ui .lks14-lessonrow>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
696
|
-
.lks-ui .lks14-threadpills{display:flex;gap:4px;margin-left:auto;overflow-x:auto;max-width:55%;scrollbar-width:none}
|
|
697
|
-
.lks-ui .lks14-threadpill{flex:none;border:1px solid var(--border-faint);background:var(--surface-1);color:var(--ink-muted);font:inherit;font-size:10.5px;padding:2.5px 8px;border-radius:999px;cursor:pointer;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:all .15s}
|
|
698
|
-
.lks-ui .lks14-threadpill:hover{color:var(--ink);border-color:var(--border)}
|
|
699
|
-
.lks-ui .lks14-threadpill.on{background:color-mix(in srgb,var(--brand) 16%,transparent);border-color:rgb(var(--brand-rgb)/0.4);color:var(--brand)}
|
|
700
|
-
.lks-ui .lks14-threadpill:focus-visible{box-shadow:0 0 0 2px var(--business-primary);outline:none}
|
|
701
697
|
/* E7: the font-scale pair in the app header */
|
|
702
698
|
.lks-ui .lks-hdr-zoom{display:flex;align-items:center;gap:4px;margin-left:4px}
|
|
703
699
|
.lks-ui .lks-hdr-zoom-btn{border:1px solid var(--border-faint);background:var(--surface-1);color:var(--ink-muted);font:inherit;font-size:10px;font-weight:700;border-radius:6px;padding:2px 6px;cursor:pointer;transition:all .15s}
|
|
@@ -706,12 +702,30 @@ window.__ModuleLoader__.load({
|
|
|
706
702
|
.lks-ui .lks-hdr-zoom-btn:focus-visible{box-shadow:0 0 0 2px var(--business-primary);outline:none}
|
|
707
703
|
.lks-ui .lks-hdr-zoom-val{font-size:10px;color:var(--ink-faint);min-width:32px;text-align:center}
|
|
708
704
|
|
|
709
|
-
/* E1: the context
|
|
710
|
-
|
|
711
|
-
.lks-ui .lks14-
|
|
712
|
-
.lks-ui .lks14-
|
|
713
|
-
.lks-ui .lks14-
|
|
714
|
-
.lks-ui .lks14-
|
|
705
|
+
/* E1: the context ring — the host composer's occupancy meter (a 14px ring
|
|
706
|
+
beside send; hover reads, click opens the figures panel). */
|
|
707
|
+
.lks-ui .lks14-ctxring-wrap{position:relative;display:inline-flex;align-items:center}
|
|
708
|
+
.lks-ui .lks14-ctxring{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:none;border-radius:50%;background:transparent;cursor:pointer;color:var(--ink-faint);transition:color .15s}
|
|
709
|
+
.lks-ui .lks14-ctxring:hover{color:var(--ink-muted)}
|
|
710
|
+
.lks-ui .lks14-ctxring:focus-visible{outline:none;box-shadow:0 0 0 2px var(--business-primary)}
|
|
711
|
+
.lks-ui .lks14-ctxring .track{fill:none;stroke:rgb(var(--ink-rgb)/0.18);stroke-width:2}
|
|
712
|
+
.lks-ui .lks14-ctxring .fill{fill:none;stroke:var(--brand);stroke-width:2;stroke-linecap:round;transition:stroke-dasharray .3s var(--ease-out-expo)}
|
|
713
|
+
.lks-ui .lks14-ctxring .fill.hot{stroke:var(--warning)}
|
|
714
|
+
.lks-ui .lks14-ctxpanel{position:absolute;bottom:calc(100% + 8px);right:0;z-index:30;width:240px;padding:10px 12px;border-radius:10px;background:var(--surface-2);border:1px solid var(--border-faint);box-shadow:0 8px 24px rgb(0 0 0/0.25);font-size:11px;color:var(--ink);text-align:left}
|
|
715
|
+
.lks-ui .lks14-ctxpanel-head{display:flex;align-items:baseline;gap:8px;margin-bottom:7px}
|
|
716
|
+
.lks-ui .lks14-ctxpanel-pct{font-size:15px;font-weight:700;color:var(--ink-strong)}
|
|
717
|
+
.lks-ui .lks14-ctxpanel-figs{margin-left:auto;color:var(--ink-muted);font-variant-numeric:tabular-nums}
|
|
718
|
+
.lks-ui .lks14-ctxpanel-bar{display:flex;height:4px;border-radius:999px;background:rgb(var(--ink-rgb)/0.1);overflow:hidden}
|
|
719
|
+
.lks-ui .lks14-ctxpanel-bar i{display:block;height:100%;background:var(--brand)}
|
|
720
|
+
.lks-ui .lks14-ctxpanel-bar i.tools{background:color-mix(in srgb,var(--brand) 55%,var(--warning))}
|
|
721
|
+
.lks-ui .lks14-ctxpanel-bar i.msgs{background:var(--warning)}
|
|
722
|
+
.lks-ui .lks14-ctxpanel-rows{margin-top:7px;display:flex;flex-direction:column;gap:3px}
|
|
723
|
+
.lks-ui .lks14-ctxpanel-row{display:flex;align-items:center;gap:6px;color:var(--ink-muted)}
|
|
724
|
+
.lks-ui .lks14-ctxpanel-row b{margin-left:auto;font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}
|
|
725
|
+
.lks-ui .lks14-ctxpanel-row .swatch{flex:none;width:7px;height:7px;border-radius:2px;background:var(--brand)}
|
|
726
|
+
.lks-ui .lks14-ctxpanel-row .swatch.tools{background:color-mix(in srgb,var(--brand) 55%,var(--warning))}
|
|
727
|
+
.lks-ui .lks14-ctxpanel-row .swatch.msgs{background:var(--warning)}
|
|
728
|
+
.lks-ui .lks14-ctxpanel-note{margin-top:6px;font-size:10px;color:var(--ink-faint)}
|
|
715
729
|
/* E6: the model face chip + catalog popover */
|
|
716
730
|
.lks-ui .lks-modelface{position:relative;display:flex}
|
|
717
731
|
.lks-ui .lks-modelface-chip{display:flex;align-items:center;gap:4px;border:1px solid var(--border-faint);background:var(--surface-1);color:var(--ink-muted);font:inherit;font-size:10px;font-weight:700;border-radius:999px;padding:2.5px 8px;cursor:pointer;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:all .15s}
|
|
@@ -730,7 +744,6 @@ window.__ModuleLoader__.load({
|
|
|
730
744
|
.lks-ui .lks-btn-attach:hover:not(:disabled){color:var(--ink);background:var(--surface-2)}
|
|
731
745
|
.lks-ui .lks-btn-attach:disabled{opacity:.4;cursor:not-allowed}
|
|
732
746
|
.lks-ui .lks-btn-attach:focus-visible{box-shadow:0 0 0 2px var(--business-primary);outline:none}
|
|
733
|
-
.lks-ui .lks14-threadlabel{color:var(--ink-faint)}
|
|
734
747
|
/* ── the distilled two-row head (0.19): identity+pct+delete over a hairline
|
|
735
748
|
mastery bar, then search/review/worldswitch sharing one tools row ── */
|
|
736
749
|
.lks-ui .lks14-railcard-row.main{min-width:0;max-width:100%;overflow:hidden}
|
|
@@ -747,6 +760,22 @@ window.__ModuleLoader__.load({
|
|
|
747
760
|
.lks-ui .lks-audio-toggle{display:inline-flex;align-items:center;gap:4px}
|
|
748
761
|
.lks-ui .lks-audio-toggle .lks-audio-label{display:none}
|
|
749
762
|
.lks-ui .lks14-msgwrap:hover .lks-audio-toggle .lks-audio-label,.lks-ui .lks-audio-toggle:focus-visible .lks-audio-label{display:inline}
|
|
763
|
+
/* ── the thread switcher as one labeled chip + menu (0.19 owner note) ── */
|
|
764
|
+
.lks-ui .lks14-threadswitch{position:relative;margin-left:auto}
|
|
765
|
+
.lks-ui .lks14-threadchip{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border:none;border-radius:999px;background:rgb(var(--ink-rgb)/0.06);color:var(--ink-muted);font:inherit;font-size:11px;font-weight:700;cursor:pointer;transition:color .15s,background .15s;max-width:220px}
|
|
766
|
+
.lks-ui .lks14-threadchip:hover{background:rgb(var(--ink-rgb)/0.1);color:var(--ink)}
|
|
767
|
+
.lks-ui .lks14-threadchip-caret{font-size:9px}
|
|
768
|
+
.lks-ui .lks14-threadmenu{position:absolute;top:calc(100% + 6px);right:0;z-index:60;min-width:240px;max-width:300px;max-height:320px;overflow-y:auto;display:flex;flex-direction:column;gap:1px;padding:4px;border-radius:10px;background:var(--surface-0);border:1px solid var(--border);box-shadow:0 12px 32px -8px rgb(var(--shadow-rgb)/0.5)}
|
|
769
|
+
.lks-ui .lks14-threadmenu-row{display:flex;align-items:center;gap:7px;width:100%;padding:6px 8px;border:none;border-radius:7px;background:none;font:inherit;font-size:12px;color:var(--ink);cursor:pointer;text-align:left}
|
|
770
|
+
.lks-ui .lks14-threadmenu-row:hover{background:rgb(var(--ink-rgb)/0.06)}
|
|
771
|
+
.lks-ui .lks14-threadmenu-row.on{background:rgb(var(--brand-rgb)/0.1)}
|
|
772
|
+
.lks-ui .lks14-threadmenu-row.on .lks14-threadmenu-title{color:var(--brand);font-weight:700}
|
|
773
|
+
.lks-ui .lks14-threadmenu-dot{flex:none;width:6px;height:6px;border-radius:999px;background:var(--ink-faint)}
|
|
774
|
+
.lks-ui .lks14-threadmenu-row.on .lks14-threadmenu-dot{background:var(--brand)}
|
|
775
|
+
.lks-ui .lks14-threadmenu-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
776
|
+
.lks-ui .lks14-threadmenu-now{flex:none;font-size:10px;font-weight:800;color:var(--brand)}
|
|
777
|
+
|
|
778
|
+
|
|
750
779
|
/* ── light-mode chrome sweep: the rail-head glass family hardcoded dark for
|
|
751
780
|
the map era; the list rail follows the theme, so the chrome reads
|
|
752
781
|
ink-on-surface here too ── */
|
|
@@ -785,6 +814,8 @@ window.__ModuleLoader__.load({
|
|
|
785
814
|
.lks-ui[data-lks-theme='light'] .lks14-importprog-step.pending .lks14-importprog-text{color:var(--ink-faint)}
|
|
786
815
|
.lks-ui[data-lks-theme='light'] .lks14-importprog-step.pending .lks14-importprog-dots{border-color:var(--border)}
|
|
787
816
|
.lks-ui[data-lks-theme='light'] .lks14-importprog-elapsed{color:var(--ink-muted)}
|
|
817
|
+
.lks-ui[data-lks-theme='light'] .lks14-threadmenu{background:rgb(255 255 255/0.98);border-color:var(--border-faint);box-shadow:0 12px 32px -8px rgb(0 0 0/0.18)}
|
|
818
|
+
.lks-ui[data-lks-theme='light'] .lks14-ctxpanel{background:rgb(255 255 255/0.98);box-shadow:0 8px 24px rgb(0 0 0/0.12)}
|
|
788
819
|
/* ambient glyph motion sleeps under reduced-motion (A7 never had a guard) */
|
|
789
820
|
@media (prefers-reduced-motion: reduce){
|
|
790
821
|
.lks-ui .lks-lessorow.st-mastered .lks-lessorow-glyph svg,.lks-ui .lks-hdr-xp .lks-hdr-glyph,.lks-ui .lks-hdr-streak .lks-hdr-glyph{animation:none}
|
|
@@ -953,19 +984,6 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
953
984
|
|
|
954
985
|
|
|
955
986
|
|
|
956
|
-
/* the companion creature (P7): bottom-right of the panel; gentle bob idle,
|
|
957
|
-
bounce while talking, pop on celebration; reduced-motion = static (a11y). */
|
|
958
|
-
.lks-companion{position:absolute;right:18px;bottom:14px;z-index:20;width:56px;height:56px;cursor:pointer;filter:drop-shadow(0 4px 10px rgba(0,0,0,.18));animation:lks-comp-bob 3.2s ease-in-out infinite}
|
|
959
|
-
.lks-companion:focus-visible{outline:none;filter:drop-shadow(0 0 0 2px var(--dsw-alias-state-business-primary)) drop-shadow(0 4px 10px rgba(0,0,0,.18))}
|
|
960
|
-
.lks-companion.mood-talking{animation:lks-comp-talk .6s ease-in-out infinite}
|
|
961
|
-
.lks-companion.mood-celebrating{animation:lks-comp-pop .5s cubic-bezier(.34,1.56,.64,1) 3}
|
|
962
|
-
.lks-companion.mood-encouraging{animation:lks-comp-nudge 1.2s ease-in-out 2}
|
|
963
|
-
@keyframes lks-comp-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
|
|
964
|
-
@keyframes lks-comp-talk{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-7px) scale(1.06)}}
|
|
965
|
-
@keyframes lks-comp-pop{0%{transform:scale(1)}40%{transform:scale(1.25) rotate(-8deg)}100%{transform:scale(1)}}
|
|
966
|
-
@keyframes lks-comp-nudge{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px) rotate(-4deg)}75%{transform:translateX(5px) rotate(4deg)}}
|
|
967
|
-
@media (prefers-reduced-motion:reduce){.lks-companion{animation:none}}
|
|
968
|
-
|
|
969
987
|
/* P5: proposal banner (ConfirmCard semantics) + thinking row + narrow switch */
|
|
970
988
|
.lks-propbanner{flex:none;display:flex;align-items:center;gap:8px;margin:8px 12px 0;padding:8px 12px;border-radius:10px;background:var(--dsw-alias-state-business-tertiary);border:1px solid var(--dsw-alias-border-l2);font-size:12.5px;color:var(--dsw-alias-label-primary-bluish)}
|
|
971
989
|
.lks-propbanner .lks-propbanner-why{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
@@ -1017,7 +1035,6 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
1017
1035
|
|
|
1018
1036
|
/* rail search results panel (P4, upstream CourseSearchPanel) */
|
|
1019
1037
|
.lks14-searchpanel{margin:4px 0 6px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}
|
|
1020
|
-
.lks14-threadlabel{flex:none;padding:8px 20px 0;font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1021
1038
|
.lks14-searchrow{display:flex;flex-direction:column;align-items:flex-start;gap:1px;width:100%;text-align:left;font:inherit;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
1022
1039
|
.lks14-searchrow:last-child{border-bottom:none}
|
|
1023
1040
|
.lks14-searchrow:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -2046,7 +2063,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2046
2063
|
"note.src.learner": "我的笔记",
|
|
2047
2064
|
"error.send": "消息没有送达——请稍后再试一次。",
|
|
2048
2065
|
"error.action": "操作没有完成——请稍后再试。",
|
|
2049
|
-
"thread.label": "「{title}」的对话",
|
|
2050
2066
|
"thread.streamingBadge": "AI 正在输出",
|
|
2051
2067
|
"map.search.label": "搜索",
|
|
2052
2068
|
"map.review.label": "复习",
|
|
@@ -2063,15 +2079,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2063
2079
|
"quiz.submit": "提交",
|
|
2064
2080
|
"proposal.applied": "《{lesson}》已采纳提前毕业提议",
|
|
2065
2081
|
"proposal.rejected": "《{lesson}》提议已忽略,继续保持练习",
|
|
2066
|
-
"companion.poke": "戳一戳伴学伙伴",
|
|
2067
|
-
"companion.poked": "伴学伙伴向你眨了眨眼",
|
|
2068
|
-
"companion.form.ember": "小焰",
|
|
2069
|
-
"companion.form.frost": "霜绒",
|
|
2070
|
-
"companion.form.moss": "苔芽",
|
|
2071
|
-
"companion.form.star": "星尘",
|
|
2072
|
-
"companion.form.ink": "墨墨",
|
|
2073
|
-
"settings.companion": "伴学伙伴",
|
|
2074
|
-
"settings.companion.hint": "选择学习面板右下角的伴学生物形态。",
|
|
2075
2082
|
"quiz.msg.explain-wrong": "我刚才有题答错了,帮我讲讲为什么错、正确的思路是什么。",
|
|
2076
2083
|
"quiz.msg.retry": "再来一组类似的题巩固一下。",
|
|
2077
2084
|
"quiz.msg.go-deeper": "这组我答得不错,帮我深入讲讲背后的原理和容易混淆的地方。",
|
|
@@ -2130,9 +2137,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2130
2137
|
"palette.study": "开始学习",
|
|
2131
2138
|
"palette.kind.action": "操作",
|
|
2132
2139
|
"palette.empty": "没有匹配项",
|
|
2140
|
+
"threads.open": "{n} 条对话",
|
|
2141
|
+
"threads.now": "当前",
|
|
2133
2142
|
"threads.label": "课时线程",
|
|
2134
2143
|
"meter.label": "上下文用量",
|
|
2135
|
-
"meter.
|
|
2144
|
+
"meter.aria": "上下文已用 {pct}%",
|
|
2145
|
+
"meter.brk.system": "系统提示",
|
|
2146
|
+
"meter.brk.tools": "工具",
|
|
2147
|
+
"meter.brk.messages": "对话",
|
|
2148
|
+
"meter.estimate.note": "按事件字符估算",
|
|
2136
2149
|
"model.unknown": "当前模型",
|
|
2137
2150
|
"model.switched": "已切换到 {model}",
|
|
2138
2151
|
"model.switchFailed": "切换失败——请看宿主模型设置",
|
|
@@ -2198,7 +2211,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2198
2211
|
"quiz.answer": "选 {letter}:{text}",
|
|
2199
2212
|
"tutor.dormant": "学习模式已关闭 — 在下方对话区点「开始学习」即可唤醒导师。",
|
|
2200
2213
|
"tutor.empty": "对话会出现在这里",
|
|
2201
|
-
"tutor.empty.hint": "
|
|
2214
|
+
"tutor.empty.hint": "和导师聊聊这一课",
|
|
2202
2215
|
"proposal.text": "导师提议你已掌握「{lesson}」:{rationale}",
|
|
2203
2216
|
"proposal.accept": "接受",
|
|
2204
2217
|
"proposal.decline": "再练练",
|
|
@@ -2393,7 +2406,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2393
2406
|
"note.src.learner": "My note",
|
|
2394
2407
|
"error.send": "The message did not go through — please try again.",
|
|
2395
2408
|
"error.action": "The action did not complete — please try again.",
|
|
2396
|
-
"thread.label": "Conversation · {title}",
|
|
2397
2409
|
"thread.streamingBadge": "AI is responding",
|
|
2398
2410
|
"map.search.label": "Search",
|
|
2399
2411
|
"map.review.label": "Review",
|
|
@@ -2410,15 +2422,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2410
2422
|
"quiz.submit": "Submit",
|
|
2411
2423
|
"proposal.applied": "Graduation proposal for \"{lesson}\" accepted",
|
|
2412
2424
|
"proposal.rejected": "Proposal for \"{lesson}\" declined — keep practicing",
|
|
2413
|
-
"companion.poke": "Poke the companion",
|
|
2414
|
-
"companion.poked": "The companion winks at you",
|
|
2415
|
-
"companion.form.ember": "Ember",
|
|
2416
|
-
"companion.form.frost": "Frost",
|
|
2417
|
-
"companion.form.moss": "Moss",
|
|
2418
|
-
"companion.form.star": "Stardust",
|
|
2419
|
-
"companion.form.ink": "Ink",
|
|
2420
|
-
"settings.companion": "Companion",
|
|
2421
|
-
"settings.companion.hint": "Choose the companion creature in the study panel corner.",
|
|
2422
2425
|
"quiz.msg.explain-wrong": "I got some questions wrong — walk me through why and the right line of thinking.",
|
|
2423
2426
|
"quiz.msg.retry": "Give me another similar set to consolidate.",
|
|
2424
2427
|
"quiz.msg.go-deeper": "I did well on this set — go deeper into the principles and common confusions.",
|
|
@@ -2478,8 +2481,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2478
2481
|
"palette.kind.action": "Action",
|
|
2479
2482
|
"palette.empty": "No matches",
|
|
2480
2483
|
"threads.label": "Lesson threads",
|
|
2484
|
+
"threads.open": "{n} threads",
|
|
2485
|
+
"threads.now": "current",
|
|
2481
2486
|
"meter.label": "Context usage",
|
|
2482
|
-
"meter.
|
|
2487
|
+
"meter.aria": "{pct}% of context used",
|
|
2488
|
+
"meter.brk.system": "System prompt",
|
|
2489
|
+
"meter.brk.tools": "Tools",
|
|
2490
|
+
"meter.brk.messages": "Messages",
|
|
2491
|
+
"meter.estimate.note": "Char-volume estimate",
|
|
2483
2492
|
"model.unknown": "Current model",
|
|
2484
2493
|
"model.switched": "Switched to {model}",
|
|
2485
2494
|
"model.switchFailed": "Switch failed — check the host model settings",
|
|
@@ -3168,20 +3177,25 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3168
3177
|
let node;
|
|
3169
3178
|
while ((node = walker.nextNode()) !== null) if (findMathSpan(node.data) !== null && node.parentElement?.tagName !== "CODE") targets.push(node);
|
|
3170
3179
|
for (const t of targets) {
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3180
|
+
let guard = 0;
|
|
3181
|
+
while (guard++ < 200) {
|
|
3182
|
+
const span = findMathSpan(t.data);
|
|
3183
|
+
if (span === null) break;
|
|
3184
|
+
try {
|
|
3185
|
+
const html = katex.renderToString(span.tex, {
|
|
3186
|
+
displayMode: span.display,
|
|
3187
|
+
throwOnError: false
|
|
3188
|
+
});
|
|
3189
|
+
const holder = document.createElement("span");
|
|
3190
|
+
holder.innerHTML = html;
|
|
3191
|
+
if (span.before !== "") t.before(document.createTextNode(span.before));
|
|
3192
|
+
t.before(holder);
|
|
3193
|
+
t.data = span.after;
|
|
3194
|
+
replaced++;
|
|
3195
|
+
} catch {
|
|
3196
|
+
break;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3185
3199
|
}
|
|
3186
3200
|
return replaced;
|
|
3187
3201
|
}
|
|
@@ -3191,6 +3205,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3191
3205
|
*/
|
|
3192
3206
|
function attachCodeHeader(wrap, lang) {
|
|
3193
3207
|
if (wrap.parentElement?.querySelector(".lks-codehead") !== null) return;
|
|
3208
|
+
wrap.closest(".lks-codeblock")?.querySelector(":scope > .lks-codeblock-head")?.remove();
|
|
3194
3209
|
const head = document.createElement("div");
|
|
3195
3210
|
head.className = "lks-codehead";
|
|
3196
3211
|
const label = document.createElement("span");
|
|
@@ -3291,6 +3306,242 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3291
3306
|
} catch {}
|
|
3292
3307
|
}
|
|
3293
3308
|
//#endregion
|
|
3309
|
+
//#region src/vendor/math-normalize.ts
|
|
3310
|
+
/**
|
|
3311
|
+
* math-normalize —— 数学记法归一(v0.19 数学渲染前置,纯函数)。
|
|
3312
|
+
*
|
|
3313
|
+
* `\(...\)` / `\[...\]` 是 LaTeX 的原生行内/行间记法,remark-math 只认 `$`/
|
|
3314
|
+
* `$$`。导入的内容(AI 讲解/转写稿/粘贴文本)两种记法都可能出现,渲染前统一
|
|
3315
|
+
* 归一到 `$` 记法。幂等;**围栏代码块内不动**(代码里的 `\(` 是字面量)。
|
|
3316
|
+
*/
|
|
3317
|
+
function normalizeMathNotation(md) {
|
|
3318
|
+
const out = [];
|
|
3319
|
+
let inFence = false;
|
|
3320
|
+
for (const line of md.split("\n")) {
|
|
3321
|
+
if (/^[ \t]*(?:```|~~~)/.test(line)) {
|
|
3322
|
+
out.push(line);
|
|
3323
|
+
inFence = !inFence;
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
out.push(inFence ? line : line.replace(/\\\[|\\\]|\\\(|\\\)/g, (tok) => tok === "\\[" || tok === "\\]" ? "$$" : "$"));
|
|
3327
|
+
}
|
|
3328
|
+
return out.join("\n");
|
|
3329
|
+
}
|
|
3330
|
+
//#endregion
|
|
3331
|
+
//#region src/client/views.tsx
|
|
3332
|
+
/**
|
|
3333
|
+
* Shared pure helpers for the study panel's pieces: chat-row shapes, tree
|
|
3334
|
+
* status copy, quiz-option extraction, and section default expansion. The
|
|
3335
|
+
* panel itself lives in `panel.tsx`; since 0.14.0 nothing here touches the
|
|
3336
|
+
* host composer or the conversation view.
|
|
3337
|
+
* @module dsh-plugin-lookatstudy/client/views
|
|
3338
|
+
*/
|
|
3339
|
+
/** Small inline error surface for failed write actions (shared with settings). */
|
|
3340
|
+
function ActionError({ error }) {
|
|
3341
|
+
if (error === null) return null;
|
|
3342
|
+
return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Tooltip text for one course-tree status glyph. Pure (the translator is an
|
|
3346
|
+
* optional parameter defaulting to the active one, evaluated per call).
|
|
3347
|
+
*/
|
|
3348
|
+
function statusTitle(kind, status, t = tr) {
|
|
3349
|
+
if (kind === "exam") return t("status.exam");
|
|
3350
|
+
switch (status) {
|
|
3351
|
+
case "mastered": return t("status.mastered");
|
|
3352
|
+
case "in_progress": return t("status.in_progress");
|
|
3353
|
+
case "available": return t("status.available");
|
|
3354
|
+
default: return t("status.locked");
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
/**
|
|
3358
|
+
* Extract the last quiz option block (2–4 consecutive `A. …` `B. …` lines,
|
|
3359
|
+
* letters strictly consecutive from A) from an assistant reply — the raw text
|
|
3360
|
+
* of the tutor's question becomes the clickable answer surface. Pure.
|
|
3361
|
+
*/
|
|
3362
|
+
function quizOptions(text) {
|
|
3363
|
+
const runs = [];
|
|
3364
|
+
let run = [];
|
|
3365
|
+
const flush = () => {
|
|
3366
|
+
if (run.length >= 2) runs.push(run);
|
|
3367
|
+
run = [];
|
|
3368
|
+
};
|
|
3369
|
+
for (const line of text.split("\n")) {
|
|
3370
|
+
const plain = /^([A-D])[.、:)]\s*(.+)$/.exec(line.trim());
|
|
3371
|
+
const table = /^\|\s*\*{0,2}([A-D])\*{0,2}\s*\|\s*(.+?)\s*\|$/.exec(line.trim());
|
|
3372
|
+
const match = plain ?? (table !== null ? [
|
|
3373
|
+
table[0],
|
|
3374
|
+
table[1],
|
|
3375
|
+
table[2].replaceAll("**", "").replaceAll("`", "")
|
|
3376
|
+
] : null);
|
|
3377
|
+
if (match === null) {
|
|
3378
|
+
flush();
|
|
3379
|
+
continue;
|
|
3380
|
+
}
|
|
3381
|
+
const nextLetter = run.length === 0 ? "A" : String.fromCharCode(run[run.length - 1].letter.charCodeAt(0) + 1);
|
|
3382
|
+
if (match[1] === nextLetter) run.push({
|
|
3383
|
+
letter: match[1],
|
|
3384
|
+
text: match[2]
|
|
3385
|
+
});
|
|
3386
|
+
else {
|
|
3387
|
+
flush();
|
|
3388
|
+
if (match[1] === "A") run.push({
|
|
3389
|
+
letter: match[1],
|
|
3390
|
+
text: match[2]
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
flush();
|
|
3395
|
+
return runs.at(-1) ?? [];
|
|
3396
|
+
}
|
|
3397
|
+
/**
|
|
3398
|
+
* Merge the rail's two search tracks into the results panel: title hits
|
|
3399
|
+
* (lessons whose name matches — they are also filtered into the tree) first,
|
|
3400
|
+
* then full-text hits not already covered, snippets carried for the body
|
|
3401
|
+
* matches. Pure; empty queries yield no panel.
|
|
3402
|
+
*/
|
|
3403
|
+
function mergeRailSearch(query, lessons, textHits) {
|
|
3404
|
+
const keys = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
3405
|
+
if (keys.length === 0) return [];
|
|
3406
|
+
const rows = [];
|
|
3407
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3408
|
+
const matches = (title) => keys.every((k) => title.toLowerCase().includes(k));
|
|
3409
|
+
for (const lesson of lessons) {
|
|
3410
|
+
if (lesson.kind === "exam" || lesson.status === "locked" || !matches(lesson.title)) continue;
|
|
3411
|
+
rows.push({
|
|
3412
|
+
lessonId: lesson.id,
|
|
3413
|
+
title: lesson.title,
|
|
3414
|
+
snippet: "",
|
|
3415
|
+
courseTitle: "",
|
|
3416
|
+
fromTitle: true
|
|
3417
|
+
});
|
|
3418
|
+
seen.add(lesson.id);
|
|
3419
|
+
}
|
|
3420
|
+
for (const hit of textHits) {
|
|
3421
|
+
if (seen.has(hit.lessonId)) continue;
|
|
3422
|
+
rows.push({
|
|
3423
|
+
lessonId: hit.lessonId,
|
|
3424
|
+
title: hit.lessonTitle,
|
|
3425
|
+
snippet: hit.snippet,
|
|
3426
|
+
courseTitle: hit.courseTitle,
|
|
3427
|
+
fromTitle: false
|
|
3428
|
+
});
|
|
3429
|
+
seen.add(hit.lessonId);
|
|
3430
|
+
}
|
|
3431
|
+
return rows.slice(0, 12);
|
|
3432
|
+
}
|
|
3433
|
+
/**
|
|
3434
|
+
* A section's effective openness: the user's explicit toggle (persisted per
|
|
3435
|
+
* course) overrides the frontier-based default. Pure.
|
|
3436
|
+
*/
|
|
3437
|
+
function effectiveOpen(sectionTitle, defaultOpen, overrides) {
|
|
3438
|
+
return overrides[sectionTitle] ?? defaultOpen;
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* The narrow-mode pane picker: only valid ids, remembering the last choice
|
|
3442
|
+
* (upstream's narrow switcher semantics). Pure.
|
|
3443
|
+
*/
|
|
3444
|
+
function pickNarrowPane(stored) {
|
|
3445
|
+
return stored === "rail" || stored === "chat" || stored === "note" ? stored : "chat";
|
|
3446
|
+
}
|
|
3447
|
+
/**
|
|
3448
|
+
* Default expansion for one rail section: collapsed when every study lesson is
|
|
3449
|
+
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
3450
|
+
* stays open; exam nodes never force a section open. Pure.
|
|
3451
|
+
*/
|
|
3452
|
+
function sectionDefaultOpen(section) {
|
|
3453
|
+
return section.lessons.some((l) => l.focus || l.kind !== "exam" && l.status !== "mastered" && l.status !== "locked");
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* C1 sticky-follow: the stream follows new rows only while the reader is
|
|
3457
|
+
* within `tolerance` of the bottom (upstream's 80px rule — scroll up and the
|
|
3458
|
+
* feed stops chasing you). Pure.
|
|
3459
|
+
*/
|
|
3460
|
+
function isStuck(scrollTop, scrollHeight, clientHeight, tolerance = 80) {
|
|
3461
|
+
return scrollHeight - scrollTop - clientHeight <= tolerance;
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* C12 touch pane swipe: a horizontal flick (>50px, dominance >2x) switches the
|
|
3465
|
+
* narrow pane neighbor; anything else is not a pane gesture. Pure
|
|
3466
|
+
* (upstream swipeTarget semantics).
|
|
3467
|
+
*/
|
|
3468
|
+
function swipePane(pane, dx, dy) {
|
|
3469
|
+
if (Math.abs(dx) < 50 || Math.abs(dx) <= 2 * Math.abs(dy)) return null;
|
|
3470
|
+
const order = [
|
|
3471
|
+
"rail",
|
|
3472
|
+
"chat",
|
|
3473
|
+
"note"
|
|
3474
|
+
];
|
|
3475
|
+
const idx = order.indexOf(pane);
|
|
3476
|
+
if (idx < 0) return null;
|
|
3477
|
+
const next = dx < 0 ? idx + 1 : idx - 1;
|
|
3478
|
+
return next >= 0 && next < order.length ? order[next] : null;
|
|
3479
|
+
}
|
|
3480
|
+
/**
|
|
3481
|
+
* C12 selection-popover settle: coarse pointers need a longer quiet window
|
|
3482
|
+
* (upstream's 600/250 two-tier). Pure.
|
|
3483
|
+
*/
|
|
3484
|
+
function settleMs(coarse) {
|
|
3485
|
+
return coarse ? 600 : 250;
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* C9 interleaved review: pick one due lesson at random (Math.random; empty in,
|
|
3489
|
+
* null out). Pure.
|
|
3490
|
+
*/
|
|
3491
|
+
function pickRandomDue(items) {
|
|
3492
|
+
if (items.length === 0) return null;
|
|
3493
|
+
return items[Math.floor(Math.random() * items.length)] ?? null;
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Display-side section-title humanizer (0.19 critique round): import-design
|
|
3497
|
+
* section ids arrive as repo-folder slugs ("0-course-setup", "1-Intro") —
|
|
3498
|
+
* strip the leading episode number, hyphen/underscore runs become spaces.
|
|
3499
|
+
* Keys, anchors, and ids stay raw; this is presentation only. Never returns
|
|
3500
|
+
* an empty string (a title that humanizes to nothing keeps its raw form).
|
|
3501
|
+
*/
|
|
3502
|
+
function humanizeSectionTitle(raw) {
|
|
3503
|
+
const t = raw.trim().replace(/^\d+[.\-_ ]+/, "").replace(/[\-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
3504
|
+
return t === "" ? raw.trim() : t;
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Proportion the panel's bar through the host's heuristic composition, in the
|
|
3508
|
+
* segment order system → tools → messages; without a breakdown a single
|
|
3509
|
+
* brand segment carries the whole reading. A zero-width part is dropped.
|
|
3510
|
+
*/
|
|
3511
|
+
function ctxSegments(pct, breakdown) {
|
|
3512
|
+
if (pct <= 0) return [];
|
|
3513
|
+
const total = breakdown === null ? 0 : breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
|
|
3514
|
+
if (total <= 0) return [{
|
|
3515
|
+
key: "total",
|
|
3516
|
+
cls: "",
|
|
3517
|
+
width: pct
|
|
3518
|
+
}];
|
|
3519
|
+
return [
|
|
3520
|
+
["sys", breakdown.systemTokens],
|
|
3521
|
+
["tools", breakdown.toolsTokens],
|
|
3522
|
+
["msgs", breakdown.messageTokens]
|
|
3523
|
+
].map(([key, tokens]) => ({
|
|
3524
|
+
key,
|
|
3525
|
+
cls: key,
|
|
3526
|
+
width: pct * tokens / total
|
|
3527
|
+
})).filter((s) => s.width > 0);
|
|
3528
|
+
}
|
|
3529
|
+
/** Compact token figure for the context panel (host formatTokens arrangement). */
|
|
3530
|
+
function fmtTokens(value) {
|
|
3531
|
+
if (value < 1e3) return String(value);
|
|
3532
|
+
if (value < 1e6) return `${String(Math.round(value / 100) / 10)}K`;
|
|
3533
|
+
return `${String(Math.round(value / 1e5) / 10)}M`;
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3536
|
+
* Panel-facing error text (0.19 critique round): raw err.message (host RPC
|
|
3537
|
+
* internals like "prompt rejected: …") never renders in the DOM — the user
|
|
3538
|
+
* gets one honest locale line, the raw error goes to the console.
|
|
3539
|
+
*/
|
|
3540
|
+
function friendlyError(err, kind = "action", t = tr) {
|
|
3541
|
+
console.error("[lookatstudy]", err);
|
|
3542
|
+
return t(kind === "send" ? "error.send" : "error.action");
|
|
3543
|
+
}
|
|
3544
|
+
//#endregion
|
|
3294
3545
|
//#region src/vendor/cmap-elk-layout.ts
|
|
3295
3546
|
/** 字符宽度估算:CJK 全宽,ASCII 约 0.58em,其余(BBOP…)同 ASCII。 */
|
|
3296
3547
|
function estTextWidth(text, fontSize = 13) {
|
|
@@ -5562,225 +5813,40 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5562
5813
|
}
|
|
5563
5814
|
}
|
|
5564
5815
|
//#endregion
|
|
5565
|
-
//#region src/client/
|
|
5816
|
+
//#region src/client/highlights.ts
|
|
5566
5817
|
/**
|
|
5567
|
-
*
|
|
5568
|
-
*
|
|
5569
|
-
*
|
|
5570
|
-
*
|
|
5571
|
-
* Upstream's Electron-native life (mic envelopes, typing squeeze, drag-throw)
|
|
5572
|
-
* has no web-panel equivalent and stays unported by design.
|
|
5573
|
-
* @module dsh-plugin-lookatstudy/client/companion
|
|
5818
|
+
* Locate a selection inside the model text: exact `selected` text search,
|
|
5819
|
+
* disambiguated by a fuzzy `surrounding` fingerprint (the same sentence
|
|
5820
|
+
* appearing twice resolves to the occurrence whose context matches).
|
|
5821
|
+
* @returns the global [start, end) in model.text, or null when absent.
|
|
5574
5822
|
*/
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
body: "#7dd3fc",
|
|
5586
|
-
accent: "#0284c7"
|
|
5587
|
-
},
|
|
5588
|
-
{
|
|
5589
|
-
id: "moss",
|
|
5590
|
-
labelKey: "companion.form.moss",
|
|
5591
|
-
body: "#86efac",
|
|
5592
|
-
accent: "#16a34a"
|
|
5593
|
-
},
|
|
5594
|
-
{
|
|
5595
|
-
id: "star",
|
|
5596
|
-
labelKey: "companion.form.star",
|
|
5597
|
-
body: "#c4b5fd",
|
|
5598
|
-
accent: "#7c3aed"
|
|
5599
|
-
},
|
|
5600
|
-
{
|
|
5601
|
-
id: "ink",
|
|
5602
|
-
labelKey: "companion.form.ink",
|
|
5603
|
-
body: "#cbd5e1",
|
|
5604
|
-
accent: "#334155"
|
|
5823
|
+
function locateInModel(model, selected, surrounding) {
|
|
5824
|
+
const needle = selected.trim();
|
|
5825
|
+
if (needle.length < 2) return null;
|
|
5826
|
+
const hits = [];
|
|
5827
|
+
let from = 0;
|
|
5828
|
+
for (;;) {
|
|
5829
|
+
const hit = model.text.indexOf(needle, from);
|
|
5830
|
+
if (hit === -1) break;
|
|
5831
|
+
hits.push(hit);
|
|
5832
|
+
from = hit + 1;
|
|
5605
5833
|
}
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
}
|
|
5623
|
-
/**
|
|
5624
|
-
* The mood machine — pure. `celebrate` and `encourage` are momentary (the
|
|
5625
|
-
* view decays them back through `decay`); `talking` latches while read-aloud
|
|
5626
|
-
* runs; poke is a self-echoing celebration.
|
|
5627
|
-
*/
|
|
5628
|
-
function nextCompanionMood(current, event) {
|
|
5629
|
-
switch (event) {
|
|
5630
|
-
case "talk-start": return "talking";
|
|
5631
|
-
case "talk-end": return current === "talking" ? "idle" : current;
|
|
5632
|
-
case "celebrate":
|
|
5633
|
-
case "poke": return "celebrating";
|
|
5634
|
-
case "encourage": return "encouraging";
|
|
5635
|
-
case "decay": return current === "celebrating" || current === "encouraging" ? "idle" : current;
|
|
5636
|
-
default: return current;
|
|
5637
|
-
}
|
|
5638
|
-
}
|
|
5639
|
-
/** One creature face — the shared skin every form wears. */
|
|
5640
|
-
function CompanionFace({ form, mood }) {
|
|
5641
|
-
const skin = COMPANION_FORMS.find((f) => f.id === form) ?? COMPANION_FORMS[0];
|
|
5642
|
-
const eye = mood === "celebrating" ? "M -3.4 0 q 1.7 -2.6 3.4 0" : "M -3.4 -1.4 a 1.4 1.4 0 1 0 0.01 0";
|
|
5643
|
-
const mouth = mood === "celebrating" ? "M 20 21 q 4 4 8 0" : mood === "encouraging" ? "M 21 22 q 3 -2.5 6 0" : "M 21 21 q 3 2.4 6 0";
|
|
5644
|
-
return (0, react.createElement)("svg", {
|
|
5645
|
-
width: 64,
|
|
5646
|
-
height: 64,
|
|
5647
|
-
viewBox: "0 0 64 64",
|
|
5648
|
-
"aria-hidden": "true"
|
|
5649
|
-
}, (0, react.createElement)("g", null, (0, react.createElement)("circle", {
|
|
5650
|
-
cx: 32,
|
|
5651
|
-
cy: 34,
|
|
5652
|
-
r: 20,
|
|
5653
|
-
fill: skin.body,
|
|
5654
|
-
stroke: skin.accent,
|
|
5655
|
-
strokeWidth: 2
|
|
5656
|
-
}), form === "ember" ? (0, react.createElement)("path", {
|
|
5657
|
-
d: "M32 4 q6 8 3 12 q8 -2 9 6 q4 -1 4 4 l-2 4 H18 l-2 -4 q0 -5 4 -4 q1 -8 9 -6 q-3 -4 3 -12z",
|
|
5658
|
-
fill: skin.accent,
|
|
5659
|
-
opacity: .85
|
|
5660
|
-
}) : form === "star" ? (0, react.createElement)("path", {
|
|
5661
|
-
d: "M32 2 l4 8 9 1 -6.5 6 1.5 9 -8.5 -4.5 -8 4.5 1.5 -9 -6.5 -6 9 -1z",
|
|
5662
|
-
fill: skin.accent,
|
|
5663
|
-
opacity: .85
|
|
5664
|
-
}) : form === "moss" ? (0, react.createElement)("path", {
|
|
5665
|
-
d: "M32 2 q-2 6 2 10 q-8 0 -8 6 H40 q0 -6 -8 -6 q4 -4 0 -10z",
|
|
5666
|
-
fill: skin.accent,
|
|
5667
|
-
opacity: .85
|
|
5668
|
-
}) : form === "ink" ? (0, react.createElement)("path", {
|
|
5669
|
-
d: "M32 2 q8 12 8 18 a8 8 0 1 1 -16 0 q0 -6 8 -18z",
|
|
5670
|
-
fill: skin.accent,
|
|
5671
|
-
opacity: .55
|
|
5672
|
-
}) : (0, react.createElement)("path", {
|
|
5673
|
-
d: "M18 12 l3 5 5 -1 -1 5 5 3 -5 3 1 5 -5 -1 -3 5 -3 -5 -5 1 1 -5 -5 -3 5 -3 -1 -5 5 1z",
|
|
5674
|
-
fill: skin.accent,
|
|
5675
|
-
opacity: .7
|
|
5676
|
-
}), (0, react.createElement)("path", {
|
|
5677
|
-
d: eye,
|
|
5678
|
-
transform: "translate(25 30)",
|
|
5679
|
-
stroke: skin.accent,
|
|
5680
|
-
strokeWidth: 1.6,
|
|
5681
|
-
fill: mood === "celebrating" ? "none" : skin.accent,
|
|
5682
|
-
strokeLinecap: "round"
|
|
5683
|
-
}), (0, react.createElement)("path", {
|
|
5684
|
-
d: eye,
|
|
5685
|
-
transform: "translate(14 30)",
|
|
5686
|
-
stroke: skin.accent,
|
|
5687
|
-
strokeWidth: 1.6,
|
|
5688
|
-
fill: mood === "celebrating" ? "none" : skin.accent,
|
|
5689
|
-
strokeLinecap: "round"
|
|
5690
|
-
}), (0, react.createElement)("path", {
|
|
5691
|
-
d: mouth,
|
|
5692
|
-
stroke: skin.accent,
|
|
5693
|
-
strokeWidth: 1.8,
|
|
5694
|
-
fill: "none",
|
|
5695
|
-
strokeLinecap: "round"
|
|
5696
|
-
})));
|
|
5697
|
-
}
|
|
5698
|
-
/** The creature: fixed bottom-right inside the panel, mood-animated, pokeable. */
|
|
5699
|
-
function Companion({ mood, onPoke }) {
|
|
5700
|
-
const form = storedCompanionForm();
|
|
5701
|
-
return (0, react.createElement)("div", {
|
|
5702
|
-
className: `lks-companion mood-${mood}`,
|
|
5703
|
-
"data-lks-companion": form,
|
|
5704
|
-
title: tr("companion.poke"),
|
|
5705
|
-
role: "button",
|
|
5706
|
-
tabIndex: 0,
|
|
5707
|
-
"aria-label": tr("companion.poke"),
|
|
5708
|
-
onClick: () => {
|
|
5709
|
-
onPoke?.();
|
|
5710
|
-
},
|
|
5711
|
-
onKeyDown: (e) => {
|
|
5712
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
5713
|
-
e.preventDefault();
|
|
5714
|
-
onPoke?.();
|
|
5715
|
-
}
|
|
5716
|
-
}
|
|
5717
|
-
}, (0, react.createElement)(CompanionFace, {
|
|
5718
|
-
form,
|
|
5719
|
-
mood
|
|
5720
|
-
}));
|
|
5721
|
-
}
|
|
5722
|
-
/** The hook driving one creature instance: event → mood, momentary decay. */
|
|
5723
|
-
function useCompanionMood() {
|
|
5724
|
-
const [mood, setMood] = (0, react.useState)("idle");
|
|
5725
|
-
const moodRef = (0, react.useRef)(mood);
|
|
5726
|
-
moodRef.current = mood;
|
|
5727
|
-
const emit = (event) => {
|
|
5728
|
-
const next = nextCompanionMood(moodRef.current, event);
|
|
5729
|
-
if (next !== moodRef.current) {
|
|
5730
|
-
moodRef.current = next;
|
|
5731
|
-
setMood(next);
|
|
5732
|
-
}
|
|
5733
|
-
};
|
|
5734
|
-
(0, react.useEffect)(() => {
|
|
5735
|
-
if (mood !== "celebrating" && mood !== "encouraging") return;
|
|
5736
|
-
const timer = setTimeout(() => {
|
|
5737
|
-
moodRef.current = nextCompanionMood(moodRef.current, "decay");
|
|
5738
|
-
setMood(moodRef.current);
|
|
5739
|
-
}, 2400);
|
|
5740
|
-
return () => {
|
|
5741
|
-
clearTimeout(timer);
|
|
5742
|
-
};
|
|
5743
|
-
}, [mood]);
|
|
5744
|
-
return {
|
|
5745
|
-
mood,
|
|
5746
|
-
emit
|
|
5747
|
-
};
|
|
5748
|
-
}
|
|
5749
|
-
//#endregion
|
|
5750
|
-
//#region src/client/highlights.ts
|
|
5751
|
-
/**
|
|
5752
|
-
* Locate a selection inside the model text: exact `selected` text search,
|
|
5753
|
-
* disambiguated by a fuzzy `surrounding` fingerprint (the same sentence
|
|
5754
|
-
* appearing twice resolves to the occurrence whose context matches).
|
|
5755
|
-
* @returns the global [start, end) in model.text, or null when absent.
|
|
5756
|
-
*/
|
|
5757
|
-
function locateInModel(model, selected, surrounding) {
|
|
5758
|
-
const needle = selected.trim();
|
|
5759
|
-
if (needle.length < 2) return null;
|
|
5760
|
-
const hits = [];
|
|
5761
|
-
let from = 0;
|
|
5762
|
-
for (;;) {
|
|
5763
|
-
const hit = model.text.indexOf(needle, from);
|
|
5764
|
-
if (hit === -1) break;
|
|
5765
|
-
hits.push(hit);
|
|
5766
|
-
from = hit + 1;
|
|
5767
|
-
}
|
|
5768
|
-
if (hits.length === 0) return null;
|
|
5769
|
-
if (hits.length === 1 || surrounding === void 0 || surrounding.trim() === "") return {
|
|
5770
|
-
start: hits[0],
|
|
5771
|
-
end: hits[0] + needle.length
|
|
5772
|
-
};
|
|
5773
|
-
const finger = surrounding.replace(/\s+/g, "");
|
|
5774
|
-
let best = hits[0];
|
|
5775
|
-
let bestScore = -1;
|
|
5776
|
-
for (const hit of hits) {
|
|
5777
|
-
const context = model.text.slice(Math.max(0, hit - 30), hit + needle.length + 30).replace(/\s+/g, "");
|
|
5778
|
-
let score = 0;
|
|
5779
|
-
for (let i = 0; i < finger.length; i += 4) if (context.includes(finger.slice(i, i + 4))) score += 1;
|
|
5780
|
-
if (score > bestScore) {
|
|
5781
|
-
bestScore = score;
|
|
5782
|
-
best = hit;
|
|
5783
|
-
}
|
|
5834
|
+
if (hits.length === 0) return null;
|
|
5835
|
+
if (hits.length === 1 || surrounding === void 0 || surrounding.trim() === "") return {
|
|
5836
|
+
start: hits[0],
|
|
5837
|
+
end: hits[0] + needle.length
|
|
5838
|
+
};
|
|
5839
|
+
const finger = surrounding.replace(/\s+/g, "");
|
|
5840
|
+
let best = hits[0];
|
|
5841
|
+
let bestScore = -1;
|
|
5842
|
+
for (const hit of hits) {
|
|
5843
|
+
const context = model.text.slice(Math.max(0, hit - 30), hit + needle.length + 30).replace(/\s+/g, "");
|
|
5844
|
+
let score = 0;
|
|
5845
|
+
for (let i = 0; i < finger.length; i += 4) if (context.includes(finger.slice(i, i + 4))) score += 1;
|
|
5846
|
+
if (score > bestScore) {
|
|
5847
|
+
bestScore = score;
|
|
5848
|
+
best = hit;
|
|
5849
|
+
}
|
|
5784
5850
|
}
|
|
5785
5851
|
return {
|
|
5786
5852
|
start: best,
|
|
@@ -5874,191 +5940,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5874
5940
|
return applied;
|
|
5875
5941
|
}
|
|
5876
5942
|
//#endregion
|
|
5877
|
-
//#region src/client/views.tsx
|
|
5878
|
-
/**
|
|
5879
|
-
* Shared pure helpers for the study panel's pieces: chat-row shapes, tree
|
|
5880
|
-
* status copy, quiz-option extraction, and section default expansion. The
|
|
5881
|
-
* panel itself lives in `panel.tsx`; since 0.14.0 nothing here touches the
|
|
5882
|
-
* host composer or the conversation view.
|
|
5883
|
-
* @module dsh-plugin-lookatstudy/client/views
|
|
5884
|
-
*/
|
|
5885
|
-
/** Small inline error surface for failed write actions (shared with settings). */
|
|
5886
|
-
function ActionError({ error }) {
|
|
5887
|
-
if (error === null) return null;
|
|
5888
|
-
return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
|
|
5889
|
-
}
|
|
5890
|
-
/**
|
|
5891
|
-
* Tooltip text for one course-tree status glyph. Pure (the translator is an
|
|
5892
|
-
* optional parameter defaulting to the active one, evaluated per call).
|
|
5893
|
-
*/
|
|
5894
|
-
function statusTitle(kind, status, t = tr) {
|
|
5895
|
-
if (kind === "exam") return t("status.exam");
|
|
5896
|
-
switch (status) {
|
|
5897
|
-
case "mastered": return t("status.mastered");
|
|
5898
|
-
case "in_progress": return t("status.in_progress");
|
|
5899
|
-
case "available": return t("status.available");
|
|
5900
|
-
default: return t("status.locked");
|
|
5901
|
-
}
|
|
5902
|
-
}
|
|
5903
|
-
/**
|
|
5904
|
-
* Extract the last quiz option block (2–4 consecutive `A. …` `B. …` lines,
|
|
5905
|
-
* letters strictly consecutive from A) from an assistant reply — the raw text
|
|
5906
|
-
* of the tutor's question becomes the clickable answer surface. Pure.
|
|
5907
|
-
*/
|
|
5908
|
-
function quizOptions(text) {
|
|
5909
|
-
const runs = [];
|
|
5910
|
-
let run = [];
|
|
5911
|
-
const flush = () => {
|
|
5912
|
-
if (run.length >= 2) runs.push(run);
|
|
5913
|
-
run = [];
|
|
5914
|
-
};
|
|
5915
|
-
for (const line of text.split("\n")) {
|
|
5916
|
-
const plain = /^([A-D])[.、:)]\s*(.+)$/.exec(line.trim());
|
|
5917
|
-
const table = /^\|\s*\*{0,2}([A-D])\*{0,2}\s*\|\s*(.+?)\s*\|$/.exec(line.trim());
|
|
5918
|
-
const match = plain ?? (table !== null ? [
|
|
5919
|
-
table[0],
|
|
5920
|
-
table[1],
|
|
5921
|
-
table[2].replaceAll("**", "").replaceAll("`", "")
|
|
5922
|
-
] : null);
|
|
5923
|
-
if (match === null) {
|
|
5924
|
-
flush();
|
|
5925
|
-
continue;
|
|
5926
|
-
}
|
|
5927
|
-
const nextLetter = run.length === 0 ? "A" : String.fromCharCode(run[run.length - 1].letter.charCodeAt(0) + 1);
|
|
5928
|
-
if (match[1] === nextLetter) run.push({
|
|
5929
|
-
letter: match[1],
|
|
5930
|
-
text: match[2]
|
|
5931
|
-
});
|
|
5932
|
-
else {
|
|
5933
|
-
flush();
|
|
5934
|
-
if (match[1] === "A") run.push({
|
|
5935
|
-
letter: match[1],
|
|
5936
|
-
text: match[2]
|
|
5937
|
-
});
|
|
5938
|
-
}
|
|
5939
|
-
}
|
|
5940
|
-
flush();
|
|
5941
|
-
return runs.at(-1) ?? [];
|
|
5942
|
-
}
|
|
5943
|
-
/**
|
|
5944
|
-
* Merge the rail's two search tracks into the results panel: title hits
|
|
5945
|
-
* (lessons whose name matches — they are also filtered into the tree) first,
|
|
5946
|
-
* then full-text hits not already covered, snippets carried for the body
|
|
5947
|
-
* matches. Pure; empty queries yield no panel.
|
|
5948
|
-
*/
|
|
5949
|
-
function mergeRailSearch(query, lessons, textHits) {
|
|
5950
|
-
const keys = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
5951
|
-
if (keys.length === 0) return [];
|
|
5952
|
-
const rows = [];
|
|
5953
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5954
|
-
const matches = (title) => keys.every((k) => title.toLowerCase().includes(k));
|
|
5955
|
-
for (const lesson of lessons) {
|
|
5956
|
-
if (lesson.kind === "exam" || lesson.status === "locked" || !matches(lesson.title)) continue;
|
|
5957
|
-
rows.push({
|
|
5958
|
-
lessonId: lesson.id,
|
|
5959
|
-
title: lesson.title,
|
|
5960
|
-
snippet: "",
|
|
5961
|
-
courseTitle: "",
|
|
5962
|
-
fromTitle: true
|
|
5963
|
-
});
|
|
5964
|
-
seen.add(lesson.id);
|
|
5965
|
-
}
|
|
5966
|
-
for (const hit of textHits) {
|
|
5967
|
-
if (seen.has(hit.lessonId)) continue;
|
|
5968
|
-
rows.push({
|
|
5969
|
-
lessonId: hit.lessonId,
|
|
5970
|
-
title: hit.lessonTitle,
|
|
5971
|
-
snippet: hit.snippet,
|
|
5972
|
-
courseTitle: hit.courseTitle,
|
|
5973
|
-
fromTitle: false
|
|
5974
|
-
});
|
|
5975
|
-
seen.add(hit.lessonId);
|
|
5976
|
-
}
|
|
5977
|
-
return rows.slice(0, 12);
|
|
5978
|
-
}
|
|
5979
|
-
/**
|
|
5980
|
-
* A section's effective openness: the user's explicit toggle (persisted per
|
|
5981
|
-
* course) overrides the frontier-based default. Pure.
|
|
5982
|
-
*/
|
|
5983
|
-
function effectiveOpen(sectionTitle, defaultOpen, overrides) {
|
|
5984
|
-
return overrides[sectionTitle] ?? defaultOpen;
|
|
5985
|
-
}
|
|
5986
|
-
/**
|
|
5987
|
-
* The narrow-mode pane picker: only valid ids, remembering the last choice
|
|
5988
|
-
* (upstream's narrow switcher semantics). Pure.
|
|
5989
|
-
*/
|
|
5990
|
-
function pickNarrowPane(stored) {
|
|
5991
|
-
return stored === "rail" || stored === "chat" || stored === "note" ? stored : "chat";
|
|
5992
|
-
}
|
|
5993
|
-
/**
|
|
5994
|
-
* Default expansion for one rail section: collapsed when every study lesson is
|
|
5995
|
-
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
5996
|
-
* stays open; exam nodes never force a section open. Pure.
|
|
5997
|
-
*/
|
|
5998
|
-
function sectionDefaultOpen(section) {
|
|
5999
|
-
return section.lessons.some((l) => l.focus || l.kind !== "exam" && l.status !== "mastered" && l.status !== "locked");
|
|
6000
|
-
}
|
|
6001
|
-
/**
|
|
6002
|
-
* C1 sticky-follow: the stream follows new rows only while the reader is
|
|
6003
|
-
* within `tolerance` of the bottom (upstream's 80px rule — scroll up and the
|
|
6004
|
-
* feed stops chasing you). Pure.
|
|
6005
|
-
*/
|
|
6006
|
-
function isStuck(scrollTop, scrollHeight, clientHeight, tolerance = 80) {
|
|
6007
|
-
return scrollHeight - scrollTop - clientHeight <= tolerance;
|
|
6008
|
-
}
|
|
6009
|
-
/**
|
|
6010
|
-
* C12 touch pane swipe: a horizontal flick (>50px, dominance >2x) switches the
|
|
6011
|
-
* narrow pane neighbor; anything else is not a pane gesture. Pure
|
|
6012
|
-
* (upstream swipeTarget semantics).
|
|
6013
|
-
*/
|
|
6014
|
-
function swipePane(pane, dx, dy) {
|
|
6015
|
-
if (Math.abs(dx) < 50 || Math.abs(dx) <= 2 * Math.abs(dy)) return null;
|
|
6016
|
-
const order = [
|
|
6017
|
-
"rail",
|
|
6018
|
-
"chat",
|
|
6019
|
-
"note"
|
|
6020
|
-
];
|
|
6021
|
-
const idx = order.indexOf(pane);
|
|
6022
|
-
if (idx < 0) return null;
|
|
6023
|
-
const next = dx < 0 ? idx + 1 : idx - 1;
|
|
6024
|
-
return next >= 0 && next < order.length ? order[next] : null;
|
|
6025
|
-
}
|
|
6026
|
-
/**
|
|
6027
|
-
* C12 selection-popover settle: coarse pointers need a longer quiet window
|
|
6028
|
-
* (upstream's 600/250 two-tier). Pure.
|
|
6029
|
-
*/
|
|
6030
|
-
function settleMs(coarse) {
|
|
6031
|
-
return coarse ? 600 : 250;
|
|
6032
|
-
}
|
|
6033
|
-
/**
|
|
6034
|
-
* C9 interleaved review: pick one due lesson at random (Math.random; empty in,
|
|
6035
|
-
* null out). Pure.
|
|
6036
|
-
*/
|
|
6037
|
-
function pickRandomDue(items) {
|
|
6038
|
-
if (items.length === 0) return null;
|
|
6039
|
-
return items[Math.floor(Math.random() * items.length)] ?? null;
|
|
6040
|
-
}
|
|
6041
|
-
/**
|
|
6042
|
-
* Display-side section-title humanizer (0.19 critique round): import-design
|
|
6043
|
-
* section ids arrive as repo-folder slugs ("0-course-setup", "1-Intro") —
|
|
6044
|
-
* strip the leading episode number, hyphen/underscore runs become spaces.
|
|
6045
|
-
* Keys, anchors, and ids stay raw; this is presentation only. Never returns
|
|
6046
|
-
* an empty string (a title that humanizes to nothing keeps its raw form).
|
|
6047
|
-
*/
|
|
6048
|
-
function humanizeSectionTitle(raw) {
|
|
6049
|
-
const t = raw.trim().replace(/^\d+[.\-_ ]+/, "").replace(/[\-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
6050
|
-
return t === "" ? raw.trim() : t;
|
|
6051
|
-
}
|
|
6052
|
-
/**
|
|
6053
|
-
* Panel-facing error text (0.19 critique round): raw err.message (host RPC
|
|
6054
|
-
* internals like "prompt rejected: …") never renders in the DOM — the user
|
|
6055
|
-
* gets one honest locale line, the raw error goes to the console.
|
|
6056
|
-
*/
|
|
6057
|
-
function friendlyError(err, kind = "action", t = tr) {
|
|
6058
|
-
console.error("[lookatstudy]", err);
|
|
6059
|
-
return t(kind === "send" ? "error.send" : "error.action");
|
|
6060
|
-
}
|
|
6061
|
-
//#endregion
|
|
6062
5943
|
//#region src/client/maprail.tsx
|
|
6063
5944
|
/**
|
|
6064
5945
|
* RailList — the course rail as a quiet list (owner decision 2026-09-08: the
|
|
@@ -6173,6 +6054,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6173
6054
|
y
|
|
6174
6055
|
});
|
|
6175
6056
|
};
|
|
6057
|
+
const panelZoom = () => {
|
|
6058
|
+
const z = parseFloat(document.querySelector(".lks-ui")?.style.zoom ?? "1");
|
|
6059
|
+
return Number.isFinite(z) && z > 0 ? z : 1;
|
|
6060
|
+
};
|
|
6176
6061
|
const onMove = (e) => {
|
|
6177
6062
|
if (coarse) return;
|
|
6178
6063
|
const el = target(e);
|
|
@@ -6180,7 +6065,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6180
6065
|
setTip(null);
|
|
6181
6066
|
return;
|
|
6182
6067
|
}
|
|
6183
|
-
|
|
6068
|
+
const z = panelZoom();
|
|
6069
|
+
show(el, (e.clientX + 10) / z, (e.clientY + 12) / z);
|
|
6184
6070
|
};
|
|
6185
6071
|
const onLeave = () => {
|
|
6186
6072
|
setTip(null);
|
|
@@ -6197,7 +6083,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6197
6083
|
pressTimer.current = setTimeout(() => {
|
|
6198
6084
|
if (Math.hypot(e.clientX - start.x, e.clientY - start.y) < 10) {
|
|
6199
6085
|
const r = el.getBoundingClientRect();
|
|
6200
|
-
|
|
6086
|
+
const z = panelZoom();
|
|
6087
|
+
show(el, (r.left + r.width / 2 - 80) / z, (r.top - 36) / z);
|
|
6201
6088
|
}
|
|
6202
6089
|
}, 500);
|
|
6203
6090
|
};
|
|
@@ -6225,12 +6112,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6225
6112
|
};
|
|
6226
6113
|
}, []);
|
|
6227
6114
|
if (tip === null) return null;
|
|
6115
|
+
const z = parseFloat(document.querySelector(".lks-ui")?.style.zoom ?? "1") || 1;
|
|
6116
|
+
const clamped = clampTip(tip.x, tip.y, 200, 40, window.innerWidth / z, window.innerHeight / z);
|
|
6228
6117
|
return (0, react.createElement)("div", {
|
|
6229
6118
|
className: "lks-tip",
|
|
6230
6119
|
role: "tooltip",
|
|
6231
6120
|
style: {
|
|
6232
|
-
left: `${String(
|
|
6233
|
-
top: `${String(
|
|
6121
|
+
left: `${String(clamped.x)}px`,
|
|
6122
|
+
top: `${String(clamped.y)}px`
|
|
6234
6123
|
}
|
|
6235
6124
|
}, tip.text);
|
|
6236
6125
|
}
|
|
@@ -6757,7 +6646,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6757
6646
|
* transition here — the client bundle stays dependency-free.)
|
|
6758
6647
|
* @module dsh-plugin-lookatstudy/client/celebration-layer
|
|
6759
6648
|
*/
|
|
6760
|
-
/** prefers-reduced-motion, live-updating
|
|
6649
|
+
/** prefers-reduced-motion, live-updating. */
|
|
6761
6650
|
function usePrefersReducedMotion() {
|
|
6762
6651
|
const [reduced, setReduced] = (0, react.useState)(() => typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
6763
6652
|
(0, react.useEffect)(() => {
|
|
@@ -6987,7 +6876,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6987
6876
|
const body = (0, react.createElement)(ErrorBoundary, { key: `b-${row.key}` }, (0, react.createElement)("div", {
|
|
6988
6877
|
className: cls,
|
|
6989
6878
|
"data-row-key": row.key,
|
|
6990
|
-
dangerouslySetInnerHTML: { __html: renderMarkdown(row.text) }
|
|
6879
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(normalizeMathNotation(row.text)) }
|
|
6991
6880
|
}));
|
|
6992
6881
|
if (audio !== void 0 && row.role === "assistant") return (0, react.createElement)("div", {
|
|
6993
6882
|
key: row.key,
|
|
@@ -7097,24 +6986,39 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7097
6986
|
}, []);
|
|
7098
6987
|
const [rows, setRows] = (0, react.useState)([]);
|
|
7099
6988
|
const [pressure, setPressure] = (0, react.useState)(null);
|
|
6989
|
+
const [breakdown, setBreakdown] = (0, react.useState)(null);
|
|
7100
6990
|
const importProgress = (0, react.useMemo)(() => importProgressOf(rows), [rows]);
|
|
7101
6991
|
const contextMeter = (0, react.useMemo)(() => {
|
|
7102
6992
|
if (pressure !== null && pressure.contextWindow !== void 0 && pressure.contextWindow > 0 && pressure.projectedTokens !== void 0) return {
|
|
7103
6993
|
pct: Math.min(100, Math.round(pressure.projectedTokens / pressure.contextWindow * 100)),
|
|
7104
|
-
|
|
7105
|
-
|
|
6994
|
+
estimated: false,
|
|
6995
|
+
used: pressure.projectedTokens,
|
|
6996
|
+
window: pressure.contextWindow
|
|
6997
|
+
};
|
|
6998
|
+
const est = Math.round(rows.reduce((n, r) => n + r.text.length, 0) / 2.2);
|
|
6999
|
+
const window = data?.model?.contextWindow ?? null;
|
|
7000
|
+
if (window !== null && window > 0) return {
|
|
7001
|
+
pct: Math.min(100, Math.round(est / window * 100)),
|
|
7002
|
+
estimated: true,
|
|
7003
|
+
used: est,
|
|
7004
|
+
window
|
|
7106
7005
|
};
|
|
7107
|
-
const chars = rows.reduce((n, r) => n + r.text.length, 0);
|
|
7108
7006
|
return {
|
|
7109
7007
|
pct: null,
|
|
7110
|
-
|
|
7111
|
-
|
|
7008
|
+
estimated: true,
|
|
7009
|
+
used: est,
|
|
7010
|
+
window: null
|
|
7112
7011
|
};
|
|
7113
|
-
}, [
|
|
7012
|
+
}, [
|
|
7013
|
+
pressure,
|
|
7014
|
+
rows,
|
|
7015
|
+
data?.model
|
|
7016
|
+
]);
|
|
7114
7017
|
const [feedAttached, setFeedAttached] = (0, react.useState)(false);
|
|
7115
7018
|
const [draft, setDraft] = (0, react.useState)("");
|
|
7116
7019
|
const [narrowPane, setNarrowPane] = (0, react.useState)(pickNarrowPane(null));
|
|
7117
7020
|
const lesson = data?.lesson ?? null;
|
|
7021
|
+
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 ?? []);
|
|
7118
7022
|
const localBound = (0, react.useRef)(null);
|
|
7119
7023
|
const streamEl = (0, react.useRef)(null);
|
|
7120
7024
|
const touchStart = (0, react.useRef)(null);
|
|
@@ -7187,14 +7091,25 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7187
7091
|
let off;
|
|
7188
7092
|
try {
|
|
7189
7093
|
const actx = ctx.sessions.scope(boundId);
|
|
7190
|
-
const
|
|
7094
|
+
const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
|
|
7095
|
+
const proj = face?.projections?.faceOf("contextPressure");
|
|
7096
|
+
const bproj = face?.projections?.faceOf("contextBreakdown");
|
|
7191
7097
|
if (proj !== void 0) {
|
|
7192
7098
|
const read = () => {
|
|
7099
|
+
if (disposed) return;
|
|
7193
7100
|
const snap = proj.getSnapshot();
|
|
7194
|
-
if (
|
|
7101
|
+
if (snap !== void 0 && typeof snap === "object") setPressure({
|
|
7195
7102
|
projectedTokens: snap.projectedTokens,
|
|
7196
7103
|
contextWindow: snap.contextWindow
|
|
7197
7104
|
});
|
|
7105
|
+
if (bproj !== void 0) {
|
|
7106
|
+
const bsnap = bproj.getSnapshot();
|
|
7107
|
+
if (bsnap !== void 0 && typeof bsnap === "object") setBreakdown({
|
|
7108
|
+
systemTokens: bsnap.systemTokens ?? 0,
|
|
7109
|
+
toolsTokens: bsnap.toolsTokens ?? 0,
|
|
7110
|
+
messageTokens: bsnap.messageTokens ?? 0
|
|
7111
|
+
});
|
|
7112
|
+
}
|
|
7198
7113
|
};
|
|
7199
7114
|
read();
|
|
7200
7115
|
off = proj.subscribe(read);
|
|
@@ -7361,8 +7276,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7361
7276
|
window.removeEventListener("lookatstudy-exam-generate", onGenerate);
|
|
7362
7277
|
};
|
|
7363
7278
|
}, [send]);
|
|
7364
|
-
const companion = useCompanionMood();
|
|
7365
|
-
const setCompanionEvent = companion.emit;
|
|
7366
7279
|
const progress = data?.progress ?? null;
|
|
7367
7280
|
const body = (0, react.createElement)("div", {
|
|
7368
7281
|
className: "lks14-body",
|
|
@@ -7392,9 +7305,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7392
7305
|
onJumped: () => {
|
|
7393
7306
|
setNarrowPane("chat");
|
|
7394
7307
|
},
|
|
7395
|
-
onBlankTap: () => {
|
|
7396
|
-
setCompanionEvent("poke");
|
|
7397
|
-
},
|
|
7398
7308
|
streamingLessonId: feedGen && lesson !== null ? lesson.lessonId : null,
|
|
7399
7309
|
turnActive: feedGen,
|
|
7400
7310
|
importProgress,
|
|
@@ -7429,7 +7339,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7429
7339
|
onClick: () => {
|
|
7430
7340
|
setZoom(zoomTier + .1);
|
|
7431
7341
|
}
|
|
7432
|
-
}, "A+"))), (0, react.createElement)("div", { className: "lks14-row" }, (0, react.createElement)(
|
|
7342
|
+
}, "A+"))), (0, react.createElement)("div", { className: "lks14-row" }, examLessonActive ? (0, react.createElement)("div", { className: "lks14-col lks14-chat" }, (0, react.createElement)(ExamView, {
|
|
7343
|
+
lessonId: lesson.lessonId,
|
|
7344
|
+
sectionTitle: lesson.sectionTitle,
|
|
7345
|
+
paused: examLeave !== null,
|
|
7346
|
+
onSessionChange: onExamSession
|
|
7347
|
+
})) : (0, react.createElement)(ChatPane, {
|
|
7433
7348
|
data,
|
|
7434
7349
|
lesson,
|
|
7435
7350
|
rows,
|
|
@@ -7443,20 +7358,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7443
7358
|
stop,
|
|
7444
7359
|
setMode,
|
|
7445
7360
|
narrowPane,
|
|
7446
|
-
companionEvent: setCompanionEvent,
|
|
7447
7361
|
onThreadJump: (id) => {
|
|
7448
7362
|
setNarrowPane("chat");
|
|
7449
7363
|
guardedSetFocus(id);
|
|
7450
7364
|
},
|
|
7451
7365
|
contextMeter,
|
|
7366
|
+
contextBreakdown: breakdown,
|
|
7452
7367
|
uploadAttachment
|
|
7453
7368
|
}), (0, react.createElement)(NotebookPane, {
|
|
7454
7369
|
data,
|
|
7455
7370
|
deleteNote,
|
|
7456
|
-
send
|
|
7457
|
-
companionEvent: setCompanionEvent,
|
|
7458
|
-
onExamSession,
|
|
7459
|
-
examPaused: examLeave !== null
|
|
7371
|
+
send
|
|
7460
7372
|
}))));
|
|
7461
7373
|
return (0, react.createElement)("div", {
|
|
7462
7374
|
className: "lks14 lks-ui",
|
|
@@ -7490,13 +7402,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7490
7402
|
setNarrowPane("chat");
|
|
7491
7403
|
send(tr("prompt.start"));
|
|
7492
7404
|
}
|
|
7493
|
-
}) : null, (0, react.createElement)(
|
|
7494
|
-
mood: companion.mood,
|
|
7495
|
-
onPoke: () => {
|
|
7496
|
-
setCompanionEvent("poke");
|
|
7497
|
-
showStudyToast(tr("companion.poked"), { severity: "success" });
|
|
7498
|
-
}
|
|
7499
|
-
}), (0, react.createElement)("div", { className: "lks14-switch" }, ...[
|
|
7405
|
+
}) : null, (0, react.createElement)("div", { className: "lks14-switch" }, ...[
|
|
7500
7406
|
"rail",
|
|
7501
7407
|
"chat",
|
|
7502
7408
|
"note"
|
|
@@ -7764,7 +7670,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7764
7670
|
PANEL_SHELL.current = shell;
|
|
7765
7671
|
}
|
|
7766
7672
|
/** 左栏:course picker, tree, review box, import. */
|
|
7767
|
-
function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send, onJumped,
|
|
7673
|
+
function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send, onJumped, streamingLessonId, turnActive, importProgress, stop, paletteBus }) {
|
|
7768
7674
|
const [selectedCourse, setSelectedCourse] = (0, react.useState)("");
|
|
7769
7675
|
const [query, setQuery] = (0, react.useState)("");
|
|
7770
7676
|
const [searchRows, setSearchRows] = (0, react.useState)([]);
|
|
@@ -7835,7 +7741,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7835
7741
|
turnActive
|
|
7836
7742
|
]);
|
|
7837
7743
|
const course = data?.courses.find((c) => c.courseId === courseId) ?? null;
|
|
7838
|
-
const blankDown = (0, react.useRef)(null);
|
|
7839
7744
|
(0, react.useEffect)(() => {
|
|
7840
7745
|
if (optFocus !== null && data?.focusLessonId === optFocus) setOptFocus(null);
|
|
7841
7746
|
}, [data?.focusLessonId, optFocus]);
|
|
@@ -7955,20 +7860,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7955
7860
|
}, (0, react.createElement)(IconWrenchOutline16, { size: 13 }), tr("map.world.practice"))) : null)) : railtabs);
|
|
7956
7861
|
const mapPane = data === null ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("loading")) : course === null ? null : (0, react.createElement)("div", {
|
|
7957
7862
|
className: "lks14-railscroll",
|
|
7958
|
-
ref: scrollEl
|
|
7959
|
-
onPointerDown: (e) => {
|
|
7960
|
-
blankDown.current = {
|
|
7961
|
-
x: e.clientX,
|
|
7962
|
-
y: e.clientY
|
|
7963
|
-
};
|
|
7964
|
-
},
|
|
7965
|
-
onClick: (e) => {
|
|
7966
|
-
const d = blankDown.current;
|
|
7967
|
-
blankDown.current = null;
|
|
7968
|
-
if (d === null || Math.hypot(e.clientX - d.x, e.clientY - d.y) > 6) return;
|
|
7969
|
-
if (e.target.closest("button, a, input, textarea, select") !== null) return;
|
|
7970
|
-
onBlankTap();
|
|
7971
|
-
}
|
|
7863
|
+
ref: scrollEl
|
|
7972
7864
|
}, (0, react.createElement)("div", { className: "lks-raillist" }, query.trim() === "" && world === "practice" && !course.sections.some((s) => sectionWorldOf(s) === "practice") ? (0, react.createElement)("div", { className: "lks-empty-practice" }, tr("map.empty.practice")) : course.sections.filter((s) => query.trim() !== "" || sectionWorldOf(s) === world).flatMap((section) => {
|
|
7973
7865
|
const examAllowed = examOpen(section.lessons);
|
|
7974
7866
|
const lessons = section.lessons.filter((l) => query.trim() === "" || titleMatches(l.title, query) || l.focus);
|
|
@@ -8375,9 +8267,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8375
8267
|
return cut <= 0 ? path : norm.slice(0, cut);
|
|
8376
8268
|
}
|
|
8377
8269
|
/** 中栏:the tutor chat stream with its own composer (upstream ChatStream + ChatComposer). */
|
|
8378
|
-
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, stop, setMode, narrowPane,
|
|
8270
|
+
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, stop, setMode, narrowPane, onThreadJump, contextMeter, contextBreakdown, uploadAttachment }) {
|
|
8379
8271
|
const stuck = (0, react.useRef)(true);
|
|
8380
8272
|
const [showFab, setShowFab] = (0, react.useState)(false);
|
|
8273
|
+
const [threadsOpen, setThreadsOpen] = (0, react.useState)(false);
|
|
8381
8274
|
const onStreamScroll = () => {
|
|
8382
8275
|
const el = streamEl.current;
|
|
8383
8276
|
if (el === null) return;
|
|
@@ -8391,6 +8284,15 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8391
8284
|
const el = streamEl.current;
|
|
8392
8285
|
if (el !== null) el.scrollTop = el.scrollHeight;
|
|
8393
8286
|
}, [rows.length, lastIsUser]);
|
|
8287
|
+
(0, react.useEffect)(() => {
|
|
8288
|
+
const root = streamEl.current;
|
|
8289
|
+
if (root === null) return;
|
|
8290
|
+
for (const el of root.querySelectorAll(".lks14-msg-assistant:not(.streaming)")) {
|
|
8291
|
+
if (el.dataset.lksFeedEnhanced === "1") continue;
|
|
8292
|
+
el.dataset.lksFeedEnhanced = "1";
|
|
8293
|
+
enhanceRendered(el).catch(() => {});
|
|
8294
|
+
}
|
|
8295
|
+
}, [rows]);
|
|
8394
8296
|
const dormant = data?.active !== true;
|
|
8395
8297
|
(0, react.useEffect)(() => {
|
|
8396
8298
|
if (!busy) return;
|
|
@@ -8402,6 +8304,37 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8402
8304
|
window.removeEventListener("keydown", onKey);
|
|
8403
8305
|
};
|
|
8404
8306
|
}, [busy, stop]);
|
|
8307
|
+
(0, react.useEffect)(() => {
|
|
8308
|
+
if (!threadsOpen) return;
|
|
8309
|
+
const onKey = (e) => {
|
|
8310
|
+
if (e.key === "Escape") setThreadsOpen(false);
|
|
8311
|
+
};
|
|
8312
|
+
const onDown = (e) => {
|
|
8313
|
+
if (e.target.closest(".lks14-threadswitch") === null) setThreadsOpen(false);
|
|
8314
|
+
};
|
|
8315
|
+
window.addEventListener("keydown", onKey);
|
|
8316
|
+
document.addEventListener("pointerdown", onDown);
|
|
8317
|
+
return () => {
|
|
8318
|
+
window.removeEventListener("keydown", onKey);
|
|
8319
|
+
document.removeEventListener("pointerdown", onDown);
|
|
8320
|
+
};
|
|
8321
|
+
}, [threadsOpen]);
|
|
8322
|
+
const [ctxOpen, setCtxOpen] = (0, react.useState)(false);
|
|
8323
|
+
(0, react.useEffect)(() => {
|
|
8324
|
+
if (!ctxOpen) return;
|
|
8325
|
+
const onKey = (e) => {
|
|
8326
|
+
if (e.key === "Escape") setCtxOpen(false);
|
|
8327
|
+
};
|
|
8328
|
+
const onDown = (e) => {
|
|
8329
|
+
if (e.target.closest(".lks14-ctxring-wrap") === null) setCtxOpen(false);
|
|
8330
|
+
};
|
|
8331
|
+
window.addEventListener("keydown", onKey);
|
|
8332
|
+
document.addEventListener("pointerdown", onDown);
|
|
8333
|
+
return () => {
|
|
8334
|
+
window.removeEventListener("keydown", onKey);
|
|
8335
|
+
document.removeEventListener("pointerdown", onDown);
|
|
8336
|
+
};
|
|
8337
|
+
}, [ctxOpen]);
|
|
8405
8338
|
const { tts } = useStudy();
|
|
8406
8339
|
const [msgAudio, setMsgAudio] = (0, react.useState)(null);
|
|
8407
8340
|
const msgReadCtl = (0, react.useRef)(null);
|
|
@@ -8437,11 +8370,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8437
8370
|
}
|
|
8438
8371
|
};
|
|
8439
8372
|
const playMessage = (key) => {
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8373
|
+
if (rowElement(key) === null) return;
|
|
8374
|
+
const raw = rowsView.find((r) => r.key === key && r.role === "assistant")?.text;
|
|
8375
|
+
if (raw === void 0) return;
|
|
8376
|
+
const sentences = speechSentencesOf(raw);
|
|
8443
8377
|
if (sentences.length === 0) return;
|
|
8444
|
-
companionEvent("talk-start");
|
|
8445
8378
|
msgReadCtl.current?.stop();
|
|
8446
8379
|
const voice = storedTtsVoice();
|
|
8447
8380
|
const prefetch = /* @__PURE__ */ new Map();
|
|
@@ -8454,7 +8387,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8454
8387
|
}
|
|
8455
8388
|
return tts(text, voice);
|
|
8456
8389
|
};
|
|
8457
|
-
const controller = new ReadAloudController(sentences, {
|
|
8390
|
+
const controller = new ReadAloudController(sentences.map((s) => speakMathInSentence(s)), {
|
|
8458
8391
|
speak: (text) => fetchAudio(text).then((buf) => new Promise((resolve, reject) => {
|
|
8459
8392
|
const blobUrl = URL.createObjectURL(new Blob([buf], { type: "audio/mpeg" }));
|
|
8460
8393
|
const audio = new Audio(blobUrl);
|
|
@@ -8522,7 +8455,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8522
8455
|
});
|
|
8523
8456
|
};
|
|
8524
8457
|
const stopMessage = () => {
|
|
8525
|
-
companionEvent("talk-end");
|
|
8526
8458
|
msgReadCtl.current?.stop();
|
|
8527
8459
|
setMsgAudio(null);
|
|
8528
8460
|
document.querySelectorAll("mark.lks-reading").forEach((m) => {
|
|
@@ -8550,9 +8482,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8550
8482
|
data: artifact.data,
|
|
8551
8483
|
masteryPct: lesson.masteryPct,
|
|
8552
8484
|
send,
|
|
8553
|
-
onFinished: (
|
|
8554
|
-
companionEvent(allCorrect ? "celebrate" : "encourage");
|
|
8555
|
-
}
|
|
8485
|
+
onFinished: () => {}
|
|
8556
8486
|
}) : (0, react.createElement)(ArtifactCard, {
|
|
8557
8487
|
artifact,
|
|
8558
8488
|
send
|
|
@@ -8650,20 +8580,29 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8650
8580
|
}));
|
|
8651
8581
|
send(tr("proposal.decline.msg", { lesson: proposal.lessonTitle }));
|
|
8652
8582
|
}
|
|
8653
|
-
}, tr("proposal.decline"))) : decidedBadge !== null ? (0, react.createElement)("div", { className: `lks-propbanner decided ${decidedBadge.kind}` }, decidedBadge.kind === "accepted" ? (0, react.createElement)(IconCrownFill16, { size: 14 }) : (0, react.createElement)(IconBookFill16, { size: 14 }), (0, react.createElement)("span", { className: "lks-propbanner-why" }, tr(decidedBadge.kind === "accepted" ? "proposal.applied" : "proposal.rejected", { lesson: decidedBadge.title }))) : null, lesson !== null || !dormant ? (0, react.createElement)("div", { className: "lks14-lessonrow" }, lesson !== null ? (0, react.createElement)("span", null, lesson.title) : (0, react.createElement)("span", null, tr("col.tutor")), threadPills.length > 0 ? (0, react.createElement)("
|
|
8654
|
-
className: "lks14-
|
|
8655
|
-
|
|
8656
|
-
"
|
|
8583
|
+
}, tr("proposal.decline"))) : decidedBadge !== null ? (0, react.createElement)("div", { className: `lks-propbanner decided ${decidedBadge.kind}` }, decidedBadge.kind === "accepted" ? (0, react.createElement)(IconCrownFill16, { size: 14 }) : (0, react.createElement)(IconBookFill16, { size: 14 }), (0, react.createElement)("span", { className: "lks-propbanner-why" }, tr(decidedBadge.kind === "accepted" ? "proposal.applied" : "proposal.rejected", { lesson: decidedBadge.title }))) : null, lesson !== null || !dormant ? (0, react.createElement)("div", { className: "lks14-lessonrow" }, lesson !== null ? (0, react.createElement)("span", null, lesson.title) : (0, react.createElement)("span", null, tr("col.tutor")), threadPills.length > 0 ? (0, react.createElement)("div", { className: "lks14-threadswitch" }, (0, react.createElement)("button", {
|
|
8584
|
+
className: "lks14-threadchip",
|
|
8585
|
+
"aria-expanded": String(threadsOpen),
|
|
8586
|
+
"data-tooltip": tr("threads.label"),
|
|
8587
|
+
onClick: () => {
|
|
8588
|
+
setThreadsOpen((v) => !v);
|
|
8589
|
+
}
|
|
8590
|
+
}, tr("threads.open", { n: String(threadPills.length) }), (0, react.createElement)("span", { className: "lks14-threadchip-caret" }, threadsOpen ? "▾" : "▸")), threadsOpen ? (0, react.createElement)("div", {
|
|
8591
|
+
className: "lks14-threadmenu",
|
|
8592
|
+
role: "menu"
|
|
8657
8593
|
}, ...threadPills.map((p) => (0, react.createElement)("button", {
|
|
8658
8594
|
key: p.id,
|
|
8659
|
-
className: `lks14-
|
|
8660
|
-
role: "
|
|
8661
|
-
"aria-
|
|
8662
|
-
"data-tooltip": p.title,
|
|
8595
|
+
className: `lks14-threadmenu-row${p.current ? " on" : ""}`,
|
|
8596
|
+
role: "menuitem",
|
|
8597
|
+
"aria-current": p.current ? "true" : void 0,
|
|
8663
8598
|
onClick: () => {
|
|
8599
|
+
setThreadsOpen(false);
|
|
8664
8600
|
if (!p.current) onThreadJump(p.id);
|
|
8665
8601
|
}
|
|
8666
|
-
},
|
|
8602
|
+
}, (0, react.createElement)("span", {
|
|
8603
|
+
className: "lks14-threadmenu-dot",
|
|
8604
|
+
"aria-hidden": "true"
|
|
8605
|
+
}), (0, react.createElement)("span", { className: "lks14-threadmenu-title" }, p.title), p.current ? (0, react.createElement)("span", { className: "lks14-threadmenu-now" }, tr("threads.now")) : null))) : null) : null) : null, (0, react.createElement)("div", {
|
|
8667
8606
|
className: "lks14-stream",
|
|
8668
8607
|
onScroll: onStreamScroll,
|
|
8669
8608
|
ref: streamEl,
|
|
@@ -8687,9 +8626,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8687
8626
|
data: a.data,
|
|
8688
8627
|
masteryPct: lesson.masteryPct,
|
|
8689
8628
|
send,
|
|
8690
|
-
onFinished: (
|
|
8691
|
-
companionEvent(allCorrect ? "celebrate" : "encourage");
|
|
8692
|
-
}
|
|
8629
|
+
onFinished: () => {}
|
|
8693
8630
|
})), ...backlog.filter((a) => a.artifactType !== "quiz").map((a) => (0, react.createElement)(ArtifactCard, {
|
|
8694
8631
|
key: a.id,
|
|
8695
8632
|
artifact: a,
|
|
@@ -8703,14 +8640,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8703
8640
|
if (!dormant) send(s.message);
|
|
8704
8641
|
}
|
|
8705
8642
|
}, s.label))) : null, (0, react.createElement)("div", { className: "lks14-composer" }, (0, react.createElement)("div", {
|
|
8706
|
-
className: "lks14-ctxmeter",
|
|
8707
|
-
"data-testid": "context-meter",
|
|
8708
|
-
"data-estimated": String(contextMeter.estimated),
|
|
8709
|
-
title: tr("meter.label")
|
|
8710
|
-
}, (0, react.createElement)("span", { className: "lks14-ctxmeter-bar" }, (0, react.createElement)("i", {
|
|
8711
|
-
style: { transform: `scaleX(${String((contextMeter.pct ?? 0) / 100)})` },
|
|
8712
|
-
className: contextMeter.pct !== null && contextMeter.pct > 80 ? "hot" : void 0
|
|
8713
|
-
})), (0, react.createElement)("span", { className: "lks14-ctxmeter-label" }, contextMeter.estimated ? contextMeter.label : `${String(contextMeter.pct ?? 0)}% · ${contextMeter.label}`)), (0, react.createElement)("div", {
|
|
8714
8643
|
className: "lks14-composer-card",
|
|
8715
8644
|
onDragOver: (e) => {
|
|
8716
8645
|
if (e.dataTransfer?.types.includes("Files") === true) e.preventDefault();
|
|
@@ -8765,7 +8694,55 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8765
8694
|
if (file !== void 0 && file !== null) attachFile(file);
|
|
8766
8695
|
e.target.value = "";
|
|
8767
8696
|
}
|
|
8768
|
-
}),
|
|
8697
|
+
}), contextMeter.pct !== null ? (0, react.createElement)("span", {
|
|
8698
|
+
className: "lks14-ctxring-wrap",
|
|
8699
|
+
key: "ctxring"
|
|
8700
|
+
}, (0, react.createElement)("button", {
|
|
8701
|
+
className: "lks14-ctxring",
|
|
8702
|
+
"data-testid": "context-meter",
|
|
8703
|
+
"data-estimated": String(contextMeter.estimated),
|
|
8704
|
+
"aria-label": tr("meter.aria", { pct: String(contextMeter.pct) }),
|
|
8705
|
+
"data-tooltip": tr("meter.aria", { pct: String(contextMeter.pct) }),
|
|
8706
|
+
"aria-haspopup": "dialog",
|
|
8707
|
+
"aria-expanded": String(ctxOpen),
|
|
8708
|
+
onClick: () => {
|
|
8709
|
+
setCtxOpen((v) => !v);
|
|
8710
|
+
}
|
|
8711
|
+
}, (0, react.createElement)("svg", {
|
|
8712
|
+
viewBox: "0 0 14 14",
|
|
8713
|
+
width: 14,
|
|
8714
|
+
height: 14,
|
|
8715
|
+
"aria-hidden": "true"
|
|
8716
|
+
}, (0, react.createElement)("circle", {
|
|
8717
|
+
className: "track",
|
|
8718
|
+
cx: 7,
|
|
8719
|
+
cy: 7,
|
|
8720
|
+
r: 5.5
|
|
8721
|
+
}), (0, react.createElement)("circle", {
|
|
8722
|
+
className: `fill${contextMeter.pct > 80 ? " hot" : ""}`,
|
|
8723
|
+
cx: 7,
|
|
8724
|
+
cy: 7,
|
|
8725
|
+
r: 5.5,
|
|
8726
|
+
strokeDasharray: `${String(2 * Math.PI * 5.5 * contextMeter.pct / 100)} ${String(2 * Math.PI * 5.5)}`,
|
|
8727
|
+
transform: "rotate(-90 7 7)"
|
|
8728
|
+
}))), ctxOpen ? (0, react.createElement)("div", {
|
|
8729
|
+
className: "lks14-ctxpanel",
|
|
8730
|
+
role: "dialog",
|
|
8731
|
+
"aria-label": tr("meter.label")
|
|
8732
|
+
}, (0, react.createElement)("div", { className: "lks14-ctxpanel-head" }, (0, react.createElement)("span", { className: "lks14-ctxpanel-pct" }, `${String(contextMeter.pct)}%`), (0, react.createElement)("span", { className: "lks14-ctxpanel-figs" }, `~${fmtTokens(contextMeter.used ?? 0)} / ${fmtTokens(contextMeter.window ?? 0)}`)), (0, react.createElement)("div", { className: "lks14-ctxpanel-bar" }, ...ctxSegments(contextMeter.pct, contextBreakdown).map((s) => (0, react.createElement)("i", {
|
|
8733
|
+
key: s.key,
|
|
8734
|
+
className: s.cls,
|
|
8735
|
+
style: { width: `${String(s.width)}%` }
|
|
8736
|
+
}))), contextBreakdown !== null ? (0, react.createElement)("div", { className: "lks14-ctxpanel-rows" }, (0, react.createElement)("div", { className: "lks14-ctxpanel-row" }, (0, react.createElement)("span", {
|
|
8737
|
+
className: "swatch sys",
|
|
8738
|
+
"aria-hidden": "true"
|
|
8739
|
+
}), tr("meter.brk.system"), (0, react.createElement)("b", null, `~${fmtTokens(contextBreakdown.systemTokens)}`)), (0, react.createElement)("div", { className: "lks14-ctxpanel-row" }, (0, react.createElement)("span", {
|
|
8740
|
+
className: "swatch tools",
|
|
8741
|
+
"aria-hidden": "true"
|
|
8742
|
+
}), tr("meter.brk.tools"), (0, react.createElement)("b", null, `~${fmtTokens(contextBreakdown.toolsTokens)}`)), (0, react.createElement)("div", { className: "lks14-ctxpanel-row" }, (0, react.createElement)("span", {
|
|
8743
|
+
className: "swatch msgs",
|
|
8744
|
+
"aria-hidden": "true"
|
|
8745
|
+
}), tr("meter.brk.messages"), (0, react.createElement)("b", null, `~${fmtTokens(contextBreakdown.messageTokens)}`))) : null, contextMeter.estimated ? (0, react.createElement)("div", { className: "lks14-ctxpanel-note" }, tr("meter.estimate.note")) : null) : null) : null, busy ? (0, react.createElement)("button", {
|
|
8769
8746
|
className: "lks-btn-send stop",
|
|
8770
8747
|
"aria-label": tr("chat.stop"),
|
|
8771
8748
|
"data-tooltip": tr("chat.stop"),
|
|
@@ -8790,7 +8767,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8790
8767
|
}, "▾") : null, sendError !== null || error !== null ? (0, react.createElement)("div", { className: "lks-propcard-err" }, sendError ?? error) : null));
|
|
8791
8768
|
}
|
|
8792
8769
|
/** 右栏:the notebook — 讲解/概念图/笔记 tabs (upstream NotebookPanel arrangement). */
|
|
8793
|
-
function NotebookPane({ data, deleteNote, send
|
|
8770
|
+
function NotebookPane({ data, deleteNote, send }) {
|
|
8794
8771
|
const lesson = data?.lesson ?? null;
|
|
8795
8772
|
const [tab, setTab] = (0, react.useState)("teach");
|
|
8796
8773
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -8824,18 +8801,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8824
8801
|
setError(friendlyError(err, "action"));
|
|
8825
8802
|
});
|
|
8826
8803
|
};
|
|
8827
|
-
const examContent = lesson !== null && lesson.kind === "exam" ? (() => {
|
|
8828
|
-
const section = data?.courses.flatMap((c) => c.sections).find((s) => s.lessons.some((l) => l.id === lesson.lessonId));
|
|
8829
|
-
return examOpen(section?.lessons ?? []) ? (0, react.createElement)(ExamView, {
|
|
8830
|
-
lessonId: lesson.lessonId,
|
|
8831
|
-
sectionTitle: lesson.sectionTitle,
|
|
8832
|
-
paused: examPaused,
|
|
8833
|
-
onSessionChange: onExamSession
|
|
8834
|
-
}) : (0, react.createElement)("div", {
|
|
8835
|
-
className: "lks14-empty",
|
|
8836
|
-
style: { padding: "16px 0" }
|
|
8837
|
-
}, tr("status.exam"));
|
|
8838
|
-
})() : null;
|
|
8839
8804
|
const artifacts = lesson?.artifacts ?? [];
|
|
8840
8805
|
const [, setUnseenTick] = (0, react.useState)(0);
|
|
8841
8806
|
const unseen = unseenArtifacts(lesson?.lessonId ?? "", artifacts);
|
|
@@ -8923,7 +8888,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8923
8888
|
const [rateBusy, setRateBusy] = (0, react.useState)(false);
|
|
8924
8889
|
const startReading = () => {
|
|
8925
8890
|
if (lesson === null || lesson.speechText.trim() === "") return;
|
|
8926
|
-
companionEvent("talk-start");
|
|
8927
8891
|
readCtl.current?.stop();
|
|
8928
8892
|
const sentences = speechSentencesOf(lesson.speechText);
|
|
8929
8893
|
const voice = storedTtsVoice();
|
|
@@ -9004,7 +8968,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9004
8968
|
});
|
|
9005
8969
|
};
|
|
9006
8970
|
const stopReading = () => {
|
|
9007
|
-
companionEvent("talk-end");
|
|
9008
8971
|
readCtl.current?.stop();
|
|
9009
8972
|
setRead(null);
|
|
9010
8973
|
};
|
|
@@ -9079,7 +9042,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9079
9042
|
onClick: () => {
|
|
9080
9043
|
setRateBusy(true);
|
|
9081
9044
|
recordReview(lesson.lessonId, Number(q)).then(() => {
|
|
9082
|
-
companionEvent(Number(q) >= 4 ? "celebrate" : "encourage");
|
|
9083
9045
|
const rateEl = document.querySelector(`[data-lks-rate="${lesson.lessonId}"]`);
|
|
9084
9046
|
const rr = Number(q) >= 4 && rateEl !== null ? rateEl.getBoundingClientRect() : null;
|
|
9085
9047
|
celebrate(Number(q) >= 4 ? "correct" : "wrong", rr !== null ? { origin: {
|
|
@@ -9307,7 +9269,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9307
9269
|
dangerouslySetInnerHTML: { __html: renderMarkdown(n.text) }
|
|
9308
9270
|
})), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `'${n.quote}'`) : null)) : []);
|
|
9309
9271
|
})));
|
|
9310
|
-
return (0, react.createElement)("div", { className: "lks14-col lks14-note" },
|
|
9272
|
+
return (0, react.createElement)("div", { className: "lks14-col lks14-note" }, body, confirmNoteDel !== null ? (0, react.createElement)(ConfirmCard, {
|
|
9311
9273
|
anchor: confirmNoteDel.rect,
|
|
9312
9274
|
message: tr("note.delete.confirm"),
|
|
9313
9275
|
danger: true,
|
|
@@ -9575,7 +9537,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9575
9537
|
const { data, setMode, activate, setHistoryBudget } = useStudy();
|
|
9576
9538
|
const [error, setError] = (0, react.useState)(null);
|
|
9577
9539
|
const [voice, setVoice] = (0, react.useState)(storedTtsVoice);
|
|
9578
|
-
const [companion, setCompanion] = (0, react.useState)(storedCompanionForm);
|
|
9579
9540
|
const fire = (action) => {
|
|
9580
9541
|
action.then(() => {
|
|
9581
9542
|
setError(null);
|
|
@@ -9602,17 +9563,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9602
9563
|
onClick: () => {
|
|
9603
9564
|
if (data !== null) fire(setHistoryBudget(!(data.historyBudget === true)));
|
|
9604
9565
|
}
|
|
9605
|
-
}, data?.historyBudget === true ? tr("settings.budget.off") : tr("settings.budget.on")))), (0, react.createElement)("section", { className: "lks-set-row" }, (0, react.createElement)("h3", null, tr("settings.
|
|
9606
|
-
className: "lks-set-select",
|
|
9607
|
-
value: companion,
|
|
9608
|
-
onChange: (e) => {
|
|
9609
|
-
storeCompanionForm(normalizeCompanionForm(e.target.value));
|
|
9610
|
-
setCompanion(normalizeCompanionForm(e.target.value));
|
|
9611
|
-
}
|
|
9612
|
-
}, ...COMPANION_FORMS.map((f) => (0, react.createElement)("option", {
|
|
9613
|
-
key: f.id,
|
|
9614
|
-
value: f.id
|
|
9615
|
-
}, tr(f.labelKey)))))), (0, react.createElement)("section", { className: "lks-set-row" }, (0, react.createElement)("h3", null, tr("settings.voice")), (0, react.createElement)("p", { className: "lks-set-hint" }, tr("settings.voice.hint")), (0, react.createElement)("div", null, (0, react.createElement)("select", {
|
|
9566
|
+
}, data?.historyBudget === true ? tr("settings.budget.off") : tr("settings.budget.on")))), (0, react.createElement)("section", { className: "lks-set-row" }, (0, react.createElement)("h3", null, tr("settings.voice")), (0, react.createElement)("p", { className: "lks-set-hint" }, tr("settings.voice.hint")), (0, react.createElement)("div", null, (0, react.createElement)("select", {
|
|
9616
9567
|
className: "lks-set-select",
|
|
9617
9568
|
value: voice,
|
|
9618
9569
|
onChange: (e) => {
|