@widgetic/creator 0.3.50 → 0.3.53
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
CHANGED
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
export let apiUrl: CreatorAppProps['apiUrl'] = null;
|
|
86
86
|
export let widgetBuilderUrl: CreatorAppProps['widgetBuilderUrl'] = null;
|
|
87
87
|
export let initialDebugMode: CreatorAppProps['initialDebugMode'] = undefined;
|
|
88
|
+
export let initialAdvancedFeatures: CreatorAppProps['initialAdvancedFeatures'] = false;
|
|
88
89
|
export let defaultCoderModelId: CreatorAppProps['defaultCoderModelId'] = undefined;
|
|
89
90
|
export let defaultPlannerModelId: CreatorAppProps['defaultPlannerModelId'] = undefined;
|
|
90
91
|
export let showWidgetsList: CreatorAppProps['showWidgetsList'] = true;
|
|
@@ -97,6 +98,11 @@
|
|
|
97
98
|
// Opt-in Dexie owner — reactive to prop changes without $effect (would force runes mode).
|
|
98
99
|
$: setEnableLocalCache(!!enableLocalCache);
|
|
99
100
|
|
|
101
|
+
function getRealTokenExpUnix(token: string): number | null {
|
|
102
|
+
const decoded = decodeJWT(token);
|
|
103
|
+
return decoded?.exp ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
100
106
|
let didForceCreateNewWidget = false;
|
|
101
107
|
/** Site "+ New widget": never hydrate ConvertedW / last-canvas JSON onto this session. */
|
|
102
108
|
let ignoreIncomingCanvasLoads = !!(forceCreateNewWidget && !initialWidgetId);
|
|
@@ -105,6 +111,21 @@
|
|
|
105
111
|
|
|
106
112
|
function notifyHostWidgetName(widget: { id?: string | null; name?: string | null } | null | undefined): void {
|
|
107
113
|
if (!embedded || !widget?.id) return;
|
|
114
|
+
const primaryId = (canvasPrimaryWidgetId || '').trim();
|
|
115
|
+
if (!primaryId) {
|
|
116
|
+
console.log('[CreatorApp] Skip host title — primary widget unknown');
|
|
117
|
+
emitCanvasWidgetsList('skip-host-title');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (widget.id !== primaryId) {
|
|
121
|
+
console.log(
|
|
122
|
+
'[CreatorApp] Skip host title — not primary',
|
|
123
|
+
widget.id.substring(0, 8),
|
|
124
|
+
primaryId.substring(0, 8)
|
|
125
|
+
);
|
|
126
|
+
emitCanvasWidgetsList('skip-host-title');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
108
129
|
const name = (widget.name || '').trim() || 'Untitled Widget';
|
|
109
130
|
console.log('[CreatorApp] Notify host widget name:', widget.id, name);
|
|
110
131
|
onWidgetRenamed?.({ widgetId: widget.id, name });
|
|
@@ -132,11 +153,18 @@
|
|
|
132
153
|
const HOST_REQUEST_PUBLISH_EVENT = 'widgetic:host-request-publish';
|
|
133
154
|
const HOST_TOGGLE_DEBUG_EVENT = 'widgetic:host-toggle-debug';
|
|
134
155
|
const HOST_TOGGLE_WIREFRAME_EVENT = 'widgetic:host-toggle-wireframe';
|
|
156
|
+
const HOST_TOGGLE_ADVANCED_EVENT = 'widgetic:host-toggle-advanced';
|
|
135
157
|
const HOST_OPEN_WIDGET_DETAILS_EVENT = 'widgetic:host-open-widget-details';
|
|
136
158
|
const HOST_DUPLICATE_WIDGET_EVENT = 'widgetic:host-duplicate-widget';
|
|
137
159
|
const HOST_DELETE_WIDGET_EVENT = 'widgetic:host-delete-widget';
|
|
160
|
+
const HOST_SAVE_CANVAS_EVENT = 'widgetic:host-save-canvas';
|
|
161
|
+
const HOST_SET_PRIMARY_WIDGET_EVENT = 'widgetic:host-set-primary-widget';
|
|
162
|
+
const HOST_REQUEST_CANVAS_WIDGETS_EVENT = 'widgetic:host-request-canvas-widgets';
|
|
163
|
+
const HOST_MOVE_CANVAS_FOLDER_EVENT = 'widgetic:host-move-canvas-folder';
|
|
138
164
|
const CREATOR_DEBUG_CHANGED_EVENT = 'widgetic:creator-debug-changed';
|
|
139
165
|
const CREATOR_WIREFRAME_CHANGED_EVENT = 'widgetic:creator-wireframe-changed';
|
|
166
|
+
const CREATOR_ADVANCED_CHANGED_EVENT = 'widgetic:creator-advanced-changed';
|
|
167
|
+
const CREATOR_CANVAS_WIDGETS_EVENT = 'widgetic:creator-canvas-widgets';
|
|
140
168
|
|
|
141
169
|
/** Site AppHeader rename — embed is a second Svelte runtime, so mount props do not update. */
|
|
142
170
|
async function applyHostWidgetName(widgetId: string | null | undefined, newName: string): Promise<void> {
|
|
@@ -238,7 +266,7 @@
|
|
|
238
266
|
/**
|
|
239
267
|
* WIDGETIC EDITOR — PropsEditor for design/content property editing
|
|
240
268
|
*/
|
|
241
|
-
import { PropsEditor, setFileBrowserUploadConfig } from '@widgetic/editor';
|
|
269
|
+
import { PropsEditor, setFileBrowserUploadConfig, setGoogleFontsApiKey } from '@widgetic/editor';
|
|
242
270
|
|
|
243
271
|
// API root: embedded hosts pass apiUrl; standalone uses VITE_API_URL / PUBLIC_API_URL.
|
|
244
272
|
// If unset, @widgetic/api-sdk falls back to BASE_PATH = dev-api.widgetic.com (OpenAPI default).
|
|
@@ -316,9 +344,41 @@
|
|
|
316
344
|
: new UploadsApi({ accessToken: token, middleware: [idempotencyKeyMiddleware] });
|
|
317
345
|
|
|
318
346
|
authenticatedClientsToken = token;
|
|
347
|
+
try {
|
|
348
|
+
setWsAuthToken(token);
|
|
349
|
+
} catch {
|
|
350
|
+
/* websocket store may not be ready yet */
|
|
351
|
+
}
|
|
319
352
|
syncFileBrowserUploadConfigForCreator();
|
|
320
353
|
}
|
|
321
354
|
|
|
355
|
+
function applySiteSessionToCreator(): boolean {
|
|
356
|
+
const synced = syncSessionFromSiteAuth();
|
|
357
|
+
if (!synced) return false;
|
|
358
|
+
try {
|
|
359
|
+
const sessionStr = localStorage.getItem('session');
|
|
360
|
+
const session = sessionStr ? JSON.parse(sessionStr) : null;
|
|
361
|
+
currentUserToken =
|
|
362
|
+
session?.accessToken ||
|
|
363
|
+
session?.access_token ||
|
|
364
|
+
localStorage.getItem('jwt') ||
|
|
365
|
+
currentUserToken;
|
|
366
|
+
} catch {
|
|
367
|
+
currentUserToken = localStorage.getItem('jwt') || currentUserToken;
|
|
368
|
+
}
|
|
369
|
+
if (currentUserToken) {
|
|
370
|
+
authenticatedClientsToken = null;
|
|
371
|
+
initializeAuthenticatedClients(currentUserToken);
|
|
372
|
+
}
|
|
373
|
+
updateTokenStatus();
|
|
374
|
+
console.log('[Auth] Creator SDK clients re-bound from site session');
|
|
375
|
+
return hasValidToken;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function handleSiteSessionExtended(): void {
|
|
379
|
+
applySiteSessionToCreator();
|
|
380
|
+
}
|
|
381
|
+
|
|
322
382
|
function syncFileBrowserUploadConfigForCreator() {
|
|
323
383
|
if (!authenticatedClientsToken) {
|
|
324
384
|
setFileBrowserUploadConfig(null);
|
|
@@ -509,6 +569,7 @@
|
|
|
509
569
|
let linkedWidgetIds: Set<string> = new Set();
|
|
510
570
|
function refreshLinkedWidgetIds() {
|
|
511
571
|
linkedWidgetIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set();
|
|
572
|
+
emitCanvasWidgetsList('linked-ids');
|
|
512
573
|
}
|
|
513
574
|
|
|
514
575
|
// Delete Widget Shape confirmation dialog state
|
|
@@ -517,6 +578,8 @@
|
|
|
517
578
|
let deleteDialogWidgetId = '';
|
|
518
579
|
let deleteDialogStep: 'confirm-remove' | 'confirm-delete-db' = 'confirm-remove';
|
|
519
580
|
let deleteDialogResolve: ((result: { proceed: boolean; deleteFromDb: boolean }) => void) | null = null;
|
|
581
|
+
/** Keep second-step UI/code; MVP deletes the DB widget after the first confirm. */
|
|
582
|
+
const ASK_DELETE_FROM_DB_CONFIRM = false;
|
|
520
583
|
|
|
521
584
|
function showDeleteWidgetDialog(widgetId: string, widgetName: string): Promise<{ proceed: boolean; deleteFromDb: boolean }> {
|
|
522
585
|
return new Promise((resolve) => {
|
|
@@ -535,10 +598,15 @@
|
|
|
535
598
|
if (deleteDialogResolve) deleteDialogResolve({ proceed: false, deleteFromDb: false });
|
|
536
599
|
break;
|
|
537
600
|
case 'remove-and-ask-db':
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
601
|
+
if (ASK_DELETE_FROM_DB_CONFIRM) {
|
|
602
|
+
if (deleteDialogResolve) deleteDialogResolve({ proceed: true, deleteFromDb: false });
|
|
603
|
+
deleteDialogResolve = null;
|
|
604
|
+
deleteDialogStep = 'confirm-delete-db';
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
deleteDialogOpen = false;
|
|
608
|
+
if (deleteDialogResolve) deleteDialogResolve({ proceed: true, deleteFromDb: true });
|
|
609
|
+
break;
|
|
542
610
|
case 'keep-in-db':
|
|
543
611
|
deleteDialogOpen = false;
|
|
544
612
|
showToast('info', 'Shape removed', { description: 'Widget kept in database' });
|
|
@@ -559,6 +627,7 @@
|
|
|
559
627
|
if (!widgetsClient || !hasValidToken) return;
|
|
560
628
|
try {
|
|
561
629
|
console.log('[WidgetCreator] Deleting widget from DB:', widgetId);
|
|
630
|
+
closeWidgetDetailsPanel(widgetId);
|
|
562
631
|
await widgetsClient.deleteWidget({ id: widgetId });
|
|
563
632
|
widgets = widgets.filter((w: any) => w.id !== widgetId);
|
|
564
633
|
if (selectedWidgetId === widgetId) {
|
|
@@ -663,6 +732,8 @@
|
|
|
663
732
|
const draftConvertScreenshots = new Map<string, string>();
|
|
664
733
|
/** True while a draft conversion is being finalized (widget creation + shape replace). */
|
|
665
734
|
const finalizingDraftConversions = new Set<string>();
|
|
735
|
+
/** Keep Widget Details on this convert/draft widget until the user closes the panel. */
|
|
736
|
+
let detailsSessionWidgetId: string | null = null;
|
|
666
737
|
|
|
667
738
|
// Chat component reference and context (focused panel)
|
|
668
739
|
let chatMethods: {
|
|
@@ -681,10 +752,13 @@
|
|
|
681
752
|
markWidgetHeadAtLatestCommit?: () => void;
|
|
682
753
|
updateLatestCodegenStatusMessage?: (content: string) => void;
|
|
683
754
|
reloadConversationMessages?: () => Promise<void>;
|
|
684
|
-
|
|
685
|
-
|
|
755
|
+
selectConversation?: (conversationId: string) => void | Promise<void>;
|
|
756
|
+
updateLastUserMessageAttachments?: (attachments: Array<{ id?: string; url: string; file_type?: string; file_name?: string | null }>) => void;
|
|
686
757
|
} | null = null;
|
|
687
758
|
let chatMethodsByWidgetId: Record<string, NonNullable<typeof chatMethods>> = {};
|
|
759
|
+
/** Conversation `completed` flag per widget — ActionBar Complete / Completed. */
|
|
760
|
+
let conversationCompletedByWidgetId: Record<string, boolean> = {};
|
|
761
|
+
let conversationsByWidgetId: Record<string, any[]> = {};
|
|
688
762
|
let pendingChatDraftText = '';
|
|
689
763
|
let chatDraftRestoredForWidgetId: string | null = null;
|
|
690
764
|
/**
|
|
@@ -704,7 +778,7 @@
|
|
|
704
778
|
|
|
705
779
|
/** Prompt mentions a reference image but chat has no attachment (common UX mistake). */
|
|
706
780
|
function promptImpliesVisionReference(prompt: string): boolean {
|
|
707
|
-
return
|
|
781
|
+
return /(attached\s+image|image\s+attached|screenshot|reference\s+image|see\s+(the\s+)?image|cu\s+imagine|imagine[aă]?\s+ata[sș]|ata[sș]at[ăa]?\s+imagine)/i.test(
|
|
708
782
|
prompt,
|
|
709
783
|
);
|
|
710
784
|
}
|
|
@@ -713,6 +787,9 @@
|
|
|
713
787
|
file?: File,
|
|
714
788
|
uploadedUrl?: string,
|
|
715
789
|
): Promise<string | null> {
|
|
790
|
+
if (file && !file.type.startsWith('image/')) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
716
793
|
if (uploadedUrl && /^https?:\/\//i.test(uploadedUrl)) {
|
|
717
794
|
try {
|
|
718
795
|
const response = await fetch(uploadedUrl);
|
|
@@ -740,7 +817,7 @@
|
|
|
740
817
|
|
|
741
818
|
async function handleChatUploadFile(
|
|
742
819
|
file: File,
|
|
743
|
-
options?: { onProgress?: (percentage: number) => void; source?: string },
|
|
820
|
+
options?: { onProgress?: (percentage: number) => void; source?: string; widgetId?: string },
|
|
744
821
|
) {
|
|
745
822
|
if (!authenticatedClientsToken) {
|
|
746
823
|
throw new Error('Upload client not initialized');
|
|
@@ -750,6 +827,7 @@
|
|
|
750
827
|
const uploadBasePath =
|
|
751
828
|
apiBasePath ||
|
|
752
829
|
`${(apiRoot || import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/+$/, '')}/v1`;
|
|
830
|
+
const persistWidgetId = options?.widgetId || selectedWidgetId || undefined;
|
|
753
831
|
const result = await uploadChatFileToWidgeticApi(
|
|
754
832
|
file,
|
|
755
833
|
{
|
|
@@ -757,7 +835,7 @@
|
|
|
757
835
|
accessToken: authenticatedClientsToken,
|
|
758
836
|
contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
|
|
759
837
|
source: options?.source || 'chat-upload',
|
|
760
|
-
widgetId:
|
|
838
|
+
widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
|
|
761
839
|
conversationId: currentChatConversationId || undefined,
|
|
762
840
|
},
|
|
763
841
|
{ onProgress: options?.onProgress },
|
|
@@ -816,6 +894,7 @@
|
|
|
816
894
|
|
|
817
895
|
const panelChat = getChatMethodsForWidget(codegenWidgetId);
|
|
818
896
|
const cdnUploadIds = (attachments || [])
|
|
897
|
+
.filter((attachment) => attachment.uploadId && attachment.fileType.startsWith('image/'))
|
|
819
898
|
.map((attachment) => attachment.uploadId)
|
|
820
899
|
.filter((id): id is string => Boolean(id));
|
|
821
900
|
|
|
@@ -866,24 +945,58 @@
|
|
|
866
945
|
? promptImages
|
|
867
946
|
: getPanelState(codegenWidgetId).promptImages;
|
|
868
947
|
const hasImages = effectivePromptImages.length > 0 || cdnUploadIds.length > 0;
|
|
869
|
-
|
|
948
|
+
const hasNonImageFiles = (attachments || []).some(
|
|
949
|
+
(attachment) => attachment.fileType && !attachment.fileType.startsWith('image/'),
|
|
950
|
+
);
|
|
951
|
+
if (!hasImages && !hasNonImageFiles && promptImpliesVisionReference(content)) {
|
|
952
|
+
const missingImageError =
|
|
953
|
+
'Promptul menționează o imagine atașată, dar în chat nu e niciun attachment. Folosește butonul Upload.';
|
|
870
954
|
showToast('warning', 'Lipsește imaginea de referință', {
|
|
871
|
-
description:
|
|
872
|
-
'Promptul menționează o imagine atașată, dar în chat nu e niciun attachment. Folosește butonul Upload sau ?attachStairsRef=1.',
|
|
955
|
+
description: missingImageError,
|
|
873
956
|
duration: 10000,
|
|
874
957
|
});
|
|
958
|
+
patchCodegenUiState(codegenWidgetId, {
|
|
959
|
+
lastGenerationFailed: true,
|
|
960
|
+
lastGenerationError: missingImageError,
|
|
961
|
+
generateCodeStatus: '❌ Missing reference image',
|
|
962
|
+
});
|
|
963
|
+
pushCodegenFailureToChat(codegenWidgetId, missingImageError);
|
|
875
964
|
return;
|
|
876
965
|
}
|
|
877
|
-
|
|
966
|
+
const persistFileAttachments = (attachments || [])
|
|
967
|
+
.filter((attachment) =>
|
|
968
|
+
!!attachment.file
|
|
969
|
+
|| (attachment.url && (attachment.url.startsWith('blob:') || attachment.url.startsWith('data:') || /^https?:\/\//i.test(attachment.url))),
|
|
970
|
+
)
|
|
971
|
+
.map((attachment, i) => ({
|
|
972
|
+
url: attachment.url,
|
|
973
|
+
file: attachment.file,
|
|
974
|
+
file_type: guessAttachmentFileType(attachment.url || '', attachment.fileName, attachment.fileType),
|
|
975
|
+
file_name: attachment.fileName,
|
|
976
|
+
file_size: attachment.fileSize,
|
|
977
|
+
display_order: i + 1,
|
|
978
|
+
}));
|
|
979
|
+
// Chat.onSaveMessage already persisted this turn. generateCode keeps one
|
|
980
|
+
// ensureCodegenUserMessagePersisted after imagesToSend is finalized.
|
|
878
981
|
await generateCode({
|
|
879
982
|
prompt: content,
|
|
880
983
|
fromChat: true,
|
|
881
984
|
widgetId: codegenWidgetId,
|
|
882
985
|
attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
|
|
883
986
|
promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
|
|
987
|
+
persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
|
|
884
988
|
});
|
|
885
989
|
}
|
|
886
990
|
|
|
991
|
+
function pushCodegenFailureToChat(widgetId: string, errorText: string) {
|
|
992
|
+
const panelChat = getChatMethodsForWidget(widgetId);
|
|
993
|
+
const content = /^Code generation failed/i.test(errorText)
|
|
994
|
+
? errorText
|
|
995
|
+
: `Code generation failed: ${errorText}`;
|
|
996
|
+
panelChat?.addAssistantMessage?.(content);
|
|
997
|
+
console.log('[Chat] Codegen failure bubble:', content.slice(0, 160));
|
|
998
|
+
}
|
|
999
|
+
|
|
887
1000
|
/** Retry failed codegen without duplicating the user bubble in chat. */
|
|
888
1001
|
async function handleChatRetryMessage(content: string, sourceWidgetId?: string) {
|
|
889
1002
|
const widgetId = sourceWidgetId ?? selectedWidgetId;
|
|
@@ -894,7 +1007,53 @@
|
|
|
894
1007
|
':',
|
|
895
1008
|
content.substring(0, 80),
|
|
896
1009
|
);
|
|
897
|
-
|
|
1010
|
+
const widget = getWidgetById(widgetId) || (selectedWidget?.id === widgetId ? selectedWidget : null);
|
|
1011
|
+
if (widget) {
|
|
1012
|
+
await ensureWidgetRepository(widget, { forceRecreate: true, silent: true });
|
|
1013
|
+
}
|
|
1014
|
+
const panelChat = getChatMethodsForWidget(widgetId);
|
|
1015
|
+
const draftUploadIds = panelChat?.getAttachmentUploadIds?.() ?? [];
|
|
1016
|
+
const draftImageUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
|
|
1017
|
+
|
|
1018
|
+
// Re-use attachments from previous user messages in the conversation
|
|
1019
|
+
// so that Retry doesn't lose PDFs/files that were uploaded with the original prompt.
|
|
1020
|
+
let retryFileAttachments: Array<{ url: string; file_type: string; file_name: string | null; file_size: number | null }> = [];
|
|
1021
|
+
if (draftUploadIds.length === 0 && draftImageUrls.length === 0) {
|
|
1022
|
+
try {
|
|
1023
|
+
const panelState = getPanelState(widgetId);
|
|
1024
|
+
const convId = panelChat?.getConversationId?.() ?? panelState.currentChatConversationId ?? null;
|
|
1025
|
+
if (convId) {
|
|
1026
|
+
const convMessages = await handleLoadMessages(convId, widgetId);
|
|
1027
|
+
// Find most-recent user message that has persisted attachments
|
|
1028
|
+
const userMsgsWithAtt = (convMessages || [])
|
|
1029
|
+
.filter((m: any) => (m.messageType || m.message_type) === 'user' && (m.attachments || []).length > 0);
|
|
1030
|
+
if (userMsgsWithAtt.length > 0) {
|
|
1031
|
+
const latestWithAtt = userMsgsWithAtt[userMsgsWithAtt.length - 1];
|
|
1032
|
+
retryFileAttachments = (latestWithAtt.attachments || [])
|
|
1033
|
+
.filter((a: any) => a.url && /^https?:\/\//i.test(a.url))
|
|
1034
|
+
.map((a: any) => ({
|
|
1035
|
+
url: a.url,
|
|
1036
|
+
file_type: a.file_type || 'application/octet-stream',
|
|
1037
|
+
file_name: a.file_name || null,
|
|
1038
|
+
file_size: a.file_size || null,
|
|
1039
|
+
}));
|
|
1040
|
+
console.log('[Chat→Retry] Re-using', retryFileAttachments.length, 'attachment(s) from previous user message for retry');
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
} catch (retryAttErr) {
|
|
1044
|
+
console.warn('[Chat→Retry] Could not load previous attachments for retry:', retryAttErr);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
await generateCode({
|
|
1049
|
+
prompt: content,
|
|
1050
|
+
fromChat: true,
|
|
1051
|
+
widgetId,
|
|
1052
|
+
forceRecreateRepo: true,
|
|
1053
|
+
attachmentIds: draftUploadIds.length > 0 ? draftUploadIds : undefined,
|
|
1054
|
+
promptImagesOverride: draftImageUrls.length > 0 ? draftImageUrls : undefined,
|
|
1055
|
+
persistFileAttachments: retryFileAttachments.length > 0 ? retryFileAttachments : undefined,
|
|
1056
|
+
});
|
|
898
1057
|
}
|
|
899
1058
|
|
|
900
1059
|
function handleChatConversationChange(conversationId: string | null, sourceWidgetId?: string) {
|
|
@@ -905,6 +1064,86 @@
|
|
|
905
1064
|
currentChatConversationId = conversationId;
|
|
906
1065
|
}
|
|
907
1066
|
console.log('[Chat] Active conversation changed for', widgetId.substring(0, 8), ':', conversationId || 'none');
|
|
1067
|
+
const conv = (conversationsByWidgetId[widgetId] || []).find((c) => c.id === conversationId);
|
|
1068
|
+
if (conv) {
|
|
1069
|
+
const completed = !!(conv.completed ?? conv.is_completed ?? conv.isCompleted);
|
|
1070
|
+
conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: completed };
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function isConversationRecordCompleted(conversation: any): boolean {
|
|
1075
|
+
return !!(conversation?.completed ?? conversation?.is_completed ?? conversation?.isCompleted);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function isUuid(value: string | null | undefined): value is string {
|
|
1079
|
+
return !!value && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function resolvePersistableConversationId(widgetId: string, preferredId?: string | null): string | null {
|
|
1083
|
+
const knownIds = (conversationsByWidgetId[widgetId] || [])
|
|
1084
|
+
.map((c) => c.id)
|
|
1085
|
+
.filter((id) => isUuid(id));
|
|
1086
|
+
const fromChat = getChatMethodsForWidget(widgetId)?.getConversationId?.();
|
|
1087
|
+
const fromFocus = widgetId === focusedPanelWidgetId ? currentChatConversationId : null;
|
|
1088
|
+
const preferredKnown = [preferredId, fromChat, fromFocus].find((id) => id && knownIds.includes(id));
|
|
1089
|
+
if (preferredKnown) return preferredKnown;
|
|
1090
|
+
if (knownIds[0]) return knownIds[0];
|
|
1091
|
+
if (isUuid(preferredId)) return preferredId;
|
|
1092
|
+
if (isUuid(fromChat)) return fromChat;
|
|
1093
|
+
return isUuid(fromFocus) ? fromFocus : null;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function handleChatCompletionToggle(
|
|
1097
|
+
widgetId: string,
|
|
1098
|
+
detail?: { conversationId?: string | null; completed?: boolean },
|
|
1099
|
+
) {
|
|
1100
|
+
const next = typeof detail?.completed === 'boolean'
|
|
1101
|
+
? detail.completed
|
|
1102
|
+
: !conversationCompletedByWidgetId[widgetId];
|
|
1103
|
+
conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: next };
|
|
1104
|
+
let conversationId = resolvePersistableConversationId(widgetId, detail?.conversationId);
|
|
1105
|
+
console.log('[Chat] Complete toggle', widgetId.substring(0, 8), next ? 'completed' : 'active', conversationId);
|
|
1106
|
+
if (!conversationsClient) {
|
|
1107
|
+
console.warn('[Chat] Complete toggle skipped persist — missing conversations client');
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
if (!conversationId) {
|
|
1112
|
+
const created = await handleCreateConversation(widgetId, 'Widget conversation');
|
|
1113
|
+
conversationId = created?.id || null;
|
|
1114
|
+
console.log('[Chat] Complete created conversation for persist', conversationId);
|
|
1115
|
+
}
|
|
1116
|
+
if (!conversationId || !isUuid(conversationId)) {
|
|
1117
|
+
throw new Error('No persistable conversation id');
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
await conversationsClient.updateConversation({
|
|
1121
|
+
conversationId,
|
|
1122
|
+
updateConversationRequest: { completed: next },
|
|
1123
|
+
});
|
|
1124
|
+
} catch (updateError) {
|
|
1125
|
+
const listed = await handleLoadConversations(widgetId);
|
|
1126
|
+
const fallbackId = (listed || []).map((c) => c.id).find((id) => isUuid(id) && id !== conversationId);
|
|
1127
|
+
if (!fallbackId) throw updateError;
|
|
1128
|
+
console.warn('[Chat] Complete retry with loaded conversation', fallbackId);
|
|
1129
|
+
conversationId = fallbackId;
|
|
1130
|
+
await conversationsClient.updateConversation({
|
|
1131
|
+
conversationId,
|
|
1132
|
+
updateConversationRequest: { completed: next },
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
conversationsByWidgetId = {
|
|
1136
|
+
...conversationsByWidgetId,
|
|
1137
|
+
[widgetId]: (conversationsByWidgetId[widgetId] || []).map((c) =>
|
|
1138
|
+
c.id === conversationId ? { ...c, completed: next } : c,
|
|
1139
|
+
),
|
|
1140
|
+
};
|
|
1141
|
+
console.log('[Chat] Conversation marked', next ? 'completed' : 'active', conversationId);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
console.error('[Chat] Failed to persist conversation completed:', error);
|
|
1144
|
+
showToast('error', 'Could not update conversation status');
|
|
1145
|
+
conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: !next };
|
|
1146
|
+
}
|
|
908
1147
|
}
|
|
909
1148
|
|
|
910
1149
|
/** Sync unsent chat draft + attachments to the durable per-widget draft (or clear when empty). */
|
|
@@ -970,7 +1209,43 @@
|
|
|
970
1209
|
* picked, which may differ from convos[0] when several empty conversations
|
|
971
1210
|
* exist (feedback c: chat looked empty while messages went elsewhere).
|
|
972
1211
|
*/
|
|
973
|
-
|
|
1212
|
+
function guessAttachmentFileType(url: string, fileName?: string | null, fallback?: string | null): string {
|
|
1213
|
+
if (fallback && String(fallback).includes('/')) return String(fallback);
|
|
1214
|
+
const hint = `${url || ''} ${fileName || ''}`;
|
|
1215
|
+
if (/\.pdf(\?|$)/i.test(hint)) return 'application/pdf';
|
|
1216
|
+
if (/\.(mp4|webm|mov)(\?|$)/i.test(hint)) return 'video/mp4';
|
|
1217
|
+
if (/\.(png|jpe?g|gif|webp|svg)(\?|$)/i.test(hint)) return 'image/png';
|
|
1218
|
+
if (fallback) return String(fallback);
|
|
1219
|
+
return 'application/octet-stream';
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function uniquePersistImageUrls(urls: string[]): string[] {
|
|
1223
|
+
const seen = new Set<string>();
|
|
1224
|
+
const httpsUrls: string[] = [];
|
|
1225
|
+
const dataUrls: string[] = [];
|
|
1226
|
+
for (const url of urls) {
|
|
1227
|
+
if (typeof url !== 'string' || url.length < 20 || seen.has(url)) continue;
|
|
1228
|
+
seen.add(url);
|
|
1229
|
+
if (/^https?:\/\//i.test(url)) httpsUrls.push(url);
|
|
1230
|
+
else if (url.startsWith('data:image/')) dataUrls.push(url);
|
|
1231
|
+
}
|
|
1232
|
+
// Same screenshot often exists as CDN URL + compressed data URL — keep HTTPS only.
|
|
1233
|
+
return httpsUrls.length > 0 ? httpsUrls : dataUrls;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async function ensureCodegenUserMessagePersisted(
|
|
1237
|
+
widgetId: string,
|
|
1238
|
+
prompt: string,
|
|
1239
|
+
imageUrls?: string[],
|
|
1240
|
+
fileAttachments?: Array<{
|
|
1241
|
+
url?: string;
|
|
1242
|
+
file?: File;
|
|
1243
|
+
file_type?: string;
|
|
1244
|
+
file_name?: string | null;
|
|
1245
|
+
file_size?: number | null;
|
|
1246
|
+
display_order?: number;
|
|
1247
|
+
}>,
|
|
1248
|
+
): Promise<void> {
|
|
974
1249
|
if (!conversationsClient || !messagesClient) return;
|
|
975
1250
|
try {
|
|
976
1251
|
const panelChat = getChatMethodsForWidget(widgetId);
|
|
@@ -990,20 +1265,123 @@
|
|
|
990
1265
|
convId = conv?.id;
|
|
991
1266
|
}
|
|
992
1267
|
if (!convId) return;
|
|
993
|
-
if (convId !== visibleConversationId) {
|
|
994
|
-
// Point the panel chat at the conversation that will hold the messages
|
|
995
|
-
// so a later reopen shows the same thread the codegen used.
|
|
996
|
-
panelChat?.selectConversation?.(convId);
|
|
997
|
-
patchPanelSnapshot(widgetId, { currentChatConversationId: convId });
|
|
998
|
-
}
|
|
999
1268
|
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1269
|
+
const persistImages = uniquePersistImageUrls([
|
|
1270
|
+
...(imageUrls || []),
|
|
1271
|
+
...(getPanelState(widgetId).promptImages ?? []),
|
|
1272
|
+
]);
|
|
1273
|
+
const compressedPersistImages: string[] = [];
|
|
1274
|
+
for (const url of persistImages) {
|
|
1275
|
+
if (typeof url === 'string' && url.startsWith('data:image/')) {
|
|
1276
|
+
compressedPersistImages.push(await compressImageDataUrlForCodegen(url));
|
|
1277
|
+
} else if (typeof url === 'string' && /^https?:\/\//i.test(url)) {
|
|
1278
|
+
compressedPersistImages.push(url);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const persistFileItems = (fileAttachments || [])
|
|
1282
|
+
.filter((att) =>
|
|
1283
|
+
(typeof att.url === 'string' && (att.url.startsWith('blob:') || att.url.startsWith('data:') || /^https?:\/\//i.test(att.url)))
|
|
1284
|
+
|| !!(att as { file?: File }).file,
|
|
1285
|
+
)
|
|
1286
|
+
.map((att, i) => ({
|
|
1287
|
+
url: att.url || '',
|
|
1288
|
+
file: (att as { file?: File }).file,
|
|
1289
|
+
file_type: guessAttachmentFileType(att.url || '', att.file_name, att.file_type),
|
|
1290
|
+
file_name: att.file_name || (att as { file?: File }).file?.name || `attachment-${i + 1}`,
|
|
1291
|
+
file_size: att.file_size && att.file_size > 0 ? att.file_size : (att as { file?: File }).file?.size,
|
|
1292
|
+
display_order: att.display_order ?? i + 1,
|
|
1293
|
+
}));
|
|
1294
|
+
if (compressedPersistImages.length === 0 && persistFileItems.length === 0) {
|
|
1295
|
+
console.warn('[Codegen] Persist user message has no attachment URLs for widget', widgetId?.substring(0, 8));
|
|
1296
|
+
}
|
|
1297
|
+
const fileHasImage = persistFileItems.some((att) => {
|
|
1298
|
+
const type = String(att.file_type || '');
|
|
1299
|
+
const url = String(att.url || '');
|
|
1300
|
+
return type.startsWith('image/') || url.startsWith('data:image/') || /\.(png|jpe?g|gif|webp|svg)(\?|$)/i.test(url);
|
|
1301
|
+
});
|
|
1302
|
+
const imagePersistItems = fileHasImage
|
|
1303
|
+
? []
|
|
1304
|
+
: compressedPersistImages.map((url, i) => ({
|
|
1305
|
+
url,
|
|
1306
|
+
file_type: guessAttachmentFileType(url, 'design-screenshot.png', 'image/png'),
|
|
1307
|
+
file_name: 'design-screenshot.png',
|
|
1308
|
+
display_order: i + 1,
|
|
1309
|
+
}));
|
|
1310
|
+
const persistAttachments = [...persistFileItems, ...imagePersistItems].map((att, i) => ({
|
|
1311
|
+
...att,
|
|
1312
|
+
display_order: i + 1,
|
|
1313
|
+
}));
|
|
1314
|
+
const convMessages = await handleLoadMessages(convId, widgetId);
|
|
1315
|
+
const userMessages = (convMessages || []).filter(
|
|
1002
1316
|
(m) => (m.messageType || (m as any).message_type) === 'user'
|
|
1003
1317
|
);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1318
|
+
const promptNeedle = (prompt || '').trim();
|
|
1319
|
+
const targetUser =
|
|
1320
|
+
(promptNeedle
|
|
1321
|
+
? [...userMessages].reverse().find((m) => {
|
|
1322
|
+
const text = String(m.messageContent || (m as any).message_content || '').trim();
|
|
1323
|
+
return (
|
|
1324
|
+
text === promptNeedle ||
|
|
1325
|
+
text.endsWith(promptNeedle) ||
|
|
1326
|
+
(promptNeedle.length >= 12 && text.includes(promptNeedle.slice(0, 80)))
|
|
1327
|
+
);
|
|
1328
|
+
})
|
|
1329
|
+
: null) || userMessages[userMessages.length - 1] || null;
|
|
1330
|
+
const existingUrls = new Set(
|
|
1331
|
+
(targetUser?.attachments || [])
|
|
1332
|
+
.map((a: { url?: string }) => a.url)
|
|
1333
|
+
.filter((url: string | undefined): url is string => !!url),
|
|
1334
|
+
);
|
|
1335
|
+
const existingHasRemoteImage = [...existingUrls].some((url) => /^https?:\/\//i.test(url));
|
|
1336
|
+
const missingPersist = persistAttachments.filter((att) => {
|
|
1337
|
+
if (att.url && existingUrls.has(att.url)) return false;
|
|
1338
|
+
if (existingHasRemoteImage && typeof att.url === 'string' && att.url.startsWith('data:')) return false;
|
|
1339
|
+
if (existingHasRemoteImage && String(att.file_type || '').startsWith('image/')) return false;
|
|
1340
|
+
return true;
|
|
1341
|
+
});
|
|
1342
|
+
let savedAttachmentCount = targetUser?.attachments?.length || 0;
|
|
1343
|
+
if (!targetUser) {
|
|
1344
|
+
const saved = await handleSaveMessage(
|
|
1345
|
+
convId,
|
|
1346
|
+
buildUserPromptMessageContent(prompt),
|
|
1347
|
+
'user',
|
|
1348
|
+
persistAttachments.length > 0 ? persistAttachments : undefined,
|
|
1349
|
+
widgetId,
|
|
1350
|
+
);
|
|
1351
|
+
const savedAtt = saved?.attachments || saved?.data?.attachments || [];
|
|
1352
|
+
savedAttachmentCount = Array.isArray(savedAtt) ? savedAtt.length : 0;
|
|
1353
|
+
console.log('[Codegen] Persisted initial user prompt on generate:', convId, 'attachments:', persistAttachments.length, 'saved:', savedAttachmentCount);
|
|
1354
|
+
if (Array.isArray(savedAtt) && savedAtt.length > 0) {
|
|
1355
|
+
panelChat?.updateLastUserMessageAttachments?.(savedAtt);
|
|
1356
|
+
}
|
|
1357
|
+
} else if (missingPersist.length > 0) {
|
|
1358
|
+
const merged = [...(targetUser.attachments || []), ...missingPersist];
|
|
1359
|
+
const apiAtt = await mapChatAttachmentsForApi(merged, widgetId);
|
|
1360
|
+
if (apiAtt.length > 0) {
|
|
1361
|
+
try {
|
|
1362
|
+
await messagesClient.updateMessageRaw({
|
|
1363
|
+
conversationId: convId,
|
|
1364
|
+
messageId: targetUser.id,
|
|
1365
|
+
updateMessageRequest: { attachments: apiAtt },
|
|
1366
|
+
});
|
|
1367
|
+
savedAttachmentCount = apiAtt.length;
|
|
1368
|
+
console.log('[Codegen] Saved missing attachments on user message:', targetUser.id, apiAtt.length);
|
|
1369
|
+
} catch (updateErr) {
|
|
1370
|
+
console.warn('[Codegen] Failed to PATCH user message attachments:', updateErr);
|
|
1371
|
+
}
|
|
1372
|
+
panelChat?.updateLastUserMessageAttachments?.(apiAtt);
|
|
1373
|
+
} else {
|
|
1374
|
+
console.warn('[Codegen] mapChatAttachmentsForApi returned 0 items — attachments not stored');
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (persistAttachments.length > 0 && savedAttachmentCount === 0) {
|
|
1378
|
+
console.error('[Codegen] User prompt saved without attachments — chat reopen will show placeholder');
|
|
1379
|
+
}
|
|
1380
|
+
if (convId !== visibleConversationId) {
|
|
1381
|
+
await panelChat?.selectConversation?.(convId);
|
|
1382
|
+
patchPanelSnapshot(widgetId, { currentChatConversationId: convId });
|
|
1383
|
+
} else {
|
|
1384
|
+
console.log('[Codegen] Keep local chat attachments after persist; skip reload wipe');
|
|
1007
1385
|
}
|
|
1008
1386
|
} catch (err) {
|
|
1009
1387
|
console.warn('[Codegen] Could not persist user prompt before generation:', err);
|
|
@@ -1028,18 +1406,27 @@
|
|
|
1028
1406
|
console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
|
|
1029
1407
|
}
|
|
1030
1408
|
|
|
1031
|
-
/** Screenshot: capture widget preview iframe and return data URL */
|
|
1032
|
-
async function handleChatScreenshot(): Promise<string | null> {
|
|
1033
|
-
|
|
1034
|
-
|
|
1409
|
+
/** Screenshot: capture this panel's widget preview iframe and return data URL */
|
|
1410
|
+
async function handleChatScreenshot(widgetId?: string | null): Promise<string | null> {
|
|
1411
|
+
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1412
|
+
const panelRef = getWidgetDetailsRef(targetId);
|
|
1413
|
+
const iframe =
|
|
1414
|
+
(panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null) ||
|
|
1415
|
+
(document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement | null);
|
|
1416
|
+
|
|
1417
|
+
if (!iframe?.contentWindow) {
|
|
1418
|
+
console.log('[Chat→Screenshot] Preview iframe not ready', {
|
|
1419
|
+
targetId: targetId?.substring(0, 8),
|
|
1420
|
+
hasValidPreview: panelRef?.hasValidPreview?.() ?? false,
|
|
1421
|
+
});
|
|
1035
1422
|
return null;
|
|
1036
1423
|
}
|
|
1037
|
-
const iframe = document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement;
|
|
1038
|
-
if (!iframe?.contentWindow) return null;
|
|
1039
1424
|
|
|
1425
|
+
console.log('[Chat→Screenshot] Requesting screenshot from preview iframe', targetId?.substring(0, 8));
|
|
1040
1426
|
return new Promise<string | null>((resolve) => {
|
|
1041
1427
|
const timeout = setTimeout(() => {
|
|
1042
1428
|
window.removeEventListener('message', handler);
|
|
1429
|
+
console.warn('[Chat→Screenshot] Timed out waiting for widgetic:screenshot');
|
|
1043
1430
|
resolve(null);
|
|
1044
1431
|
}, 8000);
|
|
1045
1432
|
|
|
@@ -1087,6 +1474,13 @@
|
|
|
1087
1474
|
});
|
|
1088
1475
|
const json = await response.raw.json();
|
|
1089
1476
|
const conversations = normalizeConversationRecords(json);
|
|
1477
|
+
conversationsByWidgetId = { ...conversationsByWidgetId, [contextId]: conversations };
|
|
1478
|
+
if (conversations[0]) {
|
|
1479
|
+
conversationCompletedByWidgetId = {
|
|
1480
|
+
...conversationCompletedByWidgetId,
|
|
1481
|
+
[contextId]: isConversationRecordCompleted(conversations[0]),
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1090
1484
|
console.log('[Chat→Backend] Loaded conversations for widget:', contextId, conversations.length);
|
|
1091
1485
|
return conversations;
|
|
1092
1486
|
} catch (error) {
|
|
@@ -1139,26 +1533,40 @@
|
|
|
1139
1533
|
const json = await response.raw.json();
|
|
1140
1534
|
const messages = json?.data || [];
|
|
1141
1535
|
console.log('[Chat→Backend] Loaded messages for conversation:', conversationId, messages.length);
|
|
1142
|
-
const mappedMessages = messages.map((m: any) =>
|
|
1536
|
+
const mappedMessages = messages.map((m: any) => {
|
|
1537
|
+
const rawAttachments = m.attachments || m.message_contents || m.messageContents || [];
|
|
1538
|
+
const attachments = (Array.isArray(rawAttachments) ? rawAttachments : []).map((a: any, idx: number) => {
|
|
1539
|
+
const nested = a?.contents || a?.content;
|
|
1540
|
+
const content = Array.isArray(nested) ? (nested[0] || {}) : (nested || a || {});
|
|
1541
|
+
const url = content.url || content.public_url || content.publicUrl || content.file_url || a.url || '';
|
|
1542
|
+
let fileType = content.file_type || content.fileType || a.file_type || a.fileType || '';
|
|
1543
|
+
const fileName = content.file_name || content.fileName || a.file_name || a.fileName || null;
|
|
1544
|
+
if (!fileType || !String(fileType).includes('/')) {
|
|
1545
|
+
fileType = guessAttachmentFileType(url, fileName, fileType);
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
id: content.id || a.id || `att_${idx}`,
|
|
1549
|
+
url,
|
|
1550
|
+
file_type: fileType,
|
|
1551
|
+
file_name: fileName,
|
|
1552
|
+
file_size: content.file_size ?? content.fileSize ?? a.file_size ?? a.fileSize ?? null,
|
|
1553
|
+
display_order: a.display_order ?? a.displayOrder ?? content.display_order ?? idx + 1,
|
|
1554
|
+
created_at: content.created_at || content.createdAt || a.created_at || a.createdAt || new Date().toISOString(),
|
|
1555
|
+
};
|
|
1556
|
+
}).filter((a: { url: string }) => !!a.url);
|
|
1557
|
+
return {
|
|
1143
1558
|
id: m.id,
|
|
1144
1559
|
conversationId: m.conversation_id || m.conversationId || conversationId,
|
|
1145
1560
|
messageContent: m.message_content || m.messageContent || '',
|
|
1146
1561
|
messageType: m.message_type || m.messageType || 'user',
|
|
1147
|
-
attachments
|
|
1148
|
-
id: a.id || `att_${idx}`,
|
|
1149
|
-
url: a.url || '',
|
|
1150
|
-
file_type: a.file_type || a.fileType || 'image/png',
|
|
1151
|
-
file_name: a.file_name || a.fileName || null,
|
|
1152
|
-
file_size: a.file_size ?? a.fileSize ?? null,
|
|
1153
|
-
display_order: a.display_order ?? a.displayOrder ?? idx + 1,
|
|
1154
|
-
created_at: a.created_at || a.createdAt || new Date().toISOString(),
|
|
1155
|
-
})),
|
|
1562
|
+
attachments,
|
|
1156
1563
|
isCommit: m.is_commit || m.isCommit || false,
|
|
1157
1564
|
userId: m.user_id || m.userId || '',
|
|
1158
1565
|
createdAt: new Date(m.created_at || m.createdAt),
|
|
1159
1566
|
updatedAt: new Date(m.updated_at || m.updatedAt || m.created_at || m.createdAt),
|
|
1160
1567
|
rolledBackAt: m.rolled_back_at || m.rolledBackAt || null,
|
|
1161
|
-
}
|
|
1568
|
+
};
|
|
1569
|
+
});
|
|
1162
1570
|
const withHead = markWidgetHeadOnLoadedMessages(mappedMessages);
|
|
1163
1571
|
const syncWidgetId = widgetIdForSync ?? selectedWidgetId;
|
|
1164
1572
|
if (syncWidgetId) {
|
|
@@ -1188,6 +1596,15 @@
|
|
|
1188
1596
|
const conversation = json?.data || json;
|
|
1189
1597
|
const conversationId = conversation?.id || conversation?.conversation_id || conversation?.conversationId;
|
|
1190
1598
|
console.log('[Chat→Backend] Created conversation:', conversationId);
|
|
1599
|
+
if (conversationId && contextId) {
|
|
1600
|
+
conversationsByWidgetId = {
|
|
1601
|
+
...conversationsByWidgetId,
|
|
1602
|
+
[contextId]: [
|
|
1603
|
+
{ ...conversation, id: conversationId },
|
|
1604
|
+
...(conversationsByWidgetId[contextId] || []).filter((c) => c.id !== conversationId),
|
|
1605
|
+
],
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1191
1608
|
return conversationId ? { ...conversation, id: conversationId } : conversation;
|
|
1192
1609
|
} catch (error) {
|
|
1193
1610
|
console.error('[Chat→Backend] Failed to create conversation:', error);
|
|
@@ -1195,48 +1612,96 @@
|
|
|
1195
1612
|
}
|
|
1196
1613
|
}
|
|
1197
1614
|
|
|
1198
|
-
|
|
1615
|
+
function dataUrlToUploadFile(dataUrl: string, fileName: string): File | null {
|
|
1616
|
+
try {
|
|
1617
|
+
const comma = dataUrl.indexOf(',');
|
|
1618
|
+
if (comma < 0) return null;
|
|
1619
|
+
const header = dataUrl.slice(0, comma);
|
|
1620
|
+
const mimeMatch = header.match(/data:([^;,]+)/);
|
|
1621
|
+
const mimeType = mimeMatch?.[1] || 'application/octet-stream';
|
|
1622
|
+
const binary = atob(dataUrl.slice(comma + 1));
|
|
1623
|
+
const bytes = new Uint8Array(binary.length);
|
|
1624
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1625
|
+
return new File([bytes], fileName, { type: mimeType });
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
console.warn('[Chat→Backend] Could not convert data URL to File:', err);
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/** Map chat attachments to API items. Inline data/blob files are uploaded to user storage (CDN URL). */
|
|
1199
1633
|
async function mapChatAttachmentsForApi(
|
|
1200
1634
|
attachments?: Array<{
|
|
1201
1635
|
url?: string;
|
|
1636
|
+
file?: File;
|
|
1202
1637
|
file_type?: string;
|
|
1203
1638
|
file_name?: string | null;
|
|
1204
1639
|
file_size?: number | null;
|
|
1205
1640
|
display_order?: number;
|
|
1206
1641
|
}>,
|
|
1642
|
+
persistWidgetId?: string | null,
|
|
1207
1643
|
): Promise<Array<{ url: string; file_type: string; file_name?: string; file_size?: number; display_order?: number }>> {
|
|
1208
1644
|
if (!attachments?.length) return [];
|
|
1209
1645
|
const items: Array<{ url: string; file_type: string; file_name?: string; file_size?: number; display_order?: number }> = [];
|
|
1210
1646
|
for (let i = 0; i < attachments.length; i++) {
|
|
1211
1647
|
const att = attachments[i];
|
|
1212
1648
|
let url = att.url || '';
|
|
1213
|
-
|
|
1214
|
-
if (url.startsWith('blob:')) {
|
|
1649
|
+
const fileName = att.file_name || att.file?.name || `attachment-${i + 1}`;
|
|
1650
|
+
if (att.file instanceof File && (!url || url.startsWith('blob:') || url.startsWith('data:'))) {
|
|
1215
1651
|
try {
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1652
|
+
const uploaded = await handleChatUploadFile(att.file, {
|
|
1653
|
+
source: 'chat-message-persist',
|
|
1654
|
+
widgetId: persistWidgetId || undefined,
|
|
1655
|
+
});
|
|
1656
|
+
url = uploaded.url;
|
|
1657
|
+
console.log('[Chat→Backend] Uploaded chat File to user storage:', uploaded.id, url.slice(0, 80));
|
|
1658
|
+
items.push({
|
|
1659
|
+
url,
|
|
1660
|
+
file_type: guessAttachmentFileType(url, uploaded.fileName || fileName, uploaded.fileType || att.file_type || att.file.type),
|
|
1661
|
+
file_name: uploaded.fileName || fileName,
|
|
1662
|
+
file_size: uploaded.fileSize,
|
|
1663
|
+
display_order: att.display_order ?? i + 1,
|
|
1222
1664
|
});
|
|
1665
|
+
continue;
|
|
1223
1666
|
} catch (err) {
|
|
1224
|
-
console.warn('[Chat→Backend]
|
|
1667
|
+
console.warn('[Chat→Backend] Chat File upload failed, skipping attachment:', err);
|
|
1225
1668
|
continue;
|
|
1226
1669
|
}
|
|
1227
1670
|
}
|
|
1228
|
-
if (url
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1671
|
+
if (!url) continue;
|
|
1672
|
+
if (url.startsWith('blob:') || url.startsWith('data:')) {
|
|
1673
|
+
try {
|
|
1674
|
+
let file: File | null = null;
|
|
1675
|
+
if (url.startsWith('blob:')) {
|
|
1676
|
+
const blob = await fetch(url).then((r) => r.blob());
|
|
1677
|
+
file = new File([blob], fileName, { type: blob.type || att.file_type || 'application/octet-stream' });
|
|
1678
|
+
} else {
|
|
1679
|
+
file = dataUrlToUploadFile(url, fileName);
|
|
1680
|
+
}
|
|
1681
|
+
if (!file) continue;
|
|
1682
|
+
const uploaded = await handleChatUploadFile(file, {
|
|
1683
|
+
source: 'chat-message-persist',
|
|
1684
|
+
widgetId: persistWidgetId || undefined,
|
|
1685
|
+
});
|
|
1686
|
+
url = uploaded.url;
|
|
1687
|
+
console.log('[Chat→Backend] Uploaded chat file to user storage:', uploaded.id, url.slice(0, 80));
|
|
1688
|
+
items.push({
|
|
1689
|
+
url,
|
|
1690
|
+
file_type: guessAttachmentFileType(url, uploaded.fileName || fileName, uploaded.fileType || att.file_type),
|
|
1691
|
+
file_name: uploaded.fileName || fileName,
|
|
1692
|
+
file_size: uploaded.fileSize,
|
|
1693
|
+
display_order: att.display_order ?? i + 1,
|
|
1694
|
+
});
|
|
1695
|
+
continue;
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
console.warn('[Chat→Backend] Chat file upload failed, skipping attachment:', err);
|
|
1233
1698
|
continue;
|
|
1234
1699
|
}
|
|
1235
1700
|
}
|
|
1236
1701
|
items.push({
|
|
1237
1702
|
url,
|
|
1238
|
-
file_type: att.file_type
|
|
1239
|
-
file_name:
|
|
1703
|
+
file_type: guessAttachmentFileType(url, fileName, att.file_type),
|
|
1704
|
+
file_name: fileName,
|
|
1240
1705
|
file_size: att.file_size ?? undefined,
|
|
1241
1706
|
display_order: att.display_order ?? i + 1,
|
|
1242
1707
|
});
|
|
@@ -1251,11 +1716,13 @@
|
|
|
1251
1716
|
messageType: string,
|
|
1252
1717
|
attachments?: Array<{
|
|
1253
1718
|
url?: string;
|
|
1719
|
+
file?: File;
|
|
1254
1720
|
file_type?: string;
|
|
1255
1721
|
file_name?: string | null;
|
|
1256
1722
|
file_size?: number | null;
|
|
1257
1723
|
display_order?: number;
|
|
1258
1724
|
}>,
|
|
1725
|
+
persistWidgetId?: string | null,
|
|
1259
1726
|
): Promise<any> {
|
|
1260
1727
|
try {
|
|
1261
1728
|
if (!messagesClient) {
|
|
@@ -1268,7 +1735,17 @@
|
|
|
1268
1735
|
return null;
|
|
1269
1736
|
}
|
|
1270
1737
|
const apiAttachments =
|
|
1271
|
-
messageType === 'user' ? await mapChatAttachmentsForApi(
|
|
1738
|
+
messageType === 'user' ? await mapChatAttachmentsForApi(
|
|
1739
|
+
(attachments || []).map((att, i) => ({
|
|
1740
|
+
url: att.url,
|
|
1741
|
+
file: att.file,
|
|
1742
|
+
file_type: guessAttachmentFileType(att.url || '', att.file_name, att.file_type),
|
|
1743
|
+
file_name: att.file_name || att.file?.name || `attachment-${i + 1}`,
|
|
1744
|
+
file_size: att.file_size && att.file_size > 0 ? att.file_size : att.file?.size,
|
|
1745
|
+
display_order: att.display_order ?? i + 1,
|
|
1746
|
+
})),
|
|
1747
|
+
persistWidgetId,
|
|
1748
|
+
) : [];
|
|
1272
1749
|
const createPayload = {
|
|
1273
1750
|
message_content: content,
|
|
1274
1751
|
message_type: messageType as 'user' | 'assistant',
|
|
@@ -1283,25 +1760,18 @@
|
|
|
1283
1760
|
});
|
|
1284
1761
|
const json = await response.raw.json();
|
|
1285
1762
|
console.log('[Chat→Backend] Saved message to conversation:', conversationId);
|
|
1286
|
-
|
|
1763
|
+
const saved = json?.data || json;
|
|
1764
|
+
const savedAtt = saved?.attachments;
|
|
1765
|
+
return {
|
|
1766
|
+
...saved,
|
|
1767
|
+
attachments: (Array.isArray(savedAtt) && savedAtt.length > 0) ? savedAtt : apiAttachments,
|
|
1768
|
+
};
|
|
1287
1769
|
} catch (withAttachmentsError) {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if (apiAttachments.length === 0) throw withAttachmentsError;
|
|
1291
|
-
console.warn(
|
|
1292
|
-
'[Chat→Backend] Save with attachments failed, retrying text-only:',
|
|
1770
|
+
console.error(
|
|
1771
|
+
'[Chat→Backend] Save with attachments failed — not falling back to text-only:',
|
|
1293
1772
|
withAttachmentsError,
|
|
1294
1773
|
);
|
|
1295
|
-
|
|
1296
|
-
conversationId,
|
|
1297
|
-
createMessageRequest: {
|
|
1298
|
-
...createPayload,
|
|
1299
|
-
attachments: undefined,
|
|
1300
|
-
},
|
|
1301
|
-
});
|
|
1302
|
-
const json = await response.raw.json();
|
|
1303
|
-
console.log('[Chat→Backend] Saved message (text-only fallback):', conversationId);
|
|
1304
|
-
return json?.data || json;
|
|
1774
|
+
throw withAttachmentsError;
|
|
1305
1775
|
}
|
|
1306
1776
|
} catch (error) {
|
|
1307
1777
|
console.error('[Chat→Backend] Failed to save message:', error);
|
|
@@ -1417,6 +1887,11 @@
|
|
|
1417
1887
|
let publishingWidgetId: string | null = null;
|
|
1418
1888
|
let publishWidgetStatus: string | null = null;
|
|
1419
1889
|
|
|
1890
|
+
$: if (isPublishing && !publishingWidgetId) {
|
|
1891
|
+
console.warn('[Publish] Clearing stuck isPublishing without publishingWidgetId');
|
|
1892
|
+
isPublishing = false;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1420
1895
|
// Cross-tab operation guards — detect when another tab is generating/publishing on the same widget
|
|
1421
1896
|
let isGeneratingInOtherTab = false;
|
|
1422
1897
|
let isPublishingInOtherTab = false;
|
|
@@ -1496,6 +1971,9 @@
|
|
|
1496
1971
|
);
|
|
1497
1972
|
const sessionAhead = sessionOrdered.find((candidate) => !commitsMatch(candidate, liveSha));
|
|
1498
1973
|
if (sessionAhead) return sessionAhead;
|
|
1974
|
+
// localStorage HEAD written at codegen complete — must win over live SHA
|
|
1975
|
+
// even before snap/live.lastCommitId propagate (reopen currently "fixed" this).
|
|
1976
|
+
if (stored && !commitsMatch(stored, liveSha)) return stored;
|
|
1499
1977
|
const sessionMatch = sessionOrdered.find((candidate) => commitsMatch(candidate, liveSha));
|
|
1500
1978
|
if (sessionMatch) return sessionMatch;
|
|
1501
1979
|
if (stored && commitsMatch(stored, liveSha)) return stored;
|
|
@@ -1554,18 +2032,21 @@
|
|
|
1554
2032
|
let editingCompositionName = '';
|
|
1555
2033
|
let deletingCompositionId: string | null = null;
|
|
1556
2034
|
|
|
1557
|
-
/**
|
|
1558
|
-
const
|
|
2035
|
+
/** Explicit chat box height (px). Drag grows this and pushes Publish down. */
|
|
2036
|
+
const CHAT_DEFAULT_HEIGHT = 550;
|
|
1559
2037
|
const CHAT_MIN_HEIGHT = 260;
|
|
1560
|
-
const CHAT_MAX_HEIGHT =
|
|
2038
|
+
const CHAT_MAX_HEIGHT = 2000;
|
|
1561
2039
|
|
|
1562
2040
|
function getChatContainerHeightForPanel(widgetId: string): number {
|
|
1563
|
-
|
|
2041
|
+
const stored = getPanelState(widgetId).chatContainerHeight;
|
|
2042
|
+
if (stored == null || stored <= 0) return CHAT_DEFAULT_HEIGHT;
|
|
2043
|
+
return Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, stored));
|
|
1564
2044
|
}
|
|
1565
2045
|
|
|
1566
2046
|
function handleChatMessagesResize(widgetId: string, deltaPx: number): void {
|
|
1567
2047
|
const current = getChatContainerHeightForPanel(widgetId);
|
|
1568
2048
|
const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
|
|
2049
|
+
console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
|
|
1569
2050
|
patchPanelSnapshot(widgetId, { chatContainerHeight: next });
|
|
1570
2051
|
}
|
|
1571
2052
|
|
|
@@ -1627,6 +2108,50 @@
|
|
|
1627
2108
|
return null;
|
|
1628
2109
|
}
|
|
1629
2110
|
|
|
2111
|
+
/** If the widget only has titleFont, expose subtitleFont in Edit (same FontIC). */
|
|
2112
|
+
function ensureSubtitleFontInDesignSchema(schema: unknown): unknown {
|
|
2113
|
+
if (!schema) return schema;
|
|
2114
|
+
try {
|
|
2115
|
+
const clone = JSON.parse(JSON.stringify(schema));
|
|
2116
|
+
const groups = Array.isArray(clone) ? clone : clone.componentGroups;
|
|
2117
|
+
if (!Array.isArray(groups) || groups.length === 0) return schema;
|
|
2118
|
+
const allProps = groups.flatMap((group: { components?: Array<{ properties?: unknown[] }> }) =>
|
|
2119
|
+
(group.components || []).flatMap((component) => component.properties || [])
|
|
2120
|
+
);
|
|
2121
|
+
const hasSubtitle = allProps.some((prop: { name?: string; id?: string }) =>
|
|
2122
|
+
/subtitleFont/i.test(String(prop?.name || prop?.id || ''))
|
|
2123
|
+
);
|
|
2124
|
+
if (hasSubtitle) return clone;
|
|
2125
|
+
const titleProp = allProps.find((prop: { name?: string; id?: string; inputController?: { type?: string } }) => {
|
|
2126
|
+
const key = String(prop?.name || prop?.id || '');
|
|
2127
|
+
const type = String(prop?.inputController?.type || '').toLowerCase();
|
|
2128
|
+
return type === 'font' && /titleFont|^font$/i.test(key);
|
|
2129
|
+
});
|
|
2130
|
+
if (!titleProp || typeof titleProp !== 'object') return clone;
|
|
2131
|
+
const firstComponent = groups[0]?.components?.[0];
|
|
2132
|
+
if (!firstComponent) return clone;
|
|
2133
|
+
firstComponent.properties = Array.isArray(firstComponent.properties) ? firstComponent.properties : [];
|
|
2134
|
+
const subtitleProp = {
|
|
2135
|
+
...JSON.parse(JSON.stringify(titleProp)),
|
|
2136
|
+
id: 'subtitleFont',
|
|
2137
|
+
name: 'subtitleFont',
|
|
2138
|
+
description: 'Subtitle Font',
|
|
2139
|
+
};
|
|
2140
|
+
const titleIndex = firstComponent.properties.findIndex(
|
|
2141
|
+
(prop: { name?: string; id?: string }) =>
|
|
2142
|
+
(prop?.name || prop?.id) === ((titleProp as { name?: string; id?: string }).name
|
|
2143
|
+
|| (titleProp as { name?: string; id?: string }).id)
|
|
2144
|
+
);
|
|
2145
|
+
if (titleIndex >= 0) firstComponent.properties.splice(titleIndex + 1, 0, subtitleProp);
|
|
2146
|
+
else firstComponent.properties.push(subtitleProp);
|
|
2147
|
+
console.log('[CompositionEditor] Added subtitleFont FontIC next to titleFont');
|
|
2148
|
+
return Array.isArray(schema) ? clone : clone;
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
console.warn('[CompositionEditor] subtitleFont schema inject skipped:', error);
|
|
2151
|
+
return schema;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1630
2155
|
function normalizeContentSchemaFromApiResponse(contentRes: unknown): any {
|
|
1631
2156
|
if (!contentRes) return null;
|
|
1632
2157
|
const payload = (contentRes as { data?: unknown })?.data ?? contentRes;
|
|
@@ -1756,14 +2281,20 @@
|
|
|
1756
2281
|
}
|
|
1757
2282
|
|
|
1758
2283
|
/** Load design & content schemas for a widget — per-panel; never wipes other panels. */
|
|
1759
|
-
async function loadWidgetSchemas(widgetId: string): Promise<void> {
|
|
2284
|
+
async function loadWidgetSchemas(widgetId: string, options?: { force?: boolean }): Promise<void> {
|
|
1760
2285
|
if (!widgetsClient || !hasValidToken) {
|
|
1761
2286
|
patchPanelSnapshot(widgetId, { compositionSchemasLoading: false, schemaLoadStartedAt: null });
|
|
1762
2287
|
return;
|
|
1763
2288
|
}
|
|
1764
2289
|
|
|
2290
|
+
if (options?.force) {
|
|
2291
|
+
widgetSchemaCache.delete(widgetId);
|
|
2292
|
+
cancelPanelSchemaLoad(widgetId);
|
|
2293
|
+
abandonSchemaLoadAttempt(widgetId);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
1765
2296
|
const existingLoad = schemaLoadPromises.get(widgetId);
|
|
1766
|
-
if (existingLoad) {
|
|
2297
|
+
if (existingLoad && !options?.force) {
|
|
1767
2298
|
const snap = getPanelState(widgetId);
|
|
1768
2299
|
if (!isStalePanelSchemaLoading(widgetId, snap)) return existingLoad;
|
|
1769
2300
|
cancelPanelSchemaLoad(widgetId);
|
|
@@ -1779,7 +2310,7 @@
|
|
|
1779
2310
|
|| existing.compositionContentSchema
|
|
1780
2311
|
);
|
|
1781
2312
|
|
|
1782
|
-
if (hasCachedSchemas) {
|
|
2313
|
+
if (hasCachedSchemas && !options?.force) {
|
|
1783
2314
|
const designSchema = cached?.design ?? existing.compositionDesignSchema;
|
|
1784
2315
|
const contentSchema = cached?.content ?? existing.compositionContentSchema;
|
|
1785
2316
|
const schemaError = cached?.error ?? existing.compositionSchemasError;
|
|
@@ -1874,7 +2405,9 @@
|
|
|
1874
2405
|
'Content schema',
|
|
1875
2406
|
);
|
|
1876
2407
|
|
|
1877
|
-
designSchema =
|
|
2408
|
+
designSchema = ensureSubtitleFontInDesignSchema(
|
|
2409
|
+
normalizeDesignSchemaFromApiResponse(designRes)
|
|
2410
|
+
);
|
|
1878
2411
|
contentSchema = normalizeContentSchemaFromApiResponse(contentRes);
|
|
1879
2412
|
|
|
1880
2413
|
if (!designSchema && !contentSchema) {
|
|
@@ -1924,7 +2457,11 @@
|
|
|
1924
2457
|
compositionSchemasError = schemaError;
|
|
1925
2458
|
}
|
|
1926
2459
|
if (designSchema || contentSchema) {
|
|
1927
|
-
queueMicrotask(() =>
|
|
2460
|
+
queueMicrotask(() => {
|
|
2461
|
+
void loadCompositionsForPanel(widgetId).then(() => {
|
|
2462
|
+
applyPanelCompositionToPreview(widgetId);
|
|
2463
|
+
});
|
|
2464
|
+
});
|
|
1928
2465
|
}
|
|
1929
2466
|
} finally {
|
|
1930
2467
|
schemaLoadInFlight.delete(widgetId);
|
|
@@ -1962,20 +2499,32 @@
|
|
|
1962
2499
|
return values;
|
|
1963
2500
|
}
|
|
1964
2501
|
|
|
1965
|
-
/** After codegen, push design.json defaults into Create preview
|
|
2502
|
+
/** After codegen, push design.json + content.json defaults into Create preview. */
|
|
1966
2503
|
async function applyDesignSchemaDefaultsToPreview(widgetId: string) {
|
|
1967
2504
|
if (!widgetId || !widgetsClient) return;
|
|
1968
2505
|
await scheduleWidgetSchemaLoad(widgetId);
|
|
1969
2506
|
const schema = getPanelDisplayState(widgetId).compositionDesignSchema;
|
|
1970
2507
|
const defaults = extractDesignDefaultsFromSchema(schema);
|
|
1971
|
-
if (Object.keys(defaults).length
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2508
|
+
if (Object.keys(defaults).length > 0) {
|
|
2509
|
+
if (widgetId === focusedPanelWidgetId) {
|
|
2510
|
+
liveDesignValues = defaults;
|
|
2511
|
+
} else {
|
|
2512
|
+
patchPanelSnapshot(widgetId, { liveDesignValues: defaults });
|
|
2513
|
+
}
|
|
2514
|
+
getWidgetDetailsRef(widgetId)?.sendMessageToPreview({ type: 'widgetic:update', design: flattenDesignValuesForPreview(defaults) });
|
|
2515
|
+
console.log('[Create Preview] Applied design schema defaults:', Object.keys(defaults));
|
|
2516
|
+
}
|
|
2517
|
+
const contentSchema = getPanelDisplayState(widgetId).compositionContentSchema;
|
|
2518
|
+
const schemaItems = contentSchema?.contentItems ?? contentSchema?.data?.contentItems;
|
|
2519
|
+
if (Array.isArray(schemaItems) && schemaItems.length > 0) {
|
|
2520
|
+
const existing = getPanelDisplayState(widgetId).liveContentItems || [];
|
|
2521
|
+
const merged = mergeContentItemsWithSchema(existing, schemaItems);
|
|
2522
|
+
patchPanelSnapshot(widgetId, { liveContentItems: merged });
|
|
2523
|
+
if (widgetId === focusedPanelWidgetId) liveContentItems = merged;
|
|
1976
2524
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
2525
|
+
applyPanelCompositionToPreview(widgetId);
|
|
2526
|
+
setTimeout(() => applyPanelCompositionToPreview(widgetId), 250);
|
|
2527
|
+
setTimeout(() => applyPanelCompositionToPreview(widgetId), 800);
|
|
1979
2528
|
}
|
|
1980
2529
|
|
|
1981
2530
|
/** Toggle composition editor panel open/closed */
|
|
@@ -2045,9 +2594,20 @@
|
|
|
2045
2594
|
});
|
|
2046
2595
|
}
|
|
2047
2596
|
if (event.detail.step === 'create') {
|
|
2048
|
-
|
|
2049
|
-
|
|
2597
|
+
let psCreate = getPanelDisplayState(panelWidgetId);
|
|
2598
|
+
if (!psCreate.compositionContentSchema && !psCreate.compositionSchemasLoading) {
|
|
2599
|
+
void scheduleWidgetSchemaLoad(panelWidgetId);
|
|
2050
2600
|
}
|
|
2601
|
+
if (!psCreate.liveContentItems || psCreate.liveContentItems.length === 0) {
|
|
2602
|
+
const schemaItems = psCreate.compositionContentSchema?.contentItems
|
|
2603
|
+
?? psCreate.compositionContentSchema?.data?.contentItems;
|
|
2604
|
+
if (Array.isArray(schemaItems) && schemaItems.length > 0) {
|
|
2605
|
+
const seeded = JSON.parse(JSON.stringify(schemaItems));
|
|
2606
|
+
patchPanelSnapshot(panelWidgetId, { liveContentItems: seeded });
|
|
2607
|
+
if (panelWidgetId === focusedPanelWidgetId) liveContentItems = seeded;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
applyPanelCompositionToPreview(panelWidgetId);
|
|
2051
2611
|
if (panelWidgetId && pendingCreatePreviewDefaultsWidgetId === panelWidgetId) {
|
|
2052
2612
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
2053
2613
|
if (panelWidgetId === focusedPanelWidgetId) {
|
|
@@ -2089,15 +2649,22 @@
|
|
|
2089
2649
|
const versionedUrl = `${cdnBase}/widgets/${widgetId}/v${version}/widget.html`;
|
|
2090
2650
|
|
|
2091
2651
|
if (jsPath) {
|
|
2652
|
+
const trimmed = jsPath.replace(/\/+$/, '');
|
|
2653
|
+
let resolved = trimmed
|
|
2654
|
+
.replace(/\/widget\.js$/i, '/widget.html')
|
|
2655
|
+
.replace(/\.js$/i, '.html');
|
|
2656
|
+
if (!/\.html$/i.test(resolved)) {
|
|
2657
|
+
resolved = `${resolved}/widget.html`;
|
|
2658
|
+
}
|
|
2659
|
+
if (resolved.startsWith('/')) {
|
|
2660
|
+
resolved = `${cdnBase}${resolved}`;
|
|
2661
|
+
}
|
|
2092
2662
|
const jsPathVersionMatch = jsPath.match(/\/v(\d+)(?:\/|$)/);
|
|
2093
2663
|
const jsPathVersion = jsPathVersionMatch ? parseInt(jsPathVersionMatch[1], 10) : null;
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
if (resolved.startsWith('/')) {
|
|
2099
|
-
resolved = `${cdnBase}${resolved}`;
|
|
2100
|
-
}
|
|
2664
|
+
const jsPathWidgetId = (resolved.match(/\/widgets\/([^/]+)\//i) || [])[1] || null;
|
|
2665
|
+
const pointsAtOtherWidget = !!(jsPathWidgetId && jsPathWidgetId !== widgetId);
|
|
2666
|
+
if (pointsAtOtherWidget || jsPathVersion === version || /\/widgets\//i.test(resolved)) {
|
|
2667
|
+
console.log('[Publish] Using js_path artifact URL', resolved, pointsAtOtherWidget ? '(source widget)' : '');
|
|
2101
2668
|
return resolved;
|
|
2102
2669
|
}
|
|
2103
2670
|
}
|
|
@@ -2128,6 +2695,68 @@
|
|
|
2128
2695
|
let suppressNextBuildFromRepoChatMessage = false;
|
|
2129
2696
|
let _isRestoringCheckpoint = false;
|
|
2130
2697
|
|
|
2698
|
+
function mapFontWeightToCss(weight: string | undefined): string {
|
|
2699
|
+
const name = String(weight || 'Regular').toLowerCase().replace(/[\s_-]/g, '');
|
|
2700
|
+
const table: Record<string, string> = {
|
|
2701
|
+
thin: '100', hairline: '100',
|
|
2702
|
+
extralight: '200', ultralight: '200',
|
|
2703
|
+
light: '300',
|
|
2704
|
+
regular: '400', normal: '400',
|
|
2705
|
+
medium: '500',
|
|
2706
|
+
semibold: '600', demibold: '600',
|
|
2707
|
+
bold: '700',
|
|
2708
|
+
extrabold: '800', ultrabold: '800',
|
|
2709
|
+
black: '900', heavy: '900',
|
|
2710
|
+
};
|
|
2711
|
+
if (/^\d{3}$/.test(name)) return name;
|
|
2712
|
+
return table[name] || '400';
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function mapAspectRatioToCss(raw: string): string {
|
|
2716
|
+
const key = raw.trim().toLowerCase();
|
|
2717
|
+
const table: Record<string, string> = {
|
|
2718
|
+
square: '1/1',
|
|
2719
|
+
'1:1': '1/1',
|
|
2720
|
+
'1/1': '1/1',
|
|
2721
|
+
landscape: '4/3',
|
|
2722
|
+
'4:3': '4/3',
|
|
2723
|
+
'4/3': '4/3',
|
|
2724
|
+
wide: '16/9',
|
|
2725
|
+
'16:9': '16/9',
|
|
2726
|
+
'16/9': '16/9',
|
|
2727
|
+
portrait: '3/4',
|
|
2728
|
+
'3:4': '3/4',
|
|
2729
|
+
'3/4': '3/4',
|
|
2730
|
+
};
|
|
2731
|
+
return table[key] || raw;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
/** CSS custom properties for widgetic:update — FontIC objects and dropdown {value} become strings. */
|
|
2735
|
+
function flattenDesignValuesForPreview(design: Record<string, unknown>): Record<string, string> {
|
|
2736
|
+
const out: Record<string, string> = {};
|
|
2737
|
+
for (const [key, value] of Object.entries(design || {})) {
|
|
2738
|
+
if (value && typeof value === 'object' && 'family' in (value as Record<string, unknown>)) {
|
|
2739
|
+
const font = value as { family?: string; weight?: string };
|
|
2740
|
+
const family = String(font.family || 'Inter');
|
|
2741
|
+
out[key] = `"${family}", sans-serif`;
|
|
2742
|
+
out[`${key}Family`] = family;
|
|
2743
|
+
out[`${key}Weight`] = mapFontWeightToCss(font.weight);
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
if (value && typeof value === 'object' && 'value' in (value as Record<string, unknown>)) {
|
|
2747
|
+
out[key] = mapAspectRatioToCss(String((value as { value?: unknown }).value ?? ''));
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2750
|
+
if (typeof value === 'string') {
|
|
2751
|
+
out[key] = /aspect/i.test(key) ? mapAspectRatioToCss(value) : value;
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
if (value == null) continue;
|
|
2755
|
+
out[key] = String(value);
|
|
2756
|
+
}
|
|
2757
|
+
return out;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2131
2760
|
/** Handle property change from PropsEditor — send to widget preview iframe */
|
|
2132
2761
|
function handleEditorPropertyChange(event: CustomEvent<{ schema: string; propertyId: string; value: unknown; opacity?: number; itemId?: string }>) {
|
|
2133
2762
|
const { schema, propertyId, value, itemId } = event.detail;
|
|
@@ -2137,7 +2766,7 @@
|
|
|
2137
2766
|
liveDesignValues = { ...liveDesignValues, [propertyId]: value };
|
|
2138
2767
|
getWidgetDetailsRef(focusedPanelWidgetId)?.sendMessageToPreview({
|
|
2139
2768
|
type: 'widgetic:update',
|
|
2140
|
-
design: liveDesignValues
|
|
2769
|
+
design: flattenDesignValuesForPreview(liveDesignValues)
|
|
2141
2770
|
});
|
|
2142
2771
|
} else if (schema === 'content') {
|
|
2143
2772
|
if (itemId) {
|
|
@@ -2320,11 +2949,16 @@
|
|
|
2320
2949
|
|
|
2321
2950
|
/** Push composition overlay to a specific panel preview iframe. */
|
|
2322
2951
|
function applyPanelCompositionToPreview(widgetId: string): void {
|
|
2323
|
-
const ps = getPanelDisplayState(widgetId);
|
|
2324
2952
|
const panelRef = getWidgetDetailsRef(widgetId);
|
|
2325
2953
|
if (!panelRef) return;
|
|
2954
|
+
const step = widgetId === focusedPanelWidgetId ? widgetDetailsStep : 'create';
|
|
2955
|
+
if (step === 'create') {
|
|
2956
|
+
console.log('[Preview] Skip composition overlay on Create step', widgetId?.substring(0, 8));
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
const ps = getPanelDisplayState(widgetId);
|
|
2326
2960
|
if (Object.keys(ps.liveDesignValues).length > 0) {
|
|
2327
|
-
panelRef.sendMessageToPreview({ type: 'widgetic:update', design: ps.liveDesignValues });
|
|
2961
|
+
panelRef.sendMessageToPreview({ type: 'widgetic:update', design: flattenDesignValuesForPreview(ps.liveDesignValues as Record<string, unknown>) });
|
|
2328
2962
|
}
|
|
2329
2963
|
if (Object.keys(ps.liveContentValues).length > 0) {
|
|
2330
2964
|
panelRef.sendMessageToPreview({ type: 'widgetic:update', content: ps.liveContentValues });
|
|
@@ -2586,6 +3220,10 @@
|
|
|
2586
3220
|
/** Send current composition design/content values to the preview iframe via postMessage.
|
|
2587
3221
|
* Called on: composition select, iframe ready, and step switch to Edit/Embed. */
|
|
2588
3222
|
function applyCompositionToPreview() {
|
|
3223
|
+
if (widgetDetailsStep === 'create') {
|
|
3224
|
+
console.log('[Preview] Skip composition overlay on Create step');
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
2589
3227
|
if (Object.keys(liveDesignValues).length > 0) {
|
|
2590
3228
|
widgetDetails?.sendMessageToPreview({ type: 'widgetic:update', design: liveDesignValues });
|
|
2591
3229
|
}
|
|
@@ -2593,7 +3231,10 @@
|
|
|
2593
3231
|
widgetDetails?.sendMessageToPreview({ type: 'widgetic:update', content: liveContentValues });
|
|
2594
3232
|
}
|
|
2595
3233
|
if (liveContentItems.length > 0) {
|
|
2596
|
-
widgetDetails?.sendMessageToPreview({
|
|
3234
|
+
widgetDetails?.sendMessageToPreview({
|
|
3235
|
+
type: 'widgetic:update',
|
|
3236
|
+
contentItems: flattenContentItemsForPreview(liveContentItems)
|
|
3237
|
+
});
|
|
2597
3238
|
}
|
|
2598
3239
|
}
|
|
2599
3240
|
|
|
@@ -3003,6 +3644,8 @@
|
|
|
3003
3644
|
}
|
|
3004
3645
|
|
|
3005
3646
|
let canvasWireframeMode = true;
|
|
3647
|
+
/** Extra canvas chrome — default off. Not tied to debugMode. */
|
|
3648
|
+
let canvasAdvancedFeatures = !!initialAdvancedFeatures;
|
|
3006
3649
|
let canvasPrimaryWidgetId: string | null = null;
|
|
3007
3650
|
|
|
3008
3651
|
function emitCreatorWireframeChanged() {
|
|
@@ -3020,6 +3663,21 @@
|
|
|
3020
3663
|
emitCreatorWireframeChanged();
|
|
3021
3664
|
}
|
|
3022
3665
|
|
|
3666
|
+
function emitCreatorAdvancedChanged() {
|
|
3667
|
+
if (typeof window === 'undefined') return;
|
|
3668
|
+
window.dispatchEvent(
|
|
3669
|
+
new CustomEvent(CREATOR_ADVANCED_CHANGED_EVENT, {
|
|
3670
|
+
detail: { advancedFeatures: canvasAdvancedFeatures }
|
|
3671
|
+
})
|
|
3672
|
+
);
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
function handleHostToggleAdvancedEvent() {
|
|
3676
|
+
canvasAdvancedFeatures = !canvasAdvancedFeatures;
|
|
3677
|
+
console.log('[CreatorApp] Advanced canvas features:', canvasAdvancedFeatures ? 'ON' : 'OFF');
|
|
3678
|
+
emitCreatorAdvancedChanged();
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3023
3681
|
function resolveHostTargetWidgetId(): string | null {
|
|
3024
3682
|
const id = focusedPanelWidgetId || selectedWidgetId;
|
|
3025
3683
|
if (!id || isDraftConvertId(id)) return null;
|
|
@@ -3039,19 +3697,21 @@
|
|
|
3039
3697
|
}
|
|
3040
3698
|
|
|
3041
3699
|
async function handleHostDuplicateWidgetEvent(): Promise<void> {
|
|
3042
|
-
const widgetId = resolveHostTargetWidgetId();
|
|
3043
|
-
if (!widgetId ||
|
|
3700
|
+
const widgetId = canvasPrimaryWidgetId || resolveHostTargetWidgetId();
|
|
3701
|
+
if (!widgetId || isDraftConvertId(widgetId)) {
|
|
3044
3702
|
showToast('warning', 'Select a widget first', {
|
|
3045
|
-
description: '
|
|
3703
|
+
description: 'Duplicate from the header copies the widget in the title.',
|
|
3046
3704
|
});
|
|
3047
3705
|
return;
|
|
3048
3706
|
}
|
|
3049
|
-
|
|
3050
|
-
if (typeof canvasRef.duplicateSelectedObjects === 'function') {
|
|
3051
|
-
|
|
3052
|
-
|
|
3707
|
+
console.log('[CreatorApp] Host Duplicate — primary widget', widgetId.substring(0, 8));
|
|
3708
|
+
if (canvasRef?.selectWidgetShape && typeof canvasRef.duplicateSelectedObjects === 'function') {
|
|
3709
|
+
if (canvasRef.selectWidgetShape(widgetId)) {
|
|
3710
|
+
await canvasRef.duplicateSelectedObjects();
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3053
3713
|
}
|
|
3054
|
-
|
|
3714
|
+
await handleWidgetShapeDuplicated(widgetId, null);
|
|
3055
3715
|
}
|
|
3056
3716
|
|
|
3057
3717
|
async function handleHostDeleteWidgetEvent(): Promise<void> {
|
|
@@ -3071,6 +3731,15 @@
|
|
|
3071
3731
|
}
|
|
3072
3732
|
}
|
|
3073
3733
|
|
|
3734
|
+
function emitCreatorSaveState(dirty: boolean): void {
|
|
3735
|
+
if (typeof window === 'undefined' || !embedded) return;
|
|
3736
|
+
window.dispatchEvent(new CustomEvent('widgetic:creator-save-state', { detail: { dirty } }));
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
function handleHostSaveCanvasEvent(): void {
|
|
3740
|
+
canvasRef?.saveNow?.();
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3074
3743
|
async function persistPrimaryWidgetId(widgetId: string) {
|
|
3075
3744
|
if (!widgetId) return;
|
|
3076
3745
|
canvasPrimaryWidgetId = widgetId;
|
|
@@ -3086,7 +3755,6 @@
|
|
|
3086
3755
|
}
|
|
3087
3756
|
}
|
|
3088
3757
|
const widget = getWidgetById(widgetId);
|
|
3089
|
-
if (widget) notifyHostWidgetName(widget);
|
|
3090
3758
|
if (embedded && typeof window !== 'undefined') {
|
|
3091
3759
|
try {
|
|
3092
3760
|
const url = new URL(window.location.href);
|
|
@@ -3098,12 +3766,110 @@
|
|
|
3098
3766
|
console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
|
|
3099
3767
|
}
|
|
3100
3768
|
}
|
|
3769
|
+
if (widget) notifyHostWidgetName(widget);
|
|
3770
|
+
emitCanvasWidgetsList('primary-changed');
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
function emitCanvasWidgetsList(reason: string) {
|
|
3774
|
+
if (!embedded || typeof window === 'undefined') return;
|
|
3775
|
+
const ids = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
|
|
3776
|
+
const items: { id: string; name: string }[] = [];
|
|
3777
|
+
for (const id of ids) {
|
|
3778
|
+
const widget = getWidgetById(id);
|
|
3779
|
+
items.push({
|
|
3780
|
+
id,
|
|
3781
|
+
name: (widget?.name || '').trim() || 'Untitled Widget'
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
if (canvasPrimaryWidgetId && !items.some((item) => item.id === canvasPrimaryWidgetId)) {
|
|
3785
|
+
const primary = getWidgetById(canvasPrimaryWidgetId);
|
|
3786
|
+
if (primary?.id) {
|
|
3787
|
+
items.unshift({
|
|
3788
|
+
id: primary.id,
|
|
3789
|
+
name: (primary.name || '').trim() || 'Untitled Widget'
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
const primaryWidgetId = canvasPrimaryWidgetId || '';
|
|
3794
|
+
console.log('[CreatorApp] Emit canvas widgets for header', reason, items.length, primaryWidgetId);
|
|
3795
|
+
window.dispatchEvent(new CustomEvent(CREATOR_CANVAS_WIDGETS_EVENT, {
|
|
3796
|
+
detail: { widgets: items, primaryWidgetId }
|
|
3797
|
+
}));
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
function handleHostRequestCanvasWidgetsEvent() {
|
|
3801
|
+
emitCanvasWidgetsList('host-request');
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
function handleHostSetPrimaryWidgetEvent(event: Event) {
|
|
3805
|
+
const widgetId = (event as CustomEvent).detail?.widgetId;
|
|
3806
|
+
if (!widgetId || typeof widgetId !== 'string') {
|
|
3807
|
+
console.warn('[CreatorApp] Host set primary ignored — missing widgetId');
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3810
|
+
console.log('[CreatorApp] Host set primary widget', widgetId.substring(0, 8));
|
|
3811
|
+
canvasRef?.setPrimaryWidgetShape?.(widgetId);
|
|
3812
|
+
canvasRef?.selectWidgetShape?.(widgetId);
|
|
3813
|
+
void persistPrimaryWidgetId(widgetId);
|
|
3101
3814
|
}
|
|
3102
3815
|
|
|
3103
3816
|
function handleSetPrimaryWidget(widgetId: string) {
|
|
3104
3817
|
void persistPrimaryWidgetId(widgetId);
|
|
3105
3818
|
}
|
|
3106
3819
|
|
|
3820
|
+
async function hydrateCanvasPrimaryWidget(linkedIds?: string[]): Promise<string | null> {
|
|
3821
|
+
const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
|
|
3822
|
+
if (!targetCanvasId) return canvasPrimaryWidgetId || null;
|
|
3823
|
+
let primaryId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
|
|
3824
|
+
if (!primaryId) {
|
|
3825
|
+
const fromLinks = (linkedIds || []).filter((id) => id && !isDraftConvertId(id) && !String(id).startsWith('draft'));
|
|
3826
|
+
const fromList = widgets
|
|
3827
|
+
.filter((w: any) => {
|
|
3828
|
+
const cid = w.canvasId || w.canvas_id;
|
|
3829
|
+
return cid === targetCanvasId && w.id && !isDraftConvertId(w.id);
|
|
3830
|
+
})
|
|
3831
|
+
.map((w: any) => w.id);
|
|
3832
|
+
primaryId = fromLinks[0] || fromList[0] || null;
|
|
3833
|
+
if (primaryId) {
|
|
3834
|
+
console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
|
|
3835
|
+
await persistPrimaryWidgetId(primaryId);
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
if (primaryId) {
|
|
3839
|
+
const widget = getWidgetById(primaryId);
|
|
3840
|
+
if (widget) notifyHostWidgetName(widget);
|
|
3841
|
+
emitCanvasWidgetsList('hydrate-primary');
|
|
3842
|
+
}
|
|
3843
|
+
return primaryId || canvasPrimaryWidgetId || null;
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
async function handleHostMoveCanvasFolderEvent(event: Event) {
|
|
3847
|
+
const folderId = (event as CustomEvent).detail?.folderId;
|
|
3848
|
+
if (!folderId || typeof folderId !== 'string' || !widgetsClient) return;
|
|
3849
|
+
const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
|
|
3850
|
+
let primaryId = canvasPrimaryWidgetId || (targetCanvasId ? await resolvePrimaryWidgetIdForCanvas(targetCanvasId) : null);
|
|
3851
|
+
if (!primaryId) {
|
|
3852
|
+
primaryId = await hydrateCanvasPrimaryWidget();
|
|
3853
|
+
}
|
|
3854
|
+
const ids = new Set<string>();
|
|
3855
|
+
if (primaryId) ids.add(primaryId);
|
|
3856
|
+
for (const w of widgets) {
|
|
3857
|
+
const cid = (w as any).canvasId || (w as any).canvas_id;
|
|
3858
|
+
if (targetCanvasId && cid === targetCanvasId && w.id) ids.add(w.id);
|
|
3859
|
+
}
|
|
3860
|
+
console.log('[CreatorApp] Move canvas widgets to folder', folderId, [...ids].map((id) => id.substring(0, 8)));
|
|
3861
|
+
for (const id of ids) {
|
|
3862
|
+
try {
|
|
3863
|
+
await widgetsClient.updateWidget({
|
|
3864
|
+
id,
|
|
3865
|
+
updateWidgetRequest: { projectId: folderId, projectFolder: folderId }
|
|
3866
|
+
});
|
|
3867
|
+
} catch (err) {
|
|
3868
|
+
console.warn('[CreatorApp] Failed to move widget to folder', id.substring(0, 8), err);
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3107
3873
|
// Preview Modal State Variables
|
|
3108
3874
|
let previewIframe: HTMLIFrameElement | null = null;
|
|
3109
3875
|
const { DEV: isDevMode } = import.meta.env;
|
|
@@ -3608,18 +4374,11 @@
|
|
|
3608
4374
|
const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
3609
4375
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
3610
4376
|
await maybeAutoRestoreWidgetShapes();
|
|
3611
|
-
// Backfill widget name + thumbnail on shapes that were saved before those
|
|
3612
|
-
// fields existed on WidgetShape (fixes "Widget" placeholder on old canvases).
|
|
3613
4377
|
backfillWidgetShapeMetadata();
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
selectedWidgetId;
|
|
3619
|
-
if (primaryId) {
|
|
3620
|
-
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
3621
|
-
canvasRef.selectWidgetShape(primaryId);
|
|
3622
|
-
}
|
|
4378
|
+
const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
|
|
4379
|
+
if (loaded && canvasRef && primaryId && !isConvertOrDraftPanelOpen()) {
|
|
4380
|
+
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
4381
|
+
canvasRef.selectWidgetShape(primaryId);
|
|
3623
4382
|
}
|
|
3624
4383
|
} catch (error) {
|
|
3625
4384
|
console.error('Widget Creator: Load canvases error:', error);
|
|
@@ -4481,6 +5240,11 @@
|
|
|
4481
5240
|
if (embeddedDeepLinkAutoOpenDone) {
|
|
4482
5241
|
return;
|
|
4483
5242
|
}
|
|
5243
|
+
if (isConvertOrDraftPanelOpen()) {
|
|
5244
|
+
console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
|
|
5245
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
5246
|
+
return;
|
|
5247
|
+
}
|
|
4484
5248
|
|
|
4485
5249
|
let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
|
|
4486
5250
|
// Canvas-only deep-link: no ?widget_id= / initialWidgetId — select canvases.primary_widget_id.
|
|
@@ -4878,20 +5642,19 @@
|
|
|
4878
5642
|
}
|
|
4879
5643
|
}
|
|
4880
5644
|
|
|
4881
|
-
/** After hydrate,
|
|
4882
|
-
*
|
|
4883
|
-
* ("Newer code than Live") can detect unpublished code even without recent codegen locally. */
|
|
5645
|
+
/** After hydrate, probe GitLab HEAD only when this widget has no codegen commit yet.
|
|
5646
|
+
* Do not promote HEAD over lastCommitId — that marks a new publish version on every Details open. */
|
|
4884
5647
|
async function reconcileWidgetHeadCommit(widgetId: string): Promise<void> {
|
|
4885
5648
|
const currentPanelLastCommit = getPanelState(widgetId).lastCommitId;
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
5649
|
+
if (currentPanelLastCommit) {
|
|
5650
|
+
console.log(
|
|
5651
|
+
`[Version] Skip HEAD probe for "${widgetId}": lastCommitId already ${currentPanelLastCommit.substring(0, 8)}`
|
|
5652
|
+
);
|
|
4889
5653
|
return;
|
|
4890
5654
|
}
|
|
4891
5655
|
const headSha = await fetchRepoHeadCommitSha(widgetId);
|
|
4892
5656
|
if (!headSha) return;
|
|
4893
|
-
|
|
4894
|
-
console.log(`[Version] Repo HEAD probe for "${widgetId}": ${headSha.substring(0, 8)} (was ${currentPanelLastCommit?.substring(0, 8) ?? 'null'})`);
|
|
5657
|
+
console.log(`[Version] Repo HEAD probe for "${widgetId}": ${headSha.substring(0, 8)} (was null)`);
|
|
4895
5658
|
patchPanelSnapshot(widgetId, { lastCommitId: headSha, hasGeneratedCode: true });
|
|
4896
5659
|
saveCommitIdForWidget(widgetId, headSha);
|
|
4897
5660
|
if (widgetId === focusedPanelWidgetId) {
|
|
@@ -5164,9 +5927,11 @@
|
|
|
5164
5927
|
// Ensure we have an id
|
|
5165
5928
|
const newWidget = response.data || response; // handle envelope or direct
|
|
5166
5929
|
if (newWidget?.id) {
|
|
5167
|
-
// Optimistically add to list and select
|
|
5168
5930
|
widgets = [{ ...newWidget }, ...widgets];
|
|
5169
5931
|
selectWidget(newWidget);
|
|
5932
|
+
if (!canvasPrimaryWidgetId) {
|
|
5933
|
+
void persistPrimaryWidgetId(newWidget.id);
|
|
5934
|
+
}
|
|
5170
5935
|
authStatus = 'Widget created.';
|
|
5171
5936
|
} else {
|
|
5172
5937
|
authStatus = 'Widget created but response missing id.';
|
|
@@ -5247,11 +6012,7 @@
|
|
|
5247
6012
|
|
|
5248
6013
|
const status = (op?.status || '').toLowerCase();
|
|
5249
6014
|
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
|
5250
|
-
|
|
5251
|
-
settle('reject', new Error(op?.error || 'Operation failed.'));
|
|
5252
|
-
} else {
|
|
5253
|
-
settle('resolve', op);
|
|
5254
|
-
}
|
|
6015
|
+
settle('resolve', op);
|
|
5255
6016
|
return;
|
|
5256
6017
|
}
|
|
5257
6018
|
|
|
@@ -5290,11 +6051,7 @@
|
|
|
5290
6051
|
signal
|
|
5291
6052
|
})
|
|
5292
6053
|
.then((finalOp) => {
|
|
5293
|
-
|
|
5294
|
-
settle('reject', new Error(finalOp?.error || 'Operation failed.'));
|
|
5295
|
-
} else {
|
|
5296
|
-
settle('resolve', finalOp as OperationStatus);
|
|
5297
|
-
}
|
|
6054
|
+
settle('resolve', finalOp as OperationStatus);
|
|
5298
6055
|
})
|
|
5299
6056
|
.catch((streamError) => {
|
|
5300
6057
|
if (settled || signal?.aborted) return;
|
|
@@ -5822,11 +6579,22 @@
|
|
|
5822
6579
|
const quiet = !!options.quiet;
|
|
5823
6580
|
console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
|
|
5824
6581
|
|
|
6582
|
+
if (isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
|
|
6583
|
+
console.log('[handleRefreshScreenshot] Skip iframe capture — convert/draft panel is open:', widgetId.substring(0, 8));
|
|
6584
|
+
return;
|
|
6585
|
+
}
|
|
6586
|
+
|
|
5825
6587
|
// Quiet canvas-thumbnail sync must never force-open Widget Details.
|
|
5826
|
-
//
|
|
6588
|
+
// Convert drafts capture from the canvas shape, never by opening a panel.
|
|
5827
6589
|
if (!openWidgetPanelIds.includes(widgetId)) {
|
|
5828
|
-
if (quiet) {
|
|
5829
|
-
|
|
6590
|
+
if (quiet || isConvertOrDraftPanelOpen()) {
|
|
6591
|
+
const shapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
|
|
6592
|
+
if (shapeDataUrl && canvasRef) {
|
|
6593
|
+
console.log('[handleRefreshScreenshot] Using canvas shape screenshot (panel closed):', widgetId.substring(0, 8));
|
|
6594
|
+
canvasRef.updateWidgetImage(widgetId, shapeDataUrl);
|
|
6595
|
+
} else {
|
|
6596
|
+
console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
|
|
6597
|
+
}
|
|
5830
6598
|
return;
|
|
5831
6599
|
}
|
|
5832
6600
|
// Manual "Refresh Widget Preview" always opens the panel so the iframe exists.
|
|
@@ -5836,8 +6604,9 @@
|
|
|
5836
6604
|
}
|
|
5837
6605
|
|
|
5838
6606
|
const panelRef = getWidgetDetailsRef(widgetId);
|
|
5839
|
-
|
|
5840
|
-
|
|
6607
|
+
const iframe = panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null;
|
|
6608
|
+
if (!iframe?.contentWindow) {
|
|
6609
|
+
console.log('[handleRefreshScreenshot] Preview iframe not ready — skipping capture');
|
|
5841
6610
|
if (!quiet) {
|
|
5842
6611
|
showToast('error', 'No valid preview available', { description: 'Generate code first to capture a screenshot.', duration: 5000 });
|
|
5843
6612
|
}
|
|
@@ -5848,14 +6617,6 @@
|
|
|
5848
6617
|
showToast('info', 'Capturing widget screenshot...', { duration: 3000 });
|
|
5849
6618
|
}
|
|
5850
6619
|
|
|
5851
|
-
const iframe = panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null;
|
|
5852
|
-
if (!iframe?.contentWindow) {
|
|
5853
|
-
if (!quiet) {
|
|
5854
|
-
showToast('error', 'No preview iframe available', { description: 'Generate code first to capture a screenshot.', duration: 5000 });
|
|
5855
|
-
}
|
|
5856
|
-
return;
|
|
5857
|
-
}
|
|
5858
|
-
|
|
5859
6620
|
try {
|
|
5860
6621
|
const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
|
|
5861
6622
|
const timeout = setTimeout(() => {
|
|
@@ -5982,10 +6743,21 @@ Requirements:
|
|
|
5982
6743
|
fromChat?: boolean;
|
|
5983
6744
|
widgetId?: string;
|
|
5984
6745
|
compileFixOnly?: boolean;
|
|
6746
|
+
/** Recreate GitLab repo when the previous turn failed with missing repository. */
|
|
6747
|
+
forceRecreateRepo?: boolean;
|
|
5985
6748
|
/** CDN upload row IDs captured before chat clearDraft. */
|
|
5986
6749
|
attachmentIds?: string[];
|
|
5987
6750
|
/** Image URLs / data URLs captured before chat clearDraft. */
|
|
5988
6751
|
promptImagesOverride?: string[];
|
|
6752
|
+
/** Chat files (PDF, video, images) to store on the user message before codegen. */
|
|
6753
|
+
persistFileAttachments?: Array<{
|
|
6754
|
+
url?: string;
|
|
6755
|
+
file?: File;
|
|
6756
|
+
file_type?: string;
|
|
6757
|
+
file_name?: string | null;
|
|
6758
|
+
file_size?: number | null;
|
|
6759
|
+
display_order?: number;
|
|
6760
|
+
}>;
|
|
5989
6761
|
} = {}) {
|
|
5990
6762
|
const targetWidgetId = options.widgetId ?? selectedWidgetId;
|
|
5991
6763
|
if (!targetWidgetId) return;
|
|
@@ -6062,8 +6834,10 @@ Requirements:
|
|
|
6062
6834
|
fromChat: options.fromChat,
|
|
6063
6835
|
widgetId: codegenTargetWidgetId,
|
|
6064
6836
|
compileFixOnly: options.compileFixOnly,
|
|
6837
|
+
forceRecreateRepo: options.forceRecreateRepo,
|
|
6065
6838
|
attachmentIds: options.attachmentIds,
|
|
6066
6839
|
promptImagesOverride: options.promptImagesOverride,
|
|
6840
|
+
persistFileAttachments: options.persistFileAttachments,
|
|
6067
6841
|
});
|
|
6068
6842
|
}
|
|
6069
6843
|
|
|
@@ -6082,8 +6856,17 @@ Requirements:
|
|
|
6082
6856
|
fromChat?: boolean;
|
|
6083
6857
|
widgetId?: string;
|
|
6084
6858
|
compileFixOnly?: boolean;
|
|
6859
|
+
forceRecreateRepo?: boolean;
|
|
6085
6860
|
attachmentIds?: string[];
|
|
6086
6861
|
promptImagesOverride?: string[];
|
|
6862
|
+
persistFileAttachments?: Array<{
|
|
6863
|
+
url?: string;
|
|
6864
|
+
file?: File;
|
|
6865
|
+
file_type?: string;
|
|
6866
|
+
file_name?: string | null;
|
|
6867
|
+
file_size?: number | null;
|
|
6868
|
+
display_order?: number;
|
|
6869
|
+
}>;
|
|
6087
6870
|
} = {},
|
|
6088
6871
|
) {
|
|
6089
6872
|
const generationWidgetId = options.widgetId ?? selectedWidgetId;
|
|
@@ -6106,7 +6889,6 @@ Requirements:
|
|
|
6106
6889
|
lastHarnessChatLine = '';
|
|
6107
6890
|
|
|
6108
6891
|
// Show user prompt in chat when triggered outside Chat (confirm dialog / Generate Now).
|
|
6109
|
-
// DB write happens here too — not on widget select (so "Edit Prompt First" stays editable).
|
|
6110
6892
|
if (panelChat && !options.fromChat) {
|
|
6111
6893
|
const snapImages =
|
|
6112
6894
|
generationWidgetId === focusedPanelWidgetId ? promptImages : panelSnap.promptImages;
|
|
@@ -6120,8 +6902,7 @@ Requirements:
|
|
|
6120
6902
|
: undefined;
|
|
6121
6903
|
panelChat.addUserMessage(prompt, codegenImageAttachments);
|
|
6122
6904
|
}
|
|
6123
|
-
|
|
6124
|
-
|
|
6905
|
+
|
|
6125
6906
|
isGeneratingCode = generationWidgetId === focusedPanelWidgetId;
|
|
6126
6907
|
setCrossTabGenerating(true, generationWidgetId);
|
|
6127
6908
|
lastGenerationPrompt = prompt;
|
|
@@ -6132,7 +6913,7 @@ Requirements:
|
|
|
6132
6913
|
? 'Processing queued code generation request...'
|
|
6133
6914
|
: 'Preparing code generation...',
|
|
6134
6915
|
});
|
|
6135
|
-
|
|
6916
|
+
|
|
6136
6917
|
// Keep preview tab active so user sees the "Generating..." overlay.
|
|
6137
6918
|
// Console logs are still accessible via the Console tab button.
|
|
6138
6919
|
getWidgetDetailsRef(generationWidgetId)?.setInlinePanelView('preview');
|
|
@@ -6141,22 +6922,52 @@ Requirements:
|
|
|
6141
6922
|
console.log('Widget Creator: Starting code generation for widget:', generationWidgetId);
|
|
6142
6923
|
|
|
6143
6924
|
try {
|
|
6144
|
-
const generationWidget =
|
|
6145
|
-
|
|
6146
|
-
?
|
|
6147
|
-
|
|
6925
|
+
const generationWidget =
|
|
6926
|
+
getWidgetById(generationWidgetId) ||
|
|
6927
|
+
(selectedWidget?.id === generationWidgetId ? selectedWidget : null);
|
|
6928
|
+
let repoId: string | null = null;
|
|
6929
|
+
try {
|
|
6930
|
+
repoId = generationWidget
|
|
6931
|
+
? await ensureWidgetRepository(generationWidget, {
|
|
6932
|
+
forceRecreate: !!options.forceRecreateRepo,
|
|
6933
|
+
silent: true,
|
|
6934
|
+
})
|
|
6935
|
+
: panelSnap.currentRepositoryId;
|
|
6936
|
+
if (!repoId && generationWidget) {
|
|
6937
|
+
console.warn('[CodeGen] Repository missing — retrying create for widget', generationWidgetId.substring(0, 8));
|
|
6938
|
+
repoId = await ensureWidgetRepository(generationWidget, { forceRecreate: true, silent: true });
|
|
6939
|
+
}
|
|
6940
|
+
} catch (repoError) {
|
|
6941
|
+
if (repoError instanceof ResponseError && repoError.response?.status === 401) {
|
|
6942
|
+
const sessionError = 'Session expired. Sign in again, then retry.';
|
|
6943
|
+
patchCodegenUiState(generationWidgetId, {
|
|
6944
|
+
generateCodeStatus: '❌ Session expired.',
|
|
6945
|
+
lastGenerationFailed: true,
|
|
6946
|
+
lastGenerationError: sessionError,
|
|
6947
|
+
});
|
|
6948
|
+
showToast('error', 'Session expired', {
|
|
6949
|
+
description: sessionError,
|
|
6950
|
+
duration: 5000
|
|
6951
|
+
});
|
|
6952
|
+
pushCodegenFailureToChat(generationWidgetId, sessionError);
|
|
6953
|
+
return;
|
|
6954
|
+
}
|
|
6955
|
+
throw repoError;
|
|
6956
|
+
}
|
|
6148
6957
|
console.log('Widget Creator: Repository resolved for code generation:', repoId);
|
|
6149
6958
|
|
|
6150
6959
|
if (!repoId) {
|
|
6960
|
+
const missingRepoError = 'Unable to resolve repository for this widget.';
|
|
6151
6961
|
patchCodegenUiState(generationWidgetId, {
|
|
6152
6962
|
generateCodeStatus: '❌ Repository is required to generate code.',
|
|
6153
6963
|
lastGenerationFailed: true,
|
|
6154
|
-
lastGenerationError:
|
|
6964
|
+
lastGenerationError: missingRepoError,
|
|
6155
6965
|
});
|
|
6156
6966
|
showToast('error', 'Missing repository', {
|
|
6157
|
-
description:
|
|
6967
|
+
description: missingRepoError,
|
|
6158
6968
|
duration: 5000
|
|
6159
6969
|
});
|
|
6970
|
+
pushCodegenFailureToChat(generationWidgetId, missingRepoError);
|
|
6160
6971
|
return;
|
|
6161
6972
|
}
|
|
6162
6973
|
|
|
@@ -6182,7 +6993,7 @@ Requirements:
|
|
|
6182
6993
|
// Drop invalid entries (empty strings, non-data-URLs) so a failed capture
|
|
6183
6994
|
// can never silently become a vision-less generation request.
|
|
6184
6995
|
panelPromptImages = panelPromptImages.filter(
|
|
6185
|
-
(url) => typeof url === 'string' && url.startsWith('data:image/') && url.length >
|
|
6996
|
+
(url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
|
|
6186
6997
|
);
|
|
6187
6998
|
const chatAttachmentUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
|
|
6188
6999
|
const chatUploadIds = [
|
|
@@ -6190,9 +7001,12 @@ Requirements:
|
|
|
6190
7001
|
...(panelChat?.getAttachmentUploadIds?.() ?? []),
|
|
6191
7002
|
].filter((id, index, arr) => id && arr.indexOf(id) === index);
|
|
6192
7003
|
if (chatAttachmentUrls.length > 0 && !(options.promptImagesOverride?.length)) {
|
|
6193
|
-
|
|
6194
|
-
(url) => typeof url === 'string' && url.startsWith('data:image/') && url.length >
|
|
7004
|
+
const fromChat = chatAttachmentUrls.filter(
|
|
7005
|
+
(url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
|
|
6195
7006
|
);
|
|
7007
|
+
if (fromChat.length > 0) {
|
|
7008
|
+
panelPromptImages = fromChat;
|
|
7009
|
+
}
|
|
6196
7010
|
}
|
|
6197
7011
|
let imagesToSend: string[] | undefined;
|
|
6198
7012
|
let attachmentIdsToSend: string[] | undefined;
|
|
@@ -6250,6 +7064,18 @@ Requirements:
|
|
|
6250
7064
|
});
|
|
6251
7065
|
}
|
|
6252
7066
|
|
|
7067
|
+
const persistImageUrls = uniquePersistImageUrls([
|
|
7068
|
+
...(imagesToSend || []),
|
|
7069
|
+
...(panelPromptImages || []),
|
|
7070
|
+
...(options.promptImagesOverride || []),
|
|
7071
|
+
]);
|
|
7072
|
+
await ensureCodegenUserMessagePersisted(
|
|
7073
|
+
generationWidgetId,
|
|
7074
|
+
prompt,
|
|
7075
|
+
persistImageUrls,
|
|
7076
|
+
options.persistFileAttachments,
|
|
7077
|
+
);
|
|
7078
|
+
|
|
6253
7079
|
// Clear unsent draft/attachments once payload is captured — prevents restore on widget return
|
|
6254
7080
|
clearPromptDraftForWidget(generationWidgetId);
|
|
6255
7081
|
|
|
@@ -6405,6 +7231,11 @@ Requirements:
|
|
|
6405
7231
|
hasGeneratedCode: true,
|
|
6406
7232
|
...(completedCommitSha ? { lastCommitId: completedCommitSha } : {}),
|
|
6407
7233
|
});
|
|
7234
|
+
if (isFocusedPanel && completedCommitSha) {
|
|
7235
|
+
lastCommitId = completedCommitSha;
|
|
7236
|
+
hasGeneratedCode = true;
|
|
7237
|
+
}
|
|
7238
|
+
panelDisplayRevision += 1;
|
|
6408
7239
|
console.log(
|
|
6409
7240
|
'[Publish] Codegen completed — pendingPublishAfterCodegen=true',
|
|
6410
7241
|
generationWidgetId.substring(0, 8),
|
|
@@ -6449,6 +7280,11 @@ Requirements:
|
|
|
6449
7280
|
}
|
|
6450
7281
|
})();
|
|
6451
7282
|
}
|
|
7283
|
+
if (generationWidgetId) {
|
|
7284
|
+
console.log('[CompositionEditor] Reloading schemas after codegen', generationWidgetId.substring(0, 8));
|
|
7285
|
+
void loadWidgetSchemas(generationWidgetId, { force: true });
|
|
7286
|
+
}
|
|
7287
|
+
|
|
6452
7288
|
const completedRepoId = resolveRepositoryIdForWidget(generationWidgetId);
|
|
6453
7289
|
if (completedRepoId) {
|
|
6454
7290
|
stampWidgetRepositoryId(generationWidgetId, completedRepoId);
|
|
@@ -6548,8 +7384,9 @@ Requirements:
|
|
|
6548
7384
|
);
|
|
6549
7385
|
setTimeout(async () => {
|
|
6550
7386
|
await panelChat?.reloadConversationMessages?.();
|
|
6551
|
-
|
|
6552
|
-
|
|
7387
|
+
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
7388
|
+
}, 2500);
|
|
7389
|
+
setTimeout(() => handleRefreshScreenshot(generationWidgetId, { quiet: true }), 3000);
|
|
6553
7390
|
} else {
|
|
6554
7391
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
6555
7392
|
`Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
@@ -6621,6 +7458,7 @@ Requirements:
|
|
|
6621
7458
|
});
|
|
6622
7459
|
clearActiveOperationId(); // Clear persisted operationId on error
|
|
6623
7460
|
showToast('error', 'Code generation error', { description: message, duration: 6000 });
|
|
7461
|
+
pushCodegenFailureToChat(generationWidgetId, message);
|
|
6624
7462
|
} finally {
|
|
6625
7463
|
const { [generationWidgetId]: _done, ...restFinally } = generatingWidgetIds;
|
|
6626
7464
|
generatingWidgetIds = restFinally;
|
|
@@ -6678,7 +7516,11 @@ Requirements:
|
|
|
6678
7516
|
commitSha?: string | null
|
|
6679
7517
|
): void {
|
|
6680
7518
|
const jsPath = resolvePublishedJsPath(widgetId, version, artifactUrl);
|
|
6681
|
-
const
|
|
7519
|
+
const prevSnap = getPanelState(widgetId);
|
|
7520
|
+
const normalizedCommit =
|
|
7521
|
+
normalizeCommitSha(commitSha)
|
|
7522
|
+
|| normalizeCommitSha(prevSnap.lastCommitId)
|
|
7523
|
+
|| normalizeCommitSha(loadCommitIdForWidget(widgetId));
|
|
6682
7524
|
|
|
6683
7525
|
widgets = widgets.map((w) =>
|
|
6684
7526
|
w.id === widgetId
|
|
@@ -6695,7 +7537,6 @@ Requirements:
|
|
|
6695
7537
|
: w
|
|
6696
7538
|
);
|
|
6697
7539
|
|
|
6698
|
-
const prevSnap = getPanelState(widgetId);
|
|
6699
7540
|
const optimisticHistoryEntry = normalizedCommit
|
|
6700
7541
|
? {
|
|
6701
7542
|
version,
|
|
@@ -6726,6 +7567,9 @@ Requirements:
|
|
|
6726
7567
|
saveCommitIdForWidget(widgetId, normalizedCommit);
|
|
6727
7568
|
}
|
|
6728
7569
|
|
|
7570
|
+
console.log('[CompositionEditor] Reloading schemas after publish', widgetId.substring(0, 8));
|
|
7571
|
+
void loadWidgetSchemas(widgetId, { force: true });
|
|
7572
|
+
|
|
6729
7573
|
if (selectedWidgetId === widgetId) {
|
|
6730
7574
|
lastPublishedVersion = version;
|
|
6731
7575
|
selectedWidgetJsPath = jsPath;
|
|
@@ -6920,6 +7764,16 @@ Requirements:
|
|
|
6920
7764
|
|
|
6921
7765
|
// Restore session on mount with expiration check
|
|
6922
7766
|
onMount(() => {
|
|
7767
|
+
const googleFontsKey =
|
|
7768
|
+
(typeof import.meta !== 'undefined' &&
|
|
7769
|
+
(import.meta.env?.PUBLIC_GOOGLE_FONTS_API_KEY || import.meta.env?.VITE_GOOGLE_FONTS_API_KEY)) ||
|
|
7770
|
+
'';
|
|
7771
|
+
if (googleFontsKey) {
|
|
7772
|
+
setGoogleFontsApiKey(String(googleFontsKey));
|
|
7773
|
+
console.log('[CreatorApp] Google Fonts API key set for FontIC');
|
|
7774
|
+
} else {
|
|
7775
|
+
console.warn('[CreatorApp] No PUBLIC_GOOGLE_FONTS_API_KEY / VITE_GOOGLE_FONTS_API_KEY — FontIC uses fallback catalog');
|
|
7776
|
+
}
|
|
6923
7777
|
setWebsocketApiUrl(apiRoot ?? null);
|
|
6924
7778
|
if (embedded) {
|
|
6925
7779
|
syncSessionFromSiteAuth();
|
|
@@ -6965,6 +7819,7 @@ Requirements:
|
|
|
6965
7819
|
|
|
6966
7820
|
// Cross-tab operation sync: listen for localStorage changes from other tabs
|
|
6967
7821
|
window.addEventListener('storage', handleCrossTabStorageEvent);
|
|
7822
|
+
window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
6968
7823
|
|
|
6969
7824
|
// Clean up cross-tab flags when tab is closed/refreshed
|
|
6970
7825
|
window.addEventListener('beforeunload', () => {
|
|
@@ -7058,14 +7913,20 @@ Requirements:
|
|
|
7058
7913
|
// Cross-tab cleanup: remove flags so other tabs don't stay blocked
|
|
7059
7914
|
if (typeof window !== 'undefined') {
|
|
7060
7915
|
window.removeEventListener('storage', handleCrossTabStorageEvent);
|
|
7916
|
+
window.removeEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
7061
7917
|
window.removeEventListener('click', handleCanvasDropdownOutsideClick);
|
|
7062
7918
|
window.removeEventListener(HOST_WIDGET_RENAMED_EVENT, handleHostWidgetRenamedEvent);
|
|
7063
7919
|
window.removeEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
|
|
7064
7920
|
window.removeEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
|
|
7065
7921
|
window.removeEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
|
|
7922
|
+
window.removeEventListener(HOST_TOGGLE_ADVANCED_EVENT, handleHostToggleAdvancedEvent);
|
|
7066
7923
|
window.removeEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
|
|
7067
7924
|
window.removeEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
|
|
7068
7925
|
window.removeEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
|
|
7926
|
+
window.removeEventListener(HOST_SAVE_CANVAS_EVENT, handleHostSaveCanvasEvent);
|
|
7927
|
+
window.removeEventListener(HOST_SET_PRIMARY_WIDGET_EVENT, handleHostSetPrimaryWidgetEvent);
|
|
7928
|
+
window.removeEventListener(HOST_REQUEST_CANVAS_WIDGETS_EVENT, handleHostRequestCanvasWidgetsEvent);
|
|
7929
|
+
window.removeEventListener(HOST_MOVE_CANVAS_FOLDER_EVENT, handleHostMoveCanvasFolderEvent);
|
|
7069
7930
|
for (const wId of Object.keys(generatingWidgetIds)) {
|
|
7070
7931
|
setCrossTabGenerating(false, wId);
|
|
7071
7932
|
}
|
|
@@ -7091,11 +7952,18 @@ Requirements:
|
|
|
7091
7952
|
window.addEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
|
|
7092
7953
|
window.addEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
|
|
7093
7954
|
window.addEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
|
|
7955
|
+
window.addEventListener(HOST_TOGGLE_ADVANCED_EVENT, handleHostToggleAdvancedEvent);
|
|
7094
7956
|
window.addEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
|
|
7095
7957
|
window.addEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
|
|
7096
7958
|
window.addEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
|
|
7959
|
+
window.addEventListener(HOST_SAVE_CANVAS_EVENT, handleHostSaveCanvasEvent);
|
|
7960
|
+
window.addEventListener(HOST_SET_PRIMARY_WIDGET_EVENT, handleHostSetPrimaryWidgetEvent);
|
|
7961
|
+
window.addEventListener(HOST_REQUEST_CANVAS_WIDGETS_EVENT, handleHostRequestCanvasWidgetsEvent);
|
|
7962
|
+
window.addEventListener(HOST_MOVE_CANVAS_FOLDER_EVENT, handleHostMoveCanvasFolderEvent);
|
|
7097
7963
|
emitCreatorDebugChanged();
|
|
7098
7964
|
emitCreatorWireframeChanged();
|
|
7965
|
+
emitCreatorAdvancedChanged();
|
|
7966
|
+
emitCreatorSaveState(false);
|
|
7099
7967
|
});
|
|
7100
7968
|
|
|
7101
7969
|
// Capture-phase Escape: close lightbox before Canvas can deselect → close WidgetDetails
|
|
@@ -7133,8 +8001,6 @@ Requirements:
|
|
|
7133
8001
|
if (file) {
|
|
7134
8002
|
const base64 = await processImageFile(file);
|
|
7135
8003
|
if (base64) {
|
|
7136
|
-
promptImages = [...promptImages, base64];
|
|
7137
|
-
savePromptImagesToSession();
|
|
7138
8004
|
chatMethods?.addImageAttachment?.(base64, file.name || 'pasted-image.png');
|
|
7139
8005
|
showToast('success', 'Image pasted to prompt', { duration: 2000 });
|
|
7140
8006
|
}
|
|
@@ -7488,7 +8354,7 @@ Requirements:
|
|
|
7488
8354
|
compositionSchemasLoading,
|
|
7489
8355
|
compositionSchemasError,
|
|
7490
8356
|
schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
|
|
7491
|
-
chatContainerHeight: focusedSnap?.chatContainerHeight ??
|
|
8357
|
+
chatContainerHeight: focusedSnap?.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7492
8358
|
panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
|
|
7493
8359
|
};
|
|
7494
8360
|
}
|
|
@@ -7541,7 +8407,7 @@ Requirements:
|
|
|
7541
8407
|
compositionSchemasLoading: false,
|
|
7542
8408
|
compositionSchemasError: null,
|
|
7543
8409
|
schemaLoadStartedAt: null,
|
|
7544
|
-
chatContainerHeight:
|
|
8410
|
+
chatContainerHeight: 550,
|
|
7545
8411
|
panelPreviewCompileError: null,
|
|
7546
8412
|
};
|
|
7547
8413
|
}
|
|
@@ -7567,16 +8433,18 @@ Requirements:
|
|
|
7567
8433
|
const snapPublishReady =
|
|
7568
8434
|
snap.publishHistoryResolved && snap.publishHistoryResolvedForWidgetId === widgetId;
|
|
7569
8435
|
const livePublishMatchesPanel = live.publishHistoryResolvedForWidgetId === widgetId;
|
|
7570
|
-
const resolvedHead = resolvePanelHeadCommitId(widgetId, snap, live);
|
|
8436
|
+
const resolvedHead = isDraftConvertId(widgetId) ? null : resolvePanelHeadCommitId(widgetId, snap, live);
|
|
7571
8437
|
return {
|
|
7572
8438
|
...snap,
|
|
7573
8439
|
...live,
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
8440
|
+
lastCommitId: isDraftConvertId(widgetId) ? null : resolvedHead,
|
|
8441
|
+
hasGeneratedCode: isDraftConvertId(widgetId)
|
|
8442
|
+
? false
|
|
8443
|
+
: !!(live.hasGeneratedCode || snap.hasGeneratedCode || resolvedHead),
|
|
7578
8444
|
pendingPublishAfterCodegen: !!(snap.pendingPublishAfterCodegen || live.pendingPublishAfterCodegen),
|
|
7579
|
-
lastPublishedVersion:
|
|
8445
|
+
lastPublishedVersion: isDraftConvertId(widgetId)
|
|
8446
|
+
? null
|
|
8447
|
+
: (live.lastPublishedVersion ?? snap.lastPublishedVersion),
|
|
7580
8448
|
currentRepositoryId: live.currentRepositoryId ?? snap.currentRepositoryId,
|
|
7581
8449
|
selectedWidgetJsPath: live.selectedWidgetJsPath ?? snap.selectedWidgetJsPath,
|
|
7582
8450
|
// Once publish history loaded into this panel's snapshot, prefer it over stale globals.
|
|
@@ -7595,7 +8463,7 @@ Requirements:
|
|
|
7595
8463
|
: livePublishMatchesPanel
|
|
7596
8464
|
? live.publishHistoryResolvedForWidgetId
|
|
7597
8465
|
: snap.publishHistoryResolvedForWidgetId,
|
|
7598
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
8466
|
+
chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7599
8467
|
compositionDesignSchema: resolvedDesignSchema,
|
|
7600
8468
|
compositionContentSchema: resolvedContentSchema,
|
|
7601
8469
|
compositionSchemasLoading: schemaLoading,
|
|
@@ -7613,7 +8481,7 @@ Requirements:
|
|
|
7613
8481
|
...snap,
|
|
7614
8482
|
lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
|
|
7615
8483
|
hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
|
|
7616
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
8484
|
+
chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7617
8485
|
compositionDesignSchema: resolvedDesignSchema,
|
|
7618
8486
|
compositionContentSchema: resolvedContentSchema,
|
|
7619
8487
|
compositionSchemasError: resolvedSchemaError,
|
|
@@ -7903,6 +8771,22 @@ Requirements:
|
|
|
7903
8771
|
selectedWidget = widget;
|
|
7904
8772
|
saveSelectedWidgetId(widgetId);
|
|
7905
8773
|
notifyHostWidgetName(widget);
|
|
8774
|
+
if (isDraftConvertId(widgetId)) {
|
|
8775
|
+
lastPublishedVersion = null;
|
|
8776
|
+
lastCommitId = null;
|
|
8777
|
+
hasGeneratedCode = false;
|
|
8778
|
+
publishHistory = [];
|
|
8779
|
+
publishHistoryResolved = true;
|
|
8780
|
+
publishHistoryResolvedForWidgetId = widgetId;
|
|
8781
|
+
widgetDetailsStep = 'create';
|
|
8782
|
+
patchPanelSnapshot(widgetId, {
|
|
8783
|
+
lastPublishedVersion: null,
|
|
8784
|
+
lastCommitId: null,
|
|
8785
|
+
hasGeneratedCode: false,
|
|
8786
|
+
publishHistory: [],
|
|
8787
|
+
widgetDetailsStep: 'create',
|
|
8788
|
+
});
|
|
8789
|
+
}
|
|
7906
8790
|
if (widget?.id && canvasRef) {
|
|
7907
8791
|
canvasRef.selectWidgetShape(widget.id);
|
|
7908
8792
|
}
|
|
@@ -7920,8 +8804,20 @@ Requirements:
|
|
|
7920
8804
|
}
|
|
7921
8805
|
|
|
7922
8806
|
function positionOpenDetailsPanel(widgetId: string): void {
|
|
7923
|
-
const
|
|
7924
|
-
|
|
8807
|
+
const apply = () => {
|
|
8808
|
+
let rect = canvasRef?.getWidgetShapeScreenRect?.(widgetId) ?? null;
|
|
8809
|
+
if (!rect) {
|
|
8810
|
+
const objectId = draftConvertObjectIds.get(widgetId);
|
|
8811
|
+
if (objectId && canvasRef?.getObjectScreenRect) {
|
|
8812
|
+
rect = canvasRef.getObjectScreenRect(objectId);
|
|
8813
|
+
}
|
|
8814
|
+
}
|
|
8815
|
+
getWidgetDetailsRef(widgetId)?.positionBesideShape?.(rect);
|
|
8816
|
+
};
|
|
8817
|
+
apply();
|
|
8818
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
8819
|
+
requestAnimationFrame(apply);
|
|
8820
|
+
}
|
|
7925
8821
|
}
|
|
7926
8822
|
|
|
7927
8823
|
async function openWidgetDetailsPanel(widgetId: string): Promise<void> {
|
|
@@ -7952,6 +8848,14 @@ Requirements:
|
|
|
7952
8848
|
openWidgetPanelIds = [...openWidgetPanelIds, widgetId];
|
|
7953
8849
|
}
|
|
7954
8850
|
focusWidgetPanel(widgetId);
|
|
8851
|
+
if (isDraftConvertId(widgetId)) {
|
|
8852
|
+
detailsSessionWidgetId = widgetId;
|
|
8853
|
+
}
|
|
8854
|
+
const alreadyMounted = getWidgetDetailsRef(widgetId);
|
|
8855
|
+
if (alreadyMounted && !alreadyMounted.getShowWidgetDetails?.()) {
|
|
8856
|
+
alreadyMounted.openWidgetDetails();
|
|
8857
|
+
positionOpenDetailsPanel(widgetId);
|
|
8858
|
+
}
|
|
7955
8859
|
if (openGen !== widgetDetailsOpenGen || widgetDetailsOpenTargetId !== widgetId) {
|
|
7956
8860
|
console.log('[CreatorApp] Skip stale Widget Details open:', widgetId?.substring(0, 8));
|
|
7957
8861
|
return;
|
|
@@ -8048,6 +8952,8 @@ Requirements:
|
|
|
8048
8952
|
openWidgetPanelIds = replaced.includes(toId) ? replaced : [...replaced, toId];
|
|
8049
8953
|
if (focusedPanelWidgetId === fromId) focusedPanelWidgetId = toId;
|
|
8050
8954
|
if (selectedWidgetId === fromId) selectedWidgetId = toId;
|
|
8955
|
+
if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
|
|
8956
|
+
if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
|
|
8051
8957
|
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
8052
8958
|
console.log(
|
|
8053
8959
|
'[CreatorApp] Migrated Widget Details panel',
|
|
@@ -8074,6 +8980,9 @@ Requirements:
|
|
|
8074
8980
|
if (!options.fromShapeSwitch && widgetDetailsOpenTargetId === widgetId) {
|
|
8075
8981
|
widgetDetailsOpenTargetId = null;
|
|
8076
8982
|
}
|
|
8983
|
+
if (detailsSessionWidgetId === widgetId) {
|
|
8984
|
+
detailsSessionWidgetId = null;
|
|
8985
|
+
}
|
|
8077
8986
|
getWidgetDetailsRef(widgetId)?.closeWidgetDetails({ silent: true });
|
|
8078
8987
|
openWidgetPanelIds = openWidgetPanelIds.filter((id) => id !== widgetId);
|
|
8079
8988
|
const { [widgetId]: _z, ...restZ } = panelZIndexByWidgetId;
|
|
@@ -8106,6 +9015,8 @@ Requirements:
|
|
|
8106
9015
|
panelInstanceKeyByWidgetId = {};
|
|
8107
9016
|
focusedPanelWidgetId = null;
|
|
8108
9017
|
panelZIndexByWidgetId = {};
|
|
9018
|
+
detailsSessionWidgetId = null;
|
|
9019
|
+
widgetDetailsOpenTargetId = null;
|
|
8109
9020
|
showWidgetDetails = false;
|
|
8110
9021
|
}
|
|
8111
9022
|
|
|
@@ -8378,11 +9289,10 @@ Requirements:
|
|
|
8378
9289
|
// If the token expired during generation, we delayed logout to avoid disrupting the operation.
|
|
8379
9290
|
// Now that the operation is done, proceed with logout.
|
|
8380
9291
|
$: if (pendingTokenExpiryLogout && !isGeneratingCode && Object.keys(generatingWidgetIds).length === 0) {
|
|
8381
|
-
console.log('WidgetCreator:
|
|
9292
|
+
console.log('WidgetCreator: Token expired after operations — keeping canvas, prompting re-auth');
|
|
8382
9293
|
pendingTokenExpiryLogout = false;
|
|
8383
9294
|
tokenExpiryWarningShown = false;
|
|
8384
|
-
|
|
8385
|
-
authStatus = 'Token expired. Please login again.';
|
|
9295
|
+
authStatus = 'Session expired. Sign in again to keep saving.';
|
|
8386
9296
|
}
|
|
8387
9297
|
|
|
8388
9298
|
|
|
@@ -8464,35 +9374,56 @@ Requirements:
|
|
|
8464
9374
|
return [];
|
|
8465
9375
|
}
|
|
8466
9376
|
|
|
9377
|
+
function unwrapContentMediaValue(raw: unknown): unknown {
|
|
9378
|
+
if (raw && typeof raw === 'object' && 'url' in (raw as Record<string, unknown>)) {
|
|
9379
|
+
return (raw as { url?: string }).url ?? '';
|
|
9380
|
+
}
|
|
9381
|
+
return raw;
|
|
9382
|
+
}
|
|
9383
|
+
|
|
9384
|
+
function pickLiveContentValue(...candidates: unknown[]): unknown {
|
|
9385
|
+
for (const candidate of candidates) {
|
|
9386
|
+
const unwrapped = unwrapContentMediaValue(candidate);
|
|
9387
|
+
if (unwrapped === undefined || unwrapped === null || unwrapped === '') continue;
|
|
9388
|
+
return unwrapped;
|
|
9389
|
+
}
|
|
9390
|
+
return '';
|
|
9391
|
+
}
|
|
9392
|
+
|
|
8467
9393
|
/** Flatten schema-shaped content items to runtime values for preview postMessage. */
|
|
8468
9394
|
function flattenContentItemsForPreview(items: any[]): any[] {
|
|
8469
9395
|
return items.map((item) => {
|
|
8470
9396
|
if (!item || typeof item !== 'object') return item;
|
|
8471
9397
|
const flat: Record<string, unknown> = { id: item.id, title: item.title };
|
|
8472
|
-
|
|
8473
|
-
|
|
9398
|
+
for (const [key, value] of Object.entries(item)) {
|
|
9399
|
+
if (key === 'properties' || key === 'inputController' || key === 'id' || key === 'title') continue;
|
|
9400
|
+
const unwrapped = unwrapContentMediaValue(value);
|
|
9401
|
+
if (unwrapped === undefined) continue;
|
|
9402
|
+
flat[key] = unwrapped;
|
|
9403
|
+
}
|
|
8474
9404
|
const props = item.properties;
|
|
8475
9405
|
if (Array.isArray(props)) {
|
|
8476
9406
|
for (const prop of props) {
|
|
8477
9407
|
const name = prop?.name || prop?.id;
|
|
8478
9408
|
if (!name) continue;
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
if (
|
|
8482
|
-
flat[name] = (raw as { url?: string }).url ?? '';
|
|
8483
|
-
} else {
|
|
8484
|
-
flat[name] = raw;
|
|
8485
|
-
}
|
|
9409
|
+
if (flat[name] !== undefined && flat[name] !== '') continue;
|
|
9410
|
+
const picked = pickLiveContentValue(prop.value, prop.defaultValue);
|
|
9411
|
+
if (picked !== '') flat[name] = picked;
|
|
8486
9412
|
}
|
|
8487
9413
|
}
|
|
8488
|
-
for (const [key, value] of Object.entries(item)) {
|
|
8489
|
-
if (key === 'properties' || key === 'inputController') continue;
|
|
8490
|
-
if (flat[key] === undefined && value !== undefined) flat[key] = value;
|
|
8491
|
-
}
|
|
8492
9414
|
return flat;
|
|
8493
9415
|
});
|
|
8494
9416
|
}
|
|
8495
9417
|
|
|
9418
|
+
function contentItemsToEditorValueMap(items: any[]): Record<string, Record<string, unknown>> {
|
|
9419
|
+
const map: Record<string, Record<string, unknown>> = {};
|
|
9420
|
+
for (const item of flattenContentItemsForPreview(items || [])) {
|
|
9421
|
+
if (!item?.id) continue;
|
|
9422
|
+
map[String(item.id)] = item as Record<string, unknown>;
|
|
9423
|
+
}
|
|
9424
|
+
return map;
|
|
9425
|
+
}
|
|
9426
|
+
|
|
8496
9427
|
function contentItemHasProperties(item: any): boolean {
|
|
8497
9428
|
return Array.isArray(item?.properties) && item.properties.length > 0;
|
|
8498
9429
|
}
|
|
@@ -8500,22 +9431,28 @@ Requirements:
|
|
|
8500
9431
|
/** New items from the editor arrive flat ({id, caption, image}) — reattach schema fields so the form stays editable. */
|
|
8501
9432
|
function attachSchemaProperties(item: any, template: any): any {
|
|
8502
9433
|
if (!item || typeof item !== 'object') return item;
|
|
8503
|
-
|
|
8504
|
-
if (!contentItemHasProperties(
|
|
8505
|
-
const
|
|
8506
|
-
|
|
9434
|
+
const templateHasProps = contentItemHasProperties(template);
|
|
9435
|
+
if (!contentItemHasProperties(item) && !templateHasProps) return item;
|
|
9436
|
+
const sourceProperties = contentItemHasProperties(item)
|
|
9437
|
+
? item.properties
|
|
9438
|
+
: JSON.parse(JSON.stringify(template.properties));
|
|
9439
|
+
const nextProperties = sourceProperties.map((prop: any) => {
|
|
8507
9440
|
const key = prop.id || prop.name;
|
|
8508
|
-
const
|
|
9441
|
+
const hasOwn = key != null && Object.prototype.hasOwnProperty.call(item, key);
|
|
9442
|
+
const incoming = hasOwn ? item[key] : undefined;
|
|
9443
|
+
const picked = hasOwn
|
|
9444
|
+
? (unwrapContentMediaValue(incoming) ?? '')
|
|
9445
|
+
: pickLiveContentValue(prop.value, prop.defaultValue);
|
|
8509
9446
|
return {
|
|
8510
9447
|
...prop,
|
|
8511
|
-
value:
|
|
9448
|
+
value: picked
|
|
8512
9449
|
};
|
|
8513
9450
|
});
|
|
8514
9451
|
return {
|
|
8515
|
-
...
|
|
9452
|
+
...(templateHasProps ? JSON.parse(JSON.stringify(template)) : {}),
|
|
8516
9453
|
...item,
|
|
8517
9454
|
id: item.id,
|
|
8518
|
-
title: item.title ||
|
|
9455
|
+
title: item.title || template?.title,
|
|
8519
9456
|
properties: nextProperties
|
|
8520
9457
|
};
|
|
8521
9458
|
}
|
|
@@ -8683,20 +9620,11 @@ Requirements:
|
|
|
8683
9620
|
const effectiveCanvasId = getEffectiveCanvasId();
|
|
8684
9621
|
const panelIsPublishing = isPublishing && publishingWidgetId === widgetId;
|
|
8685
9622
|
const anotherPanelIsPublishing = isPublishing && publishingWidgetId !== null && publishingWidgetId !== widgetId;
|
|
8686
|
-
const stuckGlobalPublishing = isPublishing && !publishingWidgetId;
|
|
8687
9623
|
const nextVersion = (ps.lastPublishedVersion ?? 0) + 1;
|
|
8688
9624
|
|
|
8689
9625
|
if (panelIsPublishing) {
|
|
8690
9626
|
return { disabled: true, label: 'Publishing...', reason: 'Publishing in progress…', showDisabledHint: false };
|
|
8691
9627
|
}
|
|
8692
|
-
if (stuckGlobalPublishing) {
|
|
8693
|
-
return {
|
|
8694
|
-
disabled: true,
|
|
8695
|
-
label: getPublishButtonLabelFallback(ps),
|
|
8696
|
-
reason: 'Publish state is busy — try again in a moment.',
|
|
8697
|
-
showDisabledHint: true,
|
|
8698
|
-
};
|
|
8699
|
-
}
|
|
8700
9628
|
if (anotherPanelIsPublishing) {
|
|
8701
9629
|
return {
|
|
8702
9630
|
disabled: true,
|
|
@@ -8705,7 +9633,7 @@ Requirements:
|
|
|
8705
9633
|
showDisabledHint: true,
|
|
8706
9634
|
};
|
|
8707
9635
|
}
|
|
8708
|
-
if (
|
|
9636
|
+
if (checkCrossTabPublishing(widgetId)) {
|
|
8709
9637
|
return {
|
|
8710
9638
|
disabled: true,
|
|
8711
9639
|
label: getPublishButtonLabelFallback(ps),
|
|
@@ -8764,8 +9692,10 @@ Requirements:
|
|
|
8764
9692
|
getLivePublishedCommitShaForPanel(widgetId)
|
|
8765
9693
|
?? getWidgetLastPublishCommitSha(widgetId);
|
|
8766
9694
|
const pendingPublish = !!ps.pendingPublishAfterCodegen;
|
|
9695
|
+
const unpublishedAhead = hasUnpublishedChangesForPanel(widgetId)
|
|
9696
|
+
|| !!(ps.lastCommitId && liveSha && !commitsMatch(ps.lastCommitId, liveSha));
|
|
8767
9697
|
|
|
8768
|
-
// Known up-to-date: HEAD matches live
|
|
9698
|
+
// Known up-to-date: HEAD matches live. Must win over leftover pendingPublish flags.
|
|
8769
9699
|
if (headSha && liveSha && commitsMatch(headSha, liveSha)) {
|
|
8770
9700
|
return {
|
|
8771
9701
|
disabled: true,
|
|
@@ -8775,10 +9705,24 @@ Requirements:
|
|
|
8775
9705
|
};
|
|
8776
9706
|
}
|
|
8777
9707
|
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
9708
|
+
if (pendingPublish || unpublishedAhead) {
|
|
9709
|
+
console.log('[Publish] Gate: unpublished HEAD — enable', widgetId.substring(0, 8), {
|
|
9710
|
+
pendingPublish,
|
|
9711
|
+
unpublishedAhead,
|
|
9712
|
+
headSha: headSha?.substring(0, 8),
|
|
9713
|
+
displayHead: ps.lastCommitId?.substring(0, 8),
|
|
9714
|
+
liveSha: liveSha?.substring(0, 8),
|
|
9715
|
+
});
|
|
9716
|
+
return {
|
|
9717
|
+
disabled: false,
|
|
9718
|
+
label: `Publish new v${nextVersion}`,
|
|
9719
|
+
reason: null,
|
|
9720
|
+
showDisabledHint: false,
|
|
9721
|
+
};
|
|
9722
|
+
}
|
|
9723
|
+
|
|
9724
|
+
// After a live version exists, enable vN+1 when HEAD is proven different.
|
|
9725
|
+
if (headSha && liveSha && !commitsMatch(headSha, liveSha)) {
|
|
8782
9726
|
return {
|
|
8783
9727
|
disabled: false,
|
|
8784
9728
|
label: `Publish new v${nextVersion}`,
|
|
@@ -8920,7 +9864,7 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
|
|
|
8920
9864
|
}
|
|
8921
9865
|
if (publishWidgetId) {
|
|
8922
9866
|
console.log('[Publish] Refreshing canvas widget screenshot after successful publish');
|
|
8923
|
-
setTimeout(() => handleRefreshScreenshot(publishWidgetId), 2000);
|
|
9867
|
+
setTimeout(() => handleRefreshScreenshot(publishWidgetId, { quiet: true }), 2000);
|
|
8924
9868
|
}
|
|
8925
9869
|
if (embedded && onPublishComplete && publishWidgetId) {
|
|
8926
9870
|
onPublishComplete({
|
|
@@ -9316,6 +10260,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9316
10260
|
hasValidToken = false;
|
|
9317
10261
|
inlinePanelView = null;
|
|
9318
10262
|
selectedWidget = null;
|
|
10263
|
+
if (typeof window !== 'undefined') {
|
|
10264
|
+
window.location.href = '/login';
|
|
10265
|
+
}
|
|
9319
10266
|
}
|
|
9320
10267
|
|
|
9321
10268
|
// decodeJWT imported from ./pageHelpers.ts
|
|
@@ -9332,11 +10279,12 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9332
10279
|
|
|
9333
10280
|
// if the decoded token has an expiration field, update the token status
|
|
9334
10281
|
if (decoded && decoded.exp) {
|
|
9335
|
-
// calculate the token time left
|
|
9336
10282
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
9337
|
-
|
|
10283
|
+
const expUnix = decoded.exp;
|
|
10284
|
+
tokenTimeLeft = expUnix - currentTime;
|
|
9338
10285
|
tokenExpired = tokenTimeLeft <= 0;
|
|
9339
|
-
|
|
10286
|
+
// API calls must follow the real JWT, never the sessionBannerTest clock.
|
|
10287
|
+
hasValidToken = decoded.exp - currentTime > 0;
|
|
9340
10288
|
|
|
9341
10289
|
// log the token expiration status
|
|
9342
10290
|
if (tokenExpired) {
|
|
@@ -9384,33 +10332,26 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9384
10332
|
// If there are active operations (generating, loading preview), defer logout
|
|
9385
10333
|
// and show a warning instead of abruptly cutting off the operation
|
|
9386
10334
|
function checkTokenExpiration() {
|
|
10335
|
+
updateTokenStatus();
|
|
9387
10336
|
if (currentUserToken) {
|
|
9388
|
-
if (
|
|
9389
|
-
// Check if there are active operations that would be disrupted by logout
|
|
10337
|
+
if (tokenExpired || tokenTimeLeft <= 0) {
|
|
9390
10338
|
const hasActiveOperations = isGeneratingCode || (showPreviewModal && previewLoading);
|
|
9391
10339
|
|
|
9392
10340
|
if (hasActiveOperations) {
|
|
9393
|
-
// Don't logout yet — defer until operations complete
|
|
9394
10341
|
pendingTokenExpiryLogout = true;
|
|
9395
|
-
|
|
9396
10342
|
if (!tokenExpiryWarningShown) {
|
|
9397
10343
|
tokenExpiryWarningShown = true;
|
|
9398
|
-
console.warn('WidgetCreator.checkTokenExpiration: Token expired but active operations in progress —
|
|
9399
|
-
authStatus = '
|
|
9400
|
-
showToast('warning', 'Session expiring', {
|
|
9401
|
-
description: 'Your login token has expired. You will be logged out after the current operation completes.',
|
|
9402
|
-
duration: 8000
|
|
9403
|
-
});
|
|
10344
|
+
console.warn('WidgetCreator.checkTokenExpiration: Token expired but active operations in progress — keeping the workspace open');
|
|
10345
|
+
authStatus = 'Session expired — finish this operation, then sign in again to save.';
|
|
9404
10346
|
}
|
|
9405
10347
|
return false;
|
|
9406
10348
|
}
|
|
9407
10349
|
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
authStatus = 'Token expired. Please login again.';
|
|
10350
|
+
if (!tokenExpiryWarningShown) {
|
|
10351
|
+
tokenExpiryWarningShown = true;
|
|
10352
|
+
console.log('WidgetCreator.checkTokenExpiration: Token expired — waiting for site session handler');
|
|
10353
|
+
authStatus = 'Session expired.';
|
|
10354
|
+
}
|
|
9414
10355
|
return false;
|
|
9415
10356
|
}
|
|
9416
10357
|
}
|
|
@@ -9582,6 +10523,24 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9582
10523
|
});
|
|
9583
10524
|
return;
|
|
9584
10525
|
}
|
|
10526
|
+
const lockedPanelVisible = !!(
|
|
10527
|
+
detailsSessionWidgetId
|
|
10528
|
+
&& openWidgetPanelIds.includes(detailsSessionWidgetId)
|
|
10529
|
+
&& (getWidgetDetailsRef(detailsSessionWidgetId)?.getShowWidgetDetails?.() ?? false)
|
|
10530
|
+
);
|
|
10531
|
+
if (
|
|
10532
|
+
detailsSessionWidgetId
|
|
10533
|
+
&& detailsSessionWidgetId !== widgetId
|
|
10534
|
+
&& lockedPanelVisible
|
|
10535
|
+
) {
|
|
10536
|
+
console.log('[WidgetCreator] Skip widget-shape click — convert session locked to', detailsSessionWidgetId.substring(0, 8));
|
|
10537
|
+
return;
|
|
10538
|
+
}
|
|
10539
|
+
if (lockedPanelVisible && isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
|
|
10540
|
+
console.log('[WidgetCreator] Skip widget-shape click — convert/draft panel is open:', widgetId.substring(0, 8));
|
|
10541
|
+
return;
|
|
10542
|
+
}
|
|
10543
|
+
|
|
9585
10544
|
console.log('[WidgetCreator] Widget Image clicked, opening widget details panel:', widgetId);
|
|
9586
10545
|
|
|
9587
10546
|
// Explicit user intent — cancel any in-flight deep-link auto-open and allow this panel.
|
|
@@ -9595,8 +10554,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9595
10554
|
}
|
|
9596
10555
|
}
|
|
9597
10556
|
|
|
9598
|
-
// Keep address-bar widget_id
|
|
9599
|
-
if (typeof window !== 'undefined' && embedded) {
|
|
10557
|
+
// Keep address-bar widget_id on the canvas primary (duplicate/select must not steal it).
|
|
10558
|
+
if (typeof window !== 'undefined' && embedded && canvasPrimaryWidgetId && widgetId === canvasPrimaryWidgetId) {
|
|
9600
10559
|
try {
|
|
9601
10560
|
const url = new URL(window.location.href);
|
|
9602
10561
|
if (url.searchParams.get('widget_id') !== widgetId) {
|
|
@@ -9646,7 +10605,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9646
10605
|
id: sourceWidgetId,
|
|
9647
10606
|
duplicateWidgetRequest: { canvas_id: canvasId } as any
|
|
9648
10607
|
});
|
|
9649
|
-
|
|
10608
|
+
let newWidget = response?.data;
|
|
9650
10609
|
|
|
9651
10610
|
if (!newWidget?.id) {
|
|
9652
10611
|
console.error('[WidgetCreator] duplicateWidget returned no data', response);
|
|
@@ -9672,37 +10631,94 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9672
10631
|
}
|
|
9673
10632
|
|
|
9674
10633
|
// Re-link the cloned canvas shape to the new widget (also clears _isDuplicating on clone)
|
|
9675
|
-
|
|
10634
|
+
if (clonedObject) {
|
|
10635
|
+
canvasRef?.linkObjectToWidget(clonedObject, newWidget.id, newWidget.name ?? null);
|
|
10636
|
+
} else if (canvasRef?.addWidgetShape) {
|
|
10637
|
+
canvasRef.addWidgetShape(newWidget.id, newWidget.name, 0, 1, null);
|
|
10638
|
+
}
|
|
9676
10639
|
setTimeout(() => {
|
|
9677
10640
|
const json = canvasRef?.saveCanvas();
|
|
9678
10641
|
if (json) handleCanvasAutoSave(json);
|
|
9679
10642
|
}, 500);
|
|
9680
10643
|
|
|
9681
|
-
// Add to beginning of list (newest first) and auto-select
|
|
9682
10644
|
widgets = [{ ...newWidget }, ...widgets];
|
|
9683
10645
|
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
10646
|
+
const sourceWidget = getWidgetById(sourceWidgetId);
|
|
10647
|
+
const sourceJsPath =
|
|
10648
|
+
((sourceWidget as { jsPath?: string | null; js_path?: string | null } | null)?.jsPath
|
|
10649
|
+
?? (sourceWidget as { js_path?: string | null } | null)?.js_path)
|
|
10650
|
+
?? null;
|
|
10651
|
+
const duplicateJsPath =
|
|
10652
|
+
sourceJsPath
|
|
10653
|
+
?? ((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
|
|
10654
|
+
?? (newWidget as { js_path?: string | null }).js_path);
|
|
10655
|
+
if (sourceWidget && duplicateJsPath) {
|
|
10656
|
+
Object.assign(newWidget, {
|
|
10657
|
+
jsPath: duplicateJsPath,
|
|
10658
|
+
js_path: duplicateJsPath,
|
|
10659
|
+
version: (newWidget as { version?: number }).version ?? (sourceWidget as { version?: number }).version,
|
|
10660
|
+
isDraft: (newWidget as { isDraft?: boolean }).isDraft
|
|
10661
|
+
?? (sourceWidget as { isDraft?: boolean; is_draft?: boolean }).isDraft
|
|
10662
|
+
?? (sourceWidget as { is_draft?: boolean }).is_draft,
|
|
10663
|
+
lastCpgCommitSha:
|
|
10664
|
+
(newWidget as { lastCpgCommitSha?: string | null }).lastCpgCommitSha
|
|
10665
|
+
?? (sourceWidget as { lastCpgCommitSha?: string | null }).lastCpgCommitSha,
|
|
10666
|
+
lastPublishCommitSha:
|
|
10667
|
+
(newWidget as { lastPublishCommitSha?: string | null }).lastPublishCommitSha
|
|
10668
|
+
?? (sourceWidget as { lastPublishCommitSha?: string | null; last_publish_commit_sha?: string | null }).lastPublishCommitSha
|
|
10669
|
+
?? (sourceWidget as { last_publish_commit_sha?: string | null }).last_publish_commit_sha,
|
|
10670
|
+
});
|
|
10671
|
+
widgets = widgets.map((w) => (w.id === newWidget.id ? { ...newWidget } : w));
|
|
10672
|
+
}
|
|
10673
|
+
|
|
10674
|
+
const sourceCommit = loadCommitIdForWidget(sourceWidgetId);
|
|
10675
|
+
if (sourceCommit) {
|
|
10676
|
+
saveCommitIdForWidget(newWidget.id, sourceCommit);
|
|
10677
|
+
}
|
|
10678
|
+
|
|
10679
|
+
const sourceHasCode =
|
|
10680
|
+
!!normalizeRepositoryId(newWidget)
|
|
10681
|
+
|| !!((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
|
|
10682
|
+
?? (newWidget as { js_path?: string | null }).js_path)
|
|
10683
|
+
|| resolvePublishedVersionFromWidget(newWidget) !== null
|
|
10684
|
+
|| !!sourceCommit;
|
|
10685
|
+
|
|
10686
|
+
if (!canvasPrimaryWidgetId) {
|
|
10687
|
+
canvasPrimaryWidgetId = sourceWidgetId;
|
|
10688
|
+
}
|
|
10689
|
+
|
|
10690
|
+
// Duplicate stays a sibling on the canvas — do not make it the primary widget.
|
|
10691
|
+
await selectWidget(newWidget, { skipPlaceShape: true });
|
|
10692
|
+
notifyHostWidgetName(getWidgetById(canvasPrimaryWidgetId) ?? getWidgetById(sourceWidgetId));
|
|
9688
10693
|
|
|
9689
|
-
const newRepoId = normalizeRepositoryId(newWidget);
|
|
9690
10694
|
showToast('success', 'Widget duplicated', {
|
|
9691
|
-
description:
|
|
10695
|
+
description: sourceHasCode
|
|
10696
|
+
? `"${newWidget.name}" created with existing code`
|
|
10697
|
+
: `"${newWidget.name}" created`
|
|
9692
10698
|
});
|
|
9693
10699
|
|
|
9694
|
-
// Re-fetch widget from API to ensure repo_id and other fields are current
|
|
9695
10700
|
setTimeout(() => handleWidgetDataChanged(newWidget.id), 1500);
|
|
9696
10701
|
|
|
9697
|
-
|
|
9698
|
-
|
|
10702
|
+
if (sourceHasCode) {
|
|
10703
|
+
await openWidgetDetailsPanel(newWidget.id);
|
|
10704
|
+
await tick();
|
|
10705
|
+
getWidgetDetailsRef(newWidget.id)?.startPreviewLoadingPublic();
|
|
10706
|
+
try {
|
|
10707
|
+
await getWidgetDetailsRef(newWidget.id)?.retryPreviewIfEmpty?.();
|
|
10708
|
+
} catch (previewErr) {
|
|
10709
|
+
console.warn('[WidgetCreator] Duplicate preview fallback failed:', previewErr);
|
|
10710
|
+
}
|
|
10711
|
+
return;
|
|
10712
|
+
}
|
|
10713
|
+
|
|
10714
|
+
pendingDuplicateBuildWidgetId = newWidget.id;
|
|
9699
10715
|
setTimeout(async () => {
|
|
9700
10716
|
if (selectedWidgetId !== newWidget.id) return;
|
|
9701
10717
|
pendingDuplicateBuildWidgetId = null;
|
|
9702
10718
|
try {
|
|
9703
10719
|
await handleWidgetDataChanged(newWidget.id);
|
|
9704
10720
|
const freshWidget = getWidgetById(newWidget.id);
|
|
9705
|
-
let repoId = freshWidget ? normalizeRepositoryId(freshWidget) :
|
|
10721
|
+
let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(newWidget);
|
|
9706
10722
|
|
|
9707
10723
|
if (!repoId) {
|
|
9708
10724
|
console.log('[WidgetCreator] Duplicate has no repo after re-fetch, creating one...');
|
|
@@ -9736,7 +10752,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9736
10752
|
/**
|
|
9737
10753
|
* Called when a widget shape is about to be deleted from canvas.
|
|
9738
10754
|
* Step 1: Confirm removing the shape from canvas.
|
|
9739
|
-
* Step 2: Ask if user also wants to delete the widget from DB.
|
|
10755
|
+
* Step 2 (ASK_DELETE_FROM_DB_CONFIRM): Ask if user also wants to delete the widget from DB.
|
|
10756
|
+
* MVP: first confirm also deletes the associated widget from the database.
|
|
9740
10757
|
* Returns true to proceed with canvas shape deletion, false to cancel.
|
|
9741
10758
|
*/
|
|
9742
10759
|
async function handleWidgetShapeDeleting(widgetId: string): Promise<boolean> {
|
|
@@ -9749,7 +10766,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9749
10766
|
|
|
9750
10767
|
if (!result.proceed) return false;
|
|
9751
10768
|
|
|
9752
|
-
|
|
10769
|
+
closeWidgetDetailsPanel(widgetId);
|
|
10770
|
+
|
|
10771
|
+
if (result.deleteFromDb) {
|
|
10772
|
+
await deleteWidgetFromDb(widgetId);
|
|
10773
|
+
} else {
|
|
10774
|
+
showToast('info', 'Shape removed from canvas', { description: `Widget "${widgetName}" kept in database` });
|
|
10775
|
+
}
|
|
9753
10776
|
return true;
|
|
9754
10777
|
}
|
|
9755
10778
|
|
|
@@ -9772,7 +10795,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9772
10795
|
if (selectedWidget?.id === widgetId) {
|
|
9773
10796
|
selectedWidget = { ...selectedWidget, name: newName };
|
|
9774
10797
|
}
|
|
9775
|
-
|
|
10798
|
+
notifyHostWidgetName({ id: widgetId, name: newName });
|
|
10799
|
+
emitCanvasWidgetsList('widget-renamed');
|
|
9776
10800
|
showToast('success', `Widget renamed to "${newName}"`);
|
|
9777
10801
|
} catch (error) {
|
|
9778
10802
|
console.error('[WidgetCreator] Failed to rename widget:', error);
|
|
@@ -9819,14 +10843,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9819
10843
|
}
|
|
9820
10844
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
9821
10845
|
await maybeAutoRestoreWidgetShapes();
|
|
9822
|
-
const primaryId =
|
|
9823
|
-
|
|
9824
|
-
canvasRef.getPrimaryWidgetId?.() ||
|
|
9825
|
-
selectedWidgetId;
|
|
9826
|
-
if (primaryId && canvasRef) {
|
|
10846
|
+
const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
|
|
10847
|
+
if (primaryId && canvasRef && !isConvertOrDraftPanelOpen()) {
|
|
9827
10848
|
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
9828
10849
|
const found = canvasRef.selectWidgetShape(primaryId);
|
|
9829
10850
|
console.log('[CreatorApp] Primary widget shape after canvas load:', primaryId, found);
|
|
10851
|
+
} else if (isConvertOrDraftPanelOpen()) {
|
|
10852
|
+
console.log('[CreatorApp] Skip primary shape select — convert/draft panel is open');
|
|
9830
10853
|
}
|
|
9831
10854
|
}
|
|
9832
10855
|
|
|
@@ -10017,6 +11040,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10017
11040
|
const widgetIdsOnCanvas = extractWidgetIdsFromCanvasContent(parsed);
|
|
10018
11041
|
await canvasRef.loadCanvas(saved);
|
|
10019
11042
|
markCanvasStateLoaded();
|
|
11043
|
+
canvasApiLoadError = null;
|
|
10020
11044
|
isLoadCooldownActive = true;
|
|
10021
11045
|
setTimeout(() => { isLoadCooldownActive = false; }, 3000);
|
|
10022
11046
|
lastSavedCanvasObjectCount = objectCount;
|
|
@@ -10100,6 +11124,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10100
11124
|
|
|
10101
11125
|
lastSavedCanvasObjectCount = objectCount;
|
|
10102
11126
|
try { sessionStorage.setItem(CANVAS_OBJ_COUNT_KEY, String(objectCount)); } catch {}
|
|
11127
|
+
emitCreatorSaveState(false);
|
|
10103
11128
|
|
|
10104
11129
|
// Save to durable L1 fallback (always, even with 0 objects for legitimate clears)
|
|
10105
11130
|
saveCanvasState(canvasId, json);
|
|
@@ -10173,6 +11198,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10173
11198
|
: `ConvertedW-${Date.now()}`;
|
|
10174
11199
|
draftConvertScreenshots.set(draftId, payload.screenshot);
|
|
10175
11200
|
draftConvertObjectIds.set(draftId, payload.frameId);
|
|
11201
|
+
// Grouping deselects the sketch; that must not close the draft or open the primary widget.
|
|
11202
|
+
suppressWidgetDetailsClose = true;
|
|
10176
11203
|
|
|
10177
11204
|
// Group the sketch into one editable object and keep it on canvas — the
|
|
10178
11205
|
// user may keep editing it (ungroup, move, restyle) and re-convert later.
|
|
@@ -10200,6 +11227,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10200
11227
|
// Refresh the stored screenshot so finalize uses the latest sketch look
|
|
10201
11228
|
draftConvertScreenshots.set(existingDraftId, payload.screenshot);
|
|
10202
11229
|
authStatus = 'Widget draft ready — send the prompt to generate.';
|
|
11230
|
+
setTimeout(() => {
|
|
11231
|
+
if (!finalizingDraftConversions.size) {
|
|
11232
|
+
suppressWidgetDetailsClose = false;
|
|
11233
|
+
}
|
|
11234
|
+
}, 1500);
|
|
10203
11235
|
return;
|
|
10204
11236
|
}
|
|
10205
11237
|
|
|
@@ -10210,6 +11242,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10210
11242
|
// Draft panel snapshot (no widget record in DB). WidgetDetails is
|
|
10211
11243
|
// null-safe for a missing widget — the panel shows the draft name.
|
|
10212
11244
|
await openWidgetDetailsPanel(draftId);
|
|
11245
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
10213
11246
|
patchPanelSnapshot(draftId, {
|
|
10214
11247
|
codeGenerationPrompt: CONVERT_PROMPT,
|
|
10215
11248
|
promptImages: [payload.screenshot],
|
|
@@ -10251,6 +11284,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10251
11284
|
showToast('error', 'Failed to prepare widget draft');
|
|
10252
11285
|
} finally {
|
|
10253
11286
|
isWidgetOperationInProgress = false;
|
|
11287
|
+
setTimeout(() => {
|
|
11288
|
+
if (!finalizingDraftConversions.size) {
|
|
11289
|
+
suppressWidgetDetailsClose = false;
|
|
11290
|
+
}
|
|
11291
|
+
}, 1500);
|
|
10254
11292
|
}
|
|
10255
11293
|
}
|
|
10256
11294
|
|
|
@@ -10325,6 +11363,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10325
11363
|
// Add to widgets list so getWidgetById resolves the real widget from now on.
|
|
10326
11364
|
widgets = [{ ...newWidget }, ...widgets.filter((w) => w.id !== draftId)];
|
|
10327
11365
|
|
|
11366
|
+
if (canvasId) {
|
|
11367
|
+
const existingPrimary = await resolvePrimaryWidgetIdForCanvas(canvasId);
|
|
11368
|
+
if (!existingPrimary) {
|
|
11369
|
+
await persistPrimaryWidgetId(widgetId);
|
|
11370
|
+
}
|
|
11371
|
+
}
|
|
11372
|
+
|
|
10328
11373
|
// Replace the grouped sketch with a WidgetShape screenshot. On failure
|
|
10329
11374
|
// fall back to linking the sketch object to the widget id.
|
|
10330
11375
|
if (canvasRef && canvasObjectId) {
|
|
@@ -10364,7 +11409,10 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10364
11409
|
// panel remounts: the freshly mounted Chat then activates this
|
|
10365
11410
|
// conversation (best-conversation = one with messages) instead of
|
|
10366
11411
|
// starting empty after a close/reopen.
|
|
10367
|
-
await ensureCodegenUserMessagePersisted(widgetId, prompt
|
|
11412
|
+
await ensureCodegenUserMessagePersisted(widgetId, prompt, [
|
|
11413
|
+
...(draftPanelState.promptImages ?? []),
|
|
11414
|
+
screenshot,
|
|
11415
|
+
].filter(Boolean));
|
|
10368
11416
|
// NOTE: never copy currentChatConversationId from the draft — the draft
|
|
10369
11417
|
// chat's conversation is a local UUID that doesn't exist in the DB, and
|
|
10370
11418
|
// migrating it would point codegen/summaries at a phantom conversation.
|
|
@@ -10372,6 +11420,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10372
11420
|
draftPanelState as Record<string, unknown>;
|
|
10373
11421
|
patchPanelSnapshot(widgetId, {
|
|
10374
11422
|
...draftPanelStateClean,
|
|
11423
|
+
lastPublishedVersion: null,
|
|
11424
|
+
lastCommitId: null,
|
|
11425
|
+
hasGeneratedCode: false,
|
|
11426
|
+
publishHistory: [],
|
|
11427
|
+
widgetDetailsStep: 'create',
|
|
10375
11428
|
} as Partial<WidgetPanelSnapshot>);
|
|
10376
11429
|
migrateOpenPanelWidgetId(draftId, widgetId);
|
|
10377
11430
|
widgetPanelSnapshots = Object.fromEntries(
|
|
@@ -10387,8 +11440,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10387
11440
|
await tick();
|
|
10388
11441
|
const migratedRef = getWidgetDetailsRef(widgetId);
|
|
10389
11442
|
if (migratedRef?.getShowWidgetDetails?.()) {
|
|
10390
|
-
console.log('[CreatorApp] Draft→widget: Widget Details stayed open
|
|
10391
|
-
positionOpenDetailsPanel(widgetId);
|
|
11443
|
+
console.log('[CreatorApp] Draft→widget: Widget Details stayed open — keep current position');
|
|
10392
11444
|
} else {
|
|
10393
11445
|
await openWidgetDetailsPanel(widgetId);
|
|
10394
11446
|
}
|
|
@@ -10425,6 +11477,16 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10425
11477
|
return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
|
|
10426
11478
|
}
|
|
10427
11479
|
|
|
11480
|
+
function isConvertOrDraftPanelOpen(): boolean {
|
|
11481
|
+
return (
|
|
11482
|
+
isDraftConvertId(focusedPanelWidgetId)
|
|
11483
|
+
|| isDraftConvertId(widgetDetailsOpenTargetId)
|
|
11484
|
+
|| isDraftConvertId(selectedWidgetId)
|
|
11485
|
+
|| openWidgetPanelIds.some((id) => isDraftConvertId(id))
|
|
11486
|
+
|| (!!detailsSessionWidgetId && openWidgetPanelIds.includes(detailsSessionWidgetId))
|
|
11487
|
+
);
|
|
11488
|
+
}
|
|
11489
|
+
|
|
10428
11490
|
|
|
10429
11491
|
/**
|
|
10430
11492
|
* Creates or recreates a repository for a specific widget.
|
|
@@ -10525,6 +11587,30 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10525
11587
|
} catch (error) {
|
|
10526
11588
|
if (error instanceof ResponseError) {
|
|
10527
11589
|
const status = error.response?.status;
|
|
11590
|
+
if (status === 401) {
|
|
11591
|
+
console.warn('[Auth] Repository verify got 401 — rebinding session and retrying once', {
|
|
11592
|
+
repoId
|
|
11593
|
+
});
|
|
11594
|
+
applySiteSessionToCreator();
|
|
11595
|
+
try {
|
|
11596
|
+
const retryDetails = await agentClient.agentRepositoriesRepoIdGet({ repoId });
|
|
11597
|
+
if (retryDetails?.id) return true;
|
|
11598
|
+
} catch (retryError) {
|
|
11599
|
+
if (retryError instanceof ResponseError && retryError.response?.status === 401) {
|
|
11600
|
+
if (!silent) {
|
|
11601
|
+
showToast('warning', 'Session expired', {
|
|
11602
|
+
description: 'Sign in again, then retry. The widget repository is still linked.',
|
|
11603
|
+
duration: 6000
|
|
11604
|
+
});
|
|
11605
|
+
}
|
|
11606
|
+
if (typeof window !== 'undefined') {
|
|
11607
|
+
window.dispatchEvent(new CustomEvent('widgetic:session-needs-reauth'));
|
|
11608
|
+
}
|
|
11609
|
+
throw retryError;
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11612
|
+
return false;
|
|
11613
|
+
}
|
|
10528
11614
|
if (status === 404 || status === 403) {
|
|
10529
11615
|
console.warn('Repository reference is stale or forbidden; it will be recreated.', {
|
|
10530
11616
|
repoId,
|
|
@@ -10873,6 +11959,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10873
11959
|
</div>
|
|
10874
11960
|
{/if}
|
|
10875
11961
|
|
|
11962
|
+
|
|
10876
11963
|
<!-- Widgetic Canvas Editor - Full width background (lazy-loaded) -->
|
|
10877
11964
|
<div class="wc-canvas-area flex-1 min-h-0 relative">
|
|
10878
11965
|
<!-- Embedded: Debug toggle lives in site AppHeader (avoids overlapping Props Panel). -->
|
|
@@ -10903,6 +11990,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10903
11990
|
bind:this={canvasRef}
|
|
10904
11991
|
showContainerFrame={!embedded}
|
|
10905
11992
|
bind:wireframeMode={canvasWireframeMode}
|
|
11993
|
+
advancedFeatures={canvasAdvancedFeatures}
|
|
10906
11994
|
onReady={handleCanvasReady}
|
|
10907
11995
|
onAutoSave={handleCanvasAutoSave}
|
|
10908
11996
|
onBeforeUnmount={(json) => {
|
|
@@ -10914,6 +12002,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10914
12002
|
}}
|
|
10915
12003
|
onConvertToWidget={handleConvertToWidget}
|
|
10916
12004
|
onWidgetImageClick={handleWidgetImageClick}
|
|
12005
|
+
onCanvasDirty={() => emitCreatorSaveState(true)}
|
|
10917
12006
|
onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
|
|
10918
12007
|
onWidgetShapeDeleting={handleWidgetShapeDeleting}
|
|
10919
12008
|
onWidgetRename={handleWidgetRename}
|
|
@@ -10922,8 +12011,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10922
12011
|
getWidgetName={getWidgetNameById}
|
|
10923
12012
|
isWidgetKnown={isWidgetKnownById}
|
|
10924
12013
|
onNonWidgetSelected={() => {
|
|
10925
|
-
if (suppressWidgetDetailsClose) {
|
|
10926
|
-
console.log('[CreatorApp] Skip Widget Details close —
|
|
12014
|
+
if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
|
|
12015
|
+
console.log('[CreatorApp] Skip Widget Details close — convert/draft panel is open');
|
|
10927
12016
|
return;
|
|
10928
12017
|
}
|
|
10929
12018
|
const active = canvasRef?.getSelectedObject?.() ?? canvasRef?.selectedObject;
|
|
@@ -11259,7 +12348,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11259
12348
|
{isPublishing}
|
|
11260
12349
|
{publishWidgetStatus}
|
|
11261
12350
|
publishNeedsSaveFirst={publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
11262
|
-
lastPublishedVersion={ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null)}
|
|
12351
|
+
lastPublishedVersion={isDraftConvertId(panelWidgetId) ? null : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
|
|
11263
12352
|
widgetJsPath={ps.selectedWidgetJsPath ?? panelWidget?.jsPath ?? null}
|
|
11264
12353
|
{isGeneratingInOtherTab}
|
|
11265
12354
|
{isPublishingInOtherTab}
|
|
@@ -11335,6 +12424,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11335
12424
|
}
|
|
11336
12425
|
// Keep canvas WidgetShape thumbnail aligned with the live Worker preview.
|
|
11337
12426
|
scheduleCanvasShapePreviewSync(previewWidgetId);
|
|
12427
|
+
if (detail?.step === 'create' || ps.widgetDetailsStep === 'create') {
|
|
12428
|
+
applyPanelCompositionToPreview(previewWidgetId);
|
|
12429
|
+
}
|
|
11338
12430
|
}}
|
|
11339
12431
|
on:widgetReady={({ detail }) => {
|
|
11340
12432
|
if (detail.widgetId !== panelWidgetId) return;
|
|
@@ -11349,21 +12441,15 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11349
12441
|
detail.repoHeadCommitSha ||
|
|
11350
12442
|
detail.repo_head_commit_sha ||
|
|
11351
12443
|
null;
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
saveCommitIdForWidget(detail.widgetId, repoHead);
|
|
11355
|
-
}
|
|
11356
|
-
// Always persist hasGeneratedCode on the snapshot so Publish re-renders
|
|
11357
|
-
// even when Worker cache hits without a SHA and the panel is unfocused.
|
|
12444
|
+
// Rebuild is the same saved tree — do not promote GitLab HEAD into lastCommitId
|
|
12445
|
+
// or Publish will offer vN+1 without a new codegen commit.
|
|
11358
12446
|
patchPanelSnapshot(detail.widgetId, {
|
|
11359
12447
|
panelPreviewCompileError: null,
|
|
11360
12448
|
hasGeneratedCode: true,
|
|
11361
|
-
...(hasSha ? { lastCommitId: repoHead } : {}),
|
|
11362
12449
|
});
|
|
11363
12450
|
if (detail.widgetId === panelWidgetId) {
|
|
11364
12451
|
hasGeneratedCode = true;
|
|
11365
|
-
if (
|
|
11366
|
-
if (!hasSha) {
|
|
12452
|
+
if (!getPanelState(detail.widgetId).lastCommitId && !loadCommitIdForWidget(detail.widgetId)) {
|
|
11367
12453
|
void reconcileWidgetHeadCommit(detail.widgetId);
|
|
11368
12454
|
}
|
|
11369
12455
|
if (isPanelFocused && generateCodeStatus?.includes('preview build failed')) {
|
|
@@ -11485,6 +12571,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11485
12571
|
console.log('[WidgetCreator] Generate Now clicked for widget:', detail.widgetId);
|
|
11486
12572
|
const widgetId = detail.widgetId;
|
|
11487
12573
|
const ps = getPanelDisplayState(widgetId);
|
|
12574
|
+
if (ps.lastGenerationFailed) {
|
|
12575
|
+
void handleChatRetryMessage(
|
|
12576
|
+
lastGenerationPrompt || ps.codeGenerationPrompt,
|
|
12577
|
+
widgetId,
|
|
12578
|
+
);
|
|
12579
|
+
return;
|
|
12580
|
+
}
|
|
11488
12581
|
const hasCode = !!(ps.lastCommitId || ps.lastPublishedVersion !== null);
|
|
11489
12582
|
const compileErr = getPanelPreviewCompileError(widgetId);
|
|
11490
12583
|
if (hasCode && compileErr) {
|
|
@@ -11501,10 +12594,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11501
12594
|
console.log('[CreatorApp] Ignoring Widget Details close during draft→widget migrate');
|
|
11502
12595
|
return;
|
|
11503
12596
|
}
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
}
|
|
12597
|
+
// After convert, the panel id changes but the X is still the visible
|
|
12598
|
+
// panel — always close it. Stale-target ignore left the child hidden
|
|
12599
|
+
// while openWidgetPanelIds stayed set, so nothing would reopen.
|
|
11508
12600
|
if (isPanelFocused && selectedWidgetId) {
|
|
11509
12601
|
savePromptForWidget(selectedWidgetId);
|
|
11510
12602
|
captureFocusedPanelSnapshot();
|
|
@@ -11534,8 +12626,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11534
12626
|
>
|
|
11535
12627
|
<svelte:fragment slot="code-generation">
|
|
11536
12628
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
12629
|
+
{@const chatUserHeight = (panelDisplayRevision, Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, (ps.chatContainerHeight > 0 ? ps.chatContainerHeight : CHAT_DEFAULT_HEIGHT))))}
|
|
11537
12630
|
<div
|
|
11538
|
-
class="parallel-panel-left-slot-ct flex flex-col
|
|
12631
|
+
class="parallel-panel-left-slot-ct flex flex-col shrink-0 overflow-visible"
|
|
11539
12632
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
11540
12633
|
>
|
|
11541
12634
|
<!-- Sticky header: title + model selectors (debug mode).
|
|
@@ -11582,11 +12675,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11582
12675
|
</div>
|
|
11583
12676
|
{/if}
|
|
11584
12677
|
|
|
11585
|
-
<div class="generation-scroll-ct flex flex-col
|
|
12678
|
+
<div class="generation-scroll-ct flex flex-col shrink-0 overflow-visible">
|
|
11586
12679
|
<!-- Chat-based Code Generation Panel -->
|
|
11587
12680
|
{#key panelInstanceKey}
|
|
11588
|
-
<Chat class="code-generation-chat
|
|
11589
|
-
style="min-height: {
|
|
12681
|
+
<Chat class="code-generation-chat w-full shrink-0"
|
|
12682
|
+
style="height: {chatUserHeight}px; min-height: {CHAT_MIN_HEIGHT}px; flex: 0 0 {chatUserHeight}px;"
|
|
11590
12683
|
resizeExpandsContainer={true}
|
|
11591
12684
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
11592
12685
|
showLogs={debugMode}
|
|
@@ -11616,11 +12709,15 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11616
12709
|
onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
|
|
11617
12710
|
onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
|
|
11618
12711
|
onUploadChatFile={handleChatUploadFile}
|
|
11619
|
-
onTakeScreenshot={handleChatScreenshot}
|
|
12712
|
+
onTakeScreenshot={() => handleChatScreenshot(panelWidgetId)}
|
|
12713
|
+
isCompleted={!!conversationCompletedByWidgetId[panelWidgetId]}
|
|
12714
|
+
onCompletionToggle={(detail) => handleChatCompletionToggle(panelWidgetId, detail)}
|
|
11620
12715
|
onLoadConversations={handleLoadConversations}
|
|
11621
12716
|
onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
|
|
11622
12717
|
onCreateConversation={handleCreateConversation}
|
|
11623
|
-
onSaveMessage={
|
|
12718
|
+
onSaveMessage={(conversationId, content, messageType, attachments) =>
|
|
12719
|
+
handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
|
|
12720
|
+
}
|
|
11624
12721
|
onRestoreCheckpointBackend={handleRestoreCheckpoint}
|
|
11625
12722
|
onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
|
|
11626
12723
|
onDraftChange={() => handleChatDraftChange(panelWidgetId)}
|
|
@@ -11695,42 +12792,39 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11695
12792
|
</div>
|
|
11696
12793
|
{/if}
|
|
11697
12794
|
</div>
|
|
12795
|
+
</div> <!-- generation-scroll-ct -->
|
|
12796
|
+
</div> <!-- parallel-panel-left-slot-ct -->
|
|
12797
|
+
</svelte:fragment>
|
|
12798
|
+
|
|
12799
|
+
<svelte:fragment slot="publish-widget">
|
|
11698
12800
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
11699
12801
|
<div
|
|
11700
|
-
class="parallel-panel-publish-slot-ct shrink-0
|
|
12802
|
+
class="parallel-panel-publish-slot-ct shrink-0"
|
|
11701
12803
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
11702
12804
|
>
|
|
11703
12805
|
<!-- ===== Publish Widget Section ===== -->
|
|
11704
|
-
<div class="publish-widget-section flex flex-col gap-
|
|
11705
|
-
<!-- Section Header -->
|
|
12806
|
+
<div class="publish-widget-section flex flex-col gap-1 rounded border border-emerald-200 bg-emerald-50 px-2 py-1.5 mt-1 shrink-0">
|
|
11706
12807
|
<div class="publish-widget-section-header flex items-center justify-between gap-2">
|
|
11707
|
-
<span class="text-
|
|
11708
|
-
<!-- Current published version badge — shown when widget has been published -->
|
|
12808
|
+
<span class="text-xs font-semibold text-emerald-800">🚀 Publish</span>
|
|
11709
12809
|
{#if ps.lastPublishedVersion !== null}
|
|
11710
|
-
<span class="text-
|
|
12810
|
+
<span class="text-[10px] font-mono bg-emerald-700 text-white px-1.5 py-0.5 rounded">
|
|
11711
12811
|
live: v{ps.lastPublishedVersion}
|
|
11712
12812
|
</span>
|
|
11713
12813
|
{/if}
|
|
11714
12814
|
</div>
|
|
11715
12815
|
|
|
11716
|
-
|
|
11717
|
-
<p class="text-xs text-emerald-700 leading-relaxed">
|
|
11718
|
-
Publish when you’re happy with the preview. You’ll get a stable embed URL for your site.
|
|
11719
|
-
</p>
|
|
11720
|
-
|
|
11721
|
-
<!-- Publish Button + Build Status -->
|
|
11722
|
-
<div class="publish-widget-section-actions flex flex-col gap-2 items-center">
|
|
11723
|
-
<!-- Publish Widget Button — triggers production build via Trigger.dev -->
|
|
12816
|
+
<div class="publish-widget-section-actions flex flex-col gap-1 items-center">
|
|
11724
12817
|
<Button
|
|
11725
12818
|
onclick={() => runPublishForPanel(panelWidgetId)}
|
|
11726
|
-
disabled={isPublishDisabledForPanel(panelWidgetId)}
|
|
12819
|
+
disabled={(panelDisplayRevision, isPublishDisabledForPanel(panelWidgetId))}
|
|
11727
12820
|
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? undefined}
|
|
11728
|
-
|
|
12821
|
+
size="sm"
|
|
12822
|
+
class={`publish-widget-btn flex justify-center items-center border text-white w-full h-8 text-xs
|
|
11729
12823
|
${isPublishing && publishingWidgetId === panelWidgetId
|
|
11730
12824
|
? 'bg-emerald-500 border-emerald-500 opacity-80 cursor-not-allowed'
|
|
11731
12825
|
: isPublishDisabledForPanel(panelWidgetId)
|
|
11732
|
-
? 'bg-emerald-600 border-emerald-600 opacity-
|
|
11733
|
-
: 'bg-emerald-600 border-emerald-600 hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'
|
|
12826
|
+
? 'bg-emerald-600 border-emerald-600 opacity-40 cursor-not-allowed'
|
|
12827
|
+
: '!opacity-100 bg-emerald-600 border-emerald-600 hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'
|
|
11734
12828
|
} rounded-lg`}
|
|
11735
12829
|
>
|
|
11736
12830
|
{#if isPublishing && publishingWidgetId === panelWidgetId && publishWidgetStatus === 'Building...'}
|
|
@@ -11740,7 +12834,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11740
12834
|
<span class="mr-1 inline-block">⏳</span>
|
|
11741
12835
|
<span>Publishing...</span>
|
|
11742
12836
|
{:else}
|
|
11743
|
-
<span>{getPublishButtonLabelForPanel(panelWidgetId)}</span>
|
|
12837
|
+
<span>{(panelDisplayRevision, getPublishButtonLabelForPanel(panelWidgetId))}</span>
|
|
11744
12838
|
{#if isPublishingInOtherTab}
|
|
11745
12839
|
<!-- svelte-ignore a11y-missing-attribute -->
|
|
11746
12840
|
<span class="text-xs opacity-70 ml-1" title="Publish is running in another tab">⏳</span>
|
|
@@ -11753,25 +12847,24 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11753
12847
|
{/if}
|
|
11754
12848
|
</Button>
|
|
11755
12849
|
|
|
11756
|
-
{#if isPublishDisabledForPanel(panelWidgetId)
|
|
11757
|
-
<p class="publish-widget-disabled-hint text-
|
|
11758
|
-
{getPublishDisabledReasonForPanel(panelWidgetId)}
|
|
12850
|
+
{#if isPublishDisabledForPanel(panelWidgetId)}
|
|
12851
|
+
<p class="publish-widget-disabled-hint text-[10px] text-center text-emerald-800/80 leading-snug px-1">
|
|
12852
|
+
{getPublishDisabledReasonForPanel(panelWidgetId) || getPublishButtonLabelForPanel(panelWidgetId)}
|
|
11759
12853
|
</p>
|
|
11760
12854
|
{/if}
|
|
11761
12855
|
|
|
11762
12856
|
{#if ps.lastPublishedVersion !== null}
|
|
11763
|
-
<a href="/test-widget?widgetId={panelWidgetId}&version={ps.lastPublishedVersion}&url={encodeURIComponent(publishedWidgetHtmlUrl(panelWidgetId, ps.lastPublishedVersion))}" target="_blank" rel="noopener noreferrer" class="
|
|
11764
|
-
Open
|
|
12857
|
+
<a href="/test-widget?widgetId={panelWidgetId}&version={ps.lastPublishedVersion}&url={encodeURIComponent(publishedWidgetHtmlUrl(panelWidgetId, ps.lastPublishedVersion))}" target="_blank" rel="noopener noreferrer" class="text-[11px] text-emerald-700 hover:underline">
|
|
12858
|
+
Open published v{ps.lastPublishedVersion} ↗
|
|
11765
12859
|
</a>
|
|
11766
12860
|
{/if}
|
|
11767
12861
|
|
|
11768
|
-
<!-- Save-first hint — shown when changes aren't saved yet -->
|
|
11769
12862
|
{#if publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
11770
|
-
<p class="text-
|
|
12863
|
+
<p class="text-[10px] text-center text-amber-700">
|
|
11771
12864
|
{#if !resolvePublishHeadCommitId(panelWidgetId, ps)}
|
|
11772
|
-
|
|
12865
|
+
Save the widget before publishing.
|
|
11773
12866
|
{:else}
|
|
11774
|
-
|
|
12867
|
+
Save your changes before publishing.
|
|
11775
12868
|
{/if}
|
|
11776
12869
|
</p>
|
|
11777
12870
|
{/if}
|
|
@@ -11784,7 +12877,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11784
12877
|
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
11785
12878
|
togglePublishHistory();
|
|
11786
12879
|
}}
|
|
11787
|
-
class="publish-history-toggle flex items-center justify-between w-full text-
|
|
12880
|
+
class="publish-history-toggle flex items-center justify-between w-full text-[10px] text-emerald-700 hover:text-emerald-900 py-0.5 px-0.5 rounded transition-colors"
|
|
11788
12881
|
>
|
|
11789
12882
|
<span class="font-medium">📋 Version History</span>
|
|
11790
12883
|
<span class="text-emerald-400">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
|
|
@@ -11842,53 +12935,41 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11842
12935
|
|
|
11843
12936
|
</div> <!-- end of publish-widget-section -->
|
|
11844
12937
|
</div>
|
|
11845
|
-
</div> <!-- generation-scroll-ct -->
|
|
11846
|
-
</div> <!-- parallel-panel-left-slot-ct -->
|
|
11847
12938
|
</svelte:fragment>
|
|
11848
12939
|
|
|
11849
|
-
<svelte:fragment slot="publish-widget"></svelte:fragment>
|
|
11850
|
-
|
|
11851
12940
|
<svelte:fragment slot="composition-editor">
|
|
11852
12941
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
11853
12942
|
<div
|
|
11854
|
-
class="parallel-panel-composition-slot-ct flex flex-col h-full min-h-0"
|
|
12943
|
+
class="parallel-panel-composition-slot-ct flex flex-col h-full min-h-0 min-w-0 overflow-y-auto overflow-x-hidden scrollbar-thin"
|
|
11855
12944
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
11856
12945
|
>
|
|
11857
12946
|
<!-- ===== Composition Editor (Step 2: Edit) ===== -->
|
|
11858
12947
|
{#if ps.lastCommitId || ps.lastPublishedVersion !== null}
|
|
11859
|
-
<div class="composition-editor-section flex flex-col
|
|
12948
|
+
<div class="composition-editor-section flex flex-col flex-1 min-h-0 h-full">
|
|
11860
12949
|
|
|
11861
12950
|
{#if panelHasUnpublishedChanges}
|
|
11862
|
-
<div class="edit-unpublished-banner-ct mx-3 mt-2 px-
|
|
11863
|
-
<p class="edit-unpublished-banner-text leading-
|
|
11864
|
-
<strong class="font-semibold">Newer
|
|
11865
|
-
Widget is published, but latest commit
|
|
12951
|
+
<div class="edit-unpublished-banner-ct mx-3 mt-2 px-2 py-1.5 rounded-md border border-amber-200 bg-amber-50 text-amber-800 text-xs flex items-center gap-2 shrink-0">
|
|
12952
|
+
<p class="edit-unpublished-banner-text leading-snug min-w-0 flex-1 truncate" title="Newer code than Live v{ps.lastPublishedVersion ?? '—'}. Publish to promote commit {formatCommitShaShort(ps.lastCommitId)}.">
|
|
12953
|
+
<strong class="font-semibold">Newer than Live v{ps.lastPublishedVersion ?? '—'}</strong>
|
|
11866
12954
|
<span class="font-mono">{formatCommitShaShort(ps.lastCommitId)}</span>
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
Edit preview shows <strong>Live v{ps.lastPublishedVersion ?? '—'}</strong> with your composition settings.
|
|
11870
|
-
Publish to promote the latest code to Live — this preview will refresh automatically.
|
|
12955
|
+
ahead of
|
|
12956
|
+
<span class="font-mono">{formatCommitShaShort(getLivePublishedCommitShaForPanel(panelWidgetId))}</span>
|
|
11871
12957
|
</p>
|
|
11872
|
-
<
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
<span>🚀 Publish to Live</span>
|
|
11886
|
-
{/if}
|
|
11887
|
-
</Button>
|
|
11888
|
-
{#if publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
11889
|
-
<span class="text-amber-700">Generate & save code before publishing.</span>
|
|
12958
|
+
<Button
|
|
12959
|
+
onclick={() => runPublishForPanel(panelWidgetId)}
|
|
12960
|
+
disabled={isPublishDisabledForPanel(panelWidgetId)}
|
|
12961
|
+
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? undefined}
|
|
12962
|
+
class="edit-banner-publish-btn shrink-0 flex items-center justify-center gap-1 px-2 py-1 text-xs font-semibold rounded-md border transition-colors
|
|
12963
|
+
{isPublishDisabledForPanel(panelWidgetId)
|
|
12964
|
+
? 'bg-emerald-600/40 border-emerald-600/40 text-white cursor-not-allowed'
|
|
12965
|
+
: 'bg-emerald-600 border-emerald-600 text-white hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'}"
|
|
12966
|
+
>
|
|
12967
|
+
{#if isPublishing && publishingWidgetId === panelWidgetId}
|
|
12968
|
+
<span>⏳</span>
|
|
12969
|
+
{:else}
|
|
12970
|
+
<span>🚀 Publish</span>
|
|
11890
12971
|
{/if}
|
|
11891
|
-
</
|
|
12972
|
+
</Button>
|
|
11892
12973
|
</div>
|
|
11893
12974
|
{/if}
|
|
11894
12975
|
|
|
@@ -12050,14 +13131,17 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
12050
13131
|
</button>
|
|
12051
13132
|
</div>
|
|
12052
13133
|
{:else if ps.compositionDesignSchema || ps.compositionContentSchema}
|
|
12053
|
-
<div class="composition-editor-props-ct flex flex-1 min-h-
|
|
13134
|
+
<div class="composition-editor-props-ct flex flex-col flex-1 min-h-[550px] max-h-[800px] overflow-y-auto overflow-x-hidden p-2">
|
|
12054
13135
|
{#key `${panelWidgetId}-${ps.selectedComposition?.id ?? 'new'}`}
|
|
12055
13136
|
<PropsEditor
|
|
12056
13137
|
class="composition-props-editor flex-1 min-h-0 min-w-0"
|
|
12057
13138
|
designSchema={ps.compositionDesignSchema}
|
|
12058
13139
|
contentSchema={buildPropsEditorContentSchema(ps.compositionContentSchema, ps.liveContentItems)}
|
|
12059
13140
|
initialDesignValues={ps.liveDesignValues}
|
|
12060
|
-
initialContentValues={
|
|
13141
|
+
initialContentValues={{
|
|
13142
|
+
...ps.liveContentValues,
|
|
13143
|
+
items: contentItemsToEditorValueMap(ps.liveContentItems)
|
|
13144
|
+
}}
|
|
12061
13145
|
width="100%"
|
|
12062
13146
|
height="100%"
|
|
12063
13147
|
showDragHandle={false}
|
|
@@ -12336,7 +13420,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
12336
13420
|
<Dialog.Title>Delete Widget Shape</Dialog.Title>
|
|
12337
13421
|
<Dialog.Description>
|
|
12338
13422
|
{#if deleteDialogStep === 'confirm-remove'}
|
|
12339
|
-
Remove <strong>"{deleteDialogWidgetName}"</strong>
|
|
13423
|
+
Remove <strong>"{deleteDialogWidgetName}"</strong> from the canvas? This also deletes the associated widget.
|
|
12340
13424
|
{:else}
|
|
12341
13425
|
Also delete the widget <strong>"{deleteDialogWidgetName}"</strong> from the database?
|
|
12342
13426
|
{/if}
|
|
@@ -12354,8 +13438,8 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
12354
13438
|
type="button"
|
|
12355
13439
|
class="px-4 py-2 text-sm rounded-lg bg-red-600 hover:bg-red-700 text-white cursor-pointer"
|
|
12356
13440
|
onclick={() => handleDeleteDialogResponse('remove-and-ask-db')}
|
|
12357
|
-
>
|
|
12358
|
-
{:else}
|
|
13441
|
+
>Delete</button>
|
|
13442
|
+
{:else if ASK_DELETE_FROM_DB_CONFIRM}
|
|
12359
13443
|
<button
|
|
12360
13444
|
type="button"
|
|
12361
13445
|
class="px-4 py-2 text-sm rounded-lg border border-gray-300 hover:bg-gray-100 cursor-pointer"
|