@vue-skuilder/db 0.2.2 → 0.2.4
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/dist/{contentSource-Ht3N2f-y.d.ts → contentSource-Cplhv3bJ.d.ts} +1 -1
- package/dist/{contentSource-BMlMwSiG.d.cts → contentSource-kI9_jwTu.d.cts} +1 -1
- package/dist/core/index.d.cts +5 -5
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BEqB8VBR.d.cts → dataLayerProvider-CiA2Rr0v.d.cts} +1 -1
- package/dist/{dataLayerProvider-DObSXjnf.d.ts → dataLayerProvider-DrBqOUa3.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +3 -3
- package/dist/impl/couch/index.d.ts +3 -3
- package/dist/impl/couch/index.js +2 -1
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2 -1
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -4
- package/dist/impl/static/index.d.ts +4 -4
- package/dist/impl/static/index.js +2 -1
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2 -1
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-BWvO-_rJ.d.ts → index-BLLT5BYE.d.ts} +1 -1
- package/dist/{index-Ba7hYbHj.d.cts → index-k9NFHpS1.d.cts} +1 -1
- package/dist/index.d.cts +209 -10
- package/dist/index.d.ts +209 -10
- package/dist/index.js +361 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +361 -17
- package/dist/index.mjs.map +1 -1
- package/dist/{types-W8n-B6HG.d.cts → types-BFUa1pa3.d.cts} +1 -1
- package/dist/{types-CJrLM1Ew.d.ts → types-CHgpWQAY.d.ts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-4tlwHnXo.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-4tlwHnXo.d.ts} +1 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +3 -3
- package/src/core/navigators/Pipeline.ts +1 -1
- package/src/study/SessionController.ts +347 -22
- package/src/study/SessionDebugger.ts +10 -0
- package/src/study/SessionOverlay.ts +276 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { logger } from '../util/logger';
|
|
2
|
+
import type { ReplanHints } from '@db/core/navigators/generators/types';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// SESSION OVERLAY
|
|
6
|
+
// ============================================================================
|
|
7
|
+
//
|
|
8
|
+
// A pinned, vanilla-DOM debug overlay for the LIVE SessionController. Unlike
|
|
9
|
+
// `SessionDebugger` (a passive tracker of pushed snapshots), this reads the
|
|
10
|
+
// active controller directly each tick — current queues, session hints, timer.
|
|
11
|
+
//
|
|
12
|
+
// Toggled via `window.skuilder.session.dbgOverlay()`.
|
|
13
|
+
//
|
|
14
|
+
// The `db` package is framework-agnostic, so this renders with raw DOM and
|
|
15
|
+
// no-ops gracefully in non-browser hosts (e.g. the tuilder TUI). It is the
|
|
16
|
+
// first DOM-rendering debug util in the package — kept self-contained here.
|
|
17
|
+
//
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/** Per-queue debug view: total length, cumulative draws, and head-first cardIDs. */
|
|
21
|
+
export interface SessionQueueDebug {
|
|
22
|
+
length: number;
|
|
23
|
+
dequeueCount: number;
|
|
24
|
+
/** cardIDs in queue order, head (next draw) first. */
|
|
25
|
+
cards: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Live snapshot of the controller, read fresh on each overlay tick. */
|
|
29
|
+
export interface SessionDebugSnapshot {
|
|
30
|
+
secondsRemaining: number;
|
|
31
|
+
hasCardGuarantee: boolean;
|
|
32
|
+
minCardsGuarantee: number;
|
|
33
|
+
wellIndicatedRemaining: number;
|
|
34
|
+
/** cardID of the card currently in front of the learner, if any. */
|
|
35
|
+
currentCard: string | null;
|
|
36
|
+
/** Session-durable hints re-merged into every pipeline run this session. */
|
|
37
|
+
sessionHints: ReplanHints | null;
|
|
38
|
+
/** True while a replan is executing (in-flight). */
|
|
39
|
+
replanActive: boolean;
|
|
40
|
+
/** Reason for the in-flight replan (caller label, or '(auto)'); may be stale when idle. */
|
|
41
|
+
replanLabel: string | null;
|
|
42
|
+
reviewQ: SessionQueueDebug;
|
|
43
|
+
newQ: SessionQueueDebug;
|
|
44
|
+
failedQ: SessionQueueDebug;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The narrow surface the overlay needs from a SessionController. */
|
|
48
|
+
export interface SessionDebugTarget {
|
|
49
|
+
getDebugSnapshot(): SessionDebugSnapshot;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ----------------------------------------------------------------------------
|
|
53
|
+
// Active-controller registry
|
|
54
|
+
// ----------------------------------------------------------------------------
|
|
55
|
+
//
|
|
56
|
+
// The controller registers itself on construction; a new session overwrites the
|
|
57
|
+
// prior handle. Kept here (a leaf module) so SessionController can import the
|
|
58
|
+
// registrar without pulling in the overlay's DOM code or risking an import
|
|
59
|
+
// cycle with SessionDebugger.
|
|
60
|
+
|
|
61
|
+
let activeController: SessionDebugTarget | null = null;
|
|
62
|
+
|
|
63
|
+
/** Called by SessionController's constructor. Pass `null` to deregister. */
|
|
64
|
+
export function registerActiveController(controller: SessionDebugTarget | null): void {
|
|
65
|
+
activeController = controller;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getActiveController(): SessionDebugTarget | null {
|
|
69
|
+
return activeController;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ----------------------------------------------------------------------------
|
|
73
|
+
// Overlay rendering (vanilla DOM)
|
|
74
|
+
// ----------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const OVERLAY_ID = 'skuilder-session-overlay';
|
|
77
|
+
const POLL_MS = 300;
|
|
78
|
+
/** Queues with at most this many cards are listed outright; larger ones collapse to a count. */
|
|
79
|
+
const INLINE_THRESHOLD = 5;
|
|
80
|
+
|
|
81
|
+
/** Braille spinner frames, advanced once per render tick (≈POLL_MS cadence). */
|
|
82
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
83
|
+
let spinnerFrame = 0;
|
|
84
|
+
|
|
85
|
+
let overlayEl: HTMLElement | null = null;
|
|
86
|
+
let pollHandle: ReturnType<typeof setInterval> | null = null;
|
|
87
|
+
|
|
88
|
+
/** Expansion state for collapsible (large) queues, preserved across re-renders. */
|
|
89
|
+
const expanded: Record<string, boolean> = { reviewQ: false, newQ: false, failedQ: false };
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Toggle the pinned overlay on/off. No-ops (with a console hint) when there is
|
|
93
|
+
* no DOM, so it is safe to call from any host environment.
|
|
94
|
+
*/
|
|
95
|
+
export function toggleSessionOverlay(): void {
|
|
96
|
+
if (typeof document === 'undefined') {
|
|
97
|
+
logger.info('[Session Overlay] No DOM available (non-browser host); overlay unavailable.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (overlayEl) {
|
|
101
|
+
teardown();
|
|
102
|
+
logger.info('[Session Overlay] Hidden.');
|
|
103
|
+
} else {
|
|
104
|
+
mount();
|
|
105
|
+
logger.info('[Session Overlay] Shown. Toggle off with window.skuilder.session.dbgOverlay().');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mount(): void {
|
|
110
|
+
overlayEl = document.createElement('div');
|
|
111
|
+
overlayEl.id = OVERLAY_ID;
|
|
112
|
+
Object.assign(overlayEl.style, {
|
|
113
|
+
position: 'fixed',
|
|
114
|
+
top: '8px',
|
|
115
|
+
left: '8px',
|
|
116
|
+
zIndex: '2147483647',
|
|
117
|
+
maxWidth: '320px',
|
|
118
|
+
maxHeight: '90vh',
|
|
119
|
+
overflowY: 'auto',
|
|
120
|
+
padding: '8px 10px',
|
|
121
|
+
background: 'rgba(17, 24, 39, 0.92)',
|
|
122
|
+
color: '#e5e7eb',
|
|
123
|
+
font: '11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
124
|
+
borderRadius: '6px',
|
|
125
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
|
|
126
|
+
pointerEvents: 'auto',
|
|
127
|
+
userSelect: 'none',
|
|
128
|
+
});
|
|
129
|
+
document.body.appendChild(overlayEl);
|
|
130
|
+
render();
|
|
131
|
+
pollHandle = setInterval(render, POLL_MS);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function teardown(): void {
|
|
135
|
+
if (pollHandle !== null) {
|
|
136
|
+
clearInterval(pollHandle);
|
|
137
|
+
pollHandle = null;
|
|
138
|
+
}
|
|
139
|
+
if (overlayEl?.parentNode) {
|
|
140
|
+
overlayEl.parentNode.removeChild(overlayEl);
|
|
141
|
+
}
|
|
142
|
+
overlayEl = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function render(): void {
|
|
146
|
+
if (!overlayEl) return;
|
|
147
|
+
|
|
148
|
+
spinnerFrame++;
|
|
149
|
+
|
|
150
|
+
const ctrl = getActiveController();
|
|
151
|
+
if (!ctrl) {
|
|
152
|
+
overlayEl.innerHTML = headerHtml() + `<div style="opacity:.65">No active session.</div>`;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const s = ctrl.getDebugSnapshot();
|
|
157
|
+
overlayEl.innerHTML =
|
|
158
|
+
headerHtml() +
|
|
159
|
+
replanHtml(s) +
|
|
160
|
+
metaHtml(s) +
|
|
161
|
+
hintsHtml(s.sessionHints) +
|
|
162
|
+
queueHtml('reviewQ', 'reviewQ', s.reviewQ) +
|
|
163
|
+
queueHtml('newQ', 'newQ', s.newQ) +
|
|
164
|
+
queueHtml('failedQ', 'failedQ', s.failedQ);
|
|
165
|
+
|
|
166
|
+
// Re-attach toggle handlers for collapsible queue headers each render.
|
|
167
|
+
overlayEl.querySelectorAll<HTMLElement>('[data-q]').forEach((el) => {
|
|
168
|
+
el.onclick = () => {
|
|
169
|
+
const key = el.dataset.q;
|
|
170
|
+
if (!key) return;
|
|
171
|
+
expanded[key] = !expanded[key];
|
|
172
|
+
render();
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function headerHtml(): string {
|
|
178
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:4px">⚙ SessionController</div>`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function replanHtml(s: SessionDebugSnapshot): string {
|
|
182
|
+
if (!s.replanActive) {
|
|
183
|
+
return `<div style="margin-bottom:6px;opacity:.45">○ idle</div>`;
|
|
184
|
+
}
|
|
185
|
+
const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
|
186
|
+
const reason = esc(s.replanLabel ?? '(auto)');
|
|
187
|
+
return (
|
|
188
|
+
`<div style="margin-bottom:6px;color:#fde047">` +
|
|
189
|
+
`${frame} replanning <span style="opacity:.85">[${reason}]</span></div>`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function metaHtml(s: SessionDebugSnapshot): string {
|
|
194
|
+
const mmss = formatTime(s.secondsRemaining);
|
|
195
|
+
const guarantee = s.hasCardGuarantee
|
|
196
|
+
? ` · <span style="color:#fbbf24">guarantee ${s.minCardsGuarantee}</span>`
|
|
197
|
+
: '';
|
|
198
|
+
const rows = [
|
|
199
|
+
`time ${mmss}${guarantee}`,
|
|
200
|
+
`well-indicated left: ${s.wellIndicatedRemaining}`,
|
|
201
|
+
`current: ${s.currentCard ? esc(s.currentCard) : '<span style="opacity:.6">—</span>'}`,
|
|
202
|
+
];
|
|
203
|
+
return `<div style="margin-bottom:6px">${rows.map((r) => `<div>${r}</div>`).join('')}</div>`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hintsHtml(h: ReplanHints | null): string {
|
|
207
|
+
const parts: string[] = [];
|
|
208
|
+
if (h) {
|
|
209
|
+
if (h.boostTags && Object.keys(h.boostTags).length) {
|
|
210
|
+
parts.push(
|
|
211
|
+
`boost: ` +
|
|
212
|
+
Object.entries(h.boostTags)
|
|
213
|
+
.map(([k, v]) => `${esc(k)}<span style="opacity:.6">×${v}</span>`)
|
|
214
|
+
.join(', ')
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (h.boostCards && Object.keys(h.boostCards).length) {
|
|
218
|
+
parts.push(
|
|
219
|
+
`boostCards: ` +
|
|
220
|
+
Object.entries(h.boostCards)
|
|
221
|
+
.map(([k, v]) => `${esc(k)}<span style="opacity:.6">×${v}</span>`)
|
|
222
|
+
.join(', ')
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (h.requireCards?.length) parts.push(`require: ${h.requireCards.map(esc).join(', ')}`);
|
|
226
|
+
if (h.requireTags?.length) parts.push(`requireTags: ${h.requireTags.map(esc).join(', ')}`);
|
|
227
|
+
if (h.excludeTags?.length) parts.push(`exclude: ${h.excludeTags.map(esc).join(', ')}`);
|
|
228
|
+
if (h.excludeCards?.length) parts.push(`excludeCards: ${h.excludeCards.map(esc).join(', ')}`);
|
|
229
|
+
}
|
|
230
|
+
const body = parts.length
|
|
231
|
+
? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join('')
|
|
232
|
+
: `<div style="margin-left:6px;opacity:.6">none</div>`;
|
|
233
|
+
return (
|
|
234
|
+
`<div style="margin-bottom:6px">` +
|
|
235
|
+
`<div style="color:#86efac">sessionHints</div>${body}</div>`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function queueHtml(key: string, label: string, q: SessionQueueDebug): string {
|
|
240
|
+
const collapsible = q.length > INLINE_THRESHOLD;
|
|
241
|
+
const isOpen = !collapsible || expanded[key];
|
|
242
|
+
const caret = collapsible ? (expanded[key] ? '▾ ' : '▸ ') : '';
|
|
243
|
+
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : '';
|
|
244
|
+
const titleStyle = collapsible
|
|
245
|
+
? 'cursor:pointer;color:#f9a8d4'
|
|
246
|
+
: 'color:#f9a8d4';
|
|
247
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : '';
|
|
248
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
249
|
+
|
|
250
|
+
let body = '';
|
|
251
|
+
if (isOpen && q.cards.length) {
|
|
252
|
+
body =
|
|
253
|
+
`<ol style="margin:2px 0 6px 0;padding-left:20px">` +
|
|
254
|
+
q.cards.map((c) => `<li style="white-space:nowrap">${esc(c)}</li>`).join('') +
|
|
255
|
+
`</ol>`;
|
|
256
|
+
} else if (!q.cards.length) {
|
|
257
|
+
body = `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
258
|
+
} else {
|
|
259
|
+
body = `<div style="margin:1px 0 6px 6px;opacity:.55">(${q.length} cards — click to expand)</div>`;
|
|
260
|
+
}
|
|
261
|
+
return title + body;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatTime(totalSeconds: number): string {
|
|
265
|
+
const s = Math.max(0, Math.round(totalSeconds));
|
|
266
|
+
const m = Math.floor(s / 60);
|
|
267
|
+
const r = s % 60;
|
|
268
|
+
return `${m}:${r.toString().padStart(2, '0')}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function esc(value: string): string {
|
|
272
|
+
return value
|
|
273
|
+
.replace(/&/g, '&')
|
|
274
|
+
.replace(/</g, '<')
|
|
275
|
+
.replace(/>/g, '>');
|
|
276
|
+
}
|