@widgetic/creator 0.3.63 → 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 +282 -41
- package/dist/components/VersionDropdownRow.svelte +5 -3
- package/dist/components/WidgetDetails.svelte +18 -2
- package/dist/components/WidgetDetails.svelte.d.ts +2 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +100 -35
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +5 -0
- package/dist/stores/websocketStore.js +26 -4
- 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
|
|
|
@@ -494,7 +496,10 @@
|
|
|
494
496
|
let WidgeticCanvas: any;
|
|
495
497
|
let canvasRef: any;
|
|
496
498
|
/** HTML iframe overlays over Fabric WidgetShapes (W3). */
|
|
497
|
-
let widgetShapeLivePreviewsRef: {
|
|
499
|
+
let widgetShapeLivePreviewsRef: {
|
|
500
|
+
sync?: () => void;
|
|
501
|
+
requestReload?: (widgetId: string) => void;
|
|
502
|
+
} | null = null;
|
|
498
503
|
/** Selected WidgetShape id for chrome highlight (from Fabric selection). */
|
|
499
504
|
let canvasSelectedWidgetShapeId: string | null = null;
|
|
500
505
|
let widgetDetailsModulePromise: Promise<any> | null = null;
|
|
@@ -515,8 +520,40 @@
|
|
|
515
520
|
const canvasModule = await canvasModulePromise;
|
|
516
521
|
WidgeticCanvas = canvasModule.Canvas;
|
|
517
522
|
}
|
|
523
|
+
installLongTaskFreezeReporter();
|
|
524
|
+
installFreezeWatchdog();
|
|
518
525
|
});
|
|
519
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
|
+
|
|
520
557
|
// Trigger WidgetDetails load as soon as a panel is opened or requested
|
|
521
558
|
$: if (openWidgetPanelIds.length > 0 && !WidgetDetails) {
|
|
522
559
|
void loadWidgetDetailsModule();
|
|
@@ -794,6 +831,9 @@
|
|
|
794
831
|
let codeGenerationPrompt = DEFAULT_PROMPT;
|
|
795
832
|
let isGeneratingCode = false;
|
|
796
833
|
let generatingWidgetIds: Record<string, boolean> = {};
|
|
834
|
+
function isAnyWidgetGenerating(): boolean {
|
|
835
|
+
return isGeneratingCode || Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id]);
|
|
836
|
+
}
|
|
797
837
|
const widgetAbortControllers = new Map<string, AbortController>();
|
|
798
838
|
let currentGenerationOperationId: string | null = null;
|
|
799
839
|
let generateCodeStatus: string | null = null;
|
|
@@ -1575,6 +1615,9 @@
|
|
|
1575
1615
|
iframe: HTMLIFrameElement,
|
|
1576
1616
|
timeoutMs = 6000,
|
|
1577
1617
|
): Promise<{ data: string | null; error: string | null }> {
|
|
1618
|
+
if (isAnyWidgetGenerating()) {
|
|
1619
|
+
return Promise.resolve({ data: null, error: 'generation in progress' });
|
|
1620
|
+
}
|
|
1578
1621
|
return new Promise((resolve) => {
|
|
1579
1622
|
if (!iframe.contentWindow) {
|
|
1580
1623
|
resolve({ data: null, error: 'no-content-window' });
|
|
@@ -1654,19 +1697,20 @@
|
|
|
1654
1697
|
|
|
1655
1698
|
/** Explicit Refresh only: capture the loaded frame without opening a panel or publishing. */
|
|
1656
1699
|
async function handleWidgetShapeRefresh(widgetId, iframe, isCurrentFrame) {
|
|
1700
|
+
if (isAnyWidgetGenerating()) return;
|
|
1657
1701
|
const capturedCanvasId = getEffectiveCanvasId();
|
|
1658
1702
|
const capturedCanvas = canvasRef;
|
|
1659
1703
|
const capturedSource = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
1660
1704
|
// Frame/intent identity owns this capture, not focused-panel HEAD hydration
|
|
1661
1705
|
// or another widget's global generation operation.
|
|
1662
1706
|
const isCurrent = () => isCurrentFrame() && !!getWidgetById(widgetId)
|
|
1663
|
-
&& !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId]
|
|
1707
|
+
&& !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId] && !isAnyWidgetGenerating()
|
|
1664
1708
|
&& capturedCanvasId === getEffectiveCanvasId() && capturedCanvas === canvasRef
|
|
1665
1709
|
&& capturedSource === (shapePreviewSourceByWidgetId[widgetId] || 'live')
|
|
1666
1710
|
&& resolveLiveWidgetShapeIframe(widgetId) === iframe;
|
|
1667
1711
|
if (!isCurrent()) return;
|
|
1668
1712
|
try {
|
|
1669
|
-
const screenshot = await handleChatScreenshot(widgetId, iframe);
|
|
1713
|
+
const screenshot = await handleChatScreenshot(widgetId, iframe, true);
|
|
1670
1714
|
if (!screenshot || !isCurrent()) return;
|
|
1671
1715
|
// Persist before Fabric/autosave side effects can resync the live overlay.
|
|
1672
1716
|
await persistWidgetListThumbnail(widgetId, screenshot, isCurrent);
|
|
@@ -1680,13 +1724,34 @@
|
|
|
1680
1724
|
}
|
|
1681
1725
|
|
|
1682
1726
|
/** Screenshot: capture the live WidgetShape iframe (CDN or Worker), not Fabric wireframe. */
|
|
1683
|
-
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> {
|
|
1684
1728
|
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1685
1729
|
if (!targetId) {
|
|
1686
1730
|
console.warn('[Chat→Screenshot] No widget id');
|
|
1687
1731
|
return null;
|
|
1688
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
|
+
}
|
|
1689
1753
|
|
|
1754
|
+
async function performChatScreenshot(targetId: string, requiredIframe: HTMLIFrameElement | null, quiet = false): Promise<string | null> {
|
|
1690
1755
|
const liveIframe = requiredIframe ?? resolveLiveWidgetShapeIframe(targetId);
|
|
1691
1756
|
if (requiredIframe && (requiredIframe !== resolveLiveWidgetShapeIframe(targetId) || !requiredIframe.isConnected)) return null;
|
|
1692
1757
|
const liveSrc = liveIframe
|
|
@@ -1709,6 +1774,8 @@
|
|
|
1709
1774
|
|
|
1710
1775
|
// Explicit refresh must not persist a different version or a stale Fabric image.
|
|
1711
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;
|
|
1712
1779
|
|
|
1713
1780
|
// 2) Hidden Worker /preview (same capture script; wait for load)
|
|
1714
1781
|
const fromWorker = await captureViaHiddenWorkerPreview(targetId);
|
|
@@ -1734,7 +1801,6 @@
|
|
|
1734
1801
|
});
|
|
1735
1802
|
return null;
|
|
1736
1803
|
}
|
|
1737
|
-
|
|
1738
1804
|
function normalizeConversationRecords(json: any): any[] {
|
|
1739
1805
|
const raw = json?.data ?? json?.conversations ?? json;
|
|
1740
1806
|
const list = Array.isArray(raw)
|
|
@@ -7616,12 +7682,12 @@
|
|
|
7616
7682
|
canvasShapePreviewSyncTimers.clear();
|
|
7617
7683
|
}
|
|
7618
7684
|
function scheduleCanvasShapePreviewSync(widgetId: string, delayMs = 1800): void {
|
|
7619
|
-
if (!widgetId) return;
|
|
7685
|
+
if (!widgetId || isAnyWidgetGenerating()) return;
|
|
7620
7686
|
const existing = canvasShapePreviewSyncTimers.get(widgetId);
|
|
7621
7687
|
if (existing) clearTimeout(existing);
|
|
7622
7688
|
const timer = setTimeout(() => {
|
|
7623
7689
|
canvasShapePreviewSyncTimers.delete(widgetId);
|
|
7624
|
-
if (generatingWidgetIds[widgetId]) {
|
|
7690
|
+
if (isAnyWidgetGenerating() || generatingWidgetIds[widgetId]) {
|
|
7625
7691
|
// console.log('[CreatorApp] Skip shape preview sync — codegen in progress:', widgetId.substring(0, 8));
|
|
7626
7692
|
return;
|
|
7627
7693
|
}
|
|
@@ -7640,6 +7706,24 @@
|
|
|
7640
7706
|
const quiet = !!options.quiet;
|
|
7641
7707
|
// console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
|
|
7642
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 {
|
|
7643
7727
|
if (isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
|
|
7644
7728
|
// console.log('[handleRefreshScreenshot] Skip iframe capture — convert/draft panel is open:', widgetId.substring(0, 8));
|
|
7645
7729
|
return;
|
|
@@ -7670,7 +7754,7 @@
|
|
|
7670
7754
|
if (!shapeThumbnailCaptureInFlight.has(widgetId)) {
|
|
7671
7755
|
shapeThumbnailCaptureInFlight.add(widgetId);
|
|
7672
7756
|
try {
|
|
7673
|
-
const freshCapture = await handleChatScreenshot(widgetId);
|
|
7757
|
+
const freshCapture = await handleChatScreenshot(widgetId, null, quiet);
|
|
7674
7758
|
if (freshCapture) {
|
|
7675
7759
|
canvasRef?.updateWidgetImage(widgetId, freshCapture);
|
|
7676
7760
|
const freshJson = canvasRef?.saveCanvas?.();
|
|
@@ -7710,8 +7794,7 @@
|
|
|
7710
7794
|
showToast('info', 'Capturing widget screenshot...', { duration: 3000 });
|
|
7711
7795
|
}
|
|
7712
7796
|
|
|
7713
|
-
|
|
7714
|
-
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) => {
|
|
7715
7798
|
const timeout = setTimeout(() => {
|
|
7716
7799
|
window.removeEventListener('message', handler);
|
|
7717
7800
|
resolve({ data: null, error: 'timeout' });
|
|
@@ -7762,6 +7845,8 @@
|
|
|
7762
7845
|
if (!quiet) {
|
|
7763
7846
|
showToast('error', 'Screenshot capture failed', { duration: 3000 });
|
|
7764
7847
|
}
|
|
7848
|
+
} finally {
|
|
7849
|
+
screenshotCaptureInFlight.delete(widgetId);
|
|
7765
7850
|
}
|
|
7766
7851
|
}
|
|
7767
7852
|
|
|
@@ -7968,6 +8053,7 @@ Requirements:
|
|
|
7968
8053
|
|
|
7969
8054
|
const panelChat = getChatMethodsForWidget(generationWidgetId);
|
|
7970
8055
|
const panelSnap = getPanelState(generationWidgetId);
|
|
8056
|
+
const hadCodeBeforeGeneration = !!(panelSnap?.hasGeneratedCode || panelSnap?.lastCommitId);
|
|
7971
8057
|
|
|
7972
8058
|
// Per-widget abort: only abort if the SAME widget already has a running generation
|
|
7973
8059
|
const existingController = widgetAbortControllers.get(generationWidgetId);
|
|
@@ -8230,6 +8316,11 @@ Requirements:
|
|
|
8230
8316
|
const responseData = response?.data || response;
|
|
8231
8317
|
const operationId = responseData?.operationId;
|
|
8232
8318
|
|
|
8319
|
+
// The Worker serves rebuilt code at the same preview URL, so the panel
|
|
8320
|
+
// must know when the current generation started — hasLiveWorkerPreview()
|
|
8321
|
+
// only counts frames loaded after this moment as "fresh".
|
|
8322
|
+
getWidgetDetailsRef(generationWidgetId)?.markCodegenStarted?.();
|
|
8323
|
+
|
|
8233
8324
|
if (!operationId) {
|
|
8234
8325
|
patchCodegenUiState(generationWidgetId, {
|
|
8235
8326
|
generateCodeStatus: '❌ Code generation started, but no operation ID was returned.',
|
|
@@ -8343,18 +8434,25 @@ Requirements:
|
|
|
8343
8434
|
// Refresh version history after every generation so the new commit shows
|
|
8344
8435
|
// up in the version menu immediately. Publishing is deliberately not
|
|
8345
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.
|
|
8346
8440
|
if (generationWidgetId) {
|
|
8347
8441
|
const { [generationWidgetId]: _resolved, ...restResolved } = shapeHistoryResolvedByWidgetId;
|
|
8348
8442
|
shapeHistoryResolvedByWidgetId = restResolved;
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8443
|
+
setTimeout(() => {
|
|
8444
|
+
void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
|
|
8445
|
+
syncWidgetShapeLivePreviews();
|
|
8446
|
+
if (isFocusedPanel) panelDisplayRevision += 1;
|
|
8447
|
+
});
|
|
8448
|
+
}, 1200);
|
|
8353
8449
|
}
|
|
8354
8450
|
|
|
8355
8451
|
// Always reconcile GitLab HEAD after codegen (metadata.commitId can be missing/stale).
|
|
8452
|
+
// Deferred — same window as the schema/history reloads above.
|
|
8356
8453
|
if (generationWidgetId) {
|
|
8357
|
-
|
|
8454
|
+
setTimeout(() => {
|
|
8455
|
+
void (async () => {
|
|
8358
8456
|
try {
|
|
8359
8457
|
const fetchedHead = await fetchRepoHeadCommitSha(generationWidgetId);
|
|
8360
8458
|
// Prefer the codegen commit from the operation. GitLab HEAD can still
|
|
@@ -8396,11 +8494,15 @@ Requirements:
|
|
|
8396
8494
|
} catch (err) {
|
|
8397
8495
|
console.warn('[Version] Failed to reconcile HEAD after codegen:', err);
|
|
8398
8496
|
}
|
|
8399
|
-
|
|
8497
|
+
})();
|
|
8498
|
+
}, 1000);
|
|
8400
8499
|
}
|
|
8401
8500
|
if (generationWidgetId) {
|
|
8402
8501
|
// console.log('[CompositionEditor] Reloading schemas after codegen', generationWidgetId.substring(0, 8));
|
|
8403
|
-
|
|
8502
|
+
// Deferred with the publish-history reload — see comment above.
|
|
8503
|
+
setTimeout(() => {
|
|
8504
|
+
void loadWidgetSchemas(generationWidgetId, { force: true });
|
|
8505
|
+
}, 1600);
|
|
8404
8506
|
}
|
|
8405
8507
|
|
|
8406
8508
|
const completedRepoId = resolveRepositoryIdForWidget(generationWidgetId);
|
|
@@ -8498,12 +8600,19 @@ Requirements:
|
|
|
8498
8600
|
await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
|
|
8499
8601
|
}
|
|
8500
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.
|
|
8501
8606
|
// Show Worker preview on the canvas shape (not the previous Live CDN artifact).
|
|
8502
8607
|
shapePreviewSourceByWidgetId = {
|
|
8503
8608
|
...shapePreviewSourceByWidgetId,
|
|
8504
8609
|
[generationWidgetId]: 'preview',
|
|
8505
8610
|
};
|
|
8506
8611
|
syncWidgetShapeLivePreviews();
|
|
8612
|
+
// The Worker serves the rebuilt code at the SAME /preview URL — sync()
|
|
8613
|
+
// only reloads when src changes, so force a cache-busted iframe reload
|
|
8614
|
+
// on the canvas shape overlay or it keeps showing the previous version.
|
|
8615
|
+
widgetShapeLivePreviewsRef?.requestReload?.(generationWidgetId);
|
|
8507
8616
|
panelDisplayRevision += 1;
|
|
8508
8617
|
if (!completedPreviewBuildErr) {
|
|
8509
8618
|
await applyDesignSchemaDefaultsToPreview(generationWidgetId);
|
|
@@ -8519,10 +8628,13 @@ Requirements:
|
|
|
8519
8628
|
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
8520
8629
|
}, 2500);
|
|
8521
8630
|
setTimeout(() => {
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
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
|
+
}
|
|
8526
8638
|
}, 3000);
|
|
8527
8639
|
// Do NOT auto-publish — user previews first, then clicks Publish / Set live.
|
|
8528
8640
|
} else {
|
|
@@ -8932,6 +9044,7 @@ Requirements:
|
|
|
8932
9044
|
widgetPublishStatus,
|
|
8933
9045
|
codeGenerationStatus,
|
|
8934
9046
|
agentHarnessOutput,
|
|
9047
|
+
dynamicWorkerPreviewStatus,
|
|
8935
9048
|
setAuthToken as setWsAuthToken,
|
|
8936
9049
|
setWebsocketApiUrl
|
|
8937
9050
|
} from './stores/websocketStore.js';
|
|
@@ -9318,10 +9431,46 @@ Requirements:
|
|
|
9318
9431
|
|
|
9319
9432
|
function pushCodegenProgressToUi(op: OperationStatus, widgetId: string | null | undefined) {
|
|
9320
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);
|
|
9437
|
+
// SSE progress frames can arrive many times per second while the LLM
|
|
9438
|
+
// streams. Each one re-renders every open panel — coalesce them.
|
|
9321
9439
|
if (widgetId) {
|
|
9322
|
-
|
|
9323
|
-
}
|
|
9324
|
-
|
|
9440
|
+
scheduleCodegenStatusPush(widgetId, line);
|
|
9441
|
+
}
|
|
9442
|
+
scheduleCodegenChatStatusPush(widgetId, line);
|
|
9443
|
+
}
|
|
9444
|
+
|
|
9445
|
+
const CODEGEN_STATUS_THROTTLE_MS = 400;
|
|
9446
|
+
const codegenStatusTimers: Record<string, ReturnType<typeof setTimeout>> = {};
|
|
9447
|
+
const codegenStatusPendingLine: Record<string, string> = {};
|
|
9448
|
+
const codegenChatStatusTimers: Record<string, ReturnType<typeof setTimeout>> = {};
|
|
9449
|
+
const codegenChatStatusPendingLine: Record<string, string> = {};
|
|
9450
|
+
|
|
9451
|
+
/** At most one panel status write per widget per interval; trailing line always lands. */
|
|
9452
|
+
function scheduleCodegenStatusPush(widgetId: string, line: string): void {
|
|
9453
|
+
codegenStatusPendingLine[widgetId] = line;
|
|
9454
|
+
if (codegenStatusTimers[widgetId]) return;
|
|
9455
|
+
codegenStatusTimers[widgetId] = setTimeout(() => {
|
|
9456
|
+
delete codegenStatusTimers[widgetId];
|
|
9457
|
+
const pending = codegenStatusPendingLine[widgetId];
|
|
9458
|
+
delete codegenStatusPendingLine[widgetId];
|
|
9459
|
+
if (pending) patchCodegenUiState(widgetId, { generateCodeStatus: pending });
|
|
9460
|
+
}, CODEGEN_STATUS_THROTTLE_MS);
|
|
9461
|
+
}
|
|
9462
|
+
|
|
9463
|
+
/** Same throttle for the in-chat status bubble (updateLatestCodegenStatusMessage). */
|
|
9464
|
+
function scheduleCodegenChatStatusPush(widgetId: string | null | undefined, line: string): void {
|
|
9465
|
+
const key = widgetId || '_focused';
|
|
9466
|
+
codegenChatStatusPendingLine[key] = line;
|
|
9467
|
+
if (codegenChatStatusTimers[key]) return;
|
|
9468
|
+
codegenChatStatusTimers[key] = setTimeout(() => {
|
|
9469
|
+
delete codegenChatStatusTimers[key];
|
|
9470
|
+
const pending = codegenChatStatusPendingLine[key];
|
|
9471
|
+
delete codegenChatStatusPendingLine[key];
|
|
9472
|
+
if (pending) getChatMethodsForWidget(widgetId)?.updateLatestCodegenStatusMessage?.(pending);
|
|
9473
|
+
}, CODEGEN_STATUS_THROTTLE_MS);
|
|
9325
9474
|
}
|
|
9326
9475
|
|
|
9327
9476
|
function patchCodegenUiState(
|
|
@@ -9875,6 +10024,26 @@ Requirements:
|
|
|
9875
10024
|
|
|
9876
10025
|
function getPanelEditPreviewUrl(widgetId: string): string | null {
|
|
9877
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
|
+
}
|
|
9878
10047
|
const widget = getWidgetById(widgetId);
|
|
9879
10048
|
const version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(widget);
|
|
9880
10049
|
if (version == null) return null;
|
|
@@ -9977,6 +10146,15 @@ Requirements:
|
|
|
9977
10146
|
return fromPanel?.length ? fromPanel : [];
|
|
9978
10147
|
}
|
|
9979
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
|
+
|
|
9980
10158
|
function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
|
|
9981
10159
|
if (!widgetId) return null;
|
|
9982
10160
|
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
@@ -10004,6 +10182,11 @@ Requirements:
|
|
|
10004
10182
|
const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
|
|
10005
10183
|
if (url) return url;
|
|
10006
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
|
+
}
|
|
10007
10190
|
return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
|
|
10008
10191
|
}
|
|
10009
10192
|
return null;
|
|
@@ -10029,6 +10212,13 @@ Requirements:
|
|
|
10029
10212
|
if (!Number.isFinite(version)) return null;
|
|
10030
10213
|
return `v${version}`;
|
|
10031
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
|
+
}
|
|
10032
10222
|
return liveVersion != null ? `v${liveVersion}` : null;
|
|
10033
10223
|
}
|
|
10034
10224
|
|
|
@@ -10115,6 +10305,13 @@ Requirements:
|
|
|
10115
10305
|
function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
|
|
10116
10306
|
const source = shapePreviewSourceByWidgetId[widgetId];
|
|
10117
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
|
+
}
|
|
10118
10315
|
const ps = getPanelDisplayState(widgetId);
|
|
10119
10316
|
const liveVersion =
|
|
10120
10317
|
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
@@ -10146,9 +10343,12 @@ Requirements:
|
|
|
10146
10343
|
// Published artifacts need no build. Drafts must never substitute HEAD.
|
|
10147
10344
|
if (versionId.startsWith('v:')) {
|
|
10148
10345
|
const version = Number(versionId.slice(2));
|
|
10149
|
-
|
|
10346
|
+
const directUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
|
|
10347
|
+
if (directUrl) {
|
|
10150
10348
|
const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
|
|
10151
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);
|
|
10152
10352
|
} else {
|
|
10153
10353
|
void ensureWidgetShapeVersionPreview(widgetId, version);
|
|
10154
10354
|
}
|
|
@@ -10199,6 +10399,8 @@ Requirements:
|
|
|
10199
10399
|
panelDisplayRevision += 1;
|
|
10200
10400
|
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${widgetId}:${versionId}`]: null };
|
|
10201
10401
|
console.log('[VersionPreview] Selection', { widgetId, version, commitSha });
|
|
10402
|
+
const detailsRef = getWidgetDetailsRef(widgetId);
|
|
10403
|
+
detailsRef?.showPublishedArtifact?.(previewUrl);
|
|
10202
10404
|
} catch (error: unknown) {
|
|
10203
10405
|
if (
|
|
10204
10406
|
shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
@@ -11684,6 +11886,17 @@ $: if ($agentHarnessOutput?.timestamp) {
|
|
|
11684
11886
|
}
|
|
11685
11887
|
}
|
|
11686
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
|
+
|
|
11687
11900
|
// ===================== Publish History =====================
|
|
11688
11901
|
|
|
11689
11902
|
/**
|
|
@@ -11848,7 +12061,9 @@ function togglePublishHistory(triggerEl?: HTMLElement | null) {
|
|
|
11848
12061
|
const cluster = triggerEl.closest('.wc-version-history-ct') as HTMLElement | null;
|
|
11849
12062
|
const rect = (cluster ?? triggerEl).getBoundingClientRect();
|
|
11850
12063
|
liveHistoryPanelTop = Math.round(rect.bottom + 4);
|
|
11851
|
-
|
|
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;
|
|
11852
12067
|
}
|
|
11853
12068
|
loadPublishHistory();
|
|
11854
12069
|
}
|
|
@@ -12340,6 +12555,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12340
12555
|
*/
|
|
12341
12556
|
const shapeThumbnailCaptureInFlight = new Set<string>();
|
|
12342
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
|
+
|
|
12343
12566
|
/**
|
|
12344
12567
|
* Called when a Widget Image shape is clicked on the canvas.
|
|
12345
12568
|
* Selects the linked widget and opens WidgetDetails.
|
|
@@ -12418,7 +12641,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12418
12641
|
notifyHostWidgetName(widget);
|
|
12419
12642
|
}
|
|
12420
12643
|
await openWidgetDetailsPanel(widgetId);
|
|
12421
|
-
scheduleCanvasShapePreviewSync(widgetId);
|
|
12422
12644
|
}
|
|
12423
12645
|
|
|
12424
12646
|
/**
|
|
@@ -14107,6 +14329,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14107
14329
|
onTranslateEnd={(widgetId) => {
|
|
14108
14330
|
canvasRef?.commitWidgetShapeTranslate?.(widgetId);
|
|
14109
14331
|
}}
|
|
14332
|
+
onOverlayDragChange={(active) => {
|
|
14333
|
+
canvasRef?.setWidgetShapeOverlayDragActive?.(active);
|
|
14334
|
+
}}
|
|
14110
14335
|
onSelectShape={(widgetId) => {
|
|
14111
14336
|
canvasRef?.selectWidgetShapeById?.(widgetId);
|
|
14112
14337
|
canvasSelectedWidgetShapeId = widgetId;
|
|
@@ -14116,6 +14341,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14116
14341
|
void openWidgetDetailsPanel(widgetId);
|
|
14117
14342
|
}}
|
|
14118
14343
|
onRefresh={handleWidgetShapeRefresh}
|
|
14344
|
+
isGenerating={isAnyWidgetGenerating()}
|
|
14119
14345
|
/>
|
|
14120
14346
|
|
|
14121
14347
|
<!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
|
|
@@ -14519,8 +14745,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14519
14745
|
if (isPanelFocused && previewWidgetId === panelWidgetId) {
|
|
14520
14746
|
chatMethods?.clearStalePreviewBuildFailures?.();
|
|
14521
14747
|
}
|
|
14522
|
-
// Keep canvas WidgetShape thumbnail aligned with the live Worker preview.
|
|
14523
|
-
scheduleCanvasShapePreviewSync(previewWidgetId);
|
|
14524
14748
|
if (detail?.step === 'create' || ps.widgetDetailsStep === 'create') {
|
|
14525
14749
|
applyPanelCompositionToPreview(previewWidgetId);
|
|
14526
14750
|
}
|
|
@@ -14755,7 +14979,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14755
14979
|
{@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
|
|
14756
14980
|
{@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
|
|
14757
14981
|
? parseInt(shapeSource.slice(2), 10)
|
|
14758
|
-
:
|
|
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}
|
|
14759
14989
|
{@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
|
|
14760
14990
|
<!--
|
|
14761
14991
|
A version is a Draft until it is published. The history is one
|
|
@@ -14793,13 +15023,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14793
15023
|
}}
|
|
14794
15024
|
disabled={isPublishDisabledForPanel(panelWidgetId) || (isPublishing && publishingWidgetId === panelWidgetId)}
|
|
14795
15025
|
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? 'Publish the latest generated code as the Live version'}
|
|
14796
|
-
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"
|
|
14797
15027
|
>
|
|
14798
15028
|
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14799
15029
|
</button>
|
|
14800
15030
|
{:else if viewingIsLive}
|
|
14801
15031
|
<span
|
|
14802
|
-
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"
|
|
14803
15033
|
title="Live — generate new code to Publish a new version"
|
|
14804
15034
|
>Live</span>
|
|
14805
15035
|
{:else if viewingIsDraft}
|
|
@@ -14815,7 +15045,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14815
15045
|
}}
|
|
14816
15046
|
disabled={!canPublishDraftVersionForPanel(panelWidgetId, viewingVersion) || getPublishGateForPanel(panelWidgetId, Number(viewingVersion)).disabled}
|
|
14817
15047
|
title="Build this draft version and make it the Live version"
|
|
14818
|
-
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"
|
|
14819
15049
|
>
|
|
14820
15050
|
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14821
15051
|
</button>
|
|
@@ -14832,7 +15062,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14832
15062
|
}}
|
|
14833
15063
|
disabled={isSettingLiveVersion === Number(viewingVersion)}
|
|
14834
15064
|
title="Publish this version so visitors see it on your website"
|
|
14835
|
-
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"
|
|
14836
15066
|
>
|
|
14837
15067
|
{isSettingLiveVersion === Number(viewingVersion) ? '...' : 'Set live'}
|
|
14838
15068
|
</button>
|
|
@@ -14845,15 +15075,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14845
15075
|
aria-label="Close version history"
|
|
14846
15076
|
onclick={() => { isHistoryOpen = false; }}
|
|
14847
15077
|
></button>
|
|
15078
|
+
{@const hasChatScroll = ps.publishHistory.length > 5}
|
|
14848
15079
|
<div
|
|
14849
|
-
class="wc-version-history-panel
|
|
14850
|
-
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;"
|
|
14851
15082
|
>
|
|
14852
15083
|
{#if isLoadingHistory}
|
|
14853
15084
|
<p class="text-xs text-emerald-500 animate-pulse text-center py-2">Loading history...</p>
|
|
14854
15085
|
{:else if ps.publishHistory.length === 0}
|
|
14855
15086
|
<p class="text-xs text-emerald-400 text-center py-2">No history records yet. Generate or publish to start tracking.</p>
|
|
14856
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
|
+
>
|
|
14857
15092
|
{#each ps.publishHistory as record (record.id)}
|
|
14858
15093
|
{@const rowVersion = Number(record.version)}
|
|
14859
15094
|
{@const rowIsDraft = !(record.publishedAt ?? record.published_at)}
|
|
@@ -14874,6 +15109,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14874
15109
|
publishDisabledReason={publishGate.disabled ? publishGate.reason : null}
|
|
14875
15110
|
setLiveDisabled={isSettingLiveVersion === record.version}
|
|
14876
15111
|
onselect={() => {
|
|
15112
|
+
isHistoryOpen = false;
|
|
14877
15113
|
handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
|
|
14878
15114
|
}}
|
|
14879
15115
|
onpublish={() => {
|
|
@@ -14885,6 +15121,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14885
15121
|
}}
|
|
14886
15122
|
/>
|
|
14887
15123
|
{/each}
|
|
15124
|
+
</div>
|
|
14888
15125
|
{/if}
|
|
14889
15126
|
</div>
|
|
14890
15127
|
</Portal>
|
|
@@ -14973,6 +15210,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14973
15210
|
class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
|
|
14974
15211
|
style="padding: 0;"
|
|
14975
15212
|
use:nativeScrollbarPadding={0}
|
|
15213
|
+
use:pinScrollToBottom
|
|
14976
15214
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
14977
15215
|
>
|
|
14978
15216
|
{#key panelInstanceKey}
|
|
@@ -15773,21 +16011,24 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15773
16011
|
outline: none;
|
|
15774
16012
|
box-shadow: none;
|
|
15775
16013
|
}
|
|
15776
|
-
.wc-version-history-
|
|
16014
|
+
.wc-version-history-scroll {
|
|
15777
16015
|
scrollbar-width: thin;
|
|
15778
16016
|
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
|
15779
16017
|
}
|
|
15780
|
-
.wc-version-history-
|
|
15781
|
-
width:
|
|
16018
|
+
.wc-version-history-scroll::-webkit-scrollbar {
|
|
16019
|
+
width: 4px;
|
|
15782
16020
|
background: transparent;
|
|
15783
16021
|
}
|
|
15784
|
-
.wc-version-history-
|
|
16022
|
+
.wc-version-history-scroll::-webkit-scrollbar-track {
|
|
15785
16023
|
background: transparent;
|
|
15786
16024
|
}
|
|
15787
|
-
.wc-version-history-
|
|
16025
|
+
.wc-version-history-scroll::-webkit-scrollbar-thumb {
|
|
15788
16026
|
background: rgba(0, 0, 0, 0.2);
|
|
15789
16027
|
border-radius: 4px;
|
|
15790
16028
|
}
|
|
16029
|
+
.wc-version-history-scroll::-webkit-scrollbar-thumb:hover {
|
|
16030
|
+
background: rgba(0, 0, 0, 0.35);
|
|
16031
|
+
}
|
|
15791
16032
|
.generation-scroll-ct {
|
|
15792
16033
|
scrollbar-width: thin;
|
|
15793
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"
|
|
@@ -801,7 +801,20 @@
|
|
|
801
801
|
// console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
|
|
802
802
|
}
|
|
803
803
|
|
|
804
|
-
/**
|
|
804
|
+
/** Timestamp of generation start — set by the parent when the operation begins. */
|
|
805
|
+
let codegenStartedAt: number | null = null;
|
|
806
|
+
/** Parent calls this when a generation starts for THIS widget's panel. */
|
|
807
|
+
export function markCodegenStarted(): void {
|
|
808
|
+
codegenStartedAt = Date.now();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* True when the iframe is already showing this widget's Worker preview AND was
|
|
813
|
+
* reloaded after the current generation started. The Worker serves new code at
|
|
814
|
+
* the same `/preview/{id}` URL, so URL equality alone cannot tell a stale frame
|
|
815
|
+
* (pre-codegen) from a fresh one — that mismatch left the old version on screen
|
|
816
|
+
* after generation.
|
|
817
|
+
*/
|
|
805
818
|
export function hasLiveWorkerPreview(): boolean {
|
|
806
819
|
if (!selectedWidgetId) return false;
|
|
807
820
|
if (!iframeSrc || iframeSrc === 'about:blank') return false;
|
|
@@ -809,7 +822,10 @@
|
|
|
809
822
|
const workerUrl = dynamicWorkerPreviewUrl || buildWorkerPreviewUrl(selectedWidgetId);
|
|
810
823
|
const currentBase = normalizePreviewBaseUrl(iframeSrc);
|
|
811
824
|
const workerBase = normalizePreviewBaseUrl(toPreviewFetchUrl(workerUrl));
|
|
812
|
-
|
|
825
|
+
if (!currentBase || !workerBase || currentBase !== workerBase) return false;
|
|
826
|
+
// During/after a generation, only a frame (re)loaded after it started counts.
|
|
827
|
+
if (codegenStartedAt !== null && lastPreviewIframeLoadAt < codegenStartedAt) return false;
|
|
828
|
+
return true;
|
|
813
829
|
}
|
|
814
830
|
|
|
815
831
|
// Preview resize
|
|
@@ -108,6 +108,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
|
|
|
108
108
|
showPublishedArtifact?: (url: string | null) => void;
|
|
109
109
|
finishCompositionHydration?: () => void;
|
|
110
110
|
markCodegenPreviewFresh?: () => void;
|
|
111
|
+
markCodegenStarted?: () => void;
|
|
111
112
|
hasLiveWorkerPreview?: () => boolean;
|
|
112
113
|
getLatestRuntimeLogs?: (maxLines?: number) => string[];
|
|
113
114
|
resumeDeferredPreviewLoad?: () => Promise<void>;
|
|
@@ -204,6 +205,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
|
|
|
204
205
|
showPublishedArtifact: (url: string | null) => void;
|
|
205
206
|
finishCompositionHydration: () => void;
|
|
206
207
|
markCodegenPreviewFresh: () => void;
|
|
208
|
+
markCodegenStarted: () => void;
|
|
207
209
|
hasLiveWorkerPreview: () => boolean;
|
|
208
210
|
getLatestRuntimeLogs: (maxLines?: number) => string[];
|
|
209
211
|
resumeDeferredPreviewLoad: () => Promise<void>;
|
|
@@ -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;
|
|
@@ -171,8 +181,33 @@
|
|
|
171
181
|
}
|
|
172
182
|
pendingRefreshTokens.delete(widgetId);
|
|
173
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Parent-driven forced reload for widgets whose Worker preview content changed
|
|
187
|
+
* at the same URL (codegen rebuild). `sync()` only reloads when `src` changes,
|
|
188
|
+
* so without a cache-bust token the overlay keeps showing pre-codegen content.
|
|
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.
|
|
191
|
+
*/
|
|
192
|
+
let lastReloadAtByWidgetId: Record<string, number> = {};
|
|
193
|
+
export function requestReload(widgetId: string): void {
|
|
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);
|
|
202
|
+
reloadTokens = { ...reloadTokens, [widgetId]: token };
|
|
203
|
+
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
|
|
204
|
+
sync();
|
|
205
|
+
}
|
|
206
|
+
|
|
174
207
|
/** Which shape chrome version menu is open. */
|
|
175
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>>({});
|
|
176
211
|
/** Hide Fabric placeholder flash — show loader until iframe fires load. */
|
|
177
212
|
let iframeLoadedByWidgetId = $state<Record<string, boolean>>({});
|
|
178
213
|
|
|
@@ -250,11 +285,13 @@
|
|
|
250
285
|
document.body.style.removeProperty('user-select');
|
|
251
286
|
document.body.style.removeProperty('cursor');
|
|
252
287
|
onTranslateEnd?.(widgetId);
|
|
288
|
+
onOverlayDragChange?.(false);
|
|
253
289
|
console.log('[WidgetShapeLivePreviews] Drag end', widgetId.substring(0, 8));
|
|
254
290
|
}
|
|
255
291
|
|
|
256
292
|
document.body.style.userSelect = 'none';
|
|
257
293
|
document.body.style.cursor = 'grabbing';
|
|
294
|
+
onOverlayDragChange?.(true);
|
|
258
295
|
document.addEventListener('pointermove', onMove);
|
|
259
296
|
document.addEventListener('pointerup', endDrag);
|
|
260
297
|
document.addEventListener('pointercancel', endDrag);
|
|
@@ -290,6 +327,11 @@
|
|
|
290
327
|
// An old load event must not mark the replacement loaded or claim its timer.
|
|
291
328
|
if (!matchesFrame()) return;
|
|
292
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
|
+
}
|
|
293
335
|
const token = pendingRefreshTokens.get(widgetId);
|
|
294
336
|
if (!token || refreshSettleTimers.has(widgetId)) return;
|
|
295
337
|
const isCurrent = () => matchesFrame()
|
|
@@ -319,6 +361,8 @@
|
|
|
319
361
|
}
|
|
320
362
|
|
|
321
363
|
function resolveSelectedVersionId(widgetId: string, options: VersionOption[]): string {
|
|
364
|
+
const local = localSelectedVersionByWidgetId[widgetId];
|
|
365
|
+
if (local && options.some((o) => o.id === local)) return local;
|
|
322
366
|
const fromParent = resolveVersionSelectedId?.(widgetId)?.trim();
|
|
323
367
|
if (fromParent && options.some((o) => o.id === fromParent)) return fromParent;
|
|
324
368
|
const liveOpt = options.find((o) => o.isLive);
|
|
@@ -391,6 +435,7 @@
|
|
|
391
435
|
function selectVersionFromMenu(widgetId: string, versionId: string) {
|
|
392
436
|
openVersionMenuWidgetId = null;
|
|
393
437
|
if (!versionId) return;
|
|
438
|
+
localSelectedVersionByWidgetId = { ...localSelectedVersionByWidgetId, [widgetId]: versionId };
|
|
394
439
|
onVersionChange?.(widgetId, versionId);
|
|
395
440
|
}
|
|
396
441
|
|
|
@@ -425,8 +470,15 @@
|
|
|
425
470
|
*/
|
|
426
471
|
function trackIframeSelection(iframe, widgetId) {
|
|
427
472
|
let focusTimer;
|
|
473
|
+
let pointerIsDown = false;
|
|
474
|
+
const onWindowPointerDown = () => { pointerIsDown = true; };
|
|
475
|
+
const onWindowPointerUp = () => { pointerIsDown = false; };
|
|
428
476
|
function select() {
|
|
429
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;
|
|
430
482
|
// Keep the menu owner selected while its dropdown is being used.
|
|
431
483
|
if (openVersionMenuWidgetId && openVersionMenuWidgetId !== widgetId) return;
|
|
432
484
|
if (selectedWidgetId !== widgetId) onSelectShape?.(widgetId);
|
|
@@ -437,6 +489,8 @@
|
|
|
437
489
|
if (document.activeElement === iframe) select();
|
|
438
490
|
}, 0);
|
|
439
491
|
}
|
|
492
|
+
window.addEventListener('pointerdown', onWindowPointerDown, true);
|
|
493
|
+
window.addEventListener('pointerup', onWindowPointerUp, true);
|
|
440
494
|
iframe.addEventListener('pointerenter', select);
|
|
441
495
|
iframe.addEventListener('focus', select);
|
|
442
496
|
window.addEventListener('blur', checkFocus);
|
|
@@ -444,6 +498,8 @@
|
|
|
444
498
|
update(nextWidgetId) { widgetId = nextWidgetId; },
|
|
445
499
|
destroy() {
|
|
446
500
|
clearTimeout(focusTimer);
|
|
501
|
+
window.removeEventListener('pointerdown', onWindowPointerDown, true);
|
|
502
|
+
window.removeEventListener('pointerup', onWindowPointerUp, true);
|
|
447
503
|
iframe.removeEventListener('pointerenter', select);
|
|
448
504
|
iframe.removeEventListener('focus', select);
|
|
449
505
|
window.removeEventListener('blur', checkFocus);
|
|
@@ -591,7 +647,7 @@
|
|
|
591
647
|
>
|
|
592
648
|
<button
|
|
593
649
|
type="button"
|
|
594
|
-
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"
|
|
595
651
|
disabled={isPublishing || selectedPreviewBlocked || !!resolvePublishDisabled?.(overlay.widgetId, selectedVersion ? Number(selectedVersion.id.slice(2)) : null)}
|
|
596
652
|
onclick={(e) => {
|
|
597
653
|
e.stopPropagation();
|
|
@@ -607,7 +663,7 @@
|
|
|
607
663
|
class="inline-flex shrink-0"
|
|
608
664
|
>
|
|
609
665
|
<span
|
|
610
|
-
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"
|
|
611
667
|
>Live</span>
|
|
612
668
|
</HoverTooltip>
|
|
613
669
|
{:else if selectedVersion && onSetLive && !isDraftVersion(selectedVersion)}
|
|
@@ -618,7 +674,7 @@
|
|
|
618
674
|
>
|
|
619
675
|
<button
|
|
620
676
|
type="button"
|
|
621
|
-
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"
|
|
622
678
|
disabled={settingLiveVersion === Number(selectedVersion.id.slice(2))}
|
|
623
679
|
onclick={(e) => handleSetLiveClick(e, overlay.widgetId, selectedVersion.id)}
|
|
624
680
|
onpointerdown={(e) => e.stopPropagation()}
|
|
@@ -633,7 +689,7 @@
|
|
|
633
689
|
>
|
|
634
690
|
<button
|
|
635
691
|
type="button"
|
|
636
|
-
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"
|
|
637
693
|
disabled={isPublishing || selectedPreviewBlocked || !resolveCanPublishDraft?.(overlay.widgetId, Number(selectedVersion.id.slice(2))) || !!resolvePublishDisabled?.(overlay.widgetId, Number(selectedVersion.id.slice(2)))}
|
|
638
694
|
onclick={(e) => handlePublishDraftClick(e, overlay.widgetId, selectedVersion.id)}
|
|
639
695
|
onpointerdown={(e) => e.stopPropagation()}
|
|
@@ -646,39 +702,45 @@
|
|
|
646
702
|
class="inline-flex shrink-0"
|
|
647
703
|
>
|
|
648
704
|
<span
|
|
649
|
-
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"
|
|
650
706
|
>Draft</span>
|
|
651
707
|
</HoverTooltip>
|
|
652
708
|
{/if}
|
|
653
709
|
{/if}
|
|
654
710
|
{#if versionMenuOpen}
|
|
711
|
+
{@const hasScroll = versionOptions.length > 5}
|
|
655
712
|
<div
|
|
656
|
-
class="widget-shape-version-menu absolute
|
|
657
|
-
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;"
|
|
658
715
|
role="listbox"
|
|
659
716
|
>
|
|
660
|
-
|
|
661
|
-
{
|
|
662
|
-
{
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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>
|
|
682
744
|
</div>
|
|
683
745
|
{/if}
|
|
684
746
|
</div>
|
|
@@ -799,16 +861,19 @@
|
|
|
799
861
|
{/if}
|
|
800
862
|
|
|
801
863
|
<style>
|
|
802
|
-
.widget-shape-version-
|
|
803
|
-
width:
|
|
804
|
-
height:
|
|
864
|
+
.widget-shape-version-scroll::-webkit-scrollbar {
|
|
865
|
+
width: 4px;
|
|
866
|
+
height: 4px;
|
|
805
867
|
background: transparent;
|
|
806
868
|
}
|
|
807
|
-
.widget-shape-version-
|
|
869
|
+
.widget-shape-version-scroll::-webkit-scrollbar-track {
|
|
808
870
|
background: transparent;
|
|
809
871
|
}
|
|
810
|
-
.widget-shape-version-
|
|
872
|
+
.widget-shape-version-scroll::-webkit-scrollbar-thumb {
|
|
811
873
|
background: rgba(0, 0, 0, 0.2);
|
|
812
874
|
border-radius: 4px;
|
|
813
875
|
}
|
|
876
|
+
.widget-shape-version-scroll::-webkit-scrollbar-thumb:hover {
|
|
877
|
+
background: rgba(0, 0, 0, 0.35);
|
|
878
|
+
}
|
|
814
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,8 +90,11 @@ 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, {
|
|
97
|
+
requestReload: (widgetId: string) => void;
|
|
93
98
|
sync: () => void;
|
|
94
99
|
}, "">;
|
|
95
100
|
type WidgetShapeLivePreviews = ReturnType<typeof WidgetShapeLivePreviews>;
|
|
@@ -39,6 +39,30 @@ export const codeGenerationStatus = writable({
|
|
|
39
39
|
timestamp: null
|
|
40
40
|
});
|
|
41
41
|
export const agentHarnessOutput = writable(null);
|
|
42
|
+
/**
|
|
43
|
+
* Throttle for high-frequency agent harness frames. The stream used to emit one
|
|
44
|
+
* WS event per LLM token (plus one SSE progress frame every 10 tokens), and each
|
|
45
|
+
* frame re-rendered every open panel — with `structuredClone` on schemas per
|
|
46
|
+
* render. That saturated the browser main thread and froze the whole site during
|
|
47
|
+
* generation. Frames now coalesce into one store write per interval; the final
|
|
48
|
+
* frame always flushes so the last line is never lost.
|
|
49
|
+
*/
|
|
50
|
+
const AGENT_HARNESS_FLUSH_MS = 300;
|
|
51
|
+
let agentHarnessPending = null;
|
|
52
|
+
let agentHarnessFlushTimer = null;
|
|
53
|
+
function flushAgentHarnessOutput() {
|
|
54
|
+
agentHarnessFlushTimer = null;
|
|
55
|
+
if (!agentHarnessPending)
|
|
56
|
+
return;
|
|
57
|
+
agentHarnessOutput.set(agentHarnessPending);
|
|
58
|
+
agentHarnessPending = null;
|
|
59
|
+
}
|
|
60
|
+
function pushAgentHarnessOutput(output) {
|
|
61
|
+
agentHarnessPending = { output, timestamp: new Date().toISOString() };
|
|
62
|
+
if (!agentHarnessFlushTimer) {
|
|
63
|
+
agentHarnessFlushTimer = setTimeout(flushAgentHarnessOutput, AGENT_HARNESS_FLUSH_MS);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
42
66
|
// =============================================================================
|
|
43
67
|
// Socket.IO Connection
|
|
44
68
|
// =============================================================================
|
|
@@ -204,10 +228,8 @@ function setupSocketListeners(sock) {
|
|
|
204
228
|
return;
|
|
205
229
|
if (showLogs)
|
|
206
230
|
console.log('[WsStore] Agent harness output:', output.slice(0, 120));
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
timestamp: new Date().toISOString()
|
|
210
|
-
});
|
|
231
|
+
// Coalesced write — see AGENT_HARNESS_FLUSH_MS above.
|
|
232
|
+
pushAgentHarnessOutput(output);
|
|
211
233
|
});
|
|
212
234
|
sock.on('dynamic_worker_preview_ready', (data) => {
|
|
213
235
|
if (showLogs)
|
|
@@ -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
|
+
}
|