@widgetic/creator 0.3.59 → 0.3.60
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 +1412 -284
- package/dist/components/WidgetDetails.svelte +10 -34
- package/dist/components/WidgetDetails.svelte.d.ts +1 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +414 -43
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +53 -29
- package/package.json +1 -1
package/dist/CreatorApp.svelte
CHANGED
|
@@ -147,18 +147,31 @@
|
|
|
147
147
|
const name = (widget.name || '').trim() || 'Untitled Widget';
|
|
148
148
|
// console.log('[CreatorApp] Notify host widget name:', widget.id, name);
|
|
149
149
|
onWidgetRenamed?.({ widgetId: widget.id, name });
|
|
150
|
+
// Also broadcast — mount props can be stale across embed remounts.
|
|
151
|
+
if (typeof window !== 'undefined') {
|
|
152
|
+
window.dispatchEvent(
|
|
153
|
+
new CustomEvent('widgetic:creator-primary-renamed', {
|
|
154
|
+
detail: { widgetId: widget.id, name },
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
/**
|
|
153
161
|
* Rewrite the embedded URL so a refresh resumes the current canvas instead of
|
|
154
162
|
* re-running the new-widget bootstrap (which creates a blank canvas every time).
|
|
163
|
+
*
|
|
164
|
+
* The canvas id goes into the **path** (`/canvas/<canvasId>`), matching the host route.
|
|
155
165
|
* Uses history.replaceState — no navigation, no host remount.
|
|
156
166
|
*/
|
|
157
167
|
function syncEmbeddedCanvasUrl(canvasIdToSync: string): void {
|
|
158
168
|
if (!embedded || !canvasIdToSync) return;
|
|
159
169
|
try {
|
|
160
170
|
const url = new URL(window.location.href);
|
|
161
|
-
url.
|
|
171
|
+
url.pathname = `/canvas/${encodeURIComponent(canvasIdToSync)}`;
|
|
172
|
+
url.searchParams.delete('canvas_id');
|
|
173
|
+
url.searchParams.delete('canvas-id');
|
|
174
|
+
url.searchParams.delete('widget_id');
|
|
162
175
|
url.searchParams.delete('new_widget');
|
|
163
176
|
window.history.replaceState(window.history.state, '', url.toString());
|
|
164
177
|
// console.log('[CreatorApp] URL synced to canvas:', canvasIdToSync);
|
|
@@ -200,8 +213,11 @@
|
|
|
200
213
|
if (selectedWidget?.id === targetId) {
|
|
201
214
|
selectedWidget = { ...selectedWidget, name: trimmed };
|
|
202
215
|
}
|
|
216
|
+
widgetNamesRevision += 1;
|
|
203
217
|
panelDisplayRevision += 1;
|
|
204
218
|
backfillWidgetShapeMetadata();
|
|
219
|
+
syncWidgetShapeLivePreviews();
|
|
220
|
+
emitCanvasWidgetsList('host-widget-renamed');
|
|
205
221
|
// console.log('[CreatorApp] Applied host widget rename:', targetId.substring(0, 8), trimmed);
|
|
206
222
|
|
|
207
223
|
if (!widgetsClient) return;
|
|
@@ -448,6 +464,7 @@
|
|
|
448
464
|
import SelectorDropdown from './components/SelectorDropdown.svelte';
|
|
449
465
|
import type { DropdownItem } from './components/SelectorDropdown.svelte';
|
|
450
466
|
import WidgetShapeLivePreviews from './components/WidgetShapeLivePreviews.svelte';
|
|
467
|
+
import HoverTooltip from './components/HoverTooltip.svelte';
|
|
451
468
|
|
|
452
469
|
// WIDGET DETAILS COMPONENT — lazy-loaded to reduce peak build memory
|
|
453
470
|
let WidgetDetails: any;
|
|
@@ -558,6 +575,18 @@
|
|
|
558
575
|
// Delete canvas dialog
|
|
559
576
|
let showDeleteCanvasDialog = false;
|
|
560
577
|
let deleteCanvasTarget: { id: string; name: string } | null = null;
|
|
578
|
+
/**
|
|
579
|
+
* Widgets the pending canvas deletion would remove, grouped by project.
|
|
580
|
+
* A canvas can hold widgets from more than one project (moving a widget
|
|
581
|
+
* rewrites `project_id` but leaves `canvas_id`), so the dialog states exactly
|
|
582
|
+
* what is about to be lost instead of only reassuring that it is irreversible.
|
|
583
|
+
*/
|
|
584
|
+
let deleteCanvasPreview: {
|
|
585
|
+
widgetCount: number;
|
|
586
|
+
groups: Array<{ projectName: string; widgetNames: string[] }>;
|
|
587
|
+
} | null = null;
|
|
588
|
+
let deleteCanvasPreviewLoading = false;
|
|
589
|
+
let deleteCanvasError = '';
|
|
561
590
|
|
|
562
591
|
// Widgets state
|
|
563
592
|
interface WidgetSummary {
|
|
@@ -890,7 +919,10 @@
|
|
|
890
919
|
contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
|
|
891
920
|
source: options?.source || 'chat-upload',
|
|
892
921
|
widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
|
|
893
|
-
conversationId:
|
|
922
|
+
conversationId:
|
|
923
|
+
currentChatConversationId && !isLocalOnlyConversationId(currentChatConversationId)
|
|
924
|
+
? currentChatConversationId
|
|
925
|
+
: undefined,
|
|
894
926
|
},
|
|
895
927
|
{ onProgress: options?.onProgress },
|
|
896
928
|
);
|
|
@@ -1485,40 +1517,164 @@
|
|
|
1485
1517
|
// console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
|
|
1486
1518
|
}
|
|
1487
1519
|
|
|
1488
|
-
/**
|
|
1489
|
-
|
|
1520
|
+
/** Live WidgetShape iframe only — never the hidden panel preview column. */
|
|
1521
|
+
function resolveLiveWidgetShapeIframe(widgetId: string | null | undefined): HTMLIFrameElement | null {
|
|
1490
1522
|
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1523
|
+
if (targetId) {
|
|
1524
|
+
const liveById = document.querySelector(
|
|
1525
|
+
`iframe.widget-shape-live-preview-iframe[data-widget-id="${CSS.escape(targetId)}"]`,
|
|
1526
|
+
) as HTMLIFrameElement | null;
|
|
1527
|
+
if (liveById?.contentWindow) return liveById;
|
|
1528
|
+
}
|
|
1529
|
+
return (
|
|
1530
|
+
(document.querySelector(
|
|
1531
|
+
'iframe.widget-shape-live-preview-iframe',
|
|
1532
|
+
) as HTMLIFrameElement | null) || null
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function isWorkerPreviewUrl(url: string | null | undefined): boolean {
|
|
1537
|
+
if (!url) return false;
|
|
1538
|
+
try {
|
|
1539
|
+
const u = new URL(url, window.location.origin);
|
|
1540
|
+
return /\/preview\/[^/]+/i.test(u.pathname) || u.pathname.includes('/__widget_builder__/');
|
|
1541
|
+
} catch {
|
|
1542
|
+
return /\/preview\//i.test(url);
|
|
1502
1543
|
}
|
|
1544
|
+
}
|
|
1503
1545
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1546
|
+
function requestIframeScreenshot(
|
|
1547
|
+
iframe: HTMLIFrameElement,
|
|
1548
|
+
timeoutMs = 6000,
|
|
1549
|
+
): Promise<{ data: string | null; error: string | null }> {
|
|
1550
|
+
return new Promise((resolve) => {
|
|
1551
|
+
if (!iframe.contentWindow) {
|
|
1552
|
+
resolve({ data: null, error: 'no-content-window' });
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1506
1555
|
const timeout = setTimeout(() => {
|
|
1507
1556
|
window.removeEventListener('message', handler);
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
}, 8000);
|
|
1557
|
+
resolve({ data: null, error: 'timeout' });
|
|
1558
|
+
}, timeoutMs);
|
|
1511
1559
|
|
|
1512
1560
|
function handler(event: MessageEvent) {
|
|
1513
1561
|
if (event.data?.type !== 'widgetic:screenshot') return;
|
|
1562
|
+
// Prefer replies from this iframe when source is available
|
|
1563
|
+
if (event.source && event.source !== iframe.contentWindow) return;
|
|
1514
1564
|
clearTimeout(timeout);
|
|
1515
1565
|
window.removeEventListener('message', handler);
|
|
1516
|
-
resolve(
|
|
1566
|
+
resolve({
|
|
1567
|
+
data: typeof event.data.data === 'string' ? event.data.data : null,
|
|
1568
|
+
error: typeof event.data.error === 'string' ? event.data.error : null,
|
|
1569
|
+
});
|
|
1517
1570
|
}
|
|
1518
1571
|
window.addEventListener('message', handler);
|
|
1519
|
-
iframe.contentWindow
|
|
1572
|
+
iframe.contentWindow.postMessage({ type: 'widgetic:request-screenshot' }, '*');
|
|
1520
1573
|
});
|
|
1521
1574
|
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Published CDN artifacts often lack the Worker screenshot script.
|
|
1578
|
+
* Capture via a temporary same-path Worker /preview iframe (has html2canvas handler).
|
|
1579
|
+
*/
|
|
1580
|
+
async function captureViaHiddenWorkerPreview(widgetId: string): Promise<string | null> {
|
|
1581
|
+
const workerUrl = `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
|
|
1582
|
+
console.log('[Chat→Screenshot] Capturing via hidden Worker preview', {
|
|
1583
|
+
widgetId: widgetId.substring(0, 8),
|
|
1584
|
+
workerUrl,
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
const host = document.createElement('div');
|
|
1588
|
+
host.className = 'wc-hidden-screenshot-host';
|
|
1589
|
+
host.setAttribute(
|
|
1590
|
+
'style',
|
|
1591
|
+
'position:fixed;left:-10000px;top:0;width:800px;height:600px;opacity:0;pointer-events:none;z-index:-1;',
|
|
1592
|
+
);
|
|
1593
|
+
const iframe = document.createElement('iframe');
|
|
1594
|
+
iframe.title = 'Hidden widget screenshot';
|
|
1595
|
+
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms');
|
|
1596
|
+
iframe.style.cssText = 'width:800px;height:600px;border:0;';
|
|
1597
|
+
host.appendChild(iframe);
|
|
1598
|
+
document.body.appendChild(host);
|
|
1599
|
+
|
|
1600
|
+
try {
|
|
1601
|
+
await new Promise<void>((resolve, reject) => {
|
|
1602
|
+
const timeout = setTimeout(() => reject(new Error('worker-preview-load-timeout')), 12000);
|
|
1603
|
+
iframe.onload = () => {
|
|
1604
|
+
clearTimeout(timeout);
|
|
1605
|
+
resolve();
|
|
1606
|
+
};
|
|
1607
|
+
iframe.onerror = () => {
|
|
1608
|
+
clearTimeout(timeout);
|
|
1609
|
+
reject(new Error('worker-preview-load-error'));
|
|
1610
|
+
};
|
|
1611
|
+
iframe.src = workerUrl;
|
|
1612
|
+
});
|
|
1613
|
+
// Give injected screenshot handler + images a beat to settle
|
|
1614
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
1615
|
+
const result = await requestIframeScreenshot(iframe, 12000);
|
|
1616
|
+
if (result.data) return result.data;
|
|
1617
|
+
console.warn('[Chat→Screenshot] Hidden Worker capture failed:', result.error);
|
|
1618
|
+
return null;
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
console.warn('[Chat→Screenshot] Hidden Worker preview error:', err);
|
|
1621
|
+
return null;
|
|
1622
|
+
} finally {
|
|
1623
|
+
host.remove();
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/** Screenshot: capture the live WidgetShape iframe (CDN or Worker), not Fabric wireframe. */
|
|
1628
|
+
async function handleChatScreenshot(widgetId?: string | null): Promise<string | null> {
|
|
1629
|
+
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1630
|
+
if (!targetId) {
|
|
1631
|
+
console.warn('[Chat→Screenshot] No widget id');
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const liveIframe = resolveLiveWidgetShapeIframe(targetId);
|
|
1636
|
+
const liveSrc = liveIframe
|
|
1637
|
+
? liveIframe.getAttribute('src') || liveIframe.src || ''
|
|
1638
|
+
: '';
|
|
1639
|
+
|
|
1640
|
+
// 1) Always try the visible live iframe first (published HTML from Worker includes screenshot script)
|
|
1641
|
+
if (liveIframe?.contentWindow) {
|
|
1642
|
+
console.log('[Chat→Screenshot] Live WidgetShape iframe', {
|
|
1643
|
+
targetId: targetId.substring(0, 8),
|
|
1644
|
+
isWorker: isWorkerPreviewUrl(liveSrc),
|
|
1645
|
+
src: liveSrc.slice(0, 120),
|
|
1646
|
+
});
|
|
1647
|
+
const fromLive = await requestIframeScreenshot(liveIframe, 12000);
|
|
1648
|
+
if (fromLive.data && fromLive.data.length > 500) {
|
|
1649
|
+
return fromLive.data;
|
|
1650
|
+
}
|
|
1651
|
+
console.warn('[Chat→Screenshot] Live iframe capture failed:', fromLive.error);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// 2) Hidden Worker /preview (same capture script; wait for load)
|
|
1655
|
+
const fromWorker = await captureViaHiddenWorkerPreview(targetId);
|
|
1656
|
+
if (fromWorker && fromWorker.length > 500) return fromWorker;
|
|
1657
|
+
|
|
1658
|
+
// 3) Last resort: Fabric thumbnail (often stale / wireframe) — only if nothing else works
|
|
1659
|
+
const shapeDataUrl =
|
|
1660
|
+
typeof canvasRef?.getWidgetShapeDataUrl === 'function'
|
|
1661
|
+
? canvasRef.getWidgetShapeDataUrl(targetId)
|
|
1662
|
+
: null;
|
|
1663
|
+
if (shapeDataUrl && shapeDataUrl.length > 100) {
|
|
1664
|
+
console.warn(
|
|
1665
|
+
'[Chat→Screenshot] Falling back to Fabric thumbnail (may be stale wireframe)',
|
|
1666
|
+
targetId.substring(0, 8),
|
|
1667
|
+
);
|
|
1668
|
+
return shapeDataUrl;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
console.warn('[Chat→Screenshot] All capture paths failed', {
|
|
1672
|
+
targetId: targetId.substring(0, 8),
|
|
1673
|
+
hadLiveIframe: !!liveIframe,
|
|
1674
|
+
liveSrc: liveSrc.slice(0, 100),
|
|
1675
|
+
});
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1522
1678
|
|
|
1523
1679
|
function normalizeConversationRecords(json: any): any[] {
|
|
1524
1680
|
const raw = json?.data ?? json?.conversations ?? json;
|
|
@@ -1606,6 +1762,8 @@
|
|
|
1606
1762
|
/** Load messages for a conversation from backend */
|
|
1607
1763
|
async function handleLoadMessages(conversationId: string, widgetIdForSync?: string | null): Promise<any[]> {
|
|
1608
1764
|
try {
|
|
1765
|
+
// Client-only conversations (draft convert panels) have no DB row.
|
|
1766
|
+
if (isLocalOnlyConversationId(conversationId)) return [];
|
|
1609
1767
|
if (!messagesClient) {
|
|
1610
1768
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
1611
1769
|
return [];
|
|
@@ -1669,7 +1827,17 @@
|
|
|
1669
1827
|
try {
|
|
1670
1828
|
if (!conversationsClient) {
|
|
1671
1829
|
console.warn('[Chat→Backend] conversationsClient not initialized');
|
|
1672
|
-
return
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
// Draft convert panels have no DB widget yet, so there is no table row to
|
|
1833
|
+
// attach a conversation to. `@widgetic/chat` requires an object with an
|
|
1834
|
+
// `id` here and treats anything else as a fatal init error, so hand it a
|
|
1835
|
+
// clearly-marked client-only id. `finalizeDraftConversion` never migrates
|
|
1836
|
+
// this id into the real widget's panel state, so messages sent before the
|
|
1837
|
+
// widget exists can never be persisted against a phantom conversation.
|
|
1838
|
+
if (!contextId || isDraftConvertId(contextId) || String(contextId).startsWith('draft')) {
|
|
1839
|
+
const localConversationId = `local_${contextId || 'draft'}_${Date.now()}`;
|
|
1840
|
+
return { id: localConversationId, title, isLocalOnly: true };
|
|
1673
1841
|
}
|
|
1674
1842
|
const response = await conversationsClient.createConversationForWidgetRaw({
|
|
1675
1843
|
createConversationForWidgetRequest: {
|
|
@@ -1693,7 +1861,8 @@
|
|
|
1693
1861
|
return conversationId ? { ...conversation, id: conversationId } : conversation;
|
|
1694
1862
|
} catch (error) {
|
|
1695
1863
|
console.error('[Chat→Backend] Failed to create conversation:', error);
|
|
1696
|
-
|
|
1864
|
+
// Never invent a local UUID — saving messages against it yields 403 and stops codegen.
|
|
1865
|
+
return null;
|
|
1697
1866
|
}
|
|
1698
1867
|
}
|
|
1699
1868
|
|
|
@@ -1810,6 +1979,11 @@
|
|
|
1810
1979
|
persistWidgetId?: string | null,
|
|
1811
1980
|
): Promise<any> {
|
|
1812
1981
|
try {
|
|
1982
|
+
// Draft convert panels hold a client-only conversation until the widget
|
|
1983
|
+
// exists; there is no `conversations` row to attach messages to yet.
|
|
1984
|
+
if (isLocalOnlyConversationId(conversationId)) {
|
|
1985
|
+
return { id: `local_msg_${Date.now()}`, attachments: attachments ?? [] };
|
|
1986
|
+
}
|
|
1813
1987
|
if (!messagesClient) {
|
|
1814
1988
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
1815
1989
|
return null;
|
|
@@ -1865,7 +2039,11 @@
|
|
|
1865
2039
|
}
|
|
1866
2040
|
|
|
1867
2041
|
/** Restore conversation to a checkpoint message via backend (deletes messages + git rollback) */
|
|
1868
|
-
async function handleRestoreCheckpoint(
|
|
2042
|
+
async function handleRestoreCheckpoint(
|
|
2043
|
+
conversationId: string,
|
|
2044
|
+
checkpointMessageId: string,
|
|
2045
|
+
restoreWidgetId?: string | null,
|
|
2046
|
+
): Promise<any> {
|
|
1869
2047
|
try {
|
|
1870
2048
|
if (!messagesClient) {
|
|
1871
2049
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
@@ -1882,16 +2060,22 @@
|
|
|
1882
2060
|
const data = json?.data || json;
|
|
1883
2061
|
// console.log('[Chat→Backend] Checkpoint restored:', data);
|
|
1884
2062
|
|
|
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
2063
|
const gitRollback = data?.gitRollback || data?.git_rollback || data?.data?.gitRollback || data?.data?.git_rollback;
|
|
1889
2064
|
const undoMode = data?.undoMode ?? data?.undo_mode ?? false;
|
|
1890
2065
|
const targetCommit = gitRollback?.targetCommit ?? gitRollback?.target_commit;
|
|
1891
|
-
|
|
2066
|
+
const panelWidgetId =
|
|
2067
|
+
restoreWidgetId ||
|
|
2068
|
+
focusedPanelWidgetId ||
|
|
2069
|
+
selectedWidgetId ||
|
|
2070
|
+
null;
|
|
2071
|
+
// console.log('[Chat→Backend] Git rollback result:', gitRollback, { undoMode, targetCommit, panelWidgetId });
|
|
1892
2072
|
if (gitRollback?.success) {
|
|
1893
2073
|
if (targetCommit) {
|
|
1894
2074
|
lastCommitId = targetCommit;
|
|
2075
|
+
if (panelWidgetId) {
|
|
2076
|
+
patchPanelSnapshot(panelWidgetId, { lastCommitId: targetCommit });
|
|
2077
|
+
patchCodegenUiState(panelWidgetId, {});
|
|
2078
|
+
}
|
|
1895
2079
|
}
|
|
1896
2080
|
showToast('success', undoMode ? 'Change undone' : 'Checkpoint restored', {
|
|
1897
2081
|
description: undoMode
|
|
@@ -1905,20 +2089,27 @@
|
|
|
1905
2089
|
compositionDesignSchema = null;
|
|
1906
2090
|
compositionContentSchema = null;
|
|
1907
2091
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
1908
|
-
// Rollback commit may take a moment to be visible from the Worker's clone; retry
|
|
1909
|
-
// build-from-repo with escalating delays to survive GitLab propagation lag.
|
|
1910
2092
|
_isRestoringCheckpoint = true;
|
|
2093
|
+
const panelRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
|
|
1911
2094
|
const scheduleBuildRetry = async () => {
|
|
1912
2095
|
const delays = [2500, 5000, 8000];
|
|
1913
2096
|
for (let i = 0; i < delays.length; i++) {
|
|
1914
2097
|
await new Promise((r) => setTimeout(r, delays[i]));
|
|
1915
2098
|
if (i === 0) suppressNextBuildFromRepoChatMessage = true;
|
|
1916
2099
|
try {
|
|
1917
|
-
|
|
1918
|
-
|
|
2100
|
+
const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : panelRef;
|
|
2101
|
+
await activeRef?.triggerBuildFromRepo({ force: true });
|
|
2102
|
+
const err = activeRef?.getPreviewError?.() ?? null;
|
|
1919
2103
|
if (!err) {
|
|
1920
|
-
// console.log('[Restore] triggerBuildFromRepo succeeded on attempt', i + 1);
|
|
1921
2104
|
_isRestoringCheckpoint = false;
|
|
2105
|
+
if (panelWidgetId) {
|
|
2106
|
+
shapePreviewSourceByWidgetId = {
|
|
2107
|
+
...shapePreviewSourceByWidgetId,
|
|
2108
|
+
[panelWidgetId]: 'preview',
|
|
2109
|
+
};
|
|
2110
|
+
syncWidgetShapeLivePreviews();
|
|
2111
|
+
setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
|
|
2112
|
+
}
|
|
1922
2113
|
return;
|
|
1923
2114
|
}
|
|
1924
2115
|
console.warn('[Restore] triggerBuildFromRepo attempt', i + 1, 'has error:', err);
|
|
@@ -1941,14 +2132,16 @@
|
|
|
1941
2132
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
1942
2133
|
setTimeout(() => {
|
|
1943
2134
|
suppressNextBuildFromRepoChatMessage = true;
|
|
1944
|
-
|
|
2135
|
+
const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
|
|
2136
|
+
void activeRef?.triggerBuildFromRepo({ force: true });
|
|
1945
2137
|
}, 1500);
|
|
1946
2138
|
}
|
|
1947
|
-
|
|
1948
2139
|
return data;
|
|
1949
2140
|
} catch (error) {
|
|
1950
2141
|
console.error('[Chat→Backend] Failed to restore checkpoint:', error);
|
|
1951
|
-
|
|
2142
|
+
showToast('error', 'Failed to restore checkpoint', {
|
|
2143
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
2144
|
+
});
|
|
1952
2145
|
return null;
|
|
1953
2146
|
}
|
|
1954
2147
|
}
|
|
@@ -2123,14 +2316,15 @@
|
|
|
2123
2316
|
let editingCompositionName = '';
|
|
2124
2317
|
let deletingCompositionId: string | null = null;
|
|
2125
2318
|
|
|
2126
|
-
/** Explicit chat box height (px).
|
|
2127
|
-
const CHAT_DEFAULT_HEIGHT =
|
|
2128
|
-
const CHAT_MIN_HEIGHT =
|
|
2319
|
+
/** Explicit chat box height (px). null/0 = fill available; user drag sets a fixed px. */
|
|
2320
|
+
const CHAT_DEFAULT_HEIGHT = 400;
|
|
2321
|
+
const CHAT_MIN_HEIGHT = 400;
|
|
2129
2322
|
const CHAT_MAX_HEIGHT = 2000;
|
|
2130
2323
|
|
|
2131
|
-
function getChatContainerHeightForPanel(widgetId: string): number {
|
|
2324
|
+
function getChatContainerHeightForPanel(widgetId: string): number | null {
|
|
2132
2325
|
const stored = getPanelState(widgetId).chatContainerHeight;
|
|
2133
|
-
|
|
2326
|
+
// null/0 → fill available space (flex-grow). Explicit resize stores a px value.
|
|
2327
|
+
if (stored == null || stored <= 0) return null;
|
|
2134
2328
|
return Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, stored));
|
|
2135
2329
|
}
|
|
2136
2330
|
|
|
@@ -2138,9 +2332,12 @@
|
|
|
2138
2332
|
const measured = document
|
|
2139
2333
|
.querySelector(`[data-widget-chat-height="${widgetId}"]`)
|
|
2140
2334
|
?.getBoundingClientRect().height;
|
|
2141
|
-
const current =
|
|
2335
|
+
const current =
|
|
2336
|
+
measured && measured > 0
|
|
2337
|
+
? measured
|
|
2338
|
+
: (getChatContainerHeightForPanel(widgetId) ?? CHAT_DEFAULT_HEIGHT);
|
|
2142
2339
|
const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
|
|
2143
|
-
|
|
2340
|
+
console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
|
|
2144
2341
|
patchPanelSnapshot(widgetId, { chatContainerHeight: next });
|
|
2145
2342
|
}
|
|
2146
2343
|
|
|
@@ -2293,6 +2490,11 @@
|
|
|
2293
2490
|
}
|
|
2294
2491
|
|
|
2295
2492
|
function scheduleWidgetSchemaLoad(widgetId: string): Promise<void> {
|
|
2493
|
+
// Draft convert panels have no DB widget yet — `/widgets/:id/schema/*` validates
|
|
2494
|
+
// a UUID, so calling it with a `draft_convert_*` id only produces a 400 and a
|
|
2495
|
+
// console error. The real schemas are loaded once the panel migrates to the
|
|
2496
|
+
// widget id after the first send.
|
|
2497
|
+
if (isDraftConvertId(widgetId)) return Promise.resolve();
|
|
2296
2498
|
const cached = widgetSchemaCache.get(widgetId);
|
|
2297
2499
|
if (cached?.design || cached?.content) {
|
|
2298
2500
|
applyWidgetSchemaCacheToSnapshot(widgetId);
|
|
@@ -2462,6 +2664,9 @@
|
|
|
2462
2664
|
}
|
|
2463
2665
|
|
|
2464
2666
|
async function executeWidgetSchemaLoad(widgetId: string): Promise<void> {
|
|
2667
|
+
// Defence in depth: callers already skip drafts, but any path that reaches
|
|
2668
|
+
// here with a synthetic id would hit `/widgets/:id/schema/*` and get a 400.
|
|
2669
|
+
if (isDraftConvertId(widgetId)) return;
|
|
2465
2670
|
schemaLoadInFlight.add(widgetId);
|
|
2466
2671
|
|
|
2467
2672
|
try {
|
|
@@ -2760,7 +2965,6 @@
|
|
|
2760
2965
|
// Only reuse js_path when it already points at this widget + this version.
|
|
2761
2966
|
// `/widgets/` alone used to win and keep Edit on the previous CDN version after publish.
|
|
2762
2967
|
if (!pointsAtOtherWidget && jsPathVersion === version) {
|
|
2763
|
-
console.log('[Publish] Using js_path artifact URL', resolved);
|
|
2764
2968
|
return resolved;
|
|
2765
2969
|
}
|
|
2766
2970
|
}
|
|
@@ -3883,63 +4087,162 @@
|
|
|
3883
4087
|
}
|
|
3884
4088
|
|
|
3885
4089
|
function handleHostSaveCanvasEvent(): void {
|
|
3886
|
-
|
|
4090
|
+
// Explicit Save must always persist — bypass soft load-cooldown / not-ready guards.
|
|
4091
|
+
const json = canvasRef?.saveCanvas?.() ?? null;
|
|
4092
|
+
if (!json) {
|
|
4093
|
+
console.warn('[CreatorApp] Host Save ignored — saveCanvas returned null');
|
|
4094
|
+
showToast('error', 'Nothing to save', { description: 'Canvas is not ready yet.' });
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
4097
|
+
void handleCanvasAutoSave(json, false, { force: true });
|
|
3887
4098
|
}
|
|
3888
4099
|
|
|
3889
|
-
async function persistPrimaryWidgetId(widgetId: string) {
|
|
3890
|
-
if (!widgetId) return;
|
|
4100
|
+
async function persistPrimaryWidgetId(widgetId: string): Promise<boolean> {
|
|
4101
|
+
if (!widgetId) return false;
|
|
4102
|
+
const targetCanvasId =
|
|
4103
|
+
(typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
|
|
4104
|
+
(typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null);
|
|
4105
|
+
const widget = getWidgetById(widgetId);
|
|
4106
|
+
const widgetCanvasId =
|
|
4107
|
+
(widget as { canvasId?: string | null; canvas_id?: string | null } | null)?.canvasId ??
|
|
4108
|
+
(widget as { canvasId?: string | null; canvas_id?: string | null } | null)?.canvas_id ??
|
|
4109
|
+
null;
|
|
4110
|
+
const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
|
|
4111
|
+
const shapeOnThisCanvas = linkedIds instanceof Set ? linkedIds.has(widgetId) : false;
|
|
4112
|
+
// Explicit user intent (picking a primary) is the strongest ownership signal, so a
|
|
4113
|
+
// shape the user drew / sees on THIS canvas always wins over a stale canvas_id.
|
|
4114
|
+
// Legacy canvas duplication left the same shape spread across many canvases; refusing
|
|
4115
|
+
// here made primary unsettable. Adopt (move) instead — never touch the shape itself.
|
|
4116
|
+
const shouldAdoptToThisCanvas =
|
|
4117
|
+
!!targetCanvasId && shapeOnThisCanvas && (!widgetCanvasId || widgetCanvasId !== targetCanvasId);
|
|
4118
|
+
if (targetCanvasId && widgetCanvasId && widgetCanvasId !== targetCanvasId && !shapeOnThisCanvas) {
|
|
4119
|
+
console.warn(
|
|
4120
|
+
'[CreatorApp] Refusing primary_widget_id — widget belongs to another canvas and has no shape here',
|
|
4121
|
+
widgetId.slice(0, 8),
|
|
4122
|
+
'widgetCanvas',
|
|
4123
|
+
String(widgetCanvasId).slice(0, 8),
|
|
4124
|
+
'targetCanvas',
|
|
4125
|
+
targetCanvasId.slice(0, 8),
|
|
4126
|
+
);
|
|
4127
|
+
emitCanvasWidgetsList('primary-refused');
|
|
4128
|
+
return false;
|
|
4129
|
+
}
|
|
3891
4130
|
canvasPrimaryWidgetId = widgetId;
|
|
4131
|
+
saveSelectedWidgetId(widgetId);
|
|
3892
4132
|
// console.log('[CreatorApp] Set primary widget:', widgetId);
|
|
3893
|
-
if (
|
|
4133
|
+
if (targetCanvasId && canvasesClient) {
|
|
3894
4134
|
try {
|
|
3895
4135
|
await canvasesClient.updateCanvas({
|
|
3896
|
-
id:
|
|
4136
|
+
id: targetCanvasId,
|
|
3897
4137
|
updateCanvasRequest: { primaryWidgetId: widgetId }
|
|
3898
4138
|
});
|
|
3899
4139
|
} catch (err) {
|
|
3900
4140
|
console.warn('[CreatorApp] Failed to save primary_widget_id:', err);
|
|
3901
4141
|
}
|
|
3902
4142
|
}
|
|
3903
|
-
|
|
3904
|
-
if (
|
|
4143
|
+
// Adopt orphans AND cross-canvas strays whose shape lives here (explicit user intent).
|
|
4144
|
+
if (targetCanvasId && widgetsClient && shouldAdoptToThisCanvas) {
|
|
3905
4145
|
try {
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4146
|
+
await widgetsClient.updateWidget({
|
|
4147
|
+
id: widgetId,
|
|
4148
|
+
updateWidgetRequest: { canvasId: targetCanvasId },
|
|
4149
|
+
});
|
|
4150
|
+
widgets = widgets.map((w: any) =>
|
|
4151
|
+
w.id === widgetId ? { ...w, canvasId: targetCanvasId, canvas_id: targetCanvasId } : w,
|
|
4152
|
+
);
|
|
4153
|
+
console.log(
|
|
4154
|
+
'[CreatorApp] Adopted widget canvas_id to host canvas (primary pick)',
|
|
4155
|
+
widgetId.slice(0, 8),
|
|
4156
|
+
'from',
|
|
4157
|
+
widgetCanvasId ? String(widgetCanvasId).slice(0, 8) : 'null',
|
|
4158
|
+
'to',
|
|
4159
|
+
targetCanvasId.slice(0, 8),
|
|
4160
|
+
);
|
|
3911
4161
|
} catch (err) {
|
|
3912
|
-
console.warn('[CreatorApp] Failed to
|
|
4162
|
+
console.warn('[CreatorApp] Failed to heal widget canvas_id:', err);
|
|
3913
4163
|
}
|
|
3914
4164
|
}
|
|
4165
|
+
if (embedded) {
|
|
4166
|
+
syncUrlWidgetId(widgetId);
|
|
4167
|
+
}
|
|
3915
4168
|
if (widget) notifyHostWidgetName(widget);
|
|
3916
4169
|
emitCanvasWidgetsList('primary-changed');
|
|
4170
|
+
const anyW = widget as { thumbnailUrl?: string | null; thumbnail_url?: string | null } | null;
|
|
4171
|
+
const hasThumb = !!(anyW?.thumbnailUrl || anyW?.thumbnail_url);
|
|
4172
|
+
if (!hasThumb) {
|
|
4173
|
+
setTimeout(() => {
|
|
4174
|
+
void handleRefreshScreenshot(widgetId, { quiet: true });
|
|
4175
|
+
}, 1200);
|
|
4176
|
+
}
|
|
4177
|
+
return true;
|
|
3917
4178
|
}
|
|
3918
4179
|
|
|
3919
4180
|
function emitCanvasWidgetsList(reason: string) {
|
|
3920
4181
|
if (!embedded || typeof window === 'undefined') return;
|
|
4182
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
3921
4183
|
const ids = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
|
|
3922
4184
|
const items: { id: string; name: string }[] = [];
|
|
4185
|
+
const seen = new Set<string>();
|
|
4186
|
+
const pushItem = (id: string, name?: string | null) => {
|
|
4187
|
+
if (!id || seen.has(id) || isDraftConvertId(id)) return;
|
|
4188
|
+
seen.add(id);
|
|
4189
|
+
items.push({ id, name: (name || '').trim() || 'Untitled Widget' });
|
|
4190
|
+
};
|
|
4191
|
+
// Canvas widgets are those whose DB canvas_id matches this canvas (shapes may lag behind).
|
|
4192
|
+
if (hostCanvasId) {
|
|
4193
|
+
for (const widget of widgets) {
|
|
4194
|
+
const id = (widget as { id?: string }).id;
|
|
4195
|
+
if (!id) continue;
|
|
4196
|
+
const widgetCanvas = widgetCanvasIdOf(
|
|
4197
|
+
widget as { canvasId?: string; canvas_id?: string } | null,
|
|
4198
|
+
);
|
|
4199
|
+
if (widgetCanvas !== hostCanvasId) continue;
|
|
4200
|
+
pushItem(id, (widget as { name?: string }).name);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
// Linked shapes that live on THIS canvas. An orphan (null canvas_id) drawn here is
|
|
4204
|
+
// legitimately this canvas's widget — only foreign canvas_id ownership is excluded.
|
|
3923
4205
|
for (const id of ids) {
|
|
4206
|
+
if (!id || isDraftConvertId(id)) continue;
|
|
3924
4207
|
const widget = getWidgetById(id);
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
4208
|
+
if (!widget?.id) continue;
|
|
4209
|
+
const widgetCanvas = widgetCanvasIdOf(
|
|
4210
|
+
widget as { canvasId?: string; canvas_id?: string } | null,
|
|
4211
|
+
);
|
|
4212
|
+
if (!hostCanvasId || (widgetCanvas && widgetCanvas !== hostCanvasId)) continue;
|
|
4213
|
+
pushItem(id, widget.name);
|
|
4214
|
+
}
|
|
4215
|
+
let primaryWidgetId = (canvasPrimaryWidgetId || '').trim();
|
|
4216
|
+
const primaryOnList = primaryWidgetId && items.some((item) => item.id === primaryWidgetId);
|
|
4217
|
+
if (primaryWidgetId && !primaryOnList) {
|
|
4218
|
+
const primary = getWidgetById(primaryWidgetId);
|
|
4219
|
+
const primaryCanvas = widgetCanvasIdOf(
|
|
4220
|
+
primary as { canvasId?: string; canvas_id?: string } | null,
|
|
4221
|
+
);
|
|
4222
|
+
if (hostCanvasId && primaryCanvas && primaryCanvas === hostCanvasId && primary?.id) {
|
|
3933
4223
|
items.unshift({
|
|
3934
4224
|
id: primary.id,
|
|
3935
4225
|
name: (primary.name || '').trim() || 'Untitled Widget'
|
|
3936
4226
|
});
|
|
4227
|
+
} else {
|
|
4228
|
+
console.warn(
|
|
4229
|
+
'[CreatorApp] Dropping stale primary from canvas widgets emit',
|
|
4230
|
+
reason,
|
|
4231
|
+
primaryWidgetId.slice(0, 8),
|
|
4232
|
+
'host',
|
|
4233
|
+
hostCanvasId?.slice(0, 8) || 'none',
|
|
4234
|
+
);
|
|
4235
|
+
primaryWidgetId = items[0]?.id || '';
|
|
4236
|
+
canvasPrimaryWidgetId = primaryWidgetId || null;
|
|
4237
|
+
if (primaryWidgetId) syncUrlWidgetId(primaryWidgetId);
|
|
3937
4238
|
}
|
|
3938
4239
|
}
|
|
3939
|
-
|
|
4240
|
+
if (!primaryWidgetId && items[0]?.id) {
|
|
4241
|
+
primaryWidgetId = items[0].id;
|
|
4242
|
+
}
|
|
3940
4243
|
// console.log('[CreatorApp] Emit canvas widgets for header', reason, items.length, primaryWidgetId);
|
|
3941
4244
|
window.dispatchEvent(new CustomEvent(CREATOR_CANVAS_WIDGETS_EVENT, {
|
|
3942
|
-
detail: { widgets: items, primaryWidgetId }
|
|
4245
|
+
detail: { widgets: items, primaryWidgetId: primaryWidgetId || '' }
|
|
3943
4246
|
}));
|
|
3944
4247
|
}
|
|
3945
4248
|
|
|
@@ -3953,10 +4256,19 @@
|
|
|
3953
4256
|
console.warn('[CreatorApp] Host set primary ignored — missing widgetId');
|
|
3954
4257
|
return;
|
|
3955
4258
|
}
|
|
4259
|
+
const previousPrimary = canvasPrimaryWidgetId;
|
|
4260
|
+
// Optimistic — so host-request emits during persist cannot resurrect the previous primary.
|
|
4261
|
+
canvasPrimaryWidgetId = widgetId;
|
|
4262
|
+
saveSelectedWidgetId(widgetId);
|
|
3956
4263
|
// console.log('[CreatorApp] Host set primary widget', widgetId.substring(0, 8));
|
|
3957
4264
|
canvasRef?.setPrimaryWidgetShape?.(widgetId);
|
|
3958
4265
|
canvasRef?.selectWidgetShape?.(widgetId);
|
|
3959
|
-
void persistPrimaryWidgetId(widgetId)
|
|
4266
|
+
void persistPrimaryWidgetId(widgetId).then((ok) => {
|
|
4267
|
+
if (ok) return;
|
|
4268
|
+
canvasPrimaryWidgetId = previousPrimary;
|
|
4269
|
+
if (previousPrimary) saveSelectedWidgetId(previousPrimary);
|
|
4270
|
+
emitCanvasWidgetsList('primary-reverted');
|
|
4271
|
+
});
|
|
3960
4272
|
}
|
|
3961
4273
|
|
|
3962
4274
|
function handleSetPrimaryWidget(widgetId: string) {
|
|
@@ -3966,7 +4278,33 @@
|
|
|
3966
4278
|
async function hydrateCanvasPrimaryWidget(linkedIds?: string[]): Promise<string | null> {
|
|
3967
4279
|
const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
|
|
3968
4280
|
if (!targetCanvasId) return canvasPrimaryWidgetId || null;
|
|
4281
|
+
|
|
4282
|
+
// NOTE: `?widget_id=` is deliberately NOT consulted here. The canvas's DB
|
|
4283
|
+
// `primary_widget_id` is the single source of truth for what opens selected/red on
|
|
4284
|
+
// this canvas. A widget_id coming from a link is treated as a one-time intent in
|
|
4285
|
+
// ensureEmbeddedWidgetDeepLink(), which validates it and promotes it to primary —
|
|
4286
|
+
// so a stale id from a previously opened canvas can never win on load again.
|
|
4287
|
+
|
|
3969
4288
|
let primaryId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
|
|
4289
|
+
// Reject cross-canvas primary (stale DB rows mix project cards / header) unless the
|
|
4290
|
+
// widget's shape actually lives on this canvas — then the shape wins over stale metadata.
|
|
4291
|
+
if (primaryId) {
|
|
4292
|
+
const primaryWidget = getWidgetById(primaryId);
|
|
4293
|
+
const primaryCanvasId = widgetCanvasIdOf(
|
|
4294
|
+
primaryWidget as { canvasId?: string | null; canvas_id?: string | null } | null,
|
|
4295
|
+
);
|
|
4296
|
+
const primaryShapeHere = !!canvasRef?.getLinkedWidgetIds?.()?.has(primaryId);
|
|
4297
|
+
if (primaryCanvasId && primaryCanvasId !== targetCanvasId && !primaryShapeHere) {
|
|
4298
|
+
console.warn(
|
|
4299
|
+
'[CreatorApp] Ignoring cross-canvas primary_widget_id',
|
|
4300
|
+
primaryId.slice(0, 8),
|
|
4301
|
+
'on canvas',
|
|
4302
|
+
targetCanvasId.slice(0, 8),
|
|
4303
|
+
);
|
|
4304
|
+
primaryId = null;
|
|
4305
|
+
canvasPrimaryWidgetId = null;
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
3970
4308
|
if (!primaryId) {
|
|
3971
4309
|
const fromLinks = (linkedIds || []).filter((id) => id && !isDraftConvertId(id) && !String(id).startsWith('draft'));
|
|
3972
4310
|
const fromList = widgets
|
|
@@ -3975,7 +4313,11 @@
|
|
|
3975
4313
|
return cid === targetCanvasId && w.id && !isDraftConvertId(w.id);
|
|
3976
4314
|
})
|
|
3977
4315
|
.map((w: any) => w.id);
|
|
3978
|
-
primaryId = fromLinks
|
|
4316
|
+
primaryId = fromLinks.find((id) => {
|
|
4317
|
+
const w = getWidgetById(id);
|
|
4318
|
+
const cid = (w as any)?.canvasId || (w as any)?.canvas_id;
|
|
4319
|
+
return !cid || cid === targetCanvasId;
|
|
4320
|
+
}) || fromList[0] || null;
|
|
3979
4321
|
if (primaryId) {
|
|
3980
4322
|
// console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
|
|
3981
4323
|
await persistPrimaryWidgetId(primaryId);
|
|
@@ -3989,6 +4331,12 @@
|
|
|
3989
4331
|
return primaryId || canvasPrimaryWidgetId || null;
|
|
3990
4332
|
}
|
|
3991
4333
|
|
|
4334
|
+
function applyPrimarySelectionOnCanvas(primaryId: string | null | undefined): void {
|
|
4335
|
+
if (!primaryId || !canvasRef || isConvertOrDraftPanelOpen()) return;
|
|
4336
|
+
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
4337
|
+
canvasRef.selectWidgetShape?.(primaryId);
|
|
4338
|
+
}
|
|
4339
|
+
|
|
3992
4340
|
async function handleHostMoveCanvasFolderEvent(event: Event) {
|
|
3993
4341
|
const folderId = (event as CustomEvent).detail?.folderId;
|
|
3994
4342
|
if (!folderId || typeof folderId !== 'string' || !widgetsClient) return;
|
|
@@ -4441,12 +4789,46 @@
|
|
|
4441
4789
|
}, delayMs);
|
|
4442
4790
|
}
|
|
4443
4791
|
|
|
4792
|
+
/**
|
|
4793
|
+
* Read the canvas id from the address bar.
|
|
4794
|
+
*
|
|
4795
|
+
* The canvas id lives in the **path** (`/canvas/<canvasId>`), not in a query string.
|
|
4796
|
+
*
|
|
4797
|
+
* `loadCurrentCanvas()` is also triggered by a reactive auth effect, which can run BEFORE
|
|
4798
|
+
* the onMount route subscription populates `routeCanvasId`. In that race, `routeCanvasId`
|
|
4799
|
+
* was still null, so the loader fell through to `getSavedSelectedCanvasId()` and a canvas
|
|
4800
|
+
* remembered from a *previous* session silently replaced the canvas in the URL. Reading the
|
|
4801
|
+
* address bar synchronously here makes the opened canvas authoritative in every entry path.
|
|
4802
|
+
*/
|
|
4803
|
+
function readCanvasIdFromAddressBar(): string | null {
|
|
4804
|
+
if (typeof window === 'undefined') return null;
|
|
4805
|
+
try {
|
|
4806
|
+
const pathParts = window.location.pathname.split('/');
|
|
4807
|
+
const canvasIndex = pathParts.indexOf('canvas');
|
|
4808
|
+
const fromPath = canvasIndex >= 0 ? (pathParts[canvasIndex + 1] || '').trim() : '';
|
|
4809
|
+
if (fromPath) return decodeURIComponent(fromPath);
|
|
4810
|
+
// Legacy query string — kept only so an old bookmark still resolves to its canvas.
|
|
4811
|
+
const params = new URLSearchParams(window.location.search);
|
|
4812
|
+
const fromQuery = params.get('canvas_id') || params.get('canvas-id');
|
|
4813
|
+
return fromQuery && fromQuery.trim() ? fromQuery.trim() : null;
|
|
4814
|
+
} catch {
|
|
4815
|
+
return null;
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
|
|
4444
4819
|
async function loadCurrentCanvas() {
|
|
4445
4820
|
// console.log('Widget Creator: loadCurrentCanvas():', routeCanvasId, isLoadingCanvasOnce);
|
|
4446
4821
|
if (isLoadingCanvasOnce) return;
|
|
4447
4822
|
try {
|
|
4448
4823
|
isLoadingCanvasOnce = true;
|
|
4449
4824
|
|
|
4825
|
+
// Resolve the canvas from the address bar before any localStorage fallback — see
|
|
4826
|
+
// readCanvasIdFromAddressBar(). This must happen before initialCandidateCanvasId.
|
|
4827
|
+
if (!routeCanvasId) {
|
|
4828
|
+
const fromAddressBar = readCanvasIdFromAddressBar();
|
|
4829
|
+
if (fromAddressBar) routeCanvasId = fromAddressBar;
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4450
4832
|
// set widgets loading state
|
|
4451
4833
|
isLoadingWidgets = true;
|
|
4452
4834
|
widgetsError = null;
|
|
@@ -4479,6 +4861,16 @@
|
|
|
4479
4861
|
// Prefer canvasId from route (path or query)
|
|
4480
4862
|
if (routeCanvasId) {
|
|
4481
4863
|
canvasListResolved = true;
|
|
4864
|
+
// The route is authoritative — keep the remembered canvas in step with it. Since the
|
|
4865
|
+
// host now passes `canvasId` as a prop, the `!canvasId` guard above can skip the sync
|
|
4866
|
+
// and leave localStorage pointing at the previously opened canvas, which then wins
|
|
4867
|
+
// the next time a bare `/canvas` is opened.
|
|
4868
|
+
if (sessionCanvasId !== routeCanvasId || canvasId !== routeCanvasId) {
|
|
4869
|
+
rememberCanvasId(routeCanvasId);
|
|
4870
|
+
}
|
|
4871
|
+
// New canvas session — clear primary from previous canvas before hydrate.
|
|
4872
|
+
canvasPrimaryWidgetId = null;
|
|
4873
|
+
embeddedDeepLinkAutoOpenDone = false;
|
|
4482
4874
|
// console.log('WidgetCreator.loadCurrentCanvas: Loading data for route canvasId:', canvasId);
|
|
4483
4875
|
}
|
|
4484
4876
|
// Fallback: get latest canvas from current user's canvases
|
|
@@ -4582,10 +4974,16 @@
|
|
|
4582
4974
|
const { loaded, widgetIds: widgetIdsOnCanvas } = await parallelCanvasLoadPromise;
|
|
4583
4975
|
await maybeAutoRestoreWidgetShapes();
|
|
4584
4976
|
backfillWidgetShapeMetadata();
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4977
|
+
const linkedIds =
|
|
4978
|
+
widgetIdsOnCanvas.length > 0
|
|
4979
|
+
? widgetIdsOnCanvas
|
|
4980
|
+
: [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
|
|
4981
|
+
const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
|
|
4982
|
+
if (loaded) {
|
|
4983
|
+
applyPrimarySelectionOnCanvas(primaryId);
|
|
4984
|
+
requestAnimationFrame(() => {
|
|
4985
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
|
|
4986
|
+
});
|
|
4589
4987
|
}
|
|
4590
4988
|
return;
|
|
4591
4989
|
}
|
|
@@ -4602,10 +5000,16 @@
|
|
|
4602
5000
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
4603
5001
|
await maybeAutoRestoreWidgetShapes();
|
|
4604
5002
|
backfillWidgetShapeMetadata();
|
|
4605
|
-
const
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
5003
|
+
const linkedIds =
|
|
5004
|
+
widgetIdsOnCanvas.length > 0
|
|
5005
|
+
? widgetIdsOnCanvas
|
|
5006
|
+
: [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
|
|
5007
|
+
const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
|
|
5008
|
+
if (loaded || isCanvasStateLoaded) {
|
|
5009
|
+
applyPrimarySelectionOnCanvas(primaryId);
|
|
5010
|
+
requestAnimationFrame(() => {
|
|
5011
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
|
|
5012
|
+
});
|
|
4609
5013
|
}
|
|
4610
5014
|
} catch (error) {
|
|
4611
5015
|
console.error('Widget Creator: Load canvases error:', error);
|
|
@@ -4675,6 +5079,11 @@
|
|
|
4675
5079
|
sessionCanvasId = entry.id;
|
|
4676
5080
|
activeCanvasName = entry.name;
|
|
4677
5081
|
saveSelectedCanvasId(canvasId);
|
|
5082
|
+
// Address bar must track the canvas we actually switched to, and must not keep a
|
|
5083
|
+
// widget_id belonging to the previous canvas.
|
|
5084
|
+
routeCanvasId = entry.id;
|
|
5085
|
+
canvasPrimaryWidgetId = null;
|
|
5086
|
+
syncUrlCanvasId(entry.id);
|
|
4678
5087
|
|
|
4679
5088
|
// Clear existing widgets and canvas visually
|
|
4680
5089
|
widgets = [];
|
|
@@ -4692,6 +5101,21 @@
|
|
|
4692
5101
|
}
|
|
4693
5102
|
}
|
|
4694
5103
|
|
|
5104
|
+
/**
|
|
5105
|
+
* Clear the cross-tab "generating"/"publishing" flags when the tab closes.
|
|
5106
|
+
*
|
|
5107
|
+
* Lives at component scope (not inside `onMount`) so `onDestroy` can pass the
|
|
5108
|
+
* same reference to `removeEventListener`. When it was declared inside
|
|
5109
|
+
* `onMount`, leaving the canvas threw `handleBeforeUnload is not defined`,
|
|
5110
|
+
* which aborted the rest of the cleanup and blocked the navigation.
|
|
5111
|
+
*/
|
|
5112
|
+
function handleBeforeUnload() {
|
|
5113
|
+
for (const wId of Object.keys(generatingWidgetIds)) {
|
|
5114
|
+
setCrossTabGenerating(false, wId);
|
|
5115
|
+
}
|
|
5116
|
+
if (isPublishing) setCrossTabPublishing(false);
|
|
5117
|
+
}
|
|
5118
|
+
|
|
4695
5119
|
/** Close canvas dropdown on outside click */
|
|
4696
5120
|
function handleCanvasDropdownOutsideClick(e: MouseEvent) {
|
|
4697
5121
|
const target = e.target as HTMLElement;
|
|
@@ -4878,7 +5302,15 @@
|
|
|
4878
5302
|
}
|
|
4879
5303
|
|
|
4880
5304
|
try {
|
|
4881
|
-
|
|
5305
|
+
// Names must be unique per user — duplicate keys ("Canvas 1" x N) made
|
|
5306
|
+
// canvases indistinguishable and contributed to cross-canvas widget mix-ups.
|
|
5307
|
+
const usedNames = new Set(userCanvases.map((c) => (c.name || '').trim()).filter(Boolean));
|
|
5308
|
+
let canvasIndex = userCanvases.length + 1;
|
|
5309
|
+
let newName = `Canvas ${canvasIndex}`;
|
|
5310
|
+
while (usedNames.has(newName)) {
|
|
5311
|
+
canvasIndex += 1;
|
|
5312
|
+
newName = `Canvas ${canvasIndex}`;
|
|
5313
|
+
}
|
|
4882
5314
|
const createdRaw = await canvasesClient.createCanvas({
|
|
4883
5315
|
createCanvasRequest: { persistenceKey: newName }
|
|
4884
5316
|
});
|
|
@@ -4991,16 +5423,51 @@
|
|
|
4991
5423
|
deleteCanvasTarget = entry;
|
|
4992
5424
|
showDeleteCanvasDialog = true;
|
|
4993
5425
|
showCanvasDropdown = false;
|
|
5426
|
+
deleteCanvasError = '';
|
|
5427
|
+
void loadDeleteCanvasPreview(entry.id);
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
/**
|
|
5431
|
+
* Fetch what the deletion would remove so the dialog can name it.
|
|
5432
|
+
* Failure is non-fatal: the dialog still explains the action, just without
|
|
5433
|
+
* the per-project breakdown.
|
|
5434
|
+
*/
|
|
5435
|
+
async function loadDeleteCanvasPreview(canvasId: string): Promise<void> {
|
|
5436
|
+
deleteCanvasPreview = null;
|
|
5437
|
+
deleteCanvasPreviewLoading = true;
|
|
5438
|
+
try {
|
|
5439
|
+
const response = await canvasesClient?.previewDeleteCanvas?.({ id: canvasId });
|
|
5440
|
+
const preview = response?.data;
|
|
5441
|
+
|
|
5442
|
+
if (!preview) {
|
|
5443
|
+
// Older SDK build without the delete-preview method — no breakdown.
|
|
5444
|
+
console.warn('[WidgetCreator] Canvas delete preview unavailable');
|
|
5445
|
+
return;
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5448
|
+
deleteCanvasPreview = {
|
|
5449
|
+
widgetCount: preview.widgetCount ?? 0,
|
|
5450
|
+
groups: (preview.groups ?? []).map((group) => ({
|
|
5451
|
+
projectName: group.projectName || 'Unknown project',
|
|
5452
|
+
widgetNames: group.widgetNames ?? []
|
|
5453
|
+
}))
|
|
5454
|
+
};
|
|
5455
|
+
} catch (err) {
|
|
5456
|
+
console.warn('[WidgetCreator] Could not load canvas delete preview:', err);
|
|
5457
|
+
} finally {
|
|
5458
|
+
deleteCanvasPreviewLoading = false;
|
|
5459
|
+
}
|
|
4994
5460
|
}
|
|
4995
5461
|
|
|
4996
5462
|
async function handleDeleteCanvasConfirm() {
|
|
4997
5463
|
if (!deleteCanvasTarget || !canvasesClient) return;
|
|
4998
5464
|
const targetId = deleteCanvasTarget.id;
|
|
4999
5465
|
const targetName = deleteCanvasTarget.name;
|
|
5000
|
-
|
|
5466
|
+
deleteCanvasError = '';
|
|
5001
5467
|
try {
|
|
5002
5468
|
await canvasesClient.deleteCanvas({ id: targetId });
|
|
5003
5469
|
// console.log('[WidgetCreator] Canvas deleted:', targetId, targetName);
|
|
5470
|
+
showDeleteCanvasDialog = false;
|
|
5004
5471
|
userCanvases = userCanvases.filter(c => c.id !== targetId);
|
|
5005
5472
|
if (canvasId === targetId) {
|
|
5006
5473
|
const next = userCanvases[0] || null;
|
|
@@ -5016,6 +5483,10 @@
|
|
|
5016
5483
|
}
|
|
5017
5484
|
} catch (err) {
|
|
5018
5485
|
console.error('[WidgetCreator] Failed to delete canvas:', targetId, err);
|
|
5486
|
+
// Keep the dialog open so the user can react; a partial failure leaves
|
|
5487
|
+
// the canvas intact on purpose (the API refuses to orphan widgets).
|
|
5488
|
+
deleteCanvasError = 'Could not delete this canvas. Some of its widgets could not be removed, so nothing was deleted.';
|
|
5489
|
+
return;
|
|
5019
5490
|
}
|
|
5020
5491
|
deleteCanvasTarget = null;
|
|
5021
5492
|
}
|
|
@@ -5085,15 +5556,39 @@
|
|
|
5085
5556
|
// console.log('Widgets for canvas:', canvasId, ':', widgets);
|
|
5086
5557
|
syncOrphanedWidgetShapes();
|
|
5087
5558
|
|
|
5088
|
-
// Auto-select: embedded deep-link > localStorage > latest
|
|
5559
|
+
// Auto-select: embedded deep-link > localStorage > latest.
|
|
5560
|
+
// Both sources must belong to THIS canvas — localStorage is shared across canvases,
|
|
5561
|
+
// so a saved id from a previous canvas would otherwise leak in as primary.
|
|
5089
5562
|
if (embedded && forceCreateNewWidget && !initialWidgetId) {
|
|
5090
5563
|
// console.log('[WidgetCreator] Skipping auto-select of existing widgets (new widget from site)');
|
|
5091
5564
|
} else if (widgets.length > 0) {
|
|
5092
|
-
const
|
|
5093
|
-
|
|
5565
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
5566
|
+
// A saved widget is only trustworthy when we can PROVE it belongs here: the
|
|
5567
|
+
// canvas must be known and the widget's canvas_id must match (null canvas_id
|
|
5568
|
+
// is only adopted via an explicit primary pick, not via localStorage).
|
|
5569
|
+
const savedWidgetRaw = getSavedSelectedWidgetId()
|
|
5570
|
+
? getWidgetById(getSavedSelectedWidgetId() as string)
|
|
5094
5571
|
: null;
|
|
5095
|
-
const
|
|
5096
|
-
|
|
5572
|
+
const savedWidget =
|
|
5573
|
+
savedWidgetRaw &&
|
|
5574
|
+
hostCanvasId &&
|
|
5575
|
+
widgetCanvasIdOf(
|
|
5576
|
+
savedWidgetRaw as { canvasId?: string | null; canvas_id?: string | null },
|
|
5577
|
+
) === hostCanvasId
|
|
5578
|
+
? savedWidgetRaw
|
|
5579
|
+
: null;
|
|
5580
|
+
if (savedWidgetRaw && !savedWidget) {
|
|
5581
|
+
console.warn(
|
|
5582
|
+
'[CreatorApp] Ignoring saved widget from another canvas',
|
|
5583
|
+
savedWidgetRaw.id?.slice(0, 8),
|
|
5584
|
+
'host',
|
|
5585
|
+
hostCanvasId?.slice(0, 8) || 'unknown',
|
|
5586
|
+
);
|
|
5587
|
+
saveSelectedWidgetId(null);
|
|
5588
|
+
}
|
|
5589
|
+
// The explicit deep-link is validated by ensureEmbeddedWidgetDeepLink(); a stale
|
|
5590
|
+
// URL id is never used to auto-select a widget from another canvas.
|
|
5591
|
+
const deepLinkedWidget = initialWidgetId ? getWidgetById(initialWidgetId) : null;
|
|
5097
5592
|
const widgetToSelect = deepLinkedWidget ?? savedWidget ?? widgets[0];
|
|
5098
5593
|
if (deepLinkedWidget) {
|
|
5099
5594
|
// console.log('[WidgetCreator] Selecting widget from embedded deep-link:', deepLinkedWidget.name);
|
|
@@ -5360,6 +5855,17 @@
|
|
|
5360
5855
|
|
|
5361
5856
|
if (missingIds.length === 0) return;
|
|
5362
5857
|
|
|
5858
|
+
// Scope merge to THIS canvas: a shape can linger on several canvases (legacy canvas
|
|
5859
|
+
// duplication), but a widget belongs to exactly one. Merging foreign widgets is what
|
|
5860
|
+
// made another canvas's widgets show up in this canvas's list/picker.
|
|
5861
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
5862
|
+
const belongsHere = (widget: WidgetSummary): boolean => {
|
|
5863
|
+
const widgetCanvas = widgetCanvasIdOf(
|
|
5864
|
+
widget as { canvasId?: string | null; canvas_id?: string | null },
|
|
5865
|
+
);
|
|
5866
|
+
return !widgetCanvas || !hostCanvasId || widgetCanvas === hostCanvasId;
|
|
5867
|
+
};
|
|
5868
|
+
|
|
5363
5869
|
try {
|
|
5364
5870
|
// console.log(
|
|
5365
5871
|
// '[WidgetCreator] Merging',
|
|
@@ -5370,8 +5876,21 @@
|
|
|
5370
5876
|
getWidgetsBatchRequest: { widgetIds: missingIds }
|
|
5371
5877
|
});
|
|
5372
5878
|
const batch = response as { widgets?: WidgetSummary[]; data?: { widgets?: WidgetSummary[] } };
|
|
5373
|
-
const
|
|
5374
|
-
if (!Array.isArray(
|
|
5879
|
+
const fetchedRaw = batch?.widgets ?? batch?.data?.widgets ?? [];
|
|
5880
|
+
if (!Array.isArray(fetchedRaw) || fetchedRaw.length === 0) return;
|
|
5881
|
+
const fetched = fetchedRaw.filter(belongsHere);
|
|
5882
|
+
const foreignCount = fetchedRaw.length - fetched.length;
|
|
5883
|
+
if (foreignCount > 0) {
|
|
5884
|
+
console.warn(
|
|
5885
|
+
'[CreatorApp] Ignoring',
|
|
5886
|
+
foreignCount,
|
|
5887
|
+
'widget(s) linked by shape but owned by another canvas',
|
|
5888
|
+
);
|
|
5889
|
+
// Keep the stale shape but stop treating it as a widget of this canvas.
|
|
5890
|
+
syncOrphanedWidgetShapes();
|
|
5891
|
+
emitCanvasWidgetsList('foreign-shapes');
|
|
5892
|
+
}
|
|
5893
|
+
if (fetched.length === 0) return;
|
|
5375
5894
|
|
|
5376
5895
|
const previousById = new Map(
|
|
5377
5896
|
widgets.filter((w) => w.id).map((w) => [w.id as string, w]),
|
|
@@ -5393,9 +5912,26 @@
|
|
|
5393
5912
|
widgets.length > 0 &&
|
|
5394
5913
|
!(embedded && forceCreateNewWidget && !initialWidgetId)
|
|
5395
5914
|
) {
|
|
5915
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
5916
|
+
const primaryOnCanvas =
|
|
5917
|
+
(canvasPrimaryWidgetId && getWidgetById(canvasPrimaryWidgetId)) ||
|
|
5918
|
+
widgets.find((w: any) => {
|
|
5919
|
+
const cid = w.canvasId || w.canvas_id;
|
|
5920
|
+
return hostCanvasId && cid === hostCanvasId && w.id === canvasPrimaryWidgetId;
|
|
5921
|
+
}) ||
|
|
5922
|
+
null;
|
|
5396
5923
|
const savedWidgetId = getSavedSelectedWidgetId();
|
|
5397
5924
|
const savedWidget = savedWidgetId ? getWidgetById(savedWidgetId) : null;
|
|
5398
|
-
|
|
5925
|
+
const savedOnThisCanvas =
|
|
5926
|
+
savedWidget &&
|
|
5927
|
+
(!hostCanvasId ||
|
|
5928
|
+
widgetCanvasIdOf(savedWidget as { canvasId?: string; canvas_id?: string }) ===
|
|
5929
|
+
hostCanvasId)
|
|
5930
|
+
? savedWidget
|
|
5931
|
+
: null;
|
|
5932
|
+
applyInitialWidgetSelectionFromList(
|
|
5933
|
+
primaryOnCanvas || savedOnThisCanvas || widgets[0],
|
|
5934
|
+
);
|
|
5399
5935
|
}
|
|
5400
5936
|
} catch (err) {
|
|
5401
5937
|
console.warn('[WidgetCreator] Failed to merge widgets linked on canvas:', err);
|
|
@@ -5404,8 +5940,8 @@
|
|
|
5404
5940
|
|
|
5405
5941
|
/**
|
|
5406
5942
|
* Site embeds CreatorApp once; `initialWidgetId` props stay frozen in prod.
|
|
5407
|
-
*
|
|
5408
|
-
*
|
|
5943
|
+
* Prefer live `?widget_id=` only when it belongs to the host canvas — otherwise
|
|
5944
|
+
* a stale widget_id from the previous canvas overwrites the header/primary.
|
|
5409
5945
|
*/
|
|
5410
5946
|
function resolveEmbeddedDeepLinkWidgetId(): string | null {
|
|
5411
5947
|
if (typeof window !== 'undefined') {
|
|
@@ -5419,6 +5955,84 @@
|
|
|
5419
5955
|
return initialWidgetId ?? null;
|
|
5420
5956
|
}
|
|
5421
5957
|
|
|
5958
|
+
function resolveHostCanvasId(): string | null {
|
|
5959
|
+
// Priority: the canvas id in the address bar is authoritative (it is what the user
|
|
5960
|
+
// opened and what a refresh resumes). It lives in the route path (`/canvas/<id>`).
|
|
5961
|
+
// `canvasId`/`sessionCanvasId` can still hold a stale value hydrated from
|
|
5962
|
+
// localStorage on a previous canvas, which is what allowed another canvas's widgets
|
|
5963
|
+
// to leak into this canvas's list/picker.
|
|
5964
|
+
const fromUrl = readCanvasIdFromAddressBar();
|
|
5965
|
+
return (
|
|
5966
|
+
fromUrl ||
|
|
5967
|
+
(typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
|
|
5968
|
+
(typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null) ||
|
|
5969
|
+
(typeof routeCanvasId === 'string' && routeCanvasId.trim() ? routeCanvasId.trim() : null)
|
|
5970
|
+
);
|
|
5971
|
+
}
|
|
5972
|
+
|
|
5973
|
+
function widgetCanvasIdOf(widget: {
|
|
5974
|
+
canvasId?: string | null;
|
|
5975
|
+
canvas_id?: string | null;
|
|
5976
|
+
} | null | undefined): string | null {
|
|
5977
|
+
if (!widget) return null;
|
|
5978
|
+
const cid = widget.canvasId ?? widget.canvas_id ?? null;
|
|
5979
|
+
return typeof cid === 'string' && cid.trim() ? cid.trim() : null;
|
|
5980
|
+
}
|
|
5981
|
+
|
|
5982
|
+
async function fetchWidgetCanvasId(widgetId: string): Promise<string | null> {
|
|
5983
|
+
const local = getWidgetById(widgetId);
|
|
5984
|
+
const localCanvas = widgetCanvasIdOf(local as { canvasId?: string; canvas_id?: string } | null);
|
|
5985
|
+
if (localCanvas) return localCanvas;
|
|
5986
|
+
if (!widgetsClient) return null;
|
|
5987
|
+
try {
|
|
5988
|
+
const batch = await widgetsClient.getWidgetsBatch({
|
|
5989
|
+
getWidgetsBatchRequest: { widgetIds: [widgetId] },
|
|
5990
|
+
});
|
|
5991
|
+
const row =
|
|
5992
|
+
(batch as { widgets?: Array<{ canvasId?: string; canvas_id?: string }> })?.widgets?.[0] ??
|
|
5993
|
+
(batch as { data?: { widgets?: Array<{ canvasId?: string; canvas_id?: string }> } })?.data
|
|
5994
|
+
?.widgets?.[0];
|
|
5995
|
+
return widgetCanvasIdOf(row);
|
|
5996
|
+
} catch (err) {
|
|
5997
|
+
console.warn('[CreatorApp] Failed to fetch widget canvas for deep-link', widgetId.slice(0, 8), err);
|
|
5998
|
+
return null;
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
|
|
6002
|
+
/**
|
|
6003
|
+
* `canvas_id` + the canvas's DB `primary_widget_id` are the source of truth for
|
|
6004
|
+
* "which widget is open on this canvas". A `widget_id` in the address bar is only a
|
|
6005
|
+
* transient intent read once at mount.
|
|
6006
|
+
*
|
|
6007
|
+
* IMPORTANT: this deliberately does NOT touch `widget_id` any more. Rewriting the URL
|
|
6008
|
+
* here used to (a) persist a stale id from a previously opened canvas as if it were the
|
|
6009
|
+
* current primary, and (b) trigger a host remount because the site keys CreatorApp's
|
|
6010
|
+
* mount on the URL params. Leaving the address bar untouched is what makes the primary
|
|
6011
|
+
* stable; the canvas's DB `primary_widget_id` decides what opens.
|
|
6012
|
+
*/
|
|
6013
|
+
function syncUrlWidgetId(_widgetId: string | null): void {
|
|
6014
|
+
if (typeof window === 'undefined' || !embedded) return;
|
|
6015
|
+
syncEmbeddedDeepLinkDismissTracking();
|
|
6016
|
+
}
|
|
6017
|
+
|
|
6018
|
+
/** Ensure the address bar points at the canvas we actually loaded (refresh-safe). */
|
|
6019
|
+
function syncUrlCanvasId(canvasIdToSync: string | null): void {
|
|
6020
|
+
if (typeof window === 'undefined' || !embedded) return;
|
|
6021
|
+
if (!canvasIdToSync || !canvasIdToSync.trim()) return;
|
|
6022
|
+
try {
|
|
6023
|
+
const url = new URL(window.location.href);
|
|
6024
|
+
const nextPath = `/canvas/${encodeURIComponent(canvasIdToSync)}`;
|
|
6025
|
+
if (url.pathname === nextPath && !url.searchParams.has('canvas_id')) return;
|
|
6026
|
+
url.pathname = nextPath;
|
|
6027
|
+
// The canvas id belongs in the path only — never duplicated into the query string.
|
|
6028
|
+
url.searchParams.delete('canvas_id');
|
|
6029
|
+
url.searchParams.delete('canvas-id');
|
|
6030
|
+
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
6031
|
+
} catch (err) {
|
|
6032
|
+
console.warn('[CreatorApp] Failed to sync canvas_id in URL:', err);
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
|
|
5422
6036
|
/**
|
|
5423
6037
|
* Canvas-only embed deep-link (My widgets / canvas without widget_id):
|
|
5424
6038
|
* read canvases.primary_widget_id so Creator can open that widget panel.
|
|
@@ -5435,20 +6049,44 @@
|
|
|
5435
6049
|
(record as { primary_widget_id?: unknown })?.primary_widget_id ??
|
|
5436
6050
|
null;
|
|
5437
6051
|
const primaryId = typeof raw === 'string' && raw.trim() ? raw.trim() : null;
|
|
5438
|
-
if (primaryId) {
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
6052
|
+
if (!primaryId) {
|
|
6053
|
+
return null;
|
|
6054
|
+
}
|
|
6055
|
+
// Validate primary belongs to this canvas (DB has many cross-canvas orphans).
|
|
6056
|
+
let widgetCanvasId: string | null = null;
|
|
6057
|
+
const local = getWidgetById(primaryId);
|
|
6058
|
+
if (local) {
|
|
6059
|
+
widgetCanvasId =
|
|
6060
|
+
(local as { canvasId?: string | null; canvas_id?: string | null }).canvasId ??
|
|
6061
|
+
(local as { canvasId?: string | null; canvas_id?: string | null }).canvas_id ??
|
|
6062
|
+
null;
|
|
6063
|
+
} else if (widgetsClient) {
|
|
6064
|
+
try {
|
|
6065
|
+
const batch = await widgetsClient.getWidgetsBatch({
|
|
6066
|
+
getWidgetsBatchRequest: { widgetIds: [primaryId] },
|
|
6067
|
+
});
|
|
6068
|
+
const row =
|
|
6069
|
+
(batch as { widgets?: Array<{ canvasId?: string; canvas_id?: string }> })?.widgets?.[0] ??
|
|
6070
|
+
(batch as { data?: { widgets?: Array<{ canvasId?: string; canvas_id?: string }> } })?.data
|
|
6071
|
+
?.widgets?.[0];
|
|
6072
|
+
widgetCanvasId = row?.canvasId ?? row?.canvas_id ?? null;
|
|
6073
|
+
} catch (batchErr) {
|
|
6074
|
+
console.warn('[CreatorApp] Failed to validate primary widget canvas', batchErr);
|
|
6075
|
+
}
|
|
5451
6076
|
}
|
|
6077
|
+
if (widgetCanvasId && widgetCanvasId !== targetCanvasId) {
|
|
6078
|
+
console.warn(
|
|
6079
|
+
'[CreatorApp] Canvas primary_widget_id points at another canvas — treating as empty',
|
|
6080
|
+
primaryId.slice(0, 8),
|
|
6081
|
+
'target',
|
|
6082
|
+
targetCanvasId.slice(0, 8),
|
|
6083
|
+
'widgetCanvas',
|
|
6084
|
+
widgetCanvasId.slice(0, 8),
|
|
6085
|
+
);
|
|
6086
|
+
canvasPrimaryWidgetId = null;
|
|
6087
|
+
return null;
|
|
6088
|
+
}
|
|
6089
|
+
canvasPrimaryWidgetId = primaryId;
|
|
5452
6090
|
return primaryId;
|
|
5453
6091
|
} catch (err) {
|
|
5454
6092
|
console.warn(
|
|
@@ -5465,11 +6103,6 @@
|
|
|
5465
6103
|
syncEmbeddedDeepLinkDismissTracking();
|
|
5466
6104
|
if (!embedded || !widgetsClient || !hasValidToken) return;
|
|
5467
6105
|
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
6106
|
return;
|
|
5474
6107
|
}
|
|
5475
6108
|
// Select primary widget once per Creator mount. Do not auto-open Widget Details.
|
|
@@ -5477,70 +6110,79 @@
|
|
|
5477
6110
|
return;
|
|
5478
6111
|
}
|
|
5479
6112
|
if (isConvertOrDraftPanelOpen()) {
|
|
5480
|
-
// console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
|
|
5481
6113
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5482
6114
|
return;
|
|
5483
6115
|
}
|
|
5484
6116
|
|
|
6117
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
5485
6118
|
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
|
-
}
|
|
6119
|
+
|
|
6120
|
+
// Stale ?widget_id= from a previous canvas must NOT win over this canvas's primary.
|
|
6121
|
+
if (deepLinkWidgetId && hostCanvasId) {
|
|
6122
|
+
const widgetCanvasId = await fetchWidgetCanvasId(deepLinkWidgetId);
|
|
6123
|
+
if (widgetCanvasId && widgetCanvasId !== hostCanvasId) {
|
|
6124
|
+
console.warn(
|
|
6125
|
+
'[CreatorApp] Ignoring stale URL widget_id from another canvas',
|
|
6126
|
+
deepLinkWidgetId.slice(0, 8),
|
|
6127
|
+
'widgetCanvas',
|
|
6128
|
+
widgetCanvasId.slice(0, 8),
|
|
6129
|
+
'host',
|
|
6130
|
+
hostCanvasId.slice(0, 8),
|
|
6131
|
+
);
|
|
6132
|
+
deepLinkWidgetId = null;
|
|
6133
|
+
syncUrlWidgetId(null);
|
|
6134
|
+
saveSelectedWidgetId(null);
|
|
5510
6135
|
}
|
|
5511
6136
|
}
|
|
5512
|
-
|
|
6137
|
+
|
|
6138
|
+
if (!deepLinkWidgetId && hostCanvasId && canvasesClient) {
|
|
6139
|
+
deepLinkWidgetId = await resolvePrimaryWidgetIdForCanvas(hostCanvasId);
|
|
6140
|
+
if (embeddedDeepLinkPanelDismissed || embeddedDeepLinkAutoOpenDone) {
|
|
6141
|
+
return;
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6145
|
+
if (!deepLinkWidgetId) {
|
|
6146
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
6147
|
+
return;
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
// Keep URL + localStorage aligned to THIS canvas's primary only.
|
|
6151
|
+
canvasPrimaryWidgetId = deepLinkWidgetId;
|
|
6152
|
+
saveSelectedWidgetId(deepLinkWidgetId);
|
|
6153
|
+
syncUrlWidgetId(deepLinkWidgetId);
|
|
5513
6154
|
|
|
5514
6155
|
const requestGen = ++embeddedDeepLinkRequestGen;
|
|
6156
|
+
const expectedWidgetId = deepLinkWidgetId;
|
|
5515
6157
|
|
|
5516
6158
|
function deepLinkOpenStillValid(): boolean {
|
|
5517
6159
|
return (
|
|
5518
6160
|
requestGen === embeddedDeepLinkRequestGen &&
|
|
5519
6161
|
!embeddedDeepLinkPanelDismissed &&
|
|
5520
|
-
|
|
6162
|
+
canvasPrimaryWidgetId === expectedWidgetId
|
|
5521
6163
|
);
|
|
5522
6164
|
}
|
|
5523
6165
|
|
|
5524
6166
|
const alreadyLinked =
|
|
5525
|
-
selectedWidgetId ===
|
|
6167
|
+
selectedWidgetId === expectedWidgetId && !!getWidgetById(expectedWidgetId);
|
|
5526
6168
|
if (alreadyLinked) {
|
|
5527
6169
|
if (embeddedInitialStepPending && initialStep) {
|
|
5528
6170
|
widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
|
|
5529
6171
|
embeddedInitialStepPending = false;
|
|
5530
6172
|
}
|
|
5531
6173
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
6174
|
+
applyPrimarySelectionOnCanvas(expectedWidgetId);
|
|
6175
|
+
const primaryWidget = getWidgetById(expectedWidgetId);
|
|
6176
|
+
if (primaryWidget) notifyHostWidgetName(primaryWidget);
|
|
6177
|
+
emitCanvasWidgetsList('deeplink-already-linked');
|
|
5535
6178
|
return;
|
|
5536
6179
|
}
|
|
5537
6180
|
|
|
5538
6181
|
try {
|
|
5539
6182
|
const response = await widgetsClient.getWidgetsBatch({
|
|
5540
|
-
getWidgetsBatchRequest: { widgetIds: [
|
|
6183
|
+
getWidgetsBatchRequest: { widgetIds: [expectedWidgetId] }
|
|
5541
6184
|
});
|
|
5542
6185
|
if (!deepLinkOpenStillValid()) {
|
|
5543
|
-
// console.log('[CreatorApp] Stale embedded deep-link fetch — user changed selection or closed panel');
|
|
5544
6186
|
return;
|
|
5545
6187
|
}
|
|
5546
6188
|
|
|
@@ -5550,47 +6192,52 @@
|
|
|
5550
6192
|
};
|
|
5551
6193
|
const target = batch?.widgets?.[0] ?? batch?.data?.widgets?.[0];
|
|
5552
6194
|
if (!target?.id) {
|
|
5553
|
-
console.warn('[CreatorApp] Embedded deep-link widget not found:',
|
|
6195
|
+
console.warn('[CreatorApp] Embedded deep-link widget not found:', expectedWidgetId);
|
|
5554
6196
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5555
6197
|
return;
|
|
5556
6198
|
}
|
|
5557
6199
|
|
|
5558
|
-
const targetCanvasId =
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
6200
|
+
const targetCanvasId = widgetCanvasIdOf(
|
|
6201
|
+
target as { canvasId?: string | null; canvas_id?: string | null },
|
|
6202
|
+
);
|
|
6203
|
+
if (hostCanvasId && targetCanvasId && targetCanvasId !== hostCanvasId) {
|
|
6204
|
+
console.warn(
|
|
6205
|
+
'[CreatorApp] Deep-link widget does not belong to host canvas — abort select',
|
|
6206
|
+
target.id.slice(0, 8),
|
|
6207
|
+
'target',
|
|
6208
|
+
targetCanvasId.slice(0, 8),
|
|
6209
|
+
'host',
|
|
6210
|
+
hostCanvasId.slice(0, 8),
|
|
6211
|
+
);
|
|
6212
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
6213
|
+
const healed = await hydrateCanvasPrimaryWidget(
|
|
6214
|
+
[...(canvasRef?.getLinkedWidgetIds?.() ?? [])],
|
|
6215
|
+
);
|
|
6216
|
+
applyPrimarySelectionOnCanvas(healed);
|
|
6217
|
+
return;
|
|
6218
|
+
}
|
|
5562
6219
|
|
|
5563
6220
|
if (!getWidgetById(target.id)) {
|
|
5564
6221
|
widgets = sortWidgetSummaries([...widgets, target]);
|
|
5565
6222
|
}
|
|
5566
6223
|
if (!deepLinkOpenStillValid()) {
|
|
5567
|
-
// console.log('[CreatorApp] Skip embedded deep-link open — stale after widget fetch');
|
|
5568
6224
|
return;
|
|
5569
6225
|
}
|
|
5570
6226
|
|
|
5571
6227
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5572
|
-
|
|
6228
|
+
canvasPrimaryWidgetId = target.id;
|
|
5573
6229
|
await selectWidget(target, { skipLoadWidget: true, skipPlaceShape: true });
|
|
5574
6230
|
if (!deepLinkOpenStillValid()) return;
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
6231
|
+
applyPrimarySelectionOnCanvas(target.id);
|
|
6232
|
+
requestAnimationFrame(() => {
|
|
6233
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(target.id), 80);
|
|
6234
|
+
});
|
|
6235
|
+
notifyHostWidgetName(target);
|
|
6236
|
+
emitCanvasWidgetsList('deeplink-primary');
|
|
5578
6237
|
if (embeddedInitialStepPending && initialStep) {
|
|
5579
6238
|
widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
|
|
5580
6239
|
embeddedInitialStepPending = false;
|
|
5581
6240
|
}
|
|
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
6241
|
} catch (err) {
|
|
5595
6242
|
console.error('[CreatorApp] ensureEmbeddedWidgetDeepLink failed:', err);
|
|
5596
6243
|
embeddedDeepLinkAutoOpenDone = true;
|
|
@@ -6143,6 +6790,11 @@
|
|
|
6143
6790
|
}
|
|
6144
6791
|
|
|
6145
6792
|
notifyHostWidgetName({ id: widgetId, name: newName });
|
|
6793
|
+
emitCanvasWidgetsList('widget-renamed');
|
|
6794
|
+
widgetNamesRevision += 1;
|
|
6795
|
+
panelDisplayRevision += 1;
|
|
6796
|
+
backfillWidgetShapeMetadata();
|
|
6797
|
+
syncWidgetShapeLivePreviews();
|
|
6146
6798
|
// console.log('[WidgetRename] Successfully renamed widget:', widgetId, 'to:', newName);
|
|
6147
6799
|
showToast('success', `Widget renamed to "${newName}"`);
|
|
6148
6800
|
} catch (error) {
|
|
@@ -6337,26 +6989,37 @@
|
|
|
6337
6989
|
* Aborts the polling and resets UI state
|
|
6338
6990
|
*/
|
|
6339
6991
|
function stopGeneration() {
|
|
6340
|
-
//
|
|
6341
|
-
|
|
6342
|
-
|
|
6992
|
+
// Prefer the focused panel widget (Stop lives in that panel's Chat), then selected, then all in-flight.
|
|
6993
|
+
const stopTargetIds = Array.from(
|
|
6994
|
+
new Set(
|
|
6995
|
+
[focusedPanelWidgetId, selectedWidgetId, ...Object.keys(generatingWidgetIds)].filter(
|
|
6996
|
+
(id): id is string => typeof id === 'string' && id.length > 0,
|
|
6997
|
+
),
|
|
6998
|
+
),
|
|
6999
|
+
);
|
|
7000
|
+
|
|
7001
|
+
for (const widgetId of stopTargetIds) {
|
|
7002
|
+
const controller = widgetAbortControllers.get(widgetId);
|
|
6343
7003
|
if (controller) {
|
|
6344
7004
|
controller.abort();
|
|
6345
|
-
widgetAbortControllers.delete(
|
|
7005
|
+
widgetAbortControllers.delete(widgetId);
|
|
6346
7006
|
}
|
|
6347
|
-
|
|
6348
|
-
generatingWidgetIds = restStop;
|
|
7007
|
+
clearActiveOperationId(widgetId);
|
|
6349
7008
|
}
|
|
7009
|
+
|
|
7010
|
+
generatingWidgetIds = {};
|
|
6350
7011
|
// Legacy fallback
|
|
6351
7012
|
if (codeGenerationAbortController) {
|
|
6352
7013
|
codeGenerationAbortController.abort();
|
|
6353
7014
|
codeGenerationAbortController = null;
|
|
6354
7015
|
}
|
|
6355
|
-
|
|
7016
|
+
|
|
7017
|
+
currentGenerationOperationId = null;
|
|
6356
7018
|
isGeneratingCode = false;
|
|
6357
7019
|
setCrossTabGenerating(false);
|
|
6358
7020
|
generateCodeStatus = '⏹ Generation stopped by user';
|
|
6359
|
-
|
|
7021
|
+
const statusWidgetId = focusedPanelWidgetId || selectedWidgetId || stopTargetIds[0] || null;
|
|
7022
|
+
getChatMethodsForWidget(statusWidgetId)?.updateLatestCodegenStatusMessage?.(
|
|
6360
7023
|
'Generation stopped by user. You can Retry this prompt.',
|
|
6361
7024
|
);
|
|
6362
7025
|
showToast('info', 'Generation stopped', {
|
|
@@ -6866,7 +7529,8 @@
|
|
|
6866
7529
|
const shapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
|
|
6867
7530
|
if (shapeDataUrl && canvasRef) {
|
|
6868
7531
|
canvasRef.updateWidgetImage(widgetId, shapeDataUrl);
|
|
6869
|
-
|
|
7532
|
+
// Do NOT persist Fabric placeholder/icon as project-list thumbnail —
|
|
7533
|
+
// only iframe screenshots (below) represent the real widget.
|
|
6870
7534
|
} else {
|
|
6871
7535
|
// console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
|
|
6872
7536
|
}
|
|
@@ -7659,37 +8323,49 @@ Requirements:
|
|
|
7659
8323
|
// cache-busts the same URL and looks like an extra reload.
|
|
7660
8324
|
// console.log('[CreatorApp] Skipping extra preview remount — Worker iframe already showing');
|
|
7661
8325
|
activePanelRef?.setPreviewLoadingState(false);
|
|
8326
|
+
// previewReady may not re-fire when we skip remount — clear stuck status.
|
|
8327
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8328
|
+
generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
|
|
8329
|
+
});
|
|
7662
8330
|
} else {
|
|
7663
8331
|
activePanelRef?.setPreviewLoadingState(true);
|
|
7664
8332
|
await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
|
|
7665
8333
|
}
|
|
7666
8334
|
activePanelRef?.setPreviewBuildError(null);
|
|
8335
|
+
// Show Worker preview on the canvas shape (not the previous Live CDN artifact).
|
|
8336
|
+
shapePreviewSourceByWidgetId = {
|
|
8337
|
+
...shapePreviewSourceByWidgetId,
|
|
8338
|
+
[generationWidgetId]: 'preview',
|
|
8339
|
+
};
|
|
8340
|
+
syncWidgetShapeLivePreviews();
|
|
8341
|
+
panelDisplayRevision += 1;
|
|
7667
8342
|
if (!completedPreviewBuildErr) {
|
|
7668
8343
|
await applyDesignSchemaDefaultsToPreview(generationWidgetId);
|
|
7669
8344
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
7670
8345
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
7671
8346
|
`Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''}\nPreview ready.`,
|
|
7672
8347
|
);
|
|
8348
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8349
|
+
generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
|
|
8350
|
+
});
|
|
7673
8351
|
setTimeout(async () => {
|
|
7674
8352
|
await panelChat?.reloadConversationMessages?.();
|
|
7675
8353
|
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
7676
8354
|
}, 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
|
-
})();
|
|
8355
|
+
setTimeout(() => {
|
|
8356
|
+
void (async () => {
|
|
8357
|
+
await handleRefreshScreenshot(generationWidgetId, { quiet: true });
|
|
8358
|
+
// List thumb only from live iframe capture — never Fabric shape icon.
|
|
8359
|
+
})();
|
|
8360
|
+
}, 3000);
|
|
8361
|
+
// Do NOT auto-publish — user previews first, then clicks Publish / Set live.
|
|
7689
8362
|
} else {
|
|
7690
8363
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
7691
8364
|
`Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
7692
8365
|
);
|
|
8366
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8367
|
+
generateCodeStatus: `Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
8368
|
+
});
|
|
7693
8369
|
}
|
|
7694
8370
|
}, completedPreviewBuildErr ? 400 : 800);
|
|
7695
8371
|
} else {
|
|
@@ -8132,22 +8808,21 @@ Requirements:
|
|
|
8132
8808
|
window.addEventListener('storage', handleCrossTabStorageEvent);
|
|
8133
8809
|
window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
8134
8810
|
|
|
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
|
-
};
|
|
8811
|
+
// Clean up cross-tab flags when tab is closed/refreshed. The handler lives at
|
|
8812
|
+
// component scope so `onDestroy` can remove the same reference.
|
|
8142
8813
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
8143
8814
|
|
|
8144
|
-
// Read canvas id from route
|
|
8145
|
-
|
|
8815
|
+
// Read canvas id from the route. The route path (`/canvas/<canvasId>`) is the
|
|
8816
|
+
// AUTHORITATIVE canvas (site links, refresh, deep-links); localStorage is only a
|
|
8817
|
+
// fallback and must never win over it.
|
|
8818
|
+
if (!routeCanvasId) {
|
|
8146
8819
|
try {
|
|
8147
8820
|
const sub = pageStore.subscribe(($page) => {
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8821
|
+
// `readCanvasIdFromAddressBar()` also covers the legacy query string, so an
|
|
8822
|
+
// old bookmark `/canvas?canvas_id=…` still resolves to its canvas.
|
|
8823
|
+
const nextCanvasId = readCanvasIdFromAddressBar();
|
|
8824
|
+
// Never clobber an already-known canvas with null.
|
|
8825
|
+
if (nextCanvasId) routeCanvasId = nextCanvasId;
|
|
8151
8826
|
});
|
|
8152
8827
|
setTimeout(() => {
|
|
8153
8828
|
if (sub && typeof sub === 'function') sub();
|
|
@@ -8439,6 +9114,10 @@ Requirements:
|
|
|
8439
9114
|
function formatCodegenProgressStatus(op: OperationStatus): string {
|
|
8440
9115
|
const stage = getOperationStageMessage(op.metadata as Record<string, unknown> | undefined);
|
|
8441
9116
|
const pct = typeof op.progress === 'number' ? op.progress : null;
|
|
9117
|
+
// User mode: never expose model names / provider in the status line.
|
|
9118
|
+
if (!debugMode) {
|
|
9119
|
+
return pct != null && pct > 0 ? `Generating code... (${pct}%)` : 'Generating code...';
|
|
9120
|
+
}
|
|
8442
9121
|
if (stage) {
|
|
8443
9122
|
return pct != null && pct > 0 ? `${stage} (${pct}%)` : stage;
|
|
8444
9123
|
}
|
|
@@ -8468,13 +9147,22 @@ Requirements:
|
|
|
8468
9147
|
>,
|
|
8469
9148
|
): void {
|
|
8470
9149
|
ensurePanelState(widgetId);
|
|
8471
|
-
|
|
9150
|
+
const nextPatch = { ...patch };
|
|
9151
|
+
if (typeof nextPatch.generateCodeStatus === 'string' && !debugMode) {
|
|
9152
|
+
nextPatch.generateCodeStatus = nextPatch.generateCodeStatus
|
|
9153
|
+
.replace(/\s*Commit:\s*[a-f0-9]{7,40}/gi, '')
|
|
9154
|
+
.replace(/\s*\(\s*Commit:\s*[a-f0-9]{7,40}\s*\)/gi, '')
|
|
9155
|
+
.replace(/\s*\(\s*[a-f0-9]{7,40}\s*\)/g, '')
|
|
9156
|
+
.replace(/\s{2,}/g, ' ')
|
|
9157
|
+
.trim();
|
|
9158
|
+
}
|
|
9159
|
+
patchPanelSnapshot(widgetId, nextPatch);
|
|
8472
9160
|
if (widgetId === focusedPanelWidgetId) {
|
|
8473
|
-
if (
|
|
8474
|
-
if (
|
|
8475
|
-
if (
|
|
8476
|
-
if (
|
|
8477
|
-
if (
|
|
9161
|
+
if (nextPatch.generateCodeStatus !== undefined) generateCodeStatus = nextPatch.generateCodeStatus;
|
|
9162
|
+
if (nextPatch.lastGenerationFailed !== undefined) lastGenerationFailed = nextPatch.lastGenerationFailed;
|
|
9163
|
+
if (nextPatch.lastGenerationError !== undefined) lastGenerationError = nextPatch.lastGenerationError;
|
|
9164
|
+
if (nextPatch.lastCommitId !== undefined) lastCommitId = nextPatch.lastCommitId;
|
|
9165
|
+
if (nextPatch.hasGeneratedCode !== undefined) hasGeneratedCode = nextPatch.hasGeneratedCode;
|
|
8478
9166
|
}
|
|
8479
9167
|
}
|
|
8480
9168
|
|
|
@@ -8614,13 +9302,15 @@ Requirements:
|
|
|
8614
9302
|
compositionSchemasLoading: boolean;
|
|
8615
9303
|
compositionSchemasError: string | null;
|
|
8616
9304
|
schemaLoadStartedAt: number | null;
|
|
8617
|
-
chatContainerHeight: number;
|
|
9305
|
+
chatContainerHeight: number; // 0 = fill available; >0 = user-resized fixed px
|
|
8618
9306
|
panelPreviewCompileError: string | null;
|
|
8619
9307
|
};
|
|
8620
9308
|
|
|
8621
9309
|
let widgetPanelSnapshots: Record<string, WidgetPanelSnapshot> = {};
|
|
8622
9310
|
/** Bumped on every panel snapshot write so {#each} panels re-read display state. */
|
|
8623
9311
|
let panelDisplayRevision = 0;
|
|
9312
|
+
/** Bumped on rename so shape chrome / host title re-read widget names. */
|
|
9313
|
+
let widgetNamesRevision = 0;
|
|
8624
9314
|
const schemaLoadInFlight = new Set<string>();
|
|
8625
9315
|
const schemaLoadPromises = new Map<string, Promise<void>>();
|
|
8626
9316
|
const compositionsLoadInFlight = new Set<string>();
|
|
@@ -8716,7 +9406,7 @@ Requirements:
|
|
|
8716
9406
|
compositionSchemasLoading,
|
|
8717
9407
|
compositionSchemasError,
|
|
8718
9408
|
schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
|
|
8719
|
-
chatContainerHeight: focusedSnap?.chatContainerHeight ??
|
|
9409
|
+
chatContainerHeight: focusedSnap?.chatContainerHeight ?? 0,
|
|
8720
9410
|
panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
|
|
8721
9411
|
};
|
|
8722
9412
|
}
|
|
@@ -8782,7 +9472,7 @@ Requirements:
|
|
|
8782
9472
|
compositionSchemasLoading: false,
|
|
8783
9473
|
compositionSchemasError: null,
|
|
8784
9474
|
schemaLoadStartedAt: null,
|
|
8785
|
-
chatContainerHeight:
|
|
9475
|
+
chatContainerHeight: 0,
|
|
8786
9476
|
panelPreviewCompileError: null,
|
|
8787
9477
|
};
|
|
8788
9478
|
}
|
|
@@ -8838,7 +9528,7 @@ Requirements:
|
|
|
8838
9528
|
: livePublishMatchesPanel
|
|
8839
9529
|
? live.publishHistoryResolvedForWidgetId
|
|
8840
9530
|
: snap.publishHistoryResolvedForWidgetId,
|
|
8841
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
9531
|
+
chatContainerHeight: snap.chatContainerHeight ?? 0,
|
|
8842
9532
|
compositionDesignSchema: resolvedDesignSchema,
|
|
8843
9533
|
compositionContentSchema: resolvedContentSchema,
|
|
8844
9534
|
compositionSchemasLoading: schemaLoading,
|
|
@@ -8856,7 +9546,7 @@ Requirements:
|
|
|
8856
9546
|
...snap,
|
|
8857
9547
|
lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
|
|
8858
9548
|
hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
|
|
8859
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
9549
|
+
chatContainerHeight: snap.chatContainerHeight ?? 0,
|
|
8860
9550
|
compositionDesignSchema: resolvedDesignSchema,
|
|
8861
9551
|
compositionContentSchema: resolvedContentSchema,
|
|
8862
9552
|
compositionSchemasError: resolvedSchemaError,
|
|
@@ -9017,27 +9707,226 @@ Requirements:
|
|
|
9017
9707
|
|
|
9018
9708
|
/**
|
|
9019
9709
|
* Live preview URL for a canvas WidgetShape overlay.
|
|
9020
|
-
*
|
|
9021
|
-
*
|
|
9710
|
+
* Source: 'live' | 'preview' | 'v:{N}' (historical published version).
|
|
9711
|
+
* Publish history is loaded without requiring chat to be open.
|
|
9022
9712
|
*/
|
|
9713
|
+
let shapePreviewSourceByWidgetId: Record<string, string> = {};
|
|
9714
|
+
let shapePublishHistoryByWidgetId: Record<string, any[]> = {};
|
|
9715
|
+
let shapeHistoryLoadInFlight: Record<string, boolean> = {};
|
|
9716
|
+
|
|
9717
|
+
async function ensureShapePublishHistory(widgetId: string): Promise<void> {
|
|
9718
|
+
if (!widgetId || shapeHistoryLoadInFlight[widgetId]) return;
|
|
9719
|
+
const cached = shapePublishHistoryByWidgetId[widgetId];
|
|
9720
|
+
if (cached && cached.length > 0) return;
|
|
9721
|
+
const fromPanel = getPanelDisplayState(widgetId).publishHistory;
|
|
9722
|
+
if (fromPanel && fromPanel.length > 0) {
|
|
9723
|
+
shapePublishHistoryByWidgetId = {
|
|
9724
|
+
...shapePublishHistoryByWidgetId,
|
|
9725
|
+
[widgetId]: [...fromPanel],
|
|
9726
|
+
};
|
|
9727
|
+
return;
|
|
9728
|
+
}
|
|
9729
|
+
shapeHistoryLoadInFlight = { ...shapeHistoryLoadInFlight, [widgetId]: true };
|
|
9730
|
+
try {
|
|
9731
|
+
await loadPublishHistoryForPanel(widgetId, { silent: true });
|
|
9732
|
+
const history = getPanelDisplayState(widgetId).publishHistory || [];
|
|
9733
|
+
shapePublishHistoryByWidgetId = {
|
|
9734
|
+
...shapePublishHistoryByWidgetId,
|
|
9735
|
+
[widgetId]: [...history],
|
|
9736
|
+
};
|
|
9737
|
+
syncWidgetShapeLivePreviews();
|
|
9738
|
+
} finally {
|
|
9739
|
+
const { [widgetId]: _, ...rest } = shapeHistoryLoadInFlight;
|
|
9740
|
+
shapeHistoryLoadInFlight = rest;
|
|
9741
|
+
}
|
|
9742
|
+
}
|
|
9743
|
+
|
|
9744
|
+
function getShapePublishHistory(widgetId: string): any[] {
|
|
9745
|
+
const cached = shapePublishHistoryByWidgetId[widgetId];
|
|
9746
|
+
if (cached?.length) return cached;
|
|
9747
|
+
const fromPanel = getPanelDisplayState(widgetId).publishHistory;
|
|
9748
|
+
return fromPanel?.length ? fromPanel : [];
|
|
9749
|
+
}
|
|
9750
|
+
|
|
9023
9751
|
function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
|
|
9024
9752
|
if (!widgetId) return null;
|
|
9753
|
+
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
9754
|
+
const ps = getPanelDisplayState(widgetId);
|
|
9755
|
+
const hasCode = !!(ps.lastCommitId || loadCommitIdForWidget(widgetId));
|
|
9756
|
+
const workerUrl = hasCode
|
|
9757
|
+
? `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`
|
|
9758
|
+
: null;
|
|
9759
|
+
|
|
9760
|
+
if (source.startsWith('v:')) {
|
|
9761
|
+
const version = parseInt(source.slice(2), 10);
|
|
9762
|
+
if (Number.isFinite(version)) {
|
|
9763
|
+
const record = getShapePublishHistory(widgetId).find(
|
|
9764
|
+
(row) => Number(row.version) === version,
|
|
9765
|
+
);
|
|
9766
|
+
const artifact =
|
|
9767
|
+
record?.artifactUrl ||
|
|
9768
|
+
record?.artifact_url ||
|
|
9769
|
+
(version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
|
|
9770
|
+
const url = resolvePublishedWidgetUrl(widgetId, version, artifact ?? null);
|
|
9771
|
+
if (url) return url;
|
|
9772
|
+
}
|
|
9773
|
+
}
|
|
9774
|
+
|
|
9775
|
+
// After codegen (unpublished), prefer Worker preview over the Live CDN artifact.
|
|
9776
|
+
if (source === 'preview' || ps.pendingPublishAfterCodegen) {
|
|
9777
|
+
if (workerUrl) return workerUrl;
|
|
9778
|
+
}
|
|
9779
|
+
|
|
9025
9780
|
const published = getPanelEditPreviewUrl(widgetId);
|
|
9026
|
-
if (published) return published;
|
|
9781
|
+
if (published && source !== 'preview') return published;
|
|
9782
|
+
return workerUrl;
|
|
9783
|
+
}
|
|
9027
9784
|
|
|
9028
|
-
|
|
9029
|
-
|
|
9785
|
+
function resolveWidgetShapeVersionLabel(widgetId: string): string | null {
|
|
9786
|
+
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
9030
9787
|
const ps = getPanelDisplayState(widgetId);
|
|
9031
|
-
const
|
|
9032
|
-
|
|
9788
|
+
const liveVersion =
|
|
9789
|
+
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
9790
|
+
if (source.startsWith('v:')) {
|
|
9791
|
+
const version = source.slice(2);
|
|
9792
|
+
return `v${version}`;
|
|
9793
|
+
}
|
|
9794
|
+
return liveVersion != null ? `v${liveVersion}` : null;
|
|
9795
|
+
}
|
|
9796
|
+
|
|
9797
|
+
function formatPublishHistoryDate(record: {
|
|
9798
|
+
publishedAt?: string;
|
|
9799
|
+
published_at?: string;
|
|
9800
|
+
}): string {
|
|
9801
|
+
const d = new Date(record.publishedAt ?? record.published_at ?? '');
|
|
9802
|
+
if (isNaN(d.getTime())) return '—';
|
|
9803
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
9804
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
9805
|
+
const timePart = d.toLocaleTimeString(undefined, {
|
|
9806
|
+
hour: '2-digit',
|
|
9807
|
+
minute: '2-digit',
|
|
9808
|
+
hour12: false,
|
|
9809
|
+
});
|
|
9810
|
+
return `${month}/${day}/${d.getFullYear()} ${timePart}`;
|
|
9811
|
+
}
|
|
9812
|
+
|
|
9813
|
+
function resolveWidgetShapeVersionOptions(
|
|
9814
|
+
widgetId: string,
|
|
9815
|
+
): Array<{
|
|
9816
|
+
id: string;
|
|
9817
|
+
label: string;
|
|
9818
|
+
shortLabel?: string;
|
|
9819
|
+
dateLabel?: string;
|
|
9820
|
+
isLive?: boolean;
|
|
9821
|
+
}> | null {
|
|
9822
|
+
const ps = getPanelDisplayState(widgetId);
|
|
9823
|
+
const liveVersion =
|
|
9824
|
+
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
9825
|
+
const history = [...getShapePublishHistory(widgetId)].sort(
|
|
9826
|
+
(a, b) => Number(b.version) - Number(a.version),
|
|
9827
|
+
);
|
|
9828
|
+
const options: Array<{
|
|
9829
|
+
id: string;
|
|
9830
|
+
label: string;
|
|
9831
|
+
shortLabel?: string;
|
|
9832
|
+
dateLabel?: string;
|
|
9833
|
+
isLive?: boolean;
|
|
9834
|
+
}> = [];
|
|
9835
|
+
for (const record of history) {
|
|
9836
|
+
const version = Number(record.version);
|
|
9837
|
+
if (!Number.isFinite(version)) continue;
|
|
9838
|
+
options.push({
|
|
9839
|
+
id: `v:${version}`,
|
|
9840
|
+
label: `Version ${version}`,
|
|
9841
|
+
shortLabel: `v${version}`,
|
|
9842
|
+
dateLabel: formatPublishHistoryDate(record),
|
|
9843
|
+
isLive: version === liveVersion,
|
|
9844
|
+
});
|
|
9845
|
+
}
|
|
9846
|
+
if (options.length === 0 && liveVersion != null) {
|
|
9847
|
+
options.push({
|
|
9848
|
+
id: `v:${liveVersion}`,
|
|
9849
|
+
label: `Version ${liveVersion}`,
|
|
9850
|
+
shortLabel: `v${liveVersion}`,
|
|
9851
|
+
dateLabel: '—',
|
|
9852
|
+
isLive: true,
|
|
9853
|
+
});
|
|
9854
|
+
}
|
|
9855
|
+
// Kick off history fetch so the dropdown fills even before chat opens
|
|
9856
|
+
if (history.length === 0 && liveVersion != null) {
|
|
9857
|
+
void ensureShapePublishHistory(widgetId);
|
|
9858
|
+
}
|
|
9859
|
+
return options.length > 0 ? options : null;
|
|
9860
|
+
}
|
|
9861
|
+
|
|
9862
|
+
function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
|
|
9863
|
+
const source = shapePreviewSourceByWidgetId[widgetId];
|
|
9864
|
+
if (source?.startsWith('v:')) return source;
|
|
9865
|
+
const ps = getPanelDisplayState(widgetId);
|
|
9866
|
+
const liveVersion =
|
|
9867
|
+
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
9868
|
+
return liveVersion != null ? `v:${liveVersion}` : null;
|
|
9869
|
+
}
|
|
9870
|
+
|
|
9871
|
+
function handleWidgetShapeVersionChange(widgetId: string, versionId: string): void {
|
|
9872
|
+
if (!versionId) return;
|
|
9873
|
+
shapePreviewSourceByWidgetId = {
|
|
9874
|
+
...shapePreviewSourceByWidgetId,
|
|
9875
|
+
[widgetId]: versionId,
|
|
9876
|
+
};
|
|
9877
|
+
syncWidgetShapeLivePreviews();
|
|
9878
|
+
panelDisplayRevision += 1;
|
|
9879
|
+
console.log('[WidgetShape] Preview source →', versionId, widgetId.substring(0, 8));
|
|
9880
|
+
}
|
|
9033
9881
|
|
|
9034
|
-
|
|
9882
|
+
/** Commit SHA for the version currently selected in the version menu (drives Head-at-commit badge). */
|
|
9883
|
+
function resolveViewingCommitShaForPanel(widgetId: string): string | null {
|
|
9884
|
+
const ps = getPanelDisplayState(widgetId);
|
|
9885
|
+
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
9886
|
+
if (source === 'preview') {
|
|
9887
|
+
return normalizeCommitSha(ps.lastCommitId);
|
|
9888
|
+
}
|
|
9889
|
+
let version: number | null = null;
|
|
9890
|
+
if (source.startsWith('v:')) {
|
|
9891
|
+
const parsed = parseInt(source.slice(2), 10);
|
|
9892
|
+
version = Number.isFinite(parsed) ? parsed : null;
|
|
9893
|
+
} else {
|
|
9894
|
+
version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
9895
|
+
}
|
|
9896
|
+
if (version == null) return normalizeCommitSha(ps.lastCommitId);
|
|
9897
|
+
const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
|
|
9898
|
+
const record = history.find((r) => Number(r.version) === Number(version));
|
|
9899
|
+
const fromHistory = normalizeCommitSha(record?.commitSha ?? record?.commit_sha);
|
|
9900
|
+
if (fromHistory) return fromHistory;
|
|
9901
|
+
// Live version with no history row yet — fall back to published SHA, then HEAD.
|
|
9902
|
+
if (Number(version) === Number(ps.lastPublishedVersion)) {
|
|
9903
|
+
return (
|
|
9904
|
+
normalizeCommitSha(getLivePublishedCommitShaForPanel(widgetId))
|
|
9905
|
+
?? normalizeCommitSha(ps.lastCommitId)
|
|
9906
|
+
);
|
|
9907
|
+
}
|
|
9908
|
+
return normalizeCommitSha(ps.lastCommitId);
|
|
9035
9909
|
}
|
|
9036
9910
|
|
|
9037
9911
|
function syncWidgetShapeLivePreviews(): void {
|
|
9038
9912
|
try {
|
|
9039
9913
|
canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
|
|
9040
9914
|
widgetShapeLivePreviewsRef?.sync?.();
|
|
9915
|
+
// Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
|
|
9916
|
+
const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
|
|
9917
|
+
const liveWidgetIds = new Set<string>();
|
|
9918
|
+
for (const rect of rects) {
|
|
9919
|
+
if (!rect?.widgetId) continue;
|
|
9920
|
+
if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
|
|
9921
|
+
liveWidgetIds.add(rect.widgetId);
|
|
9922
|
+
canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
|
|
9923
|
+
}
|
|
9924
|
+
const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
|
|
9925
|
+
for (const widgetId of linkedIds) {
|
|
9926
|
+
if (!liveWidgetIds.has(widgetId)) {
|
|
9927
|
+
canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9041
9930
|
} catch (err) {
|
|
9042
9931
|
console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
|
|
9043
9932
|
}
|
|
@@ -9380,6 +10269,9 @@ Requirements:
|
|
|
9380
10269
|
if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
|
|
9381
10270
|
if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
|
|
9382
10271
|
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
10272
|
+
// Any *other* draft panel still registered would keep a real widget panel
|
|
10273
|
+
// bound to a `draft_convert_*` context id — clear those too.
|
|
10274
|
+
purgeDraftConvertPanels();
|
|
9383
10275
|
// console.log(
|
|
9384
10276
|
// '[CreatorApp] Migrated Widget Details panel',
|
|
9385
10277
|
// fromId.substring(0, 16),
|
|
@@ -9975,6 +10867,8 @@ Requirements:
|
|
|
9975
10867
|
|
|
9976
10868
|
function hasUnpublishedChangesForPanel(widgetId: string): boolean {
|
|
9977
10869
|
const ps = getPanelDisplayState(widgetId);
|
|
10870
|
+
if (ps.pendingPublishAfterCodegen) return true;
|
|
10871
|
+
const pendingSha = loadPendingPublishShaForWidget(widgetId);
|
|
9978
10872
|
const headSha = resolvePublishHeadCommitId(widgetId, ps);
|
|
9979
10873
|
if (!headSha || ps.lastPublishedVersion === null) return false;
|
|
9980
10874
|
|
|
@@ -9982,6 +10876,7 @@ Requirements:
|
|
|
9982
10876
|
getLivePublishedCommitShaForPanel(widgetId)
|
|
9983
10877
|
?? getWidgetLastPublishCommitSha(widgetId);
|
|
9984
10878
|
if (!liveSha) return false;
|
|
10879
|
+
if (pendingSha && !commitsMatch(pendingSha, liveSha)) return true;
|
|
9985
10880
|
|
|
9986
10881
|
return !commitsMatch(headSha, liveSha);
|
|
9987
10882
|
}
|
|
@@ -10422,6 +11317,10 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
|
|
|
10422
11317
|
publishHistoryResolved: true,
|
|
10423
11318
|
publishHistoryResolvedForWidgetId: widgetId,
|
|
10424
11319
|
});
|
|
11320
|
+
shapePublishHistoryByWidgetId = {
|
|
11321
|
+
...shapePublishHistoryByWidgetId,
|
|
11322
|
+
[widgetId]: [...history],
|
|
11323
|
+
};
|
|
10425
11324
|
if (widgetId === focusedPanelWidgetId) {
|
|
10426
11325
|
publishHistory = history;
|
|
10427
11326
|
publishHistoryResolved = true;
|
|
@@ -10464,8 +11363,8 @@ async function loadPublishHistory(opts: { silent?: boolean } = {}) {
|
|
|
10464
11363
|
* Sets a historical version as the current live version without rebuilding.
|
|
10465
11364
|
* Updates widgets.js_path to point to the older version's artifact URL.
|
|
10466
11365
|
*/
|
|
10467
|
-
async function
|
|
10468
|
-
if (!
|
|
11366
|
+
async function setLiveVersionForWidget(widgetId: string, version: number) {
|
|
11367
|
+
if (!widgetId || !$userSession?.jwt) return;
|
|
10469
11368
|
|
|
10470
11369
|
isSettingLiveVersion = version;
|
|
10471
11370
|
if (!widgetsClient) {
|
|
@@ -10475,12 +11374,12 @@ async function setLiveVersion(version: number) {
|
|
|
10475
11374
|
}
|
|
10476
11375
|
try {
|
|
10477
11376
|
const liveRes = await widgetsClient.setWidgetPublishHistoryLive({
|
|
10478
|
-
widgetId
|
|
11377
|
+
widgetId,
|
|
10479
11378
|
version
|
|
10480
11379
|
});
|
|
10481
11380
|
const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string } | undefined;
|
|
10482
11381
|
const liveData = nested?.data ?? nested ?? {};
|
|
10483
|
-
const history = getPanelDisplayState(
|
|
11382
|
+
const history = getPanelDisplayState(widgetId).publishHistory;
|
|
10484
11383
|
const record = history.find((row) => row.version === version);
|
|
10485
11384
|
const commitSha = (liveData as { commitSha?: string; commit_sha?: string }).commitSha
|
|
10486
11385
|
|| (liveData as { commit_sha?: string }).commit_sha
|
|
@@ -10488,15 +11387,23 @@ async function setLiveVersion(version: number) {
|
|
|
10488
11387
|
const artifactUrl = (liveData as { artifactUrl?: string; artifact_url?: string }).artifactUrl
|
|
10489
11388
|
|| (liveData as { artifact_url?: string }).artifact_url
|
|
10490
11389
|
|| record?.artifactUrl || record?.artifact_url || null;
|
|
10491
|
-
applyPublishSuccessToWidgetState(
|
|
11390
|
+
applyPublishSuccessToWidgetState(widgetId, version, artifactUrl, commitSha);
|
|
11391
|
+
shapePreviewSourceByWidgetId = {
|
|
11392
|
+
...shapePreviewSourceByWidgetId,
|
|
11393
|
+
[widgetId]: `v:${version}`,
|
|
11394
|
+
};
|
|
11395
|
+
syncWidgetShapeLivePreviews();
|
|
11396
|
+
panelDisplayRevision += 1;
|
|
10492
11397
|
isHistoryOpen = false;
|
|
10493
|
-
const liveUrl = resolvePublishedWidgetUrl(
|
|
10494
|
-
const detailsRef = getWidgetDetailsRef(
|
|
11398
|
+
const liveUrl = resolvePublishedWidgetUrl(widgetId, version, artifactUrl);
|
|
11399
|
+
const detailsRef = getWidgetDetailsRef(widgetId);
|
|
10495
11400
|
detailsRef?.showPublishedArtifact?.(liveUrl);
|
|
10496
11401
|
detailsRef?.beginCompositionHydration?.();
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
11402
|
+
if (widgetId === selectedWidgetId || widgetId === focusedPanelWidgetId) {
|
|
11403
|
+
revealCompositionPreview();
|
|
11404
|
+
}
|
|
11405
|
+
showToast('success', `Live: v${version}`, {
|
|
11406
|
+
description: 'This version is now what visitors see on your sites.',
|
|
10500
11407
|
duration: 5000
|
|
10501
11408
|
});
|
|
10502
11409
|
} catch (err: any) {
|
|
@@ -10507,6 +11414,11 @@ async function setLiveVersion(version: number) {
|
|
|
10507
11414
|
}
|
|
10508
11415
|
}
|
|
10509
11416
|
|
|
11417
|
+
async function setLiveVersion(version: number) {
|
|
11418
|
+
if (!selectedWidgetId) return;
|
|
11419
|
+
await setLiveVersionForWidget(selectedWidgetId, version);
|
|
11420
|
+
}
|
|
11421
|
+
|
|
10510
11422
|
/** Toggle publish history panel — reloads data on every open */
|
|
10511
11423
|
function togglePublishHistory(triggerEl?: HTMLElement | null) {
|
|
10512
11424
|
isHistoryOpen = !isHistoryOpen;
|
|
@@ -11065,18 +11977,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11065
11977
|
}
|
|
11066
11978
|
}
|
|
11067
11979
|
|
|
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
|
-
}
|
|
11980
|
+
// Clicking a widget shape means "open this widget" — make it the canvas primary so a
|
|
11981
|
+
// refresh reopens it. The URL only carries canvas_id; widget_id is no longer written.
|
|
11982
|
+
if (embedded && widgetId !== canvasPrimaryWidgetId) {
|
|
11983
|
+
void persistPrimaryWidgetId(widgetId);
|
|
11080
11984
|
}
|
|
11081
11985
|
// Full select so commit/preview/header match the shape's linked widget (not a stale URL widget).
|
|
11082
11986
|
if (selectedWidgetId !== widgetId) {
|
|
@@ -11308,6 +12212,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11308
12212
|
}
|
|
11309
12213
|
notifyHostWidgetName({ id: widgetId, name: newName });
|
|
11310
12214
|
emitCanvasWidgetsList('widget-renamed');
|
|
12215
|
+
widgetNamesRevision += 1;
|
|
12216
|
+
panelDisplayRevision += 1;
|
|
12217
|
+
backfillWidgetShapeMetadata();
|
|
12218
|
+
syncWidgetShapeLivePreviews();
|
|
11311
12219
|
showToast('success', `Widget renamed to "${newName}"`);
|
|
11312
12220
|
} catch (error) {
|
|
11313
12221
|
console.error('[WidgetCreator] Failed to rename widget:', error);
|
|
@@ -11359,14 +12267,15 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11359
12267
|
}
|
|
11360
12268
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
11361
12269
|
await maybeAutoRestoreWidgetShapes();
|
|
11362
|
-
const
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
12270
|
+
const linkedIds =
|
|
12271
|
+
widgetIdsOnCanvas.length > 0
|
|
12272
|
+
? widgetIdsOnCanvas
|
|
12273
|
+
: [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
|
|
12274
|
+
const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
|
|
12275
|
+
applyPrimarySelectionOnCanvas(primaryId);
|
|
12276
|
+
requestAnimationFrame(() => {
|
|
12277
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
|
|
12278
|
+
});
|
|
11370
12279
|
syncWidgetShapeLivePreviews();
|
|
11371
12280
|
}
|
|
11372
12281
|
|
|
@@ -11641,14 +12550,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11641
12550
|
let autosaveSequence = 0;
|
|
11642
12551
|
let lastResolvedAutosaveSequence = 0;
|
|
11643
12552
|
|
|
11644
|
-
async function handleCanvasAutoSave(
|
|
12553
|
+
async function handleCanvasAutoSave(
|
|
12554
|
+
json: string | null,
|
|
12555
|
+
isUnmountSave = false,
|
|
12556
|
+
opts: { force?: boolean } = {},
|
|
12557
|
+
) {
|
|
11645
12558
|
if (!json) {
|
|
11646
12559
|
if (isUnmountSave) console.warn('[CanvasDebug] onBeforeUnmount: saveCanvas returned null — canvas not initialized');
|
|
11647
12560
|
return;
|
|
11648
12561
|
}
|
|
11649
12562
|
|
|
12563
|
+
const force = opts.force === true;
|
|
12564
|
+
|
|
11650
12565
|
// Guard: don't overwrite DB with an empty canvas if state hasn't loaded yet
|
|
11651
|
-
if (!isCanvasStateLoaded) {
|
|
12566
|
+
if (!force && !isCanvasStateLoaded) {
|
|
11652
12567
|
// console.log(
|
|
11653
12568
|
// '[CanvasDebug] Skipping autosave — canvas state not loaded yet (guard active, avoids overwrite with empty)'
|
|
11654
12569
|
// );
|
|
@@ -11656,7 +12571,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11656
12571
|
}
|
|
11657
12572
|
|
|
11658
12573
|
// Guard: skip autosaves triggered by loadCanvas() object:added events
|
|
11659
|
-
if (isLoadCooldownActive && !isUnmountSave) {
|
|
12574
|
+
if (!force && isLoadCooldownActive && !isUnmountSave) {
|
|
11660
12575
|
// console.log('[CanvasDebug] Skipping autosave — load cooldown active (prevents overwriting freshly loaded data)');
|
|
11661
12576
|
return;
|
|
11662
12577
|
}
|
|
@@ -11679,11 +12594,17 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11679
12594
|
// Unmount saves (HMR) are always blocked when going from N→0.
|
|
11680
12595
|
// Normal autosaves going from N→0 are allowed only if the drop is gradual
|
|
11681
12596
|
// (user deleted objects one by one) — a sudden jump from many to 0 is suspicious.
|
|
12597
|
+
// Explicit Save (force) still blocks sudden N→0 to avoid wiping the canvas.
|
|
11682
12598
|
if (objectCount === 0 && lastSavedCanvasObjectCount > 0) {
|
|
11683
12599
|
if (isUnmountSave || lastSavedCanvasObjectCount > 1) {
|
|
11684
12600
|
console.warn(
|
|
11685
|
-
`[CanvasDebug] BLOCKING save: would overwrite ${lastSavedCanvasObjectCount} shapes with 0 (isUnmount=${isUnmountSave})`
|
|
12601
|
+
`[CanvasDebug] BLOCKING save: would overwrite ${lastSavedCanvasObjectCount} shapes with 0 (isUnmount=${isUnmountSave}, force=${force})`
|
|
11686
12602
|
);
|
|
12603
|
+
if (force) {
|
|
12604
|
+
showToast('error', 'Save blocked', {
|
|
12605
|
+
description: 'Canvas snapshot looked empty — refresh and try again.',
|
|
12606
|
+
});
|
|
12607
|
+
}
|
|
11687
12608
|
return;
|
|
11688
12609
|
}
|
|
11689
12610
|
// lastSavedCanvasObjectCount === 1 → user deleted the last object, allow it
|
|
@@ -11720,12 +12641,25 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11720
12641
|
return;
|
|
11721
12642
|
}
|
|
11722
12643
|
lastResolvedAutosaveSequence = currentSeq;
|
|
12644
|
+
if (force) {
|
|
12645
|
+
showToast('success', 'Canvas saved', { duration: 2000 });
|
|
12646
|
+
}
|
|
11723
12647
|
// console.log(
|
|
11724
12648
|
// `[CanvasDebug] Canvas ${canvasId} saved to API: ${objectCount} shapes (seq ${currentSeq})`
|
|
11725
12649
|
// );
|
|
11726
12650
|
} catch (err) {
|
|
11727
12651
|
console.warn('[WidgetCreator] Failed to save canvas to API:', err);
|
|
12652
|
+
if (force) {
|
|
12653
|
+
showToast('error', 'Canvas save failed', {
|
|
12654
|
+
description: 'Kept a local copy — check your connection and retry.',
|
|
12655
|
+
});
|
|
12656
|
+
}
|
|
11728
12657
|
}
|
|
12658
|
+
} else if (force) {
|
|
12659
|
+
showToast('success', 'Canvas saved locally', {
|
|
12660
|
+
description: canvasId ? 'API client unavailable.' : 'No canvas id yet.',
|
|
12661
|
+
duration: 2500,
|
|
12662
|
+
});
|
|
11729
12663
|
}
|
|
11730
12664
|
|
|
11731
12665
|
refreshLinkedWidgetIds();
|
|
@@ -12069,6 +13003,68 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12069
13003
|
return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
|
|
12070
13004
|
}
|
|
12071
13005
|
|
|
13006
|
+
/**
|
|
13007
|
+
* True for conversations that exist only in the browser.
|
|
13008
|
+
*
|
|
13009
|
+
* Draft convert panels need a conversation object before the widget has a DB
|
|
13010
|
+
* row, so `handleCreateConversation` issues a `local_*` id. Message load/save
|
|
13011
|
+
* must treat those as local-only rather than querying the API with a key that
|
|
13012
|
+
* cannot exist there.
|
|
13013
|
+
*/
|
|
13014
|
+
function isLocalOnlyConversationId(conversationId: string | null | undefined): boolean {
|
|
13015
|
+
return !!conversationId && conversationId.startsWith('local_');
|
|
13016
|
+
}
|
|
13017
|
+
|
|
13018
|
+
/**
|
|
13019
|
+
* Drop synthetic draft ids from the panel registry.
|
|
13020
|
+
*
|
|
13021
|
+
* `migrateOpenPanelWidgetId` renames every map keyed by widget id, but the
|
|
13022
|
+
* `{#each openWidgetPanelIds}` block is keyed by `panelInstanceKeyByWidgetId`,
|
|
13023
|
+
* so a draft id that survives anywhere in that list can re-render a real
|
|
13024
|
+
* widget's panel while `context.id` still points at the draft. That panel then
|
|
13025
|
+
* asks the API for `draft_convert_*` conversations and schemas — 400s and
|
|
13026
|
+
* "Failed to initialize chat" for a widget whose history is perfectly fine.
|
|
13027
|
+
*
|
|
13028
|
+
* Draft panels are only ever opened through the convert flow, which re-creates
|
|
13029
|
+
* them from `draftConvertObjectIds`, so dropping them here is safe.
|
|
13030
|
+
*/
|
|
13031
|
+
function purgeDraftConvertPanels(): void {
|
|
13032
|
+
if (!openWidgetPanelIds.some((id) => isDraftConvertId(id))) return;
|
|
13033
|
+
|
|
13034
|
+
const draftIds = openWidgetPanelIds.filter((id) => isDraftConvertId(id));
|
|
13035
|
+
const remainingIds = openWidgetPanelIds.filter((id) => !isDraftConvertId(id));
|
|
13036
|
+
const nextInstanceKeys = { ...panelInstanceKeyByWidgetId };
|
|
13037
|
+
const nextZIndexes = { ...panelZIndexByWidgetId };
|
|
13038
|
+
const nextRefs = { ...widgetDetailsRefs };
|
|
13039
|
+
const nextChatMethods = { ...chatMethodsByWidgetId };
|
|
13040
|
+
for (const draftId of draftIds) {
|
|
13041
|
+
delete nextInstanceKeys[draftId];
|
|
13042
|
+
delete nextZIndexes[draftId];
|
|
13043
|
+
delete nextRefs[draftId];
|
|
13044
|
+
delete nextChatMethods[draftId];
|
|
13045
|
+
}
|
|
13046
|
+
panelInstanceKeyByWidgetId = nextInstanceKeys;
|
|
13047
|
+
panelZIndexByWidgetId = nextZIndexes;
|
|
13048
|
+
widgetDetailsRefs = nextRefs;
|
|
13049
|
+
chatMethodsByWidgetId = nextChatMethods;
|
|
13050
|
+
openWidgetPanelIds = remainingIds;
|
|
13051
|
+
|
|
13052
|
+
if (focusedPanelWidgetId && isDraftConvertId(focusedPanelWidgetId)) {
|
|
13053
|
+
focusedPanelWidgetId = remainingIds.at(-1) ?? null;
|
|
13054
|
+
}
|
|
13055
|
+
if (selectedWidgetId && isDraftConvertId(selectedWidgetId)) {
|
|
13056
|
+
selectedWidgetId = focusedPanelWidgetId;
|
|
13057
|
+
}
|
|
13058
|
+
if (detailsSessionWidgetId && isDraftConvertId(detailsSessionWidgetId)) {
|
|
13059
|
+
detailsSessionWidgetId = null;
|
|
13060
|
+
}
|
|
13061
|
+
if (widgetDetailsOpenTargetId && isDraftConvertId(widgetDetailsOpenTargetId)) {
|
|
13062
|
+
widgetDetailsOpenTargetId = null;
|
|
13063
|
+
}
|
|
13064
|
+
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
13065
|
+
console.warn('[CreatorApp] Purged draft convert panels:', draftIds.length);
|
|
13066
|
+
}
|
|
13067
|
+
|
|
12072
13068
|
function isConvertOrDraftPanelOpen(): boolean {
|
|
12073
13069
|
return (
|
|
12074
13070
|
isDraftConvertId(focusedPanelWidgetId)
|
|
@@ -12507,13 +13503,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12507
13503
|
<!-- Right: Debug toggle + Auth info (2 lines) + Logout -->
|
|
12508
13504
|
<div class="wc-top-bar-right flex items-center gap-3 shrink-0">
|
|
12509
13505
|
{#if debugModeAvailable}
|
|
13506
|
+
<HoverTooltip text={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}>
|
|
12510
13507
|
<button
|
|
12511
13508
|
onclick={toggleDebugMode}
|
|
12512
13509
|
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
13510
|
>
|
|
12515
13511
|
🐛 Debug {debugMode ? 'ON' : 'OFF'}
|
|
12516
13512
|
</button>
|
|
13513
|
+
</HoverTooltip>
|
|
12517
13514
|
{/if}
|
|
12518
13515
|
<div class="wc-auth-info flex flex-col gap-px items-start">
|
|
12519
13516
|
<div class="wc-auth-line1 flex items-center gap-2">
|
|
@@ -12598,10 +13595,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12598
13595
|
onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
|
|
12599
13596
|
onWidgetShapeDeleting={handleWidgetShapeDeleting}
|
|
12600
13597
|
onWidgetRename={handleWidgetRename}
|
|
12601
|
-
onRefreshScreenshot={
|
|
13598
|
+
onRefreshScreenshot={null}
|
|
12602
13599
|
onSetPrimaryWidget={handleSetPrimaryWidget}
|
|
12603
13600
|
onAfterRender={syncWidgetShapeLivePreviews}
|
|
12604
13601
|
getWidgetName={getWidgetNameById}
|
|
13602
|
+
widgetNameRevision={widgetNamesRevision}
|
|
12605
13603
|
isWidgetKnown={isWidgetKnownById}
|
|
12606
13604
|
onNonWidgetSelected={() => {
|
|
12607
13605
|
if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
|
|
@@ -12630,17 +13628,37 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12630
13628
|
bind:this={widgetShapeLivePreviewsRef}
|
|
12631
13629
|
getRects={() => canvasRef?.listWidgetShapeScreenRects?.() ?? []}
|
|
12632
13630
|
resolveUrl={resolveWidgetShapeLivePreviewUrl}
|
|
13631
|
+
resolveTitle={getWidgetNameById}
|
|
13632
|
+
titleRevision={widgetNamesRevision + panelDisplayRevision}
|
|
13633
|
+
resolveVersionLabel={resolveWidgetShapeVersionLabel}
|
|
13634
|
+
resolveVersionOptions={resolveWidgetShapeVersionOptions}
|
|
13635
|
+
resolveVersionSelectedId={resolveWidgetShapeVersionSelectedId}
|
|
13636
|
+
onVersionChange={handleWidgetShapeVersionChange}
|
|
13637
|
+
onSetLive={(widgetId, version) => {
|
|
13638
|
+
void setLiveVersionForWidget(widgetId, version);
|
|
13639
|
+
}}
|
|
13640
|
+
settingLiveVersion={isSettingLiveVersion}
|
|
13641
|
+
resolveCanPublish={(widgetId) => hasUnpublishedChangesForPanel(widgetId)}
|
|
13642
|
+
resolveIsPublishing={(widgetId) => isPublishing && publishingWidgetId === widgetId}
|
|
13643
|
+
onPublish={(widgetId) => runPublishForPanel(widgetId)}
|
|
12633
13644
|
selectedWidgetId={canvasSelectedWidgetShapeId}
|
|
12634
13645
|
onTranslate={(widgetId, dx, dy) => {
|
|
12635
13646
|
canvasRef?.translateWidgetShapeByScreen?.(widgetId, dx, dy);
|
|
12636
13647
|
}}
|
|
13648
|
+
onTranslateEnd={(widgetId) => {
|
|
13649
|
+
canvasRef?.commitWidgetShapeTranslate?.(widgetId);
|
|
13650
|
+
}}
|
|
12637
13651
|
onSelectShape={(widgetId) => {
|
|
12638
13652
|
canvasRef?.selectWidgetShapeById?.(widgetId);
|
|
12639
13653
|
canvasSelectedWidgetShapeId = widgetId;
|
|
13654
|
+
void ensureShapePublishHistory(widgetId);
|
|
12640
13655
|
}}
|
|
12641
13656
|
onOpenChat={(widgetId) => {
|
|
12642
13657
|
void openWidgetDetailsPanel(widgetId);
|
|
12643
13658
|
}}
|
|
13659
|
+
onRefresh={(widgetId) => {
|
|
13660
|
+
console.log('[WidgetShape] Iframe refreshed', widgetId.substring(0, 8));
|
|
13661
|
+
}}
|
|
12644
13662
|
/>
|
|
12645
13663
|
|
|
12646
13664
|
<!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
|
|
@@ -13236,9 +14254,32 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13236
14254
|
hasAutoOpenedPreview = detail.hasAutoOpenedPreview;
|
|
13237
14255
|
}}
|
|
13238
14256
|
>
|
|
14257
|
+
<svelte:fragment slot="header-meta">
|
|
14258
|
+
{#if saveStatus === 'saving' && isPanelFocused}
|
|
14259
|
+
<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>
|
|
14260
|
+
{:else if saveStatus === 'saved' && isPanelFocused}
|
|
14261
|
+
<span class="wc-header-save-status text-[10px] px-1.5 py-0.5 rounded bg-green-100 text-green-700">Saved</span>
|
|
14262
|
+
{:else if saveStatus === 'error' && isPanelFocused}
|
|
14263
|
+
<span class="wc-header-save-status text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-700">Save Error</span>
|
|
14264
|
+
{/if}
|
|
14265
|
+
{#if debugMode}
|
|
14266
|
+
<span class="wc-header-commit text-[10px] text-gray-500 whitespace-nowrap">
|
|
14267
|
+
Commit:
|
|
14268
|
+
<span class="font-mono font-medium text-gray-600" title={ps.lastCommitId || ''}>
|
|
14269
|
+
{ps.lastCommitId ? ps.lastCommitId.substring(0, 8) : '—'}
|
|
14270
|
+
</span>
|
|
14271
|
+
</span>
|
|
14272
|
+
{/if}
|
|
14273
|
+
</svelte:fragment>
|
|
13239
14274
|
<svelte:fragment slot="version-history">
|
|
13240
14275
|
{#if ps.lastPublishedVersion !== null}
|
|
13241
|
-
|
|
14276
|
+
{@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || `v:${ps.lastPublishedVersion}`}
|
|
14277
|
+
{@const viewingVersion = shapeSource.startsWith('v:')
|
|
14278
|
+
? parseInt(shapeSource.slice(2), 10)
|
|
14279
|
+
: ps.lastPublishedVersion}
|
|
14280
|
+
{@const viewingIsLive = Number(viewingVersion) === Number(ps.lastPublishedVersion)}
|
|
14281
|
+
<div class="wc-version-history-ct relative shrink-0 flex items-center gap-0.5">
|
|
14282
|
+
<HoverTooltip text="Live is the version visitors see on your website. Open the list to preview another version, or Set live to publish it.">
|
|
13242
14283
|
<button
|
|
13243
14284
|
type="button"
|
|
13244
14285
|
onclick={(e) => {
|
|
@@ -13246,11 +14287,48 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13246
14287
|
togglePublishHistory(e.currentTarget);
|
|
13247
14288
|
}}
|
|
13248
14289
|
onmousedown={(e) => e.stopPropagation()}
|
|
13249
|
-
class="wc-version-history-toggle flex items-center gap-
|
|
14290
|
+
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
14291
|
>
|
|
13251
|
-
|
|
14292
|
+
v{Number.isFinite(viewingVersion) ? viewingVersion : ps.lastPublishedVersion}
|
|
13252
14293
|
<span class="text-emerald-500">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
|
|
13253
14294
|
</button>
|
|
14295
|
+
</HoverTooltip>
|
|
14296
|
+
{#if panelHasUnpublishedChanges}
|
|
14297
|
+
<button
|
|
14298
|
+
type="button"
|
|
14299
|
+
onclick={(e) => {
|
|
14300
|
+
e.stopPropagation();
|
|
14301
|
+
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
14302
|
+
runPublishForPanel(panelWidgetId);
|
|
14303
|
+
}}
|
|
14304
|
+
disabled={isPublishDisabledForPanel(panelWidgetId) || (isPublishing && publishingWidgetId === panelWidgetId)}
|
|
14305
|
+
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? 'Publish the latest generated code as the Live version'}
|
|
14306
|
+
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"
|
|
14307
|
+
>
|
|
14308
|
+
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14309
|
+
</button>
|
|
14310
|
+
{:else if viewingIsLive}
|
|
14311
|
+
<span
|
|
14312
|
+
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"
|
|
14313
|
+
title="Live — generate new code to Publish a new version"
|
|
14314
|
+
>Live</span>
|
|
14315
|
+
{:else}
|
|
14316
|
+
<button
|
|
14317
|
+
type="button"
|
|
14318
|
+
onclick={(e) => {
|
|
14319
|
+
e.stopPropagation();
|
|
14320
|
+
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
14321
|
+
if (Number.isFinite(viewingVersion)) {
|
|
14322
|
+
void setLiveVersionForWidget(panelWidgetId, Number(viewingVersion));
|
|
14323
|
+
}
|
|
14324
|
+
}}
|
|
14325
|
+
disabled={isSettingLiveVersion === Number(viewingVersion)}
|
|
14326
|
+
title="Publish this version so visitors see it on your website"
|
|
14327
|
+
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"
|
|
14328
|
+
>
|
|
14329
|
+
{isSettingLiveVersion === Number(viewingVersion) ? '...' : 'Set live'}
|
|
14330
|
+
</button>
|
|
14331
|
+
{/if}
|
|
13254
14332
|
{#if isHistoryOpen && isPanelFocused}
|
|
13255
14333
|
<Portal target="wc-live-history-portal-{panelWidgetId}">
|
|
13256
14334
|
<button
|
|
@@ -13269,38 +14347,49 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13269
14347
|
<p class="text-xs text-emerald-400 text-center py-2">No history records yet. Publish to start tracking.</p>
|
|
13270
14348
|
{:else}
|
|
13271
14349
|
{#each ps.publishHistory as record (record.id)}
|
|
13272
|
-
|
|
13273
|
-
|
|
14350
|
+
{@const rowIsViewing = Number(record.version) === Number(viewingVersion)}
|
|
14351
|
+
{@const rowIsLive = Number(record.version) === Number(ps.lastPublishedVersion)}
|
|
14352
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
14353
|
+
<div
|
|
14354
|
+
class="publish-history-row flex items-center gap-2 rounded border px-2 py-1.5 w-max cursor-pointer
|
|
14355
|
+
{rowIsViewing ? 'border-emerald-400 bg-emerald-50' : 'border-emerald-100 bg-white hover:bg-emerald-50/60'}"
|
|
14356
|
+
role="option"
|
|
14357
|
+
aria-selected={rowIsViewing}
|
|
14358
|
+
tabindex="0"
|
|
14359
|
+
onclick={() => {
|
|
14360
|
+
handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
|
|
14361
|
+
}}
|
|
14362
|
+
onkeydown={(e) => {
|
|
14363
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
14364
|
+
e.preventDefault();
|
|
14365
|
+
handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
|
|
14366
|
+
}
|
|
14367
|
+
}}
|
|
14368
|
+
>
|
|
14369
|
+
<span class="publish-history-version-label text-xs font-mono font-bold text-emerald-700 shrink-0">v{record.version}</span>
|
|
13274
14370
|
{#if debugMode && (record.commitSha || record.commit_sha)}
|
|
13275
14371
|
<span class="text-xs font-mono text-slate-400 shrink-0" title={record.commitSha ?? record.commit_sha}>
|
|
13276
14372
|
{(record.commitSha ?? record.commit_sha).slice(0, 7)}
|
|
13277
14373
|
</span>
|
|
13278
14374
|
{/if}
|
|
13279
14375
|
<span class="wc-live-history-date text-xs text-slate-400 shrink-0 whitespace-nowrap tabular-nums" style="padding-right: 4px;">
|
|
13280
|
-
{(
|
|
13281
|
-
const d = new Date(record.publishedAt ?? record.published_at ?? '');
|
|
13282
|
-
if (isNaN(d.getTime())) return '—';
|
|
13283
|
-
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
13284
|
-
const day = String(d.getDate()).padStart(2, '0');
|
|
13285
|
-
const timePart = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
13286
|
-
return `${month}/${day}/${d.getFullYear()} ${timePart}`;
|
|
13287
|
-
})()}
|
|
14376
|
+
{formatPublishHistoryDate(record)}
|
|
13288
14377
|
</span>
|
|
13289
|
-
{#if
|
|
14378
|
+
{#if !rowIsLive}
|
|
13290
14379
|
<button
|
|
13291
|
-
|
|
14380
|
+
type="button"
|
|
14381
|
+
onclick={(e) => {
|
|
14382
|
+
e.stopPropagation();
|
|
14383
|
+
void setLiveVersionForWidget(panelWidgetId, record.version);
|
|
14384
|
+
}}
|
|
13292
14385
|
disabled={isSettingLiveVersion === record.version}
|
|
13293
14386
|
class="shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-xs 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 transition-colors"
|
|
13294
|
-
title="
|
|
14387
|
+
title="Publish this version so visitors see it on your website"
|
|
13295
14388
|
>
|
|
13296
14389
|
{isSettingLiveVersion === record.version ? '...' : 'Set live'}
|
|
13297
14390
|
</button>
|
|
13298
14391
|
{:else}
|
|
13299
|
-
<
|
|
13300
|
-
type="button"
|
|
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>
|
|
14392
|
+
<span 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">live</span>
|
|
13304
14393
|
{/if}
|
|
13305
14394
|
</div>
|
|
13306
14395
|
{/each}
|
|
@@ -13338,7 +14427,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13338
14427
|
</svelte:fragment>
|
|
13339
14428
|
|
|
13340
14429
|
<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;">
|
|
14430
|
+
<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
14431
|
<div class="flex items-center justify-between gap-2 min-w-0">
|
|
13343
14432
|
<div class="generate-code-status-el text-xs flex-1 min-w-0 truncate {ps.lastGenerationFailed ? 'text-red-600' : 'text-gray-500'}">
|
|
13344
14433
|
{#if ps.lastGenerationFailed}
|
|
@@ -13349,7 +14438,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13349
14438
|
</div>
|
|
13350
14439
|
</div>
|
|
13351
14440
|
{#if ps.lastGenerationFailed}
|
|
13352
|
-
<div class="p-3 bg-red-50 border border-red-200
|
|
14441
|
+
<div class="p-3 bg-red-50 border border-red-200" style="border-radius: 0;">
|
|
13353
14442
|
<p class="text-xs text-red-700 whitespace-pre-wrap">{ps.lastGenerationError}</p>
|
|
13354
14443
|
<div class="flex gap-2 mt-3 justify-center">
|
|
13355
14444
|
<Button
|
|
@@ -13387,20 +14476,22 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13387
14476
|
|
|
13388
14477
|
<svelte:fragment slot="code-generation">
|
|
13389
14478
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
13390
|
-
{@const chatUserHeight = (panelDisplayRevision,
|
|
14479
|
+
{@const chatUserHeight = (panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
|
|
13391
14480
|
<div
|
|
13392
14481
|
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={
|
|
14482
|
+
style="padding: 0;"
|
|
14483
|
+
use:nativeScrollbarPadding={0}
|
|
13395
14484
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
13396
14485
|
>
|
|
13397
14486
|
{#key panelInstanceKey}
|
|
13398
14487
|
<div
|
|
13399
|
-
class="code-generation-chat-host w-full min-h-0"
|
|
14488
|
+
class="code-generation-chat-host w-full min-h-0 {chatUserHeight == null ? 'flex-1' : 'shrink-0'}"
|
|
13400
14489
|
data-widget-chat-height={panelWidgetId}
|
|
13401
|
-
style=
|
|
14490
|
+
style={chatUserHeight == null
|
|
14491
|
+
? `flex: 1 1 auto; min-height: ${CHAT_MIN_HEIGHT}px;`
|
|
14492
|
+
: `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
|
|
13402
14493
|
>
|
|
13403
|
-
<Chat class="code-generation-chat w-full h-full"
|
|
14494
|
+
<Chat class="code-generation-chat w-full h-full rounded-none border-0"
|
|
13404
14495
|
style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
|
|
13405
14496
|
resizeExpandsContainer={true}
|
|
13406
14497
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
@@ -13428,7 +14519,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13428
14519
|
autoCodeDetection: false,
|
|
13429
14520
|
}
|
|
13430
14521
|
}}
|
|
13431
|
-
widgetHeadCommitSha={ps.lastCommitId}
|
|
14522
|
+
widgetHeadCommitSha={resolveViewingCommitShaForPanel(panelWidgetId) || ps.lastCommitId}
|
|
13432
14523
|
onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
|
|
13433
14524
|
onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
|
|
13434
14525
|
onUploadChatFile={handleChatUploadFile}
|
|
@@ -13441,7 +14532,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13441
14532
|
onSaveMessage={(conversationId, content, messageType, attachments) =>
|
|
13442
14533
|
handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
|
|
13443
14534
|
}
|
|
13444
|
-
onRestoreCheckpointBackend={
|
|
14535
|
+
onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
|
|
14536
|
+
handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
|
|
14537
|
+
}
|
|
13445
14538
|
onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
|
|
13446
14539
|
onDraftChange={() => handleChatDraftChange(panelWidgetId)}
|
|
13447
14540
|
onConversationChange={(conversationId) => handleChatConversationChange(conversationId, panelWidgetId)}
|
|
@@ -13503,7 +14596,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13503
14596
|
{#if isEditStepWithoutPublishForPanel(panelWidgetId)}
|
|
13504
14597
|
<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
14598
|
<strong class="font-semibold">Not published yet.</strong>
|
|
13506
|
-
Generate on Create
|
|
14599
|
+
Generate on Create, preview the result, then Publish when you are ready.
|
|
13507
14600
|
</div>
|
|
13508
14601
|
{/if}
|
|
13509
14602
|
|
|
@@ -13993,11 +15086,46 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13993
15086
|
Are you sure you want to delete <strong>"{deleteCanvasTarget?.name || ''}"</strong>? This action cannot be undone.
|
|
13994
15087
|
</Dialog.Description>
|
|
13995
15088
|
</Dialog.Header>
|
|
15089
|
+
|
|
15090
|
+
{#if deleteCanvasPreviewLoading}
|
|
15091
|
+
<p class="confirm-delete-canvas-preview-loading text-sm text-gray-500 pt-2">
|
|
15092
|
+
Checking what will be deleted…
|
|
15093
|
+
</p>
|
|
15094
|
+
{:else if deleteCanvasPreview && deleteCanvasPreview.widgetCount > 0}
|
|
15095
|
+
<div class="confirm-delete-canvas-preview pt-2">
|
|
15096
|
+
<p class="text-sm text-gray-700">
|
|
15097
|
+
This will also delete
|
|
15098
|
+
<strong>{deleteCanvasPreview.widgetCount}</strong>
|
|
15099
|
+
{deleteCanvasPreview.widgetCount === 1 ? 'widget' : 'widgets'} on this canvas:
|
|
15100
|
+
</p>
|
|
15101
|
+
<ul class="confirm-delete-canvas-preview-groups mt-2 space-y-1.5 max-h-40 overflow-y-auto scrollbar-thin text-sm">
|
|
15102
|
+
{#each deleteCanvasPreview.groups as group}
|
|
15103
|
+
<li class="confirm-delete-canvas-preview-group">
|
|
15104
|
+
<span class="font-medium text-gray-800">{group.projectName}</span>
|
|
15105
|
+
<span class="text-gray-500"> — {group.widgetNames.length}</span>
|
|
15106
|
+
<p class="text-gray-500 text-xs truncate" title={group.widgetNames.join(', ')}>
|
|
15107
|
+
{group.widgetNames.join(', ')}
|
|
15108
|
+
</p>
|
|
15109
|
+
</li>
|
|
15110
|
+
{/each}
|
|
15111
|
+
</ul>
|
|
15112
|
+
{#if deleteCanvasPreview.groups.length > 1}
|
|
15113
|
+
<p class="confirm-delete-canvas-preview-warning text-xs text-amber-700 mt-2">
|
|
15114
|
+
These widgets belong to more than one project.
|
|
15115
|
+
</p>
|
|
15116
|
+
{/if}
|
|
15117
|
+
</div>
|
|
15118
|
+
{/if}
|
|
15119
|
+
|
|
15120
|
+
{#if deleteCanvasError}
|
|
15121
|
+
<p class="confirm-delete-canvas-error text-sm text-red-600 pt-2">{deleteCanvasError}</p>
|
|
15122
|
+
{/if}
|
|
15123
|
+
|
|
13996
15124
|
<div class="confirm-delete-canvas-dialog-actions flex justify-end gap-2 pt-2">
|
|
13997
15125
|
<button
|
|
13998
15126
|
type="button"
|
|
13999
15127
|
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; }}
|
|
15128
|
+
onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; deleteCanvasError = ''; }}
|
|
14001
15129
|
>Cancel</button>
|
|
14002
15130
|
<button
|
|
14003
15131
|
type="button"
|