@vue-skuilder/db 0.2.3 → 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/index.mjs CHANGED
@@ -13054,6 +13054,161 @@ mountMixerDebugger();
13054
13054
  // src/study/SessionDebugger.ts
13055
13055
  init_logger();
13056
13056
  init_PipelineDebugger();
13057
+
13058
+ // src/study/SessionOverlay.ts
13059
+ init_logger();
13060
+ var activeController = null;
13061
+ function registerActiveController(controller) {
13062
+ activeController = controller;
13063
+ }
13064
+ function getActiveController() {
13065
+ return activeController;
13066
+ }
13067
+ var OVERLAY_ID = "skuilder-session-overlay";
13068
+ var POLL_MS = 300;
13069
+ var INLINE_THRESHOLD = 5;
13070
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
13071
+ var spinnerFrame = 0;
13072
+ var overlayEl = null;
13073
+ var pollHandle = null;
13074
+ var expanded = { reviewQ: false, newQ: false, failedQ: false };
13075
+ function toggleSessionOverlay() {
13076
+ if (typeof document === "undefined") {
13077
+ logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
13078
+ return;
13079
+ }
13080
+ if (overlayEl) {
13081
+ teardown();
13082
+ logger.info("[Session Overlay] Hidden.");
13083
+ } else {
13084
+ mount();
13085
+ logger.info("[Session Overlay] Shown. Toggle off with window.skuilder.session.dbgOverlay().");
13086
+ }
13087
+ }
13088
+ function mount() {
13089
+ overlayEl = document.createElement("div");
13090
+ overlayEl.id = OVERLAY_ID;
13091
+ Object.assign(overlayEl.style, {
13092
+ position: "fixed",
13093
+ top: "8px",
13094
+ left: "8px",
13095
+ zIndex: "2147483647",
13096
+ maxWidth: "320px",
13097
+ maxHeight: "90vh",
13098
+ overflowY: "auto",
13099
+ padding: "8px 10px",
13100
+ background: "rgba(17, 24, 39, 0.92)",
13101
+ color: "#e5e7eb",
13102
+ font: "11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
13103
+ borderRadius: "6px",
13104
+ boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
13105
+ pointerEvents: "auto",
13106
+ userSelect: "none"
13107
+ });
13108
+ document.body.appendChild(overlayEl);
13109
+ render();
13110
+ pollHandle = setInterval(render, POLL_MS);
13111
+ }
13112
+ function teardown() {
13113
+ if (pollHandle !== null) {
13114
+ clearInterval(pollHandle);
13115
+ pollHandle = null;
13116
+ }
13117
+ if (overlayEl?.parentNode) {
13118
+ overlayEl.parentNode.removeChild(overlayEl);
13119
+ }
13120
+ overlayEl = null;
13121
+ }
13122
+ function render() {
13123
+ if (!overlayEl) return;
13124
+ spinnerFrame++;
13125
+ const ctrl = getActiveController();
13126
+ if (!ctrl) {
13127
+ overlayEl.innerHTML = headerHtml() + `<div style="opacity:.65">No active session.</div>`;
13128
+ return;
13129
+ }
13130
+ const s = ctrl.getDebugSnapshot();
13131
+ 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);
13132
+ overlayEl.querySelectorAll("[data-q]").forEach((el) => {
13133
+ el.onclick = () => {
13134
+ const key = el.dataset.q;
13135
+ if (!key) return;
13136
+ expanded[key] = !expanded[key];
13137
+ render();
13138
+ };
13139
+ });
13140
+ }
13141
+ function headerHtml() {
13142
+ return `<div style="font-weight:600;color:#93c5fd;margin-bottom:4px">\u2699 SessionController</div>`;
13143
+ }
13144
+ function replanHtml(s) {
13145
+ if (!s.replanActive) {
13146
+ return `<div style="margin-bottom:6px;opacity:.45">\u25CB idle</div>`;
13147
+ }
13148
+ const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
13149
+ const reason = esc(s.replanLabel ?? "(auto)");
13150
+ return `<div style="margin-bottom:6px;color:#fde047">${frame} replanning <span style="opacity:.85">[${reason}]</span></div>`;
13151
+ }
13152
+ function metaHtml(s) {
13153
+ const mmss = formatTime(s.secondsRemaining);
13154
+ const guarantee = s.hasCardGuarantee ? ` \xB7 <span style="color:#fbbf24">guarantee ${s.minCardsGuarantee}</span>` : "";
13155
+ const rows = [
13156
+ `time ${mmss}${guarantee}`,
13157
+ `well-indicated left: ${s.wellIndicatedRemaining}`,
13158
+ `current: ${s.currentCard ? esc(s.currentCard) : '<span style="opacity:.6">\u2014</span>'}`
13159
+ ];
13160
+ return `<div style="margin-bottom:6px">${rows.map((r) => `<div>${r}</div>`).join("")}</div>`;
13161
+ }
13162
+ function hintsHtml(h) {
13163
+ const parts = [];
13164
+ if (h) {
13165
+ if (h.boostTags && Object.keys(h.boostTags).length) {
13166
+ parts.push(
13167
+ `boost: ` + Object.entries(h.boostTags).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
13168
+ );
13169
+ }
13170
+ if (h.boostCards && Object.keys(h.boostCards).length) {
13171
+ parts.push(
13172
+ `boostCards: ` + Object.entries(h.boostCards).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
13173
+ );
13174
+ }
13175
+ if (h.requireCards?.length) parts.push(`require: ${h.requireCards.map(esc).join(", ")}`);
13176
+ if (h.requireTags?.length) parts.push(`requireTags: ${h.requireTags.map(esc).join(", ")}`);
13177
+ if (h.excludeTags?.length) parts.push(`exclude: ${h.excludeTags.map(esc).join(", ")}`);
13178
+ if (h.excludeCards?.length) parts.push(`excludeCards: ${h.excludeCards.map(esc).join(", ")}`);
13179
+ }
13180
+ const body = parts.length ? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join("") : `<div style="margin-left:6px;opacity:.6">none</div>`;
13181
+ return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
13182
+ }
13183
+ function queueHtml(key, label, q) {
13184
+ const collapsible = q.length > INLINE_THRESHOLD;
13185
+ const isOpen = !collapsible || expanded[key];
13186
+ const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
13187
+ const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
13188
+ const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
13189
+ const titleAttr = collapsible ? ` data-q="${key}"` : "";
13190
+ const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
13191
+ let body = "";
13192
+ if (isOpen && q.cards.length) {
13193
+ body = `<ol style="margin:2px 0 6px 0;padding-left:20px">` + q.cards.map((c) => `<li style="white-space:nowrap">${esc(c)}</li>`).join("") + `</ol>`;
13194
+ } else if (!q.cards.length) {
13195
+ body = `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
13196
+ } else {
13197
+ body = `<div style="margin:1px 0 6px 6px;opacity:.55">(${q.length} cards \u2014 click to expand)</div>`;
13198
+ }
13199
+ return title + body;
13200
+ }
13201
+ function formatTime(totalSeconds) {
13202
+ const s = Math.max(0, Math.round(totalSeconds));
13203
+ const m = Math.floor(s / 60);
13204
+ const r = s % 60;
13205
+ return `${m}:${r.toString().padStart(2, "0")}`;
13206
+ }
13207
+ function esc(value) {
13208
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13209
+ }
13210
+
13211
+ // src/study/SessionDebugger.ts
13057
13212
  var activeSession = null;
13058
13213
  var sessionHistory = [];
13059
13214
  var MAX_HISTORY = 5;
@@ -13234,6 +13389,13 @@ var sessionDebugAPI = {
13234
13389
  showQueue() {
13235
13390
  showCurrentQueue();
13236
13391
  },
13392
+ /**
13393
+ * Toggle the pinned, live-updating DOM overlay for the active controller
13394
+ * (queues, session hints, timer). No-ops in non-browser hosts.
13395
+ */
13396
+ dbgOverlay() {
13397
+ toggleSessionOverlay();
13398
+ },
13237
13399
  /**
13238
13400
  * Show presentation history for current or past session.
13239
13401
  */
@@ -13295,6 +13457,7 @@ var sessionDebugAPI = {
13295
13457
  \u{1F3AF} Session Debug API
13296
13458
 
13297
13459
  Commands:
13460
+ .dbgOverlay() Toggle the pinned live overlay (queues, hints, timer)
13298
13461
  .showQueue() Show current queue state (active session only)
13299
13462
  .showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
13300
13463
  .showInterleaving(index?) Analyze course interleaving pattern
@@ -13363,6 +13526,13 @@ var SessionController = class _SessionController extends Loggable {
13363
13526
  * Used by nextCard() to await completion before drawing from queues.
13364
13527
  */
13365
13528
  _replanPromise = null;
13529
+ /**
13530
+ * Reason for the replan currently executing in `_runReplan`, surfaced by the
13531
+ * debug overlay's spinner. The caller's `opts.label` when present, else
13532
+ * `'(auto)'`. Only meaningful while `_replanPromise` is non-null; cleared
13533
+ * when the in-flight chain settles.
13534
+ */
13535
+ _activeReplanLabel = null;
13366
13536
  /**
13367
13537
  * Number of well-indicated new cards remaining before the queue
13368
13538
  * degrades to poorly-indicated content. Decremented on each newQ
@@ -13475,6 +13645,7 @@ var SessionController = class _SessionController extends Loggable {
13475
13645
  endTime: ${this.endTime}
13476
13646
  defaultBatchLimit: ${this._defaultBatchLimit}
13477
13647
  initialReviewCap: ${this._initialReviewCap}`);
13648
+ registerActiveController(this);
13478
13649
  }
