dsh-plugin-lookatstudy 0.13.1 → 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 +12 -12
- package/lib/client.js +831 -1017
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +2 -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,15 +166,28 @@ 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}
|
|
@@ -264,17 +197,20 @@ window.__ModuleLoader__.load({
|
|
|
264
197
|
.lks-set-stats{margin:0;padding-left:18px;font-size:14px;line-height:1.9}
|
|
265
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}
|
|
266
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
|
+
|
|
267
207
|
/* composer dock status pill (conversation.composer.dock entry) */
|
|
268
208
|
.lks-dockpill{display:inline-flex;gap:10px;align-items:center;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
269
|
-
.lks-dockseg{white-space:nowrap}
|
|
209
|
+
.lks-dockseg{white-space:nowrap;display:inline-flex;align-items:center;gap:3px}
|
|
270
210
|
.lks-dock-due{color:var(--lks-warn-ink)}
|
|
271
211
|
.lks-dock-streak{color:var(--dsw-alias-state-business-primary)}
|
|
272
212
|
.lks-dockseg.lks-muted{color:var(--dsw-alias-label-secondary)}
|
|
273
213
|
|
|
274
|
-
/* concept map legend (amber = weak) */
|
|
275
|
-
.lks-cmap-legend{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:4px}
|
|
276
|
-
.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)}
|
|
277
|
-
|
|
278
214
|
/* keyed tool.call.toolview cards (conversation tab tool rows) */
|
|
279
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}
|
|
280
216
|
.lks-tv.err{border-color:var(--dsw-alias-state-error-primary)}
|
|
@@ -282,6 +218,7 @@ window.__ModuleLoader__.load({
|
|
|
282
218
|
.lks-tv-chip{align-self:flex-start;border-radius:999px;padding:2px 10px;font-size:12.5px;font-weight:600}
|
|
283
219
|
.lks-tv-chip.ok{background:var(--dsw-alias-state-success-tertiary);color:var(--lks-ok-ink)}
|
|
284
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}
|
|
285
222
|
.lks-tv-line{color:var(--dsw-alias-label-secondary);line-height:1.6;white-space:pre-wrap}
|
|
286
223
|
/* exam star card + the failure moment */
|
|
287
224
|
.lks-tv-stars{display:inline-flex;align-items:center;gap:5px;font-size:13px;font-weight:600;color:var(--lks-warn-ink)}
|
|
@@ -319,25 +256,6 @@ window.__ModuleLoader__.load({
|
|
|
319
256
|
* language — those are NOT ic_ds, see their section comment below.
|
|
320
257
|
* @module dsh-plugin-lookatstudy/client/icons
|
|
321
258
|
*/
|
|
322
|
-
/** ic_ds_think_outline_16 */
|
|
323
|
-
function IconThinkOutline16({ size = 16, className }) {
|
|
324
|
-
return (0, react.createElement)("svg", {
|
|
325
|
-
width: size,
|
|
326
|
-
height: size,
|
|
327
|
-
className,
|
|
328
|
-
viewBox: "0 0 16 16",
|
|
329
|
-
fill: "none",
|
|
330
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
331
|
-
}, (0, react.createElement)("path", {
|
|
332
|
-
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",
|
|
333
|
-
fill: "currentColor"
|
|
334
|
-
}), (0, react.createElement)("path", {
|
|
335
|
-
"fill-rule": "evenodd",
|
|
336
|
-
"clip-rule": "evenodd",
|
|
337
|
-
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",
|
|
338
|
-
fill: "currentColor"
|
|
339
|
-
}));
|
|
340
|
-
}
|
|
341
259
|
/** ic_ds_loading_outline_16 */
|
|
342
260
|
function IconLoadingOutline16({ size = 16, className }) {
|
|
343
261
|
return (0, react.createElement)("svg", {
|
|
@@ -366,38 +284,6 @@ window.__ModuleLoader__.load({
|
|
|
366
284
|
fill: "currentColor"
|
|
367
285
|
}));
|
|
368
286
|
}
|
|
369
|
-
/** Vendored 2026-09-01 for the critique icon round (same figma source as the three above). */
|
|
370
|
-
/** IconPlayOutline16 */
|
|
371
|
-
function IconPlayOutline16({ size = 16, className }) {
|
|
372
|
-
return (0, react.createElement)("svg", {
|
|
373
|
-
width: size,
|
|
374
|
-
height: size,
|
|
375
|
-
className,
|
|
376
|
-
viewBox: "0 0 16 16",
|
|
377
|
-
fill: "none",
|
|
378
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
379
|
-
}, (0, react.createElement)("path", {
|
|
380
|
-
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",
|
|
381
|
-
fill: "currentColor"
|
|
382
|
-
}), (0, react.createElement)("path", {
|
|
383
|
-
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",
|
|
384
|
-
fill: "currentColor"
|
|
385
|
-
}));
|
|
386
|
-
}
|
|
387
|
-
/** IconPlusOutline16 */
|
|
388
|
-
function IconPlusOutline16({ size = 16, className }) {
|
|
389
|
-
return (0, react.createElement)("svg", {
|
|
390
|
-
width: size,
|
|
391
|
-
height: size,
|
|
392
|
-
className,
|
|
393
|
-
viewBox: "0 0 16 16",
|
|
394
|
-
fill: "none",
|
|
395
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
396
|
-
}, (0, react.createElement)("path", {
|
|
397
|
-
d: "M8.64453 1.5V7.34961H14.5V8.65039H8.64453V14.5H7.34473V8.65039H1.5V7.34961H7.34473V1.5H8.64453Z",
|
|
398
|
-
fill: "currentColor"
|
|
399
|
-
}));
|
|
400
|
-
}
|
|
401
287
|
/** IconTrashOutline16 */
|
|
402
288
|
function IconTrashOutline16({ size = 16, className }) {
|
|
403
289
|
return (0, react.createElement)("svg", {
|
|
@@ -460,26 +346,6 @@ window.__ModuleLoader__.load({
|
|
|
460
346
|
fill: "currentColor"
|
|
461
347
|
}));
|
|
462
348
|
}
|
|
463
|
-
/** IconWarningOutline16 */
|
|
464
|
-
function IconWarningOutline16({ size = 16, className }) {
|
|
465
|
-
return (0, react.createElement)("svg", {
|
|
466
|
-
width: size,
|
|
467
|
-
height: size,
|
|
468
|
-
className,
|
|
469
|
-
viewBox: "0 0 16 16",
|
|
470
|
-
fill: "none",
|
|
471
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
472
|
-
}, (0, react.createElement)("path", {
|
|
473
|
-
d: "M6.3002 3.32843L7.69986 3.32843L7.69986 7.79657H6.3002L6.3002 3.32843Z",
|
|
474
|
-
fill: "currentColor"
|
|
475
|
-
}), (0, react.createElement)("path", {
|
|
476
|
-
d: "M6.3002 9.01935H7.69986V10.6711H6.3002V9.01935Z",
|
|
477
|
-
fill: "currentColor"
|
|
478
|
-
}), (0, react.createElement)("path", {
|
|
479
|
-
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",
|
|
480
|
-
fill: "currentColor"
|
|
481
|
-
}));
|
|
482
|
-
}
|
|
483
349
|
/**
|
|
484
350
|
* Plugin-original companions for learning-domain glyphs the host set does not
|
|
485
351
|
* carry (lock/star/book/crown/bolt/power/pin/flame) — authored here in the
|
|
@@ -560,41 +426,6 @@ window.__ModuleLoader__.load({
|
|
|
560
426
|
fill: "currentColor"
|
|
561
427
|
}));
|
|
562
428
|
}
|
|
563
|
-
/** IconPowerFill16 */
|
|
564
|
-
function IconPowerFill16({ size = 16, className }) {
|
|
565
|
-
return (0, react.createElement)("svg", {
|
|
566
|
-
width: size,
|
|
567
|
-
height: size,
|
|
568
|
-
className,
|
|
569
|
-
viewBox: "0 0 16 16",
|
|
570
|
-
fill: "none",
|
|
571
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
572
|
-
}, (0, react.createElement)("path", {
|
|
573
|
-
d: "M7.2 1h1.6v5.6H7.2V1Z",
|
|
574
|
-
fill: "currentColor"
|
|
575
|
-
}), (0, react.createElement)("path", {
|
|
576
|
-
"fill-rule": "evenodd",
|
|
577
|
-
"clip-rule": "evenodd",
|
|
578
|
-
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",
|
|
579
|
-
fill: "currentColor"
|
|
580
|
-
}));
|
|
581
|
-
}
|
|
582
|
-
/** IconPinFill16 */
|
|
583
|
-
function IconPinFill16({ size = 16, className }) {
|
|
584
|
-
return (0, react.createElement)("svg", {
|
|
585
|
-
width: size,
|
|
586
|
-
height: size,
|
|
587
|
-
className,
|
|
588
|
-
viewBox: "0 0 16 16",
|
|
589
|
-
fill: "none",
|
|
590
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
591
|
-
}, (0, react.createElement)("path", {
|
|
592
|
-
"fill-rule": "evenodd",
|
|
593
|
-
"clip-rule": "evenodd",
|
|
594
|
-
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",
|
|
595
|
-
fill: "currentColor"
|
|
596
|
-
}));
|
|
597
|
-
}
|
|
598
429
|
/** IconFlameFill16 */
|
|
599
430
|
function IconFlameFill16({ size = 16, className }) {
|
|
600
431
|
return (0, react.createElement)("svg", {
|
|
@@ -1638,6 +1469,8 @@ window.__ModuleLoader__.load({
|
|
|
1638
1469
|
"rail.empty.hint2": "本地文件夹 → 说「导入 D:/path/to/folder」",
|
|
1639
1470
|
"rail.empty.placeholder": "GitHub 仓库链接,如 microsoft/AI-For-Beginners",
|
|
1640
1471
|
"rail.empty.button": "导入",
|
|
1472
|
+
"composer.send": "发送",
|
|
1473
|
+
"composer.busy": "导师正在回复…",
|
|
1641
1474
|
"rail.empty.demo": "导入示例课程",
|
|
1642
1475
|
"rail.mastered": "毕业 {mastered}/{total} 课",
|
|
1643
1476
|
"rail.avg": "平均掌握度 {pct}%",
|
|
@@ -1774,6 +1607,8 @@ window.__ModuleLoader__.load({
|
|
|
1774
1607
|
"rail.empty.hint2": "Local folder → say \"import D:/path/to/folder\"",
|
|
1775
1608
|
"rail.empty.placeholder": "GitHub repo link, e.g. microsoft/AI-For-Beginners",
|
|
1776
1609
|
"rail.empty.button": "Import",
|
|
1610
|
+
"composer.send": "Send",
|
|
1611
|
+
"composer.busy": "The tutor is replying…",
|
|
1777
1612
|
"rail.empty.demo": "Import the demo course",
|
|
1778
1613
|
"rail.mastered": "{mastered}/{total} lessons graduated",
|
|
1779
1614
|
"rail.avg": "avg mastery {pct}%",
|
|
@@ -2348,6 +2183,74 @@ window.__ModuleLoader__.load({
|
|
|
2348
2183
|
});
|
|
2349
2184
|
}
|
|
2350
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
|
|
2351
2254
|
//#region src/client/readaloud.ts
|
|
2352
2255
|
const sleep = (ms) => new Promise((resolve) => {
|
|
2353
2256
|
setTimeout(resolve, ms);
|
|
@@ -2454,97 +2357,20 @@ window.__ModuleLoader__.load({
|
|
|
2454
2357
|
//#endregion
|
|
2455
2358
|
//#region src/client/views.tsx
|
|
2456
2359
|
/**
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
2459
|
-
*
|
|
2460
|
-
*
|
|
2461
|
-
* snapshot (user text, assistant text rendered through the plugin's own
|
|
2462
|
-
* markdown pipeline; tool calls stay in the host conversation's toolview
|
|
2463
|
-
* cards — a mirrored second record diverged) plus the soul pills, starters,
|
|
2464
|
-
* pending proposal banner, and a reverse-channel input. Right: the blackboard —
|
|
2465
|
-
* focus-lesson 讲解 and the Cornell 笔记 zones. Nothing outside this tab
|
|
2466
|
-
* 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.
|
|
2467
2364
|
* @module dsh-plugin-lookatstudy/client/views
|
|
2468
2365
|
*/
|
|
2469
|
-
/**
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
labelKey: "soul.direct",
|
|
2474
|
-
hintKey: "soul.direct.hint"
|
|
2475
|
-
},
|
|
2476
|
-
{
|
|
2477
|
-
id: "guide",
|
|
2478
|
-
labelKey: "soul.guide",
|
|
2479
|
-
hintKey: "soul.guide.hint"
|
|
2480
|
-
},
|
|
2481
|
-
{
|
|
2482
|
-
id: "practice",
|
|
2483
|
-
labelKey: "soul.practice",
|
|
2484
|
-
hintKey: "soul.practice.hint"
|
|
2485
|
-
}
|
|
2486
|
-
];
|
|
2487
|
-
/** Zone keys for the Cornell notebook (understand / record / practice). */
|
|
2488
|
-
const ZONES = [
|
|
2489
|
-
["understand", "zone.understand"],
|
|
2490
|
-
["record", "zone.record"],
|
|
2491
|
-
["practice", "zone.practice"]
|
|
2492
|
-
];
|
|
2493
|
-
/** Status icon for one lesson row (LookatStudy's map icon semantics, SVG glyphs). */
|
|
2494
|
-
function statusIcon(kind, status) {
|
|
2495
|
-
if (kind === "exam") return (0, react.createElement)(IconGoalOutline16, { size: 14 });
|
|
2496
|
-
if (status === "mastered") return (0, react.createElement)(IconCrownFill16, { size: 14 });
|
|
2497
|
-
if (status === "in_progress") return (0, react.createElement)(IconBookFill16, { size: 14 });
|
|
2498
|
-
if (status === "available") return (0, react.createElement)(IconStarFill16, { size: 14 });
|
|
2499
|
-
return (0, react.createElement)(IconLockFill16, { size: 14 });
|
|
2500
|
-
}
|
|
2501
|
-
/** LookatStudy's exam gate: an exam node opens only when every sibling study lesson reached mastery ≥50%. */
|
|
2502
|
-
function examOpen(lessons) {
|
|
2503
|
-
return lessons.every((l) => l.kind !== "study" || (l.masteryPct ?? 0) >= 50);
|
|
2504
|
-
}
|
|
2505
|
-
/** Multi-keyword AND title filter (LookatStudy course-tree-filter). */
|
|
2506
|
-
function titleMatches(title, query) {
|
|
2507
|
-
return query.trim().toLowerCase().split(/\s+/).filter(Boolean).every((k) => title.toLowerCase().includes(k));
|
|
2508
|
-
}
|
|
2509
|
-
/**
|
|
2510
|
-
* Default expansion for one rail section: collapsed when every study lesson is
|
|
2511
|
-
* done (mastered) or not yet reachable (locked) — long courses otherwise scroll
|
|
2512
|
-
* 5× their viewport. The active frontier and the focus lesson's section stay
|
|
2513
|
-
* open; exam nodes never force a section open (they are gated separately).
|
|
2514
|
-
* Pure.
|
|
2515
|
-
* @param section - one course section projection with lesson kinds/statuses.
|
|
2516
|
-
*/
|
|
2517
|
-
function sectionDefaultOpen(section) {
|
|
2518
|
-
return section.lessons.some((l) => l.focus || l.kind !== "exam" && l.status !== "mastered" && l.status !== "locked");
|
|
2519
|
-
}
|
|
2520
|
-
/** The rail's import row: a GitHub URL input plus the paste/folder hints. */
|
|
2521
|
-
function ImportRow({ send }) {
|
|
2522
|
-
const [url, setUrl] = (0, react.useState)("");
|
|
2523
|
-
return (0, react.createElement)("div", { className: "lks-import" }, (0, react.createElement)("div", { className: "lks-inputrow" }, (0, react.createElement)("input", {
|
|
2524
|
-
className: "lks-input",
|
|
2525
|
-
type: "url",
|
|
2526
|
-
placeholder: tr("rail.empty.placeholder"),
|
|
2527
|
-
value: url,
|
|
2528
|
-
onChange: (e) => {
|
|
2529
|
-
setUrl(e.target.value);
|
|
2530
|
-
},
|
|
2531
|
-
onKeyDown: (e) => {
|
|
2532
|
-
if (e.key === "Enter" && url.trim() !== "") send(tr("prompt.import", { url: url.trim() }));
|
|
2533
|
-
}
|
|
2534
|
-
}), (0, react.createElement)("button", {
|
|
2535
|
-
className: "lks-btn primary",
|
|
2536
|
-
disabled: url.trim() === "",
|
|
2537
|
-
onClick: () => {
|
|
2538
|
-
send(tr("prompt.import", { url: url.trim() }));
|
|
2539
|
-
}
|
|
2540
|
-
}, 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);
|
|
2541
2370
|
}
|
|
2542
2371
|
/**
|
|
2543
2372
|
* Tooltip text for one course-tree status glyph. Pure (the translator is an
|
|
2544
2373
|
* optional parameter defaulting to the active one, evaluated per call).
|
|
2545
|
-
* @param kind - lesson kind ('study' | 'practice' | 'exam').
|
|
2546
|
-
* @param status - lesson status ('locked' | 'available' | 'in_progress' | 'mastered').
|
|
2547
|
-
* @param t - translator (defaults to the module's active translator).
|
|
2548
2374
|
*/
|
|
2549
2375
|
function statusTitle(kind, status, t = tr) {
|
|
2550
2376
|
if (kind === "exam") return t("status.exam");
|
|
@@ -2559,8 +2385,6 @@ window.__ModuleLoader__.load({
|
|
|
2559
2385
|
* Extract the last quiz option block (2–4 consecutive `A. …` `B. …` lines,
|
|
2560
2386
|
* letters strictly consecutive from A) from an assistant reply — the raw text
|
|
2561
2387
|
* of the tutor's question becomes the clickable answer surface. Pure.
|
|
2562
|
-
* @param text - one assistant reply's markdown.
|
|
2563
|
-
* @returns the options of the last valid block, or [] when none.
|
|
2564
2388
|
*/
|
|
2565
2389
|
function quizOptions(text) {
|
|
2566
2390
|
const runs = [];
|
|
@@ -2591,268 +2415,216 @@ window.__ModuleLoader__.load({
|
|
|
2591
2415
|
flush();
|
|
2592
2416
|
return runs.at(-1) ?? [];
|
|
2593
2417
|
}
|
|
2594
|
-
function textOf(blocks) {
|
|
2595
|
-
return (blocks ?? []).filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n\n").trim();
|
|
2596
|
-
}
|
|
2597
|
-
function assistantText(blocks) {
|
|
2598
|
-
return (blocks ?? []).filter((b) => b.kind === "text").map((b) => b.text ?? "").join("\n\n").trim();
|
|
2599
|
-
}
|
|
2600
2418
|
/**
|
|
2601
|
-
*
|
|
2602
|
-
*
|
|
2603
|
-
*
|
|
2604
|
-
* surface (a transcript mirror there grew a second, diverging record).
|
|
2605
|
-
* Both inputs tolerate undefined: newer hosts (DSH v0.1.2-alpha.4) mount
|
|
2606
|
-
* the conversation view before session data exists, and the fold must
|
|
2607
|
-
* degrade to an empty transcript instead of blanking the whole tab.
|
|
2608
|
-
* @param nodes - finalized conversation nodes from the snapshot.
|
|
2609
|
-
* @param partial - the in-flight assistant partial, or null.
|
|
2610
|
-
* @param t - translator (defaults to the active one, per call).
|
|
2611
|
-
* @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.
|
|
2612
2422
|
*/
|
|
2613
|
-
function
|
|
2614
|
-
|
|
2615
|
-
for (const node of nodes ?? []) switch (node.kind) {
|
|
2616
|
-
case "user":
|
|
2617
|
-
case "steering": {
|
|
2618
|
-
const text = textOf(node.content);
|
|
2619
|
-
if (text !== "") rows.push({
|
|
2620
|
-
key: `u${node.seq}`,
|
|
2621
|
-
role: "user",
|
|
2622
|
-
text
|
|
2623
|
-
});
|
|
2624
|
-
break;
|
|
2625
|
-
}
|
|
2626
|
-
case "assistant": {
|
|
2627
|
-
const text = assistantText(node.blocks);
|
|
2628
|
-
if (text !== "") rows.push({
|
|
2629
|
-
key: `a${node.seq}`,
|
|
2630
|
-
role: "assistant",
|
|
2631
|
-
text
|
|
2632
|
-
});
|
|
2633
|
-
break;
|
|
2634
|
-
}
|
|
2635
|
-
case "turn-error": rows.push({
|
|
2636
|
-
key: `e${node.seq}`,
|
|
2637
|
-
role: "error",
|
|
2638
|
-
text: node.message ?? ""
|
|
2639
|
-
});
|
|
2640
|
-
}
|
|
2641
|
-
if (partial != null) {
|
|
2642
|
-
const text = assistantText(partial.blocks);
|
|
2643
|
-
if (text !== "") rows.push({
|
|
2644
|
-
key: "streaming",
|
|
2645
|
-
role: "streaming",
|
|
2646
|
-
text
|
|
2647
|
-
});
|
|
2648
|
-
else rows.push({
|
|
2649
|
-
key: "thinking",
|
|
2650
|
-
role: "thinking",
|
|
2651
|
-
text: t("row.thinking")
|
|
2652
|
-
});
|
|
2653
|
-
}
|
|
2654
|
-
return rows;
|
|
2655
|
-
}
|
|
2656
|
-
/** Small inline error surface for failed write actions (shared with the settings page). */
|
|
2657
|
-
function ActionError({ error }) {
|
|
2658
|
-
if (error === null) return null;
|
|
2659
|
-
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");
|
|
2660
2425
|
}
|
|
2426
|
+
//#endregion
|
|
2427
|
+
//#region src/client/panel.tsx
|
|
2661
2428
|
/**
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2665
|
-
*
|
|
2666
|
-
*
|
|
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
|
|
2667
2441
|
*/
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
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"
|
|
2684
2458
|
}
|
|
2685
|
-
|
|
2686
|
-
const
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
};
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
function
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
(0, react.
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
const rail = root.querySelector(".lks-col-rail");
|
|
2719
|
-
if (tutor === null || rail === null || getComputedStyle(rail).display === "none") return clear();
|
|
2720
|
-
const seatRect = seat.getBoundingClientRect();
|
|
2721
|
-
const tutorRect = tutor.getBoundingClientRect();
|
|
2722
|
-
if (seatRect.width === 0 || tutorRect.width === 0) return clear();
|
|
2723
|
-
const pad = parseFloat(getComputedStyle(seat).paddingLeft) || 0;
|
|
2724
|
-
const cardW = card.getBoundingClientRect().width;
|
|
2725
|
-
const max = seatRect.width - 2 * pad - cardW;
|
|
2726
|
-
const shift = Math.round(Math.max(0, Math.min(tutorRect.left - (seatRect.left + pad), max)));
|
|
2727
|
-
if (shift < 2) return clear();
|
|
2728
|
-
card.style.setProperty("--lks-composer-shift", `${shift}px`);
|
|
2729
|
-
card.style.setProperty("--lks-composer-follow-width", `${Math.round(tutorRect.width)}px`);
|
|
2730
|
-
card.classList.add("lks-composer-follow");
|
|
2731
|
-
};
|
|
2732
|
-
apply();
|
|
2733
|
-
const ro = new ResizeObserver(apply);
|
|
2734
|
-
ro.observe(root);
|
|
2735
|
-
ro.observe(seat);
|
|
2736
|
-
window.addEventListener("resize", apply);
|
|
2737
|
-
return () => {
|
|
2738
|
-
ro.disconnect();
|
|
2739
|
-
window.removeEventListener("resize", apply);
|
|
2740
|
-
clear();
|
|
2741
|
-
};
|
|
2742
|
-
}, []);
|
|
2743
|
-
const send = (text) => {
|
|
2744
|
-
(async () => {
|
|
2745
|
-
if (data?.active !== true) await activate(true);
|
|
2746
|
-
inputActions.setDraft(text);
|
|
2747
|
-
inputActions.submit();
|
|
2748
|
-
})();
|
|
2749
|
-
};
|
|
2750
|
-
const [, bumpLocale] = (0, react.useState)(0);
|
|
2751
|
-
(0, react.useEffect)(() => {
|
|
2752
|
-
const localeSvc = ctx.locale;
|
|
2753
|
-
if (localeSvc === void 0) return;
|
|
2754
|
-
return localeSvc.subscribe(() => {
|
|
2755
|
-
bumpLocale((v) => v + 1);
|
|
2756
|
-
});
|
|
2757
|
-
}, [ctx]);
|
|
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;
|
|
2758
2492
|
return (0, react.createElement)("div", {
|
|
2759
|
-
|
|
2760
|
-
className: "
|
|
2761
|
-
|
|
2762
|
-
"
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
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",
|
|
2766
2501
|
onClick: () => {
|
|
2767
|
-
|
|
2502
|
+
interactive.send(tr("quiz.answer", {
|
|
2503
|
+
letter: opt.letter,
|
|
2504
|
+
text: opt.text
|
|
2505
|
+
}));
|
|
2768
2506
|
}
|
|
2769
|
-
},
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
{
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
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"
|
|
2515
|
+
});
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
function StudyPanelBody({ ctx }) {
|
|
2519
|
+
const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession } = useStudy();
|
|
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);
|
|
2528
|
+
(0, react.useEffect)(() => {
|
|
2529
|
+
if (boundId === null) {
|
|
2530
|
+
setRows([]);
|
|
2531
|
+
return;
|
|
2781
2532
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
onClick: () => {
|
|
2787
|
-
setPane(p.id);
|
|
2788
|
-
try {
|
|
2789
|
-
sessionStorage.setItem(PANE_KEY, p.id);
|
|
2790
|
-
} catch {}
|
|
2533
|
+
const source = ctx.sessions.binding(boundId)?.eventSource;
|
|
2534
|
+
if (source === void 0) {
|
|
2535
|
+
setRows([]);
|
|
2536
|
+
return;
|
|
2791
2537
|
}
|
|
2792
|
-
|
|
2538
|
+
const update = () => {
|
|
2539
|
+
setRows(feedRows(source.getSnapshot()));
|
|
2540
|
+
};
|
|
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. */
|
|
2549
|
+
const send = (text) => {
|
|
2550
|
+
(async () => {
|
|
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
|
+
}
|
|
2585
|
+
})();
|
|
2586
|
+
};
|
|
2587
|
+
const body = (0, react.createElement)("div", { className: "lks14-body" }, (0, react.createElement)(CourseRail, {
|
|
2793
2588
|
data,
|
|
2794
2589
|
activate,
|
|
2795
2590
|
setFocus,
|
|
2796
2591
|
searchLessons,
|
|
2797
2592
|
deleteCourse,
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
ctx,
|
|
2801
|
-
currentSessionId
|
|
2802
|
-
}), (0, react.createElement)(TutorColumn, {
|
|
2593
|
+
send
|
|
2594
|
+
}), (0, react.createElement)(ChatPane, {
|
|
2803
2595
|
data,
|
|
2804
|
-
|
|
2596
|
+
lesson,
|
|
2597
|
+
rows,
|
|
2598
|
+
busy,
|
|
2599
|
+
sendError,
|
|
2600
|
+
draft,
|
|
2601
|
+
setDraft,
|
|
2805
2602
|
send,
|
|
2806
|
-
|
|
2807
|
-
}), (0, react.createElement)(
|
|
2603
|
+
setMode
|
|
2604
|
+
}), (0, react.createElement)(NotebookPane, {
|
|
2808
2605
|
data,
|
|
2809
2606
|
deleteNote
|
|
2810
|
-
}))
|
|
2607
|
+
}));
|
|
2608
|
+
return (0, react.createElement)("div", {
|
|
2609
|
+
className: "lks14",
|
|
2610
|
+
"data-lks-panel": ""
|
|
2611
|
+
}, body);
|
|
2811
2612
|
}
|
|
2812
|
-
/**
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
if (mapped === currentSessionId) {
|
|
2820
|
-
setFocus(lesson.id).catch(reportError);
|
|
2821
|
-
return;
|
|
2822
|
-
}
|
|
2823
|
-
if (mapped !== void 0 && ctx.sessions.binding(sessionId(mapped)) !== void 0) {
|
|
2824
|
-
setFocus(lesson.id).catch(reportError);
|
|
2825
|
-
ctx.sessions.open(sessionId(mapped));
|
|
2826
|
-
return;
|
|
2827
|
-
}
|
|
2828
|
-
setJumping(lesson.id);
|
|
2829
|
-
setFocus(lesson.id).then(() => (async () => {
|
|
2830
|
-
if (data?.active !== true) await activate(true);
|
|
2831
|
-
const area = await fetch("/lookatstudy/api/study-workspace");
|
|
2832
|
-
if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
|
|
2833
|
-
const { path } = await area.json();
|
|
2834
|
-
const workspace = await ctx.workspaces.create({ path });
|
|
2835
|
-
const sessionId = ctx.workspaces.connectWorkspace !== void 0 ? await ctx.workspaces.connectWorkspace(workspace.workspaceId) : await ctx.sessions.create({ workspaceId: workspace.workspaceId });
|
|
2836
|
-
const actx = ctx.sessions.scope(sessionId);
|
|
2837
|
-
const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
|
|
2838
|
-
if (face === void 0) throw new Error("lesson session is not addressable yet");
|
|
2839
|
-
const result = await face.prompt([{
|
|
2840
|
-
type: "text",
|
|
2841
|
-
text: tr("prompt.lesson", { title: lesson.title })
|
|
2842
|
-
}], "queue");
|
|
2843
|
-
if (!result.ok) throw new Error(`lesson prompt rejected: ${result.error.code}: ${result.error.message}`);
|
|
2844
|
-
await bindLessonSession(lesson.id, sessionId);
|
|
2845
|
-
ctx.sessions.open(sessionId);
|
|
2846
|
-
})()).then(() => {
|
|
2847
|
-
setJumping(null);
|
|
2848
|
-
}, (err) => {
|
|
2849
|
-
reportError(err);
|
|
2850
|
-
setJumping(null);
|
|
2851
|
-
});
|
|
2852
|
-
};
|
|
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 }) {
|
|
2853
2620
|
const [selectedCourse, setSelectedCourse] = (0, react.useState)("");
|
|
2854
2621
|
const [query, setQuery] = (0, react.useState)("");
|
|
2855
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
|
+
};
|
|
2856
2628
|
(0, react.useEffect)(() => {
|
|
2857
2629
|
if (!confirmDelete) return;
|
|
2858
2630
|
const disarm = () => {
|
|
@@ -2868,14 +2640,7 @@ window.__ModuleLoader__.load({
|
|
|
2868
2640
|
window.removeEventListener("keydown", onKey);
|
|
2869
2641
|
};
|
|
2870
2642
|
}, [confirmDelete]);
|
|
2871
|
-
const
|
|
2872
|
-
/** Per-section open overrides (courseId/sectionTitle → open?), on top of the frontier default. */
|
|
2873
|
-
const [secOpen, setSecOpen] = (0, react.useState)({});
|
|
2874
|
-
const [error, setError] = (0, react.useState)(null);
|
|
2875
|
-
const reportError = (err) => {
|
|
2876
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
2877
|
-
};
|
|
2878
|
-
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", {
|
|
2879
2644
|
className: "lks-btn primary",
|
|
2880
2645
|
style: { margin: "10px 0" },
|
|
2881
2646
|
onClick: () => {
|
|
@@ -2884,8 +2649,8 @@ window.__ModuleLoader__.load({
|
|
|
2884
2649
|
}, (0, react.createElement)(IconDownloadOutline16, null), tr("rail.empty.demo")), (0, react.createElement)(ImportRow, { send })) : (() => {
|
|
2885
2650
|
const courseId = data.courses.some((c) => c.courseId === selectedCourse) ? selectedCourse : data.courses[0].courseId;
|
|
2886
2651
|
const course = data.courses.find((c) => c.courseId === courseId);
|
|
2887
|
-
return (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "
|
|
2888
|
-
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",
|
|
2889
2654
|
value: courseId,
|
|
2890
2655
|
onChange: (e) => {
|
|
2891
2656
|
setSelectedCourse(e.target.value);
|
|
@@ -2895,7 +2660,7 @@ window.__ModuleLoader__.load({
|
|
|
2895
2660
|
key: c.courseId,
|
|
2896
2661
|
value: c.courseId
|
|
2897
2662
|
}, c.title))) : (0, react.createElement)("div", {
|
|
2898
|
-
className: "
|
|
2663
|
+
className: "lks14-railtitle",
|
|
2899
2664
|
title: course.title
|
|
2900
2665
|
}, course.title), (0, react.createElement)("button", {
|
|
2901
2666
|
className: `lks-btn ${confirmDelete ? "primary" : "ghost"}`,
|
|
@@ -2911,14 +2676,14 @@ window.__ModuleLoader__.load({
|
|
|
2911
2676
|
setSelectedCourse("");
|
|
2912
2677
|
}, reportError);
|
|
2913
2678
|
}
|
|
2914
|
-
}, 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", {
|
|
2915
2680
|
mastered: course.mastered,
|
|
2916
2681
|
total: course.total
|
|
2917
2682
|
})), (0, react.createElement)("div", {
|
|
2918
|
-
className: `
|
|
2683
|
+
className: `lks14-masterybar${course.avgMasteryPct === 100 ? " gold" : ""}`,
|
|
2919
2684
|
title: course.avgMasteryPct === null ? tr("rail.avg.none") : tr("rail.avg", { pct: course.avgMasteryPct })
|
|
2920
2685
|
}, (0, react.createElement)("i", { style: { transform: `scaleX(${(course.avgMasteryPct ?? 0) / 100})` } })), (0, react.createElement)("input", {
|
|
2921
|
-
className: "
|
|
2686
|
+
className: "lks14-search",
|
|
2922
2687
|
type: "search",
|
|
2923
2688
|
placeholder: tr("rail.search"),
|
|
2924
2689
|
value: query,
|
|
@@ -2942,32 +2707,10 @@ window.__ModuleLoader__.load({
|
|
|
2942
2707
|
}
|
|
2943
2708
|
}).catch(reportError);
|
|
2944
2709
|
}
|
|
2945
|
-
}),
|
|
2946
|
-
className: "lks-btn ghost",
|
|
2947
|
-
style: {
|
|
2948
|
-
margin: "0 0 6px",
|
|
2949
|
-
padding: "3px 8px",
|
|
2950
|
-
fontSize: "13px"
|
|
2951
|
-
},
|
|
2952
|
-
title: tr("rail.locate.title"),
|
|
2953
|
-
onClick: () => {
|
|
2954
|
-
const focusSection = course.sections.find((s) => s.lessons.some((l) => l.focus));
|
|
2955
|
-
if (focusSection === void 0) return;
|
|
2956
|
-
setSecOpen((m) => ({
|
|
2957
|
-
...m,
|
|
2958
|
-
[`${courseId}/${focusSection.title}`]: true
|
|
2959
|
-
}));
|
|
2960
|
-
window.setTimeout(() => {
|
|
2961
|
-
document.querySelector(".lks-col-rail .lks-node.focus")?.scrollIntoView({
|
|
2962
|
-
block: "center",
|
|
2963
|
-
behavior: "smooth"
|
|
2964
|
-
});
|
|
2965
|
-
}, 60);
|
|
2966
|
-
}
|
|
2967
|
-
}, (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", {
|
|
2968
2711
|
key: d.lessonId,
|
|
2969
|
-
className: "
|
|
2970
|
-
}, (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", {
|
|
2971
2714
|
className: "lks-btn ghost",
|
|
2972
2715
|
style: { marginTop: "6px" },
|
|
2973
2716
|
onClick: () => {
|
|
@@ -2977,56 +2720,33 @@ window.__ModuleLoader__.load({
|
|
|
2977
2720
|
const examAllowed = examOpen(section.lessons);
|
|
2978
2721
|
const lessons = section.lessons.filter((l) => query.trim() === "" || titleMatches(l.title, query) || l.focus);
|
|
2979
2722
|
if (lessons.length === 0) return [];
|
|
2980
|
-
const
|
|
2981
|
-
const open = query.trim() !== "" || (secOpen[secKey] ?? sectionDefaultOpen(section));
|
|
2723
|
+
const open = query.trim() !== "" || sectionDefaultOpen(section);
|
|
2982
2724
|
return [(0, react.createElement)("button", {
|
|
2983
2725
|
key: section.title,
|
|
2984
2726
|
type: "button",
|
|
2985
|
-
className: "
|
|
2727
|
+
className: "lks14-sechead",
|
|
2986
2728
|
"aria-expanded": String(open),
|
|
2987
2729
|
title: open ? tr("rail.section.collapse") : tr("rail.section.expand", { count: section.lessons.length }),
|
|
2988
|
-
onClick: () => {
|
|
2989
|
-
|
|
2990
|
-
...m,
|
|
2991
|
-
[secKey]: !open
|
|
2992
|
-
}));
|
|
2993
|
-
}
|
|
2994
|
-
}, (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) => {
|
|
2995
2732
|
const locked = lesson.status === "locked" || lesson.kind === "exam" && !examAllowed;
|
|
2996
2733
|
return (0, react.createElement)("button", {
|
|
2997
2734
|
key: lesson.id,
|
|
2998
2735
|
type: "button",
|
|
2999
|
-
className: `
|
|
2736
|
+
className: `lks14-node${lesson.focus ? " focus" : ""}`,
|
|
3000
2737
|
"aria-disabled": locked || void 0,
|
|
3001
|
-
title:
|
|
2738
|
+
title: `${lesson.title} — ${statusTitle(lesson.kind, locked ? "locked" : lesson.status)}`,
|
|
3002
2739
|
onClick: () => {
|
|
3003
2740
|
if (locked) return;
|
|
3004
|
-
|
|
3005
|
-
setFocus(lesson.id).catch(reportError);
|
|
3006
|
-
send(tr("prompt.exam", { section: section.title }));
|
|
3007
|
-
return;
|
|
3008
|
-
}
|
|
3009
|
-
openLessonThread(lesson);
|
|
2741
|
+
setFocus(lesson.id).catch(reportError);
|
|
3010
2742
|
}
|
|
3011
|
-
}, (0, react.createElement)("span", { className: "
|
|
3012
|
-
|
|
3013
|
-
className: "lks-spin"
|
|
3014
|
-
}) : statusIcon(lesson.kind, locked ? "locked" : lesson.status)), (0, react.createElement)("span", { className: "lks-t" }, lesson.title), lesson.due ? (0, react.createElement)("span", {
|
|
3015
|
-
className: "lks-tag due",
|
|
3016
|
-
title: tr("rail.due.tag")
|
|
3017
|
-
}, (0, react.createElement)(IconRefreshOutline16, { size: 11 })) : null, lesson.weakConcepts > 0 ? (0, react.createElement)("span", {
|
|
3018
|
-
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",
|
|
3019
2745
|
title: tr("tag.weak", { count: lesson.weakConcepts })
|
|
3020
|
-
}, (0, react.createElement)(IconBoltFill16, { size: 10 }), String(lesson.weakConcepts)) : null, lesson.
|
|
3021
|
-
className: "
|
|
3022
|
-
title: tr("tag.friction", { count: lesson.frictionCount })
|
|
3023
|
-
}, (0, react.createElement)(IconWarningOutline16, { size: 10 }), String(lesson.frictionCount)) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", {
|
|
3024
|
-
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",
|
|
3025
2748
|
title: tr("tag.mastery", { pct: lesson.masteryPct })
|
|
3026
|
-
}, (0, react.createElement)("i", { style: { transform: `scaleX(${lesson.masteryPct / 100})` } })) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", {
|
|
3027
|
-
className: "lks-pct",
|
|
3028
|
-
title: tr("tag.mastery.short")
|
|
3029
|
-
}, `${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);
|
|
3030
2750
|
}) : []];
|
|
3031
2751
|
}), (0, react.createElement)("button", {
|
|
3032
2752
|
className: "lks-btn ghost",
|
|
@@ -3034,71 +2754,41 @@ window.__ModuleLoader__.load({
|
|
|
3034
2754
|
onClick: () => {
|
|
3035
2755
|
setShowImport(!showImport);
|
|
3036
2756
|
}
|
|
3037
|
-
}, showImport ? tr("rail.import.close") :
|
|
2757
|
+
}, showImport ? tr("rail.import.close") : null, tr("rail.import.toggle")), showImport ? (0, react.createElement)(ImportRow, { send }) : null);
|
|
3038
2758
|
})();
|
|
3039
|
-
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));
|
|
3040
2760
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
className: "lks-spin"
|
|
3058
|
-
}), row.text);
|
|
3059
|
-
const cls = row.role === "streaming" ? "lks-msg assistant lks-prose streaming" : "lks-msg assistant lks-prose";
|
|
3060
|
-
const body = (0, react.createElement)("div", {
|
|
3061
|
-
key: row.key,
|
|
3062
|
-
className: cls,
|
|
3063
|
-
dangerouslySetInnerHTML: { __html: renderMarkdown(row.text) }
|
|
3064
|
-
});
|
|
3065
|
-
const options = interactive === void 0 || row.role === "streaming" ? [] : quizOptions(row.text);
|
|
3066
|
-
if (options.length < 2) return body;
|
|
3067
|
-
return (0, react.createElement)("div", {
|
|
3068
|
-
key: row.key,
|
|
3069
|
-
className: "lks-turn"
|
|
3070
|
-
}, body, (0, react.createElement)("div", {
|
|
3071
|
-
className: "lks-quiz",
|
|
3072
|
-
title: tr("quiz.title")
|
|
3073
|
-
}, ...options.map((opt) => (0, react.createElement)("button", {
|
|
3074
|
-
key: opt.letter,
|
|
3075
|
-
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() === "",
|
|
3076
2777
|
onClick: () => {
|
|
3077
|
-
|
|
3078
|
-
letter: opt.letter,
|
|
3079
|
-
text: opt.text
|
|
3080
|
-
}));
|
|
2778
|
+
send(tr("prompt.import", { url: url.trim() }));
|
|
3081
2779
|
}
|
|
3082
|
-
}, (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")));
|
|
3083
2781
|
}
|
|
3084
|
-
/**
|
|
3085
|
-
function
|
|
3086
|
-
const
|
|
3087
|
-
const rows = transcriptRows(snapshot?.nodes, snapshot?.partial ?? null);
|
|
3088
|
-
const lastAssistant = rows.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
3089
|
-
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);
|
|
3090
2785
|
(0, react.useEffect)(() => {
|
|
3091
|
-
const el =
|
|
2786
|
+
const el = streamEl.current;
|
|
3092
2787
|
if (el !== null) el.scrollTop = el.scrollHeight;
|
|
3093
2788
|
}, [rows.length]);
|
|
3094
|
-
(0, react.useEffect)(() => {
|
|
3095
|
-
const el = scrollRef.current;
|
|
3096
|
-
if (el !== null && rows.length > 0) enhanceRendered(el).catch(() => {});
|
|
3097
|
-
}, [rows.length]);
|
|
3098
|
-
const proposal = data?.pendingProposals[0] ?? null;
|
|
3099
|
-
const lesson = data?.lesson ?? null;
|
|
3100
2789
|
const dormant = data?.active !== true;
|
|
3101
|
-
|
|
2790
|
+
const lastAssistant = rows.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
|
|
2791
|
+
const [error, setError] = (0, react.useState)(null);
|
|
3102
2792
|
const fire = (action) => {
|
|
3103
2793
|
action.then(() => {
|
|
3104
2794
|
setError(null);
|
|
@@ -3106,155 +2796,68 @@ window.__ModuleLoader__.load({
|
|
|
3106
2796
|
setError(err instanceof Error ? err.message : String(err));
|
|
3107
2797
|
});
|
|
3108
2798
|
};
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
rationale: proposal.rationale
|
|
3115
|
-
})), (0, react.createElement)("button", {
|
|
3116
|
-
className: "lks-btn primary",
|
|
3117
|
-
onClick: () => {
|
|
3118
|
-
send(tr("prompt.proposal.accept", { id: proposal.id }));
|
|
3119
|
-
}
|
|
3120
|
-
}, tr("proposal.accept")), (0, react.createElement)("button", {
|
|
3121
|
-
className: "lks-btn ghost",
|
|
3122
|
-
onClick: () => {
|
|
3123
|
-
send(tr("prompt.proposal.decline", { id: proposal.id }));
|
|
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);
|
|
3124
2804
|
}
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
style: {
|
|
3128
|
-
height: "auto",
|
|
3129
|
-
padding: "4px 0"
|
|
3130
|
-
}
|
|
3131
|
-
}, ...MODES$1.map((mode) => (0, react.createElement)("button", {
|
|
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", {
|
|
3132
2807
|
key: mode.id,
|
|
3133
|
-
className: `
|
|
2808
|
+
className: `lks14-pill${data?.mode === mode.id ? " on" : ""}`,
|
|
3134
2809
|
"aria-pressed": String(data?.mode === mode.id),
|
|
3135
2810
|
"aria-disabled": dormant || void 0,
|
|
3136
2811
|
title: dormant ? tr("tutor.dormant") : tr(mode.hintKey),
|
|
3137
2812
|
onClick: () => {
|
|
3138
2813
|
if (!dormant) fire(setMode(mode.id));
|
|
3139
2814
|
}
|
|
3140
|
-
}, tr(mode.labelKey)))),
|
|
3141
|
-
className: "
|
|
3142
|
-
|
|
3143
|
-
},
|
|
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", {
|
|
3144
2819
|
key: s.label,
|
|
3145
|
-
className: "
|
|
2820
|
+
className: "lks14-starter",
|
|
3146
2821
|
"aria-disabled": dormant || void 0,
|
|
3147
2822
|
title: dormant ? tr("tutor.dormant") : s.message,
|
|
3148
2823
|
onClick: () => {
|
|
3149
2824
|
if (!dormant) send(s.message);
|
|
3150
2825
|
}
|
|
3151
|
-
}, s.label)))
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
if (synth === void 0) {
|
|
3160
|
-
reject(/* @__PURE__ */ new Error("no speechSynthesis"));
|
|
3161
|
-
return;
|
|
3162
|
-
}
|
|
3163
|
-
const utterance = new SpeechSynthesisUtterance(text);
|
|
3164
|
-
utterance.lang = "zh-CN";
|
|
3165
|
-
utterance.onend = () => resolve();
|
|
3166
|
-
utterance.onerror = () => reject(/* @__PURE__ */ new Error("speechSynthesis failed"));
|
|
3167
|
-
synth.speak(utterance);
|
|
3168
|
-
});
|
|
3169
|
-
},
|
|
3170
|
-
pause() {
|
|
3171
|
-
window.speechSynthesis?.pause();
|
|
3172
|
-
},
|
|
3173
|
-
resume() {
|
|
3174
|
-
window.speechSynthesis?.resume();
|
|
3175
|
-
},
|
|
3176
|
-
cancel() {
|
|
3177
|
-
window.speechSynthesis?.cancel();
|
|
3178
|
-
}
|
|
3179
|
-
};
|
|
3180
|
-
}
|
|
3181
|
-
/** Edge-over-dashboard engine: MP3 from /api/tts played through an <audio>, with per-session prefetch. */
|
|
3182
|
-
function audioSpeechEngine(fetchTts) {
|
|
3183
|
-
let audio = null;
|
|
3184
|
-
const prefetch = /* @__PURE__ */ new Map();
|
|
3185
|
-
const load = (text) => {
|
|
3186
|
-
let pending = prefetch.get(text);
|
|
3187
|
-
if (pending === void 0) {
|
|
3188
|
-
pending = fetchTts(text);
|
|
3189
|
-
pending.catch(() => prefetch.delete(text));
|
|
3190
|
-
prefetch.set(text, pending);
|
|
3191
|
-
}
|
|
3192
|
-
return pending;
|
|
3193
|
-
};
|
|
3194
|
-
return {
|
|
3195
|
-
speak(text) {
|
|
3196
|
-
return load(text).then((buf) => new Promise((resolve, reject) => {
|
|
3197
|
-
const blobUrl = URL.createObjectURL(new Blob([buf], { type: "audio/mpeg" }));
|
|
3198
|
-
audio = new Audio(blobUrl);
|
|
3199
|
-
const done = () => {
|
|
3200
|
-
URL.revokeObjectURL(blobUrl);
|
|
3201
|
-
resolve();
|
|
3202
|
-
};
|
|
3203
|
-
audio.onended = done;
|
|
3204
|
-
audio.onerror = () => {
|
|
3205
|
-
URL.revokeObjectURL(blobUrl);
|
|
3206
|
-
reject(/* @__PURE__ */ new Error("audio playback failed"));
|
|
3207
|
-
};
|
|
3208
|
-
audio.play().catch(reject);
|
|
3209
|
-
}));
|
|
3210
|
-
},
|
|
3211
|
-
pause() {
|
|
3212
|
-
audio?.pause();
|
|
3213
|
-
},
|
|
3214
|
-
resume() {
|
|
3215
|
-
audio?.play();
|
|
3216
|
-
},
|
|
3217
|
-
cancel() {
|
|
3218
|
-
audio?.pause();
|
|
3219
|
-
audio = null;
|
|
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);
|
|
3220
2834
|
},
|
|
3221
|
-
|
|
3222
|
-
|
|
2835
|
+
onKeyDown: onComposerKey
|
|
2836
|
+
}), (0, react.createElement)("button", {
|
|
2837
|
+
className: "lks-btn primary",
|
|
2838
|
+
disabled: busy || dormant || draft.trim() === "",
|
|
2839
|
+
onClick: () => {
|
|
2840
|
+
send(draft);
|
|
3223
2841
|
}
|
|
3224
|
-
}
|
|
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);
|
|
3225
2846
|
}
|
|
3226
|
-
/**
|
|
3227
|
-
function
|
|
2847
|
+
/** 右栏:the notebook — 讲解/概念图/笔记 tabs (upstream NotebookPanel arrangement). */
|
|
2848
|
+
function NotebookPane({ data, deleteNote }) {
|
|
3228
2849
|
const lesson = data?.lesson ?? null;
|
|
3229
|
-
const
|
|
3230
|
-
const [pane, setPane] = (0, react.useState)("teach");
|
|
2850
|
+
const [tab, setTab] = (0, react.useState)("teach");
|
|
3231
2851
|
const [error, setError] = (0, react.useState)(null);
|
|
2852
|
+
const [armedNote, setArmedNote] = (0, react.useState)(null);
|
|
3232
2853
|
const [read, setRead] = (0, react.useState)(null);
|
|
3233
2854
|
const readCtl = (0, react.useRef)(null);
|
|
3234
2855
|
const [readError, setReadError] = (0, react.useState)(null);
|
|
3235
|
-
const
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
const sentences = speechSentencesOf(lesson.speechText);
|
|
3239
|
-
const voice = storedTtsVoice();
|
|
3240
|
-
const controller = new ReadAloudController(sentences.map((s) => speakMathInSentence(s)), audioSpeechEngine((text) => tts(text, voice)), systemSpeechEngine(), (s) => {
|
|
3241
|
-
setRead(s);
|
|
3242
|
-
if (s.degraded && s.engine === "system" && !readError) setReadError(tr("read.engine.system"));
|
|
3243
|
-
});
|
|
3244
|
-
readCtl.current = controller;
|
|
3245
|
-
setReadError(null);
|
|
3246
|
-
controller.start().catch((err) => {
|
|
3247
|
-
setReadError(err instanceof Error ? err.message : String(err));
|
|
3248
|
-
});
|
|
3249
|
-
};
|
|
3250
|
-
const stopReading = () => {
|
|
3251
|
-
readCtl.current?.stop();
|
|
3252
|
-
setRead(null);
|
|
3253
|
-
setReadError(null);
|
|
3254
|
-
};
|
|
3255
|
-
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);
|
|
3256
2859
|
(0, react.useEffect)(() => {
|
|
3257
|
-
if (armedNote
|
|
2860
|
+
if (!armedNote) return;
|
|
3258
2861
|
const disarm = () => {
|
|
3259
2862
|
setArmedNote(null);
|
|
3260
2863
|
};
|
|
@@ -3275,46 +2878,130 @@ window.__ModuleLoader__.load({
|
|
|
3275
2878
|
setError(err instanceof Error ? err.message : String(err));
|
|
3276
2879
|
});
|
|
3277
2880
|
};
|
|
3278
|
-
const
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
const
|
|
3283
|
-
|
|
3284
|
-
|
|
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
|
+
};
|
|
3285
2966
|
(0, react.useEffect)(() => {
|
|
3286
|
-
if (
|
|
2967
|
+
if (tab !== "cmap" || diagRef.current === null || lesson === null) return;
|
|
3287
2968
|
const el = diagRef.current;
|
|
3288
2969
|
el.textContent = "";
|
|
3289
2970
|
(lesson.concepts.length === 0 ? Promise.reject(/* @__PURE__ */ new Error("no concepts")) : renderLessonConceptMap(el, lesson.title, lesson.concepts.map((c) => ({
|
|
3290
2971
|
title: c.title,
|
|
3291
2972
|
masteryPct: c.masteryPct
|
|
3292
2973
|
})))).catch((err) => {
|
|
3293
|
-
console.error("lks diagram pane failed:", pane, err);
|
|
3294
2974
|
el.textContent = lesson.concepts.length === 0 ? tr("bb.fallback.cmap.empty") : tr("bb.fallback.cmap");
|
|
3295
2975
|
});
|
|
3296
2976
|
}, [
|
|
3297
|
-
|
|
2977
|
+
tab,
|
|
3298
2978
|
lesson?.lessonId,
|
|
3299
2979
|
lesson?.concepts.length
|
|
3300
2980
|
]);
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
},
|
|
3305
|
-
|
|
3306
|
-
|
|
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"),
|
|
3307
2988
|
onClick: () => {
|
|
3308
|
-
|
|
2989
|
+
setTab("teach");
|
|
3309
2990
|
}
|
|
3310
2991
|
}, tr("viewtab.teach")), (0, react.createElement)("button", {
|
|
3311
|
-
className: `
|
|
3312
|
-
"aria-pressed": String(
|
|
2992
|
+
className: `lks14-viewtab${tab === "cmap" ? " on" : ""}`,
|
|
2993
|
+
"aria-pressed": String(tab === "cmap"),
|
|
3313
2994
|
title: tr("viewtab.cmap.title"),
|
|
3314
2995
|
onClick: () => {
|
|
3315
|
-
|
|
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");
|
|
3316
3003
|
}
|
|
3317
|
-
},
|
|
3004
|
+
}, tr("bb.notes"))), tab === "teach" ? (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "lks-readbar" }, (0, react.createElement)("button", {
|
|
3318
3005
|
className: "lks-btn ghost",
|
|
3319
3006
|
style: {
|
|
3320
3007
|
padding: "3px 8px",
|
|
@@ -3333,7 +3020,7 @@ window.__ModuleLoader__.load({
|
|
|
3333
3020
|
}
|
|
3334
3021
|
startReading();
|
|
3335
3022
|
}
|
|
3336
|
-
},
|
|
3023
|
+
}, read !== null && read.state === "speaking" ? tr("read.pause") : tr("read.play")), read !== null ? (0, react.createElement)("button", {
|
|
3337
3024
|
className: "lks-btn ghost",
|
|
3338
3025
|
style: {
|
|
3339
3026
|
padding: "3px 8px",
|
|
@@ -3342,20 +3029,20 @@ window.__ModuleLoader__.load({
|
|
|
3342
3029
|
},
|
|
3343
3030
|
title: tr("read.stop"),
|
|
3344
3031
|
onClick: stopReading
|
|
3345
|
-
}, 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
|
|
3346
|
-
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",
|
|
3347
3034
|
ref: proseRef,
|
|
3348
3035
|
dangerouslySetInnerHTML: { __html: lesson.html }
|
|
3349
|
-
}) : (0, react.createElement)("div", {
|
|
3350
|
-
className: "
|
|
3036
|
+
})) : tab === "cmap" ? (0, react.createElement)("div", {
|
|
3037
|
+
className: "lks14-prose",
|
|
3351
3038
|
ref: diagRef
|
|
3352
|
-
})
|
|
3353
|
-
className: "
|
|
3039
|
+
}) : (0, react.createElement)("div", { className: "lks14-zones" }, lesson.notes.length === 0 ? (0, react.createElement)("div", {
|
|
3040
|
+
className: "lks14-empty",
|
|
3354
3041
|
style: { padding: "16px 0" }
|
|
3355
3042
|
}, tr("bb.notes.empty")) : ZONES.filter(([zone]) => lesson.notes.some((n) => n.zone === zone)).map(([zone, labelKey]) => (0, react.createElement)("div", {
|
|
3356
3043
|
key: zone,
|
|
3357
|
-
className: "
|
|
3358
|
-
}, (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", {
|
|
3359
3046
|
key: n.id,
|
|
3360
3047
|
className: "lks-note"
|
|
3361
3048
|
}, (0, react.createElement)("span", { className: "lks-note-src" }, n.source), (0, react.createElement)("button", {
|
|
@@ -3375,83 +3062,223 @@ window.__ModuleLoader__.load({
|
|
|
3375
3062
|
className: "lks-note-text",
|
|
3376
3063
|
dangerouslySetInnerHTML: { __html: renderMarkdown(n.text) }
|
|
3377
3064
|
}), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `“${n.quote}”`) : null))))));
|
|
3378
|
-
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));
|
|
3379
3066
|
}
|
|
3380
3067
|
//#endregion
|
|
3381
|
-
//#region src/client/
|
|
3068
|
+
//#region src/client/shell-entry.ts
|
|
3382
3069
|
/**
|
|
3383
|
-
* The
|
|
3384
|
-
*
|
|
3385
|
-
*
|
|
3386
|
-
*
|
|
3387
|
-
*
|
|
3388
|
-
*
|
|
3389
|
-
*
|
|
3390
|
-
*
|
|
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
|
|
3391
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>";
|
|
3392
3141
|
/**
|
|
3393
|
-
*
|
|
3394
|
-
* @param
|
|
3395
|
-
* @
|
|
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.
|
|
3396
3149
|
*/
|
|
3397
|
-
function
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
});
|
|
3150
|
+
function mountStudyShell(view, label, dom, sessions) {
|
|
3151
|
+
if (typeof document === "undefined") return {
|
|
3152
|
+
dispose: () => {},
|
|
3153
|
+
close: () => {},
|
|
3154
|
+
suppressHandBack: (fn) => {
|
|
3155
|
+
fn();
|
|
3156
|
+
}
|
|
3405
3157
|
};
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
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;
|
|
3417
3261
|
try {
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
const { path } = await area.json();
|
|
3422
|
-
const workspace = await ctx.workspaces.create({ path });
|
|
3423
|
-
const sessionId = ctx.workspaces.connectWorkspace !== void 0 ? await ctx.workspaces.connectWorkspace(workspace.workspaceId) : await ctx.sessions.create({ workspaceId: workspace.workspaceId });
|
|
3424
|
-
const actx = ctx.sessions.scope(sessionId);
|
|
3425
|
-
const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
|
|
3426
|
-
if (face === void 0) throw new Error("study session is not addressable yet");
|
|
3427
|
-
const result = await face.prompt([{
|
|
3428
|
-
type: "text",
|
|
3429
|
-
text: tr("prompt.kickoff")
|
|
3430
|
-
}], "queue");
|
|
3431
|
-
if (!result.ok) throw new Error(`kickoff prompt rejected: ${result.error.code}: ${result.error.message}`);
|
|
3432
|
-
ctx.sessions.open(sessionId);
|
|
3433
|
-
} catch (err) {
|
|
3434
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
3435
|
-
setBusy(false);
|
|
3262
|
+
fn();
|
|
3263
|
+
} finally {
|
|
3264
|
+
suppressed = false;
|
|
3436
3265
|
}
|
|
3437
|
-
}
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
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();
|
|
3445
3276
|
}
|
|
3446
|
-
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
}, 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", {
|
|
3452
|
-
className: "lks-propcard-err",
|
|
3453
|
-
style: { marginTop: 0 }
|
|
3454
|
-
}, 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;
|
|
3455
3282
|
}
|
|
3456
3283
|
//#endregion
|
|
3457
3284
|
//#region src/client/settings.tsx
|
|
@@ -3689,14 +3516,9 @@ window.__ModuleLoader__.load({
|
|
|
3689
3516
|
"locale"
|
|
3690
3517
|
];
|
|
3691
3518
|
/**
|
|
3692
|
-
* Register the study surfaces: styles
|
|
3693
|
-
*
|
|
3694
|
-
*
|
|
3695
|
-
* study workspace + session + kickoff prompt in one click. The `lookatstudy`
|
|
3696
|
-
* locale namespace rides the framework locale service (zh/en) and the
|
|
3697
|
-
* client-wide translator is installed before any component renders; both
|
|
3698
|
-
* slot entries declare the namespace so the framework synthesizes their `t`
|
|
3699
|
-
* 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.
|
|
3700
3522
|
* @param ctx - client root context.
|
|
3701
3523
|
*/
|
|
3702
3524
|
function apply(ctx) {
|
|
@@ -3704,19 +3526,12 @@ window.__ModuleLoader__.load({
|
|
|
3704
3526
|
const localeSvc = ctx.locale;
|
|
3705
3527
|
ctx.effect(() => registerStudyLocale(localeSvc), "lookatstudy.locale()");
|
|
3706
3528
|
setStudyTranslator(studyTranslator(localeSvc));
|
|
3707
|
-
ctx
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
}, studyView(ctx)));
|
|
3714
|
-
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
3715
|
-
name: "conversation.input.left",
|
|
3716
|
-
id: "lookatstudy-start",
|
|
3717
|
-
order: 10,
|
|
3718
|
-
locale: "lookatstudy"
|
|
3719
|
-
}, 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()");
|
|
3720
3535
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
3721
3536
|
name: "settings.section",
|
|
3722
3537
|
id: "lookatstudy",
|
|
@@ -3744,9 +3559,8 @@ window.__ModuleLoader__.load({
|
|
|
3744
3559
|
}
|
|
3745
3560
|
//#endregion
|
|
3746
3561
|
exports.apply = apply;
|
|
3562
|
+
exports.feedRows = feedRows;
|
|
3747
3563
|
exports.inject = inject;
|
|
3748
|
-
exports.studyView = studyView;
|
|
3749
|
-
exports.transcriptRows = transcriptRows;
|
|
3750
3564
|
return module.exports;
|
|
3751
3565
|
}
|
|
3752
3566
|
});
|