pi-studio 0.9.3 → 0.9.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.
@@ -77,6 +77,7 @@
77
77
  const pullLatestBtn = document.getElementById("pullLatestBtn");
78
78
  const insertHeaderBtn = document.getElementById("insertHeaderBtn");
79
79
  const critiqueBtn = document.getElementById("critiqueBtn");
80
+ const quizBtn = document.getElementById("quizBtn");
80
81
  const lensSelect = document.getElementById("lensSelect");
81
82
  const fileInput = document.getElementById("fileInput");
82
83
  const resourceDirBtn = document.getElementById("resourceDirBtn");
@@ -235,6 +236,37 @@
235
236
  const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
236
237
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
237
238
  const EDITOR_TAB_TEXT = " ";
239
+ const QUIZ_DEFAULT_COUNT = 5;
240
+ const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
241
+ const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
242
+ const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
243
+ let quizOverlayEl = null;
244
+ let quizDialogEl = null;
245
+ let quizPreviewRenderNonce = 0;
246
+ const quizMarkdownRenderCache = new Map();
247
+ let quizState = {
248
+ open: false,
249
+ requestId: null,
250
+ pending: false,
251
+ sourceText: "",
252
+ sourceLabel: "Studio editor",
253
+ sourcePath: "",
254
+ contextPath: "",
255
+ resourceDir: "",
256
+ focusPrompt: "",
257
+ includeEditorContext: false,
258
+ scope: "editor",
259
+ angle: "general",
260
+ thinking: "minimal",
261
+ questionCount: QUIZ_DEFAULT_COUNT,
262
+ cards: [],
263
+ index: 0,
264
+ answer: "",
265
+ feedback: null,
266
+ discussion: [],
267
+ status: "",
268
+ error: "",
269
+ };
238
270
  let replTmuxAvailable = null;
239
271
  let replSessions = [];
240
272
  let replActiveSessionName = "";
@@ -2002,7 +2034,7 @@
2002
2034
  if (!isEditorOnlyMode && critiqueBtn && lensSelect) {
2003
2035
  const reviewButton = makeStudioUiRefreshElement("button", "studio-refresh-tool-tab studio-refresh-review-btn", "Review");
2004
2036
  reviewMenu = makeStudioUiRefreshMenu(reviewButton, "review", "studio-refresh-review-anchor");
2005
- appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn]);
2037
+ appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, quizBtn]);
2006
2038
  appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
2007
2039
  }
2008
2040
 
@@ -3010,6 +3042,18 @@
3010
3042
  && typeof studioPdfFocusDialogEl.contains === "function"
3011
3043
  && studioPdfFocusDialogEl.contains(event.target)
3012
3044
  );
3045
+ const quizOwnsEvent = Boolean(
3046
+ quizDialogEl
3047
+ && event.target
3048
+ && typeof quizDialogEl.contains === "function"
3049
+ && quizDialogEl.contains(event.target)
3050
+ );
3051
+
3052
+ if (isQuizOpen() && plainEscape) {
3053
+ event.preventDefault();
3054
+ minimizeQuizOverlay();
3055
+ return;
3056
+ }
3013
3057
 
3014
3058
  if (isStudioPdfFocusOpen() && plainEscape) {
3015
3059
  event.preventDefault();
@@ -3035,7 +3079,7 @@
3035
3079
  return;
3036
3080
  }
3037
3081
 
3038
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent) {
3082
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent || quizOwnsEvent) {
3039
3083
  return;
3040
3084
  }
3041
3085
 
@@ -7272,6 +7316,768 @@
7272
7316
  return "draft_" + makeRequestId();
7273
7317
  }
7274
7318
 
