@widgetic/creator 0.3.64 → 0.3.66
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/README.md +15 -0
- package/dist/CreatorApp.svelte +650 -176
- package/dist/CreatorApp.svelte.d.ts +3 -1
- package/dist/components/VersionDropdownRow.svelte +5 -3
- package/dist/components/WidgetDetails.svelte +34 -14
- package/dist/components/WidgetDetails.svelte.d.ts +3 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +114 -36
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +4 -0
- package/dist/utils/freezeWatchdog.d.ts +38 -0
- package/dist/utils/freezeWatchdog.js +161 -0
- package/dist/utils/persistWidgetThumbnail.d.ts +12 -1
- package/dist/utils/persistWidgetThumbnail.js +14 -7
- package/dist/utils/pinScrollToBottom.d.ts +12 -0
- package/dist/utils/pinScrollToBottom.js +43 -0
- package/package.json +4 -2
package/dist/CreatorApp.svelte
CHANGED
|
@@ -27,7 +27,12 @@
|
|
|
27
27
|
UploadsApi,
|
|
28
28
|
ResponseError
|
|
29
29
|
} from '@widgetic/api-sdk';
|
|
30
|
-
import type {
|
|
30
|
+
import type {
|
|
31
|
+
Middleware,
|
|
32
|
+
UpdateComposition,
|
|
33
|
+
GenerateCodeRequest,
|
|
34
|
+
AgentModelsGet200ResponseModelsInner
|
|
35
|
+
} from '@widgetic/api-sdk';
|
|
31
36
|
import { parseVersionPreviewError } from './utils/versionPreviewError.js';
|
|
32
37
|
|
|
33
38
|
/**
|
|
@@ -70,6 +75,8 @@
|
|
|
70
75
|
import { uploadChatFileToWidgeticApi } from './utils/widgeticChatUpload.js';
|
|
71
76
|
import { persistWidgetThumbnail } from './utils/persistWidgetThumbnail.js';
|
|
72
77
|
import { nativeScrollbarPadding } from './utils/nativeScrollbarPadding.js';
|
|
78
|
+
import { pinScrollToBottom } from './utils/pinScrollToBottom.js';
|
|
79
|
+
import { installFreezeWatchdog, measureSync } from './utils/freezeWatchdog.js';
|
|
73
80
|
import { streamOperationUpdates } from './utils/operationStream.js';
|
|
74
81
|
import { generateEmbedCode, EMBED_FORMATS, EMBED_FORMAT_LABELS, type EmbedFormat } from './utils/embedCode.js';
|
|
75
82
|
|
|
@@ -111,7 +118,9 @@
|
|
|
111
118
|
/** Opt-in Dexie L1 — host (site / standalone) passes true. */
|
|
112
119
|
export let enableLocalCache: CreatorAppProps['enableLocalCache'] = false;
|
|
113
120
|
export let onPublishComplete: CreatorAppProps['onPublishComplete'] = undefined;
|
|
114
|
-
|
|
121
|
+
// Passed by the site host but never read here — export const keeps the
|
|
122
|
+
// init-set contract without the unused-reactive warning.
|
|
123
|
+
export const onBack: CreatorAppProps['onBack'] = undefined;
|
|
115
124
|
export let onWidgetRenamed: CreatorAppProps['onWidgetRenamed'] = undefined;
|
|
116
125
|
|
|
117
126
|
// Opt-in Dexie owner — reactive to prop changes without $effect (would force runes mode).
|
|
@@ -119,7 +128,8 @@
|
|
|
119
128
|
|
|
120
129
|
function getRealTokenExpUnix(token: string): number | null {
|
|
121
130
|
const decoded = decodeJWT(token);
|
|
122
|
-
|
|
131
|
+
const exp = decoded?.exp;
|
|
132
|
+
return typeof exp === 'number' && Number.isFinite(exp) ? exp : null;
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
let didForceCreateNewWidget = false;
|
|
@@ -483,7 +493,10 @@
|
|
|
483
493
|
|
|
484
494
|
// Lightweight static imports from canvas (excludes Canvas.svelte — 10K+ lines)
|
|
485
495
|
import { Tooltip } from '@widgetic/canvas/components';
|
|
486
|
-
|
|
496
|
+
// WidgetConversionPayload lives on the canvas root export (re-exported from
|
|
497
|
+
// ./canvas); the components barrel only carries the input controls. Type-only
|
|
498
|
+
// import, so the 10K-line Canvas.svelte still never loads.
|
|
499
|
+
import type { WidgetConversionPayload } from '@widgetic/canvas';
|
|
487
500
|
|
|
488
501
|
// Chat module for code generation conversation UI
|
|
489
502
|
import { Chat, ContextType } from '@widgetic/chat';
|
|
@@ -518,8 +531,40 @@
|
|
|
518
531
|
const canvasModule = await canvasModulePromise;
|
|
519
532
|
WidgeticCanvas = canvasModule.Canvas;
|
|
520
533
|
}
|
|
534
|
+
installLongTaskFreezeReporter();
|
|
535
|
+
installFreezeWatchdog();
|
|
521
536
|
});
|
|
522
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Freeze attribution: while a code generation is tracked, any main-thread
|
|
540
|
+
* block ≥ LONG_TASK_FREEZE_MS is reported with its most likely culprit (the
|
|
541
|
+
* last status line + open panels). This gives a concrete answer to "why does
|
|
542
|
+
* the site freeze during generation" instead of a silent stuck progress bar.
|
|
543
|
+
*/
|
|
544
|
+
const LONG_TASK_FREEZE_MS = 250;
|
|
545
|
+
let freezeObserver: PerformanceObserver | null = null;
|
|
546
|
+
function installLongTaskFreezeReporter(): void {
|
|
547
|
+
if (typeof window === 'undefined' || !('PerformanceObserver' in window)) return;
|
|
548
|
+
if (freezeObserver) return;
|
|
549
|
+
try {
|
|
550
|
+
freezeObserver = new PerformanceObserver((list) => {
|
|
551
|
+
if (!Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id])) return;
|
|
552
|
+
for (const entry of list.getEntries()) {
|
|
553
|
+
if (entry.duration < LONG_TASK_FREEZE_MS) continue;
|
|
554
|
+
console.warn(
|
|
555
|
+
`[FreezeReport] Main thread blocked ${Math.round(entry.duration)}ms during generation.`
|
|
556
|
+
+ ` Status: "${generateCodeStatus ?? '—'}".`
|
|
557
|
+
+ ` Open panels: ${openWidgetPanelIds.length}.`
|
|
558
|
+
+ ` Start: ${Math.round(entry.startTime)}ms after nav.`,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
freezeObserver.observe({ entryTypes: ['longtask'] });
|
|
563
|
+
} catch (err) {
|
|
564
|
+
console.warn('[FreezeReport] longtask observer unavailable', err);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
523
568
|
// Trigger WidgetDetails load as soon as a panel is opened or requested
|
|
524
569
|
$: if (openWidgetPanelIds.length > 0 && !WidgetDetails) {
|
|
525
570
|
void loadWidgetDetailsModule();
|
|
@@ -601,10 +646,31 @@
|
|
|
601
646
|
repositoryId?: string | null;
|
|
602
647
|
repository_id?: string | null;
|
|
603
648
|
repoId?: string | null;
|
|
649
|
+
repo_id?: string | null;
|
|
604
650
|
name?: string;
|
|
605
651
|
[extra: string]: unknown;
|
|
606
652
|
}
|
|
607
653
|
|
|
654
|
+
/**
|
|
655
|
+
* View any API payload as the app's open WidgetSummary shape. The SDK's
|
|
656
|
+
* generated models (Widget, CreateWidget201Response, …) lack the index
|
|
657
|
+
* signature callers rely on for snake_case/extra fields, so direct
|
|
658
|
+
* assignments fail structurally; this documents the deliberate widening.
|
|
659
|
+
*/
|
|
660
|
+
function asWidgetSummary(payload: unknown): WidgetSummary {
|
|
661
|
+
return (payload ?? {}) as WidgetSummary;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Reactivity marker for template {@const}s: a Svelte 4 {@const} only re-runs
|
|
666
|
+
* when a value referenced in its expression changes, so the panel-revision
|
|
667
|
+
* counter is passed explicitly. The bare comma idiom this replaces
|
|
668
|
+
* (`(revision, value)`) is a TS unused-expression error.
|
|
669
|
+
*/
|
|
670
|
+
function rev<T>(revision: number, value: T, ..._alsoTracked: unknown[]): T {
|
|
671
|
+
return value;
|
|
672
|
+
}
|
|
673
|
+
|
|
608
674
|
// LocalStorage keys for persisting selection across refresh
|
|
609
675
|
const SELECTED_WIDGET_STORAGE_KEY = 'widgetCreator_selectedWidgetId';
|
|
610
676
|
const SELECTED_CANVAS_STORAGE_KEY = 'widgetCreator_selectedCanvasId';
|
|
@@ -797,6 +863,9 @@
|
|
|
797
863
|
let codeGenerationPrompt = DEFAULT_PROMPT;
|
|
798
864
|
let isGeneratingCode = false;
|
|
799
865
|
let generatingWidgetIds: Record<string, boolean> = {};
|
|
866
|
+
function isAnyWidgetGenerating(): boolean {
|
|
867
|
+
return isGeneratingCode || Object.keys(generatingWidgetIds).some((id) => generatingWidgetIds[id]);
|
|
868
|
+
}
|
|
800
869
|
const widgetAbortControllers = new Map<string, AbortController>();
|
|
801
870
|
let currentGenerationOperationId: string | null = null;
|
|
802
871
|
let generateCodeStatus: string | null = null;
|
|
@@ -831,7 +900,7 @@
|
|
|
831
900
|
// Chat component reference and context (focused panel)
|
|
832
901
|
let chatMethods: {
|
|
833
902
|
sendMessage: (content: string, attachments?: File[]) => Promise<void>;
|
|
834
|
-
addAssistantMessage: (content: string, skipPersist?: boolean) => void;
|
|
903
|
+
addAssistantMessage: (content: string, skipPersist?: boolean, suggestions?: string[]) => void;
|
|
835
904
|
addUserMessage: (content: string, attachments?: Array<{ id: string; url: string; file_type: string; file_name: string }>) => void;
|
|
836
905
|
getConversationId: () => string | null;
|
|
837
906
|
addImageAttachment?: (dataUrl: string, fileName?: string) => void;
|
|
@@ -967,6 +1036,7 @@
|
|
|
967
1036
|
fileSize?: number;
|
|
968
1037
|
}>,
|
|
969
1038
|
sourceWidgetId?: string,
|
|
1039
|
+
sendOptions?: { skipIntentGate?: boolean },
|
|
970
1040
|
) {
|
|
971
1041
|
const widgetId = sourceWidgetId ?? selectedWidgetId;
|
|
972
1042
|
if (!widgetId) return;
|
|
@@ -1089,6 +1159,7 @@
|
|
|
1089
1159
|
attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
|
|
1090
1160
|
promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
|
|
1091
1161
|
persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
|
|
1162
|
+
skipIntentGate: sendOptions?.skipIntentGate === true,
|
|
1092
1163
|
});
|
|
1093
1164
|
}
|
|
1094
1165
|
|
|
@@ -1453,10 +1524,12 @@
|
|
|
1453
1524
|
: null) || userMessages[userMessages.length - 1] || null;
|
|
1454
1525
|
const existingUrls = new Set(
|
|
1455
1526
|
(targetUser?.attachments || [])
|
|
1456
|
-
.map((a: { url?:
|
|
1457
|
-
.filter((url: string
|
|
1527
|
+
.map((a: { url?: unknown }) => a.url)
|
|
1528
|
+
.filter((url): url is string => typeof url === 'string' && url.length > 0),
|
|
1529
|
+
);
|
|
1530
|
+
const existingHasRemoteImage = [...existingUrls].some(
|
|
1531
|
+
(url) => typeof url === 'string' && /^https?:\/\//i.test(url)
|
|
1458
1532
|
);
|
|
1459
|
-
const existingHasRemoteImage = [...existingUrls].some((url) => /^https?:\/\//i.test(url));
|
|
1460
1533
|
// Chat already persisted CDN attachments for this turn — do not merge data URLs/canvas shots.
|
|
1461
1534
|
if (targetUser && existingHasRemoteImage) {
|
|
1462
1535
|
if (convId !== visibleConversationId) {
|
|
@@ -1578,6 +1651,9 @@
|
|
|
1578
1651
|
iframe: HTMLIFrameElement,
|
|
1579
1652
|
timeoutMs = 6000,
|
|
1580
1653
|
): Promise<{ data: string | null; error: string | null }> {
|
|
1654
|
+
if (isAnyWidgetGenerating()) {
|
|
1655
|
+
return Promise.resolve({ data: null, error: 'generation in progress' });
|
|
1656
|
+
}
|
|
1581
1657
|
return new Promise((resolve) => {
|
|
1582
1658
|
if (!iframe.contentWindow) {
|
|
1583
1659
|
resolve({ data: null, error: 'no-content-window' });
|
|
@@ -1609,6 +1685,14 @@
|
|
|
1609
1685
|
* Capture via a temporary same-path Worker /preview iframe (has html2canvas handler).
|
|
1610
1686
|
*/
|
|
1611
1687
|
async function captureViaHiddenWorkerPreview(widgetId: string): Promise<string | null> {
|
|
1688
|
+
// Loading a widget in a hidden iframe costs a full Svelte + Tailwind CDN
|
|
1689
|
+
// startup on the main thread this page shares with it, and the capture that
|
|
1690
|
+
// follows is refused during a generation anyway (`requestIframeScreenshot`),
|
|
1691
|
+
// so the whole path is skipped rather than paying the load for nothing.
|
|
1692
|
+
if (isAnyWidgetGenerating()) {
|
|
1693
|
+
console.log('[Chat→Screenshot] Skip hidden Worker capture — codegen in progress', widgetId?.substring(0, 8));
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1612
1696
|
const workerUrl = `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
|
|
1613
1697
|
console.log('[Chat→Screenshot] Capturing via hidden Worker preview', {
|
|
1614
1698
|
widgetId: widgetId.substring(0, 8),
|
|
@@ -1657,25 +1741,29 @@
|
|
|
1657
1741
|
|
|
1658
1742
|
/** Explicit Refresh only: capture the loaded frame without opening a panel or publishing. */
|
|
1659
1743
|
async function handleWidgetShapeRefresh(widgetId, iframe, isCurrentFrame) {
|
|
1744
|
+
if (isAnyWidgetGenerating()) return;
|
|
1660
1745
|
const capturedCanvasId = getEffectiveCanvasId();
|
|
1661
1746
|
const capturedCanvas = canvasRef;
|
|
1662
1747
|
const capturedSource = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
1663
1748
|
// Frame/intent identity owns this capture, not focused-panel HEAD hydration
|
|
1664
1749
|
// or another widget's global generation operation.
|
|
1665
1750
|
const isCurrent = () => isCurrentFrame() && !!getWidgetById(widgetId)
|
|
1666
|
-
&& !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId]
|
|
1751
|
+
&& !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId] && !isAnyWidgetGenerating()
|
|
1667
1752
|
&& capturedCanvasId === getEffectiveCanvasId() && capturedCanvas === canvasRef
|
|
1668
1753
|
&& capturedSource === (shapePreviewSourceByWidgetId[widgetId] || 'live')
|
|
1669
1754
|
&& resolveLiveWidgetShapeIframe(widgetId) === iframe;
|
|
1670
1755
|
if (!isCurrent()) return;
|
|
1671
1756
|
try {
|
|
1672
|
-
const screenshot = await handleChatScreenshot(widgetId, iframe);
|
|
1757
|
+
const screenshot = await handleChatScreenshot(widgetId, iframe, true);
|
|
1673
1758
|
if (!screenshot || !isCurrent()) return;
|
|
1674
1759
|
// Persist before Fabric/autosave side effects can resync the live overlay.
|
|
1675
1760
|
await persistWidgetListThumbnail(widgetId, screenshot, isCurrent);
|
|
1676
1761
|
if (!isCurrent()) return;
|
|
1677
1762
|
capturedCanvas?.updateWidgetImage(widgetId, screenshot);
|
|
1678
|
-
const canvasJson =
|
|
1763
|
+
const canvasJson = measureSync(
|
|
1764
|
+
'canvas-serialize:shape-refresh-thumbnail',
|
|
1765
|
+
() => capturedCanvas?.saveCanvas?.() ?? null,
|
|
1766
|
+
);
|
|
1679
1767
|
if (canvasJson) void handleCanvasAutoSave(canvasJson);
|
|
1680
1768
|
} catch (error) {
|
|
1681
1769
|
console.warn('[WidgetShape] Refresh thumbnail capture failed', widgetId, error);
|
|
@@ -1683,13 +1771,34 @@
|
|
|
1683
1771
|
}
|
|
1684
1772
|
|
|
1685
1773
|
/** 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> {
|
|
1774
|
+
async function handleChatScreenshot(widgetId?: string | null, requiredIframe: HTMLIFrameElement | null = null, quiet = false): Promise<string | null> {
|
|
1687
1775
|
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1688
1776
|
if (!targetId) {
|
|
1689
1777
|
console.warn('[Chat→Screenshot] No widget id');
|
|
1690
1778
|
return null;
|
|
1691
1779
|
}
|
|
1780
|
+
// Never run background / refresh screenshot captures while code generation is running —
|
|
1781
|
+
// html2canvas inside any same-origin iframe freezes the browser main thread.
|
|
1782
|
+
if ((quiet || requiredIframe != null) && isAnyWidgetGenerating()) {
|
|
1783
|
+
console.log('[Chat→Screenshot] Skip capture — generation in progress');
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
// Same single-flight as handleRefreshScreenshot: both paths run html2canvas
|
|
1787
|
+
// inside preview iframes that share the page main thread. Overlapping
|
|
1788
|
+
// captures during generation were a major freeze source.
|
|
1789
|
+
if (screenshotCaptureInFlight.has(targetId)) {
|
|
1790
|
+
console.log('[Chat→Screenshot] Capture already in flight — skipping', targetId.substring(0, 8));
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
screenshotCaptureInFlight.add(targetId);
|
|
1794
|
+
try {
|
|
1795
|
+
return await performChatScreenshot(targetId, requiredIframe, quiet);
|
|
1796
|
+
} finally {
|
|
1797
|
+
screenshotCaptureInFlight.delete(targetId);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1692
1800
|
|
|
1801
|
+
async function performChatScreenshot(targetId: string, requiredIframe: HTMLIFrameElement | null, quiet = false): Promise<string | null> {
|
|
1693
1802
|
const liveIframe = requiredIframe ?? resolveLiveWidgetShapeIframe(targetId);
|
|
1694
1803
|
if (requiredIframe && (requiredIframe !== resolveLiveWidgetShapeIframe(targetId) || !requiredIframe.isConnected)) return null;
|
|
1695
1804
|
const liveSrc = liveIframe
|
|
@@ -1712,6 +1821,8 @@
|
|
|
1712
1821
|
|
|
1713
1822
|
// Explicit refresh must not persist a different version or a stale Fabric image.
|
|
1714
1823
|
if (requiredIframe) return null;
|
|
1824
|
+
// Quiet thumbnail sync must NEVER construct a temporary hidden iframe and compile worker preview.
|
|
1825
|
+
if (quiet) return null;
|
|
1715
1826
|
|
|
1716
1827
|
// 2) Hidden Worker /preview (same capture script; wait for load)
|
|
1717
1828
|
const fromWorker = await captureViaHiddenWorkerPreview(targetId);
|
|
@@ -1737,7 +1848,6 @@
|
|
|
1737
1848
|
});
|
|
1738
1849
|
return null;
|
|
1739
1850
|
}
|
|
1740
|
-
|
|
1741
1851
|
function normalizeConversationRecords(json: any): any[] {
|
|
1742
1852
|
const raw = json?.data ?? json?.conversations ?? json;
|
|
1743
1853
|
const list = Array.isArray(raw)
|
|
@@ -2416,8 +2526,14 @@
|
|
|
2416
2526
|
|
|
2417
2527
|
/** Explicit chat box height (px). null/0 = fill available; user drag sets a fixed px. */
|
|
2418
2528
|
const CHAT_DEFAULT_HEIGHT = 400;
|
|
2419
|
-
const CHAT_MIN_HEIGHT =
|
|
2529
|
+
const CHAT_MIN_HEIGHT = 260;
|
|
2420
2530
|
const CHAT_MAX_HEIGHT = 2000;
|
|
2531
|
+
/**
|
|
2532
|
+
* Floor applied to the chat box while it auto-fills the create step. A floor
|
|
2533
|
+
* taller than the available area makes the surrounding scroll container
|
|
2534
|
+
* scroll, which puts the chat input below the fold on open.
|
|
2535
|
+
*/
|
|
2536
|
+
const CHAT_AUTO_MIN_HEIGHT = 0;
|
|
2421
2537
|
|
|
2422
2538
|
function getChatContainerHeightForPanel(widgetId: string): number | null {
|
|
2423
2539
|
const stored = getPanelState(widgetId).chatContainerHeight;
|
|
@@ -2502,7 +2618,7 @@
|
|
|
2502
2618
|
if (!schema) return schema;
|
|
2503
2619
|
try {
|
|
2504
2620
|
const clone = structuredClone(schema);
|
|
2505
|
-
const groups = Array.isArray(clone) ? clone : clone.componentGroups;
|
|
2621
|
+
const groups = Array.isArray(clone) ? clone : (clone as { componentGroups?: unknown }).componentGroups;
|
|
2506
2622
|
if (!Array.isArray(groups) || groups.length === 0) return schema;
|
|
2507
2623
|
const allProps = groups.flatMap((group: { components?: Array<{ properties?: unknown[] }> }) =>
|
|
2508
2624
|
(group.components || []).flatMap((component) => component.properties || [])
|
|
@@ -3594,7 +3710,7 @@
|
|
|
3594
3710
|
!compositionRecordProjectId(selectedComposition as Record<string, unknown>);
|
|
3595
3711
|
const res = await compositionsClient.updateComposition({
|
|
3596
3712
|
id: selectedComposition.id,
|
|
3597
|
-
|
|
3713
|
+
updateComposition: {
|
|
3598
3714
|
content: buildCompositionContentPayload(),
|
|
3599
3715
|
...(needsProjectBackfill ? projectBackfill : {}),
|
|
3600
3716
|
}
|
|
@@ -3686,7 +3802,7 @@
|
|
|
3686
3802
|
try {
|
|
3687
3803
|
await compositionsClient.updateComposition({
|
|
3688
3804
|
id: compositionId,
|
|
3689
|
-
|
|
3805
|
+
updateComposition: { name: trimmed }
|
|
3690
3806
|
});
|
|
3691
3807
|
showToast('success', 'Composition renamed');
|
|
3692
3808
|
await loadCompositions();
|
|
@@ -3747,7 +3863,9 @@
|
|
|
3747
3863
|
};
|
|
3748
3864
|
await compositionsClient.updateComposition({
|
|
3749
3865
|
id: selectedComposition.id,
|
|
3750
|
-
|
|
3866
|
+
// The gateway accepts embedSettings (compositionSchemas update schema);
|
|
3867
|
+
// the generated UpdateComposition model predates that field.
|
|
3868
|
+
updateComposition: { embedSettings } as unknown as UpdateComposition
|
|
3751
3869
|
});
|
|
3752
3870
|
selectedComposition = { ...selectedComposition, embedSettings };
|
|
3753
3871
|
embedDimensionsChanged = false;
|
|
@@ -3943,11 +4061,11 @@
|
|
|
3943
4061
|
// SDK returns { success, models } directly (not nested under data)
|
|
3944
4062
|
if (response.success && Array.isArray(response.models)) {
|
|
3945
4063
|
// Transform API response to match our interface
|
|
3946
|
-
availableModels = response.models.map((m:
|
|
3947
|
-
id: m.id,
|
|
3948
|
-
name: m.displayName,
|
|
3949
|
-
provider: m.provider.charAt(0).toUpperCase() + m.provider.slice(1), // Capitalize provider
|
|
3950
|
-
description: m.description,
|
|
4064
|
+
availableModels = response.models.map((m: AgentModelsGet200ResponseModelsInner) => ({
|
|
4065
|
+
id: m.id ?? '',
|
|
4066
|
+
name: m.displayName ?? '',
|
|
4067
|
+
provider: (m.provider ?? '').charAt(0).toUpperCase() + (m.provider ?? '').slice(1), // Capitalize provider
|
|
4068
|
+
description: m.description ?? '',
|
|
3951
4069
|
supportsVision: m.supportsVision === true,
|
|
3952
4070
|
}));
|
|
3953
4071
|
|
|
@@ -4687,7 +4805,12 @@
|
|
|
4687
4805
|
|
|
4688
4806
|
// Step 3: Check operation status
|
|
4689
4807
|
try {
|
|
4690
|
-
const op = await operationsClient.operationsIdGet({ id: operationId })
|
|
4808
|
+
const op = (await operationsClient.operationsIdGet({ id: operationId })) as {
|
|
4809
|
+
status?: string;
|
|
4810
|
+
error?: string;
|
|
4811
|
+
progress?: number;
|
|
4812
|
+
metadata?: Record<string, unknown>;
|
|
4813
|
+
};
|
|
4691
4814
|
const status = (op?.status || '').toLowerCase();
|
|
4692
4815
|
|
|
4693
4816
|
if (status === 'completed') {
|
|
@@ -5635,22 +5758,29 @@
|
|
|
5635
5758
|
const res = await widgetsClient.getWidgets({ canvasId: canvasId ?? undefined });
|
|
5636
5759
|
|
|
5637
5760
|
// normalize error shape from apiRequest
|
|
5638
|
-
|
|
5761
|
+
const envelope = res as unknown as {
|
|
5762
|
+
error?: string;
|
|
5763
|
+
message?: string;
|
|
5764
|
+
data?: unknown;
|
|
5765
|
+
};
|
|
5766
|
+
if (envelope && (envelope.error || envelope.message)) {
|
|
5639
5767
|
// console.error('Widget Creator: loadWidgetsForCanvas: response not ok:', res);
|
|
5640
5768
|
widgets = [];
|
|
5641
5769
|
selectedWidgetId = null;
|
|
5642
5770
|
isLoadingWidgets = false;
|
|
5643
|
-
widgetsError =
|
|
5771
|
+
widgetsError = envelope.message || envelope.error || 'Failed to load widgets';
|
|
5644
5772
|
throw new Error(widgetsError);
|
|
5645
5773
|
}
|
|
5646
5774
|
|
|
5647
|
-
// if the response is ok, parse the data
|
|
5775
|
+
// if the response is ok, parse the data (bare list, {data: list} or {data: {data: list}})
|
|
5776
|
+
const envelopeData = envelope?.data;
|
|
5777
|
+
const envelopeNestedData = (envelopeData as { data?: unknown } | undefined)?.data;
|
|
5648
5778
|
const fetched = Array.isArray(res)
|
|
5649
|
-
? (res as WidgetSummary[])
|
|
5650
|
-
: Array.isArray(
|
|
5651
|
-
? (
|
|
5652
|
-
: Array.isArray(
|
|
5653
|
-
? (
|
|
5779
|
+
? (res as unknown as WidgetSummary[])
|
|
5780
|
+
: Array.isArray(envelopeNestedData)
|
|
5781
|
+
? (envelopeNestedData as unknown as WidgetSummary[])
|
|
5782
|
+
: Array.isArray(envelopeData)
|
|
5783
|
+
? (envelopeData as unknown as WidgetSummary[])
|
|
5654
5784
|
: [];
|
|
5655
5785
|
|
|
5656
5786
|
const previousById = new Map(
|
|
@@ -5747,9 +5877,13 @@
|
|
|
5747
5877
|
}
|
|
5748
5878
|
|
|
5749
5879
|
function getWidgetCreatedAtMs(widget: WidgetSummary): number {
|
|
5750
|
-
const
|
|
5751
|
-
if (!
|
|
5752
|
-
const ms = new Date(
|
|
5880
|
+
const rawCreated = widget.createdAt ?? widget.created_at;
|
|
5881
|
+
if (!rawCreated) return 0;
|
|
5882
|
+
const ms = new Date(
|
|
5883
|
+
typeof rawCreated === 'string' || typeof rawCreated === 'number' || rawCreated instanceof Date
|
|
5884
|
+
? rawCreated
|
|
5885
|
+
: String(rawCreated)
|
|
5886
|
+
).getTime();
|
|
5753
5887
|
return Number.isFinite(ms) ? ms : 0;
|
|
5754
5888
|
}
|
|
5755
5889
|
|
|
@@ -6155,10 +6289,10 @@
|
|
|
6155
6289
|
async function resolvePrimaryWidgetIdForCanvas(targetCanvasId: string): Promise<string | null> {
|
|
6156
6290
|
if (!canvasesClient || !targetCanvasId?.trim()) return null;
|
|
6157
6291
|
try {
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6292
|
+
const canvasRecord = await canvasesClient.getCanvas({ id: targetCanvasId });
|
|
6293
|
+
const record =
|
|
6294
|
+
(canvasRecord as { data?: Record<string, unknown> })?.data ??
|
|
6295
|
+
(canvasRecord as unknown as Record<string, unknown>);
|
|
6162
6296
|
const raw =
|
|
6163
6297
|
(record as { primaryWidgetId?: unknown })?.primaryWidgetId ??
|
|
6164
6298
|
(record as { primary_widget_id?: unknown })?.primary_widget_id ??
|
|
@@ -6640,7 +6774,12 @@
|
|
|
6640
6774
|
if (!widgetId || !agentClient) return null;
|
|
6641
6775
|
try {
|
|
6642
6776
|
const json = await agentClient.agentWidgetBuilderRepoHeadWidgetIdGet({ widgetId });
|
|
6643
|
-
const
|
|
6777
|
+
const repoHeadEnvelope = json as unknown as {
|
|
6778
|
+
data?: { repoHeadCommitSha?: string | null; repo_head_commit_sha?: string | null };
|
|
6779
|
+
repoHeadCommitSha?: string | null;
|
|
6780
|
+
repo_head_commit_sha?: string | null;
|
|
6781
|
+
};
|
|
6782
|
+
const data = repoHeadEnvelope?.data ?? repoHeadEnvelope;
|
|
6644
6783
|
const sha = data?.repoHeadCommitSha ?? data?.repo_head_commit_sha ?? null;
|
|
6645
6784
|
return typeof sha === 'string' && sha.length > 0 ? sha : null;
|
|
6646
6785
|
} catch (err) {
|
|
@@ -6958,13 +7097,14 @@
|
|
|
6958
7097
|
}
|
|
6959
7098
|
}
|
|
6960
7099
|
);
|
|
6961
|
-
|
|
7100
|
+
const createEnvelope = response as unknown as { error?: string; message?: string };
|
|
7101
|
+
if (!response || createEnvelope.error || createEnvelope.message) {
|
|
6962
7102
|
console.error('Widget create failed:', response);
|
|
6963
|
-
authStatus = `Create widget failed: ${
|
|
7103
|
+
authStatus = `Create widget failed: ${createEnvelope.message || createEnvelope.error || 'Unknown error'}`;
|
|
6964
7104
|
return;
|
|
6965
7105
|
}
|
|
6966
7106
|
// Ensure we have an id
|
|
6967
|
-
const newWidget = response.data
|
|
7107
|
+
const newWidget = asWidgetSummary((response as { data?: unknown }).data ?? response); // handle envelope or direct
|
|
6968
7108
|
if (newWidget?.id) {
|
|
6969
7109
|
widgets = [{ ...newWidget }, ...widgets];
|
|
6970
7110
|
selectWidget(newWidget);
|
|
@@ -7619,12 +7759,12 @@
|
|
|
7619
7759
|
canvasShapePreviewSyncTimers.clear();
|
|
7620
7760
|
}
|
|
7621
7761
|
function scheduleCanvasShapePreviewSync(widgetId: string, delayMs = 1800): void {
|
|
7622
|
-
if (!widgetId) return;
|
|
7762
|
+
if (!widgetId || isAnyWidgetGenerating()) return;
|
|
7623
7763
|
const existing = canvasShapePreviewSyncTimers.get(widgetId);
|
|
7624
7764
|
if (existing) clearTimeout(existing);
|
|
7625
7765
|
const timer = setTimeout(() => {
|
|
7626
7766
|
canvasShapePreviewSyncTimers.delete(widgetId);
|
|
7627
|
-
if (generatingWidgetIds[widgetId]) {
|
|
7767
|
+
if (isAnyWidgetGenerating() || generatingWidgetIds[widgetId]) {
|
|
7628
7768
|
// console.log('[CreatorApp] Skip shape preview sync — codegen in progress:', widgetId.substring(0, 8));
|
|
7629
7769
|
return;
|
|
7630
7770
|
}
|
|
@@ -7639,10 +7779,38 @@
|
|
|
7639
7779
|
canvasShapePreviewSyncTimers.set(widgetId, timer);
|
|
7640
7780
|
}
|
|
7641
7781
|
|
|
7782
|
+
/**
|
|
7783
|
+
* Serializing the canvas stringifies every shape's embedded image, so it is
|
|
7784
|
+
* megabytes of synchronous work on a busy canvas. The thumbnail paths run it
|
|
7785
|
+
* immediately after a capture, which is exactly where the freeze reports land,
|
|
7786
|
+
* so each call is measured and named.
|
|
7787
|
+
*/
|
|
7788
|
+
function serializeCanvasJson(reason: string): string | null {
|
|
7789
|
+
return measureSync(`canvas-serialize:${reason}`, () => canvasRef?.saveCanvas?.() ?? null);
|
|
7790
|
+
}
|
|
7791
|
+
|
|
7642
7792
|
async function handleRefreshScreenshot(widgetId: string, options: { quiet?: boolean } = {}) {
|
|
7643
7793
|
const quiet = !!options.quiet;
|
|
7644
7794
|
// console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
|
|
7645
7795
|
|
|
7796
|
+
// Never run quiet background screenshot captures while ANY code generation is running —
|
|
7797
|
+
// html2canvas execution inside any preview iframe blocks the single shared main thread.
|
|
7798
|
+
if (quiet && isAnyWidgetGenerating()) {
|
|
7799
|
+
console.log('[handleRefreshScreenshot] Skip quiet capture — codegen in progress', widgetId?.substring(0, 8));
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
|
|
7803
|
+
// Single-flight per widget: overlapping captures stack html2canvas runs in
|
|
7804
|
+
// the preview iframe — same-origin iframes share this page's main thread,
|
|
7805
|
+
// and stacked captures blocked the whole site during generation (no
|
|
7806
|
+
// [FreezeReport] because the long task is attributed to the iframe).
|
|
7807
|
+
if (screenshotCaptureInFlight.has(widgetId)) {
|
|
7808
|
+
console.log('[handleRefreshScreenshot] Capture already in flight — skipping', widgetId?.substring(0, 8));
|
|
7809
|
+
return;
|
|
7810
|
+
}
|
|
7811
|
+
screenshotCaptureInFlight.add(widgetId);
|
|
7812
|
+
|
|
7813
|
+
try {
|
|
7646
7814
|
if (isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
|
|
7647
7815
|
// console.log('[handleRefreshScreenshot] Skip iframe capture — convert/draft panel is open:', widgetId.substring(0, 8));
|
|
7648
7816
|
return;
|
|
@@ -7673,10 +7841,10 @@
|
|
|
7673
7841
|
if (!shapeThumbnailCaptureInFlight.has(widgetId)) {
|
|
7674
7842
|
shapeThumbnailCaptureInFlight.add(widgetId);
|
|
7675
7843
|
try {
|
|
7676
|
-
const freshCapture = await handleChatScreenshot(widgetId);
|
|
7844
|
+
const freshCapture = await handleChatScreenshot(widgetId, null, quiet);
|
|
7677
7845
|
if (freshCapture) {
|
|
7678
7846
|
canvasRef?.updateWidgetImage(widgetId, freshCapture);
|
|
7679
|
-
const freshJson =
|
|
7847
|
+
const freshJson = serializeCanvasJson('quiet-shape-thumbnail');
|
|
7680
7848
|
if (freshJson) void handleCanvasAutoSave(freshJson);
|
|
7681
7849
|
void persistWidgetListThumbnail(widgetId, freshCapture);
|
|
7682
7850
|
} else {
|
|
@@ -7713,8 +7881,7 @@
|
|
|
7713
7881
|
showToast('info', 'Capturing widget screenshot...', { duration: 3000 });
|
|
7714
7882
|
}
|
|
7715
7883
|
|
|
7716
|
-
|
|
7717
|
-
const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
|
|
7884
|
+
const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
|
|
7718
7885
|
const timeout = setTimeout(() => {
|
|
7719
7886
|
window.removeEventListener('message', handler);
|
|
7720
7887
|
resolve({ data: null, error: 'timeout' });
|
|
@@ -7736,7 +7903,7 @@
|
|
|
7736
7903
|
if (result.data && canvasRef) {
|
|
7737
7904
|
canvasRef.updateWidgetImage(widgetId, result.data);
|
|
7738
7905
|
setTimeout(() => {
|
|
7739
|
-
const json =
|
|
7906
|
+
const json = serializeCanvasJson('screenshot-refresh');
|
|
7740
7907
|
if (json) handleCanvasAutoSave(json);
|
|
7741
7908
|
}, 500);
|
|
7742
7909
|
void persistWidgetListThumbnail(widgetId, result.data);
|
|
@@ -7765,6 +7932,8 @@
|
|
|
7765
7932
|
if (!quiet) {
|
|
7766
7933
|
showToast('error', 'Screenshot capture failed', { duration: 3000 });
|
|
7767
7934
|
}
|
|
7935
|
+
} finally {
|
|
7936
|
+
screenshotCaptureInFlight.delete(widgetId);
|
|
7768
7937
|
}
|
|
7769
7938
|
}
|
|
7770
7939
|
|
|
@@ -7846,6 +8015,8 @@ Requirements:
|
|
|
7846
8015
|
attachmentIds?: string[];
|
|
7847
8016
|
/** Image URLs / data URLs captured before chat clearDraft. */
|
|
7848
8017
|
promptImagesOverride?: string[];
|
|
8018
|
+
/** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
|
|
8019
|
+
skipIntentGate?: boolean;
|
|
7849
8020
|
/** Chat files (PDF, video, images) to store on the user message before codegen. */
|
|
7850
8021
|
persistFileAttachments?: Array<{
|
|
7851
8022
|
url?: string;
|
|
@@ -7935,6 +8106,7 @@ Requirements:
|
|
|
7935
8106
|
attachmentIds: options.attachmentIds,
|
|
7936
8107
|
promptImagesOverride: options.promptImagesOverride,
|
|
7937
8108
|
persistFileAttachments: options.persistFileAttachments,
|
|
8109
|
+
skipIntentGate: options.skipIntentGate,
|
|
7938
8110
|
});
|
|
7939
8111
|
}
|
|
7940
8112
|
|
|
@@ -7956,6 +8128,8 @@ Requirements:
|
|
|
7956
8128
|
forceRecreateRepo?: boolean;
|
|
7957
8129
|
attachmentIds?: string[];
|
|
7958
8130
|
promptImagesOverride?: string[];
|
|
8131
|
+
/** Skip the server's intent gate — the clarification card's explicit "generate anyway" choice. */
|
|
8132
|
+
skipIntentGate?: boolean;
|
|
7959
8133
|
persistFileAttachments?: Array<{
|
|
7960
8134
|
url?: string;
|
|
7961
8135
|
file?: File;
|
|
@@ -7971,6 +8145,7 @@ Requirements:
|
|
|
7971
8145
|
|
|
7972
8146
|
const panelChat = getChatMethodsForWidget(generationWidgetId);
|
|
7973
8147
|
const panelSnap = getPanelState(generationWidgetId);
|
|
8148
|
+
const hadCodeBeforeGeneration = !!(panelSnap?.hasGeneratedCode || panelSnap?.lastCommitId);
|
|
7974
8149
|
|
|
7975
8150
|
// Per-widget abort: only abort if the SAME widget already has a running generation
|
|
7976
8151
|
const existingController = widgetAbortControllers.get(generationWidgetId);
|
|
@@ -8191,17 +8366,67 @@ Requirements:
|
|
|
8191
8366
|
modelId: coderModelForRequest || undefined,
|
|
8192
8367
|
plannerModelId: plannerModelIdForRequest,
|
|
8193
8368
|
widgetId: generationWidgetId,
|
|
8194
|
-
conversationId: activeConversationId ?? undefined,
|
|
8195
|
-
images: imagesToSend,
|
|
8369
|
+
conversationId: activeConversationId ?? undefined, images: imagesToSend,
|
|
8196
8370
|
attachmentIds: attachmentIdsToSend,
|
|
8197
8371
|
runtimeLogs: runtimeLogsForRequest,
|
|
8198
8372
|
compileFixOnly: options.compileFixOnly === true,
|
|
8199
|
-
|
|
8373
|
+
skipIntentGate: options.skipIntentGate === true,
|
|
8374
|
+
} as GenerateCodeRequest
|
|
8200
8375
|
});
|
|
8201
8376
|
// console.log('[WidgetCreator] Code generation request sent for widget', generationWidgetId.substring(0, 8), 'with model:', coderModelForRequest, 'planner:', plannerModelIdForRequest || 'none', 'conversationId:', activeConversationId || 'none', 'images:', imagesToSend?.length || 0, 'attachmentIds:', attachmentIdsToSend?.length || 0, 'runtimeLogs:', runtimeLogsForRequest?.length ?? 0);
|
|
8202
8377
|
// console.log('Widget Creator: Code generation response:', response);
|
|
8203
8378
|
|
|
8204
|
-
|
|
8379
|
+
// --- Intent gate: the server answered instead of generating ---
|
|
8380
|
+
// A question got a meta answer, an ambiguous ask got a clarification
|
|
8381
|
+
// card, or a publish/settings ask was redirected. None of these spent
|
|
8382
|
+
// a generation slot. Anything unexpected here falls through to the
|
|
8383
|
+
// normal path (operation tracking), never blocks a real build.
|
|
8384
|
+
const gatedData = (response as any)?.data;
|
|
8385
|
+
if (gatedData?.gated === true && typeof gatedData.action === 'string' && gatedData.action !== 'generate') {
|
|
8386
|
+
const gatedAction = gatedData.action as 'answer' | 'redirect' | 'clarify';
|
|
8387
|
+
const gatedReply: string = gatedData.reply ?? '';
|
|
8388
|
+
const suggestions: string[] = Array.isArray(gatedData.suggestions) ? gatedData.suggestions : [];
|
|
8389
|
+
|
|
8390
|
+
clearActiveOperationId();
|
|
8391
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8392
|
+
lastGenerationFailed: false,
|
|
8393
|
+
lastGenerationError: null,
|
|
8394
|
+
generateCodeStatus:
|
|
8395
|
+
gatedAction === 'answer'
|
|
8396
|
+
? 'Answered from widget details — no generation needed.'
|
|
8397
|
+
: gatedAction === 'clarify'
|
|
8398
|
+
? 'Need a bit more detail before generating.'
|
|
8399
|
+
: 'No generation needed — use the publish or settings control.',
|
|
8400
|
+
});
|
|
8401
|
+
|
|
8402
|
+
const chatMethods = getChatMethodsForWidget(generationWidgetId);
|
|
8403
|
+
if (gatedAction === 'clarify' && suggestions.length > 0 && chatMethods?.addAssistantMessage) {
|
|
8404
|
+
// Suggestions travel as message data, not flattened text — the chat
|
|
8405
|
+
// renders them as clickable chips that send the next user message.
|
|
8406
|
+
chatMethods.addAssistantMessage(gatedReply, false, suggestions);
|
|
8407
|
+
} else if (chatMethods?.addAssistantMessage) {
|
|
8408
|
+
chatMethods.addAssistantMessage(gatedReply);
|
|
8409
|
+
}
|
|
8410
|
+
|
|
8411
|
+
showToast(
|
|
8412
|
+
gatedAction === 'answer' ? 'info' : 'warning',
|
|
8413
|
+
gatedAction === 'answer' ? 'Answered without generating' : 'No generation started',
|
|
8414
|
+
{
|
|
8415
|
+
description: gatedReply.slice(0, 140),
|
|
8416
|
+
duration: 8000,
|
|
8417
|
+
},
|
|
8418
|
+
);
|
|
8419
|
+
generatingWidgetIds = (() => {
|
|
8420
|
+
const { [generationWidgetId]: _gated, ...rest } = generatingWidgetIds;
|
|
8421
|
+
return rest;
|
|
8422
|
+
})();
|
|
8423
|
+
isGeneratingCode = !!(focusedPanelWidgetId && generatingWidgetIds[focusedPanelWidgetId]);
|
|
8424
|
+
setCrossTabGenerating(false, generationWidgetId);
|
|
8425
|
+
widgetAbortControllers.delete(generationWidgetId);
|
|
8426
|
+
return;
|
|
8427
|
+
}
|
|
8428
|
+
|
|
8429
|
+
if (response && (response as { error?: string }).error === 'TOKEN_EXPIRED') {
|
|
8205
8430
|
patchCodegenUiState(generationWidgetId, {
|
|
8206
8431
|
generateCodeStatus: '❌ Authentication expired. Please login again.',
|
|
8207
8432
|
lastGenerationFailed: true,
|
|
@@ -8214,7 +8439,8 @@ Requirements:
|
|
|
8214
8439
|
return;
|
|
8215
8440
|
}
|
|
8216
8441
|
|
|
8217
|
-
const
|
|
8442
|
+
const generateEnvelope = response as unknown as { error?: string; message?: string };
|
|
8443
|
+
const generateErrorMessage = generateEnvelope && (generateEnvelope.error || generateEnvelope.message);
|
|
8218
8444
|
if (generateErrorMessage) {
|
|
8219
8445
|
const normalizedMessage = normalizeErrorMessage(generateErrorMessage);
|
|
8220
8446
|
patchCodegenUiState(generationWidgetId, {
|
|
@@ -8230,8 +8456,10 @@ Requirements:
|
|
|
8230
8456
|
return;
|
|
8231
8457
|
}
|
|
8232
8458
|
|
|
8233
|
-
const
|
|
8234
|
-
|
|
8459
|
+
const generateData = ((response as { data?: unknown }).data ?? response) as {
|
|
8460
|
+
operationId?: string;
|
|
8461
|
+
};
|
|
8462
|
+
const operationId = generateData?.operationId;
|
|
8235
8463
|
|
|
8236
8464
|
// The Worker serves rebuilt code at the same preview URL, so the panel
|
|
8237
8465
|
// must know when the current generation started — hasLiveWorkerPreview()
|
|
@@ -8314,6 +8542,16 @@ Requirements:
|
|
|
8314
8542
|
|
|
8315
8543
|
const completedCommitSha = getOperationCommitId(op.metadata);
|
|
8316
8544
|
const completedPreviewBuildErr = getOperationPreviewBuildError(op.metadata);
|
|
8545
|
+
const completedCached = getOperationCached(op.metadata);
|
|
8546
|
+
const completedCachedFiles = getOperationCachedFiles(op.metadata);
|
|
8547
|
+
const cachedChatSummary = completedCached
|
|
8548
|
+
? `Already done — the widget is in the state you asked for.` +
|
|
8549
|
+
(completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : '') +
|
|
8550
|
+
'\nThat change was made earlier in this conversation, so no new generation was needed' +
|
|
8551
|
+
(completedCachedFiles.length > 0
|
|
8552
|
+
? ':\n' + completedCachedFiles.map((p) => `- ${p}`).join('\n')
|
|
8553
|
+
: '.')
|
|
8554
|
+
: null;
|
|
8317
8555
|
const generationPanelRef = getWidgetDetailsRef(generationWidgetId);
|
|
8318
8556
|
const isFocusedPanel = generationWidgetId === focusedPanelWidgetId;
|
|
8319
8557
|
const completedWidgetName = getWidgetById(generationWidgetId)?.name || 'widget';
|
|
@@ -8351,18 +8589,25 @@ Requirements:
|
|
|
8351
8589
|
// Refresh version history after every generation so the new commit shows
|
|
8352
8590
|
// up in the version menu immediately. Publishing is deliberately not
|
|
8353
8591
|
// automatic — the user previews and presses Publish themselves.
|
|
8592
|
+
// Deferred past the preview remount: running this synchronously against
|
|
8593
|
+
// `dynamic_worker_preview_ready` stacked three heavy re-renders on the
|
|
8594
|
+
// main thread in the same 800ms window — the "frozen at 80%" report.
|
|
8354
8595
|
if (generationWidgetId) {
|
|
8355
8596
|
const { [generationWidgetId]: _resolved, ...restResolved } = shapeHistoryResolvedByWidgetId;
|
|
8356
8597
|
shapeHistoryResolvedByWidgetId = restResolved;
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8598
|
+
setTimeout(() => {
|
|
8599
|
+
void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
|
|
8600
|
+
syncWidgetShapeLivePreviews();
|
|
8601
|
+
if (isFocusedPanel) panelDisplayRevision += 1;
|
|
8602
|
+
});
|
|
8603
|
+
}, 1200);
|
|
8361
8604
|
}
|
|
8362
8605
|
|
|
8363
8606
|
// Always reconcile GitLab HEAD after codegen (metadata.commitId can be missing/stale).
|
|
8607
|
+
// Deferred — same window as the schema/history reloads above.
|
|
8364
8608
|
if (generationWidgetId) {
|
|
8365
|
-
|
|
8609
|
+
setTimeout(() => {
|
|
8610
|
+
void (async () => {
|
|
8366
8611
|
try {
|
|
8367
8612
|
const fetchedHead = await fetchRepoHeadCommitSha(generationWidgetId);
|
|
8368
8613
|
// Prefer the codegen commit from the operation. GitLab HEAD can still
|
|
@@ -8404,11 +8649,15 @@ Requirements:
|
|
|
8404
8649
|
} catch (err) {
|
|
8405
8650
|
console.warn('[Version] Failed to reconcile HEAD after codegen:', err);
|
|
8406
8651
|
}
|
|
8407
|
-
|
|
8652
|
+
})();
|
|
8653
|
+
}, 1000);
|
|
8408
8654
|
}
|
|
8409
8655
|
if (generationWidgetId) {
|
|
8410
8656
|
// console.log('[CompositionEditor] Reloading schemas after codegen', generationWidgetId.substring(0, 8));
|
|
8411
|
-
|
|
8657
|
+
// Deferred with the publish-history reload — see comment above.
|
|
8658
|
+
setTimeout(() => {
|
|
8659
|
+
void loadWidgetSchemas(generationWidgetId, { force: true });
|
|
8660
|
+
}, 1600);
|
|
8412
8661
|
}
|
|
8413
8662
|
|
|
8414
8663
|
const completedRepoId = resolveRepositoryIdForWidget(generationWidgetId);
|
|
@@ -8427,11 +8676,13 @@ Requirements:
|
|
|
8427
8676
|
});
|
|
8428
8677
|
|
|
8429
8678
|
const commitShort = completedCommitSha ? completedCommitSha.substring(0, 7) : '';
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8679
|
+
if (!completedCached) {
|
|
8680
|
+
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
8681
|
+
completedPreviewBuildErr
|
|
8682
|
+
? `Code saved${commitShort ? ` (${commitShort})` : ''} — building preview…`
|
|
8683
|
+
: `Code saved${commitShort ? ` (${commitShort})` : ''} — loading preview…`,
|
|
8684
|
+
);
|
|
8685
|
+
}
|
|
8435
8686
|
|
|
8436
8687
|
// console.log('Widget Creator: Code generation operation completed for widget', generationWidgetId.substring(0, 8), ':', op);
|
|
8437
8688
|
|
|
@@ -8467,13 +8718,17 @@ Requirements:
|
|
|
8467
8718
|
// Final chat message regardless of panel focus — without this, a
|
|
8468
8719
|
// closed/unfocused panel never receives the terminal update and the
|
|
8469
8720
|
// chat stays on the last progress line (e.g. stuck at 80%).
|
|
8721
|
+
// A cache-served completion gets the "already done" affordance:
|
|
8722
|
+
// it names the earlier change (and files) instead of claiming a
|
|
8723
|
+
// generation just happened.
|
|
8470
8724
|
panelChat?.addAssistantMessage(
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8725
|
+
cachedChatSummary ??
|
|
8726
|
+
(completedPreviewBuildErr
|
|
8727
|
+
? `Code saved${completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : ''}, but preview build failed:\n${completedPreviewBuildErr}\n\nRebuilding preview…`
|
|
8728
|
+
: `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`),
|
|
8474
8729
|
true
|
|
8475
8730
|
);
|
|
8476
|
-
if (!completedPreviewBuildErr) {
|
|
8731
|
+
if (!completedPreviewBuildErr && !completedCached) {
|
|
8477
8732
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
8478
8733
|
`Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
|
|
8479
8734
|
);
|
|
@@ -8506,6 +8761,9 @@ Requirements:
|
|
|
8506
8761
|
await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
|
|
8507
8762
|
}
|
|
8508
8763
|
activePanelRef?.setPreviewBuildError(null);
|
|
8764
|
+
// TODO(tomorrow): Dupa o generare noua, sincronizarea versiunii in dropdown si istoric se desincronizeaza.
|
|
8765
|
+
// Trebuie armonizata selectia automata a noii versiuni draft (vN) create in istoric,
|
|
8766
|
+
// actualizat trigger-ul pe canvas si in chat la noul numar de versiune si pastrata coerenta intre preview si dropdown.
|
|
8509
8767
|
// Show Worker preview on the canvas shape (not the previous Live CDN artifact).
|
|
8510
8768
|
shapePreviewSourceByWidgetId = {
|
|
8511
8769
|
...shapePreviewSourceByWidgetId,
|
|
@@ -8531,10 +8789,13 @@ Requirements:
|
|
|
8531
8789
|
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
8532
8790
|
}, 2500);
|
|
8533
8791
|
setTimeout(() => {
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8792
|
+
// Only capture screenshot after the very first generation or at publish
|
|
8793
|
+
// Subsequent generations skip this heavy operation to keep the main thread smooth.
|
|
8794
|
+
if (!hadCodeBeforeGeneration) {
|
|
8795
|
+
void (async () => {
|
|
8796
|
+
await handleRefreshScreenshot(generationWidgetId, { quiet: true });
|
|
8797
|
+
})();
|
|
8798
|
+
}
|
|
8538
8799
|
}, 3000);
|
|
8539
8800
|
// Do NOT auto-publish — user previews first, then clicks Publish / Set live.
|
|
8540
8801
|
} else {
|
|
@@ -8878,11 +9139,17 @@ Requirements:
|
|
|
8878
9139
|
publishingWidgetId = null;
|
|
8879
9140
|
setCrossTabPublishing(false, capturedWidgetId);
|
|
8880
9141
|
|
|
8881
|
-
const
|
|
8882
|
-
|
|
9142
|
+
const publishEnvelope = publishData as unknown as {
|
|
9143
|
+
data?: { version?: number; artifactUrl?: string | null; commitSha?: string | null };
|
|
9144
|
+
version?: number;
|
|
9145
|
+
artifactUrl?: string | null;
|
|
9146
|
+
commitSha?: string | null;
|
|
9147
|
+
};
|
|
9148
|
+
const version = publishEnvelope?.data?.version || publishEnvelope?.version || 1;
|
|
9149
|
+
const artifactUrl = publishEnvelope?.data?.artifactUrl || publishEnvelope?.artifactUrl || null;
|
|
8883
9150
|
const commitSha =
|
|
8884
|
-
|
|
8885
|
-
??
|
|
9151
|
+
publishEnvelope?.data?.commitSha
|
|
9152
|
+
?? publishEnvelope?.commitSha
|
|
8886
9153
|
?? commitShaForPublish
|
|
8887
9154
|
?? null;
|
|
8888
9155
|
applyPublishSuccessToWidgetState(capturedWidgetId, version, artifactUrl, commitSha);
|
|
@@ -8930,9 +9197,9 @@ Requirements:
|
|
|
8930
9197
|
|
|
8931
9198
|
|
|
8932
9199
|
// Token expiration check interval
|
|
8933
|
-
let tokenCheckInterval:
|
|
9200
|
+
let tokenCheckInterval: ReturnType<typeof setInterval>;
|
|
8934
9201
|
let tokenCheckIntervalTime: number = 10; // seconds — check frequently so logout is prompt
|
|
8935
|
-
let tokenDisplayInterval:
|
|
9202
|
+
let tokenDisplayInterval: ReturnType<typeof setInterval>;
|
|
8936
9203
|
|
|
8937
9204
|
// Reactive variables for real-time token status
|
|
8938
9205
|
let tokenTimeLeft: number = 0;
|
|
@@ -8944,6 +9211,7 @@ Requirements:
|
|
|
8944
9211
|
widgetPublishStatus,
|
|
8945
9212
|
codeGenerationStatus,
|
|
8946
9213
|
agentHarnessOutput,
|
|
9214
|
+
dynamicWorkerPreviewStatus,
|
|
8947
9215
|
setAuthToken as setWsAuthToken,
|
|
8948
9216
|
setWebsocketApiUrl
|
|
8949
9217
|
} from './stores/websocketStore.js';
|
|
@@ -9014,6 +9282,9 @@ Requirements:
|
|
|
9014
9282
|
// Clean up cross-tab flags when tab is closed/refreshed. The handler lives at
|
|
9015
9283
|
// component scope so `onDestroy` can remove the same reference.
|
|
9016
9284
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
9285
|
+
// Keep a component-scope binding so onDestroy can remove the listener —
|
|
9286
|
+
// the arrow itself is scoped to this onMount callback.
|
|
9287
|
+
handleBeforeUnloadRef = handleBeforeUnload;
|
|
9017
9288
|
|
|
9018
9289
|
// Read canvas id from the route. The route path (`/canvas/<canvasId>`) is the
|
|
9019
9290
|
// AUTHORITATIVE canvas (site links, refresh, deep-links); localStorage is only a
|
|
@@ -9095,6 +9366,8 @@ Requirements:
|
|
|
9095
9366
|
};
|
|
9096
9367
|
});
|
|
9097
9368
|
|
|
9369
|
+
let handleBeforeUnloadRef: (() => void) | null = null;
|
|
9370
|
+
|
|
9098
9371
|
// Clean up interval on component destroy
|
|
9099
9372
|
onDestroy(() => {
|
|
9100
9373
|
clearCanvasLoadFailsafe();
|
|
@@ -9107,7 +9380,7 @@ Requirements:
|
|
|
9107
9380
|
}
|
|
9108
9381
|
// Cross-tab cleanup: remove flags so other tabs don't stay blocked
|
|
9109
9382
|
if (typeof window !== 'undefined') {
|
|
9110
|
-
window.removeEventListener('beforeunload',
|
|
9383
|
+
window.removeEventListener('beforeunload', handleBeforeUnloadRef);
|
|
9111
9384
|
window.removeEventListener('storage', handleCrossTabStorageEvent);
|
|
9112
9385
|
window.removeEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
9113
9386
|
window.removeEventListener('click', handleCanvasDropdownOutsideClick);
|
|
@@ -9298,6 +9571,19 @@ Requirements:
|
|
|
9298
9571
|
return sanitizePreviewBuildError(raw);
|
|
9299
9572
|
}
|
|
9300
9573
|
|
|
9574
|
+
/** True when the operation completed from the semantic cache (no new generation ran). */
|
|
9575
|
+
function getOperationCached(metadata?: Record<string, unknown>): boolean {
|
|
9576
|
+
return metadata?.cached === true;
|
|
9577
|
+
}
|
|
9578
|
+
|
|
9579
|
+
/** Changed-file paths reported by a semantic-cache-served operation. */
|
|
9580
|
+
function getOperationCachedFiles(metadata?: Record<string, unknown>): string[] {
|
|
9581
|
+
if (!metadata) return [];
|
|
9582
|
+
const raw = metadata.generatedFiles;
|
|
9583
|
+
if (!Array.isArray(raw)) return [];
|
|
9584
|
+
return raw.filter((p): p is string => typeof p === 'string');
|
|
9585
|
+
}
|
|
9586
|
+
|
|
9301
9587
|
function getChatMethodsForWidget(widgetId: string | null | undefined) {
|
|
9302
9588
|
if (!widgetId) return chatMethods ?? null;
|
|
9303
9589
|
const instanceKey = panelInstanceKeyByWidgetId[widgetId] ?? widgetId;
|
|
@@ -9330,6 +9616,9 @@ Requirements:
|
|
|
9330
9616
|
|
|
9331
9617
|
function pushCodegenProgressToUi(op: OperationStatus, widgetId: string | null | undefined) {
|
|
9332
9618
|
const line = formatCodegenProgressStatus(op);
|
|
9619
|
+
// Feed the freeze watchdog the latest status so a [FreezeReport] names the
|
|
9620
|
+
// stage the UI was showing when the main thread stopped responding.
|
|
9621
|
+
(window as any).__widgeticFreezeWatchdog?.setStatus?.(line);
|
|
9333
9622
|
// SSE progress frames can arrive many times per second while the LLM
|
|
9334
9623
|
// streams. Each one re-renders every open panel — coalesce them.
|
|
9335
9624
|
if (widgetId) {
|
|
@@ -9684,8 +9973,7 @@ Requirements:
|
|
|
9684
9973
|
codeGenerationPrompt: storedPrompt,
|
|
9685
9974
|
promptImages: [],
|
|
9686
9975
|
lastCommitId: hydratedHead,
|
|
9687
|
-
lastPublishedVersion: publishedVersion,
|
|
9688
|
-
selectedWidgetJsPath: widget?.jsPath ?? null,
|
|
9976
|
+
lastPublishedVersion: publishedVersion, selectedWidgetJsPath: (widget as { jsPath?: string | null } | undefined)?.jsPath ?? null,
|
|
9689
9977
|
currentRepositoryId: normalizeRepositoryId(widget),
|
|
9690
9978
|
widgetDetailsStep: resolveCreatorWorkflowStep(publishedVersion !== null ? 'edit' : 'create'),
|
|
9691
9979
|
generateCodeStatus: null,
|
|
@@ -9920,10 +10208,30 @@ Requirements:
|
|
|
9920
10208
|
|
|
9921
10209
|
function getPanelEditPreviewUrl(widgetId: string): string | null {
|
|
9922
10210
|
const ps = getPanelDisplayState(widgetId);
|
|
10211
|
+
const source = shapePreviewSourceByWidgetId[widgetId];
|
|
10212
|
+
if (source && source.startsWith('v:')) {
|
|
10213
|
+
const version = parseInt(source.slice(2), 10);
|
|
10214
|
+
if (Number.isFinite(version)) {
|
|
10215
|
+
const record = getShapePublishHistory(widgetId).find(
|
|
10216
|
+
(row) => Number(row.version) === version,
|
|
10217
|
+
);
|
|
10218
|
+
const isPublished = !!(record?.publishedAt ?? record?.published_at);
|
|
10219
|
+
const artifact =
|
|
10220
|
+
record?.artifactUrl ||
|
|
10221
|
+
record?.artifact_url ||
|
|
10222
|
+
(version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
|
|
10223
|
+
if (isPublished && artifact) {
|
|
10224
|
+
const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
|
|
10225
|
+
if (url) return url;
|
|
10226
|
+
}
|
|
10227
|
+
const previewState = resolveWidgetShapeVersionPreviewState(widgetId);
|
|
10228
|
+
if (previewState?.url) return previewState.url;
|
|
10229
|
+
}
|
|
10230
|
+
}
|
|
9923
10231
|
const widget = getWidgetById(widgetId);
|
|
9924
10232
|
const version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(widget);
|
|
9925
10233
|
if (version == null) return null;
|
|
9926
|
-
const jsPath = ps.selectedWidgetJsPath ?? widget?.jsPath ?? null;
|
|
10234
|
+
const jsPath = ps.selectedWidgetJsPath ?? (widget as { jsPath?: string | null } | undefined)?.jsPath ?? null;
|
|
9927
10235
|
return resolvePublishedWidgetUrl(widgetId, version, jsPath);
|
|
9928
10236
|
}
|
|
9929
10237
|
|
|
@@ -9947,6 +10255,8 @@ Requirements:
|
|
|
9947
10255
|
// Historical previews are never interchangeable with the shared HEAD preview.
|
|
9948
10256
|
let shapeVersionPreviewByWidgetId: Record<string, {
|
|
9949
10257
|
versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
|
|
10258
|
+
/** Commit the selected version was built from — drives the chat's Head-at-commit badge. */
|
|
10259
|
+
commitSha?: string | null;
|
|
9950
10260
|
errorDetails?: Awaited<ReturnType<typeof parseVersionPreviewError>> | null;
|
|
9951
10261
|
}> = {};
|
|
9952
10262
|
let shapeVersionPreviewRequestId = 0;
|
|
@@ -10022,6 +10332,15 @@ Requirements:
|
|
|
10022
10332
|
return fromPanel?.length ? fromPanel : [];
|
|
10023
10333
|
}
|
|
10024
10334
|
|
|
10335
|
+
/** Newest unpublished version — Worker `/preview` (repo HEAD) shows this version's code. */
|
|
10336
|
+
function getNewestDraftVersionId(widgetId: string): number | null {
|
|
10337
|
+
const draftVersions = getShapePublishHistory(widgetId)
|
|
10338
|
+
.filter((row) => !(row.publishedAt ?? row.published_at))
|
|
10339
|
+
.map((row) => Number(row.version))
|
|
10340
|
+
.filter((v) => Number.isFinite(v));
|
|
10341
|
+
return draftVersions.length > 0 ? Math.max(...draftVersions) : null;
|
|
10342
|
+
}
|
|
10343
|
+
|
|
10025
10344
|
function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
|
|
10026
10345
|
if (!widgetId) return null;
|
|
10027
10346
|
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
@@ -10049,6 +10368,11 @@ Requirements:
|
|
|
10049
10368
|
const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
|
|
10050
10369
|
if (url) return url;
|
|
10051
10370
|
}
|
|
10371
|
+
// The newest draft IS repo HEAD — Worker `/preview/{widgetId}` already serves this code!
|
|
10372
|
+
const newestDraft = getNewestDraftVersionId(widgetId);
|
|
10373
|
+
if (!isPublished && newestDraft != null && version === newestDraft && workerUrl) {
|
|
10374
|
+
return workerUrl;
|
|
10375
|
+
}
|
|
10052
10376
|
return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
|
|
10053
10377
|
}
|
|
10054
10378
|
return null;
|
|
@@ -10074,6 +10398,13 @@ Requirements:
|
|
|
10074
10398
|
if (!Number.isFinite(version)) return null;
|
|
10075
10399
|
return `v${version}`;
|
|
10076
10400
|
}
|
|
10401
|
+
// 'preview' = Worker HEAD = newest generated (draft) version. Showing the
|
|
10402
|
+
// Live version here hid every fresh generation behind the old artifact.
|
|
10403
|
+
if (source === 'preview') {
|
|
10404
|
+
const newestDraft = getNewestDraftVersionId(widgetId);
|
|
10405
|
+
if (newestDraft != null) return `v${newestDraft}`;
|
|
10406
|
+
return null;
|
|
10407
|
+
}
|
|
10077
10408
|
return liveVersion != null ? `v${liveVersion}` : null;
|
|
10078
10409
|
}
|
|
10079
10410
|
|
|
@@ -10160,6 +10491,13 @@ Requirements:
|
|
|
10160
10491
|
function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
|
|
10161
10492
|
const source = shapePreviewSourceByWidgetId[widgetId];
|
|
10162
10493
|
if (source?.startsWith('v:')) return source;
|
|
10494
|
+
if (source === 'preview') {
|
|
10495
|
+
// Worker HEAD preview = the newest draft — keep the dropdown selection
|
|
10496
|
+
// on it, otherwise the picker kept pointing at the stale Live version.
|
|
10497
|
+
const newestDraft = getNewestDraftVersionId(widgetId);
|
|
10498
|
+
if (newestDraft != null) return `v:${newestDraft}`;
|
|
10499
|
+
return null;
|
|
10500
|
+
}
|
|
10163
10501
|
const ps = getPanelDisplayState(widgetId);
|
|
10164
10502
|
const liveVersion =
|
|
10165
10503
|
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
@@ -10191,9 +10529,12 @@ Requirements:
|
|
|
10191
10529
|
// Published artifacts need no build. Drafts must never substitute HEAD.
|
|
10192
10530
|
if (versionId.startsWith('v:')) {
|
|
10193
10531
|
const version = Number(versionId.slice(2));
|
|
10194
|
-
|
|
10532
|
+
const directUrl = resolveWidgetShapeLivePreviewUrl(widgetId);
|
|
10533
|
+
if (directUrl) {
|
|
10195
10534
|
const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
|
|
10196
10535
|
console.log('[VersionPreview] Selection', { widgetId, version, commitSha: record?.commitSha ?? record?.commit_sha ?? null, source: 'published' });
|
|
10536
|
+
const detailsRef = getWidgetDetailsRef(widgetId);
|
|
10537
|
+
detailsRef?.showPublishedArtifact?.(directUrl);
|
|
10197
10538
|
} else {
|
|
10198
10539
|
void ensureWidgetShapeVersionPreview(widgetId, version);
|
|
10199
10540
|
}
|
|
@@ -10238,12 +10579,17 @@ Requirements:
|
|
|
10238
10579
|
}
|
|
10239
10580
|
shapeVersionPreviewByWidgetId = {
|
|
10240
10581
|
...shapeVersionPreviewByWidgetId,
|
|
10241
|
-
[widgetId]: {
|
|
10582
|
+
[widgetId]: {
|
|
10583
|
+
versionId, requestId, loading: false, error: null, url: previewUrl,
|
|
10584
|
+
commitSha: normalizeCommitSha(commitSha),
|
|
10585
|
+
},
|
|
10242
10586
|
};
|
|
10243
10587
|
syncWidgetShapeLivePreviews();
|
|
10244
10588
|
panelDisplayRevision += 1;
|
|
10245
10589
|
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${widgetId}:${versionId}`]: null };
|
|
10246
10590
|
console.log('[VersionPreview] Selection', { widgetId, version, commitSha });
|
|
10591
|
+
const detailsRef = getWidgetDetailsRef(widgetId);
|
|
10592
|
+
detailsRef?.showPublishedArtifact?.(previewUrl);
|
|
10247
10593
|
} catch (error: unknown) {
|
|
10248
10594
|
if (
|
|
10249
10595
|
shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
@@ -10275,6 +10621,14 @@ Requirements:
|
|
|
10275
10621
|
if (source.startsWith('v:')) {
|
|
10276
10622
|
const parsed = parseInt(source.slice(2), 10);
|
|
10277
10623
|
version = Number.isFinite(parsed) ? parsed : null;
|
|
10624
|
+
// A draft version has no publish-history row to read a commit from, so
|
|
10625
|
+
// without the sha the version preview reported we fell back to repo HEAD
|
|
10626
|
+
// and the badge stayed on the newest turn instead of moving to the
|
|
10627
|
+
// selected version's own turn.
|
|
10628
|
+
const previewState = shapeVersionPreviewByWidgetId[widgetId];
|
|
10629
|
+
if (previewState?.versionId === source && previewState.commitSha) {
|
|
10630
|
+
return normalizeCommitSha(previewState.commitSha);
|
|
10631
|
+
}
|
|
10278
10632
|
} else {
|
|
10279
10633
|
version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
10280
10634
|
}
|
|
@@ -10293,28 +10647,84 @@ Requirements:
|
|
|
10293
10647
|
return normalizeCommitSha(ps.lastCommitId);
|
|
10294
10648
|
}
|
|
10295
10649
|
|
|
10650
|
+
/**
|
|
10651
|
+
* Head commit per open panel, for the chat's Head-at-commit badge.
|
|
10652
|
+
*
|
|
10653
|
+
* The resolver reads the selected version and its preview state out of records
|
|
10654
|
+
* the compiler cannot follow through the call, so calling it straight from the
|
|
10655
|
+
* template never re-ran on a version switch: the chat kept the previous
|
|
10656
|
+
* commit and the badge stayed on the newest turn. Naming the records here is
|
|
10657
|
+
* what makes the switch reach the chat.
|
|
10658
|
+
*/
|
|
10659
|
+
let headCommitShaByWidgetId: Record<string, string | null> = {};
|
|
10660
|
+
/**
|
|
10661
|
+
* Short commit sha → "Version N" per panel, so a chat turn reads as the version
|
|
10662
|
+
* it produced. A hash is meaningless to a non-developer, so it stays behind
|
|
10663
|
+
* debug mode and this label is what the turn shows.
|
|
10664
|
+
*/
|
|
10665
|
+
let versionLabelsByWidgetId: Record<string, Record<string, string>> = {};
|
|
10666
|
+
$: {
|
|
10667
|
+
void shapePreviewSourceByWidgetId;
|
|
10668
|
+
void shapeVersionPreviewByWidgetId;
|
|
10669
|
+
void panelDisplayRevision;
|
|
10670
|
+
const nextHeads: Record<string, string | null> = {};
|
|
10671
|
+
const nextLabels: Record<string, Record<string, string>> = {};
|
|
10672
|
+
for (const widgetId of openWidgetPanelIds) {
|
|
10673
|
+
nextHeads[widgetId] = resolveViewingCommitShaForPanel(widgetId);
|
|
10674
|
+
nextLabels[widgetId] = buildVersionLabelsForPanel(widgetId);
|
|
10675
|
+
}
|
|
10676
|
+
headCommitShaByWidgetId = nextHeads;
|
|
10677
|
+
versionLabelsByWidgetId = nextLabels;
|
|
10678
|
+
}
|
|
10679
|
+
|
|
10680
|
+
/** Commit shas are keyed on 7 chars — that is what the chat summaries carry. */
|
|
10681
|
+
function versionLabelKey(sha: string | null | undefined): string | null {
|
|
10682
|
+
const normalized = normalizeCommitSha(sha);
|
|
10683
|
+
return normalized ? normalized.slice(0, 7) : null;
|
|
10684
|
+
}
|
|
10685
|
+
|
|
10686
|
+
function buildVersionLabelsForPanel(widgetId: string): Record<string, string> {
|
|
10687
|
+
const labels: Record<string, string> = {};
|
|
10688
|
+
const ps = getPanelDisplayState(widgetId);
|
|
10689
|
+
const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
|
|
10690
|
+
for (const record of history || []) {
|
|
10691
|
+
const key = versionLabelKey(record?.commitSha ?? record?.commit_sha);
|
|
10692
|
+
if (key && record?.version != null) labels[key] = `Version ${record.version}`;
|
|
10693
|
+
}
|
|
10694
|
+
// The version the user is previewing right now may have been fetched rather
|
|
10695
|
+
// than read from history, and it is the one whose label matters most.
|
|
10696
|
+
const previewState = shapeVersionPreviewByWidgetId[widgetId];
|
|
10697
|
+
const previewKey = versionLabelKey(previewState?.commitSha);
|
|
10698
|
+
if (previewKey && previewState?.versionId?.startsWith('v:')) {
|
|
10699
|
+
labels[previewKey] = `Version ${previewState.versionId.slice(2)}`;
|
|
10700
|
+
}
|
|
10701
|
+
return labels;
|
|
10702
|
+
}
|
|
10703
|
+
|
|
10296
10704
|
function syncWidgetShapeLivePreviews(): void {
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
|
|
10310
|
-
for (const widgetId of linkedIds) {
|
|
10311
|
-
if (!liveWidgetIds.has(widgetId)) {
|
|
10312
|
-
canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
|
|
10705
|
+
measureSync('sync-widget-shape-previews', () => {
|
|
10706
|
+
try {
|
|
10707
|
+
canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
|
|
10708
|
+
widgetShapeLivePreviewsRef?.sync?.();
|
|
10709
|
+
// Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
|
|
10710
|
+
const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
|
|
10711
|
+
const liveWidgetIds = new Set<string>();
|
|
10712
|
+
for (const rect of rects) {
|
|
10713
|
+
if (!rect?.widgetId) continue;
|
|
10714
|
+
if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
|
|
10715
|
+
liveWidgetIds.add(rect.widgetId);
|
|
10716
|
+
canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
|
|
10313
10717
|
}
|
|
10718
|
+
const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
|
|
10719
|
+
for (const widgetId of linkedIds) {
|
|
10720
|
+
if (!liveWidgetIds.has(widgetId)) {
|
|
10721
|
+
canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
|
|
10722
|
+
}
|
|
10723
|
+
}
|
|
10724
|
+
} catch (err) {
|
|
10725
|
+
console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
|
|
10314
10726
|
}
|
|
10315
|
-
}
|
|
10316
|
-
console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
|
|
10317
|
-
}
|
|
10727
|
+
});
|
|
10318
10728
|
}
|
|
10319
10729
|
|
|
10320
10730
|
function getPanelEmbedPreviewUrl(widgetId: string): string | null {
|
|
@@ -11729,6 +12139,17 @@ $: if ($agentHarnessOutput?.timestamp) {
|
|
|
11729
12139
|
}
|
|
11730
12140
|
}
|
|
11731
12141
|
|
|
12142
|
+
// Worker preview ready via WebSocket event — reload canvas shape preview immediately
|
|
12143
|
+
$: if ($dynamicWorkerPreviewStatus.widgetId && $dynamicWorkerPreviewStatus.previewUrl) {
|
|
12144
|
+
const wsReadyWidgetId = $dynamicWorkerPreviewStatus.widgetId;
|
|
12145
|
+
shapePreviewSourceByWidgetId = {
|
|
12146
|
+
...shapePreviewSourceByWidgetId,
|
|
12147
|
+
[wsReadyWidgetId]: 'preview',
|
|
12148
|
+
};
|
|
12149
|
+
syncWidgetShapeLivePreviews();
|
|
12150
|
+
widgetShapeLivePreviewsRef?.requestReload?.(wsReadyWidgetId);
|
|
12151
|
+
}
|
|
12152
|
+
|
|
11732
12153
|
// ===================== Publish History =====================
|
|
11733
12154
|
|
|
11734
12155
|
/**
|
|
@@ -11749,13 +12170,12 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
|
|
|
11749
12170
|
});
|
|
11750
12171
|
return;
|
|
11751
12172
|
}
|
|
11752
|
-
try {
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
: []);
|
|
12173
|
+
try { const historyRes = await widgetsClient.getWidgetPublishHistory({ widgetId });
|
|
12174
|
+
const historyEnvelope = historyRes as unknown as { data?: unknown };
|
|
12175
|
+
// SDK types the envelope ({ success, data }); a response-transform middleware
|
|
12176
|
+
// may unwrap it, so a bare array stays tolerated.
|
|
12177
|
+
const historyData = Array.isArray(historyRes) ? historyRes : historyEnvelope?.data;
|
|
12178
|
+
const history = (Array.isArray(historyData) ? historyData : []) as PublishHistoryRecord[];
|
|
11759
12179
|
patchPanelSnapshot(widgetId, {
|
|
11760
12180
|
publishHistory: history,
|
|
11761
12181
|
publishHistoryResolved: true,
|
|
@@ -11893,7 +12313,9 @@ function togglePublishHistory(triggerEl?: HTMLElement | null) {
|
|
|
11893
12313
|
const cluster = triggerEl.closest('.wc-version-history-ct') as HTMLElement | null;
|
|
11894
12314
|
const rect = (cluster ?? triggerEl).getBoundingClientRect();
|
|
11895
12315
|
liveHistoryPanelTop = Math.round(rect.bottom + 4);
|
|
11896
|
-
|
|
12316
|
+
// Rows inset their action button 12px from the panel edge (panel p-1 4px + row px-2 8px).
|
|
12317
|
+
// Shift right by 12px so the per-row action buttons line up under the header action button.
|
|
12318
|
+
liveHistoryPanelRight = Math.round(window.innerWidth - rect.right) - 12;
|
|
11897
12319
|
}
|
|
11898
12320
|
loadPublishHistory();
|
|
11899
12321
|
}
|
|
@@ -12053,8 +12475,12 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12053
12475
|
// console.log('Signup response:', signupResponse);
|
|
12054
12476
|
|
|
12055
12477
|
// show error signup status
|
|
12056
|
-
|
|
12057
|
-
|
|
12478
|
+
// signupUser is typed Promise<void>; it resolves to undefined on the
|
|
12479
|
+
// success path, so a falsy response IS the success signal. Error details
|
|
12480
|
+
// arrive via the thrown ResponseError caught below.
|
|
12481
|
+
const signupEnvelope = signupResponse as unknown as { user?: unknown; message?: string } | undefined;
|
|
12482
|
+
if (!signupEnvelope || !signupEnvelope.user) {
|
|
12483
|
+
authStatus = signupEnvelope?.message ?? 'Signup failed.';
|
|
12058
12484
|
return;
|
|
12059
12485
|
}
|
|
12060
12486
|
|
|
@@ -12076,7 +12502,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12076
12502
|
// console.log('Login response:', loginResponse);
|
|
12077
12503
|
|
|
12078
12504
|
// show success login status
|
|
12079
|
-
|
|
12505
|
+
// The generated loginUser return type is mis-derived (a billing endpoint's
|
|
12506
|
+
// shape); the real /auth/login payload is a session envelope. Cast honestly.
|
|
12507
|
+
const payload = (loginResponse as unknown as { data?: { accessToken?: string; user?: Record<string, unknown> } })?.data;
|
|
12080
12508
|
if (payload?.accessToken) {
|
|
12081
12509
|
currentUserToken = payload.accessToken;
|
|
12082
12510
|
|
|
@@ -12142,7 +12570,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12142
12570
|
// console.log('WidgetCreator.updateTokenStatus: Decoded token:', decoded);
|
|
12143
12571
|
|
|
12144
12572
|
// if the decoded token has an expiration field, update the token status
|
|
12145
|
-
if (decoded && decoded.exp) {
|
|
12573
|
+
if (decoded && typeof decoded.exp === 'number') {
|
|
12146
12574
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
12147
12575
|
const expUnix = decoded.exp;
|
|
12148
12576
|
tokenTimeLeft = expUnix - currentTime;
|
|
@@ -12316,17 +12744,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12316
12744
|
// console.log('Available fields:', Object.keys(decodedToken));
|
|
12317
12745
|
|
|
12318
12746
|
// Extract user info from Supabase JWT structure
|
|
12747
|
+
// user_metadata is an untyped JWT claim blob — index it explicitly so the
|
|
12748
|
+
// accesses below are checked reads, not property probes on `unknown`.
|
|
12749
|
+
const userMetadata = (decodedToken.user_metadata ?? {}) as Record<string, unknown>;
|
|
12319
12750
|
user = {
|
|
12320
12751
|
id: decodedToken.sub || decodedToken.user_id || decodedToken.id || 'token-user',
|
|
12321
|
-
email:
|
|
12752
|
+
email: userMetadata.email || 'user@token-auth.com',
|
|
12322
12753
|
name:
|
|
12323
|
-
|
|
12324
|
-
|
|
12754
|
+
userMetadata.name ||
|
|
12755
|
+
userMetadata.full_name ||
|
|
12325
12756
|
decodedToken.name ||
|
|
12326
12757
|
decodedToken.full_name ||
|
|
12327
12758
|
'Token User',
|
|
12328
12759
|
role: decodedToken.role || 'user',
|
|
12329
|
-
avatar:
|
|
12760
|
+
avatar: userMetadata.avatar_url || userMetadata.picture
|
|
12330
12761
|
};
|
|
12331
12762
|
|
|
12332
12763
|
// Show more detailed status with actual extracted info
|
|
@@ -12385,6 +12816,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12385
12816
|
*/
|
|
12386
12817
|
const shapeThumbnailCaptureInFlight = new Set<string>();
|
|
12387
12818
|
|
|
12819
|
+
/**
|
|
12820
|
+
* Whole-function single-flight guard for handleRefreshScreenshot.
|
|
12821
|
+
* Stacked html2canvas captures run inside the preview iframe, which shares
|
|
12822
|
+
* the page main thread — a capture storm froze the entire site with no
|
|
12823
|
+
* parent-page [FreezeReport] (long tasks attributed to the iframe).
|
|
12824
|
+
*/
|
|
12825
|
+
const screenshotCaptureInFlight = new Set<string>();
|
|
12826
|
+
|
|
12388
12827
|
/**
|
|
12389
12828
|
* Called when a Widget Image shape is clicked on the canvas.
|
|
12390
12829
|
* Selects the linked widget and opens WidgetDetails.
|
|
@@ -12463,7 +12902,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12463
12902
|
notifyHostWidgetName(widget);
|
|
12464
12903
|
}
|
|
12465
12904
|
await openWidgetDetailsPanel(widgetId);
|
|
12466
|
-
scheduleCanvasShapePreviewSync(widgetId);
|
|
12467
12905
|
}
|
|
12468
12906
|
|
|
12469
12907
|
/**
|
|
@@ -12566,10 +13004,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12566
13004
|
}
|
|
12567
13005
|
|
|
12568
13006
|
const sourceHasCode =
|
|
12569
|
-
!!normalizeRepositoryId(newWidget)
|
|
13007
|
+
!!normalizeRepositoryId(asWidgetSummary(newWidget))
|
|
12570
13008
|
|| !!((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
|
|
12571
13009
|
?? (newWidget as { js_path?: string | null }).js_path)
|
|
12572
|
-
|| resolvePublishedVersionFromWidget(newWidget) !== null
|
|
13010
|
+
|| resolvePublishedVersionFromWidget(asWidgetSummary(newWidget)) !== null
|
|
12573
13011
|
|| !!sourceCommit;
|
|
12574
13012
|
|
|
12575
13013
|
if (!canvasPrimaryWidgetId) {
|
|
@@ -12577,7 +13015,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12577
13015
|
}
|
|
12578
13016
|
|
|
12579
13017
|
// Duplicate stays a sibling on the canvas — do not make it the primary widget.
|
|
12580
|
-
await selectWidget(newWidget, { skipPlaceShape: true });
|
|
13018
|
+
await selectWidget(asWidgetSummary(newWidget), { skipPlaceShape: true });
|
|
12581
13019
|
notifyHostWidgetName(getWidgetById(canvasPrimaryWidgetId) ?? getWidgetById(sourceWidgetId));
|
|
12582
13020
|
|
|
12583
13021
|
showToast('success', 'Widget duplicated', {
|
|
@@ -12607,11 +13045,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12607
13045
|
try {
|
|
12608
13046
|
await handleWidgetDataChanged(newWidget.id);
|
|
12609
13047
|
const freshWidget = getWidgetById(newWidget.id);
|
|
12610
|
-
|
|
13048
|
+
let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(asWidgetSummary(newWidget));
|
|
12611
13049
|
|
|
12612
13050
|
if (!repoId) {
|
|
12613
13051
|
// console.log('[WidgetCreator] Duplicate has no repo after re-fetch, creating one...');
|
|
12614
|
-
repoId = await ensureWidgetRepository(freshWidget || newWidget, { silent: true });
|
|
13052
|
+
repoId = await ensureWidgetRepository(freshWidget || asWidgetSummary(newWidget), { silent: true });
|
|
12615
13053
|
if (repoId) {
|
|
12616
13054
|
widgets = widgets.map((w) => w.id === newWidget.id ? { ...w, repositoryId: repoId } : w);
|
|
12617
13055
|
// console.log('[WidgetCreator] Repository created for duplicate:', repoId);
|
|
@@ -13072,14 +13510,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13072
13510
|
}
|
|
13073
13511
|
|
|
13074
13512
|
let objectCount = 0;
|
|
13075
|
-
|
|
13076
|
-
|
|
13077
|
-
|
|
13078
|
-
|
|
13079
|
-
|
|
13080
|
-
|
|
13081
|
-
|
|
13082
|
-
|
|
13513
|
+
// Canvas JSON carries the shapes' embedded images, so it reaches megabytes on
|
|
13514
|
+
// a busy canvas and every parse of it blocks the main thread. Parse once and
|
|
13515
|
+
// reuse it for the API payload below.
|
|
13516
|
+
let parsedCanvas: any = null;
|
|
13517
|
+
measureSync('canvas-autosave:parse', () => {
|
|
13518
|
+
try {
|
|
13519
|
+
parsedCanvas = JSON.parse(json);
|
|
13520
|
+
// saveCanvas() wraps as { version, savedAt, viewport, canvas: { objects: [...] } }
|
|
13521
|
+
const objects = parsedCanvas?.canvas?.objects ?? parsedCanvas?.objects ?? [];
|
|
13522
|
+
objectCount = Array.isArray(objects) ? objects.length : 0;
|
|
13523
|
+
} catch {
|
|
13524
|
+
// ignore parse errors for debug
|
|
13525
|
+
}
|
|
13526
|
+
});
|
|
13083
13527
|
|
|
13084
13528
|
if (isUnmountSave) {
|
|
13085
13529
|
// console.log(`[CanvasDebug] Unmount save: objectCount=${objectCount}, lastSavedCount=${lastSavedCanvasObjectCount}`);
|
|
@@ -13112,11 +13556,18 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13112
13556
|
|
|
13113
13557
|
// Save to durable L1 fallback (always, even with 0 objects for legitimate clears)
|
|
13114
13558
|
saveCanvasState(canvasId, json);
|
|
13115
|
-
// Keep the legacy localStorage copy in sync for downgrade safety.
|
|
13116
|
-
|
|
13117
|
-
|
|
13118
|
-
|
|
13119
|
-
|
|
13559
|
+
// Keep the legacy localStorage copy in sync for downgrade safety. `setItem` is
|
|
13560
|
+
// a synchronous disk write of the whole canvas, so during a generation it is
|
|
13561
|
+
// deferred to the end: the durable IndexedDB copy above is already written,
|
|
13562
|
+
// and the mirror exists only for a downgrade.
|
|
13563
|
+
if (!isAnyWidgetGenerating() || force || isUnmountSave) {
|
|
13564
|
+
measureSync('canvas-autosave:localStorage-mirror', () => {
|
|
13565
|
+
try {
|
|
13566
|
+
localStorage.setItem(getCanvasStorageKey(), json);
|
|
13567
|
+
} catch (err) {
|
|
13568
|
+
console.warn('[WidgetCreator] Failed to save canvas to localStorage:', err);
|
|
13569
|
+
}
|
|
13570
|
+
});
|
|
13120
13571
|
}
|
|
13121
13572
|
|
|
13122
13573
|
// Save to API via SDK with sequence race protection
|
|
@@ -13126,7 +13577,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13126
13577
|
await canvasesClient.updateCanvas({
|
|
13127
13578
|
id: canvasId,
|
|
13128
13579
|
updateCanvasRequest: {
|
|
13129
|
-
canvasContent: JSON.parse(json)
|
|
13580
|
+
canvasContent: parsedCanvas ?? JSON.parse(json)
|
|
13130
13581
|
}
|
|
13131
13582
|
});
|
|
13132
13583
|
if (currentSeq < lastResolvedAutosaveSequence) {
|
|
@@ -13336,13 +13787,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13336
13787
|
{ headers: { 'Idempotency-Key': idempotencyKey } }
|
|
13337
13788
|
);
|
|
13338
13789
|
|
|
13339
|
-
|
|
13790
|
+
const createEnvelope = response as unknown as { error?: string; message?: string };
|
|
13791
|
+
if (!response || createEnvelope.error || createEnvelope.message) {
|
|
13340
13792
|
console.error('[WidgetCreator] Widget create on first send failed:', response);
|
|
13341
13793
|
showToast('error', 'Failed to create widget from design');
|
|
13342
13794
|
return null;
|
|
13343
13795
|
}
|
|
13344
13796
|
|
|
13345
|
-
const newWidget = response.data
|
|
13797
|
+
const newWidget = asWidgetSummary((response as { data?: unknown }).data ?? response);
|
|
13346
13798
|
if (!newWidget?.id) {
|
|
13347
13799
|
showToast('warning', 'Widget created but no ID returned');
|
|
13348
13800
|
return null;
|
|
@@ -13613,7 +14065,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13613
14065
|
}
|
|
13614
14066
|
|
|
13615
14067
|
async function ensureWidgetRepository(
|
|
13616
|
-
widget: WidgetSummary,
|
|
14068
|
+
widget: WidgetSummary | null | undefined,
|
|
13617
14069
|
options: { forceRecreate?: boolean; silent?: boolean } = {}
|
|
13618
14070
|
): Promise<string | null> {
|
|
13619
14071
|
const { forceRecreate = false, silent = false } = options;
|
|
@@ -13738,8 +14190,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13738
14190
|
});
|
|
13739
14191
|
// console.log('Widget Creator: Repository creation response:', response);
|
|
13740
14192
|
|
|
13741
|
-
const repoData = response
|
|
13742
|
-
const repoId = repoData?.id || repoData?.repoId || repoData?.repo_id || null;
|
|
14193
|
+
const repoData = ((response as unknown as { data?: Record<string, unknown> }).data ?? response) as Record<string, unknown>;
|
|
14194
|
+
const repoId = (repoData?.id || repoData?.repoId || repoData?.repo_id || null) as string | null;
|
|
13743
14195
|
|
|
13744
14196
|
if (!repoId) {
|
|
13745
14197
|
showToast('error', 'Repository creation failed', {
|
|
@@ -13826,7 +14278,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13826
14278
|
const response = await widgetsClient.deleteWidget(
|
|
13827
14279
|
{
|
|
13828
14280
|
id: adminDeleteWidget.id,
|
|
13829
|
-
reason: 'admin_delete'
|
|
14281
|
+
deleteWidgetRequest: { reason: 'admin_delete' }
|
|
13830
14282
|
},
|
|
13831
14283
|
{
|
|
13832
14284
|
headers: {
|
|
@@ -13835,10 +14287,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13835
14287
|
}
|
|
13836
14288
|
);
|
|
13837
14289
|
|
|
13838
|
-
|
|
14290
|
+
// deleteWidget resolves void; only a thrown ResponseError (caught below)
|
|
14291
|
+
// carries a failure. The defensive envelope check is kept for the response-
|
|
14292
|
+
// transform middleware, but must go through an honest cast.
|
|
14293
|
+
const deleteEnvelope = response as unknown as { error?: string; message?: string } | undefined;
|
|
14294
|
+
if (deleteEnvelope && (deleteEnvelope.error || deleteEnvelope.message)) {
|
|
13839
14295
|
console.error('Admin widget deletion failed:', response);
|
|
13840
14296
|
showToast('error', 'Delete failed', {
|
|
13841
|
-
description:
|
|
14297
|
+
description: deleteEnvelope?.message || deleteEnvelope?.error || 'Unknown error'
|
|
13842
14298
|
});
|
|
13843
14299
|
adminDeleteLoading = false;
|
|
13844
14300
|
return;
|
|
@@ -14152,6 +14608,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14152
14608
|
onTranslateEnd={(widgetId) => {
|
|
14153
14609
|
canvasRef?.commitWidgetShapeTranslate?.(widgetId);
|
|
14154
14610
|
}}
|
|
14611
|
+
onOverlayDragChange={(active) => {
|
|
14612
|
+
canvasRef?.setWidgetShapeOverlayDragActive?.(active);
|
|
14613
|
+
}}
|
|
14155
14614
|
onSelectShape={(widgetId) => {
|
|
14156
14615
|
canvasRef?.selectWidgetShapeById?.(widgetId);
|
|
14157
14616
|
canvasSelectedWidgetShapeId = widgetId;
|
|
@@ -14161,6 +14620,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14161
14620
|
void openWidgetDetailsPanel(widgetId);
|
|
14162
14621
|
}}
|
|
14163
14622
|
onRefresh={handleWidgetShapeRefresh}
|
|
14623
|
+
isGenerating={isAnyWidgetGenerating()}
|
|
14164
14624
|
/>
|
|
14165
14625
|
|
|
14166
14626
|
<!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
|
|
@@ -14564,8 +15024,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14564
15024
|
if (isPanelFocused && previewWidgetId === panelWidgetId) {
|
|
14565
15025
|
chatMethods?.clearStalePreviewBuildFailures?.();
|
|
14566
15026
|
}
|
|
14567
|
-
// Keep canvas WidgetShape thumbnail aligned with the live Worker preview.
|
|
14568
|
-
scheduleCanvasShapePreviewSync(previewWidgetId);
|
|
14569
15027
|
if (detail?.step === 'create' || ps.widgetDetailsStep === 'create') {
|
|
14570
15028
|
applyPanelCompositionToPreview(previewWidgetId);
|
|
14571
15029
|
}
|
|
@@ -14800,10 +15258,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14800
15258
|
{@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
|
|
14801
15259
|
{@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
|
|
14802
15260
|
? parseInt(shapeSource.slice(2), 10)
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
15261
|
+
: shapeSource === 'preview'
|
|
15262
|
+
// A 'preview' source (Worker HEAD, unpublished) is not a published
|
|
15263
|
+
// version — falling back to panelPublishedVersion produced `vnull`
|
|
15264
|
+
// right after generation AND showed the stale Live version when a
|
|
15265
|
+
// draft existed. The newest draft IS this HEAD's version.
|
|
15266
|
+
? (newestDraft ? newestDraft.version : panelPublishedVersion)
|
|
15267
|
+
: panelPublishedVersion}
|
|
14807
15268
|
{@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
|
|
14808
15269
|
<!--
|
|
14809
15270
|
A version is a Draft until it is published. The history is one
|
|
@@ -14841,13 +15302,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14841
15302
|
}}
|
|
14842
15303
|
disabled={isPublishDisabledForPanel(panelWidgetId) || (isPublishing && publishingWidgetId === panelWidgetId)}
|
|
14843
15304
|
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"
|
|
15305
|
+
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
15306
|
>
|
|
14846
15307
|
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14847
15308
|
</button>
|
|
14848
15309
|
{:else if viewingIsLive}
|
|
14849
15310
|
<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"
|
|
15311
|
+
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
15312
|
title="Live — generate new code to Publish a new version"
|
|
14852
15313
|
>Live</span>
|
|
14853
15314
|
{:else if viewingIsDraft}
|
|
@@ -14863,7 +15324,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14863
15324
|
}}
|
|
14864
15325
|
disabled={!canPublishDraftVersionForPanel(panelWidgetId, viewingVersion) || getPublishGateForPanel(panelWidgetId, Number(viewingVersion)).disabled}
|
|
14865
15326
|
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"
|
|
15327
|
+
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
15328
|
>
|
|
14868
15329
|
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14869
15330
|
</button>
|
|
@@ -14880,7 +15341,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14880
15341
|
}}
|
|
14881
15342
|
disabled={isSettingLiveVersion === Number(viewingVersion)}
|
|
14882
15343
|
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"
|
|
15344
|
+
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
15345
|
>
|
|
14885
15346
|
{isSettingLiveVersion === Number(viewingVersion) ? '...' : 'Set live'}
|
|
14886
15347
|
</button>
|
|
@@ -14893,15 +15354,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14893
15354
|
aria-label="Close version history"
|
|
14894
15355
|
onclick={() => { isHistoryOpen = false; }}
|
|
14895
15356
|
></button>
|
|
15357
|
+
{@const hasChatScroll = ps.publishHistory.length > 5}
|
|
14896
15358
|
<div
|
|
14897
|
-
class="wc-version-history-panel
|
|
14898
|
-
style="position: fixed; top: {liveHistoryPanelTop}px; right: {liveHistoryPanelRight}px;
|
|
15359
|
+
class="wc-version-history-panel rounded-md border border-emerald-200 bg-white shadow-lg p-1 z-[9999] w-max"
|
|
15360
|
+
style="position: fixed; top: {liveHistoryPanelTop}px; right: {liveHistoryPanelRight}px;"
|
|
14899
15361
|
>
|
|
14900
15362
|
{#if isLoadingHistory}
|
|
14901
15363
|
<p class="text-xs text-emerald-500 animate-pulse text-center py-2">Loading history...</p>
|
|
14902
15364
|
{:else if ps.publishHistory.length === 0}
|
|
14903
15365
|
<p class="text-xs text-emerald-400 text-center py-2">No history records yet. Generate or publish to start tracking.</p>
|
|
14904
15366
|
{:else}
|
|
15367
|
+
<div
|
|
15368
|
+
class="wc-version-history-scroll flex flex-col gap-1 overflow-x-hidden {hasChatScroll ? 'overflow-y-auto' : 'overflow-y-visible'}"
|
|
15369
|
+
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;"
|
|
15370
|
+
>
|
|
14905
15371
|
{#each ps.publishHistory as record (record.id)}
|
|
14906
15372
|
{@const rowVersion = Number(record.version)}
|
|
14907
15373
|
{@const rowIsDraft = !(record.publishedAt ?? record.published_at)}
|
|
@@ -14922,6 +15388,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14922
15388
|
publishDisabledReason={publishGate.disabled ? publishGate.reason : null}
|
|
14923
15389
|
setLiveDisabled={isSettingLiveVersion === record.version}
|
|
14924
15390
|
onselect={() => {
|
|
15391
|
+
isHistoryOpen = false;
|
|
14925
15392
|
handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
|
|
14926
15393
|
}}
|
|
14927
15394
|
onpublish={() => {
|
|
@@ -14933,6 +15400,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14933
15400
|
}}
|
|
14934
15401
|
/>
|
|
14935
15402
|
{/each}
|
|
15403
|
+
</div>
|
|
14936
15404
|
{/if}
|
|
14937
15405
|
</div>
|
|
14938
15406
|
</Portal>
|
|
@@ -15021,6 +15489,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15021
15489
|
class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
|
|
15022
15490
|
style="padding: 0;"
|
|
15023
15491
|
use:nativeScrollbarPadding={0}
|
|
15492
|
+
use:pinScrollToBottom
|
|
15024
15493
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
15025
15494
|
>
|
|
15026
15495
|
{#key panelInstanceKey}
|
|
@@ -15028,11 +15497,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15028
15497
|
class="code-generation-chat-host w-full min-h-0 {chatUserHeight == null ? 'flex-1' : 'shrink-0'}"
|
|
15029
15498
|
data-widget-chat-height={panelWidgetId}
|
|
15030
15499
|
style={chatUserHeight == null
|
|
15031
|
-
? `flex: 1 1 auto; min-height: ${
|
|
15500
|
+
? `flex: 1 1 auto; min-height: ${CHAT_AUTO_MIN_HEIGHT}px;`
|
|
15032
15501
|
: `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
|
|
15033
15502
|
>
|
|
15034
15503
|
<Chat class="code-generation-chat w-full h-full rounded-none border-0"
|
|
15035
|
-
style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
|
|
15504
|
+
style="height: 100%; min-height: {chatUserHeight == null ? CHAT_AUTO_MIN_HEIGHT : CHAT_MIN_HEIGHT}px;"
|
|
15036
15505
|
resizeExpandsContainer={true}
|
|
15037
15506
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
15038
15507
|
showLogs={debugMode}
|
|
@@ -15059,7 +15528,8 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15059
15528
|
autoCodeDetection: false,
|
|
15060
15529
|
}
|
|
15061
15530
|
}}
|
|
15062
|
-
widgetHeadCommitSha={
|
|
15531
|
+
widgetHeadCommitSha={headCommitShaByWidgetId[panelWidgetId] || ps.lastCommitId}
|
|
15532
|
+
versionLabelByCommitSha={versionLabelsByWidgetId[panelWidgetId] || {}}
|
|
15063
15533
|
onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
|
|
15064
15534
|
onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
|
|
15065
15535
|
onUploadChatFile={handleChatUploadFile}
|
|
@@ -15271,7 +15741,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15271
15741
|
{/if}
|
|
15272
15742
|
|
|
15273
15743
|
<!-- ─── Schema loading / error / PropsEditor ─── -->
|
|
15274
|
-
{@const panelSchemasLoading = (panelDisplayRevision, ps.compositionSchemasLoading)}
|
|
15744
|
+
{@const panelSchemasLoading = rev(panelDisplayRevision, ps.compositionSchemasLoading)}
|
|
15275
15745
|
{@const panelSchemaLoadLabel = getPanelSchemaLoadLabel(panelWidgetId)}
|
|
15276
15746
|
{#if panelSchemasLoading}
|
|
15277
15747
|
<div class="composition-editor-loading flex flex-col items-center justify-center py-8">
|
|
@@ -15576,7 +16046,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15576
16046
|
{/if}
|
|
15577
16047
|
|
|
15578
16048
|
<!-- Delete Widget Shape Confirmation Dialog -->
|
|
15579
|
-
|
|
16049
|
+
<!-- Dialog.Root (bits-ui AlertDialogRoot) exposes no class prop — styling hooks
|
|
16050
|
+
live on Content/Header below. -->
|
|
16051
|
+
<Dialog.Root bind:open={deleteDialogOpen} onOpenChange={(open) => { if (!open) handleDeleteDialogResponse('cancel'); }}>
|
|
15580
16052
|
<Dialog.Content class="confirm-delete-dialog-content max-w-md bg-white z-9999">
|
|
15581
16053
|
<Dialog.Header class="confirm-delete-dialog-header">
|
|
15582
16054
|
<Dialog.Title>Delete Widget Shape</Dialog.Title>
|
|
@@ -15618,7 +16090,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15618
16090
|
</Dialog.Root>
|
|
15619
16091
|
|
|
15620
16092
|
<!-- Delete Canvas Confirmation Dialog -->
|
|
15621
|
-
<Dialog.Root
|
|
16093
|
+
<Dialog.Root bind:open={showDeleteCanvasDialog} onOpenChange={(open) => { if (!open) { showDeleteCanvasDialog = false; deleteCanvasTarget = null; } }}>
|
|
15622
16094
|
<Dialog.Content class="confirm-delete-canvas-dialog-content max-w-sm bg-white z-9999">
|
|
15623
16095
|
<Dialog.Header class="confirm-delete-canvas-dialog-header">
|
|
15624
16096
|
<Dialog.Title>Delete Canvas</Dialog.Title>
|
|
@@ -15678,7 +16150,6 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15678
16150
|
|
|
15679
16151
|
<!-- Confirm Generation Dialog (shown on convert-to-widget before auto-generating) -->
|
|
15680
16152
|
<Dialog.Root
|
|
15681
|
-
class="confirm-generate-dialog-root"
|
|
15682
16153
|
open={confirmGenerateDialogOpen}
|
|
15683
16154
|
onOpenChange={(open) => {
|
|
15684
16155
|
confirmGenerateDialogOpen = open;
|
|
@@ -15821,21 +16292,24 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
15821
16292
|
outline: none;
|
|
15822
16293
|
box-shadow: none;
|
|
15823
16294
|
}
|
|
15824
|
-
.wc-version-history-
|
|
16295
|
+
.wc-version-history-scroll {
|
|
15825
16296
|
scrollbar-width: thin;
|
|
15826
16297
|
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
|
15827
16298
|
}
|
|
15828
|
-
.wc-version-history-
|
|
15829
|
-
width:
|
|
16299
|
+
.wc-version-history-scroll::-webkit-scrollbar {
|
|
16300
|
+
width: 4px;
|
|
15830
16301
|
background: transparent;
|
|
15831
16302
|
}
|
|
15832
|
-
.wc-version-history-
|
|
16303
|
+
.wc-version-history-scroll::-webkit-scrollbar-track {
|
|
15833
16304
|
background: transparent;
|
|
15834
16305
|
}
|
|
15835
|
-
.wc-version-history-
|
|
16306
|
+
.wc-version-history-scroll::-webkit-scrollbar-thumb {
|
|
15836
16307
|
background: rgba(0, 0, 0, 0.2);
|
|
15837
16308
|
border-radius: 4px;
|
|
15838
16309
|
}
|
|
16310
|
+
.wc-version-history-scroll::-webkit-scrollbar-thumb:hover {
|
|
16311
|
+
background: rgba(0, 0, 0, 0.35);
|
|
16312
|
+
}
|
|
15839
16313
|
.generation-scroll-ct {
|
|
15840
16314
|
scrollbar-width: thin;
|
|
15841
16315
|
scrollbar-color: rgba(0, 0, 0, 0.22) transparent;
|