@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.mjs
CHANGED
|
@@ -1917,12 +1917,13 @@ __export(elo_exports, {
|
|
|
1917
1917
|
default: () => ELONavigator
|
|
1918
1918
|
});
|
|
1919
1919
|
import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
|
|
1920
|
-
var ELONavigator;
|
|
1920
|
+
var ELO_RELEVANCE_SIGMA, ELONavigator;
|
|
1921
1921
|
var init_elo = __esm({
|
|
1922
1922
|
"src/core/navigators/generators/elo.ts"() {
|
|
1923
1923
|
"use strict";
|
|
1924
1924
|
init_navigators();
|
|
1925
1925
|
init_logger();
|
|
1926
|
+
ELO_RELEVANCE_SIGMA = 300;
|
|
1926
1927
|
ELONavigator = class extends ContentNavigator {
|
|
1927
1928
|
/** Human-readable name for CardGenerator interface */
|
|
1928
1929
|
name;
|
|
@@ -1962,8 +1963,8 @@ var init_elo = __esm({
|
|
|
1962
1963
|
const scored = newCards.map((c) => {
|
|
1963
1964
|
const cardElo = c.elo ?? 1e3;
|
|
1964
1965
|
const distance = Math.abs(cardElo - userGlobalElo);
|
|
1965
|
-
const
|
|
1966
|
-
const samplingKey =
|
|
1966
|
+
const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
|
|
1967
|
+
const samplingKey = relevance * (0.5 + 0.5 * Math.random());
|
|
1967
1968
|
return {
|
|
1968
1969
|
cardId: c.cardID,
|
|
1969
1970
|
courseId: c.courseID,
|
|
@@ -1975,7 +1976,7 @@ var init_elo = __esm({
|
|
|
1975
1976
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
|
|
1976
1977
|
action: "generated",
|
|
1977
1978
|
score: samplingKey,
|
|
1978
|
-
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}),
|
|
1979
|
+
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
|
|
1979
1980
|
}
|
|
1980
1981
|
]
|
|
1981
1982
|
};
|
|
@@ -13054,6 +13055,294 @@ mountMixerDebugger();
|
|
|
13054
13055
|
// src/study/SessionDebugger.ts
|
|
13055
13056
|
init_logger();
|
|
13056
13057
|
init_PipelineDebugger();
|
|
13058
|
+
|
|
13059
|
+
// src/study/SessionOverlay.ts
|
|
13060
|
+
init_logger();
|
|
13061
|
+
var activeController = null;
|
|
13062
|
+
function registerActiveController(controller) {
|
|
13063
|
+
activeController = controller;
|
|
13064
|
+
}
|
|
13065
|
+
function getActiveController() {
|
|
13066
|
+
return activeController;
|
|
13067
|
+
}
|
|
13068
|
+
var OVERLAY_ID = "skuilder-session-overlay";
|
|
13069
|
+
var POLL_MS = 300;
|
|
13070
|
+
var INLINE_THRESHOLD = 5;
|
|
13071
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
13072
|
+
var spinnerFrame = 0;
|
|
13073
|
+
var overlayEl = null;
|
|
13074
|
+
var pollHandle = null;
|
|
13075
|
+
var lastSnapshot = null;
|
|
13076
|
+
var copyFlashUntil = 0;
|
|
13077
|
+
var minified = false;
|
|
13078
|
+
var expanded = {
|
|
13079
|
+
reviewQ: false,
|
|
13080
|
+
newQ: false,
|
|
13081
|
+
failedQ: false,
|
|
13082
|
+
drawn: false
|
|
13083
|
+
};
|
|
13084
|
+
function toggleSessionOverlay() {
|
|
13085
|
+
if (typeof document === "undefined") {
|
|
13086
|
+
logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
|
|
13087
|
+
return;
|
|
13088
|
+
}
|
|
13089
|
+
if (overlayEl) {
|
|
13090
|
+
teardown();
|
|
13091
|
+
logger.info("[Session Overlay] Hidden.");
|
|
13092
|
+
} else {
|
|
13093
|
+
mount();
|
|
13094
|
+
logger.info("[Session Overlay] Shown. Toggle off with window.skuilder.session.dbgOverlay().");
|
|
13095
|
+
}
|
|
13096
|
+
}
|
|
13097
|
+
function mount() {
|
|
13098
|
+
minified = false;
|
|
13099
|
+
overlayEl = document.createElement("div");
|
|
13100
|
+
overlayEl.id = OVERLAY_ID;
|
|
13101
|
+
Object.assign(overlayEl.style, {
|
|
13102
|
+
position: "fixed",
|
|
13103
|
+
top: "8px",
|
|
13104
|
+
left: "8px",
|
|
13105
|
+
zIndex: "2147483647",
|
|
13106
|
+
maxWidth: "320px",
|
|
13107
|
+
maxHeight: "90vh",
|
|
13108
|
+
overflowY: "auto",
|
|
13109
|
+
padding: "8px 10px",
|
|
13110
|
+
background: "rgba(17, 24, 39, 0.92)",
|
|
13111
|
+
color: "#e5e7eb",
|
|
13112
|
+
font: "11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
13113
|
+
borderRadius: "6px",
|
|
13114
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
|
|
13115
|
+
pointerEvents: "auto",
|
|
13116
|
+
userSelect: "none"
|
|
13117
|
+
});
|
|
13118
|
+
document.body.appendChild(overlayEl);
|
|
13119
|
+
render();
|
|
13120
|
+
pollHandle = setInterval(render, POLL_MS);
|
|
13121
|
+
}
|
|
13122
|
+
function teardown() {
|
|
13123
|
+
if (pollHandle !== null) {
|
|
13124
|
+
clearInterval(pollHandle);
|
|
13125
|
+
pollHandle = null;
|
|
13126
|
+
}
|
|
13127
|
+
if (overlayEl?.parentNode) {
|
|
13128
|
+
overlayEl.parentNode.removeChild(overlayEl);
|
|
13129
|
+
}
|
|
13130
|
+
overlayEl = null;
|
|
13131
|
+
}
|
|
13132
|
+
function render() {
|
|
13133
|
+
if (!overlayEl) return;
|
|
13134
|
+
spinnerFrame++;
|
|
13135
|
+
const ctrl = getActiveController();
|
|
13136
|
+
if (!ctrl) {
|
|
13137
|
+
lastSnapshot = null;
|
|
13138
|
+
overlayEl.innerHTML = headerHtml() + (minified ? "" : `<div style="opacity:.65">No active session.</div>`);
|
|
13139
|
+
attachHandlers();
|
|
13140
|
+
return;
|
|
13141
|
+
}
|
|
13142
|
+
const s = ctrl.getDebugSnapshot();
|
|
13143
|
+
lastSnapshot = s;
|
|
13144
|
+
if (minified) {
|
|
13145
|
+
overlayEl.innerHTML = headerHtml();
|
|
13146
|
+
attachHandlers();
|
|
13147
|
+
return;
|
|
13148
|
+
}
|
|
13149
|
+
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);
|
|
13150
|
+
attachHandlers();
|
|
13151
|
+
}
|
|
13152
|
+
function attachHandlers() {
|
|
13153
|
+
if (!overlayEl) return;
|
|
13154
|
+
overlayEl.querySelectorAll("[data-q]").forEach((el) => {
|
|
13155
|
+
el.onclick = () => {
|
|
13156
|
+
const key = el.dataset.q;
|
|
13157
|
+
if (!key) return;
|
|
13158
|
+
expanded[key] = !expanded[key];
|
|
13159
|
+
render();
|
|
13160
|
+
};
|
|
13161
|
+
});
|
|
13162
|
+
const copyBtn = overlayEl.querySelector("[data-copy]");
|
|
13163
|
+
if (copyBtn) {
|
|
13164
|
+
copyBtn.onclick = (ev) => {
|
|
13165
|
+
ev.stopPropagation();
|
|
13166
|
+
copySnapshot();
|
|
13167
|
+
};
|
|
13168
|
+
}
|
|
13169
|
+
const minBtn = overlayEl.querySelector("[data-min]");
|
|
13170
|
+
if (minBtn) {
|
|
13171
|
+
minBtn.onclick = (ev) => {
|
|
13172
|
+
ev.stopPropagation();
|
|
13173
|
+
minified = !minified;
|
|
13174
|
+
render();
|
|
13175
|
+
};
|
|
13176
|
+
}
|
|
13177
|
+
}
|
|
13178
|
+
function copySnapshot() {
|
|
13179
|
+
const text = snapshotToText(lastSnapshot);
|
|
13180
|
+
const flash = () => {
|
|
13181
|
+
copyFlashUntil = Date.now() + 1200;
|
|
13182
|
+
render();
|
|
13183
|
+
};
|
|
13184
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
13185
|
+
if (clip?.writeText) {
|
|
13186
|
+
clip.writeText(text).then(flash, (err) => {
|
|
13187
|
+
logger.warn(`[Session Overlay] Clipboard write failed: ${String(err)}`);
|
|
13188
|
+
});
|
|
13189
|
+
} else {
|
|
13190
|
+
logger.info(`[Session Overlay] Clipboard unavailable; snapshot follows:
|
|
13191
|
+
${text}`);
|
|
13192
|
+
}
|
|
13193
|
+
}
|
|
13194
|
+
function headerHtml() {
|
|
13195
|
+
const flashing = Date.now() < copyFlashUntil;
|
|
13196
|
+
const btnLabel = flashing ? "\u2713 copied" : "\u2398 copy";
|
|
13197
|
+
const btnColor = flashing ? "#86efac" : "#93c5fd";
|
|
13198
|
+
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>`;
|
|
13199
|
+
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>`;
|
|
13200
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:${minified ? 0 : 4}px">${copyBtn}${minBtn}\u2699 SessionController</div>`;
|
|
13201
|
+
}
|
|
13202
|
+
function replanHtml(s) {
|
|
13203
|
+
if (!s.replanActive) {
|
|
13204
|
+
return `<div style="margin-bottom:6px;opacity:.45">\u25CB idle</div>`;
|
|
13205
|
+
}
|
|
13206
|
+
const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
|
13207
|
+
const reason = esc(s.replanLabel ?? "(auto)");
|
|
13208
|
+
return `<div style="margin-bottom:6px;color:#fde047">${frame} replanning <span style="opacity:.85">[${reason}]</span></div>`;
|
|
13209
|
+
}
|
|
13210
|
+
function metaHtml(s) {
|
|
13211
|
+
const mmss = formatTime(s.secondsRemaining);
|
|
13212
|
+
const guarantee = s.hasCardGuarantee ? ` \xB7 <span style="color:#fbbf24">guarantee ${s.minCardsGuarantee}</span>` : "";
|
|
13213
|
+
const rows = [
|
|
13214
|
+
`time ${mmss}${guarantee}`,
|
|
13215
|
+
`well-indicated left: ${s.wellIndicatedRemaining}`,
|
|
13216
|
+
`current: ${s.currentCard ? esc(s.currentCard) : '<span style="opacity:.6">\u2014</span>'}`
|
|
13217
|
+
];
|
|
13218
|
+
return `<div style="margin-bottom:6px">${rows.map((r) => `<div>${r}</div>`).join("")}</div>`;
|
|
13219
|
+
}
|
|
13220
|
+
function hintsHtml(h) {
|
|
13221
|
+
const parts = [];
|
|
13222
|
+
if (h) {
|
|
13223
|
+
if (h.boostTags && Object.keys(h.boostTags).length) {
|
|
13224
|
+
parts.push(
|
|
13225
|
+
`boost: ` + Object.entries(h.boostTags).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
|
|
13226
|
+
);
|
|
13227
|
+
}
|
|
13228
|
+
if (h.boostCards && Object.keys(h.boostCards).length) {
|
|
13229
|
+
parts.push(
|
|
13230
|
+
`boostCards: ` + Object.entries(h.boostCards).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
|
|
13231
|
+
);
|
|
13232
|
+
}
|
|
13233
|
+
if (h.requireCards?.length) parts.push(`require: ${h.requireCards.map(esc).join(", ")}`);
|
|
13234
|
+
if (h.requireTags?.length) parts.push(`requireTags: ${h.requireTags.map(esc).join(", ")}`);
|
|
13235
|
+
if (h.excludeTags?.length) parts.push(`exclude: ${h.excludeTags.map(esc).join(", ")}`);
|
|
13236
|
+
if (h.excludeCards?.length) parts.push(`excludeCards: ${h.excludeCards.map(esc).join(", ")}`);
|
|
13237
|
+
}
|
|
13238
|
+
const body = parts.length ? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join("") : `<div style="margin-left:6px;opacity:.6">none</div>`;
|
|
13239
|
+
return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
|
|
13240
|
+
}
|
|
13241
|
+
function queueHtml(key, label, q) {
|
|
13242
|
+
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13243
|
+
const isOpen = collapsible && expanded[key];
|
|
13244
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13245
|
+
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
|
|
13246
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
|
|
13247
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13248
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
13249
|
+
if (!q.cards.length) {
|
|
13250
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
13251
|
+
}
|
|
13252
|
+
const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
|
|
13253
|
+
const hiddenCount = q.length - shown.length;
|
|
13254
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13255
|
+
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>`;
|
|
13256
|
+
if (collapsible) {
|
|
13257
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13258
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13259
|
+
}
|
|
13260
|
+
return title + body;
|
|
13261
|
+
}
|
|
13262
|
+
function outcomeGlyph(correct) {
|
|
13263
|
+
if (correct === true) return `<span style="color:#86efac">\u2713</span>`;
|
|
13264
|
+
if (correct === false) return `<span style="color:#fca5a5">\u2717</span>`;
|
|
13265
|
+
return `<span style="opacity:.5">\xB7</span>`;
|
|
13266
|
+
}
|
|
13267
|
+
function drawnHtml(key, drawn) {
|
|
13268
|
+
const collapsible = drawn.length > INLINE_THRESHOLD;
|
|
13269
|
+
const isOpen = collapsible && expanded[key];
|
|
13270
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13271
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#c4b5fd" : "color:#c4b5fd";
|
|
13272
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13273
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}drawn: ${drawn.length}</div>`;
|
|
13274
|
+
if (!drawn.length) {
|
|
13275
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">none yet</div>`;
|
|
13276
|
+
}
|
|
13277
|
+
const shown = isOpen ? drawn : drawn.slice(0, INLINE_THRESHOLD);
|
|
13278
|
+
const hiddenCount = drawn.length - shown.length;
|
|
13279
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13280
|
+
const rows = shown.map((d) => {
|
|
13281
|
+
const retries = d.attempts > 1 ? `<span style="opacity:.5"> \xD7${d.attempts}</span>` : "";
|
|
13282
|
+
const time = `<span style="opacity:.45"> ${Math.round(d.timeSpentMs / 100) / 10}s</span>`;
|
|
13283
|
+
return `<li style="white-space:nowrap">${outcomeGlyph(d.correct)} ${esc(d.cardID)}<span style="opacity:.5"> [${esc(d.status)}]</span>${retries}${time}</li>`;
|
|
13284
|
+
}).join("");
|
|
13285
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">${rows}</ol>`;
|
|
13286
|
+
if (collapsible) {
|
|
13287
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13288
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13289
|
+
}
|
|
13290
|
+
return title + body;
|
|
13291
|
+
}
|
|
13292
|
+
function snapshotToText(s) {
|
|
13293
|
+
if (!s) return "SessionController \u2014 no active session.";
|
|
13294
|
+
const lines = [];
|
|
13295
|
+
lines.push("=== SessionController ===");
|
|
13296
|
+
lines.push(`time ${formatTime(s.secondsRemaining)}`);
|
|
13297
|
+
if (s.hasCardGuarantee) lines.push(`guarantee: ${s.minCardsGuarantee}`);
|
|
13298
|
+
lines.push(`well-indicated left: ${s.wellIndicatedRemaining}`);
|
|
13299
|
+
lines.push(`current: ${s.currentCard ?? "\u2014"}`);
|
|
13300
|
+
lines.push(
|
|
13301
|
+
s.replanActive ? `replan: ACTIVE [${s.replanLabel ?? "(auto)"}]` : "replan: idle"
|
|
13302
|
+
);
|
|
13303
|
+
lines.push("");
|
|
13304
|
+
lines.push("sessionHints:");
|
|
13305
|
+
const h = s.sessionHints;
|
|
13306
|
+
const hintParts = [];
|
|
13307
|
+
if (h) {
|
|
13308
|
+
if (h.boostTags && Object.keys(h.boostTags).length)
|
|
13309
|
+
hintParts.push(` boost: ${Object.entries(h.boostTags).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13310
|
+
if (h.boostCards && Object.keys(h.boostCards).length)
|
|
13311
|
+
hintParts.push(` boostCards: ${Object.entries(h.boostCards).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13312
|
+
if (h.requireCards?.length) hintParts.push(` require: ${h.requireCards.join(", ")}`);
|
|
13313
|
+
if (h.requireTags?.length) hintParts.push(` requireTags: ${h.requireTags.join(", ")}`);
|
|
13314
|
+
if (h.excludeTags?.length) hintParts.push(` exclude: ${h.excludeTags.join(", ")}`);
|
|
13315
|
+
if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
|
|
13316
|
+
}
|
|
13317
|
+
lines.push(hintParts.length ? hintParts.join("\n") : " none");
|
|
13318
|
+
const queueText = (label, q) => {
|
|
13319
|
+
lines.push("");
|
|
13320
|
+
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
13321
|
+
q.cards.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
|
|
13322
|
+
};
|
|
13323
|
+
queueText("reviewQ", s.reviewQ);
|
|
13324
|
+
queueText("newQ", s.newQ);
|
|
13325
|
+
queueText("failedQ", s.failedQ);
|
|
13326
|
+
lines.push("");
|
|
13327
|
+
lines.push(`drawn: ${s.drawnCards.length}`);
|
|
13328
|
+
s.drawnCards.forEach((d, i) => {
|
|
13329
|
+
const mark = d.correct === true ? "\u2713" : d.correct === false ? "\u2717" : "\xB7";
|
|
13330
|
+
const time = `${Math.round(d.timeSpentMs / 100) / 10}s`;
|
|
13331
|
+
lines.push(` ${i + 1}. ${mark} ${d.cardID} [${d.status}] \xD7${d.attempts} ${time}`);
|
|
13332
|
+
});
|
|
13333
|
+
return lines.join("\n");
|
|
13334
|
+
}
|
|
13335
|
+
function formatTime(totalSeconds) {
|
|
13336
|
+
const s = Math.max(0, Math.round(totalSeconds));
|
|
13337
|
+
const m = Math.floor(s / 60);
|
|
13338
|
+
const r = s % 60;
|
|
13339
|
+
return `${m}:${r.toString().padStart(2, "0")}`;
|
|
13340
|
+
}
|
|
13341
|
+
function esc(value) {
|
|
13342
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13343
|
+
}
|
|
13344
|
+
|
|
13345
|
+
// src/study/SessionDebugger.ts
|
|
13057
13346
|
var activeSession = null;
|
|
13058
13347
|
var sessionHistory = [];
|
|
13059
13348
|
var MAX_HISTORY = 5;
|
|
@@ -13234,6 +13523,13 @@ var sessionDebugAPI = {
|
|
|
13234
13523
|
showQueue() {
|
|
13235
13524
|
showCurrentQueue();
|
|
13236
13525
|
},
|
|
13526
|
+
/**
|
|
13527
|
+
* Toggle the pinned, live-updating DOM overlay for the active controller
|
|
13528
|
+
* (queues, session hints, timer). No-ops in non-browser hosts.
|
|
13529
|
+
*/
|
|
13530
|
+
dbgOverlay() {
|
|
13531
|
+
toggleSessionOverlay();
|
|
13532
|
+
},
|
|
13237
13533
|
/**
|
|
13238
13534
|
* Show presentation history for current or past session.
|
|
13239
13535
|
*/
|
|
@@ -13295,6 +13591,7 @@ var sessionDebugAPI = {
|
|
|
13295
13591
|
\u{1F3AF} Session Debug API
|
|
13296
13592
|
|
|
13297
13593
|
Commands:
|
|
13594
|
+
.dbgOverlay() Toggle the pinned live overlay (queues, hints, timer)
|
|
13298
13595
|
.showQueue() Show current queue state (active session only)
|
|
13299
13596
|
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
13300
13597
|
.showInterleaving(index?) Analyze course interleaving pattern
|
|
@@ -13363,6 +13660,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13363
13660
|
* Used by nextCard() to await completion before drawing from queues.
|
|
13364
13661
|
*/
|
|
13365
13662
|
_replanPromise = null;
|
|
13663
|
+
/**
|
|
13664
|
+
* Reason for the replan currently executing in `_runReplan`, surfaced by the
|
|
13665
|
+
* debug overlay's spinner. The caller's `opts.label` when present, else
|
|
13666
|
+
* `'(auto)'`. Only meaningful while `_replanPromise` is non-null; cleared
|
|
13667
|
+
* when the in-flight chain settles.
|
|
13668
|
+
*/
|
|
13669
|
+
_activeReplanLabel = null;
|
|
13366
13670
|
/**
|
|
13367
13671
|
* Number of well-indicated new cards remaining before the queue
|
|
13368
13672
|
* degrades to poorly-indicated content. Decremented on each newQ
|
|
@@ -13397,6 +13701,21 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13397
13701
|
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
13398
13702
|
*/
|
|
13399
13703
|
_sessionHints = null;
|
|
13704
|
+
/**
|
|
13705
|
+
* Card IDs that have been *served* (drawn/consumed) this session. Populated
|
|
13706
|
+
* at the single consumption choke-point (removeItemFromQueue), so it reflects
|
|
13707
|
+
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
13708
|
+
* lands once the card is *responded to*.
|
|
13709
|
+
*
|
|
13710
|
+
* Used to keep already-served cards out of newQ on every (re)plan: a `new`
|
|
13711
|
+
* card shown once must never re-enter newQ this session. This is the general
|
|
13712
|
+
* guard against re-presentation — including the case where a replan in flight
|
|
13713
|
+
* captured a now-drawn card (e.g. a +INF require-injected follow-up the
|
|
13714
|
+
* depletion prefetch grabbed just before it was drawn). Reviews/failed cards
|
|
13715
|
+
* legitimately recur and are tracked by their own queues, so this only gates
|
|
13716
|
+
* `new`-origin candidates.
|
|
13717
|
+
*/
|
|
13718
|
+
_servedCardIds = /* @__PURE__ */ new Set();
|
|
13400
13719
|
/**
|
|
13401
13720
|
* Consumer-supplied hooks invoked after each question response is processed.
|
|
13402
13721
|
* Seeded from constructor options (threaded from
|
|
@@ -13475,6 +13794,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13475
13794
|
endTime: ${this.endTime}
|
|
13476
13795
|
defaultBatchLimit: ${this._defaultBatchLimit}
|
|
13477
13796
|
initialReviewCap: ${this._initialReviewCap}`);
|
|
13797
|
+
registerActiveController(this);
|
|
13478
13798
|
}
|
|
13479
13799
|
tick() {
|
|
13480
13800
|
this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
|
|
@@ -13575,15 +13895,23 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13575
13895
|
);
|
|
13576
13896
|
const inflight = this._replanPromise;
|
|
13577
13897
|
const queued = inflight.catch(() => void 0).then(() => this._runReplan(opts));
|
|
13578
|
-
|
|
13579
|
-
if (this._replanPromise ===
|
|
13898
|
+
const tracked2 = queued.finally(() => {
|
|
13899
|
+
if (this._replanPromise === tracked2) {
|
|
13900
|
+
this._replanPromise = null;
|
|
13901
|
+
this._activeReplanLabel = null;
|
|
13902
|
+
}
|
|
13580
13903
|
});
|
|
13904
|
+
this._replanPromise = tracked2;
|
|
13581
13905
|
return queued;
|
|
13582
13906
|
}
|
|
13583
13907
|
const run = this._runReplan(opts);
|
|
13584
|
-
|
|
13585
|
-
if (this._replanPromise ===
|
|
13908
|
+
const tracked = run.finally(() => {
|
|
13909
|
+
if (this._replanPromise === tracked) {
|
|
13910
|
+
this._replanPromise = null;
|
|
13911
|
+
this._activeReplanLabel = null;
|
|
13912
|
+
}
|
|
13586
13913
|
});
|
|
13914
|
+
this._replanPromise = tracked;
|
|
13587
13915
|
await run;
|
|
13588
13916
|
}
|
|
13589
13917
|
/**
|
|
@@ -13592,12 +13920,12 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13592
13920
|
* triggers in nextCard) return false and may coalesce.
|
|
13593
13921
|
*/
|
|
13594
13922
|
_replanHasIntent(opts) {
|
|
13595
|
-
if (opts.label) return true;
|
|
13596
13923
|
if (opts.limit !== void 0) return true;
|
|
13597
13924
|
if (opts.minFollowUpCards !== void 0) return true;
|
|
13598
13925
|
if (opts.mode && opts.mode !== "replace") return true;
|
|
13599
13926
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13600
13927
|
if (opts.sessionHints !== void 0) return true;
|
|
13928
|
+
if (opts.mergeSessionHints !== void 0) return true;
|
|
13601
13929
|
return false;
|
|
13602
13930
|
}
|
|
13603
13931
|
/**
|
|
@@ -13613,6 +13941,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13613
13941
|
* newQ.peek(0) is the imminent draw we need to exclude.
|
|
13614
13942
|
*/
|
|
13615
13943
|
async _runReplan(opts) {
|
|
13944
|
+
this._activeReplanLabel = opts.label ?? "(auto)";
|
|
13616
13945
|
if (!opts.hints) opts.hints = {};
|
|
13617
13946
|
const hints = opts.hints;
|
|
13618
13947
|
const excludeSet = new Set(hints.excludeCards ?? []);
|
|
@@ -13632,6 +13961,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13632
13961
|
`[Replan] Session hints ${opts.sessionHints ? "set" : "cleared"}: ${JSON.stringify(opts.sessionHints)}`
|
|
13633
13962
|
);
|
|
13634
13963
|
}
|
|
13964
|
+
if (opts.mergeSessionHints !== void 0) {
|
|
13965
|
+
this._sessionHints = mergeHints2([this._sessionHints, opts.mergeSessionHints]) ?? null;
|
|
13966
|
+
this.log(`[Replan] Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
13967
|
+
}
|
|
13635
13968
|
this._applyHintsToSources(opts.hints, opts.label);
|
|
13636
13969
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13637
13970
|
this.log(
|
|
@@ -13670,6 +14003,45 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13670
14003
|
getSessionHints() {
|
|
13671
14004
|
return this._sessionHints;
|
|
13672
14005
|
}
|
|
14006
|
+
/**
|
|
14007
|
+
* Live state snapshot for the debug overlay (window.skuilder.session
|
|
14008
|
+
* .dbgOverlay()). Reads directly from the private queues and hints, so it
|
|
14009
|
+
* always reflects the current moment — unlike the passive SessionDebugger
|
|
14010
|
+
* snapshots, which only capture what was explicitly pushed to them.
|
|
14011
|
+
*/
|
|
14012
|
+
getDebugSnapshot() {
|
|
14013
|
+
const describe = (q) => {
|
|
14014
|
+
const cards = [];
|
|
14015
|
+
for (let i = 0; i < q.length; i++) {
|
|
14016
|
+
cards.push(q.peek(i).cardID);
|
|
14017
|
+
}
|
|
14018
|
+
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
14019
|
+
};
|
|
14020
|
+
const drawnCards = this._sessionRecord.map((r) => {
|
|
14021
|
+
const last = r.records[r.records.length - 1];
|
|
14022
|
+
return {
|
|
14023
|
+
cardID: r.item.cardID,
|
|
14024
|
+
status: r.item.status,
|
|
14025
|
+
attempts: r.records.length,
|
|
14026
|
+
correct: last && isQuestionRecord(last) ? last.isCorrect : null,
|
|
14027
|
+
timeSpentMs: r.records.reduce((sum, rec) => sum + rec.timeSpent, 0)
|
|
14028
|
+
};
|
|
14029
|
+
});
|
|
14030
|
+
return {
|
|
14031
|
+
secondsRemaining: this.secondsRemaining,
|
|
14032
|
+
hasCardGuarantee: this.hasCardGuarantee,
|
|
14033
|
+
minCardsGuarantee: this._minCardsGuarantee,
|
|
14034
|
+
wellIndicatedRemaining: this._wellIndicatedRemaining,
|
|
14035
|
+
currentCard: this._currentCard?.item.cardID ?? null,
|
|
14036
|
+
sessionHints: this._sessionHints,
|
|
14037
|
+
replanActive: this._replanPromise !== null,
|
|
14038
|
+
replanLabel: this._activeReplanLabel,
|
|
14039
|
+
reviewQ: describe(this.reviewQ),
|
|
14040
|
+
newQ: describe(this.newQ),
|
|
14041
|
+
failedQ: describe(this.failedQ),
|
|
14042
|
+
drawnCards
|
|
14043
|
+
};
|
|
14044
|
+
}
|
|
13673
14045
|
/**
|
|
13674
14046
|
* Merge `hints` into the durable session hints via the pipeline's
|
|
13675
14047
|
* `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
|
|
@@ -13751,9 +14123,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13751
14123
|
*/
|
|
13752
14124
|
async _replanUncoalesced(opts) {
|
|
13753
14125
|
const run = this._runReplan(opts);
|
|
13754
|
-
|
|
13755
|
-
if (this._replanPromise ===
|
|
14126
|
+
const tracked = run.finally(() => {
|
|
14127
|
+
if (this._replanPromise === tracked) {
|
|
14128
|
+
this._replanPromise = null;
|
|
14129
|
+
this._activeReplanLabel = null;
|
|
14130
|
+
}
|
|
13756
14131
|
});
|
|
14132
|
+
this._replanPromise = tracked;
|
|
13757
14133
|
await run;
|
|
13758
14134
|
}
|
|
13759
14135
|
/**
|
|
@@ -13972,7 +14348,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13972
14348
|
mixedWeighted
|
|
13973
14349
|
);
|
|
13974
14350
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
13975
|
-
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new").slice(0, newLimit);
|
|
14351
|
+
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)).slice(0, newLimit);
|
|
13976
14352
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
13977
14353
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
13978
14354
|
if (!replan) {
|
|
@@ -14109,14 +14485,14 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14109
14485
|
this.log(
|
|
14110
14486
|
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14111
14487
|
);
|
|
14112
|
-
void this.requestReplan();
|
|
14488
|
+
void this.requestReplan({ label: "auto:depletion", mode: "merge" });
|
|
14113
14489
|
}
|
|
14114
14490
|
const REPLAN_BUFFER = 3;
|
|
14115
14491
|
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
|
|
14116
14492
|
this.log(
|
|
14117
14493
|
`[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
|
|
14118
14494
|
);
|
|
14119
|
-
void this.requestReplan();
|
|
14495
|
+
void this.requestReplan({ label: "auto:quality" });
|
|
14120
14496
|
}
|
|
14121
14497
|
if (this._secondsRemaining <= 0 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
|
|
14122
14498
|
this._currentCard = null;
|
|
@@ -14250,6 +14626,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14250
14626
|
* Remove an item from its source queue after consumption by nextCard().
|
|
14251
14627
|
*/
|
|
14252
14628
|
removeItemFromQueue(item) {
|
|
14629
|
+
this._clearDurableRequirement(item.cardID);
|
|
14630
|
+
this._servedCardIds.add(item.cardID);
|
|
14253
14631
|
if (this.reviewQ.peek(0)?.cardID === item.cardID) {
|
|
14254
14632
|
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
14255
14633
|
} else if (this.newQ.peek(0)?.cardID === item.cardID) {
|
|
@@ -14261,6 +14639,27 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14261
14639
|
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
14262
14640
|
}
|
|
14263
14641
|
}
|
|
14642
|
+
/**
|
|
14643
|
+
* Remove a satisfied card ID from the durable session-hint `requireCards`
|
|
14644
|
+
* list. Called when a card is consumed (see removeItemFromQueue). No-op if
|
|
14645
|
+
* the card was not a durable requirement.
|
|
14646
|
+
*
|
|
14647
|
+
* Matches literal IDs only: a glob/pattern requirement (which may stand for
|
|
14648
|
+
* several cards) is NOT considered satisfied by a single draw and is left in
|
|
14649
|
+
* place — durable patterns are the caller's responsibility, one-shot `hints`
|
|
14650
|
+
* remain the right tool for them.
|
|
14651
|
+
*/
|
|
14652
|
+
_clearDurableRequirement(cardID) {
|
|
14653
|
+
const req = this._sessionHints?.requireCards;
|
|
14654
|
+
if (!req || req.length === 0) return;
|
|
14655
|
+
const next = req.filter((id) => id !== cardID);
|
|
14656
|
+
if (next.length === req.length) return;
|
|
14657
|
+
this._sessionHints = {
|
|
14658
|
+
...this._sessionHints,
|
|
14659
|
+
requireCards: next.length > 0 ? next : void 0
|
|
14660
|
+
};
|
|
14661
|
+
this.log(`[Replan] Durable requirement satisfied & cleared on draw: ${cardID}`);
|
|
14662
|
+
}
|
|
14264
14663
|
/**
|
|
14265
14664
|
* End the session and record learning outcomes.
|
|
14266
14665
|
*
|