7319
+ function normalizeQuizScope(scope) {
7320
+ const value = String(scope || "").trim().toLowerCase();
7321
+ return QUIZ_SCOPES.includes(value) ? value : "editor";
7322
+ }
7323
+
7324
+ function getQuizScopeLabel(scope) {
7325
+ switch (normalizeQuizScope(scope)) {
7326
+ case "selection": return "Selection";
7327
+ case "file": return "Current file";
7328
+ case "folder": return "Folder";
7329
+ case "repo": return "Repo";
7330
+ default: return "Editor";
7331
+ }
7332
+ }
7333
+
7334
+ function normalizeQuizAngle(angle) {
7335
+ const value = String(angle || "").trim().toLowerCase();
7336
+ return QUIZ_ANGLES.includes(value) ? value : "general";
7337
+ }
7338
+
7339
+ function getQuizAngleLabel(angle) {
7340
+ switch (normalizeQuizAngle(angle)) {
7341
+ case "scientist": return "Scientist";
7342
+ case "mathematician": return "Mathematician";
7343
+ case "statistician": return "Statistician";
7344
+ case "developer": return "Developer";
7345
+ case "reviewer": return "Reviewer";
7346
+ default: return "General";
7347
+ }
7348
+ }
7349
+
7350
+ function normalizeQuizThinking(thinking) {
7351
+ const value = String(thinking || "").trim().toLowerCase();
7352
+ return QUIZ_THINKING_LEVELS.includes(value) ? value : "minimal";
7353
+ }
7354
+
7355
+ function getQuizThinkingLabel(thinking) {
7356
+ switch (normalizeQuizThinking(thinking)) {
7357
+ case "off": return "Off";
7358
+ case "low": return "Low";
7359
+ case "medium": return "Medium";
7360
+ case "high": return "High";
7361
+ default: return "Minimal";
7362
+ }
7363
+ }
7364
+
7365
+ function getQuizKindLabel(kind) {
7366
+ const value = String(kind || "").trim().toLowerCase();
7367
+ if (!value) return "";
7368
+ return value
7369
+ .split(/[-_\s]+/)
7370
+ .filter(Boolean)
7371
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
7372
+ .join(" ");
7373
+ }
7374
+
7375
+ function getQuizModelLabel() {
7376
+ const label = String(modelLabel || "").trim();
7377
+ const withoutThinking = label.replace(/\s*\((?:off|minimal|low|medium|high|xhigh)\)\s*$/i, "").trim();
7378
+ return withoutThinking || label || "current Pi model";
7379
+ }
7380
+
7381
+ function shouldRenderQuizMarkdownPreview() {
7382
+ const lang = normalizeFenceLanguage(editorLanguage || "");
7383
+ return !lang || lang === "markdown" || lang === "latex";
7384
+ }
7385
+
7386
+ function renderQuizMarkdownBlockHtml(markdown, className) {
7387
+ const source = String(markdown || "");
7388
+ return "<div class='studio-quiz-markdown-body rendered-markdown " + escapeHtml(className || "") + "' data-quiz-markdown='" + escapeHtml(source) + "'>"
7389
+ + "<div class='studio-quiz-markdown-fallback'>" + escapeHtml(source) + "</div>"
7390
+ + "</div>";
7391
+ }
7392
+
7393
+ function restoreQuizScrollTop(scrollTop) {
7394
+ const scrollEl = getQuizScrollContainer();
7395
+ if (!scrollEl) return;
7396
+ scrollEl.scrollTop = Math.max(0, Number(scrollTop) || 0);
7397
+ }
7398
+
7399
+ function restoreQuizScrollTopSoon(scrollTop) {
7400
+ restoreQuizScrollTop(scrollTop);
7401
+ window.requestAnimationFrame(() => restoreQuizScrollTop(scrollTop));
7402
+ }
7403
+
7404
+ function isQuizScrollNearBottom(scrollEl) {
7405
+ if (!scrollEl) return false;
7406
+ return (scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 80;
7407
+ }
7408
+
7409
+ function scrollQuizToBottom() {
7410
+ const scrollEl = getQuizScrollContainer();
7411
+ if (!scrollEl) return;
7412
+ scrollEl.scrollTop = scrollEl.scrollHeight;
7413
+ }
7414
+
7415
+ function revealQuizTarget(selector) {
7416
+ if (!quizDialogEl || !selector) return false;
7417
+ const scrollEl = getQuizScrollContainer();
7418
+ const target = quizDialogEl.querySelector(selector);
7419
+ if (!scrollEl || !(target instanceof HTMLElement)) return false;
7420
+ const targetTop = target.offsetTop;
7421
+ const targetBottom = targetTop + target.offsetHeight;
7422
+ const visibleTop = scrollEl.scrollTop;
7423
+ const visibleBottom = visibleTop + scrollEl.clientHeight;
7424
+ if (targetTop >= visibleTop + 12 && targetBottom <= visibleBottom - 12) return true;
7425
+ scrollEl.scrollTop = Math.max(0, targetTop - 18);
7426
+ return true;
7427
+ }
7428
+
7429
+ function applyQuizScrollIntent(options, fallbackScrollTop, wasNearBottom) {
7430
+ const opts = options && typeof options === "object" ? options : {};
7431
+ if (opts.scrollToBottom || (opts.followBottomIfNearBottom && wasNearBottom)) {
7432
+ scrollQuizToBottom();
7433
+ return;
7434
+ }
7435
+ if (opts.revealSelector && revealQuizTarget(opts.revealSelector)) {
7436
+ return;
7437
+ }
7438
+ if (opts.preserveScroll) restoreQuizScrollTop(fallbackScrollTop);
7439
+ }
7440
+
7441
+ function applyQuizScrollIntentSoon(options, fallbackScrollTop, wasNearBottom) {
7442
+ applyQuizScrollIntent(options, fallbackScrollTop, wasNearBottom);
7443
+ window.requestAnimationFrame(() => applyQuizScrollIntent(options, fallbackScrollTop, wasNearBottom));
7444
+ }
7445
+
7446
+ function trimQuizMarkdownRenderCache() {
7447
+ while (quizMarkdownRenderCache.size > 80) {
7448
+ const firstKey = quizMarkdownRenderCache.keys().next().value;
7449
+ if (!firstKey) break;
7450
+ quizMarkdownRenderCache.delete(firstKey);
7451
+ }
7452
+ }
7453
+
7454
+ async function renderQuizMarkdownToHtml(markdown) {
7455
+ const source = String(markdown || "");
7456
+ const cacheKey = String(editorLanguage || "markdown") + "\n" + source;
7457
+ if (quizMarkdownRenderCache.has(cacheKey)) return quizMarkdownRenderCache.get(cacheKey);
7458
+ const renderedHtml = await renderMarkdownWithPandoc(source, { includeEditorLanguage: true });
7459
+ const sanitized = sanitizeRenderedHtml(renderedHtml, source, { stripMarkdownHtmlComments: editorLanguage !== "latex" });
7460
+ quizMarkdownRenderCache.set(cacheKey, sanitized);
7461
+ trimQuizMarkdownRenderCache();
7462
+ return sanitized;
7463
+ }
7464
+
7465
+ async function renderQuizMarkdownFields(nonce, options) {
7466
+ const opts = options && typeof options === "object" ? options : {};
7467
+ const fallbackScrollTop = Number(opts.fallbackScrollTop) || 0;
7468
+ const wasNearBottom = Boolean(opts.wasNearBottom);
7469
+ if (!quizDialogEl || !shouldRenderQuizMarkdownPreview()) {
7470
+ applyQuizScrollIntentSoon(opts, fallbackScrollTop, wasNearBottom);
7471
+ return;
7472
+ }
7473
+ const targets = Array.from(quizDialogEl.querySelectorAll("[data-quiz-markdown]")).filter((target) => target instanceof HTMLElement);
7474
+ const preserveScroll = Boolean(opts.preserveScroll || opts.revealSelector || opts.scrollToBottom || opts.followBottomIfNearBottom);
7475
+ for (const target of targets) {
7476
+ const markdown = target.getAttribute("data-quiz-markdown") || "";
7477
+ if (!markdown.trim()) continue;
7478
+ const scrollEl = preserveScroll ? getQuizScrollContainer() : null;
7479
+ const scrollTop = scrollEl ? scrollEl.scrollTop : fallbackScrollTop;
7480
+ try {
7481
+ const html = await renderQuizMarkdownToHtml(markdown);
7482
+ if (nonce !== quizPreviewRenderNonce || !quizDialogEl || !quizDialogEl.contains(target)) return;
7483
+ target.innerHTML = html;
7484
+ await renderAnnotationMathInElement(target);
7485
+ decoratePdfEmbeds(target);
7486
+ await renderPdfPreviewsInElement(target);
7487
+ await renderMermaidInElement(target);
7488
+ await renderMathFallbackInElement(target);
7489
+ decorateCopyablePreviewBlocks(target);
7490
+ if (preserveScroll) restoreQuizScrollTopSoon(scrollTop);
7491
+ } catch (error) {
7492
+ console.error("Quiz markdown preview render failed:", error);
7493
+ target.classList.add("studio-quiz-markdown-render-failed");
7494
+ }
7495
+ }
7496
+ applyQuizScrollIntentSoon(opts, fallbackScrollTop, wasNearBottom);
7497
+ }
7498
+
7499
+ function isQuizOpen() {
7500
+ return Boolean(quizOverlayEl && !quizOverlayEl.hidden);
7501
+ }
7502
+
7503
+ function getQuizCurrentCard() {
7504
+ if (!Array.isArray(quizState.cards) || quizState.cards.length === 0) return null;
7505
+ const index = Math.max(0, Math.min(quizState.index || 0, quizState.cards.length - 1));
7506
+ return quizState.cards[index] || null;
7507
+ }
7508
+
7509
+ function getQuizSourceLabel(scope) {
7510
+ const base = sourceState && sourceState.label ? sourceState.label : "Studio editor";
7511
+ const normalizedScope = normalizeQuizScope(scope);
7512
+ if (normalizedScope === "selection") return base + " selection";
7513
+ if (normalizedScope === "file") return base === "blank" ? "current file" : base;
7514
+ if (normalizedScope === "folder") return "folder context";
7515
+ if (normalizedScope === "repo") return "repo context";
7516
+ return base;
7517
+ }
7518
+
7519
+ function dirnameForDisplayPath(path) {
7520
+ const value = String(path || "").replace(/\\/g, "/");
7521
+ const index = value.lastIndexOf("/");
7522
+ return index > 0 ? value.slice(0, index) : "";
7523
+ }
7524
+
7525
+ function getCurrentResourceDirValue() {
7526
+ return resourceDirInput ? String(resourceDirInput.value || "").trim() : "";
7527
+ }
7528
+
7529
+ function getDefaultQuizContextPath(scope) {
7530
+ const normalizedScope = normalizeQuizScope(scope);
7531
+ const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
7532
+ const resourceDir = getCurrentResourceDirValue();
7533
+ if (normalizedScope === "file") return sourcePath || "";
7534
+ if (normalizedScope === "folder") return resourceDir || dirnameForDisplayPath(sourcePath) || "";
7535
+ if (normalizedScope === "repo") return sourcePath || resourceDir || "";
7536
+ return "";
7537
+ }
7538
+
7539
+ function isQuizContextScope(scope) {
7540
+ const normalizedScope = normalizeQuizScope(scope);
7541
+ return normalizedScope === "file" || normalizedScope === "folder" || normalizedScope === "repo";
7542
+ }
7543
+
7544
+ function getQuizScopeFocusHint(scope) {
7545
+ const normalizedScope = normalizeQuizScope(scope);
7546
+ const focus = String(quizState.focusPrompt || "").toLowerCase();
7547
+ const asksForCode = /\b(code|implementation|technical|source|actual code)\b/.test(focus);
7548
+ const editorLang = normalizeFenceLanguage(editorLanguage || "");
7549
+ const editorLooksLikeDoc = !editorLang || editorLang === "markdown" || editorLang === "latex";
7550
+ if (asksForCode && (normalizedScope === "editor" || normalizedScope === "selection") && editorLooksLikeDoc) {
7551
+ return "Focus guidance only applies to the selected scope. Choose Folder or Repo to include code files.";
7552
+ }
7553
+ if ((normalizedScope === "folder" || normalizedScope === "repo") && asksForCode) {
7554
+ return "Code-focused guidance will prioritize source/test files over README and docs.";
7555
+ }
7556
+ return "";
7557
+ }
7558
+
7559
+ function ensureQuizOverlay() {
7560
+ if (quizOverlayEl && quizDialogEl) return quizOverlayEl;
7561
+ quizOverlayEl = document.createElement("div");
7562
+ quizOverlayEl.className = "studio-quiz-overlay";
7563
+ quizOverlayEl.setAttribute("role", "presentation");
7564
+ quizOverlayEl.hidden = true;
7565
+ quizOverlayEl.innerHTML = "<div class='studio-quiz-dialog' role='dialog' aria-modal='true' aria-label='Studio quiz'></div>";
7566
+ document.body.appendChild(quizOverlayEl);
7567
+ quizDialogEl = quizOverlayEl.querySelector(".studio-quiz-dialog");
7568
+ quizOverlayEl.addEventListener("click", (event) => {
7569
+ if (event.target === quizOverlayEl) minimizeQuizOverlay();
7570
+ });
7571
+ quizDialogEl.addEventListener("input", (event) => {
7572
+ const target = event.target;
7573
+ if (!(target instanceof HTMLElement)) return;
7574
+ if (target.matches("[data-quiz-input='answer']")) {
7575
+ const card = getQuizCurrentCard();
7576
+ if (card) card.answer = target.value;
7577
+ quizState.answer = target.value;
7578
+ }
7579
+ if (target.matches("[data-quiz-field='contextPath']")) {
7580
+ quizState.contextPath = target.value;
7581
+ }
7582
+ if (target.matches("[data-quiz-field='focusPrompt']")) {
7583
+ quizState.focusPrompt = target.value;
7584
+ }
7585
+ if (target.matches("[data-quiz-field='includeEditorContext']")) {
7586
+ quizState.includeEditorContext = Boolean(target.checked);
7587
+ }
7588
+ });
7589
+ quizDialogEl.addEventListener("click", (event) => {
7590
+ const target = event.target instanceof Element ? event.target.closest("[data-quiz-action]") : null;
7591
+ if (!target) return;
7592
+ event.preventDefault();
7593
+ handleQuizAction(target.getAttribute("data-quiz-action") || "");
7594
+ });
7595
+ quizDialogEl.addEventListener("change", (event) => {
7596
+ const target = event.target;
7597
+ if (!(target instanceof HTMLElement) || !target.matches("[data-quiz-field]")) return;
7598
+ if (target.matches("[data-quiz-field='contextPath']")) return;
7599
+ readQuizSetupFields();
7600
+ renderQuizOverlay({ preserveScroll: true });
7601
+ });
7602
+ quizDialogEl.addEventListener("keydown", handleQuizKeydown);
7603
+ return quizOverlayEl;
7604
+ }
7605
+
7606
+ function resetQuizStateFromEditor() {
7607
+ const previousAngle = normalizeQuizAngle(quizState.angle);
7608
+ const previousThinking = normalizeQuizThinking(quizState.thinking);
7609
+ const previousFocusPrompt = String(quizState.focusPrompt || "");
7610
+ const previousIncludeEditorContext = Boolean(quizState.includeEditorContext);
7611
+ const previousCount = quizState.questionCount || QUIZ_DEFAULT_COUNT;
7612
+ const selection = getEditorSelectionRange();
7613
+ const hasSelection = Boolean(selection.selected && selection.selected.trim());
7614
+ const scope = hasSelection ? "selection" : "editor";
7615
+ const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
7616
+ const resourceDir = getCurrentResourceDirValue();
7617
+ quizState = {
7618
+ open: true,
7619
+ requestId: null,
7620
+ pending: false,
7621
+ sourceText: hasSelection ? selection.selected : selection.raw,
7622
+ sourceLabel: getQuizSourceLabel(scope),
7623
+ sourcePath,
7624
+ contextPath: getDefaultQuizContextPath(scope),
7625
+ resourceDir,
7626
+ focusPrompt: previousFocusPrompt,
7627
+ includeEditorContext: previousIncludeEditorContext,
7628
+ scope,
7629
+ angle: previousAngle,
7630
+ thinking: previousThinking,
7631
+ questionCount: previousCount,
7632
+ cards: [],
7633
+ index: 0,
7634
+ answer: "",
7635
+ feedback: null,
7636
+ discussion: [],
7637
+ status: "",
7638
+ error: "",
7639
+ };
7640
+ }
7641
+
7642
+ function hasResumableQuiz() {
7643
+ return Boolean(
7644
+ quizState.pending ||
7645
+ (Array.isArray(quizState.cards) && quizState.cards.length > 0) ||
7646
+ (quizState.sourceText && (quizState.status || quizState.error))
7647
+ );
7648
+ }
7649
+
7650
+ function openQuizOverlay() {
7651
+ ensureQuizOverlay();
7652
+ if (!hasResumableQuiz()) {
7653
+ resetQuizStateFromEditor();
7654
+ } else {
7655
+ quizState.open = true;
7656
+ }
7657
+ quizOverlayEl.hidden = false;
7658
+ document.body.classList.add("studio-quiz-open");
7659
+ renderQuizOverlay();
7660
+ }
7661
+
7662
+ function closeQuizOverlay() {
7663
+ if (!quizOverlayEl) return;
7664
+ quizOverlayEl.hidden = true;
7665
+ document.body.classList.remove("studio-quiz-open");
7666
+ quizState.open = false;
7667
+ syncActionButtons();
7668
+ }
7669
+
7670
+ function minimizeQuizOverlay() {
7671
+ closeQuizOverlay();
7672
+ setStatus("Quiz minimized — use Review → Quiz me to resume.", "success");
7673
+ }
7674
+
7675
+ function endQuizOverlay() {
7676
+ const hadResumableQuiz = hasResumableQuiz();
7677
+ closeQuizOverlay();
7678
+ resetQuizStateFromEditor();
7679
+ quizState.open = false;
7680
+ syncActionButtons();
7681
+ if (hadResumableQuiz) setStatus("Quiz closed.", "success");
7682
+ }
7683
+
7684
+ function handleQuizKeydown(event) {
7685
+ if (!event) return;
7686
+ const key = typeof event.key === "string" ? event.key : "";
7687
+ const plainEscape = key === "Escape" && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
7688
+ const submitShortcut = key === "Enter" && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey;
7689
+ if (plainEscape) {
7690
+ event.preventDefault();
7691
+ event.stopPropagation();
7692
+ minimizeQuizOverlay();
7693
+ return;
7694
+ }
7695
+ if (submitShortcut) {
7696
+ event.preventDefault();
7697
+ event.stopPropagation();
7698
+ const card = getQuizCurrentCard();
7699
+ if (!card) {
7700
+ startQuizRequest();
7701
+ return;
7702
+ }
7703
+ if (!card.feedback) {
7704
+ checkQuizAnswer();
7705
+ return;
7706
+ }
7707
+ const promptEl = quizDialogEl ? quizDialogEl.querySelector("[data-quiz-field='discussion']") : null;
7708
+ const prompt = promptEl ? String(promptEl.value || "").trim() : "";
7709
+ if (prompt) {
7710
+ discussQuizCard();
7711
+ } else if (quizState.index < quizState.cards.length - 1) {
7712
+ quizState.index = Math.min(quizState.cards.length - 1, (quizState.index || 0) + 1);
7713
+ quizState.error = "";
7714
+ quizState.status = "";
7715
+ renderQuizOverlay();
7716
+ }
7717
+ return;
7718
+ }
7719
+ event.stopPropagation();
7720
+ }
7721
+
7722
+ function renderQuizOption(value, selected, label) {
7723
+ return "<option value='" + escapeHtml(value) + "'" + (value === selected ? " selected" : "") + ">" + escapeHtml(label) + "</option>";
7724
+ }
7725
+
7726
+ function renderQuizSetupHtml() {
7727
+ const scope = normalizeQuizScope(quizState.scope);
7728
+ const angle = normalizeQuizAngle(quizState.angle);
7729
+ const thinking = normalizeQuizThinking(quizState.thinking);
7730
+ const count = Math.max(1, Math.min(8, Math.floor(Number(quizState.questionCount) || QUIZ_DEFAULT_COUNT)));
7731
+ const selection = getEditorSelectionRange();
7732
+ const hasSelection = Boolean(selection.selected && selection.selected.trim());
7733
+ const contextPath = String(quizState.contextPath || getDefaultQuizContextPath(scope) || "");
7734
+ const includeEditorContext = Boolean(quizState.includeEditorContext);
7735
+ const contextScope = isQuizContextScope(scope);
7736
+ const contextScopeUsesEditor = scope === "file" || includeEditorContext;
7737
+ const focusHint = getQuizScopeFocusHint(scope);
7738
+ const scopeText = scope === "selection"
7739
+ ? selection.selected
7740
+ : ((scope === "editor" || contextScopeUsesEditor) ? selection.raw : "");
7741
+ return "<div class='studio-quiz-setup'>"
7742
+ + "<p class='studio-quiz-copy'>A short active-recall loop: answer one question, check it, ask about the card if useful, then move on.</p>"
7743
+ + "<div class='studio-quiz-fields'>"
7744
+ + "<label>Scope<select data-quiz-field='scope'>"
7745
+ + QUIZ_SCOPES.map((candidate) => candidate === "selection" && !hasSelection ? "" : renderQuizOption(candidate, scope, getQuizScopeLabel(candidate))).join("")
7746
+ + "</select></label>"
7747
+ + "<label>Angle<select data-quiz-field='angle'>"
7748
+ + QUIZ_ANGLES.map((candidate) => renderQuizOption(candidate, angle, getQuizAngleLabel(candidate))).join("")
7749
+ + "</select></label>"
7750
+ + "<label>Thinking<select data-quiz-field='thinking'>"
7751
+ + QUIZ_THINKING_LEVELS.map((candidate) => renderQuizOption(candidate, thinking, getQuizThinkingLabel(candidate))).join("")
7752
+ + "</select></label>"
7753
+ + "<label>Questions<input data-quiz-field='count' type='number' min='1' max='8' value='" + String(count) + "'></label>"
7754
+ + "</div>"
7755
+ + (contextScope ? "<label class='studio-quiz-context-path-label'>Context path<input data-quiz-field='contextPath' type='text' value='" + escapeHtml(contextPath) + "' placeholder='Folder, file, or repo path; blank uses Studio working directory'></label>" : "")
7756
+ + ((scope === "folder" || scope === "repo") ? "<label class='studio-quiz-include-editor-label'><input data-quiz-field='includeEditorContext' type='checkbox'" + (includeEditorContext ? " checked" : "") + "> Include current editor text as an anchor</label>" : "")
7757
+ + "<label class='studio-quiz-focus-label'>Focus guidance<textarea data-quiz-field='focusPrompt' rows='2' placeholder='Optional: e.g. focus on implementation details in code files; avoid README overview questions'>" + escapeHtml(quizState.focusPrompt || "") + "</textarea></label>"
7758
+ + "<div class='studio-quiz-source-note'>Scope: " + escapeHtml(getQuizScopeLabel(scope)) + (scopeText.trim() ? " · " + escapeHtml(String(scopeText.trim().length)) + " active chars" : (scope === "folder" || scope === "repo" ? " · editor text excluded" : "")) + (contextScope && contextPath ? " · Context: " + escapeHtml(contextPath) : "") + " · Studio model: " + escapeHtml(getQuizModelLabel()) + "</div>"
7759
+ + (focusHint ? "<div class='studio-quiz-hint'>" + escapeHtml(focusHint) + "</div>" : "")
7760
+ + (quizState.error ? "<div class='studio-quiz-error'>" + escapeHtml(quizState.error) + "</div>" : "")
7761
+ + (quizState.status ? "<div class='studio-quiz-status'>" + escapeHtml(quizState.status) + "</div>" : "")
7762
+ + "<div class='studio-quiz-actions'><button data-quiz-action='start' type='button'" + (quizState.pending ? " disabled" : "") + ">" + (quizState.pending ? "Generating…" : "Start quiz") + "</button></div>"
7763
+ + "</div>";
7764
+ }
7765
+
7766
+ function getQuizScrollContainer() {
7767
+ if (!quizDialogEl) return null;
7768
+ return quizDialogEl.querySelector(".studio-quiz-card, .studio-quiz-setup");
7769
+ }
7770
+
7771
+ function renderQuizCardHtml() {
7772
+ const card = getQuizCurrentCard();
7773
+ if (!card) return renderQuizSetupHtml();
7774
+ const total = quizState.cards.length;
7775
+ const index = Math.max(0, Math.min(quizState.index || 0, total - 1));
7776
+ const feedback = card.feedback || null;
7777
+ const answer = typeof card.answer === "string" ? card.answer : "";
7778
+ const discussion = Array.isArray(card.discussion) ? card.discussion : [];
7779
+ const scoreClass = feedback && feedback.score ? String(feedback.score).toLowerCase().replace(/[^a-z0-9_-]/g, "") : "";
7780
+ const idealAnswer = feedback && feedback.idealAnswer ? feedback.idealAnswer : (card.idealAnswer || "");
7781
+ const kindLabel = getQuizKindLabel(card.kind);
7782
+ const cardMeta = [kindLabel, getQuizAngleLabel(quizState.angle), quizState.sourceLabel || "Studio editor"].filter(Boolean).join(" · ");
7783
+ const renderMarkdown = shouldRenderQuizMarkdownPreview();
7784
+ return "<div class='studio-quiz-card'>"
7785
+ + "<div class='studio-quiz-meta'><span>Question " + String(index + 1) + " of " + String(total) + "</span><span>" + escapeHtml(cardMeta) + "</span></div>"
7786
+ + (card.snippet ? (renderMarkdown ? renderQuizMarkdownBlockHtml(card.snippet, "studio-quiz-snippet") : "<pre class='studio-quiz-snippet'><code>" + escapeHtml(card.snippet) + "</code></pre>") : "")
7787
+ + (renderMarkdown ? renderQuizMarkdownBlockHtml(card.question || "", "studio-quiz-question") : "<div class='studio-quiz-question'>" + escapeHtml(card.question || "") + "</div>")
7788
+ + "<label class='studio-quiz-answer-label'>Your answer<textarea data-quiz-input='answer' rows='6' placeholder='Explain it in your own words…'" + (feedback ? " disabled" : "") + ">" + escapeHtml(answer) + "</textarea></label>"
7789
+ + (feedback ? "<div class='studio-quiz-feedback studio-quiz-score-" + escapeHtml(scoreClass) + "'>"
7790
+ + "<div class='studio-quiz-feedback-title'>" + escapeHtml(feedback.score || "feedback") + "</div>"
7791
+ + (feedback.feedback ? renderQuizMarkdownBlockHtml(feedback.feedback, "studio-quiz-feedback-text") : "")
7792
+ + (idealAnswer ? "<div class='studio-quiz-ideal'><strong>Stronger answer</strong>" + renderQuizMarkdownBlockHtml(idealAnswer, "studio-quiz-feedback-text") + "</div>" : "")
7793
+ + (feedback.followUp ? "<div class='studio-quiz-follow-up'><strong>Suggested stretch question</strong>" + renderQuizMarkdownBlockHtml(feedback.followUp, "studio-quiz-feedback-text") + "</div>" : "")
7794
+ + "</div>" : "")
7795
+ + (discussion.length ? "<div class='studio-quiz-discussion'>" + discussion.map((entry) => "<div class='studio-quiz-discussion-entry studio-quiz-discussion-" + escapeHtml(entry.role || "assistant") + "'><strong>" + escapeHtml(entry.role === "user" ? "You" : "Tutor") + "</strong><p>" + escapeHtml(entry.text || "") + "</p></div>").join("") + "</div>" : "")
7796
+ + (feedback ? "<div class='studio-quiz-discuss-row'><textarea data-quiz-field='discussion' rows='2' placeholder='Ask the tutor about this card…'></textarea><button data-quiz-action='discuss' type='button'" + (quizState.pending ? " disabled" : "") + ">Ask</button></div>" : "")
7797
+ + (quizState.error ? "<div class='studio-quiz-error'>" + escapeHtml(quizState.error) + "</div>" : "")
7798
+ + (quizState.status ? "<div class='studio-quiz-status'>" + escapeHtml(quizState.status) + "</div>" : "")
7799
+ + "<div class='studio-quiz-actions studio-quiz-card-actions'>"
7800
+ + "<button data-quiz-action='previous' type='button'" + (index <= 0 ? " disabled" : "") + ">Previous</button>"
7801
+ + (feedback ? "<button data-quiz-action='next' type='button'" + (index >= total - 1 ? " disabled" : "") + ">Next</button>" : "<button data-quiz-action='check' type='button'" + (quizState.pending ? " disabled" : "") + ">" + (quizState.pending ? "Checking…" : "Check answer") + "</button>")
7802
+ + "<button data-quiz-action='restart' type='button'>New quiz</button>"
7803
+ + "</div>"
7804
+ + "</div>";
7805
+ }
7806
+
7807
+ function renderQuizOverlay(options) {
7808
+ if (!quizDialogEl) return;
7809
+ const scrollOptions = options && typeof options === "object" ? options : {};
7810
+ const preserveScroll = Boolean(scrollOptions.preserveScroll || scrollOptions.revealSelector || scrollOptions.scrollToBottom || scrollOptions.followBottomIfNearBottom);
7811
+ const previousScrollEl = getQuizScrollContainer();
7812
+ const previousScrollTop = previousScrollEl ? previousScrollEl.scrollTop : 0;
7813
+ const wasNearBottom = isQuizScrollNearBottom(previousScrollEl);
7814
+ const bodyHtml = quizState.cards && quizState.cards.length ? renderQuizCardHtml() : renderQuizSetupHtml();
7815
+ quizDialogEl.innerHTML = "<div class='studio-quiz-header'>"
7816
+ + "<div><div class='studio-quiz-eyebrow'>Review</div><h2>Quiz me</h2></div>"
7817
+ + "<div class='studio-quiz-header-actions'>"
7818
+ + "<button class='studio-quiz-minimize' data-quiz-action='minimize' type='button'>Minimize</button>"
7819
+ + "<button class='studio-quiz-close' data-quiz-action='close' type='button' aria-label='Close and discard quiz' title='Close and discard this quiz'>Close</button>"
7820
+ + "</div>"
7821
+ + "</div>"
7822
+ + bodyHtml;
7823
+ if (preserveScroll) {
7824
+ const nextScrollEl = getQuizScrollContainer();
7825
+ if (nextScrollEl) {
7826
+ nextScrollEl.scrollTop = previousScrollTop;
7827
+ window.requestAnimationFrame(() => {
7828
+ const rafScrollEl = getQuizScrollContainer();
7829
+ if (rafScrollEl) rafScrollEl.scrollTop = previousScrollTop;
7830
+ });
7831
+ }
7832
+ }
7833
+ applyQuizScrollIntentSoon(scrollOptions, previousScrollTop, wasNearBottom);
7834
+ const renderNonce = ++quizPreviewRenderNonce;
7835
+ void renderQuizMarkdownFields(renderNonce, {
7836
+ ...scrollOptions,
7837
+ preserveScroll,
7838
+ fallbackScrollTop: previousScrollTop,
7839
+ wasNearBottom,
7840
+ });
7841
+ }
7842
+
7843
+ function readQuizSetupFields() {
7844
+ if (!quizDialogEl) return;
7845
+ const scopeEl = quizDialogEl.querySelector("[data-quiz-field='scope']");
7846
+ const angleEl = quizDialogEl.querySelector("[data-quiz-field='angle']");
7847
+ const thinkingEl = quizDialogEl.querySelector("[data-quiz-field='thinking']");
7848
+ const countEl = quizDialogEl.querySelector("[data-quiz-field='count']");
7849
+ const contextPathEl = quizDialogEl.querySelector("[data-quiz-field='contextPath']");
7850
+ const focusPromptEl = quizDialogEl.querySelector("[data-quiz-field='focusPrompt']");
7851
+ const includeEditorContextEl = quizDialogEl.querySelector("[data-quiz-field='includeEditorContext']");
7852
+ const selection = getEditorSelectionRange();
7853
+ let scope = normalizeQuizScope(scopeEl ? scopeEl.value : quizState.scope);
7854
+ if (scope === "selection" && !selection.selected.trim()) scope = "editor";
7855
+ const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
7856
+ const resourceDir = getCurrentResourceDirValue();
7857
+ quizState.scope = scope;
7858
+ quizState.angle = normalizeQuizAngle(angleEl ? angleEl.value : quizState.angle);
7859
+ quizState.thinking = normalizeQuizThinking(thinkingEl ? thinkingEl.value : quizState.thinking);
7860
+ quizState.questionCount = Math.max(1, Math.min(8, Math.floor(Number(countEl ? countEl.value : quizState.questionCount) || QUIZ_DEFAULT_COUNT)));
7861
+ quizState.includeEditorContext = Boolean(includeEditorContextEl && includeEditorContextEl.checked);
7862
+ const shouldSendEditorText = scope === "selection" || scope === "editor" || scope === "file" || quizState.includeEditorContext;
7863
+ quizState.sourceText = scope === "selection" ? selection.selected : (shouldSendEditorText ? selection.raw : "");
7864
+ quizState.sourceLabel = shouldSendEditorText ? (sourceState && sourceState.label ? sourceState.label : getQuizSourceLabel(scope)) : getQuizSourceLabel(scope);
7865
+ quizState.sourcePath = sourcePath;
7866
+ quizState.resourceDir = resourceDir;
7867
+ quizState.contextPath = isQuizContextScope(scope)
7868
+ ? String(contextPathEl ? contextPathEl.value : (quizState.contextPath || getDefaultQuizContextPath(scope)) || "").trim()
7869
+ : "";
7870
+ quizState.focusPrompt = String(focusPromptEl ? focusPromptEl.value : quizState.focusPrompt || "").trim();
7871
+ }
7872
+
7873
+ function startQuizRequest() {
7874
+ readQuizSetupFields();
7875
+ const sourceText = String(quizState.sourceText || "").trim();
7876
+ if (!sourceText && !isQuizContextScope(quizState.scope)) {
7877
+ quizState.error = "Quiz source is empty.";
7878
+ renderQuizOverlay({ preserveScroll: true });
7879
+ return;
7880
+ }
7881
+ const requestId = makeRequestId();
7882
+ quizState.requestId = requestId;
7883
+ quizState.pending = true;
7884
+ quizState.error = "";
7885
+ quizState.status = "Generating quiz…";
7886
+ renderQuizOverlay({ preserveScroll: true });
7887
+ if (!sendMessage({
7888
+ type: "quiz_generate_request",
7889
+ requestId,
7890
+ sourceText,
7891
+ sourceLabel: quizState.sourceLabel,
7892
+ sourcePath: quizState.sourcePath || "",
7893
+ contextPath: quizState.contextPath || "",
7894
+ resourceDir: quizState.resourceDir || "",
7895
+ focusPrompt: quizState.focusPrompt || "",
7896
+ scope: quizState.scope,
7897
+ angle: quizState.angle,
7898
+ thinking: quizState.thinking,
7899
+ questionCount: quizState.questionCount,
7900
+ })) {
7901
+ quizState.pending = false;
7902
+ quizState.status = "";
7903
+ quizState.error = "Not connected to Studio server.";
7904
+ renderQuizOverlay({ preserveScroll: true });
7905
+ }
7906
+ }
7907
+
7908
+ function checkQuizAnswer() {
7909
+ const card = getQuizCurrentCard();
7910
+ if (!card) return;
7911
+ const answer = String(card.answer || quizState.answer || "").trim();
7912
+ if (!answer) {
7913
+ quizState.error = "Write an answer first.";
7914
+ renderQuizOverlay({ preserveScroll: true });
7915
+ return;
7916
+ }
7917
+ const requestId = makeRequestId();
7918
+ quizState.requestId = requestId;
7919
+ quizState.pending = true;
7920
+ quizState.error = "";
7921
+ quizState.status = "Checking answer…";
7922
+ renderQuizOverlay({ preserveScroll: true });
7923
+ if (!sendMessage({
7924
+ type: "quiz_answer_request",
7925
+ requestId,
7926
+ question: card.question || "",
7927
+ snippet: card.snippet || "",
7928
+ answer,
7929
+ idealAnswer: card.idealAnswer || "",
7930
+ angle: quizState.angle,
7931
+ thinking: quizState.thinking,
7932
+ sourceLabel: quizState.sourceLabel,
7933
+ })) {
7934
+ quizState.pending = false;
7935
+ quizState.status = "";
7936
+ quizState.error = "Not connected to Studio server.";
7937
+ renderQuizOverlay({ preserveScroll: true });
7938
+ }
7939
+ }
7940
+
7941
+ function discussQuizCard() {
7942
+ const card = getQuizCurrentCard();
7943
+ if (!card || !quizDialogEl) return;
7944
+ const promptEl = quizDialogEl.querySelector("[data-quiz-field='discussion']");
7945
+ const prompt = promptEl ? String(promptEl.value || "").trim() : "";
7946
+ if (!prompt) {
7947
+ quizState.error = "Write a follow-up question first.";
7948
+ renderQuizOverlay({ preserveScroll: true });
7949
+ return;
7950
+ }
7951
+ const requestId = makeRequestId();
7952
+ quizState.requestId = requestId;
7953
+ quizState.pending = true;
7954
+ quizState.error = "";
7955
+ quizState.status = "Discussing…";
7956
+ card.discussion = Array.isArray(card.discussion) ? card.discussion.concat([{ role: "user", text: prompt }]) : [{ role: "user", text: prompt }];
7957
+ renderQuizOverlay({ preserveScroll: true });
7958
+ if (!sendMessage({
7959
+ type: "quiz_discuss_request",
7960
+ requestId,
7961
+ question: card.question || "",
7962
+ snippet: card.snippet || "",
7963
+ answer: card.answer || "",
7964
+ feedback: card.feedback && card.feedback.feedback ? card.feedback.feedback : "",
7965
+ prompt,
7966
+ angle: quizState.angle,
7967
+ thinking: quizState.thinking,
7968
+ sourceLabel: quizState.sourceLabel,
7969
+ })) {
7970
+ quizState.pending = false;
7971
+ quizState.status = "";
7972
+ quizState.error = "Not connected to Studio server.";
7973
+ renderQuizOverlay({ preserveScroll: true });
7974
+ }
7975
+ }
7976
+
7977
+ function handleQuizAction(action) {
7978
+ if (action === "close") {
7979
+ endQuizOverlay();
7980
+ return;
7981
+ }
7982
+ if (action === "minimize") {
7983
+ minimizeQuizOverlay();
7984
+ return;
7985
+ }
7986
+ if (action === "start") {
7987
+ startQuizRequest();
7988
+ return;
7989
+ }
7990
+ if (action === "check") {
7991
+ checkQuizAnswer();
7992
+ return;
7993
+ }
7994
+ if (action === "discuss") {
7995
+ discussQuizCard();
7996
+ return;
7997
+ }
7998
+ if (action === "previous") {
7999
+ quizState.index = Math.max(0, (quizState.index || 0) - 1);
8000
+ quizState.error = "";
8001
+ quizState.status = "";
8002
+ renderQuizOverlay();
8003
+ return;
8004
+ }
8005
+ if (action === "next") {
8006
+ quizState.index = Math.min(Math.max(0, quizState.cards.length - 1), (quizState.index || 0) + 1);
8007
+ quizState.error = "";
8008
+ quizState.status = "";
8009
+ renderQuizOverlay();
8010
+ return;
8011
+ }
8012
+ if (action === "restart") {
8013
+ resetQuizStateFromEditor();
8014
+ renderQuizOverlay();
8015
+ }
8016
+ }
8017
+
8018
+ function handleQuizServerMessage(message) {
8019
+ if (!quizState.requestId || typeof message.requestId !== "string" || message.requestId !== quizState.requestId) return false;
8020
+ if (message.type === "quiz_progress") {
8021
+ quizState.pending = true;
8022
+ quizState.status = typeof message.message === "string" ? message.message : "Working…";
8023
+ quizState.error = "";
8024
+ renderQuizOverlay({ preserveScroll: true });
8025
+ return true;
8026
+ }
8027
+ if (message.type === "quiz_error") {
8028
+ quizState.pending = false;
8029
+ quizState.status = "";
8030
+ quizState.error = typeof message.message === "string" ? message.message : "Quiz request failed.";
8031
+ renderQuizOverlay({ preserveScroll: true });
8032
+ return true;
8033
+ }
8034
+ if (message.type === "quiz_generated") {
8035
+ const cards = Array.isArray(message.cards) ? message.cards : [];
8036
+ quizState.pending = false;
8037
+ quizState.status = "";
8038
+ quizState.error = "";
8039
+ quizState.cards = cards.map((card, index) => ({
8040
+ id: typeof card.id === "string" ? card.id : "q" + String(index + 1),
8041
+ kind: typeof card.kind === "string" ? card.kind : "",
8042
+ snippet: typeof card.snippet === "string" ? card.snippet : "",
8043
+ question: typeof card.question === "string" ? card.question : "",
8044
+ idealAnswer: typeof card.idealAnswer === "string" ? card.idealAnswer : "",
8045
+ answer: "",
8046
+ feedback: null,
8047
+ discussion: [],
8048
+ })).filter((card) => card.question);
8049
+ quizState.index = 0;
8050
+ quizState.angle = normalizeQuizAngle(message.angle || quizState.angle);
8051
+ quizState.thinking = normalizeQuizThinking(message.thinking || quizState.thinking);
8052
+ quizState.sourceLabel = typeof message.sourceLabel === "string" ? message.sourceLabel : quizState.sourceLabel;
8053
+ if (!quizState.cards.length) quizState.error = "No quiz questions were generated.";
8054
+ renderQuizOverlay();
8055
+ return true;
8056
+ }
8057
+ if (message.type === "quiz_feedback") {
8058
+ const card = getQuizCurrentCard();
8059
+ quizState.pending = false;
8060
+ quizState.status = "";
8061
+ quizState.error = "";
8062
+ if (card) card.feedback = message.feedback || null;
8063
+ renderQuizOverlay({ revealSelector: ".studio-quiz-feedback", followBottomIfNearBottom: true });
8064
+ return true;
8065
+ }
8066
+ if (message.type === "quiz_discussion") {
8067
+ const card = getQuizCurrentCard();
8068
+ quizState.pending = false;
8069
+ quizState.status = "";
8070
+ quizState.error = "";
8071
+ if (card) {
8072
+ const answer = typeof message.answer === "string" ? message.answer : "";
8073
+ card.discussion = Array.isArray(card.discussion) ? card.discussion.concat([{ role: "assistant", text: answer }]) : [{ role: "assistant", text: answer }];
8074
+ }
8075
+ renderQuizOverlay({ scrollToBottom: true });
8076
+ return true;
8077
+ }
8078
+ return false;
8079
+ }
8080
+
7275
8081
  function escapeHtml(text) {
7276
8082
  return text
7277
8083
  .replace(/&/g, "&amp;")
@@ -12403,6 +13209,10 @@
12403
13209
  critiqueBtn.disabled = true;
12404
13210
  critiqueBtn.title = "Critique is unavailable in editor-only mode.";
12405
13211
  }
13212
+ if (quizBtn) {
13213
+ quizBtn.disabled = true;
13214
+ quizBtn.title = "Quiz is unavailable in editor-only mode.";
13215
+ }
12406
13216
  syncStudioUiRefreshReviewTrigger();
12407
13217
  return;
12408
13218
  }
