@widgetic/creator 0.3.59 → 0.3.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CreatorApp.svelte +1952 -339
- package/dist/components/HoverTooltip.svelte +8 -2
- package/dist/components/HoverTooltip.svelte.d.ts +5 -0
- package/dist/components/VersionDropdownRow.svelte +101 -0
- package/dist/components/VersionDropdownRow.svelte.d.ts +29 -0
- package/dist/components/WidgetDetails.svelte +10 -34
- package/dist/components/WidgetDetails.svelte.d.ts +1 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +704 -60
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +91 -35
- package/dist/utils/versionPreviewError.d.ts +9 -0
- package/dist/utils/versionPreviewError.js +19 -0
- package/package.json +1 -1
package/dist/CreatorApp.svelte
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
ResponseError
|
|
29
29
|
} from '@widgetic/api-sdk';
|
|
30
30
|
import type { Middleware } from '@widgetic/api-sdk';
|
|
31
|
+
import { parseVersionPreviewError } from './utils/versionPreviewError.js';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Idempotency middleware (Phase 2 hardening): the gateway rejects
|
|
@@ -147,18 +148,31 @@
|
|
|
147
148
|
const name = (widget.name || '').trim() || 'Untitled Widget';
|
|
148
149
|
// console.log('[CreatorApp] Notify host widget name:', widget.id, name);
|
|
149
150
|
onWidgetRenamed?.({ widgetId: widget.id, name });
|
|
151
|
+
// Also broadcast — mount props can be stale across embed remounts.
|
|
152
|
+
if (typeof window !== 'undefined') {
|
|
153
|
+
window.dispatchEvent(
|
|
154
|
+
new CustomEvent('widgetic:creator-primary-renamed', {
|
|
155
|
+
detail: { widgetId: widget.id, name },
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
150
159
|
}
|
|
151
160
|
|
|
152
161
|
/**
|
|
153
162
|
* Rewrite the embedded URL so a refresh resumes the current canvas instead of
|
|
154
163
|
* re-running the new-widget bootstrap (which creates a blank canvas every time).
|
|
164
|
+
*
|
|
165
|
+
* The canvas id goes into the **path** (`/canvas/<canvasId>`), matching the host route.
|
|
155
166
|
* Uses history.replaceState — no navigation, no host remount.
|
|
156
167
|
*/
|
|
157
168
|
function syncEmbeddedCanvasUrl(canvasIdToSync: string): void {
|
|
158
169
|
if (!embedded || !canvasIdToSync) return;
|
|
159
170
|
try {
|
|
160
171
|
const url = new URL(window.location.href);
|
|
161
|
-
url.
|
|
172
|
+
url.pathname = `/canvas/${encodeURIComponent(canvasIdToSync)}`;
|
|
173
|
+
url.searchParams.delete('canvas_id');
|
|
174
|
+
url.searchParams.delete('canvas-id');
|
|
175
|
+
url.searchParams.delete('widget_id');
|
|
162
176
|
url.searchParams.delete('new_widget');
|
|
163
177
|
window.history.replaceState(window.history.state, '', url.toString());
|
|
164
178
|
// console.log('[CreatorApp] URL synced to canvas:', canvasIdToSync);
|
|
@@ -200,8 +214,11 @@
|
|
|
200
214
|
if (selectedWidget?.id === targetId) {
|
|
201
215
|
selectedWidget = { ...selectedWidget, name: trimmed };
|
|
202
216
|
}
|
|
217
|
+
widgetNamesRevision += 1;
|
|
203
218
|
panelDisplayRevision += 1;
|
|
204
219
|
backfillWidgetShapeMetadata();
|
|
220
|
+
syncWidgetShapeLivePreviews();
|
|
221
|
+
emitCanvasWidgetsList('host-widget-renamed');
|
|
205
222
|
// console.log('[CreatorApp] Applied host widget rename:', targetId.substring(0, 8), trimmed);
|
|
206
223
|
|
|
207
224
|
if (!widgetsClient) return;
|
|
@@ -448,6 +465,8 @@
|
|
|
448
465
|
import SelectorDropdown from './components/SelectorDropdown.svelte';
|
|
449
466
|
import type { DropdownItem } from './components/SelectorDropdown.svelte';
|
|
450
467
|
import WidgetShapeLivePreviews from './components/WidgetShapeLivePreviews.svelte';
|
|
468
|
+
import HoverTooltip from './components/HoverTooltip.svelte';
|
|
469
|
+
import VersionDropdownRow from './components/VersionDropdownRow.svelte';
|
|
451
470
|
|
|
452
471
|
// WIDGET DETAILS COMPONENT — lazy-loaded to reduce peak build memory
|
|
453
472
|
let WidgetDetails: any;
|
|
@@ -558,6 +577,18 @@
|
|
|
558
577
|
// Delete canvas dialog
|
|
559
578
|
let showDeleteCanvasDialog = false;
|
|
560
579
|
let deleteCanvasTarget: { id: string; name: string } | null = null;
|
|
580
|
+
/**
|
|
581
|
+
* Widgets the pending canvas deletion would remove, grouped by project.
|
|
582
|
+
* A canvas can hold widgets from more than one project (moving a widget
|
|
583
|
+
* rewrites `project_id` but leaves `canvas_id`), so the dialog states exactly
|
|
584
|
+
* what is about to be lost instead of only reassuring that it is irreversible.
|
|
585
|
+
*/
|
|
586
|
+
let deleteCanvasPreview: {
|
|
587
|
+
widgetCount: number;
|
|
588
|
+
groups: Array<{ projectName: string; widgetNames: string[] }>;
|
|
589
|
+
} | null = null;
|
|
590
|
+
let deleteCanvasPreviewLoading = false;
|
|
591
|
+
let deleteCanvasError = '';
|
|
561
592
|
|
|
562
593
|
// Widgets state
|
|
563
594
|
interface WidgetSummary {
|
|
@@ -679,6 +710,21 @@
|
|
|
679
710
|
}
|
|
680
711
|
showToast('success', 'Widget deleted from database');
|
|
681
712
|
} catch (err) {
|
|
713
|
+
// A shape can outlive its widget row (deleted from the Projects page or
|
|
714
|
+
// another session). The canvas only needs the shape and local entry gone,
|
|
715
|
+
// so treat "already gone" as success instead of a blocking error — that
|
|
716
|
+
// left orphan shapes impossible to remove from the canvas.
|
|
717
|
+
if (err instanceof ResponseError && err.response?.status === 404) {
|
|
718
|
+
console.warn('[WidgetCreator] Widget already absent from DB — removing locally:', widgetId);
|
|
719
|
+
widgets = widgets.filter((w: any) => w.id !== widgetId);
|
|
720
|
+
if (selectedWidgetId === widgetId) {
|
|
721
|
+
selectedWidgetId = null;
|
|
722
|
+
}
|
|
723
|
+
showToast('info', 'Widget was already deleted', {
|
|
724
|
+
description: 'Removed the leftover shape from this canvas.',
|
|
725
|
+
});
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
682
728
|
console.error('[WidgetCreator] Failed to delete widget from DB:', err);
|
|
683
729
|
showToast('error', 'Failed to delete widget from database');
|
|
684
730
|
}
|
|
@@ -890,7 +936,10 @@
|
|
|
890
936
|
contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
|
|
891
937
|
source: options?.source || 'chat-upload',
|
|
892
938
|
widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
|
|
893
|
-
conversationId:
|
|
939
|
+
conversationId:
|
|
940
|
+
currentChatConversationId && !isLocalOnlyConversationId(currentChatConversationId)
|
|
941
|
+
? currentChatConversationId
|
|
942
|
+
: undefined,
|
|
894
943
|
},
|
|
895
944
|
{ onProgress: options?.onProgress },
|
|
896
945
|
);
|
|
@@ -1287,14 +1336,15 @@
|
|
|
1287
1336
|
return dataUrls.slice(0, 1);
|
|
1288
1337
|
}
|
|
1289
1338
|
|
|
1290
|
-
async function persistWidgetListThumbnail(widgetId: string, imageDataUrl: string): Promise<void> {
|
|
1291
|
-
if (!widgetsClient || !widgetId || !imageDataUrl) return;
|
|
1339
|
+
async function persistWidgetListThumbnail(widgetId: string, imageDataUrl: string, isCurrent = () => true): Promise<void> {
|
|
1340
|
+
if (!widgetsClient || !widgetId || !imageDataUrl || !isCurrent()) return;
|
|
1292
1341
|
try {
|
|
1293
1342
|
const compact = imageDataUrl.startsWith('data:image/')
|
|
1294
1343
|
? await compressImageDataUrlForCodegen(imageDataUrl, 384, 180000)
|
|
1295
1344
|
: imageDataUrl;
|
|
1345
|
+
if (!isCurrent()) return;
|
|
1296
1346
|
const thumbnailUrl = await persistWidgetThumbnail(widgetsClient, widgetId, compact);
|
|
1297
|
-
if (thumbnailUrl) {
|
|
1347
|
+
if (thumbnailUrl && isCurrent()) {
|
|
1298
1348
|
widgets = widgets.map((w) =>
|
|
1299
1349
|
w.id === widgetId ? { ...w, thumbnailUrl, thumbnail_url: thumbnailUrl } : w
|
|
1300
1350
|
);
|
|
@@ -1485,40 +1535,205 @@
|
|
|
1485
1535
|
// console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
|
|
1486
1536
|
}
|
|
1487
1537
|
|
|
1488
|
-
/**
|
|
1489
|
-
|
|
1538
|
+
/** Live WidgetShape iframe only — never the hidden panel preview column. */
|
|
1539
|
+
/**
|
|
1540
|
+
* Find the live preview iframe for a widget.
|
|
1541
|
+
*
|
|
1542
|
+
* When a widget id is given we must return *that* widget's iframe or nothing
|
|
1543
|
+
* at all. Falling back to "the first widget-shape iframe on the page" meant a
|
|
1544
|
+
* screenshot request for widget A could capture widget B's preview and store it
|
|
1545
|
+
* as A's thumbnail — which is how a gallery thumbnail ended up on an unrelated
|
|
1546
|
+
* widget in the Projects list.
|
|
1547
|
+
*/
|
|
1548
|
+
function resolveLiveWidgetShapeIframe(widgetId: string | null | undefined): HTMLIFrameElement | null {
|
|
1490
1549
|
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1550
|
+
if (targetId) {
|
|
1551
|
+
const liveById = document.querySelector(
|
|
1552
|
+
`iframe.widget-shape-live-preview-iframe[data-widget-id="${CSS.escape(targetId)}"]`,
|
|
1553
|
+
) as HTMLIFrameElement | null;
|
|
1554
|
+
return liveById?.contentWindow ? liveById : null;
|
|
1555
|
+
}
|
|
1556
|
+
// No id known: only unambiguous when exactly one shape is mounted.
|
|
1557
|
+
const all = Array.from(
|
|
1558
|
+
document.querySelectorAll('iframe.widget-shape-live-preview-iframe'),
|
|
1559
|
+
) as HTMLIFrameElement[];
|
|
1560
|
+
const withWindow = all.filter((frame) => !!frame.contentWindow);
|
|
1561
|
+
return withWindow.length === 1 ? withWindow[0] : null;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function isWorkerPreviewUrl(url: string | null | undefined): boolean {
|
|
1565
|
+
if (!url) return false;
|
|
1566
|
+
try {
|
|
1567
|
+
const u = new URL(url, window.location.origin);
|
|
1568
|
+
return /\/preview\/[^/]+/i.test(u.pathname) || u.pathname.includes('/__widget_builder__/');
|
|
1569
|
+
} catch {
|
|
1570
|
+
return /\/preview\//i.test(url);
|
|
1502
1571
|
}
|
|
1572
|
+
}
|
|
1503
1573
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1574
|
+
function requestIframeScreenshot(
|
|
1575
|
+
iframe: HTMLIFrameElement,
|
|
1576
|
+
timeoutMs = 6000,
|
|
1577
|
+
): Promise<{ data: string | null; error: string | null }> {
|
|
1578
|
+
return new Promise((resolve) => {
|
|
1579
|
+
if (!iframe.contentWindow) {
|
|
1580
|
+
resolve({ data: null, error: 'no-content-window' });
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1506
1583
|
const timeout = setTimeout(() => {
|
|
1507
1584
|
window.removeEventListener('message', handler);
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
}, 8000);
|
|
1585
|
+
resolve({ data: null, error: 'timeout' });
|
|
1586
|
+
}, timeoutMs);
|
|
1511
1587
|
|
|
1512
1588
|
function handler(event: MessageEvent) {
|
|
1513
1589
|
if (event.data?.type !== 'widgetic:screenshot') return;
|
|
1590
|
+
// Ignore other widgets and synthetic replies with no source window.
|
|
1591
|
+
if (event.source !== iframe.contentWindow) return;
|
|
1514
1592
|
clearTimeout(timeout);
|
|
1515
1593
|
window.removeEventListener('message', handler);
|
|
1516
|
-
resolve(
|
|
1594
|
+
resolve({
|
|
1595
|
+
data: typeof event.data.data === 'string' ? event.data.data : null,
|
|
1596
|
+
error: typeof event.data.error === 'string' ? event.data.error : null,
|
|
1597
|
+
});
|
|
1517
1598
|
}
|
|
1518
1599
|
window.addEventListener('message', handler);
|
|
1519
|
-
iframe.contentWindow
|
|
1600
|
+
iframe.contentWindow.postMessage({ type: 'widgetic:request-screenshot' }, '*');
|
|
1520
1601
|
});
|
|
1521
1602
|
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Published CDN artifacts often lack the Worker screenshot script.
|
|
1606
|
+
* Capture via a temporary same-path Worker /preview iframe (has html2canvas handler).
|
|
1607
|
+
*/
|
|
1608
|
+
async function captureViaHiddenWorkerPreview(widgetId: string): Promise<string | null> {
|
|
1609
|
+
const workerUrl = `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
|
|
1610
|
+
console.log('[Chat→Screenshot] Capturing via hidden Worker preview', {
|
|
1611
|
+
widgetId: widgetId.substring(0, 8),
|
|
1612
|
+
workerUrl,
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
const host = document.createElement('div');
|
|
1616
|
+
host.className = 'wc-hidden-screenshot-host';
|
|
1617
|
+
host.setAttribute(
|
|
1618
|
+
'style',
|
|
1619
|
+
'position:fixed;left:-10000px;top:0;width:800px;height:600px;opacity:0;pointer-events:none;z-index:-1;',
|
|
1620
|
+
);
|
|
1621
|
+
const iframe = document.createElement('iframe');
|
|
1622
|
+
iframe.title = 'Hidden widget screenshot';
|
|
1623
|
+
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms');
|
|
1624
|
+
iframe.style.cssText = 'width:800px;height:600px;border:0;';
|
|
1625
|
+
host.appendChild(iframe);
|
|
1626
|
+
document.body.appendChild(host);
|
|
1627
|
+
|
|
1628
|
+
try {
|
|
1629
|
+
await new Promise<void>((resolve, reject) => {
|
|
1630
|
+
const timeout = setTimeout(() => reject(new Error('worker-preview-load-timeout')), 12000);
|
|
1631
|
+
iframe.onload = () => {
|
|
1632
|
+
clearTimeout(timeout);
|
|
1633
|
+
resolve();
|
|
1634
|
+
};
|
|
1635
|
+
iframe.onerror = () => {
|
|
1636
|
+
clearTimeout(timeout);
|
|
1637
|
+
reject(new Error('worker-preview-load-error'));
|
|
1638
|
+
};
|
|
1639
|
+
iframe.src = workerUrl;
|
|
1640
|
+
});
|
|
1641
|
+
// Give injected screenshot handler + images a beat to settle
|
|
1642
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
1643
|
+
const result = await requestIframeScreenshot(iframe, 12000);
|
|
1644
|
+
if (result.data) return result.data;
|
|
1645
|
+
console.warn('[Chat→Screenshot] Hidden Worker capture failed:', result.error);
|
|
1646
|
+
return null;
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
console.warn('[Chat→Screenshot] Hidden Worker preview error:', err);
|
|
1649
|
+
return null;
|
|
1650
|
+
} finally {
|
|
1651
|
+
host.remove();
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/** Explicit Refresh only: capture the loaded frame without opening a panel or publishing. */
|
|
1656
|
+
async function handleWidgetShapeRefresh(widgetId, iframe, isCurrentFrame) {
|
|
1657
|
+
const capturedCanvasId = getEffectiveCanvasId();
|
|
1658
|
+
const capturedCanvas = canvasRef;
|
|
1659
|
+
const capturedSource = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
1660
|
+
// Frame/intent identity owns this capture, not focused-panel HEAD hydration
|
|
1661
|
+
// or another widget's global generation operation.
|
|
1662
|
+
const isCurrent = () => isCurrentFrame() && !!getWidgetById(widgetId)
|
|
1663
|
+
&& !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId]
|
|
1664
|
+
&& capturedCanvasId === getEffectiveCanvasId() && capturedCanvas === canvasRef
|
|
1665
|
+
&& capturedSource === (shapePreviewSourceByWidgetId[widgetId] || 'live')
|
|
1666
|
+
&& resolveLiveWidgetShapeIframe(widgetId) === iframe;
|
|
1667
|
+
if (!isCurrent()) return;
|
|
1668
|
+
try {
|
|
1669
|
+
const screenshot = await handleChatScreenshot(widgetId, iframe);
|
|
1670
|
+
if (!screenshot || !isCurrent()) return;
|
|
1671
|
+
// Persist before Fabric/autosave side effects can resync the live overlay.
|
|
1672
|
+
await persistWidgetListThumbnail(widgetId, screenshot, isCurrent);
|
|
1673
|
+
if (!isCurrent()) return;
|
|
1674
|
+
capturedCanvas?.updateWidgetImage(widgetId, screenshot);
|
|
1675
|
+
const canvasJson = capturedCanvas?.saveCanvas?.();
|
|
1676
|
+
if (canvasJson) void handleCanvasAutoSave(canvasJson);
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
console.warn('[WidgetShape] Refresh thumbnail capture failed', widgetId, error);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/** Screenshot: capture the live WidgetShape iframe (CDN or Worker), not Fabric wireframe. */
|
|
1683
|
+
async function handleChatScreenshot(widgetId?: string | null, requiredIframe: HTMLIFrameElement | null = null): Promise<string | null> {
|
|
1684
|
+
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1685
|
+
if (!targetId) {
|
|
1686
|
+
console.warn('[Chat→Screenshot] No widget id');
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const liveIframe = requiredIframe ?? resolveLiveWidgetShapeIframe(targetId);
|
|
1691
|
+
if (requiredIframe && (requiredIframe !== resolveLiveWidgetShapeIframe(targetId) || !requiredIframe.isConnected)) return null;
|
|
1692
|
+
const liveSrc = liveIframe
|
|
1693
|
+
? liveIframe.getAttribute('src') || liveIframe.src || ''
|
|
1694
|
+
: '';
|
|
1695
|
+
|
|
1696
|
+
// 1) Always try the visible live iframe first (published HTML from Worker includes screenshot script)
|
|
1697
|
+
if (liveIframe?.contentWindow) {
|
|
1698
|
+
console.log('[Chat→Screenshot] Live WidgetShape iframe', {
|
|
1699
|
+
targetId: targetId.substring(0, 8),
|
|
1700
|
+
isWorker: isWorkerPreviewUrl(liveSrc),
|
|
1701
|
+
src: liveSrc.slice(0, 120),
|
|
1702
|
+
});
|
|
1703
|
+
const fromLive = await requestIframeScreenshot(liveIframe, 12000);
|
|
1704
|
+
if (fromLive.data && fromLive.data.length > 500) {
|
|
1705
|
+
return fromLive.data;
|
|
1706
|
+
}
|
|
1707
|
+
console.warn('[Chat→Screenshot] Live iframe capture failed:', fromLive.error);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// Explicit refresh must not persist a different version or a stale Fabric image.
|
|
1711
|
+
if (requiredIframe) return null;
|
|
1712
|
+
|
|
1713
|
+
// 2) Hidden Worker /preview (same capture script; wait for load)
|
|
1714
|
+
const fromWorker = await captureViaHiddenWorkerPreview(targetId);
|
|
1715
|
+
if (fromWorker && fromWorker.length > 500) return fromWorker;
|
|
1716
|
+
|
|
1717
|
+
// 3) Last resort: Fabric thumbnail (often stale / wireframe) — only if nothing else works
|
|
1718
|
+
const shapeDataUrl =
|
|
1719
|
+
typeof canvasRef?.getWidgetShapeDataUrl === 'function'
|
|
1720
|
+
? canvasRef.getWidgetShapeDataUrl(targetId)
|
|
1721
|
+
: null;
|
|
1722
|
+
if (shapeDataUrl && shapeDataUrl.length > 100) {
|
|
1723
|
+
console.warn(
|
|
1724
|
+
'[Chat→Screenshot] Falling back to Fabric thumbnail (may be stale wireframe)',
|
|
1725
|
+
targetId.substring(0, 8),
|
|
1726
|
+
);
|
|
1727
|
+
return shapeDataUrl;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
console.warn('[Chat→Screenshot] All capture paths failed', {
|
|
1731
|
+
targetId: targetId.substring(0, 8),
|
|
1732
|
+
hadLiveIframe: !!liveIframe,
|
|
1733
|
+
liveSrc: liveSrc.slice(0, 100),
|
|
1734
|
+
});
|
|
1735
|
+
return null;
|
|
1736
|
+
}
|
|
1522
1737
|
|
|
1523
1738
|
function normalizeConversationRecords(json: any): any[] {
|
|
1524
1739
|
const raw = json?.data ?? json?.conversations ?? json;
|
|
@@ -1606,6 +1821,8 @@
|
|
|
1606
1821
|
/** Load messages for a conversation from backend */
|
|
1607
1822
|
async function handleLoadMessages(conversationId: string, widgetIdForSync?: string | null): Promise<any[]> {
|
|
1608
1823
|
try {
|
|
1824
|
+
// Client-only conversations (draft convert panels) have no DB row.
|
|
1825
|
+
if (isLocalOnlyConversationId(conversationId)) return [];
|
|
1609
1826
|
if (!messagesClient) {
|
|
1610
1827
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
1611
1828
|
return [];
|
|
@@ -1669,7 +1886,17 @@
|
|
|
1669
1886
|
try {
|
|
1670
1887
|
if (!conversationsClient) {
|
|
1671
1888
|
console.warn('[Chat→Backend] conversationsClient not initialized');
|
|
1672
|
-
return
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
// Draft convert panels have no DB widget yet, so there is no table row to
|
|
1892
|
+
// attach a conversation to. `@widgetic/chat` requires an object with an
|
|
1893
|
+
// `id` here and treats anything else as a fatal init error, so hand it a
|
|
1894
|
+
// clearly-marked client-only id. `finalizeDraftConversion` never migrates
|
|
1895
|
+
// this id into the real widget's panel state, so messages sent before the
|
|
1896
|
+
// widget exists can never be persisted against a phantom conversation.
|
|
1897
|
+
if (!contextId || isDraftConvertId(contextId) || String(contextId).startsWith('draft')) {
|
|
1898
|
+
const localConversationId = `local_${contextId || 'draft'}_${Date.now()}`;
|
|
1899
|
+
return { id: localConversationId, title, isLocalOnly: true };
|
|
1673
1900
|
}
|
|
1674
1901
|
const response = await conversationsClient.createConversationForWidgetRaw({
|
|
1675
1902
|
createConversationForWidgetRequest: {
|
|
@@ -1693,7 +1920,8 @@
|
|
|
1693
1920
|
return conversationId ? { ...conversation, id: conversationId } : conversation;
|
|
1694
1921
|
} catch (error) {
|
|
1695
1922
|
console.error('[Chat→Backend] Failed to create conversation:', error);
|
|
1696
|
-
|
|
1923
|
+
// Never invent a local UUID — saving messages against it yields 403 and stops codegen.
|
|
1924
|
+
return null;
|
|
1697
1925
|
}
|
|
1698
1926
|
}
|
|
1699
1927
|
|
|
@@ -1810,6 +2038,11 @@
|
|
|
1810
2038
|
persistWidgetId?: string | null,
|
|
1811
2039
|
): Promise<any> {
|
|
1812
2040
|
try {
|
|
2041
|
+
// Draft convert panels hold a client-only conversation until the widget
|
|
2042
|
+
// exists; there is no `conversations` row to attach messages to yet.
|
|
2043
|
+
if (isLocalOnlyConversationId(conversationId)) {
|
|
2044
|
+
return { id: `local_msg_${Date.now()}`, attachments: attachments ?? [] };
|
|
2045
|
+
}
|
|
1813
2046
|
if (!messagesClient) {
|
|
1814
2047
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
1815
2048
|
return null;
|
|
@@ -1865,7 +2098,11 @@
|
|
|
1865
2098
|
}
|
|
1866
2099
|
|
|
1867
2100
|
/** Restore conversation to a checkpoint message via backend (deletes messages + git rollback) */
|
|
1868
|
-
async function handleRestoreCheckpoint(
|
|
2101
|
+
async function handleRestoreCheckpoint(
|
|
2102
|
+
conversationId: string,
|
|
2103
|
+
checkpointMessageId: string,
|
|
2104
|
+
restoreWidgetId?: string | null,
|
|
2105
|
+
): Promise<any> {
|
|
1869
2106
|
try {
|
|
1870
2107
|
if (!messagesClient) {
|
|
1871
2108
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
@@ -1882,16 +2119,39 @@
|
|
|
1882
2119
|
const data = json?.data || json;
|
|
1883
2120
|
// console.log('[Chat→Backend] Checkpoint restored:', data);
|
|
1884
2121
|
|
|
1885
|
-
// Refresh the widget preview after restore — the backend rebuilds the widget
|
|
1886
|
-
// from the reverted code, so we just need to reload the iframe.
|
|
1887
|
-
// Check both camelCase (after responseTransformMiddleware) and snake_case paths.
|
|
1888
2122
|
const gitRollback = data?.gitRollback || data?.git_rollback || data?.data?.gitRollback || data?.data?.git_rollback;
|
|
1889
2123
|
const undoMode = data?.undoMode ?? data?.undo_mode ?? false;
|
|
1890
2124
|
const targetCommit = gitRollback?.targetCommit ?? gitRollback?.target_commit;
|
|
1891
|
-
|
|
2125
|
+
const panelWidgetId =
|
|
2126
|
+
restoreWidgetId ||
|
|
2127
|
+
focusedPanelWidgetId ||
|
|
2128
|
+
selectedWidgetId ||
|
|
2129
|
+
null;
|
|
2130
|
+
// console.log('[Chat→Backend] Git rollback result:', gitRollback, { undoMode, targetCommit, panelWidgetId });
|
|
1892
2131
|
if (gitRollback?.success) {
|
|
2132
|
+
// Fast path: a recorded version at this commit has an immutable cached
|
|
2133
|
+
// artifact — reuse it instead of the forced repo rebuild + retry ladder.
|
|
2134
|
+
let previewFromVersionCache = false;
|
|
1893
2135
|
if (targetCommit) {
|
|
1894
2136
|
lastCommitId = targetCommit;
|
|
2137
|
+
if (panelWidgetId) {
|
|
2138
|
+
patchPanelSnapshot(panelWidgetId, { lastCommitId: targetCommit });
|
|
2139
|
+
patchCodegenUiState(panelWidgetId, {});
|
|
2140
|
+
// Message-extracted SHAs may be short (7+ chars); compare by prefix.
|
|
2141
|
+
const targetSha = String(targetCommit).toLowerCase();
|
|
2142
|
+
const matchedVersion = (resolveWidgetShapeVersionOptions(panelWidgetId) ?? [])
|
|
2143
|
+
.find((option) => {
|
|
2144
|
+
const optionSha = String(option.commitSha ?? '').toLowerCase();
|
|
2145
|
+
return optionSha.length >= 7 && targetSha.length >= 7
|
|
2146
|
+
&& (optionSha === targetSha || optionSha.startsWith(targetSha) || targetSha.startsWith(optionSha));
|
|
2147
|
+
});
|
|
2148
|
+
if (matchedVersion) {
|
|
2149
|
+
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${panelWidgetId}:v:${matchedVersion.id.slice(2)}`]: null };
|
|
2150
|
+
shapePreviewSourceByWidgetId = { ...shapePreviewSourceByWidgetId, [panelWidgetId]: matchedVersion.id };
|
|
2151
|
+
handleWidgetShapeVersionChange(panelWidgetId, matchedVersion.id);
|
|
2152
|
+
previewFromVersionCache = true;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
1895
2155
|
}
|
|
1896
2156
|
showToast('success', undoMode ? 'Change undone' : 'Checkpoint restored', {
|
|
1897
2157
|
description: undoMode
|
|
@@ -1905,20 +2165,33 @@
|
|
|
1905
2165
|
compositionDesignSchema = null;
|
|
1906
2166
|
compositionContentSchema = null;
|
|
1907
2167
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2168
|
+
_isRestoringCheckpoint = !previewFromVersionCache;
|
|
2169
|
+
if (previewFromVersionCache) {
|
|
2170
|
+
// Immutable artifact already serves the preview; no rebuild ladder.
|
|
2171
|
+
syncWidgetShapeLivePreviews();
|
|
2172
|
+
setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
|
|
2173
|
+
return { ...data, previewFromVersionCache };
|
|
2174
|
+
}
|
|
2175
|
+
const panelRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
|
|
1911
2176
|
const scheduleBuildRetry = async () => {
|
|
1912
2177
|
const delays = [2500, 5000, 8000];
|
|
1913
2178
|
for (let i = 0; i < delays.length; i++) {
|
|
1914
2179
|
await new Promise((r) => setTimeout(r, delays[i]));
|
|
1915
2180
|
if (i === 0) suppressNextBuildFromRepoChatMessage = true;
|
|
1916
2181
|
try {
|
|
1917
|
-
|
|
1918
|
-
|
|
2182
|
+
const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : panelRef;
|
|
2183
|
+
await activeRef?.triggerBuildFromRepo({ force: true });
|
|
2184
|
+
const err = activeRef?.getPreviewError?.() ?? null;
|
|
1919
2185
|
if (!err) {
|
|
1920
|
-
// console.log('[Restore] triggerBuildFromRepo succeeded on attempt', i + 1);
|
|
1921
2186
|
_isRestoringCheckpoint = false;
|
|
2187
|
+
if (panelWidgetId) {
|
|
2188
|
+
shapePreviewSourceByWidgetId = {
|
|
2189
|
+
...shapePreviewSourceByWidgetId,
|
|
2190
|
+
[panelWidgetId]: 'preview',
|
|
2191
|
+
};
|
|
2192
|
+
syncWidgetShapeLivePreviews();
|
|
2193
|
+
setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
|
|
2194
|
+
}
|
|
1922
2195
|
return;
|
|
1923
2196
|
}
|
|
1924
2197
|
console.warn('[Restore] triggerBuildFromRepo attempt', i + 1, 'has error:', err);
|
|
@@ -1941,14 +2214,16 @@
|
|
|
1941
2214
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
1942
2215
|
setTimeout(() => {
|
|
1943
2216
|
suppressNextBuildFromRepoChatMessage = true;
|
|
1944
|
-
|
|
2217
|
+
const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
|
|
2218
|
+
void activeRef?.triggerBuildFromRepo({ force: true });
|
|
1945
2219
|
}, 1500);
|
|
1946
2220
|
}
|
|
1947
|
-
|
|
1948
2221
|
return data;
|
|
1949
2222
|
} catch (error) {
|
|
1950
2223
|
console.error('[Chat→Backend] Failed to restore checkpoint:', error);
|
|
1951
|
-
|
|
2224
|
+
showToast('error', 'Failed to restore checkpoint', {
|
|
2225
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
2226
|
+
});
|
|
1952
2227
|
return null;
|
|
1953
2228
|
}
|
|
1954
2229
|
}
|
|
@@ -1985,7 +2260,7 @@
|
|
|
1985
2260
|
let lastPublishedVersion: number | null = null;
|
|
1986
2261
|
let selectedWidgetJsPath: string | null = null;
|
|
1987
2262
|
|
|
1988
|
-
//
|
|
2263
|
+
// Version history state — loaded when user expands the history panel
|
|
1989
2264
|
interface PublishHistoryRecord {
|
|
1990
2265
|
id: string;
|
|
1991
2266
|
version: number;
|
|
@@ -1994,10 +2269,23 @@
|
|
|
1994
2269
|
commit_sha?: string | null;
|
|
1995
2270
|
artifactUrl?: string;
|
|
1996
2271
|
artifact_url?: string;
|
|
1997
|
-
|
|
1998
|
-
|
|
2272
|
+
/**
|
|
2273
|
+
* When this version went Live. Null/absent means it is a Draft — a version
|
|
2274
|
+
* that has been generated but never published. This is the single
|
|
2275
|
+
* discriminator between Draft and Live; there is no second version type.
|
|
2276
|
+
*/
|
|
2277
|
+
publishedAt?: string | null;
|
|
2278
|
+
published_at?: string | null;
|
|
1999
2279
|
publishedBy?: string | null;
|
|
2000
2280
|
published_by?: string | null;
|
|
2281
|
+
createdAt?: string | null;
|
|
2282
|
+
created_at?: string | null;
|
|
2283
|
+
/** Derived server-side so the client does not re-infer the Draft/Live split. */
|
|
2284
|
+
isPublished?: boolean;
|
|
2285
|
+
is_published?: boolean;
|
|
2286
|
+
thumbnailUrl?: string | null;
|
|
2287
|
+
thumbnail_url?: string | null;
|
|
2288
|
+
prompt?: string | null;
|
|
2001
2289
|
}
|
|
2002
2290
|
let publishHistory: PublishHistoryRecord[] = [];
|
|
2003
2291
|
let isHistoryOpen = false;
|
|
@@ -2123,14 +2411,15 @@
|
|
|
2123
2411
|
let editingCompositionName = '';
|
|
2124
2412
|
let deletingCompositionId: string | null = null;
|
|
2125
2413
|
|
|
2126
|
-
/** Explicit chat box height (px).
|
|
2127
|
-
const CHAT_DEFAULT_HEIGHT =
|
|
2128
|
-
const CHAT_MIN_HEIGHT =
|
|
2414
|
+
/** Explicit chat box height (px). null/0 = fill available; user drag sets a fixed px. */
|
|
2415
|
+
const CHAT_DEFAULT_HEIGHT = 400;
|
|
2416
|
+
const CHAT_MIN_HEIGHT = 400;
|
|
2129
2417
|
const CHAT_MAX_HEIGHT = 2000;
|
|
2130
2418
|
|
|
2131
|
-
function getChatContainerHeightForPanel(widgetId: string): number {
|
|
2419
|
+
function getChatContainerHeightForPanel(widgetId: string): number | null {
|
|
2132
2420
|
const stored = getPanelState(widgetId).chatContainerHeight;
|
|
2133
|
-
|
|
2421
|
+
// null/0 → fill available space (flex-grow). Explicit resize stores a px value.
|
|
2422
|
+
if (stored == null || stored <= 0) return null;
|
|
2134
2423
|
return Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, stored));
|
|
2135
2424
|
}
|
|
2136
2425
|
|
|
@@ -2138,9 +2427,12 @@
|
|
|
2138
2427
|
const measured = document
|
|
2139
2428
|
.querySelector(`[data-widget-chat-height="${widgetId}"]`)
|
|
2140
2429
|
?.getBoundingClientRect().height;
|
|
2141
|
-
const current =
|
|
2430
|
+
const current =
|
|
2431
|
+
measured && measured > 0
|
|
2432
|
+
? measured
|
|
2433
|
+
: (getChatContainerHeightForPanel(widgetId) ?? CHAT_DEFAULT_HEIGHT);
|
|
2142
2434
|
const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
|
|
2143
|
-
|
|
2435
|
+
console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
|
|
2144
2436
|
patchPanelSnapshot(widgetId, { chatContainerHeight: next });
|
|
2145
2437
|
}
|
|
2146
2438
|
|
|
@@ -2293,6 +2585,11 @@
|
|
|
2293
2585
|
}
|
|
2294
2586
|
|
|
2295
2587
|
function scheduleWidgetSchemaLoad(widgetId: string): Promise<void> {
|
|
2588
|
+
// Draft convert panels have no DB widget yet — `/widgets/:id/schema/*` validates
|
|
2589
|
+
// a UUID, so calling it with a `draft_convert_*` id only produces a 400 and a
|
|
2590
|
+
// console error. The real schemas are loaded once the panel migrates to the
|
|
2591
|
+
// widget id after the first send.
|
|
2592
|
+
if (isDraftConvertId(widgetId)) return Promise.resolve();
|
|
2296
2593
|
const cached = widgetSchemaCache.get(widgetId);
|
|
2297
2594
|
if (cached?.design || cached?.content) {
|
|
2298
2595
|
applyWidgetSchemaCacheToSnapshot(widgetId);
|
|
@@ -2462,6 +2759,9 @@
|
|
|
2462
2759
|
}
|
|
2463
2760
|
|
|
2464
2761
|
async function executeWidgetSchemaLoad(widgetId: string): Promise<void> {
|
|
2762
|
+
// Defence in depth: callers already skip drafts, but any path that reaches
|
|
2763
|
+
// here with a synthetic id would hit `/widgets/:id/schema/*` and get a 400.
|
|
2764
|
+
if (isDraftConvertId(widgetId)) return;
|
|
2465
2765
|
schemaLoadInFlight.add(widgetId);
|
|
2466
2766
|
|
|
2467
2767
|
try {
|
|
@@ -2760,7 +3060,6 @@
|
|
|
2760
3060
|
// Only reuse js_path when it already points at this widget + this version.
|
|
2761
3061
|
// `/widgets/` alone used to win and keep Edit on the previous CDN version after publish.
|
|
2762
3062
|
if (!pointsAtOtherWidget && jsPathVersion === version) {
|
|
2763
|
-
console.log('[Publish] Using js_path artifact URL', resolved);
|
|
2764
3063
|
return resolved;
|
|
2765
3064
|
}
|
|
2766
3065
|
}
|
|
@@ -3883,63 +4182,162 @@
|
|
|
3883
4182
|
}
|
|
3884
4183
|
|
|
3885
4184
|
function handleHostSaveCanvasEvent(): void {
|
|
3886
|
-
|
|
4185
|
+
// Explicit Save must always persist — bypass soft load-cooldown / not-ready guards.
|
|
4186
|
+
const json = canvasRef?.saveCanvas?.() ?? null;
|
|
4187
|
+
if (!json) {
|
|
4188
|
+
console.warn('[CreatorApp] Host Save ignored — saveCanvas returned null');
|
|
4189
|
+
showToast('error', 'Nothing to save', { description: 'Canvas is not ready yet.' });
|
|
4190
|
+
return;
|
|
4191
|
+
}
|
|
4192
|
+
void handleCanvasAutoSave(json, false, { force: true });
|
|
3887
4193
|
}
|
|
3888
4194
|
|
|
3889
|
-
async function persistPrimaryWidgetId(widgetId: string) {
|
|
3890
|
-
if (!widgetId) return;
|
|
4195
|
+
async function persistPrimaryWidgetId(widgetId: string): Promise<boolean> {
|
|
4196
|
+
if (!widgetId) return false;
|
|
4197
|
+
const targetCanvasId =
|
|
4198
|
+
(typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
|
|
4199
|
+
(typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null);
|
|
4200
|
+
const widget = getWidgetById(widgetId);
|
|
4201
|
+
const widgetCanvasId =
|
|
4202
|
+
(widget as { canvasId?: string | null; canvas_id?: string | null } | null)?.canvasId ??
|
|
4203
|
+
(widget as { canvasId?: string | null; canvas_id?: string | null } | null)?.canvas_id ??
|
|
4204
|
+
null;
|
|
4205
|
+
const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
|
|
4206
|
+
const shapeOnThisCanvas = linkedIds instanceof Set ? linkedIds.has(widgetId) : false;
|
|
4207
|
+
// Explicit user intent (picking a primary) is the strongest ownership signal, so a
|
|
4208
|
+
// shape the user drew / sees on THIS canvas always wins over a stale canvas_id.
|
|
4209
|
+
// Legacy canvas duplication left the same shape spread across many canvases; refusing
|
|
4210
|
+
// here made primary unsettable. Adopt (move) instead — never touch the shape itself.
|
|
4211
|
+
const shouldAdoptToThisCanvas =
|
|
4212
|
+
!!targetCanvasId && shapeOnThisCanvas && (!widgetCanvasId || widgetCanvasId !== targetCanvasId);
|
|
4213
|
+
if (targetCanvasId && widgetCanvasId && widgetCanvasId !== targetCanvasId && !shapeOnThisCanvas) {
|
|
4214
|
+
console.warn(
|
|
4215
|
+
'[CreatorApp] Refusing primary_widget_id — widget belongs to another canvas and has no shape here',
|
|
4216
|
+
widgetId.slice(0, 8),
|
|
4217
|
+
'widgetCanvas',
|
|
4218
|
+
String(widgetCanvasId).slice(0, 8),
|
|
4219
|
+
'targetCanvas',
|
|
4220
|
+
targetCanvasId.slice(0, 8),
|
|
4221
|
+
);
|
|
4222
|
+
emitCanvasWidgetsList('primary-refused');
|
|
4223
|
+
return false;
|
|
4224
|
+
}
|
|
3891
4225
|
canvasPrimaryWidgetId = widgetId;
|
|
4226
|
+
saveSelectedWidgetId(widgetId);
|
|
3892
4227
|
// console.log('[CreatorApp] Set primary widget:', widgetId);
|
|
3893
|
-
if (
|
|
4228
|
+
if (targetCanvasId && canvasesClient) {
|
|
3894
4229
|
try {
|
|
3895
4230
|
await canvasesClient.updateCanvas({
|
|
3896
|
-
id:
|
|
4231
|
+
id: targetCanvasId,
|
|
3897
4232
|
updateCanvasRequest: { primaryWidgetId: widgetId }
|
|
3898
4233
|
});
|
|
3899
4234
|
} catch (err) {
|
|
3900
4235
|
console.warn('[CreatorApp] Failed to save primary_widget_id:', err);
|
|
3901
4236
|
}
|
|
3902
4237
|
}
|
|
3903
|
-
|
|
3904
|
-
if (
|
|
4238
|
+
// Adopt orphans AND cross-canvas strays whose shape lives here (explicit user intent).
|
|
4239
|
+
if (targetCanvasId && widgetsClient && shouldAdoptToThisCanvas) {
|
|
3905
4240
|
try {
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4241
|
+
await widgetsClient.updateWidget({
|
|
4242
|
+
id: widgetId,
|
|
4243
|
+
updateWidgetRequest: { canvasId: targetCanvasId },
|
|
4244
|
+
});
|
|
4245
|
+
widgets = widgets.map((w: any) =>
|
|
4246
|
+
w.id === widgetId ? { ...w, canvasId: targetCanvasId, canvas_id: targetCanvasId } : w,
|
|
4247
|
+
);
|
|
4248
|
+
console.log(
|
|
4249
|
+
'[CreatorApp] Adopted widget canvas_id to host canvas (primary pick)',
|
|
4250
|
+
widgetId.slice(0, 8),
|
|
4251
|
+
'from',
|
|
4252
|
+
widgetCanvasId ? String(widgetCanvasId).slice(0, 8) : 'null',
|
|
4253
|
+
'to',
|
|
4254
|
+
targetCanvasId.slice(0, 8),
|
|
4255
|
+
);
|
|
3911
4256
|
} catch (err) {
|
|
3912
|
-
console.warn('[CreatorApp] Failed to
|
|
4257
|
+
console.warn('[CreatorApp] Failed to heal widget canvas_id:', err);
|
|
3913
4258
|
}
|
|
3914
4259
|
}
|
|
4260
|
+
if (embedded) {
|
|
4261
|
+
syncUrlWidgetId(widgetId);
|
|
4262
|
+
}
|
|
3915
4263
|
if (widget) notifyHostWidgetName(widget);
|
|
3916
4264
|
emitCanvasWidgetsList('primary-changed');
|
|
4265
|
+
const anyW = widget as { thumbnailUrl?: string | null; thumbnail_url?: string | null } | null;
|
|
4266
|
+
const hasThumb = !!(anyW?.thumbnailUrl || anyW?.thumbnail_url);
|
|
4267
|
+
if (!hasThumb) {
|
|
4268
|
+
setTimeout(() => {
|
|
4269
|
+
void handleRefreshScreenshot(widgetId, { quiet: true });
|
|
4270
|
+
}, 1200);
|
|
4271
|
+
}
|
|
4272
|
+
return true;
|
|
3917
4273
|
}
|
|
3918
4274
|
|
|
3919
4275
|
function emitCanvasWidgetsList(reason: string) {
|
|
3920
4276
|
if (!embedded || typeof window === 'undefined') return;
|
|
4277
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
3921
4278
|
const ids = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
|
|
3922
4279
|
const items: { id: string; name: string }[] = [];
|
|
4280
|
+
const seen = new Set<string>();
|
|
4281
|
+
const pushItem = (id: string, name?: string | null) => {
|
|
4282
|
+
if (!id || seen.has(id) || isDraftConvertId(id)) return;
|
|
4283
|
+
seen.add(id);
|
|
4284
|
+
items.push({ id, name: (name || '').trim() || 'Untitled Widget' });
|
|
4285
|
+
};
|
|
4286
|
+
// Canvas widgets are those whose DB canvas_id matches this canvas (shapes may lag behind).
|
|
4287
|
+
if (hostCanvasId) {
|
|
4288
|
+
for (const widget of widgets) {
|
|
4289
|
+
const id = (widget as { id?: string }).id;
|
|
4290
|
+
if (!id) continue;
|
|
4291
|
+
const widgetCanvas = widgetCanvasIdOf(
|
|
4292
|
+
widget as { canvasId?: string; canvas_id?: string } | null,
|
|
4293
|
+
);
|
|
4294
|
+
if (widgetCanvas !== hostCanvasId) continue;
|
|
4295
|
+
pushItem(id, (widget as { name?: string }).name);
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
// Linked shapes that live on THIS canvas. An orphan (null canvas_id) drawn here is
|
|
4299
|
+
// legitimately this canvas's widget — only foreign canvas_id ownership is excluded.
|
|
3923
4300
|
for (const id of ids) {
|
|
4301
|
+
if (!id || isDraftConvertId(id)) continue;
|
|
3924
4302
|
const widget = getWidgetById(id);
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
4303
|
+
if (!widget?.id) continue;
|
|
4304
|
+
const widgetCanvas = widgetCanvasIdOf(
|
|
4305
|
+
widget as { canvasId?: string; canvas_id?: string } | null,
|
|
4306
|
+
);
|
|
4307
|
+
if (!hostCanvasId || (widgetCanvas && widgetCanvas !== hostCanvasId)) continue;
|
|
4308
|
+
pushItem(id, widget.name);
|
|
4309
|
+
}
|
|
4310
|
+
let primaryWidgetId = (canvasPrimaryWidgetId || '').trim();
|
|
4311
|
+
const primaryOnList = primaryWidgetId && items.some((item) => item.id === primaryWidgetId);
|
|
4312
|
+
if (primaryWidgetId && !primaryOnList) {
|
|
4313
|
+
const primary = getWidgetById(primaryWidgetId);
|
|
4314
|
+
const primaryCanvas = widgetCanvasIdOf(
|
|
4315
|
+
primary as { canvasId?: string; canvas_id?: string } | null,
|
|
4316
|
+
);
|
|
4317
|
+
if (hostCanvasId && primaryCanvas && primaryCanvas === hostCanvasId && primary?.id) {
|
|
3933
4318
|
items.unshift({
|
|
3934
4319
|
id: primary.id,
|
|
3935
4320
|
name: (primary.name || '').trim() || 'Untitled Widget'
|
|
3936
4321
|
});
|
|
4322
|
+
} else {
|
|
4323
|
+
console.warn(
|
|
4324
|
+
'[CreatorApp] Dropping stale primary from canvas widgets emit',
|
|
4325
|
+
reason,
|
|
4326
|
+
primaryWidgetId.slice(0, 8),
|
|
4327
|
+
'host',
|
|
4328
|
+
hostCanvasId?.slice(0, 8) || 'none',
|
|
4329
|
+
);
|
|
4330
|
+
primaryWidgetId = items[0]?.id || '';
|
|
4331
|
+
canvasPrimaryWidgetId = primaryWidgetId || null;
|
|
4332
|
+
if (primaryWidgetId) syncUrlWidgetId(primaryWidgetId);
|
|
3937
4333
|
}
|
|
3938
4334
|
}
|
|
3939
|
-
|
|
4335
|
+
if (!primaryWidgetId && items[0]?.id) {
|
|
4336
|
+
primaryWidgetId = items[0].id;
|
|
4337
|
+
}
|
|
3940
4338
|
// console.log('[CreatorApp] Emit canvas widgets for header', reason, items.length, primaryWidgetId);
|
|
3941
4339
|
window.dispatchEvent(new CustomEvent(CREATOR_CANVAS_WIDGETS_EVENT, {
|
|
3942
|
-
detail: { widgets: items, primaryWidgetId }
|
|
4340
|
+
detail: { widgets: items, primaryWidgetId: primaryWidgetId || '' }
|
|
3943
4341
|
}));
|
|
3944
4342
|
}
|
|
3945
4343
|
|
|
@@ -3953,10 +4351,19 @@
|
|
|
3953
4351
|
console.warn('[CreatorApp] Host set primary ignored — missing widgetId');
|
|
3954
4352
|
return;
|
|
3955
4353
|
}
|
|
4354
|
+
const previousPrimary = canvasPrimaryWidgetId;
|
|
4355
|
+
// Optimistic — so host-request emits during persist cannot resurrect the previous primary.
|
|
4356
|
+
canvasPrimaryWidgetId = widgetId;
|
|
4357
|
+
saveSelectedWidgetId(widgetId);
|
|
3956
4358
|
// console.log('[CreatorApp] Host set primary widget', widgetId.substring(0, 8));
|
|
3957
4359
|
canvasRef?.setPrimaryWidgetShape?.(widgetId);
|
|
3958
4360
|
canvasRef?.selectWidgetShape?.(widgetId);
|
|
3959
|
-
void persistPrimaryWidgetId(widgetId)
|
|
4361
|
+
void persistPrimaryWidgetId(widgetId).then((ok) => {
|
|
4362
|
+
if (ok) return;
|
|
4363
|
+
canvasPrimaryWidgetId = previousPrimary;
|
|
4364
|
+
if (previousPrimary) saveSelectedWidgetId(previousPrimary);
|
|
4365
|
+
emitCanvasWidgetsList('primary-reverted');
|
|
4366
|
+
});
|
|
3960
4367
|
}
|
|
3961
4368
|
|
|
3962
4369
|
function handleSetPrimaryWidget(widgetId: string) {
|
|
@@ -3966,7 +4373,33 @@
|
|
|
3966
4373
|
async function hydrateCanvasPrimaryWidget(linkedIds?: string[]): Promise<string | null> {
|
|
3967
4374
|
const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
|
|
3968
4375
|
if (!targetCanvasId) return canvasPrimaryWidgetId || null;
|
|
4376
|
+
|
|
4377
|
+
// NOTE: `?widget_id=` is deliberately NOT consulted here. The canvas's DB
|
|
4378
|
+
// `primary_widget_id` is the single source of truth for what opens selected/red on
|
|
4379
|
+
// this canvas. A widget_id coming from a link is treated as a one-time intent in
|
|
4380
|
+
// ensureEmbeddedWidgetDeepLink(), which validates it and promotes it to primary —
|
|
4381
|
+
// so a stale id from a previously opened canvas can never win on load again.
|
|
4382
|
+
|
|
3969
4383
|
let primaryId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
|
|
4384
|
+
// Reject cross-canvas primary (stale DB rows mix project cards / header) unless the
|
|
4385
|
+
// widget's shape actually lives on this canvas — then the shape wins over stale metadata.
|
|
4386
|
+
if (primaryId) {
|
|
4387
|
+
const primaryWidget = getWidgetById(primaryId);
|
|
4388
|
+
const primaryCanvasId = widgetCanvasIdOf(
|
|
4389
|
+
primaryWidget as { canvasId?: string | null; canvas_id?: string | null } | null,
|
|
4390
|
+
);
|
|
4391
|
+
const primaryShapeHere = !!canvasRef?.getLinkedWidgetIds?.()?.has(primaryId);
|
|
4392
|
+
if (primaryCanvasId && primaryCanvasId !== targetCanvasId && !primaryShapeHere) {
|
|
4393
|
+
console.warn(
|
|
4394
|
+
'[CreatorApp] Ignoring cross-canvas primary_widget_id',
|
|
4395
|
+
primaryId.slice(0, 8),
|
|
4396
|
+
'on canvas',
|
|
4397
|
+
targetCanvasId.slice(0, 8),
|
|
4398
|
+
);
|
|
4399
|
+
primaryId = null;
|
|
4400
|
+
canvasPrimaryWidgetId = null;
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
3970
4403
|
if (!primaryId) {
|
|
3971
4404
|
const fromLinks = (linkedIds || []).filter((id) => id && !isDraftConvertId(id) && !String(id).startsWith('draft'));
|
|
3972
4405
|
const fromList = widgets
|
|
@@ -3975,7 +4408,11 @@
|
|
|
3975
4408
|
return cid === targetCanvasId && w.id && !isDraftConvertId(w.id);
|
|
3976
4409
|
})
|
|
3977
4410
|
.map((w: any) => w.id);
|
|
3978
|
-
primaryId = fromLinks
|
|
4411
|
+
primaryId = fromLinks.find((id) => {
|
|
4412
|
+
const w = getWidgetById(id);
|
|
4413
|
+
const cid = (w as any)?.canvasId || (w as any)?.canvas_id;
|
|
4414
|
+
return !cid || cid === targetCanvasId;
|
|
4415
|
+
}) || fromList[0] || null;
|
|
3979
4416
|
if (primaryId) {
|
|
3980
4417
|
// console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
|
|
3981
4418
|
await persistPrimaryWidgetId(primaryId);
|
|
@@ -3989,6 +4426,12 @@
|
|
|
3989
4426
|
return primaryId || canvasPrimaryWidgetId || null;
|
|
3990
4427
|
}
|
|
3991
4428
|
|
|
4429
|
+
function applyPrimarySelectionOnCanvas(primaryId: string | null | undefined): void {
|
|
4430
|
+
if (!primaryId || !canvasRef || isConvertOrDraftPanelOpen()) return;
|
|
4431
|
+
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
4432
|
+
canvasRef.selectWidgetShape?.(primaryId);
|
|
4433
|
+
}
|
|
4434
|
+
|
|
3992
4435
|
async function handleHostMoveCanvasFolderEvent(event: Event) {
|
|
3993
4436
|
const folderId = (event as CustomEvent).detail?.folderId;
|
|
3994
4437
|
if (!folderId || typeof folderId !== 'string' || !widgetsClient) return;
|
|
@@ -4441,12 +4884,46 @@
|
|
|
4441
4884
|
}, delayMs);
|
|
4442
4885
|
}
|
|
4443
4886
|
|
|
4887
|
+
/**
|
|
4888
|
+
* Read the canvas id from the address bar.
|
|
4889
|
+
*
|
|
4890
|
+
* The canvas id lives in the **path** (`/canvas/<canvasId>`), not in a query string.
|
|
4891
|
+
*
|
|
4892
|
+
* `loadCurrentCanvas()` is also triggered by a reactive auth effect, which can run BEFORE
|
|
4893
|
+
* the onMount route subscription populates `routeCanvasId`. In that race, `routeCanvasId`
|
|
4894
|
+
* was still null, so the loader fell through to `getSavedSelectedCanvasId()` and a canvas
|
|
4895
|
+
* remembered from a *previous* session silently replaced the canvas in the URL. Reading the
|
|
4896
|
+
* address bar synchronously here makes the opened canvas authoritative in every entry path.
|
|
4897
|
+
*/
|
|
4898
|
+
function readCanvasIdFromAddressBar(): string | null {
|
|
4899
|
+
if (typeof window === 'undefined') return null;
|
|
4900
|
+
try {
|
|
4901
|
+
const pathParts = window.location.pathname.split('/');
|
|
4902
|
+
const canvasIndex = pathParts.indexOf('canvas');
|
|
4903
|
+
const fromPath = canvasIndex >= 0 ? (pathParts[canvasIndex + 1] || '').trim() : '';
|
|
4904
|
+
if (fromPath) return decodeURIComponent(fromPath);
|
|
4905
|
+
// Legacy query string — kept only so an old bookmark still resolves to its canvas.
|
|
4906
|
+
const params = new URLSearchParams(window.location.search);
|
|
4907
|
+
const fromQuery = params.get('canvas_id') || params.get('canvas-id');
|
|
4908
|
+
return fromQuery && fromQuery.trim() ? fromQuery.trim() : null;
|
|
4909
|
+
} catch {
|
|
4910
|
+
return null;
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4444
4914
|
async function loadCurrentCanvas() {
|
|
4445
4915
|
// console.log('Widget Creator: loadCurrentCanvas():', routeCanvasId, isLoadingCanvasOnce);
|
|
4446
4916
|
if (isLoadingCanvasOnce) return;
|
|
4447
4917
|
try {
|
|
4448
4918
|
isLoadingCanvasOnce = true;
|
|
4449
4919
|
|
|
4920
|
+
// Resolve the canvas from the address bar before any localStorage fallback — see
|
|
4921
|
+
// readCanvasIdFromAddressBar(). This must happen before initialCandidateCanvasId.
|
|
4922
|
+
if (!routeCanvasId) {
|
|
4923
|
+
const fromAddressBar = readCanvasIdFromAddressBar();
|
|
4924
|
+
if (fromAddressBar) routeCanvasId = fromAddressBar;
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4450
4927
|
// set widgets loading state
|
|
4451
4928
|
isLoadingWidgets = true;
|
|
4452
4929
|
widgetsError = null;
|
|
@@ -4479,6 +4956,16 @@
|
|
|
4479
4956
|
// Prefer canvasId from route (path or query)
|
|
4480
4957
|
if (routeCanvasId) {
|
|
4481
4958
|
canvasListResolved = true;
|
|
4959
|
+
// The route is authoritative — keep the remembered canvas in step with it. Since the
|
|
4960
|
+
// host now passes `canvasId` as a prop, the `!canvasId` guard above can skip the sync
|
|
4961
|
+
// and leave localStorage pointing at the previously opened canvas, which then wins
|
|
4962
|
+
// the next time a bare `/canvas` is opened.
|
|
4963
|
+
if (sessionCanvasId !== routeCanvasId || canvasId !== routeCanvasId) {
|
|
4964
|
+
rememberCanvasId(routeCanvasId);
|
|
4965
|
+
}
|
|
4966
|
+
// New canvas session — clear primary from previous canvas before hydrate.
|
|
4967
|
+
canvasPrimaryWidgetId = null;
|
|
4968
|
+
embeddedDeepLinkAutoOpenDone = false;
|
|
4482
4969
|
// console.log('WidgetCreator.loadCurrentCanvas: Loading data for route canvasId:', canvasId);
|
|
4483
4970
|
}
|
|
4484
4971
|
// Fallback: get latest canvas from current user's canvases
|
|
@@ -4582,10 +5069,16 @@
|
|
|
4582
5069
|
const { loaded, widgetIds: widgetIdsOnCanvas } = await parallelCanvasLoadPromise;
|
|
4583
5070
|
await maybeAutoRestoreWidgetShapes();
|
|
4584
5071
|
backfillWidgetShapeMetadata();
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
5072
|
+
const linkedIds =
|
|
5073
|
+
widgetIdsOnCanvas.length > 0
|
|
5074
|
+
? widgetIdsOnCanvas
|
|
5075
|
+
: [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
|
|
5076
|
+
const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
|
|
5077
|
+
if (loaded) {
|
|
5078
|
+
applyPrimarySelectionOnCanvas(primaryId);
|
|
5079
|
+
requestAnimationFrame(() => {
|
|
5080
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
|
|
5081
|
+
});
|
|
4589
5082
|
}
|
|
4590
5083
|
return;
|
|
4591
5084
|
}
|
|
@@ -4602,10 +5095,16 @@
|
|
|
4602
5095
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
4603
5096
|
await maybeAutoRestoreWidgetShapes();
|
|
4604
5097
|
backfillWidgetShapeMetadata();
|
|
4605
|
-
const
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
5098
|
+
const linkedIds =
|
|
5099
|
+
widgetIdsOnCanvas.length > 0
|
|
5100
|
+
? widgetIdsOnCanvas
|
|
5101
|
+
: [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
|
|
5102
|
+
const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
|
|
5103
|
+
if (loaded || isCanvasStateLoaded) {
|
|
5104
|
+
applyPrimarySelectionOnCanvas(primaryId);
|
|
5105
|
+
requestAnimationFrame(() => {
|
|
5106
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
|
|
5107
|
+
});
|
|
4609
5108
|
}
|
|
4610
5109
|
} catch (error) {
|
|
4611
5110
|
console.error('Widget Creator: Load canvases error:', error);
|
|
@@ -4675,6 +5174,13 @@
|
|
|
4675
5174
|
sessionCanvasId = entry.id;
|
|
4676
5175
|
activeCanvasName = entry.name;
|
|
4677
5176
|
saveSelectedCanvasId(canvasId);
|
|
5177
|
+
// The deferred cooldown save (if any) holds the previous canvas's shapes.
|
|
5178
|
+
discardPendingCooldownAutosave();
|
|
5179
|
+
// Address bar must track the canvas we actually switched to, and must not keep a
|
|
5180
|
+
// widget_id belonging to the previous canvas.
|
|
5181
|
+
routeCanvasId = entry.id;
|
|
5182
|
+
canvasPrimaryWidgetId = null;
|
|
5183
|
+
syncUrlCanvasId(entry.id);
|
|
4678
5184
|
|
|
4679
5185
|
// Clear existing widgets and canvas visually
|
|
4680
5186
|
widgets = [];
|
|
@@ -4692,6 +5198,21 @@
|
|
|
4692
5198
|
}
|
|
4693
5199
|
}
|
|
4694
5200
|
|
|
5201
|
+
/**
|
|
5202
|
+
* Clear the cross-tab "generating"/"publishing" flags when the tab closes.
|
|
5203
|
+
*
|
|
5204
|
+
* Lives at component scope (not inside `onMount`) so `onDestroy` can pass the
|
|
5205
|
+
* same reference to `removeEventListener`. When it was declared inside
|
|
5206
|
+
* `onMount`, leaving the canvas threw `handleBeforeUnload is not defined`,
|
|
5207
|
+
* which aborted the rest of the cleanup and blocked the navigation.
|
|
5208
|
+
*/
|
|
5209
|
+
function handleBeforeUnload() {
|
|
5210
|
+
for (const wId of Object.keys(generatingWidgetIds)) {
|
|
5211
|
+
setCrossTabGenerating(false, wId);
|
|
5212
|
+
}
|
|
5213
|
+
if (isPublishing) setCrossTabPublishing(false);
|
|
5214
|
+
}
|
|
5215
|
+
|
|
4695
5216
|
/** Close canvas dropdown on outside click */
|
|
4696
5217
|
function handleCanvasDropdownOutsideClick(e: MouseEvent) {
|
|
4697
5218
|
const target = e.target as HTMLElement;
|
|
@@ -4796,6 +5317,7 @@
|
|
|
4796
5317
|
function beginBlankCanvasSession(reason: string): void {
|
|
4797
5318
|
ignoreIncomingCanvasLoads = true;
|
|
4798
5319
|
canvasLoadEpoch += 1;
|
|
5320
|
+
discardPendingCooldownAutosave();
|
|
4799
5321
|
lastCommitId = null;
|
|
4800
5322
|
lastPublishedVersion = null;
|
|
4801
5323
|
selectedWidgetJsPath = null;
|
|
@@ -4806,6 +5328,19 @@
|
|
|
4806
5328
|
// console.log('[CreatorApp] Blank canvas session:', reason, 'epoch', canvasLoadEpoch);
|
|
4807
5329
|
}
|
|
4808
5330
|
|
|
5331
|
+
/**
|
|
5332
|
+
* Drop a deferred cooldown autosave. Must run whenever the active canvas
|
|
5333
|
+
* changes: the captured JSON belongs to the previous canvas id, and flushing
|
|
5334
|
+
* it later would write one canvas's shapes into another.
|
|
5335
|
+
*/
|
|
5336
|
+
function discardPendingCooldownAutosave(): void {
|
|
5337
|
+
if (cooldownAutosaveTimer) {
|
|
5338
|
+
clearTimeout(cooldownAutosaveTimer);
|
|
5339
|
+
cooldownAutosaveTimer = null;
|
|
5340
|
+
}
|
|
5341
|
+
pendingCooldownAutosaveJson = null;
|
|
5342
|
+
}
|
|
5343
|
+
|
|
4809
5344
|
function relinkSelectedWidgetShape(): void {
|
|
4810
5345
|
placeSelectedWidgetShapeOnCanvas();
|
|
4811
5346
|
}
|
|
@@ -4878,7 +5413,15 @@
|
|
|
4878
5413
|
}
|
|
4879
5414
|
|
|
4880
5415
|
try {
|
|
4881
|
-
|
|
5416
|
+
// Names must be unique per user — duplicate keys ("Canvas 1" x N) made
|
|
5417
|
+
// canvases indistinguishable and contributed to cross-canvas widget mix-ups.
|
|
5418
|
+
const usedNames = new Set(userCanvases.map((c) => (c.name || '').trim()).filter(Boolean));
|
|
5419
|
+
let canvasIndex = userCanvases.length + 1;
|
|
5420
|
+
let newName = `Canvas ${canvasIndex}`;
|
|
5421
|
+
while (usedNames.has(newName)) {
|
|
5422
|
+
canvasIndex += 1;
|
|
5423
|
+
newName = `Canvas ${canvasIndex}`;
|
|
5424
|
+
}
|
|
4882
5425
|
const createdRaw = await canvasesClient.createCanvas({
|
|
4883
5426
|
createCanvasRequest: { persistenceKey: newName }
|
|
4884
5427
|
});
|
|
@@ -4887,6 +5430,7 @@
|
|
|
4887
5430
|
|
|
4888
5431
|
if (createdId) {
|
|
4889
5432
|
canvasLoadEpoch += 1;
|
|
5433
|
+
discardPendingCooldownAutosave();
|
|
4890
5434
|
isCanvasStateLoaded = false;
|
|
4891
5435
|
userCanvases = [{ id: createdId, name: newName, hasContent: false }, ...userCanvases];
|
|
4892
5436
|
showCanvasDropdown = false;
|
|
@@ -4991,16 +5535,51 @@
|
|
|
4991
5535
|
deleteCanvasTarget = entry;
|
|
4992
5536
|
showDeleteCanvasDialog = true;
|
|
4993
5537
|
showCanvasDropdown = false;
|
|
5538
|
+
deleteCanvasError = '';
|
|
5539
|
+
void loadDeleteCanvasPreview(entry.id);
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
/**
|
|
5543
|
+
* Fetch what the deletion would remove so the dialog can name it.
|
|
5544
|
+
* Failure is non-fatal: the dialog still explains the action, just without
|
|
5545
|
+
* the per-project breakdown.
|
|
5546
|
+
*/
|
|
5547
|
+
async function loadDeleteCanvasPreview(canvasId: string): Promise<void> {
|
|
5548
|
+
deleteCanvasPreview = null;
|
|
5549
|
+
deleteCanvasPreviewLoading = true;
|
|
5550
|
+
try {
|
|
5551
|
+
const response = await canvasesClient?.previewDeleteCanvas?.({ id: canvasId });
|
|
5552
|
+
const preview = response?.data;
|
|
5553
|
+
|
|
5554
|
+
if (!preview) {
|
|
5555
|
+
// Older SDK build without the delete-preview method — no breakdown.
|
|
5556
|
+
console.warn('[WidgetCreator] Canvas delete preview unavailable');
|
|
5557
|
+
return;
|
|
5558
|
+
}
|
|
5559
|
+
|
|
5560
|
+
deleteCanvasPreview = {
|
|
5561
|
+
widgetCount: preview.widgetCount ?? 0,
|
|
5562
|
+
groups: (preview.groups ?? []).map((group) => ({
|
|
5563
|
+
projectName: group.projectName || 'Unknown project',
|
|
5564
|
+
widgetNames: group.widgetNames ?? []
|
|
5565
|
+
}))
|
|
5566
|
+
};
|
|
5567
|
+
} catch (err) {
|
|
5568
|
+
console.warn('[WidgetCreator] Could not load canvas delete preview:', err);
|
|
5569
|
+
} finally {
|
|
5570
|
+
deleteCanvasPreviewLoading = false;
|
|
5571
|
+
}
|
|
4994
5572
|
}
|
|
4995
5573
|
|
|
4996
5574
|
async function handleDeleteCanvasConfirm() {
|
|
4997
5575
|
if (!deleteCanvasTarget || !canvasesClient) return;
|
|
4998
5576
|
const targetId = deleteCanvasTarget.id;
|
|
4999
5577
|
const targetName = deleteCanvasTarget.name;
|
|
5000
|
-
|
|
5578
|
+
deleteCanvasError = '';
|
|
5001
5579
|
try {
|
|
5002
5580
|
await canvasesClient.deleteCanvas({ id: targetId });
|
|
5003
5581
|
// console.log('[WidgetCreator] Canvas deleted:', targetId, targetName);
|
|
5582
|
+
showDeleteCanvasDialog = false;
|
|
5004
5583
|
userCanvases = userCanvases.filter(c => c.id !== targetId);
|
|
5005
5584
|
if (canvasId === targetId) {
|
|
5006
5585
|
const next = userCanvases[0] || null;
|
|
@@ -5016,6 +5595,10 @@
|
|
|
5016
5595
|
}
|
|
5017
5596
|
} catch (err) {
|
|
5018
5597
|
console.error('[WidgetCreator] Failed to delete canvas:', targetId, err);
|
|
5598
|
+
// Keep the dialog open so the user can react; a partial failure leaves
|
|
5599
|
+
// the canvas intact on purpose (the API refuses to orphan widgets).
|
|
5600
|
+
deleteCanvasError = 'Could not delete this canvas. Some of its widgets could not be removed, so nothing was deleted.';
|
|
5601
|
+
return;
|
|
5019
5602
|
}
|
|
5020
5603
|
deleteCanvasTarget = null;
|
|
5021
5604
|
}
|
|
@@ -5085,15 +5668,39 @@
|
|
|
5085
5668
|
// console.log('Widgets for canvas:', canvasId, ':', widgets);
|
|
5086
5669
|
syncOrphanedWidgetShapes();
|
|
5087
5670
|
|
|
5088
|
-
// Auto-select: embedded deep-link > localStorage > latest
|
|
5671
|
+
// Auto-select: embedded deep-link > localStorage > latest.
|
|
5672
|
+
// Both sources must belong to THIS canvas — localStorage is shared across canvases,
|
|
5673
|
+
// so a saved id from a previous canvas would otherwise leak in as primary.
|
|
5089
5674
|
if (embedded && forceCreateNewWidget && !initialWidgetId) {
|
|
5090
5675
|
// console.log('[WidgetCreator] Skipping auto-select of existing widgets (new widget from site)');
|
|
5091
5676
|
} else if (widgets.length > 0) {
|
|
5092
|
-
const
|
|
5093
|
-
|
|
5677
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
5678
|
+
// A saved widget is only trustworthy when we can PROVE it belongs here: the
|
|
5679
|
+
// canvas must be known and the widget's canvas_id must match (null canvas_id
|
|
5680
|
+
// is only adopted via an explicit primary pick, not via localStorage).
|
|
5681
|
+
const savedWidgetRaw = getSavedSelectedWidgetId()
|
|
5682
|
+
? getWidgetById(getSavedSelectedWidgetId() as string)
|
|
5094
5683
|
: null;
|
|
5095
|
-
const
|
|
5096
|
-
|
|
5684
|
+
const savedWidget =
|
|
5685
|
+
savedWidgetRaw &&
|
|
5686
|
+
hostCanvasId &&
|
|
5687
|
+
widgetCanvasIdOf(
|
|
5688
|
+
savedWidgetRaw as { canvasId?: string | null; canvas_id?: string | null },
|
|
5689
|
+
) === hostCanvasId
|
|
5690
|
+
? savedWidgetRaw
|
|
5691
|
+
: null;
|
|
5692
|
+
if (savedWidgetRaw && !savedWidget) {
|
|
5693
|
+
console.warn(
|
|
5694
|
+
'[CreatorApp] Ignoring saved widget from another canvas',
|
|
5695
|
+
savedWidgetRaw.id?.slice(0, 8),
|
|
5696
|
+
'host',
|
|
5697
|
+
hostCanvasId?.slice(0, 8) || 'unknown',
|
|
5698
|
+
);
|
|
5699
|
+
saveSelectedWidgetId(null);
|
|
5700
|
+
}
|
|
5701
|
+
// The explicit deep-link is validated by ensureEmbeddedWidgetDeepLink(); a stale
|
|
5702
|
+
// URL id is never used to auto-select a widget from another canvas.
|
|
5703
|
+
const deepLinkedWidget = initialWidgetId ? getWidgetById(initialWidgetId) : null;
|
|
5097
5704
|
const widgetToSelect = deepLinkedWidget ?? savedWidget ?? widgets[0];
|
|
5098
5705
|
if (deepLinkedWidget) {
|
|
5099
5706
|
// console.log('[WidgetCreator] Selecting widget from embedded deep-link:', deepLinkedWidget.name);
|
|
@@ -5360,6 +5967,17 @@
|
|
|
5360
5967
|
|
|
5361
5968
|
if (missingIds.length === 0) return;
|
|
5362
5969
|
|
|
5970
|
+
// Scope merge to THIS canvas: a shape can linger on several canvases (legacy canvas
|
|
5971
|
+
// duplication), but a widget belongs to exactly one. Merging foreign widgets is what
|
|
5972
|
+
// made another canvas's widgets show up in this canvas's list/picker.
|
|
5973
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
5974
|
+
const belongsHere = (widget: WidgetSummary): boolean => {
|
|
5975
|
+
const widgetCanvas = widgetCanvasIdOf(
|
|
5976
|
+
widget as { canvasId?: string | null; canvas_id?: string | null },
|
|
5977
|
+
);
|
|
5978
|
+
return !widgetCanvas || !hostCanvasId || widgetCanvas === hostCanvasId;
|
|
5979
|
+
};
|
|
5980
|
+
|
|
5363
5981
|
try {
|
|
5364
5982
|
// console.log(
|
|
5365
5983
|
// '[WidgetCreator] Merging',
|
|
@@ -5370,8 +5988,21 @@
|
|
|
5370
5988
|
getWidgetsBatchRequest: { widgetIds: missingIds }
|
|
5371
5989
|
});
|
|
5372
5990
|
const batch = response as { widgets?: WidgetSummary[]; data?: { widgets?: WidgetSummary[] } };
|
|
5373
|
-
const
|
|
5374
|
-
if (!Array.isArray(
|
|
5991
|
+
const fetchedRaw = batch?.widgets ?? batch?.data?.widgets ?? [];
|
|
5992
|
+
if (!Array.isArray(fetchedRaw) || fetchedRaw.length === 0) return;
|
|
5993
|
+
const fetched = fetchedRaw.filter(belongsHere);
|
|
5994
|
+
const foreignCount = fetchedRaw.length - fetched.length;
|
|
5995
|
+
if (foreignCount > 0) {
|
|
5996
|
+
console.warn(
|
|
5997
|
+
'[CreatorApp] Ignoring',
|
|
5998
|
+
foreignCount,
|
|
5999
|
+
'widget(s) linked by shape but owned by another canvas',
|
|
6000
|
+
);
|
|
6001
|
+
// Keep the stale shape but stop treating it as a widget of this canvas.
|
|
6002
|
+
syncOrphanedWidgetShapes();
|
|
6003
|
+
emitCanvasWidgetsList('foreign-shapes');
|
|
6004
|
+
}
|
|
6005
|
+
if (fetched.length === 0) return;
|
|
5375
6006
|
|
|
5376
6007
|
const previousById = new Map(
|
|
5377
6008
|
widgets.filter((w) => w.id).map((w) => [w.id as string, w]),
|
|
@@ -5393,9 +6024,26 @@
|
|
|
5393
6024
|
widgets.length > 0 &&
|
|
5394
6025
|
!(embedded && forceCreateNewWidget && !initialWidgetId)
|
|
5395
6026
|
) {
|
|
6027
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
6028
|
+
const primaryOnCanvas =
|
|
6029
|
+
(canvasPrimaryWidgetId && getWidgetById(canvasPrimaryWidgetId)) ||
|
|
6030
|
+
widgets.find((w: any) => {
|
|
6031
|
+
const cid = w.canvasId || w.canvas_id;
|
|
6032
|
+
return hostCanvasId && cid === hostCanvasId && w.id === canvasPrimaryWidgetId;
|
|
6033
|
+
}) ||
|
|
6034
|
+
null;
|
|
5396
6035
|
const savedWidgetId = getSavedSelectedWidgetId();
|
|
5397
6036
|
const savedWidget = savedWidgetId ? getWidgetById(savedWidgetId) : null;
|
|
5398
|
-
|
|
6037
|
+
const savedOnThisCanvas =
|
|
6038
|
+
savedWidget &&
|
|
6039
|
+
(!hostCanvasId ||
|
|
6040
|
+
widgetCanvasIdOf(savedWidget as { canvasId?: string; canvas_id?: string }) ===
|
|
6041
|
+
hostCanvasId)
|
|
6042
|
+
? savedWidget
|
|
6043
|
+
: null;
|
|
6044
|
+
applyInitialWidgetSelectionFromList(
|
|
6045
|
+
primaryOnCanvas || savedOnThisCanvas || widgets[0],
|
|
6046
|
+
);
|
|
5399
6047
|
}
|
|
5400
6048
|
} catch (err) {
|
|
5401
6049
|
console.warn('[WidgetCreator] Failed to merge widgets linked on canvas:', err);
|
|
@@ -5404,8 +6052,8 @@
|
|
|
5404
6052
|
|
|
5405
6053
|
/**
|
|
5406
6054
|
* Site embeds CreatorApp once; `initialWidgetId` props stay frozen in prod.
|
|
5407
|
-
*
|
|
5408
|
-
*
|
|
6055
|
+
* Prefer live `?widget_id=` only when it belongs to the host canvas — otherwise
|
|
6056
|
+
* a stale widget_id from the previous canvas overwrites the header/primary.
|
|
5409
6057
|
*/
|
|
5410
6058
|
function resolveEmbeddedDeepLinkWidgetId(): string | null {
|
|
5411
6059
|
if (typeof window !== 'undefined') {
|
|
@@ -5419,6 +6067,84 @@
|
|
|
5419
6067
|
return initialWidgetId ?? null;
|
|
5420
6068
|
}
|
|
5421
6069
|
|
|
6070
|
+
function resolveHostCanvasId(): string | null {
|
|
6071
|
+
// Priority: the canvas id in the address bar is authoritative (it is what the user
|
|
6072
|
+
// opened and what a refresh resumes). It lives in the route path (`/canvas/<id>`).
|
|
6073
|
+
// `canvasId`/`sessionCanvasId` can still hold a stale value hydrated from
|
|
6074
|
+
// localStorage on a previous canvas, which is what allowed another canvas's widgets
|
|
6075
|
+
// to leak into this canvas's list/picker.
|
|
6076
|
+
const fromUrl = readCanvasIdFromAddressBar();
|
|
6077
|
+
return (
|
|
6078
|
+
fromUrl ||
|
|
6079
|
+
(typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
|
|
6080
|
+
(typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null) ||
|
|
6081
|
+
(typeof routeCanvasId === 'string' && routeCanvasId.trim() ? routeCanvasId.trim() : null)
|
|
6082
|
+
);
|
|
6083
|
+
}
|
|
6084
|
+
|
|
6085
|
+
function widgetCanvasIdOf(widget: {
|
|
6086
|
+
canvasId?: string | null;
|
|
6087
|
+
canvas_id?: string | null;
|
|
6088
|
+
} | null | undefined): string | null {
|
|
6089
|
+
if (!widget) return null;
|
|
6090
|
+
const cid = widget.canvasId ?? widget.canvas_id ?? null;
|
|
6091
|
+
return typeof cid === 'string' && cid.trim() ? cid.trim() : null;
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
async function fetchWidgetCanvasId(widgetId: string): Promise<string | null> {
|
|
6095
|
+
const local = getWidgetById(widgetId);
|
|
6096
|
+
const localCanvas = widgetCanvasIdOf(local as { canvasId?: string; canvas_id?: string } | null);
|
|
6097
|
+
if (localCanvas) return localCanvas;
|
|
6098
|
+
if (!widgetsClient) return null;
|
|
6099
|
+
try {
|
|
6100
|
+
const batch = await widgetsClient.getWidgetsBatch({
|
|
6101
|
+
getWidgetsBatchRequest: { widgetIds: [widgetId] },
|
|
6102
|
+
});
|
|
6103
|
+
const row =
|
|
6104
|
+
(batch as { widgets?: Array<{ canvasId?: string; canvas_id?: string }> })?.widgets?.[0] ??
|
|
6105
|
+
(batch as { data?: { widgets?: Array<{ canvasId?: string; canvas_id?: string }> } })?.data
|
|
6106
|
+
?.widgets?.[0];
|
|
6107
|
+
return widgetCanvasIdOf(row);
|
|
6108
|
+
} catch (err) {
|
|
6109
|
+
console.warn('[CreatorApp] Failed to fetch widget canvas for deep-link', widgetId.slice(0, 8), err);
|
|
6110
|
+
return null;
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
|
|
6114
|
+
/**
|
|
6115
|
+
* `canvas_id` + the canvas's DB `primary_widget_id` are the source of truth for
|
|
6116
|
+
* "which widget is open on this canvas". A `widget_id` in the address bar is only a
|
|
6117
|
+
* transient intent read once at mount.
|
|
6118
|
+
*
|
|
6119
|
+
* IMPORTANT: this deliberately does NOT touch `widget_id` any more. Rewriting the URL
|
|
6120
|
+
* here used to (a) persist a stale id from a previously opened canvas as if it were the
|
|
6121
|
+
* current primary, and (b) trigger a host remount because the site keys CreatorApp's
|
|
6122
|
+
* mount on the URL params. Leaving the address bar untouched is what makes the primary
|
|
6123
|
+
* stable; the canvas's DB `primary_widget_id` decides what opens.
|
|
6124
|
+
*/
|
|
6125
|
+
function syncUrlWidgetId(_widgetId: string | null): void {
|
|
6126
|
+
if (typeof window === 'undefined' || !embedded) return;
|
|
6127
|
+
syncEmbeddedDeepLinkDismissTracking();
|
|
6128
|
+
}
|
|
6129
|
+
|
|
6130
|
+
/** Ensure the address bar points at the canvas we actually loaded (refresh-safe). */
|
|
6131
|
+
function syncUrlCanvasId(canvasIdToSync: string | null): void {
|
|
6132
|
+
if (typeof window === 'undefined' || !embedded) return;
|
|
6133
|
+
if (!canvasIdToSync || !canvasIdToSync.trim()) return;
|
|
6134
|
+
try {
|
|
6135
|
+
const url = new URL(window.location.href);
|
|
6136
|
+
const nextPath = `/canvas/${encodeURIComponent(canvasIdToSync)}`;
|
|
6137
|
+
if (url.pathname === nextPath && !url.searchParams.has('canvas_id')) return;
|
|
6138
|
+
url.pathname = nextPath;
|
|
6139
|
+
// The canvas id belongs in the path only — never duplicated into the query string.
|
|
6140
|
+
url.searchParams.delete('canvas_id');
|
|
6141
|
+
url.searchParams.delete('canvas-id');
|
|
6142
|
+
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
6143
|
+
} catch (err) {
|
|
6144
|
+
console.warn('[CreatorApp] Failed to sync canvas_id in URL:', err);
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
|
|
5422
6148
|
/**
|
|
5423
6149
|
* Canvas-only embed deep-link (My widgets / canvas without widget_id):
|
|
5424
6150
|
* read canvases.primary_widget_id so Creator can open that widget panel.
|
|
@@ -5435,20 +6161,44 @@
|
|
|
5435
6161
|
(record as { primary_widget_id?: unknown })?.primary_widget_id ??
|
|
5436
6162
|
null;
|
|
5437
6163
|
const primaryId = typeof raw === 'string' && raw.trim() ? raw.trim() : null;
|
|
5438
|
-
if (primaryId) {
|
|
5439
|
-
|
|
5440
|
-
// console.log(
|
|
5441
|
-
// '[CreatorApp] Resolved primary widget for canvas',
|
|
5442
|
-
// targetCanvasId.substring(0, 8),
|
|
5443
|
-
// '→',
|
|
5444
|
-
// primaryId.substring(0, 8)
|
|
5445
|
-
// );
|
|
5446
|
-
} else {
|
|
5447
|
-
// console.log(
|
|
5448
|
-
// '[CreatorApp] No primary_widget_id on canvas',
|
|
5449
|
-
// targetCanvasId.substring(0, 8)
|
|
5450
|
-
// );
|
|
6164
|
+
if (!primaryId) {
|
|
6165
|
+
return null;
|
|
5451
6166
|
}
|
|
6167
|
+
// Validate primary belongs to this canvas (DB has many cross-canvas orphans).
|
|
6168
|
+
let widgetCanvasId: string | null = null;
|
|
6169
|
+
const local = getWidgetById(primaryId);
|
|
6170
|
+
if (local) {
|
|
6171
|
+
widgetCanvasId =
|
|
6172
|
+
(local as { canvasId?: string | null; canvas_id?: string | null }).canvasId ??
|
|
6173
|
+
(local as { canvasId?: string | null; canvas_id?: string | null }).canvas_id ??
|
|
6174
|
+
null;
|
|
6175
|
+
} else if (widgetsClient) {
|
|
6176
|
+
try {
|
|
6177
|
+
const batch = await widgetsClient.getWidgetsBatch({
|
|
6178
|
+
getWidgetsBatchRequest: { widgetIds: [primaryId] },
|
|
6179
|
+
});
|
|
6180
|
+
const row =
|
|
6181
|
+
(batch as { widgets?: Array<{ canvasId?: string; canvas_id?: string }> })?.widgets?.[0] ??
|
|
6182
|
+
(batch as { data?: { widgets?: Array<{ canvasId?: string; canvas_id?: string }> } })?.data
|
|
6183
|
+
?.widgets?.[0];
|
|
6184
|
+
widgetCanvasId = row?.canvasId ?? row?.canvas_id ?? null;
|
|
6185
|
+
} catch (batchErr) {
|
|
6186
|
+
console.warn('[CreatorApp] Failed to validate primary widget canvas', batchErr);
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
6189
|
+
if (widgetCanvasId && widgetCanvasId !== targetCanvasId) {
|
|
6190
|
+
console.warn(
|
|
6191
|
+
'[CreatorApp] Canvas primary_widget_id points at another canvas — treating as empty',
|
|
6192
|
+
primaryId.slice(0, 8),
|
|
6193
|
+
'target',
|
|
6194
|
+
targetCanvasId.slice(0, 8),
|
|
6195
|
+
'widgetCanvas',
|
|
6196
|
+
widgetCanvasId.slice(0, 8),
|
|
6197
|
+
);
|
|
6198
|
+
canvasPrimaryWidgetId = null;
|
|
6199
|
+
return null;
|
|
6200
|
+
}
|
|
6201
|
+
canvasPrimaryWidgetId = primaryId;
|
|
5452
6202
|
return primaryId;
|
|
5453
6203
|
} catch (err) {
|
|
5454
6204
|
console.warn(
|
|
@@ -5465,11 +6215,6 @@
|
|
|
5465
6215
|
syncEmbeddedDeepLinkDismissTracking();
|
|
5466
6216
|
if (!embedded || !widgetsClient || !hasValidToken) return;
|
|
5467
6217
|
if (embeddedDeepLinkPanelDismissed) {
|
|
5468
|
-
const dismissedId = resolveEmbeddedDeepLinkWidgetId();
|
|
5469
|
-
// console.log(
|
|
5470
|
-
// '[CreatorApp] Skip embedded deep-link select — user dismissed panel:',
|
|
5471
|
-
// dismissedId ? dismissedId.substring(0, 8) : '(none)'
|
|
5472
|
-
// );
|
|
5473
6218
|
return;
|
|
5474
6219
|
}
|
|
5475
6220
|
// Select primary widget once per Creator mount. Do not auto-open Widget Details.
|
|
@@ -5477,70 +6222,79 @@
|
|
|
5477
6222
|
return;
|
|
5478
6223
|
}
|
|
5479
6224
|
if (isConvertOrDraftPanelOpen()) {
|
|
5480
|
-
// console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
|
|
5481
6225
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5482
6226
|
return;
|
|
5483
6227
|
}
|
|
5484
6228
|
|
|
6229
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
5485
6230
|
let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
url.searchParams.set('widget_id', deepLinkWidgetId);
|
|
5503
|
-
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
5504
|
-
syncEmbeddedDeepLinkDismissTracking();
|
|
5505
|
-
}
|
|
5506
|
-
} catch (err) {
|
|
5507
|
-
console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
|
|
5508
|
-
}
|
|
5509
|
-
}
|
|
6231
|
+
|
|
6232
|
+
// Stale ?widget_id= from a previous canvas must NOT win over this canvas's primary.
|
|
6233
|
+
if (deepLinkWidgetId && hostCanvasId) {
|
|
6234
|
+
const widgetCanvasId = await fetchWidgetCanvasId(deepLinkWidgetId);
|
|
6235
|
+
if (widgetCanvasId && widgetCanvasId !== hostCanvasId) {
|
|
6236
|
+
console.warn(
|
|
6237
|
+
'[CreatorApp] Ignoring stale URL widget_id from another canvas',
|
|
6238
|
+
deepLinkWidgetId.slice(0, 8),
|
|
6239
|
+
'widgetCanvas',
|
|
6240
|
+
widgetCanvasId.slice(0, 8),
|
|
6241
|
+
'host',
|
|
6242
|
+
hostCanvasId.slice(0, 8),
|
|
6243
|
+
);
|
|
6244
|
+
deepLinkWidgetId = null;
|
|
6245
|
+
syncUrlWidgetId(null);
|
|
6246
|
+
saveSelectedWidgetId(null);
|
|
5510
6247
|
}
|
|
5511
6248
|
}
|
|
5512
|
-
|
|
6249
|
+
|
|
6250
|
+
if (!deepLinkWidgetId && hostCanvasId && canvasesClient) {
|
|
6251
|
+
deepLinkWidgetId = await resolvePrimaryWidgetIdForCanvas(hostCanvasId);
|
|
6252
|
+
if (embeddedDeepLinkPanelDismissed || embeddedDeepLinkAutoOpenDone) {
|
|
6253
|
+
return;
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
if (!deepLinkWidgetId) {
|
|
6258
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
6259
|
+
return;
|
|
6260
|
+
}
|
|
6261
|
+
|
|
6262
|
+
// Keep URL + localStorage aligned to THIS canvas's primary only.
|
|
6263
|
+
canvasPrimaryWidgetId = deepLinkWidgetId;
|
|
6264
|
+
saveSelectedWidgetId(deepLinkWidgetId);
|
|
6265
|
+
syncUrlWidgetId(deepLinkWidgetId);
|
|
5513
6266
|
|
|
5514
6267
|
const requestGen = ++embeddedDeepLinkRequestGen;
|
|
6268
|
+
const expectedWidgetId = deepLinkWidgetId;
|
|
5515
6269
|
|
|
5516
6270
|
function deepLinkOpenStillValid(): boolean {
|
|
5517
6271
|
return (
|
|
5518
6272
|
requestGen === embeddedDeepLinkRequestGen &&
|
|
5519
6273
|
!embeddedDeepLinkPanelDismissed &&
|
|
5520
|
-
|
|
6274
|
+
canvasPrimaryWidgetId === expectedWidgetId
|
|
5521
6275
|
);
|
|
5522
6276
|
}
|
|
5523
6277
|
|
|
5524
6278
|
const alreadyLinked =
|
|
5525
|
-
selectedWidgetId ===
|
|
6279
|
+
selectedWidgetId === expectedWidgetId && !!getWidgetById(expectedWidgetId);
|
|
5526
6280
|
if (alreadyLinked) {
|
|
5527
6281
|
if (embeddedInitialStepPending && initialStep) {
|
|
5528
6282
|
widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
|
|
5529
6283
|
embeddedInitialStepPending = false;
|
|
5530
6284
|
}
|
|
5531
6285
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
6286
|
+
applyPrimarySelectionOnCanvas(expectedWidgetId);
|
|
6287
|
+
const primaryWidget = getWidgetById(expectedWidgetId);
|
|
6288
|
+
if (primaryWidget) notifyHostWidgetName(primaryWidget);
|
|
6289
|
+
emitCanvasWidgetsList('deeplink-already-linked');
|
|
5535
6290
|
return;
|
|
5536
6291
|
}
|
|
5537
6292
|
|
|
5538
6293
|
try {
|
|
5539
6294
|
const response = await widgetsClient.getWidgetsBatch({
|
|
5540
|
-
getWidgetsBatchRequest: { widgetIds: [
|
|
6295
|
+
getWidgetsBatchRequest: { widgetIds: [expectedWidgetId] }
|
|
5541
6296
|
});
|
|
5542
6297
|
if (!deepLinkOpenStillValid()) {
|
|
5543
|
-
// console.log('[CreatorApp] Stale embedded deep-link fetch — user changed selection or closed panel');
|
|
5544
6298
|
return;
|
|
5545
6299
|
}
|
|
5546
6300
|
|
|
@@ -5550,47 +6304,52 @@
|
|
|
5550
6304
|
};
|
|
5551
6305
|
const target = batch?.widgets?.[0] ?? batch?.data?.widgets?.[0];
|
|
5552
6306
|
if (!target?.id) {
|
|
5553
|
-
console.warn('[CreatorApp] Embedded deep-link widget not found:',
|
|
6307
|
+
console.warn('[CreatorApp] Embedded deep-link widget not found:', expectedWidgetId);
|
|
5554
6308
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5555
6309
|
return;
|
|
5556
6310
|
}
|
|
5557
6311
|
|
|
5558
|
-
const targetCanvasId =
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
6312
|
+
const targetCanvasId = widgetCanvasIdOf(
|
|
6313
|
+
target as { canvasId?: string | null; canvas_id?: string | null },
|
|
6314
|
+
);
|
|
6315
|
+
if (hostCanvasId && targetCanvasId && targetCanvasId !== hostCanvasId) {
|
|
6316
|
+
console.warn(
|
|
6317
|
+
'[CreatorApp] Deep-link widget does not belong to host canvas — abort select',
|
|
6318
|
+
target.id.slice(0, 8),
|
|
6319
|
+
'target',
|
|
6320
|
+
targetCanvasId.slice(0, 8),
|
|
6321
|
+
'host',
|
|
6322
|
+
hostCanvasId.slice(0, 8),
|
|
6323
|
+
);
|
|
6324
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
6325
|
+
const healed = await hydrateCanvasPrimaryWidget(
|
|
6326
|
+
[...(canvasRef?.getLinkedWidgetIds?.() ?? [])],
|
|
6327
|
+
);
|
|
6328
|
+
applyPrimarySelectionOnCanvas(healed);
|
|
6329
|
+
return;
|
|
6330
|
+
}
|
|
5562
6331
|
|
|
5563
6332
|
if (!getWidgetById(target.id)) {
|
|
5564
6333
|
widgets = sortWidgetSummaries([...widgets, target]);
|
|
5565
6334
|
}
|
|
5566
6335
|
if (!deepLinkOpenStillValid()) {
|
|
5567
|
-
// console.log('[CreatorApp] Skip embedded deep-link open — stale after widget fetch');
|
|
5568
6336
|
return;
|
|
5569
6337
|
}
|
|
5570
6338
|
|
|
5571
6339
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5572
|
-
|
|
6340
|
+
canvasPrimaryWidgetId = target.id;
|
|
5573
6341
|
await selectWidget(target, { skipLoadWidget: true, skipPlaceShape: true });
|
|
5574
6342
|
if (!deepLinkOpenStillValid()) return;
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
6343
|
+
applyPrimarySelectionOnCanvas(target.id);
|
|
6344
|
+
requestAnimationFrame(() => {
|
|
6345
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(target.id), 80);
|
|
6346
|
+
});
|
|
6347
|
+
notifyHostWidgetName(target);
|
|
6348
|
+
emitCanvasWidgetsList('deeplink-primary');
|
|
5578
6349
|
if (embeddedInitialStepPending && initialStep) {
|
|
5579
6350
|
widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
|
|
5580
6351
|
embeddedInitialStepPending = false;
|
|
5581
6352
|
}
|
|
5582
|
-
|
|
5583
|
-
if (targetCanvasId && targetCanvasId !== canvasId) {
|
|
5584
|
-
// console.log('[CreatorApp] Switching canvas for embedded deep-link:', targetCanvasId);
|
|
5585
|
-
routeCanvasId = targetCanvasId;
|
|
5586
|
-
canvasId = targetCanvasId;
|
|
5587
|
-
saveSelectedCanvasId(canvasId);
|
|
5588
|
-
isLoadingCanvasOnce = false;
|
|
5589
|
-
isLoadingWidgetsOnce = false;
|
|
5590
|
-
lastWidgetsCanvasId = null;
|
|
5591
|
-
void loadCurrentCanvas();
|
|
5592
|
-
return;
|
|
5593
|
-
}
|
|
5594
6353
|
} catch (err) {
|
|
5595
6354
|
console.error('[CreatorApp] ensureEmbeddedWidgetDeepLink failed:', err);
|
|
5596
6355
|
embeddedDeepLinkAutoOpenDone = true;
|
|
@@ -5993,7 +6752,11 @@
|
|
|
5993
6752
|
...(lastCommitId ? { hasGeneratedCode: true } : {}),
|
|
5994
6753
|
});
|
|
5995
6754
|
if (lastCommitId) hasGeneratedCode = true;
|
|
5996
|
-
|
|
6755
|
+
// Fetch history whenever the widget has code (generated or published).
|
|
6756
|
+
// Gating on `lastPublishedVersion` alone meant a widget that had only
|
|
6757
|
+
// ever been generated never loaded its history — so its version picker
|
|
6758
|
+
// stayed empty even though generations were recorded server-side.
|
|
6759
|
+
if (lastPublishedVersion !== null || lastCommitId || currentRepositoryId) {
|
|
5997
6760
|
// Load history against the specific widget id (not selectedWidgetId) so the
|
|
5998
6761
|
// panel snapshot is guaranteed to be marked publishHistoryResolved even if the
|
|
5999
6762
|
// user switches widget between hydrate and API response.
|
|
@@ -6143,6 +6906,11 @@
|
|
|
6143
6906
|
}
|
|
6144
6907
|
|
|
6145
6908
|
notifyHostWidgetName({ id: widgetId, name: newName });
|
|
6909
|
+
emitCanvasWidgetsList('widget-renamed');
|
|
6910
|
+
widgetNamesRevision += 1;
|
|
6911
|
+
panelDisplayRevision += 1;
|
|
6912
|
+
backfillWidgetShapeMetadata();
|
|
6913
|
+
syncWidgetShapeLivePreviews();
|
|
6146
6914
|
// console.log('[WidgetRename] Successfully renamed widget:', widgetId, 'to:', newName);
|
|
6147
6915
|
showToast('success', `Widget renamed to "${newName}"`);
|
|
6148
6916
|
} catch (error) {
|
|
@@ -6337,26 +7105,37 @@
|
|
|
6337
7105
|
* Aborts the polling and resets UI state
|
|
6338
7106
|
*/
|
|
6339
7107
|
function stopGeneration() {
|
|
6340
|
-
//
|
|
6341
|
-
|
|
6342
|
-
|
|
7108
|
+
// Prefer the focused panel widget (Stop lives in that panel's Chat), then selected, then all in-flight.
|
|
7109
|
+
const stopTargetIds = Array.from(
|
|
7110
|
+
new Set(
|
|
7111
|
+
[focusedPanelWidgetId, selectedWidgetId, ...Object.keys(generatingWidgetIds)].filter(
|
|
7112
|
+
(id): id is string => typeof id === 'string' && id.length > 0,
|
|
7113
|
+
),
|
|
7114
|
+
),
|
|
7115
|
+
);
|
|
7116
|
+
|
|
7117
|
+
for (const widgetId of stopTargetIds) {
|
|
7118
|
+
const controller = widgetAbortControllers.get(widgetId);
|
|
6343
7119
|
if (controller) {
|
|
6344
7120
|
controller.abort();
|
|
6345
|
-
widgetAbortControllers.delete(
|
|
7121
|
+
widgetAbortControllers.delete(widgetId);
|
|
6346
7122
|
}
|
|
6347
|
-
|
|
6348
|
-
generatingWidgetIds = restStop;
|
|
7123
|
+
clearActiveOperationId(widgetId);
|
|
6349
7124
|
}
|
|
7125
|
+
|
|
7126
|
+
generatingWidgetIds = {};
|
|
6350
7127
|
// Legacy fallback
|
|
6351
7128
|
if (codeGenerationAbortController) {
|
|
6352
7129
|
codeGenerationAbortController.abort();
|
|
6353
7130
|
codeGenerationAbortController = null;
|
|
6354
7131
|
}
|
|
6355
|
-
|
|
7132
|
+
|
|
7133
|
+
currentGenerationOperationId = null;
|
|
6356
7134
|
isGeneratingCode = false;
|
|
6357
7135
|
setCrossTabGenerating(false);
|
|
6358
7136
|
generateCodeStatus = '⏹ Generation stopped by user';
|
|
6359
|
-
|
|
7137
|
+
const statusWidgetId = focusedPanelWidgetId || selectedWidgetId || stopTargetIds[0] || null;
|
|
7138
|
+
getChatMethodsForWidget(statusWidgetId)?.updateLatestCodegenStatusMessage?.(
|
|
6360
7139
|
'Generation stopped by user. You can Retry this prompt.',
|
|
6361
7140
|
);
|
|
6362
7141
|
showToast('info', 'Generation stopped', {
|
|
@@ -6863,12 +7642,44 @@
|
|
|
6863
7642
|
// Convert drafts capture from the canvas shape, never by opening a panel.
|
|
6864
7643
|
if (!openWidgetPanelIds.includes(widgetId)) {
|
|
6865
7644
|
if (quiet || isConvertOrDraftPanelOpen()) {
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
7645
|
+
// A draft convert has no widget row and no live iframe yet — the
|
|
7646
|
+
// Fabric sketch is genuinely all there is to show.
|
|
7647
|
+
if (isDraftConvertId(widgetId)) {
|
|
7648
|
+
const draftShapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
|
|
7649
|
+
if (draftShapeDataUrl && canvasRef) {
|
|
7650
|
+
canvasRef.updateWidgetImage(widgetId, draftShapeDataUrl);
|
|
7651
|
+
}
|
|
7652
|
+
return;
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
// Real widget with the details panel closed: the live shape iframe
|
|
7656
|
+
// is still on the canvas. Capture from it so both the canvas image
|
|
7657
|
+
// and the project-list thumbnail track the latest preview.
|
|
7658
|
+
//
|
|
7659
|
+
// Without this the Fabric `_imageElement` set at convert time wins
|
|
7660
|
+
// forever — it is the sketch screenshot frozen at creation, so the
|
|
7661
|
+
// Projects list kept showing the initial design after later
|
|
7662
|
+
// generations and preview refreshes.
|
|
7663
|
+
if (!shapeThumbnailCaptureInFlight.has(widgetId)) {
|
|
7664
|
+
shapeThumbnailCaptureInFlight.add(widgetId);
|
|
7665
|
+
try {
|
|
7666
|
+
const freshCapture = await handleChatScreenshot(widgetId);
|
|
7667
|
+
if (freshCapture) {
|
|
7668
|
+
canvasRef?.updateWidgetImage(widgetId, freshCapture);
|
|
7669
|
+
const freshJson = canvasRef?.saveCanvas?.();
|
|
7670
|
+
if (freshJson) void handleCanvasAutoSave(freshJson);
|
|
7671
|
+
void persistWidgetListThumbnail(widgetId, freshCapture);
|
|
7672
|
+
} else {
|
|
7673
|
+
// No live iframe/Worker preview reachable — leave the
|
|
7674
|
+
// existing image alone rather than persisting a wireframe.
|
|
7675
|
+
console.warn(
|
|
7676
|
+
'[handleRefreshScreenshot] Quiet capture produced no image — keeping existing shape preview',
|
|
7677
|
+
widgetId.substring(0, 8),
|
|
7678
|
+
);
|
|
7679
|
+
}
|
|
7680
|
+
} finally {
|
|
7681
|
+
shapeThumbnailCaptureInFlight.delete(widgetId);
|
|
7682
|
+
}
|
|
6872
7683
|
}
|
|
6873
7684
|
return;
|
|
6874
7685
|
}
|
|
@@ -7522,6 +8333,18 @@ Requirements:
|
|
|
7522
8333
|
// );
|
|
7523
8334
|
}
|
|
7524
8335
|
|
|
8336
|
+
// Refresh version history after every generation so the new commit shows
|
|
8337
|
+
// up in the version menu immediately. Publishing is deliberately not
|
|
8338
|
+
// automatic — the user previews and presses Publish themselves.
|
|
8339
|
+
if (generationWidgetId) {
|
|
8340
|
+
const { [generationWidgetId]: _resolved, ...restResolved } = shapeHistoryResolvedByWidgetId;
|
|
8341
|
+
shapeHistoryResolvedByWidgetId = restResolved;
|
|
8342
|
+
void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
|
|
8343
|
+
syncWidgetShapeLivePreviews();
|
|
8344
|
+
if (isFocusedPanel) panelDisplayRevision += 1;
|
|
8345
|
+
});
|
|
8346
|
+
}
|
|
8347
|
+
|
|
7525
8348
|
// Always reconcile GitLab HEAD after codegen (metadata.commitId can be missing/stale).
|
|
7526
8349
|
if (generationWidgetId) {
|
|
7527
8350
|
void (async () => {
|
|
@@ -7659,37 +8482,49 @@ Requirements:
|
|
|
7659
8482
|
// cache-busts the same URL and looks like an extra reload.
|
|
7660
8483
|
// console.log('[CreatorApp] Skipping extra preview remount — Worker iframe already showing');
|
|
7661
8484
|
activePanelRef?.setPreviewLoadingState(false);
|
|
8485
|
+
// previewReady may not re-fire when we skip remount — clear stuck status.
|
|
8486
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8487
|
+
generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
|
|
8488
|
+
});
|
|
7662
8489
|
} else {
|
|
7663
8490
|
activePanelRef?.setPreviewLoadingState(true);
|
|
7664
8491
|
await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
|
|
7665
8492
|
}
|
|
7666
8493
|
activePanelRef?.setPreviewBuildError(null);
|
|
8494
|
+
// Show Worker preview on the canvas shape (not the previous Live CDN artifact).
|
|
8495
|
+
shapePreviewSourceByWidgetId = {
|
|
8496
|
+
...shapePreviewSourceByWidgetId,
|
|
8497
|
+
[generationWidgetId]: 'preview',
|
|
8498
|
+
};
|
|
8499
|
+
syncWidgetShapeLivePreviews();
|
|
8500
|
+
panelDisplayRevision += 1;
|
|
7667
8501
|
if (!completedPreviewBuildErr) {
|
|
7668
8502
|
await applyDesignSchemaDefaultsToPreview(generationWidgetId);
|
|
7669
8503
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
7670
8504
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
7671
8505
|
`Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''}\nPreview ready.`,
|
|
7672
8506
|
);
|
|
8507
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8508
|
+
generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
|
|
8509
|
+
});
|
|
7673
8510
|
setTimeout(async () => {
|
|
7674
8511
|
await panelChat?.reloadConversationMessages?.();
|
|
7675
8512
|
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
7676
8513
|
}, 2500);
|
|
7677
|
-
setTimeout(() =>
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
);
|
|
7685
|
-
} catch (autoPublishErr) {
|
|
7686
|
-
console.warn('[Publish] Auto-publish after codegen failed:', autoPublishErr);
|
|
7687
|
-
}
|
|
7688
|
-
})();
|
|
8514
|
+
setTimeout(() => {
|
|
8515
|
+
void (async () => {
|
|
8516
|
+
await handleRefreshScreenshot(generationWidgetId, { quiet: true });
|
|
8517
|
+
// List thumb only from live iframe capture — never Fabric shape icon.
|
|
8518
|
+
})();
|
|
8519
|
+
}, 3000);
|
|
8520
|
+
// Do NOT auto-publish — user previews first, then clicks Publish / Set live.
|
|
7689
8521
|
} else {
|
|
7690
8522
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
7691
8523
|
`Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
7692
8524
|
);
|
|
8525
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8526
|
+
generateCodeStatus: `Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
8527
|
+
});
|
|
7693
8528
|
}
|
|
7694
8529
|
}, completedPreviewBuildErr ? 400 : 800);
|
|
7695
8530
|
} else {
|
|
@@ -7905,11 +8740,26 @@ Requirements:
|
|
|
7905
8740
|
*/
|
|
7906
8741
|
async function executePublishWidget(
|
|
7907
8742
|
publishForWidgetId?: string | null,
|
|
7908
|
-
publishOpts: { silent?: boolean; fromCodegen?: boolean } = {},
|
|
8743
|
+
publishOpts: { silent?: boolean; fromCodegen?: boolean; targetVersion?: number | null } = {},
|
|
7909
8744
|
) {
|
|
7910
8745
|
const capturedWidgetId = publishForWidgetId ?? focusedPanelWidgetId ?? selectedWidgetId;
|
|
7911
8746
|
const panelState = capturedWidgetId ? getPanelDisplayState(capturedWidgetId) : null;
|
|
7912
8747
|
const capturedCanvasId = getEffectiveCanvasId();
|
|
8748
|
+
// An explicit version publishes *that* version — it is a Draft the user
|
|
8749
|
+
// picked in the version list. Without one, the newest Draft / current
|
|
8750
|
+
// commit is promoted, which is the "publish latest" flow.
|
|
8751
|
+
// `Number(null)` is 0 (finite), so reject null/undefined/'' explicitly
|
|
8752
|
+
// rather than letting them silently become version 0.
|
|
8753
|
+
const rawTargetVersion = publishOpts.targetVersion;
|
|
8754
|
+
const targetVersion =
|
|
8755
|
+
rawTargetVersion == null || rawTargetVersion === ''
|
|
8756
|
+
? null
|
|
8757
|
+
: Number.isFinite(Number(rawTargetVersion))
|
|
8758
|
+
? Number(rawTargetVersion)
|
|
8759
|
+
: null;
|
|
8760
|
+
// Recheck after async focus (and for direct callers), never substitute HEAD
|
|
8761
|
+
// when a requested Draft is missing or no longer publishable.
|
|
8762
|
+
if (rawTargetVersion != null && getPublishGateForPanel(capturedWidgetId, rawTargetVersion).disabled) return;
|
|
7913
8763
|
let capturedRepositoryId = resolveRepositoryIdForWidget(capturedWidgetId);
|
|
7914
8764
|
if (!capturedRepositoryId && capturedWidgetId) {
|
|
7915
8765
|
const widget = getWidgetById(capturedWidgetId) ?? selectedWidget;
|
|
@@ -7918,10 +8768,16 @@ Requirements:
|
|
|
7918
8768
|
if (capturedRepositoryId) stampWidgetRepositoryId(capturedWidgetId, capturedRepositoryId);
|
|
7919
8769
|
}
|
|
7920
8770
|
}
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
8771
|
+
// A targeted publish builds from that version's own commit, so the server
|
|
8772
|
+
// must receive the version — sending HEAD's SHA would publish the wrong code.
|
|
8773
|
+
const targetRecord = targetVersion !== null
|
|
8774
|
+
? getShapePublishHistory(capturedWidgetId).find((row) => Number(row.version) === targetVersion)
|
|
8775
|
+
: undefined;
|
|
8776
|
+
const commitShaForPublish = targetVersion !== null
|
|
8777
|
+
? (targetRecord?.commitSha ?? targetRecord?.commit_sha ?? null)
|
|
8778
|
+
: (panelState?.lastCommitId
|
|
8779
|
+
|| (capturedWidgetId ? loadCommitIdForWidget(capturedWidgetId) : null)
|
|
8780
|
+
|| lastCommitId);
|
|
7925
8781
|
|
|
7926
8782
|
if (!capturedWidgetId || !capturedCanvasId || !capturedRepositoryId) {
|
|
7927
8783
|
showToast('error', 'Cannot publish', { description: 'Widget, canvas, and repository are required.' });
|
|
@@ -7990,7 +8846,10 @@ Requirements:
|
|
|
7990
8846
|
widgetId: capturedWidgetId,
|
|
7991
8847
|
agentWidgetBuilderPublishWidgetIdPostRequest: {
|
|
7992
8848
|
componentId,
|
|
7993
|
-
commitSha: commitShaForPublish || undefined
|
|
8849
|
+
commitSha: commitShaForPublish || undefined,
|
|
8850
|
+
// Publishing a Draft the user chose: the server rebuilds from that
|
|
8851
|
+
// version's commit and stamps it Live instead of minting a new one.
|
|
8852
|
+
version: targetVersion ?? undefined
|
|
7994
8853
|
}
|
|
7995
8854
|
});
|
|
7996
8855
|
// console.log('[Publish] Widget Builder publish completed:', publishData);
|
|
@@ -8099,6 +8958,7 @@ Requirements:
|
|
|
8099
8958
|
selectedWidgetId = null;
|
|
8100
8959
|
ignoreIncomingCanvasLoads = true;
|
|
8101
8960
|
canvasLoadEpoch += 1;
|
|
8961
|
+
discardPendingCooldownAutosave();
|
|
8102
8962
|
lastCommitId = null;
|
|
8103
8963
|
lastPublishedVersion = null;
|
|
8104
8964
|
widgetDetailsStep = 'create';
|
|
@@ -8132,22 +8992,21 @@ Requirements:
|
|
|
8132
8992
|
window.addEventListener('storage', handleCrossTabStorageEvent);
|
|
8133
8993
|
window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
8134
8994
|
|
|
8135
|
-
// Clean up cross-tab flags when tab is closed/refreshed
|
|
8136
|
-
|
|
8137
|
-
for (const wId of Object.keys(generatingWidgetIds)) {
|
|
8138
|
-
setCrossTabGenerating(false, wId);
|
|
8139
|
-
}
|
|
8140
|
-
if (isPublishing) setCrossTabPublishing(false);
|
|
8141
|
-
};
|
|
8995
|
+
// Clean up cross-tab flags when tab is closed/refreshed. The handler lives at
|
|
8996
|
+
// component scope so `onDestroy` can remove the same reference.
|
|
8142
8997
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
8143
8998
|
|
|
8144
|
-
// Read canvas id from route
|
|
8145
|
-
|
|
8999
|
+
// Read canvas id from the route. The route path (`/canvas/<canvasId>`) is the
|
|
9000
|
+
// AUTHORITATIVE canvas (site links, refresh, deep-links); localStorage is only a
|
|
9001
|
+
// fallback and must never win over it.
|
|
9002
|
+
if (!routeCanvasId) {
|
|
8146
9003
|
try {
|
|
8147
9004
|
const sub = pageStore.subscribe(($page) => {
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
9005
|
+
// `readCanvasIdFromAddressBar()` also covers the legacy query string, so an
|
|
9006
|
+
// old bookmark `/canvas?canvas_id=…` still resolves to its canvas.
|
|
9007
|
+
const nextCanvasId = readCanvasIdFromAddressBar();
|
|
9008
|
+
// Never clobber an already-known canvas with null.
|
|
9009
|
+
if (nextCanvasId) routeCanvasId = nextCanvasId;
|
|
8151
9010
|
});
|
|
8152
9011
|
setTimeout(() => {
|
|
8153
9012
|
if (sub && typeof sub === 'function') sub();
|
|
@@ -8439,6 +9298,10 @@ Requirements:
|
|
|
8439
9298
|
function formatCodegenProgressStatus(op: OperationStatus): string {
|
|
8440
9299
|
const stage = getOperationStageMessage(op.metadata as Record<string, unknown> | undefined);
|
|
8441
9300
|
const pct = typeof op.progress === 'number' ? op.progress : null;
|
|
9301
|
+
// User mode: never expose model names / provider in the status line.
|
|
9302
|
+
if (!debugMode) {
|
|
9303
|
+
return pct != null && pct > 0 ? `Generating code... (${pct}%)` : 'Generating code...';
|
|
9304
|
+
}
|
|
8442
9305
|
if (stage) {
|
|
8443
9306
|
return pct != null && pct > 0 ? `${stage} (${pct}%)` : stage;
|
|
8444
9307
|
}
|
|
@@ -8468,13 +9331,22 @@ Requirements:
|
|
|
8468
9331
|
>,
|
|
8469
9332
|
): void {
|
|
8470
9333
|
ensurePanelState(widgetId);
|
|
8471
|
-
|
|
9334
|
+
const nextPatch = { ...patch };
|
|
9335
|
+
if (typeof nextPatch.generateCodeStatus === 'string' && !debugMode) {
|
|
9336
|
+
nextPatch.generateCodeStatus = nextPatch.generateCodeStatus
|
|
9337
|
+
.replace(/\s*Commit:\s*[a-f0-9]{7,40}/gi, '')
|
|
9338
|
+
.replace(/\s*\(\s*Commit:\s*[a-f0-9]{7,40}\s*\)/gi, '')
|
|
9339
|
+
.replace(/\s*\(\s*[a-f0-9]{7,40}\s*\)/g, '')
|
|
9340
|
+
.replace(/\s{2,}/g, ' ')
|
|
9341
|
+
.trim();
|
|
9342
|
+
}
|
|
9343
|
+
patchPanelSnapshot(widgetId, nextPatch);
|
|
8472
9344
|
if (widgetId === focusedPanelWidgetId) {
|
|
8473
|
-
if (
|
|
8474
|
-
if (
|
|
8475
|
-
if (
|
|
8476
|
-
if (
|
|
8477
|
-
if (
|
|
9345
|
+
if (nextPatch.generateCodeStatus !== undefined) generateCodeStatus = nextPatch.generateCodeStatus;
|
|
9346
|
+
if (nextPatch.lastGenerationFailed !== undefined) lastGenerationFailed = nextPatch.lastGenerationFailed;
|
|
9347
|
+
if (nextPatch.lastGenerationError !== undefined) lastGenerationError = nextPatch.lastGenerationError;
|
|
9348
|
+
if (nextPatch.lastCommitId !== undefined) lastCommitId = nextPatch.lastCommitId;
|
|
9349
|
+
if (nextPatch.hasGeneratedCode !== undefined) hasGeneratedCode = nextPatch.hasGeneratedCode;
|
|
8478
9350
|
}
|
|
8479
9351
|
}
|
|
8480
9352
|
|
|
@@ -8614,13 +9486,15 @@ Requirements:
|
|
|
8614
9486
|
compositionSchemasLoading: boolean;
|
|
8615
9487
|
compositionSchemasError: string | null;
|
|
8616
9488
|
schemaLoadStartedAt: number | null;
|
|
8617
|
-
chatContainerHeight: number;
|
|
9489
|
+
chatContainerHeight: number; // 0 = fill available; >0 = user-resized fixed px
|
|
8618
9490
|
panelPreviewCompileError: string | null;
|
|
8619
9491
|
};
|
|
8620
9492
|
|
|
8621
9493
|
let widgetPanelSnapshots: Record<string, WidgetPanelSnapshot> = {};
|
|
8622
9494
|
/** Bumped on every panel snapshot write so {#each} panels re-read display state. */
|
|
8623
9495
|
let panelDisplayRevision = 0;
|
|
9496
|
+
/** Bumped on rename so shape chrome / host title re-read widget names. */
|
|
9497
|
+
let widgetNamesRevision = 0;
|
|
8624
9498
|
const schemaLoadInFlight = new Set<string>();
|
|
8625
9499
|
const schemaLoadPromises = new Map<string, Promise<void>>();
|
|
8626
9500
|
const compositionsLoadInFlight = new Set<string>();
|
|
@@ -8716,7 +9590,7 @@ Requirements:
|
|
|
8716
9590
|
compositionSchemasLoading,
|
|
8717
9591
|
compositionSchemasError,
|
|
8718
9592
|
schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
|
|
8719
|
-
chatContainerHeight: focusedSnap?.chatContainerHeight ??
|
|
9593
|
+
chatContainerHeight: focusedSnap?.chatContainerHeight ?? 0,
|
|
8720
9594
|
panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
|
|
8721
9595
|
};
|
|
8722
9596
|
}
|
|
@@ -8782,7 +9656,7 @@ Requirements:
|
|
|
8782
9656
|
compositionSchemasLoading: false,
|
|
8783
9657
|
compositionSchemasError: null,
|
|
8784
9658
|
schemaLoadStartedAt: null,
|
|
8785
|
-
chatContainerHeight:
|
|
9659
|
+
chatContainerHeight: 0,
|
|
8786
9660
|
panelPreviewCompileError: null,
|
|
8787
9661
|
};
|
|
8788
9662
|
}
|
|
@@ -8838,7 +9712,7 @@ Requirements:
|
|
|
8838
9712
|
: livePublishMatchesPanel
|
|
8839
9713
|
? live.publishHistoryResolvedForWidgetId
|
|
8840
9714
|
: snap.publishHistoryResolvedForWidgetId,
|
|
8841
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
9715
|
+
chatContainerHeight: snap.chatContainerHeight ?? 0,
|
|
8842
9716
|
compositionDesignSchema: resolvedDesignSchema,
|
|
8843
9717
|
compositionContentSchema: resolvedContentSchema,
|
|
8844
9718
|
compositionSchemasLoading: schemaLoading,
|
|
@@ -8856,7 +9730,7 @@ Requirements:
|
|
|
8856
9730
|
...snap,
|
|
8857
9731
|
lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
|
|
8858
9732
|
hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
|
|
8859
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
9733
|
+
chatContainerHeight: snap.chatContainerHeight ?? 0,
|
|
8860
9734
|
compositionDesignSchema: resolvedDesignSchema,
|
|
8861
9735
|
compositionContentSchema: resolvedContentSchema,
|
|
8862
9736
|
compositionSchemasError: resolvedSchemaError,
|
|
@@ -9001,43 +9875,391 @@ Requirements:
|
|
|
9001
9875
|
return resolvePublishedWidgetUrl(widgetId, version, jsPath);
|
|
9002
9876
|
}
|
|
9003
9877
|
|
|
9004
|
-
/**
|
|
9878
|
+
/** Public display origin for shape/hidden capture iframes; keep WidgetDetails fetch proxy separate. */
|
|
9005
9879
|
function widgetBuilderPreviewBaseUrl(): string {
|
|
9006
|
-
|
|
9880
|
+
// Do not proxy sandboxed display frames onto the editor's origin.
|
|
9881
|
+
return (
|
|
9007
9882
|
widgetBuilderUrl ||
|
|
9008
9883
|
import.meta.env.VITE_WIDGET_BUILDER_URL ||
|
|
9009
9884
|
import.meta.env.PUBLIC_WIDGET_BUILDER_URL ||
|
|
9010
9885
|
'https://wbuilder.widgetic.com'
|
|
9011
9886
|
).replace(/\/+$/, '');
|
|
9012
|
-
if (import.meta.env.DEV && publicBase.includes('wbuilder.widgetic.com')) {
|
|
9013
|
-
return '/__widget_builder__';
|
|
9014
|
-
}
|
|
9015
|
-
return publicBase;
|
|
9016
9887
|
}
|
|
9017
9888
|
|
|
9018
9889
|
/**
|
|
9019
9890
|
* Live preview URL for a canvas WidgetShape overlay.
|
|
9020
|
-
*
|
|
9021
|
-
*
|
|
9891
|
+
* Source: 'live' | 'preview' | 'v:{N}' (historical published version).
|
|
9892
|
+
* Publish history is loaded without requiring chat to be open.
|
|
9022
9893
|
*/
|
|
9894
|
+
let shapePreviewSourceByWidgetId: Record<string, string> = {};
|
|
9895
|
+
// Historical previews are never interchangeable with the shared HEAD preview.
|
|
9896
|
+
let shapeVersionPreviewByWidgetId: Record<string, {
|
|
9897
|
+
versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
|
|
9898
|
+
errorDetails?: Awaited<ReturnType<typeof parseVersionPreviewError>> | null;
|
|
9899
|
+
}> = {};
|
|
9900
|
+
let shapeVersionPreviewRequestId = 0;
|
|
9901
|
+
/** Remember failed versions after switching rows; unrelated versions stay publishable. */
|
|
9902
|
+
let shapeVersionPreviewErrorsByKey = {};
|
|
9903
|
+
|
|
9904
|
+
function getVersionPreviewPublishBlockReason(widgetId, targetVersion = null) {
|
|
9905
|
+
const selectedState = resolveWidgetShapeVersionPreviewState(widgetId);
|
|
9906
|
+
const versionId = targetVersion == null
|
|
9907
|
+
? shapePreviewSourceByWidgetId[widgetId]
|
|
9908
|
+
: `v:${targetVersion}`;
|
|
9909
|
+
const state = selectedState?.versionId === versionId ? selectedState : null;
|
|
9910
|
+
if (state?.loading) return 'Wait for this version preview to finish building.';
|
|
9911
|
+
return state?.error || shapeVersionPreviewErrorsByKey[`${widgetId}:${versionId}`] || null;
|
|
9912
|
+
}
|
|
9913
|
+
function resolveWidgetShapeVersionPreviewState(widgetId: string) {
|
|
9914
|
+
const state = shapeVersionPreviewByWidgetId[widgetId];
|
|
9915
|
+
return state?.versionId === shapePreviewSourceByWidgetId[widgetId] ? state : null;
|
|
9916
|
+
}
|
|
9917
|
+
let shapePublishHistoryByWidgetId: Record<string, any[]> = {};
|
|
9918
|
+
let shapeHistoryLoadInFlight: Record<string, boolean> = {};
|
|
9919
|
+
/**
|
|
9920
|
+
* Widgets whose shape publish history has already been fetched — *including*
|
|
9921
|
+
* widgets that came back with zero rows.
|
|
9922
|
+
*
|
|
9923
|
+
* Without this, an empty result is indistinguishable from "not fetched yet"
|
|
9924
|
+
* (see the `cached.length > 0` check below), so a widget that is published
|
|
9925
|
+
* (`js_path`/`version` set) but has no `widget_publish_history` rows re-issued
|
|
9926
|
+
* the request on every render. That thrashed the network and kept remounting
|
|
9927
|
+
* the live preview — the canvas looked like it constantly refreshed.
|
|
9928
|
+
*/
|
|
9929
|
+
let shapeHistoryResolvedByWidgetId: Record<string, boolean> = {};
|
|
9930
|
+
|
|
9931
|
+
async function ensureShapePublishHistory(widgetId: string): Promise<void> {
|
|
9932
|
+
if (!widgetId || shapeHistoryLoadInFlight[widgetId]) return;
|
|
9933
|
+
// Already fetched once — an empty history is a valid, final answer.
|
|
9934
|
+
if (shapeHistoryResolvedByWidgetId[widgetId]) return;
|
|
9935
|
+
const cached = shapePublishHistoryByWidgetId[widgetId];
|
|
9936
|
+
if (cached && cached.length > 0) {
|
|
9937
|
+
shapeHistoryResolvedByWidgetId = { ...shapeHistoryResolvedByWidgetId, [widgetId]: true };
|
|
9938
|
+
return;
|
|
9939
|
+
}
|
|
9940
|
+
const fromPanel = getPanelDisplayState(widgetId).publishHistory;
|
|
9941
|
+
if (fromPanel && fromPanel.length > 0) {
|
|
9942
|
+
shapePublishHistoryByWidgetId = {
|
|
9943
|
+
...shapePublishHistoryByWidgetId,
|
|
9944
|
+
[widgetId]: [...fromPanel],
|
|
9945
|
+
};
|
|
9946
|
+
shapeHistoryResolvedByWidgetId = { ...shapeHistoryResolvedByWidgetId, [widgetId]: true };
|
|
9947
|
+
return;
|
|
9948
|
+
}
|
|
9949
|
+
shapeHistoryLoadInFlight = { ...shapeHistoryLoadInFlight, [widgetId]: true };
|
|
9950
|
+
try {
|
|
9951
|
+
await loadPublishHistoryForPanel(widgetId, { silent: true });
|
|
9952
|
+
const history = getPanelDisplayState(widgetId).publishHistory || [];
|
|
9953
|
+
shapePublishHistoryByWidgetId = {
|
|
9954
|
+
...shapePublishHistoryByWidgetId,
|
|
9955
|
+
[widgetId]: [...history],
|
|
9956
|
+
};
|
|
9957
|
+
shapeHistoryResolvedByWidgetId = { ...shapeHistoryResolvedByWidgetId, [widgetId]: true };
|
|
9958
|
+
syncWidgetShapeLivePreviews();
|
|
9959
|
+
} finally {
|
|
9960
|
+
const { [widgetId]: _, ...rest } = shapeHistoryLoadInFlight;
|
|
9961
|
+
shapeHistoryLoadInFlight = rest;
|
|
9962
|
+
}
|
|
9963
|
+
}
|
|
9964
|
+
|
|
9965
|
+
function getShapePublishHistory(widgetId: string): any[] {
|
|
9966
|
+
const cached = shapePublishHistoryByWidgetId[widgetId];
|
|
9967
|
+
// Honour a cached empty array: it means the fetch already happened.
|
|
9968
|
+
if (cached) return cached;
|
|
9969
|
+
const fromPanel = getPanelDisplayState(widgetId).publishHistory;
|
|
9970
|
+
return fromPanel?.length ? fromPanel : [];
|
|
9971
|
+
}
|
|
9972
|
+
|
|
9023
9973
|
function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
|
|
9024
9974
|
if (!widgetId) return null;
|
|
9975
|
+
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
9976
|
+
const ps = getPanelDisplayState(widgetId);
|
|
9977
|
+
const hasCode = !!(ps.lastCommitId || loadCommitIdForWidget(widgetId));
|
|
9978
|
+
const workerUrl = hasCode
|
|
9979
|
+
? `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`
|
|
9980
|
+
: null;
|
|
9981
|
+
|
|
9982
|
+
if (source.startsWith('v:')) {
|
|
9983
|
+
const version = parseInt(source.slice(2), 10);
|
|
9984
|
+
if (Number.isFinite(version)) {
|
|
9985
|
+
const record = getShapePublishHistory(widgetId).find(
|
|
9986
|
+
(row) => Number(row.version) === version,
|
|
9987
|
+
);
|
|
9988
|
+
// Published versions use their stable artifact; drafts require an
|
|
9989
|
+
// explicit commit-specific build. Never substitute shared HEAD.
|
|
9990
|
+
const isPublished = !!(record?.publishedAt ?? record?.published_at);
|
|
9991
|
+
const artifact =
|
|
9992
|
+
record?.artifactUrl ||
|
|
9993
|
+
record?.artifact_url ||
|
|
9994
|
+
(version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
|
|
9995
|
+
|
|
9996
|
+
if (isPublished && artifact) {
|
|
9997
|
+
const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
|
|
9998
|
+
if (url) return url;
|
|
9999
|
+
}
|
|
10000
|
+
return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
|
|
10001
|
+
}
|
|
10002
|
+
return null;
|
|
10003
|
+
}
|
|
10004
|
+
|
|
10005
|
+
// After codegen (unpublished), prefer Worker preview over the Live CDN artifact.
|
|
10006
|
+
if (source === 'preview' || ps.pendingPublishAfterCodegen) {
|
|
10007
|
+
if (workerUrl) return workerUrl;
|
|
10008
|
+
}
|
|
10009
|
+
|
|
9025
10010
|
const published = getPanelEditPreviewUrl(widgetId);
|
|
9026
|
-
if (published) return published;
|
|
10011
|
+
if (published && source !== 'preview') return published;
|
|
10012
|
+
return workerUrl;
|
|
10013
|
+
}
|
|
9027
10014
|
|
|
9028
|
-
|
|
9029
|
-
|
|
10015
|
+
function resolveWidgetShapeVersionLabel(widgetId: string): string | null {
|
|
10016
|
+
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
9030
10017
|
const ps = getPanelDisplayState(widgetId);
|
|
9031
|
-
const
|
|
9032
|
-
|
|
10018
|
+
const liveVersion =
|
|
10019
|
+
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
10020
|
+
if (source.startsWith('v:')) {
|
|
10021
|
+
const version = parseInt(source.slice(2), 10);
|
|
10022
|
+
if (!Number.isFinite(version)) return null;
|
|
10023
|
+
return `v${version}`;
|
|
10024
|
+
}
|
|
10025
|
+
return liveVersion != null ? `v${liveVersion}` : null;
|
|
10026
|
+
}
|
|
10027
|
+
|
|
10028
|
+
function formatPublishHistoryDate(record: {
|
|
10029
|
+
publishedAt?: string;
|
|
10030
|
+
published_at?: string;
|
|
10031
|
+
}): string {
|
|
10032
|
+
const d = new Date(record.publishedAt ?? record.published_at ?? '');
|
|
10033
|
+
if (isNaN(d.getTime())) return '—';
|
|
10034
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
10035
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
10036
|
+
const timePart = d.toLocaleTimeString(undefined, {
|
|
10037
|
+
hour: '2-digit',
|
|
10038
|
+
minute: '2-digit',
|
|
10039
|
+
hour12: false,
|
|
10040
|
+
});
|
|
10041
|
+
return `${month}/${day}/${d.getFullYear()} ${timePart}`;
|
|
10042
|
+
}
|
|
10043
|
+
|
|
10044
|
+
function resolveWidgetShapeVersionOptions(
|
|
10045
|
+
widgetId: string,
|
|
10046
|
+
): Array<{
|
|
10047
|
+
id: string;
|
|
10048
|
+
label: string;
|
|
10049
|
+
shortLabel?: string;
|
|
10050
|
+
dateLabel?: string;
|
|
10051
|
+
isLive?: boolean;
|
|
10052
|
+
isDraft?: boolean;
|
|
10053
|
+
/** Immutable commit; Restore Checkpoint reuses this artifact when it matches. */
|
|
10054
|
+
commitSha?: string;
|
|
10055
|
+
}> | null {
|
|
10056
|
+
const ps = getPanelDisplayState(widgetId);
|
|
10057
|
+
const liveVersion =
|
|
10058
|
+
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
10059
|
+
// Newest first. Versions are one ascending sequence, so the number orders
|
|
10060
|
+
// them correctly on its own; sorting by time is a stable tiebreak.
|
|
10061
|
+
const history = [...getShapePublishHistory(widgetId)].sort(
|
|
10062
|
+
(a, b) => Number(b.version) - Number(a.version),
|
|
10063
|
+
);
|
|
10064
|
+
const options: Array<{
|
|
10065
|
+
id: string;
|
|
10066
|
+
label: string;
|
|
10067
|
+
shortLabel?: string;
|
|
10068
|
+
dateLabel?: string;
|
|
10069
|
+
isLive?: boolean;
|
|
10070
|
+
isDraft?: boolean;
|
|
10071
|
+
}> = [];
|
|
10072
|
+
for (const record of history) {
|
|
10073
|
+
const version = Number(record.version);
|
|
10074
|
+
if (!Number.isFinite(version)) continue;
|
|
10075
|
+
// One version sequence: a version is a Draft until it is published.
|
|
10076
|
+
// `published_at` is the discriminator, so there is no second numbering
|
|
10077
|
+
// scheme and no `gN` prefix — the history reads as plain `v1, v2, v3…`.
|
|
10078
|
+
const isPublished = !!(record.publishedAt ?? record.published_at);
|
|
10079
|
+
options.push({
|
|
10080
|
+
id: `v:${version}`,
|
|
10081
|
+
label: `Version ${version}`,
|
|
10082
|
+
shortLabel: `v${version}`,
|
|
10083
|
+
dateLabel: formatPublishHistoryDate(record),
|
|
10084
|
+
// Only a published version can be what visitors see.
|
|
10085
|
+
isLive: isPublished && version === liveVersion,
|
|
10086
|
+
isDraft: !isPublished,
|
|
10087
|
+
commitSha: record.commitSha ?? record.commit_sha,
|
|
10088
|
+
});
|
|
10089
|
+
}
|
|
10090
|
+
if (options.length === 0 && liveVersion != null) {
|
|
10091
|
+
options.push({
|
|
10092
|
+
id: `v:${liveVersion}`,
|
|
10093
|
+
label: `Version ${liveVersion}`,
|
|
10094
|
+
shortLabel: `v${liveVersion}`,
|
|
10095
|
+
dateLabel: '—',
|
|
10096
|
+
isLive: true,
|
|
10097
|
+
});
|
|
10098
|
+
}
|
|
10099
|
+
// Kick off history fetch so the dropdown fills even before chat opens.
|
|
10100
|
+
// `ensureShapePublishHistory` is idempotent per widget (it records resolved
|
|
10101
|
+
// widgets, including empty results), so calling it from render is safe.
|
|
10102
|
+
if (history.length === 0) {
|
|
10103
|
+
void ensureShapePublishHistory(widgetId);
|
|
10104
|
+
}
|
|
10105
|
+
return options.length > 0 ? options : null;
|
|
10106
|
+
}
|
|
9033
10107
|
|
|
9034
|
-
|
|
10108
|
+
function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
|
|
10109
|
+
const source = shapePreviewSourceByWidgetId[widgetId];
|
|
10110
|
+
if (source?.startsWith('v:')) return source;
|
|
10111
|
+
const ps = getPanelDisplayState(widgetId);
|
|
10112
|
+
const liveVersion =
|
|
10113
|
+
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
10114
|
+
return liveVersion != null ? `v:${liveVersion}` : null;
|
|
10115
|
+
}
|
|
10116
|
+
|
|
10117
|
+
function handleWidgetShapeVersionChange(widgetId: string, versionId: string, buildPreview = true): void {
|
|
10118
|
+
if (!versionId) return;
|
|
10119
|
+
// Re-selecting an already successful/in-flight preview must not reset its state.
|
|
10120
|
+
const currentPreview = resolveWidgetShapeVersionPreviewState(widgetId);
|
|
10121
|
+
if (shapePreviewSourceByWidgetId[widgetId] === versionId
|
|
10122
|
+
&& (currentPreview?.loading || currentPreview?.url)) return;
|
|
10123
|
+
shapePreviewSourceByWidgetId = {
|
|
10124
|
+
...shapePreviewSourceByWidgetId,
|
|
10125
|
+
[widgetId]: versionId,
|
|
10126
|
+
};
|
|
10127
|
+
shapeVersionPreviewRequestId += 1;
|
|
10128
|
+
shapeVersionPreviewByWidgetId = {
|
|
10129
|
+
...shapeVersionPreviewByWidgetId,
|
|
10130
|
+
[widgetId]: {
|
|
10131
|
+
versionId, requestId: shapeVersionPreviewRequestId,
|
|
10132
|
+
loading: false, error: null, url: null,
|
|
10133
|
+
},
|
|
10134
|
+
};
|
|
10135
|
+
syncWidgetShapeLivePreviews();
|
|
10136
|
+
panelDisplayRevision += 1;
|
|
10137
|
+
// Explicit Publish builds its own target; do not race it with a preview build.
|
|
10138
|
+
if (!buildPreview) return;
|
|
10139
|
+
// Published artifacts need no build. Drafts must never substitute HEAD.
|
|
10140
|
+
if (versionId.startsWith('v:')) {
|
|
10141
|
+
const version = Number(versionId.slice(2));
|
|
10142
|
+
if (resolveWidgetShapeLivePreviewUrl(widgetId)) {
|
|
10143
|
+
const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
|
|
10144
|
+
console.log('[VersionPreview] Selection', { widgetId, version, commitSha: record?.commitSha ?? record?.commit_sha ?? null, source: 'published' });
|
|
10145
|
+
} else {
|
|
10146
|
+
void ensureWidgetShapeVersionPreview(widgetId, version);
|
|
10147
|
+
}
|
|
10148
|
+
}
|
|
10149
|
+
}
|
|
10150
|
+
|
|
10151
|
+
/** Explicit retry retains the selected version and its remembered publish block. */
|
|
10152
|
+
async function retryWidgetShapeVersionPreview(widgetId) {
|
|
10153
|
+
const state = resolveWidgetShapeVersionPreviewState(widgetId);
|
|
10154
|
+
if (!state?.error || state.loading || !state.versionId.startsWith('v:')) return;
|
|
10155
|
+
await ensureWidgetShapeVersionPreview(widgetId, Number(state.versionId.slice(2)), true);
|
|
10156
|
+
}
|
|
10157
|
+
|
|
10158
|
+
async function ensureWidgetShapeVersionPreview(widgetId: string, version: number, retry = false): Promise<void> {
|
|
10159
|
+
if (!widgetId || !Number.isSafeInteger(version) || version < 1) return;
|
|
10160
|
+
const versionId = `v:${version}`;
|
|
10161
|
+
if (shapePreviewSourceByWidgetId[widgetId] !== versionId) return;
|
|
10162
|
+
const previous = shapeVersionPreviewByWidgetId[widgetId];
|
|
10163
|
+
if (previous?.versionId === versionId && (previous.loading || (!retry && (previous.url || previous.error)))) return;
|
|
10164
|
+
const requestId = ++shapeVersionPreviewRequestId;
|
|
10165
|
+
shapeVersionPreviewByWidgetId = {
|
|
10166
|
+
...shapeVersionPreviewByWidgetId,
|
|
10167
|
+
[widgetId]: { versionId, requestId, loading: true, url: null,
|
|
10168
|
+
error: retry ? previous?.error ?? null : null,
|
|
10169
|
+
errorDetails: retry ? previous?.errorDetails ?? null : null },
|
|
10170
|
+
};
|
|
10171
|
+
syncWidgetShapeLivePreviews();
|
|
10172
|
+
panelDisplayRevision += 1;
|
|
10173
|
+
try {
|
|
10174
|
+
const response = await agentClient.previewWidgetVersion({ widgetId, version });
|
|
10175
|
+
const previewUrl = response?.data?.previewUrl ?? null;
|
|
10176
|
+
const commitSha = response?.data?.commitSha ?? null;
|
|
10177
|
+
// Drop stale responses: the selector may have moved on (or away) meanwhile.
|
|
10178
|
+
if (
|
|
10179
|
+
shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
10180
|
+
|| shapeVersionPreviewByWidgetId[widgetId]?.requestId !== requestId
|
|
10181
|
+
) {
|
|
10182
|
+
return;
|
|
10183
|
+
}
|
|
10184
|
+
if (!previewUrl) {
|
|
10185
|
+
throw new Error('Preview URL missing from response');
|
|
10186
|
+
}
|
|
10187
|
+
shapeVersionPreviewByWidgetId = {
|
|
10188
|
+
...shapeVersionPreviewByWidgetId,
|
|
10189
|
+
[widgetId]: { versionId, requestId, loading: false, error: null, url: previewUrl },
|
|
10190
|
+
};
|
|
10191
|
+
syncWidgetShapeLivePreviews();
|
|
10192
|
+
panelDisplayRevision += 1;
|
|
10193
|
+
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${widgetId}:${versionId}`]: null };
|
|
10194
|
+
console.log('[VersionPreview] Selection', { widgetId, version, commitSha });
|
|
10195
|
+
} catch (error: unknown) {
|
|
10196
|
+
if (
|
|
10197
|
+
shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
10198
|
+
|| shapeVersionPreviewByWidgetId[widgetId]?.requestId !== requestId
|
|
10199
|
+
) return;
|
|
10200
|
+
const errorDetails = await parseVersionPreviewError(error);
|
|
10201
|
+
if (shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
10202
|
+
|| shapeVersionPreviewByWidgetId[widgetId]?.requestId !== requestId) return;
|
|
10203
|
+
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${widgetId}:${versionId}`]: errorDetails.message };
|
|
10204
|
+
// One diagnostic per failed request; raw server wording never reaches the overlay.
|
|
10205
|
+
console.warn('[VersionPreview] Selection', { widgetId, version, ...errorDetails });
|
|
10206
|
+
shapeVersionPreviewByWidgetId = {
|
|
10207
|
+
...shapeVersionPreviewByWidgetId,
|
|
10208
|
+
[widgetId]: { versionId, requestId, loading: false, error: errorDetails.message, errorDetails, url: null },
|
|
10209
|
+
};
|
|
10210
|
+
syncWidgetShapeLivePreviews();
|
|
10211
|
+
panelDisplayRevision += 1;
|
|
10212
|
+
}
|
|
10213
|
+
}
|
|
10214
|
+
|
|
10215
|
+
/** Commit SHA for the version currently selected in the version menu (drives Head-at-commit badge). */
|
|
10216
|
+
function resolveViewingCommitShaForPanel(widgetId: string): string | null {
|
|
10217
|
+
const ps = getPanelDisplayState(widgetId);
|
|
10218
|
+
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
10219
|
+
if (source === 'preview') {
|
|
10220
|
+
return normalizeCommitSha(ps.lastCommitId);
|
|
10221
|
+
}
|
|
10222
|
+
let version: number | null = null;
|
|
10223
|
+
if (source.startsWith('v:')) {
|
|
10224
|
+
const parsed = parseInt(source.slice(2), 10);
|
|
10225
|
+
version = Number.isFinite(parsed) ? parsed : null;
|
|
10226
|
+
} else {
|
|
10227
|
+
version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
10228
|
+
}
|
|
10229
|
+
if (version == null) return normalizeCommitSha(ps.lastCommitId);
|
|
10230
|
+
const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
|
|
10231
|
+
const record = history.find((r) => Number(r.version) === Number(version));
|
|
10232
|
+
const fromHistory = normalizeCommitSha(record?.commitSha ?? record?.commit_sha);
|
|
10233
|
+
if (fromHistory) return fromHistory;
|
|
10234
|
+
// Live version with no history row yet — fall back to published SHA, then HEAD.
|
|
10235
|
+
if (Number(version) === Number(ps.lastPublishedVersion)) {
|
|
10236
|
+
return (
|
|
10237
|
+
normalizeCommitSha(getLivePublishedCommitShaForPanel(widgetId))
|
|
10238
|
+
?? normalizeCommitSha(ps.lastCommitId)
|
|
10239
|
+
);
|
|
10240
|
+
}
|
|
10241
|
+
return normalizeCommitSha(ps.lastCommitId);
|
|
9035
10242
|
}
|
|
9036
10243
|
|
|
9037
10244
|
function syncWidgetShapeLivePreviews(): void {
|
|
9038
10245
|
try {
|
|
9039
10246
|
canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
|
|
9040
10247
|
widgetShapeLivePreviewsRef?.sync?.();
|
|
10248
|
+
// Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
|
|
10249
|
+
const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
|
|
10250
|
+
const liveWidgetIds = new Set<string>();
|
|
10251
|
+
for (const rect of rects) {
|
|
10252
|
+
if (!rect?.widgetId) continue;
|
|
10253
|
+
if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
|
|
10254
|
+
liveWidgetIds.add(rect.widgetId);
|
|
10255
|
+
canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
|
|
10256
|
+
}
|
|
10257
|
+
const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
|
|
10258
|
+
for (const widgetId of linkedIds) {
|
|
10259
|
+
if (!liveWidgetIds.has(widgetId)) {
|
|
10260
|
+
canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
|
|
10261
|
+
}
|
|
10262
|
+
}
|
|
9041
10263
|
} catch (err) {
|
|
9042
10264
|
console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
|
|
9043
10265
|
}
|
|
@@ -9380,6 +10602,9 @@ Requirements:
|
|
|
9380
10602
|
if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
|
|
9381
10603
|
if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
|
|
9382
10604
|
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
10605
|
+
// Any *other* draft panel still registered would keep a real widget panel
|
|
10606
|
+
// bound to a `draft_convert_*` context id — clear those too.
|
|
10607
|
+
purgeDraftConvertPanels();
|
|
9383
10608
|
// console.log(
|
|
9384
10609
|
// '[CreatorApp] Migrated Widget Details panel',
|
|
9385
10610
|
// fromId.substring(0, 16),
|
|
@@ -9975,6 +11200,16 @@ Requirements:
|
|
|
9975
11200
|
|
|
9976
11201
|
function hasUnpublishedChangesForPanel(widgetId: string): boolean {
|
|
9977
11202
|
const ps = getPanelDisplayState(widgetId);
|
|
11203
|
+
if (ps.pendingPublishAfterCodegen) return true;
|
|
11204
|
+
|
|
11205
|
+
// Never published but code exists: this is the first publishable version.
|
|
11206
|
+
// Without this the panel showed a "Draft" badge that could not be acted on —
|
|
11207
|
+
// there was no way to promote the first generation to Live.
|
|
11208
|
+
if (ps.lastPublishedVersion === null) {
|
|
11209
|
+
return panelHasPublishableCode(widgetId, ps);
|
|
11210
|
+
}
|
|
11211
|
+
|
|
11212
|
+
const pendingSha = loadPendingPublishShaForWidget(widgetId);
|
|
9978
11213
|
const headSha = resolvePublishHeadCommitId(widgetId, ps);
|
|
9979
11214
|
if (!headSha || ps.lastPublishedVersion === null) return false;
|
|
9980
11215
|
|
|
@@ -9982,6 +11217,7 @@ Requirements:
|
|
|
9982
11217
|
getLivePublishedCommitShaForPanel(widgetId)
|
|
9983
11218
|
?? getWidgetLastPublishCommitSha(widgetId);
|
|
9984
11219
|
if (!liveSha) return false;
|
|
11220
|
+
if (pendingSha && !commitsMatch(pendingSha, liveSha)) return true;
|
|
9985
11221
|
|
|
9986
11222
|
return !commitsMatch(headSha, liveSha);
|
|
9987
11223
|
}
|
|
@@ -10064,7 +11300,7 @@ Requirements:
|
|
|
10064
11300
|
* Single source of truth for Publish button label / disabled / toast reason.
|
|
10065
11301
|
* Prevents "Publish new vN" while click toasts "already live".
|
|
10066
11302
|
*/
|
|
10067
|
-
function getPublishGateForPanel(widgetId: string): {
|
|
11303
|
+
function getPublishGateForPanel(widgetId: string, targetVersion = null): {
|
|
10068
11304
|
disabled: boolean;
|
|
10069
11305
|
label: string;
|
|
10070
11306
|
reason: string | null;
|
|
@@ -10129,6 +11365,27 @@ Requirements:
|
|
|
10129
11365
|
showDisabledHint: true,
|
|
10130
11366
|
};
|
|
10131
11367
|
}
|
|
11368
|
+
const previewBlockReason = getVersionPreviewPublishBlockReason(widgetId, targetVersion);
|
|
11369
|
+
if (previewBlockReason) {
|
|
11370
|
+
return { disabled: true, label: 'Publish', reason: previewBlockReason, showDisabledHint: true };
|
|
11371
|
+
}
|
|
11372
|
+
// An explicit Draft owns its commit: HEAD being Live must not block it.
|
|
11373
|
+
// Keep all shared auth/canvas/repository/concurrency checks above this branch.
|
|
11374
|
+
if (targetVersion !== null) {
|
|
11375
|
+
const record = getShapePublishHistory(widgetId).find(
|
|
11376
|
+
(row) => Number(row.version) === Number(targetVersion),
|
|
11377
|
+
);
|
|
11378
|
+
const validVersion = targetVersion !== '' && Number.isInteger(Number(targetVersion)) && Number(targetVersion) > 0;
|
|
11379
|
+
const reason = !validVersion || !record
|
|
11380
|
+
? 'The selected draft version was not found.'
|
|
11381
|
+
: (record.publishedAt ?? record.published_at)
|
|
11382
|
+
? 'This version is already published.'
|
|
11383
|
+
: !(record.commitSha ?? record.commit_sha)
|
|
11384
|
+
? 'This draft has no recorded commit to publish.'
|
|
11385
|
+
: null;
|
|
11386
|
+
return { disabled: !!reason, label: 'Publish', reason, showDisabledHint: !!reason };
|
|
11387
|
+
}
|
|
11388
|
+
|
|
10132
11389
|
if (!hasCode) {
|
|
10133
11390
|
return {
|
|
10134
11391
|
disabled: true,
|
|
@@ -10216,6 +11473,31 @@ Requirements:
|
|
|
10216
11473
|
return 'No new version to publish';
|
|
10217
11474
|
}
|
|
10218
11475
|
|
|
11476
|
+
/**
|
|
11477
|
+
* Whether a specific Draft can be published.
|
|
11478
|
+
*
|
|
11479
|
+
* The main publish gate answers "is there newer code than Live?", which is
|
|
11480
|
+
* about HEAD. Selecting an older Draft in the version list is a different
|
|
11481
|
+
* question — that row has its own commit and can be promoted regardless of
|
|
11482
|
+
* where HEAD sits. Drafts with no recorded commit (a row that predates commit
|
|
11483
|
+
* tracking) cannot be rebuilt, so they are excluded rather than published
|
|
11484
|
+
* from whatever HEAD happens to contain.
|
|
11485
|
+
*/
|
|
11486
|
+
function canPublishDraftVersionForPanel(widgetId: string, version: number | null | undefined): boolean {
|
|
11487
|
+
if (version == null || version === '' || !Number.isFinite(Number(version))) return false;
|
|
11488
|
+
if (isPublishing && publishingWidgetId === widgetId) return false;
|
|
11489
|
+
if (isPublishing && publishingWidgetId !== null && publishingWidgetId !== widgetId) return false;
|
|
11490
|
+
if (generatingWidgetIds[widgetId]) return false;
|
|
11491
|
+
if (!widgetId || !getEffectiveCanvasId()) return false;
|
|
11492
|
+
if (!hasValidToken || !$userSession?.user) return false;
|
|
11493
|
+
const record = getPanelDisplayState(widgetId).publishHistory.find(
|
|
11494
|
+
(row) => Number(row.version) === Number(version),
|
|
11495
|
+
);
|
|
11496
|
+
if (!record) return false;
|
|
11497
|
+
if (record.publishedAt ?? record.published_at) return false;
|
|
11498
|
+
return !!(record.commitSha ?? record.commit_sha);
|
|
11499
|
+
}
|
|
11500
|
+
|
|
10219
11501
|
function isPublishDisabledForPanel(widgetId: string): boolean {
|
|
10220
11502
|
return getPublishGateForPanel(widgetId).disabled;
|
|
10221
11503
|
}
|
|
@@ -10250,7 +11532,12 @@ Requirements:
|
|
|
10250
11532
|
);
|
|
10251
11533
|
}
|
|
10252
11534
|
|
|
10253
|
-
|
|
11535
|
+
/**
|
|
11536
|
+
* Publish for a panel. Pass `targetVersion` to publish a specific Draft the
|
|
11537
|
+
* user selected in the version list (built from that version's own commit);
|
|
11538
|
+
* omit it to publish the newest generated code.
|
|
11539
|
+
*/
|
|
11540
|
+
function runPublishForPanel(panelWidgetId: string, targetVersion?: number | null): void {
|
|
10254
11541
|
void (async () => {
|
|
10255
11542
|
if (isPublishing && !publishingWidgetId) {
|
|
10256
11543
|
console.warn('[Publish] Clearing stuck global isPublishing flag');
|
|
@@ -10263,7 +11550,7 @@ Requirements:
|
|
|
10263
11550
|
|
|
10264
11551
|
// Gate BEFORE focus/tick — focusing can reconcile HEAD/live SHA mid-click and
|
|
10265
11552
|
// used to toast "already live" after the button still said "Publish new vN".
|
|
10266
|
-
const gateBefore = getPublishGateForPanel(panelWidgetId);
|
|
11553
|
+
const gateBefore = getPublishGateForPanel(panelWidgetId, targetVersion ?? null);
|
|
10267
11554
|
if (gateBefore.disabled) {
|
|
10268
11555
|
if (gateBefore.reason) {
|
|
10269
11556
|
showToast('info', 'Cannot publish yet', { description: gateBefore.reason, duration: 5000 });
|
|
@@ -10276,7 +11563,7 @@ Requirements:
|
|
|
10276
11563
|
await tick();
|
|
10277
11564
|
}
|
|
10278
11565
|
|
|
10279
|
-
const gateAfter = getPublishGateForPanel(panelWidgetId);
|
|
11566
|
+
const gateAfter = getPublishGateForPanel(panelWidgetId, targetVersion ?? null);
|
|
10280
11567
|
if (gateAfter.disabled) {
|
|
10281
11568
|
if (gateAfter.reason) {
|
|
10282
11569
|
showToast('info', 'Cannot publish yet', { description: gateAfter.reason, duration: 5000 });
|
|
@@ -10284,7 +11571,7 @@ Requirements:
|
|
|
10284
11571
|
return;
|
|
10285
11572
|
}
|
|
10286
11573
|
|
|
10287
|
-
await executePublishWidget(panelWidgetId);
|
|
11574
|
+
await executePublishWidget(panelWidgetId, { targetVersion: targetVersion ?? null });
|
|
10288
11575
|
})();
|
|
10289
11576
|
}
|
|
10290
11577
|
|
|
@@ -10422,6 +11709,13 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
|
|
|
10422
11709
|
publishHistoryResolved: true,
|
|
10423
11710
|
publishHistoryResolvedForWidgetId: widgetId,
|
|
10424
11711
|
});
|
|
11712
|
+
shapePublishHistoryByWidgetId = {
|
|
11713
|
+
...shapePublishHistoryByWidgetId,
|
|
11714
|
+
[widgetId]: [...history],
|
|
11715
|
+
};
|
|
11716
|
+
// Mark resolved even when empty — an empty history is a final answer and
|
|
11717
|
+
// must stop `ensureShapePublishHistory` from re-fetching on every render.
|
|
11718
|
+
shapeHistoryResolvedByWidgetId = { ...shapeHistoryResolvedByWidgetId, [widgetId]: true };
|
|
10425
11719
|
if (widgetId === focusedPanelWidgetId) {
|
|
10426
11720
|
publishHistory = history;
|
|
10427
11721
|
publishHistoryResolved = true;
|
|
@@ -10461,11 +11755,26 @@ async function loadPublishHistory(opts: { silent?: boolean } = {}) {
|
|
|
10461
11755
|
}
|
|
10462
11756
|
|
|
10463
11757
|
/**
|
|
10464
|
-
* Sets
|
|
10465
|
-
* Updates widgets.js_path to point to
|
|
11758
|
+
* Sets an already-published version as the current live version without rebuilding.
|
|
11759
|
+
* Updates widgets.js_path to point to that version's artifact URL.
|
|
11760
|
+
*
|
|
11761
|
+
* Only published versions can be promoted — a Draft has never been built to a
|
|
11762
|
+
* stable artifact. Publishing a Draft goes through the normal Publish flow, which
|
|
11763
|
+
* is why the UI never offers "Set live" for one.
|
|
10466
11764
|
*/
|
|
10467
|
-
async function
|
|
10468
|
-
if (!
|
|
11765
|
+
async function setLiveVersionForWidget(widgetId: string, version: number) {
|
|
11766
|
+
if (!widgetId || !$userSession?.jwt) return;
|
|
11767
|
+
if (!Number.isFinite(version)) return;
|
|
11768
|
+
|
|
11769
|
+
const record = getPanelDisplayState(widgetId).publishHistory.find(
|
|
11770
|
+
(row) => Number(row.version) === version,
|
|
11771
|
+
);
|
|
11772
|
+
if (record && !(record.publishedAt ?? record.published_at)) {
|
|
11773
|
+
showToast('info', 'This is a draft version', {
|
|
11774
|
+
description: 'Press Publish to make the latest generated code live.',
|
|
11775
|
+
});
|
|
11776
|
+
return;
|
|
11777
|
+
}
|
|
10469
11778
|
|
|
10470
11779
|
isSettingLiveVersion = version;
|
|
10471
11780
|
if (!widgetsClient) {
|
|
@@ -10475,12 +11784,12 @@ async function setLiveVersion(version: number) {
|
|
|
10475
11784
|
}
|
|
10476
11785
|
try {
|
|
10477
11786
|
const liveRes = await widgetsClient.setWidgetPublishHistoryLive({
|
|
10478
|
-
widgetId
|
|
11787
|
+
widgetId,
|
|
10479
11788
|
version
|
|
10480
11789
|
});
|
|
10481
11790
|
const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string } | undefined;
|
|
10482
11791
|
const liveData = nested?.data ?? nested ?? {};
|
|
10483
|
-
const history = getPanelDisplayState(
|
|
11792
|
+
const history = getPanelDisplayState(widgetId).publishHistory;
|
|
10484
11793
|
const record = history.find((row) => row.version === version);
|
|
10485
11794
|
const commitSha = (liveData as { commitSha?: string; commit_sha?: string }).commitSha
|
|
10486
11795
|
|| (liveData as { commit_sha?: string }).commit_sha
|
|
@@ -10488,15 +11797,23 @@ async function setLiveVersion(version: number) {
|
|
|
10488
11797
|
const artifactUrl = (liveData as { artifactUrl?: string; artifact_url?: string }).artifactUrl
|
|
10489
11798
|
|| (liveData as { artifact_url?: string }).artifact_url
|
|
10490
11799
|
|| record?.artifactUrl || record?.artifact_url || null;
|
|
10491
|
-
applyPublishSuccessToWidgetState(
|
|
11800
|
+
applyPublishSuccessToWidgetState(widgetId, version, artifactUrl, commitSha);
|
|
11801
|
+
shapePreviewSourceByWidgetId = {
|
|
11802
|
+
...shapePreviewSourceByWidgetId,
|
|
11803
|
+
[widgetId]: `v:${version}`,
|
|
11804
|
+
};
|
|
11805
|
+
syncWidgetShapeLivePreviews();
|
|
11806
|
+
panelDisplayRevision += 1;
|
|
10492
11807
|
isHistoryOpen = false;
|
|
10493
|
-
const liveUrl = resolvePublishedWidgetUrl(
|
|
10494
|
-
const detailsRef = getWidgetDetailsRef(
|
|
11808
|
+
const liveUrl = resolvePublishedWidgetUrl(widgetId, version, artifactUrl);
|
|
11809
|
+
const detailsRef = getWidgetDetailsRef(widgetId);
|
|
10495
11810
|
detailsRef?.showPublishedArtifact?.(liveUrl);
|
|
10496
11811
|
detailsRef?.beginCompositionHydration?.();
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
11812
|
+
if (widgetId === selectedWidgetId || widgetId === focusedPanelWidgetId) {
|
|
11813
|
+
revealCompositionPreview();
|
|
11814
|
+
}
|
|
11815
|
+
showToast('success', `Live: v${version}`, {
|
|
11816
|
+
description: 'This version is now what visitors see on your sites.',
|
|
10500
11817
|
duration: 5000
|
|
10501
11818
|
});
|
|
10502
11819
|
} catch (err: any) {
|
|
@@ -10507,12 +11824,22 @@ async function setLiveVersion(version: number) {
|
|
|
10507
11824
|
}
|
|
10508
11825
|
}
|
|
10509
11826
|
|
|
10510
|
-
|
|
11827
|
+
async function setLiveVersion(version: number) {
|
|
11828
|
+
if (!selectedWidgetId) return;
|
|
11829
|
+
await setLiveVersionForWidget(selectedWidgetId, version);
|
|
11830
|
+
}
|
|
11831
|
+
|
|
11832
|
+
/** Toggle publish history panel — reloads data on every open.
|
|
11833
|
+
* Right-aligns the dropdown with the whole header cluster (vN trigger +
|
|
11834
|
+
* LIVE/PUBLISH badge), not just the trigger button, per UX request. */
|
|
10511
11835
|
function togglePublishHistory(triggerEl?: HTMLElement | null) {
|
|
10512
11836
|
isHistoryOpen = !isHistoryOpen;
|
|
10513
11837
|
if (isHistoryOpen) {
|
|
10514
11838
|
if (triggerEl) {
|
|
10515
|
-
|
|
11839
|
+
// The vN trigger sits left of the LIVE/PUBLISH badge inside the same
|
|
11840
|
+
// header cluster; align the panel to that cluster's right edge.
|
|
11841
|
+
const cluster = triggerEl.closest('.wc-version-history-ct') as HTMLElement | null;
|
|
11842
|
+
const rect = (cluster ?? triggerEl).getBoundingClientRect();
|
|
10516
11843
|
liveHistoryPanelTop = Math.round(rect.bottom + 4);
|
|
10517
11844
|
liveHistoryPanelRight = Math.round(window.innerWidth - rect.right);
|
|
10518
11845
|
}
|
|
@@ -10999,6 +12326,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
10999
12326
|
let lastWidgetShapeClickId = '';
|
|
11000
12327
|
const WIDGET_SHAPE_CLICK_DEBOUNCE_MS = 450;
|
|
11001
12328
|
|
|
12329
|
+
/**
|
|
12330
|
+
* Widgets whose live-iframe thumbnail capture is currently running.
|
|
12331
|
+
* Prevents overlapping captures when several generation/refresh events fire
|
|
12332
|
+
* close together for the same widget.
|
|
12333
|
+
*/
|
|
12334
|
+
const shapeThumbnailCaptureInFlight = new Set<string>();
|
|
12335
|
+
|
|
11002
12336
|
/**
|
|
11003
12337
|
* Called when a Widget Image shape is clicked on the canvas.
|
|
11004
12338
|
* Selects the linked widget and opens WidgetDetails.
|
|
@@ -11065,18 +12399,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11065
12399
|
}
|
|
11066
12400
|
}
|
|
11067
12401
|
|
|
11068
|
-
//
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
if (url.searchParams.get('widget_id') !== widgetId) {
|
|
11073
|
-
url.searchParams.set('widget_id', widgetId);
|
|
11074
|
-
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
11075
|
-
syncEmbeddedDeepLinkDismissTracking();
|
|
11076
|
-
}
|
|
11077
|
-
} catch (err) {
|
|
11078
|
-
console.warn('[WidgetCreator] Failed to sync widget_id in URL:', err);
|
|
11079
|
-
}
|
|
12402
|
+
// Clicking a widget shape means "open this widget" — make it the canvas primary so a
|
|
12403
|
+
// refresh reopens it. The URL only carries canvas_id; widget_id is no longer written.
|
|
12404
|
+
if (embedded && widgetId !== canvasPrimaryWidgetId) {
|
|
12405
|
+
void persistPrimaryWidgetId(widgetId);
|
|
11080
12406
|
}
|
|
11081
12407
|
// Full select so commit/preview/header match the shape's linked widget (not a stale URL widget).
|
|
11082
12408
|
if (selectedWidgetId !== widgetId) {
|
|
@@ -11308,6 +12634,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11308
12634
|
}
|
|
11309
12635
|
notifyHostWidgetName({ id: widgetId, name: newName });
|
|
11310
12636
|
emitCanvasWidgetsList('widget-renamed');
|
|
12637
|
+
widgetNamesRevision += 1;
|
|
12638
|
+
panelDisplayRevision += 1;
|
|
12639
|
+
backfillWidgetShapeMetadata();
|
|
12640
|
+
syncWidgetShapeLivePreviews();
|
|
11311
12641
|
showToast('success', `Widget renamed to "${newName}"`);
|
|
11312
12642
|
} catch (error) {
|
|
11313
12643
|
console.error('[WidgetCreator] Failed to rename widget:', error);
|
|
@@ -11359,14 +12689,15 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11359
12689
|
}
|
|
11360
12690
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
11361
12691
|
await maybeAutoRestoreWidgetShapes();
|
|
11362
|
-
const
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
12692
|
+
const linkedIds =
|
|
12693
|
+
widgetIdsOnCanvas.length > 0
|
|
12694
|
+
? widgetIdsOnCanvas
|
|
12695
|
+
: [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
|
|
12696
|
+
const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
|
|
12697
|
+
applyPrimarySelectionOnCanvas(primaryId);
|
|
12698
|
+
requestAnimationFrame(() => {
|
|
12699
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
|
|
12700
|
+
});
|
|
11370
12701
|
syncWidgetShapeLivePreviews();
|
|
11371
12702
|
}
|
|
11372
12703
|
|
|
@@ -11640,15 +12971,29 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11640
12971
|
*/
|
|
11641
12972
|
let autosaveSequence = 0;
|
|
11642
12973
|
let lastResolvedAutosaveSequence = 0;
|
|
11643
|
-
|
|
11644
|
-
|
|
12974
|
+
/**
|
|
12975
|
+
* Trailing save deferred while `isLoadCooldownActive` is set.
|
|
12976
|
+
* See {@link handleCanvasAutoSave}: dropping cooldown saves lost user edits.
|
|
12977
|
+
*/
|
|
12978
|
+
let pendingCooldownAutosaveJson: string | null = null;
|
|
12979
|
+
let cooldownAutosaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
12980
|
+
/** How long after a canvas load we still defer (not drop) autosaves. */
|
|
12981
|
+
const CANVAS_COOLDOWN_AUTOSAVE_DELAY_MS = 3000;
|
|
12982
|
+
|
|
12983
|
+
async function handleCanvasAutoSave(
|
|
12984
|
+
json: string | null,
|
|
12985
|
+
isUnmountSave = false,
|
|
12986
|
+
opts: { force?: boolean } = {},
|
|
12987
|
+
) {
|
|
11645
12988
|
if (!json) {
|
|
11646
12989
|
if (isUnmountSave) console.warn('[CanvasDebug] onBeforeUnmount: saveCanvas returned null — canvas not initialized');
|
|
11647
12990
|
return;
|
|
11648
12991
|
}
|
|
11649
12992
|
|
|
12993
|
+
const force = opts.force === true;
|
|
12994
|
+
|
|
11650
12995
|
// Guard: don't overwrite DB with an empty canvas if state hasn't loaded yet
|
|
11651
|
-
if (!isCanvasStateLoaded) {
|
|
12996
|
+
if (!force && !isCanvasStateLoaded) {
|
|
11652
12997
|
// console.log(
|
|
11653
12998
|
// '[CanvasDebug] Skipping autosave — canvas state not loaded yet (guard active, avoids overwrite with empty)'
|
|
11654
12999
|
// );
|
|
@@ -11656,8 +13001,21 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11656
13001
|
}
|
|
11657
13002
|
|
|
11658
13003
|
// Guard: skip autosaves triggered by loadCanvas() object:added events
|
|
11659
|
-
|
|
11660
|
-
|
|
13004
|
+
//
|
|
13005
|
+
// Dropping the save outright loses any edit the user made during the cooldown
|
|
13006
|
+
// (canvas hydration, shape restore, and `loadCanvas`'s object:added events all
|
|
13007
|
+
// run inside it). Defer one trailing save instead: the load burst still cannot
|
|
13008
|
+
// overwrite freshly painted state, but a real move/resize a moment later is
|
|
13009
|
+
// persisted rather than silently discarded.
|
|
13010
|
+
if (!force && isLoadCooldownActive && !isUnmountSave) {
|
|
13011
|
+
pendingCooldownAutosaveJson = json;
|
|
13012
|
+
if (cooldownAutosaveTimer) clearTimeout(cooldownAutosaveTimer);
|
|
13013
|
+
cooldownAutosaveTimer = setTimeout(() => {
|
|
13014
|
+
cooldownAutosaveTimer = null;
|
|
13015
|
+
const deferred = pendingCooldownAutosaveJson;
|
|
13016
|
+
pendingCooldownAutosaveJson = null;
|
|
13017
|
+
if (deferred) void handleCanvasAutoSave(deferred);
|
|
13018
|
+
}, CANVAS_COOLDOWN_AUTOSAVE_DELAY_MS);
|
|
11661
13019
|
return;
|
|
11662
13020
|
}
|
|
11663
13021
|
|
|
@@ -11679,11 +13037,17 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11679
13037
|
// Unmount saves (HMR) are always blocked when going from N→0.
|
|
11680
13038
|
// Normal autosaves going from N→0 are allowed only if the drop is gradual
|
|
11681
13039
|
// (user deleted objects one by one) — a sudden jump from many to 0 is suspicious.
|
|
13040
|
+
// Explicit Save (force) still blocks sudden N→0 to avoid wiping the canvas.
|
|
11682
13041
|
if (objectCount === 0 && lastSavedCanvasObjectCount > 0) {
|
|
11683
13042
|
if (isUnmountSave || lastSavedCanvasObjectCount > 1) {
|
|
11684
13043
|
console.warn(
|
|
11685
|
-
`[CanvasDebug] BLOCKING save: would overwrite ${lastSavedCanvasObjectCount} shapes with 0 (isUnmount=${isUnmountSave})`
|
|
13044
|
+
`[CanvasDebug] BLOCKING save: would overwrite ${lastSavedCanvasObjectCount} shapes with 0 (isUnmount=${isUnmountSave}, force=${force})`
|
|
11686
13045
|
);
|
|
13046
|
+
if (force) {
|
|
13047
|
+
showToast('error', 'Save blocked', {
|
|
13048
|
+
description: 'Canvas snapshot looked empty — refresh and try again.',
|
|
13049
|
+
});
|
|
13050
|
+
}
|
|
11687
13051
|
return;
|
|
11688
13052
|
}
|
|
11689
13053
|
// lastSavedCanvasObjectCount === 1 → user deleted the last object, allow it
|
|
@@ -11720,12 +13084,25 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11720
13084
|
return;
|
|
11721
13085
|
}
|
|
11722
13086
|
lastResolvedAutosaveSequence = currentSeq;
|
|
13087
|
+
if (force) {
|
|
13088
|
+
showToast('success', 'Canvas saved', { duration: 2000 });
|
|
13089
|
+
}
|
|
11723
13090
|
// console.log(
|
|
11724
13091
|
// `[CanvasDebug] Canvas ${canvasId} saved to API: ${objectCount} shapes (seq ${currentSeq})`
|
|
11725
13092
|
// );
|
|
11726
13093
|
} catch (err) {
|
|
11727
13094
|
console.warn('[WidgetCreator] Failed to save canvas to API:', err);
|
|
13095
|
+
if (force) {
|
|
13096
|
+
showToast('error', 'Canvas save failed', {
|
|
13097
|
+
description: 'Kept a local copy — check your connection and retry.',
|
|
13098
|
+
});
|
|
13099
|
+
}
|
|
11728
13100
|
}
|
|
13101
|
+
} else if (force) {
|
|
13102
|
+
showToast('success', 'Canvas saved locally', {
|
|
13103
|
+
description: canvasId ? 'API client unavailable.' : 'No canvas id yet.',
|
|
13104
|
+
duration: 2500,
|
|
13105
|
+
});
|
|
11729
13106
|
}
|
|
11730
13107
|
|
|
11731
13108
|
refreshLinkedWidgetIds();
|
|
@@ -12069,6 +13446,68 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12069
13446
|
return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
|
|
12070
13447
|
}
|
|
12071
13448
|
|
|
13449
|
+
/**
|
|
13450
|
+
* True for conversations that exist only in the browser.
|
|
13451
|
+
*
|
|
13452
|
+
* Draft convert panels need a conversation object before the widget has a DB
|
|
13453
|
+
* row, so `handleCreateConversation` issues a `local_*` id. Message load/save
|
|
13454
|
+
* must treat those as local-only rather than querying the API with a key that
|
|
13455
|
+
* cannot exist there.
|
|
13456
|
+
*/
|
|
13457
|
+
function isLocalOnlyConversationId(conversationId: string | null | undefined): boolean {
|
|
13458
|
+
return !!conversationId && conversationId.startsWith('local_');
|
|
13459
|
+
}
|
|
13460
|
+
|
|
13461
|
+
/**
|
|
13462
|
+
* Drop synthetic draft ids from the panel registry.
|
|
13463
|
+
*
|
|
13464
|
+
* `migrateOpenPanelWidgetId` renames every map keyed by widget id, but the
|
|
13465
|
+
* `{#each openWidgetPanelIds}` block is keyed by `panelInstanceKeyByWidgetId`,
|
|
13466
|
+
* so a draft id that survives anywhere in that list can re-render a real
|
|
13467
|
+
* widget's panel while `context.id` still points at the draft. That panel then
|
|
13468
|
+
* asks the API for `draft_convert_*` conversations and schemas — 400s and
|
|
13469
|
+
* "Failed to initialize chat" for a widget whose history is perfectly fine.
|
|
13470
|
+
*
|
|
13471
|
+
* Draft panels are only ever opened through the convert flow, which re-creates
|
|
13472
|
+
* them from `draftConvertObjectIds`, so dropping them here is safe.
|
|
13473
|
+
*/
|
|
13474
|
+
function purgeDraftConvertPanels(): void {
|
|
13475
|
+
if (!openWidgetPanelIds.some((id) => isDraftConvertId(id))) return;
|
|
13476
|
+
|
|
13477
|
+
const draftIds = openWidgetPanelIds.filter((id) => isDraftConvertId(id));
|
|
13478
|
+
const remainingIds = openWidgetPanelIds.filter((id) => !isDraftConvertId(id));
|
|
13479
|
+
const nextInstanceKeys = { ...panelInstanceKeyByWidgetId };
|
|
13480
|
+
const nextZIndexes = { ...panelZIndexByWidgetId };
|
|
13481
|
+
const nextRefs = { ...widgetDetailsRefs };
|
|
13482
|
+
const nextChatMethods = { ...chatMethodsByWidgetId };
|
|
13483
|
+
for (const draftId of draftIds) {
|
|
13484
|
+
delete nextInstanceKeys[draftId];
|
|
13485
|
+
delete nextZIndexes[draftId];
|
|
13486
|
+
delete nextRefs[draftId];
|
|
13487
|
+
delete nextChatMethods[draftId];
|
|
13488
|
+
}
|
|
13489
|
+
panelInstanceKeyByWidgetId = nextInstanceKeys;
|
|
13490
|
+
panelZIndexByWidgetId = nextZIndexes;
|
|
13491
|
+
widgetDetailsRefs = nextRefs;
|
|
13492
|
+
chatMethodsByWidgetId = nextChatMethods;
|
|
13493
|
+
openWidgetPanelIds = remainingIds;
|
|
13494
|
+
|
|
13495
|
+
if (focusedPanelWidgetId && isDraftConvertId(focusedPanelWidgetId)) {
|
|
13496
|
+
focusedPanelWidgetId = remainingIds.at(-1) ?? null;
|
|
13497
|
+
}
|
|
13498
|
+
if (selectedWidgetId && isDraftConvertId(selectedWidgetId)) {
|
|
13499
|
+
selectedWidgetId = focusedPanelWidgetId;
|
|
13500
|
+
}
|
|
13501
|
+
if (detailsSessionWidgetId && isDraftConvertId(detailsSessionWidgetId)) {
|
|
13502
|
+
detailsSessionWidgetId = null;
|
|
13503
|
+
}
|
|
13504
|
+
if (widgetDetailsOpenTargetId && isDraftConvertId(widgetDetailsOpenTargetId)) {
|
|
13505
|
+
widgetDetailsOpenTargetId = null;
|
|
13506
|
+
}
|
|
13507
|
+
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
13508
|
+
console.warn('[CreatorApp] Purged draft convert panels:', draftIds.length);
|
|
13509
|
+
}
|
|
13510
|
+
|
|
12072
13511
|
function isConvertOrDraftPanelOpen(): boolean {
|
|
12073
13512
|
return (
|
|
12074
13513
|
isDraftConvertId(focusedPanelWidgetId)
|
|
@@ -12507,13 +13946,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12507
13946
|
<!-- Right: Debug toggle + Auth info (2 lines) + Logout -->
|
|
12508
13947
|
<div class="wc-top-bar-right flex items-center gap-3 shrink-0">
|
|
12509
13948
|
{#if debugModeAvailable}
|
|
13949
|
+
<HoverTooltip text={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}>
|
|
12510
13950
|
<button
|
|
12511
13951
|
onclick={toggleDebugMode}
|
|
12512
13952
|
class="wc-debug-toggle px-2.5 py-1 cursor-pointer text-xs font-medium rounded-lg border transition-colors {debugMode ? 'bg-red-50 border-red-400 text-red-600 hover:bg-red-100' : 'bg-gray-100 border-gray-300 text-gray-500 hover:bg-gray-200'}"
|
|
12513
|
-
title={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}
|
|
12514
13953
|
>
|
|
12515
13954
|
🐛 Debug {debugMode ? 'ON' : 'OFF'}
|
|
12516
13955
|
</button>
|
|
13956
|
+
</HoverTooltip>
|
|
12517
13957
|
{/if}
|
|
12518
13958
|
<div class="wc-auth-info flex flex-col gap-px items-start">
|
|
12519
13959
|
<div class="wc-auth-line1 flex items-center gap-2">
|
|
@@ -12581,6 +14021,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12581
14021
|
<svelte:component this={WidgeticCanvas}
|
|
12582
14022
|
bind:this={canvasRef}
|
|
12583
14023
|
showContainerFrame={!embedded}
|
|
14024
|
+
widgetSelectionLayerTarget="floating-panels-overlay"
|
|
12584
14025
|
bind:wireframeMode={canvasWireframeMode}
|
|
12585
14026
|
advancedFeatures={canvasAdvancedFeatures}
|
|
12586
14027
|
onReady={handleCanvasReady}
|
|
@@ -12598,10 +14039,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12598
14039
|
onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
|
|
12599
14040
|
onWidgetShapeDeleting={handleWidgetShapeDeleting}
|
|
12600
14041
|
onWidgetRename={handleWidgetRename}
|
|
12601
|
-
onRefreshScreenshot={
|
|
14042
|
+
onRefreshScreenshot={null}
|
|
12602
14043
|
onSetPrimaryWidget={handleSetPrimaryWidget}
|
|
12603
14044
|
onAfterRender={syncWidgetShapeLivePreviews}
|
|
12604
14045
|
getWidgetName={getWidgetNameById}
|
|
14046
|
+
widgetNameRevision={widgetNamesRevision}
|
|
12605
14047
|
isWidgetKnown={isWidgetKnownById}
|
|
12606
14048
|
onNonWidgetSelected={() => {
|
|
12607
14049
|
if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
|
|
@@ -12630,17 +14072,43 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12630
14072
|
bind:this={widgetShapeLivePreviewsRef}
|
|
12631
14073
|
getRects={() => canvasRef?.listWidgetShapeScreenRects?.() ?? []}
|
|
12632
14074
|
resolveUrl={resolveWidgetShapeLivePreviewUrl}
|
|
14075
|
+
resolvePreviewState={resolveWidgetShapeVersionPreviewState}
|
|
14076
|
+
onRetryPreview={retryWidgetShapeVersionPreview}
|
|
14077
|
+
primaryWidgetId={canvasPrimaryWidgetId}
|
|
14078
|
+
onSetPrimary={(widgetId) => handleHostSetPrimaryWidgetEvent(new CustomEvent(HOST_SET_PRIMARY_WIDGET_EVENT, { detail: { widgetId } }))}
|
|
14079
|
+
resolvePublishDisabled={(widgetId, version) => getPublishGateForPanel(widgetId, version ?? null).disabled}
|
|
14080
|
+
resolvePublishDisabledReason={(widgetId, version) => getPublishGateForPanel(widgetId, version ?? null).reason}
|
|
14081
|
+
resolveTitle={getWidgetNameById}
|
|
14082
|
+
titleRevision={widgetNamesRevision + panelDisplayRevision}
|
|
14083
|
+
resolveVersionLabel={resolveWidgetShapeVersionLabel}
|
|
14084
|
+
resolveVersionOptions={resolveWidgetShapeVersionOptions}
|
|
14085
|
+
resolveVersionSelectedId={resolveWidgetShapeVersionSelectedId}
|
|
14086
|
+
onVersionChange={handleWidgetShapeVersionChange}
|
|
14087
|
+
onSetLive={(widgetId, version) => {
|
|
14088
|
+
void setLiveVersionForWidget(widgetId, version);
|
|
14089
|
+
}}
|
|
14090
|
+
settingLiveVersion={isSettingLiveVersion}
|
|
14091
|
+
resolveCanPublish={(widgetId) => hasUnpublishedChangesForPanel(widgetId)}
|
|
14092
|
+
resolveCanPublishDraft={(widgetId, version) => canPublishDraftVersionForPanel(widgetId, version)}
|
|
14093
|
+
resolveIsPublishing={(widgetId) => isPublishing && publishingWidgetId === widgetId}
|
|
14094
|
+
onPublish={(widgetId) => runPublishForPanel(widgetId)}
|
|
14095
|
+
onPublishDraft={(widgetId, version) => runPublishForPanel(widgetId, version)}
|
|
12633
14096
|
selectedWidgetId={canvasSelectedWidgetShapeId}
|
|
12634
14097
|
onTranslate={(widgetId, dx, dy) => {
|
|
12635
14098
|
canvasRef?.translateWidgetShapeByScreen?.(widgetId, dx, dy);
|
|
12636
14099
|
}}
|
|
14100
|
+
onTranslateEnd={(widgetId) => {
|
|
14101
|
+
canvasRef?.commitWidgetShapeTranslate?.(widgetId);
|
|
14102
|
+
}}
|
|
12637
14103
|
onSelectShape={(widgetId) => {
|
|
12638
14104
|
canvasRef?.selectWidgetShapeById?.(widgetId);
|
|
12639
14105
|
canvasSelectedWidgetShapeId = widgetId;
|
|
14106
|
+
void ensureShapePublishHistory(widgetId);
|
|
12640
14107
|
}}
|
|
12641
14108
|
onOpenChat={(widgetId) => {
|
|
12642
14109
|
void openWidgetDetailsPanel(widgetId);
|
|
12643
14110
|
}}
|
|
14111
|
+
onRefresh={handleWidgetShapeRefresh}
|
|
12644
14112
|
/>
|
|
12645
14113
|
|
|
12646
14114
|
<!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
|
|
@@ -12934,6 +14402,16 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12934
14402
|
{@const panelWidget = widgetsById.get(panelWidgetId) ?? null}
|
|
12935
14403
|
{@const isPanelFocused = focusedPanelWidgetId === panelWidgetId}
|
|
12936
14404
|
{@const ps = (panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
|
|
14405
|
+
<!--
|
|
14406
|
+
Same published-version resolution the WidgetDetails instance below gets.
|
|
14407
|
+
`ps.lastPublishedVersion` alone is only hydrated from the focused panel /
|
|
14408
|
+
DB snapshot; a widget whose summary carries `jsPath` (/v<N>/) or `version`
|
|
14409
|
+
can be published while `ps.lastPublishedVersion` is still null. The header
|
|
14410
|
+
version menu must not disagree with the panel's own Publish/Live state.
|
|
14411
|
+
-->
|
|
14412
|
+
{@const panelPublishedVersion = isDraftConvertId(panelWidgetId)
|
|
14413
|
+
? null
|
|
14414
|
+
: (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
|
|
12937
14415
|
{@const panelHasUnpublishedChanges = (panelDisplayRevision, widgets.length, hasUnpublishedChangesForPanel(panelWidgetId))}
|
|
12938
14416
|
{@const panelHasCompileErrorState = (panelDisplayRevision, panelHasCompileError(panelWidgetId))}
|
|
12939
14417
|
{@const panelHasInfraPreviewError = (panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
|
|
@@ -13236,9 +14714,55 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13236
14714
|
hasAutoOpenedPreview = detail.hasAutoOpenedPreview;
|
|
13237
14715
|
}}
|
|
13238
14716
|
>
|
|
14717
|
+
<svelte:fragment slot="header-meta">
|
|
14718
|
+
{#if saveStatus === 'saving' && isPanelFocused}
|
|
14719
|
+
<span class="wc-header-save-status text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 animate-pulse">Saving...</span>
|
|
14720
|
+
{:else if saveStatus === 'saved' && isPanelFocused}
|
|
14721
|
+
<span class="wc-header-save-status text-[10px] px-1.5 py-0.5 rounded bg-green-100 text-green-700">Saved</span>
|
|
14722
|
+
{:else if saveStatus === 'error' && isPanelFocused}
|
|
14723
|
+
<span class="wc-header-save-status text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-700">Save Error</span>
|
|
14724
|
+
{/if}
|
|
14725
|
+
{#if debugMode}
|
|
14726
|
+
<span class="wc-header-commit text-[10px] text-gray-500 whitespace-nowrap">
|
|
14727
|
+
Commit:
|
|
14728
|
+
<span class="font-mono font-medium text-gray-600" title={ps.lastCommitId || ''}>
|
|
14729
|
+
{ps.lastCommitId ? ps.lastCommitId.substring(0, 8) : '—'}
|
|
14730
|
+
</span>
|
|
14731
|
+
</span>
|
|
14732
|
+
{/if}
|
|
14733
|
+
</svelte:fragment>
|
|
13239
14734
|
<svelte:fragment slot="version-history">
|
|
13240
|
-
|
|
13241
|
-
|
|
14735
|
+
<!--
|
|
14736
|
+
A widget that has only ever been generated (never published) still
|
|
14737
|
+
has versions to preview — the history holds them as Drafts. Gating
|
|
14738
|
+
purely on `panelPublishedVersion` hid the picker for exactly those
|
|
14739
|
+
widgets, which is why the dropdown looked missing.
|
|
14740
|
+
-->
|
|
14741
|
+
{#if panelPublishedVersion !== null || ps.publishHistory.length > 0}
|
|
14742
|
+
{@const newestDraft = ps.publishHistory.find(
|
|
14743
|
+
(row) => !(row.publishedAt ?? row.published_at),
|
|
14744
|
+
)}
|
|
14745
|
+
{@const defaultVersionSource = panelPublishedVersion !== null
|
|
14746
|
+
? `v:${panelPublishedVersion}`
|
|
14747
|
+
: (newestDraft ? `v:${newestDraft.version}` : null)}
|
|
14748
|
+
{@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
|
|
14749
|
+
{@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
|
|
14750
|
+
? parseInt(shapeSource.slice(2), 10)
|
|
14751
|
+
: panelPublishedVersion}
|
|
14752
|
+
{@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
|
|
14753
|
+
<!--
|
|
14754
|
+
A version is a Draft until it is published. The history is one
|
|
14755
|
+
sequence (`v1, v2, v3…`), so "draft" is read from the record's
|
|
14756
|
+
`publishedAt`, not from any second numbering scheme.
|
|
14757
|
+
-->
|
|
14758
|
+
{@const viewingRecord = ps.publishHistory.find(
|
|
14759
|
+
(row) => Number(row.version) === Number(viewingVersion),
|
|
14760
|
+
)}
|
|
14761
|
+
{@const viewingIsDraft = viewingRecord
|
|
14762
|
+
? !(viewingRecord.publishedAt ?? viewingRecord.published_at)
|
|
14763
|
+
: false}
|
|
14764
|
+
<div class="wc-version-history-ct relative shrink-0 flex items-center gap-0.5">
|
|
14765
|
+
<HoverTooltip text="Switch version" maxWidth={120}>
|
|
13242
14766
|
<button
|
|
13243
14767
|
type="button"
|
|
13244
14768
|
onclick={(e) => {
|
|
@@ -13246,11 +14770,66 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13246
14770
|
togglePublishHistory(e.currentTarget);
|
|
13247
14771
|
}}
|
|
13248
14772
|
onmousedown={(e) => e.stopPropagation()}
|
|
13249
|
-
class="wc-version-history-toggle flex items-center gap-
|
|
14773
|
+
class="wc-version-history-toggle flex items-center gap-0.5 text-[10px] font-semibold tabular-nums px-1.5 py-0.5 rounded border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 cursor-pointer whitespace-nowrap"
|
|
13250
14774
|
>
|
|
13251
|
-
|
|
14775
|
+
{`v${Number.isFinite(viewingVersion) ? viewingVersion : panelPublishedVersion}`}
|
|
13252
14776
|
<span class="text-emerald-500">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
|
|
13253
14777
|
</button>
|
|
14778
|
+
</HoverTooltip>
|
|
14779
|
+
{#if panelHasUnpublishedChanges && !Number.isFinite(viewingVersion)}
|
|
14780
|
+
<button
|
|
14781
|
+
type="button"
|
|
14782
|
+
onclick={(e) => {
|
|
14783
|
+
e.stopPropagation();
|
|
14784
|
+
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
14785
|
+
runPublishForPanel(panelWidgetId);
|
|
14786
|
+
}}
|
|
14787
|
+
disabled={isPublishDisabledForPanel(panelWidgetId) || (isPublishing && publishingWidgetId === panelWidgetId)}
|
|
14788
|
+
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? 'Publish the latest generated code as the Live version'}
|
|
14789
|
+
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"
|
|
14790
|
+
>
|
|
14791
|
+
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14792
|
+
</button>
|
|
14793
|
+
{:else if viewingIsLive}
|
|
14794
|
+
<span
|
|
14795
|
+
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"
|
|
14796
|
+
title="Live — generate new code to Publish a new version"
|
|
14797
|
+
>Live</span>
|
|
14798
|
+
{:else if viewingIsDraft}
|
|
14799
|
+
{#if viewingIsDraft}
|
|
14800
|
+
<button
|
|
14801
|
+
type="button"
|
|
14802
|
+
onclick={(e) => {
|
|
14803
|
+
e.stopPropagation();
|
|
14804
|
+
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
14805
|
+
if (getPublishGateForPanel(panelWidgetId, Number(viewingVersion)).disabled) return;
|
|
14806
|
+
handleWidgetShapeVersionChange(panelWidgetId, `v:${viewingVersion}`, false);
|
|
14807
|
+
runPublishForPanel(panelWidgetId, Number(viewingVersion));
|
|
14808
|
+
}}
|
|
14809
|
+
disabled={!canPublishDraftVersionForPanel(panelWidgetId, viewingVersion) || getPublishGateForPanel(panelWidgetId, Number(viewingVersion)).disabled}
|
|
14810
|
+
title="Build this draft version and make it the Live version"
|
|
14811
|
+
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"
|
|
14812
|
+
>
|
|
14813
|
+
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14814
|
+
</button>
|
|
14815
|
+
{/if}
|
|
14816
|
+
{:else}
|
|
14817
|
+
<button
|
|
14818
|
+
type="button"
|
|
14819
|
+
onclick={(e) => {
|
|
14820
|
+
e.stopPropagation();
|
|
14821
|
+
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
14822
|
+
if (Number.isFinite(viewingVersion)) {
|
|
14823
|
+
void setLiveVersionForWidget(panelWidgetId, Number(viewingVersion));
|
|
14824
|
+
}
|
|
14825
|
+
}}
|
|
14826
|
+
disabled={isSettingLiveVersion === Number(viewingVersion)}
|
|
14827
|
+
title="Publish this version so visitors see it on your website"
|
|
14828
|
+
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"
|
|
14829
|
+
>
|
|
14830
|
+
{isSettingLiveVersion === Number(viewingVersion) ? '...' : 'Set live'}
|
|
14831
|
+
</button>
|
|
14832
|
+
{/if}
|
|
13254
14833
|
{#if isHistoryOpen && isPanelFocused}
|
|
13255
14834
|
<Portal target="wc-live-history-portal-{panelWidgetId}">
|
|
13256
14835
|
<button
|
|
@@ -13260,49 +14839,44 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13260
14839
|
onclick={() => { isHistoryOpen = false; }}
|
|
13261
14840
|
></button>
|
|
13262
14841
|
<div
|
|
13263
|
-
class="wc-version-history-panel flex flex-col gap-1
|
|
14842
|
+
class="wc-version-history-panel flex flex-col gap-1 rounded-md border border-emerald-200 bg-white shadow-lg p-1.5 z-[9999] w-max"
|
|
13264
14843
|
style="position: fixed; top: {liveHistoryPanelTop}px; right: {liveHistoryPanelRight}px; max-width: min(22rem, calc(100vw - 16px));"
|
|
13265
14844
|
>
|
|
13266
14845
|
{#if isLoadingHistory}
|
|
13267
14846
|
<p class="text-xs text-emerald-500 animate-pulse text-center py-2">Loading history...</p>
|
|
13268
14847
|
{:else if ps.publishHistory.length === 0}
|
|
13269
|
-
<p class="text-xs text-emerald-400 text-center py-2">No history records yet.
|
|
14848
|
+
<p class="text-xs text-emerald-400 text-center py-2">No history records yet. Generate or publish to start tracking.</p>
|
|
13270
14849
|
{:else}
|
|
13271
14850
|
{#each ps.publishHistory as record (record.id)}
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
{
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
class="shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-xs px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 font-medium cursor-pointer"
|
|
13302
|
-
onclick={() => { isHistoryOpen = false; }}
|
|
13303
|
-
>live</button>
|
|
13304
|
-
{/if}
|
|
13305
|
-
</div>
|
|
14851
|
+
{@const rowVersion = Number(record.version)}
|
|
14852
|
+
{@const rowIsDraft = !(record.publishedAt ?? record.published_at)}
|
|
14853
|
+
{@const rowIsViewing = rowVersion === Number(viewingVersion)}
|
|
14854
|
+
{@const rowIsLive = !rowIsDraft && rowVersion === Number(panelPublishedVersion)}
|
|
14855
|
+
{@const rowDateLabel = formatPublishHistoryDate(record)}
|
|
14856
|
+
{@const publishGate = getPublishGateForPanel(panelWidgetId, rowVersion)}
|
|
14857
|
+
<VersionDropdownRow
|
|
14858
|
+
versionLabel={'v' + rowVersion}
|
|
14859
|
+
commitSha={record.commitSha ?? record.commit_sha ?? null}
|
|
14860
|
+
showCommit={debugMode}
|
|
14861
|
+
dateLabel={rowDateLabel}
|
|
14862
|
+
isLive={rowIsLive}
|
|
14863
|
+
isDraft={rowIsDraft}
|
|
14864
|
+
selected={rowIsViewing}
|
|
14865
|
+
busy={isPublishing && publishingWidgetId === panelWidgetId}
|
|
14866
|
+
publishDisabled={!canPublishDraftVersionForPanel(panelWidgetId, rowVersion) || publishGate.disabled}
|
|
14867
|
+
publishDisabledReason={publishGate.disabled ? publishGate.reason : null}
|
|
14868
|
+
setLiveDisabled={isSettingLiveVersion === record.version}
|
|
14869
|
+
onselect={() => {
|
|
14870
|
+
handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
|
|
14871
|
+
}}
|
|
14872
|
+
onpublish={() => {
|
|
14873
|
+
handleWidgetShapeVersionChange(panelWidgetId, `v:${rowVersion}`, false);
|
|
14874
|
+
runPublishForPanel(panelWidgetId, rowVersion);
|
|
14875
|
+
}}
|
|
14876
|
+
onsetlive={() => {
|
|
14877
|
+
void setLiveVersionForWidget(panelWidgetId, record.version);
|
|
14878
|
+
}}
|
|
14879
|
+
/>
|
|
13306
14880
|
{/each}
|
|
13307
14881
|
{/if}
|
|
13308
14882
|
</div>
|
|
@@ -13338,7 +14912,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13338
14912
|
</svelte:fragment>
|
|
13339
14913
|
|
|
13340
14914
|
<svelte:fragment slot="code-generation-status">
|
|
13341
|
-
<div class="generate-code-status-ct flex flex-col gap-2 shrink-0" style="padding: 10px; margin: 0px;">
|
|
14915
|
+
<div class="generate-code-status-ct flex flex-col gap-2 shrink-0 border-b border-gray-200 bg-gray-50" style="padding: 10px; margin: 0px; border-radius: 0;">
|
|
13342
14916
|
<div class="flex items-center justify-between gap-2 min-w-0">
|
|
13343
14917
|
<div class="generate-code-status-el text-xs flex-1 min-w-0 truncate {ps.lastGenerationFailed ? 'text-red-600' : 'text-gray-500'}">
|
|
13344
14918
|
{#if ps.lastGenerationFailed}
|
|
@@ -13349,7 +14923,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13349
14923
|
</div>
|
|
13350
14924
|
</div>
|
|
13351
14925
|
{#if ps.lastGenerationFailed}
|
|
13352
|
-
<div class="p-3 bg-red-50 border border-red-200
|
|
14926
|
+
<div class="p-3 bg-red-50 border border-red-200" style="border-radius: 0;">
|
|
13353
14927
|
<p class="text-xs text-red-700 whitespace-pre-wrap">{ps.lastGenerationError}</p>
|
|
13354
14928
|
<div class="flex gap-2 mt-3 justify-center">
|
|
13355
14929
|
<Button
|
|
@@ -13387,20 +14961,22 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13387
14961
|
|
|
13388
14962
|
<svelte:fragment slot="code-generation">
|
|
13389
14963
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
13390
|
-
{@const chatUserHeight = (panelDisplayRevision,
|
|
14964
|
+
{@const chatUserHeight = (panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
|
|
13391
14965
|
<div
|
|
13392
14966
|
class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
|
|
13393
|
-
style="padding:
|
|
13394
|
-
use:nativeScrollbarPadding={
|
|
14967
|
+
style="padding: 0;"
|
|
14968
|
+
use:nativeScrollbarPadding={0}
|
|
13395
14969
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
13396
14970
|
>
|
|
13397
14971
|
{#key panelInstanceKey}
|
|
13398
14972
|
<div
|
|
13399
|
-
class="code-generation-chat-host w-full min-h-0"
|
|
14973
|
+
class="code-generation-chat-host w-full min-h-0 {chatUserHeight == null ? 'flex-1' : 'shrink-0'}"
|
|
13400
14974
|
data-widget-chat-height={panelWidgetId}
|
|
13401
|
-
style=
|
|
14975
|
+
style={chatUserHeight == null
|
|
14976
|
+
? `flex: 1 1 auto; min-height: ${CHAT_MIN_HEIGHT}px;`
|
|
14977
|
+
: `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
|
|
13402
14978
|
>
|
|
13403
|
-
<Chat class="code-generation-chat w-full h-full"
|
|
14979
|
+
<Chat class="code-generation-chat w-full h-full rounded-none border-0"
|
|
13404
14980
|
style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
|
|
13405
14981
|
resizeExpandsContainer={true}
|
|
13406
14982
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
@@ -13428,7 +15004,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13428
15004
|
autoCodeDetection: false,
|
|
13429
15005
|
}
|
|
13430
15006
|
}}
|
|
13431
|
-
widgetHeadCommitSha={ps.lastCommitId}
|
|
15007
|
+
widgetHeadCommitSha={resolveViewingCommitShaForPanel(panelWidgetId) || ps.lastCommitId}
|
|
13432
15008
|
onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
|
|
13433
15009
|
onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
|
|
13434
15010
|
onUploadChatFile={handleChatUploadFile}
|
|
@@ -13441,7 +15017,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13441
15017
|
onSaveMessage={(conversationId, content, messageType, attachments) =>
|
|
13442
15018
|
handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
|
|
13443
15019
|
}
|
|
13444
|
-
onRestoreCheckpointBackend={
|
|
15020
|
+
onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
|
|
15021
|
+
handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
|
|
15022
|
+
}
|
|
13445
15023
|
onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
|
|
13446
15024
|
onDraftChange={() => handleChatDraftChange(panelWidgetId)}
|
|
13447
15025
|
onConversationChange={(conversationId) => handleChatConversationChange(conversationId, panelWidgetId)}
|
|
@@ -13503,7 +15081,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13503
15081
|
{#if isEditStepWithoutPublishForPanel(panelWidgetId)}
|
|
13504
15082
|
<div class="edit-not-published-banner-ct mx-3 mt-2 px-3 py-2 rounded-md border border-amber-200 bg-amber-50 text-amber-800 text-xs">
|
|
13505
15083
|
<strong class="font-semibold">Not published yet.</strong>
|
|
13506
|
-
Generate on Create
|
|
15084
|
+
Generate on Create, preview the result, then Publish when you are ready.
|
|
13507
15085
|
</div>
|
|
13508
15086
|
{/if}
|
|
13509
15087
|
|
|
@@ -13993,11 +15571,46 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13993
15571
|
Are you sure you want to delete <strong>"{deleteCanvasTarget?.name || ''}"</strong>? This action cannot be undone.
|
|
13994
15572
|
</Dialog.Description>
|
|
13995
15573
|
</Dialog.Header>
|
|
15574
|
+
|
|
15575
|
+
{#if deleteCanvasPreviewLoading}
|
|
15576
|
+
<p class="confirm-delete-canvas-preview-loading text-sm text-gray-500 pt-2">
|
|
15577
|
+
Checking what will be deleted…
|
|
15578
|
+
</p>
|
|
15579
|
+
{:else if deleteCanvasPreview && deleteCanvasPreview.widgetCount > 0}
|
|
15580
|
+
<div class="confirm-delete-canvas-preview pt-2">
|
|
15581
|
+
<p class="text-sm text-gray-700">
|
|
15582
|
+
This will also delete
|
|
15583
|
+
<strong>{deleteCanvasPreview.widgetCount}</strong>
|
|
15584
|
+
{deleteCanvasPreview.widgetCount === 1 ? 'widget' : 'widgets'} on this canvas:
|
|
15585
|
+
</p>
|
|
15586
|
+
<ul class="confirm-delete-canvas-preview-groups mt-2 space-y-1.5 max-h-40 overflow-y-auto scrollbar-thin text-sm">
|
|
15587
|
+
{#each deleteCanvasPreview.groups as group}
|
|
15588
|
+
<li class="confirm-delete-canvas-preview-group">
|
|
15589
|
+
<span class="font-medium text-gray-800">{group.projectName}</span>
|
|
15590
|
+
<span class="text-gray-500"> — {group.widgetNames.length}</span>
|
|
15591
|
+
<p class="text-gray-500 text-xs truncate" title={group.widgetNames.join(', ')}>
|
|
15592
|
+
{group.widgetNames.join(', ')}
|
|
15593
|
+
</p>
|
|
15594
|
+
</li>
|
|
15595
|
+
{/each}
|
|
15596
|
+
</ul>
|
|
15597
|
+
{#if deleteCanvasPreview.groups.length > 1}
|
|
15598
|
+
<p class="confirm-delete-canvas-preview-warning text-xs text-amber-700 mt-2">
|
|
15599
|
+
These widgets belong to more than one project.
|
|
15600
|
+
</p>
|
|
15601
|
+
{/if}
|
|
15602
|
+
</div>
|
|
15603
|
+
{/if}
|
|
15604
|
+
|
|
15605
|
+
{#if deleteCanvasError}
|
|
15606
|
+
<p class="confirm-delete-canvas-error text-sm text-red-600 pt-2">{deleteCanvasError}</p>
|
|
15607
|
+
{/if}
|
|
15608
|
+
|
|
13996
15609
|
<div class="confirm-delete-canvas-dialog-actions flex justify-end gap-2 pt-2">
|
|
13997
15610
|
<button
|
|
13998
15611
|
type="button"
|
|
13999
15612
|
class="px-4 py-2 text-sm rounded-lg border border-gray-300 hover:bg-gray-100 cursor-pointer"
|
|
14000
|
-
onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; }}
|
|
15613
|
+
onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; deleteCanvasError = ''; }}
|
|
14001
15614
|
>Cancel</button>
|
|
14002
15615
|
<button
|
|
14003
15616
|
type="button"
|