dsh-plugin-lookatstudy 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1214 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-plugin-lookatstudy",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ //#region src/client/styles.ts
9
+ /**
10
+ * Style injection for the study tab: one `<style>` element of `.lks-*`
11
+ * classes authored against the dsh `--dsw-*` design tokens, so the tab
12
+ * follows the app's theme (light/dark) instead of carrying its own palette.
13
+ * CSS Modules are unavailable to patch-layer bundles, so the stylesheet is
14
+ * injected once at client-plugin apply; the `data-` attribute keeps the
15
+ * injection idempotent.
16
+ * @module dsh-plugin-lookatstudy/client/styles
17
+ */
18
+ /** Marker attribute proving the stylesheet is already in the document. */
19
+ const STYLE_ID = "data-dsh-plugin-lookatstudy";
20
+ /**
21
+ * The study tab's stylesheet. Class names are `lks-*` namespaced; colors,
22
+ * fonts, and radii come from `--dsw-*` tokens wherever the concept exists.
23
+ */
24
+ const CSS = `
25
+ .lks-root{font-family:var(--dsw-font-family)}
26
+ .lks-root :where(button){font-family:var(--dsw-font-family);cursor:pointer;border:none;background:none;padding:0}
27
+ .lks-root :where(a){color:var(--dsw-alias-state-business-primary)}
28
+
29
+ /* ── the study tab: composer-width center, self-scrolling flanks ──
30
+ data-conversation-composer-overlay (set on the root element) opts the
31
+ view into the host's overlay mode: viewArea becomes a fixed-height
32
+ non-growing host and the composer floats over the tab's bottom band, so
33
+ the tab itself never feeds the page scroll. The bottom padding clears the
34
+ floating composer via the host-published live --dsh-composer-height. The
35
+ center strip reuses the conversation column's width axis
36
+ (--dsh-composer-card-max-width inherits from ConversationRoot's root), so
37
+ the tutor transcript is exactly as wide as the native composer; the
38
+ flanking columns absorb the remaining width and scroll independently. */
39
+ .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}
40
+ /* Direction carrier (a container query cannot style the container itself). */
41
+ .lks-body{flex:1;min-height:0;min-width:0;display:flex;gap:16px}
42
+ .lks-col{display:flex;flex-direction:column;min-width:0}
43
+ .lks-colhead{flex:none;font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary);letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px}
44
+ .lks-col-rail{flex:1 1 0;min-width:190px;overflow-y:auto;padding:2px 6px}
45
+ .lks-col-tutor{flex:0 0 auto;width:min(100%,var(--dsh-composer-card-max-width,780px))}
46
+ .lks-col-bb{flex:1 1 0;min-width:230px;overflow-y:auto;padding:2px 6px}
47
+ /* narrow (<1220px): one composer-width pane at a time, chosen by a centered
48
+ segmented pill group (joined buttons in one capsule — button language, not
49
+ tab language, against the host's view tabs right above). */
50
+ .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}
51
+ .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}
52
+ .lks-switch-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
53
+ .lks-switch-btn.on{background:var(--dsw-alias-state-business-primary);color:#fff;font-weight:600}
54
+ @container lksstudy (max-width: 1220px){
55
+ .lks-body{flex-direction:column}
56
+ .lks-switch{display:flex}
57
+ .lks-colhead{display:none}
58
+ .lks-col{flex:1 1 auto;min-height:0;width:auto;overflow-y:auto}
59
+ .lks-col-tutor{width:min(100%,var(--dsh-composer-card-max-width,780px));overflow:hidden;display:flex;flex-direction:column}
60
+ .lks-study[data-pane='rail'] .lks-col-tutor,.lks-study[data-pane='rail'] .lks-col-bb,
61
+ .lks-study[data-pane='tutor'] .lks-col-rail,.lks-study[data-pane='tutor'] .lks-col-bb,
62
+ .lks-study[data-pane='bb'] .lks-col-rail,.lks-study[data-pane='bb'] .lks-col-tutor{display:none}
63
+ }
64
+
65
+ /* ── left column: course rail ── */
66
+ .lks-rail-head{display:flex;align-items:center;gap:6px;margin-bottom:4px}
67
+ .lks-rail-head .lks-rail-select{flex:1;min-width:0;margin-bottom:0}
68
+ .lks-rail-head .lks-btn{padding:3px 8px;font-size:13px}
69
+ .lks-rail-title{font-size:16px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
70
+ .lks-rail-sub{font-size:13px;color:var(--dsw-alias-label-tertiary);margin:4px 0 6px}
71
+ .lks-masterybar{height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-3);overflow:hidden;margin-bottom:8px}
72
+ .lks-masterybar i{display:block;height:100%;background:var(--dsw-alias-state-business-primary);transition:width .3s ease}
73
+ .lks-masterybar.gold i{background:var(--dsw-alias-state-warn-primary)}
74
+ .lks-search{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13.5px;padding:5px 9px;margin-bottom:4px}
75
+ .lks-search:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
76
+ .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(--dsw-alias-state-warn-label);font-size:11px;font-weight:700;margin-right:4px}
77
+ .lks-tag.due{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}
78
+ .lks-import{margin-top:8px}
79
+ .lks-import .lks-inputrow{margin-top:0}
80
+ .lks-inputrow{display:flex;gap:8px;align-items:flex-end;flex:none;margin-top:8px}
81
+ .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}
82
+ .lks-input:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
83
+ .lks-input:disabled{opacity:.55}
84
+ .lks-import-hint{font-size:12.5px;color:var(--dsw-alias-label-tertiary);margin-top:6px;line-height:1.7}
85
+ .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}
86
+ .lks-duebox .lks-due-item{color:var(--dsw-alias-label-secondary);margin-top:4px;display:flex;justify-content:space-between;gap:10px}
87
+ .lks-duebox .lks-over{color:var(--dsw-alias-state-warn-label);flex:none}
88
+
89
+ .lks-sec{font-size:12px;color:var(--dsw-alias-label-tertiary);text-transform:uppercase;letter-spacing:.05em;margin:14px 0 4px}
90
+ .lks-node{display:flex;align-items:center;gap:8px;padding:6px 9px;border-radius:8px;margin:1px 0;cursor:pointer}
91
+ .lks-node:hover{background:var(--dsw-alias-interactive-bg-hover)}
92
+ .lks-node.focus{background:var(--dsw-alias-bg-layer-3);outline:1px solid var(--dsw-alias-border-l2)}
93
+ .lks-node .lks-g{width:18px;text-align:center;flex:none}
94
+ .lks-node .lks-t{flex:1;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
95
+ .lks-node.locked{opacity:.45;cursor:not-allowed}
96
+ .lks-node.locked:hover{background:none}
97
+ .lks-tag{font-size:11px;border-radius:4px;padding:0 4px;flex:none}
98
+ .lks-tag.weak{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}
99
+ .lks-tag.fric{background:var(--dsw-alias-state-error-primary);color:#fff}
100
+ .lks-bar{width:44px;height:4px;border-radius:2px;background:var(--dsw-alias-bg-layer-3);overflow:hidden;flex:none}
101
+ .lks-bar i{display:block;height:100%;background:var(--dsw-alias-state-success-primary)}
102
+ .lks-pct{font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none;width:32px;text-align:right}
103
+
104
+ .lks-empty{color:var(--dsw-alias-label-tertiary);text-align:center;padding:48px 8px;font-size:14px;line-height:2}
105
+
106
+ /* ── middle column: the tutor ── */
107
+ .lks-transcript{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding:4px 2px}
108
+ .lks-msg{max-width:94%;font-size:16px;line-height:1.7;border-radius:12px;padding:8px 12px;word-break:break-word}
109
+ .lks-msg.user{align-self:flex-end;white-space:pre-wrap;background:var(--dsw-alias-state-business-tertiary);color:var(--dsw-alias-label-primary-bluish)}
110
+ .lks-msg.assistant{align-self:flex-start;background:var(--dsw-alias-bg-layer-2)}
111
+ .lks-msg.streaming{opacity:.7}
112
+ .lks-msg.thinking{align-self:flex-start;display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--dsw-alias-label-tertiary);background:none;border-radius:0;padding:2px}
113
+ .lks-msg.tool{align-self:flex-start;font-size:12px;color:var(--dsw-alias-label-tertiary);background:none;border-radius:0;padding:1px 2px}
114
+ .lks-msg.tool.ok{color:var(--dsw-alias-state-success-primary)}
115
+ .lks-msg.tool.bad{color:var(--dsw-alias-state-error-primary)}
116
+ .lks-msg.error{align-self:stretch;color:var(--dsw-alias-state-error-primary);font-size:13px;background:none;border-radius:0;padding:1px 2px}
117
+ /* quiz-taking surface: the tutor's A–D options become clickable answers on the last settled reply */
118
+ .lks-turn{display:flex;flex-direction:column;align-items:flex-start;max-width:94%}
119
+ .lks-quiz{display:flex;flex-direction:column;gap:6px;width:100%;margin:2px 0 4px}
120
+ .lks-opt{display:flex;align-items:baseline;gap:10px;text-align:left;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:8px 14px;font-size:14px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;transition:border-color .12s ease,color .12s ease,background .12s ease}
121
+ .lks-opt:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-state-business-tertiary)}
122
+ .lks-opt:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}
123
+ .lks-optletter{font-weight:700;color:var(--dsw-alias-label-primary-bluish);flex:none}
124
+
125
+ /* soul pills (native tool-row trigger language: 28px transparent, tinted active) */
126
+ .lks-pills{display:inline-flex;align-items:center;height:28px}
127
+ .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}
128
+ .lks-pill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
129
+ .lks-pill:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}
130
+ .lks-pill.on{background:var(--dsw-alias-state-business-tertiary);color:var(--dsw-alias-label-primary-bluish)}
131
+ .lks-pill.on:hover{background:var(--dsw-alias-state-business-tertiary);color:var(--dsw-alias-label-primary-bluish)}
132
+
133
+ /* ic_ds_* glyph carriers + busy spinner (native .8s linear spin) */
134
+ @keyframes lks-spin{to{transform:rotate(360deg)}}
135
+ .lks-spin{animation:lks-spin .8s linear infinite}
136
+
137
+ /* starter chips */
138
+ .lks-dock{display:flex;align-items:center;flex-wrap:wrap;gap:6px;padding:0 2px 6px;flex:none}
139
+ .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}
140
+ .lks-starter:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}
141
+
142
+ /* proposal banner + shared buttons */
143
+ .lks-banner{display:flex;align-items:center;gap:10px;background:var(--dsw-alias-state-warn-tertiary);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 14px;margin:8px 0;font-size:14px;flex:none}
144
+ .lks-banner .lks-why{flex:1;color:var(--dsw-alias-label-secondary)}
145
+ .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}
146
+ .lks-btn.primary{background:var(--dsw-alias-state-business-primary);color:#fff}
147
+ .lks-btn.primary:hover{filter:brightness(1.1)}
148
+ .lks-btn.primary:disabled{opacity:.5;cursor:default;filter:none}
149
+ .lks-btn.ghost{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2)}
150
+ .lks-btn.ghost:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
151
+
152
+ /* ── right column: blackboard ── */
153
+ .lks-lessonhead h2{font-size:18px;margin:0 0 4px;font-weight:700}
154
+ .lks-lessonhead .lks-meta{color:var(--dsw-alias-label-tertiary);font-size:13px;margin-bottom:4px}
155
+ .lks-chips{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
156
+ .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)}
157
+ .lks-chip.weak{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}
158
+
159
+ /* rendered markdown (host-sanitized lesson bodies and assistant replies) */
160
+ .lks-prose{font-size:16px;line-height:1.75}
161
+ .lks-prose h1,.lks-prose h2,.lks-prose h3,.lks-prose h4{margin:14px 0 6px;line-height:1.4}
162
+ .lks-prose p{margin:6px 0}
163
+ .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)}
164
+ .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}
165
+ .lks-prose pre code{background:none;padding:0}
166
+ .lks-prose table{border-collapse:collapse;margin:8px 0}
167
+ .lks-prose th,.lks-prose td{border:1px solid var(--dsw-alias-border-l2);padding:5px 11px;font-size:14px}
168
+ .lks-prose th{background:var(--dsw-alias-bg-layer-2)}
169
+ .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}
170
+ .lks-prose ul,.lks-prose ol{padding-left:22px;margin:6px 0}
171
+ .lks-prose img{max-width:100%}
172
+
173
+ /* Cornell notes zones */
174
+ .lks-bb-notes{margin-top:14px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}
175
+ .lks-zone{margin-bottom:14px}
176
+ .lks-zone h4{font-size:14px;color:var(--dsw-alias-label-secondary);margin:0 0 8px;font-weight:600}
177
+ .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}
178
+ .lks-note .lks-note-src{float:right;font-size:10px;color:var(--dsw-alias-label-tertiary)}
179
+ .lks-note .lks-note-title{font-weight:600;font-size:13px}
180
+ .lks-note .lks-note-text{margin-top:4px;white-space:pre-wrap;font-size:13px;color:var(--dsw-alias-label-secondary)}
181
+ .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}
182
+
183
+ /* inline error text (write-action failures) */
184
+ .lks-propcard-err{color:var(--dsw-alias-state-error-primary);font-size:12px;margin-top:6px;flex:none}
185
+ `;
186
+ /**
187
+ * Inject the study stylesheet into `document.head` (idempotent).
188
+ * @returns the stylesheet element (existing one when already injected).
189
+ */
190
+ function ensureStudyStyles() {
191
+ const existing = document.head.querySelector(`style[${STYLE_ID}]`);
192
+ if (existing !== null) return existing;
193
+ const style = document.createElement("style");
194
+ style.setAttribute(STYLE_ID, "");
195
+ style.textContent = CSS;
196
+ document.head.appendChild(style);
197
+ return style;
198
+ }
199
+ //#endregion
200
+ //#region src/client/icons.tsx
201
+ /**
202
+ * ic_ds_* glyphs vendored from @deepseek-ai/dsh-client-ui-primitives
203
+ * (src/icons/index.tsx, same figma source as the host's icon set) so the
204
+ * plugin's client bundle stays self-contained per the dsh client-bundle
205
+ * contract — only react and dsh-client-runtime/client are runtime-external;
206
+ * importing the host package root would drag its CSS modules into the
207
+ * inlined bundle. Re-sync these three paths if the host icon set redesigns
208
+ * them; scripts/verify.mjs pins the path data.
209
+ * @module dsh-plugin-lookatstudy/client/icons
210
+ */
211
+ /** ic_ds_think_outline_16 */
212
+ function IconThinkOutline16({ size = 16, className }) {
213
+ return (0, react.createElement)("svg", {
214
+ width: size,
215
+ height: size,
216
+ className,
217
+ viewBox: "0 0 16 16",
218
+ fill: "none",
219
+ xmlns: "http://www.w3.org/2000/svg"
220
+ }, (0, react.createElement)("path", {
221
+ 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",
222
+ fill: "currentColor"
223
+ }), (0, react.createElement)("path", {
224
+ "fill-rule": "evenodd",
225
+ "clip-rule": "evenodd",
226
+ 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",
227
+ fill: "currentColor"
228
+ }));
229
+ }
230
+ /** ic_ds_loading_outline_16 */
231
+ function IconLoadingOutline16({ size = 16, className }) {
232
+ return (0, react.createElement)("svg", {
233
+ width: size,
234
+ height: size,
235
+ className,
236
+ viewBox: "0 0 16 16",
237
+ fill: "none",
238
+ xmlns: "http://www.w3.org/2000/svg"
239
+ }, (0, react.createElement)("path", {
240
+ d: "M2.871 13.1286C0.0387669 10.2962 0.0387669 5.70383 2.871 2.87141C5.70341 0.0390029 10.2957 0.0391154 13.1282 2.87141L12.1387 3.86094C9.85292 1.57538 6.1469 1.57596 3.86123 3.86163C1.57573 6.14732 1.57573 9.85269 3.86123 12.1384C6.1469 14.424 9.85292 14.4246 12.1387 12.1391L13.1282 13.1286C10.2957 15.9609 5.70341 15.961 2.871 13.1286Z",
241
+ fill: "currentColor"
242
+ }));
243
+ }
244
+ /** ic_ds_download_outline_16 */
245
+ function IconDownloadOutline16({ size = 16, className }) {
246
+ return (0, react.createElement)("svg", {
247
+ width: size,
248
+ height: size,
249
+ className,
250
+ viewBox: "0 0 16 16",
251
+ fill: "none",
252
+ xmlns: "http://www.w3.org/2000/svg"
253
+ }, (0, react.createElement)("path", {
254
+ d: "M15.3695 11.411L15.1234 12.8866C14.8869 14.3042 13.6603 15.3436 12.223 15.3436H3.77673C2.33958 15.3434 1.1128 14.3042 0.876343 12.8866L0.630249 11.411L2.05408 11.1747L2.29919 12.6493C2.41973 13.3713 3.04475 13.9001 3.77673 13.9003H12.223C12.9551 13.9002 13.58 13.3713 13.7006 12.6493L13.9457 11.1747L15.3695 11.411ZM8.72205 8.994C8.77717 8.93934 8.83792 8.88106 8.90271 8.81627L12.4828 5.23424L13.5043 6.25572L9.92224 9.8358C9.6395 10.1185 9.38763 10.3732 9.15857 10.5575C8.91892 10.7503 8.63953 10.9224 8.2865 10.9784C8.09711 11.0083 7.90363 11.0083 7.71423 10.9784C7.36106 10.9224 7.0809 10.7503 6.84119 10.5575C6.61215 10.3732 6.36022 10.1185 6.07751 9.8358L2.49646 6.25572L3.51697 5.23424L7.09705 8.81627C7.16219 8.88142 7.22331 8.94006 7.27869 8.99498V1.3065H8.72205V8.994Z",
255
+ fill: "currentColor"
256
+ }));
257
+ }
258
+ //#endregion
259
+ //#region src/client/data.ts
260
+ /**
261
+ * Shared client-side study store: one module-level poller over the host
262
+ * plugin's `/lookatstudy/api/state` endpoint, plus the three write actions
263
+ * (focus, mode, reverse message). Every seat component binds through
264
+ * {@link useStudy}; subscribers share the single 3 s poll cycle and the last
265
+ * snapshot, so mounting more controls costs no extra requests.
266
+ * @module dsh-plugin-lookatstudy/client/data
267
+ */
268
+ /** Poll cadence for the shared store; one cycle serves every mounted seat. */
269
+ const POLL_MS = 3e3;
270
+ async function fetchJson(url, init) {
271
+ const res = await fetch(url, init);
272
+ const body = await res.json().catch(() => null);
273
+ if (!res.ok) {
274
+ const error = body !== null && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `HTTP ${res.status}`;
275
+ throw new Error(error);
276
+ }
277
+ return body;
278
+ }
279
+ var StudyStore = class {
280
+ snapshot = null;
281
+ listeners = /* @__PURE__ */ new Set();
282
+ timer;
283
+ inflight = false;
284
+ subscribe = (listener) => {
285
+ this.listeners.add(listener);
286
+ if (this.listeners.size === 1) {
287
+ this.tick();
288
+ this.timer = setInterval(() => {
289
+ this.tick();
290
+ }, POLL_MS);
291
+ }
292
+ return () => {
293
+ this.listeners.delete(listener);
294
+ if (this.listeners.size === 0 && this.timer !== void 0) {
295
+ clearInterval(this.timer);
296
+ this.timer = void 0;
297
+ }
298
+ };
299
+ };
300
+ getSnapshot = () => this.snapshot;
301
+ emit() {
302
+ for (const listener of this.listeners) listener();
303
+ }
304
+ async tick() {
305
+ if (this.inflight) return;
306
+ this.inflight = true;
307
+ try {
308
+ const next = await fetchJson("/lookatstudy/api/state");
309
+ this.snapshot = next;
310
+ this.emit();
311
+ } catch {} finally {
312
+ this.inflight = false;
313
+ }
314
+ }
315
+ /** One immediate re-read (after a write action). */
316
+ refresh() {
317
+ this.tick();
318
+ }
319
+ /** Switch the tutor's soul mode directly (host persists and applies it on the next request). */
320
+ async setMode(mode) {
321
+ await fetchJson("/lookatstudy/api/mode", {
322
+ method: "POST",
323
+ headers: { "content-type": "application/json" },
324
+ body: JSON.stringify({ mode })
325
+ });
326
+ this.refresh();
327
+ }
328
+ /** Point the tutor's focus at one lesson. */
329
+ async setFocus(lessonId) {
330
+ await fetchJson("/lookatstudy/api/focus", {
331
+ method: "POST",
332
+ headers: { "content-type": "application/json" },
333
+ body: JSON.stringify({ lessonId })
334
+ });
335
+ this.refresh();
336
+ }
337
+ /** Record the dsh session backing one lesson (the simplified thread system). */
338
+ async bindLessonSession(lessonId, sessionId) {
339
+ await fetchJson("/lookatstudy/api/lesson-session", {
340
+ method: "POST",
341
+ headers: { "content-type": "application/json" },
342
+ body: JSON.stringify({
343
+ lessonId,
344
+ sessionId
345
+ })
346
+ });
347
+ this.refresh();
348
+ }
349
+ /** Delete one course (and its proposals) from the host state. */
350
+ async deleteCourse(courseId) {
351
+ await fetchJson("/lookatstudy/api/course/delete", {
352
+ method: "POST",
353
+ headers: { "content-type": "application/json" },
354
+ body: JSON.stringify({ courseId })
355
+ });
356
+ this.refresh();
357
+ }
358
+ };
359
+ /** The one shared store instance backing every study seat component. */
360
+ const studyStore = new StudyStore();
361
+ /** React binding: live snapshot plus the write actions. */
362
+ function useStudy() {
363
+ return {
364
+ data: (0, react.useSyncExternalStore)(studyStore.subscribe, studyStore.getSnapshot, studyStore.getSnapshot),
365
+ setMode: studyStore.setMode.bind(studyStore),
366
+ setFocus: studyStore.setFocus.bind(studyStore),
367
+ deleteCourse: studyStore.deleteCourse.bind(studyStore),
368
+ bindLessonSession: studyStore.bindLessonSession.bind(studyStore)
369
+ };
370
+ }
371
+ //#endregion
372
+ //#region src/markdown.ts
373
+ /**
374
+ * Server-side markdown → HTML for the study workbench's 讲解 view. Escapes
375
+ * every HTML character first, then renders a pragmatic GFM subset (headings,
376
+ * fenced code, inline code, bold/italic, links, lists, blockquotes, tables,
377
+ * hr, paragraphs). Lesson bodies are imported teaching material, so raw HTML
378
+ * never passes through.
379
+ * @module dsh-plugin-lookatstudy/markdown
380
+ */
381
+ /** Escape all HTML-significant characters. */
382
+ function escapeHtml(text) {
383
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
384
+ }
385
+ /** Render inline markup (code, bold, italic, links) over escaped text. */
386
+ function inline(escaped) {
387
+ return escaped.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noreferrer\">$1</a>");
388
+ }
389
+ /** True when the line opens a GFM table row (pipes with a delimiter row next). */
390
+ function isTableRow(line) {
391
+ return line.trim().startsWith("|") && line.trim().endsWith("|") && line.includes("|", 1);
392
+ }
393
+ function isDelimiterRow(line) {
394
+ const trimmed = line.trim();
395
+ if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) return false;
396
+ const cells = trimmed.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
397
+ return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));
398
+ }
399
+ /** Split a table row into trimmed cells (leading/trailing pipes removed). */
400
+ function rowCells(line) {
401
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
402
+ }
403
+ /**
404
+ * Render markdown text to sanitized HTML.
405
+ * @param md - markdown source (lesson body).
406
+ * @returns HTML string safe to inject into the page.
407
+ */
408
+ function renderMarkdown(md) {
409
+ const lines = escapeHtml(md).split(/\r?\n/);
410
+ const out = [];
411
+ let i = 0;
412
+ const flushParagraph = (buffer) => {
413
+ if (buffer.length > 0) out.push(`<p>${inline(buffer.join(" "))}</p>`);
414
+ };
415
+ let paragraph = [];
416
+ while (i < lines.length) {
417
+ const line = lines[i];
418
+ const fence = /^```(\w*)\s*$/.exec(line.trim());
419
+ if (fence) {
420
+ flushParagraph(paragraph);
421
+ paragraph = [];
422
+ const lang = fence[1] ?? "";
423
+ const code = [];
424
+ i++;
425
+ while (i < lines.length && lines[i].trim() !== "```") {
426
+ code.push(lines[i]);
427
+ i++;
428
+ }
429
+ i++;
430
+ out.push(`<pre><code${lang === "" ? "" : ` class="lang-${lang}"`}>${code.join("\n")}</code></pre>`);
431
+ continue;
432
+ }
433
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
434
+ if (heading) {
435
+ flushParagraph(paragraph);
436
+ paragraph = [];
437
+ const level = heading[1].length;
438
+ out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
439
+ i++;
440
+ continue;
441
+ }
442
+ if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
443
+ flushParagraph(paragraph);
444
+ paragraph = [];
445
+ out.push("<hr>");
446
+ i++;
447
+ continue;
448
+ }
449
+ if (/^\s*&gt;\s?/.test(line)) {
450
+ flushParagraph(paragraph);
451
+ paragraph = [];
452
+ const quote = [];
453
+ while (i < lines.length && /^\s*&gt;\s?/.test(lines[i])) {
454
+ quote.push(lines[i].replace(/^\s*&gt;\s?/, ""));
455
+ i++;
456
+ }
457
+ out.push(`<blockquote><p>${inline(quote.join(" "))}</p></blockquote>`);
458
+ continue;
459
+ }
460
+ if (/^\s*[-*+]\s+/.test(line)) {
461
+ flushParagraph(paragraph);
462
+ paragraph = [];
463
+ const items = [];
464
+ while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i])) {
465
+ items.push(`<li>${inline(lines[i].replace(/^\s*[-*+]\s+/, ""))}</li>`);
466
+ i++;
467
+ }
468
+ out.push(`<ul>${items.join("")}</ul>`);
469
+ continue;
470
+ }
471
+ if (/^\s*\d+\.\s+/.test(line)) {
472
+ flushParagraph(paragraph);
473
+ paragraph = [];
474
+ const items = [];
475
+ while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
476
+ items.push(`<li>${inline(lines[i].replace(/^\s*\d+\.\s+/, ""))}</li>`);
477
+ i++;
478
+ }
479
+ out.push(`<ol>${items.join("")}</ol>`);
480
+ continue;
481
+ }
482
+ if (isTableRow(line) && i + 1 < lines.length && isDelimiterRow(lines[i + 1])) {
483
+ flushParagraph(paragraph);
484
+ paragraph = [];
485
+ const headers = rowCells(line);
486
+ i += 2;
487
+ const rows = [];
488
+ while (i < lines.length && isTableRow(lines[i])) {
489
+ const cells = rowCells(lines[i]);
490
+ rows.push(`<tr>${cells.map((c) => `<td>${inline(c)}</td>`).join("")}</tr>`);
491
+ i++;
492
+ }
493
+ out.push(`<table><thead><tr>${headers.map((h) => `<th>${inline(h)}</th>`).join("")}</tr></thead><tbody>${rows.join("")}</tbody></table>`);
494
+ continue;
495
+ }
496
+ if (line.trim() === "") {
497
+ flushParagraph(paragraph);
498
+ paragraph = [];
499
+ i++;
500
+ continue;
501
+ }
502
+ paragraph.push(line.trim());
503
+ i++;
504
+ }
505
+ flushParagraph(paragraph);
506
+ return out.join("\n");
507
+ }
508
+ //#endregion
509
+ //#region src/client/views.tsx
510
+ /**
511
+ * The study tab: one `conversation.view` entry rendering the whole plugin —
512
+ * a simplified LookatStudy inside dsh, three columns in the tab. Left: the
513
+ * course map (selector, tree, due box, one-click demo import). Middle: the
514
+ * tutor conversation — a read-only mini transcript folded from the session
515
+ * snapshot (user text, assistant text rendered through the plugin's own
516
+ * markdown pipeline, tool chips) plus the soul pills, starters, pending
517
+ * proposal banner, and a reverse-channel input. Right: the blackboard —
518
+ * focus-lesson 讲解 and the Cornell 笔记 zones. Nothing outside this tab
519
+ * touches dsh chrome.
520
+ * @module dsh-plugin-lookatstudy/client/views
521
+ */
522
+ /** The three souls, in pill order (labels from LookatStudy's mode switcher). */
523
+ const MODES = [
524
+ {
525
+ id: "direct",
526
+ label: "直讲",
527
+ hint: "direct 精讲:先讲清楚,再确认懂没懂"
528
+ },
529
+ {
530
+ id: "guide",
531
+ label: "引导",
532
+ hint: "guide 引导:让你自己往前推一步,导师递台阶"
533
+ },
534
+ {
535
+ id: "practice",
536
+ label: "实战",
537
+ hint: "practice 实战:在真实世界的乱问题里学"
538
+ }
539
+ ];
540
+ /** Zone labels for the Cornell notebook (understand / record / practice). */
541
+ const ZONES = [
542
+ ["understand", "🧠 理解区 — 知识结构"],
543
+ ["record", "📝 记录区 — 我的话"],
544
+ ["practice", "✍️ 练习区 — 答题日志"]
545
+ ];
546
+ /** Status glyph for one lesson row (LookatStudy's map icons). */
547
+ function glyph(kind, status) {
548
+ if (kind === "exam") return "🎯";
549
+ if (status === "mastered") return "👑";
550
+ if (status === "in_progress") return "📖";
551
+ if (status === "available") return "⭐";
552
+ return "🔒";
553
+ }
554
+ /** LookatStudy's exam gate: an exam node opens only when every sibling study lesson reached mastery ≥50%. */
555
+ function examOpen(lessons) {
556
+ return lessons.every((l) => l.kind !== "study" || (l.masteryPct ?? 0) >= 50);
557
+ }
558
+ /** Multi-keyword AND title filter (LookatStudy course-tree-filter). */
559
+ function titleMatches(title, query) {
560
+ return query.trim().toLowerCase().split(/\s+/).filter(Boolean).every((k) => title.toLowerCase().includes(k));
561
+ }
562
+ /** The rail's import row: a GitHub URL input plus the paste/folder hints. */
563
+ function ImportRow({ send }) {
564
+ const [url, setUrl] = (0, react.useState)("");
565
+ return (0, react.createElement)("div", { className: "lks-import" }, (0, react.createElement)("div", { className: "lks-inputrow" }, (0, react.createElement)("input", {
566
+ className: "lks-input",
567
+ type: "url",
568
+ placeholder: "GitHub 仓库链接,如 microsoft/AI-For-Beginners",
569
+ value: url,
570
+ onChange: (e) => {
571
+ setUrl(e.target.value);
572
+ },
573
+ onKeyDown: (e) => {
574
+ if (e.key === "Enter" && url.trim() !== "") send(`导入课程:用 study_import_github 抓取 ${url.trim()}`);
575
+ }
576
+ }), (0, react.createElement)("button", {
577
+ className: "lks-btn primary",
578
+ disabled: url.trim() === "",
579
+ onClick: () => {
580
+ send(`导入课程:用 study_import_github 抓取 ${url.trim()}`);
581
+ }
582
+ }, "导入")), (0, react.createElement)("div", { className: "lks-import-hint" }, "粘贴 markdown → 说「导入为课程」", (0, react.createElement)("br"), "本地文件夹 → 说「导入 D:/path/to/folder」"));
583
+ }
584
+ /**
585
+ * Tooltip text for one course-tree status glyph. Pure.
586
+ * @param kind - lesson kind ('study' | 'practice' | 'exam').
587
+ * @param status - lesson status ('locked' | 'available' | 'in_progress' | 'mastered').
588
+ */
589
+ function statusTitle(kind, status) {
590
+ if (kind === "exam") return "章节测验:本节全部课时掌握度 ≥50% 后开放";
591
+ switch (status) {
592
+ case "mastered": return "已毕业:掌握度 ≥90%(或你接受了掌握提案)";
593
+ case "in_progress": return "学习中:已开课,掌握度从 50% 起步";
594
+ case "available": return "可开始:已解锁,尚未学习";
595
+ default: return "未解锁:先完成前面的课时";
596
+ }
597
+ }
598
+ /**
599
+ * Extract the last quiz option block (2–4 consecutive `A. …` `B. …` lines,
600
+ * letters strictly consecutive from A) from an assistant reply — the raw text
601
+ * of the tutor's question becomes the clickable answer surface. Pure.
602
+ * @param text - one assistant reply's markdown.
603
+ * @returns the options of the last valid block, or [] when none.
604
+ */
605
+ function quizOptions(text) {
606
+ const runs = [];
607
+ let run = [];
608
+ const flush = () => {
609
+ if (run.length >= 2) runs.push(run);
610
+ run = [];
611
+ };
612
+ for (const line of text.split("\n")) {
613
+ const match = /^([A-D])[.、:)]\s+(.+)$/.exec(line.trim());
614
+ if (match === null) {
615
+ flush();
616
+ continue;
617
+ }
618
+ const nextLetter = run.length === 0 ? "A" : String.fromCharCode(run[run.length - 1].letter.charCodeAt(0) + 1);
619
+ if (match[1] === nextLetter) run.push({
620
+ letter: match[1],
621
+ text: match[2]
622
+ });
623
+ else {
624
+ flush();
625
+ if (match[1] === "A") run.push({
626
+ letter: match[1],
627
+ text: match[2]
628
+ });
629
+ }
630
+ }
631
+ flush();
632
+ return runs.at(-1) ?? [];
633
+ }
634
+ /**
635
+ * Learning-aware label for one settled tool call; null keeps the generic
636
+ * 🔧-name chip. Pure.
637
+ * @param name - tool call name.
638
+ * @param argsRaw - the call's raw JSON args.
639
+ */
640
+ function toolChipLabel(name, argsRaw) {
641
+ if (name === "study_record_answer") {
642
+ let concept = "未归因";
643
+ let correct = false;
644
+ try {
645
+ const args = JSON.parse(argsRaw);
646
+ if (typeof args.concept === "string" && args.concept !== "") concept = args.concept;
647
+ correct = args.correct === true;
648
+ } catch {}
649
+ return correct ? {
650
+ label: `✓ 答对 · ${concept}`,
651
+ tone: "ok"
652
+ } : {
653
+ label: `✗ 答错 · ${concept}`,
654
+ tone: "bad"
655
+ };
656
+ }
657
+ if (name === "study_import_github" || name === "study_import_markdown" || name === "study_import_folder") return {
658
+ label: "📦 导入课程",
659
+ tone: "ok"
660
+ };
661
+ if (name === "study_define_concepts") return {
662
+ label: "🧠 提炼知识点",
663
+ tone: "ok"
664
+ };
665
+ return null;
666
+ }
667
+ function textOf(blocks) {
668
+ return blocks.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n\n").trim();
669
+ }
670
+ function assistantText(blocks) {
671
+ return blocks.filter((b) => b.kind === "text").map((b) => b.text).join("\n\n").trim();
672
+ }
673
+ /**
674
+ * Fold conversation nodes (plus the streaming partial) into render rows;
675
+ * tool activity condenses to one muted chip per settled call.
676
+ * @param nodes - finalized conversation nodes from the snapshot.
677
+ * @param partial - the in-flight assistant partial, or null.
678
+ * @returns ordered rows; never mutates its inputs.
679
+ */
680
+ function transcriptRows(nodes, partial) {
681
+ const rows = [];
682
+ for (const node of nodes) switch (node.kind) {
683
+ case "user":
684
+ case "steering": {
685
+ const text = textOf(node.content);
686
+ if (text !== "") rows.push({
687
+ key: `u${node.seq}`,
688
+ role: "user",
689
+ text
690
+ });
691
+ break;
692
+ }
693
+ case "assistant": {
694
+ const text = assistantText(node.blocks);
695
+ if (text !== "") rows.push({
696
+ key: `a${node.seq}`,
697
+ role: "assistant",
698
+ text
699
+ });
700
+ break;
701
+ }
702
+ case "tool-result":
703
+ rows.push({
704
+ key: `t${node.seq}`,
705
+ role: "tool",
706
+ text: node.call?.name ?? node.callId,
707
+ call: node.call
708
+ });
709
+ break;
710
+ case "turn-error":
711
+ rows.push({
712
+ key: `e${node.seq}`,
713
+ role: "error",
714
+ text: node.message
715
+ });
716
+ break;
717
+ default: break;
718
+ }
719
+ if (partial !== null) {
720
+ const text = assistantText(partial.blocks);
721
+ if (text !== "") rows.push({
722
+ key: "streaming",
723
+ role: "streaming",
724
+ text
725
+ });
726
+ else rows.push({
727
+ key: "thinking",
728
+ role: "thinking",
729
+ text: "导师思考中…"
730
+ });
731
+ }
732
+ return rows;
733
+ }
734
+ /** Small inline error surface for failed write actions. */
735
+ function ActionError({ error }) {
736
+ if (error === null) return null;
737
+ return (0, react.createElement)("div", { className: "lks-propcard-err" }, error);
738
+ }
739
+ /** sessionStorage key keeping the narrow-mode pane across view remounts (tab switches) and reloads. */
740
+ const PANE_KEY = "dsh-plugin-lookatstudy:pane";
741
+ /** Narrow the stored value back to a pane id; anything else (or nothing stored) falls back to 导师. Pure, unit-testable. */
742
+ function pickPane(stored) {
743
+ return stored === "rail" || stored === "tutor" || stored === "bb" ? stored : "tutor";
744
+ }
745
+ /** Read the persisted pane; the try swallows only the SecurityError privacy modes throw on storage access — the fallback is the default pane. */
746
+ function storedPane() {
747
+ try {
748
+ return pickPane(sessionStorage.getItem(PANE_KEY));
749
+ } catch {
750
+ return "tutor";
751
+ }
752
+ }
753
+ /** The whole study tab. Wide = three columns (课程 | 导师 | 黑板); narrow (<1220px) = one composer-width pane with a three-way switcher. */
754
+ function studyView(ctx) {
755
+ return function StudyView(props) {
756
+ return (0, react.createElement)(StudyTab, {
757
+ ...props,
758
+ ctx,
759
+ key: "tab"
760
+ });
761
+ };
762
+ }
763
+ /** Tab body: the factory-bound ctx carries workspaces/sessions for the per-lesson session jumps. */
764
+ function StudyTab({ useSession, inputActions, ctx }) {
765
+ const { data, setMode, setFocus, deleteCourse, bindLessonSession } = useStudy();
766
+ const snapshot = useSession((s) => s);
767
+ const [pane, setPane] = (0, react.useState)(storedPane);
768
+ const send = (text) => {
769
+ inputActions.setDraft(text);
770
+ inputActions.submit();
771
+ };
772
+ return (0, react.createElement)("div", {
773
+ className: "lks-root lks-study",
774
+ "data-conversation-composer-overlay": "",
775
+ "data-pane": pane
776
+ }, (0, react.createElement)("div", { className: "lks-body" }, (0, react.createElement)("div", { className: "lks-switch" }, ...[
777
+ {
778
+ id: "rail",
779
+ label: "课程"
780
+ },
781
+ {
782
+ id: "tutor",
783
+ label: "导师"
784
+ },
785
+ {
786
+ id: "bb",
787
+ label: "黑板"
788
+ }
789
+ ].map((p) => (0, react.createElement)("button", {
790
+ key: p.id,
791
+ className: `lks-switch-btn${pane === p.id ? " on" : ""}`,
792
+ onClick: () => {
793
+ setPane(p.id);
794
+ try {
795
+ sessionStorage.setItem(PANE_KEY, p.id);
796
+ } catch {}
797
+ }
798
+ }, p.label))), (0, react.createElement)(CourseRail, {
799
+ data,
800
+ setFocus,
801
+ deleteCourse,
802
+ bindLessonSession,
803
+ send,
804
+ ctx,
805
+ currentSessionId: snapshot.sessionId
806
+ }), (0, react.createElement)(TutorColumn, {
807
+ data,
808
+ setMode,
809
+ send,
810
+ snapshot
811
+ }), (0, react.createElement)(BlackboardColumn, { data })));
812
+ }
813
+ /** Left column: course management (pick/delete/search/import), lesson tree, due box. */
814
+ function CourseRail({ data, setFocus, deleteCourse, bindLessonSession, send, ctx, currentSessionId }) {
815
+ const [jumping, setJumping] = (0, react.useState)(null);
816
+ /** Open (or mint) the lesson's own session — the simplified thread system. */
817
+ const openLessonThread = (lesson) => {
818
+ if (jumping !== null) return;
819
+ const mapped = data?.lessonSessions[lesson.id];
820
+ if (mapped === currentSessionId) {
821
+ setFocus(lesson.id).catch(reportError);
822
+ return;
823
+ }
824
+ if (mapped !== void 0 && ctx.sessions.binding(mapped) !== void 0) {
825
+ setFocus(lesson.id).catch(reportError);
826
+ ctx.sessions.open(mapped);
827
+ return;
828
+ }
829
+ setJumping(lesson.id);
830
+ setFocus(lesson.id).then(() => (async () => {
831
+ const area = await fetch("/lookatstudy/api/study-workspace");
832
+ if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
833
+ const { path } = await area.json();
834
+ const workspace = await ctx.workspaces.create({ path });
835
+ const sessionId = await ctx.workspaces.connectWorkspace(workspace.workspaceId);
836
+ const actx = ctx.sessions.scope(sessionId);
837
+ const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
838
+ if (face === void 0) throw new Error("lesson session is not addressable yet");
839
+ const result = await face.prompt([{
840
+ type: "text",
841
+ text: `学习「${lesson.title}」:用 study_lesson 打开这一课开始学习。`
842
+ }], "queue");
843
+ if (!result.ok) throw new Error(`lesson prompt rejected: ${result.error.code}: ${result.error.message}`);
844
+ await bindLessonSession(lesson.id, sessionId);
845
+ ctx.sessions.open(sessionId);
846
+ })()).then(() => {
847
+ setJumping(null);
848
+ }, (err) => {
849
+ reportError(err);
850
+ setJumping(null);
851
+ });
852
+ };
853
+ const [selectedCourse, setSelectedCourse] = (0, react.useState)("");
854
+ const [query, setQuery] = (0, react.useState)("");
855
+ const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
856
+ const [showImport, setShowImport] = (0, react.useState)(false);
857
+ const [error, setError] = (0, react.useState)(null);
858
+ const reportError = (err) => {
859
+ setError(err instanceof Error ? err.message : String(err));
860
+ };
861
+ const body = data === null ? (0, react.createElement)("div", { className: "lks-empty" }, "加载中…") : data.courses.length === 0 ? (0, react.createElement)("div", { className: "lks-empty" }, "暂无课程", (0, react.createElement)("br"), (0, react.createElement)("button", {
862
+ className: "lks-btn primary",
863
+ style: { margin: "10px 0" },
864
+ onClick: () => {
865
+ send("导入课程:用 study_import_github 抓取 https://github.com/microsoft/AI-For-Beginners");
866
+ }
867
+ }, (0, react.createElement)(IconDownloadOutline16, null), "导入示例课程"), (0, react.createElement)(ImportRow, { send })) : (() => {
868
+ const courseId = data.courses.some((c) => c.courseId === selectedCourse) ? selectedCourse : data.courses[0].courseId;
869
+ const course = data.courses.find((c) => c.courseId === courseId);
870
+ return (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "lks-rail-head" }, data.courses.length > 1 ? (0, react.createElement)("select", {
871
+ className: "lks-rail-select",
872
+ value: courseId,
873
+ onChange: (e) => {
874
+ setSelectedCourse(e.target.value);
875
+ setConfirmDelete(false);
876
+ }
877
+ }, ...data.courses.map((c) => (0, react.createElement)("option", {
878
+ key: c.courseId,
879
+ value: c.courseId
880
+ }, c.title))) : (0, react.createElement)("div", { className: "lks-rail-title" }, course.title), (0, react.createElement)("button", {
881
+ className: `lks-btn ${confirmDelete ? "primary" : "ghost"}`,
882
+ title: confirmDelete ? "再点一次确认删除(含全部进度与笔记)" : "删除本课程",
883
+ onClick: () => {
884
+ if (!confirmDelete) {
885
+ setConfirmDelete(true);
886
+ return;
887
+ }
888
+ setConfirmDelete(false);
889
+ deleteCourse(courseId).then(() => {
890
+ setSelectedCourse("");
891
+ }, reportError);
892
+ }
893
+ }, confirmDelete ? "确认删除?" : "🗑")), (0, react.createElement)("div", { className: "lks-rail-sub" }, `${course.mastered}/${course.total} 已掌握`), (0, react.createElement)("div", {
894
+ className: `lks-masterybar${course.avgMasteryPct === 100 ? " gold" : ""}`,
895
+ title: course.avgMasteryPct === null ? "尚无掌握度数据" : `平均掌握度 ${course.avgMasteryPct}%`
896
+ }, (0, react.createElement)("i", { style: { width: `${course.avgMasteryPct ?? 0}%` } })), (0, react.createElement)("input", {
897
+ className: "lks-search",
898
+ type: "search",
899
+ placeholder: "搜索课时…(多关键词空格分隔)",
900
+ value: query,
901
+ onChange: (e) => {
902
+ setQuery(e.target.value);
903
+ },
904
+ onKeyDown: (e) => {
905
+ if (e.key !== "Enter" || query.trim() === "") return;
906
+ for (const section of course.sections) {
907
+ const hit = section.lessons.find((l) => l.kind !== "exam" && l.status !== "locked" && titleMatches(l.title, query));
908
+ if (hit !== void 0) {
909
+ setFocus(hit.id).catch(reportError);
910
+ return;
911
+ }
912
+ }
913
+ }
914
+ }), data.dueCount > 0 ? (0, react.createElement)("div", { className: "lks-duebox" }, `🔁 待复习 ${data.dueCount}`, ...data.due.map((d) => (0, react.createElement)("div", {
915
+ key: d.lessonId,
916
+ className: "lks-due-item"
917
+ }, (0, react.createElement)("span", null, d.lessonTitle), d.overdueDays > 0 ? (0, react.createElement)("span", { className: "lks-over" }, `超${d.overdueDays}天`) : null)), (0, react.createElement)("button", {
918
+ className: "lks-btn ghost",
919
+ style: { marginTop: "6px" },
920
+ onClick: () => {
921
+ send("开始今天的复习,从最到期的课时开始");
922
+ }
923
+ }, "开始复习")) : null, ...course.sections.flatMap((section) => {
924
+ const examAllowed = examOpen(section.lessons);
925
+ const lessons = section.lessons.filter((l) => query.trim() === "" || titleMatches(l.title, query) || l.focus);
926
+ if (lessons.length === 0) return [];
927
+ return [(0, react.createElement)("div", {
928
+ key: section.title,
929
+ className: "lks-sec"
930
+ }, (0, react.createElement)("span", { className: "lks-sec-num" }, String(section.index + 1)), ` ${section.title}`), ...lessons.map((lesson) => {
931
+ const locked = lesson.status === "locked" || lesson.kind === "exam" && !examAllowed;
932
+ return (0, react.createElement)("div", {
933
+ key: lesson.id,
934
+ className: `lks-node${locked ? " locked" : ""}${lesson.focus ? " focus" : ""}`,
935
+ title: jumping === lesson.id ? "正在打开课时会话…" : statusTitle(lesson.kind, locked ? "locked" : lesson.status),
936
+ onClick: () => {
937
+ if (locked) return;
938
+ if (lesson.kind === "exam") {
939
+ setFocus(lesson.id).catch(reportError);
940
+ send(`开始「${section.title}」的章节测验:按本节课时出题,答完逐题判分`);
941
+ return;
942
+ }
943
+ openLessonThread(lesson);
944
+ }
945
+ }, (0, react.createElement)("span", { className: "lks-g" }, jumping === lesson.id ? "⏳" : glyph(lesson.kind, locked ? "locked" : lesson.status)), (0, react.createElement)("span", { className: "lks-t" }, lesson.title), lesson.due ? (0, react.createElement)("span", {
946
+ className: "lks-tag due",
947
+ title: "这课时的复习今天到期(SM-2)"
948
+ }, "🔁") : null, lesson.weakConcepts > 0 ? (0, react.createElement)("span", {
949
+ className: "lks-tag weak",
950
+ title: `${lesson.weakConcepts} 个薄弱知识点,测验会优先考察`
951
+ }, `⚡${lesson.weakConcepts}`) : null, lesson.frictionCount > 0 ? (0, react.createElement)("span", {
952
+ className: "lks-tag fric",
953
+ title: `${lesson.frictionCount} 次卡点记录(你说"不懂"时导师记下的)`
954
+ }, `😣${lesson.frictionCount}`) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", {
955
+ className: "lks-bar",
956
+ title: `课时掌握度 ${lesson.masteryPct}%(取最薄弱知识点)`
957
+ }, (0, react.createElement)("i", { style: { width: `${lesson.masteryPct}%` } })) : null, lesson.masteryPct !== null ? (0, react.createElement)("span", {
958
+ className: "lks-pct",
959
+ title: "课时掌握度 = 最薄弱知识点的掌握度"
960
+ }, `${lesson.masteryPct}%`) : null);
961
+ })];
962
+ }), (0, react.createElement)("button", {
963
+ className: "lks-btn ghost",
964
+ style: { marginTop: "10px" },
965
+ onClick: () => {
966
+ setShowImport(!showImport);
967
+ }
968
+ }, showImport ? "收起导入" : "+ 导入课程"), showImport ? (0, react.createElement)(ImportRow, { send }) : null);
969
+ })();
970
+ return (0, react.createElement)("div", { className: "lks-col lks-col-rail" }, (0, react.createElement)("div", { className: "lks-colhead" }, "课程"), body, (0, react.createElement)(ActionError, { error }));
971
+ }
972
+ /** One transcript row's element; `interactive` adds the quiz-answer buttons to an assistant row. */
973
+ function chatRowElement(row, interactive) {
974
+ if (row.role === "user") return (0, react.createElement)("div", {
975
+ key: row.key,
976
+ className: "lks-msg user"
977
+ }, row.text);
978
+ if (row.role === "tool") {
979
+ const chip = row.call === void 0 || row.call === null ? null : toolChipLabel(row.call.name, row.call.argsRaw);
980
+ if (chip !== null) return (0, react.createElement)("div", {
981
+ key: row.key,
982
+ className: `lks-msg tool ${chip.tone}`
983
+ }, chip.label);
984
+ return (0, react.createElement)("div", {
985
+ key: row.key,
986
+ className: "lks-msg tool"
987
+ }, `🔧 ${row.text}`);
988
+ }
989
+ if (row.role === "error") return (0, react.createElement)("div", {
990
+ key: row.key,
991
+ className: "lks-msg error"
992
+ }, `⚠ ${row.text}`);
993
+ if (row.role === "thinking") return (0, react.createElement)("div", {
994
+ key: row.key,
995
+ className: "lks-msg thinking",
996
+ title: "导师正在推理,回复马上就来"
997
+ }, (0, react.createElement)(IconLoadingOutline16, {
998
+ size: 14,
999
+ className: "lks-spin"
1000
+ }), row.text);
1001
+ const cls = row.role === "streaming" ? "lks-msg assistant lks-prose streaming" : "lks-msg assistant lks-prose";
1002
+ const body = (0, react.createElement)("div", {
1003
+ key: row.key,
1004
+ className: cls,
1005
+ dangerouslySetInnerHTML: { __html: renderMarkdown(row.text) }
1006
+ });
1007
+ const options = interactive === void 0 || row.role === "streaming" ? [] : quizOptions(row.text);
1008
+ if (options.length < 2) return body;
1009
+ return (0, react.createElement)("div", {
1010
+ key: row.key,
1011
+ className: "lks-turn"
1012
+ }, body, (0, react.createElement)("div", {
1013
+ className: "lks-quiz",
1014
+ title: "点击选项作答,也可以直接打字回答"
1015
+ }, ...options.map((opt) => (0, react.createElement)("button", {
1016
+ key: opt.letter,
1017
+ className: "lks-opt",
1018
+ onClick: () => {
1019
+ interactive.send(`选 ${opt.letter}:${opt.text}`);
1020
+ }
1021
+ }, (0, react.createElement)("span", { className: "lks-optletter" }, opt.letter), (0, react.createElement)("span", null, opt.text)))));
1022
+ }
1023
+ /** Middle column: the tutor — transcript, proposal banner, pills, starters. Typing happens in the native composer below the tab. */
1024
+ function TutorColumn({ data, setMode, send, snapshot }) {
1025
+ const [error, setError] = (0, react.useState)(null);
1026
+ const rows = transcriptRows(snapshot.nodes, snapshot.partial);
1027
+ const lastAssistant = rows.reduce((acc, row, i) => row.role === "assistant" ? i : acc, -1);
1028
+ const scrollRef = (0, react.useRef)(null);
1029
+ (0, react.useEffect)(() => {
1030
+ const el = scrollRef.current;
1031
+ if (el !== null) el.scrollTop = el.scrollHeight;
1032
+ }, [rows.length]);
1033
+ const proposal = data?.pendingProposals[0] ?? null;
1034
+ const lesson = data?.lesson ?? null;
1035
+ /** Mode switches are the only fallible write left here (host route); surface failures inline. */
1036
+ const fire = (action) => {
1037
+ action.then(() => {
1038
+ setError(null);
1039
+ }, (err) => {
1040
+ setError(err instanceof Error ? err.message : String(err));
1041
+ });
1042
+ };
1043
+ return (0, react.createElement)("div", { className: "lks-col lks-col-tutor" }, (0, react.createElement)("div", { className: "lks-colhead" }, "导师"), (0, react.createElement)("div", {
1044
+ className: "lks-transcript",
1045
+ ref: scrollRef
1046
+ }, rows.length === 0 ? (0, react.createElement)("div", { className: "lks-empty" }, "对话会出现在这里", (0, react.createElement)("br"), "在下方输入框和导师说话") : rows.map((row, i) => chatRowElement(row, i === lastAssistant ? { send } : void 0))), proposal !== null ? (0, react.createElement)("div", { className: "lks-banner" }, (0, react.createElement)("span", null, "🎓"), (0, react.createElement)("span", { className: "lks-why" }, `导师提议你已掌握「${proposal.lessonTitle}」:${proposal.rationale}`), (0, react.createElement)("button", {
1047
+ className: "lks-btn primary",
1048
+ onClick: () => {
1049
+ send(`接受提案 ${proposal.id} —— 确认标记这课为已掌握`);
1050
+ }
1051
+ }, "接受"), (0, react.createElement)("button", {
1052
+ className: "lks-btn ghost",
1053
+ onClick: () => {
1054
+ send(`拒绝提案 ${proposal.id} —— 我想再练练`);
1055
+ }
1056
+ }, "再练练")) : null, (0, react.createElement)("div", {
1057
+ className: "lks-pills",
1058
+ style: {
1059
+ height: "auto",
1060
+ padding: "4px 0"
1061
+ }
1062
+ }, ...MODES.map((mode) => (0, react.createElement)("button", {
1063
+ key: mode.id,
1064
+ className: `lks-pill${data?.mode === mode.id ? " on" : ""}`,
1065
+ title: mode.hint,
1066
+ onClick: () => {
1067
+ fire(setMode(mode.id));
1068
+ }
1069
+ }, mode.label))), lesson !== null && lesson.starters.length > 0 ? (0, react.createElement)("div", {
1070
+ className: "lks-dock",
1071
+ style: { padding: "0 0 6px" }
1072
+ }, ...lesson.starters.map((s) => (0, react.createElement)("button", {
1073
+ key: s.label,
1074
+ className: "lks-starter",
1075
+ title: s.message,
1076
+ onClick: () => {
1077
+ send(s.message);
1078
+ }
1079
+ }, s.label))) : null, (0, react.createElement)(ActionError, { error }));
1080
+ }
1081
+ /** Right column: the blackboard — focus-lesson 讲解 plus the Cornell 笔记. */
1082
+ function BlackboardColumn({ data }) {
1083
+ const lesson = data?.lesson ?? null;
1084
+ const body = lesson === null ? (0, react.createElement)("div", { className: "lks-empty" }, "黑板还空着", (0, react.createElement)("br"), "在左侧课程树选择一课") : (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "lks-lessonhead" }, (0, react.createElement)("h2", null, lesson.title), (0, react.createElement)("div", { className: "lks-meta" }, `${lesson.courseTitle} · ${lesson.status}` + (lesson.masteryPct === null ? "" : ` · 掌握度 ${lesson.masteryPct}%`) + ` · ${lesson.strategy}`), lesson.concepts.length > 0 ? (0, react.createElement)("div", { className: "lks-chips" }, ...lesson.concepts.map((c) => (0, react.createElement)("span", {
1085
+ key: c.title,
1086
+ className: `lks-chip${c.weak ? " weak" : ""}`
1087
+ }, `${c.title} ${c.masteryPct}%${c.weak ? " ⚡" : ""}`))) : null), (0, react.createElement)("div", {
1088
+ className: "lks-prose",
1089
+ dangerouslySetInnerHTML: { __html: lesson.html }
1090
+ }), (0, react.createElement)("div", { className: "lks-bb-notes" }, (0, react.createElement)("div", { className: "lks-sec" }, "笔记"), lesson.notes.length === 0 ? (0, react.createElement)("div", {
1091
+ className: "lks-empty",
1092
+ style: { padding: "16px 0" }
1093
+ }, "这一课还没有笔记") : ZONES.filter(([zone]) => lesson.notes.some((n) => n.zone === zone)).map(([zone, label]) => (0, react.createElement)("div", {
1094
+ key: zone,
1095
+ className: "lks-zone"
1096
+ }, (0, react.createElement)("h4", null, label), ...lesson.notes.filter((n) => n.zone === zone).map((n) => (0, react.createElement)("div", {
1097
+ key: n.id,
1098
+ className: "lks-note"
1099
+ }, (0, react.createElement)("span", { className: "lks-note-src" }, n.source), (0, react.createElement)("div", { className: "lks-note-title" }, n.title), (0, react.createElement)("div", { className: "lks-note-text" }, n.text), n.quote !== null ? (0, react.createElement)("div", { className: "lks-note-q" }, `“${n.quote}”`) : null))))));
1100
+ return (0, react.createElement)("div", { className: "lks-col lks-col-bb" }, (0, react.createElement)("div", { className: "lks-colhead" }, "黑板"), body);
1101
+ }
1102
+ //#endregion
1103
+ //#region src/client/starter.tsx
1104
+ /**
1105
+ * The one-click study starter: a pill button in the blank-session (hero)
1106
+ * composer tool row. One click adopts the plugin's dedicated study workspace
1107
+ * (created by the host beside the state file), opens a session in it, sends
1108
+ * the kickoff prompt through the session API, and makes that session current
1109
+ * — the learner lands in the chat with the tutor already responding, one
1110
+ * click away from the 学习 tab (view switching is ui-conversation-private,
1111
+ * not reachable from an additive plugin).
1112
+ * @module dsh-plugin-lookatstudy/client/starter
1113
+ */
1114
+ /** The kickoff prompt: starts the tutoring loop and points at the study tab. */
1115
+ const KICKOFF = "开始学习:查看我的学习状态并打开当前焦点课时。如果我还没有课程,推荐我导入示例课程(AI-For-Beginners)并说明怎么开始。回复最后请提醒我点上方「学习」页签进入学习界面。";
1116
+ /**
1117
+ * Build the hero starter button bound to the framework services.
1118
+ * @param ctx - client root context (workspaces + sessions injected).
1119
+ * @returns the component for `conversation.input.left`.
1120
+ */
1121
+ function studyStartButton(ctx) {
1122
+ return function StudyStartButton({ session }) {
1123
+ if (session.blank !== true) return null;
1124
+ return (0, react.createElement)(Inner, {
1125
+ key: "inner",
1126
+ ctx
1127
+ });
1128
+ };
1129
+ }
1130
+ /** Button body in its own component so hook order stays constant. */
1131
+ function Inner({ ctx }) {
1132
+ const [busy, setBusy] = (0, react.useState)(false);
1133
+ const [error, setError] = (0, react.useState)(null);
1134
+ const start = () => {
1135
+ if (busy) return;
1136
+ setBusy(true);
1137
+ setError(null);
1138
+ (async () => {
1139
+ try {
1140
+ const area = await fetch("/lookatstudy/api/study-workspace");
1141
+ if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
1142
+ const { path } = await area.json();
1143
+ const workspace = await ctx.workspaces.create({ path });
1144
+ const sessionId = await ctx.workspaces.connectWorkspace(workspace.workspaceId);
1145
+ const actx = ctx.sessions.scope(sessionId);
1146
+ const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
1147
+ if (face === void 0) throw new Error("study session is not addressable yet");
1148
+ const result = await face.prompt([{
1149
+ type: "text",
1150
+ text: KICKOFF
1151
+ }], "queue");
1152
+ if (!result.ok) throw new Error(`kickoff prompt rejected: ${result.error.code}: ${result.error.message}`);
1153
+ ctx.sessions.open(sessionId);
1154
+ } catch (err) {
1155
+ setError(err instanceof Error ? err.message : String(err));
1156
+ setBusy(false);
1157
+ }
1158
+ })();
1159
+ };
1160
+ return (0, react.createElement)("span", {
1161
+ className: "lks-root",
1162
+ style: {
1163
+ display: "inline-flex",
1164
+ alignItems: "center",
1165
+ gap: "8px"
1166
+ }
1167
+ }, (0, react.createElement)("button", {
1168
+ className: "lks-pill on",
1169
+ title: "一键准备学习:建立学习工作区、开启会话并让导师就位",
1170
+ disabled: busy,
1171
+ onClick: start
1172
+ }, busy ? (0, react.createElement)(IconLoadingOutline16, { className: "lks-spin" }) : (0, react.createElement)(IconThinkOutline16, null), busy ? "正在准备学习区…" : "开始学习"), error !== null ? (0, react.createElement)("span", {
1173
+ className: "lks-propcard-err",
1174
+ style: { marginTop: 0 }
1175
+ }, error) : null);
1176
+ }
1177
+ //#endregion
1178
+ //#region src/client/index.ts
1179
+ const inject = [
1180
+ "slots",
1181
+ "workspaces",
1182
+ "sessions"
1183
+ ];
1184
+ /**
1185
+ * Register the study surfaces: styles inject once; the single
1186
+ * `conversation.view` tab carries the whole plugin; the hero starter button
1187
+ * (`conversation.input.left`, blank sessions only) automates the onboarding —
1188
+ * study workspace + session + kickoff prompt in one click.
1189
+ * @param ctx - client root context.
1190
+ */
1191
+ function apply(ctx) {
1192
+ ensureStudyStyles();
1193
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
1194
+ name: "conversation.view",
1195
+ id: "lookatstudy-study",
1196
+ order: 15,
1197
+ label: "学习"
1198
+ }, studyView(ctx)));
1199
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
1200
+ name: "conversation.input.left",
1201
+ id: "lookatstudy-start",
1202
+ order: 10
1203
+ }, studyStartButton(ctx)));
1204
+ }
1205
+ //#endregion
1206
+ exports.apply = apply;
1207
+ exports.inject = inject;
1208
+ exports.studyView = studyView;
1209
+ exports.transcriptRows = transcriptRows;
1210
+ return module.exports;
1211
+ }
1212
+ });
1213
+
1214
+ //# sourceMappingURL=client.js.map