@@ -12463,6 +13273,15 @@
12463
13273
  ? "Critique text as-is (includes [an: ...] markers)."
12464
13274
  : "Critique text with [an: ...] markers stripped."));
12465
13275
  }
13276
+ if (quizBtn) {
13277
+ quizBtn.textContent = hasResumableQuiz() ? "Resume quiz" : "Quiz me";
13278
+ quizBtn.disabled = wsState === "Disconnected" || uiBusy || canQueueSteering;
13279
+ quizBtn.title = canQueueSteering
13280
+ ? "Quiz is unavailable while Run editor text is active."
13281
+ : (hasResumableQuiz()
13282
+ ? "Resume the current Studio quiz."
13283
+ : "Open an active quiz for the current editor selection or document.");
13284
+ }
12466
13285
  syncStudioUiRefreshReviewTrigger();
12467
13286
  }
12468
13287
 
@@ -12606,6 +13425,16 @@
12606
13425
  updateFooterMeta();
12607
13426
  }
12608
13427
 
13428
+ if (
13429
+ message.type === "quiz_progress" ||
13430
+ message.type === "quiz_generated" ||
13431
+ message.type === "quiz_feedback" ||
13432
+ message.type === "quiz_discussion" ||
13433
+ message.type === "quiz_error"
13434
+ ) {
13435
+ if (handleQuizServerMessage(message)) return;
13436
+ }
13437
+
12609
13438
  if (message.type === "debug_event") {
12610
13439
  debugTrace("server_debug_event", summarizeServerMessage(message));
12611
13440
  return;
@@ -13933,6 +14762,12 @@
13933
14762
  }
13934
14763
  });
13935
14764
 
14765
+ if (quizBtn) {
14766
+ quizBtn.addEventListener("click", () => {
14767
+ openQuizOverlay();
14768
+ });
14769
+ }
14770
+
13936
14771
  loadResponseBtn.addEventListener("click", () => {
13937
14772
  if (!latestResponseMarkdown.trim()) {
13938
14773
  setStatus("No response available yet.", "warning");