pi-studio 0.5.30 → 0.5.32
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 +19 -0
- package/README.md +1 -1
- package/client/studio-client.js +283 -9
- package/client/studio.css +164 -0
- package/index.ts +876 -51
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.32] — 2026-03-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `/studio-pdf <path>` now accepts a curated set of advanced layout controls for file-based exports, including font size, margins, line stretch, main font, paper size, geometry, heading sizes, heading spacing, and footer skip.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Large-font Markdown/QMD Studio PDF exports now switch to a more suitable LaTeX document class and use a safer default footer skip unless you explicitly override the geometry.
|
|
14
|
+
- PDF callout blocks now render more compactly, reducing extra vertical whitespace around note/tip/warning content.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Studio preview/PDF preparation now treats `.qmd` files like Markdown, strips HTML comments more narrowly, shows standalone LaTeX page-break commands as subtle preview dividers, and supports common Quarto-style callout and `fig-align` patterns in preview/PDF output.
|
|
18
|
+
- Markdown/QMD preview now renders embedded local PDF figures more reliably via `pdf.js`, avoiding grey-box browser embed failures in the Studio preview surface.
|
|
19
|
+
|
|
20
|
+
## [0.5.31] — 2026-03-24
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- The right-pane response view now nudges the browser to repaint after response renders complete, reducing cases where freshly rendered response content stayed visually blank until the user scrolled or interacted with the pane.
|
|
24
|
+
- Newly selected or newly arrived responses now reset the right-pane scroll position to the top by default, while **Editor (Preview)** continues to preserve scroll position so in-place edit/preview workflows still feel natural.
|
|
25
|
+
|
|
7
26
|
## [0.5.30] — 2026-03-24
|
|
8
27
|
|
|
9
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
43
43
|
| `/studio --stop` | Stop studio server |
|
|
44
44
|
| `/studio --help` | Show help |
|
|
45
45
|
| `/studio-current <path>` | Load a file into currently open Studio tab(s) without opening a new browser window |
|
|
46
|
-
| `/studio-pdf <path
|
|
46
|
+
| `/studio-pdf <path> [options]` | Export a local file to `<name>.studio.pdf` via the Studio PDF pipeline, with optional layout controls |
|
|
47
47
|
|
|
48
48
|
## Install
|
|
49
49
|
|
package/client/studio-client.js
CHANGED
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
189
189
|
// Single source of truth: language -> file extensions (and display label)
|
|
190
190
|
var LANG_EXT_MAP = {
|
|
191
|
-
markdown: { label: "Markdown", exts: ["md", "markdown", "mdx"] },
|
|
191
|
+
markdown: { label: "Markdown", exts: ["md", "markdown", "mdx", "qmd"] },
|
|
192
192
|
javascript: { label: "JavaScript", exts: ["js", "mjs", "cjs", "jsx"] },
|
|
193
193
|
typescript: { label: "TypeScript", exts: ["ts", "mts", "cts", "tsx"] },
|
|
194
194
|
python: { label: "Python", exts: ["py", "pyw"] },
|
|
@@ -234,6 +234,7 @@
|
|
|
234
234
|
let sourcePreviewRenderNonce = 0;
|
|
235
235
|
let responsePreviewRenderNonce = 0;
|
|
236
236
|
let responseEditorPreviewTimer = null;
|
|
237
|
+
let pendingResponseScrollReset = false;
|
|
237
238
|
let editorMetaUpdateRaf = null;
|
|
238
239
|
let editorHighlightEnabled = false;
|
|
239
240
|
let editorLanguage = "markdown";
|
|
@@ -244,6 +245,8 @@
|
|
|
244
245
|
const EMPTY_OVERLAY_LINE = "\u200b";
|
|
245
246
|
const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
246
247
|
const MATHJAX_CDN_URL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
|
|
248
|
+
const PDFJS_CDN_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/legacy/build/pdf.min.mjs";
|
|
249
|
+
const PDFJS_WORKER_CDN_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/legacy/build/pdf.worker.min.mjs";
|
|
247
250
|
const BOOT = (typeof window.__PI_STUDIO_BOOT__ === "object" && window.__PI_STUDIO_BOOT__)
|
|
248
251
|
? window.__PI_STUDIO_BOOT__
|
|
249
252
|
: {};
|
|
@@ -254,9 +257,12 @@
|
|
|
254
257
|
const MERMAID_RENDER_FAIL_MESSAGE = "Mermaid render failed. Showing diagram source text.";
|
|
255
258
|
const MATHJAX_UNAVAILABLE_MESSAGE = "Math fallback unavailable. Some unsupported equations may remain as raw TeX.";
|
|
256
259
|
const MATHJAX_RENDER_FAIL_MESSAGE = "Math fallback could not render some unsupported equations.";
|
|
260
|
+
const PDF_PREVIEW_UNAVAILABLE_MESSAGE = "PDF figure preview unavailable. Inline PDF rendering is not supported in this Studio browser environment.";
|
|
261
|
+
const PDF_PREVIEW_RENDER_FAIL_MESSAGE = "PDF figure preview could not be rendered.";
|
|
257
262
|
let mermaidModulePromise = null;
|
|
258
263
|
let mermaidInitialized = false;
|
|
259
264
|
let mathJaxPromise = null;
|
|
265
|
+
let pdfJsPromise = null;
|
|
260
266
|
|
|
261
267
|
const DEBUG_ENABLED = (() => {
|
|
262
268
|
try {
|
|
@@ -971,6 +977,7 @@
|
|
|
971
977
|
}
|
|
972
978
|
|
|
973
979
|
function clearActiveResponseView() {
|
|
980
|
+
pendingResponseScrollReset = false;
|
|
974
981
|
latestResponseMarkdown = "";
|
|
975
982
|
latestResponseThinking = "";
|
|
976
983
|
latestResponseKind = "annotation";
|
|
@@ -1016,13 +1023,13 @@
|
|
|
1016
1023
|
}
|
|
1017
1024
|
}
|
|
1018
1025
|
|
|
1019
|
-
function applySelectedHistoryItem() {
|
|
1026
|
+
function applySelectedHistoryItem(options) {
|
|
1020
1027
|
const item = getSelectedHistoryItem();
|
|
1021
1028
|
if (!item) {
|
|
1022
1029
|
clearActiveResponseView();
|
|
1023
1030
|
return false;
|
|
1024
1031
|
}
|
|
1025
|
-
handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking);
|
|
1032
|
+
handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking, options);
|
|
1026
1033
|
return true;
|
|
1027
1034
|
}
|
|
1028
1035
|
|
|
@@ -1035,9 +1042,13 @@
|
|
|
1035
1042
|
return false;
|
|
1036
1043
|
}
|
|
1037
1044
|
|
|
1045
|
+
const previousItem = getSelectedHistoryItem();
|
|
1046
|
+
const previousId = previousItem && typeof previousItem.id === "string" ? previousItem.id : null;
|
|
1038
1047
|
const nextIndex = Math.max(0, Math.min(total - 1, Number(index) || 0));
|
|
1039
1048
|
responseHistoryIndex = nextIndex;
|
|
1040
|
-
const
|
|
1049
|
+
const nextItem = getSelectedHistoryItem();
|
|
1050
|
+
const nextId = nextItem && typeof nextItem.id === "string" ? nextItem.id : null;
|
|
1051
|
+
const applied = applySelectedHistoryItem({ resetScroll: previousId !== nextId });
|
|
1041
1052
|
updateHistoryControls();
|
|
1042
1053
|
|
|
1043
1054
|
if (applied && !(options && options.silent)) {
|
|
@@ -1242,11 +1253,210 @@
|
|
|
1242
1253
|
mathMl: true,
|
|
1243
1254
|
svg: true,
|
|
1244
1255
|
},
|
|
1256
|
+
ADD_TAGS: ["embed"],
|
|
1257
|
+
ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align"],
|
|
1258
|
+
ADD_DATA_URI_TAGS: ["embed"],
|
|
1245
1259
|
});
|
|
1246
1260
|
}
|
|
1247
1261
|
return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
|
|
1248
1262
|
}
|
|
1249
1263
|
|
|
1264
|
+
function isPdfPreviewSource(src) {
|
|
1265
|
+
return Boolean(src) && (/^data:application\/pdf(?:;|,)/i.test(src) || /\.pdf(?:$|[?#])/i.test(src));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function decoratePdfEmbeds(targetEl) {
|
|
1269
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const embeds = targetEl.querySelectorAll("embed[src]");
|
|
1274
|
+
embeds.forEach(function(embedEl) {
|
|
1275
|
+
const src = typeof embedEl.getAttribute === "function" ? (embedEl.getAttribute("src") || "") : "";
|
|
1276
|
+
if (!isPdfPreviewSource(src)) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (!embedEl.getAttribute("type")) {
|
|
1280
|
+
embedEl.setAttribute("type", "application/pdf");
|
|
1281
|
+
}
|
|
1282
|
+
if (!embedEl.getAttribute("title")) {
|
|
1283
|
+
embedEl.setAttribute("title", "Embedded PDF figure");
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function decodePdfDataUri(src) {
|
|
1289
|
+
const match = String(src || "").match(/^data:application\/pdf(?:;[^,]*)?,([A-Za-z0-9+/=\s]+)$/i);
|
|
1290
|
+
if (!match) return null;
|
|
1291
|
+
const payload = (match[1] || "").replace(/\s+/g, "");
|
|
1292
|
+
if (!payload) return null;
|
|
1293
|
+
const binary = window.atob(payload);
|
|
1294
|
+
const bytes = new Uint8Array(binary.length);
|
|
1295
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
1296
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1297
|
+
}
|
|
1298
|
+
return bytes;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function ensurePdfJs() {
|
|
1302
|
+
if (window.pdfjsLib && typeof window.pdfjsLib.getDocument === "function") {
|
|
1303
|
+
return Promise.resolve(window.pdfjsLib);
|
|
1304
|
+
}
|
|
1305
|
+
if (pdfJsPromise) {
|
|
1306
|
+
return pdfJsPromise;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
pdfJsPromise = import(PDFJS_CDN_URL)
|
|
1310
|
+
.then((module) => {
|
|
1311
|
+
const api = module && typeof module.getDocument === "function"
|
|
1312
|
+
? module
|
|
1313
|
+
: (module && module.default && typeof module.default.getDocument === "function" ? module.default : null);
|
|
1314
|
+
if (!api || typeof api.getDocument !== "function") {
|
|
1315
|
+
throw new Error("pdf.js did not initialize.");
|
|
1316
|
+
}
|
|
1317
|
+
if (api.GlobalWorkerOptions && !api.GlobalWorkerOptions.workerSrc) {
|
|
1318
|
+
api.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_CDN_URL;
|
|
1319
|
+
}
|
|
1320
|
+
window.pdfjsLib = api;
|
|
1321
|
+
return api;
|
|
1322
|
+
})
|
|
1323
|
+
.catch((error) => {
|
|
1324
|
+
pdfJsPromise = null;
|
|
1325
|
+
throw error;
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
return pdfJsPromise;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function appendPdfPreviewNotice(targetEl, message) {
|
|
1332
|
+
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (targetEl.querySelector(".preview-pdf-warning")) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const warningEl = document.createElement("div");
|
|
1339
|
+
warningEl.className = "preview-warning preview-pdf-warning";
|
|
1340
|
+
warningEl.textContent = String(message || PDF_PREVIEW_UNAVAILABLE_MESSAGE);
|
|
1341
|
+
targetEl.appendChild(warningEl);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
async function loadPdfDocumentSource(src) {
|
|
1345
|
+
const embedded = decodePdfDataUri(src);
|
|
1346
|
+
if (embedded) {
|
|
1347
|
+
return { data: embedded };
|
|
1348
|
+
}
|
|
1349
|
+
const response = await fetch(src);
|
|
1350
|
+
if (!response.ok) {
|
|
1351
|
+
throw new Error("Failed to fetch PDF figure for preview.");
|
|
1352
|
+
}
|
|
1353
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1354
|
+
return { data: bytes };
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
async function renderSinglePdfPreviewEmbed(embedEl, pdfjsLib) {
|
|
1358
|
+
if (!embedEl || embedEl.dataset.studioPdfPreviewRendered === "1") {
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const src = embedEl.getAttribute("src") || "";
|
|
1363
|
+
if (!isPdfPreviewSource(src)) {
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const measuredWidth = Math.max(1, Math.round(embedEl.getBoundingClientRect().width || 0));
|
|
1368
|
+
const styleText = embedEl.getAttribute("style") || "";
|
|
1369
|
+
const widthAttr = embedEl.getAttribute("width") || "";
|
|
1370
|
+
const figAlign = embedEl.getAttribute("data-fig-align") || "";
|
|
1371
|
+
const pdfSource = await loadPdfDocumentSource(src);
|
|
1372
|
+
const loadingTask = pdfjsLib.getDocument(pdfSource);
|
|
1373
|
+
const pdfDocument = await loadingTask.promise;
|
|
1374
|
+
|
|
1375
|
+
try {
|
|
1376
|
+
const page = await pdfDocument.getPage(1);
|
|
1377
|
+
const baseViewport = page.getViewport({ scale: 1 });
|
|
1378
|
+
const cssWidth = Math.max(1, measuredWidth || Math.round(baseViewport.width));
|
|
1379
|
+
const renderScale = Math.max(0.25, cssWidth / baseViewport.width) * Math.min(window.devicePixelRatio || 1, 2);
|
|
1380
|
+
const viewport = page.getViewport({ scale: renderScale });
|
|
1381
|
+
const canvas = document.createElement("canvas");
|
|
1382
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
1383
|
+
if (!context) {
|
|
1384
|
+
throw new Error("Canvas 2D context unavailable.");
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
canvas.width = Math.max(1, Math.ceil(viewport.width));
|
|
1388
|
+
canvas.height = Math.max(1, Math.ceil(viewport.height));
|
|
1389
|
+
canvas.style.width = "100%";
|
|
1390
|
+
canvas.style.height = "auto";
|
|
1391
|
+
canvas.setAttribute("aria-label", "PDF figure preview");
|
|
1392
|
+
|
|
1393
|
+
await page.render({
|
|
1394
|
+
canvasContext: context,
|
|
1395
|
+
viewport,
|
|
1396
|
+
}).promise;
|
|
1397
|
+
|
|
1398
|
+
const wrapper = document.createElement("div");
|
|
1399
|
+
wrapper.className = "studio-pdf-preview";
|
|
1400
|
+
if (styleText) {
|
|
1401
|
+
wrapper.style.cssText = styleText;
|
|
1402
|
+
} else if (widthAttr) {
|
|
1403
|
+
wrapper.style.width = /^\d+(?:\.\d+)?$/.test(widthAttr) ? (widthAttr + "px") : widthAttr;
|
|
1404
|
+
} else {
|
|
1405
|
+
wrapper.style.width = "100%";
|
|
1406
|
+
}
|
|
1407
|
+
if (figAlign) {
|
|
1408
|
+
wrapper.setAttribute("data-fig-align", figAlign);
|
|
1409
|
+
}
|
|
1410
|
+
wrapper.title = "PDF figure preview (page 1)";
|
|
1411
|
+
wrapper.appendChild(canvas);
|
|
1412
|
+
embedEl.dataset.studioPdfPreviewRendered = "1";
|
|
1413
|
+
embedEl.replaceWith(wrapper);
|
|
1414
|
+
return true;
|
|
1415
|
+
} finally {
|
|
1416
|
+
if (typeof pdfDocument.cleanup === "function") {
|
|
1417
|
+
try { pdfDocument.cleanup(); } catch {}
|
|
1418
|
+
}
|
|
1419
|
+
if (typeof pdfDocument.destroy === "function") {
|
|
1420
|
+
try { await pdfDocument.destroy(); } catch {}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
async function renderPdfPreviewsInElement(targetEl) {
|
|
1426
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const embeds = Array.from(targetEl.querySelectorAll("embed[src]"))
|
|
1431
|
+
.filter((embedEl) => isPdfPreviewSource(embedEl.getAttribute("src") || ""));
|
|
1432
|
+
if (embeds.length === 0) {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
let pdfjsLib;
|
|
1437
|
+
try {
|
|
1438
|
+
pdfjsLib = await ensurePdfJs();
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
console.error("pdf.js load failed:", error);
|
|
1441
|
+
appendPdfPreviewNotice(targetEl, PDF_PREVIEW_UNAVAILABLE_MESSAGE);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
let hadFailure = false;
|
|
1446
|
+
for (const embedEl of embeds) {
|
|
1447
|
+
try {
|
|
1448
|
+
await renderSinglePdfPreviewEmbed(embedEl, pdfjsLib);
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
hadFailure = true;
|
|
1451
|
+
console.error("PDF preview render failed:", error);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (hadFailure) {
|
|
1456
|
+
appendPdfPreviewNotice(targetEl, PDF_PREVIEW_RENDER_FAIL_MESSAGE);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1250
1460
|
function appendMathFallbackNotice(targetEl, message) {
|
|
1251
1461
|
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
|
|
1252
1462
|
return;
|
|
@@ -1539,6 +1749,33 @@
|
|
|
1539
1749
|
targetEl.classList.remove("preview-pending");
|
|
1540
1750
|
}
|
|
1541
1751
|
|
|
1752
|
+
function scheduleResponsePaneRepaintNudge() {
|
|
1753
|
+
if (!critiqueViewEl || typeof critiqueViewEl.getBoundingClientRect !== "function") return;
|
|
1754
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
1755
|
+
? window.requestAnimationFrame.bind(window)
|
|
1756
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
1757
|
+
|
|
1758
|
+
schedule(() => {
|
|
1759
|
+
if (!critiqueViewEl || !critiqueViewEl.isConnected) return;
|
|
1760
|
+
void critiqueViewEl.getBoundingClientRect();
|
|
1761
|
+
if (!critiqueViewEl.classList) return;
|
|
1762
|
+
critiqueViewEl.classList.add("response-repaint-nudge");
|
|
1763
|
+
schedule(() => {
|
|
1764
|
+
if (!critiqueViewEl || !critiqueViewEl.classList) return;
|
|
1765
|
+
critiqueViewEl.classList.remove("response-repaint-nudge");
|
|
1766
|
+
});
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function applyPendingResponseScrollReset() {
|
|
1771
|
+
if (!pendingResponseScrollReset || !critiqueViewEl) return false;
|
|
1772
|
+
if (rightView === "editor-preview") return false;
|
|
1773
|
+
critiqueViewEl.scrollTop = 0;
|
|
1774
|
+
critiqueViewEl.scrollLeft = 0;
|
|
1775
|
+
pendingResponseScrollReset = false;
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1542
1779
|
async function getMermaidApi() {
|
|
1543
1780
|
if (mermaidModulePromise) {
|
|
1544
1781
|
return mermaidModulePromise;
|
|
@@ -1868,6 +2105,8 @@
|
|
|
1868
2105
|
|
|
1869
2106
|
finishPreviewRender(targetEl);
|
|
1870
2107
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
2108
|
+
decoratePdfEmbeds(targetEl);
|
|
2109
|
+
await renderPdfPreviewsInElement(targetEl);
|
|
1871
2110
|
const annotationMode = (pane === "source" || pane === "response")
|
|
1872
2111
|
? (annotationsEnabled ? "highlight" : "hide")
|
|
1873
2112
|
: "none";
|
|
@@ -1883,6 +2122,11 @@
|
|
|
1883
2122
|
appendPreviewNotice(targetEl, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
|
|
1884
2123
|
}
|
|
1885
2124
|
}
|
|
2125
|
+
|
|
2126
|
+
if (pane === "response") {
|
|
2127
|
+
applyPendingResponseScrollReset();
|
|
2128
|
+
scheduleResponsePaneRepaintNudge();
|
|
2129
|
+
}
|
|
1886
2130
|
} catch (error) {
|
|
1887
2131
|
if (pane === "source") {
|
|
1888
2132
|
if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
|
|
@@ -1893,6 +2137,10 @@
|
|
|
1893
2137
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
1894
2138
|
finishPreviewRender(targetEl);
|
|
1895
2139
|
targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
|
|
2140
|
+
if (pane === "response") {
|
|
2141
|
+
applyPendingResponseScrollReset();
|
|
2142
|
+
scheduleResponsePaneRepaintNudge();
|
|
2143
|
+
}
|
|
1896
2144
|
}
|
|
1897
2145
|
}
|
|
1898
2146
|
|
|
@@ -1962,11 +2210,13 @@
|
|
|
1962
2210
|
if (!editorText.trim()) {
|
|
1963
2211
|
finishPreviewRender(critiqueViewEl);
|
|
1964
2212
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
|
|
2213
|
+
scheduleResponsePaneRepaintNudge();
|
|
1965
2214
|
return;
|
|
1966
2215
|
}
|
|
1967
2216
|
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
1968
2217
|
finishPreviewRender(critiqueViewEl);
|
|
1969
2218
|
critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
|
|
2219
|
+
scheduleResponsePaneRepaintNudge();
|
|
1970
2220
|
return;
|
|
1971
2221
|
}
|
|
1972
2222
|
const nonce = ++responsePreviewRenderNonce;
|
|
@@ -1981,6 +2231,8 @@
|
|
|
1981
2231
|
critiqueViewEl.innerHTML = thinking && thinking.trim()
|
|
1982
2232
|
? buildPlainMarkdownHtml(thinking)
|
|
1983
2233
|
: "<pre class='plain-markdown'>No thinking available for this response.</pre>";
|
|
2234
|
+
applyPendingResponseScrollReset();
|
|
2235
|
+
scheduleResponsePaneRepaintNudge();
|
|
1984
2236
|
return;
|
|
1985
2237
|
}
|
|
1986
2238
|
|
|
@@ -1988,6 +2240,8 @@
|
|
|
1988
2240
|
if (!markdown || !markdown.trim()) {
|
|
1989
2241
|
finishPreviewRender(critiqueViewEl);
|
|
1990
2242
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
|
|
2243
|
+
applyPendingResponseScrollReset();
|
|
2244
|
+
scheduleResponsePaneRepaintNudge();
|
|
1991
2245
|
return;
|
|
1992
2246
|
}
|
|
1993
2247
|
|
|
@@ -2005,16 +2259,22 @@
|
|
|
2005
2259
|
"Response is too large for markdown highlighting. Showing plain markdown.",
|
|
2006
2260
|
markdown,
|
|
2007
2261
|
);
|
|
2262
|
+
applyPendingResponseScrollReset();
|
|
2263
|
+
scheduleResponsePaneRepaintNudge();
|
|
2008
2264
|
return;
|
|
2009
2265
|
}
|
|
2010
2266
|
|
|
2011
2267
|
finishPreviewRender(critiqueViewEl);
|
|
2012
2268
|
critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
|
|
2269
|
+
applyPendingResponseScrollReset();
|
|
2270
|
+
scheduleResponsePaneRepaintNudge();
|
|
2013
2271
|
return;
|
|
2014
2272
|
}
|
|
2015
2273
|
|
|
2016
2274
|
finishPreviewRender(critiqueViewEl);
|
|
2017
2275
|
critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
|
|
2276
|
+
applyPendingResponseScrollReset();
|
|
2277
|
+
scheduleResponsePaneRepaintNudge();
|
|
2018
2278
|
}
|
|
2019
2279
|
|
|
2020
2280
|
function updateResultActionButtons(normalizedEditorText) {
|
|
@@ -3058,15 +3318,29 @@
|
|
|
3058
3318
|
return lower.indexOf("## critiques") !== -1 && lower.indexOf("## document") !== -1;
|
|
3059
3319
|
}
|
|
3060
3320
|
|
|
3061
|
-
function handleIncomingResponse(markdown, kind, timestamp, thinking) {
|
|
3321
|
+
function handleIncomingResponse(markdown, kind, timestamp, thinking, options) {
|
|
3062
3322
|
const responseTimestamp =
|
|
3063
3323
|
typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0
|
|
3064
3324
|
? timestamp
|
|
3065
3325
|
: Date.now();
|
|
3326
|
+
const responseThinking = typeof thinking === "string" ? thinking : "";
|
|
3327
|
+
const responseKind = kind === "critique" ? "critique" : "annotation";
|
|
3328
|
+
const resetScroll = options && Object.prototype.hasOwnProperty.call(options, "resetScroll")
|
|
3329
|
+
? Boolean(options.resetScroll)
|
|
3330
|
+
: (
|
|
3331
|
+
latestResponseKind !== responseKind
|
|
3332
|
+
|| latestResponseTimestamp !== responseTimestamp
|
|
3333
|
+
|| latestResponseNormalized !== normalizeForCompare(markdown)
|
|
3334
|
+
|| latestResponseThinkingNormalized !== normalizeForCompare(responseThinking)
|
|
3335
|
+
);
|
|
3336
|
+
|
|
3337
|
+
if (resetScroll) {
|
|
3338
|
+
pendingResponseScrollReset = true;
|
|
3339
|
+
}
|
|
3066
3340
|
|
|
3067
3341
|
latestResponseMarkdown = markdown;
|
|
3068
|
-
latestResponseThinking =
|
|
3069
|
-
latestResponseKind =
|
|
3342
|
+
latestResponseThinking = responseThinking;
|
|
3343
|
+
latestResponseKind = responseKind;
|
|
3070
3344
|
latestResponseTimestamp = responseTimestamp;
|
|
3071
3345
|
latestResponseIsStructuredCritique = isStructuredCritique(markdown);
|
|
3072
3346
|
latestResponseHasContent = Boolean(markdown && markdown.trim());
|
|
@@ -3084,10 +3358,10 @@
|
|
|
3084
3358
|
refreshResponseUi();
|
|
3085
3359
|
}
|
|
3086
3360
|
|
|
3087
|
-
function applyLatestPayload(payload) {
|
|
3361
|
+
function applyLatestPayload(payload, options) {
|
|
3088
3362
|
if (!payload || typeof payload.markdown !== "string") return false;
|
|
3089
3363
|
const responseKind = payload.kind === "critique" ? "critique" : "annotation";
|
|
3090
|
-
handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking);
|
|
3364
|
+
handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking, options);
|
|
3091
3365
|
return true;
|
|
3092
3366
|
}
|
|
3093
3367
|
|
package/client/studio.css
CHANGED
|
@@ -633,6 +633,91 @@
|
|
|
633
633
|
color: var(--md-quote);
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
.rendered-markdown .callout-note,
|
|
637
|
+
.rendered-markdown .callout-tip,
|
|
638
|
+
.rendered-markdown .callout-warning,
|
|
639
|
+
.rendered-markdown .callout-important,
|
|
640
|
+
.rendered-markdown .callout-caution {
|
|
641
|
+
margin: 1.15em 0;
|
|
642
|
+
padding: 0.8em 1rem 0.95em;
|
|
643
|
+
border: 1px solid var(--border-muted);
|
|
644
|
+
border-left-width: 4px;
|
|
645
|
+
border-radius: 10px;
|
|
646
|
+
background: var(--panel-2);
|
|
647
|
+
color: inherit;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.rendered-markdown .callout-note::before,
|
|
651
|
+
.rendered-markdown .callout-tip::before,
|
|
652
|
+
.rendered-markdown .callout-warning::before,
|
|
653
|
+
.rendered-markdown .callout-important::before,
|
|
654
|
+
.rendered-markdown .callout-caution::before {
|
|
655
|
+
display: inline-block;
|
|
656
|
+
margin-bottom: 0.45rem;
|
|
657
|
+
font-size: 0.76em;
|
|
658
|
+
font-weight: 700;
|
|
659
|
+
letter-spacing: 0.08em;
|
|
660
|
+
text-transform: uppercase;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.rendered-markdown .callout-note::before {
|
|
664
|
+
content: "Note";
|
|
665
|
+
color: var(--accent);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.rendered-markdown .callout-tip::before {
|
|
669
|
+
content: "Tip";
|
|
670
|
+
color: var(--ok);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.rendered-markdown .callout-warning::before {
|
|
674
|
+
content: "Warning";
|
|
675
|
+
color: var(--warn);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.rendered-markdown .callout-important::before {
|
|
679
|
+
content: "Important";
|
|
680
|
+
color: var(--error);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.rendered-markdown .callout-caution::before {
|
|
684
|
+
content: "Caution";
|
|
685
|
+
color: var(--error);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.rendered-markdown .callout-note {
|
|
689
|
+
border-left-color: var(--accent);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.rendered-markdown .callout-tip {
|
|
693
|
+
border-left-color: var(--ok);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.rendered-markdown .callout-warning {
|
|
697
|
+
border-left-color: var(--warn);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.rendered-markdown .callout-important,
|
|
701
|
+
.rendered-markdown .callout-caution {
|
|
702
|
+
border-left-color: var(--error);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.rendered-markdown .callout-note > :first-child,
|
|
706
|
+
.rendered-markdown .callout-tip > :first-child,
|
|
707
|
+
.rendered-markdown .callout-warning > :first-child,
|
|
708
|
+
.rendered-markdown .callout-important > :first-child,
|
|
709
|
+
.rendered-markdown .callout-caution > :first-child {
|
|
710
|
+
margin-top: 0;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.rendered-markdown .callout-note > :last-child,
|
|
714
|
+
.rendered-markdown .callout-tip > :last-child,
|
|
715
|
+
.rendered-markdown .callout-warning > :last-child,
|
|
716
|
+
.rendered-markdown .callout-important > :last-child,
|
|
717
|
+
.rendered-markdown .callout-caution > :last-child {
|
|
718
|
+
margin-bottom: 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
636
721
|
.rendered-markdown pre {
|
|
637
722
|
background: var(--panel-2);
|
|
638
723
|
border: 1px solid var(--md-codeblock-border);
|
|
@@ -765,10 +850,83 @@
|
|
|
765
850
|
margin: 1.25em 0;
|
|
766
851
|
}
|
|
767
852
|
|
|
853
|
+
.rendered-markdown .studio-page-break {
|
|
854
|
+
display: flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
gap: 0.85rem;
|
|
857
|
+
margin: 1.75em 0;
|
|
858
|
+
color: var(--muted);
|
|
859
|
+
font-size: 0.88em;
|
|
860
|
+
text-transform: uppercase;
|
|
861
|
+
letter-spacing: 0.08em;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.rendered-markdown .studio-page-break-rule {
|
|
865
|
+
flex: 1 1 auto;
|
|
866
|
+
height: 1px;
|
|
867
|
+
background: var(--md-hr);
|
|
868
|
+
opacity: 0.95;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.rendered-markdown .studio-page-break-label {
|
|
872
|
+
flex: 0 0 auto;
|
|
873
|
+
white-space: nowrap;
|
|
874
|
+
}
|
|
875
|
+
|
|
768
876
|
.rendered-markdown img {
|
|
769
877
|
max-width: 100%;
|
|
770
878
|
}
|
|
771
879
|
|
|
880
|
+
.rendered-markdown img[data-fig-align="center"],
|
|
881
|
+
.rendered-markdown embed[data-fig-align="center"],
|
|
882
|
+
.rendered-markdown .studio-pdf-preview[data-fig-align="center"] {
|
|
883
|
+
display: block;
|
|
884
|
+
margin-left: auto;
|
|
885
|
+
margin-right: auto;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.rendered-markdown img[data-fig-align="right"],
|
|
889
|
+
.rendered-markdown embed[data-fig-align="right"],
|
|
890
|
+
.rendered-markdown .studio-pdf-preview[data-fig-align="right"] {
|
|
891
|
+
display: block;
|
|
892
|
+
margin-left: auto;
|
|
893
|
+
margin-right: 0;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.rendered-markdown embed[type="application/pdf"],
|
|
897
|
+
.rendered-markdown embed[src^="data:application/pdf"],
|
|
898
|
+
.rendered-markdown embed[src$=".pdf"] {
|
|
899
|
+
display: block;
|
|
900
|
+
width: 100%;
|
|
901
|
+
min-height: 18rem;
|
|
902
|
+
border: 1px solid var(--md-table-border);
|
|
903
|
+
border-radius: 10px;
|
|
904
|
+
background: #fff;
|
|
905
|
+
overflow: hidden;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.rendered-markdown .studio-pdf-preview {
|
|
909
|
+
display: block;
|
|
910
|
+
max-width: 100%;
|
|
911
|
+
border: 1px solid var(--md-table-border);
|
|
912
|
+
border-radius: 10px;
|
|
913
|
+
background: #fff;
|
|
914
|
+
overflow: hidden;
|
|
915
|
+
line-height: 0;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.rendered-markdown .studio-pdf-preview[data-fig-align="center"] {
|
|
919
|
+
margin-left: auto;
|
|
920
|
+
margin-right: auto;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.rendered-markdown .studio-pdf-preview canvas {
|
|
924
|
+
display: block;
|
|
925
|
+
width: 100%;
|
|
926
|
+
height: auto;
|
|
927
|
+
max-width: 100%;
|
|
928
|
+
}
|
|
929
|
+
|
|
772
930
|
.rendered-markdown .studio-subfigure-group {
|
|
773
931
|
margin: 1.25em auto;
|
|
774
932
|
}
|
|
@@ -968,6 +1126,12 @@
|
|
|
968
1126
|
opacity: 0.64;
|
|
969
1127
|
}
|
|
970
1128
|
|
|
1129
|
+
.panel-scroll.response-repaint-nudge {
|
|
1130
|
+
outline: 1px solid transparent;
|
|
1131
|
+
-webkit-transform: translateZ(0);
|
|
1132
|
+
transform: translateZ(0);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
971
1135
|
.preview-error {
|
|
972
1136
|
color: var(--warn);
|
|
973
1137
|
margin-bottom: 0.75em;
|