13479
13650
  tick() {
13480
13651
  this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
@@ -13575,15 +13746,23 @@ var SessionController = class _SessionController extends Loggable {
13575
13746
  );
13576
13747
  const inflight = this._replanPromise;
13577
13748
  const queued = inflight.catch(() => void 0).then(() => this._runReplan(opts));
13578
- this._replanPromise = queued.finally(() => {
13579
- if (this._replanPromise === queued) this._replanPromise = null;
13749
+ const tracked2 = queued.finally(() => {
13750
+ if (this._replanPromise === tracked2) {
13751
+ this._replanPromise = null;
13752
+ this._activeReplanLabel = null;
13753
+ }
13580
13754
  });
13755
+ this._replanPromise = tracked2;
13581
13756
  return queued;
13582
13757
  }
13583
13758
  const run = this._runReplan(opts);
13584
- this._replanPromise = run.finally(() => {
13585
- if (this._replanPromise === run) this._replanPromise = null;
13759
+ const tracked = run.finally(() => {
13760
+ if (this._replanPromise === tracked) {
13761
+ this._replanPromise = null;
13762
+ this._activeReplanLabel = null;
13763
+ }
13586
13764
  });
13765
+ this._replanPromise = tracked;
13587
13766
  await run;
13588
13767
  }
13589
13768
  /**
@@ -13592,7 +13771,6 @@ var SessionController = class _SessionController extends Loggable {
13592
13771
  * triggers in nextCard) return false and may coalesce.
13593
13772
  */
13594
13773
  _replanHasIntent(opts) {
13595
- if (opts.label) return true;
13596
13774
  if (opts.limit !== void 0) return true;
13597
13775
  if (opts.minFollowUpCards !== void 0) return true;
13598
13776
  if (opts.mode && opts.mode !== "replace") return true;
@@ -13613,6 +13791,7 @@ var SessionController = class _SessionController extends Loggable {
13613
13791
  * newQ.peek(0) is the imminent draw we need to exclude.
13614
13792
  */
13615
13793
  async _runReplan(opts) {
13794
+ this._activeReplanLabel = opts.label ?? "(auto)";
13616
13795
  if (!opts.hints) opts.hints = {};
13617
13796
  const hints = opts.hints;
13618
13797
  const excludeSet = new Set(hints.excludeCards ?? []);
@@ -13670,6 +13849,34 @@ var SessionController = class _SessionController extends Loggable {
13670
13849
  getSessionHints() {
13671
13850
  return this._sessionHints;
13672
13851
  }
13852
+ /**
13853
+ * Live state snapshot for the debug overlay (window.skuilder.session
13854
+ * .dbgOverlay()). Reads directly from the private queues and hints, so it
13855
+ * always reflects the current moment — unlike the passive SessionDebugger
13856
+ * snapshots, which only capture what was explicitly pushed to them.
13857
+ */
13858
+ getDebugSnapshot() {
13859
+ const describe = (q) => {
13860
+ const cards = [];
13861
+ for (let i = 0; i < q.length; i++) {
13862
+ cards.push(q.peek(i).cardID);
13863
+ }
13864
+ return { length: q.length, dequeueCount: q.dequeueCount, cards };
13865
+ };
13866
+ return {
13867
+ secondsRemaining: this.secondsRemaining,
13868
+ hasCardGuarantee: this.hasCardGuarantee,
13869
+ minCardsGuarantee: this._minCardsGuarantee,
13870
+ wellIndicatedRemaining: this._wellIndicatedRemaining,
13871
+ currentCard: this._currentCard?.item.cardID ?? null,
13872
+ sessionHints: this._sessionHints,
13873
+ replanActive: this._replanPromise !== null,
13874
+ replanLabel: this._activeReplanLabel,
13875
+ reviewQ: describe(this.reviewQ),
13876
+ newQ: describe(this.newQ),
13877
+ failedQ: describe(this.failedQ)
13878
+ };
13879
+ }
13673
13880
  /**
13674
13881
  * Merge `hints` into the durable session hints via the pipeline's
13675
13882
  * `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
@@ -13751,9 +13958,13 @@ var SessionController = class _SessionController extends Loggable {
13751
13958
  */
13752
13959
  async _replanUncoalesced(opts) {
13753
13960
  const run = this._runReplan(opts);
13754
- this._replanPromise = run.finally(() => {
13755
- if (this._replanPromise === run) this._replanPromise = null;
13961
+ const tracked = run.finally(() => {
13962
+ if (this._replanPromise === tracked) {
13963
+ this._replanPromise = null;
13964
+ this._activeReplanLabel = null;
13965
+ }
13756
13966
  });
13967
+ this._replanPromise = tracked;
13757
13968
  await run;
13758
13969
  }
13759
13970
  /**
@@ -14109,14 +14320,14 @@ var SessionController = class _SessionController extends Loggable {
14109
14320
  this.log(
14110
14321
  `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
14111
14322
  );
14112
- void this.requestReplan();
14323
+ void this.requestReplan({ label: "auto:depletion" });
14113
14324
  }
14114
14325
  const REPLAN_BUFFER = 3;
14115
14326
  if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
14116
14327
  this.log(
14117
14328
  `[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
14118
14329
  );
14119
- void this.requestReplan();
14330
+ void this.requestReplan({ label: "auto:quality" });
14120
14331
  }
14121
14332
  if (this._secondsRemaining <= 0 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
14122
14333
  this._currentCard = null;