pi-studio 0.8.0 → 0.8.2
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 +10 -0
- package/README.md +2 -1
- package/client/studio-client.js +314 -7
- package/client/studio.css +110 -2
- package/index.ts +31 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.2] — 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Improved dark-theme contrast for Studio dropdown arrows and the HTML artifact zoom percentage by using the stronger Studio info text colour token.
|
|
11
|
+
|
|
12
|
+
## [0.8.1] — 2026-05-13
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Added first-cut HTML artifact preview for straight, unfenced HTML in Studio preview panes, rendered in a sandboxed iframe with inline scripts enabled, network requests blocked by CSP, fit/capped sizing, and toolbar zoom controls; HTML export preserves authored HTML artifacts instead of converting them through Markdown.
|
|
16
|
+
|
|
7
17
|
## [0.8.0] — 2026-05-12
|
|
8
18
|
|
|
9
19
|
### Added
|
package/README.md
CHANGED
|
@@ -35,9 +35,10 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
35
35
|
- strips markers before send (optional)
|
|
36
36
|
- saves `.annotated.md`
|
|
37
37
|
- Renders Markdown/LaTeX/code previews (math + Mermaid), theme-synced with pi
|
|
38
|
+
- Renders straight, unfenced HTML artifacts in preview via a sandboxed browser iframe with zoom controls, while fenced `html` blocks remain source code
|
|
38
39
|
- Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks
|
|
39
40
|
- Ships optional `pi-studio-dark` and `pi-studio-light` themes tuned for Studio's browser workspace
|
|
40
|
-
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML
|
|
41
|
+
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML, preserving authored HTML artifacts as HTML
|
|
41
42
|
- Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf` or `/studio-html <path>` to `<name>.studio.html`; without a path, those commands export the last model response to a timestamped file
|
|
42
43
|
- Shows model/session/context usage in the footer, plus a compact-context action
|
|
43
44
|
|
package/client/studio-client.js
CHANGED
|
@@ -760,6 +760,7 @@
|
|
|
760
760
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
761
761
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
762
762
|
const previewPendingTimers = new WeakMap();
|
|
763
|
+
const htmlArtifactFramesById = new Map();
|
|
763
764
|
let sourcePreviewRenderTimer = null;
|
|
764
765
|
let sourcePreviewRenderNonce = 0;
|
|
765
766
|
let responsePreviewRenderNonce = 0;
|
|
@@ -2550,6 +2551,283 @@
|
|
|
2550
2551
|
return "<div class='preview-error'>" + escapeHtml(String(message || "Preview rendering failed.")) + "</div>" + buildPlainMarkdownHtml(markdown, options);
|
|
2551
2552
|
}
|
|
2552
2553
|
|
|
2554
|
+
function stripLeadingHtmlPreviewTrivia(text) {
|
|
2555
|
+
let source = String(text || "").replace(/^\uFEFF/, "").trimStart();
|
|
2556
|
+
let previous = "";
|
|
2557
|
+
while (source && source !== previous) {
|
|
2558
|
+
previous = source;
|
|
2559
|
+
source = source.replace(/^<!--[\s\S]*?-->\s*/, "").trimStart();
|
|
2560
|
+
}
|
|
2561
|
+
return source;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function startsWithSingleFencedBlock(text) {
|
|
2565
|
+
const source = String(text || "").trimStart();
|
|
2566
|
+
return /^(`{3,}|~{3,})/.test(source);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function isLikelyFullHtmlDocument(text) {
|
|
2570
|
+
const source = stripLeadingHtmlPreviewTrivia(text);
|
|
2571
|
+
if (!source) return false;
|
|
2572
|
+
if (/^<!doctype\s+html\b/i.test(source)) return true;
|
|
2573
|
+
if (/^<html(?:\s|>|$)/i.test(source)) return true;
|
|
2574
|
+
if (/^<body(?:\s|>|$)/i.test(source) && /<\/body\s*>/i.test(source)) return true;
|
|
2575
|
+
return false;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
function looksLikeHtmlMarkup(text) {
|
|
2579
|
+
return /<[A-Za-z][A-Za-z0-9:-]*(?:\s[^<>]*)?>/.test(String(text || ""));
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
function isHtmlArtifactPreviewText(text, language) {
|
|
2583
|
+
const source = String(text || "");
|
|
2584
|
+
if (!source.trim()) return false;
|
|
2585
|
+
if (startsWithSingleFencedBlock(source)) return false;
|
|
2586
|
+
if (isLikelyFullHtmlDocument(source)) return true;
|
|
2587
|
+
return normalizeFenceLanguage(language || "") === "html" && looksLikeHtmlMarkup(source);
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
const HTML_ARTIFACT_PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline' data: blob:; style-src 'unsafe-inline' data: blob:; img-src data: blob:; font-src data: blob:; connect-src 'none'; media-src data: blob:; object-src 'none'; frame-src data: blob:; child-src data: blob:; worker-src blob:; form-action 'none'; base-uri 'none'; navigate-to 'none'";
|
|
2591
|
+
const HTML_ARTIFACT_FRAME_MIN_HEIGHT = 360;
|
|
2592
|
+
const HTML_ARTIFACT_FRAME_FIT_CAP_HEIGHT = 1800;
|
|
2593
|
+
const HTML_ARTIFACT_ZOOM_MIN = 0.5;
|
|
2594
|
+
const HTML_ARTIFACT_ZOOM_MAX = 1.75;
|
|
2595
|
+
const HTML_ARTIFACT_ZOOM_STEP = 0.1;
|
|
2596
|
+
|
|
2597
|
+
function buildHtmlArtifactPreviewResizeScript(previewId) {
|
|
2598
|
+
const idJson = JSON.stringify(String(previewId || ""));
|
|
2599
|
+
return "<script>\n"
|
|
2600
|
+
+ "(() => {\n"
|
|
2601
|
+
+ " const PREVIEW_ID = " + idJson.replace(/<\//g, "<\\/") + ";\n"
|
|
2602
|
+
+ " let lastHeight = 0;\n"
|
|
2603
|
+
+ " let scheduled = false;\n"
|
|
2604
|
+
+ " let currentZoom = 1;\n"
|
|
2605
|
+
+ " function applyZoom(value) {\n"
|
|
2606
|
+
+ " const next = Number(value);\n"
|
|
2607
|
+
+ " if (!Number.isFinite(next) || next <= 0) return;\n"
|
|
2608
|
+
+ " currentZoom = Math.max(0.25, Math.min(4, next));\n"
|
|
2609
|
+
+ " document.documentElement.style.zoom = String(currentZoom);\n"
|
|
2610
|
+
+ " lastHeight = 0;\n"
|
|
2611
|
+
+ " scheduleHeight();\n"
|
|
2612
|
+
+ " }\n"
|
|
2613
|
+
+ " function measureHeight() {\n"
|
|
2614
|
+
+ " const body = document.body;\n"
|
|
2615
|
+
+ " const root = document.documentElement;\n"
|
|
2616
|
+
+ " return Math.ceil(Math.max(\n"
|
|
2617
|
+
+ " body ? body.scrollHeight : 0,\n"
|
|
2618
|
+
+ " body ? body.offsetHeight : 0,\n"
|
|
2619
|
+
+ " root ? root.scrollHeight : 0,\n"
|
|
2620
|
+
+ " root ? root.offsetHeight : 0\n"
|
|
2621
|
+
+ " ));\n"
|
|
2622
|
+
+ " }\n"
|
|
2623
|
+
+ " function sendHeight() {\n"
|
|
2624
|
+
+ " scheduled = false;\n"
|
|
2625
|
+
+ " const height = measureHeight();\n"
|
|
2626
|
+
+ " if (!height || Math.abs(height - lastHeight) < 2) return;\n"
|
|
2627
|
+
+ " lastHeight = height;\n"
|
|
2628
|
+
+ " try { parent.postMessage({ type: 'pi-studio-html-artifact-size', id: PREVIEW_ID, height }, '*'); } catch {}\n"
|
|
2629
|
+
+ " }\n"
|
|
2630
|
+
+ " function scheduleHeight() {\n"
|
|
2631
|
+
+ " if (scheduled) return;\n"
|
|
2632
|
+
+ " scheduled = true;\n"
|
|
2633
|
+
+ " requestAnimationFrame(sendHeight);\n"
|
|
2634
|
+
+ " }\n"
|
|
2635
|
+
+ " window.addEventListener('message', (event) => {\n"
|
|
2636
|
+
+ " const data = event && event.data;\n"
|
|
2637
|
+
+ " if (!data || typeof data !== 'object') return;\n"
|
|
2638
|
+
+ " if (data.type !== 'pi-studio-html-artifact-zoom' || data.id !== PREVIEW_ID) return;\n"
|
|
2639
|
+
+ " applyZoom(data.zoom);\n"
|
|
2640
|
+
+ " });\n"
|
|
2641
|
+
+ " window.addEventListener('load', scheduleHeight);\n"
|
|
2642
|
+
+ " window.addEventListener('resize', scheduleHeight);\n"
|
|
2643
|
+
+ " if (typeof ResizeObserver === 'function') {\n"
|
|
2644
|
+
+ " const observer = new ResizeObserver(scheduleHeight);\n"
|
|
2645
|
+
+ " observer.observe(document.documentElement);\n"
|
|
2646
|
+
+ " if (document.body) observer.observe(document.body);\n"
|
|
2647
|
+
+ " }\n"
|
|
2648
|
+
+ " if (typeof MutationObserver === 'function') {\n"
|
|
2649
|
+
+ " const observer = new MutationObserver(scheduleHeight);\n"
|
|
2650
|
+
+ " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
|
|
2651
|
+
+ " }\n"
|
|
2652
|
+
+ " scheduleHeight();\n"
|
|
2653
|
+
+ " setTimeout(scheduleHeight, 80);\n"
|
|
2654
|
+
+ " setTimeout(scheduleHeight, 350);\n"
|
|
2655
|
+
+ "})();\n"
|
|
2656
|
+
+ "<\/script>";
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
function buildHtmlArtifactPreviewHeadMarkup(previewId) {
|
|
2660
|
+
return "<meta charset=\"utf-8\">\n"
|
|
2661
|
+
+ "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n"
|
|
2662
|
+
+ "<meta http-equiv=\"Content-Security-Policy\" content=\"" + escapeHtml(HTML_ARTIFACT_PREVIEW_CSP) + "\">\n"
|
|
2663
|
+
+ buildHtmlArtifactPreviewResizeScript(previewId);
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
function buildHtmlArtifactSrcdoc(html, previewId) {
|
|
2667
|
+
const source = String(html || "");
|
|
2668
|
+
const headMarkup = buildHtmlArtifactPreviewHeadMarkup(previewId);
|
|
2669
|
+
if (/<head\b[^>]*>/i.test(source)) {
|
|
2670
|
+
return source.replace(/<head\b[^>]*>/i, (match) => match + "\n" + headMarkup + "\n");
|
|
2671
|
+
}
|
|
2672
|
+
if (/<body\b[^>]*>/i.test(source)) {
|
|
2673
|
+
return source.replace(/<body\b/i, "<head>\n" + headMarkup + "\n</head>\n<body");
|
|
2674
|
+
}
|
|
2675
|
+
if (/<html\b[^>]*>/i.test(source)) {
|
|
2676
|
+
return source.replace(/<html\b[^>]*>/i, (match) => match + "\n<head>\n" + headMarkup + "\n</head>\n");
|
|
2677
|
+
}
|
|
2678
|
+
return "<!doctype html>\n<html>\n<head>\n" + headMarkup + "\n</head>\n<body>\n" + source + "\n</body>\n</html>";
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function pruneDisconnectedHtmlArtifactFrames() {
|
|
2682
|
+
htmlArtifactFramesById.forEach((record, id) => {
|
|
2683
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
2684
|
+
htmlArtifactFramesById.delete(id);
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function handleHtmlArtifactFrameSizeMessage(event) {
|
|
2690
|
+
const data = event && event.data;
|
|
2691
|
+
if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-size") return;
|
|
2692
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
2693
|
+
const record = id ? htmlArtifactFramesById.get(id) : null;
|
|
2694
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
2695
|
+
if (id) htmlArtifactFramesById.delete(id);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
2699
|
+
const rawHeight = Number(data.height);
|
|
2700
|
+
if (!Number.isFinite(rawHeight) || rawHeight <= 0) return;
|
|
2701
|
+
const measuredHeight = Math.ceil(rawHeight + 2);
|
|
2702
|
+
const capped = measuredHeight > HTML_ARTIFACT_FRAME_FIT_CAP_HEIGHT;
|
|
2703
|
+
const nextHeight = Math.max(
|
|
2704
|
+
HTML_ARTIFACT_FRAME_MIN_HEIGHT,
|
|
2705
|
+
Math.min(HTML_ARTIFACT_FRAME_FIT_CAP_HEIGHT, measuredHeight),
|
|
2706
|
+
);
|
|
2707
|
+
record.iframe.style.height = nextHeight + "px";
|
|
2708
|
+
record.iframe.classList.toggle("is-height-capped", capped);
|
|
2709
|
+
if (record.shell && record.shell.style) {
|
|
2710
|
+
record.shell.style.minHeight = "0";
|
|
2711
|
+
record.shell.classList.toggle("is-height-capped", capped);
|
|
2712
|
+
}
|
|
2713
|
+
if (record.detail) {
|
|
2714
|
+
record.detail.textContent = "HTML artifact";
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
|
|
2719
|
+
|
|
2720
|
+
function renderHtmlArtifactPreview(targetEl, html, pane, options) {
|
|
2721
|
+
if (!targetEl) return;
|
|
2722
|
+
const title = options && options.title ? String(options.title) : "HTML artifact preview";
|
|
2723
|
+
const previewId = "html_artifact_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10);
|
|
2724
|
+
pruneDisconnectedHtmlArtifactFrames();
|
|
2725
|
+
clearPreviewJumpHighlight(targetEl);
|
|
2726
|
+
finishPreviewRender(targetEl);
|
|
2727
|
+
targetEl.innerHTML = "";
|
|
2728
|
+
|
|
2729
|
+
const shell = document.createElement("div");
|
|
2730
|
+
shell.className = "studio-html-artifact-shell";
|
|
2731
|
+
|
|
2732
|
+
const toolbar = document.createElement("div");
|
|
2733
|
+
toolbar.className = "studio-html-artifact-toolbar";
|
|
2734
|
+
const label = document.createElement("span");
|
|
2735
|
+
label.className = "studio-html-artifact-label";
|
|
2736
|
+
label.textContent = title;
|
|
2737
|
+
const detail = document.createElement("span");
|
|
2738
|
+
detail.className = "studio-html-artifact-detail";
|
|
2739
|
+
detail.textContent = "HTML artifact";
|
|
2740
|
+
|
|
2741
|
+
const tools = document.createElement("span");
|
|
2742
|
+
tools.className = "studio-html-artifact-tools";
|
|
2743
|
+
tools.appendChild(detail);
|
|
2744
|
+
|
|
2745
|
+
const zoomControls = document.createElement("span");
|
|
2746
|
+
zoomControls.className = "studio-html-artifact-zoom-controls";
|
|
2747
|
+
let artifactZoom = 1;
|
|
2748
|
+
let iframe = null;
|
|
2749
|
+
const formatZoomLabel = () => Math.round(artifactZoom * 100) + "%";
|
|
2750
|
+
const postArtifactZoom = () => {
|
|
2751
|
+
if (!iframe || !iframe.contentWindow) return;
|
|
2752
|
+
try {
|
|
2753
|
+
iframe.contentWindow.postMessage({ type: "pi-studio-html-artifact-zoom", id: previewId, zoom: artifactZoom }, "*");
|
|
2754
|
+
} catch {
|
|
2755
|
+
// Ignore iframe postMessage failures.
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
const updateZoomUi = () => {
|
|
2759
|
+
zoomResetBtn.textContent = formatZoomLabel();
|
|
2760
|
+
zoomOutBtn.disabled = artifactZoom <= HTML_ARTIFACT_ZOOM_MIN + 0.001;
|
|
2761
|
+
zoomInBtn.disabled = artifactZoom >= HTML_ARTIFACT_ZOOM_MAX - 0.001;
|
|
2762
|
+
};
|
|
2763
|
+
const setArtifactZoom = (nextZoom) => {
|
|
2764
|
+
artifactZoom = Math.max(
|
|
2765
|
+
HTML_ARTIFACT_ZOOM_MIN,
|
|
2766
|
+
Math.min(HTML_ARTIFACT_ZOOM_MAX, Math.round(Number(nextZoom || 1) * 100) / 100),
|
|
2767
|
+
);
|
|
2768
|
+
updateZoomUi();
|
|
2769
|
+
postArtifactZoom();
|
|
2770
|
+
};
|
|
2771
|
+
const makeZoomButton = (text, title, onClick) => {
|
|
2772
|
+
const button = document.createElement("button");
|
|
2773
|
+
button.type = "button";
|
|
2774
|
+
button.className = "studio-html-artifact-zoom-btn";
|
|
2775
|
+
button.textContent = text;
|
|
2776
|
+
button.title = title;
|
|
2777
|
+
button.addEventListener("pointerdown", (event) => { event.stopPropagation(); });
|
|
2778
|
+
button.addEventListener("mousedown", (event) => { event.stopPropagation(); });
|
|
2779
|
+
button.addEventListener("click", (event) => {
|
|
2780
|
+
event.preventDefault();
|
|
2781
|
+
event.stopPropagation();
|
|
2782
|
+
onClick();
|
|
2783
|
+
});
|
|
2784
|
+
return button;
|
|
2785
|
+
};
|
|
2786
|
+
const zoomOutBtn = makeZoomButton("−", "Zoom out HTML artifact", () => setArtifactZoom(artifactZoom - HTML_ARTIFACT_ZOOM_STEP));
|
|
2787
|
+
const zoomResetBtn = makeZoomButton("100%", "Reset HTML artifact zoom", () => setArtifactZoom(1));
|
|
2788
|
+
zoomResetBtn.classList.add("studio-html-artifact-zoom-reset");
|
|
2789
|
+
const zoomInBtn = makeZoomButton("+", "Zoom in HTML artifact", () => setArtifactZoom(artifactZoom + HTML_ARTIFACT_ZOOM_STEP));
|
|
2790
|
+
zoomControls.appendChild(zoomOutBtn);
|
|
2791
|
+
zoomControls.appendChild(zoomResetBtn);
|
|
2792
|
+
zoomControls.appendChild(zoomInBtn);
|
|
2793
|
+
updateZoomUi();
|
|
2794
|
+
tools.appendChild(zoomControls);
|
|
2795
|
+
|
|
2796
|
+
toolbar.appendChild(label);
|
|
2797
|
+
toolbar.appendChild(tools);
|
|
2798
|
+
shell.appendChild(toolbar);
|
|
2799
|
+
|
|
2800
|
+
iframe = document.createElement("iframe");
|
|
2801
|
+
iframe.className = "studio-html-artifact-frame";
|
|
2802
|
+
iframe.title = title;
|
|
2803
|
+
iframe.loading = "lazy";
|
|
2804
|
+
iframe.referrerPolicy = "no-referrer";
|
|
2805
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-modals");
|
|
2806
|
+
iframe.setAttribute("allow", "clipboard-write");
|
|
2807
|
+
iframe.addEventListener("load", () => { postArtifactZoom(); });
|
|
2808
|
+
iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
|
|
2809
|
+
shell.appendChild(iframe);
|
|
2810
|
+
htmlArtifactFramesById.set(previewId, { iframe, shell, detail, zoomControls });
|
|
2811
|
+
|
|
2812
|
+
targetEl.appendChild(shell);
|
|
2813
|
+
|
|
2814
|
+
if (pane === "response") {
|
|
2815
|
+
applyPendingResponseScrollReset();
|
|
2816
|
+
scheduleResponsePaneRepaintNudge();
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
function getRightPaneHtmlArtifactSource() {
|
|
2821
|
+
if (rightView === "editor-preview") {
|
|
2822
|
+
const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
2823
|
+
return isHtmlArtifactPreviewText(editorText, editorLanguage) ? editorText : "";
|
|
2824
|
+
}
|
|
2825
|
+
if (rightView === "preview") {
|
|
2826
|
+
return isHtmlArtifactPreviewText(latestResponseMarkdown, "") ? latestResponseMarkdown : "";
|
|
2827
|
+
}
|
|
2828
|
+
return "";
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2553
2831
|
function stripMatchingQuotes(value) {
|
|
2554
2832
|
const text = String(value || "").trim();
|
|
2555
2833
|
if (text.length >= 2) {
|
|
@@ -3602,6 +3880,12 @@
|
|
|
3602
3880
|
return;
|
|
3603
3881
|
}
|
|
3604
3882
|
|
|
3883
|
+
const htmlArtifactSource = getRightPaneHtmlArtifactSource();
|
|
3884
|
+
if (htmlArtifactSource) {
|
|
3885
|
+
setStatus("PDF export does not support HTML artifacts yet. Export as HTML or use the browser print dialog inside the artifact.", "warning");
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3605
3889
|
const markdown = rightView === "editor-preview"
|
|
3606
3890
|
? prepareEditorTextForPdfExport(sourceTextEl.value)
|
|
3607
3891
|
: prepareEditorTextForPreview(latestResponseMarkdown);
|
|
@@ -3761,9 +4045,10 @@
|
|
|
3761
4045
|
return;
|
|
3762
4046
|
}
|
|
3763
4047
|
|
|
3764
|
-
const
|
|
4048
|
+
const htmlArtifactSource = getRightPaneHtmlArtifactSource();
|
|
4049
|
+
const markdown = htmlArtifactSource || (rightView === "editor-preview"
|
|
3765
4050
|
? prepareEditorTextForHtmlExport(sourceTextEl.value)
|
|
3766
|
-
: prepareEditorTextForPreview(latestResponseMarkdown);
|
|
4051
|
+
: prepareEditorTextForPreview(latestResponseMarkdown));
|
|
3767
4052
|
if (!markdown || !markdown.trim()) {
|
|
3768
4053
|
setStatus("Nothing to export yet.", "warning");
|
|
3769
4054
|
return;
|
|
@@ -3773,10 +4058,10 @@
|
|
|
3773
4058
|
const sourcePath = effectivePath || sourceState.path || "";
|
|
3774
4059
|
const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
|
|
3775
4060
|
const isEditorPreview = rightView === "editor-preview";
|
|
3776
|
-
const editorHtmlLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
|
|
3777
|
-
const isLatex = isEditorPreview
|
|
4061
|
+
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
|
|
4062
|
+
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
3778
4063
|
? editorHtmlLanguage === "latex"
|
|
3779
|
-
: /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
4064
|
+
: /\\documentclass\b|\\begin\{document\}/.test(markdown));
|
|
3780
4065
|
let filenameHint = isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html";
|
|
3781
4066
|
let titleHint = isEditorPreview ? "Studio editor preview" : "Studio response preview";
|
|
3782
4067
|
if (sourcePath) {
|
|
@@ -4114,6 +4399,10 @@
|
|
|
4114
4399
|
function renderSourcePreviewNow() {
|
|
4115
4400
|
if (editorView !== "preview") return;
|
|
4116
4401
|
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
4402
|
+
if (isHtmlArtifactPreviewText(text, editorLanguage)) {
|
|
4403
|
+
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML artifact preview" });
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4117
4406
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
4118
4407
|
renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
|
|
4119
4408
|
return;
|
|
@@ -4382,6 +4671,10 @@
|
|
|
4382
4671
|
scheduleResponsePaneRepaintNudge();
|
|
4383
4672
|
return;
|
|
4384
4673
|
}
|
|
4674
|
+
if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
|
|
4675
|
+
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML artifact preview" });
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4385
4678
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
4386
4679
|
renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
|
|
4387
4680
|
return;
|
|
@@ -4402,6 +4695,10 @@
|
|
|
4402
4695
|
}
|
|
4403
4696
|
|
|
4404
4697
|
if (rightView === "preview") {
|
|
4698
|
+
if (isHtmlArtifactPreviewText(markdown, "")) {
|
|
4699
|
+
renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML artifact preview" });
|
|
4700
|
+
return;
|
|
4701
|
+
}
|
|
4405
4702
|
const nonce = ++responsePreviewRenderNonce;
|
|
4406
4703
|
beginPreviewRender(critiqueViewEl);
|
|
4407
4704
|
void applyRenderedMarkdown(critiqueViewEl, markdown, "response", nonce);
|
|
@@ -4468,6 +4765,8 @@
|
|
|
4468
4765
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4469
4766
|
const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
|
|
4470
4767
|
const canExportPreview = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
|
|
4768
|
+
const htmlArtifactExportSource = canExportPreview ? getRightPaneHtmlArtifactSource() : "";
|
|
4769
|
+
const isHtmlArtifactPreview = Boolean(htmlArtifactExportSource);
|
|
4471
4770
|
if (exportPdfBtn) {
|
|
4472
4771
|
exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4473
4772
|
exportPdfBtn.textContent = previewExportInProgress ? "Exporting…" : "Export right preview";
|
|
@@ -4477,19 +4776,27 @@
|
|
|
4477
4776
|
exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export.";
|
|
4478
4777
|
} else if (!canExportPreview) {
|
|
4479
4778
|
exportPdfBtn.title = "Nothing to export yet.";
|
|
4779
|
+
} else if (isHtmlArtifactPreview) {
|
|
4780
|
+
exportPdfBtn.title = "This is an HTML artifact preview. Export as HTML; PDF export is not available yet.";
|
|
4480
4781
|
} else {
|
|
4481
4782
|
exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
|
|
4482
4783
|
}
|
|
4483
4784
|
}
|
|
4484
4785
|
if (exportPreviewPdfBtn) {
|
|
4485
|
-
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4786
|
+
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
4787
|
+
exportPreviewPdfBtn.title = isHtmlArtifactPreview
|
|
4788
|
+
? "HTML artifact PDF export is not available yet."
|
|
4789
|
+
: "Export the current right-pane preview as PDF.";
|
|
4486
4790
|
}
|
|
4487
4791
|
if (exportPreviewHtmlBtn) {
|
|
4488
4792
|
exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4793
|
+
exportPreviewHtmlBtn.title = isHtmlArtifactPreview
|
|
4794
|
+
? "Export the authored HTML artifact."
|
|
4795
|
+
: "Export the current right-pane preview as standalone HTML.";
|
|
4489
4796
|
}
|
|
4490
4797
|
if (exportPreviewControlsEl) {
|
|
4491
4798
|
exportPreviewControlsEl.title = canExportPreview
|
|
4492
|
-
? "Choose a format and export the current right-pane preview."
|
|
4799
|
+
? (isHtmlArtifactPreview ? "Export this HTML artifact." : "Choose a format and export the current right-pane preview.")
|
|
4493
4800
|
: "Switch right pane to a non-empty preview before exporting.";
|
|
4494
4801
|
}
|
|
4495
4802
|
if (!canExportPreview || previewExportInProgress) {
|
package/client/studio.css
CHANGED
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
|
|
78
78
|
.export-preview-trigger::after {
|
|
79
79
|
content: "⌄";
|
|
80
|
-
color: var(--
|
|
80
|
+
color: var(--studio-info-text, var(--text));
|
|
81
81
|
font-size: 14px;
|
|
82
82
|
line-height: 1;
|
|
83
83
|
transform: translateY(-1px);
|
|
@@ -1510,6 +1510,114 @@
|
|
|
1510
1510
|
background: #fff;
|
|
1511
1511
|
}
|
|
1512
1512
|
|
|
1513
|
+
.rendered-markdown .studio-html-artifact-shell {
|
|
1514
|
+
display: flex;
|
|
1515
|
+
flex-direction: column;
|
|
1516
|
+
min-height: min(760px, calc(100vh - 190px));
|
|
1517
|
+
border: 1px solid var(--panel-border);
|
|
1518
|
+
border-radius: 12px;
|
|
1519
|
+
background: var(--panel);
|
|
1520
|
+
overflow: hidden;
|
|
1521
|
+
box-shadow: 0 1px 2px var(--shadow-color);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
.rendered-markdown .studio-html-artifact-toolbar {
|
|
1525
|
+
display: flex;
|
|
1526
|
+
align-items: center;
|
|
1527
|
+
justify-content: space-between;
|
|
1528
|
+
gap: 10px;
|
|
1529
|
+
padding: 8px 10px;
|
|
1530
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
1531
|
+
background: var(--panel-2);
|
|
1532
|
+
color: var(--studio-info-text, var(--muted));
|
|
1533
|
+
font-family: var(--font-ui);
|
|
1534
|
+
font-size: 12px;
|
|
1535
|
+
line-height: 1.25;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
.rendered-markdown .studio-html-artifact-label {
|
|
1539
|
+
min-width: 0;
|
|
1540
|
+
overflow: hidden;
|
|
1541
|
+
text-overflow: ellipsis;
|
|
1542
|
+
white-space: nowrap;
|
|
1543
|
+
color: var(--text);
|
|
1544
|
+
font-weight: 600;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.rendered-markdown .studio-html-artifact-tools {
|
|
1548
|
+
flex: 0 0 auto;
|
|
1549
|
+
display: inline-flex;
|
|
1550
|
+
align-items: center;
|
|
1551
|
+
gap: 10px;
|
|
1552
|
+
min-width: 0;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
.rendered-markdown .studio-html-artifact-detail {
|
|
1556
|
+
flex: 0 0 auto;
|
|
1557
|
+
color: var(--muted);
|
|
1558
|
+
font-size: 11px;
|
|
1559
|
+
white-space: nowrap;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.rendered-markdown .studio-html-artifact-zoom-controls {
|
|
1563
|
+
flex: 0 0 auto;
|
|
1564
|
+
display: inline-flex;
|
|
1565
|
+
align-items: center;
|
|
1566
|
+
gap: 3px;
|
|
1567
|
+
padding: 2px;
|
|
1568
|
+
border: 1px solid var(--control-border);
|
|
1569
|
+
border-radius: 999px;
|
|
1570
|
+
background: var(--panel);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.rendered-markdown .studio-html-artifact-zoom-btn {
|
|
1574
|
+
min-width: 24px;
|
|
1575
|
+
height: 22px;
|
|
1576
|
+
padding: 0 7px;
|
|
1577
|
+
border: 0;
|
|
1578
|
+
border-radius: 999px;
|
|
1579
|
+
background: transparent;
|
|
1580
|
+
color: var(--text);
|
|
1581
|
+
font: inherit;
|
|
1582
|
+
font-size: 11px;
|
|
1583
|
+
line-height: 1;
|
|
1584
|
+
cursor: pointer;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
.rendered-markdown .studio-html-artifact-zoom-btn:not(:disabled):hover,
|
|
1588
|
+
.rendered-markdown .studio-html-artifact-zoom-btn:not(:disabled):focus-visible {
|
|
1589
|
+
background: var(--control-hover-bg, var(--inline-code-bg));
|
|
1590
|
+
outline: none;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
.rendered-markdown .studio-html-artifact-zoom-btn:disabled {
|
|
1594
|
+
color: var(--muted);
|
|
1595
|
+
cursor: default;
|
|
1596
|
+
opacity: 0.45;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
.rendered-markdown .studio-html-artifact-zoom-reset {
|
|
1600
|
+
min-width: 42px;
|
|
1601
|
+
color: var(--studio-info-text, var(--text));
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
.rendered-markdown .studio-html-artifact-frame {
|
|
1605
|
+
display: block;
|
|
1606
|
+
flex: 1 1 auto;
|
|
1607
|
+
width: 100%;
|
|
1608
|
+
min-height: 520px;
|
|
1609
|
+
border: 0;
|
|
1610
|
+
background: #fff;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
.rendered-markdown .studio-html-artifact-shell.is-height-capped .studio-html-artifact-toolbar {
|
|
1614
|
+
border-bottom-color: var(--control-border);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
.rendered-markdown .studio-html-artifact-frame.is-height-capped {
|
|
1618
|
+
overflow: auto;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1513
1621
|
.rendered-markdown .studio-subfigure-group {
|
|
1514
1622
|
margin: 1.25em auto;
|
|
1515
1623
|
}
|
|
@@ -3013,7 +3121,7 @@
|
|
|
3013
3121
|
|
|
3014
3122
|
body.studio-ui-refresh .studio-refresh-chip::after {
|
|
3015
3123
|
content: "⌄";
|
|
3016
|
-
color: var(--
|
|
3124
|
+
color: var(--studio-info-text, var(--text));
|
|
3017
3125
|
font-size: 14px;
|
|
3018
3126
|
line-height: 1;
|
|
3019
3127
|
transform: translateY(-1px);
|
package/index.ts
CHANGED
|
@@ -3296,6 +3296,33 @@ function normalizeStudioEditorLanguage(language: string | undefined): string | u
|
|
|
3296
3296
|
return trimmed;
|
|
3297
3297
|
}
|
|
3298
3298
|
|
|
3299
|
+
function stripLeadingStudioHtmlTrivia(text: string): string {
|
|
3300
|
+
let source = String(text ?? "").replace(/^\uFEFF/, "").trimStart();
|
|
3301
|
+
let previous = "";
|
|
3302
|
+
while (source && source !== previous) {
|
|
3303
|
+
previous = source;
|
|
3304
|
+
source = source.replace(/^<!--[\s\S]*?-->\s*/, "").trimStart();
|
|
3305
|
+
}
|
|
3306
|
+
return source;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function isStudioHtmlMarkup(text: string): boolean {
|
|
3310
|
+
return /<[A-Za-z][A-Za-z0-9:-]*(?:\s[^<>]*)?>/.test(String(text ?? ""));
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
function isLikelyStandaloneStudioHtml(text: string, editorLanguage?: string): boolean {
|
|
3314
|
+
const source = String(text ?? "");
|
|
3315
|
+
if (!source.trim()) return false;
|
|
3316
|
+
if (parseStudioSingleFencedCodeBlock(source)) return false;
|
|
3317
|
+
|
|
3318
|
+
const leading = stripLeadingStudioHtmlTrivia(source);
|
|
3319
|
+
if (/^<!doctype\s+html\b/i.test(leading)) return true;
|
|
3320
|
+
if (/^<html(?:\s|>|$)/i.test(leading)) return true;
|
|
3321
|
+
if (/^<body(?:\s|>|$)/i.test(leading) && /<\/body\s*>/i.test(leading)) return true;
|
|
3322
|
+
|
|
3323
|
+
return normalizeStudioEditorLanguage(editorLanguage) === "html" && isStudioHtmlMarkup(source);
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3299
3326
|
function parseStudioSingleFencedCodeBlock(markdown: string): { info: string; content: string } | null {
|
|
3300
3327
|
const trimmed = markdown.trim();
|
|
3301
3328
|
if (!trimmed) return null;
|
|
@@ -3468,6 +3495,7 @@ function isLikelyRawStudioGitDiff(markdown: string): boolean {
|
|
|
3468
3495
|
function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): string | undefined {
|
|
3469
3496
|
const normalizedEditorLanguage = normalizeStudioEditorLanguage(editorLanguage);
|
|
3470
3497
|
if (normalizedEditorLanguage) return normalizedEditorLanguage;
|
|
3498
|
+
if (isLikelyStandaloneStudioHtml(markdown)) return "html";
|
|
3471
3499
|
|
|
3472
3500
|
const fenced = parseStudioSingleFencedCodeBlock(markdown);
|
|
3473
3501
|
if (fenced) {
|
|
@@ -4966,6 +4994,9 @@ async function renderStudioStandaloneHtmlWithPandoc(
|
|
|
4966
4994
|
options?: StudioHtmlRenderOptions,
|
|
4967
4995
|
): Promise<{ html: Buffer; warning?: string }> {
|
|
4968
4996
|
const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorLanguage);
|
|
4997
|
+
if (!isLatex && isLikelyStandaloneStudioHtml(markdown, effectiveEditorLanguage)) {
|
|
4998
|
+
return { html: Buffer.from(String(markdown ?? ""), "utf-8") };
|
|
4999
|
+
}
|
|
4969
5000
|
const source = !isLatex
|
|
4970
5001
|
&& effectiveEditorLanguage
|
|
4971
5002
|
&& effectiveEditorLanguage !== "markdown"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|