dsh-plugin-lookatstudy 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/client.js +441 -527
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +47 -25
- 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",
|
|
@@ -3191,6 +3200,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3191
3200
|
*/
|
|
3192
3201
|
function attachCodeHeader(wrap, lang) {
|
|
3193
3202
|
if (wrap.parentElement?.querySelector(".lks-codehead") !== null) return;
|
|
3203
|
+
wrap.closest(".lks-codeblock")?.querySelector(":scope > .lks-codeblock-head")?.remove();
|
|
3194
3204
|
const head = document.createElement("div");
|
|
3195
3205
|
head.className = "lks-codehead";
|
|
3196
3206
|
const label = document.createElement("span");
|
|
@@ -3291,6 +3301,220 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3291
3301
|
} catch {}
|
|
3292
3302
|
}
|
|
3293
3303
|
//#endregion
|
|
3304
|
+
//#region src/client/views.tsx
|
|
3305
|
+
/**
|
|
3306
|
+
* Shared pure helpers for the study panel's pieces: chat-row shapes, tree
|
|
3307
|
+
* status copy, quiz-option extraction, and section default expansion. The
|
|
3308
|
+
* panel itself lives in `panel.tsx`; since 0.14.0 nothing here touches the
|
|
3309
|
+
* host composer or the conversation view.
|
|
3310
|
+
* @module dsh-plugin-lookatstudy/client/views
|
|
3311
|
+
*/
|
|
3312
|
+
/** Small inline error surface for failed write actions (shared with settings). */
|
|
3313
|
+
function ActionError({ error }) {
|
|
3314
|
+
if (error === null) return null;
|
|
3315
|
+
return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Tooltip text for one course-tree status glyph. Pure (the translator is an
|
|
3319
|
+
* optional parameter defaulting to the active one, evaluated per call).
|
|
3320
|
+
*/
|
|
3321
|
+
function statusTitle(kind, status, t = tr) {
|
|
3322
|
+
if (kind === "exam") return t("status.exam");
|
|
3323
|
+
switch (status) {
|
|
3324
|
+
case "mastered": return t("status.mastered");
|
|
3325
|
+
case "in_progress": return t("status.in_progress");
|
|
3326
|
+
case "available": return t("status.available");
|
|
3327
|
+
default: return t("status.locked");
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Extract the last quiz option block (2–4 consecutive `A. …` `B. …` lines,
|
|
3332
|
+
* letters strictly consecutive from A) from an assistant reply — the raw text
|
|
3333
|
+
* of the tutor's question becomes the clickable answer surface. Pure.
|
|
3334
|
+
*/
|
|
3335
|
+
function quizOptions(text) {
|
|
3336
|
+
const runs = [];
|
|
3337
|
+
let run = [];
|
|
3338
|
+
const flush = () => {
|
|
3339
|
+
if (run.length >= 2) runs.push(run);
|
|
3340
|
+
run = [];
|
|
3341
|
+
};
|
|
3342
|
+
for (const line of text.split("\n")) {
|
|
3343
|
+
const plain = /^([A-D])[.、:)]\s*(.+)$/.exec(line.trim());
|
|
3344
|
+
const table = /^\|\s*\*{0,2}([A-D])\*{0,2}\s*\|\s*(.+?)\s*\|$/.exec(line.trim());
|
|
3345
|
+
const match = plain ?? (table !== null ? [
|
|
3346
|
+
table[0],
|
|
3347
|
+
table[1],
|
|
3348
|
+
table[2].replaceAll("**", "").replaceAll("`", "")
|
|
3349
|
+
] : null);
|
|
3350
|
+
if (match === null) {
|
|
3351
|
+
flush();
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
const nextLetter = run.length === 0 ? "A" : String.fromCharCode(run[run.length - 1].letter.charCodeAt(0) + 1);
|
|
3355
|
+
if (match[1] === nextLetter) run.push({
|
|
3356
|
+
letter: match[1],
|
|
3357
|
+
text: match[2]
|
|
3358
|
+
});
|
|
3359
|
+
else {
|
|
3360
|
+
flush();
|
|
3361
|
+
if (match[1] === "A") run.push({
|
|
3362
|
+
letter: match[1],
|
|
3363
|
+
text: match[2]
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
flush();
|
|
3368
|
+
return runs.at(-1) ?? [];
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Merge the rail's two search tracks into the results panel: title hits
|
|
3372
|
+
* (lessons whose name matches — they are also filtered into the tree) first,
|
|
3373
|
+
* then full-text hits not already covered, snippets carried for the body
|
|
3374
|
+
* matches. Pure; empty queries yield no panel.
|
|
3375
|
+
*/
|
|
3376
|
+
function mergeRailSearch(query, lessons, textHits) {
|
|
3377
|
+
const keys = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
3378
|
+
if (keys.length === 0) return [];
|
|
3379
|
+
const rows = [];
|
|
3380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3381
|
+
const matches = (title) => keys.every((k) => title.toLowerCase().includes(k));
|
|
3382
|
+
for (const lesson of lessons) {
|
|
3383
|
+
if (lesson.kind === "exam" || lesson.status === "locked" || !matches(lesson.title)) continue;
|
|
3384
|
+
rows.push({
|
|
3385
|
+
lessonId: lesson.id,
|
|
3386
|
+
title: lesson.title,
|
|
3387
|
+
snippet: "",
|
|
3388
|
+
courseTitle: "",
|
|
3389
|
+
fromTitle: true
|
|
3390
|
+
});
|
|
3391
|
+
seen.add(lesson.id);
|
|
3392
|
+
}
|
|
3393
|
+
for (const hit of textHits) {
|
|
3394
|
+
if (seen.has(hit.lessonId)) continue;
|
|
3395
|
+
rows.push({
|
|
3396
|
+
lessonId: hit.lessonId,
|
|
3397
|
+
title: hit.lessonTitle,
|
|
3398
|
+
snippet: hit.snippet,
|
|
3399
|
+
courseTitle: hit.courseTitle,
|
|
3400
|
+
fromTitle: false
|
|
3401
|
+
});
|
|
3402
|
+
seen.add(hit.lessonId);
|
|
3403
|
+
}
|
|
3404
|
+
return rows.slice(0, 12);
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* A section's effective openness: the user's explicit toggle (persisted per
|
|
3408
|
+
* course) overrides the frontier-based default. Pure.
|
|
3409
|
+
*/
|
|
3410
|
+
function effectiveOpen(sectionTitle, defaultOpen, overrides) {
|
|
3411
|
+
return overrides[sectionTitle] ?? defaultOpen;
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* The narrow-mode pane picker: only valid ids, remembering the last choice
|
|
3415
|
+
* (upstream's narrow switcher semantics). Pure.
|
|
3416
|
+
*/
|
|
3417
|
+
function pickNarrowPane(stored) {
|
|
3418
|
+
return stored === "rail" || stored === "chat" || stored === "note" ? stored : "chat";
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Default expansion for one rail section: collapsed when every study lesson is
|
|
3422
|
+
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
3423
|
+
* stays open; exam nodes never force a section open. Pure.
|
|
3424
|
+
*/
|
|
3425
|
+
function sectionDefaultOpen(section) {
|
|
3426
|
+
return section.lessons.some((l) => l.focus || l.kind !== "exam" && l.status !== "mastered" && l.status !== "locked");
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* C1 sticky-follow: the stream follows new rows only while the reader is
|
|
3430
|
+
* within `tolerance` of the bottom (upstream's 80px rule — scroll up and the
|
|
3431
|
+
* feed stops chasing you). Pure.
|
|
3432
|
+
*/
|
|
3433
|
+
function isStuck(scrollTop, scrollHeight, clientHeight, tolerance = 80) {
|
|
3434
|
+
return scrollHeight - scrollTop - clientHeight <= tolerance;
|
|
3435
|
+
}
|
|
3436
|
+
/**
|
|
3437
|
+
* C12 touch pane swipe: a horizontal flick (>50px, dominance >2x) switches the
|
|
3438
|
+
* narrow pane neighbor; anything else is not a pane gesture. Pure
|
|
3439
|
+
* (upstream swipeTarget semantics).
|
|
3440
|
+
*/
|
|
3441
|
+
function swipePane(pane, dx, dy) {
|
|
3442
|
+
if (Math.abs(dx) < 50 || Math.abs(dx) <= 2 * Math.abs(dy)) return null;
|
|
3443
|
+
const order = [
|
|
3444
|
+
"rail",
|
|
3445
|
+
"chat",
|
|
3446
|
+
"note"
|
|
3447
|
+
];
|
|
3448
|
+
const idx = order.indexOf(pane);
|
|
3449
|
+
if (idx < 0) return null;
|
|
3450
|
+
const next = dx < 0 ? idx + 1 : idx - 1;
|
|
3451
|
+
return next >= 0 && next < order.length ? order[next] : null;
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* C12 selection-popover settle: coarse pointers need a longer quiet window
|
|
3455
|
+
* (upstream's 600/250 two-tier). Pure.
|
|
3456
|
+
*/
|
|
3457
|
+
function settleMs(coarse) {
|
|
3458
|
+
return coarse ? 600 : 250;
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* C9 interleaved review: pick one due lesson at random (Math.random; empty in,
|
|
3462
|
+
* null out). Pure.
|
|
3463
|
+
*/
|
|
3464
|
+
function pickRandomDue(items) {
|
|
3465
|
+
if (items.length === 0) return null;
|
|
3466
|
+
return items[Math.floor(Math.random() * items.length)] ?? null;
|
|
3467
|
+
}
|
|
3468
|
+
/**
|
|
3469
|
+
* Display-side section-title humanizer (0.19 critique round): import-design
|
|
3470
|
+
* section ids arrive as repo-folder slugs ("0-course-setup", "1-Intro") —
|
|
3471
|
+
* strip the leading episode number, hyphen/underscore runs become spaces.
|
|
3472
|
+
* Keys, anchors, and ids stay raw; this is presentation only. Never returns
|
|
3473
|
+
* an empty string (a title that humanizes to nothing keeps its raw form).
|
|
3474
|
+
*/
|
|
3475
|
+
function humanizeSectionTitle(raw) {
|
|
3476
|
+
const t = raw.trim().replace(/^\d+[.\-_ ]+/, "").replace(/[\-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
3477
|
+
return t === "" ? raw.trim() : t;
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Proportion the panel's bar through the host's heuristic composition, in the
|
|
3481
|
+
* segment order system → tools → messages; without a breakdown a single
|
|
3482
|
+
* brand segment carries the whole reading. A zero-width part is dropped.
|
|
3483
|
+
*/
|
|
3484
|
+
function ctxSegments(pct, breakdown) {
|
|
3485
|
+
if (pct <= 0) return [];
|
|
3486
|
+
const total = breakdown === null ? 0 : breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
|
|
3487
|
+
if (total <= 0) return [{
|
|
3488
|
+
key: "total",
|
|
3489
|
+
cls: "",
|
|
3490
|
+
width: pct
|
|
3491
|
+
}];
|
|
3492
|
+
return [
|
|
3493
|
+
["sys", breakdown.systemTokens],
|
|
3494
|
+
["tools", breakdown.toolsTokens],
|
|
3495
|
+
["msgs", breakdown.messageTokens]
|
|
3496
|
+
].map(([key, tokens]) => ({
|
|
3497
|
+
key,
|
|
3498
|
+
cls: key,
|
|
3499
|
+
width: pct * tokens / total
|
|
3500
|
+
})).filter((s) => s.width > 0);
|
|
3501
|
+
}
|
|
3502
|
+
/** Compact token figure for the context panel (host formatTokens arrangement). */
|
|
3503
|
+
function fmtTokens(value) {
|
|
3504
|
+
if (value < 1e3) return String(value);
|
|
3505
|
+
if (value < 1e6) return `${String(Math.round(value / 100) / 10)}K`;
|
|
3506
|
+
return `${String(Math.round(value / 1e5) / 10)}M`;
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Panel-facing error text (0.19 critique round): raw err.message (host RPC
|
|
3510
|
+
* internals like "prompt rejected: …") never renders in the DOM — the user
|
|
3511
|
+
* gets one honest locale line, the raw error goes to the console.
|
|
3512
|
+
*/
|
|
3513
|
+
function friendlyError(err, kind = "action", t = tr) {
|
|
3514
|
+
console.error("[lookatstudy]", err);
|
|
3515
|
+
return t(kind === "send" ? "error.send" : "error.action");
|
|
3516
|
+
}
|
|
3517
|
+
//#endregion
|
|
3294
3518
|
//#region src/vendor/cmap-elk-layout.ts
|
|
3295
3519
|
/** 字符宽度估算:CJK 全宽,ASCII 约 0.58em,其余(BBOP…)同 ASCII。 */
|
|
3296
3520
|
function estTextWidth(text, fontSize = 13) {
|
|
@@ -5562,191 +5786,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5562
5786
|
}
|
|
5563
5787
|
}
|
|
5564
5788
|
//#endregion
|
|
5565
|
-
//#region src/client/companion.tsx
|
|
5566
|
-
/**
|
|
5567
|
-
* The companion creature — the upstream 伴学生物's minimal honest DOM port
|
|
5568
|
-
* (0.15.0 P7): a small being in the panel's bottom-right corner with five
|
|
5569
|
-
* forms, a pure mood machine (idle / talking / celebrating / encouraging),
|
|
5570
|
-
* and the a11y iron rule carried over — reduced-motion renders it static.
|
|
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
|
|
5574
|
-
*/
|
|
5575
|
-
const COMPANION_FORMS = [
|
|
5576
|
-
{
|
|
5577
|
-
id: "ember",
|
|
5578
|
-
labelKey: "companion.form.ember",
|
|
5579
|
-
body: "#f59e0b",
|
|
5580
|
-
accent: "#dc2626"
|
|
5581
|
-
},
|
|
5582
|
-
{
|
|
5583
|
-
id: "frost",
|
|
5584
|
-
labelKey: "companion.form.frost",
|
|
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"
|
|
5605
|
-
}
|
|
5606
|
-
];
|
|
5607
|
-
/** Normalize a stored form id; junk falls back to 小焰 (upstream's default). */
|
|
5608
|
-
function normalizeCompanionForm(stored) {
|
|
5609
|
-
return COMPANION_FORMS.some((f) => f.id === stored) ? stored : "ember";
|
|
5610
|
-
}
|
|
5611
|
-
function storedCompanionForm() {
|
|
5612
|
-
try {
|
|
5613
|
-
return normalizeCompanionForm(localStorage.getItem("dsh-plugin-lookatstudy:companion-form"));
|
|
5614
|
-
} catch {
|
|
5615
|
-
return "ember";
|
|
5616
|
-
}
|
|
5617
|
-
}
|
|
5618
|
-
function storeCompanionForm(id) {
|
|
5619
|
-
try {
|
|
5620
|
-
localStorage.setItem("dsh-plugin-lookatstudy:companion-form", id);
|
|
5621
|
-
} catch {}
|
|
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
5789
|
//#region src/client/highlights.ts
|
|
5751
5790
|
/**
|
|
5752
5791
|
* Locate a selection inside the model text: exact `selected` text search,
|
|
@@ -5874,191 +5913,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5874
5913
|
return applied;
|
|
5875
5914
|
}
|
|
5876
5915
|
//#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
5916
|
//#region src/client/maprail.tsx
|
|
6063
5917
|
/**
|
|
6064
5918
|
* RailList — the course rail as a quiet list (owner decision 2026-09-08: the
|
|
@@ -6173,6 +6027,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6173
6027
|
y
|
|
6174
6028
|
});
|
|
6175
6029
|
};
|
|
6030
|
+
const panelZoom = () => {
|
|
6031
|
+
const z = parseFloat(document.querySelector(".lks-ui")?.style.zoom ?? "1");
|
|
6032
|
+
return Number.isFinite(z) && z > 0 ? z : 1;
|
|
6033
|
+
};
|
|
6176
6034
|
const onMove = (e) => {
|
|
6177
6035
|
if (coarse) return;
|
|
6178
6036
|
const el = target(e);
|
|
@@ -6180,7 +6038,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6180
6038
|
setTip(null);
|
|
6181
6039
|
return;
|
|
6182
6040
|
}
|
|
6183
|
-
|
|
6041
|
+
const z = panelZoom();
|
|
6042
|
+
show(el, (e.clientX + 10) / z, (e.clientY + 12) / z);
|
|
6184
6043
|
};
|
|
6185
6044
|
const onLeave = () => {
|
|
6186
6045
|
setTip(null);
|
|
@@ -6197,7 +6056,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6197
6056
|
pressTimer.current = setTimeout(() => {
|
|
6198
6057
|
if (Math.hypot(e.clientX - start.x, e.clientY - start.y) < 10) {
|
|
6199
6058
|
const r = el.getBoundingClientRect();
|
|
6200
|
-
|
|
6059
|
+
const z = panelZoom();
|
|
6060
|
+
show(el, (r.left + r.width / 2 - 80) / z, (r.top - 36) / z);
|
|
6201
6061
|
}
|
|
6202
6062
|
}, 500);
|
|
6203
6063
|
};
|
|
@@ -6225,12 +6085,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6225
6085
|
};
|
|
6226
6086
|
}, []);
|
|
6227
6087
|
if (tip === null) return null;
|
|
6088
|
+
const z = parseFloat(document.querySelector(".lks-ui")?.style.zoom ?? "1") || 1;
|
|
6089
|
+
const clamped = clampTip(tip.x, tip.y, 200, 40, window.innerWidth / z, window.innerHeight / z);
|
|
6228
6090
|
return (0, react.createElement)("div", {
|
|
6229
6091
|
className: "lks-tip",
|
|
6230
6092
|
role: "tooltip",
|
|
6231
6093
|
style: {
|
|
6232
|
-
left: `${String(
|
|
6233
|
-
top: `${String(
|
|
6094
|
+
left: `${String(clamped.x)}px`,
|
|
6095
|
+
top: `${String(clamped.y)}px`
|
|
6234
6096
|
}
|
|
6235
6097
|
}, tip.text);
|
|
6236
6098
|
}
|
|
@@ -6757,7 +6619,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6757
6619
|
* transition here — the client bundle stays dependency-free.)
|
|
6758
6620
|
* @module dsh-plugin-lookatstudy/client/celebration-layer
|
|
6759
6621
|
*/
|
|
6760
|
-
/** prefers-reduced-motion, live-updating
|
|
6622
|
+
/** prefers-reduced-motion, live-updating. */
|
|
6761
6623
|
function usePrefersReducedMotion() {
|
|
6762
6624
|
const [reduced, setReduced] = (0, react.useState)(() => typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
6763
6625
|
(0, react.useEffect)(() => {
|
|
@@ -7097,24 +6959,39 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7097
6959
|
}, []);
|
|
7098
6960
|
const [rows, setRows] = (0, react.useState)([]);
|
|
7099
6961
|
const [pressure, setPressure] = (0, react.useState)(null);
|
|
6962
|
+
const [breakdown, setBreakdown] = (0, react.useState)(null);
|
|
7100
6963
|
const importProgress = (0, react.useMemo)(() => importProgressOf(rows), [rows]);
|
|
7101
6964
|
const contextMeter = (0, react.useMemo)(() => {
|
|
7102
6965
|
if (pressure !== null && pressure.contextWindow !== void 0 && pressure.contextWindow > 0 && pressure.projectedTokens !== void 0) return {
|
|
7103
6966
|
pct: Math.min(100, Math.round(pressure.projectedTokens / pressure.contextWindow * 100)),
|
|
7104
|
-
|
|
7105
|
-
|
|
6967
|
+
estimated: false,
|
|
6968
|
+
used: pressure.projectedTokens,
|
|
6969
|
+
window: pressure.contextWindow
|
|
6970
|
+
};
|
|
6971
|
+
const est = Math.round(rows.reduce((n, r) => n + r.text.length, 0) / 2.2);
|
|
6972
|
+
const window = data?.model?.contextWindow ?? null;
|
|
6973
|
+
if (window !== null && window > 0) return {
|
|
6974
|
+
pct: Math.min(100, Math.round(est / window * 100)),
|
|
6975
|
+
estimated: true,
|
|
6976
|
+
used: est,
|
|
6977
|
+
window
|
|
7106
6978
|
};
|
|
7107
|
-
const chars = rows.reduce((n, r) => n + r.text.length, 0);
|
|
7108
6979
|
return {
|
|
7109
6980
|
pct: null,
|
|
7110
|
-
|
|
7111
|
-
|
|
6981
|
+
estimated: true,
|
|
6982
|
+
used: est,
|
|
6983
|
+
window: null
|
|
7112
6984
|
};
|
|
7113
|
-
}, [
|
|
6985
|
+
}, [
|
|
6986
|
+
pressure,
|
|
6987
|
+
rows,
|
|
6988
|
+
data?.model
|
|
6989
|
+
]);
|
|
7114
6990
|
const [feedAttached, setFeedAttached] = (0, react.useState)(false);
|
|
7115
6991
|
const [draft, setDraft] = (0, react.useState)("");
|
|
7116
6992
|
const [narrowPane, setNarrowPane] = (0, react.useState)(pickNarrowPane(null));
|
|
7117
6993
|
const lesson = data?.lesson ?? null;
|
|
6994
|
+
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
6995
|
const localBound = (0, react.useRef)(null);
|
|
7119
6996
|
const streamEl = (0, react.useRef)(null);
|
|
7120
6997
|
const touchStart = (0, react.useRef)(null);
|
|
@@ -7187,14 +7064,25 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7187
7064
|
let off;
|
|
7188
7065
|
try {
|
|
7189
7066
|
const actx = ctx.sessions.scope(boundId);
|
|
7190
|
-
const
|
|
7067
|
+
const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
|
|
7068
|
+
const proj = face?.projections?.faceOf("contextPressure");
|
|
7069
|
+
const bproj = face?.projections?.faceOf("contextBreakdown");
|
|
7191
7070
|
if (proj !== void 0) {
|
|
7192
7071
|
const read = () => {
|
|
7072
|
+
if (disposed) return;
|
|
7193
7073
|
const snap = proj.getSnapshot();
|
|
7194
|
-
if (
|
|
7074
|
+
if (snap !== void 0 && typeof snap === "object") setPressure({
|
|
7195
7075
|
projectedTokens: snap.projectedTokens,
|
|
7196
7076
|
contextWindow: snap.contextWindow
|
|
7197
7077
|
});
|
|
7078
|
+
if (bproj !== void 0) {
|
|
7079
|
+
const bsnap = bproj.getSnapshot();
|
|
7080
|
+
if (bsnap !== void 0 && typeof bsnap === "object") setBreakdown({
|
|
7081
|
+
systemTokens: bsnap.systemTokens ?? 0,
|
|
7082
|
+
toolsTokens: bsnap.toolsTokens ?? 0,
|
|
7083
|
+
messageTokens: bsnap.messageTokens ?? 0
|
|
7084
|
+
});
|
|
7085
|
+
}
|
|
7198
7086
|
};
|
|
7199
7087
|
read();
|
|
7200
7088
|
off = proj.subscribe(read);
|
|
@@ -7361,8 +7249,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7361
7249
|
window.removeEventListener("lookatstudy-exam-generate", onGenerate);
|
|
7362
7250
|
};
|
|
7363
7251
|
}, [send]);
|
|
7364
|
-
const companion = useCompanionMood();
|
|
7365
|
-
const setCompanionEvent = companion.emit;
|
|
7366
7252
|
const progress = data?.progress ?? null;
|
|
7367
7253
|
const body = (0, react.createElement)("div", {
|
|
7368
7254
|
className: "lks14-body",
|
|
@@ -7392,9 +7278,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7392
7278
|
onJumped: () => {
|
|
7393
7279
|
setNarrowPane("chat");
|
|
7394
7280
|
},
|
|
7395
|
-
onBlankTap: () => {
|
|
7396
|
-
setCompanionEvent("poke");
|
|
7397
|
-
},
|
|
7398
7281
|
streamingLessonId: feedGen && lesson !== null ? lesson.lessonId : null,
|
|
7399
7282
|
turnActive: feedGen,
|
|
7400
7283
|
importProgress,
|
|
@@ -7429,7 +7312,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7429
7312
|
onClick: () => {
|
|
7430
7313
|
setZoom(zoomTier + .1);
|
|
7431
7314
|
}
|
|
7432
|
-
}, "A+"))), (0, react.createElement)("div", { className: "lks14-row" }, (0, react.createElement)(
|
|
7315
|
+
}, "A+"))), (0, react.createElement)("div", { className: "lks14-row" }, examLessonActive ? (0, react.createElement)("div", { className: "lks14-col lks14-chat" }, (0, react.createElement)(ExamView, {
|
|
7316
|
+
lessonId: lesson.lessonId,
|
|
7317
|
+
sectionTitle: lesson.sectionTitle,
|
|
7318
|
+
paused: examLeave !== null,
|
|
7319
|
+
onSessionChange: onExamSession
|
|
7320
|
+
})) : (0, react.createElement)(ChatPane, {
|
|
7433
7321
|
data,
|
|
7434
7322
|
lesson,
|
|
7435
7323
|
rows,
|
|
@@ -7443,20 +7331,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7443
7331
|
stop,
|
|
7444
7332
|
setMode,
|
|
7445
7333
|
narrowPane,
|
|
7446
|
-
companionEvent: setCompanionEvent,
|
|
7447
7334
|
onThreadJump: (id) => {
|
|
7448
7335
|
setNarrowPane("chat");
|
|
7449
7336
|
guardedSetFocus(id);
|
|
7450
7337
|
},
|
|
7451
7338
|
contextMeter,
|
|
7339
|
+
contextBreakdown: breakdown,
|
|
7452
7340
|
uploadAttachment
|
|
7453
7341
|
}), (0, react.createElement)(NotebookPane, {
|
|
7454
7342
|
data,
|
|
7455
7343
|
deleteNote,
|
|
7456
|
-
send
|
|
7457
|
-
companionEvent: setCompanionEvent,
|
|
7458
|
-
onExamSession,
|
|
7459
|
-
examPaused: examLeave !== null
|
|
7344
|
+
send
|
|
7460
7345
|
}))));
|
|
7461
7346
|
return (0, react.createElement)("div", {
|
|
7462
7347
|
className: "lks14 lks-ui",
|
|
@@ -7490,13 +7375,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7490
7375
|
setNarrowPane("chat");
|
|
7491
7376
|
send(tr("prompt.start"));
|
|
7492
7377
|
}
|
|
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" }, ...[
|
|
7378
|
+
}) : null, (0, react.createElement)("div", { className: "lks14-switch" }, ...[
|
|
7500
7379
|
"rail",
|
|
7501
7380
|
"chat",
|
|
7502
7381
|
"note"
|
|
@@ -7764,7 +7643,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7764
7643
|
PANEL_SHELL.current = shell;
|
|
7765
7644
|
}
|
|
7766
7645
|
/** 左栏:course picker, tree, review box, import. */
|
|
7767
|
-
function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send, onJumped,
|
|
7646
|
+
function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send, onJumped, streamingLessonId, turnActive, importProgress, stop, paletteBus }) {
|
|
7768
7647
|
const [selectedCourse, setSelectedCourse] = (0, react.useState)("");
|
|
7769
7648
|
const [query, setQuery] = (0, react.useState)("");
|
|
7770
7649
|
const [searchRows, setSearchRows] = (0, react.useState)([]);
|
|
@@ -7835,7 +7714,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7835
7714
|
turnActive
|
|
7836
7715
|
]);
|
|
7837
7716
|
const course = data?.courses.find((c) => c.courseId === courseId) ?? null;
|
|
7838
|
-
const blankDown = (0, react.useRef)(null);
|
|
7839
7717
|
(0, react.useEffect)(() => {
|
|
7840
7718
|
if (optFocus !== null && data?.focusLessonId === optFocus) setOptFocus(null);
|
|
7841
7719
|
}, [data?.focusLessonId, optFocus]);
|
|
@@ -7955,20 +7833,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7955
7833
|
}, (0, react.createElement)(IconWrenchOutline16, { size: 13 }), tr("map.world.practice"))) : null)) : railtabs);
|
|
7956
7834
|
const mapPane = data === null ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("loading")) : course === null ? null : (0, react.createElement)("div", {
|
|
7957
7835
|
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
|
-
}
|
|
7836
|
+
ref: scrollEl
|
|
7972
7837
|
}, (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
7838
|
const examAllowed = examOpen(section.lessons);
|
|
7974
7839
|
const lessons = section.lessons.filter((l) => query.trim() === "" || titleMatches(l.title, query) || l.focus);
|
|
@@ -8375,9 +8240,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8375
8240
|
return cut <= 0 ? path : norm.slice(0, cut);
|
|
8376
8241
|
}
|
|
8377
8242
|
/** 中栏: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,
|
|
8243
|
+
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, stop, setMode, narrowPane, onThreadJump, contextMeter, contextBreakdown, uploadAttachment }) {
|
|
8379
8244
|
const stuck = (0, react.useRef)(true);
|
|
8380
8245
|
const [showFab, setShowFab] = (0, react.useState)(false);
|
|
8246
|
+
const [threadsOpen, setThreadsOpen] = (0, react.useState)(false);
|
|
8381
8247
|
const onStreamScroll = () => {
|
|
8382
8248
|
const el = streamEl.current;
|
|
8383
8249
|
if (el === null) return;
|
|
@@ -8402,6 +8268,37 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8402
8268
|
window.removeEventListener("keydown", onKey);
|
|
8403
8269
|
};
|
|
8404
8270
|
}, [busy, stop]);
|
|
8271
|
+
(0, react.useEffect)(() => {
|
|
8272
|
+
if (!threadsOpen) return;
|
|
8273
|
+
const onKey = (e) => {
|
|
8274
|
+
if (e.key === "Escape") setThreadsOpen(false);
|
|
8275
|
+
};
|
|
8276
|
+
const onDown = (e) => {
|
|
8277
|
+
if (e.target.closest(".lks14-threadswitch") === null) setThreadsOpen(false);
|
|
8278
|
+
};
|
|
8279
|
+
window.addEventListener("keydown", onKey);
|
|
8280
|
+
document.addEventListener("pointerdown", onDown);
|
|
8281
|
+
return () => {
|
|
8282
|
+
window.removeEventListener("keydown", onKey);
|
|
8283
|
+
document.removeEventListener("pointerdown", onDown);
|
|
8284
|
+
};
|
|
8285
|
+
}, [threadsOpen]);
|
|
8286
|
+
const [ctxOpen, setCtxOpen] = (0, react.useState)(false);
|
|
8287
|
+
(0, react.useEffect)(() => {
|
|
8288
|
+
if (!ctxOpen) return;
|
|
8289
|
+
const onKey = (e) => {
|
|
8290
|
+
if (e.key === "Escape") setCtxOpen(false);
|
|
8291
|
+
};
|
|
8292
|
+
const onDown = (e) => {
|
|
8293
|
+
if (e.target.closest(".lks14-ctxring-wrap") === null) setCtxOpen(false);
|
|
8294
|
+
};
|
|
8295
|
+
window.addEventListener("keydown", onKey);
|
|
8296
|
+
document.addEventListener("pointerdown", onDown);
|
|
8297
|
+
return () => {
|
|
8298
|
+
window.removeEventListener("keydown", onKey);
|
|
8299
|
+
document.removeEventListener("pointerdown", onDown);
|
|
8300
|
+
};
|
|
8301
|
+
}, [ctxOpen]);
|
|
8405
8302
|
const { tts } = useStudy();
|
|
8406
8303
|
const [msgAudio, setMsgAudio] = (0, react.useState)(null);
|
|
8407
8304
|
const msgReadCtl = (0, react.useRef)(null);
|
|
@@ -8441,7 +8338,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8441
8338
|
if (el === null) return;
|
|
8442
8339
|
const sentences = speechSentencesOf((el.textContent ?? "").trim());
|
|
8443
8340
|
if (sentences.length === 0) return;
|
|
8444
|
-
companionEvent("talk-start");
|
|
8445
8341
|
msgReadCtl.current?.stop();
|
|
8446
8342
|
const voice = storedTtsVoice();
|
|
8447
8343
|
const prefetch = /* @__PURE__ */ new Map();
|
|
@@ -8522,7 +8418,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8522
8418
|
});
|
|
8523
8419
|
};
|
|
8524
8420
|
const stopMessage = () => {
|
|
8525
|
-
companionEvent("talk-end");
|
|
8526
8421
|
msgReadCtl.current?.stop();
|
|
8527
8422
|
setMsgAudio(null);
|
|
8528
8423
|
document.querySelectorAll("mark.lks-reading").forEach((m) => {
|
|
@@ -8550,9 +8445,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8550
8445
|
data: artifact.data,
|
|
8551
8446
|
masteryPct: lesson.masteryPct,
|
|
8552
8447
|
send,
|
|
8553
|
-
onFinished: (
|
|
8554
|
-
companionEvent(allCorrect ? "celebrate" : "encourage");
|
|
8555
|
-
}
|
|
8448
|
+
onFinished: () => {}
|
|
8556
8449
|
}) : (0, react.createElement)(ArtifactCard, {
|
|
8557
8450
|
artifact,
|
|
8558
8451
|
send
|
|
@@ -8650,20 +8543,29 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8650
8543
|
}));
|
|
8651
8544
|
send(tr("proposal.decline.msg", { lesson: proposal.lessonTitle }));
|
|
8652
8545
|
}
|
|
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
|
-
"
|
|
8546
|
+
}, 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", {
|
|
8547
|
+
className: "lks14-threadchip",
|
|
8548
|
+
"aria-expanded": String(threadsOpen),
|
|
8549
|
+
"data-tooltip": tr("threads.label"),
|
|
8550
|
+
onClick: () => {
|
|
8551
|
+
setThreadsOpen((v) => !v);
|
|
8552
|
+
}
|
|
8553
|
+
}, tr("threads.open", { n: String(threadPills.length) }), (0, react.createElement)("span", { className: "lks14-threadchip-caret" }, threadsOpen ? "▾" : "▸")), threadsOpen ? (0, react.createElement)("div", {
|
|
8554
|
+
className: "lks14-threadmenu",
|
|
8555
|
+
role: "menu"
|
|
8657
8556
|
}, ...threadPills.map((p) => (0, react.createElement)("button", {
|
|
8658
8557
|
key: p.id,
|
|
8659
|
-
className: `lks14-
|
|
8660
|
-
role: "
|
|
8661
|
-
"aria-
|
|
8662
|
-
"data-tooltip": p.title,
|
|
8558
|
+
className: `lks14-threadmenu-row${p.current ? " on" : ""}`,
|
|
8559
|
+
role: "menuitem",
|
|
8560
|
+
"aria-current": p.current ? "true" : void 0,
|
|
8663
8561
|
onClick: () => {
|
|
8562
|
+
setThreadsOpen(false);
|
|
8664
8563
|
if (!p.current) onThreadJump(p.id);
|
|
8665
8564
|
}
|
|
8666
|
-
},
|
|
8565
|
+
}, (0, react.createElement)("span", {
|
|
8566
|
+
className: "lks14-threadmenu-dot",
|
|
8567
|
+
"aria-hidden": "true"
|
|
8568
|
+
}), (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
8569
|
className: "lks14-stream",
|
|
8668
8570
|
onScroll: onStreamScroll,
|
|
8669
8571
|
ref: streamEl,
|
|
@@ -8687,9 +8589,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8687
8589
|
data: a.data,
|
|
8688
8590
|
masteryPct: lesson.masteryPct,
|
|
8689
8591
|
send,
|
|
8690
|
-
onFinished: (
|
|
8691
|
-
companionEvent(allCorrect ? "celebrate" : "encourage");
|
|
8692
|
-
}
|
|
8592
|
+
onFinished: () => {}
|
|
8693
8593
|
})), ...backlog.filter((a) => a.artifactType !== "quiz").map((a) => (0, react.createElement)(ArtifactCard, {
|
|
8694
8594
|
key: a.id,
|
|
8695
8595
|
artifact: a,
|
|
@@ -8703,14 +8603,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8703
8603
|
if (!dormant) send(s.message);
|
|
8704
8604
|
}
|
|
8705
8605
|
}, 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
8606
|
className: "lks14-composer-card",
|
|
8715
8607
|
onDragOver: (e) => {
|
|
8716
8608
|
if (e.dataTransfer?.types.includes("Files") === true) e.preventDefault();
|
|
@@ -8765,7 +8657,55 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8765
8657
|
if (file !== void 0 && file !== null) attachFile(file);
|
|
8766
8658
|
e.target.value = "";
|
|
8767
8659
|
}
|
|
8768
|
-
}),
|
|
8660
|
+
}), contextMeter.pct !== null ? (0, react.createElement)("span", {
|
|
8661
|
+
className: "lks14-ctxring-wrap",
|
|
8662
|
+
key: "ctxring"
|
|
8663
|
+
}, (0, react.createElement)("button", {
|
|
8664
|
+
className: "lks14-ctxring",
|
|
8665
|
+
"data-testid": "context-meter",
|
|
8666
|
+
"data-estimated": String(contextMeter.estimated),
|
|
8667
|
+
"aria-label": tr("meter.aria", { pct: String(contextMeter.pct) }),
|
|
8668
|
+
"data-tooltip": tr("meter.aria", { pct: String(contextMeter.pct) }),
|
|
8669
|
+
"aria-haspopup": "dialog",
|
|
8670
|
+
"aria-expanded": String(ctxOpen),
|
|
8671
|
+
onClick: () => {
|
|
8672
|
+
setCtxOpen((v) => !v);
|
|
8673
|
+
}
|
|
8674
|
+
}, (0, react.createElement)("svg", {
|
|
8675
|
+
viewBox: "0 0 14 14",
|
|
8676
|
+
width: 14,
|
|
8677
|
+
height: 14,
|
|
8678
|
+
"aria-hidden": "true"
|
|
8679
|
+
}, (0, react.createElement)("circle", {
|
|
8680
|
+
className: "track",
|
|
8681
|
+
cx: 7,
|
|
8682
|
+
cy: 7,
|
|
8683
|
+
r: 5.5
|
|
8684
|
+
}), (0, react.createElement)("circle", {
|
|
8685
|
+
className: `fill${contextMeter.pct > 80 ? " hot" : ""}`,
|
|
8686
|
+
cx: 7,
|
|
8687
|
+
cy: 7,
|
|
8688
|
+
r: 5.5,
|
|
8689
|
+
strokeDasharray: `${String(2 * Math.PI * 5.5 * contextMeter.pct / 100)} ${String(2 * Math.PI * 5.5)}`,
|
|
8690
|
+
transform: "rotate(-90 7 7)"
|
|
8691
|
+
}))), ctxOpen ? (0, react.createElement)("div", {
|
|
8692
|
+
className: "lks14-ctxpanel",
|
|
8693
|
+
role: "dialog",
|
|
8694
|
+
"aria-label": tr("meter.label")
|
|
8695
|
+
}, (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", {
|
|
8696
|
+
key: s.key,
|
|
8697
|
+
className: s.cls,
|
|
8698
|
+
style: { width: `${String(s.width)}%` }
|
|
8699
|
+
}))), contextBreakdown !== null ? (0, react.createElement)("div", { className: "lks14-ctxpanel-rows" }, (0, react.createElement)("div", { className: "lks14-ctxpanel-row" }, (0, react.createElement)("span", {
|
|
8700
|
+
className: "swatch sys",
|
|
8701
|
+
"aria-hidden": "true"
|
|
8702
|
+
}), tr("meter.brk.system"), (0, react.createElement)("b", null, `~${fmtTokens(contextBreakdown.systemTokens)}`)), (0, react.createElement)("div", { className: "lks14-ctxpanel-row" }, (0, react.createElement)("span", {
|
|
8703
|
+
className: "swatch tools",
|
|
8704
|
+
"aria-hidden": "true"
|
|
8705
|
+
}), tr("meter.brk.tools"), (0, react.createElement)("b", null, `~${fmtTokens(contextBreakdown.toolsTokens)}`)), (0, react.createElement)("div", { className: "lks14-ctxpanel-row" }, (0, react.createElement)("span", {
|
|
8706
|
+
className: "swatch msgs",
|
|
8707
|
+
"aria-hidden": "true"
|
|
8708
|
+
}), 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
8709
|
className: "lks-btn-send stop",
|
|
8770
8710
|
"aria-label": tr("chat.stop"),
|
|
8771
8711
|
"data-tooltip": tr("chat.stop"),
|
|
@@ -8790,7 +8730,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8790
8730
|
}, "▾") : null, sendError !== null || error !== null ? (0, react.createElement)("div", { className: "lks-propcard-err" }, sendError ?? error) : null));
|
|
8791
8731
|
}
|
|
8792
8732
|
/** 右栏:the notebook — 讲解/概念图/笔记 tabs (upstream NotebookPanel arrangement). */
|
|
8793
|
-
function NotebookPane({ data, deleteNote, send
|
|
8733
|
+
function NotebookPane({ data, deleteNote, send }) {
|
|
8794
8734
|
const lesson = data?.lesson ?? null;
|
|
8795
8735
|
const [tab, setTab] = (0, react.useState)("teach");
|
|
8796
8736
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -8824,18 +8764,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8824
8764
|
setError(friendlyError(err, "action"));
|
|
8825
8765
|
});
|
|
8826
8766
|
};
|
|
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
8767
|
const artifacts = lesson?.artifacts ?? [];
|
|
8840
8768
|
const [, setUnseenTick] = (0, react.useState)(0);
|
|
8841
8769
|
const unseen = unseenArtifacts(lesson?.lessonId ?? "", artifacts);
|
|
@@ -8923,7 +8851,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
8923
8851
|
const [rateBusy, setRateBusy] = (0, react.useState)(false);
|
|
8924
8852
|
const startReading = () => {
|
|
8925
8853
|
if (lesson === null || lesson.speechText.trim() === "") return;
|
|
8926
|
-
companionEvent("talk-start");
|
|
8927
8854
|
readCtl.current?.stop();
|
|
8928
8855
|
const sentences = speechSentencesOf(lesson.speechText);
|
|
8929
8856
|
const voice = storedTtsVoice();
|
|
@@ -9004,7 +8931,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9004
8931
|
});
|
|
9005
8932
|
};
|
|
9006
8933
|
const stopReading = () => {
|
|
9007
|
-
companionEvent("talk-end");
|
|
9008
8934
|
readCtl.current?.stop();
|
|
9009
8935
|
setRead(null);
|
|
9010
8936
|
};
|
|
@@ -9079,7 +9005,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9079
9005
|
onClick: () => {
|
|
9080
9006
|
setRateBusy(true);
|
|
9081
9007
|
recordReview(lesson.lessonId, Number(q)).then(() => {
|
|
9082
|
-
companionEvent(Number(q) >= 4 ? "celebrate" : "encourage");
|
|
9083
9008
|
const rateEl = document.querySelector(`[data-lks-rate="${lesson.lessonId}"]`);
|
|
9084
9009
|
const rr = Number(q) >= 4 && rateEl !== null ? rateEl.getBoundingClientRect() : null;
|
|
9085
9010
|
celebrate(Number(q) >= 4 ? "correct" : "wrong", rr !== null ? { origin: {
|
|
@@ -9307,7 +9232,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9307
9232
|
dangerouslySetInnerHTML: { __html: renderMarkdown(n.text) }
|
|
9308
9233
|
})), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `'${n.quote}'`) : null)) : []);
|
|
9309
9234
|
})));
|
|
9310
|
-
return (0, react.createElement)("div", { className: "lks14-col lks14-note" },
|
|
9235
|
+
return (0, react.createElement)("div", { className: "lks14-col lks14-note" }, body, confirmNoteDel !== null ? (0, react.createElement)(ConfirmCard, {
|
|
9311
9236
|
anchor: confirmNoteDel.rect,
|
|
9312
9237
|
message: tr("note.delete.confirm"),
|
|
9313
9238
|
danger: true,
|
|
@@ -9575,7 +9500,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9575
9500
|
const { data, setMode, activate, setHistoryBudget } = useStudy();
|
|
9576
9501
|
const [error, setError] = (0, react.useState)(null);
|
|
9577
9502
|
const [voice, setVoice] = (0, react.useState)(storedTtsVoice);
|
|
9578
|
-
const [companion, setCompanion] = (0, react.useState)(storedCompanionForm);
|
|
9579
9503
|
const fire = (action) => {
|
|
9580
9504
|
action.then(() => {
|
|
9581
9505
|
setError(null);
|
|
@@ -9602,17 +9526,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9602
9526
|
onClick: () => {
|
|
9603
9527
|
if (data !== null) fire(setHistoryBudget(!(data.historyBudget === true)));
|
|
9604
9528
|
}
|
|
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", {
|
|
9529
|
+
}, 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
9530
|
className: "lks-set-select",
|
|
9617
9531
|
value: voice,
|
|
9618
9532
|
onChange: (e) => {
|