@vue-skuilder/db 0.2.3 → 0.2.5
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/core/index.js +5 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +5 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +5 -4
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +5 -4
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +5 -4
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +5 -4
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +103 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +413 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +413 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/navigators/generators/elo.ts +32 -11
- package/src/study/SessionController.ts +187 -10
- package/src/study/SessionDebugger.ts +10 -0
- package/src/study/SessionOverlay.ts +500 -0
package/dist/index.js
CHANGED
|
@@ -1939,13 +1939,14 @@ var elo_exports = {};
|
|
|
1939
1939
|
__export(elo_exports, {
|
|
1940
1940
|
default: () => ELONavigator
|
|
1941
1941
|
});
|
|
1942
|
-
var import_common5, ELONavigator;
|
|
1942
|
+
var import_common5, ELO_RELEVANCE_SIGMA, ELONavigator;
|
|
1943
1943
|
var init_elo = __esm({
|
|
1944
1944
|
"src/core/navigators/generators/elo.ts"() {
|
|
1945
1945
|
"use strict";
|
|
1946
1946
|
init_navigators();
|
|
1947
1947
|
import_common5 = require("@vue-skuilder/common");
|
|
1948
1948
|
init_logger();
|
|
1949
|
+
ELO_RELEVANCE_SIGMA = 300;
|
|
1949
1950
|
ELONavigator = class extends ContentNavigator {
|
|
1950
1951
|
/** Human-readable name for CardGenerator interface */
|
|
1951
1952
|
name;
|
|
@@ -1985,8 +1986,8 @@ var init_elo = __esm({
|
|
|
1985
1986
|
const scored = newCards.map((c) => {
|
|
1986
1987
|
const cardElo = c.elo ?? 1e3;
|
|
1987
1988
|
const distance = Math.abs(cardElo - userGlobalElo);
|
|
1988
|
-
const
|
|
1989
|
-
const samplingKey =
|
|
1989
|
+
const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
|
|
1990
|
+
const samplingKey = relevance * (0.5 + 0.5 * Math.random());
|
|
1990
1991
|
return {
|
|
1991
1992
|
cardId: c.cardID,
|
|
1992
1993
|
courseId: c.courseID,
|
|
@@ -1998,7 +1999,7 @@ var init_elo = __esm({
|
|
|
1998
1999
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
|
|
1999
2000
|
action: "generated",
|
|
2000
2001
|
score: samplingKey,
|
|
2001
|
-
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}),
|
|
2002
|
+
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
|
|
2002
2003
|
}
|
|
2003
2004
|
]
|
|
2004
2005
|
};
|
|
@@ -13156,6 +13157,294 @@ mountMixerDebugger();
|
|
|
13156
13157
|
// src/study/SessionDebugger.ts
|
|
13157
13158
|
init_logger();
|
|
13158
13159
|
init_PipelineDebugger();
|
|
13160
|
+
|
|
13161
|
+
// src/study/SessionOverlay.ts
|
|
13162
|
+
init_logger();
|
|
13163
|
+
var activeController = null;
|
|
13164
|
+
function registerActiveController(controller) {
|
|
13165
|
+
activeController = controller;
|
|
13166
|
+
}
|
|
13167
|
+
function getActiveController() {
|
|
13168
|
+
return activeController;
|
|
13169
|
+
}
|
|
13170
|
+
var OVERLAY_ID = "skuilder-session-overlay";
|
|
13171
|
+
var POLL_MS = 300;
|
|
13172
|
+
var INLINE_THRESHOLD = 5;
|
|
13173
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
13174
|
+
var spinnerFrame = 0;
|
|
13175
|
+
var overlayEl = null;
|
|
13176
|
+
var pollHandle = null;
|
|
13177
|
+
var lastSnapshot = null;
|
|
13178
|
+
var copyFlashUntil = 0;
|
|
13179
|
+
var minified = false;
|
|
13180
|
+
var expanded = {
|
|
13181
|
+
reviewQ: false,
|
|
13182
|
+
newQ: false,
|
|
13183
|
+
failedQ: false,
|
|
13184
|
+
drawn: false
|
|
13185
|
+
};
|
|
13186
|
+
function toggleSessionOverlay() {
|
|
13187
|
+
if (typeof document === "undefined") {
|
|
13188
|
+
logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
|
|
13189
|
+
return;
|
|
13190
|
+
}
|
|
13191
|
+
if (overlayEl) {
|
|
13192
|
+
teardown();
|
|
13193
|
+
logger.info("[Session Overlay] Hidden.");
|
|
13194
|
+
} else {
|
|
13195
|
+
mount();
|
|
13196
|
+
logger.info("[Session Overlay] Shown. Toggle off with window.skuilder.session.dbgOverlay().");
|
|
13197
|
+
}
|
|
13198
|
+
}
|
|
13199
|
+
function mount() {
|
|
13200
|
+
minified = false;
|
|
13201
|
+
overlayEl = document.createElement("div");
|
|
13202
|
+
overlayEl.id = OVERLAY_ID;
|
|
13203
|
+
Object.assign(overlayEl.style, {
|
|
13204
|
+
position: "fixed",
|
|
13205
|
+
top: "8px",
|
|
13206
|
+
left: "8px",
|
|
13207
|
+
zIndex: "2147483647",
|
|
13208
|
+
maxWidth: "320px",
|
|
13209
|
+
maxHeight: "90vh",
|
|
13210
|
+
overflowY: "auto",
|
|
13211
|
+
padding: "8px 10px",
|
|
13212
|
+
background: "rgba(17, 24, 39, 0.92)",
|
|
13213
|
+
color: "#e5e7eb",
|
|
13214
|
+
font: "11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
13215
|
+
borderRadius: "6px",
|
|
13216
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
|
|
13217
|
+
pointerEvents: "auto",
|
|
13218
|
+
userSelect: "none"
|
|
13219
|
+
});
|
|
13220
|
+
document.body.appendChild(overlayEl);
|
|
13221
|
+
render();
|
|
13222
|
+
pollHandle = setInterval(render, POLL_MS);
|
|
13223
|
+
}
|
|
13224
|
+
function teardown() {
|
|
13225
|
+
if (pollHandle !== null) {
|
|
13226
|
+
clearInterval(pollHandle);
|
|
13227
|
+
pollHandle = null;
|
|
13228
|
+
}
|
|
13229
|
+
if (overlayEl?.parentNode) {
|
|
13230
|
+
overlayEl.parentNode.removeChild(overlayEl);
|
|
13231
|
+
}
|
|
13232
|
+
overlayEl = null;
|
|
13233
|
+
}
|
|
13234
|
+
function render() {
|
|
13235
|
+
if (!overlayEl) return;
|
|
13236
|
+
spinnerFrame++;
|
|
13237
|
+
const ctrl = getActiveController();
|
|
13238
|
+
if (!ctrl) {
|
|
13239
|
+
lastSnapshot = null;
|
|
13240
|
+
overlayEl.innerHTML = headerHtml() + (minified ? "" : `<div style="opacity:.65">No active session.</div>`);
|
|
13241
|
+
attachHandlers();
|
|
13242
|
+
return;
|
|
13243
|
+
}
|
|
13244
|
+
const s = ctrl.getDebugSnapshot();
|
|
13245
|
+
lastSnapshot = s;
|
|
13246
|
+
if (minified) {
|
|
13247
|
+
overlayEl.innerHTML = headerHtml();
|
|
13248
|
+
attachHandlers();
|
|
13249
|
+
return;
|
|
13250
|
+
}
|
|
13251
|
+
overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) + queueHtml("reviewQ", "reviewQ", s.reviewQ) + queueHtml("newQ", "newQ", s.newQ) + queueHtml("failedQ", "failedQ", s.failedQ) + drawnHtml("drawn", s.drawnCards);
|
|
13252
|
+
attachHandlers();
|
|
13253
|
+
}
|
|
13254
|
+
function attachHandlers() {
|
|
13255
|
+
if (!overlayEl) return;
|
|
13256
|
+
overlayEl.querySelectorAll("[data-q]").forEach((el) => {
|
|
13257
|
+
el.onclick = () => {
|
|
13258
|
+
const key = el.dataset.q;
|
|
13259
|
+
if (!key) return;
|
|
13260
|
+
expanded[key] = !expanded[key];
|
|
13261
|
+
render();
|
|
13262
|
+
};
|
|
13263
|
+
});
|
|
13264
|
+
const copyBtn = overlayEl.querySelector("[data-copy]");
|
|
13265
|
+
if (copyBtn) {
|
|
13266
|
+
copyBtn.onclick = (ev) => {
|
|
13267
|
+
ev.stopPropagation();
|
|
13268
|
+
copySnapshot();
|
|
13269
|
+
};
|
|
13270
|
+
}
|
|
13271
|
+
const minBtn = overlayEl.querySelector("[data-min]");
|
|
13272
|
+
if (minBtn) {
|
|
13273
|
+
minBtn.onclick = (ev) => {
|
|
13274
|
+
ev.stopPropagation();
|
|
13275
|
+
minified = !minified;
|
|
13276
|
+
render();
|
|
13277
|
+
};
|
|
13278
|
+
}
|
|
13279
|
+
}
|
|
13280
|
+
function copySnapshot() {
|
|
13281
|
+
const text = snapshotToText(lastSnapshot);
|
|
13282
|
+
const flash = () => {
|
|
13283
|
+
copyFlashUntil = Date.now() + 1200;
|
|
13284
|
+
render();
|
|
13285
|
+
};
|
|
13286
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
13287
|
+
if (clip?.writeText) {
|
|
13288
|
+
clip.writeText(text).then(flash, (err) => {
|
|
13289
|
+
logger.warn(`[Session Overlay] Clipboard write failed: ${String(err)}`);
|
|
13290
|
+
});
|
|
13291
|
+
} else {
|
|
13292
|
+
logger.info(`[Session Overlay] Clipboard unavailable; snapshot follows:
|
|
13293
|
+
${text}`);
|
|
13294
|
+
}
|
|
13295
|
+
}
|
|
13296
|
+
function headerHtml() {
|
|
13297
|
+
const flashing = Date.now() < copyFlashUntil;
|
|
13298
|
+
const btnLabel = flashing ? "\u2713 copied" : "\u2398 copy";
|
|
13299
|
+
const btnColor = flashing ? "#86efac" : "#93c5fd";
|
|
13300
|
+
const copyBtn = `<span data-copy style="cursor:pointer;float:right;font-weight:400;color:${btnColor};border:1px solid currentColor;border-radius:4px;padding:0 4px;line-height:1.3">${btnLabel}</span>`;
|
|
13301
|
+
const minBtn = `<span data-min title="${minified ? "Restore" : "Minify"}" style="cursor:pointer;float:right;font-weight:400;color:#93c5fd;border:1px solid currentColor;border-radius:4px;padding:0 5px;margin-right:4px;line-height:1.3">${minified ? "\u25A2" : "\u2014"}</span>`;
|
|
13302
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:${minified ? 0 : 4}px">${copyBtn}${minBtn}\u2699 SessionController</div>`;
|
|
13303
|
+
}
|
|
13304
|
+
function replanHtml(s) {
|
|
13305
|
+
if (!s.replanActive) {
|
|
13306
|
+
return `<div style="margin-bottom:6px;opacity:.45">\u25CB idle</div>`;
|
|
13307
|
+
}
|
|
13308
|
+
const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
|
13309
|
+
const reason = esc(s.replanLabel ?? "(auto)");
|
|
13310
|
+
return `<div style="margin-bottom:6px;color:#fde047">${frame} replanning <span style="opacity:.85">[${reason}]</span></div>`;
|
|
13311
|
+
}
|
|
13312
|
+
function metaHtml(s) {
|
|
13313
|
+
const mmss = formatTime(s.secondsRemaining);
|
|
13314
|
+
const guarantee = s.hasCardGuarantee ? ` \xB7 <span style="color:#fbbf24">guarantee ${s.minCardsGuarantee}</span>` : "";
|
|
13315
|
+
const rows = [
|
|
13316
|
+
`time ${mmss}${guarantee}`,
|
|
13317
|
+
`well-indicated left: ${s.wellIndicatedRemaining}`,
|
|
13318
|
+
`current: ${s.currentCard ? esc(s.currentCard) : '<span style="opacity:.6">\u2014</span>'}`
|
|
13319
|
+
];
|
|
13320
|
+
return `<div style="margin-bottom:6px">${rows.map((r) => `<div>${r}</div>`).join("")}</div>`;
|
|
13321
|
+
}
|
|
13322
|
+
function hintsHtml(h) {
|
|
13323
|
+
const parts = [];
|
|
13324
|
+
if (h) {
|
|
13325
|
+
if (h.boostTags && Object.keys(h.boostTags).length) {
|
|
13326
|
+
parts.push(
|
|
13327
|
+
`boost: ` + Object.entries(h.boostTags).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
|
|
13328
|
+
);
|
|
13329
|
+
}
|
|
13330
|
+
if (h.boostCards && Object.keys(h.boostCards).length) {
|
|
13331
|
+
parts.push(
|
|
13332
|
+
`boostCards: ` + Object.entries(h.boostCards).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
|
|
13333
|
+
);
|
|
13334
|
+
}
|
|
13335
|
+
if (h.requireCards?.length) parts.push(`require: ${h.requireCards.map(esc).join(", ")}`);
|
|
13336
|
+
if (h.requireTags?.length) parts.push(`requireTags: ${h.requireTags.map(esc).join(", ")}`);
|
|
13337
|
+
if (h.excludeTags?.length) parts.push(`exclude: ${h.excludeTags.map(esc).join(", ")}`);
|
|
13338
|
+
if (h.excludeCards?.length) parts.push(`excludeCards: ${h.excludeCards.map(esc).join(", ")}`);
|
|
13339
|
+
}
|
|
13340
|
+
const body = parts.length ? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join("") : `<div style="margin-left:6px;opacity:.6">none</div>`;
|
|
13341
|
+
return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
|
|
13342
|
+
}
|
|
13343
|
+
function queueHtml(key, label, q) {
|
|
13344
|
+
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13345
|
+
const isOpen = collapsible && expanded[key];
|
|
13346
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13347
|
+
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
|
|
13348
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
|
|
13349
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13350
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
13351
|
+
if (!q.cards.length) {
|
|
13352
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
13353
|
+
}
|
|
13354
|
+
const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
|
|
13355
|
+
const hiddenCount = q.length - shown.length;
|
|
13356
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13357
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">` + shown.map((c) => `<li style="white-space:nowrap">${esc(c)}</li>`).join("") + `</ol>`;
|
|
13358
|
+
if (collapsible) {
|
|
13359
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13360
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13361
|
+
}
|
|
13362
|
+
return title + body;
|
|
13363
|
+
}
|
|
13364
|
+
function outcomeGlyph(correct) {
|
|
13365
|
+
if (correct === true) return `<span style="color:#86efac">\u2713</span>`;
|
|
13366
|
+
if (correct === false) return `<span style="color:#fca5a5">\u2717</span>`;
|
|
13367
|
+
return `<span style="opacity:.5">\xB7</span>`;
|
|
13368
|
+
}
|
|
13369
|
+
function drawnHtml(key, drawn) {
|
|
13370
|
+
const collapsible = drawn.length > INLINE_THRESHOLD;
|
|
13371
|
+
const isOpen = collapsible && expanded[key];
|
|
13372
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13373
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#c4b5fd" : "color:#c4b5fd";
|
|
13374
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13375
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}drawn: ${drawn.length}</div>`;
|
|
13376
|
+
if (!drawn.length) {
|
|
13377
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">none yet</div>`;
|
|
13378
|
+
}
|
|
13379
|
+
const shown = isOpen ? drawn : drawn.slice(0, INLINE_THRESHOLD);
|
|
13380
|
+
const hiddenCount = drawn.length - shown.length;
|
|
13381
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13382
|
+
const rows = shown.map((d) => {
|
|
13383
|
+
const retries = d.attempts > 1 ? `<span style="opacity:.5"> \xD7${d.attempts}</span>` : "";
|
|
13384
|
+
const time = `<span style="opacity:.45"> ${Math.round(d.timeSpentMs / 100) / 10}s</span>`;
|
|
13385
|
+
return `<li style="white-space:nowrap">${outcomeGlyph(d.correct)} ${esc(d.cardID)}<span style="opacity:.5"> [${esc(d.status)}]</span>${retries}${time}</li>`;
|
|
13386
|
+
}).join("");
|
|
13387
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">${rows}</ol>`;
|
|
13388
|
+
if (collapsible) {
|
|
13389
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13390
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13391
|
+
}
|
|
13392
|
+
return title + body;
|
|
13393
|
+
}
|
|
13394
|
+
function snapshotToText(s) {
|
|
13395
|
+
if (!s) return "SessionController \u2014 no active session.";
|
|
13396
|
+
const lines = [];
|
|
13397
|
+
lines.push("=== SessionController ===");
|
|
13398
|
+
lines.push(`time ${formatTime(s.secondsRemaining)}`);
|
|
13399
|
+
if (s.hasCardGuarantee) lines.push(`guarantee: ${s.minCardsGuarantee}`);
|
|
13400
|
+
lines.push(`well-indicated left: ${s.wellIndicatedRemaining}`);
|
|
13401
|
+
lines.push(`current: ${s.currentCard ?? "\u2014"}`);
|
|
13402
|
+
lines.push(
|
|
13403
|
+
s.replanActive ? `replan: ACTIVE [${s.replanLabel ?? "(auto)"}]` : "replan: idle"
|
|
13404
|
+
);
|
|
13405
|
+
lines.push("");
|
|
13406
|
+
lines.push("sessionHints:");
|
|
13407
|
+
const h = s.sessionHints;
|
|
13408
|
+
const hintParts = [];
|
|
13409
|
+
if (h) {
|
|
13410
|
+
if (h.boostTags && Object.keys(h.boostTags).length)
|
|
13411
|
+
hintParts.push(` boost: ${Object.entries(h.boostTags).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13412
|
+
if (h.boostCards && Object.keys(h.boostCards).length)
|
|
13413
|
+
hintParts.push(` boostCards: ${Object.entries(h.boostCards).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13414
|
+
if (h.requireCards?.length) hintParts.push(` require: ${h.requireCards.join(", ")}`);
|
|
13415
|
+
if (h.requireTags?.length) hintParts.push(` requireTags: ${h.requireTags.join(", ")}`);
|
|
13416
|
+
if (h.excludeTags?.length) hintParts.push(` exclude: ${h.excludeTags.join(", ")}`);
|
|
13417
|
+
if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
|
|
13418
|
+
}
|
|
13419
|
+
lines.push(hintParts.length ? hintParts.join("\n") : " none");
|
|
13420
|
+
const queueText = (label, q) => {
|
|
13421
|
+
lines.push("");
|
|
13422
|
+
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
13423
|
+
q.cards.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
|
|
13424
|
+
};
|
|
13425
|
+
queueText("reviewQ", s.reviewQ);
|
|
13426
|
+
queueText("newQ", s.newQ);
|
|
13427
|
+
queueText("failedQ", s.failedQ);
|
|
13428
|
+
lines.push("");
|
|
13429
|
+
lines.push(`drawn: ${s.drawnCards.length}`);
|
|
13430
|
+
s.drawnCards.forEach((d, i) => {
|
|
13431
|
+
const mark = d.correct === true ? "\u2713" : d.correct === false ? "\u2717" : "\xB7";
|
|
13432
|
+
const time = `${Math.round(d.timeSpentMs / 100) / 10}s`;
|
|
13433
|
+
lines.push(` ${i + 1}. ${mark} ${d.cardID} [${d.status}] \xD7${d.attempts} ${time}`);
|
|
13434
|
+
});
|
|
13435
|
+
return lines.join("\n");
|
|
13436
|
+
}
|
|
13437
|
+
function formatTime(totalSeconds) {
|
|
13438
|
+
const s = Math.max(0, Math.round(totalSeconds));
|
|
13439
|
+
const m = Math.floor(s / 60);
|
|
13440
|
+
const r = s % 60;
|
|
13441
|
+
return `${m}:${r.toString().padStart(2, "0")}`;
|
|
13442
|
+
}
|
|
13443
|
+
function esc(value) {
|
|
13444
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13445
|
+
}
|
|
13446
|
+
|
|
13447
|
+
// src/study/SessionDebugger.ts
|
|
13159
13448
|
var activeSession = null;
|
|
13160
13449
|
var sessionHistory = [];
|
|
13161
13450
|
var MAX_HISTORY = 5;
|
|
@@ -13336,6 +13625,13 @@ var sessionDebugAPI = {
|
|
|
13336
13625
|
showQueue() {
|
|
13337
13626
|
showCurrentQueue();
|
|
13338
13627
|
},
|
|
13628
|
+
/**
|
|
13629
|
+
* Toggle the pinned, live-updating DOM overlay for the active controller
|
|
13630
|
+
* (queues, session hints, timer). No-ops in non-browser hosts.
|
|
13631
|
+
*/
|
|
13632
|
+
dbgOverlay() {
|
|
13633
|
+
toggleSessionOverlay();
|
|
13634
|
+
},
|
|
13339
13635
|
/**
|
|
13340
13636
|
* Show presentation history for current or past session.
|
|
13341
13637
|
*/
|
|
@@ -13397,6 +13693,7 @@ var sessionDebugAPI = {
|
|
|
13397
13693
|
\u{1F3AF} Session Debug API
|
|
13398
13694
|
|
|
13399
13695
|
Commands:
|
|
13696
|
+
.dbgOverlay() Toggle the pinned live overlay (queues, hints, timer)
|
|
13400
13697
|
.showQueue() Show current queue state (active session only)
|
|
13401
13698
|
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
13402
13699
|
.showInterleaving(index?) Analyze course interleaving pattern
|
|
@@ -13465,6 +13762,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13465
13762
|
* Used by nextCard() to await completion before drawing from queues.
|
|
13466
13763
|
*/
|
|
13467
13764
|
_replanPromise = null;
|
|
13765
|
+
/**
|
|
13766
|
+
* Reason for the replan currently executing in `_runReplan`, surfaced by the
|
|
13767
|
+
* debug overlay's spinner. The caller's `opts.label` when present, else
|
|
13768
|
+
* `'(auto)'`. Only meaningful while `_replanPromise` is non-null; cleared
|
|
13769
|
+
* when the in-flight chain settles.
|
|
13770
|
+
*/
|
|
13771
|
+
_activeReplanLabel = null;
|
|
13468
13772
|
/**
|
|
13469
13773
|
* Number of well-indicated new cards remaining before the queue
|
|
13470
13774
|
* degrades to poorly-indicated content. Decremented on each newQ
|
|
@@ -13499,6 +13803,21 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13499
13803
|
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
13500
13804
|
*/
|
|
13501
13805
|
_sessionHints = null;
|
|
13806
|
+
/**
|
|
13807
|
+
* Card IDs that have been *served* (drawn/consumed) this session. Populated
|
|
13808
|
+
* at the single consumption choke-point (removeItemFromQueue), so it reflects
|
|
13809
|
+
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
13810
|
+
* lands once the card is *responded to*.
|
|
13811
|
+
*
|
|
13812
|
+
* Used to keep already-served cards out of newQ on every (re)plan: a `new`
|
|
13813
|
+
* card shown once must never re-enter newQ this session. This is the general
|
|
13814
|
+
* guard against re-presentation — including the case where a replan in flight
|
|
13815
|
+
* captured a now-drawn card (e.g. a +INF require-injected follow-up the
|
|
13816
|
+
* depletion prefetch grabbed just before it was drawn). Reviews/failed cards
|
|
13817
|
+
* legitimately recur and are tracked by their own queues, so this only gates
|
|
13818
|
+
* `new`-origin candidates.
|
|
13819
|
+
*/
|
|
13820
|
+
_servedCardIds = /* @__PURE__ */ new Set();
|
|
13502
13821
|
/**
|
|
13503
13822
|
* Consumer-supplied hooks invoked after each question response is processed.
|
|
13504
13823
|
* Seeded from constructor options (threaded from
|
|
@@ -13577,6 +13896,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13577
13896
|
endTime: ${this.endTime}
|
|
13578
13897
|
defaultBatchLimit: ${this._defaultBatchLimit}
|
|
13579
13898
|
initialReviewCap: ${this._initialReviewCap}`);
|
|
13899
|
+
registerActiveController(this);
|
|
13580
13900
|
}
|
|
13581
13901
|
tick() {
|
|
13582
13902
|
this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
|
|
@@ -13677,15 +13997,23 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13677
13997
|
);
|
|
13678
13998
|
const inflight = this._replanPromise;
|
|
13679
13999
|
const queued = inflight.catch(() => void 0).then(() => this._runReplan(opts));
|
|
13680
|
-
|
|
13681
|
-
if (this._replanPromise ===
|
|
14000
|
+
const tracked2 = queued.finally(() => {
|
|
14001
|
+
if (this._replanPromise === tracked2) {
|
|
14002
|
+
this._replanPromise = null;
|
|
14003
|
+
this._activeReplanLabel = null;
|
|
14004
|
+
}
|
|
13682
14005
|
});
|
|
14006
|
+
this._replanPromise = tracked2;
|
|
13683
14007
|
return queued;
|
|
13684
14008
|
}
|
|
13685
14009
|
const run = this._runReplan(opts);
|
|
13686
|
-
|
|
13687
|
-
if (this._replanPromise ===
|
|
14010
|
+
const tracked = run.finally(() => {
|
|
14011
|
+
if (this._replanPromise === tracked) {
|
|
14012
|
+
this._replanPromise = null;
|
|
14013
|
+
this._activeReplanLabel = null;
|
|
14014
|
+
}
|
|
13688
14015
|
});
|
|
14016
|
+
this._replanPromise = tracked;
|
|
13689
14017
|
await run;
|
|
13690
14018
|
}
|
|
13691
14019
|
/**
|
|
@@ -13694,12 +14022,12 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13694
14022
|
* triggers in nextCard) return false and may coalesce.
|
|
13695
14023
|
*/
|
|
13696
14024
|
_replanHasIntent(opts) {
|
|
13697
|
-
if (opts.label) return true;
|
|
13698
14025
|
if (opts.limit !== void 0) return true;
|
|
13699
14026
|
if (opts.minFollowUpCards !== void 0) return true;
|
|
13700
14027
|
if (opts.mode && opts.mode !== "replace") return true;
|
|
13701
14028
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13702
14029
|
if (opts.sessionHints !== void 0) return true;
|
|
14030
|
+
if (opts.mergeSessionHints !== void 0) return true;
|
|
13703
14031
|
return false;
|
|
13704
14032
|
}
|
|
13705
14033
|
/**
|
|
@@ -13715,6 +14043,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13715
14043
|
* newQ.peek(0) is the imminent draw we need to exclude.
|
|
13716
14044
|
*/
|
|
13717
14045
|
async _runReplan(opts) {
|
|
14046
|
+
this._activeReplanLabel = opts.label ?? "(auto)";
|
|
13718
14047
|
if (!opts.hints) opts.hints = {};
|
|
13719
14048
|
const hints = opts.hints;
|
|
13720
14049
|
const excludeSet = new Set(hints.excludeCards ?? []);
|
|
@@ -13734,6 +14063,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13734
14063
|
`[Replan] Session hints ${opts.sessionHints ? "set" : "cleared"}: ${JSON.stringify(opts.sessionHints)}`
|
|
13735
14064
|
);
|
|
13736
14065
|
}
|
|
14066
|
+
if (opts.mergeSessionHints !== void 0) {
|
|
14067
|
+
this._sessionHints = mergeHints2([this._sessionHints, opts.mergeSessionHints]) ?? null;
|
|
14068
|
+
this.log(`[Replan] Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
14069
|
+
}
|
|
13737
14070
|
this._applyHintsToSources(opts.hints, opts.label);
|
|
13738
14071
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13739
14072
|
this.log(
|
|
@@ -13772,6 +14105,45 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13772
14105
|
getSessionHints() {
|
|
13773
14106
|
return this._sessionHints;
|
|
13774
14107
|
}
|
|
14108
|
+
/**
|
|
14109
|
+
* Live state snapshot for the debug overlay (window.skuilder.session
|
|
14110
|
+
* .dbgOverlay()). Reads directly from the private queues and hints, so it
|
|
14111
|
+
* always reflects the current moment — unlike the passive SessionDebugger
|
|
14112
|
+
* snapshots, which only capture what was explicitly pushed to them.
|
|
14113
|
+
*/
|
|
14114
|
+
getDebugSnapshot() {
|
|
14115
|
+
const describe = (q) => {
|
|
14116
|
+
const cards = [];
|
|
14117
|
+
for (let i = 0; i < q.length; i++) {
|
|
14118
|
+
cards.push(q.peek(i).cardID);
|
|
14119
|
+
}
|
|
14120
|
+
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
14121
|
+
};
|
|
14122
|
+
const drawnCards = this._sessionRecord.map((r) => {
|
|
14123
|
+
const last = r.records[r.records.length - 1];
|
|
14124
|
+
return {
|
|
14125
|
+
cardID: r.item.cardID,
|
|
14126
|
+
status: r.item.status,
|
|
14127
|
+
attempts: r.records.length,
|
|
14128
|
+
correct: last && isQuestionRecord(last) ? last.isCorrect : null,
|
|
14129
|
+
timeSpentMs: r.records.reduce((sum, rec) => sum + rec.timeSpent, 0)
|
|
14130
|
+
};
|
|
14131
|
+
});
|
|
14132
|
+
return {
|
|
14133
|
+
secondsRemaining: this.secondsRemaining,
|
|
14134
|
+
hasCardGuarantee: this.hasCardGuarantee,
|
|
14135
|
+
minCardsGuarantee: this._minCardsGuarantee,
|
|
14136
|
+
wellIndicatedRemaining: this._wellIndicatedRemaining,
|
|
14137
|
+
currentCard: this._currentCard?.item.cardID ?? null,
|
|
14138
|
+
sessionHints: this._sessionHints,
|
|
14139
|
+
replanActive: this._replanPromise !== null,
|
|
14140
|
+
replanLabel: this._activeReplanLabel,
|
|
14141
|
+
reviewQ: describe(this.reviewQ),
|
|
14142
|
+
newQ: describe(this.newQ),
|
|
14143
|
+
failedQ: describe(this.failedQ),
|
|
14144
|
+
drawnCards
|
|
14145
|
+
};
|
|
14146
|
+
}
|
|
13775
14147
|
/**
|
|
13776
14148
|
* Merge `hints` into the durable session hints via the pipeline's
|
|
13777
14149
|
* `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
|
|
@@ -13853,9 +14225,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13853
14225
|
*/
|
|
13854
14226
|
async _replanUncoalesced(opts) {
|
|
13855
14227
|
const run = this._runReplan(opts);
|
|
13856
|
-
|
|
13857
|
-
if (this._replanPromise ===
|
|
14228
|
+
const tracked = run.finally(() => {
|
|
14229
|
+
if (this._replanPromise === tracked) {
|
|
14230
|
+
this._replanPromise = null;
|
|
14231
|
+
this._activeReplanLabel = null;
|
|
14232
|
+
}
|
|
13858
14233
|
});
|
|
14234
|
+
this._replanPromise = tracked;
|
|
13859
14235
|
await run;
|
|
13860
14236
|
}
|
|
13861
14237
|
/**
|
|
@@ -14074,7 +14450,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14074
14450
|
mixedWeighted
|
|
14075
14451
|
);
|
|
14076
14452
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14077
|
-
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new").slice(0, newLimit);
|
|
14453
|
+
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)).slice(0, newLimit);
|
|
14078
14454
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14079
14455
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14080
14456
|
if (!replan) {
|
|
@@ -14211,14 +14587,14 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14211
14587
|
this.log(
|
|
14212
14588
|
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14213
14589
|
);
|
|
14214
|
-
void this.requestReplan();
|
|
14590
|
+
void this.requestReplan({ label: "auto:depletion", mode: "merge" });
|
|
14215
14591
|
}
|
|
14216
14592
|
const REPLAN_BUFFER = 3;
|
|
14217
14593
|
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
|
|
14218
14594
|
this.log(
|
|
14219
14595
|
`[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
|
|
14220
14596
|
);
|
|
14221
|
-
void this.requestReplan();
|
|
14597
|
+
void this.requestReplan({ label: "auto:quality" });
|
|
14222
14598
|
}
|
|
14223
14599
|
if (this._secondsRemaining <= 0 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
|
|
14224
14600
|
this._currentCard = null;
|
|
@@ -14352,6 +14728,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14352
14728
|
* Remove an item from its source queue after consumption by nextCard().
|
|
14353
14729
|
*/
|
|
14354
14730
|
removeItemFromQueue(item) {
|
|
14731
|
+
this._clearDurableRequirement(item.cardID);
|
|
14732
|
+
this._servedCardIds.add(item.cardID);
|
|
14355
14733
|
if (this.reviewQ.peek(0)?.cardID === item.cardID) {
|
|
14356
14734
|
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
14357
14735
|
} else if (this.newQ.peek(0)?.cardID === item.cardID) {
|
|
@@ -14363,6 +14741,27 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14363
14741
|
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
14364
14742
|
}
|
|
14365
14743
|
}
|
|
14744
|
+
/**
|
|
14745
|
+
* Remove a satisfied card ID from the durable session-hint `requireCards`
|
|
14746
|
+
* list. Called when a card is consumed (see removeItemFromQueue). No-op if
|
|
14747
|
+
* the card was not a durable requirement.
|
|
14748
|
+
*
|
|
14749
|
+
* Matches literal IDs only: a glob/pattern requirement (which may stand for
|
|
14750
|
+
* several cards) is NOT considered satisfied by a single draw and is left in
|
|
14751
|
+
* place — durable patterns are the caller's responsibility, one-shot `hints`
|
|
14752
|
+
* remain the right tool for them.
|
|
14753
|
+
*/
|
|
14754
|
+
_clearDurableRequirement(cardID) {
|
|
14755
|
+
const req = this._sessionHints?.requireCards;
|
|
14756
|
+
if (!req || req.length === 0) return;
|
|
14757
|
+
const next = req.filter((id) => id !== cardID);
|
|
14758
|
+
if (next.length === req.length) return;
|
|
14759
|
+
this._sessionHints = {
|
|
14760
|
+
...this._sessionHints,
|
|
14761
|
+
requireCards: next.length > 0 ? next : void 0
|
|
14762
|
+
};
|
|
14763
|
+
this.log(`[Replan] Durable requirement satisfied & cleared on draw: ${cardID}`);
|
|
14764
|
+
}
|
|
14366
14765
|
/**
|
|
14367
14766
|
* End the session and record learning outcomes.
|
|
14368
14767
|
*
|