dsh-plugin-lookatstudy 0.14.1 → 0.16.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 +5 -4
- package/lib/client.js +2123 -100
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +1149 -283
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -4,10 +4,313 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __exportAll = (all, no_symbols) => {
|
|
10
|
+
let target = {};
|
|
11
|
+
for (var name in all) __defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
+
return target;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
7
19
|
let react = require("react");
|
|
8
20
|
let react_dom_client = require("react-dom/client");
|
|
21
|
+
//#region src/client/upstream-theme.ts
|
|
22
|
+
/**
|
|
23
|
+
* The upstream LookatStudy skin (0.16.0 P9a) — the 1:1 visual port. Upstream
|
|
24
|
+
* is a Duolingo-style dark-first design system defined in its tailwind.config
|
|
25
|
+
* + index.css :root; this module carries those EXACT tokens (scoped to the
|
|
26
|
+
* panel root `.lks-ui`) and redefines the panel's class vocabulary to match
|
|
27
|
+
* upstream's components: the three-column surface ladder (rail darkest / chat
|
|
28
|
+
* surface-1 / notebook surface-2, no borders — depth by color step), the
|
|
29
|
+
* floating blur app-header, 3D push-down buttons, the claude.ai-style
|
|
30
|
+
* composer capsule, chat bubbles, typing dots, and the toast capsules.
|
|
31
|
+
* Host-chrome surfaces (dock pill, settings, toolviews) keep the dsw tokens —
|
|
32
|
+
* only the study panel wears the upstream skin.
|
|
33
|
+
* @module dsh-plugin-lookatstudy/client/upstream-theme
|
|
34
|
+
*/
|
|
35
|
+
const UPSTREAM_CSS = `
|
|
36
|
+
/* ── upstream design tokens (index.css :root, verbatim values) ── */
|
|
37
|
+
.lks-ui{
|
|
38
|
+
--brand:#58CC01;--brand-rgb:88 204 2;--brand-dark:#46A302;--brand-light:#7ED957;
|
|
39
|
+
--brand-glow:#58CC0159;--brand-ring:#58CC0126;
|
|
40
|
+
--accent:#1DB0F6;--accent-rgb:28 176 246;--accent-dark:#098CDC;--accent-light:#65C6FF;
|
|
41
|
+
--accent-glow:#1DB0F64C;--accent-ring:#1DB0F626;
|
|
42
|
+
--gold:#FFC801;--gold-rgb:255 200 0;--gold-dark:#D59800;--gold-light:#FFE294;
|
|
43
|
+
--gold-glow:#FFC80173;--gold-ring:#FFC80133;
|
|
44
|
+
--warning:#FF4B4B;--warning-rgb:255 75 75;--warning-dark:#D01C29;--warning-light:#FF7E77;
|
|
45
|
+
--warning-glow:#FF4B4B4C;--warning-ring:#FF4B4B26;--warning-tint:#FF4B4B1A;--warning-tint-border:#FF4B4B40;
|
|
46
|
+
--review:#FA6E1D;--review-rgb:255 122 26;--review-glow:#FA6E1D26;--review-tint:#FA6E1D1A;
|
|
47
|
+
--exam:#A855F7;--exam-rgb:168 85 247;--exam-dark:#7E22CE;--exam-light:#C084FC;--exam-locked:#524569;
|
|
48
|
+
--exam-glow:#A855F759;--exam-ring:#A855F726;
|
|
49
|
+
--surface-rail:#08090B;--surface-rail-rgb:8 9 11;
|
|
50
|
+
--surface-0:#0C0D0F;--surface-0-rgb:12 13 15;
|
|
51
|
+
--surface-1:#111114;--surface-1-rgb:17 17 20;
|
|
52
|
+
--surface-2:#1A1A1D;--surface-2-rgb:26 26 29;
|
|
53
|
+
--surface-3:#2A2B2E;--surface-3-rgb:42 43 46;
|
|
54
|
+
--ink:#F1F2F4;--ink-rgb:245 245 250;
|
|
55
|
+
--ink-strong:#FAFAFA;--ink-strong-rgb:250 250 250;
|
|
56
|
+
--ink-muted:#9D9EA2;--ink-muted-rgb:166 166 176;
|
|
57
|
+
--ink-faint:#88898C;--ink-faint-rgb:136 137 140;
|
|
58
|
+
--border:#252629;--border-rgb:37 38 41;--border-faint:#1A1A1D;--border-faint-rgb:26 26 29;
|
|
59
|
+
--shadow-rgb:0 0 0;--inner-highlight:rgb(255 255 255/0.55);
|
|
60
|
+
--ease-out-expo:cubic-bezier(0.16,1,0.3,1);
|
|
61
|
+
--ease-out-back:cubic-bezier(0.34,1.2,0.64,1);
|
|
62
|
+
--ease-spring:cubic-bezier(0.34,1.56,0.64,1);
|
|
63
|
+
--ease-out-quart:cubic-bezier(0.25,1,0.5,1);
|
|
64
|
+
font-family:'DIN Round',system-ui,-apple-system,sans-serif;
|
|
65
|
+
background:var(--surface-1);color:var(--ink);
|
|
66
|
+
font-feature-settings:'cv02','cv03','cv04','cv11';
|
|
67
|
+
-webkit-font-smoothing:antialiased;
|
|
68
|
+
}
|
|
69
|
+
/* the six-step type scale (rem, follows the host base) */
|
|
70
|
+
.lks-ui .lks-t-caption{font-size:.75rem}.lks-ui .lks-t-label{font-size:.825rem}
|
|
71
|
+
.lks-ui .lks-t-body{font-size:.875rem}.lks-ui .lks-t-lead{font-size:1rem}
|
|
72
|
+
.lks-ui .lks-t-title{font-size:1.125rem}.lks-ui .lks-t-hero{font-size:1.5rem}
|
|
73
|
+
|
|
74
|
+
/* ── the column ladder: no borders, depth by surface step (v0.6) ── */
|
|
75
|
+
.lks-ui .lks14-rail{background:var(--surface-rail);border-right:none}
|
|
76
|
+
.lks-ui .lks14-chat{background:var(--surface-1);border-right:none}
|
|
77
|
+
.lks-ui .lks14-note{background:var(--surface-2)}
|
|
78
|
+
.lks-ui .lks14-colhead{color:var(--ink-muted);border-bottom:1px solid var(--border-faint);letter-spacing:.02em;text-transform:none;font-size:.825rem}
|
|
79
|
+
|
|
80
|
+
/* ── the floating app-header over the right half (blur + gradient fade) ── */
|
|
81
|
+
.lks-ui .lks14-appheader{position:relative;z-index:30;flex:none;display:flex;align-items:center;height:46px;padding:0 14px;gap:10px}
|
|
82
|
+
.lks-ui .lks14-appheader::before{content:'';position:absolute;inset:0;z-index:-1;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
|
|
83
|
+
-webkit-mask:linear-gradient(to bottom,black 0%,black 65%,transparent 100%);mask:linear-gradient(to bottom,black 0%,black 65%,transparent 100%);
|
|
84
|
+
background:linear-gradient(to bottom,rgb(var(--surface-1-rgb)/0.85) 0%,rgb(var(--surface-1-rgb)/0.55) 60%,rgb(var(--surface-1-rgb)/0.15) 85%,transparent 100%)}
|
|
85
|
+
.lks-ui .lks-hdr-title{flex:1;min-width:0;font-size:.825rem;font-weight:700;color:var(--ink-strong);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
86
|
+
.lks-ui .lks-hdr-xp{display:flex;align-items:center;gap:5px;flex:none}
|
|
87
|
+
.lks-ui .lks-hdr-xpbar{width:92px;height:10px;border-radius:999px;background:rgb(var(--ink-rgb)/0.08);overflow:hidden;box-shadow:inset 0 2px 3px rgb(var(--shadow-rgb)/0.3)}
|
|
88
|
+
.lks-ui .lks-hdr-xpbar i{display:block;height:100%;background:linear-gradient(90deg,var(--gold),var(--gold-light));border-radius:999px;transition:width .5s var(--ease-out-expo)}
|
|
89
|
+
.lks-ui .lks-hdr-stat{display:inline-flex;align-items:center;gap:4px;font-size:.825rem;font-weight:700;color:var(--ink);flex:none}
|
|
90
|
+
.lks-ui .lks-hdr-streak{color:var(--review)}
|
|
91
|
+
|
|
92
|
+
/* ── 3D push-down buttons (the signature style) ── */
|
|
93
|
+
.lks-ui .lks-btn{border-radius:1rem;font-weight:700;transition:all .15s var(--ease-spring)}
|
|
94
|
+
.lks-ui .lks-btn.primary{background:var(--brand);color:#fff;box-shadow:0 4px 0 0 var(--brand-dark)}
|
|
95
|
+
.lks-ui .lks-btn.primary:hover{background:var(--brand-light)}
|
|
96
|
+
.lks-ui .lks-btn.primary:active{transform:translateY(2px);box-shadow:0 1px 0 0 var(--brand-dark)}
|
|
97
|
+
.lks-ui .lks-btn.primary:disabled{background:rgb(var(--ink-rgb)/0.15);color:var(--ink-faint);box-shadow:none;transform:none;cursor:not-allowed}
|
|
98
|
+
.lks-ui .lks-btn.ghost{background:rgb(var(--ink-rgb)/0.06);color:var(--ink);border:none;box-shadow:0 3px 0 0 rgb(var(--shadow-rgb)/0.2)}
|
|
99
|
+
.lks-ui .lks-btn.ghost:hover{color:var(--ink-strong);background:rgb(var(--ink-rgb)/0.1)}
|
|
100
|
+
.lks-ui .lks-btn.ghost:active{transform:translateY(2px);box-shadow:0 1px 0 0 rgb(var(--shadow-rgb)/0.2)}
|
|
101
|
+
|
|
102
|
+
/* ── chat stream: bubbles + typing dots ── */
|
|
103
|
+
.lks-ui .lks14-msg-user{background:rgb(var(--ink-rgb)/0.04);border-radius:1rem 1rem .375rem 1rem;padding:.625rem 1rem;color:var(--ink-strong);animation:lks-msg-enter .2s var(--ease-out-expo)}
|
|
104
|
+
.lks-ui .lks14-msg-assistant{background:var(--surface-2);border:1px solid var(--border-faint);border-radius:1rem;padding:1rem;box-shadow:0 1px 3px -1px rgb(var(--shadow-rgb)/0.08);animation:lks-msg-enter .2s var(--ease-out-expo);font-size:.875rem;line-height:1.65}
|
|
105
|
+
.lks-ui .lks14-msg.streaming{opacity:1;border-style:dashed}
|
|
106
|
+
@keyframes lks-msg-enter{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
107
|
+
.lks-ui .lks14-thinking{gap:5px}
|
|
108
|
+
@keyframes lks-typing-dot{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-4px)}}
|
|
109
|
+
.lks-ui .lks14-thinking i{width:6px;height:6px;border-radius:999px;background:var(--brand);display:inline-block;animation:lks-typing-dot 1.2s ease-in-out infinite}
|
|
110
|
+
.lks-ui .lks14-thinking i:nth-child(2){animation-delay:.15s}
|
|
111
|
+
.lks-ui .lks14-thinking i:nth-child(3){animation-delay:.3s}
|
|
112
|
+
/* quiz options wear the neutral pill + brand hover */
|
|
113
|
+
.lks-ui .lks-qcard-opt{background:rgb(var(--ink-rgb)/0.05);border:2px solid transparent;border-radius:.75rem;color:var(--ink)}
|
|
114
|
+
.lks-ui .lks-qcard-opt:not(:disabled):hover{border-color:var(--brand)}
|
|
115
|
+
.lks-ui .lks-qcard-opt.right{border-color:var(--brand);color:var(--brand-light);background:rgb(var(--brand-rgb)/0.08)}
|
|
116
|
+
.lks-ui .lks-qcard-opt.wrong{border-color:var(--warning);color:var(--warning-light);background:var(--warning-tint)}
|
|
117
|
+
.lks-ui .lks-qcard-next{background:var(--brand);color:#fff;box-shadow:0 3px 0 0 var(--brand-dark);border-radius:1rem}
|
|
118
|
+
.lks-ui .lks-qcard-next:active{transform:translateY(2px);box-shadow:0 1px 0 0 var(--brand-dark)}
|
|
119
|
+
.lks-ui .lks-qcard-action{background:rgb(var(--ink-rgb)/0.06);border:none;color:var(--ink)}
|
|
120
|
+
.lks-ui .lks-qcard-action:hover{color:var(--ink-strong);background:rgb(var(--ink-rgb)/0.1)}
|
|
121
|
+
|
|
122
|
+
/* ── the composer capsule (claude.ai style) + circular 3D send ── */
|
|
123
|
+
.lks-ui .lks14-composer{border-top:none;background:linear-gradient(to bottom,transparent 0%,var(--surface-1) 40%)}
|
|
124
|
+
.lks-ui .lks14-composer-card{flex:1;display:flex;align-items:flex-end;gap:8px;background:rgb(var(--ink-rgb)/0.05);border-radius:1rem;padding:.625rem .75rem .5rem .875rem;transition:background .15s}
|
|
125
|
+
.lks-ui .lks14-composer-card:focus-within{background:rgb(var(--ink-rgb)/0.07)}
|
|
126
|
+
.lks-ui .lks14-composertext{background:transparent;border:none;box-shadow:none;color:var(--ink-strong);font-size:.875rem}
|
|
127
|
+
.lks-ui .lks14-composertext:focus{border-color:transparent}
|
|
128
|
+
.lks-ui .lks14-composertext::placeholder{color:var(--ink-faint)}
|
|
129
|
+
.lks-ui .lks-btn-send{width:36px;height:36px;border-radius:999px !important;padding:0 !important;display:inline-flex;align-items:center;justify-content:center;flex:none;background:var(--brand);color:#fff;box-shadow:0 3px 0 0 var(--brand-dark);border:none;cursor:pointer;transition:all .15s var(--ease-spring)}
|
|
130
|
+
.lks-ui .lks-btn-send:hover{background:var(--brand-light)}
|
|
131
|
+
.lks-ui .lks-btn-send:active{transform:translateY(2px);box-shadow:0 1px 0 0 var(--brand-dark)}
|
|
132
|
+
.lks-ui .lks-btn-send:disabled{background:rgb(var(--ink-rgb)/0.15);color:var(--ink-faint);box-shadow:none;transform:none;cursor:not-allowed}
|
|
133
|
+
|
|
134
|
+
/* ── toast capsules on the upstream tokens ── */
|
|
135
|
+
.lks-ui .lks-toast{background:var(--surface-0);color:var(--ink-strong);border:1px solid var(--border);box-shadow:0 8px 24px -4px rgb(var(--shadow-rgb)/0.18),0 2px 6px -2px rgb(var(--shadow-rgb)/0.12)}
|
|
136
|
+
.lks-ui .lks-toast .lks-toast-glyph.ok{color:var(--brand)}
|
|
137
|
+
.lks-ui .lks-toast .lks-toast-glyph.warn{color:var(--gold)}
|
|
138
|
+
.lks-ui .lks-toast .lks-toast-glyph.err{color:var(--warning)}
|
|
139
|
+
.lks-ui .lks-toast .lks-toast-glyph.info{color:var(--accent)}
|
|
140
|
+
.lks-ui .lks-toast-action{color:var(--brand)}
|
|
141
|
+
|
|
142
|
+
/* ── rail: signpost section heads + due box ── */
|
|
143
|
+
.lks-ui .lks14-sechead{background:rgb(var(--surface-rail-rgb)/0.55);backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);border:1px solid rgb(var(--gold-rgb)/0.18);border-top-color:rgb(var(--gold-rgb)/0.35);border-radius:.75rem;padding:7px 10px;margin:12px 0 6px;box-shadow:0 2px 8px rgb(var(--shadow-rgb)/0.35),inset 0 1px 0 rgb(255 255 255/0.06);color:var(--gold-light)}
|
|
144
|
+
.lks-ui .lks14-secnum{background:rgb(var(--gold-rgb)/0.15);color:var(--gold-light)}
|
|
145
|
+
.lks-ui .lks14-node{color:var(--ink);border-radius:.75rem}
|
|
146
|
+
.lks-ui .lks14-node:hover{background:rgb(var(--ink-rgb)/0.05)}
|
|
147
|
+
.lks-ui .lks14-node.focus{background:rgb(var(--accent-rgb)/0.1);outline:1px solid rgb(var(--accent-rgb)/0.25)}
|
|
148
|
+
.lks-ui .lks14-node[aria-disabled='true']{color:var(--ink-faint)}
|
|
149
|
+
.lks-ui .lks14-search,.lks-ui .lks-set-select{background:rgb(var(--ink-rgb)/0.05);border:1px solid var(--border-faint);color:var(--ink-strong)}
|
|
150
|
+
.lks-ui .lks14-search:focus{border-color:var(--accent)}
|
|
151
|
+
.lks-ui .lks14-duebox{background:var(--review-tint);border:1px solid rgb(var(--review-rgb)/0.25);color:var(--ink)}
|
|
152
|
+
.lks-ui .lks14-over{color:var(--warning-light)}
|
|
153
|
+
.lks-ui .lks14-tag.weak{color:var(--gold-light)}
|
|
154
|
+
.lks-ui .lks14-masterybar{background:rgb(var(--ink-rgb)/0.08)}
|
|
155
|
+
.lks-ui .lks14-masterybar i{background:var(--gold)}
|
|
156
|
+
.lks-ui .lks14-bar{background:rgb(var(--ink-rgb)/0.08)}
|
|
157
|
+
.lks-ui .lks14-bar i{background:var(--gold)}
|
|
158
|
+
.lks-ui .lks14-pill{color:var(--ink-muted);border-radius:999px}
|
|
159
|
+
.lks-ui .lks14-pill.on{background:rgb(var(--accent-rgb)/0.12);color:var(--accent-light)}
|
|
160
|
+
|
|
161
|
+
/* ── notebook: teach prose + view tabs ── */
|
|
162
|
+
.lks-ui .lks14-viewtab{color:var(--ink-muted);border-radius:999px}
|
|
163
|
+
.lks-ui .lks14-viewtab.on{background:rgb(var(--ink-rgb)/0.08);color:var(--ink-strong)}
|
|
164
|
+
.lks-ui .lks14-prose{color:var(--ink);font-size:.9375rem;line-height:1.65}
|
|
165
|
+
.lks-ui .lks14-prose h1,.lks-ui .lks-note-text h1{font-size:1.25rem;font-weight:800;color:var(--ink-strong)}
|
|
166
|
+
.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}
|
|
167
|
+
.lks-ui .lks14-prose h3,.lks-ui .lks-note-text h3{font-size:1rem;font-weight:700;color:var(--ink)}
|
|
168
|
+
.lks-ui .lks14-prose pre,.lks-ui .lks-note-text pre{background:var(--surface-0);border:1px solid var(--border);color:var(--ink)}
|
|
169
|
+
.lks-ui .lks14-prose code,.lks-ui .lks-note-text code{background:rgb(var(--surface-3-rgb)/0.6);color:var(--accent-light)}
|
|
170
|
+
.lks-ui .lks14-prose blockquote{border-left:3px solid var(--accent);background:rgb(var(--accent-rgb)/0.06);color:var(--ink-muted)}
|
|
171
|
+
.lks-ui .lks14-prose table thead{border-bottom:2px solid var(--brand)}
|
|
172
|
+
.lks-ui .lks14-prose th{color:var(--ink-strong)}
|
|
173
|
+
.lks-ui .lks14-prose tbody tr:nth-child(odd){background:rgb(var(--surface-3-rgb)/0.3)}
|
|
174
|
+
.lks-ui .lks14-prose td,.lks-ui .lks14-prose th{border-color:var(--border)}
|
|
175
|
+
.lks-ui .lks-note{background:var(--surface-1);border:1px solid var(--border-faint)}
|
|
176
|
+
.lks-ui .lks-note .lks-note-q{border-left-color:var(--gold)}
|
|
177
|
+
.lks-ui .lks14-zoneh{color:var(--ink-muted)}
|
|
178
|
+
.lks-ui mark.lks-hl{background:rgb(var(--gold-rgb)/0.25);border-bottom:2px solid var(--gold)}
|
|
179
|
+
|
|
180
|
+
/* ── artifact + rate + proposal cards on surface-card language ── */
|
|
181
|
+
.lks-ui .lks-acard,.lks-ui .lks-qcard{background:var(--surface-2);border:1px solid var(--border-faint);border-radius:1rem;box-shadow:0 1px 3px -1px rgb(var(--shadow-rgb)/0.08)}
|
|
182
|
+
.lks-ui .lks-acard-head,.lks-ui .lks-qcard-head{color:var(--ink-muted)}
|
|
183
|
+
.lks-ui .lks-acard-table th{background:transparent;border-bottom:2px solid var(--brand);color:var(--ink-strong)}
|
|
184
|
+
.lks-ui .lks-acard-table td{border-bottom:1px solid var(--border)}
|
|
185
|
+
.lks-ui .lks-acard-code{background:var(--surface-0);border:1px solid var(--border)}
|
|
186
|
+
.lks-ui .lks-acard-line i{color:var(--ink-faint)}
|
|
187
|
+
.lks-ui .lks-acard-note{border-left-color:var(--accent);color:var(--ink-muted)}
|
|
188
|
+
.lks-ui .lks-acard-modal-body{background:var(--surface-0);border-color:var(--border)}
|
|
189
|
+
.lks-ui .lks-acard-guess-opt{background:rgb(var(--ink-rgb)/0.05);border:2px solid transparent;color:var(--ink)}
|
|
190
|
+
.lks-ui .lks-acard-guess-opt:not(:disabled):hover{border-color:var(--accent)}
|
|
191
|
+
.lks-ui .lks-acard-guess-opt.picked{border-color:var(--accent);background:rgb(var(--accent-rgb)/0.1);color:var(--accent-light)}
|
|
192
|
+
.lks-ui .lks-ratecard{background:rgb(var(--review-rgb)/0.08);border:1px solid rgb(var(--review-rgb)/0.25)}
|
|
193
|
+
.lks-ui .lks-ratecard-title{color:var(--ink-strong)}
|
|
194
|
+
.lks-ui .lks-ratecard-opt{background:rgb(var(--ink-rgb)/0.05);border:2px solid transparent;color:var(--ink)}
|
|
195
|
+
.lks-ui .lks-ratecard-opt:not(:disabled):hover{border-color:var(--brand)}
|
|
196
|
+
.lks-ui .lks-ratecard-opt.best{background:var(--gold);color:#3A2E00;box-shadow:0 3px 0 0 var(--gold-dark);border-color:transparent}
|
|
197
|
+
.lks-ui .lks-ratecard-opt.best:active{transform:translateY(2px);box-shadow:0 1px 0 0 var(--gold-dark)}
|
|
198
|
+
.lks-ui .lks-propbanner{background:rgb(var(--gold-rgb)/0.06);border:1px solid rgb(var(--gold-rgb)/0.3);color:var(--ink)}
|
|
199
|
+
.lks-ui .lks-viewtab-badge{background:var(--brand);color:#fff}
|
|
200
|
+
|
|
201
|
+
/* ── the empty states + readbar + switcher ── */
|
|
202
|
+
.lks-ui .lks14-empty{color:var(--ink-muted)}
|
|
203
|
+
.lks-ui .lks-readbar-cur{border-left-color:var(--accent);color:var(--ink-muted)}
|
|
204
|
+
.lks-ui .lks14-switch{background:rgb(var(--ink-rgb)/0.06);border:1px solid var(--border-faint)}
|
|
205
|
+
.lks-ui .lks14-switch-btn.on{background:var(--brand);color:#fff}
|
|
206
|
+
.lks-ui .lks14-searchpanel{background:var(--surface-0);border:1px solid var(--border);box-shadow:0 8px 24px -4px rgb(var(--shadow-rgb)/0.18)}
|
|
207
|
+
.lks-ui .lks14-searchrow:hover{background:rgb(var(--ink-rgb)/0.05)}
|
|
208
|
+
.lks-ui .lks14-searchrow-snip{color:var(--ink-muted)}
|
|
209
|
+
.lks-ui .lks-propcard-err{color:var(--warning-light)}
|
|
210
|
+
.lks-ui .lks14-thinking{color:var(--ink-muted)}
|
|
211
|
+
|
|
212
|
+
/* ══════════ P9b: the balloon course map (upstream v0.28 MapRail, static path) ══════════ */
|
|
213
|
+
.lks-ui{--exam-locked-mix:#A7A2B2}
|
|
214
|
+
/* the sky: upstream's canvas presets reduced to layered gradients (essence port;
|
|
215
|
+
deterministic per-course via pickSky) */
|
|
216
|
+
.lks-ui .lks-map{position:relative;min-height:100%;background-repeat:no-repeat}
|
|
217
|
+
.lks-ui .lks-sky-day{background-image:
|
|
218
|
+
radial-gradient(1.5px 1.5px at 18% 12%,rgb(255 255 255/0.5),transparent 100%),
|
|
219
|
+
radial-gradient(1px 1px at 64% 8%,rgb(255 255 255/0.35),transparent 100%),
|
|
220
|
+
radial-gradient(1px 1px at 82% 22%,rgb(255 255 255/0.3),transparent 100%),
|
|
221
|
+
radial-gradient(120px 60px at 78% 6%,rgb(124 172 255/0.08),transparent 100%),
|
|
222
|
+
linear-gradient(to bottom,#16233F 0%,#0F1B33 55%,var(--surface-rail) 100%)}
|
|
223
|
+
.lks-ui .lks-sky-dusk{background-image:
|
|
224
|
+
radial-gradient(1.5px 1.5px at 24% 10%,rgb(255 255 255/0.45),transparent 100%),
|
|
225
|
+
radial-gradient(1px 1px at 70% 16%,rgb(255 255 255/0.3),transparent 100%),
|
|
226
|
+
radial-gradient(140px 70px at 22% 10%,rgb(255 160 90/0.07),transparent 100%),
|
|
227
|
+
linear-gradient(to bottom,#2A2140 0%,#1D1A38 50%,var(--surface-rail) 100%)}
|
|
228
|
+
.lks-ui .lks-sky-night{background-image:
|
|
229
|
+
radial-gradient(1.5px 1.5px at 12% 18%,rgb(255 255 255/0.55),transparent 100%),
|
|
230
|
+
radial-gradient(1px 1px at 38% 6%,rgb(255 255 255/0.4),transparent 100%),
|
|
231
|
+
radial-gradient(1px 1px at 58% 24%,rgb(255 255 255/0.35),transparent 100%),
|
|
232
|
+
radial-gradient(1.5px 1.5px at 88% 10%,rgb(255 255 255/0.5),transparent 100%),
|
|
233
|
+
radial-gradient(1px 1px at 76% 30%,rgb(255 255 255/0.3),transparent 100%),
|
|
234
|
+
radial-gradient(90px 90px at 84% 8%,rgb(200 214 255/0.06),transparent 100%),
|
|
235
|
+
linear-gradient(to bottom,#0B1024 0%,#0A0D1C 55%,var(--surface-rail) 100%)}
|
|
236
|
+
.lks-ui .lks-mapsec{padding:12px 8px 4px}
|
|
237
|
+
/* the signpost (map-signpost verbatim): frosted board with a gold rim */
|
|
238
|
+
.lks-ui .lks-signpost{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border-radius:8px;font:inherit;cursor:pointer;text-align:left;
|
|
239
|
+
background:rgb(var(--surface-rail-rgb)/0.55);backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);
|
|
240
|
+
border:1px solid rgb(var(--gold-rgb)/0.18);border-top-color:rgb(var(--gold-rgb)/0.35);
|
|
241
|
+
box-shadow:0 2px 8px rgb(var(--shadow-rgb)/0.35),inset 0 1px 0 rgb(255 255 255/0.06)}
|
|
242
|
+
.lks-ui .lks-signpost-num{width:24px;height:24px;border-radius:999px;background:var(--gold);color:#201500;font-size:11px;font-weight:800;display:flex;align-items:center;justify-content:center;flex:none;box-shadow:0 0 0 2px rgb(var(--gold-rgb)/0.4)}
|
|
243
|
+
.lks-ui .lks-signpost-title{flex:1;font-size:12.5px;font-weight:700;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-shadow:0 1px 2px rgba(0,0,0,0.7)}
|
|
244
|
+
.lks-ui .lks-signpost-caret{color:rgb(var(--ink-rgb)/0.5);font-size:10px}
|
|
245
|
+
.lks-ui .lks-mapfield{position:relative}
|
|
246
|
+
.lks-ui .lks-mapropes{position:absolute;inset:0;width:100%;pointer-events:none;overflow:visible}
|
|
247
|
+
.lks-ui .lks-mapnode{position:absolute;width:110px;display:flex;flex-direction:column;align-items:center;
|
|
248
|
+
animation:lks-balloon-bob var(--bob-duration,6s) ease-in-out infinite;animation-delay:var(--bob-delay,0s)}
|
|
249
|
+
@keyframes lks-balloon-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
|
|
250
|
+
@keyframes lks-path-draw{from{stroke-dashoffset:1}to{stroke-dashoffset:0}}
|
|
251
|
+
@media (prefers-reduced-motion: reduce){.lks-ui .lks-mapnode{animation:none}.lks-ui .lks-bubble-available{animation:none}}
|
|
252
|
+
/* the bubbles (lesson-bubble verbatim): 3D sphere lighting + ::before inner highlight */
|
|
253
|
+
.lks-ui .lks-bubble{position:relative;width:56px;height:56px;border-radius:999px;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:all .2s var(--ease-out-back);background-position:30% 25%}
|
|
254
|
+
.lks-ui .lks-bubble::before{content:'';position:absolute;top:12%;left:22%;width:38%;height:28%;border-radius:50%;
|
|
255
|
+
background:radial-gradient(ellipse at center,var(--inner-highlight) 0%,rgb(255 255 255/0) 70%);pointer-events:none;z-index:1}
|
|
256
|
+
.lks-ui .lks-bubble[aria-disabled='true']{cursor:not-allowed}
|
|
257
|
+
.lks-ui .lks-bubble.selected{outline:4px solid var(--accent);outline-offset:3px}
|
|
258
|
+
.lks-ui .lks-bubble-glyph{position:relative;z-index:2;color:#fff;display:inline-flex;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.4))}
|
|
259
|
+
.lks-ui .lks-bubble-glyph.dim{opacity:.5}
|
|
260
|
+
.lks-ui .lks-bubble-ring{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg);pointer-events:none}
|
|
261
|
+
.lks-ui .lks-bubble-due{position:absolute;top:-4px;right:-4px;width:16px;height:16px;border-radius:999px;background:var(--review);color:#fff;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 2px var(--surface-rail);z-index:3}
|
|
262
|
+
.lks-ui .lks-bubble-name{margin-top:4px;max-width:120px;font-size:11px;font-weight:700;color:#fff;background:rgb(var(--brand-rgb)/0.9);padding:2px 6px;border-radius:6px;text-align:center;line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
263
|
+
/* locked: heavy sunken stone */
|
|
264
|
+
.lks-ui .lks-bubble-locked{background:radial-gradient(circle at 35% 30%,var(--surface-3) 0%,var(--surface-2) 60%,var(--surface-1) 100%);
|
|
265
|
+
box-shadow:inset 0 3px 6px rgb(var(--shadow-rgb)/0.5),inset 0 -2px 4px rgb(255 255 255/0.04),0 2px 4px rgb(var(--shadow-rgb)/0.3)}
|
|
266
|
+
.lks-ui .lks-bubble-locked::before{background:radial-gradient(ellipse at center,rgb(255 255 255/0.12) 0%,rgb(255 255 255/0) 70%)}
|
|
267
|
+
/* available: bright glowing sphere, your move */
|
|
268
|
+
.lks-ui .lks-bubble-available{background:radial-gradient(circle at 32% 28%,var(--brand-light) 0%,var(--brand) 45%,var(--brand-dark) 100%);
|
|
269
|
+
box-shadow:0 0 0 4px var(--brand-ring),0 0 16px var(--brand-glow),0 4px 10px rgb(var(--shadow-rgb)/0.25)}
|
|
270
|
+
.lks-ui .lks-bubble-available:hover{transform:translateY(-3px) scale(1.03);
|
|
271
|
+
box-shadow:0 0 0 5px rgb(var(--brand-rgb)/0.2),0 0 22px rgb(var(--brand-rgb)/0.5),0 6px 14px rgb(var(--shadow-rgb)/0.3)}
|
|
272
|
+
.lks-ui .lks-bubble-available:active{transform:translateY(2px) scale(0.97);
|
|
273
|
+
box-shadow:0 0 0 3px var(--brand-ring),0 0 10px rgb(var(--brand-rgb)/0.3),0 2px 6px rgb(var(--shadow-rgb)/0.2)}
|
|
274
|
+
@keyframes lks-bubble-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.04)}}
|
|
275
|
+
.lks-ui .lks-bubble-available{animation:lks-bubble-pulse 2.4s ease-in-out infinite}
|
|
276
|
+
/* in_progress: blue in motion */
|
|
277
|
+
.lks-ui .lks-bubble-in-progress{background:radial-gradient(circle at 32% 28%,var(--accent-light) 0%,var(--accent) 45%,var(--accent-dark) 100%);
|
|
278
|
+
box-shadow:0 0 0 4px var(--accent-ring),0 0 14px var(--accent-glow),0 4px 10px rgb(var(--shadow-rgb)/0.25)}
|
|
279
|
+
.lks-ui .lks-bubble-in-progress:hover{transform:translateY(-2px) scale(1.03);
|
|
280
|
+
box-shadow:0 0 0 5px rgb(var(--accent-rgb)/0.2),0 0 20px rgb(var(--accent-rgb)/0.45),0 6px 14px rgb(var(--shadow-rgb)/0.3)}
|
|
281
|
+
/* mastered: gold crown halo, scaled up */
|
|
282
|
+
.lks-ui .lks-bubble-mastered{background:radial-gradient(circle at 32% 28%,var(--gold-light) 0%,var(--gold) 45%,var(--gold-dark) 100%);
|
|
283
|
+
box-shadow:0 0 0 4px var(--gold-ring),0 0 22px var(--gold-glow),0 0 40px rgb(var(--gold-rgb)/0.15),0 4px 12px rgb(var(--shadow-rgb)/0.25);transform:scale(1.1)}
|
|
284
|
+
.lks-ui .lks-bubble-mastered::before{background:radial-gradient(ellipse at center,rgb(255 255 255/0.7) 0%,rgb(255 255 255/0) 70%)}
|
|
285
|
+
.lks-ui .lks-bubble-mastered:hover{transform:scale(1.13) translateY(-2px);
|
|
286
|
+
box-shadow:0 0 0 5px rgb(var(--gold-rgb)/0.25),0 0 28px rgb(var(--gold-rgb)/0.55),0 0 50px rgb(var(--gold-rgb)/0.2),0 6px 16px rgb(var(--shadow-rgb)/0.3)}
|
|
287
|
+
/* exam: the section boss */
|
|
288
|
+
.lks-ui .lks-exam{background:radial-gradient(circle at 32% 28%,var(--exam-light) 0%,var(--exam) 45%,var(--exam-dark) 100%);
|
|
289
|
+
box-shadow:0 0 0 4px var(--exam-ring),0 0 16px var(--exam-glow),0 4px 10px rgb(var(--shadow-rgb)/0.25)}
|
|
290
|
+
.lks-ui .lks-exam-locked{background:radial-gradient(circle at 35% 30%,var(--exam-locked) 0%,var(--exam-locked-mix) 60%,var(--surface-1) 100%);
|
|
291
|
+
box-shadow:inset 0 3px 6px rgb(var(--shadow-rgb)/0.5),inset 0 -2px 4px rgb(255 255 255/0.04),0 2px 4px rgb(var(--shadow-rgb)/0.3)}
|
|
292
|
+
.lks-ui .lks-exam-passed{background:radial-gradient(circle at 32% 28%,rgb(var(--exam-light-rgb,192 132 252)) 0%,var(--exam-light) 40%,var(--exam) 100%);
|
|
293
|
+
box-shadow:0 0 0 4px var(--gold-ring),0 0 18px var(--exam-glow),0 0 28px rgb(var(--gold-rgb)/0.15),0 4px 12px rgb(var(--shadow-rgb)/0.25)}
|
|
294
|
+
/* the floating rail head card (upstream map-header card) */
|
|
295
|
+
.lks-ui .lks14-railhead{background:rgb(var(--surface-rail-rgb)/0.55);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgb(255 255 255/0.06);border-radius:12px;padding:8px 10px}
|
|
296
|
+
.lks-ui .lks14-railtitle{color:#fff;font-weight:800;font-size:.875rem;text-shadow:0 1px 3px rgba(0,0,0,0.6)}
|
|
297
|
+
.lks-ui .lks14-railsub{color:rgb(var(--ink-rgb)/0.6)}
|
|
298
|
+
.lks-ui .lks14-masterybar{height:10px;background:rgb(0 0 0/0.4);border-radius:999px;box-shadow:inset 0 0 0 1px rgb(255 255 255/0.1);overflow:hidden}
|
|
299
|
+
.lks-ui .lks14-masterybar i{background:var(--brand)}
|
|
300
|
+
.lks-ui .lks14-masterybar.gold i{background:var(--gold)}
|
|
301
|
+
.lks-ui .lks14-search{background:rgb(0 0 0/0.25);border:1px solid rgb(255 255 255/0.15);color:var(--ink-strong);border-radius:10px}
|
|
302
|
+
.lks-ui .lks14-search:focus{border-color:var(--brand);outline:none}
|
|
303
|
+
.lks-ui .lks14-search::placeholder{color:rgb(var(--ink-rgb)/0.35)}
|
|
304
|
+
`;
|
|
305
|
+
//#endregion
|
|
9
306
|
//#region src/client/styles.ts
|
|
10
307
|
/**
|
|
308
|
+
* The study stylesheet. The center-takeover visibility follows the stardeck
|
|
309
|
+
* panel doctrine: the shell view is display:none until the `<html>` active
|
|
310
|
+
* attribute flips, and the host conversation column's own children are hidden
|
|
311
|
+
* behind `!important` so React never fights the takeover.
|
|
312
|
+
*/
|
|
313
|
+
/**
|
|
11
314
|
* Style injection for the study surfaces: one `<style>` element carrying the
|
|
12
315
|
* `.lks14-*` panel classes (the sidebar-entry center-takeover, arranged like
|
|
13
316
|
* upstream LookatStudy) plus the shared `.lks-*` classes the host-native
|
|
@@ -21,12 +324,6 @@ window.__ModuleLoader__.load({
|
|
|
21
324
|
*/
|
|
22
325
|
/** Marker attribute proving the stylesheet is already in the document. */
|
|
23
326
|
const STYLE_ID = "data-dsh-plugin-lookatstudy";
|
|
24
|
-
/**
|
|
25
|
-
* The study stylesheet. The center-takeover visibility follows the stardeck
|
|
26
|
-
* panel doctrine: the shell view is display:none until the `<html>` active
|
|
27
|
-
* attribute flips, and the host conversation column's own children are hidden
|
|
28
|
-
* behind `!important` so React never fights the takeover.
|
|
29
|
-
*/
|
|
30
327
|
const STUDY_CSS = `
|
|
31
328
|
/* State inks: small colored text must clear WCAG AA (4.5:1), but the host's
|
|
32
329
|
raw state tokens (success/warn primaries) sit at 2.3–2.6:1 on light
|
|
@@ -38,7 +335,7 @@ window.__ModuleLoader__.load({
|
|
|
38
335
|
every surface family that consumes the inks: .lks-root (dock pill,
|
|
39
336
|
settings) and .lks-tv (toolview cards, which render in the host
|
|
40
337
|
conversation with no .lks-root ancestor). */
|
|
41
|
-
.lks-root,.lks-tv{--lks-warn-ink:color-mix(in srgb,var(--dsw-alias-state-warn-label) 45%,var(--dsw-alias-label-primary));--lks-ok-ink:color-mix(in srgb,var(--dsw-alias-state-success-primary) 40%,var(--dsw-alias-label-primary));--lks-err-ink:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,var(--dsw-alias-label-primary))}
|
|
338
|
+
.lks-root,.lks-tv,.lks14{--lks-warn-ink:color-mix(in srgb,var(--dsw-alias-state-warn-label) 45%,var(--dsw-alias-label-primary));--lks-ok-ink:color-mix(in srgb,var(--dsw-alias-state-success-primary) 40%,var(--dsw-alias-label-primary));--lks-err-ink:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,var(--dsw-alias-label-primary))}
|
|
42
339
|
.lks-root{font-family:var(--dsw-font-family)}
|
|
43
340
|
.lks-root :where(button){font-family:var(--dsw-font-family);cursor:pointer;border:none;background:none;padding:0}
|
|
44
341
|
|
|
@@ -59,9 +356,11 @@ html[data-dsh-lookatstudy-active] .lks14-shell-view{display:flex;flex-direction:
|
|
|
59
356
|
html[data-dsh-lookatstudy-active] [data-pane='conversation'] > :not([data-dsh-lookatstudy-view]),
|
|
60
357
|
html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatstudy-view]){display:none !important}
|
|
61
358
|
[data-dsh-lookatstudy-view]{container-type:inline-size}
|
|
62
|
-
.lks14{
|
|
359
|
+
.lks14{position:relative;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);overflow:hidden;font-family:var(--dsw-font-family)}
|
|
63
360
|
.lks14 button{font-family:var(--dsw-font-family);cursor:pointer}
|
|
64
361
|
.lks14-body{display:flex;flex:1;min-width:0;min-height:0}
|
|
362
|
+
.lks14-righthalf{flex:1;min-width:0;display:flex;flex-direction:column;min-height:0}
|
|
363
|
+
.lks14-row{flex:1;min-height:0;display:flex}
|
|
65
364
|
.lks14-col{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
|
|
66
365
|
.lks14-colhead{flex:none;display:flex;align-items:center;gap:8px;padding:10px 12px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
67
366
|
|
|
@@ -78,22 +377,6 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
78
377
|
.lks14-duebox{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;margin:8px 0;font-size:12.5px}
|
|
79
378
|
.lks14-dueitem{display:flex;justify-content:space-between;gap:6px;padding:2px 0;color:var(--dsw-alias-label-secondary)}
|
|
80
379
|
.lks14-over{color:var(--dsw-alias-state-error-primary)}
|
|
81
|
-
.lks14-sechead{display:flex;align-items:center;gap:6px;width:100%;background:none;border:none;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;font-weight:600;padding:6px 2px;cursor:pointer;text-align:left}
|
|
82
|
-
.lks14-sechead:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
83
|
-
.lks14-secnum{flex:none;width:18px;height:18px;border-radius:5px;background:var(--dsw-alias-bg-layer-2);display:inline-flex;align-items:center;justify-content:center;font-size:11px}
|
|
84
|
-
.lks14-secheadt{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
85
|
-
.lks14-node{display:flex;align-items:center;gap:6px;width:100%;background:none;border:none;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;padding:5px 6px;border-radius:8px;cursor:pointer;text-align:left}
|
|
86
|
-
.lks14-node:hover{background:var(--dsw-alias-bg-layer-1)}
|
|
87
|
-
.lks14-node:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
88
|
-
.lks14-node.focus{background:var(--dsw-alias-bg-layer-2)}
|
|
89
|
-
.lks14-node[aria-disabled='true']{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
90
|
-
.lks14-g{flex:none;display:inline-flex}
|
|
91
|
-
.lks14-t{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
92
|
-
.lks14-tag{flex:none;display:inline-flex;align-items:center;gap:2px;font-size:10.5px;border-radius:6px;padding:1px 4px}
|
|
93
|
-
.lks14-tag.weak{color:var(--dsw-alias-state-warn-primary)}
|
|
94
|
-
.lks14-bar{flex:none;width:26px;height:4px;border-radius:2px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
95
|
-
.lks14-bar i{display:block;height:100%;background:var(--dsw-alias-business-primary);transform-origin:left}
|
|
96
|
-
.lks14-pct{flex:none;font-size:10.5px;color:var(--dsw-alias-label-tertiary)}
|
|
97
380
|
.lks14-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;padding:18px 6px;line-height:1.7}
|
|
98
381
|
.lks14-import{margin:8px 0}
|
|
99
382
|
.lks14-inputrow{display:flex;gap:6px}
|
|
@@ -152,6 +435,153 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
152
435
|
.lks14-zone{margin-bottom:12px}
|
|
153
436
|
.lks14-zoneh{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:6px}
|
|
154
437
|
|
|
438
|
+
|
|
439
|
+
/* toast capsules (P6, upstream Toast port): top-center under the panel head,
|
|
440
|
+
container inert to pointers, items auto; exit is an animation handshake —
|
|
441
|
+
reduced-motion keeps a .01ms duration so the end event still fires. */
|
|
442
|
+
.lks-toasts{position:absolute;top:44px;left:50%;transform:translateX(-50%);z-index:40;display:flex;flex-direction:column;align-items:center;gap:6px;pointer-events:none;max-width:min(440px,90%)}
|
|
443
|
+
.lks-toast{pointer-events:auto;display:flex;align-items:center;gap:8px;max-width:100%;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:6px 8px 6px 14px;font-size:13px;line-height:1.4;box-shadow:0 6px 24px rgba(0,0,0,.14);animation:lks-toast-in .18s ease}
|
|
444
|
+
.lks-toast.exiting{animation:lks-toast-out .18s ease forwards}
|
|
445
|
+
.lks-toast .lks-toast-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
446
|
+
.lks-toast .lks-toast-glyph{flex:none}
|
|
447
|
+
.lks-toast .lks-toast-glyph.ok{color:var(--lks-ok-ink)}
|
|
448
|
+
.lks-toast .lks-toast-glyph.warn{color:var(--lks-warn-ink)}
|
|
449
|
+
.lks-toast .lks-toast-glyph.err{color:var(--lks-err-ink)}
|
|
450
|
+
.lks-toast .lks-toast-glyph.info{color:var(--dsw-alias-state-business-primary)}
|
|
451
|
+
.lks-toast-action{border:none;background:none;color:var(--dsw-alias-state-business-primary);font-weight:600;font-size:13px;cursor:pointer;padding:4px 8px;border-radius:999px;flex:none}
|
|
452
|
+
.lks-toast-action:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
453
|
+
.lks-toast-action:focus-visible,.lks-toast-close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
454
|
+
.lks-toast-close{border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;font-size:15px;line-height:1;padding:0;flex:none}
|
|
455
|
+
.lks-toast-close:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
456
|
+
@keyframes lks-toast-in{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
|
|
457
|
+
@keyframes lks-toast-out{to{opacity:0;transform:translateY(-6px)}}
|
|
458
|
+
@media (prefers-reduced-motion:reduce){.lks-toast{animation-duration:.01ms}}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
/* the companion creature (P7): bottom-right of the panel; gentle bob idle,
|
|
468
|
+
bounce while talking, pop on celebration; reduced-motion = static (a11y). */
|
|
469
|
+
.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}
|
|
470
|
+
.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))}
|
|
471
|
+
.lks-companion.mood-talking{animation:lks-comp-talk .6s ease-in-out infinite}
|
|
472
|
+
.lks-companion.mood-celebrating{animation:lks-comp-pop .5s cubic-bezier(.34,1.56,.64,1) 3}
|
|
473
|
+
.lks-companion.mood-encouraging{animation:lks-comp-nudge 1.2s ease-in-out 2}
|
|
474
|
+
@keyframes lks-comp-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
|
|
475
|
+
@keyframes lks-comp-talk{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-7px) scale(1.06)}}
|
|
476
|
+
@keyframes lks-comp-pop{0%{transform:scale(1)}40%{transform:scale(1.25) rotate(-8deg)}100%{transform:scale(1)}}
|
|
477
|
+
@keyframes lks-comp-nudge{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px) rotate(-4deg)}75%{transform:translateX(5px) rotate(4deg)}}
|
|
478
|
+
@media (prefers-reduced-motion:reduce){.lks-companion{animation:none}}
|
|
479
|
+
|
|
480
|
+
/* P5: proposal banner (ConfirmCard semantics) + thinking row + narrow switch */
|
|
481
|
+
.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)}
|
|
482
|
+
.lks-propbanner .lks-propbanner-why{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
483
|
+
.lks14-thinking{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--dsw-alias-label-secondary);padding:2px 2px}
|
|
484
|
+
.lks14-switch{display:none;flex-direction:row;gap:2px;align-self:center;margin:8px 0 0;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:3px}
|
|
485
|
+
.lks14-switch-btn{border-radius:999px;padding:4px 16px;font-size:13px;color:var(--dsw-alias-label-secondary);background:transparent}
|
|
486
|
+
.lks14-switch-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
487
|
+
.lks14-switch-btn.on{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 86%,#000);color:#fff;font-weight:600}
|
|
488
|
+
@container (max-width: 900px){
|
|
489
|
+
.lks14-switch{display:flex}
|
|
490
|
+
.lks14-body{flex-direction:column}
|
|
491
|
+
.lks14-rail{flex:none;max-height:32vh;border-right:none;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
492
|
+
.lks14-chat{min-width:0}
|
|
493
|
+
.lks14-note{flex:1 1 auto;min-height:0}
|
|
494
|
+
.lks14-body[data-pane='rail'] .lks14-righthalf{display:none}
|
|
495
|
+
.lks14-body[data-pane='chat'] .lks14-rail,.lks14-body[data-pane='chat'] .lks14-note,
|
|
496
|
+
.lks14-body[data-pane='note'] .lks14-rail,.lks14-body[data-pane='note'] .lks14-chat{display:none}
|
|
497
|
+
.lks14-body[data-pane='rail'] .lks14-rail{display:flex;max-height:none;flex:1 1 auto}
|
|
498
|
+
.lks14-body[data-pane='note'] .lks14-note{display:flex}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* rail search results panel (P4, upstream CourseSearchPanel) */
|
|
502
|
+
.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}
|
|
503
|
+
.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)}
|
|
504
|
+
.lks14-searchrow:last-child{border-bottom:none}
|
|
505
|
+
.lks14-searchrow:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
506
|
+
.lks14-searchrow-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
507
|
+
.lks14-searchrow-course{font-size:10.5px;color:var(--dsw-alias-label-tertiary)}
|
|
508
|
+
.lks14-searchrow-snip{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
509
|
+
|
|
510
|
+
/* review self-rating card (P3, upstream SelfRatingCard: three Memrise levels) */
|
|
511
|
+
.lks-ratecard{flex:none;display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-state-business-tertiary);padding:10px 12px;margin-bottom:8px}
|
|
512
|
+
.lks-ratecard-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary-bluish)}
|
|
513
|
+
.lks-ratecard-opts{display:flex;gap:6px}
|
|
514
|
+
.lks-ratecard-opt{flex:1;font:inherit;font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:9px;padding:7px 10px;cursor:pointer}
|
|
515
|
+
.lks-ratecard-opt:hover{border-color:var(--dsw-alias-state-business-primary)}
|
|
516
|
+
.lks-ratecard-opt.again:hover{border-color:var(--dsw-alias-state-error-primary)}
|
|
517
|
+
.lks-ratecard-opt.best{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 86%,#000);color:#fff;border-color:transparent}
|
|
518
|
+
.lks-ratecard-opt.best:hover{filter:brightness(1.1)}
|
|
519
|
+
.lks-ratecard-opt:disabled{opacity:.6;cursor:default}
|
|
520
|
+
.lks14-dueitem{font:inherit;text-align:left}
|
|
521
|
+
|
|
522
|
+
/* selection-to-note (P2, upstream selection flow): the popover floats over
|
|
523
|
+
the prose; persisted highlights are soft amber marks. */
|
|
524
|
+
.lks14-prosewrap{position:relative}
|
|
525
|
+
.lks-quote-btn{position:absolute;z-index:12;display:flex;gap:2px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:0 6px 24px rgba(0,0,0,.16);transform:translateX(-50%)}
|
|
526
|
+
.lks-quote-btn button{border:none;background:none;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;font-weight:600;padding:6px 10px;cursor:pointer;border-radius:10px}
|
|
527
|
+
.lks-quote-btn button:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
528
|
+
.lks-quote-btn button + button{border-left:1px solid var(--dsw-alias-border-l1)}
|
|
529
|
+
mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border-bottom:2px solid var(--dsw-alias-state-warn-primary);padding:0 1px}
|
|
530
|
+
|
|
531
|
+
/* artifact cards (P1b, upstream artifacts port): chat column + notebook zones */
|
|
532
|
+
.lks-acard{flex:none;margin:2px 12px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;display:flex;flex-direction:column;gap:8px;overflow:hidden}
|
|
533
|
+
.lks-acard-head{display:flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}
|
|
534
|
+
.lks-acard-expand{margin-left:auto;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;padding:2px 6px;border-radius:6px}
|
|
535
|
+
.lks-acard-expand:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
536
|
+
.lks-acard-table{border-collapse:collapse;display:block;max-width:100%;overflow-x:auto}
|
|
537
|
+
.lks-acard-table th,.lks-acard-table td{border:1px solid var(--dsw-alias-border-l2);padding:4px 9px;font-size:12.5px;text-align:left}
|
|
538
|
+
.lks-acard-table th{background:var(--dsw-alias-bg-layer-2)}
|
|
539
|
+
.lks-acard-code{background:var(--dsw-alias-markdown-code-block);border-radius:8px;padding:8px 0;overflow-x:auto;font-family:var(--dsw-font-markdown-code-block-small);font-size:12px;line-height:1.6;margin:0}
|
|
540
|
+
.lks-acard-line{display:block;white-space:pre}
|
|
541
|
+
.lks-acard-line i{display:inline-block;width:2.2em;color:var(--dsw-alias-label-tertiary);font-style:normal;text-align:right;padding-right:8px;user-select:none}
|
|
542
|
+
.lks-acard-notes{display:flex;flex-direction:column;gap:5px}
|
|
543
|
+
.lks-acard-note{font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary);border-left:2px solid var(--dsw-alias-state-business-primary);padding-left:8px}
|
|
544
|
+
.lks-acard-note b{color:var(--dsw-alias-label-primary);font-weight:600;margin-right:4px}
|
|
545
|
+
.lks-acard-diagram{overflow-x:auto;text-align:center}
|
|
546
|
+
.lks-acard-diagram svg{max-width:100%;height:auto}
|
|
547
|
+
.lks-acard-modal{position:fixed;inset:0;z-index:90;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:32px}
|
|
548
|
+
.lks-acard-modal-body{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:14px;max-width:900px;max-height:100%;overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}
|
|
549
|
+
.lks-acard-modal-title{display:flex;align-items:center;font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
550
|
+
.lks-acard-guess-prompt{font-size:13.5px;line-height:1.6;color:var(--dsw-alias-label-primary)}
|
|
551
|
+
.lks-acard-guess-opts{display:flex;gap:8px}
|
|
552
|
+
.lks-acard-guess-opt{flex:1;font:inherit;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;cursor:pointer}
|
|
553
|
+
.lks-acard-guess-opt:not(:disabled):hover{border-color:var(--dsw-alias-state-business-primary)}
|
|
554
|
+
.lks-acard-guess-opt.picked{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
|
|
555
|
+
.lks-acard-guess-wait{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
556
|
+
.lks-acard-stage{margin-top:14px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
557
|
+
.lks-acard-stage .lks-acard{margin:0}
|
|
558
|
+
.lks14-zone .lks-acard{margin:0 0 8px}
|
|
559
|
+
/* notebook tab badge (unseen artifacts) */
|
|
560
|
+
.lks-viewtab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;border-radius:8px;background:var(--dsw-alias-state-business-primary);color:#fff;font-size:10px;font-weight:700;margin-left:5px;padding:0 4px}
|
|
561
|
+
|
|
562
|
+
/* practice card (P1, upstream QuizArtifact port): chat column, below the stream */
|
|
563
|
+
.lks-qcard{flex:none;margin:2px 12px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;display:flex;flex-direction:column;gap:8px}
|
|
564
|
+
.lks-qcard-head{display:flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}
|
|
565
|
+
.lks-qcard-count{margin-left:auto;font-weight:400;color:var(--dsw-alias-label-tertiary)}
|
|
566
|
+
.lks-qcard-q{display:flex;flex-direction:column;gap:6px}
|
|
567
|
+
.lks-qcard-prompt{font-size:13.5px;line-height:1.55;color:var(--dsw-alias-label-primary)}
|
|
568
|
+
.lks-qcard-opts{display:flex;flex-direction:column;gap:4px}
|
|
569
|
+
.lks-qcard-opt{text-align:left;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:9px;padding:6px 10px;cursor:pointer}
|
|
570
|
+
.lks-qcard-opt:not(:disabled):hover{border-color:var(--dsw-alias-state-business-primary)}
|
|
571
|
+
.lks-qcard-opt:disabled{cursor:default}
|
|
572
|
+
.lks-qcard-opt.right{border-color:var(--dsw-alias-state-success-primary);color:var(--lks-ok-ink)}
|
|
573
|
+
.lks-qcard-opt.wrong{border-color:var(--dsw-alias-state-error-primary);color:var(--lks-err-ink)}
|
|
574
|
+
.lks-qcard-opt.dim{opacity:.75}
|
|
575
|
+
.lks-qcard-expl{font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary);border-left:2px solid var(--dsw-alias-state-business-primary);padding-left:8px}
|
|
576
|
+
.lks-qcard-next{margin-top:2px;border:none;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 86%,#000);color:#fff;font:inherit;font-size:12.5px;font-weight:600;border-radius:8px;padding:5px 14px;cursor:pointer}
|
|
577
|
+
.lks-qcard-next:hover{filter:brightness(1.1)}
|
|
578
|
+
.lks-qcard-review .lks-qcard-q.review{padding:6px 0;border-bottom:1px dashed var(--dsw-alias-border-l1)}
|
|
579
|
+
.lks-qcard-review .lks-qcard-q.review.wrong .lks-qcard-prompt{color:var(--lks-err-ink)}
|
|
580
|
+
.lks-qcard-ans{font-size:12.5px;color:var(--dsw-alias-label-secondary)}
|
|
581
|
+
.lks-qcard-actions{display:flex;flex-wrap:wrap;gap:6px}
|
|
582
|
+
.lks-qcard-action{display:inline-flex;align-items:center;gap:5px;font:inherit;font-size:12.5px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:4px 12px;cursor:pointer}
|
|
583
|
+
.lks-qcard-action:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
|
|
584
|
+
|
|
155
585
|
/* note cards (shared: panel notebook + settings-era shapes) */
|
|
156
586
|
.lks-note{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 14px;margin-bottom:8px}
|
|
157
587
|
.lks-note .lks-note-src{float:right;font-size:11.5px;color:var(--dsw-alias-label-secondary)}
|
|
@@ -235,7 +665,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
235
665
|
if (existing !== null) return existing;
|
|
236
666
|
const style = document.createElement("style");
|
|
237
667
|
style.setAttribute(STYLE_ID, "");
|
|
238
|
-
style.textContent = STUDY_CSS;
|
|
668
|
+
style.textContent = STUDY_CSS + UPSTREAM_CSS;
|
|
239
669
|
document.head.appendChild(style);
|
|
240
670
|
return style;
|
|
241
671
|
}
|
|
@@ -346,6 +776,26 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
346
776
|
fill: "currentColor"
|
|
347
777
|
}));
|
|
348
778
|
}
|
|
779
|
+
/** IconWarningOutline16 */
|
|
780
|
+
function IconWarningOutline16({ size = 16, className }) {
|
|
781
|
+
return (0, react.createElement)("svg", {
|
|
782
|
+
width: size,
|
|
783
|
+
height: size,
|
|
784
|
+
className,
|
|
785
|
+
viewBox: "0 0 16 16",
|
|
786
|
+
fill: "none",
|
|
787
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
788
|
+
}, (0, react.createElement)("path", {
|
|
789
|
+
d: "M6.3002 3.32843L7.69986 3.32843L7.69986 7.79657H6.3002L6.3002 3.32843Z",
|
|
790
|
+
fill: "currentColor"
|
|
791
|
+
}), (0, react.createElement)("path", {
|
|
792
|
+
d: "M6.3002 9.01935H7.69986V10.6711H6.3002V9.01935Z",
|
|
793
|
+
fill: "currentColor"
|
|
794
|
+
}), (0, react.createElement)("path", {
|
|
795
|
+
d: "M12.6328 6.99976C12.6328 3.88874 10.111 1.36694 7 1.36694C3.88899 1.36695 1.3672 3.88875 1.36719 6.99976C1.36719 10.1108 3.88899 12.6326 7 12.6326C10.111 12.6326 12.6328 10.1108 12.6328 6.99976ZM13.8582 6.99976C13.8582 10.7873 10.7876 13.8579 7 13.8579C3.21244 13.8579 0.141846 10.7873 0.141846 6.99976C0.141857 3.2122 3.21245 0.141612 7 0.141602C10.7876 0.141602 13.8581 3.21219 13.8582 6.99976Z",
|
|
796
|
+
fill: "currentColor"
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
349
799
|
/**
|
|
350
800
|
* Plugin-original companions for learning-domain glyphs the host set does not
|
|
351
801
|
* carry (lock/star/book/crown/bolt/power/pin/flame) — authored here in the
|
|
@@ -556,6 +1006,30 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
556
1006
|
});
|
|
557
1007
|
this.refresh();
|
|
558
1008
|
}
|
|
1009
|
+
/** Save a learner selection as a record-zone note (the highlight anchor is the quote). */
|
|
1010
|
+
async addUserNote(lessonId, quote) {
|
|
1011
|
+
await fetchJson("/lookatstudy/api/note/user", {
|
|
1012
|
+
method: "POST",
|
|
1013
|
+
headers: { "content-type": "application/json" },
|
|
1014
|
+
body: JSON.stringify({
|
|
1015
|
+
lessonId,
|
|
1016
|
+
quote
|
|
1017
|
+
})
|
|
1018
|
+
});
|
|
1019
|
+
this.refresh();
|
|
1020
|
+
}
|
|
1021
|
+
/** Record one SM-2 self-rating (1 again / 4 remembered / 5 mastered). */
|
|
1022
|
+
async recordReview(lessonId, quality) {
|
|
1023
|
+
await fetchJson("/lookatstudy/api/review", {
|
|
1024
|
+
method: "POST",
|
|
1025
|
+
headers: { "content-type": "application/json" },
|
|
1026
|
+
body: JSON.stringify({
|
|
1027
|
+
lessonId,
|
|
1028
|
+
quality
|
|
1029
|
+
})
|
|
1030
|
+
});
|
|
1031
|
+
this.refresh();
|
|
1032
|
+
}
|
|
559
1033
|
/** Delete one notebook entry from a lesson's Cornell zones. */
|
|
560
1034
|
async deleteNote(lessonId, noteId) {
|
|
561
1035
|
await fetchJson("/lookatstudy/api/note/delete", {
|
|
@@ -643,6 +1117,8 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
643
1117
|
searchLessons: studyStore.searchLessons.bind(studyStore),
|
|
644
1118
|
deleteCourse: studyStore.deleteCourse.bind(studyStore),
|
|
645
1119
|
deleteNote: studyStore.deleteNote.bind(studyStore),
|
|
1120
|
+
addUserNote: studyStore.addUserNote.bind(studyStore),
|
|
1121
|
+
recordReview: studyStore.recordReview.bind(studyStore),
|
|
646
1122
|
bindLessonSession: studyStore.bindLessonSession.bind(studyStore)
|
|
647
1123
|
};
|
|
648
1124
|
}
|
|
@@ -822,6 +1298,12 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
822
1298
|
}
|
|
823
1299
|
//#endregion
|
|
824
1300
|
//#region src/client/enhance.ts
|
|
1301
|
+
var enhance_exports = /* @__PURE__ */ __exportAll({
|
|
1302
|
+
enhanceCode: () => enhanceCode,
|
|
1303
|
+
enhanceDiagrams: () => enhanceDiagrams,
|
|
1304
|
+
enhanceMath: () => enhanceMath,
|
|
1305
|
+
enhanceRendered: () => enhanceRendered
|
|
1306
|
+
});
|
|
825
1307
|
/**
|
|
826
1308
|
* Post-render blackboard enhancement (SPEC Phase 2): math (KaTeX), code
|
|
827
1309
|
* highlighting (shiki), and mermaid diagrams — every dependency loaded from a
|
|
@@ -1445,8 +1927,8 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
1445
1927
|
"col.tutor": "导师",
|
|
1446
1928
|
"col.bb": "黑板",
|
|
1447
1929
|
"pane.rail": "课程",
|
|
1448
|
-
"pane.
|
|
1449
|
-
"pane.
|
|
1930
|
+
"pane.chat": "对话",
|
|
1931
|
+
"pane.note": "黑板",
|
|
1450
1932
|
"viewtab.teach": "讲解",
|
|
1451
1933
|
"viewtab.cmap": "概念图",
|
|
1452
1934
|
"viewtab.cmap.title": "本课概念之间的关系图",
|
|
@@ -1470,6 +1952,71 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
1470
1952
|
"rail.empty.placeholder": "GitHub 仓库链接,如 microsoft/AI-For-Beginners",
|
|
1471
1953
|
"rail.empty.button": "导入",
|
|
1472
1954
|
"composer.send": "发送",
|
|
1955
|
+
"quiz.card.title": "练习",
|
|
1956
|
+
"quiz.progress": "{cur}/{total}",
|
|
1957
|
+
"quiz.next": "下一题",
|
|
1958
|
+
"quiz.finish": "看成绩单",
|
|
1959
|
+
"quiz.score": "成绩:{correct}/{total}",
|
|
1960
|
+
"quiz.right": "答对了",
|
|
1961
|
+
"quiz.wrong": "答错了",
|
|
1962
|
+
"quiz.youChose": "你选了",
|
|
1963
|
+
"quiz.answer": "正确答案",
|
|
1964
|
+
"quiz.hook": "练习卡完成:{correct}/{total} 正确。请简短点评,针对错题讲讲思路。",
|
|
1965
|
+
"quiz.action.explain-wrong": "讲讲错题",
|
|
1966
|
+
"quiz.action.retry": "再来一组",
|
|
1967
|
+
"quiz.action.go-deeper": "深入原理",
|
|
1968
|
+
"quiz.action.mark-mastered": "标记我掌握了",
|
|
1969
|
+
"quiz.action.markMastered.hint": "会让导师出综合题检验,通过即标记掌握",
|
|
1970
|
+
"quiz.action.next-topic": "下一个知识点",
|
|
1971
|
+
"artifact.lines": "第 {from}-{to} 行:",
|
|
1972
|
+
"artifact.expand": "放大查看",
|
|
1973
|
+
"artifact.guess.title": "猜一猜",
|
|
1974
|
+
"artifact.guess.pick": "我选「{label}」",
|
|
1975
|
+
"artifact.guess.wait": "已记下你的直觉——导师下回合揭晓,看看你想得对不对",
|
|
1976
|
+
"artifact.sedimented": "有新的学习产物沉淀进笔记",
|
|
1977
|
+
"zone.artifacts": "AI 产物",
|
|
1978
|
+
"note.quote.ask": "提问这段",
|
|
1979
|
+
"note.quote.save": "加到笔记",
|
|
1980
|
+
"note.quote.template": "请讲解这段:「{text}」",
|
|
1981
|
+
"note.saved": "已存入记录区笔记",
|
|
1982
|
+
"review.rate.title": "复习自评——这课现在记得多牢?",
|
|
1983
|
+
"review.again": "再来一次",
|
|
1984
|
+
"review.remembered": "记住了",
|
|
1985
|
+
"review.mastered": "完全掌握",
|
|
1986
|
+
"review.done.good": "已安排下次复习",
|
|
1987
|
+
"review.done.again": "明天再来一遍",
|
|
1988
|
+
"review.jump": "点开这一课去复习",
|
|
1989
|
+
"review.nudge": "有 {n} 课到了复习时间,先回来练一遍?",
|
|
1990
|
+
"review.nudge.go": "去复习",
|
|
1991
|
+
"rail.search.jump": "打开这一课",
|
|
1992
|
+
"proposal.banner": "《{lesson}》可以提前毕业了",
|
|
1993
|
+
"proposal.accept": "接受",
|
|
1994
|
+
"proposal.decline": "再练练",
|
|
1995
|
+
"proposal.accept.msg": "我接受《{lesson}》的掌握度提议,帮我标记为掌握。",
|
|
1996
|
+
"proposal.decline.msg": "《{lesson}》我先再练练,暂不接受提前毕业。",
|
|
1997
|
+
"tutor.thinking": "导师思考中…",
|
|
1998
|
+
"header.xp": "经验值 {xp}",
|
|
1999
|
+
"header.streak": "连续学习天数",
|
|
2000
|
+
"header.level": "等级",
|
|
2001
|
+
"map.node.locked": "锁定 — 先完成前面的课时",
|
|
2002
|
+
"map.node.due": "到了复习时间",
|
|
2003
|
+
"map.exam.locked": "考试未解锁 — 本章节全部课时掌握度 ≥50% 后开放",
|
|
2004
|
+
"companion.poke": "戳一戳伴学伙伴",
|
|
2005
|
+
"companion.poked": "伴学伙伴向你眨了眨眼",
|
|
2006
|
+
"companion.form.ember": "小焰",
|
|
2007
|
+
"companion.form.frost": "霜绒",
|
|
2008
|
+
"companion.form.moss": "苔芽",
|
|
2009
|
+
"companion.form.star": "星尘",
|
|
2010
|
+
"companion.form.ink": "墨墨",
|
|
2011
|
+
"settings.companion": "伴学伙伴",
|
|
2012
|
+
"settings.companion.hint": "选择学习面板右下角的伴学生物形态。",
|
|
2013
|
+
"quiz.msg.explain-wrong": "我刚才有题答错了,帮我讲讲为什么错、正确的思路是什么。",
|
|
2014
|
+
"quiz.msg.retry": "再来一组类似的题巩固一下。",
|
|
2015
|
+
"quiz.msg.go-deeper": "这组我答得不错,帮我深入讲讲背后的原理和容易混淆的地方。",
|
|
2016
|
+
"quiz.msg.mark-mastered": "这课我觉得掌握了,帮我确认一下——出个综合题检验,通过了就标记为掌握。",
|
|
2017
|
+
"quiz.msg.next-topic": "进入下一个知识点。",
|
|
2018
|
+
"toast.region": "通知",
|
|
2019
|
+
"toast.close": "关闭",
|
|
1473
2020
|
"composer.busy": "导师正在回复…",
|
|
1474
2021
|
"rail.empty.demo": "导入示例课程",
|
|
1475
2022
|
"rail.mastered": "毕业 {mastered}/{total} 课",
|
|
@@ -1583,8 +2130,8 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
1583
2130
|
"col.tutor": "Tutor",
|
|
1584
2131
|
"col.bb": "Blackboard",
|
|
1585
2132
|
"pane.rail": "Courses",
|
|
1586
|
-
"pane.
|
|
1587
|
-
"pane.
|
|
2133
|
+
"pane.chat": "Chat",
|
|
2134
|
+
"pane.note": "Board",
|
|
1588
2135
|
"viewtab.teach": "Teach",
|
|
1589
2136
|
"viewtab.cmap": "Concepts",
|
|
1590
2137
|
"viewtab.cmap.title": "The lesson's concepts relate",
|
|
@@ -1608,6 +2155,71 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
1608
2155
|
"rail.empty.placeholder": "GitHub repo link, e.g. microsoft/AI-For-Beginners",
|
|
1609
2156
|
"rail.empty.button": "Import",
|
|
1610
2157
|
"composer.send": "Send",
|
|
2158
|
+
"quiz.card.title": "Practice",
|
|
2159
|
+
"quiz.progress": "{cur}/{total}",
|
|
2160
|
+
"quiz.next": "Next",
|
|
2161
|
+
"quiz.finish": "See score",
|
|
2162
|
+
"quiz.score": "Score: {correct}/{total}",
|
|
2163
|
+
"quiz.right": "Correct",
|
|
2164
|
+
"quiz.wrong": "Wrong",
|
|
2165
|
+
"quiz.youChose": "You chose",
|
|
2166
|
+
"quiz.answer": "Answer",
|
|
2167
|
+
"quiz.hook": "Practice card finished: {correct}/{total} correct. Please comment briefly and walk through the wrong ones.",
|
|
2168
|
+
"quiz.action.explain-wrong": "Explain my wrong answers",
|
|
2169
|
+
"quiz.action.retry": "Another set",
|
|
2170
|
+
"quiz.action.go-deeper": "Go deeper",
|
|
2171
|
+
"quiz.action.mark-mastered": "Mark me as mastered",
|
|
2172
|
+
"quiz.action.markMastered.hint": "Asks the tutor to verify with a final question, then marks mastery",
|
|
2173
|
+
"quiz.action.next-topic": "Next topic",
|
|
2174
|
+
"artifact.lines": "Lines {from}-{to}: ",
|
|
2175
|
+
"artifact.expand": "Expand",
|
|
2176
|
+
"artifact.guess.title": "Take a guess",
|
|
2177
|
+
"artifact.guess.pick": "I pick \"{label}\"",
|
|
2178
|
+
"artifact.guess.wait": "Your instinct is noted — the tutor reveals next turn",
|
|
2179
|
+
"artifact.sedimented": "A new artifact settled into the notebook",
|
|
2180
|
+
"zone.artifacts": "AI artifacts",
|
|
2181
|
+
"note.quote.ask": "Ask about this",
|
|
2182
|
+
"note.quote.save": "Add note",
|
|
2183
|
+
"note.quote.template": "Please explain this passage: \"{text}\"",
|
|
2184
|
+
"note.saved": "Saved to the record zone",
|
|
2185
|
+
"review.rate.title": "Review self-check — how well do you remember?",
|
|
2186
|
+
"review.again": "Again",
|
|
2187
|
+
"review.remembered": "Remembered",
|
|
2188
|
+
"review.mastered": "Mastered",
|
|
2189
|
+
"review.done.good": "Next review scheduled",
|
|
2190
|
+
"review.done.again": "Back again tomorrow",
|
|
2191
|
+
"review.jump": "Open this lesson to review",
|
|
2192
|
+
"review.nudge": "{n} lessons are due for review — come practice first?",
|
|
2193
|
+
"review.nudge.go": "Review now",
|
|
2194
|
+
"rail.search.jump": "Open this lesson",
|
|
2195
|
+
"proposal.banner": "{lesson} is ready to graduate early",
|
|
2196
|
+
"proposal.accept": "Accept",
|
|
2197
|
+
"proposal.decline": "Keep practicing",
|
|
2198
|
+
"proposal.accept.msg": "I accept the mastery proposal for \"{lesson}\" — mark it mastered.",
|
|
2199
|
+
"proposal.decline.msg": "I want more practice on \"{lesson}\" before graduating.",
|
|
2200
|
+
"tutor.thinking": "The tutor is thinking…",
|
|
2201
|
+
"header.xp": "{xp} XP",
|
|
2202
|
+
"header.streak": "Day streak",
|
|
2203
|
+
"header.level": "Level",
|
|
2204
|
+
"map.node.locked": "Locked — finish the previous lessons first",
|
|
2205
|
+
"map.node.due": "Due for review",
|
|
2206
|
+
"map.exam.locked": "Exam locked — every lesson in this section needs ≥50% mastery",
|
|
2207
|
+
"companion.poke": "Poke the companion",
|
|
2208
|
+
"companion.poked": "The companion winks at you",
|
|
2209
|
+
"companion.form.ember": "Ember",
|
|
2210
|
+
"companion.form.frost": "Frost",
|
|
2211
|
+
"companion.form.moss": "Moss",
|
|
2212
|
+
"companion.form.star": "Stardust",
|
|
2213
|
+
"companion.form.ink": "Ink",
|
|
2214
|
+
"settings.companion": "Companion",
|
|
2215
|
+
"settings.companion.hint": "Choose the companion creature in the study panel corner.",
|
|
2216
|
+
"quiz.msg.explain-wrong": "I got some questions wrong — walk me through why and the right line of thinking.",
|
|
2217
|
+
"quiz.msg.retry": "Give me another similar set to consolidate.",
|
|
2218
|
+
"quiz.msg.go-deeper": "I did well on this set — go deeper into the principles and common confusions.",
|
|
2219
|
+
"quiz.msg.mark-mastered": "I feel I have mastered this lesson — verify me with a final question and mark it mastered if I pass.",
|
|
2220
|
+
"quiz.msg.next-topic": "Move on to the next topic.",
|
|
2221
|
+
"toast.region": "Notifications",
|
|
2222
|
+
"toast.close": "Close",
|
|
1611
2223
|
"composer.busy": "The tutor is replying…",
|
|
1612
2224
|
"rail.empty.demo": "Import the demo course",
|
|
1613
2225
|
"rail.mastered": "{mastered}/{total} lessons graduated",
|
|
@@ -2356,6 +2968,782 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2356
2968
|
}
|
|
2357
2969
|
};
|
|
2358
2970
|
//#endregion
|
|
2971
|
+
//#region src/client/toast.ts
|
|
2972
|
+
/** Per-severity defaults (upstream Toast.tsx): error 6s, warning 5s, rest 4s. */
|
|
2973
|
+
const SEVERITY_DURATION = {
|
|
2974
|
+
default: 4e3,
|
|
2975
|
+
success: 4e3,
|
|
2976
|
+
info: 4e3,
|
|
2977
|
+
warning: 5e3,
|
|
2978
|
+
error: 6e3
|
|
2979
|
+
};
|
|
2980
|
+
const browserScheduler = (fn, ms) => {
|
|
2981
|
+
const handle = window.setTimeout(fn, ms);
|
|
2982
|
+
return () => {
|
|
2983
|
+
window.clearTimeout(handle);
|
|
2984
|
+
};
|
|
2985
|
+
};
|
|
2986
|
+
var ToastStore = class {
|
|
2987
|
+
items = [];
|
|
2988
|
+
nextId = 0;
|
|
2989
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2990
|
+
cancellers = /* @__PURE__ */ new Map();
|
|
2991
|
+
scheduler;
|
|
2992
|
+
constructor(scheduler = browserScheduler) {
|
|
2993
|
+
this.scheduler = scheduler;
|
|
2994
|
+
}
|
|
2995
|
+
getSnapshot() {
|
|
2996
|
+
return this.items;
|
|
2997
|
+
}
|
|
2998
|
+
subscribe(listener) {
|
|
2999
|
+
this.listeners.add(listener);
|
|
3000
|
+
return () => {
|
|
3001
|
+
this.listeners.delete(listener);
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
emit() {
|
|
3005
|
+
for (const listener of this.listeners) listener();
|
|
3006
|
+
}
|
|
3007
|
+
/** Enqueue one toast; its auto-exit is scheduled at its duration. */
|
|
3008
|
+
show(message, opts) {
|
|
3009
|
+
const severity = opts?.severity ?? "default";
|
|
3010
|
+
const id = ++this.nextId;
|
|
3011
|
+
const item = {
|
|
3012
|
+
id,
|
|
3013
|
+
message,
|
|
3014
|
+
severity,
|
|
3015
|
+
duration: opts?.duration ?? SEVERITY_DURATION[severity],
|
|
3016
|
+
action: opts?.action,
|
|
3017
|
+
exiting: false
|
|
3018
|
+
};
|
|
3019
|
+
this.items = [...this.items, item];
|
|
3020
|
+
this.cancellers.set(id, this.scheduler(() => this.startExit(id), item.duration));
|
|
3021
|
+
this.emit();
|
|
3022
|
+
return id;
|
|
3023
|
+
}
|
|
3024
|
+
/** Begin the exit (dismiss click or the duration timer); cancels the timer. */
|
|
3025
|
+
startExit(id) {
|
|
3026
|
+
const cancel = this.cancellers.get(id);
|
|
3027
|
+
if (cancel !== void 0) {
|
|
3028
|
+
cancel();
|
|
3029
|
+
this.cancellers.delete(id);
|
|
3030
|
+
}
|
|
3031
|
+
this.items = this.items.map((t) => t.id === id ? {
|
|
3032
|
+
...t,
|
|
3033
|
+
exiting: true
|
|
3034
|
+
} : t);
|
|
3035
|
+
this.emit();
|
|
3036
|
+
}
|
|
3037
|
+
/** Actually remove — called by the view when the exit animation ends. */
|
|
3038
|
+
finish(id) {
|
|
3039
|
+
this.items = this.items.filter((t) => t.id !== id);
|
|
3040
|
+
this.cancellers.delete(id);
|
|
3041
|
+
this.emit();
|
|
3042
|
+
}
|
|
3043
|
+
/** Drop everything now (the panel unmounted — nothing outlives the panel). */
|
|
3044
|
+
clear() {
|
|
3045
|
+
for (const cancel of this.cancellers.values()) cancel();
|
|
3046
|
+
this.cancellers.clear();
|
|
3047
|
+
this.items = [];
|
|
3048
|
+
this.emit();
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
/** The panel-wide singleton stack; consumers call showStudyToast. */
|
|
3052
|
+
const toastStore = new ToastStore();
|
|
3053
|
+
function showStudyToast(message, opts) {
|
|
3054
|
+
return toastStore.show(message, opts);
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* The exits after one finished card — never fewer than two (the anti-dead-end
|
|
3058
|
+
* promise). Wrong answers → patch the hole first; perfect + near-mastered →
|
|
3059
|
+
* offer the mastery exit; perfect + already graduated → move on.
|
|
3060
|
+
* Pure port of upstream getPostQuizActions.
|
|
3061
|
+
*/
|
|
3062
|
+
function getPostQuizActions(score, mastery) {
|
|
3063
|
+
const { correct, total } = score;
|
|
3064
|
+
const allCorrect = total > 0 && correct === total;
|
|
3065
|
+
const hasWrong = total > 0 && correct < total;
|
|
3066
|
+
const alreadyMastered = mastery !== null && mastery >= .9;
|
|
3067
|
+
const nearMastered = mastery !== null && mastery >= .85;
|
|
3068
|
+
if (hasWrong) return [{
|
|
3069
|
+
id: "explain-wrong",
|
|
3070
|
+
advancesMastery: false
|
|
3071
|
+
}, {
|
|
3072
|
+
id: "retry",
|
|
3073
|
+
advancesMastery: false
|
|
3074
|
+
}];
|
|
3075
|
+
if (allCorrect && alreadyMastered) return [{
|
|
3076
|
+
id: "next-topic",
|
|
3077
|
+
advancesMastery: false
|
|
3078
|
+
}, {
|
|
3079
|
+
id: "go-deeper",
|
|
3080
|
+
advancesMastery: false
|
|
3081
|
+
}];
|
|
3082
|
+
if (allCorrect && nearMastered) return [{
|
|
3083
|
+
id: "mark-mastered",
|
|
3084
|
+
advancesMastery: true
|
|
3085
|
+
}, {
|
|
3086
|
+
id: "go-deeper",
|
|
3087
|
+
advancesMastery: false
|
|
3088
|
+
}];
|
|
3089
|
+
if (allCorrect) return [{
|
|
3090
|
+
id: "go-deeper",
|
|
3091
|
+
advancesMastery: false
|
|
3092
|
+
}, {
|
|
3093
|
+
id: "retry",
|
|
3094
|
+
advancesMastery: false
|
|
3095
|
+
}];
|
|
3096
|
+
return [{
|
|
3097
|
+
id: "explain-wrong",
|
|
3098
|
+
advancesMastery: false
|
|
3099
|
+
}, {
|
|
3100
|
+
id: "retry",
|
|
3101
|
+
advancesMastery: false
|
|
3102
|
+
}];
|
|
3103
|
+
}
|
|
3104
|
+
/** localStorage key (upstream quizProgressKey semantics: per artifact). */
|
|
3105
|
+
function quizProgressKey(lessonId, artifactId) {
|
|
3106
|
+
return `dsh-plugin-lookatstudy:quiz:${lessonId}:${artifactId}`;
|
|
3107
|
+
}
|
|
3108
|
+
function loadQuizProgress(lessonId, artifactId, questionCount, storage = localStorage) {
|
|
3109
|
+
try {
|
|
3110
|
+
const raw = storage.getItem(quizProgressKey(lessonId, artifactId));
|
|
3111
|
+
if (raw === null) return {
|
|
3112
|
+
answers: Array(questionCount).fill(-1),
|
|
3113
|
+
done: false
|
|
3114
|
+
};
|
|
3115
|
+
const parsed = JSON.parse(raw);
|
|
3116
|
+
const answers = Array.isArray(parsed.answers) ? parsed.answers.map((a) => typeof a === "number" ? a : -1) : [];
|
|
3117
|
+
const padded = Array(questionCount).fill(-1);
|
|
3118
|
+
for (let i = 0; i < Math.min(answers.length, questionCount); i += 1) padded[i] = answers[i];
|
|
3119
|
+
return {
|
|
3120
|
+
answers: padded,
|
|
3121
|
+
done: parsed.done === true
|
|
3122
|
+
};
|
|
3123
|
+
} catch {
|
|
3124
|
+
return {
|
|
3125
|
+
answers: Array(questionCount).fill(-1),
|
|
3126
|
+
done: false
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
function saveQuizProgress(lessonId, artifactId, progress, storage = localStorage) {
|
|
3131
|
+
try {
|
|
3132
|
+
storage.setItem(quizProgressKey(lessonId, artifactId), JSON.stringify(progress));
|
|
3133
|
+
} catch {}
|
|
3134
|
+
}
|
|
3135
|
+
/** The score over recorded answers (pure). */
|
|
3136
|
+
function quizScore(data, answers) {
|
|
3137
|
+
let correct = 0;
|
|
3138
|
+
let total = 0;
|
|
3139
|
+
data.questions.forEach((q, i) => {
|
|
3140
|
+
const choice = answers[i];
|
|
3141
|
+
if (choice === void 0 || choice < 0) return;
|
|
3142
|
+
total += 1;
|
|
3143
|
+
if (choice === q.answer) correct += 1;
|
|
3144
|
+
});
|
|
3145
|
+
return {
|
|
3146
|
+
correct,
|
|
3147
|
+
total
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
const ACTION_ICONS = {
|
|
3151
|
+
"explain-wrong": IconBookFill16,
|
|
3152
|
+
"retry": IconRefreshOutline16,
|
|
3153
|
+
"go-deeper": IconBoltFill16,
|
|
3154
|
+
"mark-mastered": IconCrownFill16,
|
|
3155
|
+
"next-topic": IconStarFill16
|
|
3156
|
+
};
|
|
3157
|
+
/**
|
|
3158
|
+
* The card: one question at a time, click-to-answer (instant local judge +
|
|
3159
|
+
* explanation reveal — the cursor is explicit, so the reveal is never skipped),
|
|
3160
|
+
* progress persisted per artifact; the score screen is an explicit step, and
|
|
3161
|
+
* on first arrival its completion hook goes to the tutor exactly once.
|
|
3162
|
+
*/
|
|
3163
|
+
function QuizCard({ lessonId, artifactId, data, masteryPct, send, onFinished }) {
|
|
3164
|
+
const questions = data.questions;
|
|
3165
|
+
const [progress, setProgress] = (0, react.useState)(() => loadQuizProgress(lessonId, artifactId, questions.length));
|
|
3166
|
+
const [cursor, setCursor] = (0, react.useState)(0);
|
|
3167
|
+
const [showScore, setShowScore] = (0, react.useState)(false);
|
|
3168
|
+
const [hookSent, setHookSent] = (0, react.useState)(false);
|
|
3169
|
+
(0, react.useEffect)(() => {
|
|
3170
|
+
const next = loadQuizProgress(lessonId, artifactId, questions.length);
|
|
3171
|
+
setProgress(next);
|
|
3172
|
+
setShowScore(next.done);
|
|
3173
|
+
const firstUnanswered = next.answers.findIndex((a) => a < 0);
|
|
3174
|
+
setCursor(firstUnanswered === -1 ? questions.length - 1 : firstUnanswered);
|
|
3175
|
+
setHookSent(false);
|
|
3176
|
+
}, [
|
|
3177
|
+
lessonId,
|
|
3178
|
+
artifactId,
|
|
3179
|
+
questions.length
|
|
3180
|
+
]);
|
|
3181
|
+
const allAnswered = progress.answers.every((a) => a >= 0);
|
|
3182
|
+
const score = quizScore(data, progress.answers);
|
|
3183
|
+
const mastery = masteryPct === null ? null : masteryPct / 100;
|
|
3184
|
+
(0, react.useEffect)(() => {
|
|
3185
|
+
if (!showScore || !allAnswered || hookSent) return;
|
|
3186
|
+
setHookSent(true);
|
|
3187
|
+
onFinished?.(score.correct === score.total);
|
|
3188
|
+
const next = {
|
|
3189
|
+
answers: [...progress.answers],
|
|
3190
|
+
done: true
|
|
3191
|
+
};
|
|
3192
|
+
setProgress(next);
|
|
3193
|
+
saveQuizProgress(lessonId, artifactId, next);
|
|
3194
|
+
send(tr("quiz.hook", {
|
|
3195
|
+
correct: score.correct,
|
|
3196
|
+
total: score.total
|
|
3197
|
+
}));
|
|
3198
|
+
}, [
|
|
3199
|
+
showScore,
|
|
3200
|
+
allAnswered,
|
|
3201
|
+
hookSent,
|
|
3202
|
+
lessonId,
|
|
3203
|
+
artifactId,
|
|
3204
|
+
progress.answers,
|
|
3205
|
+
score.correct,
|
|
3206
|
+
score.total,
|
|
3207
|
+
send
|
|
3208
|
+
]);
|
|
3209
|
+
const answer = (choice) => {
|
|
3210
|
+
const next = {
|
|
3211
|
+
answers: [...progress.answers],
|
|
3212
|
+
done: progress.done
|
|
3213
|
+
};
|
|
3214
|
+
next.answers[cursor] = choice;
|
|
3215
|
+
setProgress(next);
|
|
3216
|
+
saveQuizProgress(lessonId, artifactId, next);
|
|
3217
|
+
};
|
|
3218
|
+
if (showScore && allAnswered) {
|
|
3219
|
+
const actions = getPostQuizActions(score, mastery);
|
|
3220
|
+
return (0, react.createElement)("div", {
|
|
3221
|
+
className: "lks-qcard",
|
|
3222
|
+
"data-lks-quiz": artifactId
|
|
3223
|
+
}, (0, react.createElement)("div", { className: "lks-qcard-head" }, (0, react.createElement)(IconStarFill16, { size: 14 }), tr("quiz.score", {
|
|
3224
|
+
correct: score.correct,
|
|
3225
|
+
total: score.total
|
|
3226
|
+
})), (0, react.createElement)("div", { className: "lks-qcard-review" }, ...questions.map((q, i) => {
|
|
3227
|
+
const choice = progress.answers[i] ?? -1;
|
|
3228
|
+
const right = choice === q.answer;
|
|
3229
|
+
return (0, react.createElement)("div", {
|
|
3230
|
+
key: i,
|
|
3231
|
+
className: `lks-qcard-q review${right ? "" : " wrong"}`
|
|
3232
|
+
}, (0, react.createElement)("div", { className: "lks-qcard-prompt" }, `${i + 1}. ${q.prompt}`), (0, react.createElement)("div", { className: "lks-qcard-ans" }, right ? "" : `${tr("quiz.youChose")}:${q.options[choice] ?? "—"} · `, `${tr("quiz.answer")}:${q.options[q.answer] ?? "—"}`), (0, react.createElement)("div", { className: "lks-qcard-expl" }, q.explanation));
|
|
3233
|
+
})), (0, react.createElement)("div", { className: "lks-qcard-actions" }, ...actions.map((a) => (0, react.createElement)("button", {
|
|
3234
|
+
key: a.id,
|
|
3235
|
+
className: "lks-qcard-action",
|
|
3236
|
+
title: a.advancesMastery ? tr("quiz.action.markMastered.hint") : void 0,
|
|
3237
|
+
onClick: () => {
|
|
3238
|
+
send(tr(`quiz.msg.${a.id}`));
|
|
3239
|
+
}
|
|
3240
|
+
}, (0, react.createElement)(ACTION_ICONS[a.id], { size: 13 }), tr(`quiz.action.${a.id}`)))));
|
|
3241
|
+
}
|
|
3242
|
+
const q = questions[cursor];
|
|
3243
|
+
const chosen = progress.answers[cursor] ?? -1;
|
|
3244
|
+
const options = q.options.map((opt, i) => {
|
|
3245
|
+
const picked = chosen === i;
|
|
3246
|
+
const isRight = i === q.answer;
|
|
3247
|
+
return (0, react.createElement)("button", {
|
|
3248
|
+
key: i,
|
|
3249
|
+
className: `lks-qcard-opt${chosen < 0 ? "" : picked ? isRight ? " right" : " wrong" : isRight ? " right dim" : ""}`,
|
|
3250
|
+
disabled: chosen >= 0,
|
|
3251
|
+
onClick: () => {
|
|
3252
|
+
answer(i);
|
|
3253
|
+
}
|
|
3254
|
+
}, opt);
|
|
3255
|
+
});
|
|
3256
|
+
const explanation = chosen < 0 ? null : (0, react.createElement)("div", { className: "lks-qcard-expl" }, `${chosen === q.answer ? tr("quiz.right") : tr("quiz.wrong")} — ${q.explanation}`, (0, react.createElement)("div", { style: { marginTop: "6px" } }, (0, react.createElement)("button", {
|
|
3257
|
+
className: "lks-qcard-next",
|
|
3258
|
+
onClick: () => {
|
|
3259
|
+
if (cursor + 1 < questions.length) setCursor(cursor + 1);
|
|
3260
|
+
else setShowScore(true);
|
|
3261
|
+
}
|
|
3262
|
+
}, cursor + 1 < questions.length ? tr("quiz.next") : tr("quiz.finish"))));
|
|
3263
|
+
return (0, react.createElement)("div", {
|
|
3264
|
+
className: "lks-qcard",
|
|
3265
|
+
"data-lks-quiz": artifactId
|
|
3266
|
+
}, (0, react.createElement)("div", { className: "lks-qcard-head" }, (0, react.createElement)(IconBookFill16, { size: 14 }), data.title ?? tr("quiz.card.title"), (0, react.createElement)("span", { className: "lks-qcard-count" }, tr("quiz.progress", {
|
|
3267
|
+
cur: cursor + 1,
|
|
3268
|
+
total: questions.length
|
|
3269
|
+
}))), (0, react.createElement)("div", { className: "lks-qcard-q" }, (0, react.createElement)("div", { className: "lks-qcard-prompt" }, q.prompt), (0, react.createElement)("div", { className: "lks-qcard-opts" }, ...options), explanation));
|
|
3270
|
+
}
|
|
3271
|
+
//#endregion
|
|
3272
|
+
//#region src/client/artifact-cards.tsx
|
|
3273
|
+
/**
|
|
3274
|
+
* The artifact cards (0.15.0 P1b, upstream artifacts port): compare tables,
|
|
3275
|
+
* code walkthroughs, mermaid diagrams (with the expand modal), and the
|
|
3276
|
+
* opening guess — plus the seen-tracking that feeds the notebook badge and
|
|
3277
|
+
* the "new artifact" toast. Rendering is lazy where it is heavy (mermaid via
|
|
3278
|
+
* the enhance pipeline); everything else is plain dsw-token DOM.
|
|
3279
|
+
* @module dsh-plugin-lookatstudy/client/artifact-cards
|
|
3280
|
+
*/
|
|
3281
|
+
/** localStorage key for the artifact ids the learner has already seen. */
|
|
3282
|
+
function seenArtifactsKey(lessonId) {
|
|
3283
|
+
return `dsh-plugin-lookatstudy:seen-artifacts:${lessonId}`;
|
|
3284
|
+
}
|
|
3285
|
+
/** Which artifact ids are new (not yet seen); pure over the stored set. */
|
|
3286
|
+
function unseenArtifacts(lessonId, artifacts, storage = localStorage) {
|
|
3287
|
+
let seen;
|
|
3288
|
+
try {
|
|
3289
|
+
const raw = storage.getItem(seenArtifactsKey(lessonId));
|
|
3290
|
+
seen = new Set(Array.isArray(JSON.parse(raw ?? "[]")) ? JSON.parse(raw ?? "[]") : []);
|
|
3291
|
+
} catch {
|
|
3292
|
+
seen = /* @__PURE__ */ new Set();
|
|
3293
|
+
}
|
|
3294
|
+
return artifacts.filter((a) => !seen.has(a.id)).map((a) => a.id);
|
|
3295
|
+
}
|
|
3296
|
+
/** Mark artifact ids seen (opening the notebook counts as seeing them). */
|
|
3297
|
+
function markArtifactsSeen(lessonId, ids, storage = localStorage) {
|
|
3298
|
+
try {
|
|
3299
|
+
let seen = [];
|
|
3300
|
+
try {
|
|
3301
|
+
const parsed = JSON.parse(storage.getItem(seenArtifactsKey(lessonId)) ?? "[]");
|
|
3302
|
+
if (Array.isArray(parsed)) seen = parsed.filter((x) => typeof x === "string");
|
|
3303
|
+
} catch {}
|
|
3304
|
+
storage.setItem(seenArtifactsKey(lessonId), JSON.stringify([.../* @__PURE__ */ new Set([...seen, ...ids])]));
|
|
3305
|
+
} catch {}
|
|
3306
|
+
}
|
|
3307
|
+
/** The cards render mermaid through the shared CDN pipeline
|
|
3308
|
+
* (enhanceDiagrams consumes pre > code.lang-mermaid blocks). */
|
|
3309
|
+
async function renderMermaidInto(el, code, onError) {
|
|
3310
|
+
try {
|
|
3311
|
+
const { enhanceDiagrams } = await Promise.resolve().then(() => enhance_exports);
|
|
3312
|
+
el.textContent = "";
|
|
3313
|
+
const pre = document.createElement("pre");
|
|
3314
|
+
const codeEl = document.createElement("code");
|
|
3315
|
+
codeEl.className = "lang-mermaid";
|
|
3316
|
+
codeEl.textContent = code;
|
|
3317
|
+
pre.appendChild(codeEl);
|
|
3318
|
+
el.appendChild(pre);
|
|
3319
|
+
if (await enhanceDiagrams(el) === 0) onError();
|
|
3320
|
+
} catch {
|
|
3321
|
+
onError();
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
/** Compare table card (headers + rows). */
|
|
3325
|
+
function CompareTableCard({ artifact }) {
|
|
3326
|
+
const headers = artifact.data.headers ?? [];
|
|
3327
|
+
const rows = artifact.data.rows ?? [];
|
|
3328
|
+
return (0, react.createElement)("div", {
|
|
3329
|
+
className: "lks-acard",
|
|
3330
|
+
"data-lks-artifact": artifact.id
|
|
3331
|
+
}, (0, react.createElement)("div", { className: "lks-acard-head" }, artifact.title), (0, react.createElement)("table", { className: "lks-acard-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, ...headers.map((h, i) => (0, react.createElement)("th", { key: i }, h)))), (0, react.createElement)("tbody", null, ...rows.map((row, r) => (0, react.createElement)("tr", { key: r }, ...row.map((c, i) => (0, react.createElement)("td", { key: i }, c)))))));
|
|
3332
|
+
}
|
|
3333
|
+
/** Code walkthrough card: line-numbered code + per-segment notes. */
|
|
3334
|
+
function CodeWalkthroughCard({ artifact }) {
|
|
3335
|
+
const language = typeof artifact.data.language === "string" ? artifact.data.language : "text";
|
|
3336
|
+
const code = typeof artifact.data.code === "string" ? artifact.data.code : "";
|
|
3337
|
+
const annotations = artifact.data.annotations ?? [];
|
|
3338
|
+
const lines = code.split("\n");
|
|
3339
|
+
return (0, react.createElement)("div", {
|
|
3340
|
+
className: "lks-acard",
|
|
3341
|
+
"data-lks-artifact": artifact.id
|
|
3342
|
+
}, (0, react.createElement)("div", { className: "lks-acard-head" }, `${artifact.title} · ${language}`), (0, react.createElement)("pre", { className: "lks-acard-code" }, ...lines.map((line, i) => (0, react.createElement)("span", {
|
|
3343
|
+
key: i,
|
|
3344
|
+
className: "lks-acard-line"
|
|
3345
|
+
}, (0, react.createElement)("i", null, String(i + 1)), line, "\n"))), (0, react.createElement)("div", { className: "lks-acard-notes" }, ...annotations.map((a, i) => (0, react.createElement)("div", {
|
|
3346
|
+
key: i,
|
|
3347
|
+
className: "lks-acard-note"
|
|
3348
|
+
}, (0, react.createElement)("b", null, `${tr("artifact.lines", {
|
|
3349
|
+
from: a.lineStart,
|
|
3350
|
+
to: a.lineEnd
|
|
3351
|
+
})}`), a.note))));
|
|
3352
|
+
}
|
|
3353
|
+
/** Mermaid diagram card with an expand modal (upstream DiagramViewerModal). */
|
|
3354
|
+
function DiagramCard({ artifact }) {
|
|
3355
|
+
const mermaid = typeof artifact.data.mermaid === "string" ? artifact.data.mermaid : "";
|
|
3356
|
+
const bodyRef = (0, react.useRef)(null);
|
|
3357
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
3358
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
3359
|
+
(0, react.useEffect)(() => {
|
|
3360
|
+
if (bodyRef.current === null) return;
|
|
3361
|
+
renderMermaidInto(bodyRef.current, mermaid, () => {
|
|
3362
|
+
setFailed(true);
|
|
3363
|
+
});
|
|
3364
|
+
}, [mermaid]);
|
|
3365
|
+
const body = failed ? (0, react.createElement)("pre", { className: "lks-acard-code" }, mermaid) : (0, react.createElement)("div", {
|
|
3366
|
+
className: "lks-acard-diagram",
|
|
3367
|
+
ref: bodyRef
|
|
3368
|
+
});
|
|
3369
|
+
return (0, react.createElement)("div", {
|
|
3370
|
+
className: "lks-acard",
|
|
3371
|
+
"data-lks-artifact": artifact.id
|
|
3372
|
+
}, (0, react.createElement)("div", { className: "lks-acard-head" }, artifact.title, (0, react.createElement)("button", {
|
|
3373
|
+
className: "lks-acard-expand",
|
|
3374
|
+
title: tr("artifact.expand"),
|
|
3375
|
+
onClick: () => {
|
|
3376
|
+
setExpanded(true);
|
|
3377
|
+
}
|
|
3378
|
+
}, "⤢")), body, expanded ? (0, react.createElement)("div", {
|
|
3379
|
+
className: "lks-acard-modal",
|
|
3380
|
+
onClick: () => {
|
|
3381
|
+
setExpanded(false);
|
|
3382
|
+
}
|
|
3383
|
+
}, (0, react.createElement)("div", {
|
|
3384
|
+
className: "lks-acard-modal-body",
|
|
3385
|
+
onClick: (e) => {
|
|
3386
|
+
e.stopPropagation();
|
|
3387
|
+
}
|
|
3388
|
+
}, (0, react.createElement)("div", { className: "lks-acard-modal-title" }, artifact.title, (0, react.createElement)("button", {
|
|
3389
|
+
className: "lks-acard-expand",
|
|
3390
|
+
onClick: () => {
|
|
3391
|
+
setExpanded(false);
|
|
3392
|
+
}
|
|
3393
|
+
}, "×")), failed ? (0, react.createElement)("pre", { className: "lks-acard-code" }, mermaid) : (0, react.createElement)("div", { ref: (el) => {
|
|
3394
|
+
if (el !== null) renderMermaidInto(el, mermaid, () => {
|
|
3395
|
+
setFailed(true);
|
|
3396
|
+
});
|
|
3397
|
+
} }))) : null);
|
|
3398
|
+
}
|
|
3399
|
+
/** The opening guess: two big options, click = pick (sent to the tutor, who
|
|
3400
|
+
* reveals next turn); unscored — no right/wrong language anywhere. */
|
|
3401
|
+
function GuessCard({ artifact, send }) {
|
|
3402
|
+
const prompt = typeof artifact.data.prompt === "string" ? artifact.data.prompt : "";
|
|
3403
|
+
const options = artifact.data.options ?? [];
|
|
3404
|
+
const [picked, setPicked] = (0, react.useState)(null);
|
|
3405
|
+
(0, react.useEffect)(() => {
|
|
3406
|
+
setPicked(null);
|
|
3407
|
+
}, [artifact.id]);
|
|
3408
|
+
return (0, react.createElement)("div", {
|
|
3409
|
+
className: "lks-acard lks-acard-guess",
|
|
3410
|
+
"data-lks-artifact": artifact.id
|
|
3411
|
+
}, (0, react.createElement)("div", { className: "lks-acard-head" }, tr("artifact.guess.title")), (0, react.createElement)("div", { className: "lks-acard-guess-prompt" }, prompt), (0, react.createElement)("div", { className: "lks-acard-guess-opts" }, ...options.map((opt) => (0, react.createElement)("button", {
|
|
3412
|
+
key: opt.id,
|
|
3413
|
+
className: `lks-acard-guess-opt${picked === opt.id ? " picked" : ""}`,
|
|
3414
|
+
disabled: picked !== null,
|
|
3415
|
+
onClick: () => {
|
|
3416
|
+
setPicked(opt.id);
|
|
3417
|
+
send(tr("artifact.guess.pick", { label: opt.label }));
|
|
3418
|
+
}
|
|
3419
|
+
}, opt.label))), picked !== null ? (0, react.createElement)("div", { className: "lks-acard-guess-wait" }, tr("artifact.guess.wait")) : null);
|
|
3420
|
+
}
|
|
3421
|
+
/** Dispatch one artifact row to its card. */
|
|
3422
|
+
function ArtifactCard({ artifact, send }) {
|
|
3423
|
+
switch (artifact.artifactType) {
|
|
3424
|
+
case "compare_table": return (0, react.createElement)(CompareTableCard, { artifact });
|
|
3425
|
+
case "code_walkthrough": return (0, react.createElement)(CodeWalkthroughCard, { artifact });
|
|
3426
|
+
case "diagram": return (0, react.createElement)(DiagramCard, { artifact });
|
|
3427
|
+
case "guess": return (0, react.createElement)(GuessCard, {
|
|
3428
|
+
artifact,
|
|
3429
|
+
send
|
|
3430
|
+
});
|
|
3431
|
+
default: return null;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
//#endregion
|
|
3435
|
+
//#region src/client/companion.tsx
|
|
3436
|
+
/**
|
|
3437
|
+
* The companion creature — the upstream 伴学生物's minimal honest DOM port
|
|
3438
|
+
* (0.15.0 P7): a small being in the panel's bottom-right corner with five
|
|
3439
|
+
* forms, a pure mood machine (idle / talking / celebrating / encouraging),
|
|
3440
|
+
* and the a11y iron rule carried over — reduced-motion renders it static.
|
|
3441
|
+
* Upstream's Electron-native life (mic envelopes, typing squeeze, drag-throw)
|
|
3442
|
+
* has no web-panel equivalent and stays unported by design.
|
|
3443
|
+
* @module dsh-plugin-lookatstudy/client/companion
|
|
3444
|
+
*/
|
|
3445
|
+
const COMPANION_FORMS = [
|
|
3446
|
+
{
|
|
3447
|
+
id: "ember",
|
|
3448
|
+
labelKey: "companion.form.ember",
|
|
3449
|
+
body: "#f59e0b",
|
|
3450
|
+
accent: "#dc2626"
|
|
3451
|
+
},
|
|
3452
|
+
{
|
|
3453
|
+
id: "frost",
|
|
3454
|
+
labelKey: "companion.form.frost",
|
|
3455
|
+
body: "#7dd3fc",
|
|
3456
|
+
accent: "#0284c7"
|
|
3457
|
+
},
|
|
3458
|
+
{
|
|
3459
|
+
id: "moss",
|
|
3460
|
+
labelKey: "companion.form.moss",
|
|
3461
|
+
body: "#86efac",
|
|
3462
|
+
accent: "#16a34a"
|
|
3463
|
+
},
|
|
3464
|
+
{
|
|
3465
|
+
id: "star",
|
|
3466
|
+
labelKey: "companion.form.star",
|
|
3467
|
+
body: "#c4b5fd",
|
|
3468
|
+
accent: "#7c3aed"
|
|
3469
|
+
},
|
|
3470
|
+
{
|
|
3471
|
+
id: "ink",
|
|
3472
|
+
labelKey: "companion.form.ink",
|
|
3473
|
+
body: "#cbd5e1",
|
|
3474
|
+
accent: "#334155"
|
|
3475
|
+
}
|
|
3476
|
+
];
|
|
3477
|
+
/** Normalize a stored form id; junk falls back to 小焰 (upstream's default). */
|
|
3478
|
+
function normalizeCompanionForm(stored) {
|
|
3479
|
+
return COMPANION_FORMS.some((f) => f.id === stored) ? stored : "ember";
|
|
3480
|
+
}
|
|
3481
|
+
function storedCompanionForm() {
|
|
3482
|
+
try {
|
|
3483
|
+
return normalizeCompanionForm(localStorage.getItem("dsh-plugin-lookatstudy:companion-form"));
|
|
3484
|
+
} catch {
|
|
3485
|
+
return "ember";
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
function storeCompanionForm(id) {
|
|
3489
|
+
try {
|
|
3490
|
+
localStorage.setItem("dsh-plugin-lookatstudy:companion-form", id);
|
|
3491
|
+
} catch {}
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* The mood machine — pure. `celebrate` and `encourage` are momentary (the
|
|
3495
|
+
* view decays them back through `decay`); `talking` latches while read-aloud
|
|
3496
|
+
* runs; poke is a self-echoing celebration.
|
|
3497
|
+
*/
|
|
3498
|
+
function nextCompanionMood(current, event) {
|
|
3499
|
+
switch (event) {
|
|
3500
|
+
case "talk-start": return "talking";
|
|
3501
|
+
case "talk-end": return current === "talking" ? "idle" : current;
|
|
3502
|
+
case "celebrate":
|
|
3503
|
+
case "poke": return "celebrating";
|
|
3504
|
+
case "encourage": return "encouraging";
|
|
3505
|
+
case "decay": return current === "celebrating" || current === "encouraging" ? "idle" : current;
|
|
3506
|
+
default: return current;
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
/** One creature face — the shared skin every form wears. */
|
|
3510
|
+
function CompanionFace({ form, mood }) {
|
|
3511
|
+
const skin = COMPANION_FORMS.find((f) => f.id === form) ?? COMPANION_FORMS[0];
|
|
3512
|
+
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";
|
|
3513
|
+
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";
|
|
3514
|
+
return (0, react.createElement)("svg", {
|
|
3515
|
+
width: 64,
|
|
3516
|
+
height: 64,
|
|
3517
|
+
viewBox: "0 0 64 64",
|
|
3518
|
+
"aria-hidden": "true"
|
|
3519
|
+
}, (0, react.createElement)("g", null, (0, react.createElement)("circle", {
|
|
3520
|
+
cx: 32,
|
|
3521
|
+
cy: 34,
|
|
3522
|
+
r: 20,
|
|
3523
|
+
fill: skin.body,
|
|
3524
|
+
stroke: skin.accent,
|
|
3525
|
+
strokeWidth: 2
|
|
3526
|
+
}), form === "ember" ? (0, react.createElement)("path", {
|
|
3527
|
+
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",
|
|
3528
|
+
fill: skin.accent,
|
|
3529
|
+
opacity: .85
|
|
3530
|
+
}) : form === "star" ? (0, react.createElement)("path", {
|
|
3531
|
+
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",
|
|
3532
|
+
fill: skin.accent,
|
|
3533
|
+
opacity: .85
|
|
3534
|
+
}) : form === "moss" ? (0, react.createElement)("path", {
|
|
3535
|
+
d: "M32 2 q-2 6 2 10 q-8 0 -8 6 H40 q0 -6 -8 -6 q4 -4 0 -10z",
|
|
3536
|
+
fill: skin.accent,
|
|
3537
|
+
opacity: .85
|
|
3538
|
+
}) : form === "ink" ? (0, react.createElement)("path", {
|
|
3539
|
+
d: "M32 2 q8 12 8 18 a8 8 0 1 1 -16 0 q0 -6 8 -18z",
|
|
3540
|
+
fill: skin.accent,
|
|
3541
|
+
opacity: .55
|
|
3542
|
+
}) : (0, react.createElement)("path", {
|
|
3543
|
+
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",
|
|
3544
|
+
fill: skin.accent,
|
|
3545
|
+
opacity: .7
|
|
3546
|
+
}), (0, react.createElement)("path", {
|
|
3547
|
+
d: eye,
|
|
3548
|
+
transform: "translate(25 30)",
|
|
3549
|
+
stroke: skin.accent,
|
|
3550
|
+
strokeWidth: 1.6,
|
|
3551
|
+
fill: mood === "celebrating" ? "none" : skin.accent,
|
|
3552
|
+
strokeLinecap: "round"
|
|
3553
|
+
}), (0, react.createElement)("path", {
|
|
3554
|
+
d: eye,
|
|
3555
|
+
transform: "translate(14 30)",
|
|
3556
|
+
stroke: skin.accent,
|
|
3557
|
+
strokeWidth: 1.6,
|
|
3558
|
+
fill: mood === "celebrating" ? "none" : skin.accent,
|
|
3559
|
+
strokeLinecap: "round"
|
|
3560
|
+
}), (0, react.createElement)("path", {
|
|
3561
|
+
d: mouth,
|
|
3562
|
+
stroke: skin.accent,
|
|
3563
|
+
strokeWidth: 1.8,
|
|
3564
|
+
fill: "none",
|
|
3565
|
+
strokeLinecap: "round"
|
|
3566
|
+
})));
|
|
3567
|
+
}
|
|
3568
|
+
/** The creature: fixed bottom-right inside the panel, mood-animated, pokeable. */
|
|
3569
|
+
function Companion({ mood, onPoke }) {
|
|
3570
|
+
const form = storedCompanionForm();
|
|
3571
|
+
return (0, react.createElement)("div", {
|
|
3572
|
+
className: `lks-companion mood-${mood}`,
|
|
3573
|
+
"data-lks-companion": form,
|
|
3574
|
+
title: tr("companion.poke"),
|
|
3575
|
+
role: "button",
|
|
3576
|
+
tabIndex: 0,
|
|
3577
|
+
"aria-label": tr("companion.poke"),
|
|
3578
|
+
onClick: () => {
|
|
3579
|
+
onPoke?.();
|
|
3580
|
+
},
|
|
3581
|
+
onKeyDown: (e) => {
|
|
3582
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
3583
|
+
e.preventDefault();
|
|
3584
|
+
onPoke?.();
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}, (0, react.createElement)(CompanionFace, {
|
|
3588
|
+
form,
|
|
3589
|
+
mood
|
|
3590
|
+
}));
|
|
3591
|
+
}
|
|
3592
|
+
/** The hook driving one creature instance: event → mood, momentary decay. */
|
|
3593
|
+
function useCompanionMood() {
|
|
3594
|
+
const [mood, setMood] = (0, react.useState)("idle");
|
|
3595
|
+
const moodRef = (0, react.useRef)(mood);
|
|
3596
|
+
moodRef.current = mood;
|
|
3597
|
+
const emit = (event) => {
|
|
3598
|
+
const next = nextCompanionMood(moodRef.current, event);
|
|
3599
|
+
if (next !== moodRef.current) {
|
|
3600
|
+
moodRef.current = next;
|
|
3601
|
+
setMood(next);
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
(0, react.useEffect)(() => {
|
|
3605
|
+
if (mood !== "celebrating" && mood !== "encouraging") return;
|
|
3606
|
+
const timer = setTimeout(() => {
|
|
3607
|
+
moodRef.current = nextCompanionMood(moodRef.current, "decay");
|
|
3608
|
+
setMood(moodRef.current);
|
|
3609
|
+
}, 2400);
|
|
3610
|
+
return () => {
|
|
3611
|
+
clearTimeout(timer);
|
|
3612
|
+
};
|
|
3613
|
+
}, [mood]);
|
|
3614
|
+
return {
|
|
3615
|
+
mood,
|
|
3616
|
+
emit
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
//#endregion
|
|
3620
|
+
//#region src/client/highlights.ts
|
|
3621
|
+
/**
|
|
3622
|
+
* Locate a selection inside the model text: exact `selected` text search,
|
|
3623
|
+
* disambiguated by a fuzzy `surrounding` fingerprint (the same sentence
|
|
3624
|
+
* appearing twice resolves to the occurrence whose context matches).
|
|
3625
|
+
* @returns the global [start, end) in model.text, or null when absent.
|
|
3626
|
+
*/
|
|
3627
|
+
function locateInModel(model, selected, surrounding) {
|
|
3628
|
+
const needle = selected.trim();
|
|
3629
|
+
if (needle.length < 2) return null;
|
|
3630
|
+
const hits = [];
|
|
3631
|
+
let from = 0;
|
|
3632
|
+
for (;;) {
|
|
3633
|
+
const hit = model.text.indexOf(needle, from);
|
|
3634
|
+
if (hit === -1) break;
|
|
3635
|
+
hits.push(hit);
|
|
3636
|
+
from = hit + 1;
|
|
3637
|
+
}
|
|
3638
|
+
if (hits.length === 0) return null;
|
|
3639
|
+
if (hits.length === 1 || surrounding === void 0 || surrounding.trim() === "") return {
|
|
3640
|
+
start: hits[0],
|
|
3641
|
+
end: hits[0] + needle.length
|
|
3642
|
+
};
|
|
3643
|
+
const finger = surrounding.replace(/\s+/g, "");
|
|
3644
|
+
let best = hits[0];
|
|
3645
|
+
let bestScore = -1;
|
|
3646
|
+
for (const hit of hits) {
|
|
3647
|
+
const context = model.text.slice(Math.max(0, hit - 30), hit + needle.length + 30).replace(/\s+/g, "");
|
|
3648
|
+
let score = 0;
|
|
3649
|
+
for (let i = 0; i < finger.length; i += 4) if (context.includes(finger.slice(i, i + 4))) score += 1;
|
|
3650
|
+
if (score > bestScore) {
|
|
3651
|
+
bestScore = score;
|
|
3652
|
+
best = hit;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
return {
|
|
3656
|
+
start: best,
|
|
3657
|
+
end: best + needle.length
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
/** Plan the per-node segments covering global [start, end) — pure. */
|
|
3661
|
+
function planSegments(nodes, start, end) {
|
|
3662
|
+
const segments = [];
|
|
3663
|
+
nodes.forEach((entry, index) => {
|
|
3664
|
+
if (entry.end <= start || entry.start >= end) return;
|
|
3665
|
+
segments.push({
|
|
3666
|
+
index,
|
|
3667
|
+
localStart: Math.max(0, start - entry.start),
|
|
3668
|
+
localEnd: Math.min(entry.end - entry.start, end - entry.start)
|
|
3669
|
+
});
|
|
3670
|
+
});
|
|
3671
|
+
return segments;
|
|
3672
|
+
}
|
|
3673
|
+
/** Build the DOM text model for the prose container (the walker rules mirror
|
|
3674
|
+
* upstream: skip script/style and KaTeX glyph layers; skip existing marks so
|
|
3675
|
+
* save-time and apply-time models stay symmetric). */
|
|
3676
|
+
function getTextModel(container, opts) {
|
|
3677
|
+
const nodes = [];
|
|
3678
|
+
let text = "";
|
|
3679
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, { acceptNode(node) {
|
|
3680
|
+
const parent = node.parentElement;
|
|
3681
|
+
if (parent === null) return NodeFilter.FILTER_REJECT;
|
|
3682
|
+
const tag = parent.tagName;
|
|
3683
|
+
if (tag === "SCRIPT" || tag === "STYLE") return NodeFilter.FILTER_REJECT;
|
|
3684
|
+
if (parent.closest(".katex-html")) return NodeFilter.FILTER_REJECT;
|
|
3685
|
+
if (parent.closest(".katex-mathml") !== null && parent.closest("annotation") === null) return NodeFilter.FILTER_REJECT;
|
|
3686
|
+
if (opts?.includeMarks !== true && parent.closest("mark.lks-hl") !== null) return NodeFilter.FILTER_REJECT;
|
|
3687
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
3688
|
+
} });
|
|
3689
|
+
let n;
|
|
3690
|
+
while ((n = walker.nextNode()) !== null) {
|
|
3691
|
+
const t = n;
|
|
3692
|
+
const content = t.textContent ?? "";
|
|
3693
|
+
nodes.push({
|
|
3694
|
+
node: t,
|
|
3695
|
+
start: text.length,
|
|
3696
|
+
end: text.length + content.length
|
|
3697
|
+
});
|
|
3698
|
+
text += content;
|
|
3699
|
+
}
|
|
3700
|
+
return {
|
|
3701
|
+
text,
|
|
3702
|
+
nodes
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
/** The mark class applied to persisted highlights. */
|
|
3706
|
+
const HIGHLIGHT_MARK = "lks-hl";
|
|
3707
|
+
/**
|
|
3708
|
+
* Apply one highlight range to the live DOM: split the boundary text nodes
|
|
3709
|
+
* and wrap the covering segments in marks. Returns the created marks.
|
|
3710
|
+
*/
|
|
3711
|
+
function wrapRange(model, start, end) {
|
|
3712
|
+
const marks = [];
|
|
3713
|
+
for (const seg of planSegments(model.nodes, start, end)) {
|
|
3714
|
+
const node = model.nodes[seg.index].node;
|
|
3715
|
+
if (node.parentElement === null) continue;
|
|
3716
|
+
if (node.parentElement.closest(`mark.lks-hl`) !== null) continue;
|
|
3717
|
+
const mid = seg.localStart > 0 ? node.splitText(seg.localStart) : node;
|
|
3718
|
+
const localEnd = seg.localEnd - seg.localStart;
|
|
3719
|
+
if (mid.textContent !== null && mid.textContent.length > localEnd) mid.splitText(localEnd);
|
|
3720
|
+
const mark = document.createElement("mark");
|
|
3721
|
+
mark.className = HIGHLIGHT_MARK;
|
|
3722
|
+
mid.parentElement.insertBefore(mark, mid);
|
|
3723
|
+
mark.appendChild(mid);
|
|
3724
|
+
marks.push(mark);
|
|
3725
|
+
}
|
|
3726
|
+
return marks;
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* Render the persisted highlights for one lesson: each record-zone note with a
|
|
3730
|
+
* quote anchors a mark. Idempotent per call — call on a freshly rendered
|
|
3731
|
+
* (pre-mark) container; the model skips existing marks so re-entry is safe.
|
|
3732
|
+
* @returns how many notes got at least one mark.
|
|
3733
|
+
*/
|
|
3734
|
+
function applyHighlights(container, quotes) {
|
|
3735
|
+
let applied = 0;
|
|
3736
|
+
for (const quote of quotes) {
|
|
3737
|
+
const trimmed = quote.trim();
|
|
3738
|
+
if (trimmed.length < 2) continue;
|
|
3739
|
+
const model = getTextModel(container);
|
|
3740
|
+
const located = locateInModel(model, trimmed, void 0);
|
|
3741
|
+
if (located === null) continue;
|
|
3742
|
+
if (wrapRange(model, located.start, located.end).length > 0) applied += 1;
|
|
3743
|
+
}
|
|
3744
|
+
return applied;
|
|
3745
|
+
}
|
|
3746
|
+
//#endregion
|
|
2359
3747
|
//#region src/client/views.tsx
|
|
2360
3748
|
/**
|
|
2361
3749
|
* Shared pure helpers for the study panel's pieces: chat-row shapes, tree
|
|
@@ -2423,6 +3811,56 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2423
3811
|
return runs.at(-1) ?? [];
|
|
2424
3812
|
}
|
|
2425
3813
|
/**
|
|
3814
|
+
* Merge the rail's two search tracks into the results panel: title hits
|
|
3815
|
+
* (lessons whose name matches — they are also filtered into the tree) first,
|
|
3816
|
+
* then full-text hits not already covered, snippets carried for the body
|
|
3817
|
+
* matches. Pure; empty queries yield no panel.
|
|
3818
|
+
*/
|
|
3819
|
+
function mergeRailSearch(query, lessons, textHits) {
|
|
3820
|
+
const keys = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
3821
|
+
if (keys.length === 0) return [];
|
|
3822
|
+
const rows = [];
|
|
3823
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3824
|
+
const matches = (title) => keys.every((k) => title.toLowerCase().includes(k));
|
|
3825
|
+
for (const lesson of lessons) {
|
|
3826
|
+
if (lesson.kind === "exam" || lesson.status === "locked" || !matches(lesson.title)) continue;
|
|
3827
|
+
rows.push({
|
|
3828
|
+
lessonId: lesson.id,
|
|
3829
|
+
title: lesson.title,
|
|
3830
|
+
snippet: "",
|
|
3831
|
+
courseTitle: "",
|
|
3832
|
+
fromTitle: true
|
|
3833
|
+
});
|
|
3834
|
+
seen.add(lesson.id);
|
|
3835
|
+
}
|
|
3836
|
+
for (const hit of textHits) {
|
|
3837
|
+
if (seen.has(hit.lessonId)) continue;
|
|
3838
|
+
rows.push({
|
|
3839
|
+
lessonId: hit.lessonId,
|
|
3840
|
+
title: hit.lessonTitle,
|
|
3841
|
+
snippet: hit.snippet,
|
|
3842
|
+
courseTitle: hit.courseTitle,
|
|
3843
|
+
fromTitle: false
|
|
3844
|
+
});
|
|
3845
|
+
seen.add(hit.lessonId);
|
|
3846
|
+
}
|
|
3847
|
+
return rows.slice(0, 12);
|
|
3848
|
+
}
|
|
3849
|
+
/**
|
|
3850
|
+
* A section's effective openness: the user's explicit toggle (persisted per
|
|
3851
|
+
* course) overrides the frontier-based default. Pure.
|
|
3852
|
+
*/
|
|
3853
|
+
function effectiveOpen(sectionTitle, defaultOpen, overrides) {
|
|
3854
|
+
return overrides[sectionTitle] ?? defaultOpen;
|
|
3855
|
+
}
|
|
3856
|
+
/**
|
|
3857
|
+
* The narrow-mode pane picker: only valid ids, remembering the last choice
|
|
3858
|
+
* (upstream's narrow switcher semantics). Pure.
|
|
3859
|
+
*/
|
|
3860
|
+
function pickNarrowPane(stored) {
|
|
3861
|
+
return stored === "rail" || stored === "chat" || stored === "note" ? stored : "chat";
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
2426
3864
|
* Default expansion for one rail section: collapsed when every study lesson is
|
|
2427
3865
|
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
2428
3866
|
* stays open; exam nodes never force a section open. Pure.
|
|
@@ -2431,6 +3869,248 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2431
3869
|
return section.lessons.some((l) => l.focus || l.kind !== "exam" && l.status !== "mastered" && l.status !== "locked");
|
|
2432
3870
|
}
|
|
2433
3871
|
//#endregion
|
|
3872
|
+
//#region src/vendor/map-layout.ts
|
|
3873
|
+
/** 容器宽度(章节内可用宽度,扣除 padding)。MapRail 章节容器 px-2。 */
|
|
3874
|
+
const CONTAINER_WIDTH = 268;
|
|
3875
|
+
const NODE_RADIUS = 28;
|
|
3876
|
+
const NODE_SPACING_Y = 104;
|
|
3877
|
+
const BALLOON_MARGIN = 18;
|
|
3878
|
+
/** 首节点中心下限(防顶撞上方章节路牌)。MapRail 渲染偏移 +12 + NODE_H/2=38 → wrapper 顶 = y-26 ≥ 8。 */
|
|
3879
|
+
const TOP_PAD = 34;
|
|
3880
|
+
/** y 轴抖动幅度(px)。贪心兜底后,jitter 不再是重叠源,仅负责高低参差。 */
|
|
3881
|
+
const Y_JITTER = 26;
|
|
3882
|
+
/** 绳子下垂量(px,模拟重力)。让贝塞尔像被拽下来的绳子,而非 S 形箭头。 */
|
|
3883
|
+
const ROPE_SAG = 22;
|
|
3884
|
+
/**
|
|
3885
|
+
* 确定性字符串哈希(FNV-1a 变体,32-bit)。
|
|
3886
|
+
* 同输入永远同输出 → 同一节点 id 每次渲染算出的抖动相同 → 气球位置稳定不跳。
|
|
3887
|
+
* 用于气球横向抖动 + bob 相位 + 星点位置 + 天体色切。
|
|
3888
|
+
*/
|
|
3889
|
+
function hashStr(s) {
|
|
3890
|
+
let h = 2166136261;
|
|
3891
|
+
for (let i = 0; i < s.length; i++) {
|
|
3892
|
+
h ^= s.charCodeAt(i);
|
|
3893
|
+
h = Math.imul(h, 16777619);
|
|
3894
|
+
}
|
|
3895
|
+
h ^= h >>> 16;
|
|
3896
|
+
h = Math.imul(h, 2246822507);
|
|
3897
|
+
h ^= h >>> 13;
|
|
3898
|
+
h = Math.imul(h, 3266489909);
|
|
3899
|
+
h ^= h >>> 16;
|
|
3900
|
+
return h >>> 0;
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* 算某 section 的气球节点坐标 + 绳子连接路径。
|
|
3904
|
+
* 容器高度由 nodes 数量决定(调用方据此设容器 min-height)。
|
|
3905
|
+
*
|
|
3906
|
+
* 抖动算法:v0.6 同时在 x 和 y 两轴抖动 → 气球真正"飘"在空中,
|
|
3907
|
+
* 不再是呆板的直线/微弯列。x 大幅抖动(用满容器宽度),y 在基线上下 ±Y_JITTER。
|
|
3908
|
+
* 两个独立哈希(x 用 seed:i,y 用 seed:i:y)→ 抖动方向不相关,分布更自然。
|
|
3909
|
+
*
|
|
3910
|
+
* v0.8 防重叠:抖动后跑一趟**贪心向下推**(确定性,只增 y,保持单调),
|
|
3911
|
+
* 1) 首节点中心不低于 TOP_PAD —— 防顶撞上方章节路牌;
|
|
3912
|
+
* 2) 相邻节点中心纵向距 ≥ MIN_GAP_Y(= 盒高 + 8px 呼吸)—— 视觉盒永不重叠。
|
|
3913
|
+
* 任何 seed / 任何课数都保证不重叠(verify-map-layout T11 跨 240 种子穷举验证)。
|
|
3914
|
+
*/
|
|
3915
|
+
function computeBalloonLayout(lessonCount, containerWidth = CONTAINER_WIDTH, seed = "") {
|
|
3916
|
+
const nodes = [];
|
|
3917
|
+
const centerX = containerWidth / 2;
|
|
3918
|
+
const maxDrift = containerWidth / 2 - BALLOON_MARGIN - NODE_RADIUS;
|
|
3919
|
+
for (let i = 0; i < lessonCount; i++) {
|
|
3920
|
+
const baseY = TOP_PAD + i * NODE_SPACING_Y;
|
|
3921
|
+
const x = centerX + (hashStr(`${seed}:${i}`) / 4294967295 * 2 - 1) * maxDrift;
|
|
3922
|
+
const y = baseY + (hashStr(`${seed}:${i}:y`) / 4294967295 * 2 - 1) * Y_JITTER;
|
|
3923
|
+
nodes.push({
|
|
3924
|
+
x,
|
|
3925
|
+
y,
|
|
3926
|
+
index: i
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
if (nodes.length > 0) {
|
|
3930
|
+
if (nodes[0].y < TOP_PAD) nodes[0].y = TOP_PAD;
|
|
3931
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
3932
|
+
const minY = nodes[i - 1].y + 84;
|
|
3933
|
+
if (nodes[i].y < minY) nodes[i].y = minY;
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
const segments = [];
|
|
3937
|
+
for (let i = 0; i < nodes.length - 1; i++) segments.push({
|
|
3938
|
+
from: nodes[i],
|
|
3939
|
+
to: nodes[i + 1],
|
|
3940
|
+
index: i
|
|
3941
|
+
});
|
|
3942
|
+
return {
|
|
3943
|
+
nodes,
|
|
3944
|
+
segments,
|
|
3945
|
+
height: nodes.length === 0 ? 40 : nodes[nodes.length - 1].y + 38 + 10
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* 两节点间的绳子贝塞尔(下垂)。
|
|
3950
|
+
* 控制点:y 中点 + ROPE_SAG(向下垂,模拟重力);x 用各自端点 x。
|
|
3951
|
+
* 比原 S 形软,像被拽下来的绳子。
|
|
3952
|
+
*/
|
|
3953
|
+
function balloonSegmentToPath(seg) {
|
|
3954
|
+
const { from, to } = seg;
|
|
3955
|
+
const controlY = (from.y + to.y) / 2 + ROPE_SAG;
|
|
3956
|
+
return `M ${from.x} ${from.y} C ${from.x} ${controlY}, ${to.x} ${controlY}, ${to.x} ${to.y}`;
|
|
3957
|
+
}
|
|
3958
|
+
//#endregion
|
|
3959
|
+
//#region src/client/maprail.tsx
|
|
3960
|
+
/**
|
|
3961
|
+
* MapRail balloon map (P9b) — the upstream v0.28 course map ported onto the
|
|
3962
|
+
* plugin's rail: deterministic balloon layout (vendored mapLayout engine),
|
|
3963
|
+
* sagging bezier ropes with path-draw on walked segments, state bubbles with
|
|
3964
|
+
* 3D sphere lighting, signpost section heads, and the sky backdrop. This is
|
|
3965
|
+
* upstream's own reduced-motion presentation (static layout + balloon-bob
|
|
3966
|
+
* CSS), which is exactly the deterministic half of its physics map.
|
|
3967
|
+
*/
|
|
3968
|
+
/** Upstream bubbleClass port: the four study states + the three exam variants.
|
|
3969
|
+
* Status→class goes through a table (in_progress → in-progress, like upstream's switch). */
|
|
3970
|
+
const STATUS_CLASS = {
|
|
3971
|
+
locked: "locked",
|
|
3972
|
+
available: "available",
|
|
3973
|
+
in_progress: "in-progress",
|
|
3974
|
+
mastered: "mastered"
|
|
3975
|
+
};
|
|
3976
|
+
function bubbleClasses(lesson, examAllowed) {
|
|
3977
|
+
if (lesson.kind === "exam") {
|
|
3978
|
+
if (!examAllowed) return "lks-bubble lks-exam-locked";
|
|
3979
|
+
return lesson.status === "mastered" ? "lks-bubble lks-exam-passed" : "lks-bubble lks-exam";
|
|
3980
|
+
}
|
|
3981
|
+
return `lks-bubble lks-bubble-${STATUS_CLASS[lesson.status] ?? "locked"}`;
|
|
3982
|
+
}
|
|
3983
|
+
/** A rope segment is "walked" when its FROM node has been opened
|
|
3984
|
+
* (upstream ropeStyleIsPassed: mastered | in_progress | available). */
|
|
3985
|
+
function ropePassed(from) {
|
|
3986
|
+
return from.status === "mastered" || from.status === "in_progress" || from.status === "available";
|
|
3987
|
+
}
|
|
3988
|
+
/** Deterministic sky preset per course (upstream pickPreset's CSS-essence port). */
|
|
3989
|
+
function pickSky(courseId) {
|
|
3990
|
+
return [
|
|
3991
|
+
"day",
|
|
3992
|
+
"dusk",
|
|
3993
|
+
"night"
|
|
3994
|
+
][hashStr(courseId) % 3];
|
|
3995
|
+
}
|
|
3996
|
+
/** One section: signpost head (still the plugin's collapse toggle) + the balloon field. */
|
|
3997
|
+
function MapSectionView({ section, examAllowed, open, onToggle, onJump }) {
|
|
3998
|
+
const pathRef = (0, react.useRef)(null);
|
|
3999
|
+
const [containerW, setContainerW] = (0, react.useState)(268);
|
|
4000
|
+
(0, react.useEffect)(() => {
|
|
4001
|
+
if (pathRef.current === null) return;
|
|
4002
|
+
const measure = () => {
|
|
4003
|
+
const w = pathRef.current?.clientWidth ?? 268;
|
|
4004
|
+
setContainerW(w > 0 ? w : 268);
|
|
4005
|
+
};
|
|
4006
|
+
measure();
|
|
4007
|
+
const ro = new ResizeObserver(measure);
|
|
4008
|
+
ro.observe(pathRef.current);
|
|
4009
|
+
return () => {
|
|
4010
|
+
ro.disconnect();
|
|
4011
|
+
};
|
|
4012
|
+
}, [open]);
|
|
4013
|
+
const lessons = section.lessons;
|
|
4014
|
+
const layout = computeBalloonLayout(lessons.length, containerW, section.title);
|
|
4015
|
+
return (0, react.createElement)("section", { className: "lks-mapsec" }, (0, react.createElement)("button", {
|
|
4016
|
+
type: "button",
|
|
4017
|
+
className: "lks-signpost",
|
|
4018
|
+
"aria-expanded": String(open),
|
|
4019
|
+
title: open ? tr("rail.section.collapse") : tr("rail.section.expand", { count: section.lessons.length }),
|
|
4020
|
+
onClick: onToggle
|
|
4021
|
+
}, (0, react.createElement)("span", { className: "lks-signpost-num" }, String(section.index + 1)), (0, react.createElement)("span", { className: "lks-signpost-title" }, section.title), (0, react.createElement)("span", { className: "lks-signpost-caret" }, open ? "▾" : "▸")), open && lessons.length > 0 ? (0, react.createElement)("div", {
|
|
4022
|
+
ref: pathRef,
|
|
4023
|
+
className: "lks-mapfield",
|
|
4024
|
+
style: { minHeight: `${String(layout.height)}px` }
|
|
4025
|
+
}, (0, react.createElement)("svg", {
|
|
4026
|
+
className: "lks-mapropes",
|
|
4027
|
+
"aria-hidden": "true",
|
|
4028
|
+
style: { height: `${String(layout.height)}px` }
|
|
4029
|
+
}, ...layout.segments.map((seg) => {
|
|
4030
|
+
const passed = ropePassed(lessons[seg.index] ?? { status: "locked" });
|
|
4031
|
+
return (0, react.createElement)("path", {
|
|
4032
|
+
key: seg.index,
|
|
4033
|
+
d: balloonSegmentToPath(seg),
|
|
4034
|
+
stroke: passed ? "var(--brand)" : "var(--ink-faint)",
|
|
4035
|
+
"stroke-width": passed ? "4" : "2.5",
|
|
4036
|
+
"stroke-opacity": passed ? "0.6" : "0.5",
|
|
4037
|
+
fill: "none",
|
|
4038
|
+
"stroke-linecap": "round",
|
|
4039
|
+
"stroke-dasharray": passed ? "none" : "3 7",
|
|
4040
|
+
style: passed ? {
|
|
4041
|
+
pathLength: 1,
|
|
4042
|
+
animation: "lks-path-draw 600ms var(--ease-out-expo)"
|
|
4043
|
+
} : void 0
|
|
4044
|
+
});
|
|
4045
|
+
})), ...lessons.map((lesson, i) => {
|
|
4046
|
+
const node = layout.nodes[i];
|
|
4047
|
+
if (node === void 0) return null;
|
|
4048
|
+
const h = hashStr(lesson.id);
|
|
4049
|
+
const locked = lesson.status === "locked" || lesson.kind === "exam" && !examAllowed;
|
|
4050
|
+
const showRing = lesson.kind !== "exam" && (lesson.status === "in_progress" || lesson.status === "mastered");
|
|
4051
|
+
const ringPct = lesson.status === "mastered" ? 100 : lesson.status === "in_progress" ? lesson.masteryPct ?? 0 : 0;
|
|
4052
|
+
return (0, react.createElement)("div", {
|
|
4053
|
+
key: lesson.id,
|
|
4054
|
+
className: "lks-mapnode",
|
|
4055
|
+
style: {
|
|
4056
|
+
left: `${String(node.x - 55)}px`,
|
|
4057
|
+
top: `${String(node.y - 38 + 10)}px`,
|
|
4058
|
+
"--bob-delay": `${String(h % 40 / 10)}s`,
|
|
4059
|
+
"--bob-duration": `${String(5 + h % 30 / 10)}s`
|
|
4060
|
+
}
|
|
4061
|
+
}, (0, react.createElement)("button", {
|
|
4062
|
+
type: "button",
|
|
4063
|
+
className: `${bubbleClasses(lesson, examAllowed)}${lesson.focus ? " selected" : ""}`,
|
|
4064
|
+
"aria-disabled": locked || void 0,
|
|
4065
|
+
title: `${lesson.title} — ${lesson.kind === "exam" && !examAllowed ? tr("map.exam.locked") : locked ? tr("map.node.locked") : lesson.due ? tr("map.node.due") : lesson.title}`,
|
|
4066
|
+
onClick: () => {
|
|
4067
|
+
if (!locked) onJump(lesson.id);
|
|
4068
|
+
}
|
|
4069
|
+
}, showRing ? (0, react.createElement)("svg", {
|
|
4070
|
+
className: "lks-bubble-ring",
|
|
4071
|
+
viewBox: "0 0 56 56",
|
|
4072
|
+
"aria-hidden": "true"
|
|
4073
|
+
}, (0, react.createElement)("circle", {
|
|
4074
|
+
cx: 28,
|
|
4075
|
+
cy: 28,
|
|
4076
|
+
r: 25,
|
|
4077
|
+
fill: "none",
|
|
4078
|
+
stroke: "rgb(255 255 255 / 0.2)",
|
|
4079
|
+
"stroke-width": 2.5
|
|
4080
|
+
}), (0, react.createElement)("circle", {
|
|
4081
|
+
cx: 28,
|
|
4082
|
+
cy: 28,
|
|
4083
|
+
r: 25,
|
|
4084
|
+
fill: "none",
|
|
4085
|
+
stroke: lesson.status === "mastered" ? "#ffc800" : "white",
|
|
4086
|
+
"stroke-width": 2.5,
|
|
4087
|
+
"stroke-linecap": "round",
|
|
4088
|
+
"stroke-dasharray": `${String(ringPct / 100 * 157)} 157`
|
|
4089
|
+
})) : null, lesson.kind === "exam" ? locked ? (0, react.createElement)(IconLockFill16, {
|
|
4090
|
+
size: 22,
|
|
4091
|
+
className: "lks-bubble-glyph dim"
|
|
4092
|
+
}) : (0, react.createElement)(IconGoalOutline16, {
|
|
4093
|
+
size: 24,
|
|
4094
|
+
className: "lks-bubble-glyph"
|
|
4095
|
+
}) : lesson.status === "locked" ? (0, react.createElement)(IconLockFill16, {
|
|
4096
|
+
size: 20,
|
|
4097
|
+
className: "lks-bubble-glyph dim"
|
|
4098
|
+
}) : lesson.status === "mastered" ? (0, react.createElement)(IconCrownFill16, {
|
|
4099
|
+
size: 24,
|
|
4100
|
+
className: "lks-bubble-glyph"
|
|
4101
|
+
}) : lesson.status === "in_progress" ? (0, react.createElement)(IconBookFill16, {
|
|
4102
|
+
size: 24,
|
|
4103
|
+
className: "lks-bubble-glyph"
|
|
4104
|
+
}) : (0, react.createElement)(IconStarFill16, {
|
|
4105
|
+
size: 24,
|
|
4106
|
+
className: "lks-bubble-glyph"
|
|
4107
|
+
}), lesson.due && !locked && lesson.kind !== "exam" ? (0, react.createElement)("span", {
|
|
4108
|
+
className: "lks-bubble-due",
|
|
4109
|
+
"aria-label": tr("map.node.due")
|
|
4110
|
+
}, "!") : null), lesson.focus ? (0, react.createElement)("div", { className: "lks-bubble-name" }, lesson.title) : null);
|
|
4111
|
+
})) : null);
|
|
4112
|
+
}
|
|
4113
|
+
//#endregion
|
|
2434
4114
|
//#region src/client/panel.tsx
|
|
2435
4115
|
/**
|
|
2436
4116
|
* The study panel — the sidebar-entry surface that takes over the center
|
|
@@ -2469,12 +4149,25 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2469
4149
|
["record", "zone.record"],
|
|
2470
4150
|
["practice", "zone.practice"]
|
|
2471
4151
|
];
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
if (
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
4152
|
+
/** Toast severity glyph (P6): the state color rides the icon, text stays ink. */
|
|
4153
|
+
function toastIcon(severity) {
|
|
4154
|
+
if (severity === "success") return (0, react.createElement)(IconStarFill16, {
|
|
4155
|
+
size: 14,
|
|
4156
|
+
className: "lks-toast-glyph ok"
|
|
4157
|
+
});
|
|
4158
|
+
if (severity === "warning") return (0, react.createElement)(IconWarningOutline16, {
|
|
4159
|
+
size: 14,
|
|
4160
|
+
className: "lks-toast-glyph warn"
|
|
4161
|
+
});
|
|
4162
|
+
if (severity === "error") return (0, react.createElement)(IconWarningOutline16, {
|
|
4163
|
+
size: 14,
|
|
4164
|
+
className: "lks-toast-glyph err"
|
|
4165
|
+
});
|
|
4166
|
+
if (severity === "info") return (0, react.createElement)(IconGlobeOutline14, {
|
|
4167
|
+
size: 13,
|
|
4168
|
+
className: "lks-toast-glyph info"
|
|
4169
|
+
});
|
|
4170
|
+
return null;
|
|
2478
4171
|
}
|
|
2479
4172
|
function examOpen(lessons) {
|
|
2480
4173
|
return lessons.every((l) => l.kind !== "study" || (l.masteryPct ?? 0) >= 50);
|
|
@@ -2540,10 +4233,30 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2540
4233
|
const [rows, setRows] = (0, react.useState)([]);
|
|
2541
4234
|
const [feedAttached, setFeedAttached] = (0, react.useState)(false);
|
|
2542
4235
|
const [draft, setDraft] = (0, react.useState)("");
|
|
4236
|
+
const [narrowPane, setNarrowPane] = (0, react.useState)(pickNarrowPane(null));
|
|
2543
4237
|
const lesson = data?.lesson ?? null;
|
|
2544
4238
|
const localBound = (0, react.useRef)(null);
|
|
2545
4239
|
const streamEl = (0, react.useRef)(null);
|
|
2546
4240
|
const boundId = localBound.current ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
4241
|
+
const nudged = (0, react.useRef)(false);
|
|
4242
|
+
(0, react.useEffect)(() => {
|
|
4243
|
+
if (nudged.current || (data?.dueCount ?? 0) === 0) return;
|
|
4244
|
+
nudged.current = true;
|
|
4245
|
+
showStudyToast(tr("review.nudge", { n: data.dueCount }), {
|
|
4246
|
+
severity: "warning",
|
|
4247
|
+
action: {
|
|
4248
|
+
label: tr("review.nudge.go"),
|
|
4249
|
+
onClick: () => {
|
|
4250
|
+
const first = data?.due[0];
|
|
4251
|
+
if (first !== void 0) setFocus(first.lessonId).catch(() => {});
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
});
|
|
4255
|
+
}, [
|
|
4256
|
+
data?.dueCount,
|
|
4257
|
+
data?.due,
|
|
4258
|
+
setFocus
|
|
4259
|
+
]);
|
|
2547
4260
|
(0, react.useEffect)(() => {
|
|
2548
4261
|
if (boundId === null) {
|
|
2549
4262
|
setRows([]);
|
|
@@ -2652,14 +4365,29 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2652
4365
|
}
|
|
2653
4366
|
})();
|
|
2654
4367
|
};
|
|
2655
|
-
const
|
|
4368
|
+
const companion = useCompanionMood();
|
|
4369
|
+
const setCompanionEvent = companion.emit;
|
|
4370
|
+
const progress = data?.progress ?? null;
|
|
4371
|
+
const body = (0, react.createElement)("div", {
|
|
4372
|
+
className: "lks14-body",
|
|
4373
|
+
"data-pane": narrowPane
|
|
4374
|
+
}, (0, react.createElement)(CourseRail, {
|
|
2656
4375
|
data,
|
|
2657
4376
|
activate,
|
|
2658
4377
|
setFocus,
|
|
2659
4378
|
searchLessons,
|
|
2660
4379
|
deleteCourse,
|
|
2661
4380
|
send
|
|
2662
|
-
}), (0, react.createElement)(
|
|
4381
|
+
}), (0, react.createElement)("div", { className: "lks14-righthalf" }, (0, react.createElement)("div", { className: "lks14-appheader" }, (0, react.createElement)("span", { className: "lks-hdr-title" }, lesson?.courseTitle ?? tr("tab.label")), (0, react.createElement)("span", {
|
|
4382
|
+
className: "lks-hdr-xp",
|
|
4383
|
+
title: tr("header.xp", { xp: progress?.totalXp ?? 0 })
|
|
4384
|
+
}, (0, react.createElement)("span", { className: "lks-hdr-stat" }, "⚡"), (0, react.createElement)("span", { className: "lks-hdr-xpbar" }, (0, react.createElement)("i", { style: { width: `${Math.min(100, progress?.levelPct ?? 0)}%` } }))), (0, react.createElement)("span", {
|
|
4385
|
+
className: "lks-hdr-stat lks-hdr-streak",
|
|
4386
|
+
title: tr("header.streak")
|
|
4387
|
+
}, "🔥", String(progress?.streak ?? 0)), (0, react.createElement)("span", {
|
|
4388
|
+
className: "lks-hdr-stat",
|
|
4389
|
+
title: tr("header.level")
|
|
4390
|
+
}, `Lv${String(progress?.level ?? 1)}`)), (0, react.createElement)("div", { className: "lks14-row" }, (0, react.createElement)(ChatPane, {
|
|
2663
4391
|
data,
|
|
2664
4392
|
lesson,
|
|
2665
4393
|
rows,
|
|
@@ -2670,15 +4398,78 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2670
4398
|
draft,
|
|
2671
4399
|
setDraft,
|
|
2672
4400
|
send,
|
|
2673
|
-
setMode
|
|
4401
|
+
setMode,
|
|
4402
|
+
narrowPane,
|
|
4403
|
+
companionEvent: setCompanionEvent
|
|
2674
4404
|
}), (0, react.createElement)(NotebookPane, {
|
|
2675
4405
|
data,
|
|
2676
|
-
deleteNote
|
|
2677
|
-
|
|
4406
|
+
deleteNote,
|
|
4407
|
+
send,
|
|
4408
|
+
companionEvent: setCompanionEvent
|
|
4409
|
+
}))));
|
|
2678
4410
|
return (0, react.createElement)("div", {
|
|
2679
|
-
className: "lks14",
|
|
4411
|
+
className: "lks14 lks-ui",
|
|
2680
4412
|
"data-lks-panel": ""
|
|
2681
|
-
},
|
|
4413
|
+
}, (0, react.createElement)(Companion, {
|
|
4414
|
+
mood: companion.mood,
|
|
4415
|
+
onPoke: () => {
|
|
4416
|
+
setCompanionEvent("poke");
|
|
4417
|
+
showStudyToast(tr("companion.poked"), { severity: "success" });
|
|
4418
|
+
}
|
|
4419
|
+
}), (0, react.createElement)("div", { className: "lks14-switch" }, ...[
|
|
4420
|
+
"rail",
|
|
4421
|
+
"chat",
|
|
4422
|
+
"note"
|
|
4423
|
+
].map((pane) => (0, react.createElement)("button", {
|
|
4424
|
+
key: pane,
|
|
4425
|
+
className: `lks14-switch-btn${narrowPane === pane ? " on" : ""}`,
|
|
4426
|
+
"aria-pressed": String(narrowPane === pane),
|
|
4427
|
+
onClick: () => {
|
|
4428
|
+
setNarrowPane(pane);
|
|
4429
|
+
}
|
|
4430
|
+
}, tr(`pane.${pane}`)))), body, (0, react.createElement)(StudyToastStack));
|
|
4431
|
+
}
|
|
4432
|
+
/** P6: the panel's toast stack (upstream Toast port) — severity capsules,
|
|
4433
|
+
* top-center, auto-dismiss with an exit-animation handshake; the store clears
|
|
4434
|
+
* on unmount so no toast outlives the panel. */
|
|
4435
|
+
function StudyToastStack() {
|
|
4436
|
+
const [items, setItems] = (0, react.useState)([]);
|
|
4437
|
+
(0, react.useEffect)(() => {
|
|
4438
|
+
const update = () => {
|
|
4439
|
+
setItems([...toastStore.getSnapshot()]);
|
|
4440
|
+
};
|
|
4441
|
+
update();
|
|
4442
|
+
const unsubscribe = toastStore.subscribe(update);
|
|
4443
|
+
return () => {
|
|
4444
|
+
unsubscribe();
|
|
4445
|
+
toastStore.clear();
|
|
4446
|
+
};
|
|
4447
|
+
}, []);
|
|
4448
|
+
return (0, react.createElement)("div", {
|
|
4449
|
+
className: "lks-toasts",
|
|
4450
|
+
role: "region",
|
|
4451
|
+
"aria-live": "polite",
|
|
4452
|
+
"aria-label": tr("toast.region")
|
|
4453
|
+
}, ...items.map((t) => (0, react.createElement)("div", {
|
|
4454
|
+
key: t.id,
|
|
4455
|
+
className: `lks-toast${t.exiting ? " exiting" : ""}`,
|
|
4456
|
+
"data-severity": t.severity,
|
|
4457
|
+
onAnimationEnd: t.exiting ? () => {
|
|
4458
|
+
toastStore.finish(t.id);
|
|
4459
|
+
} : void 0
|
|
4460
|
+
}, toastIcon(t.severity), (0, react.createElement)("span", { className: "lks-toast-text" }, t.message), t.action === void 0 ? null : (0, react.createElement)("button", {
|
|
4461
|
+
className: "lks-toast-action",
|
|
4462
|
+
onClick: () => {
|
|
4463
|
+
t.action?.onClick?.();
|
|
4464
|
+
toastStore.startExit(t.id);
|
|
4465
|
+
}
|
|
4466
|
+
}, t.action.label), (0, react.createElement)("button", {
|
|
4467
|
+
className: "lks-toast-close",
|
|
4468
|
+
"aria-label": tr("toast.close"),
|
|
4469
|
+
onClick: () => {
|
|
4470
|
+
toastStore.startExit(t.id);
|
|
4471
|
+
}
|
|
4472
|
+
}, "×"))));
|
|
2682
4473
|
}
|
|
2683
4474
|
/** Module-level shell handle so the panel can suppress hand-back on internal opens. */
|
|
2684
4475
|
const PANEL_SHELL = { current: null };
|
|
@@ -2689,6 +4480,14 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2689
4480
|
function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send }) {
|
|
2690
4481
|
const [selectedCourse, setSelectedCourse] = (0, react.useState)("");
|
|
2691
4482
|
const [query, setQuery] = (0, react.useState)("");
|
|
4483
|
+
const [searchRows, setSearchRows] = (0, react.useState)([]);
|
|
4484
|
+
const [sectionOverrides, setSectionOverrides] = (0, react.useState)({});
|
|
4485
|
+
const toggleSection = (title, next) => {
|
|
4486
|
+
setSectionOverrides((cur) => ({
|
|
4487
|
+
...cur,
|
|
4488
|
+
[title]: next
|
|
4489
|
+
}));
|
|
4490
|
+
};
|
|
2692
4491
|
const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
|
|
2693
4492
|
const [showImport, setShowImport] = (0, react.useState)(false);
|
|
2694
4493
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -2710,6 +4509,36 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2710
4509
|
window.removeEventListener("keydown", onKey);
|
|
2711
4510
|
};
|
|
2712
4511
|
}, [confirmDelete]);
|
|
4512
|
+
(0, react.useEffect)(() => {
|
|
4513
|
+
if (query.trim().length < 2 || data === null) {
|
|
4514
|
+
setSearchRows([]);
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4517
|
+
let cancelled = false;
|
|
4518
|
+
const timer = setTimeout(() => {
|
|
4519
|
+
searchLessons(query).then((textHits) => {
|
|
4520
|
+
if (cancelled) return;
|
|
4521
|
+
const lessons = (data.courses.find((c) => c.courseId === (data.courses.some((x) => x.courseId === selectedCourse) ? selectedCourse : data.courses[0]?.courseId))?.sections ?? []).flatMap((s) => s.lessons);
|
|
4522
|
+
setSearchRows(mergeRailSearch(query, lessons.map((l) => ({
|
|
4523
|
+
id: l.id,
|
|
4524
|
+
title: l.title,
|
|
4525
|
+
status: l.status,
|
|
4526
|
+
kind: l.kind
|
|
4527
|
+
})), textHits));
|
|
4528
|
+
}).catch(() => {
|
|
4529
|
+
if (!cancelled) setSearchRows([]);
|
|
4530
|
+
});
|
|
4531
|
+
}, 250);
|
|
4532
|
+
return () => {
|
|
4533
|
+
cancelled = true;
|
|
4534
|
+
clearTimeout(timer);
|
|
4535
|
+
};
|
|
4536
|
+
}, [
|
|
4537
|
+
query,
|
|
4538
|
+
selectedCourse,
|
|
4539
|
+
data,
|
|
4540
|
+
searchLessons
|
|
4541
|
+
]);
|
|
2713
4542
|
const body = data === null ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("loading")) : data.courses.length === 0 ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("rail.empty.title"), (0, react.createElement)("br"), (0, react.createElement)("button", {
|
|
2714
4543
|
className: "lks-btn primary",
|
|
2715
4544
|
style: { margin: "10px 0" },
|
|
@@ -2719,7 +4548,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2719
4548
|
}, (0, react.createElement)(IconDownloadOutline16, null), tr("rail.empty.demo")), (0, react.createElement)(ImportRow, { send })) : (() => {
|
|
2720
4549
|
const courseId = data.courses.some((c) => c.courseId === selectedCourse) ? selectedCourse : data.courses[0].courseId;
|
|
2721
4550
|
const course = data.courses.find((c) => c.courseId === courseId);
|
|
2722
|
-
return (0, react.createElement)("div",
|
|
4551
|
+
return (0, react.createElement)("div", { className: `lks-map lks-sky-${pickSky(courseId)}` }, (0, react.createElement)("div", { className: "lks14-railhead" }, data.courses.length > 1 ? (0, react.createElement)("select", {
|
|
2723
4552
|
className: "lks-set-select",
|
|
2724
4553
|
value: courseId,
|
|
2725
4554
|
onChange: (e) => {
|
|
@@ -2761,25 +4590,34 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2761
4590
|
setQuery(e.target.value);
|
|
2762
4591
|
},
|
|
2763
4592
|
onKeyDown: (e) => {
|
|
4593
|
+
if (e.key === "Escape") {
|
|
4594
|
+
setQuery("");
|
|
4595
|
+
setSearchRows([]);
|
|
4596
|
+
}
|
|
2764
4597
|
if (e.key !== "Enter" || query.trim() === "") return;
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
}
|
|
4598
|
+
const first = searchRows[0];
|
|
4599
|
+
if (first !== void 0) {
|
|
4600
|
+
setQuery("");
|
|
4601
|
+
setSearchRows([]);
|
|
4602
|
+
setFocus(first.lessonId).catch(reportError);
|
|
2771
4603
|
}
|
|
2772
|
-
searchLessons(query).then((hits) => {
|
|
2773
|
-
const hit = hits.find((h) => h.lessonId.startsWith(`${courseId}:`)) ?? hits[0];
|
|
2774
|
-
if (hit !== void 0) {
|
|
2775
|
-
setQuery("");
|
|
2776
|
-
setFocus(hit.lessonId).catch(reportError);
|
|
2777
|
-
}
|
|
2778
|
-
}).catch(reportError);
|
|
2779
4604
|
}
|
|
2780
|
-
}),
|
|
4605
|
+
}), searchRows.length > 0 ? (0, react.createElement)("div", { className: "lks14-searchpanel" }, ...searchRows.map((row) => (0, react.createElement)("button", {
|
|
4606
|
+
key: row.lessonId,
|
|
4607
|
+
className: "lks14-searchrow",
|
|
4608
|
+
title: tr("rail.search.jump"),
|
|
4609
|
+
onClick: () => {
|
|
4610
|
+
setQuery("");
|
|
4611
|
+
setSearchRows([]);
|
|
4612
|
+
setFocus(row.lessonId).catch(reportError);
|
|
4613
|
+
}
|
|
4614
|
+
}, (0, react.createElement)("span", { className: "lks14-searchrow-title" }, row.title), row.courseTitle !== "" ? (0, react.createElement)("span", { className: "lks14-searchrow-course" }, row.courseTitle) : null, row.snippet !== "" ? (0, react.createElement)("span", { className: "lks14-searchrow-snip" }, row.snippet) : null))) : null, data.dueCount > 0 ? (0, react.createElement)("div", { className: "lks14-duebox" }, (0, react.createElement)(IconRefreshOutline16, { size: 13 }), tr("rail.due", { count: data.dueCount }), ...data.due.map((d) => (0, react.createElement)("button", {
|
|
2781
4615
|
key: d.lessonId,
|
|
2782
|
-
className: "lks14-dueitem"
|
|
4616
|
+
className: "lks14-dueitem",
|
|
4617
|
+
title: tr("review.jump"),
|
|
4618
|
+
onClick: () => {
|
|
4619
|
+
setFocus(d.lessonId).catch(reportError);
|
|
4620
|
+
}
|
|
2783
4621
|
}, (0, react.createElement)("span", null, d.lessonTitle), d.overdueDays > 0 ? (0, react.createElement)("span", { className: "lks14-over" }, tr("rail.due.over", { days: d.overdueDays })) : null)), (0, react.createElement)("button", {
|
|
2784
4622
|
className: "lks-btn ghost",
|
|
2785
4623
|
style: { marginTop: "6px" },
|
|
@@ -2790,34 +4628,23 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2790
4628
|
const examAllowed = examOpen(section.lessons);
|
|
2791
4629
|
const lessons = section.lessons.filter((l) => query.trim() === "" || titleMatches(l.title, query) || l.focus);
|
|
2792
4630
|
if (lessons.length === 0) return [];
|
|
2793
|
-
const open = query.trim() !== "" || sectionDefaultOpen(section);
|
|
2794
|
-
return [(0, react.createElement)(
|
|
4631
|
+
const open = query.trim() !== "" || effectiveOpen(section.title, sectionDefaultOpen(section), sectionOverrides);
|
|
4632
|
+
return [(0, react.createElement)(MapSectionView, {
|
|
2795
4633
|
key: section.title,
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
if (locked) return;
|
|
2811
|
-
setFocus(lesson.id).catch(reportError);
|
|
2812
|
-
}
|
|
2813
|
-
}, (0, react.createElement)("span", { className: "lks14-g" }, statusIcon(lesson.kind, locked ? "locked" : lesson.status)), (0, react.createElement)("span", { className: "lks14-t" }, lesson.title), lesson.weakConcepts > 0 ? (0, react.createElement)("span", {
|
|
2814
|
-
className: "lks14-tag weak",
|
|
2815
|
-
title: tr("tag.weak", { count: lesson.weakConcepts })
|
|
2816
|
-
}, (0, react.createElement)(IconBoltFill16, { size: 10 }), String(lesson.weakConcepts)) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", {
|
|
2817
|
-
className: "lks14-bar",
|
|
2818
|
-
title: tr("tag.mastery", { pct: lesson.masteryPct })
|
|
2819
|
-
}, (0, react.createElement)("i", { style: { transform: `scaleX(${lesson.masteryPct / 100})` } })) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", { className: "lks14-pct" }, `${lesson.masteryPct}%`) : null);
|
|
2820
|
-
}) : []];
|
|
4634
|
+
section: {
|
|
4635
|
+
title: section.title,
|
|
4636
|
+
index: section.index,
|
|
4637
|
+
lessons
|
|
4638
|
+
},
|
|
4639
|
+
examAllowed,
|
|
4640
|
+
open,
|
|
4641
|
+
onToggle: () => {
|
|
4642
|
+
toggleSection(section.title, !open);
|
|
4643
|
+
},
|
|
4644
|
+
onJump: (id) => {
|
|
4645
|
+
setFocus(id).catch(reportError);
|
|
4646
|
+
}
|
|
4647
|
+
})];
|
|
2821
4648
|
}), (0, react.createElement)("button", {
|
|
2822
4649
|
className: "lks-btn ghost",
|
|
2823
4650
|
style: { marginTop: "10px" },
|
|
@@ -2850,7 +4677,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2850
4677
|
}, tr("rail.empty.button"))), (0, react.createElement)("div", { className: "lks14-hint" }, tr("rail.empty.hint"), (0, react.createElement)("br"), tr("rail.empty.hint2")));
|
|
2851
4678
|
}
|
|
2852
4679
|
/** 中栏:the tutor chat stream with its own composer (upstream ChatStream + ChatComposer). */
|
|
2853
|
-
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, setMode }) {
|
|
4680
|
+
function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, setMode, narrowPane, companionEvent }) {
|
|
2854
4681
|
const streamEl = (0, react.useRef)(null);
|
|
2855
4682
|
(0, react.useEffect)(() => {
|
|
2856
4683
|
const el = streamEl.current;
|
|
@@ -2873,7 +4700,26 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2873
4700
|
if (draft.trim() !== "" && !busy) send(draft);
|
|
2874
4701
|
}
|
|
2875
4702
|
};
|
|
2876
|
-
|
|
4703
|
+
const proposal = data?.pendingProposals[0] ?? null;
|
|
4704
|
+
return (0, react.createElement)("div", { className: "lks14-col lks14-chat" }, proposal !== null ? (0, react.createElement)("div", { className: "lks-propbanner" }, (0, react.createElement)(IconCrownFill16, { size: 14 }), (0, react.createElement)("span", { className: "lks-propbanner-why" }, tr("proposal.banner", { lesson: proposal.lessonTitle }), " — ", proposal.rationale), (0, react.createElement)("button", {
|
|
4705
|
+
className: "lks-btn primary",
|
|
4706
|
+
style: {
|
|
4707
|
+
padding: "4px 12px",
|
|
4708
|
+
fontSize: "12.5px"
|
|
4709
|
+
},
|
|
4710
|
+
onClick: () => {
|
|
4711
|
+
send(tr("proposal.accept.msg", { lesson: proposal.lessonTitle }));
|
|
4712
|
+
}
|
|
4713
|
+
}, tr("proposal.accept")), (0, react.createElement)("button", {
|
|
4714
|
+
className: "lks-btn ghost",
|
|
4715
|
+
style: {
|
|
4716
|
+
padding: "4px 12px",
|
|
4717
|
+
fontSize: "12.5px"
|
|
4718
|
+
},
|
|
4719
|
+
onClick: () => {
|
|
4720
|
+
send(tr("proposal.decline.msg", { lesson: proposal.lessonTitle }));
|
|
4721
|
+
}
|
|
4722
|
+
}, tr("proposal.decline"))) : null, (0, react.createElement)("div", { className: "lks14-colhead" }, tr("col.tutor"), lesson !== null ? (0, react.createElement)("span", { className: "lks14-chatlesson" }, lesson.title) : null, (0, react.createElement)("span", { className: "lks14-pills" }, ...MODES$1.map((mode) => (0, react.createElement)("button", {
|
|
2877
4723
|
key: mode.id,
|
|
2878
4724
|
className: `lks14-pill${data?.mode === mode.id ? " on" : ""}`,
|
|
2879
4725
|
"aria-pressed": String(data?.mode === mode.id),
|
|
@@ -2886,7 +4732,21 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2886
4732
|
className: "lks14-stream",
|
|
2887
4733
|
ref: streamEl,
|
|
2888
4734
|
"data-lks-feed": rows.length > 0 ? `rows:${String(rows.length)}` : bound ? feedAttached ? "attached-empty" : "waiting" : "no-thread"
|
|
2889
|
-
}, rows.length === 0 ? (0, react.createElement)("div", { className: "lks14-empty" }, dormant ? tr("tutor.dormant") : tr("tutor.empty"), (0, react.createElement)("br"), tr("tutor.empty.hint")) : rows.map((row, i) => chatRow(row, !dormant && i === lastAssistant ? { send } : void 0))
|
|
4735
|
+
}, rows.length === 0 ? (0, react.createElement)("div", { className: "lks14-empty" }, dormant ? tr("tutor.dormant") : tr("tutor.empty"), (0, react.createElement)("br"), tr("tutor.empty.hint")) : rows.map((row, i) => chatRow(row, !dormant && i === lastAssistant ? { send } : void 0)), rows.length > 0 && rows[rows.length - 1].role === "user" ? (0, react.createElement)("div", { className: "lks14-thinking" }, (0, react.createElement)("i", null), (0, react.createElement)("i", null), (0, react.createElement)("i", null)) : null), ...(lesson?.artifacts ?? []).filter((a) => a.artifactType === "quiz").map((a) => (0, react.createElement)(QuizCard, {
|
|
4736
|
+
key: a.id,
|
|
4737
|
+
lessonId: lesson.lessonId,
|
|
4738
|
+
artifactId: a.id,
|
|
4739
|
+
data: a.data,
|
|
4740
|
+
masteryPct: lesson.masteryPct,
|
|
4741
|
+
send,
|
|
4742
|
+
onFinished: (allCorrect) => {
|
|
4743
|
+
companionEvent(allCorrect ? "celebrate" : "encourage");
|
|
4744
|
+
}
|
|
4745
|
+
})), ...(lesson?.artifacts ?? []).filter((a) => a.artifactType !== "quiz").map((a) => (0, react.createElement)(ArtifactCard, {
|
|
4746
|
+
key: a.id,
|
|
4747
|
+
artifact: a,
|
|
4748
|
+
send
|
|
4749
|
+
})), (0, react.createElement)("div", { className: "lks14-starters" }, ...starters.map((s) => (0, react.createElement)("button", {
|
|
2890
4750
|
key: s.label,
|
|
2891
4751
|
className: "lks14-starter",
|
|
2892
4752
|
"aria-disabled": dormant || void 0,
|
|
@@ -2894,7 +4754,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2894
4754
|
onClick: () => {
|
|
2895
4755
|
if (!dormant) send(s.message);
|
|
2896
4756
|
}
|
|
2897
|
-
}, s.label))), (0, react.createElement)("div", { className: "lks14-composer" }, (0, react.createElement)("textarea", {
|
|
4757
|
+
}, s.label))), (0, react.createElement)("div", { className: "lks14-composer" }, (0, react.createElement)("div", { className: "lks14-composer-card" }, (0, react.createElement)("textarea", {
|
|
2898
4758
|
className: "lks14-composertext",
|
|
2899
4759
|
placeholder: busy ? tr("composer.busy") : tr("tutor.empty.hint"),
|
|
2900
4760
|
value: draft,
|
|
@@ -2905,18 +4765,19 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2905
4765
|
},
|
|
2906
4766
|
onKeyDown: onComposerKey
|
|
2907
4767
|
}), (0, react.createElement)("button", {
|
|
2908
|
-
className: "lks-btn
|
|
4768
|
+
className: "lks-btn-send",
|
|
4769
|
+
"aria-label": tr("composer.send"),
|
|
2909
4770
|
disabled: busy || dormant || draft.trim() === "",
|
|
2910
4771
|
onClick: () => {
|
|
2911
4772
|
send(draft);
|
|
2912
4773
|
}
|
|
2913
4774
|
}, busy ? (0, react.createElement)(IconLoadingOutline16, {
|
|
2914
|
-
size:
|
|
4775
|
+
size: 16,
|
|
2915
4776
|
className: "lks-spin"
|
|
2916
|
-
}) :
|
|
4777
|
+
}) : "↑")), sendError !== null || error !== null ? (0, react.createElement)("div", { className: "lks-propcard-err" }, sendError ?? error) : null));
|
|
2917
4778
|
}
|
|
2918
4779
|
/** 右栏:the notebook — 讲解/概念图/笔记 tabs (upstream NotebookPanel arrangement). */
|
|
2919
|
-
function NotebookPane({ data, deleteNote }) {
|
|
4780
|
+
function NotebookPane({ data, deleteNote, send, companionEvent }) {
|
|
2920
4781
|
const lesson = data?.lesson ?? null;
|
|
2921
4782
|
const [tab, setTab] = (0, react.useState)("teach");
|
|
2922
4783
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -2924,7 +4785,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2924
4785
|
const [read, setRead] = (0, react.useState)(null);
|
|
2925
4786
|
const readCtl = (0, react.useRef)(null);
|
|
2926
4787
|
const [readError, setReadError] = (0, react.useState)(null);
|
|
2927
|
-
const { tts } = useStudy();
|
|
4788
|
+
const { tts, addUserNote, recordReview } = useStudy();
|
|
2928
4789
|
const proseRef = (0, react.useRef)(null);
|
|
2929
4790
|
const diagRef = (0, react.useRef)(null);
|
|
2930
4791
|
(0, react.useEffect)(() => {
|
|
@@ -2949,8 +4810,94 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
2949
4810
|
setError(err instanceof Error ? err.message : String(err));
|
|
2950
4811
|
});
|
|
2951
4812
|
};
|
|
4813
|
+
const artifacts = lesson?.artifacts ?? [];
|
|
4814
|
+
const [, setUnseenTick] = (0, react.useState)(0);
|
|
4815
|
+
const unseen = unseenArtifacts(lesson?.lessonId ?? "", artifacts);
|
|
4816
|
+
const lastAnnounced = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
4817
|
+
(0, react.useEffect)(() => {
|
|
4818
|
+
const fresh = unseen.filter((id) => !lastAnnounced.current.has(id));
|
|
4819
|
+
if (fresh.length === 0) return;
|
|
4820
|
+
lastAnnounced.current = /* @__PURE__ */ new Set([...lastAnnounced.current, ...fresh]);
|
|
4821
|
+
showStudyToast(tr("artifact.sedimented"), { severity: "info" });
|
|
4822
|
+
setUnseenTick(Date.now());
|
|
4823
|
+
}, [unseen.join(","), lesson?.lessonId]);
|
|
4824
|
+
const heavy = artifacts.filter((a) => a.artifactType === "compare_table" || a.artifactType === "diagram" || a.artifactType === "code_walkthrough");
|
|
4825
|
+
const latestHeavy = heavy.length > 0 ? heavy[heavy.length - 1] : null;
|
|
4826
|
+
const [quoteBtn, setQuoteBtn] = (0, react.useState)(null);
|
|
4827
|
+
const recordQuotes = (lesson?.notes ?? []).filter((n) => n.zone === "record" && n.quote !== null && n.quote.trim().length >= 2).map((n) => n.quote);
|
|
4828
|
+
const evaluateSelection = () => {
|
|
4829
|
+
const prose = proseRef.current;
|
|
4830
|
+
const selection = window.getSelection();
|
|
4831
|
+
if (prose === null || selection === null || selection.rangeCount === 0) return;
|
|
4832
|
+
const text = selection.toString().trim();
|
|
4833
|
+
if (text.length < 2 || text.length > 600) return;
|
|
4834
|
+
const range = selection.getRangeAt(0);
|
|
4835
|
+
if (prose.contains(range.commonAncestorContainer) !== true) return;
|
|
4836
|
+
const model = getTextModel(prose);
|
|
4837
|
+
const rect = range.getBoundingClientRect();
|
|
4838
|
+
const box = prose.getBoundingClientRect();
|
|
4839
|
+
const modelText = model.text;
|
|
4840
|
+
const idx = locateInModel(model, text, void 0)?.start ?? modelText.indexOf(text);
|
|
4841
|
+
const surrounding = idx >= 0 ? modelText.slice(Math.max(0, idx - 30), idx + text.length + 30) : text;
|
|
4842
|
+
setQuoteBtn({
|
|
4843
|
+
x: Math.min(Math.max(rect.left - box.left + rect.width / 2, 90), Math.max(box.width - 90, 90)),
|
|
4844
|
+
y: rect.top - box.top - 38,
|
|
4845
|
+
text,
|
|
4846
|
+
surrounding
|
|
4847
|
+
});
|
|
4848
|
+
};
|
|
4849
|
+
(0, react.useEffect)(() => {
|
|
4850
|
+
const SETTLE = 250;
|
|
4851
|
+
let settleTimer = null;
|
|
4852
|
+
let hideTimer = null;
|
|
4853
|
+
let gesture = false;
|
|
4854
|
+
const selectionHasText = () => (window.getSelection()?.toString().trim().length ?? 0) >= 2;
|
|
4855
|
+
const onChange = () => {
|
|
4856
|
+
if (selectionHasText()) {
|
|
4857
|
+
if (hideTimer !== null) {
|
|
4858
|
+
clearTimeout(hideTimer);
|
|
4859
|
+
hideTimer = null;
|
|
4860
|
+
}
|
|
4861
|
+
setQuoteBtn((cur) => cur === null ? cur : null);
|
|
4862
|
+
if (settleTimer !== null) clearTimeout(settleTimer);
|
|
4863
|
+
settleTimer = setTimeout(() => {
|
|
4864
|
+
settleTimer = null;
|
|
4865
|
+
if (!gesture && selectionHasText()) evaluateSelection();
|
|
4866
|
+
}, SETTLE);
|
|
4867
|
+
} else if (hideTimer === null) hideTimer = setTimeout(() => {
|
|
4868
|
+
setQuoteBtn((cur) => cur === null ? cur : null);
|
|
4869
|
+
}, 250);
|
|
4870
|
+
};
|
|
4871
|
+
const onDown = (e) => {
|
|
4872
|
+
if (e.target?.closest?.("[data-lks-quote-btn]") !== null) return;
|
|
4873
|
+
gesture = true;
|
|
4874
|
+
};
|
|
4875
|
+
const onUp = () => {
|
|
4876
|
+
gesture = false;
|
|
4877
|
+
if (settleTimer !== null) {
|
|
4878
|
+
clearTimeout(settleTimer);
|
|
4879
|
+
settleTimer = null;
|
|
4880
|
+
}
|
|
4881
|
+
if (hideTimer === null && selectionHasText()) evaluateSelection();
|
|
4882
|
+
};
|
|
4883
|
+
document.addEventListener("selectionchange", onChange);
|
|
4884
|
+
window.addEventListener("pointerdown", onDown, true);
|
|
4885
|
+
window.addEventListener("pointerup", onUp, true);
|
|
4886
|
+
window.addEventListener("pointercancel", onUp, true);
|
|
4887
|
+
return () => {
|
|
4888
|
+
document.removeEventListener("selectionchange", onChange);
|
|
4889
|
+
window.removeEventListener("pointerdown", onDown, true);
|
|
4890
|
+
window.removeEventListener("pointerup", onUp, true);
|
|
4891
|
+
window.removeEventListener("pointercancel", onUp, true);
|
|
4892
|
+
if (settleTimer !== null) clearTimeout(settleTimer);
|
|
4893
|
+
if (hideTimer !== null) clearTimeout(hideTimer);
|
|
4894
|
+
};
|
|
4895
|
+
}, []);
|
|
4896
|
+
const [enhanceTick, setEnhanceTick] = (0, react.useState)(0);
|
|
4897
|
+
const [rateBusy, setRateBusy] = (0, react.useState)(false);
|
|
2952
4898
|
const startReading = () => {
|
|
2953
4899
|
if (lesson === null || lesson.speechText.trim() === "") return;
|
|
4900
|
+
companionEvent("talk-start");
|
|
2954
4901
|
readCtl.current?.stop();
|
|
2955
4902
|
const sentences = speechSentencesOf(lesson.speechText);
|
|
2956
4903
|
const voice = storedTtsVoice();
|
|
@@ -3031,6 +4978,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
3031
4978
|
});
|
|
3032
4979
|
};
|
|
3033
4980
|
const stopReading = () => {
|
|
4981
|
+
companionEvent("talk-end");
|
|
3034
4982
|
readCtl.current?.stop();
|
|
3035
4983
|
setRead(null);
|
|
3036
4984
|
};
|
|
@@ -3051,7 +4999,10 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
3051
4999
|
]);
|
|
3052
5000
|
(0, react.useEffect)(() => {
|
|
3053
5001
|
if (tab !== "teach" || proseRef.current === null || lesson === null) return;
|
|
3054
|
-
enhanceRendered(proseRef.current).catch(() => {})
|
|
5002
|
+
enhanceRendered(proseRef.current).catch(() => {}).finally(() => {
|
|
5003
|
+
applyHighlights(proseRef.current, recordQuotes);
|
|
5004
|
+
setEnhanceTick(Date.now());
|
|
5005
|
+
});
|
|
3055
5006
|
}, [tab, lesson?.html]);
|
|
3056
5007
|
const body = lesson === null ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("bb.empty"), (0, react.createElement)("br"), tr("bb.empty.hint")) : (0, react.createElement)("div", { className: "lks14-notebody" }, (0, react.createElement)("div", { className: "lks14-lessonhead" }, (0, react.createElement)("h2", null, lesson.title), (0, react.createElement)("div", { className: "lks14-meta" }, `${lesson.courseTitle} · ${statusTitle("study", lesson.status)}` + (lesson.masteryPct === null ? "" : ` · ${tr("bb.mastery", { pct: lesson.masteryPct })}`))), (0, react.createElement)("div", { className: "lks14-viewtabs" }, (0, react.createElement)("button", {
|
|
3057
5008
|
className: `lks14-viewtab${tab === "teach" ? " on" : ""}`,
|
|
@@ -3071,8 +5022,34 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
3071
5022
|
"aria-pressed": String(tab === "notes"),
|
|
3072
5023
|
onClick: () => {
|
|
3073
5024
|
setTab("notes");
|
|
5025
|
+
if (lesson !== null && unseen.length > 0) {
|
|
5026
|
+
markArtifactsSeen(lesson.lessonId, unseen);
|
|
5027
|
+
setUnseenTick(Date.now());
|
|
5028
|
+
}
|
|
3074
5029
|
}
|
|
3075
|
-
}, tr("bb.notes")
|
|
5030
|
+
}, tr("bb.notes"), unseen.length > 0 ? (0, react.createElement)("span", { className: "lks-viewtab-badge" }, String(unseen.length)) : null)), tab === "teach" ? (0, react.createElement)("div", null, lesson.due ? (0, react.createElement)("div", {
|
|
5031
|
+
className: "lks-ratecard",
|
|
5032
|
+
"data-lks-rate": lesson.lessonId
|
|
5033
|
+
}, (0, react.createElement)("div", { className: "lks-ratecard-title" }, tr("review.rate.title")), (0, react.createElement)("div", { className: "lks-ratecard-opts" }, ...[
|
|
5034
|
+
["1", "review.again"],
|
|
5035
|
+
["4", "review.remembered"],
|
|
5036
|
+
["5", "review.mastered"]
|
|
5037
|
+
].map(([q, key]) => (0, react.createElement)("button", {
|
|
5038
|
+
key: q,
|
|
5039
|
+
className: `lks-ratecard-opt${q === "1" ? " again" : q === "5" ? " best" : ""}`,
|
|
5040
|
+
disabled: rateBusy,
|
|
5041
|
+
onClick: () => {
|
|
5042
|
+
setRateBusy(true);
|
|
5043
|
+
recordReview(lesson.lessonId, Number(q)).then(() => {
|
|
5044
|
+
companionEvent(Number(q) >= 4 ? "celebrate" : "encourage");
|
|
5045
|
+
showStudyToast(Number(q) >= 4 ? tr("review.done.good", { days: 1 }) : tr("review.done.again"), { severity: Number(q) >= 4 ? "success" : "warning" });
|
|
5046
|
+
setRateBusy(false);
|
|
5047
|
+
}).catch((err) => {
|
|
5048
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
5049
|
+
setRateBusy(false);
|
|
5050
|
+
});
|
|
5051
|
+
}
|
|
5052
|
+
}, tr(key))))) : null, (0, react.createElement)("div", { className: "lks-readbar" }, (0, react.createElement)("button", {
|
|
3076
5053
|
className: "lks-btn ghost",
|
|
3077
5054
|
style: {
|
|
3078
5055
|
padding: "3px 8px",
|
|
@@ -3100,14 +5077,49 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
3100
5077
|
},
|
|
3101
5078
|
title: tr("read.stop"),
|
|
3102
5079
|
onClick: stopReading
|
|
3103
|
-
}, tr("read.stop")) : null, read !== null && read.state !== "idle" && lesson !== null ? (0, react.createElement)("span", { className: "lks-readbar-cur" }, (speechSentencesOf(lesson.speechText)[read.index] ?? "").slice(0, 80)) : null, readError !== null ? (0, react.createElement)("span", { className: "lks-readbar-notice" }, readError) : null, read !== null && read.degraded ? (0, react.createElement)("span", { className: "lks-readbar-notice" }, tr("read.engine.system")) : null), (0, react.createElement)("div", {
|
|
5080
|
+
}, tr("read.stop")) : null, read !== null && read.state !== "idle" && lesson !== null ? (0, react.createElement)("span", { className: "lks-readbar-cur" }, (speechSentencesOf(lesson.speechText)[read.index] ?? "").slice(0, 80)) : null, readError !== null ? (0, react.createElement)("span", { className: "lks-readbar-notice" }, readError) : null, read !== null && read.degraded ? (0, react.createElement)("span", { className: "lks-readbar-notice" }, tr("read.engine.system")) : null), (0, react.createElement)("div", { className: "lks14-prosewrap" }, (0, react.createElement)("div", {
|
|
3104
5081
|
className: "lks14-prose",
|
|
3105
5082
|
ref: proseRef,
|
|
3106
5083
|
dangerouslySetInnerHTML: { __html: lesson.html }
|
|
3107
|
-
})
|
|
5084
|
+
}), quoteBtn !== null ? (0, react.createElement)("div", {
|
|
5085
|
+
className: "lks-quote-btn",
|
|
5086
|
+
"data-lks-quote-btn": "",
|
|
5087
|
+
style: {
|
|
5088
|
+
left: `${quoteBtn.x}px`,
|
|
5089
|
+
top: `${quoteBtn.y}px`
|
|
5090
|
+
}
|
|
5091
|
+
}, (0, react.createElement)("button", { onClick: () => {
|
|
5092
|
+
send(tr("note.quote.template", { text: quoteBtn.text.length > 200 ? `${quoteBtn.text.slice(0, 200)}…` : quoteBtn.text }));
|
|
5093
|
+
setQuoteBtn(null);
|
|
5094
|
+
window.getSelection()?.removeAllRanges();
|
|
5095
|
+
} }, tr("note.quote.ask")), (0, react.createElement)("button", { onClick: () => {
|
|
5096
|
+
if (lesson !== null) {
|
|
5097
|
+
addUserNote(lesson.lessonId, quoteBtn.text).catch((err) => {
|
|
5098
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
5099
|
+
});
|
|
5100
|
+
applyHighlights(proseRef.current, [quoteBtn.text]);
|
|
5101
|
+
showStudyToast(tr("note.saved"), { severity: "success" });
|
|
5102
|
+
}
|
|
5103
|
+
setQuoteBtn(null);
|
|
5104
|
+
window.getSelection()?.removeAllRanges();
|
|
5105
|
+
} }, tr("note.quote.save"))) : null), latestHeavy !== null ? (0, react.createElement)("div", { className: "lks-acard-stage" }, (0, react.createElement)(ArtifactCard, {
|
|
5106
|
+
artifact: latestHeavy,
|
|
5107
|
+
send: () => {}
|
|
5108
|
+
})) : null) : tab === "cmap" ? (0, react.createElement)("div", {
|
|
3108
5109
|
className: "lks14-prose",
|
|
3109
5110
|
ref: diagRef
|
|
3110
|
-
}) : (0, react.createElement)("div", { className: "lks14-zones" },
|
|
5111
|
+
}) : (0, react.createElement)("div", { className: "lks14-zones" }, (() => {
|
|
5112
|
+
const understand = artifacts.filter((a) => a.artifactType === "compare_table" || a.artifactType === "diagram" || a.artifactType === "code_walkthrough");
|
|
5113
|
+
if (understand.length === 0) return null;
|
|
5114
|
+
return (0, react.createElement)("div", {
|
|
5115
|
+
key: "artifacts",
|
|
5116
|
+
className: "lks14-zone"
|
|
5117
|
+
}, (0, react.createElement)("div", { className: "lks14-zoneh" }, tr("zone.artifacts")), ...understand.map((a) => (0, react.createElement)(ArtifactCard, {
|
|
5118
|
+
key: a.id,
|
|
5119
|
+
artifact: a,
|
|
5120
|
+
send: () => {}
|
|
5121
|
+
})));
|
|
5122
|
+
})(), lesson.notes.length === 0 ? (0, react.createElement)("div", {
|
|
3111
5123
|
className: "lks14-empty",
|
|
3112
5124
|
style: { padding: "16px 0" }
|
|
3113
5125
|
}, tr("bb.notes.empty")) : ZONES.filter(([zone]) => lesson.notes.some((n) => n.zone === zone)).map(([zone, labelKey]) => (0, react.createElement)("div", {
|
|
@@ -3388,6 +5400,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
3388
5400
|
const { data, setMode, activate } = useStudy();
|
|
3389
5401
|
const [error, setError] = (0, react.useState)(null);
|
|
3390
5402
|
const [voice, setVoice] = (0, react.useState)(storedTtsVoice);
|
|
5403
|
+
const [companion, setCompanion] = (0, react.useState)(storedCompanionForm);
|
|
3391
5404
|
const fire = (action) => {
|
|
3392
5405
|
action.then(() => {
|
|
3393
5406
|
setError(null);
|
|
@@ -3408,7 +5421,17 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
|
|
|
3408
5421
|
onClick: () => {
|
|
3409
5422
|
if (data !== null) fire(activate(!data.active));
|
|
3410
5423
|
}
|
|
3411
|
-
}, data?.active === true ? tr("settings.turnOff") : tr("settings.turnOn")), (0, react.createElement)("span", { className: "lks-set-state" }, data?.active === true ? tr("settings.on") : tr("settings.off")))), (0, react.createElement)("section", { className: "lks-set-row" }, (0, react.createElement)("h3", null, tr("settings.
|
|
5424
|
+
}, data?.active === true ? tr("settings.turnOff") : tr("settings.turnOn")), (0, react.createElement)("span", { className: "lks-set-state" }, data?.active === true ? tr("settings.on") : tr("settings.off")))), (0, react.createElement)("section", { className: "lks-set-row" }, (0, react.createElement)("h3", null, tr("settings.companion")), (0, react.createElement)("p", { className: "lks-set-hint" }, tr("settings.companion.hint")), (0, react.createElement)("div", null, (0, react.createElement)("select", {
|
|
5425
|
+
className: "lks-set-select",
|
|
5426
|
+
value: companion,
|
|
5427
|
+
onChange: (e) => {
|
|
5428
|
+
storeCompanionForm(normalizeCompanionForm(e.target.value));
|
|
5429
|
+
setCompanion(normalizeCompanionForm(e.target.value));
|
|
5430
|
+
}
|
|
5431
|
+
}, ...COMPANION_FORMS.map((f) => (0, react.createElement)("option", {
|
|
5432
|
+
key: f.id,
|
|
5433
|
+
value: f.id
|
|
5434
|
+
}, 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", {
|
|
3412
5435
|
className: "lks-set-select",
|
|
3413
5436
|
value: voice,
|
|
3414
5437
|
onChange: (e) => {
|