@widgetic/creator 0.3.58 → 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 +1473 -276
- package/dist/components/WidgetDetails.svelte +103 -72
- package/dist/components/WidgetDetails.svelte.d.ts +1 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +541 -0
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +64 -0
- 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;
|
|
@@ -447,6 +463,8 @@
|
|
|
447
463
|
*/
|
|
448
464
|
import SelectorDropdown from './components/SelectorDropdown.svelte';
|
|
449
465
|
import type { DropdownItem } from './components/SelectorDropdown.svelte';
|
|
466
|
+
import WidgetShapeLivePreviews from './components/WidgetShapeLivePreviews.svelte';
|
|
467
|
+
import HoverTooltip from './components/HoverTooltip.svelte';
|
|
450
468
|
|
|
451
469
|
// WIDGET DETAILS COMPONENT — lazy-loaded to reduce peak build memory
|
|
452
470
|
let WidgetDetails: any;
|
|
@@ -473,6 +491,10 @@
|
|
|
473
491
|
|
|
474
492
|
let WidgeticCanvas: any;
|
|
475
493
|
let canvasRef: any;
|
|
494
|
+
/** HTML iframe overlays over Fabric WidgetShapes (W3). */
|
|
495
|
+
let widgetShapeLivePreviewsRef: { sync?: () => void } | null = null;
|
|
496
|
+
/** Selected WidgetShape id for chrome highlight (from Fabric selection). */
|
|
497
|
+
let canvasSelectedWidgetShapeId: string | null = null;
|
|
476
498
|
let widgetDetailsModulePromise: Promise<any> | null = null;
|
|
477
499
|
|
|
478
500
|
function loadWidgetDetailsModule(): Promise<any> {
|
|
@@ -553,6 +575,18 @@
|
|
|
553
575
|
// Delete canvas dialog
|
|
554
576
|
let showDeleteCanvasDialog = false;
|
|
555
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 = '';
|
|
556
590
|
|
|
557
591
|
// Widgets state
|
|
558
592
|
interface WidgetSummary {
|
|
@@ -885,7 +919,10 @@
|
|
|
885
919
|
contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
|
|
886
920
|
source: options?.source || 'chat-upload',
|
|
887
921
|
widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
|
|
888
|
-
conversationId:
|
|
922
|
+
conversationId:
|
|
923
|
+
currentChatConversationId && !isLocalOnlyConversationId(currentChatConversationId)
|
|
924
|
+
? currentChatConversationId
|
|
925
|
+
: undefined,
|
|
889
926
|
},
|
|
890
927
|
{ onProgress: options?.onProgress },
|
|
891
928
|
);
|
|
@@ -1480,39 +1517,163 @@
|
|
|
1480
1517
|
// console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
|
|
1481
1518
|
}
|
|
1482
1519
|
|
|
1483
|
-
/**
|
|
1484
|
-
|
|
1520
|
+
/** Live WidgetShape iframe only — never the hidden panel preview column. */
|
|
1521
|
+
function resolveLiveWidgetShapeIframe(widgetId: string | null | undefined): HTMLIFrameElement | null {
|
|
1485
1522
|
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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);
|
|
1497
1543
|
}
|
|
1544
|
+
}
|
|
1498
1545
|
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
+
}
|
|
1501
1555
|
const timeout = setTimeout(() => {
|
|
1502
1556
|
window.removeEventListener('message', handler);
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
}, 8000);
|
|
1557
|
+
resolve({ data: null, error: 'timeout' });
|
|
1558
|
+
}, timeoutMs);
|
|
1506
1559
|
|
|
1507
1560
|
function handler(event: MessageEvent) {
|
|
1508
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;
|
|
1509
1564
|
clearTimeout(timeout);
|
|
1510
1565
|
window.removeEventListener('message', handler);
|
|
1511
|
-
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
|
+
});
|
|
1512
1570
|
}
|
|
1513
1571
|
window.addEventListener('message', handler);
|
|
1514
|
-
iframe.contentWindow
|
|
1572
|
+
iframe.contentWindow.postMessage({ type: 'widgetic:request-screenshot' }, '*');
|
|
1573
|
+
});
|
|
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),
|
|
1515
1675
|
});
|
|
1676
|
+
return null;
|
|
1516
1677
|
}
|
|
1517
1678
|
|
|
1518
1679
|
function normalizeConversationRecords(json: any): any[] {
|
|
@@ -1601,6 +1762,8 @@
|
|
|
1601
1762
|
/** Load messages for a conversation from backend */
|
|
1602
1763
|
async function handleLoadMessages(conversationId: string, widgetIdForSync?: string | null): Promise<any[]> {
|
|
1603
1764
|
try {
|
|
1765
|
+
// Client-only conversations (draft convert panels) have no DB row.
|
|
1766
|
+
if (isLocalOnlyConversationId(conversationId)) return [];
|
|
1604
1767
|
if (!messagesClient) {
|
|
1605
1768
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
1606
1769
|
return [];
|
|
@@ -1664,7 +1827,17 @@
|
|
|
1664
1827
|
try {
|
|
1665
1828
|
if (!conversationsClient) {
|
|
1666
1829
|
console.warn('[Chat→Backend] conversationsClient not initialized');
|
|
1667
|
-
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 };
|
|
1668
1841
|
}
|
|
1669
1842
|
const response = await conversationsClient.createConversationForWidgetRaw({
|
|
1670
1843
|
createConversationForWidgetRequest: {
|
|
@@ -1688,7 +1861,8 @@
|
|
|
1688
1861
|
return conversationId ? { ...conversation, id: conversationId } : conversation;
|
|
1689
1862
|
} catch (error) {
|
|
1690
1863
|
console.error('[Chat→Backend] Failed to create conversation:', error);
|
|
1691
|
-
|
|
1864
|
+
// Never invent a local UUID — saving messages against it yields 403 and stops codegen.
|
|
1865
|
+
return null;
|
|
1692
1866
|
}
|
|
1693
1867
|
}
|
|
1694
1868
|
|
|
@@ -1805,6 +1979,11 @@
|
|
|
1805
1979
|
persistWidgetId?: string | null,
|
|
1806
1980
|
): Promise<any> {
|
|
1807
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
|
+
}
|
|
1808
1987
|
if (!messagesClient) {
|
|
1809
1988
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
1810
1989
|
return null;
|
|
@@ -1860,7 +2039,11 @@
|
|
|
1860
2039
|
}
|
|
1861
2040
|
|
|
1862
2041
|
/** Restore conversation to a checkpoint message via backend (deletes messages + git rollback) */
|
|
1863
|
-
async function handleRestoreCheckpoint(
|
|
2042
|
+
async function handleRestoreCheckpoint(
|
|
2043
|
+
conversationId: string,
|
|
2044
|
+
checkpointMessageId: string,
|
|
2045
|
+
restoreWidgetId?: string | null,
|
|
2046
|
+
): Promise<any> {
|
|
1864
2047
|
try {
|
|
1865
2048
|
if (!messagesClient) {
|
|
1866
2049
|
console.warn('[Chat→Backend] messagesClient not initialized');
|
|
@@ -1877,16 +2060,22 @@
|
|
|
1877
2060
|
const data = json?.data || json;
|
|
1878
2061
|
// console.log('[Chat→Backend] Checkpoint restored:', data);
|
|
1879
2062
|
|
|
1880
|
-
// Refresh the widget preview after restore — the backend rebuilds the widget
|
|
1881
|
-
// from the reverted code, so we just need to reload the iframe.
|
|
1882
|
-
// Check both camelCase (after responseTransformMiddleware) and snake_case paths.
|
|
1883
2063
|
const gitRollback = data?.gitRollback || data?.git_rollback || data?.data?.gitRollback || data?.data?.git_rollback;
|
|
1884
2064
|
const undoMode = data?.undoMode ?? data?.undo_mode ?? false;
|
|
1885
2065
|
const targetCommit = gitRollback?.targetCommit ?? gitRollback?.target_commit;
|
|
1886
|
-
|
|
2066
|
+
const panelWidgetId =
|
|
2067
|
+
restoreWidgetId ||
|
|
2068
|
+
focusedPanelWidgetId ||
|
|
2069
|
+
selectedWidgetId ||
|
|
2070
|
+
null;
|
|
2071
|
+
// console.log('[Chat→Backend] Git rollback result:', gitRollback, { undoMode, targetCommit, panelWidgetId });
|
|
1887
2072
|
if (gitRollback?.success) {
|
|
1888
2073
|
if (targetCommit) {
|
|
1889
2074
|
lastCommitId = targetCommit;
|
|
2075
|
+
if (panelWidgetId) {
|
|
2076
|
+
patchPanelSnapshot(panelWidgetId, { lastCommitId: targetCommit });
|
|
2077
|
+
patchCodegenUiState(panelWidgetId, {});
|
|
2078
|
+
}
|
|
1890
2079
|
}
|
|
1891
2080
|
showToast('success', undoMode ? 'Change undone' : 'Checkpoint restored', {
|
|
1892
2081
|
description: undoMode
|
|
@@ -1900,20 +2089,27 @@
|
|
|
1900
2089
|
compositionDesignSchema = null;
|
|
1901
2090
|
compositionContentSchema = null;
|
|
1902
2091
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
1903
|
-
// Rollback commit may take a moment to be visible from the Worker's clone; retry
|
|
1904
|
-
// build-from-repo with escalating delays to survive GitLab propagation lag.
|
|
1905
2092
|
_isRestoringCheckpoint = true;
|
|
2093
|
+
const panelRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
|
|
1906
2094
|
const scheduleBuildRetry = async () => {
|
|
1907
2095
|
const delays = [2500, 5000, 8000];
|
|
1908
2096
|
for (let i = 0; i < delays.length; i++) {
|
|
1909
2097
|
await new Promise((r) => setTimeout(r, delays[i]));
|
|
1910
2098
|
if (i === 0) suppressNextBuildFromRepoChatMessage = true;
|
|
1911
2099
|
try {
|
|
1912
|
-
|
|
1913
|
-
|
|
2100
|
+
const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : panelRef;
|
|
2101
|
+
await activeRef?.triggerBuildFromRepo({ force: true });
|
|
2102
|
+
const err = activeRef?.getPreviewError?.() ?? null;
|
|
1914
2103
|
if (!err) {
|
|
1915
|
-
// console.log('[Restore] triggerBuildFromRepo succeeded on attempt', i + 1);
|
|
1916
2104
|
_isRestoringCheckpoint = false;
|
|
2105
|
+
if (panelWidgetId) {
|
|
2106
|
+
shapePreviewSourceByWidgetId = {
|
|
2107
|
+
...shapePreviewSourceByWidgetId,
|
|
2108
|
+
[panelWidgetId]: 'preview',
|
|
2109
|
+
};
|
|
2110
|
+
syncWidgetShapeLivePreviews();
|
|
2111
|
+
setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
|
|
2112
|
+
}
|
|
1917
2113
|
return;
|
|
1918
2114
|
}
|
|
1919
2115
|
console.warn('[Restore] triggerBuildFromRepo attempt', i + 1, 'has error:', err);
|
|
@@ -1936,14 +2132,16 @@
|
|
|
1936
2132
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
1937
2133
|
setTimeout(() => {
|
|
1938
2134
|
suppressNextBuildFromRepoChatMessage = true;
|
|
1939
|
-
|
|
2135
|
+
const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
|
|
2136
|
+
void activeRef?.triggerBuildFromRepo({ force: true });
|
|
1940
2137
|
}, 1500);
|
|
1941
2138
|
}
|
|
1942
|
-
|
|
1943
2139
|
return data;
|
|
1944
2140
|
} catch (error) {
|
|
1945
2141
|
console.error('[Chat→Backend] Failed to restore checkpoint:', error);
|
|
1946
|
-
|
|
2142
|
+
showToast('error', 'Failed to restore checkpoint', {
|
|
2143
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
2144
|
+
});
|
|
1947
2145
|
return null;
|
|
1948
2146
|
}
|
|
1949
2147
|
}
|
|
@@ -2118,14 +2316,15 @@
|
|
|
2118
2316
|
let editingCompositionName = '';
|
|
2119
2317
|
let deletingCompositionId: string | null = null;
|
|
2120
2318
|
|
|
2121
|
-
/** Explicit chat box height (px).
|
|
2122
|
-
const CHAT_DEFAULT_HEIGHT =
|
|
2123
|
-
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;
|
|
2124
2322
|
const CHAT_MAX_HEIGHT = 2000;
|
|
2125
2323
|
|
|
2126
|
-
function getChatContainerHeightForPanel(widgetId: string): number {
|
|
2324
|
+
function getChatContainerHeightForPanel(widgetId: string): number | null {
|
|
2127
2325
|
const stored = getPanelState(widgetId).chatContainerHeight;
|
|
2128
|
-
|
|
2326
|
+
// null/0 → fill available space (flex-grow). Explicit resize stores a px value.
|
|
2327
|
+
if (stored == null || stored <= 0) return null;
|
|
2129
2328
|
return Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, stored));
|
|
2130
2329
|
}
|
|
2131
2330
|
|
|
@@ -2133,9 +2332,12 @@
|
|
|
2133
2332
|
const measured = document
|
|
2134
2333
|
.querySelector(`[data-widget-chat-height="${widgetId}"]`)
|
|
2135
2334
|
?.getBoundingClientRect().height;
|
|
2136
|
-
const current =
|
|
2335
|
+
const current =
|
|
2336
|
+
measured && measured > 0
|
|
2337
|
+
? measured
|
|
2338
|
+
: (getChatContainerHeightForPanel(widgetId) ?? CHAT_DEFAULT_HEIGHT);
|
|
2137
2339
|
const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
|
|
2138
|
-
|
|
2340
|
+
console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
|
|
2139
2341
|
patchPanelSnapshot(widgetId, { chatContainerHeight: next });
|
|
2140
2342
|
}
|
|
2141
2343
|
|
|
@@ -2288,6 +2490,11 @@
|
|
|
2288
2490
|
}
|
|
2289
2491
|
|
|
2290
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();
|
|
2291
2498
|
const cached = widgetSchemaCache.get(widgetId);
|
|
2292
2499
|
if (cached?.design || cached?.content) {
|
|
2293
2500
|
applyWidgetSchemaCacheToSnapshot(widgetId);
|
|
@@ -2457,6 +2664,9 @@
|
|
|
2457
2664
|
}
|
|
2458
2665
|
|
|
2459
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;
|
|
2460
2670
|
schemaLoadInFlight.add(widgetId);
|
|
2461
2671
|
|
|
2462
2672
|
try {
|
|
@@ -2755,7 +2965,6 @@
|
|
|
2755
2965
|
// Only reuse js_path when it already points at this widget + this version.
|
|
2756
2966
|
// `/widgets/` alone used to win and keep Edit on the previous CDN version after publish.
|
|
2757
2967
|
if (!pointsAtOtherWidget && jsPathVersion === version) {
|
|
2758
|
-
console.log('[Publish] Using js_path artifact URL', resolved);
|
|
2759
2968
|
return resolved;
|
|
2760
2969
|
}
|
|
2761
2970
|
}
|
|
@@ -3878,63 +4087,162 @@
|
|
|
3878
4087
|
}
|
|
3879
4088
|
|
|
3880
4089
|
function handleHostSaveCanvasEvent(): void {
|
|
3881
|
-
|
|
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 });
|
|
3882
4098
|
}
|
|
3883
4099
|
|
|
3884
|
-
async function persistPrimaryWidgetId(widgetId: string) {
|
|
3885
|
-
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
|
+
}
|
|
3886
4130
|
canvasPrimaryWidgetId = widgetId;
|
|
4131
|
+
saveSelectedWidgetId(widgetId);
|
|
3887
4132
|
// console.log('[CreatorApp] Set primary widget:', widgetId);
|
|
3888
|
-
if (
|
|
4133
|
+
if (targetCanvasId && canvasesClient) {
|
|
3889
4134
|
try {
|
|
3890
4135
|
await canvasesClient.updateCanvas({
|
|
3891
|
-
id:
|
|
4136
|
+
id: targetCanvasId,
|
|
3892
4137
|
updateCanvasRequest: { primaryWidgetId: widgetId }
|
|
3893
4138
|
});
|
|
3894
4139
|
} catch (err) {
|
|
3895
4140
|
console.warn('[CreatorApp] Failed to save primary_widget_id:', err);
|
|
3896
4141
|
}
|
|
3897
4142
|
}
|
|
3898
|
-
|
|
3899
|
-
if (
|
|
4143
|
+
// Adopt orphans AND cross-canvas strays whose shape lives here (explicit user intent).
|
|
4144
|
+
if (targetCanvasId && widgetsClient && shouldAdoptToThisCanvas) {
|
|
3900
4145
|
try {
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
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
|
+
);
|
|
3906
4161
|
} catch (err) {
|
|
3907
|
-
console.warn('[CreatorApp] Failed to
|
|
4162
|
+
console.warn('[CreatorApp] Failed to heal widget canvas_id:', err);
|
|
3908
4163
|
}
|
|
3909
4164
|
}
|
|
4165
|
+
if (embedded) {
|
|
4166
|
+
syncUrlWidgetId(widgetId);
|
|
4167
|
+
}
|
|
3910
4168
|
if (widget) notifyHostWidgetName(widget);
|
|
3911
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;
|
|
3912
4178
|
}
|
|
3913
4179
|
|
|
3914
4180
|
function emitCanvasWidgetsList(reason: string) {
|
|
3915
4181
|
if (!embedded || typeof window === 'undefined') return;
|
|
4182
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
3916
4183
|
const ids = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
|
|
3917
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.
|
|
3918
4205
|
for (const id of ids) {
|
|
4206
|
+
if (!id || isDraftConvertId(id)) continue;
|
|
3919
4207
|
const widget = getWidgetById(id);
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
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) {
|
|
3928
4223
|
items.unshift({
|
|
3929
4224
|
id: primary.id,
|
|
3930
4225
|
name: (primary.name || '').trim() || 'Untitled Widget'
|
|
3931
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);
|
|
3932
4238
|
}
|
|
3933
4239
|
}
|
|
3934
|
-
|
|
4240
|
+
if (!primaryWidgetId && items[0]?.id) {
|
|
4241
|
+
primaryWidgetId = items[0].id;
|
|
4242
|
+
}
|
|
3935
4243
|
// console.log('[CreatorApp] Emit canvas widgets for header', reason, items.length, primaryWidgetId);
|
|
3936
4244
|
window.dispatchEvent(new CustomEvent(CREATOR_CANVAS_WIDGETS_EVENT, {
|
|
3937
|
-
detail: { widgets: items, primaryWidgetId }
|
|
4245
|
+
detail: { widgets: items, primaryWidgetId: primaryWidgetId || '' }
|
|
3938
4246
|
}));
|
|
3939
4247
|
}
|
|
3940
4248
|
|
|
@@ -3948,10 +4256,19 @@
|
|
|
3948
4256
|
console.warn('[CreatorApp] Host set primary ignored — missing widgetId');
|
|
3949
4257
|
return;
|
|
3950
4258
|
}
|
|
4259
|
+
const previousPrimary = canvasPrimaryWidgetId;
|
|
4260
|
+
// Optimistic — so host-request emits during persist cannot resurrect the previous primary.
|
|
4261
|
+
canvasPrimaryWidgetId = widgetId;
|
|
4262
|
+
saveSelectedWidgetId(widgetId);
|
|
3951
4263
|
// console.log('[CreatorApp] Host set primary widget', widgetId.substring(0, 8));
|
|
3952
4264
|
canvasRef?.setPrimaryWidgetShape?.(widgetId);
|
|
3953
4265
|
canvasRef?.selectWidgetShape?.(widgetId);
|
|
3954
|
-
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
|
+
});
|
|
3955
4272
|
}
|
|
3956
4273
|
|
|
3957
4274
|
function handleSetPrimaryWidget(widgetId: string) {
|
|
@@ -3961,7 +4278,33 @@
|
|
|
3961
4278
|
async function hydrateCanvasPrimaryWidget(linkedIds?: string[]): Promise<string | null> {
|
|
3962
4279
|
const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
|
|
3963
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
|
+
|
|
3964
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
|
+
}
|
|
3965
4308
|
if (!primaryId) {
|
|
3966
4309
|
const fromLinks = (linkedIds || []).filter((id) => id && !isDraftConvertId(id) && !String(id).startsWith('draft'));
|
|
3967
4310
|
const fromList = widgets
|
|
@@ -3970,7 +4313,11 @@
|
|
|
3970
4313
|
return cid === targetCanvasId && w.id && !isDraftConvertId(w.id);
|
|
3971
4314
|
})
|
|
3972
4315
|
.map((w: any) => w.id);
|
|
3973
|
-
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;
|
|
3974
4321
|
if (primaryId) {
|
|
3975
4322
|
// console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
|
|
3976
4323
|
await persistPrimaryWidgetId(primaryId);
|
|
@@ -3984,6 +4331,12 @@
|
|
|
3984
4331
|
return primaryId || canvasPrimaryWidgetId || null;
|
|
3985
4332
|
}
|
|
3986
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
|
+
|
|
3987
4340
|
async function handleHostMoveCanvasFolderEvent(event: Event) {
|
|
3988
4341
|
const folderId = (event as CustomEvent).detail?.folderId;
|
|
3989
4342
|
if (!folderId || typeof folderId !== 'string' || !widgetsClient) return;
|
|
@@ -4436,12 +4789,46 @@
|
|
|
4436
4789
|
}, delayMs);
|
|
4437
4790
|
}
|
|
4438
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
|
+
|
|
4439
4819
|
async function loadCurrentCanvas() {
|
|
4440
4820
|
// console.log('Widget Creator: loadCurrentCanvas():', routeCanvasId, isLoadingCanvasOnce);
|
|
4441
4821
|
if (isLoadingCanvasOnce) return;
|
|
4442
4822
|
try {
|
|
4443
4823
|
isLoadingCanvasOnce = true;
|
|
4444
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
|
+
|
|
4445
4832
|
// set widgets loading state
|
|
4446
4833
|
isLoadingWidgets = true;
|
|
4447
4834
|
widgetsError = null;
|
|
@@ -4474,6 +4861,16 @@
|
|
|
4474
4861
|
// Prefer canvasId from route (path or query)
|
|
4475
4862
|
if (routeCanvasId) {
|
|
4476
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;
|
|
4477
4874
|
// console.log('WidgetCreator.loadCurrentCanvas: Loading data for route canvasId:', canvasId);
|
|
4478
4875
|
}
|
|
4479
4876
|
// Fallback: get latest canvas from current user's canvases
|
|
@@ -4577,10 +4974,16 @@
|
|
|
4577
4974
|
const { loaded, widgetIds: widgetIdsOnCanvas } = await parallelCanvasLoadPromise;
|
|
4578
4975
|
await maybeAutoRestoreWidgetShapes();
|
|
4579
4976
|
backfillWidgetShapeMetadata();
|
|
4580
|
-
const
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
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
|
+
});
|
|
4584
4987
|
}
|
|
4585
4988
|
return;
|
|
4586
4989
|
}
|
|
@@ -4597,10 +5000,16 @@
|
|
|
4597
5000
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
4598
5001
|
await maybeAutoRestoreWidgetShapes();
|
|
4599
5002
|
backfillWidgetShapeMetadata();
|
|
4600
|
-
const
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
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
|
+
});
|
|
4604
5013
|
}
|
|
4605
5014
|
} catch (error) {
|
|
4606
5015
|
console.error('Widget Creator: Load canvases error:', error);
|
|
@@ -4670,6 +5079,11 @@
|
|
|
4670
5079
|
sessionCanvasId = entry.id;
|
|
4671
5080
|
activeCanvasName = entry.name;
|
|
4672
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);
|
|
4673
5087
|
|
|
4674
5088
|
// Clear existing widgets and canvas visually
|
|
4675
5089
|
widgets = [];
|
|
@@ -4687,6 +5101,21 @@
|
|
|
4687
5101
|
}
|
|
4688
5102
|
}
|
|
4689
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
|
+
|
|
4690
5119
|
/** Close canvas dropdown on outside click */
|
|
4691
5120
|
function handleCanvasDropdownOutsideClick(e: MouseEvent) {
|
|
4692
5121
|
const target = e.target as HTMLElement;
|
|
@@ -4873,7 +5302,15 @@
|
|
|
4873
5302
|
}
|
|
4874
5303
|
|
|
4875
5304
|
try {
|
|
4876
|
-
|
|
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
|
+
}
|
|
4877
5314
|
const createdRaw = await canvasesClient.createCanvas({
|
|
4878
5315
|
createCanvasRequest: { persistenceKey: newName }
|
|
4879
5316
|
});
|
|
@@ -4986,16 +5423,51 @@
|
|
|
4986
5423
|
deleteCanvasTarget = entry;
|
|
4987
5424
|
showDeleteCanvasDialog = true;
|
|
4988
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
|
+
}
|
|
4989
5460
|
}
|
|
4990
5461
|
|
|
4991
5462
|
async function handleDeleteCanvasConfirm() {
|
|
4992
5463
|
if (!deleteCanvasTarget || !canvasesClient) return;
|
|
4993
5464
|
const targetId = deleteCanvasTarget.id;
|
|
4994
5465
|
const targetName = deleteCanvasTarget.name;
|
|
4995
|
-
|
|
5466
|
+
deleteCanvasError = '';
|
|
4996
5467
|
try {
|
|
4997
5468
|
await canvasesClient.deleteCanvas({ id: targetId });
|
|
4998
5469
|
// console.log('[WidgetCreator] Canvas deleted:', targetId, targetName);
|
|
5470
|
+
showDeleteCanvasDialog = false;
|
|
4999
5471
|
userCanvases = userCanvases.filter(c => c.id !== targetId);
|
|
5000
5472
|
if (canvasId === targetId) {
|
|
5001
5473
|
const next = userCanvases[0] || null;
|
|
@@ -5011,6 +5483,10 @@
|
|
|
5011
5483
|
}
|
|
5012
5484
|
} catch (err) {
|
|
5013
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;
|
|
5014
5490
|
}
|
|
5015
5491
|
deleteCanvasTarget = null;
|
|
5016
5492
|
}
|
|
@@ -5080,15 +5556,39 @@
|
|
|
5080
5556
|
// console.log('Widgets for canvas:', canvasId, ':', widgets);
|
|
5081
5557
|
syncOrphanedWidgetShapes();
|
|
5082
5558
|
|
|
5083
|
-
// 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.
|
|
5084
5562
|
if (embedded && forceCreateNewWidget && !initialWidgetId) {
|
|
5085
5563
|
// console.log('[WidgetCreator] Skipping auto-select of existing widgets (new widget from site)');
|
|
5086
5564
|
} else if (widgets.length > 0) {
|
|
5087
|
-
const
|
|
5088
|
-
|
|
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)
|
|
5089
5571
|
: null;
|
|
5090
|
-
const
|
|
5091
|
-
|
|
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;
|
|
5092
5592
|
const widgetToSelect = deepLinkedWidget ?? savedWidget ?? widgets[0];
|
|
5093
5593
|
if (deepLinkedWidget) {
|
|
5094
5594
|
// console.log('[WidgetCreator] Selecting widget from embedded deep-link:', deepLinkedWidget.name);
|
|
@@ -5355,6 +5855,17 @@
|
|
|
5355
5855
|
|
|
5356
5856
|
if (missingIds.length === 0) return;
|
|
5357
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
|
+
|
|
5358
5869
|
try {
|
|
5359
5870
|
// console.log(
|
|
5360
5871
|
// '[WidgetCreator] Merging',
|
|
@@ -5365,8 +5876,21 @@
|
|
|
5365
5876
|
getWidgetsBatchRequest: { widgetIds: missingIds }
|
|
5366
5877
|
});
|
|
5367
5878
|
const batch = response as { widgets?: WidgetSummary[]; data?: { widgets?: WidgetSummary[] } };
|
|
5368
|
-
const
|
|
5369
|
-
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;
|
|
5370
5894
|
|
|
5371
5895
|
const previousById = new Map(
|
|
5372
5896
|
widgets.filter((w) => w.id).map((w) => [w.id as string, w]),
|
|
@@ -5388,9 +5912,26 @@
|
|
|
5388
5912
|
widgets.length > 0 &&
|
|
5389
5913
|
!(embedded && forceCreateNewWidget && !initialWidgetId)
|
|
5390
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;
|
|
5391
5923
|
const savedWidgetId = getSavedSelectedWidgetId();
|
|
5392
5924
|
const savedWidget = savedWidgetId ? getWidgetById(savedWidgetId) : null;
|
|
5393
|
-
|
|
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
|
+
);
|
|
5394
5935
|
}
|
|
5395
5936
|
} catch (err) {
|
|
5396
5937
|
console.warn('[WidgetCreator] Failed to merge widgets linked on canvas:', err);
|
|
@@ -5399,8 +5940,8 @@
|
|
|
5399
5940
|
|
|
5400
5941
|
/**
|
|
5401
5942
|
* Site embeds CreatorApp once; `initialWidgetId` props stay frozen in prod.
|
|
5402
|
-
*
|
|
5403
|
-
*
|
|
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.
|
|
5404
5945
|
*/
|
|
5405
5946
|
function resolveEmbeddedDeepLinkWidgetId(): string | null {
|
|
5406
5947
|
if (typeof window !== 'undefined') {
|
|
@@ -5414,6 +5955,84 @@
|
|
|
5414
5955
|
return initialWidgetId ?? null;
|
|
5415
5956
|
}
|
|
5416
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
|
+
|
|
5417
6036
|
/**
|
|
5418
6037
|
* Canvas-only embed deep-link (My widgets / canvas without widget_id):
|
|
5419
6038
|
* read canvases.primary_widget_id so Creator can open that widget panel.
|
|
@@ -5430,20 +6049,44 @@
|
|
|
5430
6049
|
(record as { primary_widget_id?: unknown })?.primary_widget_id ??
|
|
5431
6050
|
null;
|
|
5432
6051
|
const primaryId = typeof raw === 'string' && raw.trim() ? raw.trim() : null;
|
|
5433
|
-
if (primaryId) {
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
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
|
+
}
|
|
5446
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;
|
|
5447
6090
|
return primaryId;
|
|
5448
6091
|
} catch (err) {
|
|
5449
6092
|
console.warn(
|
|
@@ -5460,11 +6103,6 @@
|
|
|
5460
6103
|
syncEmbeddedDeepLinkDismissTracking();
|
|
5461
6104
|
if (!embedded || !widgetsClient || !hasValidToken) return;
|
|
5462
6105
|
if (embeddedDeepLinkPanelDismissed) {
|
|
5463
|
-
const dismissedId = resolveEmbeddedDeepLinkWidgetId();
|
|
5464
|
-
// console.log(
|
|
5465
|
-
// '[CreatorApp] Skip embedded deep-link select — user dismissed panel:',
|
|
5466
|
-
// dismissedId ? dismissedId.substring(0, 8) : '(none)'
|
|
5467
|
-
// );
|
|
5468
6106
|
return;
|
|
5469
6107
|
}
|
|
5470
6108
|
// Select primary widget once per Creator mount. Do not auto-open Widget Details.
|
|
@@ -5472,70 +6110,79 @@
|
|
|
5472
6110
|
return;
|
|
5473
6111
|
}
|
|
5474
6112
|
if (isConvertOrDraftPanelOpen()) {
|
|
5475
|
-
// console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
|
|
5476
6113
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5477
6114
|
return;
|
|
5478
6115
|
}
|
|
5479
6116
|
|
|
6117
|
+
const hostCanvasId = resolveHostCanvasId();
|
|
5480
6118
|
let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
url.searchParams.set('widget_id', deepLinkWidgetId);
|
|
5498
|
-
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
5499
|
-
syncEmbeddedDeepLinkDismissTracking();
|
|
5500
|
-
}
|
|
5501
|
-
} catch (err) {
|
|
5502
|
-
console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
|
|
5503
|
-
}
|
|
5504
|
-
}
|
|
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);
|
|
5505
6135
|
}
|
|
5506
6136
|
}
|
|
5507
|
-
|
|
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);
|
|
5508
6154
|
|
|
5509
6155
|
const requestGen = ++embeddedDeepLinkRequestGen;
|
|
6156
|
+
const expectedWidgetId = deepLinkWidgetId;
|
|
5510
6157
|
|
|
5511
6158
|
function deepLinkOpenStillValid(): boolean {
|
|
5512
6159
|
return (
|
|
5513
6160
|
requestGen === embeddedDeepLinkRequestGen &&
|
|
5514
6161
|
!embeddedDeepLinkPanelDismissed &&
|
|
5515
|
-
|
|
6162
|
+
canvasPrimaryWidgetId === expectedWidgetId
|
|
5516
6163
|
);
|
|
5517
6164
|
}
|
|
5518
6165
|
|
|
5519
6166
|
const alreadyLinked =
|
|
5520
|
-
selectedWidgetId ===
|
|
6167
|
+
selectedWidgetId === expectedWidgetId && !!getWidgetById(expectedWidgetId);
|
|
5521
6168
|
if (alreadyLinked) {
|
|
5522
6169
|
if (embeddedInitialStepPending && initialStep) {
|
|
5523
6170
|
widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
|
|
5524
6171
|
embeddedInitialStepPending = false;
|
|
5525
6172
|
}
|
|
5526
6173
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
6174
|
+
applyPrimarySelectionOnCanvas(expectedWidgetId);
|
|
6175
|
+
const primaryWidget = getWidgetById(expectedWidgetId);
|
|
6176
|
+
if (primaryWidget) notifyHostWidgetName(primaryWidget);
|
|
6177
|
+
emitCanvasWidgetsList('deeplink-already-linked');
|
|
5530
6178
|
return;
|
|
5531
6179
|
}
|
|
5532
6180
|
|
|
5533
6181
|
try {
|
|
5534
6182
|
const response = await widgetsClient.getWidgetsBatch({
|
|
5535
|
-
getWidgetsBatchRequest: { widgetIds: [
|
|
6183
|
+
getWidgetsBatchRequest: { widgetIds: [expectedWidgetId] }
|
|
5536
6184
|
});
|
|
5537
6185
|
if (!deepLinkOpenStillValid()) {
|
|
5538
|
-
// console.log('[CreatorApp] Stale embedded deep-link fetch — user changed selection or closed panel');
|
|
5539
6186
|
return;
|
|
5540
6187
|
}
|
|
5541
6188
|
|
|
@@ -5545,47 +6192,52 @@
|
|
|
5545
6192
|
};
|
|
5546
6193
|
const target = batch?.widgets?.[0] ?? batch?.data?.widgets?.[0];
|
|
5547
6194
|
if (!target?.id) {
|
|
5548
|
-
console.warn('[CreatorApp] Embedded deep-link widget not found:',
|
|
6195
|
+
console.warn('[CreatorApp] Embedded deep-link widget not found:', expectedWidgetId);
|
|
5549
6196
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5550
6197
|
return;
|
|
5551
6198
|
}
|
|
5552
6199
|
|
|
5553
|
-
const targetCanvasId =
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
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
|
+
}
|
|
5557
6219
|
|
|
5558
6220
|
if (!getWidgetById(target.id)) {
|
|
5559
6221
|
widgets = sortWidgetSummaries([...widgets, target]);
|
|
5560
6222
|
}
|
|
5561
6223
|
if (!deepLinkOpenStillValid()) {
|
|
5562
|
-
// console.log('[CreatorApp] Skip embedded deep-link open — stale after widget fetch');
|
|
5563
6224
|
return;
|
|
5564
6225
|
}
|
|
5565
6226
|
|
|
5566
6227
|
embeddedDeepLinkAutoOpenDone = true;
|
|
5567
|
-
|
|
6228
|
+
canvasPrimaryWidgetId = target.id;
|
|
5568
6229
|
await selectWidget(target, { skipLoadWidget: true, skipPlaceShape: true });
|
|
5569
6230
|
if (!deepLinkOpenStillValid()) return;
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
6231
|
+
applyPrimarySelectionOnCanvas(target.id);
|
|
6232
|
+
requestAnimationFrame(() => {
|
|
6233
|
+
setTimeout(() => applyPrimarySelectionOnCanvas(target.id), 80);
|
|
6234
|
+
});
|
|
6235
|
+
notifyHostWidgetName(target);
|
|
6236
|
+
emitCanvasWidgetsList('deeplink-primary');
|
|
5573
6237
|
if (embeddedInitialStepPending && initialStep) {
|
|
5574
6238
|
widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
|
|
5575
6239
|
embeddedInitialStepPending = false;
|
|
5576
6240
|
}
|
|
5577
|
-
|
|
5578
|
-
if (targetCanvasId && targetCanvasId !== canvasId) {
|
|
5579
|
-
// console.log('[CreatorApp] Switching canvas for embedded deep-link:', targetCanvasId);
|
|
5580
|
-
routeCanvasId = targetCanvasId;
|
|
5581
|
-
canvasId = targetCanvasId;
|
|
5582
|
-
saveSelectedCanvasId(canvasId);
|
|
5583
|
-
isLoadingCanvasOnce = false;
|
|
5584
|
-
isLoadingWidgetsOnce = false;
|
|
5585
|
-
lastWidgetsCanvasId = null;
|
|
5586
|
-
void loadCurrentCanvas();
|
|
5587
|
-
return;
|
|
5588
|
-
}
|
|
5589
6241
|
} catch (err) {
|
|
5590
6242
|
console.error('[CreatorApp] ensureEmbeddedWidgetDeepLink failed:', err);
|
|
5591
6243
|
embeddedDeepLinkAutoOpenDone = true;
|
|
@@ -6138,6 +6790,11 @@
|
|
|
6138
6790
|
}
|
|
6139
6791
|
|
|
6140
6792
|
notifyHostWidgetName({ id: widgetId, name: newName });
|
|
6793
|
+
emitCanvasWidgetsList('widget-renamed');
|
|
6794
|
+
widgetNamesRevision += 1;
|
|
6795
|
+
panelDisplayRevision += 1;
|
|
6796
|
+
backfillWidgetShapeMetadata();
|
|
6797
|
+
syncWidgetShapeLivePreviews();
|
|
6141
6798
|
// console.log('[WidgetRename] Successfully renamed widget:', widgetId, 'to:', newName);
|
|
6142
6799
|
showToast('success', `Widget renamed to "${newName}"`);
|
|
6143
6800
|
} catch (error) {
|
|
@@ -6332,26 +6989,37 @@
|
|
|
6332
6989
|
* Aborts the polling and resets UI state
|
|
6333
6990
|
*/
|
|
6334
6991
|
function stopGeneration() {
|
|
6335
|
-
//
|
|
6336
|
-
|
|
6337
|
-
|
|
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);
|
|
6338
7003
|
if (controller) {
|
|
6339
7004
|
controller.abort();
|
|
6340
|
-
widgetAbortControllers.delete(
|
|
7005
|
+
widgetAbortControllers.delete(widgetId);
|
|
6341
7006
|
}
|
|
6342
|
-
|
|
6343
|
-
generatingWidgetIds = restStop;
|
|
7007
|
+
clearActiveOperationId(widgetId);
|
|
6344
7008
|
}
|
|
7009
|
+
|
|
7010
|
+
generatingWidgetIds = {};
|
|
6345
7011
|
// Legacy fallback
|
|
6346
7012
|
if (codeGenerationAbortController) {
|
|
6347
7013
|
codeGenerationAbortController.abort();
|
|
6348
7014
|
codeGenerationAbortController = null;
|
|
6349
7015
|
}
|
|
6350
|
-
|
|
7016
|
+
|
|
7017
|
+
currentGenerationOperationId = null;
|
|
6351
7018
|
isGeneratingCode = false;
|
|
6352
7019
|
setCrossTabGenerating(false);
|
|
6353
7020
|
generateCodeStatus = '⏹ Generation stopped by user';
|
|
6354
|
-
|
|
7021
|
+
const statusWidgetId = focusedPanelWidgetId || selectedWidgetId || stopTargetIds[0] || null;
|
|
7022
|
+
getChatMethodsForWidget(statusWidgetId)?.updateLatestCodegenStatusMessage?.(
|
|
6355
7023
|
'Generation stopped by user. You can Retry this prompt.',
|
|
6356
7024
|
);
|
|
6357
7025
|
showToast('info', 'Generation stopped', {
|
|
@@ -6861,7 +7529,8 @@
|
|
|
6861
7529
|
const shapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
|
|
6862
7530
|
if (shapeDataUrl && canvasRef) {
|
|
6863
7531
|
canvasRef.updateWidgetImage(widgetId, shapeDataUrl);
|
|
6864
|
-
|
|
7532
|
+
// Do NOT persist Fabric placeholder/icon as project-list thumbnail —
|
|
7533
|
+
// only iframe screenshots (below) represent the real widget.
|
|
6865
7534
|
} else {
|
|
6866
7535
|
// console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
|
|
6867
7536
|
}
|
|
@@ -7654,37 +8323,49 @@ Requirements:
|
|
|
7654
8323
|
// cache-busts the same URL and looks like an extra reload.
|
|
7655
8324
|
// console.log('[CreatorApp] Skipping extra preview remount — Worker iframe already showing');
|
|
7656
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
|
+
});
|
|
7657
8330
|
} else {
|
|
7658
8331
|
activePanelRef?.setPreviewLoadingState(true);
|
|
7659
8332
|
await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
|
|
7660
8333
|
}
|
|
7661
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;
|
|
7662
8342
|
if (!completedPreviewBuildErr) {
|
|
7663
8343
|
await applyDesignSchemaDefaultsToPreview(generationWidgetId);
|
|
7664
8344
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
7665
8345
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
7666
8346
|
`Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''}\nPreview ready.`,
|
|
7667
8347
|
);
|
|
8348
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8349
|
+
generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
|
|
8350
|
+
});
|
|
7668
8351
|
setTimeout(async () => {
|
|
7669
8352
|
await panelChat?.reloadConversationMessages?.();
|
|
7670
8353
|
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
7671
8354
|
}, 2500);
|
|
7672
|
-
setTimeout(() =>
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
);
|
|
7680
|
-
} catch (autoPublishErr) {
|
|
7681
|
-
console.warn('[Publish] Auto-publish after codegen failed:', autoPublishErr);
|
|
7682
|
-
}
|
|
7683
|
-
})();
|
|
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.
|
|
7684
8362
|
} else {
|
|
7685
8363
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
7686
8364
|
`Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
7687
8365
|
);
|
|
8366
|
+
patchCodegenUiState(generationWidgetId, {
|
|
8367
|
+
generateCodeStatus: `Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
8368
|
+
});
|
|
7688
8369
|
}
|
|
7689
8370
|
}, completedPreviewBuildErr ? 400 : 800);
|
|
7690
8371
|
} else {
|
|
@@ -8127,22 +8808,21 @@ Requirements:
|
|
|
8127
8808
|
window.addEventListener('storage', handleCrossTabStorageEvent);
|
|
8128
8809
|
window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
8129
8810
|
|
|
8130
|
-
// Clean up cross-tab flags when tab is closed/refreshed
|
|
8131
|
-
|
|
8132
|
-
for (const wId of Object.keys(generatingWidgetIds)) {
|
|
8133
|
-
setCrossTabGenerating(false, wId);
|
|
8134
|
-
}
|
|
8135
|
-
if (isPublishing) setCrossTabPublishing(false);
|
|
8136
|
-
};
|
|
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.
|
|
8137
8813
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
8138
8814
|
|
|
8139
|
-
// Read canvas id from route
|
|
8140
|
-
|
|
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) {
|
|
8141
8819
|
try {
|
|
8142
8820
|
const sub = pageStore.subscribe(($page) => {
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
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;
|
|
8146
8826
|
});
|
|
8147
8827
|
setTimeout(() => {
|
|
8148
8828
|
if (sub && typeof sub === 'function') sub();
|
|
@@ -8434,6 +9114,10 @@ Requirements:
|
|
|
8434
9114
|
function formatCodegenProgressStatus(op: OperationStatus): string {
|
|
8435
9115
|
const stage = getOperationStageMessage(op.metadata as Record<string, unknown> | undefined);
|
|
8436
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
|
+
}
|
|
8437
9121
|
if (stage) {
|
|
8438
9122
|
return pct != null && pct > 0 ? `${stage} (${pct}%)` : stage;
|
|
8439
9123
|
}
|
|
@@ -8463,13 +9147,22 @@ Requirements:
|
|
|
8463
9147
|
>,
|
|
8464
9148
|
): void {
|
|
8465
9149
|
ensurePanelState(widgetId);
|
|
8466
|
-
|
|
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);
|
|
8467
9160
|
if (widgetId === focusedPanelWidgetId) {
|
|
8468
|
-
if (
|
|
8469
|
-
if (
|
|
8470
|
-
if (
|
|
8471
|
-
if (
|
|
8472
|
-
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;
|
|
8473
9166
|
}
|
|
8474
9167
|
}
|
|
8475
9168
|
|
|
@@ -8609,13 +9302,15 @@ Requirements:
|
|
|
8609
9302
|
compositionSchemasLoading: boolean;
|
|
8610
9303
|
compositionSchemasError: string | null;
|
|
8611
9304
|
schemaLoadStartedAt: number | null;
|
|
8612
|
-
chatContainerHeight: number;
|
|
9305
|
+
chatContainerHeight: number; // 0 = fill available; >0 = user-resized fixed px
|
|
8613
9306
|
panelPreviewCompileError: string | null;
|
|
8614
9307
|
};
|
|
8615
9308
|
|
|
8616
9309
|
let widgetPanelSnapshots: Record<string, WidgetPanelSnapshot> = {};
|
|
8617
9310
|
/** Bumped on every panel snapshot write so {#each} panels re-read display state. */
|
|
8618
9311
|
let panelDisplayRevision = 0;
|
|
9312
|
+
/** Bumped on rename so shape chrome / host title re-read widget names. */
|
|
9313
|
+
let widgetNamesRevision = 0;
|
|
8619
9314
|
const schemaLoadInFlight = new Set<string>();
|
|
8620
9315
|
const schemaLoadPromises = new Map<string, Promise<void>>();
|
|
8621
9316
|
const compositionsLoadInFlight = new Set<string>();
|
|
@@ -8711,7 +9406,7 @@ Requirements:
|
|
|
8711
9406
|
compositionSchemasLoading,
|
|
8712
9407
|
compositionSchemasError,
|
|
8713
9408
|
schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
|
|
8714
|
-
chatContainerHeight: focusedSnap?.chatContainerHeight ??
|
|
9409
|
+
chatContainerHeight: focusedSnap?.chatContainerHeight ?? 0,
|
|
8715
9410
|
panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
|
|
8716
9411
|
};
|
|
8717
9412
|
}
|
|
@@ -8777,7 +9472,7 @@ Requirements:
|
|
|
8777
9472
|
compositionSchemasLoading: false,
|
|
8778
9473
|
compositionSchemasError: null,
|
|
8779
9474
|
schemaLoadStartedAt: null,
|
|
8780
|
-
chatContainerHeight:
|
|
9475
|
+
chatContainerHeight: 0,
|
|
8781
9476
|
panelPreviewCompileError: null,
|
|
8782
9477
|
};
|
|
8783
9478
|
}
|
|
@@ -8833,7 +9528,7 @@ Requirements:
|
|
|
8833
9528
|
: livePublishMatchesPanel
|
|
8834
9529
|
? live.publishHistoryResolvedForWidgetId
|
|
8835
9530
|
: snap.publishHistoryResolvedForWidgetId,
|
|
8836
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
9531
|
+
chatContainerHeight: snap.chatContainerHeight ?? 0,
|
|
8837
9532
|
compositionDesignSchema: resolvedDesignSchema,
|
|
8838
9533
|
compositionContentSchema: resolvedContentSchema,
|
|
8839
9534
|
compositionSchemasLoading: schemaLoading,
|
|
@@ -8851,7 +9546,7 @@ Requirements:
|
|
|
8851
9546
|
...snap,
|
|
8852
9547
|
lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
|
|
8853
9548
|
hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
|
|
8854
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
9549
|
+
chatContainerHeight: snap.chatContainerHeight ?? 0,
|
|
8855
9550
|
compositionDesignSchema: resolvedDesignSchema,
|
|
8856
9551
|
compositionContentSchema: resolvedContentSchema,
|
|
8857
9552
|
compositionSchemasError: resolvedSchemaError,
|
|
@@ -8996,6 +9691,247 @@ Requirements:
|
|
|
8996
9691
|
return resolvePublishedWidgetUrl(widgetId, version, jsPath);
|
|
8997
9692
|
}
|
|
8998
9693
|
|
|
9694
|
+
/** Widget Builder origin for canvas shape iframes (mirrors WidgetDetails). */
|
|
9695
|
+
function widgetBuilderPreviewBaseUrl(): string {
|
|
9696
|
+
const publicBase = (
|
|
9697
|
+
widgetBuilderUrl ||
|
|
9698
|
+
import.meta.env.VITE_WIDGET_BUILDER_URL ||
|
|
9699
|
+
import.meta.env.PUBLIC_WIDGET_BUILDER_URL ||
|
|
9700
|
+
'https://wbuilder.widgetic.com'
|
|
9701
|
+
).replace(/\/+$/, '');
|
|
9702
|
+
if (import.meta.env.DEV && publicBase.includes('wbuilder.widgetic.com')) {
|
|
9703
|
+
return '/__widget_builder__';
|
|
9704
|
+
}
|
|
9705
|
+
return publicBase;
|
|
9706
|
+
}
|
|
9707
|
+
|
|
9708
|
+
/**
|
|
9709
|
+
* Live preview URL for a canvas WidgetShape overlay.
|
|
9710
|
+
* Source: 'live' | 'preview' | 'v:{N}' (historical published version).
|
|
9711
|
+
* Publish history is loaded without requiring chat to be open.
|
|
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
|
+
|
|
9751
|
+
function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
|
|
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
|
+
|
|
9780
|
+
const published = getPanelEditPreviewUrl(widgetId);
|
|
9781
|
+
if (published && source !== 'preview') return published;
|
|
9782
|
+
return workerUrl;
|
|
9783
|
+
}
|
|
9784
|
+
|
|
9785
|
+
function resolveWidgetShapeVersionLabel(widgetId: string): string | null {
|
|
9786
|
+
const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
9787
|
+
const ps = getPanelDisplayState(widgetId);
|
|
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
|
+
}
|
|
9881
|
+
|
|
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);
|
|
9909
|
+
}
|
|
9910
|
+
|
|
9911
|
+
function syncWidgetShapeLivePreviews(): void {
|
|
9912
|
+
try {
|
|
9913
|
+
canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
|
|
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
|
+
}
|
|
9930
|
+
} catch (err) {
|
|
9931
|
+
console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
|
|
9932
|
+
}
|
|
9933
|
+
}
|
|
9934
|
+
|
|
8999
9935
|
function getPanelEmbedPreviewUrl(widgetId: string): string | null {
|
|
9000
9936
|
const base = getPanelEditPreviewUrl(widgetId);
|
|
9001
9937
|
if (!base) return null;
|
|
@@ -9333,6 +10269,9 @@ Requirements:
|
|
|
9333
10269
|
if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
|
|
9334
10270
|
if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
|
|
9335
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();
|
|
9336
10275
|
// console.log(
|
|
9337
10276
|
// '[CreatorApp] Migrated Widget Details panel',
|
|
9338
10277
|
// fromId.substring(0, 16),
|
|
@@ -9928,6 +10867,8 @@ Requirements:
|
|
|
9928
10867
|
|
|
9929
10868
|
function hasUnpublishedChangesForPanel(widgetId: string): boolean {
|
|
9930
10869
|
const ps = getPanelDisplayState(widgetId);
|
|
10870
|
+
if (ps.pendingPublishAfterCodegen) return true;
|
|
10871
|
+
const pendingSha = loadPendingPublishShaForWidget(widgetId);
|
|
9931
10872
|
const headSha = resolvePublishHeadCommitId(widgetId, ps);
|
|
9932
10873
|
if (!headSha || ps.lastPublishedVersion === null) return false;
|
|
9933
10874
|
|
|
@@ -9935,6 +10876,7 @@ Requirements:
|
|
|
9935
10876
|
getLivePublishedCommitShaForPanel(widgetId)
|
|
9936
10877
|
?? getWidgetLastPublishCommitSha(widgetId);
|
|
9937
10878
|
if (!liveSha) return false;
|
|
10879
|
+
if (pendingSha && !commitsMatch(pendingSha, liveSha)) return true;
|
|
9938
10880
|
|
|
9939
10881
|
return !commitsMatch(headSha, liveSha);
|
|
9940
10882
|
}
|
|
@@ -10375,6 +11317,10 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
|
|
|
10375
11317
|
publishHistoryResolved: true,
|
|
10376
11318
|
publishHistoryResolvedForWidgetId: widgetId,
|
|
10377
11319
|
});
|
|
11320
|
+
shapePublishHistoryByWidgetId = {
|
|
11321
|
+
...shapePublishHistoryByWidgetId,
|
|
11322
|
+
[widgetId]: [...history],
|
|
11323
|
+
};
|
|
10378
11324
|
if (widgetId === focusedPanelWidgetId) {
|
|
10379
11325
|
publishHistory = history;
|
|
10380
11326
|
publishHistoryResolved = true;
|
|
@@ -10417,8 +11363,8 @@ async function loadPublishHistory(opts: { silent?: boolean } = {}) {
|
|
|
10417
11363
|
* Sets a historical version as the current live version without rebuilding.
|
|
10418
11364
|
* Updates widgets.js_path to point to the older version's artifact URL.
|
|
10419
11365
|
*/
|
|
10420
|
-
async function
|
|
10421
|
-
if (!
|
|
11366
|
+
async function setLiveVersionForWidget(widgetId: string, version: number) {
|
|
11367
|
+
if (!widgetId || !$userSession?.jwt) return;
|
|
10422
11368
|
|
|
10423
11369
|
isSettingLiveVersion = version;
|
|
10424
11370
|
if (!widgetsClient) {
|
|
@@ -10428,12 +11374,12 @@ async function setLiveVersion(version: number) {
|
|
|
10428
11374
|
}
|
|
10429
11375
|
try {
|
|
10430
11376
|
const liveRes = await widgetsClient.setWidgetPublishHistoryLive({
|
|
10431
|
-
widgetId
|
|
11377
|
+
widgetId,
|
|
10432
11378
|
version
|
|
10433
11379
|
});
|
|
10434
11380
|
const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string } | undefined;
|
|
10435
11381
|
const liveData = nested?.data ?? nested ?? {};
|
|
10436
|
-
const history = getPanelDisplayState(
|
|
11382
|
+
const history = getPanelDisplayState(widgetId).publishHistory;
|
|
10437
11383
|
const record = history.find((row) => row.version === version);
|
|
10438
11384
|
const commitSha = (liveData as { commitSha?: string; commit_sha?: string }).commitSha
|
|
10439
11385
|
|| (liveData as { commit_sha?: string }).commit_sha
|
|
@@ -10441,15 +11387,23 @@ async function setLiveVersion(version: number) {
|
|
|
10441
11387
|
const artifactUrl = (liveData as { artifactUrl?: string; artifact_url?: string }).artifactUrl
|
|
10442
11388
|
|| (liveData as { artifact_url?: string }).artifact_url
|
|
10443
11389
|
|| record?.artifactUrl || record?.artifact_url || null;
|
|
10444
|
-
applyPublishSuccessToWidgetState(
|
|
11390
|
+
applyPublishSuccessToWidgetState(widgetId, version, artifactUrl, commitSha);
|
|
11391
|
+
shapePreviewSourceByWidgetId = {
|
|
11392
|
+
...shapePreviewSourceByWidgetId,
|
|
11393
|
+
[widgetId]: `v:${version}`,
|
|
11394
|
+
};
|
|
11395
|
+
syncWidgetShapeLivePreviews();
|
|
11396
|
+
panelDisplayRevision += 1;
|
|
10445
11397
|
isHistoryOpen = false;
|
|
10446
|
-
const liveUrl = resolvePublishedWidgetUrl(
|
|
10447
|
-
const detailsRef = getWidgetDetailsRef(
|
|
11398
|
+
const liveUrl = resolvePublishedWidgetUrl(widgetId, version, artifactUrl);
|
|
11399
|
+
const detailsRef = getWidgetDetailsRef(widgetId);
|
|
10448
11400
|
detailsRef?.showPublishedArtifact?.(liveUrl);
|
|
10449
11401
|
detailsRef?.beginCompositionHydration?.();
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
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.',
|
|
10453
11407
|
duration: 5000
|
|
10454
11408
|
});
|
|
10455
11409
|
} catch (err: any) {
|
|
@@ -10460,6 +11414,11 @@ async function setLiveVersion(version: number) {
|
|
|
10460
11414
|
}
|
|
10461
11415
|
}
|
|
10462
11416
|
|
|
11417
|
+
async function setLiveVersion(version: number) {
|
|
11418
|
+
if (!selectedWidgetId) return;
|
|
11419
|
+
await setLiveVersionForWidget(selectedWidgetId, version);
|
|
11420
|
+
}
|
|
11421
|
+
|
|
10463
11422
|
/** Toggle publish history panel — reloads data on every open */
|
|
10464
11423
|
function togglePublishHistory(triggerEl?: HTMLElement | null) {
|
|
10465
11424
|
isHistoryOpen = !isHistoryOpen;
|
|
@@ -11018,18 +11977,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11018
11977
|
}
|
|
11019
11978
|
}
|
|
11020
11979
|
|
|
11021
|
-
//
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
if (url.searchParams.get('widget_id') !== widgetId) {
|
|
11026
|
-
url.searchParams.set('widget_id', widgetId);
|
|
11027
|
-
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
11028
|
-
syncEmbeddedDeepLinkDismissTracking();
|
|
11029
|
-
}
|
|
11030
|
-
} catch (err) {
|
|
11031
|
-
console.warn('[WidgetCreator] Failed to sync widget_id in URL:', err);
|
|
11032
|
-
}
|
|
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);
|
|
11033
11984
|
}
|
|
11034
11985
|
// Full select so commit/preview/header match the shape's linked widget (not a stale URL widget).
|
|
11035
11986
|
if (selectedWidgetId !== widgetId) {
|
|
@@ -11261,6 +12212,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11261
12212
|
}
|
|
11262
12213
|
notifyHostWidgetName({ id: widgetId, name: newName });
|
|
11263
12214
|
emitCanvasWidgetsList('widget-renamed');
|
|
12215
|
+
widgetNamesRevision += 1;
|
|
12216
|
+
panelDisplayRevision += 1;
|
|
12217
|
+
backfillWidgetShapeMetadata();
|
|
12218
|
+
syncWidgetShapeLivePreviews();
|
|
11264
12219
|
showToast('success', `Widget renamed to "${newName}"`);
|
|
11265
12220
|
} catch (error) {
|
|
11266
12221
|
console.error('[WidgetCreator] Failed to rename widget:', error);
|
|
@@ -11298,6 +12253,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11298
12253
|
if (selectedWidget?.id && !didForceCreateNewWidget) {
|
|
11299
12254
|
placeSelectedWidgetShapeOnCanvas();
|
|
11300
12255
|
}
|
|
12256
|
+
syncWidgetShapeLivePreviews();
|
|
11301
12257
|
return;
|
|
11302
12258
|
}
|
|
11303
12259
|
const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
@@ -11306,18 +12262,21 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11306
12262
|
if (!isCanvasStateLoaded) {
|
|
11307
12263
|
scheduleCanvasHydrateRetry('canvas ready before canvasId');
|
|
11308
12264
|
}
|
|
12265
|
+
syncWidgetShapeLivePreviews();
|
|
11309
12266
|
return;
|
|
11310
12267
|
}
|
|
11311
12268
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
11312
12269
|
await maybeAutoRestoreWidgetShapes();
|
|
11313
|
-
const
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
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
|
+
});
|
|
12279
|
+
syncWidgetShapeLivePreviews();
|
|
11321
12280
|
}
|
|
11322
12281
|
|
|
11323
12282
|
/**
|
|
@@ -11591,14 +12550,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11591
12550
|
let autosaveSequence = 0;
|
|
11592
12551
|
let lastResolvedAutosaveSequence = 0;
|
|
11593
12552
|
|
|
11594
|
-
async function handleCanvasAutoSave(
|
|
12553
|
+
async function handleCanvasAutoSave(
|
|
12554
|
+
json: string | null,
|
|
12555
|
+
isUnmountSave = false,
|
|
12556
|
+
opts: { force?: boolean } = {},
|
|
12557
|
+
) {
|
|
11595
12558
|
if (!json) {
|
|
11596
12559
|
if (isUnmountSave) console.warn('[CanvasDebug] onBeforeUnmount: saveCanvas returned null — canvas not initialized');
|
|
11597
12560
|
return;
|
|
11598
12561
|
}
|
|
11599
12562
|
|
|
12563
|
+
const force = opts.force === true;
|
|
12564
|
+
|
|
11600
12565
|
// Guard: don't overwrite DB with an empty canvas if state hasn't loaded yet
|
|
11601
|
-
if (!isCanvasStateLoaded) {
|
|
12566
|
+
if (!force && !isCanvasStateLoaded) {
|
|
11602
12567
|
// console.log(
|
|
11603
12568
|
// '[CanvasDebug] Skipping autosave — canvas state not loaded yet (guard active, avoids overwrite with empty)'
|
|
11604
12569
|
// );
|
|
@@ -11606,7 +12571,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11606
12571
|
}
|
|
11607
12572
|
|
|
11608
12573
|
// Guard: skip autosaves triggered by loadCanvas() object:added events
|
|
11609
|
-
if (isLoadCooldownActive && !isUnmountSave) {
|
|
12574
|
+
if (!force && isLoadCooldownActive && !isUnmountSave) {
|
|
11610
12575
|
// console.log('[CanvasDebug] Skipping autosave — load cooldown active (prevents overwriting freshly loaded data)');
|
|
11611
12576
|
return;
|
|
11612
12577
|
}
|
|
@@ -11629,11 +12594,17 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11629
12594
|
// Unmount saves (HMR) are always blocked when going from N→0.
|
|
11630
12595
|
// Normal autosaves going from N→0 are allowed only if the drop is gradual
|
|
11631
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.
|
|
11632
12598
|
if (objectCount === 0 && lastSavedCanvasObjectCount > 0) {
|
|
11633
12599
|
if (isUnmountSave || lastSavedCanvasObjectCount > 1) {
|
|
11634
12600
|
console.warn(
|
|
11635
|
-
`[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})`
|
|
11636
12602
|
);
|
|
12603
|
+
if (force) {
|
|
12604
|
+
showToast('error', 'Save blocked', {
|
|
12605
|
+
description: 'Canvas snapshot looked empty — refresh and try again.',
|
|
12606
|
+
});
|
|
12607
|
+
}
|
|
11637
12608
|
return;
|
|
11638
12609
|
}
|
|
11639
12610
|
// lastSavedCanvasObjectCount === 1 → user deleted the last object, allow it
|
|
@@ -11670,12 +12641,25 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11670
12641
|
return;
|
|
11671
12642
|
}
|
|
11672
12643
|
lastResolvedAutosaveSequence = currentSeq;
|
|
12644
|
+
if (force) {
|
|
12645
|
+
showToast('success', 'Canvas saved', { duration: 2000 });
|
|
12646
|
+
}
|
|
11673
12647
|
// console.log(
|
|
11674
12648
|
// `[CanvasDebug] Canvas ${canvasId} saved to API: ${objectCount} shapes (seq ${currentSeq})`
|
|
11675
12649
|
// );
|
|
11676
12650
|
} catch (err) {
|
|
11677
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
|
+
}
|
|
11678
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
|
+
});
|
|
11679
12663
|
}
|
|
11680
12664
|
|
|
11681
12665
|
refreshLinkedWidgetIds();
|
|
@@ -12019,6 +13003,68 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12019
13003
|
return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
|
|
12020
13004
|
}
|
|
12021
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
|
+
|
|
12022
13068
|
function isConvertOrDraftPanelOpen(): boolean {
|
|
12023
13069
|
return (
|
|
12024
13070
|
isDraftConvertId(focusedPanelWidgetId)
|
|
@@ -12457,13 +13503,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12457
13503
|
<!-- Right: Debug toggle + Auth info (2 lines) + Logout -->
|
|
12458
13504
|
<div class="wc-top-bar-right flex items-center gap-3 shrink-0">
|
|
12459
13505
|
{#if debugModeAvailable}
|
|
13506
|
+
<HoverTooltip text={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}>
|
|
12460
13507
|
<button
|
|
12461
13508
|
onclick={toggleDebugMode}
|
|
12462
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'}"
|
|
12463
|
-
title={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}
|
|
12464
13510
|
>
|
|
12465
13511
|
🐛 Debug {debugMode ? 'ON' : 'OFF'}
|
|
12466
13512
|
</button>
|
|
13513
|
+
</HoverTooltip>
|
|
12467
13514
|
{/if}
|
|
12468
13515
|
<div class="wc-auth-info flex flex-col gap-px items-start">
|
|
12469
13516
|
<div class="wc-auth-line1 flex items-center gap-2">
|
|
@@ -12548,9 +13595,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12548
13595
|
onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
|
|
12549
13596
|
onWidgetShapeDeleting={handleWidgetShapeDeleting}
|
|
12550
13597
|
onWidgetRename={handleWidgetRename}
|
|
12551
|
-
onRefreshScreenshot={
|
|
13598
|
+
onRefreshScreenshot={null}
|
|
12552
13599
|
onSetPrimaryWidget={handleSetPrimaryWidget}
|
|
13600
|
+
onAfterRender={syncWidgetShapeLivePreviews}
|
|
12553
13601
|
getWidgetName={getWidgetNameById}
|
|
13602
|
+
widgetNameRevision={widgetNamesRevision}
|
|
12554
13603
|
isWidgetKnown={isWidgetKnownById}
|
|
12555
13604
|
onNonWidgetSelected={() => {
|
|
12556
13605
|
if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
|
|
@@ -12574,6 +13623,44 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12574
13623
|
<!-- FLOATING PANELS OVERLAY (pointer-events-none container, each panel is pointer-events-auto) -->
|
|
12575
13624
|
<div class="floating-panels-overlay absolute inset-0 z-2003 pointer-events-none">
|
|
12576
13625
|
|
|
13626
|
+
<!-- W3: live iframe + drag bar on WidgetShapes (iframe interactive; body drag locked) -->
|
|
13627
|
+
<WidgetShapeLivePreviews
|
|
13628
|
+
bind:this={widgetShapeLivePreviewsRef}
|
|
13629
|
+
getRects={() => canvasRef?.listWidgetShapeScreenRects?.() ?? []}
|
|
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)}
|
|
13644
|
+
selectedWidgetId={canvasSelectedWidgetShapeId}
|
|
13645
|
+
onTranslate={(widgetId, dx, dy) => {
|
|
13646
|
+
canvasRef?.translateWidgetShapeByScreen?.(widgetId, dx, dy);
|
|
13647
|
+
}}
|
|
13648
|
+
onTranslateEnd={(widgetId) => {
|
|
13649
|
+
canvasRef?.commitWidgetShapeTranslate?.(widgetId);
|
|
13650
|
+
}}
|
|
13651
|
+
onSelectShape={(widgetId) => {
|
|
13652
|
+
canvasRef?.selectWidgetShapeById?.(widgetId);
|
|
13653
|
+
canvasSelectedWidgetShapeId = widgetId;
|
|
13654
|
+
void ensureShapePublishHistory(widgetId);
|
|
13655
|
+
}}
|
|
13656
|
+
onOpenChat={(widgetId) => {
|
|
13657
|
+
void openWidgetDetailsPanel(widgetId);
|
|
13658
|
+
}}
|
|
13659
|
+
onRefresh={(widgetId) => {
|
|
13660
|
+
console.log('[WidgetShape] Iframe refreshed', widgetId.substring(0, 8));
|
|
13661
|
+
}}
|
|
13662
|
+
/>
|
|
13663
|
+
|
|
12577
13664
|
<!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
|
|
12578
13665
|
{#if debugMode && showWidgetsList}
|
|
12579
13666
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
@@ -13167,9 +14254,32 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13167
14254
|
hasAutoOpenedPreview = detail.hasAutoOpenedPreview;
|
|
13168
14255
|
}}
|
|
13169
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>
|
|
13170
14274
|
<svelte:fragment slot="version-history">
|
|
13171
14275
|
{#if ps.lastPublishedVersion !== null}
|
|
13172
|
-
|
|
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.">
|
|
13173
14283
|
<button
|
|
13174
14284
|
type="button"
|
|
13175
14285
|
onclick={(e) => {
|
|
@@ -13177,11 +14287,48 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13177
14287
|
togglePublishHistory(e.currentTarget);
|
|
13178
14288
|
}}
|
|
13179
14289
|
onmousedown={(e) => e.stopPropagation()}
|
|
13180
|
-
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"
|
|
13181
14291
|
>
|
|
13182
|
-
|
|
14292
|
+
v{Number.isFinite(viewingVersion) ? viewingVersion : ps.lastPublishedVersion}
|
|
13183
14293
|
<span class="text-emerald-500">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
|
|
13184
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}
|
|
13185
14332
|
{#if isHistoryOpen && isPanelFocused}
|
|
13186
14333
|
<Portal target="wc-live-history-portal-{panelWidgetId}">
|
|
13187
14334
|
<button
|
|
@@ -13200,38 +14347,49 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13200
14347
|
<p class="text-xs text-emerald-400 text-center py-2">No history records yet. Publish to start tracking.</p>
|
|
13201
14348
|
{:else}
|
|
13202
14349
|
{#each ps.publishHistory as record (record.id)}
|
|
13203
|
-
|
|
13204
|
-
|
|
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>
|
|
13205
14370
|
{#if debugMode && (record.commitSha || record.commit_sha)}
|
|
13206
14371
|
<span class="text-xs font-mono text-slate-400 shrink-0" title={record.commitSha ?? record.commit_sha}>
|
|
13207
14372
|
{(record.commitSha ?? record.commit_sha).slice(0, 7)}
|
|
13208
14373
|
</span>
|
|
13209
14374
|
{/if}
|
|
13210
14375
|
<span class="wc-live-history-date text-xs text-slate-400 shrink-0 whitespace-nowrap tabular-nums" style="padding-right: 4px;">
|
|
13211
|
-
{(
|
|
13212
|
-
const d = new Date(record.publishedAt ?? record.published_at ?? '');
|
|
13213
|
-
if (isNaN(d.getTime())) return '—';
|
|
13214
|
-
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
13215
|
-
const day = String(d.getDate()).padStart(2, '0');
|
|
13216
|
-
const timePart = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
13217
|
-
return `${month}/${day}/${d.getFullYear()} ${timePart}`;
|
|
13218
|
-
})()}
|
|
14376
|
+
{formatPublishHistoryDate(record)}
|
|
13219
14377
|
</span>
|
|
13220
|
-
{#if
|
|
14378
|
+
{#if !rowIsLive}
|
|
13221
14379
|
<button
|
|
13222
|
-
|
|
14380
|
+
type="button"
|
|
14381
|
+
onclick={(e) => {
|
|
14382
|
+
e.stopPropagation();
|
|
14383
|
+
void setLiveVersionForWidget(panelWidgetId, record.version);
|
|
14384
|
+
}}
|
|
13223
14385
|
disabled={isSettingLiveVersion === record.version}
|
|
13224
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"
|
|
13225
|
-
title="
|
|
14387
|
+
title="Publish this version so visitors see it on your website"
|
|
13226
14388
|
>
|
|
13227
14389
|
{isSettingLiveVersion === record.version ? '...' : 'Set live'}
|
|
13228
14390
|
</button>
|
|
13229
14391
|
{:else}
|
|
13230
|
-
<
|
|
13231
|
-
type="button"
|
|
13232
|
-
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"
|
|
13233
|
-
onclick={() => { isHistoryOpen = false; }}
|
|
13234
|
-
>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>
|
|
13235
14393
|
{/if}
|
|
13236
14394
|
</div>
|
|
13237
14395
|
{/each}
|
|
@@ -13269,7 +14427,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13269
14427
|
</svelte:fragment>
|
|
13270
14428
|
|
|
13271
14429
|
<svelte:fragment slot="code-generation-status">
|
|
13272
|
-
<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;">
|
|
13273
14431
|
<div class="flex items-center justify-between gap-2 min-w-0">
|
|
13274
14432
|
<div class="generate-code-status-el text-xs flex-1 min-w-0 truncate {ps.lastGenerationFailed ? 'text-red-600' : 'text-gray-500'}">
|
|
13275
14433
|
{#if ps.lastGenerationFailed}
|
|
@@ -13280,7 +14438,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13280
14438
|
</div>
|
|
13281
14439
|
</div>
|
|
13282
14440
|
{#if ps.lastGenerationFailed}
|
|
13283
|
-
<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;">
|
|
13284
14442
|
<p class="text-xs text-red-700 whitespace-pre-wrap">{ps.lastGenerationError}</p>
|
|
13285
14443
|
<div class="flex gap-2 mt-3 justify-center">
|
|
13286
14444
|
<Button
|
|
@@ -13318,20 +14476,22 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13318
14476
|
|
|
13319
14477
|
<svelte:fragment slot="code-generation">
|
|
13320
14478
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
13321
|
-
{@const chatUserHeight = (panelDisplayRevision,
|
|
14479
|
+
{@const chatUserHeight = (panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
|
|
13322
14480
|
<div
|
|
13323
14481
|
class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
|
|
13324
|
-
style="padding:
|
|
13325
|
-
use:nativeScrollbarPadding={
|
|
14482
|
+
style="padding: 0;"
|
|
14483
|
+
use:nativeScrollbarPadding={0}
|
|
13326
14484
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
13327
14485
|
>
|
|
13328
14486
|
{#key panelInstanceKey}
|
|
13329
14487
|
<div
|
|
13330
|
-
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'}"
|
|
13331
14489
|
data-widget-chat-height={panelWidgetId}
|
|
13332
|
-
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;`}
|
|
13333
14493
|
>
|
|
13334
|
-
<Chat class="code-generation-chat w-full h-full"
|
|
14494
|
+
<Chat class="code-generation-chat w-full h-full rounded-none border-0"
|
|
13335
14495
|
style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
|
|
13336
14496
|
resizeExpandsContainer={true}
|
|
13337
14497
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
@@ -13359,7 +14519,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13359
14519
|
autoCodeDetection: false,
|
|
13360
14520
|
}
|
|
13361
14521
|
}}
|
|
13362
|
-
widgetHeadCommitSha={ps.lastCommitId}
|
|
14522
|
+
widgetHeadCommitSha={resolveViewingCommitShaForPanel(panelWidgetId) || ps.lastCommitId}
|
|
13363
14523
|
onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
|
|
13364
14524
|
onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
|
|
13365
14525
|
onUploadChatFile={handleChatUploadFile}
|
|
@@ -13372,7 +14532,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13372
14532
|
onSaveMessage={(conversationId, content, messageType, attachments) =>
|
|
13373
14533
|
handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
|
|
13374
14534
|
}
|
|
13375
|
-
onRestoreCheckpointBackend={
|
|
14535
|
+
onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
|
|
14536
|
+
handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
|
|
14537
|
+
}
|
|
13376
14538
|
onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
|
|
13377
14539
|
onDraftChange={() => handleChatDraftChange(panelWidgetId)}
|
|
13378
14540
|
onConversationChange={(conversationId) => handleChatConversationChange(conversationId, panelWidgetId)}
|
|
@@ -13434,7 +14596,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13434
14596
|
{#if isEditStepWithoutPublishForPanel(panelWidgetId)}
|
|
13435
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">
|
|
13436
14598
|
<strong class="font-semibold">Not published yet.</strong>
|
|
13437
|
-
Generate on Create
|
|
14599
|
+
Generate on Create, preview the result, then Publish when you are ready.
|
|
13438
14600
|
</div>
|
|
13439
14601
|
{/if}
|
|
13440
14602
|
|
|
@@ -13924,11 +15086,46 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13924
15086
|
Are you sure you want to delete <strong>"{deleteCanvasTarget?.name || ''}"</strong>? This action cannot be undone.
|
|
13925
15087
|
</Dialog.Description>
|
|
13926
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
|
+
|
|
13927
15124
|
<div class="confirm-delete-canvas-dialog-actions flex justify-end gap-2 pt-2">
|
|
13928
15125
|
<button
|
|
13929
15126
|
type="button"
|
|
13930
15127
|
class="px-4 py-2 text-sm rounded-lg border border-gray-300 hover:bg-gray-100 cursor-pointer"
|
|
13931
|
-
onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; }}
|
|
15128
|
+
onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; deleteCanvasError = ''; }}
|
|
13932
15129
|
>Cancel</button>
|
|
13933
15130
|
<button
|
|
13934
15131
|
type="button"
|