pi-studio 0.9.2 → 0.9.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/CHANGELOG.md +12 -1
- package/README.md +2 -2
- package/client/studio-client.js +714 -45
- package/client/studio.css +276 -1
- package/index.ts +448 -2
- package/package.json +3 -2
package/client/studio-client.js
CHANGED
|
@@ -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,31 @@
|
|
|
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_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
|
|
241
|
+
const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
242
|
+
let quizOverlayEl = null;
|
|
243
|
+
let quizDialogEl = null;
|
|
244
|
+
let quizPreviewRenderNonce = 0;
|
|
245
|
+
const quizMarkdownRenderCache = new Map();
|
|
246
|
+
let quizState = {
|
|
247
|
+
open: false,
|
|
248
|
+
requestId: null,
|
|
249
|
+
pending: false,
|
|
250
|
+
sourceText: "",
|
|
251
|
+
sourceLabel: "Studio editor",
|
|
252
|
+
scope: "editor",
|
|
253
|
+
angle: "general",
|
|
254
|
+
thinking: "minimal",
|
|
255
|
+
questionCount: QUIZ_DEFAULT_COUNT,
|
|
256
|
+
cards: [],
|
|
257
|
+
index: 0,
|
|
258
|
+
answer: "",
|
|
259
|
+
feedback: null,
|
|
260
|
+
discussion: [],
|
|
261
|
+
status: "",
|
|
262
|
+
error: "",
|
|
263
|
+
};
|
|
238
264
|
let replTmuxAvailable = null;
|
|
239
265
|
let replSessions = [];
|
|
240
266
|
let replActiveSessionName = "";
|
|
@@ -1679,7 +1705,6 @@
|
|
|
1679
1705
|
let lineNumbersEnabled = false;
|
|
1680
1706
|
let lineNumbersRenderRaf = null;
|
|
1681
1707
|
let annotationsEnabled = true;
|
|
1682
|
-
const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
|
|
1683
1708
|
const STUDIO_ZEN_MODE_STORAGE_KEY = "piStudio.zenMode";
|
|
1684
1709
|
const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
|
|
1685
1710
|
const EDITOR_FONT_SIZE_OPTIONS = [10, 11, 12, 13, 14, 15, 16, 18];
|
|
@@ -1724,16 +1749,8 @@
|
|
|
1724
1749
|
const isFalsey = (value) => ["0", "false", "no", "off", "classic"].indexOf(normalize(value)) !== -1;
|
|
1725
1750
|
if (queryValue !== null) {
|
|
1726
1751
|
const normalizedQuery = normalize(queryValue);
|
|
1727
|
-
|
|
1728
|
-
try {
|
|
1729
|
-
window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
|
|
1730
|
-
} catch {}
|
|
1731
|
-
return enabled;
|
|
1752
|
+
return isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
|
|
1732
1753
|
}
|
|
1733
|
-
try {
|
|
1734
|
-
const stored = window.localStorage ? window.localStorage.getItem(STUDIO_UI_REFRESH_STORAGE_KEY) : null;
|
|
1735
|
-
if (stored !== null) return stored !== "0" && !isFalsey(stored);
|
|
1736
|
-
} catch {}
|
|
1737
1754
|
return true;
|
|
1738
1755
|
}
|
|
1739
1756
|
|
|
@@ -1762,7 +1779,7 @@
|
|
|
1762
1779
|
function syncStudioZenModeUi() {
|
|
1763
1780
|
if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
|
|
1764
1781
|
if (!zenModeBtn) return;
|
|
1765
|
-
zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "
|
|
1782
|
+
zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "Zen";
|
|
1766
1783
|
zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls." : "Hide secondary Studio controls.";
|
|
1767
1784
|
zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
|
|
1768
1785
|
}
|
|
@@ -2001,37 +2018,6 @@
|
|
|
2001
2018
|
return { name, anchor: anchorEl, button: buttonEl, menu: menuEl };
|
|
2002
2019
|
}
|
|
2003
2020
|
|
|
2004
|
-
function setStudioUiRefreshPreference(enabled) {
|
|
2005
|
-
try {
|
|
2006
|
-
window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
|
|
2007
|
-
} catch {}
|
|
2008
|
-
try {
|
|
2009
|
-
const url = new URL(window.location.href);
|
|
2010
|
-
url.searchParams.set("uiRefresh", enabled ? "1" : "0");
|
|
2011
|
-
window.location.assign(url.toString());
|
|
2012
|
-
} catch {
|
|
2013
|
-
window.location.reload();
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
function setupStudioUiRefreshToggleButton() {
|
|
2018
|
-
if (!footerMetaEl || document.getElementById("studioUiRefreshToggleBtn")) return;
|
|
2019
|
-
const button = makeStudioUiRefreshElement("button", "footer-compact-btn studio-ui-refresh-toggle", studioUiRefreshEnabled ? "UI: Fresh" : "UI: Classic");
|
|
2020
|
-
button.id = "studioUiRefreshToggleBtn";
|
|
2021
|
-
button.type = "button";
|
|
2022
|
-
button.title = studioUiRefreshEnabled
|
|
2023
|
-
? "Switch Studio to the classic layout."
|
|
2024
|
-
: "Switch Studio to the refreshed layout.";
|
|
2025
|
-
button.addEventListener("click", () => {
|
|
2026
|
-
setStudioUiRefreshPreference(!studioUiRefreshEnabled);
|
|
2027
|
-
});
|
|
2028
|
-
if (compactBtn && compactBtn.parentNode === footerMetaEl) {
|
|
2029
|
-
compactBtn.insertAdjacentElement("afterend", button);
|
|
2030
|
-
} else {
|
|
2031
|
-
footerMetaEl.appendChild(button);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
2021
|
function setupStudioUiRefreshPrototype() {
|
|
2036
2022
|
if (!studioUiRefreshEnabled || studioUiRefreshUi) return;
|
|
2037
2023
|
const leftHeaderEl = document.getElementById("leftSectionHeader");
|
|
@@ -2042,7 +2028,7 @@
|
|
|
2042
2028
|
if (!isEditorOnlyMode && critiqueBtn && lensSelect) {
|
|
2043
2029
|
const reviewButton = makeStudioUiRefreshElement("button", "studio-refresh-tool-tab studio-refresh-review-btn", "Review");
|
|
2044
2030
|
reviewMenu = makeStudioUiRefreshMenu(reviewButton, "review", "studio-refresh-review-anchor");
|
|
2045
|
-
appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn]);
|
|
2031
|
+
appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, quizBtn]);
|
|
2046
2032
|
appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
|
|
2047
2033
|
}
|
|
2048
2034
|
|
|
@@ -2161,7 +2147,6 @@
|
|
|
2161
2147
|
syncStudioUiRefreshSummaries();
|
|
2162
2148
|
}
|
|
2163
2149
|
|
|
2164
|
-
setupStudioUiRefreshToggleButton();
|
|
2165
2150
|
setupStudioUiRefreshPrototype();
|
|
2166
2151
|
syncStudioZenModeUi();
|
|
2167
2152
|
const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
|
|
@@ -3051,6 +3036,18 @@
|
|
|
3051
3036
|
&& typeof studioPdfFocusDialogEl.contains === "function"
|
|
3052
3037
|
&& studioPdfFocusDialogEl.contains(event.target)
|
|
3053
3038
|
);
|
|
3039
|
+
const quizOwnsEvent = Boolean(
|
|
3040
|
+
quizDialogEl
|
|
3041
|
+
&& event.target
|
|
3042
|
+
&& typeof quizDialogEl.contains === "function"
|
|
3043
|
+
&& quizDialogEl.contains(event.target)
|
|
3044
|
+
);
|
|
3045
|
+
|
|
3046
|
+
if (isQuizOpen() && plainEscape) {
|
|
3047
|
+
event.preventDefault();
|
|
3048
|
+
minimizeQuizOverlay();
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3054
3051
|
|
|
3055
3052
|
if (isStudioPdfFocusOpen() && plainEscape) {
|
|
3056
3053
|
event.preventDefault();
|
|
@@ -3076,7 +3073,7 @@
|
|
|
3076
3073
|
return;
|
|
3077
3074
|
}
|
|
3078
3075
|
|
|
3079
|
-
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent) {
|
|
3076
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent || quizOwnsEvent) {
|
|
3080
3077
|
return;
|
|
3081
3078
|
}
|
|
3082
3079
|
|
|
@@ -7313,6 +7310,645 @@
|
|
|
7313
7310
|
return "draft_" + makeRequestId();
|
|
7314
7311
|
}
|
|
7315
7312
|
|
|
7313
|
+
function normalizeQuizAngle(angle) {
|
|
7314
|
+
const value = String(angle || "").trim().toLowerCase();
|
|
7315
|
+
return QUIZ_ANGLES.includes(value) ? value : "general";
|
|
7316
|
+
}
|
|
7317
|
+
|
|
7318
|
+
function getQuizAngleLabel(angle) {
|
|
7319
|
+
switch (normalizeQuizAngle(angle)) {
|
|
7320
|
+
case "scientist": return "Scientist";
|
|
7321
|
+
case "mathematician": return "Mathematician";
|
|
7322
|
+
case "statistician": return "Statistician";
|
|
7323
|
+
case "developer": return "Developer";
|
|
7324
|
+
case "reviewer": return "Reviewer";
|
|
7325
|
+
default: return "General";
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
|
|
7329
|
+
function normalizeQuizThinking(thinking) {
|
|
7330
|
+
const value = String(thinking || "").trim().toLowerCase();
|
|
7331
|
+
return QUIZ_THINKING_LEVELS.includes(value) ? value : "minimal";
|
|
7332
|
+
}
|
|
7333
|
+
|
|
7334
|
+
function getQuizThinkingLabel(thinking) {
|
|
7335
|
+
switch (normalizeQuizThinking(thinking)) {
|
|
7336
|
+
case "off": return "Off";
|
|
7337
|
+
case "low": return "Low";
|
|
7338
|
+
case "medium": return "Medium";
|
|
7339
|
+
case "high": return "High";
|
|
7340
|
+
default: return "Minimal";
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7344
|
+
function getQuizKindLabel(kind) {
|
|
7345
|
+
const value = String(kind || "").trim().toLowerCase();
|
|
7346
|
+
if (!value) return "";
|
|
7347
|
+
return value
|
|
7348
|
+
.split(/[-_\s]+/)
|
|
7349
|
+
.filter(Boolean)
|
|
7350
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
7351
|
+
.join(" ");
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7354
|
+
function getQuizModelLabel() {
|
|
7355
|
+
const label = String(modelLabel || "").trim();
|
|
7356
|
+
const withoutThinking = label.replace(/\s*\((?:off|minimal|low|medium|high|xhigh)\)\s*$/i, "").trim();
|
|
7357
|
+
return withoutThinking || label || "current Pi model";
|
|
7358
|
+
}
|
|
7359
|
+
|
|
7360
|
+
function shouldRenderQuizMarkdownPreview() {
|
|
7361
|
+
const lang = normalizeFenceLanguage(editorLanguage || "");
|
|
7362
|
+
return !lang || lang === "markdown" || lang === "latex";
|
|
7363
|
+
}
|
|
7364
|
+
|
|
7365
|
+
function renderQuizMarkdownBlockHtml(markdown, className) {
|
|
7366
|
+
const source = String(markdown || "");
|
|
7367
|
+
return "<div class='studio-quiz-markdown-body rendered-markdown " + escapeHtml(className || "") + "' data-quiz-markdown='" + escapeHtml(source) + "'>"
|
|
7368
|
+
+ "<div class='studio-quiz-markdown-fallback'>" + escapeHtml(source) + "</div>"
|
|
7369
|
+
+ "</div>";
|
|
7370
|
+
}
|
|
7371
|
+
|
|
7372
|
+
function restoreQuizScrollTop(scrollTop) {
|
|
7373
|
+
const scrollEl = getQuizScrollContainer();
|
|
7374
|
+
if (!scrollEl) return;
|
|
7375
|
+
scrollEl.scrollTop = Math.max(0, Number(scrollTop) || 0);
|
|
7376
|
+
}
|
|
7377
|
+
|
|
7378
|
+
function restoreQuizScrollTopSoon(scrollTop) {
|
|
7379
|
+
restoreQuizScrollTop(scrollTop);
|
|
7380
|
+
window.requestAnimationFrame(() => restoreQuizScrollTop(scrollTop));
|
|
7381
|
+
}
|
|
7382
|
+
|
|
7383
|
+
function isQuizScrollNearBottom(scrollEl) {
|
|
7384
|
+
if (!scrollEl) return false;
|
|
7385
|
+
return (scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 80;
|
|
7386
|
+
}
|
|
7387
|
+
|
|
7388
|
+
function scrollQuizToBottom() {
|
|
7389
|
+
const scrollEl = getQuizScrollContainer();
|
|
7390
|
+
if (!scrollEl) return;
|
|
7391
|
+
scrollEl.scrollTop = scrollEl.scrollHeight;
|
|
7392
|
+
}
|
|
7393
|
+
|
|
7394
|
+
function revealQuizTarget(selector) {
|
|
7395
|
+
if (!quizDialogEl || !selector) return false;
|
|
7396
|
+
const scrollEl = getQuizScrollContainer();
|
|
7397
|
+
const target = quizDialogEl.querySelector(selector);
|
|
7398
|
+
if (!scrollEl || !(target instanceof HTMLElement)) return false;
|
|
7399
|
+
const targetTop = target.offsetTop;
|
|
7400
|
+
const targetBottom = targetTop + target.offsetHeight;
|
|
7401
|
+
const visibleTop = scrollEl.scrollTop;
|
|
7402
|
+
const visibleBottom = visibleTop + scrollEl.clientHeight;
|
|
7403
|
+
if (targetTop >= visibleTop + 12 && targetBottom <= visibleBottom - 12) return true;
|
|
7404
|
+
scrollEl.scrollTop = Math.max(0, targetTop - 18);
|
|
7405
|
+
return true;
|
|
7406
|
+
}
|
|
7407
|
+
|
|
7408
|
+
function applyQuizScrollIntent(options, fallbackScrollTop, wasNearBottom) {
|
|
7409
|
+
const opts = options && typeof options === "object" ? options : {};
|
|
7410
|
+
if (opts.scrollToBottom || (opts.followBottomIfNearBottom && wasNearBottom)) {
|
|
7411
|
+
scrollQuizToBottom();
|
|
7412
|
+
return;
|
|
7413
|
+
}
|
|
7414
|
+
if (opts.revealSelector && revealQuizTarget(opts.revealSelector)) {
|
|
7415
|
+
return;
|
|
7416
|
+
}
|
|
7417
|
+
if (opts.preserveScroll) restoreQuizScrollTop(fallbackScrollTop);
|
|
7418
|
+
}
|
|
7419
|
+
|
|
7420
|
+
function applyQuizScrollIntentSoon(options, fallbackScrollTop, wasNearBottom) {
|
|
7421
|
+
applyQuizScrollIntent(options, fallbackScrollTop, wasNearBottom);
|
|
7422
|
+
window.requestAnimationFrame(() => applyQuizScrollIntent(options, fallbackScrollTop, wasNearBottom));
|
|
7423
|
+
}
|
|
7424
|
+
|
|
7425
|
+
function trimQuizMarkdownRenderCache() {
|
|
7426
|
+
while (quizMarkdownRenderCache.size > 80) {
|
|
7427
|
+
const firstKey = quizMarkdownRenderCache.keys().next().value;
|
|
7428
|
+
if (!firstKey) break;
|
|
7429
|
+
quizMarkdownRenderCache.delete(firstKey);
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
|
|
7433
|
+
async function renderQuizMarkdownToHtml(markdown) {
|
|
7434
|
+
const source = String(markdown || "");
|
|
7435
|
+
const cacheKey = String(editorLanguage || "markdown") + "\n" + source;
|
|
7436
|
+
if (quizMarkdownRenderCache.has(cacheKey)) return quizMarkdownRenderCache.get(cacheKey);
|
|
7437
|
+
const renderedHtml = await renderMarkdownWithPandoc(source, { includeEditorLanguage: true });
|
|
7438
|
+
const sanitized = sanitizeRenderedHtml(renderedHtml, source, { stripMarkdownHtmlComments: editorLanguage !== "latex" });
|
|
7439
|
+
quizMarkdownRenderCache.set(cacheKey, sanitized);
|
|
7440
|
+
trimQuizMarkdownRenderCache();
|
|
7441
|
+
return sanitized;
|
|
7442
|
+
}
|
|
7443
|
+
|
|
7444
|
+
async function renderQuizMarkdownFields(nonce, options) {
|
|
7445
|
+
const opts = options && typeof options === "object" ? options : {};
|
|
7446
|
+
const fallbackScrollTop = Number(opts.fallbackScrollTop) || 0;
|
|
7447
|
+
const wasNearBottom = Boolean(opts.wasNearBottom);
|
|
7448
|
+
if (!quizDialogEl || !shouldRenderQuizMarkdownPreview()) {
|
|
7449
|
+
applyQuizScrollIntentSoon(opts, fallbackScrollTop, wasNearBottom);
|
|
7450
|
+
return;
|
|
7451
|
+
}
|
|
7452
|
+
const targets = Array.from(quizDialogEl.querySelectorAll("[data-quiz-markdown]")).filter((target) => target instanceof HTMLElement);
|
|
7453
|
+
const preserveScroll = Boolean(opts.preserveScroll || opts.revealSelector || opts.scrollToBottom || opts.followBottomIfNearBottom);
|
|
7454
|
+
for (const target of targets) {
|
|
7455
|
+
const markdown = target.getAttribute("data-quiz-markdown") || "";
|
|
7456
|
+
if (!markdown.trim()) continue;
|
|
7457
|
+
const scrollEl = preserveScroll ? getQuizScrollContainer() : null;
|
|
7458
|
+
const scrollTop = scrollEl ? scrollEl.scrollTop : fallbackScrollTop;
|
|
7459
|
+
try {
|
|
7460
|
+
const html = await renderQuizMarkdownToHtml(markdown);
|
|
7461
|
+
if (nonce !== quizPreviewRenderNonce || !quizDialogEl || !quizDialogEl.contains(target)) return;
|
|
7462
|
+
target.innerHTML = html;
|
|
7463
|
+
await renderAnnotationMathInElement(target);
|
|
7464
|
+
decoratePdfEmbeds(target);
|
|
7465
|
+
await renderPdfPreviewsInElement(target);
|
|
7466
|
+
await renderMermaidInElement(target);
|
|
7467
|
+
await renderMathFallbackInElement(target);
|
|
7468
|
+
decorateCopyablePreviewBlocks(target);
|
|
7469
|
+
if (preserveScroll) restoreQuizScrollTopSoon(scrollTop);
|
|
7470
|
+
} catch (error) {
|
|
7471
|
+
console.error("Quiz markdown preview render failed:", error);
|
|
7472
|
+
target.classList.add("studio-quiz-markdown-render-failed");
|
|
7473
|
+
}
|
|
7474
|
+
}
|
|
7475
|
+
applyQuizScrollIntentSoon(opts, fallbackScrollTop, wasNearBottom);
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7478
|
+
function isQuizOpen() {
|
|
7479
|
+
return Boolean(quizOverlayEl && !quizOverlayEl.hidden);
|
|
7480
|
+
}
|
|
7481
|
+
|
|
7482
|
+
function getQuizCurrentCard() {
|
|
7483
|
+
if (!Array.isArray(quizState.cards) || quizState.cards.length === 0) return null;
|
|
7484
|
+
const index = Math.max(0, Math.min(quizState.index || 0, quizState.cards.length - 1));
|
|
7485
|
+
return quizState.cards[index] || null;
|
|
7486
|
+
}
|
|
7487
|
+
|
|
7488
|
+
function getQuizSourceLabel(scope) {
|
|
7489
|
+
const base = sourceState && sourceState.label ? sourceState.label : "Studio editor";
|
|
7490
|
+
return scope === "selection" ? base + " selection" : base;
|
|
7491
|
+
}
|
|
7492
|
+
|
|
7493
|
+
function ensureQuizOverlay() {
|
|
7494
|
+
if (quizOverlayEl && quizDialogEl) return quizOverlayEl;
|
|
7495
|
+
quizOverlayEl = document.createElement("div");
|
|
7496
|
+
quizOverlayEl.className = "studio-quiz-overlay";
|
|
7497
|
+
quizOverlayEl.setAttribute("role", "presentation");
|
|
7498
|
+
quizOverlayEl.hidden = true;
|
|
7499
|
+
quizOverlayEl.innerHTML = "<div class='studio-quiz-dialog' role='dialog' aria-modal='true' aria-label='Studio quiz'></div>";
|
|
7500
|
+
document.body.appendChild(quizOverlayEl);
|
|
7501
|
+
quizDialogEl = quizOverlayEl.querySelector(".studio-quiz-dialog");
|
|
7502
|
+
quizOverlayEl.addEventListener("click", (event) => {
|
|
7503
|
+
if (event.target === quizOverlayEl) closeQuizOverlay();
|
|
7504
|
+
});
|
|
7505
|
+
quizDialogEl.addEventListener("input", (event) => {
|
|
7506
|
+
const target = event.target;
|
|
7507
|
+
if (!(target instanceof HTMLElement)) return;
|
|
7508
|
+
if (target.matches("[data-quiz-input='answer']")) {
|
|
7509
|
+
const card = getQuizCurrentCard();
|
|
7510
|
+
if (card) card.answer = target.value;
|
|
7511
|
+
quizState.answer = target.value;
|
|
7512
|
+
}
|
|
7513
|
+
});
|
|
7514
|
+
quizDialogEl.addEventListener("click", (event) => {
|
|
7515
|
+
const target = event.target instanceof Element ? event.target.closest("[data-quiz-action]") : null;
|
|
7516
|
+
if (!target) return;
|
|
7517
|
+
event.preventDefault();
|
|
7518
|
+
handleQuizAction(target.getAttribute("data-quiz-action") || "");
|
|
7519
|
+
});
|
|
7520
|
+
quizDialogEl.addEventListener("keydown", handleQuizKeydown);
|
|
7521
|
+
return quizOverlayEl;
|
|
7522
|
+
}
|
|
7523
|
+
|
|
7524
|
+
function resetQuizStateFromEditor() {
|
|
7525
|
+
const previousAngle = normalizeQuizAngle(quizState.angle);
|
|
7526
|
+
const previousThinking = normalizeQuizThinking(quizState.thinking);
|
|
7527
|
+
const previousCount = quizState.questionCount || QUIZ_DEFAULT_COUNT;
|
|
7528
|
+
const selection = getEditorSelectionRange();
|
|
7529
|
+
const hasSelection = Boolean(selection.selected && selection.selected.trim());
|
|
7530
|
+
const scope = hasSelection ? "selection" : "editor";
|
|
7531
|
+
quizState = {
|
|
7532
|
+
open: true,
|
|
7533
|
+
requestId: null,
|
|
7534
|
+
pending: false,
|
|
7535
|
+
sourceText: hasSelection ? selection.selected : selection.raw,
|
|
7536
|
+
sourceLabel: getQuizSourceLabel(scope),
|
|
7537
|
+
scope,
|
|
7538
|
+
angle: previousAngle,
|
|
7539
|
+
thinking: previousThinking,
|
|
7540
|
+
questionCount: previousCount,
|
|
7541
|
+
cards: [],
|
|
7542
|
+
index: 0,
|
|
7543
|
+
answer: "",
|
|
7544
|
+
feedback: null,
|
|
7545
|
+
discussion: [],
|
|
7546
|
+
status: "",
|
|
7547
|
+
error: "",
|
|
7548
|
+
};
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
function hasResumableQuiz() {
|
|
7552
|
+
return Boolean(
|
|
7553
|
+
quizState.pending ||
|
|
7554
|
+
(Array.isArray(quizState.cards) && quizState.cards.length > 0) ||
|
|
7555
|
+
(quizState.sourceText && (quizState.status || quizState.error))
|
|
7556
|
+
);
|
|
7557
|
+
}
|
|
7558
|
+
|
|
7559
|
+
function openQuizOverlay() {
|
|
7560
|
+
ensureQuizOverlay();
|
|
7561
|
+
if (!hasResumableQuiz()) {
|
|
7562
|
+
resetQuizStateFromEditor();
|
|
7563
|
+
} else {
|
|
7564
|
+
quizState.open = true;
|
|
7565
|
+
}
|
|
7566
|
+
quizOverlayEl.hidden = false;
|
|
7567
|
+
document.body.classList.add("studio-quiz-open");
|
|
7568
|
+
renderQuizOverlay();
|
|
7569
|
+
}
|
|
7570
|
+
|
|
7571
|
+
function closeQuizOverlay() {
|
|
7572
|
+
if (!quizOverlayEl) return;
|
|
7573
|
+
quizOverlayEl.hidden = true;
|
|
7574
|
+
document.body.classList.remove("studio-quiz-open");
|
|
7575
|
+
quizState.open = false;
|
|
7576
|
+
syncActionButtons();
|
|
7577
|
+
}
|
|
7578
|
+
|
|
7579
|
+
function minimizeQuizOverlay() {
|
|
7580
|
+
closeQuizOverlay();
|
|
7581
|
+
setStatus("Quiz minimized — use Review → Quiz me to resume.", "success");
|
|
7582
|
+
}
|
|
7583
|
+
|
|
7584
|
+
function handleQuizKeydown(event) {
|
|
7585
|
+
if (!event) return;
|
|
7586
|
+
const key = typeof event.key === "string" ? event.key : "";
|
|
7587
|
+
const plainEscape = key === "Escape" && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
|
|
7588
|
+
const submitShortcut = key === "Enter" && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey;
|
|
7589
|
+
if (plainEscape) {
|
|
7590
|
+
event.preventDefault();
|
|
7591
|
+
event.stopPropagation();
|
|
7592
|
+
minimizeQuizOverlay();
|
|
7593
|
+
return;
|
|
7594
|
+
}
|
|
7595
|
+
if (submitShortcut) {
|
|
7596
|
+
event.preventDefault();
|
|
7597
|
+
event.stopPropagation();
|
|
7598
|
+
const card = getQuizCurrentCard();
|
|
7599
|
+
if (!card) {
|
|
7600
|
+
startQuizRequest();
|
|
7601
|
+
return;
|
|
7602
|
+
}
|
|
7603
|
+
if (!card.feedback) {
|
|
7604
|
+
checkQuizAnswer();
|
|
7605
|
+
return;
|
|
7606
|
+
}
|
|
7607
|
+
const promptEl = quizDialogEl ? quizDialogEl.querySelector("[data-quiz-field='discussion']") : null;
|
|
7608
|
+
const prompt = promptEl ? String(promptEl.value || "").trim() : "";
|
|
7609
|
+
if (prompt) {
|
|
7610
|
+
discussQuizCard();
|
|
7611
|
+
} else if (quizState.index < quizState.cards.length - 1) {
|
|
7612
|
+
quizState.index = Math.min(quizState.cards.length - 1, (quizState.index || 0) + 1);
|
|
7613
|
+
quizState.error = "";
|
|
7614
|
+
quizState.status = "";
|
|
7615
|
+
renderQuizOverlay();
|
|
7616
|
+
}
|
|
7617
|
+
return;
|
|
7618
|
+
}
|
|
7619
|
+
event.stopPropagation();
|
|
7620
|
+
}
|
|
7621
|
+
|
|
7622
|
+
function renderQuizOption(value, selected, label) {
|
|
7623
|
+
return "<option value='" + escapeHtml(value) + "'" + (value === selected ? " selected" : "") + ">" + escapeHtml(label) + "</option>";
|
|
7624
|
+
}
|
|
7625
|
+
|
|
7626
|
+
function renderQuizSetupHtml() {
|
|
7627
|
+
const scope = quizState.scope === "selection" ? "selection" : "editor";
|
|
7628
|
+
const angle = normalizeQuizAngle(quizState.angle);
|
|
7629
|
+
const thinking = normalizeQuizThinking(quizState.thinking);
|
|
7630
|
+
const count = Math.max(1, Math.min(8, Math.floor(Number(quizState.questionCount) || QUIZ_DEFAULT_COUNT)));
|
|
7631
|
+
const selection = getEditorSelectionRange();
|
|
7632
|
+
const hasSelection = Boolean(selection.selected && selection.selected.trim());
|
|
7633
|
+
return "<div class='studio-quiz-setup'>"
|
|
7634
|
+
+ "<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>"
|
|
7635
|
+
+ "<div class='studio-quiz-fields'>"
|
|
7636
|
+
+ "<label>Scope<select data-quiz-field='scope'>"
|
|
7637
|
+
+ renderQuizOption("editor", scope, "Editor")
|
|
7638
|
+
+ (hasSelection ? renderQuizOption("selection", scope, "Selection") : "")
|
|
7639
|
+
+ "</select></label>"
|
|
7640
|
+
+ "<label>Angle<select data-quiz-field='angle'>"
|
|
7641
|
+
+ QUIZ_ANGLES.map((candidate) => renderQuizOption(candidate, angle, getQuizAngleLabel(candidate))).join("")
|
|
7642
|
+
+ "</select></label>"
|
|
7643
|
+
+ "<label>Thinking<select data-quiz-field='thinking'>"
|
|
7644
|
+
+ QUIZ_THINKING_LEVELS.map((candidate) => renderQuizOption(candidate, thinking, getQuizThinkingLabel(candidate))).join("")
|
|
7645
|
+
+ "</select></label>"
|
|
7646
|
+
+ "<label>Questions<input data-quiz-field='count' type='number' min='1' max='8' value='" + String(count) + "'></label>"
|
|
7647
|
+
+ "</div>"
|
|
7648
|
+
+ "<div class='studio-quiz-source-note'>Source: " + escapeHtml(getQuizSourceLabel(scope)) + " · " + escapeHtml(String((scope === "selection" ? selection.selected : selection.raw).trim().length)) + " chars · Studio model: " + escapeHtml(getQuizModelLabel()) + "</div>"
|
|
7649
|
+
+ (quizState.error ? "<div class='studio-quiz-error'>" + escapeHtml(quizState.error) + "</div>" : "")
|
|
7650
|
+
+ (quizState.status ? "<div class='studio-quiz-status'>" + escapeHtml(quizState.status) + "</div>" : "")
|
|
7651
|
+
+ "<div class='studio-quiz-actions'><button data-quiz-action='start' type='button'" + (quizState.pending ? " disabled" : "") + ">" + (quizState.pending ? "Generating…" : "Start quiz") + "</button></div>"
|
|
7652
|
+
+ "</div>";
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
function getQuizScrollContainer() {
|
|
7656
|
+
if (!quizDialogEl) return null;
|
|
7657
|
+
return quizDialogEl.querySelector(".studio-quiz-card, .studio-quiz-setup");
|
|
7658
|
+
}
|
|
7659
|
+
|
|
7660
|
+
function renderQuizCardHtml() {
|
|
7661
|
+
const card = getQuizCurrentCard();
|
|
7662
|
+
if (!card) return renderQuizSetupHtml();
|
|
7663
|
+
const total = quizState.cards.length;
|
|
7664
|
+
const index = Math.max(0, Math.min(quizState.index || 0, total - 1));
|
|
7665
|
+
const feedback = card.feedback || null;
|
|
7666
|
+
const answer = typeof card.answer === "string" ? card.answer : "";
|
|
7667
|
+
const discussion = Array.isArray(card.discussion) ? card.discussion : [];
|
|
7668
|
+
const scoreClass = feedback && feedback.score ? String(feedback.score).toLowerCase().replace(/[^a-z0-9_-]/g, "") : "";
|
|
7669
|
+
const idealAnswer = feedback && feedback.idealAnswer ? feedback.idealAnswer : (card.idealAnswer || "");
|
|
7670
|
+
const kindLabel = getQuizKindLabel(card.kind);
|
|
7671
|
+
const cardMeta = [kindLabel, getQuizAngleLabel(quizState.angle), quizState.sourceLabel || "Studio editor"].filter(Boolean).join(" · ");
|
|
7672
|
+
const renderMarkdown = shouldRenderQuizMarkdownPreview();
|
|
7673
|
+
return "<div class='studio-quiz-card'>"
|
|
7674
|
+
+ "<div class='studio-quiz-meta'><span>Question " + String(index + 1) + " of " + String(total) + "</span><span>" + escapeHtml(cardMeta) + "</span></div>"
|
|
7675
|
+
+ (card.snippet ? (renderMarkdown ? renderQuizMarkdownBlockHtml(card.snippet, "studio-quiz-snippet") : "<pre class='studio-quiz-snippet'><code>" + escapeHtml(card.snippet) + "</code></pre>") : "")
|
|
7676
|
+
+ (renderMarkdown ? renderQuizMarkdownBlockHtml(card.question || "", "studio-quiz-question") : "<div class='studio-quiz-question'>" + escapeHtml(card.question || "") + "</div>")
|
|
7677
|
+
+ "<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>"
|
|
7678
|
+
+ (feedback ? "<div class='studio-quiz-feedback studio-quiz-score-" + escapeHtml(scoreClass) + "'>"
|
|
7679
|
+
+ "<div class='studio-quiz-feedback-title'>" + escapeHtml(feedback.score || "feedback") + "</div>"
|
|
7680
|
+
+ (feedback.feedback ? renderQuizMarkdownBlockHtml(feedback.feedback, "studio-quiz-feedback-text") : "")
|
|
7681
|
+
+ (idealAnswer ? "<div class='studio-quiz-ideal'><strong>Stronger answer</strong>" + renderQuizMarkdownBlockHtml(idealAnswer, "studio-quiz-feedback-text") + "</div>" : "")
|
|
7682
|
+
+ (feedback.followUp ? "<div class='studio-quiz-follow-up'><strong>Suggested stretch question</strong>" + renderQuizMarkdownBlockHtml(feedback.followUp, "studio-quiz-feedback-text") + "</div>" : "")
|
|
7683
|
+
+ "</div>" : "")
|
|
7684
|
+
+ (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>" : "")
|
|
7685
|
+
+ (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>" : "")
|
|
7686
|
+
+ (quizState.error ? "<div class='studio-quiz-error'>" + escapeHtml(quizState.error) + "</div>" : "")
|
|
7687
|
+
+ (quizState.status ? "<div class='studio-quiz-status'>" + escapeHtml(quizState.status) + "</div>" : "")
|
|
7688
|
+
+ "<div class='studio-quiz-actions studio-quiz-card-actions'>"
|
|
7689
|
+
+ "<button data-quiz-action='previous' type='button'" + (index <= 0 ? " disabled" : "") + ">Previous</button>"
|
|
7690
|
+
+ (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>")
|
|
7691
|
+
+ "<button data-quiz-action='restart' type='button'>New quiz</button>"
|
|
7692
|
+
+ "</div>"
|
|
7693
|
+
+ "</div>";
|
|
7694
|
+
}
|
|
7695
|
+
|
|
7696
|
+
function renderQuizOverlay(options) {
|
|
7697
|
+
if (!quizDialogEl) return;
|
|
7698
|
+
const scrollOptions = options && typeof options === "object" ? options : {};
|
|
7699
|
+
const preserveScroll = Boolean(scrollOptions.preserveScroll || scrollOptions.revealSelector || scrollOptions.scrollToBottom || scrollOptions.followBottomIfNearBottom);
|
|
7700
|
+
const previousScrollEl = getQuizScrollContainer();
|
|
7701
|
+
const previousScrollTop = previousScrollEl ? previousScrollEl.scrollTop : 0;
|
|
7702
|
+
const wasNearBottom = isQuizScrollNearBottom(previousScrollEl);
|
|
7703
|
+
const bodyHtml = quizState.cards && quizState.cards.length ? renderQuizCardHtml() : renderQuizSetupHtml();
|
|
7704
|
+
quizDialogEl.innerHTML = "<div class='studio-quiz-header'>"
|
|
7705
|
+
+ "<div><div class='studio-quiz-eyebrow'>Review</div><h2>Quiz me</h2></div>"
|
|
7706
|
+
+ "<div class='studio-quiz-header-actions'>"
|
|
7707
|
+
+ "<button class='studio-quiz-minimize' data-quiz-action='minimize' type='button'>Minimize</button>"
|
|
7708
|
+
+ "<button class='studio-quiz-close' data-quiz-action='close' type='button' aria-label='Close quiz'>Close</button>"
|
|
7709
|
+
+ "</div>"
|
|
7710
|
+
+ "</div>"
|
|
7711
|
+
+ bodyHtml;
|
|
7712
|
+
if (preserveScroll) {
|
|
7713
|
+
const nextScrollEl = getQuizScrollContainer();
|
|
7714
|
+
if (nextScrollEl) {
|
|
7715
|
+
nextScrollEl.scrollTop = previousScrollTop;
|
|
7716
|
+
window.requestAnimationFrame(() => {
|
|
7717
|
+
const rafScrollEl = getQuizScrollContainer();
|
|
7718
|
+
if (rafScrollEl) rafScrollEl.scrollTop = previousScrollTop;
|
|
7719
|
+
});
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7722
|
+
applyQuizScrollIntentSoon(scrollOptions, previousScrollTop, wasNearBottom);
|
|
7723
|
+
const renderNonce = ++quizPreviewRenderNonce;
|
|
7724
|
+
void renderQuizMarkdownFields(renderNonce, {
|
|
7725
|
+
...scrollOptions,
|
|
7726
|
+
preserveScroll,
|
|
7727
|
+
fallbackScrollTop: previousScrollTop,
|
|
7728
|
+
wasNearBottom,
|
|
7729
|
+
});
|
|
7730
|
+
}
|
|
7731
|
+
|
|
7732
|
+
function readQuizSetupFields() {
|
|
7733
|
+
if (!quizDialogEl) return;
|
|
7734
|
+
const scopeEl = quizDialogEl.querySelector("[data-quiz-field='scope']");
|
|
7735
|
+
const angleEl = quizDialogEl.querySelector("[data-quiz-field='angle']");
|
|
7736
|
+
const thinkingEl = quizDialogEl.querySelector("[data-quiz-field='thinking']");
|
|
7737
|
+
const countEl = quizDialogEl.querySelector("[data-quiz-field='count']");
|
|
7738
|
+
const selection = getEditorSelectionRange();
|
|
7739
|
+
const scope = scopeEl && scopeEl.value === "selection" && selection.selected.trim() ? "selection" : "editor";
|
|
7740
|
+
quizState.scope = scope;
|
|
7741
|
+
quizState.angle = normalizeQuizAngle(angleEl ? angleEl.value : quizState.angle);
|
|
7742
|
+
quizState.thinking = normalizeQuizThinking(thinkingEl ? thinkingEl.value : quizState.thinking);
|
|
7743
|
+
quizState.questionCount = Math.max(1, Math.min(8, Math.floor(Number(countEl ? countEl.value : quizState.questionCount) || QUIZ_DEFAULT_COUNT)));
|
|
7744
|
+
quizState.sourceText = scope === "selection" ? selection.selected : selection.raw;
|
|
7745
|
+
quizState.sourceLabel = getQuizSourceLabel(scope);
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7748
|
+
function startQuizRequest() {
|
|
7749
|
+
readQuizSetupFields();
|
|
7750
|
+
const sourceText = String(quizState.sourceText || "").trim();
|
|
7751
|
+
if (!sourceText) {
|
|
7752
|
+
quizState.error = "Quiz source is empty.";
|
|
7753
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7754
|
+
return;
|
|
7755
|
+
}
|
|
7756
|
+
const requestId = makeRequestId();
|
|
7757
|
+
quizState.requestId = requestId;
|
|
7758
|
+
quizState.pending = true;
|
|
7759
|
+
quizState.error = "";
|
|
7760
|
+
quizState.status = "Generating quiz…";
|
|
7761
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7762
|
+
if (!sendMessage({
|
|
7763
|
+
type: "quiz_generate_request",
|
|
7764
|
+
requestId,
|
|
7765
|
+
sourceText,
|
|
7766
|
+
sourceLabel: quizState.sourceLabel,
|
|
7767
|
+
scope: quizState.scope,
|
|
7768
|
+
angle: quizState.angle,
|
|
7769
|
+
thinking: quizState.thinking,
|
|
7770
|
+
questionCount: quizState.questionCount,
|
|
7771
|
+
})) {
|
|
7772
|
+
quizState.pending = false;
|
|
7773
|
+
quizState.status = "";
|
|
7774
|
+
quizState.error = "Not connected to Studio server.";
|
|
7775
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7776
|
+
}
|
|
7777
|
+
}
|
|
7778
|
+
|
|
7779
|
+
function checkQuizAnswer() {
|
|
7780
|
+
const card = getQuizCurrentCard();
|
|
7781
|
+
if (!card) return;
|
|
7782
|
+
const answer = String(card.answer || quizState.answer || "").trim();
|
|
7783
|
+
if (!answer) {
|
|
7784
|
+
quizState.error = "Write an answer first.";
|
|
7785
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7786
|
+
return;
|
|
7787
|
+
}
|
|
7788
|
+
const requestId = makeRequestId();
|
|
7789
|
+
quizState.requestId = requestId;
|
|
7790
|
+
quizState.pending = true;
|
|
7791
|
+
quizState.error = "";
|
|
7792
|
+
quizState.status = "Checking answer…";
|
|
7793
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7794
|
+
if (!sendMessage({
|
|
7795
|
+
type: "quiz_answer_request",
|
|
7796
|
+
requestId,
|
|
7797
|
+
question: card.question || "",
|
|
7798
|
+
snippet: card.snippet || "",
|
|
7799
|
+
answer,
|
|
7800
|
+
idealAnswer: card.idealAnswer || "",
|
|
7801
|
+
angle: quizState.angle,
|
|
7802
|
+
thinking: quizState.thinking,
|
|
7803
|
+
sourceLabel: quizState.sourceLabel,
|
|
7804
|
+
})) {
|
|
7805
|
+
quizState.pending = false;
|
|
7806
|
+
quizState.status = "";
|
|
7807
|
+
quizState.error = "Not connected to Studio server.";
|
|
7808
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7809
|
+
}
|
|
7810
|
+
}
|
|
7811
|
+
|
|
7812
|
+
function discussQuizCard() {
|
|
7813
|
+
const card = getQuizCurrentCard();
|
|
7814
|
+
if (!card || !quizDialogEl) return;
|
|
7815
|
+
const promptEl = quizDialogEl.querySelector("[data-quiz-field='discussion']");
|
|
7816
|
+
const prompt = promptEl ? String(promptEl.value || "").trim() : "";
|
|
7817
|
+
if (!prompt) {
|
|
7818
|
+
quizState.error = "Write a follow-up question first.";
|
|
7819
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7820
|
+
return;
|
|
7821
|
+
}
|
|
7822
|
+
const requestId = makeRequestId();
|
|
7823
|
+
quizState.requestId = requestId;
|
|
7824
|
+
quizState.pending = true;
|
|
7825
|
+
quizState.error = "";
|
|
7826
|
+
quizState.status = "Discussing…";
|
|
7827
|
+
card.discussion = Array.isArray(card.discussion) ? card.discussion.concat([{ role: "user", text: prompt }]) : [{ role: "user", text: prompt }];
|
|
7828
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7829
|
+
if (!sendMessage({
|
|
7830
|
+
type: "quiz_discuss_request",
|
|
7831
|
+
requestId,
|
|
7832
|
+
question: card.question || "",
|
|
7833
|
+
snippet: card.snippet || "",
|
|
7834
|
+
answer: card.answer || "",
|
|
7835
|
+
feedback: card.feedback && card.feedback.feedback ? card.feedback.feedback : "",
|
|
7836
|
+
prompt,
|
|
7837
|
+
angle: quizState.angle,
|
|
7838
|
+
thinking: quizState.thinking,
|
|
7839
|
+
sourceLabel: quizState.sourceLabel,
|
|
7840
|
+
})) {
|
|
7841
|
+
quizState.pending = false;
|
|
7842
|
+
quizState.status = "";
|
|
7843
|
+
quizState.error = "Not connected to Studio server.";
|
|
7844
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
|
|
7848
|
+
function handleQuizAction(action) {
|
|
7849
|
+
if (action === "close") {
|
|
7850
|
+
closeQuizOverlay();
|
|
7851
|
+
return;
|
|
7852
|
+
}
|
|
7853
|
+
if (action === "minimize") {
|
|
7854
|
+
minimizeQuizOverlay();
|
|
7855
|
+
return;
|
|
7856
|
+
}
|
|
7857
|
+
if (action === "start") {
|
|
7858
|
+
startQuizRequest();
|
|
7859
|
+
return;
|
|
7860
|
+
}
|
|
7861
|
+
if (action === "check") {
|
|
7862
|
+
checkQuizAnswer();
|
|
7863
|
+
return;
|
|
7864
|
+
}
|
|
7865
|
+
if (action === "discuss") {
|
|
7866
|
+
discussQuizCard();
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
7869
|
+
if (action === "previous") {
|
|
7870
|
+
quizState.index = Math.max(0, (quizState.index || 0) - 1);
|
|
7871
|
+
quizState.error = "";
|
|
7872
|
+
quizState.status = "";
|
|
7873
|
+
renderQuizOverlay();
|
|
7874
|
+
return;
|
|
7875
|
+
}
|
|
7876
|
+
if (action === "next") {
|
|
7877
|
+
quizState.index = Math.min(Math.max(0, quizState.cards.length - 1), (quizState.index || 0) + 1);
|
|
7878
|
+
quizState.error = "";
|
|
7879
|
+
quizState.status = "";
|
|
7880
|
+
renderQuizOverlay();
|
|
7881
|
+
return;
|
|
7882
|
+
}
|
|
7883
|
+
if (action === "restart") {
|
|
7884
|
+
resetQuizStateFromEditor();
|
|
7885
|
+
renderQuizOverlay();
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
|
|
7889
|
+
function handleQuizServerMessage(message) {
|
|
7890
|
+
if (!quizState.requestId || typeof message.requestId !== "string" || message.requestId !== quizState.requestId) return false;
|
|
7891
|
+
if (message.type === "quiz_progress") {
|
|
7892
|
+
quizState.pending = true;
|
|
7893
|
+
quizState.status = typeof message.message === "string" ? message.message : "Working…";
|
|
7894
|
+
quizState.error = "";
|
|
7895
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7896
|
+
return true;
|
|
7897
|
+
}
|
|
7898
|
+
if (message.type === "quiz_error") {
|
|
7899
|
+
quizState.pending = false;
|
|
7900
|
+
quizState.status = "";
|
|
7901
|
+
quizState.error = typeof message.message === "string" ? message.message : "Quiz request failed.";
|
|
7902
|
+
renderQuizOverlay({ preserveScroll: true });
|
|
7903
|
+
return true;
|
|
7904
|
+
}
|
|
7905
|
+
if (message.type === "quiz_generated") {
|
|
7906
|
+
const cards = Array.isArray(message.cards) ? message.cards : [];
|
|
7907
|
+
quizState.pending = false;
|
|
7908
|
+
quizState.status = "";
|
|
7909
|
+
quizState.error = "";
|
|
7910
|
+
quizState.cards = cards.map((card, index) => ({
|
|
7911
|
+
id: typeof card.id === "string" ? card.id : "q" + String(index + 1),
|
|
7912
|
+
kind: typeof card.kind === "string" ? card.kind : "",
|
|
7913
|
+
snippet: typeof card.snippet === "string" ? card.snippet : "",
|
|
7914
|
+
question: typeof card.question === "string" ? card.question : "",
|
|
7915
|
+
idealAnswer: typeof card.idealAnswer === "string" ? card.idealAnswer : "",
|
|
7916
|
+
answer: "",
|
|
7917
|
+
feedback: null,
|
|
7918
|
+
discussion: [],
|
|
7919
|
+
})).filter((card) => card.question);
|
|
7920
|
+
quizState.index = 0;
|
|
7921
|
+
quizState.angle = normalizeQuizAngle(message.angle || quizState.angle);
|
|
7922
|
+
quizState.thinking = normalizeQuizThinking(message.thinking || quizState.thinking);
|
|
7923
|
+
quizState.sourceLabel = typeof message.sourceLabel === "string" ? message.sourceLabel : quizState.sourceLabel;
|
|
7924
|
+
if (!quizState.cards.length) quizState.error = "No quiz questions were generated.";
|
|
7925
|
+
renderQuizOverlay();
|
|
7926
|
+
return true;
|
|
7927
|
+
}
|
|
7928
|
+
if (message.type === "quiz_feedback") {
|
|
7929
|
+
const card = getQuizCurrentCard();
|
|
7930
|
+
quizState.pending = false;
|
|
7931
|
+
quizState.status = "";
|
|
7932
|
+
quizState.error = "";
|
|
7933
|
+
if (card) card.feedback = message.feedback || null;
|
|
7934
|
+
renderQuizOverlay({ revealSelector: ".studio-quiz-feedback", followBottomIfNearBottom: true });
|
|
7935
|
+
return true;
|
|
7936
|
+
}
|
|
7937
|
+
if (message.type === "quiz_discussion") {
|
|
7938
|
+
const card = getQuizCurrentCard();
|
|
7939
|
+
quizState.pending = false;
|
|
7940
|
+
quizState.status = "";
|
|
7941
|
+
quizState.error = "";
|
|
7942
|
+
if (card) {
|
|
7943
|
+
const answer = typeof message.answer === "string" ? message.answer : "";
|
|
7944
|
+
card.discussion = Array.isArray(card.discussion) ? card.discussion.concat([{ role: "assistant", text: answer }]) : [{ role: "assistant", text: answer }];
|
|
7945
|
+
}
|
|
7946
|
+
renderQuizOverlay({ scrollToBottom: true });
|
|
7947
|
+
return true;
|
|
7948
|
+
}
|
|
7949
|
+
return false;
|
|
7950
|
+
}
|
|
7951
|
+
|
|
7316
7952
|
function escapeHtml(text) {
|
|
7317
7953
|
return text
|
|
7318
7954
|
.replace(/&/g, "&")
|
|
@@ -12444,6 +13080,10 @@
|
|
|
12444
13080
|
critiqueBtn.disabled = true;
|
|
12445
13081
|
critiqueBtn.title = "Critique is unavailable in editor-only mode.";
|
|
12446
13082
|
}
|
|
13083
|
+
if (quizBtn) {
|
|
13084
|
+
quizBtn.disabled = true;
|
|
13085
|
+
quizBtn.title = "Quiz is unavailable in editor-only mode.";
|
|
13086
|
+
}
|
|
12447
13087
|
syncStudioUiRefreshReviewTrigger();
|
|
12448
13088
|
return;
|
|
12449
13089
|
}
|
|
@@ -12504,6 +13144,15 @@
|
|
|
12504
13144
|
? "Critique text as-is (includes [an: ...] markers)."
|
|
12505
13145
|
: "Critique text with [an: ...] markers stripped."));
|
|
12506
13146
|
}
|
|
13147
|
+
if (quizBtn) {
|
|
13148
|
+
quizBtn.textContent = hasResumableQuiz() ? "Resume quiz" : "Quiz me";
|
|
13149
|
+
quizBtn.disabled = wsState === "Disconnected" || uiBusy || canQueueSteering;
|
|
13150
|
+
quizBtn.title = canQueueSteering
|
|
13151
|
+
? "Quiz is unavailable while Run editor text is active."
|
|
13152
|
+
: (hasResumableQuiz()
|
|
13153
|
+
? "Resume the current Studio quiz."
|
|
13154
|
+
: "Open an active quiz for the current editor selection or document.");
|
|
13155
|
+
}
|
|
12507
13156
|
syncStudioUiRefreshReviewTrigger();
|
|
12508
13157
|
}
|
|
12509
13158
|
|
|
@@ -12647,6 +13296,16 @@
|
|
|
12647
13296
|
updateFooterMeta();
|
|
12648
13297
|
}
|
|
12649
13298
|
|
|
13299
|
+
if (
|
|
13300
|
+
message.type === "quiz_progress" ||
|
|
13301
|
+
message.type === "quiz_generated" ||
|
|
13302
|
+
message.type === "quiz_feedback" ||
|
|
13303
|
+
message.type === "quiz_discussion" ||
|
|
13304
|
+
message.type === "quiz_error"
|
|
13305
|
+
) {
|
|
13306
|
+
if (handleQuizServerMessage(message)) return;
|
|
13307
|
+
}
|
|
13308
|
+
|
|
12650
13309
|
if (message.type === "debug_event") {
|
|
12651
13310
|
debugTrace("server_debug_event", summarizeServerMessage(message));
|
|
12652
13311
|
return;
|
|
@@ -13974,6 +14633,16 @@
|
|
|
13974
14633
|
}
|
|
13975
14634
|
});
|
|
13976
14635
|
|
|
14636
|
+
if (quizBtn) {
|
|
14637
|
+
quizBtn.addEventListener("click", () => {
|
|
14638
|
+
if (!hasResumableQuiz() && !String(sourceTextEl.value || "").trim()) {
|
|
14639
|
+
setStatus("Add editor text before starting a quiz.", "warning");
|
|
14640
|
+
return;
|
|
14641
|
+
}
|
|
14642
|
+
openQuizOverlay();
|
|
14643
|
+
});
|
|
14644
|
+
}
|
|
14645
|
+
|
|
13977
14646
|
loadResponseBtn.addEventListener("click", () => {
|
|
13978
14647
|
if (!latestResponseMarkdown.trim()) {
|
|
13979
14648
|
setStatus("No response available yet.", "warning");
|