@widgetic/creator 0.3.50 → 0.3.51
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 +917 -272
- package/dist/CreatorApp.svelte.d.ts +1 -0
- package/dist/components/WidgetDetails.svelte +188 -41
- package/dist/creator-types.d.ts +6 -0
- package/package.json +1 -1
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);
|
|
@@ -132,11 +138,13 @@
|
|
|
132
138
|
const HOST_REQUEST_PUBLISH_EVENT = 'widgetic:host-request-publish';
|
|
133
139
|
const HOST_TOGGLE_DEBUG_EVENT = 'widgetic:host-toggle-debug';
|
|
134
140
|
const HOST_TOGGLE_WIREFRAME_EVENT = 'widgetic:host-toggle-wireframe';
|
|
141
|
+
const HOST_TOGGLE_ADVANCED_EVENT = 'widgetic:host-toggle-advanced';
|
|
135
142
|
const HOST_OPEN_WIDGET_DETAILS_EVENT = 'widgetic:host-open-widget-details';
|
|
136
143
|
const HOST_DUPLICATE_WIDGET_EVENT = 'widgetic:host-duplicate-widget';
|
|
137
144
|
const HOST_DELETE_WIDGET_EVENT = 'widgetic:host-delete-widget';
|
|
138
145
|
const CREATOR_DEBUG_CHANGED_EVENT = 'widgetic:creator-debug-changed';
|
|
139
146
|
const CREATOR_WIREFRAME_CHANGED_EVENT = 'widgetic:creator-wireframe-changed';
|
|
147
|
+
const CREATOR_ADVANCED_CHANGED_EVENT = 'widgetic:creator-advanced-changed';
|
|
140
148
|
|
|
141
149
|
/** Site AppHeader rename — embed is a second Svelte runtime, so mount props do not update. */
|
|
142
150
|
async function applyHostWidgetName(widgetId: string | null | undefined, newName: string): Promise<void> {
|
|
@@ -316,9 +324,41 @@
|
|
|
316
324
|
: new UploadsApi({ accessToken: token, middleware: [idempotencyKeyMiddleware] });
|
|
317
325
|
|
|
318
326
|
authenticatedClientsToken = token;
|
|
327
|
+
try {
|
|
328
|
+
setWsAuthToken(token);
|
|
329
|
+
} catch {
|
|
330
|
+
/* websocket store may not be ready yet */
|
|
331
|
+
}
|
|
319
332
|
syncFileBrowserUploadConfigForCreator();
|
|
320
333
|
}
|
|
321
334
|
|
|
335
|
+
function applySiteSessionToCreator(): boolean {
|
|
336
|
+
const synced = syncSessionFromSiteAuth();
|
|
337
|
+
if (!synced) return false;
|
|
338
|
+
try {
|
|
339
|
+
const sessionStr = localStorage.getItem('session');
|
|
340
|
+
const session = sessionStr ? JSON.parse(sessionStr) : null;
|
|
341
|
+
currentUserToken =
|
|
342
|
+
session?.accessToken ||
|
|
343
|
+
session?.access_token ||
|
|
344
|
+
localStorage.getItem('jwt') ||
|
|
345
|
+
currentUserToken;
|
|
346
|
+
} catch {
|
|
347
|
+
currentUserToken = localStorage.getItem('jwt') || currentUserToken;
|
|
348
|
+
}
|
|
349
|
+
if (currentUserToken) {
|
|
350
|
+
authenticatedClientsToken = null;
|
|
351
|
+
initializeAuthenticatedClients(currentUserToken);
|
|
352
|
+
}
|
|
353
|
+
updateTokenStatus();
|
|
354
|
+
console.log('[Auth] Creator SDK clients re-bound from site session');
|
|
355
|
+
return hasValidToken;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function handleSiteSessionExtended(): void {
|
|
359
|
+
applySiteSessionToCreator();
|
|
360
|
+
}
|
|
361
|
+
|
|
322
362
|
function syncFileBrowserUploadConfigForCreator() {
|
|
323
363
|
if (!authenticatedClientsToken) {
|
|
324
364
|
setFileBrowserUploadConfig(null);
|
|
@@ -517,6 +557,8 @@
|
|
|
517
557
|
let deleteDialogWidgetId = '';
|
|
518
558
|
let deleteDialogStep: 'confirm-remove' | 'confirm-delete-db' = 'confirm-remove';
|
|
519
559
|
let deleteDialogResolve: ((result: { proceed: boolean; deleteFromDb: boolean }) => void) | null = null;
|
|
560
|
+
/** Keep second-step UI/code; MVP deletes the DB widget after the first confirm. */
|
|
561
|
+
const ASK_DELETE_FROM_DB_CONFIRM = false;
|
|
520
562
|
|
|
521
563
|
function showDeleteWidgetDialog(widgetId: string, widgetName: string): Promise<{ proceed: boolean; deleteFromDb: boolean }> {
|
|
522
564
|
return new Promise((resolve) => {
|
|
@@ -535,10 +577,15 @@
|
|
|
535
577
|
if (deleteDialogResolve) deleteDialogResolve({ proceed: false, deleteFromDb: false });
|
|
536
578
|
break;
|
|
537
579
|
case 'remove-and-ask-db':
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
580
|
+
if (ASK_DELETE_FROM_DB_CONFIRM) {
|
|
581
|
+
if (deleteDialogResolve) deleteDialogResolve({ proceed: true, deleteFromDb: false });
|
|
582
|
+
deleteDialogResolve = null;
|
|
583
|
+
deleteDialogStep = 'confirm-delete-db';
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
deleteDialogOpen = false;
|
|
587
|
+
if (deleteDialogResolve) deleteDialogResolve({ proceed: true, deleteFromDb: true });
|
|
588
|
+
break;
|
|
542
589
|
case 'keep-in-db':
|
|
543
590
|
deleteDialogOpen = false;
|
|
544
591
|
showToast('info', 'Shape removed', { description: 'Widget kept in database' });
|
|
@@ -663,6 +710,8 @@
|
|
|
663
710
|
const draftConvertScreenshots = new Map<string, string>();
|
|
664
711
|
/** True while a draft conversion is being finalized (widget creation + shape replace). */
|
|
665
712
|
const finalizingDraftConversions = new Set<string>();
|
|
713
|
+
/** Keep Widget Details on this convert/draft widget until the user closes the panel. */
|
|
714
|
+
let detailsSessionWidgetId: string | null = null;
|
|
666
715
|
|
|
667
716
|
// Chat component reference and context (focused panel)
|
|
668
717
|
let chatMethods: {
|
|
@@ -681,8 +730,8 @@
|
|
|
681
730
|
markWidgetHeadAtLatestCommit?: () => void;
|
|
682
731
|
updateLatestCodegenStatusMessage?: (content: string) => void;
|
|
683
732
|
reloadConversationMessages?: () => Promise<void>;
|
|
684
|
-
|
|
685
|
-
|
|
733
|
+
selectConversation?: (conversationId: string) => void | Promise<void>;
|
|
734
|
+
updateLastUserMessageAttachments?: (attachments: Array<{ id?: string; url: string; file_type?: string; file_name?: string | null }>) => void;
|
|
686
735
|
} | null = null;
|
|
687
736
|
let chatMethodsByWidgetId: Record<string, NonNullable<typeof chatMethods>> = {};
|
|
688
737
|
let pendingChatDraftText = '';
|
|
@@ -704,7 +753,7 @@
|
|
|
704
753
|
|
|
705
754
|
/** Prompt mentions a reference image but chat has no attachment (common UX mistake). */
|
|
706
755
|
function promptImpliesVisionReference(prompt: string): boolean {
|
|
707
|
-
return
|
|
756
|
+
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
757
|
prompt,
|
|
709
758
|
);
|
|
710
759
|
}
|
|
@@ -713,6 +762,9 @@
|
|
|
713
762
|
file?: File,
|
|
714
763
|
uploadedUrl?: string,
|
|
715
764
|
): Promise<string | null> {
|
|
765
|
+
if (file && !file.type.startsWith('image/')) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
716
768
|
if (uploadedUrl && /^https?:\/\//i.test(uploadedUrl)) {
|
|
717
769
|
try {
|
|
718
770
|
const response = await fetch(uploadedUrl);
|
|
@@ -740,7 +792,7 @@
|
|
|
740
792
|
|
|
741
793
|
async function handleChatUploadFile(
|
|
742
794
|
file: File,
|
|
743
|
-
options?: { onProgress?: (percentage: number) => void; source?: string },
|
|
795
|
+
options?: { onProgress?: (percentage: number) => void; source?: string; widgetId?: string },
|
|
744
796
|
) {
|
|
745
797
|
if (!authenticatedClientsToken) {
|
|
746
798
|
throw new Error('Upload client not initialized');
|
|
@@ -750,6 +802,7 @@
|
|
|
750
802
|
const uploadBasePath =
|
|
751
803
|
apiBasePath ||
|
|
752
804
|
`${(apiRoot || import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/+$/, '')}/v1`;
|
|
805
|
+
const persistWidgetId = options?.widgetId || selectedWidgetId || undefined;
|
|
753
806
|
const result = await uploadChatFileToWidgeticApi(
|
|
754
807
|
file,
|
|
755
808
|
{
|
|
@@ -757,7 +810,7 @@
|
|
|
757
810
|
accessToken: authenticatedClientsToken,
|
|
758
811
|
contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
|
|
759
812
|
source: options?.source || 'chat-upload',
|
|
760
|
-
widgetId:
|
|
813
|
+
widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
|
|
761
814
|
conversationId: currentChatConversationId || undefined,
|
|
762
815
|
},
|
|
763
816
|
{ onProgress: options?.onProgress },
|
|
@@ -816,6 +869,7 @@
|
|
|
816
869
|
|
|
817
870
|
const panelChat = getChatMethodsForWidget(codegenWidgetId);
|
|
818
871
|
const cdnUploadIds = (attachments || [])
|
|
872
|
+
.filter((attachment) => attachment.uploadId && attachment.fileType.startsWith('image/'))
|
|
819
873
|
.map((attachment) => attachment.uploadId)
|
|
820
874
|
.filter((id): id is string => Boolean(id));
|
|
821
875
|
|
|
@@ -866,14 +920,45 @@
|
|
|
866
920
|
? promptImages
|
|
867
921
|
: getPanelState(codegenWidgetId).promptImages;
|
|
868
922
|
const hasImages = effectivePromptImages.length > 0 || cdnUploadIds.length > 0;
|
|
869
|
-
|
|
923
|
+
const hasNonImageFiles = (attachments || []).some(
|
|
924
|
+
(attachment) => attachment.fileType && !attachment.fileType.startsWith('image/'),
|
|
925
|
+
);
|
|
926
|
+
if (!hasImages && !hasNonImageFiles && promptImpliesVisionReference(content)) {
|
|
927
|
+
const missingImageError =
|
|
928
|
+
'Promptul menționează o imagine atașată, dar în chat nu e niciun attachment. Folosește butonul Upload.';
|
|
870
929
|
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.',
|
|
930
|
+
description: missingImageError,
|
|
873
931
|
duration: 10000,
|
|
874
932
|
});
|
|
933
|
+
patchCodegenUiState(codegenWidgetId, {
|
|
934
|
+
lastGenerationFailed: true,
|
|
935
|
+
lastGenerationError: missingImageError,
|
|
936
|
+
generateCodeStatus: '❌ Missing reference image',
|
|
937
|
+
});
|
|
938
|
+
pushCodegenFailureToChat(codegenWidgetId, missingImageError);
|
|
875
939
|
return;
|
|
876
940
|
}
|
|
941
|
+
const persistFileAttachments = (attachments || [])
|
|
942
|
+
.filter((attachment) =>
|
|
943
|
+
!!attachment.file
|
|
944
|
+
|| (attachment.url && (attachment.url.startsWith('blob:') || attachment.url.startsWith('data:') || /^https?:\/\//i.test(attachment.url))),
|
|
945
|
+
)
|
|
946
|
+
.map((attachment, i) => ({
|
|
947
|
+
url: attachment.url,
|
|
948
|
+
file: attachment.file,
|
|
949
|
+
file_type: guessAttachmentFileType(attachment.url || '', attachment.fileName, attachment.fileType),
|
|
950
|
+
file_name: attachment.fileName,
|
|
951
|
+
file_size: attachment.fileSize,
|
|
952
|
+
display_order: i + 1,
|
|
953
|
+
}));
|
|
954
|
+
if (persistFileAttachments.length > 0) {
|
|
955
|
+
await ensureCodegenUserMessagePersisted(
|
|
956
|
+
codegenWidgetId,
|
|
957
|
+
content,
|
|
958
|
+
undefined,
|
|
959
|
+
persistFileAttachments,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
877
962
|
// Pass upload IDs / images explicitly — clearDraft already wiped Chat's local attachments.
|
|
878
963
|
await generateCode({
|
|
879
964
|
prompt: content,
|
|
@@ -881,9 +966,19 @@
|
|
|
881
966
|
widgetId: codegenWidgetId,
|
|
882
967
|
attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
|
|
883
968
|
promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
|
|
969
|
+
persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
|
|
884
970
|
});
|
|
885
971
|
}
|
|
886
972
|
|
|
973
|
+
function pushCodegenFailureToChat(widgetId: string, errorText: string) {
|
|
974
|
+
const panelChat = getChatMethodsForWidget(widgetId);
|
|
975
|
+
const content = /^Code generation failed/i.test(errorText)
|
|
976
|
+
? errorText
|
|
977
|
+
: `Code generation failed: ${errorText}`;
|
|
978
|
+
panelChat?.addAssistantMessage?.(content);
|
|
979
|
+
console.log('[Chat] Codegen failure bubble:', content.slice(0, 160));
|
|
980
|
+
}
|
|
981
|
+
|
|
887
982
|
/** Retry failed codegen without duplicating the user bubble in chat. */
|
|
888
983
|
async function handleChatRetryMessage(content: string, sourceWidgetId?: string) {
|
|
889
984
|
const widgetId = sourceWidgetId ?? selectedWidgetId;
|
|
@@ -894,7 +989,53 @@
|
|
|
894
989
|
':',
|
|
895
990
|
content.substring(0, 80),
|
|
896
991
|
);
|
|
897
|
-
|
|
992
|
+
const widget = getWidgetById(widgetId) || (selectedWidget?.id === widgetId ? selectedWidget : null);
|
|
993
|
+
if (widget) {
|
|
994
|
+
await ensureWidgetRepository(widget, { forceRecreate: true, silent: true });
|
|
995
|
+
}
|
|
996
|
+
const panelChat = getChatMethodsForWidget(widgetId);
|
|
997
|
+
const draftUploadIds = panelChat?.getAttachmentUploadIds?.() ?? [];
|
|
998
|
+
const draftImageUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
|
|
999
|
+
|
|
1000
|
+
// Re-use attachments from previous user messages in the conversation
|
|
1001
|
+
// so that Retry doesn't lose PDFs/files that were uploaded with the original prompt.
|
|
1002
|
+
let retryFileAttachments: Array<{ url: string; file_type: string; file_name: string | null; file_size: number | null }> = [];
|
|
1003
|
+
if (draftUploadIds.length === 0 && draftImageUrls.length === 0) {
|
|
1004
|
+
try {
|
|
1005
|
+
const panelState = getPanelState(widgetId);
|
|
1006
|
+
const convId = panelChat?.getConversationId?.() ?? panelState.currentChatConversationId ?? null;
|
|
1007
|
+
if (convId) {
|
|
1008
|
+
const convMessages = await handleLoadMessages(convId, widgetId);
|
|
1009
|
+
// Find most-recent user message that has persisted attachments
|
|
1010
|
+
const userMsgsWithAtt = (convMessages || [])
|
|
1011
|
+
.filter((m: any) => (m.messageType || m.message_type) === 'user' && (m.attachments || []).length > 0);
|
|
1012
|
+
if (userMsgsWithAtt.length > 0) {
|
|
1013
|
+
const latestWithAtt = userMsgsWithAtt[userMsgsWithAtt.length - 1];
|
|
1014
|
+
retryFileAttachments = (latestWithAtt.attachments || [])
|
|
1015
|
+
.filter((a: any) => a.url && /^https?:\/\//i.test(a.url))
|
|
1016
|
+
.map((a: any) => ({
|
|
1017
|
+
url: a.url,
|
|
1018
|
+
file_type: a.file_type || 'application/octet-stream',
|
|
1019
|
+
file_name: a.file_name || null,
|
|
1020
|
+
file_size: a.file_size || null,
|
|
1021
|
+
}));
|
|
1022
|
+
console.log('[Chat→Retry] Re-using', retryFileAttachments.length, 'attachment(s) from previous user message for retry');
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} catch (retryAttErr) {
|
|
1026
|
+
console.warn('[Chat→Retry] Could not load previous attachments for retry:', retryAttErr);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
await generateCode({
|
|
1031
|
+
prompt: content,
|
|
1032
|
+
fromChat: true,
|
|
1033
|
+
widgetId,
|
|
1034
|
+
forceRecreateRepo: true,
|
|
1035
|
+
attachmentIds: draftUploadIds.length > 0 ? draftUploadIds : undefined,
|
|
1036
|
+
promptImagesOverride: draftImageUrls.length > 0 ? draftImageUrls : undefined,
|
|
1037
|
+
persistFileAttachments: retryFileAttachments.length > 0 ? retryFileAttachments : undefined,
|
|
1038
|
+
});
|
|
898
1039
|
}
|
|
899
1040
|
|
|
900
1041
|
function handleChatConversationChange(conversationId: string | null, sourceWidgetId?: string) {
|
|
@@ -970,7 +1111,43 @@
|
|
|
970
1111
|
* picked, which may differ from convos[0] when several empty conversations
|
|
971
1112
|
* exist (feedback c: chat looked empty while messages went elsewhere).
|
|
972
1113
|
*/
|
|
973
|
-
|
|
1114
|
+
function guessAttachmentFileType(url: string, fileName?: string | null, fallback?: string | null): string {
|
|
1115
|
+
if (fallback && String(fallback).includes('/')) return String(fallback);
|
|
1116
|
+
const hint = `${url || ''} ${fileName || ''}`;
|
|
1117
|
+
if (/\.pdf(\?|$)/i.test(hint)) return 'application/pdf';
|
|
1118
|
+
if (/\.(mp4|webm|mov)(\?|$)/i.test(hint)) return 'video/mp4';
|
|
1119
|
+
if (/\.(png|jpe?g|gif|webp|svg)(\?|$)/i.test(hint)) return 'image/png';
|
|
1120
|
+
if (fallback) return String(fallback);
|
|
1121
|
+
return 'application/octet-stream';
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function uniquePersistImageUrls(urls: string[]): string[] {
|
|
1125
|
+
const seen = new Set<string>();
|
|
1126
|
+
const httpsUrls: string[] = [];
|
|
1127
|
+
const dataUrls: string[] = [];
|
|
1128
|
+
for (const url of urls) {
|
|
1129
|
+
if (typeof url !== 'string' || url.length < 20 || seen.has(url)) continue;
|
|
1130
|
+
seen.add(url);
|
|
1131
|
+
if (/^https?:\/\//i.test(url)) httpsUrls.push(url);
|
|
1132
|
+
else if (url.startsWith('data:image/')) dataUrls.push(url);
|
|
1133
|
+
}
|
|
1134
|
+
// Same screenshot often exists as CDN URL + compressed data URL — keep HTTPS only.
|
|
1135
|
+
return httpsUrls.length > 0 ? httpsUrls : dataUrls;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function ensureCodegenUserMessagePersisted(
|
|
1139
|
+
widgetId: string,
|
|
1140
|
+
prompt: string,
|
|
1141
|
+
imageUrls?: string[],
|
|
1142
|
+
fileAttachments?: Array<{
|
|
1143
|
+
url?: string;
|
|
1144
|
+
file?: File;
|
|
1145
|
+
file_type?: string;
|
|
1146
|
+
file_name?: string | null;
|
|
1147
|
+
file_size?: number | null;
|
|
1148
|
+
display_order?: number;
|
|
1149
|
+
}>,
|
|
1150
|
+
): Promise<void> {
|
|
974
1151
|
if (!conversationsClient || !messagesClient) return;
|
|
975
1152
|
try {
|
|
976
1153
|
const panelChat = getChatMethodsForWidget(widgetId);
|
|
@@ -990,20 +1167,113 @@
|
|
|
990
1167
|
convId = conv?.id;
|
|
991
1168
|
}
|
|
992
1169
|
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
1170
|
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1171
|
+
const persistImages = uniquePersistImageUrls([
|
|
1172
|
+
...(imageUrls || []),
|
|
1173
|
+
...(getPanelState(widgetId).promptImages ?? []),
|
|
1174
|
+
]);
|
|
1175
|
+
const compressedPersistImages: string[] = [];
|
|
1176
|
+
for (const url of persistImages) {
|
|
1177
|
+
if (typeof url === 'string' && url.startsWith('data:image/')) {
|
|
1178
|
+
compressedPersistImages.push(await compressImageDataUrlForCodegen(url));
|
|
1179
|
+
} else if (typeof url === 'string' && /^https?:\/\//i.test(url)) {
|
|
1180
|
+
compressedPersistImages.push(url);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const persistFileItems = (fileAttachments || [])
|
|
1184
|
+
.filter((att) =>
|
|
1185
|
+
(typeof att.url === 'string' && (att.url.startsWith('blob:') || att.url.startsWith('data:') || /^https?:\/\//i.test(att.url)))
|
|
1186
|
+
|| !!(att as { file?: File }).file,
|
|
1187
|
+
)
|
|
1188
|
+
.map((att, i) => ({
|
|
1189
|
+
url: att.url || '',
|
|
1190
|
+
file: (att as { file?: File }).file,
|
|
1191
|
+
file_type: guessAttachmentFileType(att.url || '', att.file_name, att.file_type),
|
|
1192
|
+
file_name: att.file_name || (att as { file?: File }).file?.name || `attachment-${i + 1}`,
|
|
1193
|
+
file_size: att.file_size && att.file_size > 0 ? att.file_size : (att as { file?: File }).file?.size,
|
|
1194
|
+
display_order: att.display_order ?? i + 1,
|
|
1195
|
+
}));
|
|
1196
|
+
if (compressedPersistImages.length === 0 && persistFileItems.length === 0) {
|
|
1197
|
+
console.warn('[Codegen] Persist user message has no attachment URLs for widget', widgetId?.substring(0, 8));
|
|
1198
|
+
}
|
|
1199
|
+
const imagePersistItems = compressedPersistImages.map((url, i) => ({
|
|
1200
|
+
url,
|
|
1201
|
+
file_type: guessAttachmentFileType(url, 'design-screenshot.png', 'image/png'),
|
|
1202
|
+
file_name: 'design-screenshot.png',
|
|
1203
|
+
display_order: i + 1,
|
|
1204
|
+
}));
|
|
1205
|
+
const persistAttachments = [...persistFileItems, ...imagePersistItems].map((att, i) => ({
|
|
1206
|
+
...att,
|
|
1207
|
+
display_order: i + 1,
|
|
1208
|
+
}));
|
|
1209
|
+
const convMessages = await handleLoadMessages(convId, widgetId);
|
|
1210
|
+
const userMessages = (convMessages || []).filter(
|
|
1002
1211
|
(m) => (m.messageType || (m as any).message_type) === 'user'
|
|
1003
1212
|
);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1213
|
+
const promptNeedle = (prompt || '').trim();
|
|
1214
|
+
const targetUser =
|
|
1215
|
+
(promptNeedle
|
|
1216
|
+
? [...userMessages].reverse().find((m) => {
|
|
1217
|
+
const text = String(m.messageContent || (m as any).message_content || '').trim();
|
|
1218
|
+
return (
|
|
1219
|
+
text === promptNeedle ||
|
|
1220
|
+
text.endsWith(promptNeedle) ||
|
|
1221
|
+
(promptNeedle.length >= 12 && text.includes(promptNeedle.slice(0, 80)))
|
|
1222
|
+
);
|
|
1223
|
+
})
|
|
1224
|
+
: null) || userMessages[userMessages.length - 1] || null;
|
|
1225
|
+
const existingUrls = new Set(
|
|
1226
|
+
(targetUser?.attachments || [])
|
|
1227
|
+
.map((a: { url?: string }) => a.url)
|
|
1228
|
+
.filter((url: string | undefined): url is string => !!url),
|
|
1229
|
+
);
|
|
1230
|
+
const missingPersist = persistAttachments.filter((att) => {
|
|
1231
|
+
if (att.url && existingUrls.has(att.url)) return false;
|
|
1232
|
+
return true;
|
|
1233
|
+
});
|
|
1234
|
+
let savedAttachmentCount = targetUser?.attachments?.length || 0;
|
|
1235
|
+
if (!targetUser) {
|
|
1236
|
+
const saved = await handleSaveMessage(
|
|
1237
|
+
convId,
|
|
1238
|
+
buildUserPromptMessageContent(prompt),
|
|
1239
|
+
'user',
|
|
1240
|
+
persistAttachments.length > 0 ? persistAttachments : undefined,
|
|
1241
|
+
widgetId,
|
|
1242
|
+
);
|
|
1243
|
+
const savedAtt = saved?.attachments || saved?.data?.attachments || [];
|
|
1244
|
+
savedAttachmentCount = Array.isArray(savedAtt) ? savedAtt.length : 0;
|
|
1245
|
+
console.log('[Codegen] Persisted initial user prompt on generate:', convId, 'attachments:', persistAttachments.length, 'saved:', savedAttachmentCount);
|
|
1246
|
+
if (Array.isArray(savedAtt) && savedAtt.length > 0) {
|
|
1247
|
+
panelChat?.updateLastUserMessageAttachments?.(savedAtt);
|
|
1248
|
+
}
|
|
1249
|
+
} else if (missingPersist.length > 0) {
|
|
1250
|
+
const merged = [...(targetUser.attachments || []), ...missingPersist];
|
|
1251
|
+
const apiAtt = await mapChatAttachmentsForApi(merged, widgetId);
|
|
1252
|
+
if (apiAtt.length > 0) {
|
|
1253
|
+
try {
|
|
1254
|
+
await messagesClient.updateMessageRaw({
|
|
1255
|
+
conversationId: convId,
|
|
1256
|
+
messageId: targetUser.id,
|
|
1257
|
+
updateMessageRequest: { attachments: apiAtt },
|
|
1258
|
+
});
|
|
1259
|
+
savedAttachmentCount = apiAtt.length;
|
|
1260
|
+
console.log('[Codegen] Saved missing attachments on user message:', targetUser.id, apiAtt.length);
|
|
1261
|
+
} catch (updateErr) {
|
|
1262
|
+
console.warn('[Codegen] Failed to PATCH user message attachments:', updateErr);
|
|
1263
|
+
}
|
|
1264
|
+
panelChat?.updateLastUserMessageAttachments?.(apiAtt);
|
|
1265
|
+
} else {
|
|
1266
|
+
console.warn('[Codegen] mapChatAttachmentsForApi returned 0 items — attachments not stored');
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (persistAttachments.length > 0 && savedAttachmentCount === 0) {
|
|
1270
|
+
console.error('[Codegen] User prompt saved without attachments — chat reopen will show placeholder');
|
|
1271
|
+
}
|
|
1272
|
+
if (convId !== visibleConversationId) {
|
|
1273
|
+
await panelChat?.selectConversation?.(convId);
|
|
1274
|
+
patchPanelSnapshot(widgetId, { currentChatConversationId: convId });
|
|
1275
|
+
} else {
|
|
1276
|
+
console.log('[Codegen] Keep local chat attachments after persist; skip reload wipe');
|
|
1007
1277
|
}
|
|
1008
1278
|
} catch (err) {
|
|
1009
1279
|
console.warn('[Codegen] Could not persist user prompt before generation:', err);
|
|
@@ -1028,18 +1298,27 @@
|
|
|
1028
1298
|
console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
|
|
1029
1299
|
}
|
|
1030
1300
|
|
|
1031
|
-
/** Screenshot: capture widget preview iframe and return data URL */
|
|
1032
|
-
async function handleChatScreenshot(): Promise<string | null> {
|
|
1033
|
-
|
|
1034
|
-
|
|
1301
|
+
/** Screenshot: capture this panel's widget preview iframe and return data URL */
|
|
1302
|
+
async function handleChatScreenshot(widgetId?: string | null): Promise<string | null> {
|
|
1303
|
+
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1304
|
+
const panelRef = getWidgetDetailsRef(targetId);
|
|
1305
|
+
const iframe =
|
|
1306
|
+
(panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null) ||
|
|
1307
|
+
(document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement | null);
|
|
1308
|
+
|
|
1309
|
+
if (!iframe?.contentWindow) {
|
|
1310
|
+
console.log('[Chat→Screenshot] Preview iframe not ready', {
|
|
1311
|
+
targetId: targetId?.substring(0, 8),
|
|
1312
|
+
hasValidPreview: panelRef?.hasValidPreview?.() ?? false,
|
|
1313
|
+
});
|
|
1035
1314
|
return null;
|
|
1036
1315
|
}
|
|
1037
|
-
const iframe = document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement;
|
|
1038
|
-
if (!iframe?.contentWindow) return null;
|
|
1039
1316
|
|
|
1317
|
+
console.log('[Chat→Screenshot] Requesting screenshot from preview iframe', targetId?.substring(0, 8));
|
|
1040
1318
|
return new Promise<string | null>((resolve) => {
|
|
1041
1319
|
const timeout = setTimeout(() => {
|
|
1042
1320
|
window.removeEventListener('message', handler);
|
|
1321
|
+
console.warn('[Chat→Screenshot] Timed out waiting for widgetic:screenshot');
|
|
1043
1322
|
resolve(null);
|
|
1044
1323
|
}, 8000);
|
|
1045
1324
|
|
|
@@ -1139,26 +1418,40 @@
|
|
|
1139
1418
|
const json = await response.raw.json();
|
|
1140
1419
|
const messages = json?.data || [];
|
|
1141
1420
|
console.log('[Chat→Backend] Loaded messages for conversation:', conversationId, messages.length);
|
|
1142
|
-
const mappedMessages = messages.map((m: any) =>
|
|
1421
|
+
const mappedMessages = messages.map((m: any) => {
|
|
1422
|
+
const rawAttachments = m.attachments || m.message_contents || m.messageContents || [];
|
|
1423
|
+
const attachments = (Array.isArray(rawAttachments) ? rawAttachments : []).map((a: any, idx: number) => {
|
|
1424
|
+
const nested = a?.contents || a?.content;
|
|
1425
|
+
const content = Array.isArray(nested) ? (nested[0] || {}) : (nested || a || {});
|
|
1426
|
+
const url = content.url || content.public_url || content.publicUrl || content.file_url || a.url || '';
|
|
1427
|
+
let fileType = content.file_type || content.fileType || a.file_type || a.fileType || '';
|
|
1428
|
+
const fileName = content.file_name || content.fileName || a.file_name || a.fileName || null;
|
|
1429
|
+
if (!fileType || !String(fileType).includes('/')) {
|
|
1430
|
+
fileType = guessAttachmentFileType(url, fileName, fileType);
|
|
1431
|
+
}
|
|
1432
|
+
return {
|
|
1433
|
+
id: content.id || a.id || `att_${idx}`,
|
|
1434
|
+
url,
|
|
1435
|
+
file_type: fileType,
|
|
1436
|
+
file_name: fileName,
|
|
1437
|
+
file_size: content.file_size ?? content.fileSize ?? a.file_size ?? a.fileSize ?? null,
|
|
1438
|
+
display_order: a.display_order ?? a.displayOrder ?? content.display_order ?? idx + 1,
|
|
1439
|
+
created_at: content.created_at || content.createdAt || a.created_at || a.createdAt || new Date().toISOString(),
|
|
1440
|
+
};
|
|
1441
|
+
}).filter((a: { url: string }) => !!a.url);
|
|
1442
|
+
return {
|
|
1143
1443
|
id: m.id,
|
|
1144
1444
|
conversationId: m.conversation_id || m.conversationId || conversationId,
|
|
1145
1445
|
messageContent: m.message_content || m.messageContent || '',
|
|
1146
1446
|
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
|
-
})),
|
|
1447
|
+
attachments,
|
|
1156
1448
|
isCommit: m.is_commit || m.isCommit || false,
|
|
1157
1449
|
userId: m.user_id || m.userId || '',
|
|
1158
1450
|
createdAt: new Date(m.created_at || m.createdAt),
|
|
1159
1451
|
updatedAt: new Date(m.updated_at || m.updatedAt || m.created_at || m.createdAt),
|
|
1160
1452
|
rolledBackAt: m.rolled_back_at || m.rolledBackAt || null,
|
|
1161
|
-
}
|
|
1453
|
+
};
|
|
1454
|
+
});
|
|
1162
1455
|
const withHead = markWidgetHeadOnLoadedMessages(mappedMessages);
|
|
1163
1456
|
const syncWidgetId = widgetIdForSync ?? selectedWidgetId;
|
|
1164
1457
|
if (syncWidgetId) {
|
|
@@ -1195,48 +1488,96 @@
|
|
|
1195
1488
|
}
|
|
1196
1489
|
}
|
|
1197
1490
|
|
|
1198
|
-
|
|
1491
|
+
function dataUrlToUploadFile(dataUrl: string, fileName: string): File | null {
|
|
1492
|
+
try {
|
|
1493
|
+
const comma = dataUrl.indexOf(',');
|
|
1494
|
+
if (comma < 0) return null;
|
|
1495
|
+
const header = dataUrl.slice(0, comma);
|
|
1496
|
+
const mimeMatch = header.match(/data:([^;,]+)/);
|
|
1497
|
+
const mimeType = mimeMatch?.[1] || 'application/octet-stream';
|
|
1498
|
+
const binary = atob(dataUrl.slice(comma + 1));
|
|
1499
|
+
const bytes = new Uint8Array(binary.length);
|
|
1500
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1501
|
+
return new File([bytes], fileName, { type: mimeType });
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
console.warn('[Chat→Backend] Could not convert data URL to File:', err);
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/** Map chat attachments to API items. Inline data/blob files are uploaded to user storage (CDN URL). */
|
|
1199
1509
|
async function mapChatAttachmentsForApi(
|
|
1200
1510
|
attachments?: Array<{
|
|
1201
1511
|
url?: string;
|
|
1512
|
+
file?: File;
|
|
1202
1513
|
file_type?: string;
|
|
1203
1514
|
file_name?: string | null;
|
|
1204
1515
|
file_size?: number | null;
|
|
1205
1516
|
display_order?: number;
|
|
1206
1517
|
}>,
|
|
1518
|
+
persistWidgetId?: string | null,
|
|
1207
1519
|
): Promise<Array<{ url: string; file_type: string; file_name?: string; file_size?: number; display_order?: number }>> {
|
|
1208
1520
|
if (!attachments?.length) return [];
|
|
1209
1521
|
const items: Array<{ url: string; file_type: string; file_name?: string; file_size?: number; display_order?: number }> = [];
|
|
1210
1522
|
for (let i = 0; i < attachments.length; i++) {
|
|
1211
1523
|
const att = attachments[i];
|
|
1212
1524
|
let url = att.url || '';
|
|
1213
|
-
|
|
1214
|
-
if (url.startsWith('blob:')) {
|
|
1525
|
+
const fileName = att.file_name || att.file?.name || `attachment-${i + 1}`;
|
|
1526
|
+
if (att.file instanceof File && (!url || url.startsWith('blob:') || url.startsWith('data:'))) {
|
|
1215
1527
|
try {
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
reader.onload = () => resolve(String(reader.result));
|
|
1220
|
-
reader.onerror = () => reject(reader.error);
|
|
1221
|
-
reader.readAsDataURL(blob);
|
|
1528
|
+
const uploaded = await handleChatUploadFile(att.file, {
|
|
1529
|
+
source: 'chat-message-persist',
|
|
1530
|
+
widgetId: persistWidgetId || undefined,
|
|
1222
1531
|
});
|
|
1532
|
+
url = uploaded.url;
|
|
1533
|
+
console.log('[Chat→Backend] Uploaded chat File to user storage:', uploaded.id, url.slice(0, 80));
|
|
1534
|
+
items.push({
|
|
1535
|
+
url,
|
|
1536
|
+
file_type: guessAttachmentFileType(url, uploaded.fileName || fileName, uploaded.fileType || att.file_type || att.file.type),
|
|
1537
|
+
file_name: uploaded.fileName || fileName,
|
|
1538
|
+
file_size: uploaded.fileSize,
|
|
1539
|
+
display_order: att.display_order ?? i + 1,
|
|
1540
|
+
});
|
|
1541
|
+
continue;
|
|
1223
1542
|
} catch (err) {
|
|
1224
|
-
console.warn('[Chat→Backend]
|
|
1543
|
+
console.warn('[Chat→Backend] Chat File upload failed, skipping attachment:', err);
|
|
1225
1544
|
continue;
|
|
1226
1545
|
}
|
|
1227
1546
|
}
|
|
1228
|
-
if (url
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1547
|
+
if (!url) continue;
|
|
1548
|
+
if (url.startsWith('blob:') || url.startsWith('data:')) {
|
|
1549
|
+
try {
|
|
1550
|
+
let file: File | null = null;
|
|
1551
|
+
if (url.startsWith('blob:')) {
|
|
1552
|
+
const blob = await fetch(url).then((r) => r.blob());
|
|
1553
|
+
file = new File([blob], fileName, { type: blob.type || att.file_type || 'application/octet-stream' });
|
|
1554
|
+
} else {
|
|
1555
|
+
file = dataUrlToUploadFile(url, fileName);
|
|
1556
|
+
}
|
|
1557
|
+
if (!file) continue;
|
|
1558
|
+
const uploaded = await handleChatUploadFile(file, {
|
|
1559
|
+
source: 'chat-message-persist',
|
|
1560
|
+
widgetId: persistWidgetId || undefined,
|
|
1561
|
+
});
|
|
1562
|
+
url = uploaded.url;
|
|
1563
|
+
console.log('[Chat→Backend] Uploaded chat file to user storage:', uploaded.id, url.slice(0, 80));
|
|
1564
|
+
items.push({
|
|
1565
|
+
url,
|
|
1566
|
+
file_type: guessAttachmentFileType(url, uploaded.fileName || fileName, uploaded.fileType || att.file_type),
|
|
1567
|
+
file_name: uploaded.fileName || fileName,
|
|
1568
|
+
file_size: uploaded.fileSize,
|
|
1569
|
+
display_order: att.display_order ?? i + 1,
|
|
1570
|
+
});
|
|
1571
|
+
continue;
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
console.warn('[Chat→Backend] Chat file upload failed, skipping attachment:', err);
|
|
1233
1574
|
continue;
|
|
1234
1575
|
}
|
|
1235
1576
|
}
|
|
1236
1577
|
items.push({
|
|
1237
1578
|
url,
|
|
1238
|
-
file_type: att.file_type
|
|
1239
|
-
file_name:
|
|
1579
|
+
file_type: guessAttachmentFileType(url, fileName, att.file_type),
|
|
1580
|
+
file_name: fileName,
|
|
1240
1581
|
file_size: att.file_size ?? undefined,
|
|
1241
1582
|
display_order: att.display_order ?? i + 1,
|
|
1242
1583
|
});
|
|
@@ -1251,11 +1592,13 @@
|
|
|
1251
1592
|
messageType: string,
|
|
1252
1593
|
attachments?: Array<{
|
|
1253
1594
|
url?: string;
|
|
1595
|
+
file?: File;
|
|
1254
1596
|
file_type?: string;
|
|
1255
1597
|
file_name?: string | null;
|
|
1256
1598
|
file_size?: number | null;
|
|
1257
1599
|
display_order?: number;
|
|
1258
1600
|
}>,
|
|
1601
|
+
persistWidgetId?: string | null,
|
|
1259
1602
|
): Promise<any> {
|
|
1260
1603
|
try {
|
|
1261
1604
|
if (!messagesClient) {
|
|
@@ -1268,7 +1611,17 @@
|
|
|
1268
1611
|
return null;
|
|
1269
1612
|
}
|
|
1270
1613
|
const apiAttachments =
|
|
1271
|
-
messageType === 'user' ? await mapChatAttachmentsForApi(
|
|
1614
|
+
messageType === 'user' ? await mapChatAttachmentsForApi(
|
|
1615
|
+
(attachments || []).map((att, i) => ({
|
|
1616
|
+
url: att.url,
|
|
1617
|
+
file: att.file,
|
|
1618
|
+
file_type: guessAttachmentFileType(att.url || '', att.file_name, att.file_type),
|
|
1619
|
+
file_name: att.file_name || att.file?.name || `attachment-${i + 1}`,
|
|
1620
|
+
file_size: att.file_size && att.file_size > 0 ? att.file_size : att.file?.size,
|
|
1621
|
+
display_order: att.display_order ?? i + 1,
|
|
1622
|
+
})),
|
|
1623
|
+
persistWidgetId,
|
|
1624
|
+
) : [];
|
|
1272
1625
|
const createPayload = {
|
|
1273
1626
|
message_content: content,
|
|
1274
1627
|
message_type: messageType as 'user' | 'assistant',
|
|
@@ -1283,25 +1636,18 @@
|
|
|
1283
1636
|
});
|
|
1284
1637
|
const json = await response.raw.json();
|
|
1285
1638
|
console.log('[Chat→Backend] Saved message to conversation:', conversationId);
|
|
1286
|
-
|
|
1639
|
+
const saved = json?.data || json;
|
|
1640
|
+
const savedAtt = saved?.attachments;
|
|
1641
|
+
return {
|
|
1642
|
+
...saved,
|
|
1643
|
+
attachments: (Array.isArray(savedAtt) && savedAtt.length > 0) ? savedAtt : apiAttachments,
|
|
1644
|
+
};
|
|
1287
1645
|
} catch (withAttachmentsError) {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if (apiAttachments.length === 0) throw withAttachmentsError;
|
|
1291
|
-
console.warn(
|
|
1292
|
-
'[Chat→Backend] Save with attachments failed, retrying text-only:',
|
|
1646
|
+
console.error(
|
|
1647
|
+
'[Chat→Backend] Save with attachments failed — not falling back to text-only:',
|
|
1293
1648
|
withAttachmentsError,
|
|
1294
1649
|
);
|
|
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;
|
|
1650
|
+
throw withAttachmentsError;
|
|
1305
1651
|
}
|
|
1306
1652
|
} catch (error) {
|
|
1307
1653
|
console.error('[Chat→Backend] Failed to save message:', error);
|
|
@@ -1496,6 +1842,9 @@
|
|
|
1496
1842
|
);
|
|
1497
1843
|
const sessionAhead = sessionOrdered.find((candidate) => !commitsMatch(candidate, liveSha));
|
|
1498
1844
|
if (sessionAhead) return sessionAhead;
|
|
1845
|
+
// localStorage HEAD written at codegen complete — must win over live SHA
|
|
1846
|
+
// even before snap/live.lastCommitId propagate (reopen currently "fixed" this).
|
|
1847
|
+
if (stored && !commitsMatch(stored, liveSha)) return stored;
|
|
1499
1848
|
const sessionMatch = sessionOrdered.find((candidate) => commitsMatch(candidate, liveSha));
|
|
1500
1849
|
if (sessionMatch) return sessionMatch;
|
|
1501
1850
|
if (stored && commitsMatch(stored, liveSha)) return stored;
|
|
@@ -1554,18 +1903,21 @@
|
|
|
1554
1903
|
let editingCompositionName = '';
|
|
1555
1904
|
let deletingCompositionId: string | null = null;
|
|
1556
1905
|
|
|
1557
|
-
/**
|
|
1558
|
-
const
|
|
1906
|
+
/** Explicit chat box height (px). Drag grows this and pushes Publish down. */
|
|
1907
|
+
const CHAT_DEFAULT_HEIGHT = 550;
|
|
1559
1908
|
const CHAT_MIN_HEIGHT = 260;
|
|
1560
|
-
const CHAT_MAX_HEIGHT =
|
|
1909
|
+
const CHAT_MAX_HEIGHT = 2000;
|
|
1561
1910
|
|
|
1562
1911
|
function getChatContainerHeightForPanel(widgetId: string): number {
|
|
1563
|
-
|
|
1912
|
+
const stored = getPanelState(widgetId).chatContainerHeight;
|
|
1913
|
+
if (stored == null || stored <= 0) return CHAT_DEFAULT_HEIGHT;
|
|
1914
|
+
return Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, stored));
|
|
1564
1915
|
}
|
|
1565
1916
|
|
|
1566
1917
|
function handleChatMessagesResize(widgetId: string, deltaPx: number): void {
|
|
1567
1918
|
const current = getChatContainerHeightForPanel(widgetId);
|
|
1568
1919
|
const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
|
|
1920
|
+
console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
|
|
1569
1921
|
patchPanelSnapshot(widgetId, { chatContainerHeight: next });
|
|
1570
1922
|
}
|
|
1571
1923
|
|
|
@@ -1924,7 +2276,11 @@
|
|
|
1924
2276
|
compositionSchemasError = schemaError;
|
|
1925
2277
|
}
|
|
1926
2278
|
if (designSchema || contentSchema) {
|
|
1927
|
-
queueMicrotask(() =>
|
|
2279
|
+
queueMicrotask(() => {
|
|
2280
|
+
void loadCompositionsForPanel(widgetId).then(() => {
|
|
2281
|
+
applyPanelCompositionToPreview(widgetId);
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
1928
2284
|
}
|
|
1929
2285
|
} finally {
|
|
1930
2286
|
schemaLoadInFlight.delete(widgetId);
|
|
@@ -1962,20 +2318,32 @@
|
|
|
1962
2318
|
return values;
|
|
1963
2319
|
}
|
|
1964
2320
|
|
|
1965
|
-
/** After codegen, push design.json defaults into Create preview
|
|
2321
|
+
/** After codegen, push design.json + content.json defaults into Create preview. */
|
|
1966
2322
|
async function applyDesignSchemaDefaultsToPreview(widgetId: string) {
|
|
1967
2323
|
if (!widgetId || !widgetsClient) return;
|
|
1968
2324
|
await scheduleWidgetSchemaLoad(widgetId);
|
|
1969
2325
|
const schema = getPanelDisplayState(widgetId).compositionDesignSchema;
|
|
1970
2326
|
const defaults = extractDesignDefaultsFromSchema(schema);
|
|
1971
|
-
if (Object.keys(defaults).length
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2327
|
+
if (Object.keys(defaults).length > 0) {
|
|
2328
|
+
if (widgetId === focusedPanelWidgetId) {
|
|
2329
|
+
liveDesignValues = defaults;
|
|
2330
|
+
} else {
|
|
2331
|
+
patchPanelSnapshot(widgetId, { liveDesignValues: defaults });
|
|
2332
|
+
}
|
|
2333
|
+
getWidgetDetailsRef(widgetId)?.sendMessageToPreview({ type: 'widgetic:update', design: defaults });
|
|
2334
|
+
console.log('[Create Preview] Applied design schema defaults:', Object.keys(defaults));
|
|
2335
|
+
}
|
|
2336
|
+
const contentSchema = getPanelDisplayState(widgetId).compositionContentSchema;
|
|
2337
|
+
const schemaItems = contentSchema?.contentItems ?? contentSchema?.data?.contentItems;
|
|
2338
|
+
if (Array.isArray(schemaItems) && schemaItems.length > 0) {
|
|
2339
|
+
const existing = getPanelDisplayState(widgetId).liveContentItems || [];
|
|
2340
|
+
const merged = mergeContentItemsWithSchema(existing, schemaItems);
|
|
2341
|
+
patchPanelSnapshot(widgetId, { liveContentItems: merged });
|
|
2342
|
+
if (widgetId === focusedPanelWidgetId) liveContentItems = merged;
|
|
1976
2343
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
2344
|
+
applyPanelCompositionToPreview(widgetId);
|
|
2345
|
+
setTimeout(() => applyPanelCompositionToPreview(widgetId), 250);
|
|
2346
|
+
setTimeout(() => applyPanelCompositionToPreview(widgetId), 800);
|
|
1979
2347
|
}
|
|
1980
2348
|
|
|
1981
2349
|
/** Toggle composition editor panel open/closed */
|
|
@@ -2045,9 +2413,20 @@
|
|
|
2045
2413
|
});
|
|
2046
2414
|
}
|
|
2047
2415
|
if (event.detail.step === 'create') {
|
|
2048
|
-
|
|
2049
|
-
|
|
2416
|
+
let psCreate = getPanelDisplayState(panelWidgetId);
|
|
2417
|
+
if (!psCreate.compositionContentSchema && !psCreate.compositionSchemasLoading) {
|
|
2418
|
+
void scheduleWidgetSchemaLoad(panelWidgetId);
|
|
2050
2419
|
}
|
|
2420
|
+
if (!psCreate.liveContentItems || psCreate.liveContentItems.length === 0) {
|
|
2421
|
+
const schemaItems = psCreate.compositionContentSchema?.contentItems
|
|
2422
|
+
?? psCreate.compositionContentSchema?.data?.contentItems;
|
|
2423
|
+
if (Array.isArray(schemaItems) && schemaItems.length > 0) {
|
|
2424
|
+
const seeded = JSON.parse(JSON.stringify(schemaItems));
|
|
2425
|
+
patchPanelSnapshot(panelWidgetId, { liveContentItems: seeded });
|
|
2426
|
+
if (panelWidgetId === focusedPanelWidgetId) liveContentItems = seeded;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
applyPanelCompositionToPreview(panelWidgetId);
|
|
2051
2430
|
if (panelWidgetId && pendingCreatePreviewDefaultsWidgetId === panelWidgetId) {
|
|
2052
2431
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
2053
2432
|
if (panelWidgetId === focusedPanelWidgetId) {
|
|
@@ -2593,7 +2972,10 @@
|
|
|
2593
2972
|
widgetDetails?.sendMessageToPreview({ type: 'widgetic:update', content: liveContentValues });
|
|
2594
2973
|
}
|
|
2595
2974
|
if (liveContentItems.length > 0) {
|
|
2596
|
-
widgetDetails?.sendMessageToPreview({
|
|
2975
|
+
widgetDetails?.sendMessageToPreview({
|
|
2976
|
+
type: 'widgetic:update',
|
|
2977
|
+
contentItems: flattenContentItemsForPreview(liveContentItems)
|
|
2978
|
+
});
|
|
2597
2979
|
}
|
|
2598
2980
|
}
|
|
2599
2981
|
|
|
@@ -3003,6 +3385,8 @@
|
|
|
3003
3385
|
}
|
|
3004
3386
|
|
|
3005
3387
|
let canvasWireframeMode = true;
|
|
3388
|
+
/** Extra canvas chrome — default off. Not tied to debugMode. */
|
|
3389
|
+
let canvasAdvancedFeatures = !!initialAdvancedFeatures;
|
|
3006
3390
|
let canvasPrimaryWidgetId: string | null = null;
|
|
3007
3391
|
|
|
3008
3392
|
function emitCreatorWireframeChanged() {
|
|
@@ -3020,6 +3404,21 @@
|
|
|
3020
3404
|
emitCreatorWireframeChanged();
|
|
3021
3405
|
}
|
|
3022
3406
|
|
|
3407
|
+
function emitCreatorAdvancedChanged() {
|
|
3408
|
+
if (typeof window === 'undefined') return;
|
|
3409
|
+
window.dispatchEvent(
|
|
3410
|
+
new CustomEvent(CREATOR_ADVANCED_CHANGED_EVENT, {
|
|
3411
|
+
detail: { advancedFeatures: canvasAdvancedFeatures }
|
|
3412
|
+
})
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
function handleHostToggleAdvancedEvent() {
|
|
3417
|
+
canvasAdvancedFeatures = !canvasAdvancedFeatures;
|
|
3418
|
+
console.log('[CreatorApp] Advanced canvas features:', canvasAdvancedFeatures ? 'ON' : 'OFF');
|
|
3419
|
+
emitCreatorAdvancedChanged();
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3023
3422
|
function resolveHostTargetWidgetId(): string | null {
|
|
3024
3423
|
const id = focusedPanelWidgetId || selectedWidgetId;
|
|
3025
3424
|
if (!id || isDraftConvertId(id)) return null;
|
|
@@ -3616,7 +4015,7 @@
|
|
|
3616
4015
|
canvasPrimaryWidgetId ||
|
|
3617
4016
|
canvasRef.getPrimaryWidgetId?.() ||
|
|
3618
4017
|
selectedWidgetId;
|
|
3619
|
-
if (primaryId) {
|
|
4018
|
+
if (primaryId && !isConvertOrDraftPanelOpen()) {
|
|
3620
4019
|
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
3621
4020
|
canvasRef.selectWidgetShape(primaryId);
|
|
3622
4021
|
}
|
|
@@ -4481,6 +4880,11 @@
|
|
|
4481
4880
|
if (embeddedDeepLinkAutoOpenDone) {
|
|
4482
4881
|
return;
|
|
4483
4882
|
}
|
|
4883
|
+
if (isConvertOrDraftPanelOpen()) {
|
|
4884
|
+
console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
|
|
4885
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
4886
|
+
return;
|
|
4887
|
+
}
|
|
4484
4888
|
|
|
4485
4889
|
let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
|
|
4486
4890
|
// Canvas-only deep-link: no ?widget_id= / initialWidgetId — select canvases.primary_widget_id.
|
|
@@ -5247,11 +5651,7 @@
|
|
|
5247
5651
|
|
|
5248
5652
|
const status = (op?.status || '').toLowerCase();
|
|
5249
5653
|
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
|
5250
|
-
|
|
5251
|
-
settle('reject', new Error(op?.error || 'Operation failed.'));
|
|
5252
|
-
} else {
|
|
5253
|
-
settle('resolve', op);
|
|
5254
|
-
}
|
|
5654
|
+
settle('resolve', op);
|
|
5255
5655
|
return;
|
|
5256
5656
|
}
|
|
5257
5657
|
|
|
@@ -5290,11 +5690,7 @@
|
|
|
5290
5690
|
signal
|
|
5291
5691
|
})
|
|
5292
5692
|
.then((finalOp) => {
|
|
5293
|
-
|
|
5294
|
-
settle('reject', new Error(finalOp?.error || 'Operation failed.'));
|
|
5295
|
-
} else {
|
|
5296
|
-
settle('resolve', finalOp as OperationStatus);
|
|
5297
|
-
}
|
|
5693
|
+
settle('resolve', finalOp as OperationStatus);
|
|
5298
5694
|
})
|
|
5299
5695
|
.catch((streamError) => {
|
|
5300
5696
|
if (settled || signal?.aborted) return;
|
|
@@ -5822,11 +6218,22 @@
|
|
|
5822
6218
|
const quiet = !!options.quiet;
|
|
5823
6219
|
console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
|
|
5824
6220
|
|
|
6221
|
+
if (isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
|
|
6222
|
+
console.log('[handleRefreshScreenshot] Skip iframe capture — convert/draft panel is open:', widgetId.substring(0, 8));
|
|
6223
|
+
return;
|
|
6224
|
+
}
|
|
6225
|
+
|
|
5825
6226
|
// Quiet canvas-thumbnail sync must never force-open Widget Details.
|
|
5826
|
-
//
|
|
6227
|
+
// Convert drafts capture from the canvas shape, never by opening a panel.
|
|
5827
6228
|
if (!openWidgetPanelIds.includes(widgetId)) {
|
|
5828
|
-
if (quiet) {
|
|
5829
|
-
|
|
6229
|
+
if (quiet || isConvertOrDraftPanelOpen()) {
|
|
6230
|
+
const shapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
|
|
6231
|
+
if (shapeDataUrl && canvasRef) {
|
|
6232
|
+
console.log('[handleRefreshScreenshot] Using canvas shape screenshot (panel closed):', widgetId.substring(0, 8));
|
|
6233
|
+
canvasRef.updateWidgetImage(widgetId, shapeDataUrl);
|
|
6234
|
+
} else {
|
|
6235
|
+
console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
|
|
6236
|
+
}
|
|
5830
6237
|
return;
|
|
5831
6238
|
}
|
|
5832
6239
|
// Manual "Refresh Widget Preview" always opens the panel so the iframe exists.
|
|
@@ -5836,8 +6243,9 @@
|
|
|
5836
6243
|
}
|
|
5837
6244
|
|
|
5838
6245
|
const panelRef = getWidgetDetailsRef(widgetId);
|
|
5839
|
-
|
|
5840
|
-
|
|
6246
|
+
const iframe = panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null;
|
|
6247
|
+
if (!iframe?.contentWindow) {
|
|
6248
|
+
console.log('[handleRefreshScreenshot] Preview iframe not ready — skipping capture');
|
|
5841
6249
|
if (!quiet) {
|
|
5842
6250
|
showToast('error', 'No valid preview available', { description: 'Generate code first to capture a screenshot.', duration: 5000 });
|
|
5843
6251
|
}
|
|
@@ -5848,14 +6256,6 @@
|
|
|
5848
6256
|
showToast('info', 'Capturing widget screenshot...', { duration: 3000 });
|
|
5849
6257
|
}
|
|
5850
6258
|
|
|
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
6259
|
try {
|
|
5860
6260
|
const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
|
|
5861
6261
|
const timeout = setTimeout(() => {
|
|
@@ -5982,10 +6382,21 @@ Requirements:
|
|
|
5982
6382
|
fromChat?: boolean;
|
|
5983
6383
|
widgetId?: string;
|
|
5984
6384
|
compileFixOnly?: boolean;
|
|
6385
|
+
/** Recreate GitLab repo when the previous turn failed with missing repository. */
|
|
6386
|
+
forceRecreateRepo?: boolean;
|
|
5985
6387
|
/** CDN upload row IDs captured before chat clearDraft. */
|
|
5986
6388
|
attachmentIds?: string[];
|
|
5987
6389
|
/** Image URLs / data URLs captured before chat clearDraft. */
|
|
5988
6390
|
promptImagesOverride?: string[];
|
|
6391
|
+
/** Chat files (PDF, video, images) to store on the user message before codegen. */
|
|
6392
|
+
persistFileAttachments?: Array<{
|
|
6393
|
+
url?: string;
|
|
6394
|
+
file?: File;
|
|
6395
|
+
file_type?: string;
|
|
6396
|
+
file_name?: string | null;
|
|
6397
|
+
file_size?: number | null;
|
|
6398
|
+
display_order?: number;
|
|
6399
|
+
}>;
|
|
5989
6400
|
} = {}) {
|
|
5990
6401
|
const targetWidgetId = options.widgetId ?? selectedWidgetId;
|
|
5991
6402
|
if (!targetWidgetId) return;
|
|
@@ -6062,8 +6473,10 @@ Requirements:
|
|
|
6062
6473
|
fromChat: options.fromChat,
|
|
6063
6474
|
widgetId: codegenTargetWidgetId,
|
|
6064
6475
|
compileFixOnly: options.compileFixOnly,
|
|
6476
|
+
forceRecreateRepo: options.forceRecreateRepo,
|
|
6065
6477
|
attachmentIds: options.attachmentIds,
|
|
6066
6478
|
promptImagesOverride: options.promptImagesOverride,
|
|
6479
|
+
persistFileAttachments: options.persistFileAttachments,
|
|
6067
6480
|
});
|
|
6068
6481
|
}
|
|
6069
6482
|
|
|
@@ -6082,8 +6495,17 @@ Requirements:
|
|
|
6082
6495
|
fromChat?: boolean;
|
|
6083
6496
|
widgetId?: string;
|
|
6084
6497
|
compileFixOnly?: boolean;
|
|
6498
|
+
forceRecreateRepo?: boolean;
|
|
6085
6499
|
attachmentIds?: string[];
|
|
6086
6500
|
promptImagesOverride?: string[];
|
|
6501
|
+
persistFileAttachments?: Array<{
|
|
6502
|
+
url?: string;
|
|
6503
|
+
file?: File;
|
|
6504
|
+
file_type?: string;
|
|
6505
|
+
file_name?: string | null;
|
|
6506
|
+
file_size?: number | null;
|
|
6507
|
+
display_order?: number;
|
|
6508
|
+
}>;
|
|
6087
6509
|
} = {},
|
|
6088
6510
|
) {
|
|
6089
6511
|
const generationWidgetId = options.widgetId ?? selectedWidgetId;
|
|
@@ -6106,7 +6528,6 @@ Requirements:
|
|
|
6106
6528
|
lastHarnessChatLine = '';
|
|
6107
6529
|
|
|
6108
6530
|
// 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
6531
|
if (panelChat && !options.fromChat) {
|
|
6111
6532
|
const snapImages =
|
|
6112
6533
|
generationWidgetId === focusedPanelWidgetId ? promptImages : panelSnap.promptImages;
|
|
@@ -6120,8 +6541,7 @@ Requirements:
|
|
|
6120
6541
|
: undefined;
|
|
6121
6542
|
panelChat.addUserMessage(prompt, codegenImageAttachments);
|
|
6122
6543
|
}
|
|
6123
|
-
|
|
6124
|
-
|
|
6544
|
+
|
|
6125
6545
|
isGeneratingCode = generationWidgetId === focusedPanelWidgetId;
|
|
6126
6546
|
setCrossTabGenerating(true, generationWidgetId);
|
|
6127
6547
|
lastGenerationPrompt = prompt;
|
|
@@ -6132,6 +6552,16 @@ Requirements:
|
|
|
6132
6552
|
? 'Processing queued code generation request...'
|
|
6133
6553
|
: 'Preparing code generation...',
|
|
6134
6554
|
});
|
|
6555
|
+
|
|
6556
|
+
// Persist chat attachments before repo/codegen so reopen + failed GLM still show the file.
|
|
6557
|
+
if (!options.compileFixOnly) {
|
|
6558
|
+
await ensureCodegenUserMessagePersisted(
|
|
6559
|
+
generationWidgetId,
|
|
6560
|
+
prompt,
|
|
6561
|
+
options.promptImagesOverride,
|
|
6562
|
+
options.persistFileAttachments,
|
|
6563
|
+
);
|
|
6564
|
+
}
|
|
6135
6565
|
|
|
6136
6566
|
// Keep preview tab active so user sees the "Generating..." overlay.
|
|
6137
6567
|
// Console logs are still accessible via the Console tab button.
|
|
@@ -6141,22 +6571,52 @@ Requirements:
|
|
|
6141
6571
|
console.log('Widget Creator: Starting code generation for widget:', generationWidgetId);
|
|
6142
6572
|
|
|
6143
6573
|
try {
|
|
6144
|
-
const generationWidget =
|
|
6145
|
-
|
|
6146
|
-
?
|
|
6147
|
-
|
|
6574
|
+
const generationWidget =
|
|
6575
|
+
getWidgetById(generationWidgetId) ||
|
|
6576
|
+
(selectedWidget?.id === generationWidgetId ? selectedWidget : null);
|
|
6577
|
+
let repoId: string | null = null;
|
|
6578
|
+
try {
|
|
6579
|
+
repoId = generationWidget
|
|
6580
|
+
? await ensureWidgetRepository(generationWidget, {
|
|
6581
|
+
forceRecreate: !!options.forceRecreateRepo,
|
|
6582
|
+
silent: true,
|
|
6583
|
+
})
|
|
6584
|
+
: panelSnap.currentRepositoryId;
|
|
6585
|
+
if (!repoId && generationWidget) {
|
|
6586
|
+
console.warn('[CodeGen] Repository missing — retrying create for widget', generationWidgetId.substring(0, 8));
|
|
6587
|
+
repoId = await ensureWidgetRepository(generationWidget, { forceRecreate: true, silent: true });
|
|
6588
|
+
}
|
|
6589
|
+
} catch (repoError) {
|
|
6590
|
+
if (repoError instanceof ResponseError && repoError.response?.status === 401) {
|
|
6591
|
+
const sessionError = 'Session expired. Sign in again, then retry.';
|
|
6592
|
+
patchCodegenUiState(generationWidgetId, {
|
|
6593
|
+
generateCodeStatus: '❌ Session expired.',
|
|
6594
|
+
lastGenerationFailed: true,
|
|
6595
|
+
lastGenerationError: sessionError,
|
|
6596
|
+
});
|
|
6597
|
+
showToast('error', 'Session expired', {
|
|
6598
|
+
description: sessionError,
|
|
6599
|
+
duration: 5000
|
|
6600
|
+
});
|
|
6601
|
+
pushCodegenFailureToChat(generationWidgetId, sessionError);
|
|
6602
|
+
return;
|
|
6603
|
+
}
|
|
6604
|
+
throw repoError;
|
|
6605
|
+
}
|
|
6148
6606
|
console.log('Widget Creator: Repository resolved for code generation:', repoId);
|
|
6149
6607
|
|
|
6150
6608
|
if (!repoId) {
|
|
6609
|
+
const missingRepoError = 'Unable to resolve repository for this widget.';
|
|
6151
6610
|
patchCodegenUiState(generationWidgetId, {
|
|
6152
6611
|
generateCodeStatus: '❌ Repository is required to generate code.',
|
|
6153
6612
|
lastGenerationFailed: true,
|
|
6154
|
-
lastGenerationError:
|
|
6613
|
+
lastGenerationError: missingRepoError,
|
|
6155
6614
|
});
|
|
6156
6615
|
showToast('error', 'Missing repository', {
|
|
6157
|
-
description:
|
|
6616
|
+
description: missingRepoError,
|
|
6158
6617
|
duration: 5000
|
|
6159
6618
|
});
|
|
6619
|
+
pushCodegenFailureToChat(generationWidgetId, missingRepoError);
|
|
6160
6620
|
return;
|
|
6161
6621
|
}
|
|
6162
6622
|
|
|
@@ -6182,7 +6642,7 @@ Requirements:
|
|
|
6182
6642
|
// Drop invalid entries (empty strings, non-data-URLs) so a failed capture
|
|
6183
6643
|
// can never silently become a vision-less generation request.
|
|
6184
6644
|
panelPromptImages = panelPromptImages.filter(
|
|
6185
|
-
(url) => typeof url === 'string' && url.startsWith('data:image/') && url.length >
|
|
6645
|
+
(url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
|
|
6186
6646
|
);
|
|
6187
6647
|
const chatAttachmentUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
|
|
6188
6648
|
const chatUploadIds = [
|
|
@@ -6190,9 +6650,12 @@ Requirements:
|
|
|
6190
6650
|
...(panelChat?.getAttachmentUploadIds?.() ?? []),
|
|
6191
6651
|
].filter((id, index, arr) => id && arr.indexOf(id) === index);
|
|
6192
6652
|
if (chatAttachmentUrls.length > 0 && !(options.promptImagesOverride?.length)) {
|
|
6193
|
-
|
|
6194
|
-
(url) => typeof url === 'string' && url.startsWith('data:image/') && url.length >
|
|
6653
|
+
const fromChat = chatAttachmentUrls.filter(
|
|
6654
|
+
(url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
|
|
6195
6655
|
);
|
|
6656
|
+
if (fromChat.length > 0) {
|
|
6657
|
+
panelPromptImages = fromChat;
|
|
6658
|
+
}
|
|
6196
6659
|
}
|
|
6197
6660
|
let imagesToSend: string[] | undefined;
|
|
6198
6661
|
let attachmentIdsToSend: string[] | undefined;
|
|
@@ -6250,6 +6713,18 @@ Requirements:
|
|
|
6250
6713
|
});
|
|
6251
6714
|
}
|
|
6252
6715
|
|
|
6716
|
+
const persistImageUrls = uniquePersistImageUrls([
|
|
6717
|
+
...(imagesToSend || []),
|
|
6718
|
+
...(panelPromptImages || []),
|
|
6719
|
+
...(options.promptImagesOverride || []),
|
|
6720
|
+
]);
|
|
6721
|
+
await ensureCodegenUserMessagePersisted(
|
|
6722
|
+
generationWidgetId,
|
|
6723
|
+
prompt,
|
|
6724
|
+
persistImageUrls,
|
|
6725
|
+
options.persistFileAttachments,
|
|
6726
|
+
);
|
|
6727
|
+
|
|
6253
6728
|
// Clear unsent draft/attachments once payload is captured — prevents restore on widget return
|
|
6254
6729
|
clearPromptDraftForWidget(generationWidgetId);
|
|
6255
6730
|
|
|
@@ -6405,6 +6880,11 @@ Requirements:
|
|
|
6405
6880
|
hasGeneratedCode: true,
|
|
6406
6881
|
...(completedCommitSha ? { lastCommitId: completedCommitSha } : {}),
|
|
6407
6882
|
});
|
|
6883
|
+
if (isFocusedPanel && completedCommitSha) {
|
|
6884
|
+
lastCommitId = completedCommitSha;
|
|
6885
|
+
hasGeneratedCode = true;
|
|
6886
|
+
}
|
|
6887
|
+
panelDisplayRevision += 1;
|
|
6408
6888
|
console.log(
|
|
6409
6889
|
'[Publish] Codegen completed — pendingPublishAfterCodegen=true',
|
|
6410
6890
|
generationWidgetId.substring(0, 8),
|
|
@@ -6548,8 +7028,9 @@ Requirements:
|
|
|
6548
7028
|
);
|
|
6549
7029
|
setTimeout(async () => {
|
|
6550
7030
|
await panelChat?.reloadConversationMessages?.();
|
|
6551
|
-
|
|
6552
|
-
|
|
7031
|
+
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
7032
|
+
}, 2500);
|
|
7033
|
+
setTimeout(() => handleRefreshScreenshot(generationWidgetId, { quiet: true }), 3000);
|
|
6553
7034
|
} else {
|
|
6554
7035
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
6555
7036
|
`Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
@@ -6621,6 +7102,7 @@ Requirements:
|
|
|
6621
7102
|
});
|
|
6622
7103
|
clearActiveOperationId(); // Clear persisted operationId on error
|
|
6623
7104
|
showToast('error', 'Code generation error', { description: message, duration: 6000 });
|
|
7105
|
+
pushCodegenFailureToChat(generationWidgetId, message);
|
|
6624
7106
|
} finally {
|
|
6625
7107
|
const { [generationWidgetId]: _done, ...restFinally } = generatingWidgetIds;
|
|
6626
7108
|
generatingWidgetIds = restFinally;
|
|
@@ -6965,6 +7447,7 @@ Requirements:
|
|
|
6965
7447
|
|
|
6966
7448
|
// Cross-tab operation sync: listen for localStorage changes from other tabs
|
|
6967
7449
|
window.addEventListener('storage', handleCrossTabStorageEvent);
|
|
7450
|
+
window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
6968
7451
|
|
|
6969
7452
|
// Clean up cross-tab flags when tab is closed/refreshed
|
|
6970
7453
|
window.addEventListener('beforeunload', () => {
|
|
@@ -7058,11 +7541,13 @@ Requirements:
|
|
|
7058
7541
|
// Cross-tab cleanup: remove flags so other tabs don't stay blocked
|
|
7059
7542
|
if (typeof window !== 'undefined') {
|
|
7060
7543
|
window.removeEventListener('storage', handleCrossTabStorageEvent);
|
|
7544
|
+
window.removeEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
7061
7545
|
window.removeEventListener('click', handleCanvasDropdownOutsideClick);
|
|
7062
7546
|
window.removeEventListener(HOST_WIDGET_RENAMED_EVENT, handleHostWidgetRenamedEvent);
|
|
7063
7547
|
window.removeEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
|
|
7064
7548
|
window.removeEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
|
|
7065
7549
|
window.removeEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
|
|
7550
|
+
window.removeEventListener(HOST_TOGGLE_ADVANCED_EVENT, handleHostToggleAdvancedEvent);
|
|
7066
7551
|
window.removeEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
|
|
7067
7552
|
window.removeEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
|
|
7068
7553
|
window.removeEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
|
|
@@ -7091,11 +7576,13 @@ Requirements:
|
|
|
7091
7576
|
window.addEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
|
|
7092
7577
|
window.addEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
|
|
7093
7578
|
window.addEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
|
|
7579
|
+
window.addEventListener(HOST_TOGGLE_ADVANCED_EVENT, handleHostToggleAdvancedEvent);
|
|
7094
7580
|
window.addEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
|
|
7095
7581
|
window.addEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
|
|
7096
7582
|
window.addEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
|
|
7097
7583
|
emitCreatorDebugChanged();
|
|
7098
7584
|
emitCreatorWireframeChanged();
|
|
7585
|
+
emitCreatorAdvancedChanged();
|
|
7099
7586
|
});
|
|
7100
7587
|
|
|
7101
7588
|
// Capture-phase Escape: close lightbox before Canvas can deselect → close WidgetDetails
|
|
@@ -7488,7 +7975,7 @@ Requirements:
|
|
|
7488
7975
|
compositionSchemasLoading,
|
|
7489
7976
|
compositionSchemasError,
|
|
7490
7977
|
schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
|
|
7491
|
-
chatContainerHeight: focusedSnap?.chatContainerHeight ??
|
|
7978
|
+
chatContainerHeight: focusedSnap?.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7492
7979
|
panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
|
|
7493
7980
|
};
|
|
7494
7981
|
}
|
|
@@ -7541,7 +8028,7 @@ Requirements:
|
|
|
7541
8028
|
compositionSchemasLoading: false,
|
|
7542
8029
|
compositionSchemasError: null,
|
|
7543
8030
|
schemaLoadStartedAt: null,
|
|
7544
|
-
chatContainerHeight:
|
|
8031
|
+
chatContainerHeight: 550,
|
|
7545
8032
|
panelPreviewCompileError: null,
|
|
7546
8033
|
};
|
|
7547
8034
|
}
|
|
@@ -7567,16 +8054,18 @@ Requirements:
|
|
|
7567
8054
|
const snapPublishReady =
|
|
7568
8055
|
snap.publishHistoryResolved && snap.publishHistoryResolvedForWidgetId === widgetId;
|
|
7569
8056
|
const livePublishMatchesPanel = live.publishHistoryResolvedForWidgetId === widgetId;
|
|
7570
|
-
const resolvedHead = resolvePanelHeadCommitId(widgetId, snap, live);
|
|
8057
|
+
const resolvedHead = isDraftConvertId(widgetId) ? null : resolvePanelHeadCommitId(widgetId, snap, live);
|
|
7571
8058
|
return {
|
|
7572
8059
|
...snap,
|
|
7573
8060
|
...live,
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
8061
|
+
lastCommitId: isDraftConvertId(widgetId) ? null : resolvedHead,
|
|
8062
|
+
hasGeneratedCode: isDraftConvertId(widgetId)
|
|
8063
|
+
? false
|
|
8064
|
+
: !!(live.hasGeneratedCode || snap.hasGeneratedCode || resolvedHead),
|
|
7578
8065
|
pendingPublishAfterCodegen: !!(snap.pendingPublishAfterCodegen || live.pendingPublishAfterCodegen),
|
|
7579
|
-
lastPublishedVersion:
|
|
8066
|
+
lastPublishedVersion: isDraftConvertId(widgetId)
|
|
8067
|
+
? null
|
|
8068
|
+
: (live.lastPublishedVersion ?? snap.lastPublishedVersion),
|
|
7580
8069
|
currentRepositoryId: live.currentRepositoryId ?? snap.currentRepositoryId,
|
|
7581
8070
|
selectedWidgetJsPath: live.selectedWidgetJsPath ?? snap.selectedWidgetJsPath,
|
|
7582
8071
|
// Once publish history loaded into this panel's snapshot, prefer it over stale globals.
|
|
@@ -7595,7 +8084,7 @@ Requirements:
|
|
|
7595
8084
|
: livePublishMatchesPanel
|
|
7596
8085
|
? live.publishHistoryResolvedForWidgetId
|
|
7597
8086
|
: snap.publishHistoryResolvedForWidgetId,
|
|
7598
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
8087
|
+
chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7599
8088
|
compositionDesignSchema: resolvedDesignSchema,
|
|
7600
8089
|
compositionContentSchema: resolvedContentSchema,
|
|
7601
8090
|
compositionSchemasLoading: schemaLoading,
|
|
@@ -7613,7 +8102,7 @@ Requirements:
|
|
|
7613
8102
|
...snap,
|
|
7614
8103
|
lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
|
|
7615
8104
|
hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
|
|
7616
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
8105
|
+
chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7617
8106
|
compositionDesignSchema: resolvedDesignSchema,
|
|
7618
8107
|
compositionContentSchema: resolvedContentSchema,
|
|
7619
8108
|
compositionSchemasError: resolvedSchemaError,
|
|
@@ -7903,6 +8392,22 @@ Requirements:
|
|
|
7903
8392
|
selectedWidget = widget;
|
|
7904
8393
|
saveSelectedWidgetId(widgetId);
|
|
7905
8394
|
notifyHostWidgetName(widget);
|
|
8395
|
+
if (isDraftConvertId(widgetId)) {
|
|
8396
|
+
lastPublishedVersion = null;
|
|
8397
|
+
lastCommitId = null;
|
|
8398
|
+
hasGeneratedCode = false;
|
|
8399
|
+
publishHistory = [];
|
|
8400
|
+
publishHistoryResolved = true;
|
|
8401
|
+
publishHistoryResolvedForWidgetId = widgetId;
|
|
8402
|
+
widgetDetailsStep = 'create';
|
|
8403
|
+
patchPanelSnapshot(widgetId, {
|
|
8404
|
+
lastPublishedVersion: null,
|
|
8405
|
+
lastCommitId: null,
|
|
8406
|
+
hasGeneratedCode: false,
|
|
8407
|
+
publishHistory: [],
|
|
8408
|
+
widgetDetailsStep: 'create',
|
|
8409
|
+
});
|
|
8410
|
+
}
|
|
7906
8411
|
if (widget?.id && canvasRef) {
|
|
7907
8412
|
canvasRef.selectWidgetShape(widget.id);
|
|
7908
8413
|
}
|
|
@@ -7920,7 +8425,13 @@ Requirements:
|
|
|
7920
8425
|
}
|
|
7921
8426
|
|
|
7922
8427
|
function positionOpenDetailsPanel(widgetId: string): void {
|
|
7923
|
-
|
|
8428
|
+
let rect = canvasRef?.getWidgetShapeScreenRect?.(widgetId) ?? null;
|
|
8429
|
+
if (!rect) {
|
|
8430
|
+
const objectId = draftConvertObjectIds.get(widgetId);
|
|
8431
|
+
if (objectId && canvasRef?.getObjectScreenRect) {
|
|
8432
|
+
rect = canvasRef.getObjectScreenRect(objectId);
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
7924
8435
|
getWidgetDetailsRef(widgetId)?.positionBesideShape?.(rect);
|
|
7925
8436
|
}
|
|
7926
8437
|
|
|
@@ -7952,6 +8463,14 @@ Requirements:
|
|
|
7952
8463
|
openWidgetPanelIds = [...openWidgetPanelIds, widgetId];
|
|
7953
8464
|
}
|
|
7954
8465
|
focusWidgetPanel(widgetId);
|
|
8466
|
+
if (isDraftConvertId(widgetId)) {
|
|
8467
|
+
detailsSessionWidgetId = widgetId;
|
|
8468
|
+
}
|
|
8469
|
+
const alreadyMounted = getWidgetDetailsRef(widgetId);
|
|
8470
|
+
if (alreadyMounted && !alreadyMounted.getShowWidgetDetails?.()) {
|
|
8471
|
+
alreadyMounted.openWidgetDetails();
|
|
8472
|
+
positionOpenDetailsPanel(widgetId);
|
|
8473
|
+
}
|
|
7955
8474
|
if (openGen !== widgetDetailsOpenGen || widgetDetailsOpenTargetId !== widgetId) {
|
|
7956
8475
|
console.log('[CreatorApp] Skip stale Widget Details open:', widgetId?.substring(0, 8));
|
|
7957
8476
|
return;
|
|
@@ -8048,6 +8567,8 @@ Requirements:
|
|
|
8048
8567
|
openWidgetPanelIds = replaced.includes(toId) ? replaced : [...replaced, toId];
|
|
8049
8568
|
if (focusedPanelWidgetId === fromId) focusedPanelWidgetId = toId;
|
|
8050
8569
|
if (selectedWidgetId === fromId) selectedWidgetId = toId;
|
|
8570
|
+
if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
|
|
8571
|
+
if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
|
|
8051
8572
|
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
8052
8573
|
console.log(
|
|
8053
8574
|
'[CreatorApp] Migrated Widget Details panel',
|
|
@@ -8074,6 +8595,9 @@ Requirements:
|
|
|
8074
8595
|
if (!options.fromShapeSwitch && widgetDetailsOpenTargetId === widgetId) {
|
|
8075
8596
|
widgetDetailsOpenTargetId = null;
|
|
8076
8597
|
}
|
|
8598
|
+
if (detailsSessionWidgetId === widgetId) {
|
|
8599
|
+
detailsSessionWidgetId = null;
|
|
8600
|
+
}
|
|
8077
8601
|
getWidgetDetailsRef(widgetId)?.closeWidgetDetails({ silent: true });
|
|
8078
8602
|
openWidgetPanelIds = openWidgetPanelIds.filter((id) => id !== widgetId);
|
|
8079
8603
|
const { [widgetId]: _z, ...restZ } = panelZIndexByWidgetId;
|
|
@@ -8106,6 +8630,8 @@ Requirements:
|
|
|
8106
8630
|
panelInstanceKeyByWidgetId = {};
|
|
8107
8631
|
focusedPanelWidgetId = null;
|
|
8108
8632
|
panelZIndexByWidgetId = {};
|
|
8633
|
+
detailsSessionWidgetId = null;
|
|
8634
|
+
widgetDetailsOpenTargetId = null;
|
|
8109
8635
|
showWidgetDetails = false;
|
|
8110
8636
|
}
|
|
8111
8637
|
|
|
@@ -8378,11 +8904,10 @@ Requirements:
|
|
|
8378
8904
|
// If the token expired during generation, we delayed logout to avoid disrupting the operation.
|
|
8379
8905
|
// Now that the operation is done, proceed with logout.
|
|
8380
8906
|
$: if (pendingTokenExpiryLogout && !isGeneratingCode && Object.keys(generatingWidgetIds).length === 0) {
|
|
8381
|
-
console.log('WidgetCreator:
|
|
8907
|
+
console.log('WidgetCreator: Token expired after operations — keeping canvas, prompting re-auth');
|
|
8382
8908
|
pendingTokenExpiryLogout = false;
|
|
8383
8909
|
tokenExpiryWarningShown = false;
|
|
8384
|
-
|
|
8385
|
-
authStatus = 'Token expired. Please login again.';
|
|
8910
|
+
authStatus = 'Session expired. Sign in again to keep saving.';
|
|
8386
8911
|
}
|
|
8387
8912
|
|
|
8388
8913
|
|
|
@@ -8464,35 +8989,56 @@ Requirements:
|
|
|
8464
8989
|
return [];
|
|
8465
8990
|
}
|
|
8466
8991
|
|
|
8992
|
+
function unwrapContentMediaValue(raw: unknown): unknown {
|
|
8993
|
+
if (raw && typeof raw === 'object' && 'url' in (raw as Record<string, unknown>)) {
|
|
8994
|
+
return (raw as { url?: string }).url ?? '';
|
|
8995
|
+
}
|
|
8996
|
+
return raw;
|
|
8997
|
+
}
|
|
8998
|
+
|
|
8999
|
+
function pickLiveContentValue(...candidates: unknown[]): unknown {
|
|
9000
|
+
for (const candidate of candidates) {
|
|
9001
|
+
const unwrapped = unwrapContentMediaValue(candidate);
|
|
9002
|
+
if (unwrapped === undefined || unwrapped === null || unwrapped === '') continue;
|
|
9003
|
+
return unwrapped;
|
|
9004
|
+
}
|
|
9005
|
+
return '';
|
|
9006
|
+
}
|
|
9007
|
+
|
|
8467
9008
|
/** Flatten schema-shaped content items to runtime values for preview postMessage. */
|
|
8468
9009
|
function flattenContentItemsForPreview(items: any[]): any[] {
|
|
8469
9010
|
return items.map((item) => {
|
|
8470
9011
|
if (!item || typeof item !== 'object') return item;
|
|
8471
9012
|
const flat: Record<string, unknown> = { id: item.id, title: item.title };
|
|
8472
|
-
|
|
8473
|
-
|
|
9013
|
+
for (const [key, value] of Object.entries(item)) {
|
|
9014
|
+
if (key === 'properties' || key === 'inputController' || key === 'id' || key === 'title') continue;
|
|
9015
|
+
const unwrapped = unwrapContentMediaValue(value);
|
|
9016
|
+
if (unwrapped === undefined) continue;
|
|
9017
|
+
flat[key] = unwrapped;
|
|
9018
|
+
}
|
|
8474
9019
|
const props = item.properties;
|
|
8475
9020
|
if (Array.isArray(props)) {
|
|
8476
9021
|
for (const prop of props) {
|
|
8477
9022
|
const name = prop?.name || prop?.id;
|
|
8478
9023
|
if (!name) continue;
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
if (
|
|
8482
|
-
flat[name] = (raw as { url?: string }).url ?? '';
|
|
8483
|
-
} else {
|
|
8484
|
-
flat[name] = raw;
|
|
8485
|
-
}
|
|
9024
|
+
if (flat[name] !== undefined && flat[name] !== '') continue;
|
|
9025
|
+
const picked = pickLiveContentValue(prop.value, prop.defaultValue);
|
|
9026
|
+
if (picked !== '') flat[name] = picked;
|
|
8486
9027
|
}
|
|
8487
9028
|
}
|
|
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
9029
|
return flat;
|
|
8493
9030
|
});
|
|
8494
9031
|
}
|
|
8495
9032
|
|
|
9033
|
+
function contentItemsToEditorValueMap(items: any[]): Record<string, Record<string, unknown>> {
|
|
9034
|
+
const map: Record<string, Record<string, unknown>> = {};
|
|
9035
|
+
for (const item of flattenContentItemsForPreview(items || [])) {
|
|
9036
|
+
if (!item?.id) continue;
|
|
9037
|
+
map[String(item.id)] = item as Record<string, unknown>;
|
|
9038
|
+
}
|
|
9039
|
+
return map;
|
|
9040
|
+
}
|
|
9041
|
+
|
|
8496
9042
|
function contentItemHasProperties(item: any): boolean {
|
|
8497
9043
|
return Array.isArray(item?.properties) && item.properties.length > 0;
|
|
8498
9044
|
}
|
|
@@ -8500,22 +9046,28 @@ Requirements:
|
|
|
8500
9046
|
/** New items from the editor arrive flat ({id, caption, image}) — reattach schema fields so the form stays editable. */
|
|
8501
9047
|
function attachSchemaProperties(item: any, template: any): any {
|
|
8502
9048
|
if (!item || typeof item !== 'object') return item;
|
|
8503
|
-
|
|
8504
|
-
if (!contentItemHasProperties(
|
|
8505
|
-
const
|
|
8506
|
-
|
|
9049
|
+
const templateHasProps = contentItemHasProperties(template);
|
|
9050
|
+
if (!contentItemHasProperties(item) && !templateHasProps) return item;
|
|
9051
|
+
const sourceProperties = contentItemHasProperties(item)
|
|
9052
|
+
? item.properties
|
|
9053
|
+
: JSON.parse(JSON.stringify(template.properties));
|
|
9054
|
+
const nextProperties = sourceProperties.map((prop: any) => {
|
|
8507
9055
|
const key = prop.id || prop.name;
|
|
8508
|
-
const
|
|
9056
|
+
const hasOwn = key != null && Object.prototype.hasOwnProperty.call(item, key);
|
|
9057
|
+
const incoming = hasOwn ? item[key] : undefined;
|
|
9058
|
+
const picked = hasOwn
|
|
9059
|
+
? (unwrapContentMediaValue(incoming) ?? '')
|
|
9060
|
+
: pickLiveContentValue(prop.value, prop.defaultValue);
|
|
8509
9061
|
return {
|
|
8510
9062
|
...prop,
|
|
8511
|
-
value:
|
|
9063
|
+
value: picked
|
|
8512
9064
|
};
|
|
8513
9065
|
});
|
|
8514
9066
|
return {
|
|
8515
|
-
...
|
|
9067
|
+
...(templateHasProps ? JSON.parse(JSON.stringify(template)) : {}),
|
|
8516
9068
|
...item,
|
|
8517
9069
|
id: item.id,
|
|
8518
|
-
title: item.title ||
|
|
9070
|
+
title: item.title || template?.title,
|
|
8519
9071
|
properties: nextProperties
|
|
8520
9072
|
};
|
|
8521
9073
|
}
|
|
@@ -8764,8 +9316,26 @@ Requirements:
|
|
|
8764
9316
|
getLivePublishedCommitShaForPanel(widgetId)
|
|
8765
9317
|
?? getWidgetLastPublishCommitSha(widgetId);
|
|
8766
9318
|
const pendingPublish = !!ps.pendingPublishAfterCodegen;
|
|
9319
|
+
const unpublishedAhead = hasUnpublishedChangesForPanel(widgetId)
|
|
9320
|
+
|| !!(ps.lastCommitId && liveSha && !commitsMatch(ps.lastCommitId, liveSha));
|
|
9321
|
+
|
|
9322
|
+
if (pendingPublish || unpublishedAhead) {
|
|
9323
|
+
console.log('[Publish] Gate: unpublished HEAD — enable', widgetId.substring(0, 8), {
|
|
9324
|
+
pendingPublish,
|
|
9325
|
+
unpublishedAhead,
|
|
9326
|
+
headSha: headSha?.substring(0, 8),
|
|
9327
|
+
displayHead: ps.lastCommitId?.substring(0, 8),
|
|
9328
|
+
liveSha: liveSha?.substring(0, 8),
|
|
9329
|
+
});
|
|
9330
|
+
return {
|
|
9331
|
+
disabled: false,
|
|
9332
|
+
label: `Publish new v${nextVersion}`,
|
|
9333
|
+
reason: null,
|
|
9334
|
+
showDisabledHint: false,
|
|
9335
|
+
};
|
|
9336
|
+
}
|
|
8767
9337
|
|
|
8768
|
-
// Known up-to-date: HEAD matches live
|
|
9338
|
+
// Known up-to-date: HEAD matches live.
|
|
8769
9339
|
if (headSha && liveSha && commitsMatch(headSha, liveSha)) {
|
|
8770
9340
|
return {
|
|
8771
9341
|
disabled: true,
|
|
@@ -8775,10 +9345,8 @@ Requirements:
|
|
|
8775
9345
|
};
|
|
8776
9346
|
}
|
|
8777
9347
|
|
|
8778
|
-
// After a live version exists, enable vN+1
|
|
8779
|
-
|
|
8780
|
-
// live SHA is still hydrating (that looked like "always publish v2").
|
|
8781
|
-
if (pendingPublish || (headSha && liveSha && !commitsMatch(headSha, liveSha))) {
|
|
9348
|
+
// After a live version exists, enable vN+1 when HEAD is proven different.
|
|
9349
|
+
if (headSha && liveSha && !commitsMatch(headSha, liveSha)) {
|
|
8782
9350
|
return {
|
|
8783
9351
|
disabled: false,
|
|
8784
9352
|
label: `Publish new v${nextVersion}`,
|
|
@@ -8920,7 +9488,7 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
|
|
|
8920
9488
|
}
|
|
8921
9489
|
if (publishWidgetId) {
|
|
8922
9490
|
console.log('[Publish] Refreshing canvas widget screenshot after successful publish');
|
|
8923
|
-
setTimeout(() => handleRefreshScreenshot(publishWidgetId), 2000);
|
|
9491
|
+
setTimeout(() => handleRefreshScreenshot(publishWidgetId, { quiet: true }), 2000);
|
|
8924
9492
|
}
|
|
8925
9493
|
if (embedded && onPublishComplete && publishWidgetId) {
|
|
8926
9494
|
onPublishComplete({
|
|
@@ -9316,6 +9884,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9316
9884
|
hasValidToken = false;
|
|
9317
9885
|
inlinePanelView = null;
|
|
9318
9886
|
selectedWidget = null;
|
|
9887
|
+
if (typeof window !== 'undefined') {
|
|
9888
|
+
window.location.href = '/login';
|
|
9889
|
+
}
|
|
9319
9890
|
}
|
|
9320
9891
|
|
|
9321
9892
|
// decodeJWT imported from ./pageHelpers.ts
|
|
@@ -9332,11 +9903,12 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9332
9903
|
|
|
9333
9904
|
// if the decoded token has an expiration field, update the token status
|
|
9334
9905
|
if (decoded && decoded.exp) {
|
|
9335
|
-
// calculate the token time left
|
|
9336
9906
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
9337
|
-
|
|
9907
|
+
const expUnix = decoded.exp;
|
|
9908
|
+
tokenTimeLeft = expUnix - currentTime;
|
|
9338
9909
|
tokenExpired = tokenTimeLeft <= 0;
|
|
9339
|
-
|
|
9910
|
+
// API calls must follow the real JWT, never the sessionBannerTest clock.
|
|
9911
|
+
hasValidToken = decoded.exp - currentTime > 0;
|
|
9340
9912
|
|
|
9341
9913
|
// log the token expiration status
|
|
9342
9914
|
if (tokenExpired) {
|
|
@@ -9384,33 +9956,26 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9384
9956
|
// If there are active operations (generating, loading preview), defer logout
|
|
9385
9957
|
// and show a warning instead of abruptly cutting off the operation
|
|
9386
9958
|
function checkTokenExpiration() {
|
|
9959
|
+
updateTokenStatus();
|
|
9387
9960
|
if (currentUserToken) {
|
|
9388
|
-
if (
|
|
9389
|
-
// Check if there are active operations that would be disrupted by logout
|
|
9961
|
+
if (tokenExpired || tokenTimeLeft <= 0) {
|
|
9390
9962
|
const hasActiveOperations = isGeneratingCode || (showPreviewModal && previewLoading);
|
|
9391
9963
|
|
|
9392
9964
|
if (hasActiveOperations) {
|
|
9393
|
-
// Don't logout yet — defer until operations complete
|
|
9394
9965
|
pendingTokenExpiryLogout = true;
|
|
9395
|
-
|
|
9396
9966
|
if (!tokenExpiryWarningShown) {
|
|
9397
9967
|
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
|
-
});
|
|
9968
|
+
console.warn('WidgetCreator.checkTokenExpiration: Token expired but active operations in progress — keeping the workspace open');
|
|
9969
|
+
authStatus = 'Session expired — finish this operation, then sign in again to save.';
|
|
9404
9970
|
}
|
|
9405
9971
|
return false;
|
|
9406
9972
|
}
|
|
9407
9973
|
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
authStatus = 'Token expired. Please login again.';
|
|
9974
|
+
if (!tokenExpiryWarningShown) {
|
|
9975
|
+
tokenExpiryWarningShown = true;
|
|
9976
|
+
console.log('WidgetCreator.checkTokenExpiration: Token expired — waiting for site session handler');
|
|
9977
|
+
authStatus = 'Session expired.';
|
|
9978
|
+
}
|
|
9414
9979
|
return false;
|
|
9415
9980
|
}
|
|
9416
9981
|
}
|
|
@@ -9582,6 +10147,24 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9582
10147
|
});
|
|
9583
10148
|
return;
|
|
9584
10149
|
}
|
|
10150
|
+
const lockedPanelVisible = !!(
|
|
10151
|
+
detailsSessionWidgetId
|
|
10152
|
+
&& openWidgetPanelIds.includes(detailsSessionWidgetId)
|
|
10153
|
+
&& (getWidgetDetailsRef(detailsSessionWidgetId)?.getShowWidgetDetails?.() ?? false)
|
|
10154
|
+
);
|
|
10155
|
+
if (
|
|
10156
|
+
detailsSessionWidgetId
|
|
10157
|
+
&& detailsSessionWidgetId !== widgetId
|
|
10158
|
+
&& lockedPanelVisible
|
|
10159
|
+
) {
|
|
10160
|
+
console.log('[WidgetCreator] Skip widget-shape click — convert session locked to', detailsSessionWidgetId.substring(0, 8));
|
|
10161
|
+
return;
|
|
10162
|
+
}
|
|
10163
|
+
if (lockedPanelVisible && isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
|
|
10164
|
+
console.log('[WidgetCreator] Skip widget-shape click — convert/draft panel is open:', widgetId.substring(0, 8));
|
|
10165
|
+
return;
|
|
10166
|
+
}
|
|
10167
|
+
|
|
9585
10168
|
console.log('[WidgetCreator] Widget Image clicked, opening widget details panel:', widgetId);
|
|
9586
10169
|
|
|
9587
10170
|
// Explicit user intent — cancel any in-flight deep-link auto-open and allow this panel.
|
|
@@ -9736,7 +10319,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9736
10319
|
/**
|
|
9737
10320
|
* Called when a widget shape is about to be deleted from canvas.
|
|
9738
10321
|
* Step 1: Confirm removing the shape from canvas.
|
|
9739
|
-
* Step 2: Ask if user also wants to delete the widget from DB.
|
|
10322
|
+
* Step 2 (ASK_DELETE_FROM_DB_CONFIRM): Ask if user also wants to delete the widget from DB.
|
|
10323
|
+
* MVP: first confirm also deletes the associated widget from the database.
|
|
9740
10324
|
* Returns true to proceed with canvas shape deletion, false to cancel.
|
|
9741
10325
|
*/
|
|
9742
10326
|
async function handleWidgetShapeDeleting(widgetId: string): Promise<boolean> {
|
|
@@ -9749,7 +10333,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9749
10333
|
|
|
9750
10334
|
if (!result.proceed) return false;
|
|
9751
10335
|
|
|
9752
|
-
|
|
10336
|
+
if (result.deleteFromDb) {
|
|
10337
|
+
await deleteWidgetFromDb(widgetId);
|
|
10338
|
+
} else {
|
|
10339
|
+
showToast('info', 'Shape removed from canvas', { description: `Widget "${widgetName}" kept in database` });
|
|
10340
|
+
}
|
|
9753
10341
|
return true;
|
|
9754
10342
|
}
|
|
9755
10343
|
|
|
@@ -9823,10 +10411,12 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9823
10411
|
canvasPrimaryWidgetId ||
|
|
9824
10412
|
canvasRef.getPrimaryWidgetId?.() ||
|
|
9825
10413
|
selectedWidgetId;
|
|
9826
|
-
if (primaryId && canvasRef) {
|
|
10414
|
+
if (primaryId && canvasRef && !isConvertOrDraftPanelOpen()) {
|
|
9827
10415
|
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
9828
10416
|
const found = canvasRef.selectWidgetShape(primaryId);
|
|
9829
10417
|
console.log('[CreatorApp] Primary widget shape after canvas load:', primaryId, found);
|
|
10418
|
+
} else if (isConvertOrDraftPanelOpen()) {
|
|
10419
|
+
console.log('[CreatorApp] Skip primary shape select — convert/draft panel is open');
|
|
9830
10420
|
}
|
|
9831
10421
|
}
|
|
9832
10422
|
|
|
@@ -10173,6 +10763,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10173
10763
|
: `ConvertedW-${Date.now()}`;
|
|
10174
10764
|
draftConvertScreenshots.set(draftId, payload.screenshot);
|
|
10175
10765
|
draftConvertObjectIds.set(draftId, payload.frameId);
|
|
10766
|
+
// Grouping deselects the sketch; that must not close the draft or open the primary widget.
|
|
10767
|
+
suppressWidgetDetailsClose = true;
|
|
10176
10768
|
|
|
10177
10769
|
// Group the sketch into one editable object and keep it on canvas — the
|
|
10178
10770
|
// user may keep editing it (ungroup, move, restyle) and re-convert later.
|
|
@@ -10200,6 +10792,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10200
10792
|
// Refresh the stored screenshot so finalize uses the latest sketch look
|
|
10201
10793
|
draftConvertScreenshots.set(existingDraftId, payload.screenshot);
|
|
10202
10794
|
authStatus = 'Widget draft ready — send the prompt to generate.';
|
|
10795
|
+
setTimeout(() => {
|
|
10796
|
+
if (!finalizingDraftConversions.size) {
|
|
10797
|
+
suppressWidgetDetailsClose = false;
|
|
10798
|
+
}
|
|
10799
|
+
}, 1500);
|
|
10203
10800
|
return;
|
|
10204
10801
|
}
|
|
10205
10802
|
|
|
@@ -10210,6 +10807,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10210
10807
|
// Draft panel snapshot (no widget record in DB). WidgetDetails is
|
|
10211
10808
|
// null-safe for a missing widget — the panel shows the draft name.
|
|
10212
10809
|
await openWidgetDetailsPanel(draftId);
|
|
10810
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
10213
10811
|
patchPanelSnapshot(draftId, {
|
|
10214
10812
|
codeGenerationPrompt: CONVERT_PROMPT,
|
|
10215
10813
|
promptImages: [payload.screenshot],
|
|
@@ -10251,6 +10849,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10251
10849
|
showToast('error', 'Failed to prepare widget draft');
|
|
10252
10850
|
} finally {
|
|
10253
10851
|
isWidgetOperationInProgress = false;
|
|
10852
|
+
setTimeout(() => {
|
|
10853
|
+
if (!finalizingDraftConversions.size) {
|
|
10854
|
+
suppressWidgetDetailsClose = false;
|
|
10855
|
+
}
|
|
10856
|
+
}, 1500);
|
|
10254
10857
|
}
|
|
10255
10858
|
}
|
|
10256
10859
|
|
|
@@ -10364,7 +10967,10 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10364
10967
|
// panel remounts: the freshly mounted Chat then activates this
|
|
10365
10968
|
// conversation (best-conversation = one with messages) instead of
|
|
10366
10969
|
// starting empty after a close/reopen.
|
|
10367
|
-
await ensureCodegenUserMessagePersisted(widgetId, prompt
|
|
10970
|
+
await ensureCodegenUserMessagePersisted(widgetId, prompt, [
|
|
10971
|
+
...(draftPanelState.promptImages ?? []),
|
|
10972
|
+
screenshot,
|
|
10973
|
+
].filter(Boolean));
|
|
10368
10974
|
// NOTE: never copy currentChatConversationId from the draft — the draft
|
|
10369
10975
|
// chat's conversation is a local UUID that doesn't exist in the DB, and
|
|
10370
10976
|
// migrating it would point codegen/summaries at a phantom conversation.
|
|
@@ -10372,6 +10978,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10372
10978
|
draftPanelState as Record<string, unknown>;
|
|
10373
10979
|
patchPanelSnapshot(widgetId, {
|
|
10374
10980
|
...draftPanelStateClean,
|
|
10981
|
+
lastPublishedVersion: null,
|
|
10982
|
+
lastCommitId: null,
|
|
10983
|
+
hasGeneratedCode: false,
|
|
10984
|
+
publishHistory: [],
|
|
10985
|
+
widgetDetailsStep: 'create',
|
|
10375
10986
|
} as Partial<WidgetPanelSnapshot>);
|
|
10376
10987
|
migrateOpenPanelWidgetId(draftId, widgetId);
|
|
10377
10988
|
widgetPanelSnapshots = Object.fromEntries(
|
|
@@ -10387,8 +10998,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10387
10998
|
await tick();
|
|
10388
10999
|
const migratedRef = getWidgetDetailsRef(widgetId);
|
|
10389
11000
|
if (migratedRef?.getShowWidgetDetails?.()) {
|
|
10390
|
-
console.log('[CreatorApp] Draft→widget: Widget Details stayed open
|
|
10391
|
-
positionOpenDetailsPanel(widgetId);
|
|
11001
|
+
console.log('[CreatorApp] Draft→widget: Widget Details stayed open — keep current position');
|
|
10392
11002
|
} else {
|
|
10393
11003
|
await openWidgetDetailsPanel(widgetId);
|
|
10394
11004
|
}
|
|
@@ -10425,6 +11035,16 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10425
11035
|
return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
|
|
10426
11036
|
}
|
|
10427
11037
|
|
|
11038
|
+
function isConvertOrDraftPanelOpen(): boolean {
|
|
11039
|
+
return (
|
|
11040
|
+
isDraftConvertId(focusedPanelWidgetId)
|
|
11041
|
+
|| isDraftConvertId(widgetDetailsOpenTargetId)
|
|
11042
|
+
|| isDraftConvertId(selectedWidgetId)
|
|
11043
|
+
|| openWidgetPanelIds.some((id) => isDraftConvertId(id))
|
|
11044
|
+
|| (!!detailsSessionWidgetId && openWidgetPanelIds.includes(detailsSessionWidgetId))
|
|
11045
|
+
);
|
|
11046
|
+
}
|
|
11047
|
+
|
|
10428
11048
|
|
|
10429
11049
|
/**
|
|
10430
11050
|
* Creates or recreates a repository for a specific widget.
|
|
@@ -10525,6 +11145,30 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10525
11145
|
} catch (error) {
|
|
10526
11146
|
if (error instanceof ResponseError) {
|
|
10527
11147
|
const status = error.response?.status;
|
|
11148
|
+
if (status === 401) {
|
|
11149
|
+
console.warn('[Auth] Repository verify got 401 — rebinding session and retrying once', {
|
|
11150
|
+
repoId
|
|
11151
|
+
});
|
|
11152
|
+
applySiteSessionToCreator();
|
|
11153
|
+
try {
|
|
11154
|
+
const retryDetails = await agentClient.agentRepositoriesRepoIdGet({ repoId });
|
|
11155
|
+
if (retryDetails?.id) return true;
|
|
11156
|
+
} catch (retryError) {
|
|
11157
|
+
if (retryError instanceof ResponseError && retryError.response?.status === 401) {
|
|
11158
|
+
if (!silent) {
|
|
11159
|
+
showToast('warning', 'Session expired', {
|
|
11160
|
+
description: 'Sign in again, then retry. The widget repository is still linked.',
|
|
11161
|
+
duration: 6000
|
|
11162
|
+
});
|
|
11163
|
+
}
|
|
11164
|
+
if (typeof window !== 'undefined') {
|
|
11165
|
+
window.dispatchEvent(new CustomEvent('widgetic:session-needs-reauth'));
|
|
11166
|
+
}
|
|
11167
|
+
throw retryError;
|
|
11168
|
+
}
|
|
11169
|
+
}
|
|
11170
|
+
return false;
|
|
11171
|
+
}
|
|
10528
11172
|
if (status === 404 || status === 403) {
|
|
10529
11173
|
console.warn('Repository reference is stale or forbidden; it will be recreated.', {
|
|
10530
11174
|
repoId,
|
|
@@ -10873,6 +11517,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10873
11517
|
</div>
|
|
10874
11518
|
{/if}
|
|
10875
11519
|
|
|
11520
|
+
|
|
10876
11521
|
<!-- Widgetic Canvas Editor - Full width background (lazy-loaded) -->
|
|
10877
11522
|
<div class="wc-canvas-area flex-1 min-h-0 relative">
|
|
10878
11523
|
<!-- Embedded: Debug toggle lives in site AppHeader (avoids overlapping Props Panel). -->
|
|
@@ -10903,6 +11548,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10903
11548
|
bind:this={canvasRef}
|
|
10904
11549
|
showContainerFrame={!embedded}
|
|
10905
11550
|
bind:wireframeMode={canvasWireframeMode}
|
|
11551
|
+
advancedFeatures={canvasAdvancedFeatures}
|
|
10906
11552
|
onReady={handleCanvasReady}
|
|
10907
11553
|
onAutoSave={handleCanvasAutoSave}
|
|
10908
11554
|
onBeforeUnmount={(json) => {
|
|
@@ -10922,8 +11568,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10922
11568
|
getWidgetName={getWidgetNameById}
|
|
10923
11569
|
isWidgetKnown={isWidgetKnownById}
|
|
10924
11570
|
onNonWidgetSelected={() => {
|
|
10925
|
-
if (suppressWidgetDetailsClose) {
|
|
10926
|
-
console.log('[CreatorApp] Skip Widget Details close —
|
|
11571
|
+
if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
|
|
11572
|
+
console.log('[CreatorApp] Skip Widget Details close — convert/draft panel is open');
|
|
10927
11573
|
return;
|
|
10928
11574
|
}
|
|
10929
11575
|
const active = canvasRef?.getSelectedObject?.() ?? canvasRef?.selectedObject;
|
|
@@ -11259,7 +11905,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11259
11905
|
{isPublishing}
|
|
11260
11906
|
{publishWidgetStatus}
|
|
11261
11907
|
publishNeedsSaveFirst={publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
11262
|
-
lastPublishedVersion={ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null)}
|
|
11908
|
+
lastPublishedVersion={isDraftConvertId(panelWidgetId) ? null : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
|
|
11263
11909
|
widgetJsPath={ps.selectedWidgetJsPath ?? panelWidget?.jsPath ?? null}
|
|
11264
11910
|
{isGeneratingInOtherTab}
|
|
11265
11911
|
{isPublishingInOtherTab}
|
|
@@ -11335,6 +11981,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11335
11981
|
}
|
|
11336
11982
|
// Keep canvas WidgetShape thumbnail aligned with the live Worker preview.
|
|
11337
11983
|
scheduleCanvasShapePreviewSync(previewWidgetId);
|
|
11984
|
+
if (detail?.step === 'create' || ps.widgetDetailsStep === 'create') {
|
|
11985
|
+
applyPanelCompositionToPreview(previewWidgetId);
|
|
11986
|
+
}
|
|
11338
11987
|
}}
|
|
11339
11988
|
on:widgetReady={({ detail }) => {
|
|
11340
11989
|
if (detail.widgetId !== panelWidgetId) return;
|
|
@@ -11485,6 +12134,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11485
12134
|
console.log('[WidgetCreator] Generate Now clicked for widget:', detail.widgetId);
|
|
11486
12135
|
const widgetId = detail.widgetId;
|
|
11487
12136
|
const ps = getPanelDisplayState(widgetId);
|
|
12137
|
+
if (ps.lastGenerationFailed) {
|
|
12138
|
+
void handleChatRetryMessage(
|
|
12139
|
+
lastGenerationPrompt || ps.codeGenerationPrompt,
|
|
12140
|
+
widgetId,
|
|
12141
|
+
);
|
|
12142
|
+
return;
|
|
12143
|
+
}
|
|
11488
12144
|
const hasCode = !!(ps.lastCommitId || ps.lastPublishedVersion !== null);
|
|
11489
12145
|
const compileErr = getPanelPreviewCompileError(widgetId);
|
|
11490
12146
|
if (hasCode && compileErr) {
|
|
@@ -11501,10 +12157,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11501
12157
|
console.log('[CreatorApp] Ignoring Widget Details close during draft→widget migrate');
|
|
11502
12158
|
return;
|
|
11503
12159
|
}
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
}
|
|
12160
|
+
// After convert, the panel id changes but the X is still the visible
|
|
12161
|
+
// panel — always close it. Stale-target ignore left the child hidden
|
|
12162
|
+
// while openWidgetPanelIds stayed set, so nothing would reopen.
|
|
11508
12163
|
if (isPanelFocused && selectedWidgetId) {
|
|
11509
12164
|
savePromptForWidget(selectedWidgetId);
|
|
11510
12165
|
captureFocusedPanelSnapshot();
|
|
@@ -11534,8 +12189,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11534
12189
|
>
|
|
11535
12190
|
<svelte:fragment slot="code-generation">
|
|
11536
12191
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
12192
|
+
{@const chatUserHeight = (panelDisplayRevision, Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, (ps.chatContainerHeight > 0 ? ps.chatContainerHeight : CHAT_DEFAULT_HEIGHT))))}
|
|
11537
12193
|
<div
|
|
11538
|
-
class="parallel-panel-left-slot-ct flex flex-col
|
|
12194
|
+
class="parallel-panel-left-slot-ct flex flex-col shrink-0 overflow-visible"
|
|
11539
12195
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
11540
12196
|
>
|
|
11541
12197
|
<!-- Sticky header: title + model selectors (debug mode).
|
|
@@ -11582,11 +12238,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11582
12238
|
</div>
|
|
11583
12239
|
{/if}
|
|
11584
12240
|
|
|
11585
|
-
<div class="generation-scroll-ct flex flex-col
|
|
12241
|
+
<div class="generation-scroll-ct flex flex-col shrink-0 overflow-visible">
|
|
11586
12242
|
<!-- Chat-based Code Generation Panel -->
|
|
11587
12243
|
{#key panelInstanceKey}
|
|
11588
|
-
<Chat class="code-generation-chat
|
|
11589
|
-
style="min-height: {
|
|
12244
|
+
<Chat class="code-generation-chat w-full shrink-0"
|
|
12245
|
+
style="height: {chatUserHeight}px; min-height: {CHAT_MIN_HEIGHT}px; flex: 0 0 {chatUserHeight}px;"
|
|
11590
12246
|
resizeExpandsContainer={true}
|
|
11591
12247
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
11592
12248
|
showLogs={debugMode}
|
|
@@ -11616,11 +12272,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11616
12272
|
onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
|
|
11617
12273
|
onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
|
|
11618
12274
|
onUploadChatFile={handleChatUploadFile}
|
|
11619
|
-
onTakeScreenshot={handleChatScreenshot}
|
|
12275
|
+
onTakeScreenshot={() => handleChatScreenshot(panelWidgetId)}
|
|
11620
12276
|
onLoadConversations={handleLoadConversations}
|
|
11621
12277
|
onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
|
|
11622
12278
|
onCreateConversation={handleCreateConversation}
|
|
11623
|
-
onSaveMessage={
|
|
12279
|
+
onSaveMessage={(conversationId, content, messageType, attachments) =>
|
|
12280
|
+
handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
|
|
12281
|
+
}
|
|
11624
12282
|
onRestoreCheckpointBackend={handleRestoreCheckpoint}
|
|
11625
12283
|
onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
|
|
11626
12284
|
onDraftChange={() => handleChatDraftChange(panelWidgetId)}
|
|
@@ -11695,37 +12353,34 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
11695
12353
|
</div>
|
|
11696
12354
|
{/if}
|
|
11697
12355
|
</div>
|
|
12356
|
+
</div> <!-- generation-scroll-ct -->
|
|
12357
|
+
</div> <!-- parallel-panel-left-slot-ct -->
|
|
12358
|
+
</svelte:fragment>
|
|
12359
|
+
|
|
12360
|
+
<svelte:fragment slot="publish-widget">
|
|
11698
12361
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
11699
12362
|
<div
|
|
11700
|
-
class="parallel-panel-publish-slot-ct shrink-0
|
|
12363
|
+
class="parallel-panel-publish-slot-ct shrink-0"
|
|
11701
12364
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
11702
12365
|
>
|
|
11703
12366
|
<!-- ===== Publish Widget Section ===== -->
|
|
11704
|
-
<div class="publish-widget-section flex flex-col gap-
|
|
11705
|
-
<!-- Section Header -->
|
|
12367
|
+
<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
12368
|
<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 -->
|
|
12369
|
+
<span class="text-xs font-semibold text-emerald-800">🚀 Publish</span>
|
|
11709
12370
|
{#if ps.lastPublishedVersion !== null}
|
|
11710
|
-
<span class="text-
|
|
12371
|
+
<span class="text-[10px] font-mono bg-emerald-700 text-white px-1.5 py-0.5 rounded">
|
|
11711
12372
|
live: v{ps.lastPublishedVersion}
|
|
11712
12373
|
</span>
|
|
11713
12374
|
{/if}
|
|
11714
12375
|
</div>
|
|
11715
12376
|
|
|
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 -->
|
|
12377
|
+
<div class="publish-widget-section-actions flex flex-col gap-1 items-center">
|
|
11724
12378
|
<Button
|
|
11725
12379
|
onclick={() => runPublishForPanel(panelWidgetId)}
|
|
11726
|
-
disabled={isPublishDisabledForPanel(panelWidgetId)}
|
|
12380
|
+
disabled={(panelDisplayRevision, isPublishDisabledForPanel(panelWidgetId))}
|
|
11727
12381
|
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? undefined}
|
|
11728
|
-
|
|
12382
|
+
size="sm"
|
|
12383
|
+
class={`publish-widget-btn flex justify-center items-center border text-white w-full h-8 text-xs
|
|
11729
12384
|
${isPublishing && publishingWidgetId === panelWidgetId
|
|
11730
12385
|
? 'bg-emerald-500 border-emerald-500 opacity-80 cursor-not-allowed'
|
|
11731
12386
|
: isPublishDisabledForPanel(panelWidgetId)
|
|
@@ -11740,7 +12395,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11740
12395
|
<span class="mr-1 inline-block">⏳</span>
|
|
11741
12396
|
<span>Publishing...</span>
|
|
11742
12397
|
{:else}
|
|
11743
|
-
<span>{getPublishButtonLabelForPanel(panelWidgetId)}</span>
|
|
12398
|
+
<span>{(panelDisplayRevision, getPublishButtonLabelForPanel(panelWidgetId))}</span>
|
|
11744
12399
|
{#if isPublishingInOtherTab}
|
|
11745
12400
|
<!-- svelte-ignore a11y-missing-attribute -->
|
|
11746
12401
|
<span class="text-xs opacity-70 ml-1" title="Publish is running in another tab">⏳</span>
|
|
@@ -11754,24 +12409,23 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11754
12409
|
</Button>
|
|
11755
12410
|
|
|
11756
12411
|
{#if isPublishDisabledForPanel(panelWidgetId) && shouldShowPublishDisabledHintForPanel(panelWidgetId)}
|
|
11757
|
-
<p class="publish-widget-disabled-hint text-
|
|
12412
|
+
<p class="publish-widget-disabled-hint text-[10px] text-center text-emerald-800/80 leading-snug px-1">
|
|
11758
12413
|
{getPublishDisabledReasonForPanel(panelWidgetId)}
|
|
11759
12414
|
</p>
|
|
11760
12415
|
{/if}
|
|
11761
12416
|
|
|
11762
12417
|
{#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
|
|
12418
|
+
<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">
|
|
12419
|
+
Open published v{ps.lastPublishedVersion} ↗
|
|
11765
12420
|
</a>
|
|
11766
12421
|
{/if}
|
|
11767
12422
|
|
|
11768
|
-
<!-- Save-first hint — shown when changes aren't saved yet -->
|
|
11769
12423
|
{#if publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
11770
|
-
<p class="text-
|
|
12424
|
+
<p class="text-[10px] text-center text-amber-700">
|
|
11771
12425
|
{#if !resolvePublishHeadCommitId(panelWidgetId, ps)}
|
|
11772
|
-
|
|
12426
|
+
Save the widget before publishing.
|
|
11773
12427
|
{:else}
|
|
11774
|
-
|
|
12428
|
+
Save your changes before publishing.
|
|
11775
12429
|
{/if}
|
|
11776
12430
|
</p>
|
|
11777
12431
|
{/if}
|
|
@@ -11784,7 +12438,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11784
12438
|
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
11785
12439
|
togglePublishHistory();
|
|
11786
12440
|
}}
|
|
11787
|
-
class="publish-history-toggle flex items-center justify-between w-full text-
|
|
12441
|
+
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
12442
|
>
|
|
11789
12443
|
<span class="font-medium">📋 Version History</span>
|
|
11790
12444
|
<span class="text-emerald-400">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
|
|
@@ -11842,53 +12496,41 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11842
12496
|
|
|
11843
12497
|
</div> <!-- end of publish-widget-section -->
|
|
11844
12498
|
</div>
|
|
11845
|
-
</div> <!-- generation-scroll-ct -->
|
|
11846
|
-
</div> <!-- parallel-panel-left-slot-ct -->
|
|
11847
12499
|
</svelte:fragment>
|
|
11848
12500
|
|
|
11849
|
-
<svelte:fragment slot="publish-widget"></svelte:fragment>
|
|
11850
|
-
|
|
11851
12501
|
<svelte:fragment slot="composition-editor">
|
|
11852
12502
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
11853
12503
|
<div
|
|
11854
|
-
class="parallel-panel-composition-slot-ct flex flex-col h-full min-h-0"
|
|
12504
|
+
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
12505
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
11856
12506
|
>
|
|
11857
12507
|
<!-- ===== Composition Editor (Step 2: Edit) ===== -->
|
|
11858
12508
|
{#if ps.lastCommitId || ps.lastPublishedVersion !== null}
|
|
11859
|
-
<div class="composition-editor-section flex flex-col
|
|
12509
|
+
<div class="composition-editor-section flex flex-col flex-1 min-h-0 h-full">
|
|
11860
12510
|
|
|
11861
12511
|
{#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
|
|
12512
|
+
<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">
|
|
12513
|
+
<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)}.">
|
|
12514
|
+
<strong class="font-semibold">Newer than Live v{ps.lastPublishedVersion ?? '—'}</strong>
|
|
11866
12515
|
<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.
|
|
12516
|
+
ahead of
|
|
12517
|
+
<span class="font-mono">{formatCommitShaShort(getLivePublishedCommitShaForPanel(panelWidgetId))}</span>
|
|
11871
12518
|
</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>
|
|
12519
|
+
<Button
|
|
12520
|
+
onclick={() => runPublishForPanel(panelWidgetId)}
|
|
12521
|
+
disabled={isPublishDisabledForPanel(panelWidgetId)}
|
|
12522
|
+
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? undefined}
|
|
12523
|
+
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
|
|
12524
|
+
{isPublishDisabledForPanel(panelWidgetId)
|
|
12525
|
+
? 'bg-emerald-600/40 border-emerald-600/40 text-white cursor-not-allowed'
|
|
12526
|
+
: 'bg-emerald-600 border-emerald-600 text-white hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'}"
|
|
12527
|
+
>
|
|
12528
|
+
{#if isPublishing && publishingWidgetId === panelWidgetId}
|
|
12529
|
+
<span>⏳</span>
|
|
12530
|
+
{:else}
|
|
12531
|
+
<span>🚀 Publish</span>
|
|
11890
12532
|
{/if}
|
|
11891
|
-
</
|
|
12533
|
+
</Button>
|
|
11892
12534
|
</div>
|
|
11893
12535
|
{/if}
|
|
11894
12536
|
|
|
@@ -12050,14 +12692,17 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
12050
12692
|
</button>
|
|
12051
12693
|
</div>
|
|
12052
12694
|
{:else if ps.compositionDesignSchema || ps.compositionContentSchema}
|
|
12053
|
-
<div class="composition-editor-props-ct flex flex-1 min-h-
|
|
12695
|
+
<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
12696
|
{#key `${panelWidgetId}-${ps.selectedComposition?.id ?? 'new'}`}
|
|
12055
12697
|
<PropsEditor
|
|
12056
12698
|
class="composition-props-editor flex-1 min-h-0 min-w-0"
|
|
12057
12699
|
designSchema={ps.compositionDesignSchema}
|
|
12058
12700
|
contentSchema={buildPropsEditorContentSchema(ps.compositionContentSchema, ps.liveContentItems)}
|
|
12059
12701
|
initialDesignValues={ps.liveDesignValues}
|
|
12060
|
-
initialContentValues={
|
|
12702
|
+
initialContentValues={{
|
|
12703
|
+
...ps.liveContentValues,
|
|
12704
|
+
items: contentItemsToEditorValueMap(ps.liveContentItems)
|
|
12705
|
+
}}
|
|
12061
12706
|
width="100%"
|
|
12062
12707
|
height="100%"
|
|
12063
12708
|
showDragHandle={false}
|
|
@@ -12336,7 +12981,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
12336
12981
|
<Dialog.Title>Delete Widget Shape</Dialog.Title>
|
|
12337
12982
|
<Dialog.Description>
|
|
12338
12983
|
{#if deleteDialogStep === 'confirm-remove'}
|
|
12339
|
-
Remove <strong>"{deleteDialogWidgetName}"</strong>
|
|
12984
|
+
Remove <strong>"{deleteDialogWidgetName}"</strong> from the canvas? This also deletes the associated widget.
|
|
12340
12985
|
{:else}
|
|
12341
12986
|
Also delete the widget <strong>"{deleteDialogWidgetName}"</strong> from the database?
|
|
12342
12987
|
{/if}
|
|
@@ -12354,8 +12999,8 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
12354
12999
|
type="button"
|
|
12355
13000
|
class="px-4 py-2 text-sm rounded-lg bg-red-600 hover:bg-red-700 text-white cursor-pointer"
|
|
12356
13001
|
onclick={() => handleDeleteDialogResponse('remove-and-ask-db')}
|
|
12357
|
-
>
|
|
12358
|
-
{:else}
|
|
13002
|
+
>Delete</button>
|
|
13003
|
+
{:else if ASK_DELETE_FROM_DB_CONFIRM}
|
|
12359
13004
|
<button
|
|
12360
13005
|
type="button"
|
|
12361
13006
|
class="px-4 py-2 text-sm rounded-lg border border-gray-300 hover:bg-gray-100 cursor-pointer"
|