pi-studio 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -4
- package/client/studio-client.js +796 -29
- package/client/studio.css +169 -0
- package/index.ts +1151 -17
- package/package.json +1 -1
package/client/studio-client.js
CHANGED
|
@@ -88,6 +88,10 @@
|
|
|
88
88
|
const loadCritiqueNotesBtn = document.getElementById("loadCritiqueNotesBtn");
|
|
89
89
|
const loadCritiqueFullBtn = document.getElementById("loadCritiqueFullBtn");
|
|
90
90
|
const copyResponseBtn = document.getElementById("copyResponseBtn");
|
|
91
|
+
const exportPreviewControlsEl = document.getElementById("exportPreviewControls");
|
|
92
|
+
const exportPreviewMenuEl = document.getElementById("exportPreviewMenu");
|
|
93
|
+
const exportPreviewPdfBtn = document.getElementById("exportPreviewPdfBtn");
|
|
94
|
+
const exportPreviewHtmlBtn = document.getElementById("exportPreviewHtmlBtn");
|
|
91
95
|
const exportPdfBtn = document.getElementById("exportPdfBtn");
|
|
92
96
|
const historyPrevBtn = document.getElementById("historyPrevBtn");
|
|
93
97
|
const historyNextBtn = document.getElementById("historyNextBtn");
|
|
@@ -201,6 +205,9 @@
|
|
|
201
205
|
let responseHistory = [];
|
|
202
206
|
let responseHistoryIndex = -1;
|
|
203
207
|
let traceState = null;
|
|
208
|
+
let liveTraceState = null;
|
|
209
|
+
const traceSnapshotCache = new Map();
|
|
210
|
+
let traceDisplayContext = { mode: "live", responseId: null, historyIndex: -1, total: 0, summary: null };
|
|
204
211
|
let traceFilter = "all";
|
|
205
212
|
let traceAutoScroll = true;
|
|
206
213
|
let traceRenderRaf = null;
|
|
@@ -215,7 +222,7 @@
|
|
|
215
222
|
let terminalActivityLabel = "";
|
|
216
223
|
let lastSpecificToolLabel = "";
|
|
217
224
|
let uiBusy = false;
|
|
218
|
-
let
|
|
225
|
+
let previewExportInProgress = false;
|
|
219
226
|
let compactInProgress = false;
|
|
220
227
|
let modelLabel = (document.body && document.body.dataset && document.body.dataset.modelLabel) || "none";
|
|
221
228
|
let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
|
|
@@ -409,6 +416,102 @@
|
|
|
409
416
|
renderTraceViewIfActive();
|
|
410
417
|
}
|
|
411
418
|
|
|
419
|
+
function shouldDisplayLiveTrace() {
|
|
420
|
+
return !Array.isArray(responseHistory)
|
|
421
|
+
|| responseHistory.length === 0
|
|
422
|
+
|| responseHistoryIndex < 0
|
|
423
|
+
|| responseHistoryIndex >= responseHistory.length - 1;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function setTraceDisplayContext(nextContext) {
|
|
427
|
+
const fallback = { mode: "live", responseId: null, historyIndex: -1, total: 0, summary: null };
|
|
428
|
+
traceDisplayContext = Object.assign(fallback, nextContext && typeof nextContext === "object" ? nextContext : {});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function ensureLiveTraceState() {
|
|
432
|
+
if (!liveTraceState) liveTraceState = createEmptyTraceState();
|
|
433
|
+
return liveTraceState;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function upsertTraceEntryInState(state, entry) {
|
|
437
|
+
const normalized = normalizeTraceEntry(entry, Array.isArray(state.entries) ? state.entries.length : 0);
|
|
438
|
+
if (!normalized) return null;
|
|
439
|
+
if (!Array.isArray(state.entries)) state.entries = [];
|
|
440
|
+
const index = state.entries.findIndex((candidate) => candidate.id === normalized.id);
|
|
441
|
+
if (index >= 0) {
|
|
442
|
+
state.entries[index] = normalized;
|
|
443
|
+
} else {
|
|
444
|
+
state.entries.push(normalized);
|
|
445
|
+
}
|
|
446
|
+
state.updatedAt = normalized.updatedAt;
|
|
447
|
+
return normalized;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function replaceLiveTraceState(nextState) {
|
|
451
|
+
liveTraceState = normalizeTraceState(nextState);
|
|
452
|
+
if (shouldDisplayLiveTrace()) {
|
|
453
|
+
setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: responseHistoryIndex, total: responseHistory.length, summary: null });
|
|
454
|
+
replaceTraceState(liveTraceState);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function upsertLiveTraceEntry(entry) {
|
|
459
|
+
const normalized = upsertTraceEntryInState(ensureLiveTraceState(), entry);
|
|
460
|
+
if (!normalized) return;
|
|
461
|
+
if (shouldDisplayLiveTrace()) {
|
|
462
|
+
setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: responseHistoryIndex, total: responseHistory.length, summary: null });
|
|
463
|
+
upsertTraceEntry(normalized);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function appendLiveTraceAssistantDelta(entryId, deltaKind, delta, updatedAt) {
|
|
468
|
+
if (typeof delta !== "string" || !delta) return;
|
|
469
|
+
const state = ensureLiveTraceState();
|
|
470
|
+
const targetId = typeof entryId === "string" && entryId.trim() ? entryId.trim() : null;
|
|
471
|
+
let entry = targetId ? state.entries.find((candidate) => candidate.id === targetId) : null;
|
|
472
|
+
if (!entry || entry.type !== "assistant") {
|
|
473
|
+
entry = normalizeTraceEntry({
|
|
474
|
+
id: targetId || ("trace-assistant-live-" + Date.now()),
|
|
475
|
+
type: "assistant",
|
|
476
|
+
startedAt: updatedAt,
|
|
477
|
+
updatedAt,
|
|
478
|
+
thinking: "",
|
|
479
|
+
text: "",
|
|
480
|
+
status: "streaming",
|
|
481
|
+
stopReason: null,
|
|
482
|
+
}, state.entries.length);
|
|
483
|
+
if (!entry) return;
|
|
484
|
+
state.entries.push(entry);
|
|
485
|
+
}
|
|
486
|
+
if (deltaKind === "thinking") {
|
|
487
|
+
entry.thinking += delta;
|
|
488
|
+
} else {
|
|
489
|
+
entry.text += delta;
|
|
490
|
+
}
|
|
491
|
+
entry.status = "streaming";
|
|
492
|
+
entry.updatedAt = parseFiniteNumber(updatedAt) || Date.now();
|
|
493
|
+
state.updatedAt = entry.updatedAt;
|
|
494
|
+
if (shouldDisplayLiveTrace()) {
|
|
495
|
+
setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: responseHistoryIndex, total: responseHistory.length, summary: null });
|
|
496
|
+
appendTraceAssistantDelta(entryId, deltaKind, delta, updatedAt);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function updateLiveTraceStatusFromMessage(message) {
|
|
501
|
+
if (!message || typeof message !== "object") return;
|
|
502
|
+
const state = ensureLiveTraceState();
|
|
503
|
+
state.runId = parseNonEmptyString(message.runId) || state.runId;
|
|
504
|
+
if (Object.prototype.hasOwnProperty.call(message, "requestId")) state.requestId = parseNonEmptyString(message.requestId);
|
|
505
|
+
if (Object.prototype.hasOwnProperty.call(message, "requestKind")) state.requestKind = parseNonEmptyString(message.requestKind);
|
|
506
|
+
if (Object.prototype.hasOwnProperty.call(message, "startedAt")) state.startedAt = parseFiniteNumber(message.startedAt);
|
|
507
|
+
if (Object.prototype.hasOwnProperty.call(message, "updatedAt")) state.updatedAt = parseFiniteNumber(message.updatedAt);
|
|
508
|
+
if (Object.prototype.hasOwnProperty.call(message, "status")) state.status = normalizeTraceStatus(message.status);
|
|
509
|
+
if (shouldDisplayLiveTrace()) {
|
|
510
|
+
setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: responseHistoryIndex, total: responseHistory.length, summary: null });
|
|
511
|
+
updateTraceStatusFromMessage(message);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
412
515
|
function normalizeTraceFilter(filter) {
|
|
413
516
|
return filter === "thinking" || filter === "tools" ? filter : "all";
|
|
414
517
|
}
|
|
@@ -657,6 +760,7 @@
|
|
|
657
760
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
658
761
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
659
762
|
const previewPendingTimers = new WeakMap();
|
|
763
|
+
const htmlArtifactFramesById = new Map();
|
|
660
764
|
let sourcePreviewRenderTimer = null;
|
|
661
765
|
let sourcePreviewRenderNonce = 0;
|
|
662
766
|
let responsePreviewRenderNonce = 0;
|
|
@@ -1034,7 +1138,11 @@
|
|
|
1034
1138
|
}
|
|
1035
1139
|
rightIdentityEl.appendChild(rightTitleGroupEl);
|
|
1036
1140
|
const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
|
|
1037
|
-
if (
|
|
1141
|
+
if (exportPreviewControlsEl) {
|
|
1142
|
+
rightToolsEl.appendChild(exportPreviewControlsEl);
|
|
1143
|
+
} else if (exportPdfBtn) {
|
|
1144
|
+
rightToolsEl.appendChild(exportPdfBtn);
|
|
1145
|
+
}
|
|
1038
1146
|
rightHeaderEl.replaceChildren(rightIdentityEl, rightToolsEl);
|
|
1039
1147
|
}
|
|
1040
1148
|
|
|
@@ -2027,6 +2135,20 @@
|
|
|
2027
2135
|
return kind === "critique" ? "critique" : "annotation";
|
|
2028
2136
|
}
|
|
2029
2137
|
|
|
2138
|
+
function normalizeTraceSummary(summary) {
|
|
2139
|
+
if (!summary || typeof summary !== "object") return null;
|
|
2140
|
+
return {
|
|
2141
|
+
hasTrace: summary.hasTrace === true,
|
|
2142
|
+
entryCount: typeof summary.entryCount === "number" && Number.isFinite(summary.entryCount)
|
|
2143
|
+
? Math.max(0, Math.floor(summary.entryCount))
|
|
2144
|
+
: 0,
|
|
2145
|
+
startedAt: parseFiniteNumber(summary.startedAt),
|
|
2146
|
+
updatedAt: parseFiniteNumber(summary.updatedAt),
|
|
2147
|
+
status: normalizeTraceStatus(summary.status),
|
|
2148
|
+
truncated: summary.truncated === true,
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2030
2152
|
function normalizeHistoryItem(item, fallbackIndex) {
|
|
2031
2153
|
if (!item || typeof item !== "object") return null;
|
|
2032
2154
|
if (typeof item.markdown !== "string") return null;
|
|
@@ -2057,6 +2179,7 @@
|
|
|
2057
2179
|
const promptTriggerText = typeof item.promptTriggerText === "string"
|
|
2058
2180
|
? item.promptTriggerText
|
|
2059
2181
|
: (item.promptTriggerText == null ? null : String(item.promptTriggerText));
|
|
2182
|
+
const traceSummary = normalizeTraceSummary(item.traceSummary);
|
|
2060
2183
|
|
|
2061
2184
|
return {
|
|
2062
2185
|
id,
|
|
@@ -2069,6 +2192,7 @@
|
|
|
2069
2192
|
promptTriggerKind,
|
|
2070
2193
|
promptSteeringCount,
|
|
2071
2194
|
promptTriggerText,
|
|
2195
|
+
traceSummary,
|
|
2072
2196
|
};
|
|
2073
2197
|
}
|
|
2074
2198
|
|
|
@@ -2078,6 +2202,48 @@
|
|
|
2078
2202
|
return responseHistory[responseHistoryIndex] || null;
|
|
2079
2203
|
}
|
|
2080
2204
|
|
|
2205
|
+
function syncTraceForSelectedHistoryItem() {
|
|
2206
|
+
const item = getSelectedHistoryItem();
|
|
2207
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
2208
|
+
const index = responseHistoryIndex;
|
|
2209
|
+
if (!item) {
|
|
2210
|
+
setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: index, total, summary: null });
|
|
2211
|
+
replaceTraceState(liveTraceState || createEmptyTraceState());
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
if (index >= total - 1) {
|
|
2215
|
+
setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: index, total, summary: item.traceSummary || null });
|
|
2216
|
+
replaceTraceState(liveTraceState || createEmptyTraceState());
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const summary = item.traceSummary || null;
|
|
2221
|
+
if (!summary || !summary.hasTrace) {
|
|
2222
|
+
setTraceDisplayContext({ mode: "missing", responseId: item.id, historyIndex: index, total, summary });
|
|
2223
|
+
replaceTraceState(createEmptyTraceState());
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const cached = traceSnapshotCache.get(item.id);
|
|
2228
|
+
if (cached) {
|
|
2229
|
+
setTraceDisplayContext({ mode: "history", responseId: item.id, historyIndex: index, total, summary });
|
|
2230
|
+
replaceTraceState(cached);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
setTraceDisplayContext({ mode: "loading", responseId: item.id, historyIndex: index, total, summary });
|
|
2235
|
+
replaceTraceState({
|
|
2236
|
+
runId: null,
|
|
2237
|
+
requestId: null,
|
|
2238
|
+
requestKind: null,
|
|
2239
|
+
status: "idle",
|
|
2240
|
+
startedAt: summary.startedAt || null,
|
|
2241
|
+
updatedAt: summary.updatedAt || null,
|
|
2242
|
+
entries: [],
|
|
2243
|
+
});
|
|
2244
|
+
sendMessage({ type: "get_trace_snapshot", responseHistoryId: item.id });
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2081
2247
|
function clearActiveResponseView() {
|
|
2082
2248
|
pendingResponseScrollReset = false;
|
|
2083
2249
|
latestResponseMarkdown = "";
|
|
@@ -2152,6 +2318,7 @@
|
|
|
2152
2318
|
const nextId = nextItem && typeof nextItem.id === "string" ? nextItem.id : null;
|
|
2153
2319
|
const applied = applySelectedHistoryItem({ resetScroll: previousId !== nextId });
|
|
2154
2320
|
updateHistoryControls();
|
|
2321
|
+
syncTraceForSelectedHistoryItem();
|
|
2155
2322
|
|
|
2156
2323
|
if (applied && !(options && options.silent)) {
|
|
2157
2324
|
const item = getSelectedHistoryItem();
|
|
@@ -2202,20 +2369,43 @@
|
|
|
2202
2369
|
return selectHistoryIndex(targetIndex, { silent: Boolean(options && options.silent) });
|
|
2203
2370
|
}
|
|
2204
2371
|
|
|
2372
|
+
function getTraceHistoryContextLabel() {
|
|
2373
|
+
const context = traceDisplayContext || {};
|
|
2374
|
+
const total = typeof context.total === "number" && Number.isFinite(context.total) ? context.total : responseHistory.length;
|
|
2375
|
+
const index = typeof context.historyIndex === "number" && Number.isFinite(context.historyIndex) ? context.historyIndex : responseHistoryIndex;
|
|
2376
|
+
if (context.mode === "history" || context.mode === "missing" || context.mode === "loading") {
|
|
2377
|
+
return total > 0 && index >= 0 ? ("response " + (index + 1) + "/" + total) : "selected response";
|
|
2378
|
+
}
|
|
2379
|
+
return "live";
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2205
2382
|
function updateReferenceBadge() {
|
|
2206
2383
|
if (!referenceBadgeEl) return;
|
|
2207
2384
|
|
|
2208
2385
|
if (rightView === "trace") {
|
|
2209
2386
|
const state = traceState || createEmptyTraceState();
|
|
2387
|
+
const context = traceDisplayContext || {};
|
|
2210
2388
|
const entryCount = getTraceEntriesForFilter(traceFilter).length;
|
|
2211
2389
|
const time = formatReferenceTime(state.startedAt || state.updatedAt);
|
|
2390
|
+
if (context.mode === "loading") {
|
|
2391
|
+
referenceBadgeEl.textContent = "Working: loading " + getTraceHistoryContextLabel();
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (context.mode === "missing") {
|
|
2395
|
+
referenceBadgeEl.textContent = "Working: no saved working for " + getTraceHistoryContextLabel();
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2212
2398
|
if (state.status === "idle") {
|
|
2213
2399
|
referenceBadgeEl.textContent = "Working: no active run yet";
|
|
2214
2400
|
return;
|
|
2215
2401
|
}
|
|
2216
|
-
const statusLabel =
|
|
2402
|
+
const statusLabel = context.mode === "history"
|
|
2403
|
+
? "saved"
|
|
2404
|
+
: (state.status === "running" ? "live" : "complete");
|
|
2217
2405
|
referenceBadgeEl.textContent = "Working: " + statusLabel
|
|
2406
|
+
+ (context.mode === "history" ? (" · " + getTraceHistoryContextLabel()) : "")
|
|
2218
2407
|
+ (entryCount ? (" · " + entryCount + " entr" + (entryCount === 1 ? "y" : "ies")) : "")
|
|
2408
|
+
+ (context.summary && context.summary.truncated ? " · truncated" : "")
|
|
2219
2409
|
+ (time ? (" · " + time) : "");
|
|
2220
2410
|
return;
|
|
2221
2411
|
}
|
|
@@ -2305,6 +2495,15 @@
|
|
|
2305
2495
|
return prepared;
|
|
2306
2496
|
}
|
|
2307
2497
|
|
|
2498
|
+
function prepareEditorTextForHtmlExport(text) {
|
|
2499
|
+
const prepared = prepareEditorTextForPreview(text);
|
|
2500
|
+
const lang = normalizeFenceLanguage(editorLanguage || "");
|
|
2501
|
+
if (lang && lang !== "markdown" && lang !== "latex") {
|
|
2502
|
+
return wrapAsFencedCodeBlock(prepared, lang);
|
|
2503
|
+
}
|
|
2504
|
+
return prepared;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2308
2507
|
function updateSyncBadge(normalizedEditorText) {
|
|
2309
2508
|
if (!syncBadgeEl) return;
|
|
2310
2509
|
|
|
@@ -2352,6 +2551,283 @@
|
|
|
2352
2551
|
return "<div class='preview-error'>" + escapeHtml(String(message || "Preview rendering failed.")) + "</div>" + buildPlainMarkdownHtml(markdown, options);
|
|
2353
2552
|
}
|
|
2354
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
|
+
|
|
2355
2831
|
function stripMatchingQuotes(value) {
|
|
2356
2832
|
const text = String(value || "").trim();
|
|
2357
2833
|
if (text.length >= 2) {
|
|
@@ -3387,7 +3863,7 @@
|
|
|
3387
3863
|
}
|
|
3388
3864
|
|
|
3389
3865
|
async function exportRightPanePdf() {
|
|
3390
|
-
if (uiBusy ||
|
|
3866
|
+
if (uiBusy || previewExportInProgress) {
|
|
3391
3867
|
setStatus("Studio is busy.", "warning");
|
|
3392
3868
|
return;
|
|
3393
3869
|
}
|
|
@@ -3404,6 +3880,12 @@
|
|
|
3404
3880
|
return;
|
|
3405
3881
|
}
|
|
3406
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
|
+
|
|
3407
3889
|
const markdown = rightView === "editor-preview"
|
|
3408
3890
|
? prepareEditorTextForPdfExport(sourceTextEl.value)
|
|
3409
3891
|
: prepareEditorTextForPreview(latestResponseMarkdown);
|
|
@@ -3427,7 +3909,7 @@
|
|
|
3427
3909
|
filenameHint = stem + "-preview.pdf";
|
|
3428
3910
|
}
|
|
3429
3911
|
|
|
3430
|
-
|
|
3912
|
+
previewExportInProgress = true;
|
|
3431
3913
|
updateResultActionButtons();
|
|
3432
3914
|
setStatus("Exporting PDF…", "warning");
|
|
3433
3915
|
|
|
@@ -3540,11 +4022,202 @@
|
|
|
3540
4022
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
3541
4023
|
setStatus("PDF export failed: " + detail, "error");
|
|
3542
4024
|
} finally {
|
|
3543
|
-
|
|
4025
|
+
previewExportInProgress = false;
|
|
3544
4026
|
updateResultActionButtons();
|
|
3545
4027
|
}
|
|
3546
4028
|
}
|
|
3547
4029
|
|
|
4030
|
+
async function exportRightPaneHtml() {
|
|
4031
|
+
if (uiBusy || previewExportInProgress) {
|
|
4032
|
+
setStatus("Studio is busy.", "warning");
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
const token = getToken();
|
|
4037
|
+
if (!token) {
|
|
4038
|
+
setStatus("Missing Studio token in URL. Re-run /studio.", "error");
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4043
|
+
if (!rightPaneShowsPreview) {
|
|
4044
|
+
setStatus("Switch right pane to Response (Preview) or Editor (Preview) to export HTML.", "warning");
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
const htmlArtifactSource = getRightPaneHtmlArtifactSource();
|
|
4049
|
+
const markdown = htmlArtifactSource || (rightView === "editor-preview"
|
|
4050
|
+
? prepareEditorTextForHtmlExport(sourceTextEl.value)
|
|
4051
|
+
: prepareEditorTextForPreview(latestResponseMarkdown));
|
|
4052
|
+
if (!markdown || !markdown.trim()) {
|
|
4053
|
+
setStatus("Nothing to export yet.", "warning");
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
const effectivePath = getEffectiveSavePath();
|
|
4058
|
+
const sourcePath = effectivePath || sourceState.path || "";
|
|
4059
|
+
const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
|
|
4060
|
+
const isEditorPreview = rightView === "editor-preview";
|
|
4061
|
+
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
|
|
4062
|
+
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
4063
|
+
? editorHtmlLanguage === "latex"
|
|
4064
|
+
: /\\documentclass\b|\\begin\{document\}/.test(markdown));
|
|
4065
|
+
let filenameHint = isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html";
|
|
4066
|
+
let titleHint = isEditorPreview ? "Studio editor preview" : "Studio response preview";
|
|
4067
|
+
if (sourcePath) {
|
|
4068
|
+
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
4069
|
+
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
4070
|
+
filenameHint = stem + "-preview.html";
|
|
4071
|
+
titleHint = stem + " preview";
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
previewExportInProgress = true;
|
|
4075
|
+
updateResultActionButtons();
|
|
4076
|
+
setStatus("Exporting HTML…", "warning");
|
|
4077
|
+
|
|
4078
|
+
try {
|
|
4079
|
+
const response = await fetch("/export-html?token=" + encodeURIComponent(token), {
|
|
4080
|
+
method: "POST",
|
|
4081
|
+
headers: {
|
|
4082
|
+
"Content-Type": "application/json",
|
|
4083
|
+
},
|
|
4084
|
+
body: JSON.stringify({
|
|
4085
|
+
markdown: String(markdown || ""),
|
|
4086
|
+
sourcePath: sourcePath,
|
|
4087
|
+
resourceDir: resourceDir,
|
|
4088
|
+
isLatex: isLatex,
|
|
4089
|
+
editorHtmlLanguage: editorHtmlLanguage,
|
|
4090
|
+
filenameHint: filenameHint,
|
|
4091
|
+
title: titleHint,
|
|
4092
|
+
}),
|
|
4093
|
+
});
|
|
4094
|
+
|
|
4095
|
+
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
4096
|
+
if (!response.ok) {
|
|
4097
|
+
let message = "HTML export failed with HTTP " + response.status + ".";
|
|
4098
|
+
if (contentType.includes("application/json")) {
|
|
4099
|
+
const payload = await response.json().catch(() => null);
|
|
4100
|
+
if (payload && typeof payload.error === "string") {
|
|
4101
|
+
message = payload.error;
|
|
4102
|
+
}
|
|
4103
|
+
} else {
|
|
4104
|
+
const text = await response.text().catch(() => "");
|
|
4105
|
+
if (text && text.trim()) {
|
|
4106
|
+
message = text.trim();
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
throw new Error(message);
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
if (contentType.includes("application/json")) {
|
|
4113
|
+
const payload = await response.json().catch(() => null);
|
|
4114
|
+
if (!payload || typeof payload.downloadUrl !== "string") {
|
|
4115
|
+
throw new Error("HTML export prepared successfully, but Studio did not receive a download URL.");
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
|
|
4119
|
+
const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
|
|
4120
|
+
const openedExternal = payload.openedExternal === true;
|
|
4121
|
+
let downloadName = typeof payload.filename === "string" && payload.filename.trim()
|
|
4122
|
+
? payload.filename.trim()
|
|
4123
|
+
: (filenameHint || "studio-preview.html");
|
|
4124
|
+
if (!/\.html?$/i.test(downloadName)) {
|
|
4125
|
+
downloadName += ".html";
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
if (openedExternal) {
|
|
4129
|
+
if (exportWarning) {
|
|
4130
|
+
setStatus("Opened HTML in default browser with warning: " + exportWarning, "warning");
|
|
4131
|
+
} else {
|
|
4132
|
+
setStatus("Opened HTML in default browser: " + downloadName, "success");
|
|
4133
|
+
}
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
const link = document.createElement("a");
|
|
4138
|
+
link.href = payload.downloadUrl;
|
|
4139
|
+
link.download = downloadName;
|
|
4140
|
+
link.rel = "noopener";
|
|
4141
|
+
document.body.appendChild(link);
|
|
4142
|
+
link.click();
|
|
4143
|
+
link.remove();
|
|
4144
|
+
|
|
4145
|
+
if (openError) {
|
|
4146
|
+
if (exportWarning) {
|
|
4147
|
+
setStatus("Opened browser fallback because external viewer failed (" + openError + "). Warning: " + exportWarning, "warning");
|
|
4148
|
+
} else {
|
|
4149
|
+
setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
|
|
4150
|
+
}
|
|
4151
|
+
} else if (exportWarning) {
|
|
4152
|
+
setStatus("Exported HTML with warning: " + exportWarning, "warning");
|
|
4153
|
+
} else {
|
|
4154
|
+
setStatus("Exported HTML: " + downloadName, "success");
|
|
4155
|
+
}
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
|
|
4160
|
+
const blob = await response.blob();
|
|
4161
|
+
const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
|
|
4162
|
+
let downloadName = headerFilename || filenameHint || "studio-preview.html";
|
|
4163
|
+
if (!/\.html?$/i.test(downloadName)) {
|
|
4164
|
+
downloadName += ".html";
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
4168
|
+
const link = document.createElement("a");
|
|
4169
|
+
link.href = blobUrl;
|
|
4170
|
+
link.download = downloadName;
|
|
4171
|
+
link.rel = "noopener";
|
|
4172
|
+
document.body.appendChild(link);
|
|
4173
|
+
link.click();
|
|
4174
|
+
link.remove();
|
|
4175
|
+
window.setTimeout(() => {
|
|
4176
|
+
URL.revokeObjectURL(blobUrl);
|
|
4177
|
+
}, 1800);
|
|
4178
|
+
|
|
4179
|
+
if (exportWarning) {
|
|
4180
|
+
setStatus("Exported HTML with warning: " + exportWarning, "warning");
|
|
4181
|
+
} else {
|
|
4182
|
+
setStatus("Exported HTML: " + downloadName, "success");
|
|
4183
|
+
}
|
|
4184
|
+
} catch (error) {
|
|
4185
|
+
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
4186
|
+
setStatus("HTML export failed: " + detail, "error");
|
|
4187
|
+
} finally {
|
|
4188
|
+
previewExportInProgress = false;
|
|
4189
|
+
updateResultActionButtons();
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
function closeExportPreviewMenu() {
|
|
4194
|
+
if (!exportPreviewMenuEl) return;
|
|
4195
|
+
exportPreviewMenuEl.hidden = true;
|
|
4196
|
+
if (exportPdfBtn) {
|
|
4197
|
+
exportPdfBtn.classList.remove("is-open");
|
|
4198
|
+
exportPdfBtn.setAttribute("aria-expanded", "false");
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
function toggleExportPreviewMenu() {
|
|
4203
|
+
if (!exportPreviewMenuEl || !exportPdfBtn || exportPdfBtn.disabled) return;
|
|
4204
|
+
if (typeof closeStudioUiRefreshMenus === "function") {
|
|
4205
|
+
closeStudioUiRefreshMenus();
|
|
4206
|
+
}
|
|
4207
|
+
const willOpen = exportPreviewMenuEl.hidden;
|
|
4208
|
+
exportPreviewMenuEl.hidden = !willOpen;
|
|
4209
|
+
exportPdfBtn.classList.toggle("is-open", willOpen);
|
|
4210
|
+
exportPdfBtn.setAttribute("aria-expanded", willOpen ? "true" : "false");
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
function exportRightPaneFormat(format) {
|
|
4214
|
+
closeExportPreviewMenu();
|
|
4215
|
+
if (format === "html") {
|
|
4216
|
+
return exportRightPaneHtml();
|
|
4217
|
+
}
|
|
4218
|
+
return exportRightPanePdf();
|
|
4219
|
+
}
|
|
4220
|
+
|
|
3548
4221
|
function normalizeCopyableBlockText(text) {
|
|
3549
4222
|
return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
|
|
3550
4223
|
}
|
|
@@ -3726,6 +4399,10 @@
|
|
|
3726
4399
|
function renderSourcePreviewNow() {
|
|
3727
4400
|
if (editorView !== "preview") return;
|
|
3728
4401
|
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
4402
|
+
if (isHtmlArtifactPreviewText(text, editorLanguage)) {
|
|
4403
|
+
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML artifact preview" });
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
3729
4406
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
3730
4407
|
renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
|
|
3731
4408
|
return;
|
|
@@ -3857,17 +4534,27 @@
|
|
|
3857
4534
|
const visibleWorking = buildVisibleWorkingText(filter);
|
|
3858
4535
|
const hasVisibleContent = Boolean(visibleWorking.trim());
|
|
3859
4536
|
const started = formatReferenceTime(state.startedAt || state.updatedAt);
|
|
3860
|
-
const
|
|
3861
|
-
|
|
3862
|
-
|
|
4537
|
+
const context = traceDisplayContext || {};
|
|
4538
|
+
const statusLabel = context.mode === "history"
|
|
4539
|
+
? "Saved"
|
|
4540
|
+
: (context.mode === "loading"
|
|
4541
|
+
? "Loading"
|
|
4542
|
+
: (context.mode === "missing"
|
|
4543
|
+
? "Not saved"
|
|
4544
|
+
: (state.status === "running" ? "Live" : (state.status === "complete" ? "Complete" : "Idle"))));
|
|
3863
4545
|
const filterMeta = filter === "thinking"
|
|
3864
4546
|
? "Thinking only"
|
|
3865
4547
|
: (filter === "tools" ? "Tools only" : null);
|
|
4548
|
+
const historyMeta = (context.mode === "history" || context.mode === "missing" || context.mode === "loading")
|
|
4549
|
+
? getTraceHistoryContextLabel()
|
|
4550
|
+
: null;
|
|
3866
4551
|
const toolbar = "<div class='trace-toolbar'>"
|
|
3867
4552
|
+ "<div class='trace-summary'>"
|
|
3868
4553
|
+ "<span class='trace-summary-badge'>Working</span>"
|
|
3869
4554
|
+ "<span class='trace-summary-status trace-status-" + escapeHtml(String(state.status || "idle")) + "'>" + escapeHtml(statusLabel) + "</span>"
|
|
4555
|
+
+ (historyMeta ? ("<span class='trace-summary-meta'>" + escapeHtml(historyMeta) + "</span>") : "")
|
|
3870
4556
|
+ (started ? ("<span class='trace-summary-meta'>Started " + escapeHtml(started) + "</span>") : "")
|
|
4557
|
+
+ (context.summary && context.summary.truncated ? "<span class='trace-summary-meta'>Truncated</span>" : "")
|
|
3871
4558
|
+ (filterMeta ? ("<span class='trace-summary-meta'>" + escapeHtml(filterMeta) + "</span>") : "")
|
|
3872
4559
|
+ "</div>"
|
|
3873
4560
|
+ "<div class='trace-controls'>"
|
|
@@ -3882,13 +4569,17 @@
|
|
|
3882
4569
|
+ "</div>";
|
|
3883
4570
|
|
|
3884
4571
|
if (!entries.length) {
|
|
3885
|
-
const emptyMessage =
|
|
3886
|
-
? "
|
|
3887
|
-
: (
|
|
3888
|
-
? "No
|
|
3889
|
-
: (
|
|
3890
|
-
? "
|
|
3891
|
-
:
|
|
4572
|
+
const emptyMessage = context.mode === "loading"
|
|
4573
|
+
? "Loading saved working for this response…"
|
|
4574
|
+
: (context.mode === "missing"
|
|
4575
|
+
? "No working was saved for this response."
|
|
4576
|
+
: (filter === "thinking"
|
|
4577
|
+
? "No thinking steps in this working view yet."
|
|
4578
|
+
: (filter === "tools"
|
|
4579
|
+
? "No tool steps in this working view yet."
|
|
4580
|
+
: (state.status === "running"
|
|
4581
|
+
? "Waiting for the first model or tool update…"
|
|
4582
|
+
: "No live working view yet. Start a run or critique to watch working details here."))));
|
|
3892
4583
|
return "<div class='trace-panel'>" + toolbar + "<div class='trace-empty'>" + escapeHtml(emptyMessage) + "</div></div>";
|
|
3893
4584
|
}
|
|
3894
4585
|
|
|
@@ -3980,6 +4671,10 @@
|
|
|
3980
4671
|
scheduleResponsePaneRepaintNudge();
|
|
3981
4672
|
return;
|
|
3982
4673
|
}
|
|
4674
|
+
if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
|
|
4675
|
+
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML artifact preview" });
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
3983
4678
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
3984
4679
|
renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
|
|
3985
4680
|
return;
|
|
@@ -4000,6 +4695,10 @@
|
|
|
4000
4695
|
}
|
|
4001
4696
|
|
|
4002
4697
|
if (rightView === "preview") {
|
|
4698
|
+
if (isHtmlArtifactPreviewText(markdown, "")) {
|
|
4699
|
+
renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML artifact preview" });
|
|
4700
|
+
return;
|
|
4701
|
+
}
|
|
4003
4702
|
const nonce = ++responsePreviewRenderNonce;
|
|
4004
4703
|
beginPreviewRender(critiqueViewEl);
|
|
4005
4704
|
void applyRenderedMarkdown(critiqueViewEl, markdown, "response", nonce);
|
|
@@ -4065,19 +4764,44 @@
|
|
|
4065
4764
|
|
|
4066
4765
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4067
4766
|
const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
|
|
4068
|
-
const
|
|
4767
|
+
const canExportPreview = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
|
|
4768
|
+
const htmlArtifactExportSource = canExportPreview ? getRightPaneHtmlArtifactSource() : "";
|
|
4769
|
+
const isHtmlArtifactPreview = Boolean(htmlArtifactExportSource);
|
|
4069
4770
|
if (exportPdfBtn) {
|
|
4070
|
-
exportPdfBtn.disabled = uiBusy ||
|
|
4771
|
+
exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4772
|
+
exportPdfBtn.textContent = previewExportInProgress ? "Exporting…" : "Export right preview";
|
|
4071
4773
|
if (rightView === "trace") {
|
|
4072
|
-
exportPdfBtn.title = "Working view does not support
|
|
4774
|
+
exportPdfBtn.title = "Working view does not support preview export.";
|
|
4073
4775
|
} else if (rightView === "markdown") {
|
|
4074
|
-
exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export
|
|
4075
|
-
} else if (!
|
|
4776
|
+
exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export.";
|
|
4777
|
+
} else if (!canExportPreview) {
|
|
4076
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.";
|
|
4077
4781
|
} else {
|
|
4078
|
-
exportPdfBtn.title = "
|
|
4782
|
+
exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
|
|
4079
4783
|
}
|
|
4080
4784
|
}
|
|
4785
|
+
if (exportPreviewPdfBtn) {
|
|
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.";
|
|
4790
|
+
}
|
|
4791
|
+
if (exportPreviewHtmlBtn) {
|
|
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.";
|
|
4796
|
+
}
|
|
4797
|
+
if (exportPreviewControlsEl) {
|
|
4798
|
+
exportPreviewControlsEl.title = canExportPreview
|
|
4799
|
+
? (isHtmlArtifactPreview ? "Export this HTML artifact." : "Choose a format and export the current right-pane preview.")
|
|
4800
|
+
: "Switch right pane to a non-empty preview before exporting.";
|
|
4801
|
+
}
|
|
4802
|
+
if (!canExportPreview || previewExportInProgress) {
|
|
4803
|
+
closeExportPreviewMenu();
|
|
4804
|
+
}
|
|
4081
4805
|
|
|
4082
4806
|
pullLatestBtn.disabled = uiBusy || followLatest;
|
|
4083
4807
|
pullLatestBtn.textContent = queuedLatestResponse ? "Fetch latest response *" : "Fetch latest response";
|
|
@@ -10014,7 +10738,7 @@
|
|
|
10014
10738
|
}
|
|
10015
10739
|
|
|
10016
10740
|
if (message.traceState) {
|
|
10017
|
-
|
|
10741
|
+
replaceLiveTraceState(message.traceState);
|
|
10018
10742
|
}
|
|
10019
10743
|
|
|
10020
10744
|
let appliedHistory = false;
|
|
@@ -10064,22 +10788,41 @@
|
|
|
10064
10788
|
}
|
|
10065
10789
|
|
|
10066
10790
|
if (message.type === "trace_reset") {
|
|
10067
|
-
|
|
10791
|
+
replaceLiveTraceState(message.trace);
|
|
10068
10792
|
return;
|
|
10069
10793
|
}
|
|
10070
10794
|
|
|
10071
10795
|
if (message.type === "trace_status") {
|
|
10072
|
-
|
|
10796
|
+
updateLiveTraceStatusFromMessage(message);
|
|
10073
10797
|
return;
|
|
10074
10798
|
}
|
|
10075
10799
|
|
|
10076
10800
|
if (message.type === "trace_entry_upsert") {
|
|
10077
|
-
|
|
10801
|
+
upsertLiveTraceEntry(message.entry);
|
|
10078
10802
|
return;
|
|
10079
10803
|
}
|
|
10080
10804
|
|
|
10081
10805
|
if (message.type === "trace_assistant_delta") {
|
|
10082
|
-
|
|
10806
|
+
appendLiveTraceAssistantDelta(message.entryId, message.deltaKind, message.delta, message.updatedAt);
|
|
10807
|
+
return;
|
|
10808
|
+
}
|
|
10809
|
+
|
|
10810
|
+
if (message.type === "trace_snapshot") {
|
|
10811
|
+
const responseId = typeof message.responseHistoryId === "string" ? message.responseHistoryId.trim() : "";
|
|
10812
|
+
if (responseId && message.traceState) {
|
|
10813
|
+
const normalizedSnapshot = normalizeTraceState(message.traceState);
|
|
10814
|
+
traceSnapshotCache.set(responseId, normalizedSnapshot);
|
|
10815
|
+
if (traceDisplayContext && traceDisplayContext.responseId === responseId) {
|
|
10816
|
+
setTraceDisplayContext({
|
|
10817
|
+
mode: "history",
|
|
10818
|
+
responseId,
|
|
10819
|
+
historyIndex: responseHistoryIndex,
|
|
10820
|
+
total: responseHistory.length,
|
|
10821
|
+
summary: normalizeTraceSummary(message.summary) || (getSelectedHistoryItem() ? getSelectedHistoryItem().traceSummary : null),
|
|
10822
|
+
});
|
|
10823
|
+
replaceTraceState(normalizedSnapshot);
|
|
10824
|
+
}
|
|
10825
|
+
}
|
|
10083
10826
|
return;
|
|
10084
10827
|
}
|
|
10085
10828
|
|
|
@@ -11189,11 +11932,35 @@
|
|
|
11189
11932
|
});
|
|
11190
11933
|
|
|
11191
11934
|
if (exportPdfBtn) {
|
|
11192
|
-
exportPdfBtn.addEventListener("click", () => {
|
|
11193
|
-
|
|
11935
|
+
exportPdfBtn.addEventListener("click", (event) => {
|
|
11936
|
+
event.preventDefault();
|
|
11937
|
+
event.stopPropagation();
|
|
11938
|
+
toggleExportPreviewMenu();
|
|
11939
|
+
});
|
|
11940
|
+
}
|
|
11941
|
+
|
|
11942
|
+
if (exportPreviewMenuEl) {
|
|
11943
|
+
exportPreviewMenuEl.addEventListener("click", (event) => {
|
|
11944
|
+
const target = event.target;
|
|
11945
|
+
const actionBtn = target instanceof Element ? target.closest("[data-export-preview-format]") : null;
|
|
11946
|
+
if (!actionBtn) return;
|
|
11947
|
+
event.preventDefault();
|
|
11948
|
+
event.stopPropagation();
|
|
11949
|
+
if (actionBtn.disabled) return;
|
|
11950
|
+
const format = String(actionBtn.getAttribute("data-export-preview-format") || "pdf").toLowerCase();
|
|
11951
|
+
void exportRightPaneFormat(format === "html" ? "html" : "pdf");
|
|
11194
11952
|
});
|
|
11195
11953
|
}
|
|
11196
11954
|
|
|
11955
|
+
document.addEventListener("click", (event) => {
|
|
11956
|
+
const target = event.target;
|
|
11957
|
+
if (target instanceof Element && target.closest("#exportPreviewControls")) return;
|
|
11958
|
+
closeExportPreviewMenu();
|
|
11959
|
+
});
|
|
11960
|
+
document.addEventListener("keydown", (event) => {
|
|
11961
|
+
if (event.key === "Escape") closeExportPreviewMenu();
|
|
11962
|
+
});
|
|
11963
|
+
|
|
11197
11964
|
saveAsBtn.addEventListener("click", () => {
|
|
11198
11965
|
const content = sourceTextEl.value;
|
|
11199
11966
|
if (!content.trim()) {
|