@widgetic/creator 0.3.64 → 0.3.65
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/dist/CreatorApp.svelte +233 -40
- package/dist/components/VersionDropdownRow.svelte +5 -3
- package/dist/components/WidgetShapeLivePreviews.svelte +88 -36
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +4 -0
- package/dist/utils/freezeWatchdog.d.ts +22 -0
- package/dist/utils/freezeWatchdog.js +76 -0
- package/dist/utils/pinScrollToBottom.d.ts +12 -0
- package/dist/utils/pinScrollToBottom.js +43 -0
- package/package.json +1 -1
package/dist/CreatorApp.svelte
CHANGED
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
import { uploadChatFileToWidgeticApi } from './utils/widgeticChatUpload.js';
|
|
71
71
|
import { persistWidgetThumbnail } from './utils/persistWidgetThumbnail.js';
|
|
72
72
|
import { nativeScrollbarPadding } from './utils/nativeScrollbarPadding.js';
|
|
73
|
+
import { pinScrollToBottom } from './utils/pinScrollToBottom.js';
|
|
74
|
+
import { installFreezeWatchdog } from './utils/freezeWatchdog.js';
|
|
73
75
|
import { streamOperationUpdates } from './utils/operationStream.js';
|
|
74
76
|
import { generateEmbedCode, EMBED_FORMATS, EMBED_FORMAT_LABELS, type EmbedFormat } from './utils/embedCode.js';
|
|
75
77
|
|
|
@@ -518,8 +520,40 @@
|
|
|
518
520
|
const canvasModule = await canvasModulePromise;
|
|
519
521
|
WidgeticCanvas = canvasModule.Canvas;
|
|
520
522
|
}
|
|
523
|
+
installLongTaskFreezeReporter();
|
|
524
|
+
installFreezeWatchdog();
|
|
521
525
|
});
|
|
522
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Freeze attribution: while a code generation is tracked, any main-thread
|
|
529
|
+
* block ≥ LONG_TASK_FREEZE_MS is reported with its most likely culprit (the
|
|
530
|
+
* last status line + open panels). This gives a concrete answer to "why does
|
|
531
|
+
* the site freeze during generation" instead of a silent stuck progress bar.
|
|
532
|
+
*/
|
|
533
|
+
const LONG_TASK_FREEZE_MS = 250;
|
|
534
|
+
let freezeObserver: PerformanceObserver | null = null;
|
|
535
|
+
function installLongTaskFreezeReporter(): void {
|
|
536
|
+
if (typeof window === 'undefined' || !('PerformanceObserver' in window)) return;
|
|
537
|
+
if (freezeObserver) return;
|
|
538
|
+
try {
|
|
539
|
+
freezeObserver = new PerformanceObserver((list) => {
|
|
540
|
+
if (!Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id])) return;
|
|
541
|
+
for (const entry of list.getEntries()) {
|
|
542
|
+
if (entry.duration < LONG_TASK_FREEZE_MS) continue;
|
|
543
|
+
console.warn(
|
|
544
|
+
`[FreezeReport] Main thread blocked ${Math.round(entry.duration)}ms during generation.`
|
|
545
|
+
+ ` Status: "${generateCodeStatus ?? '—'}".`
|
|
546
|
+
+ ` Open panels: ${openWidgetPanelIds.length}.`
|
|
547
|
+
+ ` Start: ${Math.round(entry.startTime)}ms after nav.`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
freezeObserver.observe({ entryTypes: ['longtask'] });
|
|
552
|
+
} catch (err) {
|
|
553
|
+
console.warn('[FreezeReport] longtask observer unavailable', err);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
523
557
|
// Trigger WidgetDetails load as soon as a panel is opened or requested
|
|
524
558
|
$: if (openWidgetPanelIds.length > 0 && !WidgetDetails) {
|
|
525
559
|
void loadWidgetDetailsModule();
|
|
@@ -797,6 +831,9 @@
|
|
|
797
831
|
let codeGenerationPrompt = DEFAULT_PROMPT;
|
|
798
832
|
let isGeneratingCode = false;
|
|
799
833
|
let generatingWidgetIds: Record<string, boolean> = {};
|
|
834
|
+
function isAnyWidgetGenerating(): boolean {
|
|
835
|
+
return isGeneratingCode || Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id]);
|
|
836
|
+
}
|
|
800
837
|
const widgetAbortControllers = new Map<string, AbortController>();
|
|
801
838
|
let currentGenerationOperationId: string | null = null;
|
|
802
839
|
let generateCodeStatus: string | null = null;
|
|
@@ -1578,6 +1615,9 @@
|
|
|
1578
1615
|
iframe: HTMLIFrameElement,
|
|
1579
1616
|
timeoutMs = 6000,
|
|
1580
1617
|
): Promise<{ data: string | null; error: string | null }> {
|
|
1618
|
+
if (isAnyWidgetGenerating()) {
|
|
1619
|
+
return Promise.resolve({ data: null, error: 'generation in progress' });
|
|
1620
|
+
}
|
|
1581
1621
|
return new Promise((resolve) => {
|
|
1582
1622
|
if (!iframe.contentWindow) {
|
|
1583
1623
|
resolve({ data: null, error: 'no-content-window' });
|
|
@@ -1657,19 +1697,20 @@
|
|
|
1657
1697
|
|
|
1658
1698
|
/** Explicit Refresh only: capture the loaded frame without opening a panel or publishing. */
|
|
1659
1699
|
async function handleWidgetShapeRefresh(widgetId, iframe, isCurrentFrame) {
|
|
1700
|
+
if (isAnyWidgetGenerating()) return;
|
|
1660
1701
|
const capturedCanvasId = getEffectiveCanvasId();
|
|
1661
1702
|
const capturedCanvas = canvasRef;
|
|
1662
1703
|
const capturedSource = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
1663
1704
|
// Frame/intent identity owns this capture, not focused-panel HEAD hydration
|
|
1664
1705
|
// or another widget's global generation operation.
|
|
1665
1706
|
const isCurrent = () => isCurrentFrame() && !!getWidgetById(widgetId)
|
|
1666
|
-
&& !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId]
|
|
1707
|
+
&& !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId] && !isAnyWidgetGenerating()
|
|
1667
1708
|
&& capturedCanvasId === getEffectiveCanvasId() && capturedCanvas === canvasRef
|
|
1668
1709
|
&& capturedSource === (shapePreviewSourceByWidgetId[widgetId] || 'live')
|
|
1669
1710
|
&& resolveLiveWidgetShapeIframe(widgetId) === iframe;
|
|
1670
1711
|
if (!isCurrent()) return;
|
|
1671
1712
|
try {
|
|
1672
|
-
const screenshot = await handleChatScreenshot(widgetId, iframe);
|
|
1713
|
+
const screenshot = await handleChatScreenshot(widgetId, iframe, true);
|
|
1673
1714
|
if (!screenshot || !isCurrent()) return;
|
|
1674
1715
|
// Persist before Fabric/autosave side effects can resync the live overlay.
|
|
1675
1716
|
await persistWidgetListThumbnail(widgetId, screenshot, isCurrent);
|
|
@@ -1683,13 +1724,34 @@
|
|
|
1683
1724
|
}
|
|
1684
1725
|
|
|
1685
1726
|
/** Screenshot: capture the live WidgetShape iframe (CDN or Worker), not Fabric wireframe. */
|
|
1686
|
-
async function handleChatScreenshot(widgetId?: string | null, requiredIframe: HTMLIFrameElement | null = null): Promise<string | null> {
|
|
1727
|
+
async function handleChatScreenshot(widgetId?: string | null, requiredIframe: HTMLIFrameElement | null = null, quiet = false): Promise<string | null> {
|
|
1687
1728
|
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1688
1729
|
if (!targetId) {
|
|
1689
1730
|
console.warn('[Chat→Screenshot] No widget id');
|
|
1690
1731
|
return null;
|
|
1691
1732
|
}
|
|
1733
|
+
// Never run background / refresh screenshot captures while code generation is running —
|
|
1734
|
+
// html2canvas inside any same-origin iframe freezes the browser main thread.
|
|
1735
|
+
if ((quiet || requiredIframe != null) && isAnyWidgetGenerating()) {
|
|
1736
|
+
console.log('[Chat→Screenshot] Skip capture — generation in progress');
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
// Same single-flight as handleRefreshScreenshot: both paths run html2canvas
|
|
1740
|
+
// inside preview iframes that share the page main thread. Overlapping
|
|
1741
|
+
// captures during generation were a major freeze source.
|
|
1742
|
+
if (screenshotCaptureInFlight.has(targetId)) {
|
|
1743
|
+
console.log('[Chat→Screenshot] Capture already in flight — skipping', targetId.substring(0, 8));
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1746
|
+
screenshotCaptureInFlight.add(targetId);
|
|
1747
|
+
try {
|
|
1748
|
+
return await performChatScreenshot(targetId, requiredIframe, quiet);
|
|
1749
|
+
} finally {
|
|
1750
|
+
screenshotCaptureInFlight.delete(targetId);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1692
1753
|
|
|
1754
|
+
async function performChatScreenshot(targetId: string, requiredIframe: HTMLIFrameElement | null, quiet = false): Promise<string | null> {
|
|
1693
1755
|
const liveIframe = requiredIframe ?? resolveLiveWidgetShapeIframe(targetId);
|
|
1694
1756
|
if (requiredIframe && (requiredIframe !== resolveLiveWidgetShapeIframe(targetId) || !requiredIframe.isConnected)) return null;
|
|
1695
1757
|
const liveSrc = liveIframe
|
|
@@ -1712,6 +1774,8 @@
|
|
|
1712
1774
|
|
|
1713
1775
|
// Explicit refresh must not persist a different version or a stale Fabric image.
|
|
1714
1776
|
if (requiredIframe) return null;
|
|
1777
|
+
// Quiet thumbnail sync must NEVER construct a temporary hidden iframe and compile worker preview.
|
|
1778
|
+
if (quiet) return null;
|
|
1715
1779
|
|
|
1716
1780
|
// 2) Hidden Worker /preview (same capture script; wait for load)
|
|
1717
1781
|
const fromWorker = await captureViaHiddenWorkerPreview(targetId);
|
|
@@ -1737,7 +1801,6 @@
|
|
|
1737
1801
|
});
|
|
1738
1802
|
return null;
|
|
1739
1803
|
}
|
|
1740
|
-
|
|
1741
1804
|
function normalizeConversationRecords(json: any): any[] {
|
|
1742
1805
|
const raw = json?.data ?? json?.conversations ?? json;
|
|
1743
1806
|
const list = Array.isArray(raw)
|
|
@@ -7619,12 +7682,12 @@
|
|
|
7619
7682
|
canvasShapePreviewSyncTimers.clear();
|
|
7620
7683
|
}
|
|
7621
7684
|
function scheduleCanvasShapePreviewSync(widgetId: string, delayMs = 1800): void {
|
|
7622
|
-
if (!widgetId) return;
|
|
7685
|
+
if (!widgetId || isAnyWidgetGenerating()) return;
|
|
7623
7686
|
const existing = canvasShapePreviewSyncTimers.get(widgetId);
|
|
7624
7687
|
if (existing) clearTimeout(existing);
|
|
7625
7688
|
const timer = setTimeout(() => {
|
|
7626
7689
|
canvasShapePreviewSyncTimers.delete(widgetId);
|
|
7627
|
-
if (generatingWidgetIds[widgetId]) {
|
|
7690
|
+
if (isAnyWidgetGenerating() || generatingWidgetIds[widgetId]) {
|
|
7628
7691
|
// console.log('[CreatorApp] Skip shape preview sync — codegen in progress:', widgetId.substring(0, 8));
|
|
7629
7692
|
return;
|
|
7630
7693
|
}
|
|
@@ -7643,6 +7706,24 @@
|
|
|
7643
7706
|
const quiet = !!options.quiet;
|
|
7644
7707
|
// console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
|
|
7645
7708
|
|
|
7709
|
+
// Never run quiet background screenshot captures while ANY code generation is running —
|
|
7710
|
+
// html2canvas execution inside any preview iframe blocks the single shared main thread.
|
|
7711
|
+
if (quiet && isAnyWidgetGenerating()) {
|
|
7712
|
+
console.log('[handleRefreshScreenshot] Skip quiet capture — codegen in progress', widgetId?.substring(0, 8));
|
|
7713
|
+
return;
|
|
7714
|
+
}
|
|
7715
|
+
|
|
7716
|
+
// Single-flight per widget: overlapping captures stack html2canvas runs in
|
|
7717
|
+
// the preview iframe — same-origin iframes share this page's main thread,
|
|
7718
|
+
// and stacked captures blocked the whole site during generation (no
|
|
7719
|
+
// [FreezeReport] because the long task is attributed to the iframe).
|
|
7720
|
+
if (screenshotCaptureInFlight.has(widgetId)) {
|
|
7721
|
+
console.log('[handleRefreshScreenshot] Capture already in flight — skipping', widgetId?.substring(0, 8));
|
|
7722
|
+
return;
|
|
7723
|
+
}
|
|
7724
|
+
screenshotCaptureInFlight.add(widgetId);
|
|
7725
|
+
|
|
7726
|
+
try {
|
|
7646
7727
|
if (isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
|
|
7647
7728
|
// console.log('[handleRefreshScreenshot] Skip iframe capture — convert/draft panel is open:', widgetId.substring(0, 8));
|
|
7648
7729
|
return;
|
|
@@ -7673,7 +7754,7 @@
|
|
|
7673
7754
|
if (!shapeThumbnailCaptureInFlight.has(widgetId)) {
|
|
7674
7755
|
shapeThumbnailCaptureInFlight.add(widgetId);
|
|
7675
7756
|
try {
|
|
7676
|
-
const freshCapture = await handleChatScreenshot(widgetId);
|
|
7757
|
+
const freshCapture = await handleChatScreenshot(widgetId, null, quiet);
|
|
7677
7758
|
if (freshCapture) {
|
|
7678
7759
|
canvasRef?.updateWidgetImage(widgetId, freshCapture);
|
|
7679
7760
|
const freshJson = canvasRef?.saveCanvas?.();
|
|
@@ -7713,8 +7794,7 @@
|
|
|
7713
7794
|
showToast('info', 'Capturing widget screenshot...', { duration: 3000 });
|
|
7714
7795
|
}
|
|
7715
7796
|
|
|
7716
|
-
|
|
7717
|
-
const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
|
|
7797
|
+
const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
|
|
7718
7798
|
const timeout = setTimeout(() => {
|
|
7719
7799
|
window.removeEventListener('message', handler);
|
|
7720
7800
|
resolve({ data: null, error: 'timeout' });
|
|
@@ -7765,6 +7845,8 @@
|
|
|
7765
7845
|
if (!quiet) {
|
|
7766
7846
|
showToast('error', 'Screenshot capture failed', { duration: 3000 });
|
|
7767
7847
|
}
|
|
7848
|
+
} finally {
|
|
7849
|
+
screenshotCaptureInFlight.delete(widgetId);
|
|
7768
7850
|
}
|
|
7769
7851
|
}
|
|
7770
7852
|
|
|
@@ -7971,6 +8053,7 @@ Requirements:
|
|
|
7971
8053
|
|
|
7972
8054
|
const panelChat = getChatMethodsForWidget(generationWidgetId);
|
|
7973
8055
|
const panelSnap = getPanelState(generationWidgetId);
|
|
8056
|
+
const hadCodeBeforeGeneration = !!(panelSnap?.hasGeneratedCode || panelSnap?.lastCommitId);
|
|
7974
8057
|
|
|
7975
8058
|
// Per-widget abort: only abort if the SAME widget already has a running generation
|
|
7976
8059
|
const existingController = widgetAbortControllers.get(generationWidgetId);
|
|
@@ -8351,18 +8434,25 @@ Requirements:
|
|
|
8351
8434
|
// Refresh version history after every generation so the new commit shows
|
|
8352
8435
|
// up in the version menu immediately. Publishing is deliberately not
|
|
8353
8436
|
// automatic — the user previews and presses Publish themselves.
|
|
8437
|
+
// Deferred past the preview remount: running this synchronously against
|
|
8438
|
+
// `dynamic_worker_preview_ready` stacked three heavy re-renders on the
|
|
8439
|
+
// main thread in the same 800ms window — the "frozen at 80%" report.
|
|
8354
8440
|
if (generationWidgetId) {
|
|
8355
8441
|
const { [generationWidgetId]: _resolved, ...restResolved } = shapeHistoryResolvedByWidgetId;
|
|
8356
8442
|
shapeHistoryResolvedByWidgetId = restResolved;
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8443
|
+
setTimeout(() => {
|
|
8444
|
+
void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
|
|
8445
|
+
syncWidgetShapeLivePreviews();
|
|
8446
|
+
if (isFocusedPanel) panelDisplayRevision += 1;
|
|
8447
|
+
});
|
|
8448
|
+
}, 1200);
|
|
8361
8449
|
}
|
|
8362
8450
|
|
|
8363
8451
|
// Always reconcile GitLab HEAD after codegen (metadata.commitId can be missing/stale).
|
|
8452
|
+
// Deferred — same window as the schema/history reloads above.
|
|
8364
8453
|
if (generationWidgetId) {
|
|
8365
|
-
|
|
8454
|
+
setTimeout(() => {
|
|
8455
|
+
void (async () => {
|
|
8366
8456
|
try {
|
|
8367
8457
|
const fetchedHead = await fetchRepoHeadCommitSha(generationWidgetId);
|
|
8368
8458
|
// Prefer the codegen commit from the operation. GitLab HEAD can still
|
|
@@ -8404,11 +8494,15 @@ Requirements:
|
|
|
8404
8494
|
} catch (err) {
|
|
8405
8495
|
console.warn('[Version] Failed to reconcile HEAD after codegen:', err);
|
|
8406
8496
|
}
|
|
8407
|
-
|
|
8497
|
+
})();
|
|
8498
|
+
}, 1000);
|
|
8408
8499
|
}
|
|
8409
8500
|
if (generationWidgetId) {
|
|
8410
8501
|
// console.log('[CompositionEditor] Reloading schemas after codegen', generationWidgetId.substring(0, 8));
|
|
8411
|
-
|
|
8502
|
+
// Deferred with the publish-history reload — see comment above.
|
|
8503
|
+
setTimeout(() => {
|
|
8504
|
+
void loadWidgetSchemas(generationWidgetId, { force: true });
|
|
8505
|
+
}, 1600);
|
|
8412
8506
|
}
|
|
8413
8507
|
|
|
8414
8508
|
const completedRepoId = resolveRepositoryIdForWidget(generationWidgetId);
|
|
@@ -8506,6 +8600,9 @@ Requirements:
|
|
|
8506
8600
|
await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
|
|
8507
8601
|
}
|
|
8508
8602
|
activePanelRef?.setPreviewBuildError(null);
|
|
8603
|
+
// TODO(tomorrow): Dupa o generare noua, sincronizarea versiunii in dropdown si istoric se desincronizeaza.
|
|
8604
|
+
// Trebuie armonizata selectia automata a noii versiuni draft (vN) create in istoric,
|
|
8605
|
+
// actualizat trigger-ul pe canvas si in chat la noul numar de versiune si pastrata coerenta intre preview si dropdown.
|
|
8509
8606
|
// Show Worker preview on the canvas shape (not the previous Live CDN artifact).
|
|
8510
8607
|
shapePreviewSourceByWidgetId = {
|
|
8511
8608
|
...shapePreviewSourceByWidgetId,
|
|
@@ -8531,10 +8628,13 @@ Requirements:
|
|
|
8531
8628
|
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
8532
8629
|
}, 2500);
|
|
8533
8630
|
setTimeout(() => {
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8631
|
+
// Only capture screenshot after the very first generation or at publish
|
|
8632
|
+
// Subsequent generations skip this heavy operation to keep the main thread smooth.
|
|
8633
|
+
if (!hadCodeBeforeGeneration) {
|
|
8634
|
+
void (async () => {
|
|
8635
|
+
await handleRefreshScreenshot(generationWidgetId, { quiet: true });
|
|
8636
|
+
})();
|
|
8637
|
+
}
|
|
8538
8638
|
}, 3000);
|
|
8539
8639
|
// Do NOT auto-publish — user previews first, then clicks Publish / Set live.
|
|
8540
8640
|
} else {
|
|
@@ -8944,6 +9044,7 @@ Requirements:
|
|
|
8944
9044
|
widgetPublishStatus,
|
|
8945
9045
|
codeGenerationStatus,
|
|
8946
9046
|
agentHarnessOutput,
|
|
9047
|
+
dynamicWorkerPreviewStatus,
|
|
8947
9048
|
setAuthToken as setWsAuthToken,
|
|
8948
9049
|
setWebsocketApiUrl
|
|
8949
9050
|
} from './stores/websocketStore.js';
|
|
@@ -9330,6 +9431,9 @@ Requirements:
|
|
|
9330
9431
|
|
|
9331
9432
|
function pushCodegenProgressToUi(op: OperationStatus, widgetId: string | null | undefined) {
|
|
9332
9433
|
const line = formatCodegenProgressStatus(op);
|
|
9434
|
+
// Feed the freeze watchdog the latest status so a [FreezeReport] names the
|
|
9435
|
+
// stage the UI was showing when the main thread stopped responding.
|
|
9436
|
+
(window as any).__widgeticFreezeWatchdog?.setStatus?.(line);
|
|
9333
9437
|
// SSE progress frames can arrive many times per second while the LLM
|
|
9334
9438
|
// streams. Each one re-renders every open panel — coalesce them.
|
|
9335
9439
|
if (widgetId) {
|
|
@@ -9920,6 +10024,26 @@ Requirements:
|
|
|
9920
10024
|
|
|
9921
10025
|
function getPanelEditPreviewUrl(widgetId: string): string | null {
|
|
9922
10026
|
const ps = getPanelDisplayState(widgetId);
|
|
10027
|
+
const source = shapePreviewSourceByWidgetId[widgetId];
|
|
10028
|
+
if (source && source.startsWith('v:')) {
|
|
10029
|
+
const version = parseInt(source.slice(2), 10);
|
|
10030
|
+
if (Number.isFinite(version)) {
|
|
10031
|
+
const record = getShapePublishHistory(widgetId).find(
|
|
10032
|
+
(row) => Number(row.version) === version,
|
|
10033
|
+
);
|
|
10034
|
+
const isPublished = !!(record?.publishedAt ?? record?.published_at);
|
|
10035
|
+
const artifact =
|
|
10036
|
+
record?.artifactUrl ||
|
|
10037
|
+
record?.artifact_url ||
|
|
10038
|
+
(version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
|
|
10039
|
+
if (isPublished && artifact) {
|
|
10040
|
+
const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
|
|
10041
|
+
if (url) return url;
|
|
10042
|
+
}
|
|
10043
|
+
const previewState = resolveWidgetShapeVersionPreviewState(widgetId);
|
|
10044
|
+
if (previewState?.url) return previewState.url;
|
|
10045
|
+
}
|
|
10046
|
+
}
|
|
9923
10047
|
const widget = getWidgetById(widgetId);
|
|
9924
10048
|
const version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(widget);
|
|
9925
10049
|
if (version == null) return null;
|
|
@@ -10022,6 +10146,15 @@ Requirements:
|
|
|
10022
10146
|
return fromPanel?.length ? fromPanel : [];
|
|
10023
10147
|
}
|
|
10024
10148
|
|
|
10149
|
+
/** Newest unpublished version — Worker `/preview` (repo HEAD) shows this version's code. */
|
|
10150
|
+
function getNewestDraftVersionId(widgetId: string): number | null {
|
|
10151
|
+
const draftVersions = getShapePublishHistory(widgetId)
|
|
10152
|
+
.filter((row) => !(row.publishedAt ?? row.published_at))
|
|
10153
|
+
.map((row) => Number(row.version))
|
|
10154
|
+
.filter((v) => Number.isFinite(v));
|
|
10155
|
+
return draftVersions.length > 0 ? Math.max(...draftVersions) : null;
|
|
10156
|
+
}
|
|
10157
|
+
|
|
10025
10158
|
function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
|
|
10026
10159
|
if (!widgetId) return null;
|
|
10027
10160
|
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
@@ -10049,6 +10182,11 @@ Requirements:
|
|
|
10049
10182
|
const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
|
|
10050
10183
|
if (url) return url;
|
|
10051
10184
|
}
|
|
10185
|
+
// The newest draft IS repo HEAD — Worker `/preview/{widgetId}` already serves this code!
|
|
10186
|
+
const newestDraft = getNewestDraftVersionId(widgetId);
|
|
10187
|
+
if (!isPublished && newestDraft != null && version === newestDraft && workerUrl) {
|
|
10188
|
+
return workerUrl;
|
|
10189
|
+
}
|
|
10052
10190
|
return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
|
|
10053
10191
|
}
|
|
10054
10192
|
return null;
|
|
@@ -10074,6 +10212,13 @@ Requirements:
|
|
|
10074
10212
|
if (!Number.isFinite(version)) return null;
|
|
10075
10213
|
return `v${version}`;
|
|
10076
10214
|
}
|
|
10215
|
+
// 'preview' = Worker HEAD = newest generated (draft) version. Showing the
|
|
10216
|
+
// Live version here hid every fresh generation behind the old artifact.
|
|
10217
|
+
if (source === 'preview') {
|
|
10218
|
+
const newestDraft = getNewestDraftVersionId(widgetId);
|
|
10219
|
+
if (newestDraft != null) return `v${newestDraft}`;
|
|
10220
|
+
return null;
|
|
10221
|
+
}
|
|
10077
10222
|
return liveVersion != null ? `v${liveVersion}` : null;
|
|
10078
10223
|
}
|
|
10079
10224
|
|
|
@@ -10160,6 +10305,13 @@ Requirements:
|
|
|
10160
10305
|
function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
|
|
10161
10306
|
const source = shapePreviewSourceByWidgetId[widgetId];
|
|
10162
10307
|
if (source?.startsWith('v:')) return source;
|
|
10308
|
+
if (source === 'preview') {
|
|
10309
|
+
// Worker HEAD preview = the newest draft — keep the dropdown selection
|
|
10310
|
+
// on it, otherwise the picker kept pointing at the stale Live version.
|
|
10311
|
+
const newestDraft = getNewestDraftVersionId(widgetId);
|
|
10312
|
+
if (newestDraft != null) return `v:${newestDraft}`;
|
|
10313
|
+
return null;
|
|
10314
|
+
}
|
|
10163
10315
|
const ps = getPanelDisplayState(widgetId);
|
|
10164
10316
|
const liveVersion =
|
|
10165
10317
|
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
@@ -10191,9 +10343,12 @@ Requirements:
|
|
|
10191
10343
|
// Published artifacts need no build. Drafts must never substitute HEAD.
|
|
10192
10344
|
if (versionId.startsWith('v:')) {
|
|
10193
10345
|
const version = Number(versionId.slice(2));
|
|
10194
|
-
|
|
10346
|
+
const directUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
|
|
10347
|
+
if (directUrl) {
|
|
10195
10348
|
const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
|
|
10196
10349
|
console.log('[VersionPreview] Selection', { widgetId, version, commitSha: record?.commitSha ?? record?.commit_sha ?? null, source: 'published' });
|
|
10350
|
+
const detailsRef = getWidgetDetailsRef(widgetId);
|
|
10351
|
+
detailsRef?.showPublishedArtifact?.(directUrl);
|
|
10197
10352
|
} else {
|
|
10198
10353
|
void ensureWidgetShapeVersionPreview(widgetId, version);
|
|
10199
10354
|
}
|
|
@@ -10244,6 +10399,8 @@ Requirements:
|
|
|
10244
10399
|
panelDisplayRevision += 1;
|
|
10245
10400
|
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${widgetId}:${versionId}`]: null };
|
|
10246
10401
|
console.log('[VersionPreview] Selection', { widgetId, version, commitSha });
|
|
10402
|
+
const detailsRef = getWidgetDetailsRef(widgetId);
|
|
10403
|
+
detailsRef?.showPublishedArtifact?.(previewUrl);
|
|
10247
10404
|
} catch (error: unknown) {
|
|
10248
10405
|
if (
|
|
10249
10406
|
shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
@@ -11729,6 +11886,17 @@ $: if ($agentHarnessOutput?.timestamp) {
|
|
|
11729
11886
|
}
|
|
11730
11887
|
}
|
|
11731
11888
|
|
|
11889
|
+
// Worker preview ready via WebSocket event — reload canvas shape preview immediately
|
|
11890
|
+
$: if ($dynamicWorkerPreviewStatus.widgetId && $dynamicWorkerPreviewStatus.previewUrl) {
|
|
11891
|
+
const wsReadyWidgetId = $dynamicWorkerPreviewStatus.widgetId;
|
|
11892
|
+
shapePreviewSourceByWidgetId = {
|
|
11893
|
+
...shapePreviewSourceByWidgetId,
|
|
11894
|
+
[wsReadyWidgetId]: 'preview',
|
|
11895
|
+
};
|
|
11896
|
+
syncWidgetShapeLivePreviews();
|
|
11897
|
+
widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
|
|
11898
|
+
}
|
|
11899
|
+
|
|
11732
11900
|
// ===================== Publish History =====================
|
|
11733
11901
|
|
|
11734
11902
|
/**
|
|
@@ -11893,7 +12061,9 @@ function togglePublishHistory(triggerEl?: HTMLElement | null) {
|
|
|
11893
12061
|
const cluster = triggerEl.closest('.wc-version-history-ct') as HTMLElement | null;
|
|
11894
12062
|
const rect = (cluster ?? triggerEl).getBoundingClientRect();
|
|
11895
12063
|
liveHistoryPanelTop = Math.round(rect.bottom + 4);
|
|
11896
|
-
|
|
12064
|
+
// Rows inset their action button 12px from the panel edge (panel p-1 4px + row px-2 8px).
|
|
12065
|
+
// Shift right by 12px so the per-row action buttons line up under the header action button.
|
|
12066
|
+
liveHistoryPanelRight = Math.round(window.innerWidth - rect.right) - 12;
|
|
11897
12067
|
}
|
|
11898
12068
|
loadPublishHistory();
|
|
11899
12069
|
}
|
|
@@ -12385,6 +12555,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12385
12555
|
*/
|
|
12386
12556
|
const shapeThumbnailCaptureInFlight = new Set<string>();
|
|
12387
12557
|
|
|
12558
|
+
/**
|
|
12559
|
+
* Whole-function single-flight guard for handleRefreshScreenshot.
|
|
12560
|
+
* Stacked html2canvas captures run inside the preview iframe, which shares
|
|
12561
|
+
* the page main thread — a capture storm froze the entire site with no
|
|
12562
|
+
* parent-page [FreezeReport] (long tasks attributed to the iframe).
|
|
12563
|
+
*/
|
|
12564
|
+
const screenshotCaptureInFlight = new Set<string>();
|
|
12565
|
+
|
|
12388
12566
|
/**
|
|
12389
12567
|
* Called when a Widget Image shape is clicked on the canvas.
|
|
12390
12568
|
* Selects the linked widget and opens WidgetDetails.
|
|
@@ -12463,7 +12641,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12463
12641
|
notifyHostWidgetName(widget);
|
|
12464
12642
|
}
|
|
12465
12643
|
await openWidgetDetailsPanel(widgetId);
|
|
12466
|
-
scheduleCanvasShapePreviewSync(widgetId);
|
|
12467
12644
|
}
|
|
12468
12645
|
|
|
12469
12646
|
/**
|
|
@@ -14152,6 +14329,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14152
14329
|
onTranslateEnd={(widgetId) => {
|
|
14153
14330
|
canvasRef?.commitWidgetShapeTranslate?.(widgetId);
|
|
14154
14331
|
}}
|
|
14332
|
+
onOverlayDragChange={(active) => {
|
|
14333
|
+
canvasRef?.setWidgetShapeOverlayDragActive?.(active);
|
|
14334
|
+
}}
|
|
14155
14335
|
onSelectShape={(widgetId) => {
|
|
14156
14336
|
canvasRef?.selectWidgetShapeById?.(widgetId);
|
|
14157
14337
|
canvasSelectedWidgetShapeId = widgetId;
|
|
@@ -14161,6 +14341,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14161
14341
|
void openWidgetDetailsPanel(widgetId);
|
|
14162
14342
|
}}
|
|
14163
14343
|
onRefresh={handleWidgetShapeRefresh}
|
|
14344
|
+
isGenerating={isAnyWidgetGenerating()}
|
|
14164
14345
|
/>
|
|
14165
14346
|
|
|
14166
14347
|
<!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
|
|
@@ -14564,8 +14745,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14564
14745
|
if (isPanelFocused && previewWidgetId === panelWidgetId) {
|
|
14565
14746
|
chatMethods?.clearStalePreviewBuildFailures?.();
|
|
14566
14747
|
}
|
|
14567
|
-
// Keep canvas WidgetShape thumbnail aligned with the live Worker preview.
|
|
14568
|
-
scheduleCanvasShapePreviewSync(previewWidgetId);
|
|
14569
14748
|
if (detail?.step === 'create' || ps.widgetDetailsStep === 'create') {
|
|
14570
14749
|
applyPanelCompositionToPreview(previewWidgetId);
|
|
14571
14750
|
}
|
|
@@ -14800,10 +14979,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14800
14979
|
{@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
|
|
14801
14980
|
{@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
|
|
14802
14981
|
? parseInt(shapeSource.slice(2), 10)
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14982
|
+
: shapeSource === 'preview'
|
|
14983
|
+
// A 'preview' source (Worker HEAD, unpublished) is not a published
|
|
14984
|
+
// version — falling back to panelPublishedVersion produced `vnull`
|
|
14985
|
+
// right after generation AND showed the stale Live version when a
|
|
14986
|
+
// draft existed. The newest draft IS this HEAD's version.
|
|
14987
|
+
? (newestDraft ? newestDraft.version : panelPublishedVersion)
|
|
14988
|
+
: panelPublishedVersion}
|
|
14807
14989
|
{@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
|
|
14808
14990
|
<!--
|
|
14809
14991
|
A version is a Draft until it is published. The history is one
|
|
@@ -14841,13 +15023,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14841
15023
|
}}
|
|
14842
15024
|
disabled={isPublishDisabledForPanel(panelWidgetId) || (isPublishing && publishingWidgetId === panelWidgetId)}
|
|
14843
15025
|
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? 'Publish the latest generated code as the Live version'}
|
|
14844
|
-
class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
15026
|
+
class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
14845
15027
|
>
|
|
14846
15028
|
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14847
15029
|
</button>
|
|
14848
15030
|
{:else if viewingIsLive}
|
|
14849
15031
|
<span
|
|
14850
|
-
class="wc-version-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 opacity-80"
|
|
15032
|
+
class="wc-version-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 opacity-80"
|
|
14851
15033
|
title="Live — generate new code to Publish a new version"
|
|
14852
15034
|
>Live</span>
|
|
14853
15035
|
{:else if viewingIsDraft}
|
|
@@ -14863,7 +15045,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14863
15045
|
}}
|
|
14864
15046
|
disabled={!canPublishDraftVersionForPanel(panelWidgetId, viewingVersion) || getPublishGateForPanel(panelWidgetId, Number(viewingVersion)).disabled}
|
|
14865
15047
|
title="Build this draft version and make it the Live version"
|
|
14866
|
-
class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
15048
|
+
class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
14867
15049
|
>
|
|
14868
15050
|
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14869
15051
|
</button>
|
|
@@ -14880,7 +15062,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14880
15062
|
}}
|
|
14881
15063
|
disabled={isSettingLiveVersion === Number(viewingVersion)}
|
|
14882
15064
|
title="Publish this version so visitors see it on your website"
|
|
14883
|
-
class="wc-version-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-300 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
15065
|
+
class="wc-version-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-300 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
14884
15066
|
>
|
|
14885
15067
|
{isSettingLiveVersion === Number(viewingVersion) ? '...' : 'Set live'}
|
|
14886
15068
|
</button>
|
|
@@ -14893,15 +15075,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14893
15075
|
aria-label="Close version history"
|
|
14894
15076
|
onclick={() => { isHistoryOpen = false; }}
|
|
14895
15077
|
></button>
|
|
15078
|
+
{@const hasChatScroll = ps.publishHistory.length > 5}
|
|
14896
15079
|
<div
|
|
14897
|
-
class="wc-version-history-panel
|
|
14898
|
-
style="position: fixed; top: {liveHistoryPanelTop}px; right: {liveHistoryPanelRight}px;
|
|
15080
|
+
class="wc-version-history-panel rounded-md border border-emerald-200 bg-white shadow-lg p-1 z-[9999] w-max"
|
|
15081
|
+
style="position: fixed; top: {liveHistoryPanelTop}px; right: {liveHistoryPanelRight}px;"
|
|
14899
15082
|
>
|
|
14900
15083
|
{#if isLoadingHistory}
|
|
14901
15084
|
<p class="text-xs text-emerald-500 animate-pulse text-center py-2">Loading history...</p>
|
|
14902
15085
|
{:else if ps.publishHistory.length === 0}
|
|
14903
15086
|
<p class="text-xs text-emerald-400 text-center py-2">No history records yet. Generate or publish to start tracking.</p>
|
|
14904
15087
|
{:else}
|
|
15088
|
+
<div
|
|
15089
|
+
class="wc-version-history-scroll flex flex-col gap-1 overflow-x-hidden {hasChatScroll ? 'overflow-y-auto' : 'overflow-y-visible'}"
|
|
15090
|
+
style="{hasChatScroll ? 'max-height: 185px; margin-right: -4px; padding-right: 4px;' : 'max-height: none;'} scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
|
|
15091
|
+
>
|
|
14905
15092
|
{#each ps.publishHistory as record (record.id)}
|
|
14906
15093
|
{@const rowVersion = Number(record.version)}
|
|
14907
15094
|
{@const rowIsDraft = !(record.publishedAt ?? record.published_at)}
|
|
@@ -14922,6 +15109,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14922
15109
|
publishDisabledReason={publishGate.disabled ? publishGate.reason : null}
|
|
14923
15110
|
setLiveDisabled={isSettingLiveVersion === record.version}
|
|
14924
15111
|
onselect={() => {
|
|
15112
|
+
isHistoryOpen = false;
|
|
14925
15113
|
handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
|
|
14926
15114
|
}}
|
|
14927
15115
|
onpublish={() => {
|
|
@@ -14933,6 +15121,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14933
15121
|
}}
|
|
14934
15122
|
/>
|
|
14935
15123
|
{/each}
|
|
15124
|
+
</div>
|
|
14936
15125
|
{/if}
|
|
14937
15126
|
</div>
|
|
14938
15127
|
</Portal>
|
|
@@ -15021,6 +15210,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15021
15210
|
class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
|
|
15022
15211
|
style="padding: 0;"
|
|
15023
15212
|
use:nativeScrollbarPadding={0}
|
|
15213
|
+
use:pinScrollToBottom
|
|
15024
15214
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
15025
15215
|
>
|
|
15026
15216
|
{#key panelInstanceKey}
|
|
@@ -15821,21 +16011,24 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15821
16011
|
outline: none;
|
|
15822
16012
|
box-shadow: none;
|
|
15823
16013
|
}
|
|
15824
|
-
.wc-version-history-
|
|
16014
|
+
.wc-version-history-scroll {
|
|
15825
16015
|
scrollbar-width: thin;
|
|
15826
16016
|
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
|
15827
16017
|
}
|
|
15828
|
-
.wc-version-history-
|
|
15829
|
-
width:
|
|
16018
|
+
.wc-version-history-scroll::-webkit-scrollbar {
|
|
16019
|
+
width: 4px;
|
|
15830
16020
|
background: transparent;
|
|
15831
16021
|
}
|
|
15832
|
-
.wc-version-history-
|
|
16022
|
+
.wc-version-history-scroll::-webkit-scrollbar-track {
|
|
15833
16023
|
background: transparent;
|
|
15834
16024
|
}
|
|
15835
|
-
.wc-version-history-
|
|
16025
|
+
.wc-version-history-scroll::-webkit-scrollbar-thumb {
|
|
15836
16026
|
background: rgba(0, 0, 0, 0.2);
|
|
15837
16027
|
border-radius: 4px;
|
|
15838
16028
|
}
|
|
16029
|
+
.wc-version-history-scroll::-webkit-scrollbar-thumb:hover {
|
|
16030
|
+
background: rgba(0, 0, 0, 0.35);
|
|
16031
|
+
}
|
|
15839
16032
|
.generation-scroll-ct {
|
|
15840
16033
|
scrollbar-width: thin;
|
|
15841
16034
|
scrollbar-color: rgba(0, 0, 0, 0.22) transparent;
|
|
@@ -42,14 +42,16 @@
|
|
|
42
42
|
// One fixed width per column — identical on every row and in both dropdowns.
|
|
43
43
|
// Explicit PIXEL widths that hug the content (vN, 7-char SHA, dd/mm/yyyy
|
|
44
44
|
// hh:mm) — no rem sizing, no floating side padding.
|
|
45
|
-
const COL_VERSION = 'w-[
|
|
45
|
+
const COL_VERSION = 'w-[20px] justify-end';
|
|
46
46
|
const COL_COMMIT = 'w-[44px] justify-end';
|
|
47
47
|
const COL_DATE = 'w-[88px]';
|
|
48
|
-
|
|
48
|
+
// Same width as the header Publish/Live button (min-w-[4.75rem] / w-[4.75rem]) so every
|
|
49
|
+
// action button lines up in one column across rows, header and overlay.
|
|
50
|
+
const COL_ACTION = 'w-[4.75rem]';
|
|
49
51
|
</script>
|
|
50
52
|
|
|
51
53
|
<div
|
|
52
|
-
class="version-dropdown-row flex items-center gap-2 rounded px-2 py-1.5 w-
|
|
54
|
+
class="version-dropdown-row flex items-center justify-between gap-2 rounded px-2 py-1.5 w-full text-[10px] cursor-pointer {selected ? 'bg-emerald-50 text-emerald-800' : 'text-gray-700 hover:bg-gray-50'}"
|
|
53
55
|
role="option"
|
|
54
56
|
aria-selected={selected}
|
|
55
57
|
tabindex="0"
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
onTranslate?: ((widgetId: string, dxScreen: number, dyScreen: number) => void) | null;
|
|
50
50
|
/** Fired once when a chrome drag ends (mark canvas dirty / history). */
|
|
51
51
|
onTranslateEnd?: ((widgetId: string) => void) | null;
|
|
52
|
+
/** Tell the canvas a chrome drag started/ended (suppresses hover-select). */
|
|
53
|
+
onOverlayDragChange?: ((active: boolean) => void) | null;
|
|
52
54
|
/** Select the Fabric WidgetShape (e.g. when pressing the drag bar). */
|
|
53
55
|
onSelectShape?: ((widgetId: string) => void) | null;
|
|
54
56
|
/** Open Widget Details / chat for this widget. */
|
|
@@ -86,6 +88,8 @@
|
|
|
86
88
|
resolveIsPublishing?: ((widgetId: string) => boolean) | null;
|
|
87
89
|
/** Publish-disabled reason for a specific Draft row (null = enabled). */
|
|
88
90
|
resolvePublishDisabledReason?: ((widgetId: string, version: number) => string | null) | null;
|
|
91
|
+
/** Whether code generation is running (suppresses background captures / reloads). */
|
|
92
|
+
isGenerating?: boolean;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
let {
|
|
@@ -103,6 +107,7 @@
|
|
|
103
107
|
resolvePublishDisabled = null,
|
|
104
108
|
onTranslate = null,
|
|
105
109
|
onTranslateEnd = null,
|
|
110
|
+
onOverlayDragChange = null,
|
|
106
111
|
onSelectShape = null,
|
|
107
112
|
onOpenChat = null,
|
|
108
113
|
resolveTitle = null,
|
|
@@ -121,6 +126,7 @@
|
|
|
121
126
|
resolveIsPublishing = null,
|
|
122
127
|
/** Per-widget publish-gate reason for a Draft row (tooltip on Publish). */
|
|
123
128
|
resolvePublishDisabledReason = null,
|
|
129
|
+
isGenerating = false,
|
|
124
130
|
}: Props = $props();
|
|
125
131
|
|
|
126
132
|
const DRAG_BAR_H = 30;
|
|
@@ -130,7 +136,11 @@
|
|
|
130
136
|
const MIN_EXPANDED_CHROME_WIDTH = 220;
|
|
131
137
|
const IFRAME_INSET_PX = 2;
|
|
132
138
|
/** ~5 version rows before the menu scrolls (matches chat history panel). */
|
|
133
|
-
|
|
139
|
+
/** Row height ≈2.1rem (py-1.5 + text) + 0.6rem vertical padding for the menu box. */
|
|
140
|
+
const VERSION_MENU_ROW_REM = 2.1;
|
|
141
|
+
const VERSION_MENU_PAD_REM = 0.6;
|
|
142
|
+
/** Menu shows at most 5 rows before scrolling. */
|
|
143
|
+
const VERSION_MENU_MAX_ROWS = 5;
|
|
134
144
|
|
|
135
145
|
type ShapeRect = {
|
|
136
146
|
widgetId: string;
|
|
@@ -177,15 +187,27 @@
|
|
|
177
187
|
* at the same URL (codegen rebuild). `sync()` only reloads when `src` changes,
|
|
178
188
|
* so without a cache-bust token the overlay keeps showing pre-codegen content.
|
|
179
189
|
* Bumps the reload token → `{#key}` remounts the iframe with a fresh `_t`.
|
|
190
|
+
* Debounced by 1200ms to avoid double-refresh flicker when WS and completed SSE both fire.
|
|
180
191
|
*/
|
|
192
|
+
let lastReloadAtByWidgetId: Record<string, number> = {};
|
|
181
193
|
export function requestReload(widgetId: string): void {
|
|
182
|
-
const
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
if (now - (lastReloadAtByWidgetId[widgetId] || 0) < 1200) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
lastReloadAtByWidgetId[widgetId] = now;
|
|
199
|
+
cancelPendingRefresh(widgetId);
|
|
200
|
+
const token = Math.max(now, (reloadTokens[widgetId] || 0) + 1);
|
|
201
|
+
pendingRefreshTokens.set(widgetId, token);
|
|
183
202
|
reloadTokens = { ...reloadTokens, [widgetId]: token };
|
|
184
203
|
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
|
|
204
|
+
sync();
|
|
185
205
|
}
|
|
186
206
|
|
|
187
207
|
/** Which shape chrome version menu is open. */
|
|
188
208
|
let openVersionMenuWidgetId = $state<string | null>(null);
|
|
209
|
+
/** Local manual version selection override per widget (ensures trigger updates immediately). */
|
|
210
|
+
let localSelectedVersionByWidgetId = $state<Record<string, string>>({});
|
|
189
211
|
/** Hide Fabric placeholder flash — show loader until iframe fires load. */
|
|
190
212
|
let iframeLoadedByWidgetId = $state<Record<string, boolean>>({});
|
|
191
213
|
|
|
@@ -263,11 +285,13 @@
|
|
|
263
285
|
document.body.style.removeProperty('user-select');
|
|
264
286
|
document.body.style.removeProperty('cursor');
|
|
265
287
|
onTranslateEnd?.(widgetId);
|
|
288
|
+
onOverlayDragChange?.(false);
|
|
266
289
|
console.log('[WidgetShapeLivePreviews] Drag end', widgetId.substring(0, 8));
|
|
267
290
|
}
|
|
268
291
|
|
|
269
292
|
document.body.style.userSelect = 'none';
|
|
270
293
|
document.body.style.cursor = 'grabbing';
|
|
294
|
+
onOverlayDragChange?.(true);
|
|
271
295
|
document.addEventListener('pointermove', onMove);
|
|
272
296
|
document.addEventListener('pointerup', endDrag);
|
|
273
297
|
document.addEventListener('pointercancel', endDrag);
|
|
@@ -303,6 +327,11 @@
|
|
|
303
327
|
// An old load event must not mark the replacement loaded or claim its timer.
|
|
304
328
|
if (!matchesFrame()) return;
|
|
305
329
|
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: true };
|
|
330
|
+
// Suppress auto-refresh / screenshot capture when code generation is running.
|
|
331
|
+
if (isGenerating) {
|
|
332
|
+
cancelPendingRefresh(widgetId);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
306
335
|
const token = pendingRefreshTokens.get(widgetId);
|
|
307
336
|
if (!token || refreshSettleTimers.has(widgetId)) return;
|
|
308
337
|
const isCurrent = () => matchesFrame()
|
|
@@ -332,6 +361,8 @@
|
|
|
332
361
|
}
|
|
333
362
|
|
|
334
363
|
function resolveSelectedVersionId(widgetId: string, options: VersionOption[]): string {
|
|
364
|
+
const local = localSelectedVersionByWidgetId[widgetId];
|
|
365
|
+
if (local && options.some((o) => o.id === local)) return local;
|
|
335
366
|
const fromParent = resolveVersionSelectedId?.(widgetId)?.trim();
|
|
336
367
|
if (fromParent && options.some((o) => o.id === fromParent)) return fromParent;
|
|
337
368
|
const liveOpt = options.find((o) => o.isLive);
|
|
@@ -404,6 +435,7 @@
|
|
|
404
435
|
function selectVersionFromMenu(widgetId: string, versionId: string) {
|
|
405
436
|
openVersionMenuWidgetId = null;
|
|
406
437
|
if (!versionId) return;
|
|
438
|
+
localSelectedVersionByWidgetId = { ...localSelectedVersionByWidgetId, [widgetId]: versionId };
|
|
407
439
|
onVersionChange?.(widgetId, versionId);
|
|
408
440
|
}
|
|
409
441
|
|
|
@@ -438,8 +470,15 @@
|
|
|
438
470
|
*/
|
|
439
471
|
function trackIframeSelection(iframe, widgetId) {
|
|
440
472
|
let focusTimer;
|
|
473
|
+
let pointerIsDown = false;
|
|
474
|
+
const onWindowPointerDown = () => { pointerIsDown = true; };
|
|
475
|
+
const onWindowPointerUp = () => { pointerIsDown = false; };
|
|
441
476
|
function select() {
|
|
442
477
|
if (!enabled || draggingWidgetId || resizingWidgetId || !iframe.isConnected) return;
|
|
478
|
+
// A drag in progress anywhere (Fabric shape drag or overlay chrome drag)
|
|
479
|
+
// must not let hover over this iframe steal the selection — the pointer
|
|
480
|
+
// merely passes over other shapes while carrying the dragged one.
|
|
481
|
+
if (pointerIsDown) return;
|
|
443
482
|
// Keep the menu owner selected while its dropdown is being used.
|
|
444
483
|
if (openVersionMenuWidgetId && openVersionMenuWidgetId !== widgetId) return;
|
|
445
484
|
if (selectedWidgetId !== widgetId) onSelectShape?.(widgetId);
|
|
@@ -450,6 +489,8 @@
|
|
|
450
489
|
if (document.activeElement === iframe) select();
|
|
451
490
|
}, 0);
|
|
452
491
|
}
|
|
492
|
+
window.addEventListener('pointerdown', onWindowPointerDown, true);
|
|
493
|
+
window.addEventListener('pointerup', onWindowPointerUp, true);
|
|
453
494
|
iframe.addEventListener('pointerenter', select);
|
|
454
495
|
iframe.addEventListener('focus', select);
|
|
455
496
|
window.addEventListener('blur', checkFocus);
|
|
@@ -457,6 +498,8 @@
|
|
|
457
498
|
update(nextWidgetId) { widgetId = nextWidgetId; },
|
|
458
499
|
destroy() {
|
|
459
500
|
clearTimeout(focusTimer);
|
|
501
|
+
window.removeEventListener('pointerdown', onWindowPointerDown, true);
|
|
502
|
+
window.removeEventListener('pointerup', onWindowPointerUp, true);
|
|
460
503
|
iframe.removeEventListener('pointerenter', select);
|
|
461
504
|
iframe.removeEventListener('focus', select);
|
|
462
505
|
window.removeEventListener('blur', checkFocus);
|
|
@@ -604,7 +647,7 @@
|
|
|
604
647
|
>
|
|
605
648
|
<button
|
|
606
649
|
type="button"
|
|
607
|
-
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[
|
|
650
|
+
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded border border-emerald-600 bg-emerald-600 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-white hover:bg-emerald-700 disabled:opacity-50 cursor-pointer"
|
|
608
651
|
disabled={isPublishing || selectedPreviewBlocked || !!resolvePublishDisabled?.(overlay.widgetId, selectedVersion ? Number(selectedVersion.id.slice(2)) : null)}
|
|
609
652
|
onclick={(e) => {
|
|
610
653
|
e.stopPropagation();
|
|
@@ -620,7 +663,7 @@
|
|
|
620
663
|
class="inline-flex shrink-0"
|
|
621
664
|
>
|
|
622
665
|
<span
|
|
623
|
-
class="widget-shape-live-btn shrink-0 inline-flex items-center justify-center min-w-[
|
|
666
|
+
class="widget-shape-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded bg-emerald-100 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-600 opacity-80"
|
|
624
667
|
>Live</span>
|
|
625
668
|
</HoverTooltip>
|
|
626
669
|
{:else if selectedVersion && onSetLive && !isDraftVersion(selectedVersion)}
|
|
@@ -631,7 +674,7 @@
|
|
|
631
674
|
>
|
|
632
675
|
<button
|
|
633
676
|
type="button"
|
|
634
|
-
class="widget-shape-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[
|
|
677
|
+
class="widget-shape-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded border border-emerald-300 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 cursor-pointer"
|
|
635
678
|
disabled={settingLiveVersion === Number(selectedVersion.id.slice(2))}
|
|
636
679
|
onclick={(e) => handleSetLiveClick(e, overlay.widgetId, selectedVersion.id)}
|
|
637
680
|
onpointerdown={(e) => e.stopPropagation()}
|
|
@@ -646,7 +689,7 @@
|
|
|
646
689
|
>
|
|
647
690
|
<button
|
|
648
691
|
type="button"
|
|
649
|
-
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[
|
|
692
|
+
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded border border-emerald-600 bg-emerald-600 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-white hover:bg-emerald-700 disabled:opacity-50 cursor-pointer"
|
|
650
693
|
disabled={isPublishing || selectedPreviewBlocked || !resolveCanPublishDraft?.(overlay.widgetId, Number(selectedVersion.id.slice(2))) || !!resolvePublishDisabled?.(overlay.widgetId, Number(selectedVersion.id.slice(2)))}
|
|
651
694
|
onclick={(e) => handlePublishDraftClick(e, overlay.widgetId, selectedVersion.id)}
|
|
652
695
|
onpointerdown={(e) => e.stopPropagation()}
|
|
@@ -659,39 +702,45 @@
|
|
|
659
702
|
class="inline-flex shrink-0"
|
|
660
703
|
>
|
|
661
704
|
<span
|
|
662
|
-
class="widget-shape-draft-badge shrink-0 inline-flex items-center justify-center min-w-[
|
|
705
|
+
class="widget-shape-draft-badge shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded bg-amber-100 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700"
|
|
663
706
|
>Draft</span>
|
|
664
707
|
</HoverTooltip>
|
|
665
708
|
{/if}
|
|
666
709
|
{/if}
|
|
667
710
|
{#if versionMenuOpen}
|
|
711
|
+
{@const hasScroll = versionOptions.length > 5}
|
|
668
712
|
<div
|
|
669
|
-
class="widget-shape-version-menu absolute
|
|
670
|
-
style="
|
|
713
|
+
class="widget-shape-version-menu absolute top-full z-30 mt-0.5 w-max rounded-md border border-emerald-200 bg-white p-1 shadow-lg"
|
|
714
|
+
style="right: -12px;"
|
|
671
715
|
role="listbox"
|
|
672
716
|
>
|
|
673
|
-
|
|
674
|
-
{
|
|
675
|
-
{
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
717
|
+
<div
|
|
718
|
+
class="widget-shape-version-scroll flex flex-col gap-1 overflow-x-hidden {hasScroll ? 'overflow-y-auto' : 'overflow-y-visible'}"
|
|
719
|
+
style="{hasScroll ? 'max-height: 185px; margin-right: -4px; padding-right: 4px;' : 'max-height: none;'} scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
|
|
720
|
+
>
|
|
721
|
+
{#each versionOptions as option (option.id)}
|
|
722
|
+
{@const optionVersion = Number(option.id.slice(2))}
|
|
723
|
+
{@const optionBusy = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
|
|
724
|
+
{@const optionPublishDisabled = !resolveCanPublishDraft?.(overlay.widgetId, optionVersion)
|
|
725
|
+
|| !!resolvePublishDisabled?.(overlay.widgetId, optionVersion)
|
|
726
|
+
|| (selectedVersion?.id === option.id && selectedPreviewBlocked)}
|
|
727
|
+
{@const optionPublishReason = (void titleRevision, resolvePublishDisabledReason?.(overlay.widgetId, optionVersion))}
|
|
728
|
+
<VersionDropdownRow
|
|
729
|
+
versionLabel={option.shortLabel || option.label}
|
|
730
|
+
dateLabel={option.dateLabel}
|
|
731
|
+
isLive={option.isLive === true}
|
|
732
|
+
isDraft={isDraftVersion(option)}
|
|
733
|
+
selected={selectedVersion?.id === option.id}
|
|
734
|
+
busy={optionBusy}
|
|
735
|
+
publishDisabled={optionPublishDisabled}
|
|
736
|
+
publishDisabledReason={optionPublishReason || 'Build this draft version and make it the Live version'}
|
|
737
|
+
setLiveDisabled={settingLiveVersion === optionVersion}
|
|
738
|
+
onselect={() => selectVersionFromMenu(overlay.widgetId, option.id)}
|
|
739
|
+
onpublish={() => handlePublishDraftClickFromMenu(overlay.widgetId, option.id)}
|
|
740
|
+
onsetlive={() => handleSetLiveClickFromMenu(overlay.widgetId, option.id)}
|
|
741
|
+
/>
|
|
742
|
+
{/each}
|
|
743
|
+
</div>
|
|
695
744
|
</div>
|
|
696
745
|
{/if}
|
|
697
746
|
</div>
|
|
@@ -812,16 +861,19 @@
|
|
|
812
861
|
{/if}
|
|
813
862
|
|
|
814
863
|
<style>
|
|
815
|
-
.widget-shape-version-
|
|
816
|
-
width:
|
|
817
|
-
height:
|
|
864
|
+
.widget-shape-version-scroll::-webkit-scrollbar {
|
|
865
|
+
width: 4px;
|
|
866
|
+
height: 4px;
|
|
818
867
|
background: transparent;
|
|
819
868
|
}
|
|
820
|
-
.widget-shape-version-
|
|
869
|
+
.widget-shape-version-scroll::-webkit-scrollbar-track {
|
|
821
870
|
background: transparent;
|
|
822
871
|
}
|
|
823
|
-
.widget-shape-version-
|
|
872
|
+
.widget-shape-version-scroll::-webkit-scrollbar-thumb {
|
|
824
873
|
background: rgba(0, 0, 0, 0.2);
|
|
825
874
|
border-radius: 4px;
|
|
826
875
|
}
|
|
876
|
+
.widget-shape-version-scroll::-webkit-scrollbar-thumb:hover {
|
|
877
|
+
background: rgba(0, 0, 0, 0.35);
|
|
878
|
+
}
|
|
827
879
|
</style>
|
|
@@ -51,6 +51,8 @@ interface Props {
|
|
|
51
51
|
onTranslate?: ((widgetId: string, dxScreen: number, dyScreen: number) => void) | null;
|
|
52
52
|
/** Fired once when a chrome drag ends (mark canvas dirty / history). */
|
|
53
53
|
onTranslateEnd?: ((widgetId: string) => void) | null;
|
|
54
|
+
/** Tell the canvas a chrome drag started/ended (suppresses hover-select). */
|
|
55
|
+
onOverlayDragChange?: ((active: boolean) => void) | null;
|
|
54
56
|
/** Select the Fabric WidgetShape (e.g. when pressing the drag bar). */
|
|
55
57
|
onSelectShape?: ((widgetId: string) => void) | null;
|
|
56
58
|
/** Open Widget Details / chat for this widget. */
|
|
@@ -88,6 +90,8 @@ interface Props {
|
|
|
88
90
|
resolveIsPublishing?: ((widgetId: string) => boolean) | null;
|
|
89
91
|
/** Publish-disabled reason for a specific Draft row (null = enabled). */
|
|
90
92
|
resolvePublishDisabledReason?: ((widgetId: string, version: number) => string | null) | null;
|
|
93
|
+
/** Whether code generation is running (suppresses background captures / reloads). */
|
|
94
|
+
isGenerating?: boolean;
|
|
91
95
|
}
|
|
92
96
|
declare const WidgetShapeLivePreviews: import("svelte").Component<Props, {
|
|
93
97
|
requestReload: (widgetId: string) => void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread responsiveness watchdog.
|
|
3
|
+
*
|
|
4
|
+
* PerformanceObserver('longtask') only fires AFTER a blocking task completes —
|
|
5
|
+
* an infinite reactive loop, or heavy work running inside a same-origin iframe
|
|
6
|
+
* (html2canvas screenshot captures in the widget preview share the parent's main
|
|
7
|
+
* thread), is never attributed to the page. The result was a frozen site with no
|
|
8
|
+
* [FreezeReport] in the console.
|
|
9
|
+
*
|
|
10
|
+
* This watchdog flips the model: a Web Worker (own thread, unaffected by the
|
|
11
|
+
* blocked main thread) pings every 500ms and the main thread answers. When pongs
|
|
12
|
+
* stop arriving, the worker logs a [FreezeReport] straight to the page console
|
|
13
|
+
* (worker console output shows in DevTools) with the block duration and the last
|
|
14
|
+
* codegen status the main thread reported. It also reports recovery, so each
|
|
15
|
+
* freeze episode produces exactly two lines: block start + recovery.
|
|
16
|
+
*/
|
|
17
|
+
export interface FreezeWatchdog {
|
|
18
|
+
/** Main thread reports the current codegen status; included in freeze reports. */
|
|
19
|
+
setStatus(status: string): void;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function installFreezeWatchdog(): FreezeWatchdog | null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread responsiveness watchdog.
|
|
3
|
+
*
|
|
4
|
+
* PerformanceObserver('longtask') only fires AFTER a blocking task completes —
|
|
5
|
+
* an infinite reactive loop, or heavy work running inside a same-origin iframe
|
|
6
|
+
* (html2canvas screenshot captures in the widget preview share the parent's main
|
|
7
|
+
* thread), is never attributed to the page. The result was a frozen site with no
|
|
8
|
+
* [FreezeReport] in the console.
|
|
9
|
+
*
|
|
10
|
+
* This watchdog flips the model: a Web Worker (own thread, unaffected by the
|
|
11
|
+
* blocked main thread) pings every 500ms and the main thread answers. When pongs
|
|
12
|
+
* stop arriving, the worker logs a [FreezeReport] straight to the page console
|
|
13
|
+
* (worker console output shows in DevTools) with the block duration and the last
|
|
14
|
+
* codegen status the main thread reported. It also reports recovery, so each
|
|
15
|
+
* freeze episode produces exactly two lines: block start + recovery.
|
|
16
|
+
*/
|
|
17
|
+
const WATCHDOG_SOURCE = `
|
|
18
|
+
let lastPongAt = performance.now();
|
|
19
|
+
let freezeStartedAt = null;
|
|
20
|
+
let reported = false;
|
|
21
|
+
let lastStatus = '';
|
|
22
|
+
const PING_MS = 500;
|
|
23
|
+
const STALL_MS = 4000;
|
|
24
|
+
setInterval(() => {
|
|
25
|
+
postMessage({ type: 'ping' });
|
|
26
|
+
const now = performance.now();
|
|
27
|
+
if (now - lastPongAt >= STALL_MS) {
|
|
28
|
+
if (!freezeStartedAt) freezeStartedAt = now - STALL_MS;
|
|
29
|
+
if (!reported) {
|
|
30
|
+
reported = true;
|
|
31
|
+
console.warn('[FreezeReport] Main thread unresponsive ' + Math.round(now - freezeStartedAt) +
|
|
32
|
+
'ms. Last status: "' + (lastStatus || '—') + '"');
|
|
33
|
+
}
|
|
34
|
+
} else if (freezeStartedAt) {
|
|
35
|
+
console.warn('[FreezeReport] Main thread recovered after ' + Math.round(now - freezeStartedAt) +
|
|
36
|
+
'ms. Last status: "' + (lastStatus || '—') + '"');
|
|
37
|
+
freezeStartedAt = null;
|
|
38
|
+
reported = false;
|
|
39
|
+
}
|
|
40
|
+
}, PING_MS);
|
|
41
|
+
onmessage = (e) => {
|
|
42
|
+
if (e.data && e.data.type === 'pong') {
|
|
43
|
+
lastPongAt = performance.now();
|
|
44
|
+
if (e.data.status) lastStatus = e.data.status;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
`;
|
|
48
|
+
export function installFreezeWatchdog() {
|
|
49
|
+
if (typeof window === 'undefined' || typeof Worker === 'undefined' || typeof Blob === 'undefined') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const existing = window.__widgeticFreezeWatchdog;
|
|
53
|
+
if (existing)
|
|
54
|
+
return existing;
|
|
55
|
+
const blob = new Blob([WATCHDOG_SOURCE], { type: 'text/javascript' });
|
|
56
|
+
const url = URL.createObjectURL(blob);
|
|
57
|
+
const worker = new Worker(url);
|
|
58
|
+
URL.revokeObjectURL(url);
|
|
59
|
+
let lastStatus = '';
|
|
60
|
+
worker.onmessage = (event) => {
|
|
61
|
+
if (event.data?.type === 'ping') {
|
|
62
|
+
worker.postMessage({ type: 'pong', status: lastStatus });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const watchdog = {
|
|
66
|
+
setStatus(status) {
|
|
67
|
+
lastStatus = status;
|
|
68
|
+
},
|
|
69
|
+
dispose() {
|
|
70
|
+
worker.terminate();
|
|
71
|
+
delete window.__widgeticFreezeWatchdog;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
window.__widgeticFreezeWatchdog = watchdog;
|
|
75
|
+
return watchdog;
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a scrollable container pinned to its bottom edge until the user scrolls
|
|
3
|
+
* up. Opening the widget-details panel on a small screen lays the panel out
|
|
4
|
+
* after mount, which can leave the outer scroll area (status bar + chat +
|
|
5
|
+
* footer) resting above its bottom — clipping the chat input. The observer
|
|
6
|
+
* re-pins to bottom whenever the content height or the box changes, so the
|
|
7
|
+
* input is always visible on open. Any manual upward scroll un-pins until the
|
|
8
|
+
* user returns to the bottom, preserving normal reading behavior.
|
|
9
|
+
*/
|
|
10
|
+
export declare function pinScrollToBottom(node: HTMLElement): {
|
|
11
|
+
destroy(): void;
|
|
12
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a scrollable container pinned to its bottom edge until the user scrolls
|
|
3
|
+
* up. Opening the widget-details panel on a small screen lays the panel out
|
|
4
|
+
* after mount, which can leave the outer scroll area (status bar + chat +
|
|
5
|
+
* footer) resting above its bottom — clipping the chat input. The observer
|
|
6
|
+
* re-pins to bottom whenever the content height or the box changes, so the
|
|
7
|
+
* input is always visible on open. Any manual upward scroll un-pins until the
|
|
8
|
+
* user returns to the bottom, preserving normal reading behavior.
|
|
9
|
+
*/
|
|
10
|
+
export function pinScrollToBottom(node) {
|
|
11
|
+
let pinned = true;
|
|
12
|
+
let rafId = null;
|
|
13
|
+
const scrollToBottom = () => {
|
|
14
|
+
if (!pinned)
|
|
15
|
+
return;
|
|
16
|
+
// rAF so the layout pass has settled before reading scrollHeight.
|
|
17
|
+
rafId = requestAnimationFrame(() => {
|
|
18
|
+
node.scrollTo({ top: node.scrollHeight, behavior: 'instant' });
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const onScroll = () => {
|
|
22
|
+
const atBottom = node.scrollTop + node.clientHeight >= node.scrollHeight - 8;
|
|
23
|
+
pinned = atBottom;
|
|
24
|
+
};
|
|
25
|
+
const observer = new ResizeObserver(scrollToBottom);
|
|
26
|
+
observer.observe(node);
|
|
27
|
+
// Content growing also needs re-pinning; observe children changes.
|
|
28
|
+
const contentObserver = new MutationObserver(scrollToBottom);
|
|
29
|
+
contentObserver.observe(node, { childList: true, subtree: true, characterData: true });
|
|
30
|
+
node.addEventListener('scroll', onScroll, { passive: true });
|
|
31
|
+
// Initial pin after the first layout pass.
|
|
32
|
+
const initialRaf = requestAnimationFrame(scrollToBottom);
|
|
33
|
+
return {
|
|
34
|
+
destroy() {
|
|
35
|
+
if (rafId !== null)
|
|
36
|
+
cancelAnimationFrame(rafId);
|
|
37
|
+
cancelAnimationFrame(initialRaf);
|
|
38
|
+
observer.disconnect();
|
|
39
|
+
contentObserver.disconnect();
|
|
40
|
+
node.removeEventListener('scroll', onScroll);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|