dsh-plugin-lookatstudy 0.13.0 → 0.14.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 +14 -14
- package/lib/client.js +905 -992
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +10 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -5,11 +5,15 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
+
let react_dom_client = require("react-dom/client");
|
|
8
9
|
//#region src/client/styles.ts
|
|
9
10
|
/**
|
|
10
|
-
* Style injection for the study
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Style injection for the study surfaces: one `<style>` element carrying the
|
|
12
|
+
* `.lks14-*` panel classes (the sidebar-entry center-takeover, arranged like
|
|
13
|
+
* upstream LookatStudy) plus the shared `.lks-*` classes the host-native
|
|
14
|
+
* surfaces still use (toolview cards, dock pill, settings section, note
|
|
15
|
+
* cards, read-aloud bar). Authored against the dsh `--dsw-*` design tokens so
|
|
16
|
+
* everything follows the app's theme instead of carrying its own palette.
|
|
13
17
|
* CSS Modules are unavailable to patch-layer bundles, so the stylesheet is
|
|
14
18
|
* injected once at client-plugin apply; the `data-` attribute keeps the
|
|
15
19
|
* injection idempotent.
|
|
@@ -18,8 +22,10 @@ window.__ModuleLoader__.load({
|
|
|
18
22
|
/** Marker attribute proving the stylesheet is already in the document. */
|
|
19
23
|
const STYLE_ID = "data-dsh-plugin-lookatstudy";
|
|
20
24
|
/**
|
|
21
|
-
* The study
|
|
22
|
-
*
|
|
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.
|
|
23
29
|
*/
|
|
24
30
|
const STUDY_CSS = `
|
|
25
31
|
/* State inks: small colored text must clear WCAG AA (4.5:1), but the host's
|
|
@@ -28,211 +34,125 @@ window.__ModuleLoader__.load({
|
|
|
28
34
|
host at all (the tv-chip used to reference them and silently inherit).
|
|
29
35
|
Mixing the state color toward label-primary keeps the hue while pulling
|
|
30
36
|
luminance to the readable pole — and works in BOTH themes, because
|
|
31
|
-
label-primary is always the current theme's high-contrast ink.
|
|
32
|
-
|
|
37
|
+
label-primary is always the current theme's high-contrast ink. Declared on
|
|
38
|
+
every surface family that consumes the inks: .lks-root (dock pill,
|
|
39
|
+
settings) and .lks-tv (toolview cards, which render in the host
|
|
40
|
+
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))}
|
|
42
|
+
.lks-root{font-family:var(--dsw-font-family)}
|
|
33
43
|
.lks-root :where(button){font-family:var(--dsw-font-family);cursor:pointer;border:none;background:none;padding:0}
|
|
34
|
-
.lks-root :where(a){color:var(--dsw-alias-state-business-primary)}
|
|
35
44
|
|
|
36
|
-
/* ──
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
tutor column's real measured width (views.tsx), so the two stay visually
|
|
47
|
-
tied. Flanking columns absorb the remaining width and scroll independently. */
|
|
48
|
-
.lks-study{height:100%;min-height:0;width:100%;box-sizing:border-box;overflow:hidden;display:flex;flex-direction:column;padding:10px 14px calc(var(--dsh-composer-height, 152px) + 12px);color:var(--dsw-alias-label-primary);container:lksstudy/inline-size}
|
|
49
|
-
/* Direction carrier (a container query cannot style the container itself). */
|
|
50
|
-
.lks-body{flex:1;min-height:0;min-width:0;display:flex;gap:16px}
|
|
51
|
-
.lks-col{display:flex;flex-direction:column;min-width:0}
|
|
52
|
-
.lks-colhead{flex:none;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px}
|
|
53
|
-
.lks-col-rail{flex:0 1 260px;min-width:180px;overflow-y:auto;padding:2px 6px}
|
|
54
|
-
.lks-col-tutor{flex:0 1 auto;width:min(100%,var(--dsh-composer-card-max-width,780px),42cqi);min-width:360px;overflow:hidden}
|
|
55
|
-
.lks-col-bb{flex:1 1 0;min-width:320px;overflow-y:auto;padding:2px 6px}
|
|
56
|
-
/* While the study view is mounted in wide mode, the host composer card is
|
|
57
|
-
shifted under the tutor column (the transcript's width axis) instead of the
|
|
58
|
-
scroll body's center — views.tsx measures the offset, sets the variables
|
|
59
|
-
and toggles the class, and removes all three whenever the shift collapses
|
|
60
|
-
(narrow pane, hidden view) so the host's natural centering and width come
|
|
61
|
-
back. max-width tracks the tutor column's live width: the tutor shrinks
|
|
62
|
-
proportionally below ~1857px containers, and the card shrinks with it. */
|
|
63
|
-
[data-composer-card].lks-composer-follow{align-self:flex-start;margin-left:var(--lks-composer-shift,0px);max-width:var(--lks-composer-follow-width,var(--dsh-composer-card-max-width,780px))}
|
|
64
|
-
/* narrow (<1220px): one composer-width pane at a time, chosen by a centered
|
|
65
|
-
segmented pill group (joined buttons in one capsule — button language, not
|
|
66
|
-
tab language, against the host's view tabs right above). */
|
|
67
|
-
.lks-switch{display:none;flex-direction:row;gap:2px;align-self:center;margin-bottom:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:3px}
|
|
68
|
-
.lks-switch-btn{border-radius:999px;padding:5px 18px;font-size:14px;color:var(--dsw-alias-label-tertiary);background:transparent;transition:background .12s ease,color .12s ease}
|
|
69
|
-
.lks-switch-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
|
|
70
|
-
.lks-switch-btn.on{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 86%,#000);color:#fff;font-weight:600}
|
|
71
|
-
@container lksstudy (max-width: 1220px){
|
|
72
|
-
.lks-body{flex-direction:column}
|
|
73
|
-
.lks-switch{display:flex}
|
|
74
|
-
.lks-colhead{display:none}
|
|
75
|
-
.lks-col{flex:1 1 auto;min-height:0;width:auto;overflow-y:auto}
|
|
76
|
-
.lks-col-tutor{width:min(100%,var(--dsh-composer-card-max-width,780px));overflow:hidden;display:flex;flex-direction:column}
|
|
77
|
-
.lks-study[data-pane='rail'] .lks-col-tutor,.lks-study[data-pane='rail'] .lks-col-bb,
|
|
78
|
-
.lks-study[data-pane='tutor'] .lks-col-rail,.lks-study[data-pane='tutor'] .lks-col-bb,
|
|
79
|
-
.lks-study[data-pane='bb'] .lks-col-rail,.lks-study[data-pane='bb'] .lks-col-tutor{display:none}
|
|
80
|
-
}
|
|
45
|
+
/* ── sidebar entry row (DOM-injected next to the session family) ── */
|
|
46
|
+
.lks14-sidebar-row{display:flex;align-items:center;gap:9px;width:100%;padding:7px 10px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-family:var(--dsw-font-family);cursor:pointer;border-radius:8px;text-align:left}
|
|
47
|
+
.lks14-sidebar-row:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
48
|
+
.lks14-sidebar-row[data-active='true']{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:600}
|
|
49
|
+
.lks14-sidebar-row:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
50
|
+
.lks14-sidebar-icon{display:inline-flex;align-items:center;flex:0 0 auto;color:currentColor}
|
|
51
|
+
.lks14-sidebar-label{white-space:nowrap}
|
|
52
|
+
/* Host sidebar collapsed to the icon rail: hide the label, center the icon. */
|
|
53
|
+
[class*='_collapsed'] .lks14-sidebar-row{justify-content:center;padding:7px 0;gap:0}
|
|
54
|
+
[class*='_collapsed'] .lks14-sidebar-label{display:none}
|
|
81
55
|
|
|
82
|
-
/* ──
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
.lks-sec-num{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;border-radius:50%;background:var(--dsw-alias-state-warn-tertiary);color:var(--lks-warn-ink);font-size:11px;font-weight:700;margin-right:4px}
|
|
94
|
-
.lks-tag.due{background:var(--dsw-alias-state-warn-tertiary);color:var(--lks-warn-ink)}
|
|
95
|
-
.lks-import{margin-top:8px}
|
|
96
|
-
.lks-import .lks-inputrow{margin-top:0}
|
|
97
|
-
.lks-inputrow{display:flex;gap:8px;align-items:flex-end;flex:none;margin-top:8px}
|
|
98
|
-
.lks-input{flex:1;min-width:0;resize:none;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:1.6;padding:8px 10px}
|
|
99
|
-
.lks-input:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
|
|
100
|
-
.lks-input:disabled{opacity:.55}
|
|
101
|
-
.lks-import-hint{font-size:12.5px;color:var(--dsw-alias-label-secondary);margin-top:6px;line-height:1.7}
|
|
102
|
-
.lks-duebox{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;margin:8px 0 14px;font-size:13.5px}
|
|
103
|
-
.lks-duebox .lks-due-item{color:var(--dsw-alias-label-secondary);margin-top:4px;display:flex;justify-content:space-between;gap:10px}
|
|
104
|
-
.lks-duebox .lks-over{color:var(--lks-warn-ink);flex:none}
|
|
56
|
+
/* ── center-column takeover (the study panel) ── */
|
|
57
|
+
.lks14-shell-view{display:none}
|
|
58
|
+
html[data-dsh-lookatstudy-active] .lks14-shell-view{display:flex;flex-direction:column;height:100%;min-height:0}
|
|
59
|
+
html[data-dsh-lookatstudy-active] [data-pane='conversation'] > :not([data-dsh-lookatstudy-view]),
|
|
60
|
+
html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatstudy-view]){display:none !important}
|
|
61
|
+
[data-dsh-lookatstudy-view]{container-type:inline-size}
|
|
62
|
+
.lks14{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);overflow:hidden;font-family:var(--dsw-font-family)}
|
|
63
|
+
.lks14 button{font-family:var(--dsw-font-family);cursor:pointer}
|
|
64
|
+
.lks14-body{display:flex;flex:1;min-width:0;min-height:0}
|
|
65
|
+
.lks14-col{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
|
|
66
|
+
.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)}
|
|
105
67
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
-
.
|
|
114
|
-
.
|
|
115
|
-
.
|
|
116
|
-
.
|
|
117
|
-
.
|
|
118
|
-
.
|
|
119
|
-
.
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
.
|
|
129
|
-
.
|
|
130
|
-
.
|
|
131
|
-
.
|
|
68
|
+
/* 左 rail: course picker, tree, review, import */
|
|
69
|
+
.lks14-rail{flex:0 0 240px;border-right:1px solid var(--dsw-alias-border-l1);padding:0 10px 10px;overflow-y:auto}
|
|
70
|
+
.lks14-railhead{display:flex;align-items:center;gap:6px;margin-top:10px}
|
|
71
|
+
.lks14-railtitle{font-weight:600;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
72
|
+
.lks14-railsub{font-size:12px;color:var(--dsw-alias-label-secondary);margin:6px 0}
|
|
73
|
+
.lks14-masterybar{height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);overflow:hidden;margin-bottom:8px}
|
|
74
|
+
.lks14-masterybar i{display:block;height:100%;background:var(--dsw-alias-business-primary);transform-origin:left;transition:transform .3s}
|
|
75
|
+
.lks14-masterybar.gold i{background:var(--dsw-alias-state-warn-primary)}
|
|
76
|
+
.lks14-search{width:100%;box-sizing:border-box;font:inherit;font-size:12.5px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 8px;margin:4px 0}
|
|
77
|
+
.lks14-search:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
|
|
78
|
+
.lks14-duebox{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;margin:8px 0;font-size:12.5px}
|
|
79
|
+
.lks14-dueitem{display:flex;justify-content:space-between;gap:6px;padding:2px 0;color:var(--dsw-alias-label-secondary)}
|
|
80
|
+
.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
|
+
.lks14-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;padding:18px 6px;line-height:1.7}
|
|
98
|
+
.lks14-import{margin:8px 0}
|
|
99
|
+
.lks14-inputrow{display:flex;gap:6px}
|
|
100
|
+
.lks14-hint{font-size:11.5px;color:var(--dsw-alias-label-tertiary);margin-top:6px;line-height:1.6}
|
|
132
101
|
|
|
133
|
-
|
|
134
|
-
.
|
|
102
|
+
/* 中 chat: the tutor stream + its own composer (upstream ChatStream/ChatComposer) */
|
|
103
|
+
.lks14-chat{flex:1 1 46%;min-width:340px;border-right:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-0, transparent)}
|
|
104
|
+
.lks14-chatlesson{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;color:var(--dsw-alias-label-tertiary)}
|
|
105
|
+
.lks14-pills{display:inline-flex;gap:2px}
|
|
106
|
+
.lks14-pill{border:none;background:none;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;padding:2px 8px;border-radius:7px;cursor:pointer}
|
|
107
|
+
.lks14-pill:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
108
|
+
.lks14-pill.on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
109
|
+
.lks14-stream{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:10px}
|
|
110
|
+
.lks14-msg{max-width:92%;line-height:1.65;font-size:13.5px}
|
|
111
|
+
.lks14-msg-user{align-self:flex-end;background:var(--dsw-alias-bg-layer-2);border-radius:12px 12px 3px 12px;padding:7px 11px;white-space:pre-wrap}
|
|
112
|
+
.lks14-msg-assistant{align-self:flex-start;background:var(--dsw-alias-bg-layer-1);border-radius:12px 12px 12px 3px;padding:8px 12px}
|
|
113
|
+
.lks14-msg.streaming{opacity:.7}
|
|
114
|
+
.lks14-msg-assistant p{margin:4px 0}
|
|
115
|
+
.lks14-msg-assistant pre{overflow-x:auto;font-size:12px}
|
|
116
|
+
.lks14-msg-assistant table{border-collapse:collapse;margin:6px 0;display:block;max-width:100%;overflow-x:auto}
|
|
117
|
+
.lks14-msg-assistant th,.lks14-msg-assistant td{border:1px solid var(--dsw-alias-border-l2);padding:4px 8px;font-size:12px}
|
|
118
|
+
.lks14-turn{max-width:92%;align-self:flex-start;display:flex;flex-direction:column;gap:6px}
|
|
119
|
+
.lks14-quiz{display:flex;flex-direction:column;gap:4px}
|
|
120
|
+
.lks14-opt{display:flex;gap:6px;align-items:baseline;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:9px;padding:6px 10px;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left}
|
|
121
|
+
.lks14-opt:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
122
|
+
.lks14-opt:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
123
|
+
.lks14-optletter{flex:none;font-weight:700;color:var(--dsw-alias-business-primary)}
|
|
124
|
+
.lks14-starters{flex:none;display:flex;flex-wrap:wrap;gap:4px;padding:6px 12px}
|
|
125
|
+
.lks14-starter{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;border-radius:14px;padding:3px 10px;cursor:pointer}
|
|
126
|
+
.lks14-starter:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
127
|
+
.lks14-starter:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
128
|
+
.lks14-starter[aria-disabled='true']{opacity:.5;cursor:default}
|
|
129
|
+
.lks14-composer{flex:none;display:flex;gap:6px;align-items:flex-end;padding:8px 12px 10px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
130
|
+
.lks14-composertext{flex:1;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:7px 10px;resize:none;box-sizing:border-box}
|
|
131
|
+
.lks14-composertext:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
|
|
135
132
|
|
|
136
|
-
/*
|
|
137
|
-
.
|
|
138
|
-
.
|
|
139
|
-
.
|
|
140
|
-
.
|
|
141
|
-
.
|
|
142
|
-
.
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
.
|
|
146
|
-
.
|
|
147
|
-
.
|
|
148
|
-
.
|
|
149
|
-
.
|
|
150
|
-
.
|
|
133
|
+
/* 右 notebook: 讲解/概念图/笔记 (upstream NotebookPanel) */
|
|
134
|
+
.lks14-note{flex:0 0 clamp(320px, 30vw, 460px);padding:0 12px 12px;overflow-y:auto}
|
|
135
|
+
.lks14-lessonhead{margin:10px 0 6px}
|
|
136
|
+
.lks14-lessonhead h2{margin:0 0 4px;font-size:16px}
|
|
137
|
+
.lks14-meta{font-size:12px;color:var(--dsw-alias-label-secondary);margin:2px 0}
|
|
138
|
+
.lks14-viewtabs{display:flex;gap:2px;margin:8px 0}
|
|
139
|
+
.lks14-viewtab{border:none;background:none;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12.5px;padding:4px 10px;border-radius:8px;cursor:pointer;display:inline-flex;align-items:center;gap:4px}
|
|
140
|
+
.lks14-viewtab:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
141
|
+
.lks14-viewtab.on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
142
|
+
.lks14-prose{font-size:13.5px;line-height:1.7;color:var(--dsw-alias-label-primary);min-height:200px}
|
|
143
|
+
.lks14-prose h1,.lks14-prose h2,.lks14-prose h3{font-size:15px;margin:12px 0 4px}
|
|
144
|
+
.lks14-prose p{margin:6px 0}
|
|
145
|
+
.lks14-prose pre{overflow-x:auto;font-size:12px;background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:8px}
|
|
146
|
+
.lks14-prose code{font-family:var(--dsw-font-markdown-code);background:var(--dsw-alias-bg-layer-1);border-radius:4px;padding:1px 4px;font-size:.95em}
|
|
147
|
+
.lks14-prose table{border-collapse:collapse;margin:6px 0;display:block;max-width:100%;overflow-x:auto}
|
|
148
|
+
.lks14-prose th,.lks14-prose td{border:1px solid var(--dsw-alias-border-l2);padding:4px 8px;font-size:12.5px}
|
|
149
|
+
.lks14-prose svg{max-width:100%;height:auto}
|
|
150
|
+
.lks14-prose .katex-display{overflow-x:auto}
|
|
151
|
+
.lks14-zones{min-height:200px}
|
|
152
|
+
.lks14-zone{margin-bottom:12px}
|
|
153
|
+
.lks14-zoneh{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:6px}
|
|
151
154
|
|
|
152
|
-
/*
|
|
153
|
-
.lks-pills{display:inline-flex;align-items:center;height:28px}
|
|
154
|
-
.lks-pill{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:none;border-radius:24px;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;font-weight:500;transition:background .12s ease,color .12s ease}
|
|
155
|
-
.lks-pill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
|
|
156
|
-
.lks-pill:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
157
|
-
.lks-pill.on{background:var(--dsw-alias-state-business-tertiary);color:var(--dsw-alias-label-primary-bluish)}
|
|
158
|
-
.lks-pill.on:hover{background:var(--dsw-alias-state-business-tertiary);color:var(--dsw-alias-label-primary-bluish)}
|
|
159
|
-
|
|
160
|
-
/* SVG glyph alignment (status icons, tag badges, dock segments, icon buttons) */
|
|
161
|
-
.lks-g svg{display:block;margin:0 auto}
|
|
162
|
-
.lks-tag{display:inline-flex;align-items:center;gap:2px;line-height:1}
|
|
163
|
-
.lks-dockseg{display:inline-flex;align-items:center;gap:3px}
|
|
164
|
-
.lks-dock-due svg,.lks-dock-streak svg{flex:none}
|
|
165
|
-
.lks-dock-due.lks-muted svg,.lks-dock-streak.lks-muted svg{opacity:.75}
|
|
166
|
-
.lks-btn svg,.lks-viewtab svg,.lks-sechead svg{flex:none}
|
|
167
|
-
.lks-viewtab svg,.lks-btn svg{align-self:center}
|
|
168
|
-
|
|
169
|
-
/* ic_ds_* glyph carriers + busy spinner (native .8s linear spin) */
|
|
170
|
-
@keyframes lks-spin{to{transform:rotate(360deg)}}
|
|
171
|
-
.lks-spin{animation:lks-spin .8s linear infinite}
|
|
172
|
-
|
|
173
|
-
/* dormant state: pills/starters stay visible but inert (aria-disabled, same
|
|
174
|
-
contract as locked lesson rows — focusable so the hint explains why) */
|
|
175
|
-
.lks-pill[aria-disabled='true'],.lks-starter[aria-disabled='true']{opacity:.45;cursor:not-allowed}
|
|
176
|
-
.lks-pill[aria-disabled='true']:hover,.lks-starter[aria-disabled='true']:hover{background:transparent;border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
|
|
177
|
-
.lks-dormant{font-size:13px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;margin:8px 0;line-height:1.7}
|
|
178
|
-
|
|
179
|
-
/* starter chips */
|
|
180
|
-
.lks-dock{display:flex;align-items:center;flex-wrap:wrap;gap:6px;padding:0 2px 6px;flex:none}
|
|
181
|
-
.lks-starter{border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:4px 12px;font-size:13px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);transition:border-color .12s ease,color .12s ease}
|
|
182
|
-
.lks-starter:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}
|
|
183
|
-
|
|
184
|
-
/* proposal banner + shared buttons */
|
|
185
|
-
/* A mastery proposal is a positive, informational moment — business tint, not
|
|
186
|
-
the warn tint it previously wore (a reviewer flagged the semantic mismatch). */
|
|
187
|
-
.lks-banner{display:flex;align-items:center;gap:10px;background:var(--dsw-alias-state-business-tertiary);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 14px;margin:8px 0;font-size:14px;flex:none}
|
|
188
|
-
.lks-banner .lks-why{flex:1;color:var(--dsw-alias-label-secondary)}
|
|
189
|
-
.lks-btn{display:inline-flex;align-items:center;gap:6px;border-radius:8px;padding:6px 14px;font-size:13.5px;font-weight:600;flex:none}
|
|
190
|
-
.lks-btn:focus-visible,.lks-starter:focus-visible,.lks-switch-btn:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
191
|
-
.lks-btn.primary{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 86%,#000);color:#fff}
|
|
192
|
-
.lks-btn.primary:hover{filter:brightness(1.1)}
|
|
193
|
-
.lks-btn.primary:disabled{opacity:.5;cursor:default;filter:none}
|
|
194
|
-
.lks-btn.ghost{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2)}
|
|
195
|
-
.lks-btn.ghost:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
|
|
196
|
-
|
|
197
|
-
/* ── right column: blackboard ── */
|
|
198
|
-
.lks-lessonhead h2{font-size:18px;margin:0 0 4px;font-weight:700}
|
|
199
|
-
.lks-lessonhead .lks-meta{color:var(--dsw-alias-label-secondary);font-size:13px;margin-bottom:4px}
|
|
200
|
-
.lks-chips{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
|
|
201
|
-
.lks-chip{border-radius:999px;padding:2px 10px;font-size:12.5px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}
|
|
202
|
-
.lks-chip.weak{background:var(--dsw-alias-state-warn-tertiary);color:var(--lks-warn-ink)}
|
|
203
|
-
|
|
204
|
-
/* blackboard view tabs (same joined-capsule button language as the narrow
|
|
205
|
-
mode switcher — buttons, not tabs, against the host's view tabs above) */
|
|
206
|
-
.lks-viewtabs{display:flex;gap:2px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:3px;width:max-content;margin:10px 0}
|
|
207
|
-
.lks-viewtab{border-radius:999px;padding:4px 16px;font-size:13.5px;color:var(--dsw-alias-label-secondary);background:transparent;transition:background .12s ease,color .12s ease}
|
|
208
|
-
.lks-viewtab:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
209
|
-
.lks-viewtab.on{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 86%,#000);color:#fff;font-weight:600}
|
|
210
|
-
.lks-viewtab:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
211
|
-
|
|
212
|
-
/* rendered markdown (host-sanitized lesson bodies and assistant replies) */
|
|
213
|
-
.lks-prose{font-size:16px;line-height:1.75}
|
|
214
|
-
.lks-prose h1,.lks-prose h2,.lks-prose h3,.lks-prose h4{margin:14px 0 6px;line-height:1.4}
|
|
215
|
-
.lks-prose p{margin:6px 0}
|
|
216
|
-
.lks-prose pre{background:var(--dsw-alias-markdown-code-block);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px;overflow-x:auto;margin:8px 0;font-size:13.5px;font-family:var(--dsw-font-markdown-code-block-small)}
|
|
217
|
-
.lks-prose code{font-family:var(--dsw-font-markdown-code);background:var(--dsw-alias-bg-layer-3);border-radius:4px;padding:1px 5px;font-size:.92em}
|
|
218
|
-
.lks-prose pre code{background:none;padding:0}
|
|
219
|
-
.lks-prose table{border-collapse:collapse;margin:8px 0;display:block;max-width:100%;overflow-x:auto}
|
|
220
|
-
.lks-prose th,.lks-prose td{border:1px solid var(--dsw-alias-border-l2);padding:5px 11px;font-size:14px}
|
|
221
|
-
.lks-prose th{background:var(--dsw-alias-bg-layer-2)}
|
|
222
|
-
.lks-prose blockquote{border-left:3px solid var(--dsw-alias-state-business-primary);padding:2px 12px;color:var(--dsw-alias-label-secondary);margin:8px 0}
|
|
223
|
-
.lks-prose ul,.lks-prose ol{padding-left:22px;margin:6px 0}
|
|
224
|
-
.lks-prose img{max-width:100%}
|
|
225
|
-
/* width guards for CDN-rendered artifacts (both teach and diagram panes are
|
|
226
|
-
.lks-prose): mermaid/elk SVGs carry fixed width attributes; KaTeX
|
|
227
|
-
display math can exceed the column; both must degrade to fit/scroll
|
|
228
|
-
internally instead of stretching the blackboard. */
|
|
229
|
-
.lks-prose svg{max-width:100%;height:auto}
|
|
230
|
-
.lks-prose .katex-display{overflow-x:auto;overflow-y:hidden;padding:2px 0}
|
|
231
|
-
|
|
232
|
-
/* Cornell notes zones */
|
|
233
|
-
.lks-bb-notes{margin-top:14px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
234
|
-
.lks-zone{margin-bottom:14px}
|
|
235
|
-
.lks-zone-h{font-size:14px;color:var(--dsw-alias-label-secondary);margin:0 0 8px;font-weight:600}
|
|
155
|
+
/* note cards (shared: panel notebook + settings-era shapes) */
|
|
236
156
|
.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}
|
|
237
157
|
.lks-note .lks-note-src{float:right;font-size:11.5px;color:var(--dsw-alias-label-secondary)}
|
|
238
158
|
.lks-note-del{float:right;clear:right;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;padding:2px;border-radius:5px;line-height:0}
|
|
@@ -246,34 +166,51 @@ window.__ModuleLoader__.load({
|
|
|
246
166
|
.lks-note .lks-note-text code{font-family:var(--dsw-font-markdown-code);background:var(--dsw-alias-bg-layer-3);border-radius:4px;padding:1px 4px;font-size:.95em}
|
|
247
167
|
.lks-note .lks-note-q{margin-top:6px;color:var(--dsw-alias-label-tertiary);font-size:12px;border-left:2px solid var(--dsw-alias-state-warn-primary);padding-left:8px}
|
|
248
168
|
|
|
249
|
-
/* inline error text
|
|
169
|
+
/* shared buttons + inline error text */
|
|
170
|
+
.lks-btn{display:inline-flex;align-items:center;gap:6px;border-radius:8px;padding:6px 14px;font-size:13.5px;font-weight:600;flex:none;font-family:var(--dsw-font-family);cursor:pointer}
|
|
171
|
+
.lks-btn:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
172
|
+
.lks-btn.primary{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 86%,#000);color:#fff}
|
|
173
|
+
.lks-btn.primary:hover{filter:brightness(1.1)}
|
|
174
|
+
.lks-btn.primary:disabled{opacity:.5;cursor:default;filter:none}
|
|
175
|
+
.lks-btn.ghost{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2)}
|
|
176
|
+
.lks-btn.ghost:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
|
|
250
177
|
.lks-propcard-err{color:var(--dsw-alias-state-error-primary);font-size:12px;margin-top:6px;flex:none}
|
|
251
178
|
|
|
252
|
-
/* read-aloud bar (teach pane
|
|
179
|
+
/* read-aloud bar (notebook teach pane) + busy spinner */
|
|
180
|
+
@keyframes lks-spin{to{transform:rotate(360deg)}}
|
|
181
|
+
.lks-spin{animation:lks-spin .8s linear infinite}
|
|
253
182
|
.lks-readbar{display:flex;align-items:center;gap:6px;margin:0 0 8px;min-height:26px}
|
|
254
183
|
.lks-readbar .lks-btn{display:inline-flex;align-items:center;gap:4px}
|
|
255
184
|
.lks-readbar-cur{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;color:var(--dsw-alias-label-secondary);border-left:2px solid var(--dsw-alias-state-info-primary,var(--dsw-alias-business-primary));padding-left:8px}
|
|
256
185
|
.lks-readbar-notice{flex:none;font-size:11.5px;color:var(--dsw-alias-label-tertiary)}
|
|
257
186
|
|
|
187
|
+
/* concept map legend (amber = weak) */
|
|
188
|
+
.lks-cmap-legend{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:4px}
|
|
189
|
+
.lks-cmap-legend i{width:10px;height:10px;border-radius:3px;background:var(--dsw-alias-state-warn-tertiary,#fef5e7);border:1px solid var(--dsw-alias-state-warn-primary,#dd8629)}
|
|
190
|
+
|
|
258
191
|
/* settings section (settings.section entry inside the host settings shell) */
|
|
259
192
|
.lks-settings{display:flex;flex-direction:column;gap:18px;font-family:var(--dsw-font-family);color:var(--dsw-alias-label-primary)}
|
|
260
193
|
.lks-set-row h3{margin:0 0 4px;font-size:15px;font-weight:600}
|
|
261
194
|
.lks-set-hint{margin:0 0 10px;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
262
195
|
.lks-set-state{margin-left:10px;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
196
|
+
.lks-set-select{font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 8px;max-width:100%}
|
|
263
197
|
.lks-set-stats{margin:0;padding-left:18px;font-size:14px;line-height:1.9}
|
|
264
198
|
.lks-set-path{font-family:var(--dsw-font-markdown-code);font-size:12.5px;background:var(--dsw-alias-bg-layer-3);border-radius:6px;padding:3px 8px;word-break:break-all}
|
|
265
199
|
|
|
200
|
+
/* soul pills (settings reuse) */
|
|
201
|
+
.lks-pills{display:inline-flex;align-items:center;height:28px}
|
|
202
|
+
.lks-pill{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:none;border-radius:24px;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;font-weight:500;transition:background .12s ease,color .12s ease;font-family:var(--dsw-font-family);cursor:pointer}
|
|
203
|
+
.lks-pill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
|
|
204
|
+
.lks-pill:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary);outline:none}
|
|
205
|
+
.lks-pill.on{background:var(--dsw-alias-state-business-tertiary);color:var(--dsw-alias-label-primary-bluish)}
|
|
206
|
+
|
|
266
207
|
/* composer dock status pill (conversation.composer.dock entry) */
|
|
267
208
|
.lks-dockpill{display:inline-flex;gap:10px;align-items:center;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
268
|
-
.lks-dockseg{white-space:nowrap}
|
|
209
|
+
.lks-dockseg{white-space:nowrap;display:inline-flex;align-items:center;gap:3px}
|
|
269
210
|
.lks-dock-due{color:var(--lks-warn-ink)}
|
|
270
211
|
.lks-dock-streak{color:var(--dsw-alias-state-business-primary)}
|
|
271
212
|
.lks-dockseg.lks-muted{color:var(--dsw-alias-label-secondary)}
|
|
272
213
|
|
|
273
|
-
/* concept map legend (amber = weak) */
|
|
274
|
-
.lks-cmap-legend{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:4px}
|
|
275
|
-
.lks-cmap-legend i{width:10px;height:10px;border-radius:3px;background:var(--dsw-alias-state-warn-tertiary,#fef5e7);border:1px solid var(--dsw-alias-state-warn-primary,#dd8629)}
|
|
276
|
-
|
|
277
214
|
/* keyed tool.call.toolview cards (conversation tab tool rows) */
|
|
278
215
|
.lks-tv{display:flex;flex-direction:column;gap:4px;padding:6px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);font-size:13px}
|
|
279
216
|
.lks-tv.err{border-color:var(--dsw-alias-state-error-primary)}
|
|
@@ -281,6 +218,7 @@ window.__ModuleLoader__.load({
|
|
|
281
218
|
.lks-tv-chip{align-self:flex-start;border-radius:999px;padding:2px 10px;font-size:12.5px;font-weight:600}
|
|
282
219
|
.lks-tv-chip.ok{background:var(--dsw-alias-state-success-tertiary);color:var(--lks-ok-ink)}
|
|
283
220
|
.lks-tv-chip.bad{background:var(--dsw-alias-state-error-tertiary);color:var(--lks-err-ink)}
|
|
221
|
+
.lks-tv-lines{display:flex;flex-direction:column;gap:4px}
|
|
284
222
|
.lks-tv-line{color:var(--dsw-alias-label-secondary);line-height:1.6;white-space:pre-wrap}
|
|
285
223
|
/* exam star card + the failure moment */
|
|
286
224
|
.lks-tv-stars{display:inline-flex;align-items:center;gap:5px;font-size:13px;font-weight:600;color:var(--lks-warn-ink)}
|
|
@@ -318,25 +256,6 @@ window.__ModuleLoader__.load({
|
|
|
318
256
|
* language — those are NOT ic_ds, see their section comment below.
|
|
319
257
|
* @module dsh-plugin-lookatstudy/client/icons
|
|
320
258
|
*/
|
|
321
|
-
/** ic_ds_think_outline_16 */
|
|
322
|
-
function IconThinkOutline16({ size = 16, className }) {
|
|
323
|
-
return (0, react.createElement)("svg", {
|
|
324
|
-
width: size,
|
|
325
|
-
height: size,
|
|
326
|
-
className,
|
|
327
|
-
viewBox: "0 0 16 16",
|
|
328
|
-
fill: "none",
|
|
329
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
330
|
-
}, (0, react.createElement)("path", {
|
|
331
|
-
d: "M8.00192 6.64454C8.75026 6.64454 9.35732 7.25169 9.35739 8.00001C9.35739 8.74838 8.7503 9.35548 8.00192 9.35548C7.25367 9.35533 6.64743 8.74829 6.64743 8.00001C6.6475 7.25178 7.25371 6.64468 8.00192 6.64454Z",
|
|
332
|
-
fill: "currentColor"
|
|
333
|
-
}), (0, react.createElement)("path", {
|
|
334
|
-
"fill-rule": "evenodd",
|
|
335
|
-
"clip-rule": "evenodd",
|
|
336
|
-
d: "M9.97165 1.29981C11.5853 0.718916 13.271 0.642197 14.3144 1.68555C15.3577 2.72902 15.2811 4.41466 14.7002 6.02833C14.4707 6.66561 14.1504 7.32937 13.75 8.00001C14.1504 8.67062 14.4707 9.33444 14.7002 9.97169C15.2811 11.5854 15.3578 13.271 14.3144 14.3145C13.271 15.3579 11.5854 15.2811 9.97165 14.7002C9.3344 14.4708 8.67059 14.1505 7.99997 13.75C7.32933 14.1505 6.66558 14.4708 6.02829 14.7002C4.41461 15.2811 2.72899 15.3578 1.68552 14.3145C0.642155 13.271 0.71887 11.5854 1.29977 9.97169C1.52915 9.33454 1.84865 8.67049 2.24899 8.00001C1.84866 7.32953 1.52915 6.66544 1.29977 6.02833C0.718852 4.41459 0.64207 2.729 1.68552 1.68555C2.72897 0.642112 4.41456 0.718887 6.02829 1.29981C6.66541 1.52918 7.32949 1.8487 7.99997 2.24903C8.67045 1.84869 9.33451 1.52919 9.97165 1.29981ZM12.9404 9.2129C12.4391 9.893 11.8616 10.5681 11.2148 11.2149C10.568 11.8616 9.89296 12.4391 9.21286 12.9404C9.62532 13.1579 10.0271 13.338 10.4121 13.4766C11.9146 14.0174 12.9172 13.8738 13.3955 13.3955C13.8737 12.9173 14.0174 11.9146 13.4765 10.4121C13.3379 10.0271 13.1578 9.62535 12.9404 9.2129ZM3.05856 9.2129C2.84121 9.62523 2.66197 10.0272 2.52341 10.4121C1.98252 11.9146 2.12627 12.9172 2.60446 13.3955C3.08278 13.8737 4.08544 14.0174 5.58786 13.4766C5.97264 13.338 6.37389 13.1577 6.7861 12.9404C6.10624 12.4393 5.43168 11.8614 4.78513 11.2149C4.13823 10.5679 3.55992 9.89313 3.05856 9.2129ZM7.99899 3.792C7.23179 4.31419 6.45306 4.95512 5.70407 5.70411C4.95509 6.45309 4.31415 7.23184 3.79196 7.99903C4.3143 8.76666 4.95471 9.54653 5.70407 10.2959C6.45309 11.0449 7.23271 11.6848 7.99997 12.207C8.76725 11.6848 9.54683 11.0449 10.2959 10.2959C11.0449 9.54686 11.6848 8.76729 12.207 8.00001C11.6848 7.23275 11.0449 6.45312 10.2959 5.70411C9.5465 4.95475 8.76662 4.31434 7.99899 3.792ZM5.58786 2.52344C4.08533 1.98255 3.08272 2.12625 2.60446 2.6045C2.12621 3.08275 1.98252 4.08536 2.52341 5.5879C2.66189 5.97253 2.8414 6.37409 3.05856 6.78614C3.55983 6.10611 4.1384 5.43189 4.78513 4.78516C5.43186 4.13843 6.10606 3.55987 6.7861 3.0586C6.37405 2.84144 5.97249 2.66192 5.58786 2.52344ZM13.3955 2.6045C12.9172 2.12631 11.9146 1.98257 10.4121 2.52344C10.0272 2.66201 9.62519 2.84125 9.21286 3.0586C9.8931 3.55996 10.5679 4.13827 11.2148 4.78516C11.8614 5.43172 12.4392 6.10627 12.9404 6.78614C13.1577 6.37393 13.338 5.97267 13.4765 5.5879C14.0174 4.08549 13.8736 3.08281 13.3955 2.6045Z",
|
|
337
|
-
fill: "currentColor"
|
|
338
|
-
}));
|
|
339
|
-
}
|
|
340
259
|
/** ic_ds_loading_outline_16 */
|
|
341
260
|
function IconLoadingOutline16({ size = 16, className }) {
|
|
342
261
|
return (0, react.createElement)("svg", {
|
|
@@ -365,38 +284,6 @@ window.__ModuleLoader__.load({
|
|
|
365
284
|
fill: "currentColor"
|
|
366
285
|
}));
|
|
367
286
|
}
|
|
368
|
-
/** Vendored 2026-09-01 for the critique icon round (same figma source as the three above). */
|
|
369
|
-
/** IconPlayOutline16 */
|
|
370
|
-
function IconPlayOutline16({ size = 16, className }) {
|
|
371
|
-
return (0, react.createElement)("svg", {
|
|
372
|
-
width: size,
|
|
373
|
-
height: size,
|
|
374
|
-
className,
|
|
375
|
-
viewBox: "0 0 16 16",
|
|
376
|
-
fill: "none",
|
|
377
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
378
|
-
}, (0, react.createElement)("path", {
|
|
379
|
-
d: "M14.1446 8C14.1446 4.6062 11.3938 1.85539 8 1.85539C4.6062 1.85539 1.85539 4.6062 1.85539 8C1.85539 11.3938 4.6062 14.1446 8 14.1446C11.3938 14.1446 14.1446 11.3938 14.1446 8ZM15.511 8C15.511 12.148 12.148 15.511 8 15.511C3.85202 15.511 0.489014 12.148 0.489014 8C0.489014 3.85202 3.85202 0.489014 8 0.489014C12.148 0.489014 15.511 3.85202 15.511 8Z",
|
|
380
|
-
fill: "currentColor"
|
|
381
|
-
}), (0, react.createElement)("path", {
|
|
382
|
-
d: "M10.5617 8.42578C10.852 8.21614 10.852 7.78386 10.5617 7.57422L7.25708 5.18751C6.90974 4.93666 6.42436 5.18484 6.42436 5.61329V10.3867C6.42436 10.8152 6.90974 11.0633 7.25708 10.8125L10.5617 8.42578Z",
|
|
383
|
-
fill: "currentColor"
|
|
384
|
-
}));
|
|
385
|
-
}
|
|
386
|
-
/** IconPlusOutline16 */
|
|
387
|
-
function IconPlusOutline16({ size = 16, className }) {
|
|
388
|
-
return (0, react.createElement)("svg", {
|
|
389
|
-
width: size,
|
|
390
|
-
height: size,
|
|
391
|
-
className,
|
|
392
|
-
viewBox: "0 0 16 16",
|
|
393
|
-
fill: "none",
|
|
394
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
395
|
-
}, (0, react.createElement)("path", {
|
|
396
|
-
d: "M8.64453 1.5V7.34961H14.5V8.65039H8.64453V14.5H7.34473V8.65039H1.5V7.34961H7.34473V1.5H8.64453Z",
|
|
397
|
-
fill: "currentColor"
|
|
398
|
-
}));
|
|
399
|
-
}
|
|
400
287
|
/** IconTrashOutline16 */
|
|
401
288
|
function IconTrashOutline16({ size = 16, className }) {
|
|
402
289
|
return (0, react.createElement)("svg", {
|
|
@@ -459,26 +346,6 @@ window.__ModuleLoader__.load({
|
|
|
459
346
|
fill: "currentColor"
|
|
460
347
|
}));
|
|
461
348
|
}
|
|
462
|
-
/** IconWarningOutline16 */
|
|
463
|
-
function IconWarningOutline16({ size = 16, className }) {
|
|
464
|
-
return (0, react.createElement)("svg", {
|
|
465
|
-
width: size,
|
|
466
|
-
height: size,
|
|
467
|
-
className,
|
|
468
|
-
viewBox: "0 0 16 16",
|
|
469
|
-
fill: "none",
|
|
470
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
471
|
-
}, (0, react.createElement)("path", {
|
|
472
|
-
d: "M6.3002 3.32843L7.69986 3.32843L7.69986 7.79657H6.3002L6.3002 3.32843Z",
|
|
473
|
-
fill: "currentColor"
|
|
474
|
-
}), (0, react.createElement)("path", {
|
|
475
|
-
d: "M6.3002 9.01935H7.69986V10.6711H6.3002V9.01935Z",
|
|
476
|
-
fill: "currentColor"
|
|
477
|
-
}), (0, react.createElement)("path", {
|
|
478
|
-
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",
|
|
479
|
-
fill: "currentColor"
|
|
480
|
-
}));
|
|
481
|
-
}
|
|
482
349
|
/**
|
|
483
350
|
* Plugin-original companions for learning-domain glyphs the host set does not
|
|
484
351
|
* carry (lock/star/book/crown/bolt/power/pin/flame) — authored here in the
|
|
@@ -559,41 +426,6 @@ window.__ModuleLoader__.load({
|
|
|
559
426
|
fill: "currentColor"
|
|
560
427
|
}));
|
|
561
428
|
}
|
|
562
|
-
/** IconPowerFill16 */
|
|
563
|
-
function IconPowerFill16({ size = 16, className }) {
|
|
564
|
-
return (0, react.createElement)("svg", {
|
|
565
|
-
width: size,
|
|
566
|
-
height: size,
|
|
567
|
-
className,
|
|
568
|
-
viewBox: "0 0 16 16",
|
|
569
|
-
fill: "none",
|
|
570
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
571
|
-
}, (0, react.createElement)("path", {
|
|
572
|
-
d: "M7.2 1h1.6v5.6H7.2V1Z",
|
|
573
|
-
fill: "currentColor"
|
|
574
|
-
}), (0, react.createElement)("path", {
|
|
575
|
-
"fill-rule": "evenodd",
|
|
576
|
-
"clip-rule": "evenodd",
|
|
577
|
-
d: "M4.55 3.15A6 6 0 1 0 11.45 3.15L12.5 4.2a7.5 7.5 0 1 1-9 0l1.05-1.05Z",
|
|
578
|
-
fill: "currentColor"
|
|
579
|
-
}));
|
|
580
|
-
}
|
|
581
|
-
/** IconPinFill16 */
|
|
582
|
-
function IconPinFill16({ size = 16, className }) {
|
|
583
|
-
return (0, react.createElement)("svg", {
|
|
584
|
-
width: size,
|
|
585
|
-
height: size,
|
|
586
|
-
className,
|
|
587
|
-
viewBox: "0 0 16 16",
|
|
588
|
-
fill: "none",
|
|
589
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
590
|
-
}, (0, react.createElement)("path", {
|
|
591
|
-
"fill-rule": "evenodd",
|
|
592
|
-
"clip-rule": "evenodd",
|
|
593
|
-
d: "M8 1.4a4.8 4.8 0 0 1 4.8 4.8c0 3.4-4.8 8.4-4.8 8.4S3.2 9.6 3.2 6.2A4.8 4.8 0 0 1 8 1.4Zm0 2.7a2.1 2.1 0 1 0 0 4.2 2.1 2.1 0 0 0 0-4.2Z",
|
|
594
|
-
fill: "currentColor"
|
|
595
|
-
}));
|
|
596
|
-
}
|
|
597
429
|
/** IconFlameFill16 */
|
|
598
430
|
function IconFlameFill16({ size = 16, className }) {
|
|
599
431
|
return (0, react.createElement)("svg", {
|
|
@@ -754,6 +586,51 @@ window.__ModuleLoader__.load({
|
|
|
754
586
|
return bytes.buffer;
|
|
755
587
|
}
|
|
756
588
|
};
|
|
589
|
+
/** The voices the dashboard route accepts (same allowlist server-side). */
|
|
590
|
+
const TTS_VOICES = [
|
|
591
|
+
{
|
|
592
|
+
id: "zh-CN-XiaoxiaoNeural",
|
|
593
|
+
labelKey: "voice.xiaoxiao"
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
id: "zh-CN-YunxiNeural",
|
|
597
|
+
labelKey: "voice.yunxi"
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
id: "zh-CN-YunyangNeural",
|
|
601
|
+
labelKey: "voice.yunyang"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
id: "zh-CN-XiaoyiNeural",
|
|
605
|
+
labelKey: "voice.xiaoyi"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "en-US-AriaNeural",
|
|
609
|
+
labelKey: "voice.aria"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
id: "en-US-GuyNeural",
|
|
613
|
+
labelKey: "voice.guy"
|
|
614
|
+
}
|
|
615
|
+
];
|
|
616
|
+
const TTS_VOICE_KEY = "dsh-plugin-lookatstudy:tts-voice";
|
|
617
|
+
/** Narrow a stored voice id to the allowlist; anything else falls back to 晓晓. Pure. */
|
|
618
|
+
function normalizeStoredVoice(raw) {
|
|
619
|
+
return TTS_VOICES.some((v) => v.id === raw) ? raw : TTS_VOICES[0].id;
|
|
620
|
+
}
|
|
621
|
+
/** The selected read-aloud voice (browser-local preference; host state untouched). */
|
|
622
|
+
function storedTtsVoice() {
|
|
623
|
+
try {
|
|
624
|
+
return normalizeStoredVoice(typeof localStorage === "undefined" ? null : localStorage.getItem(TTS_VOICE_KEY));
|
|
625
|
+
} catch {
|
|
626
|
+
return TTS_VOICES[0].id;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function storeTtsVoice(id) {
|
|
630
|
+
try {
|
|
631
|
+
localStorage.setItem(TTS_VOICE_KEY, normalizeStoredVoice(id));
|
|
632
|
+
} catch {}
|
|
633
|
+
}
|
|
757
634
|
/** The one shared store instance backing every study seat component. */
|
|
758
635
|
const studyStore = new StudyStore();
|
|
759
636
|
/** React binding: live snapshot plus the write actions. */
|
|
@@ -1592,6 +1469,8 @@ window.__ModuleLoader__.load({
|
|
|
1592
1469
|
"rail.empty.hint2": "本地文件夹 → 说「导入 D:/path/to/folder」",
|
|
1593
1470
|
"rail.empty.placeholder": "GitHub 仓库链接,如 microsoft/AI-For-Beginners",
|
|
1594
1471
|
"rail.empty.button": "导入",
|
|
1472
|
+
"composer.send": "发送",
|
|
1473
|
+
"composer.busy": "导师正在回复…",
|
|
1595
1474
|
"rail.empty.demo": "导入示例课程",
|
|
1596
1475
|
"rail.mastered": "毕业 {mastered}/{total} 课",
|
|
1597
1476
|
"rail.avg": "平均掌握度 {pct}%",
|
|
@@ -1664,6 +1543,14 @@ window.__ModuleLoader__.load({
|
|
|
1664
1543
|
"settings.mode": "教学风格",
|
|
1665
1544
|
"settings.mode.hint": "导师的讲解风格;也可在学习页随时切换",
|
|
1666
1545
|
"settings.studyMode": "学习模式",
|
|
1546
|
+
"settings.voice": "朗读语音",
|
|
1547
|
+
"settings.voice.hint": "「朗读本课」使用的神经网络声音(浏览器本地偏好)",
|
|
1548
|
+
"voice.xiaoxiao": "晓晓 · 中文女声",
|
|
1549
|
+
"voice.yunxi": "云希 · 中文男声",
|
|
1550
|
+
"voice.yunyang": "云扬 · 中文男声(新闻)",
|
|
1551
|
+
"voice.xiaoyi": "晓伊 · 中文女声(轻快)",
|
|
1552
|
+
"voice.aria": "Aria · English female",
|
|
1553
|
+
"voice.guy": "Guy · English male",
|
|
1667
1554
|
"settings.studyMode.hint": "开启后注册 study_* 工具并载入导师人格;进度与笔记始终保留",
|
|
1668
1555
|
"settings.on": "状态:已开启",
|
|
1669
1556
|
"settings.off": "状态:已关闭",
|
|
@@ -1720,6 +1607,8 @@ window.__ModuleLoader__.load({
|
|
|
1720
1607
|
"rail.empty.hint2": "Local folder → say \"import D:/path/to/folder\"",
|
|
1721
1608
|
"rail.empty.placeholder": "GitHub repo link, e.g. microsoft/AI-For-Beginners",
|
|
1722
1609
|
"rail.empty.button": "Import",
|
|
1610
|
+
"composer.send": "Send",
|
|
1611
|
+
"composer.busy": "The tutor is replying…",
|
|
1723
1612
|
"rail.empty.demo": "Import the demo course",
|
|
1724
1613
|
"rail.mastered": "{mastered}/{total} lessons graduated",
|
|
1725
1614
|
"rail.avg": "avg mastery {pct}%",
|
|
@@ -1792,6 +1681,14 @@ window.__ModuleLoader__.load({
|
|
|
1792
1681
|
"settings.mode": "Teaching style",
|
|
1793
1682
|
"settings.mode.hint": "The tutor's explanation style; switchable anytime in the study tab too",
|
|
1794
1683
|
"settings.studyMode": "Study mode",
|
|
1684
|
+
"settings.voice": "Read-aloud voice",
|
|
1685
|
+
"settings.voice.hint": "The neural voice used by 朗读本课 (a browser-local preference)",
|
|
1686
|
+
"voice.xiaoxiao": "Xiaoxiao · Chinese female",
|
|
1687
|
+
"voice.yunxi": "Yunxi · Chinese male",
|
|
1688
|
+
"voice.yunyang": "Yunyang · Chinese male (news)",
|
|
1689
|
+
"voice.xiaoyi": "Xiaoyi · Chinese female (lively)",
|
|
1690
|
+
"voice.aria": "Aria · English female",
|
|
1691
|
+
"voice.guy": "Guy · English male",
|
|
1795
1692
|
"settings.studyMode.hint": "On registers the study_* tools and the tutor persona; progress and notes are always kept",
|
|
1796
1693
|
"settings.on": "Status: on",
|
|
1797
1694
|
"settings.off": "Status: off",
|
|
@@ -2286,6 +2183,74 @@ window.__ModuleLoader__.load({
|
|
|
2286
2183
|
});
|
|
2287
2184
|
}
|
|
2288
2185
|
//#endregion
|
|
2186
|
+
//#region src/client/session-feed.ts
|
|
2187
|
+
function userText(event) {
|
|
2188
|
+
const content = event.data?.content;
|
|
2189
|
+
if (content === void 0) return "";
|
|
2190
|
+
return content.filter((part) => (part.type ?? "text") === "text").map((part) => part.text ?? "").join("\n\n").trim();
|
|
2191
|
+
}
|
|
2192
|
+
function assistantText(event) {
|
|
2193
|
+
const message = event.data?.message;
|
|
2194
|
+
if (message === void 0) return "";
|
|
2195
|
+
return (message.content ?? message.blocks ?? []).filter((block) => (block.kind ?? block.type) === "text").map((block) => block.text ?? "").join("\n\n").trim();
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Fold one event-window snapshot into ordered chat rows. The transient
|
|
2199
|
+
* `text-delta` chunks of the most recent attempt accumulate into a single
|
|
2200
|
+
* streaming row (the durable `assistant/message` replaces it on settlement).
|
|
2201
|
+
* @param window - the bound session's event window snapshot, or undefined.
|
|
2202
|
+
*/
|
|
2203
|
+
function feedRows(window) {
|
|
2204
|
+
if (window === void 0) return [];
|
|
2205
|
+
const rows = [];
|
|
2206
|
+
const streams = /* @__PURE__ */ new Map();
|
|
2207
|
+
let latest;
|
|
2208
|
+
for (const entry of window.entries) {
|
|
2209
|
+
const event = entry?.event;
|
|
2210
|
+
if (event === void 0) continue;
|
|
2211
|
+
if (entry.type === "event") {
|
|
2212
|
+
if (event.type === "user/message") {
|
|
2213
|
+
const text = userText(event);
|
|
2214
|
+
if (text !== "") rows.push({
|
|
2215
|
+
key: `u${event.seq}`,
|
|
2216
|
+
role: "user",
|
|
2217
|
+
text
|
|
2218
|
+
});
|
|
2219
|
+
} else if (event.type === "assistant/message") {
|
|
2220
|
+
const text = assistantText(event);
|
|
2221
|
+
if (text !== "") rows.push({
|
|
2222
|
+
key: `a${event.seq}`,
|
|
2223
|
+
role: "assistant",
|
|
2224
|
+
text
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
for (const s of streams.values()) if (rows.length > s.anchor) s.settled = true;
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const chunk = event.data?.chunk;
|
|
2231
|
+
if (chunk === void 0 || chunk.type !== "text-delta") continue;
|
|
2232
|
+
const attemptId = event.data?.attemptId ?? String(event.seq);
|
|
2233
|
+
const stream = streams.get(attemptId);
|
|
2234
|
+
if (stream === void 0) streams.set(attemptId, {
|
|
2235
|
+
text: chunk.text ?? "",
|
|
2236
|
+
anchor: rows.length,
|
|
2237
|
+
settled: false
|
|
2238
|
+
});
|
|
2239
|
+
else stream.text += chunk.text ?? "";
|
|
2240
|
+
latest = attemptId;
|
|
2241
|
+
}
|
|
2242
|
+
const stream = latest !== void 0 ? streams.get(latest) : void 0;
|
|
2243
|
+
if (stream !== void 0 && !stream.settled) {
|
|
2244
|
+
const text = stream.text.trim();
|
|
2245
|
+
if (text !== "") rows.splice(stream.anchor, 0, {
|
|
2246
|
+
key: "streaming",
|
|
2247
|
+
role: "streaming",
|
|
2248
|
+
text
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
return rows;
|
|
2252
|
+
}
|
|
2253
|
+
//#endregion
|
|
2289
2254
|
//#region src/client/readaloud.ts
|
|
2290
2255
|
const sleep = (ms) => new Promise((resolve) => {
|
|
2291
2256
|
setTimeout(resolve, ms);
|
|
@@ -2353,7 +2318,10 @@ window.__ModuleLoader__.load({
|
|
|
2353
2318
|
return this.run;
|
|
2354
2319
|
}
|
|
2355
2320
|
async speakOn(text) {
|
|
2356
|
-
|
|
2321
|
+
const impl = this.engine === "system" ? this.fallback : this.primary;
|
|
2322
|
+
const next = this.sentences[this.index + 1];
|
|
2323
|
+
if (next !== void 0) impl.prewarm?.(next);
|
|
2324
|
+
await impl.speak(text);
|
|
2357
2325
|
while (this.paused && !this.stopped) await sleep(120);
|
|
2358
2326
|
}
|
|
2359
2327
|
pause() {
|
|
@@ -2389,97 +2357,20 @@ window.__ModuleLoader__.load({
|
|
|
2389
2357
|
//#endregion
|
|
2390
2358
|
//#region src/client/views.tsx
|
|
2391
2359
|
/**
|
|
2392
|
-
*
|
|
2393
|
-
*
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
2396
|
-
* snapshot (user text, assistant text rendered through the plugin's own
|
|
2397
|
-
* markdown pipeline; tool calls stay in the host conversation's toolview
|
|
2398
|
-
* cards — a mirrored second record diverged) plus the soul pills, starters,
|
|
2399
|
-
* pending proposal banner, and a reverse-channel input. Right: the blackboard —
|
|
2400
|
-
* focus-lesson 讲解 and the Cornell 笔记 zones. Nothing outside this tab
|
|
2401
|
-
* touches dsh chrome.
|
|
2360
|
+
* Shared pure helpers for the study panel's pieces: chat-row shapes, tree
|
|
2361
|
+
* status copy, quiz-option extraction, and section default expansion. The
|
|
2362
|
+
* panel itself lives in `panel.tsx`; since 0.14.0 nothing here touches the
|
|
2363
|
+
* host composer or the conversation view.
|
|
2402
2364
|
* @module dsh-plugin-lookatstudy/client/views
|
|
2403
2365
|
*/
|
|
2404
|
-
/**
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
labelKey: "soul.direct",
|
|
2409
|
-
hintKey: "soul.direct.hint"
|
|
2410
|
-
},
|
|
2411
|
-
{
|
|
2412
|
-
id: "guide",
|
|
2413
|
-
labelKey: "soul.guide",
|
|
2414
|
-
hintKey: "soul.guide.hint"
|
|
2415
|
-
},
|
|
2416
|
-
{
|
|
2417
|
-
id: "practice",
|
|
2418
|
-
labelKey: "soul.practice",
|
|
2419
|
-
hintKey: "soul.practice.hint"
|
|
2420
|
-
}
|
|
2421
|
-
];
|
|
2422
|
-
/** Zone keys for the Cornell notebook (understand / record / practice). */
|
|
2423
|
-
const ZONES = [
|
|
2424
|
-
["understand", "zone.understand"],
|
|
2425
|
-
["record", "zone.record"],
|
|
2426
|
-
["practice", "zone.practice"]
|
|
2427
|
-
];
|
|
2428
|
-
/** Status icon for one lesson row (LookatStudy's map icon semantics, SVG glyphs). */
|
|
2429
|
-
function statusIcon(kind, status) {
|
|
2430
|
-
if (kind === "exam") return (0, react.createElement)(IconGoalOutline16, { size: 14 });
|
|
2431
|
-
if (status === "mastered") return (0, react.createElement)(IconCrownFill16, { size: 14 });
|
|
2432
|
-
if (status === "in_progress") return (0, react.createElement)(IconBookFill16, { size: 14 });
|
|
2433
|
-
if (status === "available") return (0, react.createElement)(IconStarFill16, { size: 14 });
|
|
2434
|
-
return (0, react.createElement)(IconLockFill16, { size: 14 });
|
|
2435
|
-
}
|
|
2436
|
-
/** LookatStudy's exam gate: an exam node opens only when every sibling study lesson reached mastery ≥50%. */
|
|
2437
|
-
function examOpen(lessons) {
|
|
2438
|
-
return lessons.every((l) => l.kind !== "study" || (l.masteryPct ?? 0) >= 50);
|
|
2439
|
-
}
|
|
2440
|
-
/** Multi-keyword AND title filter (LookatStudy course-tree-filter). */
|
|
2441
|
-
function titleMatches(title, query) {
|
|
2442
|
-
return query.trim().toLowerCase().split(/\s+/).filter(Boolean).every((k) => title.toLowerCase().includes(k));
|
|
2443
|
-
}
|
|
2444
|
-
/**
|
|
2445
|
-
* Default expansion for one rail section: collapsed when every study lesson is
|
|
2446
|
-
* done (mastered) or not yet reachable (locked) — long courses otherwise scroll
|
|
2447
|
-
* 5× their viewport. The active frontier and the focus lesson's section stay
|
|
2448
|
-
* open; exam nodes never force a section open (they are gated separately).
|
|
2449
|
-
* Pure.
|
|
2450
|
-
* @param section - one course section projection with lesson kinds/statuses.
|
|
2451
|
-
*/
|
|
2452
|
-
function sectionDefaultOpen(section) {
|
|
2453
|
-
return section.lessons.some((l) => l.focus || l.kind !== "exam" && l.status !== "mastered" && l.status !== "locked");
|
|
2454
|
-
}
|
|
2455
|
-
/** The rail's import row: a GitHub URL input plus the paste/folder hints. */
|
|
2456
|
-
function ImportRow({ send }) {
|
|
2457
|
-
const [url, setUrl] = (0, react.useState)("");
|
|
2458
|
-
return (0, react.createElement)("div", { className: "lks-import" }, (0, react.createElement)("div", { className: "lks-inputrow" }, (0, react.createElement)("input", {
|
|
2459
|
-
className: "lks-input",
|
|
2460
|
-
type: "url",
|
|
2461
|
-
placeholder: tr("rail.empty.placeholder"),
|
|
2462
|
-
value: url,
|
|
2463
|
-
onChange: (e) => {
|
|
2464
|
-
setUrl(e.target.value);
|
|
2465
|
-
},
|
|
2466
|
-
onKeyDown: (e) => {
|
|
2467
|
-
if (e.key === "Enter" && url.trim() !== "") send(tr("prompt.import", { url: url.trim() }));
|
|
2468
|
-
}
|
|
2469
|
-
}), (0, react.createElement)("button", {
|
|
2470
|
-
className: "lks-btn primary",
|
|
2471
|
-
disabled: url.trim() === "",
|
|
2472
|
-
onClick: () => {
|
|
2473
|
-
send(tr("prompt.import", { url: url.trim() }));
|
|
2474
|
-
}
|
|
2475
|
-
}, tr("rail.empty.button"))), (0, react.createElement)("div", { className: "lks-import-hint" }, tr("rail.empty.hint"), (0, react.createElement)("br"), tr("rail.empty.hint2")));
|
|
2366
|
+
/** Small inline error surface for failed write actions (shared with settings). */
|
|
2367
|
+
function ActionError({ error }) {
|
|
2368
|
+
if (error === null) return null;
|
|
2369
|
+
return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
|
|
2476
2370
|
}
|
|
2477
2371
|
/**
|
|
2478
2372
|
* Tooltip text for one course-tree status glyph. Pure (the translator is an
|
|
2479
2373
|
* optional parameter defaulting to the active one, evaluated per call).
|
|
2480
|
-
* @param kind - lesson kind ('study' | 'practice' | 'exam').
|
|
2481
|
-
* @param status - lesson status ('locked' | 'available' | 'in_progress' | 'mastered').
|
|
2482
|
-
* @param t - translator (defaults to the module's active translator).
|
|
2483
2374
|
*/
|
|
2484
2375
|
function statusTitle(kind, status, t = tr) {
|
|
2485
2376
|
if (kind === "exam") return t("status.exam");
|
|
@@ -2494,8 +2385,6 @@ window.__ModuleLoader__.load({
|
|
|
2494
2385
|
* Extract the last quiz option block (2–4 consecutive `A. …` `B. …` lines,
|
|
2495
2386
|
* letters strictly consecutive from A) from an assistant reply — the raw text
|
|
2496
2387
|
* of the tutor's question becomes the clickable answer surface. Pure.
|
|
2497
|
-
* @param text - one assistant reply's markdown.
|
|
2498
|
-
* @returns the options of the last valid block, or [] when none.
|
|
2499
2388
|
*/
|
|
2500
2389
|
function quizOptions(text) {
|
|
2501
2390
|
const runs = [];
|
|
@@ -2526,268 +2415,216 @@ window.__ModuleLoader__.load({
|
|
|
2526
2415
|
flush();
|
|
2527
2416
|
return runs.at(-1) ?? [];
|
|
2528
2417
|
}
|
|
2529
|
-
function textOf(blocks) {
|
|
2530
|
-
return (blocks ?? []).filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n\n").trim();
|
|
2531
|
-
}
|
|
2532
|
-
function assistantText(blocks) {
|
|
2533
|
-
return (blocks ?? []).filter((b) => b.kind === "text").map((b) => b.text ?? "").join("\n\n").trim();
|
|
2534
|
-
}
|
|
2535
2418
|
/**
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
* surface (a transcript mirror there grew a second, diverging record).
|
|
2540
|
-
* Both inputs tolerate undefined: newer hosts (DSH v0.1.2-alpha.4) mount
|
|
2541
|
-
* the conversation view before session data exists, and the fold must
|
|
2542
|
-
* degrade to an empty transcript instead of blanking the whole tab.
|
|
2543
|
-
* @param nodes - finalized conversation nodes from the snapshot.
|
|
2544
|
-
* @param partial - the in-flight assistant partial, or null.
|
|
2545
|
-
* @param t - translator (defaults to the active one, per call).
|
|
2546
|
-
* @returns ordered rows; never mutates its inputs.
|
|
2419
|
+
* Default expansion for one rail section: collapsed when every study lesson is
|
|
2420
|
+
* done (mastered) or not yet reachable (locked). The focus lesson's section
|
|
2421
|
+
* stays open; exam nodes never force a section open. Pure.
|
|
2547
2422
|
*/
|
|
2548
|
-
function
|
|
2549
|
-
|
|
2550
|
-
for (const node of nodes ?? []) switch (node.kind) {
|
|
2551
|
-
case "user":
|
|
2552
|
-
case "steering": {
|
|
2553
|
-
const text = textOf(node.content);
|
|
2554
|
-
if (text !== "") rows.push({
|
|
2555
|
-
key: `u${node.seq}`,
|
|
2556
|
-
role: "user",
|
|
2557
|
-
text
|
|
2558
|
-
});
|
|
2559
|
-
break;
|
|
2560
|
-
}
|
|
2561
|
-
case "assistant": {
|
|
2562
|
-
const text = assistantText(node.blocks);
|
|
2563
|
-
if (text !== "") rows.push({
|
|
2564
|
-
key: `a${node.seq}`,
|
|
2565
|
-
role: "assistant",
|
|
2566
|
-
text
|
|
2567
|
-
});
|
|
2568
|
-
break;
|
|
2569
|
-
}
|
|
2570
|
-
case "turn-error": rows.push({
|
|
2571
|
-
key: `e${node.seq}`,
|
|
2572
|
-
role: "error",
|
|
2573
|
-
text: node.message ?? ""
|
|
2574
|
-
});
|
|
2575
|
-
}
|
|
2576
|
-
if (partial != null) {
|
|
2577
|
-
const text = assistantText(partial.blocks);
|
|
2578
|
-
if (text !== "") rows.push({
|
|
2579
|
-
key: "streaming",
|
|
2580
|
-
role: "streaming",
|
|
2581
|
-
text
|
|
2582
|
-
});
|
|
2583
|
-
else rows.push({
|
|
2584
|
-
key: "thinking",
|
|
2585
|
-
role: "thinking",
|
|
2586
|
-
text: t("row.thinking")
|
|
2587
|
-
});
|
|
2588
|
-
}
|
|
2589
|
-
return rows;
|
|
2590
|
-
}
|
|
2591
|
-
/** Small inline error surface for failed write actions (shared with the settings page). */
|
|
2592
|
-
function ActionError({ error }) {
|
|
2593
|
-
if (error === null) return null;
|
|
2594
|
-
return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
|
|
2423
|
+
function sectionDefaultOpen(section) {
|
|
2424
|
+
return section.lessons.some((l) => l.focus || l.kind !== "exam" && l.status !== "mastered" && l.status !== "locked");
|
|
2595
2425
|
}
|
|
2426
|
+
//#endregion
|
|
2427
|
+
//#region src/client/panel.tsx
|
|
2596
2428
|
/**
|
|
2597
|
-
*
|
|
2598
|
-
*
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2429
|
+
* The study panel — the sidebar-entry surface that takes over the center
|
|
2430
|
+
* column, arranged like upstream LookatStudy's app (App.tsx): 左 course rail
|
|
2431
|
+
* (tree/import/review) | 中 the tutor chat stream with its OWN composer (the
|
|
2432
|
+
* host composer is never involved) | 右 the notebook (讲解/概念图/笔记).
|
|
2433
|
+
*
|
|
2434
|
+
* Interaction model aligned with upstream: opening a lesson only FOCUSES it
|
|
2435
|
+
* (state-side attempt: in_progress + BKT seed + unlocks — zero LLM); the
|
|
2436
|
+
* tutor engages when the learner sends a message or taps a starter, which
|
|
2437
|
+
* lazily mints and stages the lesson's thread and prompts through the host's
|
|
2438
|
+
* session face. The host is exactly the conversation-model + agent-turn
|
|
2439
|
+
* engine; the thread's event window streams the reply back into the chat.
|
|
2440
|
+
* @module dsh-plugin-lookatstudy/client/panel
|
|
2602
2441
|
*/
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
const
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2442
|
+
/** The three souls in pill order (same shape as the study tab's pills). */
|
|
2443
|
+
const MODES$1 = [
|
|
2444
|
+
{
|
|
2445
|
+
id: "direct",
|
|
2446
|
+
labelKey: "soul.direct",
|
|
2447
|
+
hintKey: "soul.direct.hint"
|
|
2448
|
+
},
|
|
2449
|
+
{
|
|
2450
|
+
id: "guide",
|
|
2451
|
+
labelKey: "soul.guide",
|
|
2452
|
+
hintKey: "soul.guide.hint"
|
|
2453
|
+
},
|
|
2454
|
+
{
|
|
2455
|
+
id: "practice",
|
|
2456
|
+
labelKey: "soul.practice",
|
|
2457
|
+
hintKey: "soul.practice.hint"
|
|
2458
|
+
}
|
|
2459
|
+
];
|
|
2460
|
+
const ZONES = [
|
|
2461
|
+
["understand", "zone.understand"],
|
|
2462
|
+
["record", "zone.record"],
|
|
2463
|
+
["practice", "zone.practice"]
|
|
2464
|
+
];
|
|
2465
|
+
function statusIcon(kind, status) {
|
|
2466
|
+
if (kind === "exam") return (0, react.createElement)(IconGoalOutline16, { size: 14 });
|
|
2467
|
+
if (status === "mastered") return (0, react.createElement)(IconCrownFill16, { size: 14 });
|
|
2468
|
+
if (status === "in_progress") return (0, react.createElement)(IconBookFill16, { size: 14 });
|
|
2469
|
+
if (status === "available") return (0, react.createElement)(IconStarFill16, { size: 14 });
|
|
2470
|
+
return (0, react.createElement)(IconLockFill16, { size: 14 });
|
|
2471
|
+
}
|
|
2472
|
+
function examOpen(lessons) {
|
|
2473
|
+
return lessons.every((l) => l.kind !== "study" || (l.masteryPct ?? 0) >= 50);
|
|
2474
|
+
}
|
|
2475
|
+
function titleMatches(title, query) {
|
|
2476
|
+
return query.trim().toLowerCase().split(/\s+/).filter(Boolean).every((k) => title.toLowerCase().includes(k));
|
|
2477
|
+
}
|
|
2478
|
+
/** One rendered chat row (shared shape with the read-aloud era's renderer). */
|
|
2479
|
+
function chatRow(row, interactive) {
|
|
2480
|
+
if (row.role === "user") return (0, react.createElement)("div", {
|
|
2481
|
+
key: row.key,
|
|
2482
|
+
className: "lks14-msg lks14-msg-user"
|
|
2483
|
+
}, row.text);
|
|
2484
|
+
const cls = row.role === "streaming" ? "lks14-msg lks14-msg-assistant streaming" : "lks14-msg lks14-msg-assistant";
|
|
2485
|
+
const body = (0, react.createElement)("div", {
|
|
2486
|
+
key: row.key,
|
|
2487
|
+
className: cls,
|
|
2488
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(row.text) }
|
|
2489
|
+
});
|
|
2490
|
+
const options = interactive === void 0 || row.role === "streaming" ? [] : quizOptions(row.text);
|
|
2491
|
+
if (options.length < 2) return body;
|
|
2492
|
+
return (0, react.createElement)("div", {
|
|
2493
|
+
key: row.key,
|
|
2494
|
+
className: "lks14-turn"
|
|
2495
|
+
}, body, (0, react.createElement)("div", {
|
|
2496
|
+
className: "lks14-quiz",
|
|
2497
|
+
title: tr("quiz.title")
|
|
2498
|
+
}, ...options.map((opt) => (0, react.createElement)("button", {
|
|
2499
|
+
key: opt.letter,
|
|
2500
|
+
className: "lks14-opt",
|
|
2501
|
+
onClick: () => {
|
|
2502
|
+
interactive.send(tr("quiz.answer", {
|
|
2503
|
+
letter: opt.letter,
|
|
2504
|
+
text: opt.text
|
|
2505
|
+
}));
|
|
2506
|
+
}
|
|
2507
|
+
}, (0, react.createElement)("span", { className: "lks14-optletter" }, opt.letter), (0, react.createElement)("span", null, opt.text)))));
|
|
2508
|
+
}
|
|
2509
|
+
/** Build the panel component bound to the client services. */
|
|
2510
|
+
function studyPanelView(ctx) {
|
|
2511
|
+
return function StudyPanel() {
|
|
2512
|
+
return (0, react.createElement)(StudyPanelBody, {
|
|
2513
|
+
ctx,
|
|
2514
|
+
key: "panel"
|
|
2629
2515
|
});
|
|
2630
2516
|
};
|
|
2631
2517
|
}
|
|
2632
|
-
|
|
2633
|
-
function StudyTab({ inputActions, ctx, ...standard }) {
|
|
2518
|
+
function StudyPanelBody({ ctx }) {
|
|
2634
2519
|
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession } = useStudy();
|
|
2635
|
-
const
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
const
|
|
2639
|
-
const
|
|
2640
|
-
const
|
|
2520
|
+
const [sendError, setSendError] = (0, react.useState)(null);
|
|
2521
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2522
|
+
const [rows, setRows] = (0, react.useState)([]);
|
|
2523
|
+
const [draft, setDraft] = (0, react.useState)("");
|
|
2524
|
+
const lesson = data?.lesson ?? null;
|
|
2525
|
+
const localBound = (0, react.useRef)(null);
|
|
2526
|
+
const streamEl = (0, react.useRef)(null);
|
|
2527
|
+
const boundId = localBound.current ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
|
|
2641
2528
|
(0, react.useEffect)(() => {
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
const
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
}
|
|
2651
|
-
const
|
|
2652
|
-
|
|
2653
|
-
const rail = root.querySelector(".lks-col-rail");
|
|
2654
|
-
if (tutor === null || rail === null || getComputedStyle(rail).display === "none") return clear();
|
|
2655
|
-
const seatRect = seat.getBoundingClientRect();
|
|
2656
|
-
const tutorRect = tutor.getBoundingClientRect();
|
|
2657
|
-
if (seatRect.width === 0 || tutorRect.width === 0) return clear();
|
|
2658
|
-
const pad = parseFloat(getComputedStyle(seat).paddingLeft) || 0;
|
|
2659
|
-
const cardW = card.getBoundingClientRect().width;
|
|
2660
|
-
const max = seatRect.width - 2 * pad - cardW;
|
|
2661
|
-
const shift = Math.round(Math.max(0, Math.min(tutorRect.left - (seatRect.left + pad), max)));
|
|
2662
|
-
if (shift < 2) return clear();
|
|
2663
|
-
card.style.setProperty("--lks-composer-shift", `${shift}px`);
|
|
2664
|
-
card.style.setProperty("--lks-composer-follow-width", `${Math.round(tutorRect.width)}px`);
|
|
2665
|
-
card.classList.add("lks-composer-follow");
|
|
2666
|
-
};
|
|
2667
|
-
apply();
|
|
2668
|
-
const ro = new ResizeObserver(apply);
|
|
2669
|
-
ro.observe(root);
|
|
2670
|
-
ro.observe(seat);
|
|
2671
|
-
window.addEventListener("resize", apply);
|
|
2672
|
-
return () => {
|
|
2673
|
-
ro.disconnect();
|
|
2674
|
-
window.removeEventListener("resize", apply);
|
|
2675
|
-
clear();
|
|
2529
|
+
if (boundId === null) {
|
|
2530
|
+
setRows([]);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
const source = ctx.sessions.binding(boundId)?.eventSource;
|
|
2534
|
+
if (source === void 0) {
|
|
2535
|
+
setRows([]);
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
const update = () => {
|
|
2539
|
+
setRows(feedRows(source.getSnapshot()));
|
|
2676
2540
|
};
|
|
2677
|
-
|
|
2541
|
+
update();
|
|
2542
|
+
return source.subscribe(update);
|
|
2543
|
+
}, [boundId, ctx]);
|
|
2544
|
+
(0, react.useEffect)(() => {
|
|
2545
|
+
const el = streamEl.current;
|
|
2546
|
+
if (el !== null) el.scrollTop = el.scrollHeight;
|
|
2547
|
+
}, [rows]);
|
|
2548
|
+
/** The one send path: lazily stage the lesson thread, then prompt. */
|
|
2678
2549
|
const send = (text) => {
|
|
2679
2550
|
(async () => {
|
|
2680
|
-
if (
|
|
2681
|
-
|
|
2682
|
-
|
|
2551
|
+
if (busy || lesson === null || text.trim() === "") return;
|
|
2552
|
+
setBusy(true);
|
|
2553
|
+
setSendError(null);
|
|
2554
|
+
try {
|
|
2555
|
+
if (data?.active !== true) await activate(true);
|
|
2556
|
+
let sessionId = localBound.current ?? data?.lessonSessions[lesson.lessonId];
|
|
2557
|
+
if (sessionId === void 0 || sessionId === null) {
|
|
2558
|
+
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
2559
|
+
if (!area.ok) throw new Error(`study area unavailable (HTTP ${String(area.status)})`);
|
|
2560
|
+
const { path } = await area.json();
|
|
2561
|
+
const workspace = await ctx.workspaces.create({ path });
|
|
2562
|
+
sessionId = await ctx.sessions.create({ workspaceId: workspace.workspaceId });
|
|
2563
|
+
localBound.current = sessionId;
|
|
2564
|
+
await bindLessonSession(lesson.lessonId, sessionId);
|
|
2565
|
+
}
|
|
2566
|
+
const shell = PANEL_SHELL.current;
|
|
2567
|
+
if (shell !== null) shell.suppressHandBack(() => {
|
|
2568
|
+
ctx.sessions.open(sessionId);
|
|
2569
|
+
});
|
|
2570
|
+
else ctx.sessions.open(sessionId);
|
|
2571
|
+
const actx = ctx.sessions.scope(sessionId);
|
|
2572
|
+
const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
|
|
2573
|
+
if (face === void 0) throw new Error("lesson session is not addressable yet");
|
|
2574
|
+
const result = await face.prompt([{
|
|
2575
|
+
type: "text",
|
|
2576
|
+
text
|
|
2577
|
+
}], "queue");
|
|
2578
|
+
if (!result.ok) throw new Error(`prompt rejected: ${result.error.code}: ${result.error.message}`);
|
|
2579
|
+
setDraft("");
|
|
2580
|
+
} catch (err) {
|
|
2581
|
+
setSendError(err instanceof Error ? err.message : String(err));
|
|
2582
|
+
} finally {
|
|
2583
|
+
setBusy(false);
|
|
2584
|
+
}
|
|
2683
2585
|
})();
|
|
2684
2586
|
};
|
|
2685
|
-
const
|
|
2686
|
-
(0, react.useEffect)(() => {
|
|
2687
|
-
const localeSvc = ctx.locale;
|
|
2688
|
-
if (localeSvc === void 0) return;
|
|
2689
|
-
return localeSvc.subscribe(() => {
|
|
2690
|
-
bumpLocale((v) => v + 1);
|
|
2691
|
-
});
|
|
2692
|
-
}, [ctx]);
|
|
2693
|
-
return (0, react.createElement)("div", {
|
|
2694
|
-
ref: rootRef,
|
|
2695
|
-
className: "lks-root lks-study",
|
|
2696
|
-
"data-conversation-composer-overlay": "",
|
|
2697
|
-
"data-pane": pane
|
|
2698
|
-
}, (0, react.createElement)("div", { className: "lks-actbar" }, data === null ? null : (0, react.createElement)("button", {
|
|
2699
|
-
className: `lks-btn ${data.active ? "ghost" : "primary"}`,
|
|
2700
|
-
title: data.active ? tr("active.on.title") : tr("active.off.title"),
|
|
2701
|
-
onClick: () => {
|
|
2702
|
-
activate(!data.active);
|
|
2703
|
-
}
|
|
2704
|
-
}, data.active ? (0, react.createElement)(IconPowerFill16, { size: 13 }) : (0, react.createElement)(IconPlayOutline16, { size: 13 }), data.active ? tr("active.on") : tr("active.off"))), (0, react.createElement)("div", { className: "lks-body" }, (0, react.createElement)("div", { className: "lks-switch" }, ...[
|
|
2705
|
-
{
|
|
2706
|
-
id: "rail",
|
|
2707
|
-
labelKey: "pane.rail"
|
|
2708
|
-
},
|
|
2709
|
-
{
|
|
2710
|
-
id: "tutor",
|
|
2711
|
-
labelKey: "pane.tutor"
|
|
2712
|
-
},
|
|
2713
|
-
{
|
|
2714
|
-
id: "bb",
|
|
2715
|
-
labelKey: "pane.bb"
|
|
2716
|
-
}
|
|
2717
|
-
].map((p) => (0, react.createElement)("button", {
|
|
2718
|
-
key: p.id,
|
|
2719
|
-
className: `lks-switch-btn${pane === p.id ? " on" : ""}`,
|
|
2720
|
-
"aria-pressed": String(pane === p.id),
|
|
2721
|
-
onClick: () => {
|
|
2722
|
-
setPane(p.id);
|
|
2723
|
-
try {
|
|
2724
|
-
sessionStorage.setItem(PANE_KEY, p.id);
|
|
2725
|
-
} catch {}
|
|
2726
|
-
}
|
|
2727
|
-
}, tr(p.labelKey)))), (0, react.createElement)(CourseRail, {
|
|
2587
|
+
const body = (0, react.createElement)("div", { className: "lks14-body" }, (0, react.createElement)(CourseRail, {
|
|
2728
2588
|
data,
|
|
2729
2589
|
activate,
|
|
2730
2590
|
setFocus,
|
|
2731
2591
|
searchLessons,
|
|
2732
2592
|
deleteCourse,
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
ctx,
|
|
2736
|
-
currentSessionId
|
|
2737
|
-
}), (0, react.createElement)(TutorColumn, {
|
|
2593
|
+
send
|
|
2594
|
+
}), (0, react.createElement)(ChatPane, {
|
|
2738
2595
|
data,
|
|
2739
|
-
|
|
2596
|
+
lesson,
|
|
2597
|
+
rows,
|
|
2598
|
+
busy,
|
|
2599
|
+
sendError,
|
|
2600
|
+
draft,
|
|
2601
|
+
setDraft,
|
|
2740
2602
|
send,
|
|
2741
|
-
|
|
2742
|
-
}), (0, react.createElement)(
|
|
2603
|
+
setMode
|
|
2604
|
+
}), (0, react.createElement)(NotebookPane, {
|
|
2743
2605
|
data,
|
|
2744
2606
|
deleteNote
|
|
2745
|
-
}))
|
|
2607
|
+
}));
|
|
2608
|
+
return (0, react.createElement)("div", {
|
|
2609
|
+
className: "lks14",
|
|
2610
|
+
"data-lks-panel": ""
|
|
2611
|
+
}, body);
|
|
2746
2612
|
}
|
|
2747
|
-
/**
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
if (mapped === currentSessionId) {
|
|
2755
|
-
setFocus(lesson.id).catch(reportError);
|
|
2756
|
-
return;
|
|
2757
|
-
}
|
|
2758
|
-
if (mapped !== void 0 && ctx.sessions.binding(sessionId(mapped)) !== void 0) {
|
|
2759
|
-
setFocus(lesson.id).catch(reportError);
|
|
2760
|
-
ctx.sessions.open(sessionId(mapped));
|
|
2761
|
-
return;
|
|
2762
|
-
}
|
|
2763
|
-
setJumping(lesson.id);
|
|
2764
|
-
setFocus(lesson.id).then(() => (async () => {
|
|
2765
|
-
if (data?.active !== true) await activate(true);
|
|
2766
|
-
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
2767
|
-
if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
|
|
2768
|
-
const { path } = await area.json();
|
|
2769
|
-
const workspace = await ctx.workspaces.create({ path });
|
|
2770
|
-
const sessionId = ctx.workspaces.connectWorkspace !== void 0 ? await ctx.workspaces.connectWorkspace(workspace.workspaceId) : await ctx.sessions.create({ workspaceId: workspace.workspaceId });
|
|
2771
|
-
const actx = ctx.sessions.scope(sessionId);
|
|
2772
|
-
const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
|
|
2773
|
-
if (face === void 0) throw new Error("lesson session is not addressable yet");
|
|
2774
|
-
const result = await face.prompt([{
|
|
2775
|
-
type: "text",
|
|
2776
|
-
text: tr("prompt.lesson", { title: lesson.title })
|
|
2777
|
-
}], "queue");
|
|
2778
|
-
if (!result.ok) throw new Error(`lesson prompt rejected: ${result.error.code}: ${result.error.message}`);
|
|
2779
|
-
await bindLessonSession(lesson.id, sessionId);
|
|
2780
|
-
ctx.sessions.open(sessionId);
|
|
2781
|
-
})()).then(() => {
|
|
2782
|
-
setJumping(null);
|
|
2783
|
-
}, (err) => {
|
|
2784
|
-
reportError(err);
|
|
2785
|
-
setJumping(null);
|
|
2786
|
-
});
|
|
2787
|
-
};
|
|
2613
|
+
/** Module-level shell handle so the panel can suppress hand-back on internal opens. */
|
|
2614
|
+
const PANEL_SHELL = { current: null };
|
|
2615
|
+
function setPanelShell(shell) {
|
|
2616
|
+
PANEL_SHELL.current = shell;
|
|
2617
|
+
}
|
|
2618
|
+
/** 左栏:course picker, tree, review box, import. */
|
|
2619
|
+
function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send }) {
|
|
2788
2620
|
const [selectedCourse, setSelectedCourse] = (0, react.useState)("");
|
|
2789
2621
|
const [query, setQuery] = (0, react.useState)("");
|
|
2790
2622
|
const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
|
|
2623
|
+
const [showImport, setShowImport] = (0, react.useState)(false);
|
|
2624
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2625
|
+
const reportError = (err) => {
|
|
2626
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2627
|
+
};
|
|
2791
2628
|
(0, react.useEffect)(() => {
|
|
2792
2629
|
if (!confirmDelete) return;
|
|
2793
2630
|
const disarm = () => {
|
|
@@ -2803,14 +2640,7 @@ window.__ModuleLoader__.load({
|
|
|
2803
2640
|
window.removeEventListener("keydown", onKey);
|
|
2804
2641
|
};
|
|
2805
2642
|
}, [confirmDelete]);
|
|
2806
|
-
const
|
|
2807
|
-
/** Per-section open overrides (courseId/sectionTitle → open?), on top of the frontier default. */
|
|
2808
|
-
const [secOpen, setSecOpen] = (0, react.useState)({});
|
|
2809
|
-
const [error, setError] = (0, react.useState)(null);
|
|
2810
|
-
const reportError = (err) => {
|
|
2811
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
2812
|
-
};
|
|
2813
|
-
const body = data === null ? (0, react.createElement)("div", { className: "lks-empty" }, tr("loading")) : data.courses.length === 0 ? (0, react.createElement)("div", { className: "lks-empty" }, tr("rail.empty.title"), (0, react.createElement)("br"), (0, react.createElement)("button", {
|
|
2643
|
+
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", {
|
|
2814
2644
|
className: "lks-btn primary",
|
|
2815
2645
|
style: { margin: "10px 0" },
|
|
2816
2646
|
onClick: () => {
|
|
@@ -2819,8 +2649,8 @@ window.__ModuleLoader__.load({
|
|
|
2819
2649
|
}, (0, react.createElement)(IconDownloadOutline16, null), tr("rail.empty.demo")), (0, react.createElement)(ImportRow, { send })) : (() => {
|
|
2820
2650
|
const courseId = data.courses.some((c) => c.courseId === selectedCourse) ? selectedCourse : data.courses[0].courseId;
|
|
2821
2651
|
const course = data.courses.find((c) => c.courseId === courseId);
|
|
2822
|
-
return (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "
|
|
2823
|
-
className: "lks-
|
|
2652
|
+
return (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "lks14-railhead" }, data.courses.length > 1 ? (0, react.createElement)("select", {
|
|
2653
|
+
className: "lks-set-select",
|
|
2824
2654
|
value: courseId,
|
|
2825
2655
|
onChange: (e) => {
|
|
2826
2656
|
setSelectedCourse(e.target.value);
|
|
@@ -2830,7 +2660,7 @@ window.__ModuleLoader__.load({
|
|
|
2830
2660
|
key: c.courseId,
|
|
2831
2661
|
value: c.courseId
|
|
2832
2662
|
}, c.title))) : (0, react.createElement)("div", {
|
|
2833
|
-
className: "
|
|
2663
|
+
className: "lks14-railtitle",
|
|
2834
2664
|
title: course.title
|
|
2835
2665
|
}, course.title), (0, react.createElement)("button", {
|
|
2836
2666
|
className: `lks-btn ${confirmDelete ? "primary" : "ghost"}`,
|
|
@@ -2846,14 +2676,14 @@ window.__ModuleLoader__.load({
|
|
|
2846
2676
|
setSelectedCourse("");
|
|
2847
2677
|
}, reportError);
|
|
2848
2678
|
}
|
|
2849
|
-
}, confirmDelete ? tr("rail.delete.confirm") : (0, react.createElement)(IconTrashOutline16, { size: 14 }))), (0, react.createElement)("div", { className: "
|
|
2679
|
+
}, confirmDelete ? tr("rail.delete.confirm") : (0, react.createElement)(IconTrashOutline16, { size: 14 }))), (0, react.createElement)("div", { className: "lks14-railsub" }, tr("rail.mastered", {
|
|
2850
2680
|
mastered: course.mastered,
|
|
2851
2681
|
total: course.total
|
|
2852
2682
|
})), (0, react.createElement)("div", {
|
|
2853
|
-
className: `
|
|
2683
|
+
className: `lks14-masterybar${course.avgMasteryPct === 100 ? " gold" : ""}`,
|
|
2854
2684
|
title: course.avgMasteryPct === null ? tr("rail.avg.none") : tr("rail.avg", { pct: course.avgMasteryPct })
|
|
2855
2685
|
}, (0, react.createElement)("i", { style: { transform: `scaleX(${(course.avgMasteryPct ?? 0) / 100})` } })), (0, react.createElement)("input", {
|
|
2856
|
-
className: "
|
|
2686
|
+
className: "lks14-search",
|
|
2857
2687
|
type: "search",
|
|
2858
2688
|
placeholder: tr("rail.search"),
|
|
2859
2689
|
value: query,
|
|
@@ -2877,32 +2707,10 @@ window.__ModuleLoader__.load({
|
|
|
2877
2707
|
}
|
|
2878
2708
|
}).catch(reportError);
|
|
2879
2709
|
}
|
|
2880
|
-
}),
|
|
2881
|
-
className: "lks-btn ghost",
|
|
2882
|
-
style: {
|
|
2883
|
-
margin: "0 0 6px",
|
|
2884
|
-
padding: "3px 8px",
|
|
2885
|
-
fontSize: "13px"
|
|
2886
|
-
},
|
|
2887
|
-
title: tr("rail.locate.title"),
|
|
2888
|
-
onClick: () => {
|
|
2889
|
-
const focusSection = course.sections.find((s) => s.lessons.some((l) => l.focus));
|
|
2890
|
-
if (focusSection === void 0) return;
|
|
2891
|
-
setSecOpen((m) => ({
|
|
2892
|
-
...m,
|
|
2893
|
-
[`${courseId}/${focusSection.title}`]: true
|
|
2894
|
-
}));
|
|
2895
|
-
window.setTimeout(() => {
|
|
2896
|
-
document.querySelector(".lks-col-rail .lks-node.focus")?.scrollIntoView({
|
|
2897
|
-
block: "center",
|
|
2898
|
-
behavior: "smooth"
|
|
2899
|
-
});
|
|
2900
|
-
}, 60);
|
|
2901
|
-
}
|
|
2902
|
-
}, (0, react.createElement)(IconPinFill16, { size: 13 }), tr("rail.locate")) : null, data.dueCount > 0 ? (0, react.createElement)("div", { className: "lks-duebox" }, (0, react.createElement)(IconRefreshOutline16, { size: 13 }), tr("rail.due", { count: data.dueCount }), ...data.due.map((d) => (0, react.createElement)("div", {
|
|
2710
|
+
}), 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)("div", {
|
|
2903
2711
|
key: d.lessonId,
|
|
2904
|
-
className: "
|
|
2905
|
-
}, (0, react.createElement)("span", null, d.lessonTitle), d.overdueDays > 0 ? (0, react.createElement)("span", { className: "
|
|
2712
|
+
className: "lks14-dueitem"
|
|
2713
|
+
}, (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", {
|
|
2906
2714
|
className: "lks-btn ghost",
|
|
2907
2715
|
style: { marginTop: "6px" },
|
|
2908
2716
|
onClick: () => {
|
|
@@ -2912,56 +2720,33 @@ window.__ModuleLoader__.load({
|
|
|
2912
2720
|
const examAllowed = examOpen(section.lessons);
|
|
2913
2721
|
const lessons = section.lessons.filter((l) => query.trim() === "" || titleMatches(l.title, query) || l.focus);
|
|
2914
2722
|
if (lessons.length === 0) return [];
|
|
2915
|
-
const
|
|
2916
|
-
const open = query.trim() !== "" || (secOpen[secKey] ?? sectionDefaultOpen(section));
|
|
2723
|
+
const open = query.trim() !== "" || sectionDefaultOpen(section);
|
|
2917
2724
|
return [(0, react.createElement)("button", {
|
|
2918
2725
|
key: section.title,
|
|
2919
2726
|
type: "button",
|
|
2920
|
-
className: "
|
|
2727
|
+
className: "lks14-sechead",
|
|
2921
2728
|
"aria-expanded": String(open),
|
|
2922
2729
|
title: open ? tr("rail.section.collapse") : tr("rail.section.expand", { count: section.lessons.length }),
|
|
2923
|
-
onClick: () => {
|
|
2924
|
-
|
|
2925
|
-
...m,
|
|
2926
|
-
[secKey]: !open
|
|
2927
|
-
}));
|
|
2928
|
-
}
|
|
2929
|
-
}, (0, react.createElement)("span", { className: "lks-sec-num" }, String(section.index + 1)), (0, react.createElement)("span", { className: "lks-sechead-t" }, section.title), open ? null : (0, react.createElement)("span", { className: "lks-sechead-n" }, tr("rail.section.count", { count: section.lessons.length })), (0, react.createElement)("span", { className: "lks-sechead-c" }, open ? "▾" : "▸")), ...open ? lessons.map((lesson) => {
|
|
2730
|
+
onClick: () => {}
|
|
2731
|
+
}, (0, react.createElement)("span", { className: "lks14-secnum" }, String(section.index + 1)), (0, react.createElement)("span", { className: "lks14-secheadt" }, section.title), (0, react.createElement)("span", { className: "lks14-secheadc" }, open ? "▾" : "▸")), ...open ? lessons.map((lesson) => {
|
|
2930
2732
|
const locked = lesson.status === "locked" || lesson.kind === "exam" && !examAllowed;
|
|
2931
2733
|
return (0, react.createElement)("button", {
|
|
2932
2734
|
key: lesson.id,
|
|
2933
2735
|
type: "button",
|
|
2934
|
-
className: `
|
|
2736
|
+
className: `lks14-node${lesson.focus ? " focus" : ""}`,
|
|
2935
2737
|
"aria-disabled": locked || void 0,
|
|
2936
|
-
title:
|
|
2738
|
+
title: `${lesson.title} — ${statusTitle(lesson.kind, locked ? "locked" : lesson.status)}`,
|
|
2937
2739
|
onClick: () => {
|
|
2938
2740
|
if (locked) return;
|
|
2939
|
-
|
|
2940
|
-
setFocus(lesson.id).catch(reportError);
|
|
2941
|
-
send(tr("prompt.exam", { section: section.title }));
|
|
2942
|
-
return;
|
|
2943
|
-
}
|
|
2944
|
-
openLessonThread(lesson);
|
|
2741
|
+
setFocus(lesson.id).catch(reportError);
|
|
2945
2742
|
}
|
|
2946
|
-
}, (0, react.createElement)("span", { className: "
|
|
2947
|
-
|
|
2948
|
-
className: "lks-spin"
|
|
2949
|
-
}) : statusIcon(lesson.kind, locked ? "locked" : lesson.status)), (0, react.createElement)("span", { className: "lks-t" }, lesson.title), lesson.due ? (0, react.createElement)("span", {
|
|
2950
|
-
className: "lks-tag due",
|
|
2951
|
-
title: tr("rail.due.tag")
|
|
2952
|
-
}, (0, react.createElement)(IconRefreshOutline16, { size: 11 })) : null, lesson.weakConcepts > 0 ? (0, react.createElement)("span", {
|
|
2953
|
-
className: "lks-tag weak",
|
|
2743
|
+
}, (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", {
|
|
2744
|
+
className: "lks14-tag weak",
|
|
2954
2745
|
title: tr("tag.weak", { count: lesson.weakConcepts })
|
|
2955
|
-
}, (0, react.createElement)(IconBoltFill16, { size: 10 }), String(lesson.weakConcepts)) : null, lesson.
|
|
2956
|
-
className: "
|
|
2957
|
-
title: tr("tag.friction", { count: lesson.frictionCount })
|
|
2958
|
-
}, (0, react.createElement)(IconWarningOutline16, { size: 10 }), String(lesson.frictionCount)) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", {
|
|
2959
|
-
className: "lks-bar",
|
|
2746
|
+
}, (0, react.createElement)(IconBoltFill16, { size: 10 }), String(lesson.weakConcepts)) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", {
|
|
2747
|
+
className: "lks14-bar",
|
|
2960
2748
|
title: tr("tag.mastery", { pct: lesson.masteryPct })
|
|
2961
|
-
}, (0, react.createElement)("i", { style: { transform: `scaleX(${lesson.masteryPct / 100})` } })) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", {
|
|
2962
|
-
className: "lks-pct",
|
|
2963
|
-
title: tr("tag.mastery.short")
|
|
2964
|
-
}, `${lesson.masteryPct}%`) : null);
|
|
2749
|
+
}, (0, react.createElement)("i", { style: { transform: `scaleX(${lesson.masteryPct / 100})` } })) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", { className: "lks14-pct" }, `${lesson.masteryPct}%`) : null);
|
|
2965
2750
|
}) : []];
|
|
2966
2751
|
}), (0, react.createElement)("button", {
|
|
2967
2752
|
className: "lks-btn ghost",
|
|
@@ -2969,71 +2754,41 @@ window.__ModuleLoader__.load({
|
|
|
2969
2754
|
onClick: () => {
|
|
2970
2755
|
setShowImport(!showImport);
|
|
2971
2756
|
}
|
|
2972
|
-
}, showImport ? tr("rail.import.close") :
|
|
2757
|
+
}, showImport ? tr("rail.import.close") : null, tr("rail.import.toggle")), showImport ? (0, react.createElement)(ImportRow, { send }) : null);
|
|
2973
2758
|
})();
|
|
2974
|
-
return (0, react.createElement)("div", { className: "
|
|
2759
|
+
return (0, react.createElement)("div", { className: "lks14-col lks14-rail" }, (0, react.createElement)("div", { className: "lks14-colhead" }, tr("col.rail")), body, (0, react.createElement)("div", { className: "lks-propcard-err" }, error));
|
|
2975
2760
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
className: "lks-spin"
|
|
2993
|
-
}), row.text);
|
|
2994
|
-
const cls = row.role === "streaming" ? "lks-msg assistant lks-prose streaming" : "lks-msg assistant lks-prose";
|
|
2995
|
-
const body = (0, react.createElement)("div", {
|
|
2996
|
-
key: row.key,
|
|
2997
|
-
className: cls,
|
|
2998
|
-
dangerouslySetInnerHTML: { __html: renderMarkdown(row.text) }
|
|
2999
|
-
});
|
|
3000
|
-
const options = interactive === void 0 || row.role === "streaming" ? [] : quizOptions(row.text);
|
|
3001
|
-
if (options.length < 2) return body;
|
|
3002
|
-
return (0, react.createElement)("div", {
|
|
3003
|
-
key: row.key,
|
|
3004
|
-
className: "lks-turn"
|
|
3005
|
-
}, body, (0, react.createElement)("div", {
|
|
3006
|
-
className: "lks-quiz",
|
|
3007
|
-
title: tr("quiz.title")
|
|
3008
|
-
}, ...options.map((opt) => (0, react.createElement)("button", {
|
|
3009
|
-
key: opt.letter,
|
|
3010
|
-
className: "lks-opt",
|
|
2761
|
+
function ImportRow({ send }) {
|
|
2762
|
+
const [url, setUrl] = (0, react.useState)("");
|
|
2763
|
+
return (0, react.createElement)("div", { className: "lks14-import" }, (0, react.createElement)("div", { className: "lks14-inputrow" }, (0, react.createElement)("input", {
|
|
2764
|
+
className: "lks14-search",
|
|
2765
|
+
type: "url",
|
|
2766
|
+
placeholder: tr("rail.empty.placeholder"),
|
|
2767
|
+
value: url,
|
|
2768
|
+
onChange: (e) => {
|
|
2769
|
+
setUrl(e.target.value);
|
|
2770
|
+
},
|
|
2771
|
+
onKeyDown: (e) => {
|
|
2772
|
+
if (e.key === "Enter" && url.trim() !== "") send(tr("prompt.import", { url: url.trim() }));
|
|
2773
|
+
}
|
|
2774
|
+
}), (0, react.createElement)("button", {
|
|
2775
|
+
className: "lks-btn primary",
|
|
2776
|
+
disabled: url.trim() === "",
|
|
3011
2777
|
onClick: () => {
|
|
3012
|
-
|
|
3013
|
-
letter: opt.letter,
|
|
3014
|
-
text: opt.text
|
|
3015
|
-
}));
|
|
2778
|
+
send(tr("prompt.import", { url: url.trim() }));
|
|
3016
2779
|
}
|
|
3017
|
-
}, (0, react.createElement)("
|
|
2780
|
+
}, tr("rail.empty.button"))), (0, react.createElement)("div", { className: "lks14-hint" }, tr("rail.empty.hint"), (0, react.createElement)("br"), tr("rail.empty.hint2")));
|
|
3018
2781
|
}
|
|
3019
|
-
/**
|
|
3020
|
-
function
|
|
3021
|
-
const
|
|
3022
|
-
const rows = transcriptRows(snapshot?.nodes, snapshot?.partial ?? null);
|
|
3023
|
-
const lastAssistant = rows.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
3024
|
-
const scrollRef = (0, react.useRef)(null);
|
|
2782
|
+
/** 中栏:the tutor chat stream with its own composer (upstream ChatStream + ChatComposer). */
|
|
2783
|
+
function ChatPane({ data, lesson, rows, busy, sendError, draft, setDraft, send, setMode }) {
|
|
2784
|
+
const streamEl = (0, react.useRef)(null);
|
|
3025
2785
|
(0, react.useEffect)(() => {
|
|
3026
|
-
const el =
|
|
2786
|
+
const el = streamEl.current;
|
|
3027
2787
|
if (el !== null) el.scrollTop = el.scrollHeight;
|
|
3028
2788
|
}, [rows.length]);
|
|
3029
|
-
(0, react.useEffect)(() => {
|
|
3030
|
-
const el = scrollRef.current;
|
|
3031
|
-
if (el !== null && rows.length > 0) enhanceRendered(el).catch(() => {});
|
|
3032
|
-
}, [rows.length]);
|
|
3033
|
-
const proposal = data?.pendingProposals[0] ?? null;
|
|
3034
|
-
const lesson = data?.lesson ?? null;
|
|
3035
2789
|
const dormant = data?.active !== true;
|
|
3036
|
-
|
|
2790
|
+
const lastAssistant = rows.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
2791
|
+
const [error, setError] = (0, react.useState)(null);
|
|
3037
2792
|
const fire = (action) => {
|
|
3038
2793
|
action.then(() => {
|
|
3039
2794
|
setError(null);
|
|
@@ -3041,132 +2796,68 @@ window.__ModuleLoader__.load({
|
|
|
3041
2796
|
setError(err instanceof Error ? err.message : String(err));
|
|
3042
2797
|
});
|
|
3043
2798
|
};
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
rationale: proposal.rationale
|
|
3050
|
-
})), (0, react.createElement)("button", {
|
|
3051
|
-
className: "lks-btn primary",
|
|
3052
|
-
onClick: () => {
|
|
3053
|
-
send(tr("prompt.proposal.accept", { id: proposal.id }));
|
|
3054
|
-
}
|
|
3055
|
-
}, tr("proposal.accept")), (0, react.createElement)("button", {
|
|
3056
|
-
className: "lks-btn ghost",
|
|
3057
|
-
onClick: () => {
|
|
3058
|
-
send(tr("prompt.proposal.decline", { id: proposal.id }));
|
|
3059
|
-
}
|
|
3060
|
-
}, tr("proposal.decline"))) : null, (0, react.createElement)("div", {
|
|
3061
|
-
className: "lks-pills",
|
|
3062
|
-
style: {
|
|
3063
|
-
height: "auto",
|
|
3064
|
-
padding: "4px 0"
|
|
2799
|
+
const starters = lesson?.starters ?? [];
|
|
2800
|
+
const onComposerKey = (e) => {
|
|
2801
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
2802
|
+
e.preventDefault();
|
|
2803
|
+
if (draft.trim() !== "" && !busy) send(draft);
|
|
3065
2804
|
}
|
|
3066
|
-
}
|
|
2805
|
+
};
|
|
2806
|
+
return (0, react.createElement)("div", { className: "lks14-col lks14-chat" }, (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", {
|
|
3067
2807
|
key: mode.id,
|
|
3068
|
-
className: `
|
|
2808
|
+
className: `lks14-pill${data?.mode === mode.id ? " on" : ""}`,
|
|
3069
2809
|
"aria-pressed": String(data?.mode === mode.id),
|
|
3070
2810
|
"aria-disabled": dormant || void 0,
|
|
3071
2811
|
title: dormant ? tr("tutor.dormant") : tr(mode.hintKey),
|
|
3072
2812
|
onClick: () => {
|
|
3073
2813
|
if (!dormant) fire(setMode(mode.id));
|
|
3074
2814
|
}
|
|
3075
|
-
}, tr(mode.labelKey)))),
|
|
3076
|
-
className: "
|
|
3077
|
-
|
|
3078
|
-
},
|
|
2815
|
+
}, tr(mode.labelKey))))), (0, react.createElement)("div", {
|
|
2816
|
+
className: "lks14-stream",
|
|
2817
|
+
ref: streamEl
|
|
2818
|
+
}, 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))), (0, react.createElement)("div", { className: "lks14-starters" }, ...starters.map((s) => (0, react.createElement)("button", {
|
|
3079
2819
|
key: s.label,
|
|
3080
|
-
className: "
|
|
2820
|
+
className: "lks14-starter",
|
|
3081
2821
|
"aria-disabled": dormant || void 0,
|
|
3082
2822
|
title: dormant ? tr("tutor.dormant") : s.message,
|
|
3083
2823
|
onClick: () => {
|
|
3084
2824
|
if (!dormant) send(s.message);
|
|
3085
2825
|
}
|
|
3086
|
-
}, s.label)))
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
if (synth === void 0) {
|
|
3095
|
-
reject(/* @__PURE__ */ new Error("no speechSynthesis"));
|
|
3096
|
-
return;
|
|
3097
|
-
}
|
|
3098
|
-
const utterance = new SpeechSynthesisUtterance(text);
|
|
3099
|
-
utterance.lang = "zh-CN";
|
|
3100
|
-
utterance.onend = () => resolve();
|
|
3101
|
-
utterance.onerror = () => reject(/* @__PURE__ */ new Error("speechSynthesis failed"));
|
|
3102
|
-
synth.speak(utterance);
|
|
3103
|
-
});
|
|
3104
|
-
},
|
|
3105
|
-
pause() {
|
|
3106
|
-
window.speechSynthesis?.pause();
|
|
3107
|
-
},
|
|
3108
|
-
resume() {
|
|
3109
|
-
window.speechSynthesis?.resume();
|
|
3110
|
-
},
|
|
3111
|
-
cancel() {
|
|
3112
|
-
window.speechSynthesis?.cancel();
|
|
3113
|
-
}
|
|
3114
|
-
};
|
|
3115
|
-
}
|
|
3116
|
-
/** Edge-over-dashboard engine: MP3 from /api/tts played through an <audio>. */
|
|
3117
|
-
function audioSpeechEngine(fetchTts) {
|
|
3118
|
-
let audio = null;
|
|
3119
|
-
return {
|
|
3120
|
-
speak(text) {
|
|
3121
|
-
return fetchTts(text).then((buf) => new Promise((resolve, reject) => {
|
|
3122
|
-
audio = new Audio(URL.createObjectURL(new Blob([buf], { type: "audio/mpeg" })));
|
|
3123
|
-
audio.onended = () => resolve();
|
|
3124
|
-
audio.onerror = () => reject(/* @__PURE__ */ new Error("audio playback failed"));
|
|
3125
|
-
audio.play().catch(reject);
|
|
3126
|
-
}));
|
|
3127
|
-
},
|
|
3128
|
-
pause() {
|
|
3129
|
-
audio?.pause();
|
|
3130
|
-
},
|
|
3131
|
-
resume() {
|
|
3132
|
-
audio?.play();
|
|
2826
|
+
}, s.label))), (0, react.createElement)("div", { className: "lks14-composer" }, (0, react.createElement)("textarea", {
|
|
2827
|
+
className: "lks14-composertext",
|
|
2828
|
+
placeholder: busy ? tr("composer.busy") : tr("tutor.empty.hint"),
|
|
2829
|
+
value: draft,
|
|
2830
|
+
rows: 2,
|
|
2831
|
+
disabled: dormant,
|
|
2832
|
+
onChange: (e) => {
|
|
2833
|
+
setDraft(e.target.value);
|
|
3133
2834
|
},
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
2835
|
+
onKeyDown: onComposerKey
|
|
2836
|
+
}), (0, react.createElement)("button", {
|
|
2837
|
+
className: "lks-btn primary",
|
|
2838
|
+
disabled: busy || dormant || draft.trim() === "",
|
|
2839
|
+
onClick: () => {
|
|
2840
|
+
send(draft);
|
|
3137
2841
|
}
|
|
3138
|
-
}
|
|
2842
|
+
}, busy ? (0, react.createElement)(IconLoadingOutline16, {
|
|
2843
|
+
size: 13,
|
|
2844
|
+
className: "lks-spin"
|
|
2845
|
+
}) : null, tr("composer.send"))), sendError !== null || error !== null ? (0, react.createElement)("div", { className: "lks-propcard-err" }, sendError ?? error) : null);
|
|
3139
2846
|
}
|
|
3140
|
-
/**
|
|
3141
|
-
function
|
|
2847
|
+
/** 右栏:the notebook — 讲解/概念图/笔记 tabs (upstream NotebookPanel arrangement). */
|
|
2848
|
+
function NotebookPane({ data, deleteNote }) {
|
|
3142
2849
|
const lesson = data?.lesson ?? null;
|
|
3143
|
-
const
|
|
3144
|
-
const [pane, setPane] = (0, react.useState)("teach");
|
|
2850
|
+
const [tab, setTab] = (0, react.useState)("teach");
|
|
3145
2851
|
const [error, setError] = (0, react.useState)(null);
|
|
2852
|
+
const [armedNote, setArmedNote] = (0, react.useState)(null);
|
|
3146
2853
|
const [read, setRead] = (0, react.useState)(null);
|
|
3147
2854
|
const readCtl = (0, react.useRef)(null);
|
|
3148
2855
|
const [readError, setReadError] = (0, react.useState)(null);
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
const controller = new ReadAloudController(speechSentencesOf(lesson.speechText).map((s) => speakMathInSentence(s)), audioSpeechEngine((text) => tts(text)), systemSpeechEngine(), (s) => {
|
|
3153
|
-
setRead(s);
|
|
3154
|
-
if (s.degraded && s.engine === "system" && !readError) setReadError(tr("read.engine.system"));
|
|
3155
|
-
});
|
|
3156
|
-
readCtl.current = controller;
|
|
3157
|
-
setReadError(null);
|
|
3158
|
-
controller.start().catch((err) => {
|
|
3159
|
-
setReadError(err instanceof Error ? err.message : String(err));
|
|
3160
|
-
});
|
|
3161
|
-
};
|
|
3162
|
-
const stopReading = () => {
|
|
3163
|
-
readCtl.current?.stop();
|
|
3164
|
-
setRead(null);
|
|
3165
|
-
setReadError(null);
|
|
3166
|
-
};
|
|
3167
|
-
const [armedNote, setArmedNote] = (0, react.useState)(null);
|
|
2856
|
+
const { tts } = useStudy();
|
|
2857
|
+
const proseRef = (0, react.useRef)(null);
|
|
2858
|
+
const diagRef = (0, react.useRef)(null);
|
|
3168
2859
|
(0, react.useEffect)(() => {
|
|
3169
|
-
if (armedNote
|
|
2860
|
+
if (!armedNote) return;
|
|
3170
2861
|
const disarm = () => {
|
|
3171
2862
|
setArmedNote(null);
|
|
3172
2863
|
};
|
|
@@ -3187,46 +2878,130 @@ window.__ModuleLoader__.load({
|
|
|
3187
2878
|
setError(err instanceof Error ? err.message : String(err));
|
|
3188
2879
|
});
|
|
3189
2880
|
};
|
|
3190
|
-
const
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
const
|
|
3195
|
-
|
|
3196
|
-
|
|
2881
|
+
const startReading = () => {
|
|
2882
|
+
if (lesson === null || lesson.speechText.trim() === "") return;
|
|
2883
|
+
readCtl.current?.stop();
|
|
2884
|
+
const sentences = speechSentencesOf(lesson.speechText);
|
|
2885
|
+
const voice = storedTtsVoice();
|
|
2886
|
+
const prefetch = /* @__PURE__ */ new Map();
|
|
2887
|
+
let currentAudio = null;
|
|
2888
|
+
const fetchAudio = (text) => {
|
|
2889
|
+
const hit = prefetch.get(text);
|
|
2890
|
+
if (hit !== void 0) {
|
|
2891
|
+
prefetch.delete(text);
|
|
2892
|
+
return hit;
|
|
2893
|
+
}
|
|
2894
|
+
return tts(text, voice);
|
|
2895
|
+
};
|
|
2896
|
+
const controller = new ReadAloudController(sentences.map((s) => speakMathInSentence(s)), {
|
|
2897
|
+
speak(text) {
|
|
2898
|
+
return fetchAudio(text).then((buf) => new Promise((resolve, reject) => {
|
|
2899
|
+
const blobUrl = URL.createObjectURL(new Blob([buf], { type: "audio/mpeg" }));
|
|
2900
|
+
const audio = new Audio(blobUrl);
|
|
2901
|
+
currentAudio = audio;
|
|
2902
|
+
audio.onended = () => {
|
|
2903
|
+
URL.revokeObjectURL(blobUrl);
|
|
2904
|
+
if (currentAudio === audio) currentAudio = null;
|
|
2905
|
+
resolve();
|
|
2906
|
+
};
|
|
2907
|
+
audio.onerror = () => {
|
|
2908
|
+
URL.revokeObjectURL(blobUrl);
|
|
2909
|
+
if (currentAudio === audio) currentAudio = null;
|
|
2910
|
+
reject(/* @__PURE__ */ new Error("audio playback failed"));
|
|
2911
|
+
};
|
|
2912
|
+
audio.play().catch(reject);
|
|
2913
|
+
}));
|
|
2914
|
+
},
|
|
2915
|
+
pause() {
|
|
2916
|
+
currentAudio?.pause();
|
|
2917
|
+
},
|
|
2918
|
+
resume() {
|
|
2919
|
+
currentAudio?.play().catch(() => {});
|
|
2920
|
+
},
|
|
2921
|
+
cancel() {
|
|
2922
|
+
currentAudio?.pause();
|
|
2923
|
+
currentAudio = null;
|
|
2924
|
+
},
|
|
2925
|
+
prewarm(text) {
|
|
2926
|
+
if (!prefetch.has(text)) prefetch.set(text, tts(text, voice));
|
|
2927
|
+
}
|
|
2928
|
+
}, {
|
|
2929
|
+
speak(text) {
|
|
2930
|
+
return new Promise((resolve, reject) => {
|
|
2931
|
+
const synth = window.speechSynthesis;
|
|
2932
|
+
if (synth === void 0) {
|
|
2933
|
+
reject(/* @__PURE__ */ new Error("system speech unavailable"));
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
const utter = new SpeechSynthesisUtterance(text);
|
|
2937
|
+
utter.lang = voice.startsWith("en") ? "en-US" : "zh-CN";
|
|
2938
|
+
utter.onend = () => {
|
|
2939
|
+
resolve();
|
|
2940
|
+
};
|
|
2941
|
+
utter.onerror = () => {
|
|
2942
|
+
reject(/* @__PURE__ */ new Error("system speech failed"));
|
|
2943
|
+
};
|
|
2944
|
+
synth.speak(utter);
|
|
2945
|
+
});
|
|
2946
|
+
},
|
|
2947
|
+
pause() {
|
|
2948
|
+
window.speechSynthesis?.pause();
|
|
2949
|
+
},
|
|
2950
|
+
resume() {
|
|
2951
|
+
window.speechSynthesis?.resume();
|
|
2952
|
+
},
|
|
2953
|
+
cancel() {
|
|
2954
|
+
window.speechSynthesis?.cancel();
|
|
2955
|
+
}
|
|
2956
|
+
}, (s) => setRead(s));
|
|
2957
|
+
readCtl.current = controller;
|
|
2958
|
+
controller.start().catch((err) => {
|
|
2959
|
+
setReadError(err instanceof Error ? err.message : String(err));
|
|
2960
|
+
});
|
|
2961
|
+
};
|
|
2962
|
+
const stopReading = () => {
|
|
2963
|
+
readCtl.current?.stop();
|
|
2964
|
+
setRead(null);
|
|
2965
|
+
};
|
|
3197
2966
|
(0, react.useEffect)(() => {
|
|
3198
|
-
if (
|
|
2967
|
+
if (tab !== "cmap" || diagRef.current === null || lesson === null) return;
|
|
3199
2968
|
const el = diagRef.current;
|
|
3200
2969
|
el.textContent = "";
|
|
3201
2970
|
(lesson.concepts.length === 0 ? Promise.reject(/* @__PURE__ */ new Error("no concepts")) : renderLessonConceptMap(el, lesson.title, lesson.concepts.map((c) => ({
|
|
3202
2971
|
title: c.title,
|
|
3203
2972
|
masteryPct: c.masteryPct
|
|
3204
2973
|
})))).catch((err) => {
|
|
3205
|
-
console.error("lks diagram pane failed:", pane, err);
|
|
3206
2974
|
el.textContent = lesson.concepts.length === 0 ? tr("bb.fallback.cmap.empty") : tr("bb.fallback.cmap");
|
|
3207
2975
|
});
|
|
3208
2976
|
}, [
|
|
3209
|
-
|
|
2977
|
+
tab,
|
|
3210
2978
|
lesson?.lessonId,
|
|
3211
2979
|
lesson?.concepts.length
|
|
3212
2980
|
]);
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
},
|
|
3217
|
-
|
|
3218
|
-
|
|
2981
|
+
(0, react.useEffect)(() => {
|
|
2982
|
+
if (tab !== "teach" || proseRef.current === null || lesson === null) return;
|
|
2983
|
+
enhanceRendered(proseRef.current).catch(() => {});
|
|
2984
|
+
}, [tab, lesson?.html]);
|
|
2985
|
+
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", {
|
|
2986
|
+
className: `lks14-viewtab${tab === "teach" ? " on" : ""}`,
|
|
2987
|
+
"aria-pressed": String(tab === "teach"),
|
|
3219
2988
|
onClick: () => {
|
|
3220
|
-
|
|
2989
|
+
setTab("teach");
|
|
3221
2990
|
}
|
|
3222
2991
|
}, tr("viewtab.teach")), (0, react.createElement)("button", {
|
|
3223
|
-
className: `
|
|
3224
|
-
"aria-pressed": String(
|
|
2992
|
+
className: `lks14-viewtab${tab === "cmap" ? " on" : ""}`,
|
|
2993
|
+
"aria-pressed": String(tab === "cmap"),
|
|
3225
2994
|
title: tr("viewtab.cmap.title"),
|
|
3226
2995
|
onClick: () => {
|
|
3227
|
-
|
|
2996
|
+
setTab("cmap");
|
|
2997
|
+
}
|
|
2998
|
+
}, (0, react.createElement)(IconGlobeOutline14, { size: 13 }), tr("viewtab.cmap")), (0, react.createElement)("button", {
|
|
2999
|
+
className: `lks14-viewtab${tab === "notes" ? " on" : ""}`,
|
|
3000
|
+
"aria-pressed": String(tab === "notes"),
|
|
3001
|
+
onClick: () => {
|
|
3002
|
+
setTab("notes");
|
|
3228
3003
|
}
|
|
3229
|
-
},
|
|
3004
|
+
}, tr("bb.notes"))), tab === "teach" ? (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "lks-readbar" }, (0, react.createElement)("button", {
|
|
3230
3005
|
className: "lks-btn ghost",
|
|
3231
3006
|
style: {
|
|
3232
3007
|
padding: "3px 8px",
|
|
@@ -3245,7 +3020,7 @@ window.__ModuleLoader__.load({
|
|
|
3245
3020
|
}
|
|
3246
3021
|
startReading();
|
|
3247
3022
|
}
|
|
3248
|
-
},
|
|
3023
|
+
}, read !== null && read.state === "speaking" ? tr("read.pause") : tr("read.play")), read !== null ? (0, react.createElement)("button", {
|
|
3249
3024
|
className: "lks-btn ghost",
|
|
3250
3025
|
style: {
|
|
3251
3026
|
padding: "3px 8px",
|
|
@@ -3254,20 +3029,20 @@ window.__ModuleLoader__.load({
|
|
|
3254
3029
|
},
|
|
3255
3030
|
title: tr("read.stop"),
|
|
3256
3031
|
onClick: stopReading
|
|
3257
|
-
}, 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
|
|
3258
|
-
className: "
|
|
3032
|
+
}, 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", {
|
|
3033
|
+
className: "lks14-prose",
|
|
3259
3034
|
ref: proseRef,
|
|
3260
3035
|
dangerouslySetInnerHTML: { __html: lesson.html }
|
|
3261
|
-
}) : (0, react.createElement)("div", {
|
|
3262
|
-
className: "
|
|
3036
|
+
})) : tab === "cmap" ? (0, react.createElement)("div", {
|
|
3037
|
+
className: "lks14-prose",
|
|
3263
3038
|
ref: diagRef
|
|
3264
|
-
})
|
|
3265
|
-
className: "
|
|
3039
|
+
}) : (0, react.createElement)("div", { className: "lks14-zones" }, lesson.notes.length === 0 ? (0, react.createElement)("div", {
|
|
3040
|
+
className: "lks14-empty",
|
|
3266
3041
|
style: { padding: "16px 0" }
|
|
3267
3042
|
}, tr("bb.notes.empty")) : ZONES.filter(([zone]) => lesson.notes.some((n) => n.zone === zone)).map(([zone, labelKey]) => (0, react.createElement)("div", {
|
|
3268
3043
|
key: zone,
|
|
3269
|
-
className: "
|
|
3270
|
-
}, (0, react.createElement)("div", { className: "
|
|
3044
|
+
className: "lks14-zone"
|
|
3045
|
+
}, (0, react.createElement)("div", { className: "lks14-zoneh" }, tr(labelKey)), ...lesson.notes.filter((n) => n.zone === zone).map((n) => (0, react.createElement)("div", {
|
|
3271
3046
|
key: n.id,
|
|
3272
3047
|
className: "lks-note"
|
|
3273
3048
|
}, (0, react.createElement)("span", { className: "lks-note-src" }, n.source), (0, react.createElement)("button", {
|
|
@@ -3287,83 +3062,223 @@ window.__ModuleLoader__.load({
|
|
|
3287
3062
|
className: "lks-note-text",
|
|
3288
3063
|
dangerouslySetInnerHTML: { __html: renderMarkdown(n.text) }
|
|
3289
3064
|
}), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `“${n.quote}”`) : null))))));
|
|
3290
|
-
return (0, react.createElement)("div", { className: "
|
|
3065
|
+
return (0, react.createElement)("div", { className: "lks14-col lks14-note" }, (0, react.createElement)("div", { className: "lks14-colhead" }, tr("col.bb")), body, (0, react.createElement)("div", { className: "lks-propcard-err" }, error));
|
|
3291
3066
|
}
|
|
3292
3067
|
//#endregion
|
|
3293
|
-
//#region src/client/
|
|
3068
|
+
//#region src/client/shell-entry.ts
|
|
3294
3069
|
/**
|
|
3295
|
-
* The
|
|
3296
|
-
*
|
|
3297
|
-
*
|
|
3298
|
-
*
|
|
3299
|
-
*
|
|
3300
|
-
*
|
|
3301
|
-
*
|
|
3302
|
-
*
|
|
3070
|
+
* The study shell entry — the panel-entry doctrine, stardeck-style: the study
|
|
3071
|
+
* surface is CROSS-WORKSPACE (plugin state is workspace-independent by
|
|
3072
|
+
* design), so it enters through a sidebar row and takes over the center
|
|
3073
|
+
* column, never living inside one conversation. The conversation slot family
|
|
3074
|
+
* is single-occupant and external plugins cannot declare shell slots, so both
|
|
3075
|
+
* the row and the view are DOM-level: a plain-DOM button (never disturbs the
|
|
3076
|
+
* shell's reconciliation) + a container appended to the center column as an
|
|
3077
|
+
* extra trailing child React never manages. Visibility toggles via an <html>
|
|
3078
|
+
* data attribute; sibling panels evict each other through the dsh-panel-activate
|
|
3079
|
+
* event.
|
|
3080
|
+
*
|
|
3081
|
+
* Differences from stardeck's original: our own attribute namespace, a
|
|
3082
|
+
* hand-back SUPPRESSION hook (the panel itself calls sessions.open to stage
|
|
3083
|
+
* the focus lesson's thread — an internal navigation must not close the
|
|
3084
|
+
* panel; only USER session navigation hands the column back), and dsw-token
|
|
3085
|
+
* styling via the injected stylesheet.
|
|
3086
|
+
* @module dsh-plugin-lookatstudy/client/shell-entry
|
|
3303
3087
|
*/
|
|
3088
|
+
const ROW_ATTRIBUTE = "data-dsh-lookatstudy-entry";
|
|
3089
|
+
const ROW_SELECTOR = `[${ROW_ATTRIBUTE}]`;
|
|
3090
|
+
const VIEW_ATTRIBUTE = "data-dsh-lookatstudy-view";
|
|
3091
|
+
const ACTIVE_ATTR = "data-dsh-lookatstudy-active";
|
|
3092
|
+
/** Sibling panels' activation attributes, evicted when the study panel opens. */
|
|
3093
|
+
const OTHER_ACTIVE_ATTRS = [
|
|
3094
|
+
"data-dsh-warroom-active",
|
|
3095
|
+
"data-dsh-taskboard-active",
|
|
3096
|
+
"data-dsh-ssh-active"
|
|
3097
|
+
];
|
|
3098
|
+
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
3099
|
+
const ACTIVATE_EVENT = "dsh-panel-activate";
|
|
3100
|
+
const PANEL_NAME = "lookatstudy";
|
|
3101
|
+
/** Family rows we order against (the sibling-panel entry block). */
|
|
3102
|
+
const FAMILY_SELECTORS = [
|
|
3103
|
+
"[data-dsh-warroom-entry]",
|
|
3104
|
+
"[data-dsh-ssh-entry]",
|
|
3105
|
+
"[data-dsh-taskboard-entry]",
|
|
3106
|
+
ROW_SELECTOR
|
|
3107
|
+
];
|
|
3108
|
+
const SIDEBAR_COLUMN_SELECTOR = "[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]";
|
|
3109
|
+
const CENTER_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"]";
|
|
3110
|
+
/** The open/close state shared by the sidebar row and the view. */
|
|
3111
|
+
var ShellOpenState = class {
|
|
3112
|
+
open = false;
|
|
3113
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3114
|
+
isOpen() {
|
|
3115
|
+
return this.open;
|
|
3116
|
+
}
|
|
3117
|
+
setOpen(next) {
|
|
3118
|
+
if (this.open === next) return;
|
|
3119
|
+
this.open = next;
|
|
3120
|
+
for (const l of this.listeners) l();
|
|
3121
|
+
}
|
|
3122
|
+
subscribe(listener) {
|
|
3123
|
+
this.listeners.add(listener);
|
|
3124
|
+
return () => {
|
|
3125
|
+
this.listeners.delete(listener);
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
};
|
|
3129
|
+
function sidebarRoot(doc) {
|
|
3130
|
+
const column = doc.querySelector(SIDEBAR_COLUMN_SELECTOR);
|
|
3131
|
+
if (column === null) return void 0;
|
|
3132
|
+
return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
|
|
3133
|
+
}
|
|
3134
|
+
function newSessionButton(root) {
|
|
3135
|
+
const nested = root.querySelector("button[class*=\"newSession\"]");
|
|
3136
|
+
if (nested !== null) return nested;
|
|
3137
|
+
for (const child of root.children) if (child.tagName === "BUTTON") return child;
|
|
3138
|
+
}
|
|
3139
|
+
/** A 16px study glyph: an open book with a mastery spark. */
|
|
3140
|
+
const STUDY_ICON = "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M2 3.2C3.2 2.4 4.9 2.2 8 3.4c3.1-1.2 4.8-1 6-.2v9.2c-1.2-.8-2.9-1-6 .2-3.1-1.2-4.8-1-6-.2V3.2z\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/><path d=\"M8 3.4v9.2\" stroke=\"currentColor\" stroke-width=\"1.2\"/></svg>";
|
|
3304
3141
|
/**
|
|
3305
|
-
*
|
|
3306
|
-
* @param
|
|
3307
|
-
* @
|
|
3142
|
+
* Mount the study shell surface: sidebar row + center-column panel.
|
|
3143
|
+
* @param view - the panel COMPONENT (an element factory, e.g. studyPanelView(services)).
|
|
3144
|
+
* @param label - the entry label (locale-aware getter, re-read on paints).
|
|
3145
|
+
* @param dom - the DOM mounting faces (injectable for tests).
|
|
3146
|
+
* @param sessions - the host session list; USER navigation between sessions
|
|
3147
|
+
* hands the center column back (closes the panel). Hydration (the first
|
|
3148
|
+
* current arriving) and suppressed internal opens do not count.
|
|
3308
3149
|
*/
|
|
3309
|
-
function
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
});
|
|
3150
|
+
function mountStudyShell(view, label, dom, sessions) {
|
|
3151
|
+
if (typeof document === "undefined") return {
|
|
3152
|
+
dispose: () => {},
|
|
3153
|
+
close: () => {},
|
|
3154
|
+
suppressHandBack: (fn) => {
|
|
3155
|
+
fn();
|
|
3156
|
+
}
|
|
3317
3157
|
};
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3158
|
+
const doc = dom?.doc ?? document;
|
|
3159
|
+
if (doc.querySelector(ROW_SELECTOR) !== null) return {
|
|
3160
|
+
dispose: () => {},
|
|
3161
|
+
close: () => {},
|
|
3162
|
+
suppressHandBack: (fn) => {
|
|
3163
|
+
fn();
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
const state = new ShellOpenState();
|
|
3167
|
+
let suppressed = false;
|
|
3168
|
+
const row = doc.createElement("button");
|
|
3169
|
+
row.type = "button";
|
|
3170
|
+
row.setAttribute(ROW_ATTRIBUTE, "");
|
|
3171
|
+
row.setAttribute("data-dsh-plugin", "lookatstudy");
|
|
3172
|
+
row.setAttribute("data-dsh-part", "sidebar-entry");
|
|
3173
|
+
row.className = "lks14-sidebar-row";
|
|
3174
|
+
const paintRow = () => {
|
|
3175
|
+
const text = label();
|
|
3176
|
+
row.setAttribute("aria-label", text);
|
|
3177
|
+
row.setAttribute("title", text);
|
|
3178
|
+
row.innerHTML = `<span class="lks14-sidebar-icon">${STUDY_ICON}</span><span class="lks14-sidebar-label">${text}</span>`;
|
|
3179
|
+
};
|
|
3180
|
+
paintRow();
|
|
3181
|
+
row.addEventListener("click", () => state.setOpen(!state.isOpen()));
|
|
3182
|
+
const placeRow = () => {
|
|
3183
|
+
const root = sidebarRoot(doc);
|
|
3184
|
+
if (root === void 0) return false;
|
|
3185
|
+
const button = newSessionButton(root);
|
|
3186
|
+
if (button === void 0) return false;
|
|
3187
|
+
if (row.parentElement !== root) {
|
|
3188
|
+
const logoRow = button.closest("[class*=\"logoRow\"]");
|
|
3189
|
+
const base = logoRow !== null && logoRow.parentElement === root ? logoRow : button;
|
|
3190
|
+
const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(FAMILY_SELECTORS.join(", ")));
|
|
3191
|
+
const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
|
|
3192
|
+
root.insertBefore(row, anchor);
|
|
3193
|
+
}
|
|
3194
|
+
return true;
|
|
3195
|
+
};
|
|
3196
|
+
let container;
|
|
3197
|
+
let root;
|
|
3198
|
+
const roots = dom ?? { createRoot: (el) => (0, react_dom_client.createRoot)(el) };
|
|
3199
|
+
const ensureView = () => {
|
|
3200
|
+
if (container !== void 0) return;
|
|
3201
|
+
const column = doc.querySelector(CENTER_COLUMN_SELECTOR);
|
|
3202
|
+
if (column === null) return;
|
|
3203
|
+
container = doc.createElement("div");
|
|
3204
|
+
container.setAttribute(VIEW_ATTRIBUTE, "");
|
|
3205
|
+
container.setAttribute("data-dsh-plugin", "lookatstudy");
|
|
3206
|
+
container.className = "lks14-shell-view";
|
|
3207
|
+
column.appendChild(container);
|
|
3208
|
+
root = roots.createRoot(container);
|
|
3209
|
+
root.render((0, react.createElement)(view));
|
|
3210
|
+
};
|
|
3211
|
+
const applyActive = () => {
|
|
3212
|
+
if (state.isOpen()) {
|
|
3213
|
+
for (const attr of OTHER_ACTIVE_ATTRS) doc.documentElement.removeAttribute(attr);
|
|
3214
|
+
doc.documentElement.setAttribute(ACTIVE_ATTR, "");
|
|
3215
|
+
doc.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }));
|
|
3216
|
+
} else doc.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
3217
|
+
if (state.isOpen()) row.dataset.active = "true";
|
|
3218
|
+
else delete row.dataset.active;
|
|
3219
|
+
};
|
|
3220
|
+
const onOtherActivate = (event) => {
|
|
3221
|
+
if (event.detail !== PANEL_NAME) state.setOpen(false);
|
|
3222
|
+
};
|
|
3223
|
+
const SIDEBAR_ROW_SELECTOR = "[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
|
|
3224
|
+
const onClickSidebarRow = (event) => {
|
|
3225
|
+
if (!state.isOpen()) return;
|
|
3226
|
+
const target = event.target;
|
|
3227
|
+
if (target === null) return;
|
|
3228
|
+
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) state.setOpen(false);
|
|
3229
|
+
};
|
|
3230
|
+
const bodyObserver = new MutationObserver(() => {
|
|
3231
|
+
placeRow();
|
|
3232
|
+
ensureView();
|
|
3233
|
+
});
|
|
3234
|
+
bodyObserver.observe(doc.body, {
|
|
3235
|
+
childList: true,
|
|
3236
|
+
subtree: true
|
|
3237
|
+
});
|
|
3238
|
+
doc.addEventListener("click", onClickSidebarRow, true);
|
|
3239
|
+
doc.addEventListener(ACTIVATE_EVENT, onOtherActivate);
|
|
3240
|
+
let unsubscribeSessions;
|
|
3241
|
+
if (sessions?.list !== void 0) {
|
|
3242
|
+
let lastCurrent = sessions.list.getSnapshot().current;
|
|
3243
|
+
unsubscribeSessions = sessions.list.subscribe(() => {
|
|
3244
|
+
const next = sessions.list.getSnapshot().current;
|
|
3245
|
+
if (next === lastCurrent) return;
|
|
3246
|
+
const wasHydration = lastCurrent === void 0;
|
|
3247
|
+
lastCurrent = next;
|
|
3248
|
+
if (shouldHandBack(suppressed, wasHydration)) state.setOpen(false);
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
const unsubscribe = state.subscribe(applyActive);
|
|
3252
|
+
placeRow();
|
|
3253
|
+
ensureView();
|
|
3254
|
+
applyActive();
|
|
3255
|
+
return {
|
|
3256
|
+
close: () => state.setOpen(false),
|
|
3257
|
+
/** Internal session navigation (staging the lesson thread) must not hand
|
|
3258
|
+
* the column back — only USER navigation does. */
|
|
3259
|
+
suppressHandBack: (fn) => {
|
|
3260
|
+
suppressed = true;
|
|
3329
3261
|
try {
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
const { path } = await area.json();
|
|
3334
|
-
const workspace = await ctx.workspaces.create({ path });
|
|
3335
|
-
const sessionId = ctx.workspaces.connectWorkspace !== void 0 ? await ctx.workspaces.connectWorkspace(workspace.workspaceId) : await ctx.sessions.create({ workspaceId: workspace.workspaceId });
|
|
3336
|
-
const actx = ctx.sessions.scope(sessionId);
|
|
3337
|
-
const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
|
|
3338
|
-
if (face === void 0) throw new Error("study session is not addressable yet");
|
|
3339
|
-
const result = await face.prompt([{
|
|
3340
|
-
type: "text",
|
|
3341
|
-
text: tr("prompt.kickoff")
|
|
3342
|
-
}], "queue");
|
|
3343
|
-
if (!result.ok) throw new Error(`kickoff prompt rejected: ${result.error.code}: ${result.error.message}`);
|
|
3344
|
-
ctx.sessions.open(sessionId);
|
|
3345
|
-
} catch (err) {
|
|
3346
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
3347
|
-
setBusy(false);
|
|
3262
|
+
fn();
|
|
3263
|
+
} finally {
|
|
3264
|
+
suppressed = false;
|
|
3348
3265
|
}
|
|
3349
|
-
}
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3266
|
+
},
|
|
3267
|
+
dispose: () => {
|
|
3268
|
+
bodyObserver.disconnect();
|
|
3269
|
+
doc.removeEventListener("click", onClickSidebarRow, true);
|
|
3270
|
+
doc.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
|
|
3271
|
+
unsubscribeSessions?.();
|
|
3272
|
+
unsubscribe();
|
|
3273
|
+
root?.unmount();
|
|
3274
|
+
container?.remove();
|
|
3275
|
+
row.remove();
|
|
3357
3276
|
}
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
}, busy ? (0, react.createElement)(IconLoadingOutline16, { className: "lks-spin" }) : (0, react.createElement)(IconThinkOutline16, null), busy ? tr("start.busy") : data?.active === true ? tr("start.enter") : tr("start.go")), error !== null ? (0, react.createElement)("span", {
|
|
3364
|
-
className: "lks-propcard-err",
|
|
3365
|
-
style: { marginTop: 0 }
|
|
3366
|
-
}, error) : null);
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
/** Whether a session change should hand the center column back (suppressible). */
|
|
3280
|
+
function shouldHandBack(suppressed, wasHydration) {
|
|
3281
|
+
return !suppressed && !wasHydration;
|
|
3367
3282
|
}
|
|
3368
3283
|
//#endregion
|
|
3369
3284
|
//#region src/client/settings.tsx
|
|
@@ -3401,6 +3316,7 @@ window.__ModuleLoader__.load({
|
|
|
3401
3316
|
function StudySettingsSection() {
|
|
3402
3317
|
const { data, setMode, activate } = useStudy();
|
|
3403
3318
|
const [error, setError] = (0, react.useState)(null);
|
|
3319
|
+
const [voice, setVoice] = (0, react.useState)(storedTtsVoice);
|
|
3404
3320
|
const fire = (action) => {
|
|
3405
3321
|
action.then(() => {
|
|
3406
3322
|
setError(null);
|
|
@@ -3421,7 +3337,17 @@ window.__ModuleLoader__.load({
|
|
|
3421
3337
|
onClick: () => {
|
|
3422
3338
|
if (data !== null) fire(activate(!data.active));
|
|
3423
3339
|
}
|
|
3424
|
-
}, 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")))),
|
|
3340
|
+
}, 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.voice")), (0, react.createElement)("p", { className: "lks-set-hint" }, tr("settings.voice.hint")), (0, react.createElement)("div", null, (0, react.createElement)("select", {
|
|
3341
|
+
className: "lks-set-select",
|
|
3342
|
+
value: voice,
|
|
3343
|
+
onChange: (e) => {
|
|
3344
|
+
storeTtsVoice(e.target.value);
|
|
3345
|
+
setVoice(e.target.value);
|
|
3346
|
+
}
|
|
3347
|
+
}, ...TTS_VOICES.map((v) => (0, react.createElement)("option", {
|
|
3348
|
+
key: v.id,
|
|
3349
|
+
value: v.id
|
|
3350
|
+
}, tr(v.labelKey)))))), progress === void 0 ? null : (0, react.createElement)("section", { className: "lks-set-row" }, (0, react.createElement)("h3", null, tr("settings.stats")), (0, react.createElement)("ul", { className: "lks-set-stats" }, (0, react.createElement)("li", null, tr("settings.stats.courses", { count: data?.courses.length ?? 0 })), (0, react.createElement)("li", null, tr("settings.stats.xp", {
|
|
3425
3351
|
xp: progress.totalXp,
|
|
3426
3352
|
level: progress.level,
|
|
3427
3353
|
pct: progress.levelPct
|
|
@@ -3590,14 +3516,9 @@ window.__ModuleLoader__.load({
|
|
|
3590
3516
|
"locale"
|
|
3591
3517
|
];
|
|
3592
3518
|
/**
|
|
3593
|
-
* Register the study surfaces: styles
|
|
3594
|
-
*
|
|
3595
|
-
*
|
|
3596
|
-
* study workspace + session + kickoff prompt in one click. The `lookatstudy`
|
|
3597
|
-
* locale namespace rides the framework locale service (zh/en) and the
|
|
3598
|
-
* client-wide translator is installed before any component renders; both
|
|
3599
|
-
* slot entries declare the namespace so the framework synthesizes their `t`
|
|
3600
|
-
* seat and follows locale switches for the labels.
|
|
3519
|
+
* Register the study surfaces: styles + locale once; the sidebar-entry panel
|
|
3520
|
+
* carries the whole study UI; the host-native extras (settings, dock, tool
|
|
3521
|
+
* cards) ride their slots as before.
|
|
3601
3522
|
* @param ctx - client root context.
|
|
3602
3523
|
*/
|
|
3603
3524
|
function apply(ctx) {
|
|
@@ -3605,19 +3526,12 @@ window.__ModuleLoader__.load({
|
|
|
3605
3526
|
const localeSvc = ctx.locale;
|
|
3606
3527
|
ctx.effect(() => registerStudyLocale(localeSvc), "lookatstudy.locale()");
|
|
3607
3528
|
setStudyTranslator(studyTranslator(localeSvc));
|
|
3608
|
-
ctx
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
}, studyView(ctx)));
|
|
3615
|
-
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
3616
|
-
name: "conversation.input.left",
|
|
3617
|
-
id: "lookatstudy-start",
|
|
3618
|
-
order: 10,
|
|
3619
|
-
locale: "lookatstudy"
|
|
3620
|
-
}, studyStartButton(ctx)));
|
|
3529
|
+
const shell = mountStudyShell(studyPanelView(ctx), () => tr("tab.label"), void 0, ctx.sessions);
|
|
3530
|
+
setPanelShell(shell);
|
|
3531
|
+
ctx.effect(() => () => {
|
|
3532
|
+
setPanelShell(null);
|
|
3533
|
+
shell.dispose();
|
|
3534
|
+
}, "lookatstudy.studyPanel()");
|
|
3621
3535
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
3622
3536
|
name: "settings.section",
|
|
3623
3537
|
id: "lookatstudy",
|
|
@@ -3645,9 +3559,8 @@ window.__ModuleLoader__.load({
|
|
|
3645
3559
|
}
|
|
3646
3560
|
//#endregion
|
|
3647
3561
|
exports.apply = apply;
|
|
3562
|
+
exports.feedRows = feedRows;
|
|
3648
3563
|
exports.inject = inject;
|
|
3649
|
-
exports.studyView = studyView;
|
|
3650
|
-
exports.transcriptRows = transcriptRows;
|
|
3651
3564
|
return module.exports;
|
|
3652
3565
|
}
|
|
3653
3566
|
});
|