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.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/client/studio-client.js +837 -2
- package/client/studio.css +313 -0
- package/index.ts +767 -3
- 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,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, "&")
|
|
@@ -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");
|