@widgetic/creator 0.3.49 → 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 +1876 -518
- package/dist/CreatorApp.svelte.d.ts +1 -0
- package/dist/components/EditableName.svelte +2 -2
- package/dist/components/WidgetDetails.svelte +431 -71
- package/dist/components/WidgetDetails.svelte.d.ts +22 -1
- package/dist/creator-types.d.ts +6 -0
- package/dist/utils/operationStream.js +20 -1
- package/package.json +15 -15
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);
|
|
@@ -110,10 +116,35 @@
|
|
|
110
116
|
onWidgetRenamed?.({ widgetId: widget.id, name });
|
|
111
117
|
}
|
|
112
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Rewrite the embedded URL so a refresh resumes the current canvas instead of
|
|
121
|
+
* re-running the new-widget bootstrap (which creates a blank canvas every time).
|
|
122
|
+
* Uses history.replaceState — no navigation, no host remount.
|
|
123
|
+
*/
|
|
124
|
+
function syncEmbeddedCanvasUrl(canvasIdToSync: string): void {
|
|
125
|
+
if (!embedded || !canvasIdToSync) return;
|
|
126
|
+
try {
|
|
127
|
+
const url = new URL(window.location.href);
|
|
128
|
+
url.searchParams.set('canvas_id', canvasIdToSync);
|
|
129
|
+
url.searchParams.delete('new_widget');
|
|
130
|
+
window.history.replaceState(window.history.state, '', url.toString());
|
|
131
|
+
console.log('[CreatorApp] URL synced to canvas:', canvasIdToSync);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.warn('[CreatorApp] Failed to sync canvas URL:', err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
const HOST_WIDGET_RENAMED_EVENT = 'widgetic:host-widget-renamed';
|
|
114
138
|
const HOST_REQUEST_PUBLISH_EVENT = 'widgetic:host-request-publish';
|
|
115
139
|
const HOST_TOGGLE_DEBUG_EVENT = 'widgetic:host-toggle-debug';
|
|
140
|
+
const HOST_TOGGLE_WIREFRAME_EVENT = 'widgetic:host-toggle-wireframe';
|
|
141
|
+
const HOST_TOGGLE_ADVANCED_EVENT = 'widgetic:host-toggle-advanced';
|
|
142
|
+
const HOST_OPEN_WIDGET_DETAILS_EVENT = 'widgetic:host-open-widget-details';
|
|
143
|
+
const HOST_DUPLICATE_WIDGET_EVENT = 'widgetic:host-duplicate-widget';
|
|
144
|
+
const HOST_DELETE_WIDGET_EVENT = 'widgetic:host-delete-widget';
|
|
116
145
|
const CREATOR_DEBUG_CHANGED_EVENT = 'widgetic:creator-debug-changed';
|
|
146
|
+
const CREATOR_WIREFRAME_CHANGED_EVENT = 'widgetic:creator-wireframe-changed';
|
|
147
|
+
const CREATOR_ADVANCED_CHANGED_EVENT = 'widgetic:creator-advanced-changed';
|
|
117
148
|
|
|
118
149
|
/** Site AppHeader rename — embed is a second Svelte runtime, so mount props do not update. */
|
|
119
150
|
async function applyHostWidgetName(widgetId: string | null | undefined, newName: string): Promise<void> {
|
|
@@ -293,9 +324,41 @@
|
|
|
293
324
|
: new UploadsApi({ accessToken: token, middleware: [idempotencyKeyMiddleware] });
|
|
294
325
|
|
|
295
326
|
authenticatedClientsToken = token;
|
|
327
|
+
try {
|
|
328
|
+
setWsAuthToken(token);
|
|
329
|
+
} catch {
|
|
330
|
+
/* websocket store may not be ready yet */
|
|
331
|
+
}
|
|
296
332
|
syncFileBrowserUploadConfigForCreator();
|
|
297
333
|
}
|
|
298
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
|
+
|
|
299
362
|
function syncFileBrowserUploadConfigForCreator() {
|
|
300
363
|
if (!authenticatedClientsToken) {
|
|
301
364
|
setFileBrowserUploadConfig(null);
|
|
@@ -347,7 +410,14 @@
|
|
|
347
410
|
let WidgetDetails: any;
|
|
348
411
|
let widgetDetailsRefs: Record<string, any> = {};
|
|
349
412
|
let openWidgetPanelIds: string[] = [];
|
|
413
|
+
/** Stable {#each} keys so draft → real widget does not remount Widget Details. */
|
|
414
|
+
let panelInstanceKeyByWidgetId: Record<string, string> = {};
|
|
350
415
|
let focusedPanelWidgetId: string | null = null;
|
|
416
|
+
/** Ignore WidgetDetails close events while swapping draft → real widget id. */
|
|
417
|
+
let suppressWidgetDetailsClose = false;
|
|
418
|
+
/** Latest user-requested details panel — delayed screenshot/deep-link must not reopen another widget. */
|
|
419
|
+
let widgetDetailsOpenGen = 0;
|
|
420
|
+
let widgetDetailsOpenTargetId: string | null = null;
|
|
351
421
|
|
|
352
422
|
// Lightweight static imports from canvas (excludes Canvas.svelte — 10K+ lines)
|
|
353
423
|
import { Tooltip } from '@widgetic/canvas/components';
|
|
@@ -487,6 +557,8 @@
|
|
|
487
557
|
let deleteDialogWidgetId = '';
|
|
488
558
|
let deleteDialogStep: 'confirm-remove' | 'confirm-delete-db' = 'confirm-remove';
|
|
489
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;
|
|
490
562
|
|
|
491
563
|
function showDeleteWidgetDialog(widgetId: string, widgetName: string): Promise<{ proceed: boolean; deleteFromDb: boolean }> {
|
|
492
564
|
return new Promise((resolve) => {
|
|
@@ -505,10 +577,15 @@
|
|
|
505
577
|
if (deleteDialogResolve) deleteDialogResolve({ proceed: false, deleteFromDb: false });
|
|
506
578
|
break;
|
|
507
579
|
case 'remove-and-ask-db':
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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;
|
|
512
589
|
case 'keep-in-db':
|
|
513
590
|
deleteDialogOpen = false;
|
|
514
591
|
showToast('info', 'Shape removed', { description: 'Widget kept in database' });
|
|
@@ -600,6 +677,8 @@
|
|
|
600
677
|
const DEFAULT_PROMPT = 'Create a simple widget.';
|
|
601
678
|
const IMAGE_WIDGET_PROMPT =
|
|
602
679
|
'Create a widget from the attached image (short description inside).';
|
|
680
|
+
/** Prefilled prompt for Convert-to-Widget — user edits or presses Send to start. */
|
|
681
|
+
const CONVERT_PROMPT = 'Create a widget starting from the attached image';
|
|
603
682
|
let codeGenerationPrompt = DEFAULT_PROMPT;
|
|
604
683
|
let isGeneratingCode = false;
|
|
605
684
|
let generatingWidgetIds: Record<string, boolean> = {};
|
|
@@ -622,6 +701,18 @@
|
|
|
622
701
|
/** Widget targeted by the open confirm-generate dialog (may differ from focused panel). */
|
|
623
702
|
let confirmGenerateWidgetId: string | null = null;
|
|
624
703
|
|
|
704
|
+
// ─── Convert UX v2: draft panel state (no widget record until first Send) ────
|
|
705
|
+
/** Synthetic id for the draft convert panel — NOT a widget id in the DB. */
|
|
706
|
+
const DRAFT_CONVERT_ID_PREFIX = 'draft_convert_';
|
|
707
|
+
/** Canvas object ids (group or selection id) captured at convert time, per draft id. */
|
|
708
|
+
const draftConvertObjectIds = new Map<string, string>();
|
|
709
|
+
/** Captured sketch screenshot (data URL) per draft id — reused on first Send. */
|
|
710
|
+
const draftConvertScreenshots = new Map<string, string>();
|
|
711
|
+
/** True while a draft conversion is being finalized (widget creation + shape replace). */
|
|
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;
|
|
715
|
+
|
|
625
716
|
// Chat component reference and context (focused panel)
|
|
626
717
|
let chatMethods: {
|
|
627
718
|
sendMessage: (content: string, attachments?: File[]) => Promise<void>;
|
|
@@ -639,10 +730,19 @@
|
|
|
639
730
|
markWidgetHeadAtLatestCommit?: () => void;
|
|
640
731
|
updateLatestCodegenStatusMessage?: (content: string) => void;
|
|
641
732
|
reloadConversationMessages?: () => Promise<void>;
|
|
733
|
+
selectConversation?: (conversationId: string) => void | Promise<void>;
|
|
734
|
+
updateLastUserMessageAttachments?: (attachments: Array<{ id?: string; url: string; file_type?: string; file_name?: string | null }>) => void;
|
|
642
735
|
} | null = null;
|
|
643
736
|
let chatMethodsByWidgetId: Record<string, NonNullable<typeof chatMethods>> = {};
|
|
644
737
|
let pendingChatDraftText = '';
|
|
645
738
|
let chatDraftRestoredForWidgetId: string | null = null;
|
|
739
|
+
/**
|
|
740
|
+
* Monotonic generation counter for loadPromptForWidget. Each load increments it;
|
|
741
|
+
* when the async draft read resolves, a changed counter means another load or an
|
|
742
|
+
* explicit prompt set (e.g. convert flow) happened meanwhile — the stale result
|
|
743
|
+
* must NOT clobber the newer prompt state (race behind feedback b).
|
|
744
|
+
*/
|
|
745
|
+
let loadPromptGeneration = 0;
|
|
646
746
|
/** Skip draft→prompt sync while restoring chat draft (avoids clearing promptImages). */
|
|
647
747
|
let suppressChatDraftSync = false;
|
|
648
748
|
|
|
@@ -653,7 +753,7 @@
|
|
|
653
753
|
|
|
654
754
|
/** Prompt mentions a reference image but chat has no attachment (common UX mistake). */
|
|
655
755
|
function promptImpliesVisionReference(prompt: string): boolean {
|
|
656
|
-
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(
|
|
657
757
|
prompt,
|
|
658
758
|
);
|
|
659
759
|
}
|
|
@@ -662,6 +762,9 @@
|
|
|
662
762
|
file?: File,
|
|
663
763
|
uploadedUrl?: string,
|
|
664
764
|
): Promise<string | null> {
|
|
765
|
+
if (file && !file.type.startsWith('image/')) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
665
768
|
if (uploadedUrl && /^https?:\/\//i.test(uploadedUrl)) {
|
|
666
769
|
try {
|
|
667
770
|
const response = await fetch(uploadedUrl);
|
|
@@ -689,7 +792,7 @@
|
|
|
689
792
|
|
|
690
793
|
async function handleChatUploadFile(
|
|
691
794
|
file: File,
|
|
692
|
-
options?: { onProgress?: (percentage: number) => void; source?: string },
|
|
795
|
+
options?: { onProgress?: (percentage: number) => void; source?: string; widgetId?: string },
|
|
693
796
|
) {
|
|
694
797
|
if (!authenticatedClientsToken) {
|
|
695
798
|
throw new Error('Upload client not initialized');
|
|
@@ -699,6 +802,7 @@
|
|
|
699
802
|
const uploadBasePath =
|
|
700
803
|
apiBasePath ||
|
|
701
804
|
`${(apiRoot || import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/+$/, '')}/v1`;
|
|
805
|
+
const persistWidgetId = options?.widgetId || selectedWidgetId || undefined;
|
|
702
806
|
const result = await uploadChatFileToWidgeticApi(
|
|
703
807
|
file,
|
|
704
808
|
{
|
|
@@ -706,7 +810,7 @@
|
|
|
706
810
|
accessToken: authenticatedClientsToken,
|
|
707
811
|
contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
|
|
708
812
|
source: options?.source || 'chat-upload',
|
|
709
|
-
widgetId:
|
|
813
|
+
widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
|
|
710
814
|
conversationId: currentChatConversationId || undefined,
|
|
711
815
|
},
|
|
712
816
|
{ onProgress: options?.onProgress },
|
|
@@ -743,8 +847,29 @@
|
|
|
743
847
|
content.substring(0, 80),
|
|
744
848
|
);
|
|
745
849
|
|
|
746
|
-
|
|
850
|
+
// Convert UX v2: the first prompt send of a draft panel creates the widget
|
|
851
|
+
// (DB + component + repo), replaces the sketch with a WidgetShape, and
|
|
852
|
+
// repoints the panel to the real widget id. Generation continues below.
|
|
853
|
+
let codegenWidgetId = widgetId;
|
|
854
|
+
if (isDraftConvertId(widgetId)) {
|
|
855
|
+
const finalizedWidgetId = await finalizeDraftConversion(widgetId, content);
|
|
856
|
+
if (!finalizedWidgetId) {
|
|
857
|
+
console.warn('[Chat→CodeGen] Draft conversion failed — message not sent to codegen');
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
codegenWidgetId = finalizedWidgetId;
|
|
861
|
+
// Re-point the chat draft the user just typed onto the real widget panel.
|
|
862
|
+
const newPanelChat = getChatMethodsForWidget(finalizedWidgetId);
|
|
863
|
+
newPanelChat?.setDraftText?.(content);
|
|
864
|
+
const newPanelChatAttach = getPanelState(finalizedWidgetId).promptImages ?? [];
|
|
865
|
+
for (const imageDataUrl of newPanelChatAttach) {
|
|
866
|
+
newPanelChat?.addImageAttachment?.(imageDataUrl, 'design-screenshot.png');
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const panelChat = getChatMethodsForWidget(codegenWidgetId);
|
|
747
871
|
const cdnUploadIds = (attachments || [])
|
|
872
|
+
.filter((attachment) => attachment.uploadId && attachment.fileType.startsWith('image/'))
|
|
748
873
|
.map((attachment) => attachment.uploadId)
|
|
749
874
|
.filter((id): id is string => Boolean(id));
|
|
750
875
|
|
|
@@ -768,20 +893,20 @@
|
|
|
768
893
|
}
|
|
769
894
|
|
|
770
895
|
if (panelPromptImages.length > 0) {
|
|
771
|
-
patchPanelSnapshot(
|
|
772
|
-
if (
|
|
896
|
+
patchPanelSnapshot(codegenWidgetId, { promptImages: panelPromptImages });
|
|
897
|
+
if (codegenWidgetId === focusedPanelWidgetId) {
|
|
773
898
|
promptImages = panelPromptImages;
|
|
774
899
|
}
|
|
775
900
|
}
|
|
776
901
|
|
|
777
902
|
// Clear chat input immediately so thumbnails do not linger after Send
|
|
778
903
|
panelChat?.clearDraft?.();
|
|
779
|
-
void deleteWidgetDraft(
|
|
904
|
+
void deleteWidgetDraft(codegenWidgetId).catch(() => {
|
|
780
905
|
// Also drop the legacy localStorage copies if the durable clear fails.
|
|
781
906
|
try {
|
|
782
|
-
localStorage.removeItem(PROMPT_IMAGES_STORAGE_PREFIX +
|
|
783
|
-
localStorage.removeItem(CHAT_DRAFT_STORAGE_PREFIX +
|
|
784
|
-
localStorage.removeItem(PROMPT_TEXT_STORAGE_PREFIX +
|
|
907
|
+
localStorage.removeItem(PROMPT_IMAGES_STORAGE_PREFIX + codegenWidgetId);
|
|
908
|
+
localStorage.removeItem(CHAT_DRAFT_STORAGE_PREFIX + codegenWidgetId);
|
|
909
|
+
localStorage.removeItem(PROMPT_TEXT_STORAGE_PREFIX + codegenWidgetId);
|
|
785
910
|
localStorage.removeItem(PROMPT_IMAGES_STORAGE_KEY);
|
|
786
911
|
} catch {
|
|
787
912
|
/* ignore */
|
|
@@ -791,28 +916,69 @@
|
|
|
791
916
|
const effectivePromptImages =
|
|
792
917
|
panelPromptImages.length > 0
|
|
793
918
|
? panelPromptImages
|
|
794
|
-
:
|
|
919
|
+
: codegenWidgetId === focusedPanelWidgetId
|
|
795
920
|
? promptImages
|
|
796
|
-
: getPanelState(
|
|
921
|
+
: getPanelState(codegenWidgetId).promptImages;
|
|
797
922
|
const hasImages = effectivePromptImages.length > 0 || cdnUploadIds.length > 0;
|
|
798
|
-
|
|
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.';
|
|
799
929
|
showToast('warning', 'Lipsește imaginea de referință', {
|
|
800
|
-
description:
|
|
801
|
-
'Promptul menționează o imagine atașată, dar în chat nu e niciun attachment. Folosește butonul Upload sau ?attachStairsRef=1.',
|
|
930
|
+
description: missingImageError,
|
|
802
931
|
duration: 10000,
|
|
803
932
|
});
|
|
933
|
+
patchCodegenUiState(codegenWidgetId, {
|
|
934
|
+
lastGenerationFailed: true,
|
|
935
|
+
lastGenerationError: missingImageError,
|
|
936
|
+
generateCodeStatus: '❌ Missing reference image',
|
|
937
|
+
});
|
|
938
|
+
pushCodegenFailureToChat(codegenWidgetId, missingImageError);
|
|
804
939
|
return;
|
|
805
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
|
+
}
|
|
806
962
|
// Pass upload IDs / images explicitly — clearDraft already wiped Chat's local attachments.
|
|
807
963
|
await generateCode({
|
|
808
964
|
prompt: content,
|
|
809
965
|
fromChat: true,
|
|
810
|
-
widgetId,
|
|
966
|
+
widgetId: codegenWidgetId,
|
|
811
967
|
attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
|
|
812
968
|
promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
|
|
969
|
+
persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
|
|
813
970
|
});
|
|
814
971
|
}
|
|
815
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
|
+
|
|
816
982
|
/** Retry failed codegen without duplicating the user bubble in chat. */
|
|
817
983
|
async function handleChatRetryMessage(content: string, sourceWidgetId?: string) {
|
|
818
984
|
const widgetId = sourceWidgetId ?? selectedWidgetId;
|
|
@@ -823,7 +989,53 @@
|
|
|
823
989
|
':',
|
|
824
990
|
content.substring(0, 80),
|
|
825
991
|
);
|
|
826
|
-
|
|
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
|
+
});
|
|
827
1039
|
}
|
|
828
1040
|
|
|
829
1041
|
function handleChatConversationChange(conversationId: string | null, sourceWidgetId?: string) {
|
|
@@ -891,12 +1103,63 @@
|
|
|
891
1103
|
: prompt;
|
|
892
1104
|
}
|
|
893
1105
|
|
|
894
|
-
/**
|
|
895
|
-
|
|
1106
|
+
/**
|
|
1107
|
+
* Persist the user prompt to DB only when generation actually starts.
|
|
1108
|
+
* Writes into the conversation the panel Chat is currently showing (or the
|
|
1109
|
+
* newest one) so the visible chat and the codegen context never diverge —
|
|
1110
|
+
* a remounted Chat shows whatever conversation activateBestConversation
|
|
1111
|
+
* picked, which may differ from convos[0] when several empty conversations
|
|
1112
|
+
* exist (feedback c: chat looked empty while messages went elsewhere).
|
|
1113
|
+
*/
|
|
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> {
|
|
896
1151
|
if (!conversationsClient || !messagesClient) return;
|
|
897
1152
|
try {
|
|
898
|
-
|
|
899
|
-
|
|
1153
|
+
const panelChat = getChatMethodsForWidget(widgetId);
|
|
1154
|
+
const visibleConversationId =
|
|
1155
|
+
panelChat?.getConversationId?.() ??
|
|
1156
|
+
getPanelState(widgetId).currentChatConversationId ??
|
|
1157
|
+
null;
|
|
1158
|
+
const convos = await handleLoadConversations(widgetId);
|
|
1159
|
+
let convId =
|
|
1160
|
+
(visibleConversationId && convos?.some((c) => c.id === visibleConversationId))
|
|
1161
|
+
? visibleConversationId
|
|
1162
|
+
: convos?.[0]?.id;
|
|
900
1163
|
if (!convId) {
|
|
901
1164
|
const widget = getWidgetById(widgetId);
|
|
902
1165
|
const convTitle = widget?.name ? `Convert: ${widget.name}` : 'Widget conversion';
|
|
@@ -905,13 +1168,112 @@
|
|
|
905
1168
|
}
|
|
906
1169
|
if (!convId) return;
|
|
907
1170
|
|
|
908
|
-
const
|
|
909
|
-
|
|
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(
|
|
910
1211
|
(m) => (m.messageType || (m as any).message_type) === 'user'
|
|
911
1212
|
);
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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');
|
|
915
1277
|
}
|
|
916
1278
|
} catch (err) {
|
|
917
1279
|
console.warn('[Codegen] Could not persist user prompt before generation:', err);
|
|
@@ -936,18 +1298,27 @@
|
|
|
936
1298
|
console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
|
|
937
1299
|
}
|
|
938
1300
|
|
|
939
|
-
/** Screenshot: capture widget preview iframe and return data URL */
|
|
940
|
-
async function handleChatScreenshot(): Promise<string | null> {
|
|
941
|
-
|
|
942
|
-
|
|
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
|
+
});
|
|
943
1314
|
return null;
|
|
944
1315
|
}
|
|
945
|
-
const iframe = document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement;
|
|
946
|
-
if (!iframe?.contentWindow) return null;
|
|
947
1316
|
|
|
1317
|
+
console.log('[Chat→Screenshot] Requesting screenshot from preview iframe', targetId?.substring(0, 8));
|
|
948
1318
|
return new Promise<string | null>((resolve) => {
|
|
949
1319
|
const timeout = setTimeout(() => {
|
|
950
1320
|
window.removeEventListener('message', handler);
|
|
1321
|
+
console.warn('[Chat→Screenshot] Timed out waiting for widgetic:screenshot');
|
|
951
1322
|
resolve(null);
|
|
952
1323
|
}, 8000);
|
|
953
1324
|
|
|
@@ -962,8 +1333,27 @@
|
|
|
962
1333
|
});
|
|
963
1334
|
}
|
|
964
1335
|
|
|
1336
|
+
function normalizeConversationRecords(json: any): any[] {
|
|
1337
|
+
const raw = json?.data ?? json?.conversations ?? json;
|
|
1338
|
+
const list = Array.isArray(raw)
|
|
1339
|
+
? raw
|
|
1340
|
+
: Array.isArray(raw?.data)
|
|
1341
|
+
? raw.data
|
|
1342
|
+
: [];
|
|
1343
|
+
return list
|
|
1344
|
+
.map((c: any) => ({
|
|
1345
|
+
...c,
|
|
1346
|
+
id: c?.id || c?.conversation_id || c?.conversationId,
|
|
1347
|
+
}))
|
|
1348
|
+
.filter((c: any) => !!c.id);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
965
1351
|
/** Load conversations for a widget from backend. Returns null when client/API is unavailable (not an empty list). */
|
|
966
1352
|
async function handleLoadConversations(contextId: string): Promise<any[] | null> {
|
|
1353
|
+
// Draft convert panels have no DB widget — return an empty list so the
|
|
1354
|
+
// Chat initializes cleanly (creates a local conversation) instead of
|
|
1355
|
+
// showing the "Could not load chat history" error after 4 retries.
|
|
1356
|
+
if (isDraftConvertId(contextId)) return [];
|
|
967
1357
|
try {
|
|
968
1358
|
if (!conversationsClient) {
|
|
969
1359
|
console.warn('[Chat→Backend] conversationsClient not initialized');
|
|
@@ -971,11 +1361,13 @@
|
|
|
971
1361
|
}
|
|
972
1362
|
const response = await conversationsClient.getConversationsByWidgetIdRaw({
|
|
973
1363
|
widgetId: contextId,
|
|
974
|
-
sort: 'created_at:desc'
|
|
1364
|
+
sort: 'created_at:desc',
|
|
1365
|
+
size: 100,
|
|
975
1366
|
});
|
|
976
1367
|
const json = await response.raw.json();
|
|
977
|
-
|
|
978
|
-
|
|
1368
|
+
const conversations = normalizeConversationRecords(json);
|
|
1369
|
+
console.log('[Chat→Backend] Loaded conversations for widget:', contextId, conversations.length);
|
|
1370
|
+
return conversations;
|
|
979
1371
|
} catch (error) {
|
|
980
1372
|
console.error('[Chat→Backend] Failed to load conversations:', error);
|
|
981
1373
|
return null;
|
|
@@ -1026,26 +1418,40 @@
|
|
|
1026
1418
|
const json = await response.raw.json();
|
|
1027
1419
|
const messages = json?.data || [];
|
|
1028
1420
|
console.log('[Chat→Backend] Loaded messages for conversation:', conversationId, messages.length);
|
|
1029
|
-
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 {
|
|
1030
1443
|
id: m.id,
|
|
1031
1444
|
conversationId: m.conversation_id || m.conversationId || conversationId,
|
|
1032
1445
|
messageContent: m.message_content || m.messageContent || '',
|
|
1033
1446
|
messageType: m.message_type || m.messageType || 'user',
|
|
1034
|
-
attachments
|
|
1035
|
-
id: a.id || `att_${idx}`,
|
|
1036
|
-
url: a.url || '',
|
|
1037
|
-
file_type: a.file_type || a.fileType || 'image/png',
|
|
1038
|
-
file_name: a.file_name || a.fileName || null,
|
|
1039
|
-
file_size: a.file_size ?? a.fileSize ?? null,
|
|
1040
|
-
display_order: a.display_order ?? a.displayOrder ?? idx + 1,
|
|
1041
|
-
created_at: a.created_at || a.createdAt || new Date().toISOString(),
|
|
1042
|
-
})),
|
|
1447
|
+
attachments,
|
|
1043
1448
|
isCommit: m.is_commit || m.isCommit || false,
|
|
1044
1449
|
userId: m.user_id || m.userId || '',
|
|
1045
1450
|
createdAt: new Date(m.created_at || m.createdAt),
|
|
1046
1451
|
updatedAt: new Date(m.updated_at || m.updatedAt || m.created_at || m.createdAt),
|
|
1047
1452
|
rolledBackAt: m.rolled_back_at || m.rolledBackAt || null,
|
|
1048
|
-
}
|
|
1453
|
+
};
|
|
1454
|
+
});
|
|
1049
1455
|
const withHead = markWidgetHeadOnLoadedMessages(mappedMessages);
|
|
1050
1456
|
const syncWidgetId = widgetIdForSync ?? selectedWidgetId;
|
|
1051
1457
|
if (syncWidgetId) {
|
|
@@ -1066,61 +1472,112 @@
|
|
|
1066
1472
|
return { id: crypto.randomUUID(), title };
|
|
1067
1473
|
}
|
|
1068
1474
|
const response = await conversationsClient.createConversationForWidgetRaw({
|
|
1069
|
-
|
|
1070
|
-
|
|
1475
|
+
createConversationForWidgetRequest: {
|
|
1476
|
+
widget_id: contextId,
|
|
1477
|
+
title,
|
|
1478
|
+
},
|
|
1071
1479
|
});
|
|
1072
1480
|
const json = await response.raw.json();
|
|
1073
1481
|
const conversation = json?.data || json;
|
|
1074
|
-
|
|
1075
|
-
|
|
1482
|
+
const conversationId = conversation?.id || conversation?.conversation_id || conversation?.conversationId;
|
|
1483
|
+
console.log('[Chat→Backend] Created conversation:', conversationId);
|
|
1484
|
+
return conversationId ? { ...conversation, id: conversationId } : conversation;
|
|
1076
1485
|
} catch (error) {
|
|
1077
1486
|
console.error('[Chat→Backend] Failed to create conversation:', error);
|
|
1078
1487
|
return { id: crypto.randomUUID(), title };
|
|
1079
1488
|
}
|
|
1080
1489
|
}
|
|
1081
1490
|
|
|
1082
|
-
|
|
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). */
|
|
1083
1509
|
async function mapChatAttachmentsForApi(
|
|
1084
1510
|
attachments?: Array<{
|
|
1085
1511
|
url?: string;
|
|
1512
|
+
file?: File;
|
|
1086
1513
|
file_type?: string;
|
|
1087
1514
|
file_name?: string | null;
|
|
1088
1515
|
file_size?: number | null;
|
|
1089
1516
|
display_order?: number;
|
|
1090
1517
|
}>,
|
|
1518
|
+
persistWidgetId?: string | null,
|
|
1091
1519
|
): Promise<Array<{ url: string; file_type: string; file_name?: string; file_size?: number; display_order?: number }>> {
|
|
1092
1520
|
if (!attachments?.length) return [];
|
|
1093
1521
|
const items: Array<{ url: string; file_type: string; file_name?: string; file_size?: number; display_order?: number }> = [];
|
|
1094
1522
|
for (let i = 0; i < attachments.length; i++) {
|
|
1095
1523
|
const att = attachments[i];
|
|
1096
1524
|
let url = att.url || '';
|
|
1097
|
-
|
|
1098
|
-
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:'))) {
|
|
1099
1527
|
try {
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1528
|
+
const uploaded = await handleChatUploadFile(att.file, {
|
|
1529
|
+
source: 'chat-message-persist',
|
|
1530
|
+
widgetId: persistWidgetId || undefined,
|
|
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,
|
|
1106
1540
|
});
|
|
1541
|
+
continue;
|
|
1107
1542
|
} catch (err) {
|
|
1108
|
-
console.warn('[Chat→Backend]
|
|
1543
|
+
console.warn('[Chat→Backend] Chat File upload failed, skipping attachment:', err);
|
|
1109
1544
|
continue;
|
|
1110
1545
|
}
|
|
1111
1546
|
}
|
|
1112
|
-
if (url
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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);
|
|
1117
1574
|
continue;
|
|
1118
1575
|
}
|
|
1119
1576
|
}
|
|
1120
1577
|
items.push({
|
|
1121
1578
|
url,
|
|
1122
|
-
file_type: att.file_type
|
|
1123
|
-
file_name:
|
|
1579
|
+
file_type: guessAttachmentFileType(url, fileName, att.file_type),
|
|
1580
|
+
file_name: fileName,
|
|
1124
1581
|
file_size: att.file_size ?? undefined,
|
|
1125
1582
|
display_order: att.display_order ?? i + 1,
|
|
1126
1583
|
});
|
|
@@ -1135,11 +1592,13 @@
|
|
|
1135
1592
|
messageType: string,
|
|
1136
1593
|
attachments?: Array<{
|
|
1137
1594
|
url?: string;
|
|
1595
|
+
file?: File;
|
|
1138
1596
|
file_type?: string;
|
|
1139
1597
|
file_name?: string | null;
|
|
1140
1598
|
file_size?: number | null;
|
|
1141
1599
|
display_order?: number;
|
|
1142
1600
|
}>,
|
|
1601
|
+
persistWidgetId?: string | null,
|
|
1143
1602
|
): Promise<any> {
|
|
1144
1603
|
try {
|
|
1145
1604
|
if (!messagesClient) {
|
|
@@ -1152,7 +1611,17 @@
|
|
|
1152
1611
|
return null;
|
|
1153
1612
|
}
|
|
1154
1613
|
const apiAttachments =
|
|
1155
|
-
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
|
+
) : [];
|
|
1156
1625
|
const createPayload = {
|
|
1157
1626
|
message_content: content,
|
|
1158
1627
|
message_type: messageType as 'user' | 'assistant',
|
|
@@ -1167,25 +1636,18 @@
|
|
|
1167
1636
|
});
|
|
1168
1637
|
const json = await response.raw.json();
|
|
1169
1638
|
console.log('[Chat→Backend] Saved message to conversation:', conversationId);
|
|
1170
|
-
|
|
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
|
+
};
|
|
1171
1645
|
} catch (withAttachmentsError) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if (apiAttachments.length === 0) throw withAttachmentsError;
|
|
1175
|
-
console.warn(
|
|
1176
|
-
'[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:',
|
|
1177
1648
|
withAttachmentsError,
|
|
1178
1649
|
);
|
|
1179
|
-
|
|
1180
|
-
conversationId,
|
|
1181
|
-
createMessageRequest: {
|
|
1182
|
-
...createPayload,
|
|
1183
|
-
attachments: undefined,
|
|
1184
|
-
},
|
|
1185
|
-
});
|
|
1186
|
-
const json = await response.raw.json();
|
|
1187
|
-
console.log('[Chat→Backend] Saved message (text-only fallback):', conversationId);
|
|
1188
|
-
return json?.data || json;
|
|
1650
|
+
throw withAttachmentsError;
|
|
1189
1651
|
}
|
|
1190
1652
|
} catch (error) {
|
|
1191
1653
|
console.error('[Chat→Backend] Failed to save message:', error);
|
|
@@ -1358,10 +1820,6 @@
|
|
|
1358
1820
|
live: Partial<WidgetPanelSnapshot>
|
|
1359
1821
|
): string | null {
|
|
1360
1822
|
const stored = loadCommitIdForWidget(widgetId);
|
|
1361
|
-
// Prefer in-session sources first; localStorage last (often stale after publish).
|
|
1362
|
-
const ordered = [live.lastCommitId, snap.lastCommitId, stored].filter(
|
|
1363
|
-
(sha): sha is string => typeof sha === 'string' && sha.length > 0
|
|
1364
|
-
);
|
|
1365
1823
|
|
|
1366
1824
|
const publishedVersion = live.lastPublishedVersion ?? snap.lastPublishedVersion ?? null;
|
|
1367
1825
|
let liveSha: string | null = null;
|
|
@@ -1379,8 +1837,18 @@
|
|
|
1379
1837
|
}
|
|
1380
1838
|
|
|
1381
1839
|
if (liveSha) {
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1840
|
+
const sessionOrdered = [live.lastCommitId, snap.lastCommitId].filter(
|
|
1841
|
+
(sha): sha is string => typeof sha === 'string' && sha.length > 0
|
|
1842
|
+
);
|
|
1843
|
+
const sessionAhead = sessionOrdered.find((candidate) => !commitsMatch(candidate, liveSha));
|
|
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;
|
|
1848
|
+
const sessionMatch = sessionOrdered.find((candidate) => commitsMatch(candidate, liveSha));
|
|
1849
|
+
if (sessionMatch) return sessionMatch;
|
|
1850
|
+
if (stored && commitsMatch(stored, liveSha)) return stored;
|
|
1851
|
+
return liveSha;
|
|
1384
1852
|
}
|
|
1385
1853
|
|
|
1386
1854
|
return live.lastCommitId ?? snap.lastCommitId ?? stored ?? null;
|
|
@@ -1435,18 +1903,21 @@
|
|
|
1435
1903
|
let editingCompositionName = '';
|
|
1436
1904
|
let deletingCompositionId: string | null = null;
|
|
1437
1905
|
|
|
1438
|
-
/**
|
|
1439
|
-
const
|
|
1906
|
+
/** Explicit chat box height (px). Drag grows this and pushes Publish down. */
|
|
1907
|
+
const CHAT_DEFAULT_HEIGHT = 550;
|
|
1440
1908
|
const CHAT_MIN_HEIGHT = 260;
|
|
1441
|
-
const CHAT_MAX_HEIGHT =
|
|
1909
|
+
const CHAT_MAX_HEIGHT = 2000;
|
|
1442
1910
|
|
|
1443
1911
|
function getChatContainerHeightForPanel(widgetId: string): number {
|
|
1444
|
-
|
|
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));
|
|
1445
1915
|
}
|
|
1446
1916
|
|
|
1447
1917
|
function handleChatMessagesResize(widgetId: string, deltaPx: number): void {
|
|
1448
1918
|
const current = getChatContainerHeightForPanel(widgetId);
|
|
1449
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);
|
|
1450
1921
|
patchPanelSnapshot(widgetId, { chatContainerHeight: next });
|
|
1451
1922
|
}
|
|
1452
1923
|
|
|
@@ -1805,7 +2276,11 @@
|
|
|
1805
2276
|
compositionSchemasError = schemaError;
|
|
1806
2277
|
}
|
|
1807
2278
|
if (designSchema || contentSchema) {
|
|
1808
|
-
queueMicrotask(() =>
|
|
2279
|
+
queueMicrotask(() => {
|
|
2280
|
+
void loadCompositionsForPanel(widgetId).then(() => {
|
|
2281
|
+
applyPanelCompositionToPreview(widgetId);
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
1809
2284
|
}
|
|
1810
2285
|
} finally {
|
|
1811
2286
|
schemaLoadInFlight.delete(widgetId);
|
|
@@ -1843,20 +2318,32 @@
|
|
|
1843
2318
|
return values;
|
|
1844
2319
|
}
|
|
1845
2320
|
|
|
1846
|
-
/** After codegen, push design.json defaults into Create preview
|
|
2321
|
+
/** After codegen, push design.json + content.json defaults into Create preview. */
|
|
1847
2322
|
async function applyDesignSchemaDefaultsToPreview(widgetId: string) {
|
|
1848
2323
|
if (!widgetId || !widgetsClient) return;
|
|
1849
2324
|
await scheduleWidgetSchemaLoad(widgetId);
|
|
1850
2325
|
const schema = getPanelDisplayState(widgetId).compositionDesignSchema;
|
|
1851
2326
|
const defaults = extractDesignDefaultsFromSchema(schema);
|
|
1852
|
-
if (Object.keys(defaults).length
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
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;
|
|
1857
2343
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
2344
|
+
applyPanelCompositionToPreview(widgetId);
|
|
2345
|
+
setTimeout(() => applyPanelCompositionToPreview(widgetId), 250);
|
|
2346
|
+
setTimeout(() => applyPanelCompositionToPreview(widgetId), 800);
|
|
1860
2347
|
}
|
|
1861
2348
|
|
|
1862
2349
|
/** Toggle composition editor panel open/closed */
|
|
@@ -1926,9 +2413,20 @@
|
|
|
1926
2413
|
});
|
|
1927
2414
|
}
|
|
1928
2415
|
if (event.detail.step === 'create') {
|
|
1929
|
-
|
|
1930
|
-
|
|
2416
|
+
let psCreate = getPanelDisplayState(panelWidgetId);
|
|
2417
|
+
if (!psCreate.compositionContentSchema && !psCreate.compositionSchemasLoading) {
|
|
2418
|
+
void scheduleWidgetSchemaLoad(panelWidgetId);
|
|
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
|
+
}
|
|
1931
2428
|
}
|
|
2429
|
+
applyPanelCompositionToPreview(panelWidgetId);
|
|
1932
2430
|
if (panelWidgetId && pendingCreatePreviewDefaultsWidgetId === panelWidgetId) {
|
|
1933
2431
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
1934
2432
|
if (panelWidgetId === focusedPanelWidgetId) {
|
|
@@ -2052,11 +2550,15 @@
|
|
|
2052
2550
|
|
|
2053
2551
|
/** Handle content items reorder/add/delete from PropsEditor */
|
|
2054
2552
|
function handleEditorContentItemsChanged(event: CustomEvent<{ items: unknown[] }>) {
|
|
2055
|
-
|
|
2553
|
+
const incoming = Array.isArray(event.detail.items) ? event.detail.items : [];
|
|
2554
|
+
const schemaItems = compositionContentSchema?.contentItems
|
|
2555
|
+
?? compositionContentSchema?.data?.contentItems
|
|
2556
|
+
?? [];
|
|
2557
|
+
liveContentItems = mergeContentItemsWithSchema(incoming, schemaItems);
|
|
2056
2558
|
console.log('[PropsEditor→Preview] Content items changed:', liveContentItems.length, 'items');
|
|
2057
2559
|
getWidgetDetailsRef(focusedPanelWidgetId)?.sendMessageToPreview({
|
|
2058
2560
|
type: 'widgetic:update',
|
|
2059
|
-
contentItems: liveContentItems
|
|
2561
|
+
contentItems: flattenContentItemsForPreview(liveContentItems)
|
|
2060
2562
|
});
|
|
2061
2563
|
compositionDirty = true;
|
|
2062
2564
|
syncFocusedPanelEditorSnapshot();
|
|
@@ -2470,7 +2972,10 @@
|
|
|
2470
2972
|
widgetDetails?.sendMessageToPreview({ type: 'widgetic:update', content: liveContentValues });
|
|
2471
2973
|
}
|
|
2472
2974
|
if (liveContentItems.length > 0) {
|
|
2473
|
-
widgetDetails?.sendMessageToPreview({
|
|
2975
|
+
widgetDetails?.sendMessageToPreview({
|
|
2976
|
+
type: 'widgetic:update',
|
|
2977
|
+
contentItems: flattenContentItemsForPreview(liveContentItems)
|
|
2978
|
+
});
|
|
2474
2979
|
}
|
|
2475
2980
|
}
|
|
2476
2981
|
|
|
@@ -2708,7 +3213,7 @@
|
|
|
2708
3213
|
// Default coder: host prop (site embed) wins. Fallback Flash for speed.
|
|
2709
3214
|
const hostCoderModelId = (defaultCoderModelId || '').trim() || null;
|
|
2710
3215
|
const hostPlannerModelId = (defaultPlannerModelId || '').trim() || null;
|
|
2711
|
-
let defaultModelId = hostCoderModelId || '
|
|
3216
|
+
let defaultModelId = hostCoderModelId || 'zhipu-glm-5.3-flash';
|
|
2712
3217
|
|
|
2713
3218
|
// Available LLM models for code generation
|
|
2714
3219
|
interface LLMModelOption {
|
|
@@ -2716,17 +3221,19 @@
|
|
|
2716
3221
|
name: string;
|
|
2717
3222
|
provider: string;
|
|
2718
3223
|
description: string;
|
|
3224
|
+
supportsVision?: boolean;
|
|
2719
3225
|
}
|
|
2720
3226
|
|
|
2721
3227
|
// Default fallback models in case API fails (one from each provider)
|
|
2722
3228
|
const fallbackModels: LLMModelOption[] = [
|
|
2723
|
-
{ id: '
|
|
2724
|
-
{ id: 'deepseek-v4-
|
|
2725
|
-
{ id: '
|
|
2726
|
-
{ id: 'openai-gpt-5.
|
|
2727
|
-
{ id: 'openai-
|
|
2728
|
-
{ id: '
|
|
2729
|
-
{ id: '
|
|
3229
|
+
{ id: 'zhipu-glm-5.3-flash', name: 'GLM 5.3 Flash (Z.ai)', provider: 'Zhipu', description: 'Cheapest vision + coding model', supportsVision: true },
|
|
3230
|
+
{ id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', provider: 'DeepSeek', description: 'Frontier coding model, 1M context', supportsVision: false },
|
|
3231
|
+
{ id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', provider: 'DeepSeek', description: 'Fast and cost-efficient', supportsVision: false },
|
|
3232
|
+
{ id: 'openai-gpt-5.4', name: 'GPT-5.4 (OpenAI)', provider: 'OpenAI', description: 'Affordable model for coding', supportsVision: true },
|
|
3233
|
+
{ id: 'openai-gpt-5.5', name: 'GPT-5.5 (OpenAI)', provider: 'OpenAI', description: 'Frontier model for complex work', supportsVision: true },
|
|
3234
|
+
{ id: 'openai-codex-3.2', name: 'Codex 3.2 (OpenAI)', provider: 'OpenAI', description: 'Coding-specialized model', supportsVision: false },
|
|
3235
|
+
{ id: 'gemini-3-flash', name: 'Gemini 3 Flash (Google)', provider: 'Google', description: 'Google\'s fast Gemini model', supportsVision: true },
|
|
3236
|
+
{ id: 'anthropic-claude-sonnet-4.6', name: 'Claude Sonnet 4.6 (Anthropic)', provider: 'Anthropic', description: 'Best balance of intelligence, speed, and cost', supportsVision: true },
|
|
2730
3237
|
];
|
|
2731
3238
|
|
|
2732
3239
|
// Models loaded from API (single source of truth)
|
|
@@ -2744,11 +3251,12 @@
|
|
|
2744
3251
|
// SDK returns { success, models } directly (not nested under data)
|
|
2745
3252
|
if (response.success && Array.isArray(response.models)) {
|
|
2746
3253
|
// Transform API response to match our interface
|
|
2747
|
-
availableModels = response.models.map((m: { id: string; displayName: string; provider: string; description: string }) => ({
|
|
3254
|
+
availableModels = response.models.map((m: { id: string; displayName: string; provider: string; description: string; supportsVision?: boolean }) => ({
|
|
2748
3255
|
id: m.id,
|
|
2749
3256
|
name: m.displayName,
|
|
2750
3257
|
provider: m.provider.charAt(0).toUpperCase() + m.provider.slice(1), // Capitalize provider
|
|
2751
|
-
description: m.description
|
|
3258
|
+
description: m.description,
|
|
3259
|
+
supportsVision: m.supportsVision === true,
|
|
2752
3260
|
}));
|
|
2753
3261
|
|
|
2754
3262
|
// Model LIST comes from API. UI defaults come from host props when provided.
|
|
@@ -2787,7 +3295,7 @@
|
|
|
2787
3295
|
|
|
2788
3296
|
// Planner model — separate model for prompt enhancement pre-call (debug mode only)
|
|
2789
3297
|
// Empty string = user chose "None (always skip)". Non-empty = preferred planner when auto-heuristics enable it.
|
|
2790
|
-
defaultPlannerModelId = hostPlannerModelId || '
|
|
3298
|
+
defaultPlannerModelId = hostPlannerModelId || 'zhipu-glm-5.3-flash';
|
|
2791
3299
|
let userSelectedPlannerModelId = defaultPlannerModelId;
|
|
2792
3300
|
|
|
2793
3301
|
// Debug ON → dropdown (starts at host default). Debug OFF → host/fallback default.
|
|
@@ -2801,14 +3309,24 @@
|
|
|
2801
3309
|
return false;
|
|
2802
3310
|
}
|
|
2803
3311
|
|
|
2804
|
-
function resolvePlannerModelIdForGeneration(prompt: string): string | undefined {
|
|
2805
|
-
if (
|
|
2806
|
-
if (userSelectedPlannerModelId === '') {
|
|
3312
|
+
function resolvePlannerModelIdForGeneration(prompt: string, coderIdForRequest?: string): string | undefined {
|
|
3313
|
+
if (debugMode && userSelectedPlannerModelId === '') {
|
|
2807
3314
|
console.log('[WidgetCreator] Planner skipped (manual None)');
|
|
2808
3315
|
return undefined;
|
|
2809
3316
|
}
|
|
2810
3317
|
|
|
2811
|
-
const preferredPlannerId =
|
|
3318
|
+
const preferredPlannerId = debugMode
|
|
3319
|
+
? (userSelectedPlannerModelId || defaultPlannerModelId)
|
|
3320
|
+
: defaultPlannerModelId;
|
|
3321
|
+
const selectedCoderId = debugMode ? (userSelectedModelId || defaultModelId) : defaultModelId;
|
|
3322
|
+
const coderId = coderIdForRequest || selectedCoderId;
|
|
3323
|
+
if (!preferredPlannerId || preferredPlannerId === selectedCoderId || preferredPlannerId === coderId) {
|
|
3324
|
+
console.log('[WidgetCreator] Planner skipped (same model as coder — one LLM call)', {
|
|
3325
|
+
planner: preferredPlannerId,
|
|
3326
|
+
coder: coderId,
|
|
3327
|
+
});
|
|
3328
|
+
return undefined;
|
|
3329
|
+
}
|
|
2812
3330
|
const widgetHasCode = !!(lastCommitId || lastPublishedVersion);
|
|
2813
3331
|
|
|
2814
3332
|
if (!widgetHasCode) {
|
|
@@ -2866,6 +3384,125 @@
|
|
|
2866
3384
|
toggleDebugMode();
|
|
2867
3385
|
}
|
|
2868
3386
|
|
|
3387
|
+
let canvasWireframeMode = true;
|
|
3388
|
+
/** Extra canvas chrome — default off. Not tied to debugMode. */
|
|
3389
|
+
let canvasAdvancedFeatures = !!initialAdvancedFeatures;
|
|
3390
|
+
let canvasPrimaryWidgetId: string | null = null;
|
|
3391
|
+
|
|
3392
|
+
function emitCreatorWireframeChanged() {
|
|
3393
|
+
if (typeof window === 'undefined') return;
|
|
3394
|
+
window.dispatchEvent(
|
|
3395
|
+
new CustomEvent(CREATOR_WIREFRAME_CHANGED_EVENT, {
|
|
3396
|
+
detail: { wireframeMode: canvasWireframeMode }
|
|
3397
|
+
})
|
|
3398
|
+
);
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
function handleHostToggleWireframeEvent() {
|
|
3402
|
+
canvasWireframeMode = !canvasWireframeMode;
|
|
3403
|
+
console.log('[CreatorApp] Wireframe mode:', canvasWireframeMode ? 'ON' : 'OFF');
|
|
3404
|
+
emitCreatorWireframeChanged();
|
|
3405
|
+
}
|
|
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
|
+
|
|
3422
|
+
function resolveHostTargetWidgetId(): string | null {
|
|
3423
|
+
const id = focusedPanelWidgetId || selectedWidgetId;
|
|
3424
|
+
if (!id || isDraftConvertId(id)) return null;
|
|
3425
|
+
return id;
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
function handleHostOpenWidgetDetailsEvent(): void {
|
|
3429
|
+
const widgetId = resolveHostTargetWidgetId();
|
|
3430
|
+
if (!widgetId) {
|
|
3431
|
+
showToast('warning', 'Select a widget first', {
|
|
3432
|
+
description: 'Click a widget on the canvas, then use Edit to open Widget Details.',
|
|
3433
|
+
});
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
console.log('[CreatorApp] Host Edit — opening Widget Details', widgetId.substring(0, 8));
|
|
3437
|
+
void openWidgetDetailsPanel(widgetId);
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
async function handleHostDuplicateWidgetEvent(): Promise<void> {
|
|
3441
|
+
const widgetId = resolveHostTargetWidgetId();
|
|
3442
|
+
if (!widgetId || !canvasRef) {
|
|
3443
|
+
showToast('warning', 'Select a widget first', {
|
|
3444
|
+
description: 'Click the widget on the canvas, then Duplicate.',
|
|
3445
|
+
});
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
canvasRef.selectWidgetShape?.(widgetId);
|
|
3449
|
+
if (typeof canvasRef.duplicateSelectedObjects === 'function') {
|
|
3450
|
+
await canvasRef.duplicateSelectedObjects();
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
showToast('error', 'Cannot duplicate widget', { description: 'Canvas duplicate is not available' });
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
async function handleHostDeleteWidgetEvent(): Promise<void> {
|
|
3457
|
+
const widgetId = resolveHostTargetWidgetId();
|
|
3458
|
+
if (!widgetId) {
|
|
3459
|
+
showToast('warning', 'Select a widget first');
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
if (canvasRef?.selectWidgetShape?.(widgetId) && typeof canvasRef.deleteSelectedObjects === 'function') {
|
|
3463
|
+
await canvasRef.deleteSelectedObjects();
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
const widgetName = getWidgetById(widgetId)?.name || 'Widget';
|
|
3467
|
+
const result = await showDeleteWidgetDialog(widgetId, widgetName);
|
|
3468
|
+
if (result.proceed) {
|
|
3469
|
+
await deleteWidgetFromDb(widgetId);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
async function persistPrimaryWidgetId(widgetId: string) {
|
|
3474
|
+
if (!widgetId) return;
|
|
3475
|
+
canvasPrimaryWidgetId = widgetId;
|
|
3476
|
+
console.log('[CreatorApp] Set primary widget:', widgetId);
|
|
3477
|
+
if (canvasId && canvasesClient) {
|
|
3478
|
+
try {
|
|
3479
|
+
await canvasesClient.updateCanvas({
|
|
3480
|
+
id: canvasId,
|
|
3481
|
+
updateCanvasRequest: { primaryWidgetId: widgetId }
|
|
3482
|
+
});
|
|
3483
|
+
} catch (err) {
|
|
3484
|
+
console.warn('[CreatorApp] Failed to save primary_widget_id:', err);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
const widget = getWidgetById(widgetId);
|
|
3488
|
+
if (widget) notifyHostWidgetName(widget);
|
|
3489
|
+
if (embedded && typeof window !== 'undefined') {
|
|
3490
|
+
try {
|
|
3491
|
+
const url = new URL(window.location.href);
|
|
3492
|
+
if (url.searchParams.get('widget_id') !== widgetId) {
|
|
3493
|
+
url.searchParams.set('widget_id', widgetId);
|
|
3494
|
+
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
3495
|
+
}
|
|
3496
|
+
} catch (err) {
|
|
3497
|
+
console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
function handleSetPrimaryWidget(widgetId: string) {
|
|
3503
|
+
void persistPrimaryWidgetId(widgetId);
|
|
3504
|
+
}
|
|
3505
|
+
|
|
2869
3506
|
// Preview Modal State Variables
|
|
2870
3507
|
let previewIframe: HTMLIFrameElement | null = null;
|
|
2871
3508
|
const { DEV: isDevMode } = import.meta.env;
|
|
@@ -3325,7 +3962,7 @@
|
|
|
3325
3962
|
const getCanvasName = (c: any): string =>
|
|
3326
3963
|
c.persistenceKey || c.name || c.id?.substring(0, 8) || 'Untitled';
|
|
3327
3964
|
const hasCanvasContent = (c: any): boolean =>
|
|
3328
|
-
!!c.canvasContent;
|
|
3965
|
+
!!(c.canvasContent || c.canvas_content);
|
|
3329
3966
|
|
|
3330
3967
|
userCanvases = rows
|
|
3331
3968
|
.map((c: any) => ({
|
|
@@ -3367,12 +4004,22 @@
|
|
|
3367
4004
|
}
|
|
3368
4005
|
|
|
3369
4006
|
// Load canvas shapes first, then hydrate widget metadata from shapes + canvas_id
|
|
3370
|
-
const { widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
4007
|
+
const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
3371
4008
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
3372
4009
|
await maybeAutoRestoreWidgetShapes();
|
|
3373
4010
|
// Backfill widget name + thumbnail on shapes that were saved before those
|
|
3374
4011
|
// fields existed on WidgetShape (fixes "Widget" placeholder on old canvases).
|
|
3375
4012
|
backfillWidgetShapeMetadata();
|
|
4013
|
+
if (loaded && canvasRef) {
|
|
4014
|
+
const primaryId =
|
|
4015
|
+
canvasPrimaryWidgetId ||
|
|
4016
|
+
canvasRef.getPrimaryWidgetId?.() ||
|
|
4017
|
+
selectedWidgetId;
|
|
4018
|
+
if (primaryId && !isConvertOrDraftPanelOpen()) {
|
|
4019
|
+
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
4020
|
+
canvasRef.selectWidgetShape(primaryId);
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
3376
4023
|
} catch (error) {
|
|
3377
4024
|
console.error('Widget Creator: Load canvases error:', error);
|
|
3378
4025
|
const defaultError = 'Failed to load canvases';
|
|
@@ -3714,34 +4361,30 @@
|
|
|
3714
4361
|
selectedWidgetId = null;
|
|
3715
4362
|
selectedWidget = null;
|
|
3716
4363
|
beginBlankCanvasSession('site New widget');
|
|
3717
|
-
console.log('[CreatorApp]
|
|
4364
|
+
console.log('[CreatorApp] New widget: blank canvas only — widget record is created on Convert (F3)');
|
|
3718
4365
|
const clientsReady = await waitForAuthenticatedCanvasClient();
|
|
3719
4366
|
if (!clientsReady) {
|
|
3720
|
-
console.error('[CreatorApp] Auth/clients not ready —
|
|
4367
|
+
console.error('[CreatorApp] Auth/clients not ready — blank canvas without new canvas row');
|
|
3721
4368
|
} else {
|
|
3722
4369
|
const newCanvasId = await createNewCanvas({ skipSaveCurrent: true });
|
|
3723
4370
|
if (!newCanvasId) {
|
|
3724
4371
|
showToast('error', 'Could not create a blank canvas', {
|
|
3725
|
-
description: '
|
|
4372
|
+
description: 'Try reloading the page to get a fresh canvas.',
|
|
3726
4373
|
duration: 8000
|
|
3727
4374
|
});
|
|
4375
|
+
} else {
|
|
4376
|
+
// Replace `new_widget=1` with the concrete canvas_id so a page refresh
|
|
4377
|
+
// reloads THIS canvas (and its sketch) instead of re-running the
|
|
4378
|
+
// new-widget bootstrap and creating yet another blank canvas row.
|
|
4379
|
+
syncEmbeddedCanvasUrl(newCanvasId);
|
|
3728
4380
|
}
|
|
3729
4381
|
}
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
} else {
|
|
3735
|
-
canvasRef?.onClearAllObjects();
|
|
3736
|
-
relinkSelectedWidgetShape();
|
|
3737
|
-
}
|
|
3738
|
-
lastSavedCanvasObjectCount = canvasRef ? 1 : 0;
|
|
4382
|
+
// F3: New-widget flow starts with an EMPTY canvas — no widget record, no
|
|
4383
|
+
// WidgetShape, no auto-opened Widget Details. The widget record is created
|
|
4384
|
+
// only when the user sketches something and clicks "Convert to Widget".
|
|
4385
|
+
lastSavedCanvasObjectCount = 0;
|
|
3739
4386
|
markCanvasStateLoaded();
|
|
3740
4387
|
isLoadingWidgets = false;
|
|
3741
|
-
if (selectedWidgetId) {
|
|
3742
|
-
console.log('[CreatorApp] Opening Widget Details for new Untitled widget');
|
|
3743
|
-
await openWidgetDetailsPanel(selectedWidgetId);
|
|
3744
|
-
}
|
|
3745
4388
|
setTimeout(() => {
|
|
3746
4389
|
ignoreIncomingCanvasLoads = false;
|
|
3747
4390
|
console.log('[CreatorApp] New-widget canvas lock released');
|
|
@@ -3929,6 +4572,16 @@
|
|
|
3929
4572
|
|
|
3930
4573
|
/** Re-create widget shapes when canvas_content is empty but widgets exist for canvas_id. */
|
|
3931
4574
|
async function maybeAutoRestoreWidgetShapes(): Promise<void> {
|
|
4575
|
+
// F3: New-widget sessions start intentionally empty — never auto-restore
|
|
4576
|
+
// shapes for the freshly created Untitled widget.
|
|
4577
|
+
if (didForceCreateNewWidget) {
|
|
4578
|
+
canvasShapesMissingWarning = false;
|
|
4579
|
+
return;
|
|
4580
|
+
}
|
|
4581
|
+
if (!isCanvasStateLoaded) {
|
|
4582
|
+
console.log('[CanvasDebug] maybeAutoRestore skipped — canvas JSON not loaded yet');
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
3932
4585
|
if (lastSavedCanvasObjectCount !== 0 || widgets.length === 0) {
|
|
3933
4586
|
canvasShapesMissingWarning = false;
|
|
3934
4587
|
return;
|
|
@@ -4170,22 +4823,97 @@
|
|
|
4170
4823
|
return initialWidgetId ?? null;
|
|
4171
4824
|
}
|
|
4172
4825
|
|
|
4826
|
+
/**
|
|
4827
|
+
* Canvas-only embed deep-link (My widgets / canvas without widget_id):
|
|
4828
|
+
* read canvases.primary_widget_id so Creator can open that widget panel.
|
|
4829
|
+
*/
|
|
4830
|
+
async function resolvePrimaryWidgetIdForCanvas(targetCanvasId: string): Promise<string | null> {
|
|
4831
|
+
if (!canvasesClient || !targetCanvasId?.trim()) return null;
|
|
4832
|
+
try {
|
|
4833
|
+
const canvasRecord = await canvasesClient.getCanvas({ id: targetCanvasId });
|
|
4834
|
+
const record =
|
|
4835
|
+
(canvasRecord as { data?: Record<string, unknown> })?.data ??
|
|
4836
|
+
(canvasRecord as Record<string, unknown>);
|
|
4837
|
+
const raw =
|
|
4838
|
+
(record as { primaryWidgetId?: unknown })?.primaryWidgetId ??
|
|
4839
|
+
(record as { primary_widget_id?: unknown })?.primary_widget_id ??
|
|
4840
|
+
null;
|
|
4841
|
+
const primaryId = typeof raw === 'string' && raw.trim() ? raw.trim() : null;
|
|
4842
|
+
if (primaryId) {
|
|
4843
|
+
canvasPrimaryWidgetId = primaryId;
|
|
4844
|
+
console.log(
|
|
4845
|
+
'[CreatorApp] Resolved primary widget for canvas',
|
|
4846
|
+
targetCanvasId.substring(0, 8),
|
|
4847
|
+
'→',
|
|
4848
|
+
primaryId.substring(0, 8)
|
|
4849
|
+
);
|
|
4850
|
+
} else {
|
|
4851
|
+
console.log(
|
|
4852
|
+
'[CreatorApp] No primary_widget_id on canvas',
|
|
4853
|
+
targetCanvasId.substring(0, 8)
|
|
4854
|
+
);
|
|
4855
|
+
}
|
|
4856
|
+
return primaryId;
|
|
4857
|
+
} catch (err) {
|
|
4858
|
+
console.warn(
|
|
4859
|
+
'[CreatorApp] Failed to resolve primary_widget_id for canvas',
|
|
4860
|
+
targetCanvasId.substring(0, 8),
|
|
4861
|
+
err
|
|
4862
|
+
);
|
|
4863
|
+
return null;
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4173
4867
|
/** Fetch widget by id from site deep-link (My widgets → Edit / New Composition). */
|
|
4174
4868
|
async function ensureEmbeddedWidgetDeepLink(): Promise<void> {
|
|
4175
4869
|
syncEmbeddedDeepLinkDismissTracking();
|
|
4176
|
-
|
|
4177
|
-
if (!embedded || !deepLinkWidgetId || !widgetsClient || !hasValidToken) return;
|
|
4870
|
+
if (!embedded || !widgetsClient || !hasValidToken) return;
|
|
4178
4871
|
if (embeddedDeepLinkPanelDismissed) {
|
|
4872
|
+
const dismissedId = resolveEmbeddedDeepLinkWidgetId();
|
|
4179
4873
|
console.log(
|
|
4180
|
-
'[CreatorApp] Skip embedded deep-link
|
|
4181
|
-
|
|
4874
|
+
'[CreatorApp] Skip embedded deep-link select — user dismissed panel:',
|
|
4875
|
+
dismissedId ? dismissedId.substring(0, 8) : '(none)'
|
|
4182
4876
|
);
|
|
4183
4877
|
return;
|
|
4184
4878
|
}
|
|
4185
|
-
//
|
|
4879
|
+
// Select primary widget once per Creator mount. Do not auto-open Widget Details.
|
|
4186
4880
|
if (embeddedDeepLinkAutoOpenDone) {
|
|
4187
4881
|
return;
|
|
4188
4882
|
}
|
|
4883
|
+
if (isConvertOrDraftPanelOpen()) {
|
|
4884
|
+
console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
|
|
4885
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
4886
|
+
return;
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
|
|
4890
|
+
// Canvas-only deep-link: no ?widget_id= / initialWidgetId — select canvases.primary_widget_id.
|
|
4891
|
+
if (!deepLinkWidgetId && canvasesClient) {
|
|
4892
|
+
const targetCanvasId =
|
|
4893
|
+
(typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
|
|
4894
|
+
(typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null) ||
|
|
4895
|
+
(typeof routeCanvasId === 'string' && routeCanvasId.trim() ? routeCanvasId.trim() : null);
|
|
4896
|
+
if (targetCanvasId) {
|
|
4897
|
+
deepLinkWidgetId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
|
|
4898
|
+
if (embeddedDeepLinkPanelDismissed || embeddedDeepLinkAutoOpenDone) {
|
|
4899
|
+
return;
|
|
4900
|
+
}
|
|
4901
|
+
// Align address-bar widget_id so header / dismiss tracking match the opened panel.
|
|
4902
|
+
if (deepLinkWidgetId && typeof window !== 'undefined') {
|
|
4903
|
+
try {
|
|
4904
|
+
const url = new URL(window.location.href);
|
|
4905
|
+
if (url.searchParams.get('widget_id') !== deepLinkWidgetId) {
|
|
4906
|
+
url.searchParams.set('widget_id', deepLinkWidgetId);
|
|
4907
|
+
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
4908
|
+
syncEmbeddedDeepLinkDismissTracking();
|
|
4909
|
+
}
|
|
4910
|
+
} catch (err) {
|
|
4911
|
+
console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
if (!deepLinkWidgetId) return;
|
|
4189
4917
|
|
|
4190
4918
|
const requestGen = ++embeddedDeepLinkRequestGen;
|
|
4191
4919
|
|
|
@@ -4205,8 +4933,8 @@
|
|
|
4205
4933
|
embeddedInitialStepPending = false;
|
|
4206
4934
|
}
|
|
4207
4935
|
embeddedDeepLinkAutoOpenDone = true;
|
|
4208
|
-
if (
|
|
4209
|
-
|
|
4936
|
+
if (canvasRef && deepLinkWidgetId) {
|
|
4937
|
+
canvasRef.selectWidgetShape(deepLinkWidgetId);
|
|
4210
4938
|
}
|
|
4211
4939
|
return;
|
|
4212
4940
|
}
|
|
@@ -4245,11 +4973,12 @@
|
|
|
4245
4973
|
}
|
|
4246
4974
|
|
|
4247
4975
|
embeddedDeepLinkAutoOpenDone = true;
|
|
4248
|
-
//
|
|
4249
|
-
|
|
4250
|
-
await selectWidget(target);
|
|
4976
|
+
// Select the primary widget (and its canvas shape) — do not open Widget Details.
|
|
4977
|
+
await selectWidget(target, { skipLoadWidget: true, skipPlaceShape: true });
|
|
4251
4978
|
if (!deepLinkOpenStillValid()) return;
|
|
4252
|
-
|
|
4979
|
+
if (canvasRef) {
|
|
4980
|
+
canvasRef.selectWidgetShape(target.id);
|
|
4981
|
+
}
|
|
4253
4982
|
if (embeddedInitialStepPending && initialStep) {
|
|
4254
4983
|
widgetDetailsStep = initialStep;
|
|
4255
4984
|
embeddedInitialStepPending = false;
|
|
@@ -4278,7 +5007,8 @@
|
|
|
4278
5007
|
*/
|
|
4279
5008
|
async function syncWidgetsWithCanvas(widgetIdsFromCanvasContent: string[] = []) {
|
|
4280
5009
|
if (!canvasId) {
|
|
4281
|
-
|
|
5010
|
+
// Widget-only or canvas-primary deep-link (ensure resolves primary when widget_id absent).
|
|
5011
|
+
if (embedded) {
|
|
4282
5012
|
await ensureEmbeddedWidgetDeepLink();
|
|
4283
5013
|
}
|
|
4284
5014
|
return;
|
|
@@ -4291,7 +5021,7 @@
|
|
|
4291
5021
|
if (shapeWidgetIds.length > 0) {
|
|
4292
5022
|
await mergeWidgetsLinkedOnCanvas(shapeWidgetIds);
|
|
4293
5023
|
}
|
|
4294
|
-
if (embedded
|
|
5024
|
+
if (embedded) {
|
|
4295
5025
|
await ensureEmbeddedWidgetDeepLink();
|
|
4296
5026
|
}
|
|
4297
5027
|
}
|
|
@@ -4312,7 +5042,7 @@
|
|
|
4312
5042
|
loadCurrentCanvas();
|
|
4313
5043
|
}
|
|
4314
5044
|
|
|
4315
|
-
async function selectWidget(widget: WidgetSummary | null, options: { skipLoadWidget?: boolean; skipEditorContextReset?: boolean } = {}) {
|
|
5045
|
+
async function selectWidget(widget: WidgetSummary | null, options: { skipLoadWidget?: boolean; skipEditorContextReset?: boolean; skipPlaceShape?: boolean } = {}) {
|
|
4316
5046
|
try {
|
|
4317
5047
|
// Save the current widget's prompt before switching away
|
|
4318
5048
|
if (selectedWidgetId) {
|
|
@@ -4367,7 +5097,7 @@
|
|
|
4367
5097
|
syncFileBrowserUploadConfigForCreator();
|
|
4368
5098
|
|
|
4369
5099
|
// Select the linked canvas shape; if none exists, create one (empty canvas + Untitled).
|
|
4370
|
-
if (widget?.id && canvasRef) {
|
|
5100
|
+
if (widget?.id && canvasRef && !options.skipPlaceShape) {
|
|
4371
5101
|
const found = canvasRef.selectWidgetShape(widget.id);
|
|
4372
5102
|
if (!found) {
|
|
4373
5103
|
const anyW = widget as unknown as { thumbnail_url?: string | null; thumbnailUrl?: string | null };
|
|
@@ -4566,10 +5296,11 @@
|
|
|
4566
5296
|
if (!headSha) return;
|
|
4567
5297
|
if (currentPanelLastCommit && commitsMatch(currentPanelLastCommit, headSha)) return;
|
|
4568
5298
|
console.log(`[Version] Repo HEAD probe for "${widgetId}": ${headSha.substring(0, 8)} (was ${currentPanelLastCommit?.substring(0, 8) ?? 'null'})`);
|
|
4569
|
-
patchPanelSnapshot(widgetId, { lastCommitId: headSha });
|
|
5299
|
+
patchPanelSnapshot(widgetId, { lastCommitId: headSha, hasGeneratedCode: true });
|
|
4570
5300
|
saveCommitIdForWidget(widgetId, headSha);
|
|
4571
5301
|
if (widgetId === focusedPanelWidgetId) {
|
|
4572
5302
|
lastCommitId = headSha;
|
|
5303
|
+
hasGeneratedCode = true;
|
|
4573
5304
|
}
|
|
4574
5305
|
}
|
|
4575
5306
|
|
|
@@ -4632,7 +5363,9 @@
|
|
|
4632
5363
|
lastPublishedVersion,
|
|
4633
5364
|
currentRepositoryId,
|
|
4634
5365
|
selectedWidgetJsPath,
|
|
5366
|
+
...(lastCommitId ? { hasGeneratedCode: true } : {}),
|
|
4635
5367
|
});
|
|
5368
|
+
if (lastCommitId) hasGeneratedCode = true;
|
|
4636
5369
|
if (lastPublishedVersion !== null) {
|
|
4637
5370
|
// Load history against the specific widget id (not selectedWidgetId) so the
|
|
4638
5371
|
// panel snapshot is guaranteed to be marked publishHistoryResolved even if the
|
|
@@ -4646,10 +5379,9 @@
|
|
|
4646
5379
|
publishHistoryResolvedForWidgetId: widget.id,
|
|
4647
5380
|
});
|
|
4648
5381
|
}
|
|
4649
|
-
//
|
|
4650
|
-
//
|
|
4651
|
-
|
|
4652
|
-
if (lastPublishedVersion !== null) {
|
|
5382
|
+
// Reconcile GitLab HEAD whenever we have a repo — unpublished widgets
|
|
5383
|
+
// also need lastCommitId so Create preview is not stuck on "No Code Yet".
|
|
5384
|
+
if (currentRepositoryId) {
|
|
4653
5385
|
void reconcileWidgetHeadCommit(widget.id);
|
|
4654
5386
|
}
|
|
4655
5387
|
}
|
|
@@ -4919,11 +5651,7 @@
|
|
|
4919
5651
|
|
|
4920
5652
|
const status = (op?.status || '').toLowerCase();
|
|
4921
5653
|
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
|
4922
|
-
|
|
4923
|
-
settle('reject', new Error(op?.error || 'Operation failed.'));
|
|
4924
|
-
} else {
|
|
4925
|
-
settle('resolve', op);
|
|
4926
|
-
}
|
|
5654
|
+
settle('resolve', op);
|
|
4927
5655
|
return;
|
|
4928
5656
|
}
|
|
4929
5657
|
|
|
@@ -4962,11 +5690,7 @@
|
|
|
4962
5690
|
signal
|
|
4963
5691
|
})
|
|
4964
5692
|
.then((finalOp) => {
|
|
4965
|
-
|
|
4966
|
-
settle('reject', new Error(finalOp?.error || 'Operation failed.'));
|
|
4967
|
-
} else {
|
|
4968
|
-
settle('resolve', finalOp as OperationStatus);
|
|
4969
|
-
}
|
|
5693
|
+
settle('resolve', finalOp as OperationStatus);
|
|
4970
5694
|
})
|
|
4971
5695
|
.catch((streamError) => {
|
|
4972
5696
|
if (settled || signal?.aborted) return;
|
|
@@ -5238,9 +5962,23 @@
|
|
|
5238
5962
|
* Returns the default prompt if no stored prompt is found.
|
|
5239
5963
|
*/
|
|
5240
5964
|
async function loadPromptForWidget(widgetId: string) {
|
|
5965
|
+
// Draft convert panels keep their prefilled prompt — nothing stored to load.
|
|
5966
|
+
if (isDraftConvertId(widgetId)) return;
|
|
5241
5967
|
try {
|
|
5968
|
+
// Guard against the convert-flow race: selectWidget fires this loader
|
|
5969
|
+
// without awaiting it; the convert flow then sets the real prompt +
|
|
5970
|
+
// screenshot right after `await selectWidget(...)` returns. The draft
|
|
5971
|
+
// read below resolves LATER and would clobber the fresh prompt with
|
|
5972
|
+
// DEFAULT_PROMPT (feedback b). Skip when a newer load for this widget
|
|
5973
|
+
// has already been superseded by an explicit prompt set.
|
|
5974
|
+
loadPromptGeneration += 1;
|
|
5975
|
+
const loadGeneration = loadPromptGeneration;
|
|
5242
5976
|
// Restore from the durable L1 draft store (legacy localStorage fallback)
|
|
5243
5977
|
const draft = await readWidgetDraft(widgetId);
|
|
5978
|
+
if (loadGeneration !== loadPromptGeneration) {
|
|
5979
|
+
console.log('[Prompt] Skipped stale load for widget', widgetId.substring(0, 8), '— a newer prompt state was applied');
|
|
5980
|
+
return;
|
|
5981
|
+
}
|
|
5244
5982
|
codeGenerationPrompt = draft?.promptText ?? DEFAULT_PROMPT;
|
|
5245
5983
|
pendingChatDraftText = draft?.chatDraft ?? '';
|
|
5246
5984
|
|
|
@@ -5383,14 +6121,20 @@
|
|
|
5383
6121
|
}
|
|
5384
6122
|
}
|
|
5385
6123
|
|
|
5386
|
-
/**
|
|
6124
|
+
/** Vision capability comes from GET /models (`supportsVision`), not from provider-name prefixes. */
|
|
5387
6125
|
function modelSupportsVisionClient(modelId: string): boolean {
|
|
5388
6126
|
if (!modelId) return false;
|
|
6127
|
+
const listed = availableModels.find((m) => m.id === modelId);
|
|
6128
|
+
if (listed && typeof listed.supportsVision === 'boolean') {
|
|
6129
|
+
return listed.supportsVision;
|
|
6130
|
+
}
|
|
5389
6131
|
if (modelId.startsWith('deepseek-')) return false;
|
|
5390
6132
|
return (
|
|
5391
6133
|
modelId.startsWith('openai-') ||
|
|
5392
6134
|
modelId.startsWith('gemini-') ||
|
|
5393
|
-
modelId.startsWith('anthropic-')
|
|
6135
|
+
modelId.startsWith('anthropic-') ||
|
|
6136
|
+
modelId.startsWith('zhipu-') ||
|
|
6137
|
+
modelId.startsWith('glm-')
|
|
5394
6138
|
);
|
|
5395
6139
|
}
|
|
5396
6140
|
|
|
@@ -5474,20 +6218,34 @@
|
|
|
5474
6218
|
const quiet = !!options.quiet;
|
|
5475
6219
|
console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
|
|
5476
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
|
+
|
|
5477
6226
|
// Quiet canvas-thumbnail sync must never force-open Widget Details.
|
|
5478
|
-
//
|
|
6227
|
+
// Convert drafts capture from the canvas shape, never by opening a panel.
|
|
5479
6228
|
if (!openWidgetPanelIds.includes(widgetId)) {
|
|
5480
|
-
if (quiet) {
|
|
5481
|
-
|
|
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
|
+
}
|
|
5482
6237
|
return;
|
|
5483
6238
|
}
|
|
6239
|
+
// Manual "Refresh Widget Preview" always opens the panel so the iframe exists.
|
|
6240
|
+
console.log('[handleRefreshScreenshot] Opening Widget Details for manual preview refresh:', widgetId.substring(0, 8));
|
|
5484
6241
|
await openWidgetDetailsPanel(widgetId);
|
|
5485
6242
|
await new Promise(r => setTimeout(r, 300));
|
|
5486
6243
|
}
|
|
5487
6244
|
|
|
5488
6245
|
const panelRef = getWidgetDetailsRef(widgetId);
|
|
5489
|
-
|
|
5490
|
-
|
|
6246
|
+
const iframe = panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null;
|
|
6247
|
+
if (!iframe?.contentWindow) {
|
|
6248
|
+
console.log('[handleRefreshScreenshot] Preview iframe not ready — skipping capture');
|
|
5491
6249
|
if (!quiet) {
|
|
5492
6250
|
showToast('error', 'No valid preview available', { description: 'Generate code first to capture a screenshot.', duration: 5000 });
|
|
5493
6251
|
}
|
|
@@ -5498,14 +6256,6 @@
|
|
|
5498
6256
|
showToast('info', 'Capturing widget screenshot...', { duration: 3000 });
|
|
5499
6257
|
}
|
|
5500
6258
|
|
|
5501
|
-
const iframe = panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null;
|
|
5502
|
-
if (!iframe?.contentWindow) {
|
|
5503
|
-
if (!quiet) {
|
|
5504
|
-
showToast('error', 'No preview iframe available', { description: 'Generate code first to capture a screenshot.', duration: 5000 });
|
|
5505
|
-
}
|
|
5506
|
-
return;
|
|
5507
|
-
}
|
|
5508
|
-
|
|
5509
6259
|
try {
|
|
5510
6260
|
const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
|
|
5511
6261
|
const timeout = setTimeout(() => {
|
|
@@ -5632,19 +6382,46 @@ Requirements:
|
|
|
5632
6382
|
fromChat?: boolean;
|
|
5633
6383
|
widgetId?: string;
|
|
5634
6384
|
compileFixOnly?: boolean;
|
|
6385
|
+
/** Recreate GitLab repo when the previous turn failed with missing repository. */
|
|
6386
|
+
forceRecreateRepo?: boolean;
|
|
5635
6387
|
/** CDN upload row IDs captured before chat clearDraft. */
|
|
5636
6388
|
attachmentIds?: string[];
|
|
5637
6389
|
/** Image URLs / data URLs captured before chat clearDraft. */
|
|
5638
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
|
+
}>;
|
|
5639
6400
|
} = {}) {
|
|
5640
6401
|
const targetWidgetId = options.widgetId ?? selectedWidgetId;
|
|
5641
6402
|
if (!targetWidgetId) return;
|
|
5642
6403
|
|
|
6404
|
+
// Convert UX v2: a draft panel's first Generate (button / confirm dialog)
|
|
6405
|
+
// finalizes the conversion — creates widget + repo, replaces the sketch,
|
|
6406
|
+
// and repoints this call at the real widget id. Chat sends are finalized
|
|
6407
|
+
// in handleChatSendMessage before reaching this point.
|
|
6408
|
+
let codegenTargetWidgetId = targetWidgetId;
|
|
6409
|
+
if (isDraftConvertId(targetWidgetId)) {
|
|
6410
|
+
const panelPromptForDraft =
|
|
6411
|
+
targetWidgetId === focusedPanelWidgetId
|
|
6412
|
+
? codeGenerationPrompt
|
|
6413
|
+
: getPanelState(targetWidgetId).codeGenerationPrompt;
|
|
6414
|
+
const draftPrompt = options.prompt || panelPromptForDraft;
|
|
6415
|
+
const finalizedWidgetId = await finalizeDraftConversion(targetWidgetId, draftPrompt);
|
|
6416
|
+
if (!finalizedWidgetId) return;
|
|
6417
|
+
codegenTargetWidgetId = finalizedWidgetId;
|
|
6418
|
+
}
|
|
6419
|
+
|
|
5643
6420
|
// Use override prompt if provided (e.g. from Fix Errors button), otherwise use panel prompt
|
|
5644
6421
|
const panelPrompt =
|
|
5645
|
-
|
|
6422
|
+
codegenTargetWidgetId === focusedPanelWidgetId
|
|
5646
6423
|
? codeGenerationPrompt
|
|
5647
|
-
: getPanelState(
|
|
6424
|
+
: getPanelState(codegenTargetWidgetId).codeGenerationPrompt;
|
|
5648
6425
|
const promptSource = options.prompt || panelPrompt;
|
|
5649
6426
|
const trimmedPrompt = promptSource.trim();
|
|
5650
6427
|
if (trimmedPrompt.length < 10) {
|
|
@@ -5664,7 +6441,7 @@ Requirements:
|
|
|
5664
6441
|
: 'panel prompt';
|
|
5665
6442
|
console.log(
|
|
5666
6443
|
'[generateCode] Widget',
|
|
5667
|
-
|
|
6444
|
+
codegenTargetWidgetId.substring(0, 8),
|
|
5668
6445
|
'| prompt source:',
|
|
5669
6446
|
promptSourceLabel,
|
|
5670
6447
|
'| Length:',
|
|
@@ -5672,17 +6449,17 @@ Requirements:
|
|
|
5672
6449
|
);
|
|
5673
6450
|
|
|
5674
6451
|
if (forceMockReady) {
|
|
5675
|
-
generatingWidgetIds = { ...generatingWidgetIds, [
|
|
5676
|
-
isGeneratingCode =
|
|
5677
|
-
setCrossTabGenerating(true,
|
|
5678
|
-
patchCodegenUiState(
|
|
6452
|
+
generatingWidgetIds = { ...generatingWidgetIds, [codegenTargetWidgetId]: true };
|
|
6453
|
+
isGeneratingCode = codegenTargetWidgetId === focusedPanelWidgetId;
|
|
6454
|
+
setCrossTabGenerating(true, codegenTargetWidgetId);
|
|
6455
|
+
patchCodegenUiState(codegenTargetWidgetId, { generateCodeStatus: 'Generating mock code...' });
|
|
5679
6456
|
setTimeout(() => {
|
|
5680
|
-
const { [
|
|
6457
|
+
const { [codegenTargetWidgetId]: _done, ...rest } = generatingWidgetIds;
|
|
5681
6458
|
generatingWidgetIds = rest;
|
|
5682
6459
|
isGeneratingCode = !!(focusedPanelWidgetId && generatingWidgetIds[focusedPanelWidgetId]);
|
|
5683
|
-
setCrossTabGenerating(false,
|
|
5684
|
-
patchCodegenUiState(
|
|
5685
|
-
if (
|
|
6460
|
+
setCrossTabGenerating(false, codegenTargetWidgetId);
|
|
6461
|
+
patchCodegenUiState(codegenTargetWidgetId, { generateCodeStatus: 'Mock code generation complete!' });
|
|
6462
|
+
if (codegenTargetWidgetId === focusedPanelWidgetId) hasChangesToSave = true;
|
|
5686
6463
|
showToast('success', 'Mock Code Generated', {
|
|
5687
6464
|
description: 'This is a simulation. No actual code was generated.',
|
|
5688
6465
|
duration: 3000
|
|
@@ -5694,10 +6471,12 @@ Requirements:
|
|
|
5694
6471
|
await performCodeGeneration(trimmedPrompt, {
|
|
5695
6472
|
queued: false,
|
|
5696
6473
|
fromChat: options.fromChat,
|
|
5697
|
-
widgetId:
|
|
6474
|
+
widgetId: codegenTargetWidgetId,
|
|
5698
6475
|
compileFixOnly: options.compileFixOnly,
|
|
6476
|
+
forceRecreateRepo: options.forceRecreateRepo,
|
|
5699
6477
|
attachmentIds: options.attachmentIds,
|
|
5700
6478
|
promptImagesOverride: options.promptImagesOverride,
|
|
6479
|
+
persistFileAttachments: options.persistFileAttachments,
|
|
5701
6480
|
});
|
|
5702
6481
|
}
|
|
5703
6482
|
|
|
@@ -5716,8 +6495,17 @@ Requirements:
|
|
|
5716
6495
|
fromChat?: boolean;
|
|
5717
6496
|
widgetId?: string;
|
|
5718
6497
|
compileFixOnly?: boolean;
|
|
6498
|
+
forceRecreateRepo?: boolean;
|
|
5719
6499
|
attachmentIds?: string[];
|
|
5720
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
|
+
}>;
|
|
5721
6509
|
} = {},
|
|
5722
6510
|
) {
|
|
5723
6511
|
const generationWidgetId = options.widgetId ?? selectedWidgetId;
|
|
@@ -5740,7 +6528,6 @@ Requirements:
|
|
|
5740
6528
|
lastHarnessChatLine = '';
|
|
5741
6529
|
|
|
5742
6530
|
// Show user prompt in chat when triggered outside Chat (confirm dialog / Generate Now).
|
|
5743
|
-
// DB write happens here too — not on widget select (so "Edit Prompt First" stays editable).
|
|
5744
6531
|
if (panelChat && !options.fromChat) {
|
|
5745
6532
|
const snapImages =
|
|
5746
6533
|
generationWidgetId === focusedPanelWidgetId ? promptImages : panelSnap.promptImages;
|
|
@@ -5754,8 +6541,7 @@ Requirements:
|
|
|
5754
6541
|
: undefined;
|
|
5755
6542
|
panelChat.addUserMessage(prompt, codegenImageAttachments);
|
|
5756
6543
|
}
|
|
5757
|
-
|
|
5758
|
-
|
|
6544
|
+
|
|
5759
6545
|
isGeneratingCode = generationWidgetId === focusedPanelWidgetId;
|
|
5760
6546
|
setCrossTabGenerating(true, generationWidgetId);
|
|
5761
6547
|
lastGenerationPrompt = prompt;
|
|
@@ -5766,6 +6552,16 @@ Requirements:
|
|
|
5766
6552
|
? 'Processing queued code generation request...'
|
|
5767
6553
|
: 'Preparing code generation...',
|
|
5768
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
|
+
}
|
|
5769
6565
|
|
|
5770
6566
|
// Keep preview tab active so user sees the "Generating..." overlay.
|
|
5771
6567
|
// Console logs are still accessible via the Console tab button.
|
|
@@ -5775,22 +6571,52 @@ Requirements:
|
|
|
5775
6571
|
console.log('Widget Creator: Starting code generation for widget:', generationWidgetId);
|
|
5776
6572
|
|
|
5777
6573
|
try {
|
|
5778
|
-
const generationWidget =
|
|
5779
|
-
|
|
5780
|
-
?
|
|
5781
|
-
|
|
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
|
+
}
|
|
5782
6606
|
console.log('Widget Creator: Repository resolved for code generation:', repoId);
|
|
5783
6607
|
|
|
5784
6608
|
if (!repoId) {
|
|
6609
|
+
const missingRepoError = 'Unable to resolve repository for this widget.';
|
|
5785
6610
|
patchCodegenUiState(generationWidgetId, {
|
|
5786
6611
|
generateCodeStatus: '❌ Repository is required to generate code.',
|
|
5787
6612
|
lastGenerationFailed: true,
|
|
5788
|
-
lastGenerationError:
|
|
6613
|
+
lastGenerationError: missingRepoError,
|
|
5789
6614
|
});
|
|
5790
6615
|
showToast('error', 'Missing repository', {
|
|
5791
|
-
description:
|
|
6616
|
+
description: missingRepoError,
|
|
5792
6617
|
duration: 5000
|
|
5793
6618
|
});
|
|
6619
|
+
pushCodegenFailureToChat(generationWidgetId, missingRepoError);
|
|
5794
6620
|
return;
|
|
5795
6621
|
}
|
|
5796
6622
|
|
|
@@ -5813,13 +6639,23 @@ Requirements:
|
|
|
5813
6639
|
: generationWidgetId === focusedPanelWidgetId
|
|
5814
6640
|
? promptImages
|
|
5815
6641
|
: panelSnap.promptImages;
|
|
6642
|
+
// Drop invalid entries (empty strings, non-data-URLs) so a failed capture
|
|
6643
|
+
// can never silently become a vision-less generation request.
|
|
6644
|
+
panelPromptImages = panelPromptImages.filter(
|
|
6645
|
+
(url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
|
|
6646
|
+
);
|
|
5816
6647
|
const chatAttachmentUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
|
|
5817
6648
|
const chatUploadIds = [
|
|
5818
6649
|
...(options.attachmentIds || []),
|
|
5819
6650
|
...(panelChat?.getAttachmentUploadIds?.() ?? []),
|
|
5820
6651
|
].filter((id, index, arr) => id && arr.indexOf(id) === index);
|
|
5821
6652
|
if (chatAttachmentUrls.length > 0 && !(options.promptImagesOverride?.length)) {
|
|
5822
|
-
|
|
6653
|
+
const fromChat = chatAttachmentUrls.filter(
|
|
6654
|
+
(url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
|
|
6655
|
+
);
|
|
6656
|
+
if (fromChat.length > 0) {
|
|
6657
|
+
panelPromptImages = fromChat;
|
|
6658
|
+
}
|
|
5823
6659
|
}
|
|
5824
6660
|
let imagesToSend: string[] | undefined;
|
|
5825
6661
|
let attachmentIdsToSend: string[] | undefined;
|
|
@@ -5830,11 +6666,19 @@ Requirements:
|
|
|
5830
6666
|
imagesToSend = await Promise.all(
|
|
5831
6667
|
panelPromptImages.map((url) => compressImageDataUrlForCodegen(url)),
|
|
5832
6668
|
);
|
|
6669
|
+
imagesToSend = imagesToSend.filter(
|
|
6670
|
+
(url) => url.startsWith('data:image/') && url.length > 100,
|
|
6671
|
+
);
|
|
5833
6672
|
patchPanelSnapshot(generationWidgetId, { promptImages: imagesToSend });
|
|
5834
6673
|
if (generationWidgetId === focusedPanelWidgetId) {
|
|
5835
6674
|
promptImages = imagesToSend;
|
|
5836
6675
|
savePromptImagesToSession();
|
|
5837
6676
|
}
|
|
6677
|
+
} else if (promptImpliesVisionReference(prompt)) {
|
|
6678
|
+
console.warn(
|
|
6679
|
+
'[WidgetCreator] Codegen proceeding WITHOUT image although prompt references one — widget',
|
|
6680
|
+
generationWidgetId,
|
|
6681
|
+
);
|
|
5838
6682
|
}
|
|
5839
6683
|
let coderModelForRequest = selectedModelId || defaultModelId;
|
|
5840
6684
|
if (imagesToSend?.length || attachmentIdsToSend?.length) {
|
|
@@ -5850,8 +6694,11 @@ Requirements:
|
|
|
5850
6694
|
const visionFallback = modelSupportsVisionClient(defaultModelId)
|
|
5851
6695
|
? defaultModelId
|
|
5852
6696
|
: 'openai-gpt-5.4';
|
|
6697
|
+
console.log('[WidgetCreator] Coder vision fallback (request only, dropdown unchanged)', {
|
|
6698
|
+
from: coderModelForRequest,
|
|
6699
|
+
to: visionFallback,
|
|
6700
|
+
});
|
|
5853
6701
|
coderModelForRequest = visionFallback;
|
|
5854
|
-
userSelectedModelId = visionFallback;
|
|
5855
6702
|
const visionModel = availableModels.find((m) => m.id === visionFallback);
|
|
5856
6703
|
showToast('info', `Coder: ${visionModel?.name ?? visionFallback} (vision)`, {
|
|
5857
6704
|
description: 'Cu imagini atașate folosim automat un model vision — același ca pe server.',
|
|
@@ -5866,6 +6713,18 @@ Requirements:
|
|
|
5866
6713
|
});
|
|
5867
6714
|
}
|
|
5868
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
|
+
|
|
5869
6728
|
// Clear unsent draft/attachments once payload is captured — prevents restore on widget return
|
|
5870
6729
|
clearPromptDraftForWidget(generationWidgetId);
|
|
5871
6730
|
|
|
@@ -5875,7 +6734,7 @@ Requirements:
|
|
|
5875
6734
|
panelChat?.getConversationId?.() ?? panelSnap.currentChatConversationId ?? currentChatConversationId;
|
|
5876
6735
|
const plannerModelIdForRequest = options.compileFixOnly
|
|
5877
6736
|
? undefined
|
|
5878
|
-
: resolvePlannerModelIdForGeneration(prompt);
|
|
6737
|
+
: resolvePlannerModelIdForGeneration(prompt, coderModelForRequest);
|
|
5879
6738
|
const runtimeLogsForRequest = collectRuntimeLogsForCodegen(generationWidgetId);
|
|
5880
6739
|
const response = await agentClient.agentCodeGeneratePost({
|
|
5881
6740
|
generateCodeRequest: {
|
|
@@ -6021,6 +6880,11 @@ Requirements:
|
|
|
6021
6880
|
hasGeneratedCode: true,
|
|
6022
6881
|
...(completedCommitSha ? { lastCommitId: completedCommitSha } : {}),
|
|
6023
6882
|
});
|
|
6883
|
+
if (isFocusedPanel && completedCommitSha) {
|
|
6884
|
+
lastCommitId = completedCommitSha;
|
|
6885
|
+
hasGeneratedCode = true;
|
|
6886
|
+
}
|
|
6887
|
+
panelDisplayRevision += 1;
|
|
6024
6888
|
console.log(
|
|
6025
6889
|
'[Publish] Codegen completed — pendingPublishAfterCodegen=true',
|
|
6026
6890
|
generationWidgetId.substring(0, 8),
|
|
@@ -6118,6 +6982,21 @@ Requirements:
|
|
|
6118
6982
|
liveContentValues = {};
|
|
6119
6983
|
}
|
|
6120
6984
|
|
|
6985
|
+
// Final chat message regardless of panel focus — without this, a
|
|
6986
|
+
// closed/unfocused panel never receives the terminal update and the
|
|
6987
|
+
// chat stays on the last progress line (e.g. stuck at 80%).
|
|
6988
|
+
panelChat?.addAssistantMessage(
|
|
6989
|
+
completedPreviewBuildErr
|
|
6990
|
+
? `Code saved${completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : ''}, but preview build failed:\n${completedPreviewBuildErr}\n\nRebuilding preview…`
|
|
6991
|
+
: `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
|
|
6992
|
+
true
|
|
6993
|
+
);
|
|
6994
|
+
if (!completedPreviewBuildErr) {
|
|
6995
|
+
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
6996
|
+
`Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
|
|
6997
|
+
);
|
|
6998
|
+
}
|
|
6999
|
+
|
|
6121
7000
|
setTimeout(async () => {
|
|
6122
7001
|
if (!getWidgetDetailsRef(generationWidgetId)) return;
|
|
6123
7002
|
const activePanelRef = getWidgetDetailsRef(generationWidgetId);
|
|
@@ -6149,8 +7028,9 @@ Requirements:
|
|
|
6149
7028
|
);
|
|
6150
7029
|
setTimeout(async () => {
|
|
6151
7030
|
await panelChat?.reloadConversationMessages?.();
|
|
6152
|
-
|
|
6153
|
-
|
|
7031
|
+
panelChat?.markWidgetHeadAtLatestCommit?.();
|
|
7032
|
+
}, 2500);
|
|
7033
|
+
setTimeout(() => handleRefreshScreenshot(generationWidgetId, { quiet: true }), 3000);
|
|
6154
7034
|
} else {
|
|
6155
7035
|
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
6156
7036
|
`Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
|
|
@@ -6222,6 +7102,7 @@ Requirements:
|
|
|
6222
7102
|
});
|
|
6223
7103
|
clearActiveOperationId(); // Clear persisted operationId on error
|
|
6224
7104
|
showToast('error', 'Code generation error', { description: message, duration: 6000 });
|
|
7105
|
+
pushCodegenFailureToChat(generationWidgetId, message);
|
|
6225
7106
|
} finally {
|
|
6226
7107
|
const { [generationWidgetId]: _done, ...restFinally } = generatingWidgetIds;
|
|
6227
7108
|
generatingWidgetIds = restFinally;
|
|
@@ -6238,6 +7119,15 @@ Requirements:
|
|
|
6238
7119
|
return (import.meta.env.VITE_CDN_URL || 'https://cdn.widgetic.com').replace(/\/+$/, '');
|
|
6239
7120
|
}
|
|
6240
7121
|
|
|
7122
|
+
/** Published artifact URL for embed/test page — CDN, never the API gateway origin. */
|
|
7123
|
+
function publishedWidgetHtmlUrl(widgetId: string, version: number, compositionId?: string | null): string {
|
|
7124
|
+
const base = `${publishedCdnBase()}/widgets/${widgetId}/v${version}/widget.html`;
|
|
7125
|
+
if (compositionId) {
|
|
7126
|
+
return `${base}?compositionId=${compositionId}`;
|
|
7127
|
+
}
|
|
7128
|
+
return base;
|
|
7129
|
+
}
|
|
7130
|
+
|
|
6241
7131
|
/** Canonical full CDN URL for a published widget artifact (never a bare pathname). */
|
|
6242
7132
|
function resolvePublishedJsPath(widgetId: string, version: number, artifactUrl?: string | null): string {
|
|
6243
7133
|
const cdnBase = publishedCdnBase();
|
|
@@ -6557,6 +7447,7 @@ Requirements:
|
|
|
6557
7447
|
|
|
6558
7448
|
// Cross-tab operation sync: listen for localStorage changes from other tabs
|
|
6559
7449
|
window.addEventListener('storage', handleCrossTabStorageEvent);
|
|
7450
|
+
window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
6560
7451
|
|
|
6561
7452
|
// Clean up cross-tab flags when tab is closed/refreshed
|
|
6562
7453
|
window.addEventListener('beforeunload', () => {
|
|
@@ -6650,10 +7541,16 @@ Requirements:
|
|
|
6650
7541
|
// Cross-tab cleanup: remove flags so other tabs don't stay blocked
|
|
6651
7542
|
if (typeof window !== 'undefined') {
|
|
6652
7543
|
window.removeEventListener('storage', handleCrossTabStorageEvent);
|
|
7544
|
+
window.removeEventListener('widgetic:session-extended', handleSiteSessionExtended);
|
|
6653
7545
|
window.removeEventListener('click', handleCanvasDropdownOutsideClick);
|
|
6654
7546
|
window.removeEventListener(HOST_WIDGET_RENAMED_EVENT, handleHostWidgetRenamedEvent);
|
|
6655
7547
|
window.removeEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
|
|
6656
7548
|
window.removeEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
|
|
7549
|
+
window.removeEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
|
|
7550
|
+
window.removeEventListener(HOST_TOGGLE_ADVANCED_EVENT, handleHostToggleAdvancedEvent);
|
|
7551
|
+
window.removeEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
|
|
7552
|
+
window.removeEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
|
|
7553
|
+
window.removeEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
|
|
6657
7554
|
for (const wId of Object.keys(generatingWidgetIds)) {
|
|
6658
7555
|
setCrossTabGenerating(false, wId);
|
|
6659
7556
|
}
|
|
@@ -6678,8 +7575,14 @@ Requirements:
|
|
|
6678
7575
|
window.addEventListener(HOST_WIDGET_RENAMED_EVENT, handleHostWidgetRenamedEvent);
|
|
6679
7576
|
window.addEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
|
|
6680
7577
|
window.addEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
|
|
6681
|
-
|
|
7578
|
+
window.addEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
|
|
7579
|
+
window.addEventListener(HOST_TOGGLE_ADVANCED_EVENT, handleHostToggleAdvancedEvent);
|
|
7580
|
+
window.addEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
|
|
7581
|
+
window.addEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
|
|
7582
|
+
window.addEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
|
|
6682
7583
|
emitCreatorDebugChanged();
|
|
7584
|
+
emitCreatorWireframeChanged();
|
|
7585
|
+
emitCreatorAdvancedChanged();
|
|
6683
7586
|
});
|
|
6684
7587
|
|
|
6685
7588
|
// Capture-phase Escape: close lightbox before Canvas can deselect → close WidgetDetails
|
|
@@ -6794,7 +7697,12 @@ Requirements:
|
|
|
6794
7697
|
|
|
6795
7698
|
function getChatMethodsForWidget(widgetId: string | null | undefined) {
|
|
6796
7699
|
if (!widgetId) return chatMethods ?? null;
|
|
6797
|
-
|
|
7700
|
+
const instanceKey = panelInstanceKeyByWidgetId[widgetId] ?? widgetId;
|
|
7701
|
+
return (
|
|
7702
|
+
chatMethodsByWidgetId[widgetId] ??
|
|
7703
|
+
chatMethodsByWidgetId[instanceKey] ??
|
|
7704
|
+
(widgetId === selectedWidgetId ? chatMethods ?? null : null)
|
|
7705
|
+
);
|
|
6798
7706
|
}
|
|
6799
7707
|
|
|
6800
7708
|
function getOperationStageMessage(metadata?: Record<string, unknown>): string | null {
|
|
@@ -7067,7 +7975,7 @@ Requirements:
|
|
|
7067
7975
|
compositionSchemasLoading,
|
|
7068
7976
|
compositionSchemasError,
|
|
7069
7977
|
schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
|
|
7070
|
-
chatContainerHeight: focusedSnap?.chatContainerHeight ??
|
|
7978
|
+
chatContainerHeight: focusedSnap?.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7071
7979
|
panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
|
|
7072
7980
|
};
|
|
7073
7981
|
}
|
|
@@ -7108,7 +8016,7 @@ Requirements:
|
|
|
7108
8016
|
publishHistoryResolved: false,
|
|
7109
8017
|
publishHistoryResolvedForWidgetId: null,
|
|
7110
8018
|
pendingPublishAfterCodegen: false,
|
|
7111
|
-
hasGeneratedCode:
|
|
8019
|
+
hasGeneratedCode: !!loadCommitIdForWidget(widgetId),
|
|
7112
8020
|
liveDesignValues: {},
|
|
7113
8021
|
liveContentValues: {},
|
|
7114
8022
|
liveContentItems: [],
|
|
@@ -7120,7 +8028,7 @@ Requirements:
|
|
|
7120
8028
|
compositionSchemasLoading: false,
|
|
7121
8029
|
compositionSchemasError: null,
|
|
7122
8030
|
schemaLoadStartedAt: null,
|
|
7123
|
-
chatContainerHeight:
|
|
8031
|
+
chatContainerHeight: 550,
|
|
7124
8032
|
panelPreviewCompileError: null,
|
|
7125
8033
|
};
|
|
7126
8034
|
}
|
|
@@ -7146,16 +8054,18 @@ Requirements:
|
|
|
7146
8054
|
const snapPublishReady =
|
|
7147
8055
|
snap.publishHistoryResolved && snap.publishHistoryResolvedForWidgetId === widgetId;
|
|
7148
8056
|
const livePublishMatchesPanel = live.publishHistoryResolvedForWidgetId === widgetId;
|
|
7149
|
-
const resolvedHead = resolvePanelHeadCommitId(widgetId, snap, live);
|
|
8057
|
+
const resolvedHead = isDraftConvertId(widgetId) ? null : resolvePanelHeadCommitId(widgetId, snap, live);
|
|
7150
8058
|
return {
|
|
7151
8059
|
...snap,
|
|
7152
8060
|
...live,
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
8061
|
+
lastCommitId: isDraftConvertId(widgetId) ? null : resolvedHead,
|
|
8062
|
+
hasGeneratedCode: isDraftConvertId(widgetId)
|
|
8063
|
+
? false
|
|
8064
|
+
: !!(live.hasGeneratedCode || snap.hasGeneratedCode || resolvedHead),
|
|
7157
8065
|
pendingPublishAfterCodegen: !!(snap.pendingPublishAfterCodegen || live.pendingPublishAfterCodegen),
|
|
7158
|
-
lastPublishedVersion:
|
|
8066
|
+
lastPublishedVersion: isDraftConvertId(widgetId)
|
|
8067
|
+
? null
|
|
8068
|
+
: (live.lastPublishedVersion ?? snap.lastPublishedVersion),
|
|
7159
8069
|
currentRepositoryId: live.currentRepositoryId ?? snap.currentRepositoryId,
|
|
7160
8070
|
selectedWidgetJsPath: live.selectedWidgetJsPath ?? snap.selectedWidgetJsPath,
|
|
7161
8071
|
// Once publish history loaded into this panel's snapshot, prefer it over stale globals.
|
|
@@ -7174,7 +8084,7 @@ Requirements:
|
|
|
7174
8084
|
: livePublishMatchesPanel
|
|
7175
8085
|
? live.publishHistoryResolvedForWidgetId
|
|
7176
8086
|
: snap.publishHistoryResolvedForWidgetId,
|
|
7177
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
8087
|
+
chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7178
8088
|
compositionDesignSchema: resolvedDesignSchema,
|
|
7179
8089
|
compositionContentSchema: resolvedContentSchema,
|
|
7180
8090
|
compositionSchemasLoading: schemaLoading,
|
|
@@ -7192,7 +8102,7 @@ Requirements:
|
|
|
7192
8102
|
...snap,
|
|
7193
8103
|
lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
|
|
7194
8104
|
hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
|
|
7195
|
-
chatContainerHeight: snap.chatContainerHeight ??
|
|
8105
|
+
chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
|
|
7196
8106
|
compositionDesignSchema: resolvedDesignSchema,
|
|
7197
8107
|
compositionContentSchema: resolvedContentSchema,
|
|
7198
8108
|
compositionSchemasError: resolvedSchemaError,
|
|
@@ -7482,6 +8392,22 @@ Requirements:
|
|
|
7482
8392
|
selectedWidget = widget;
|
|
7483
8393
|
saveSelectedWidgetId(widgetId);
|
|
7484
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
|
+
}
|
|
7485
8411
|
if (widget?.id && canvasRef) {
|
|
7486
8412
|
canvasRef.selectWidgetShape(widget.id);
|
|
7487
8413
|
}
|
|
@@ -7498,20 +8424,57 @@ Requirements:
|
|
|
7498
8424
|
retryStalePanelSchemaLoads();
|
|
7499
8425
|
}
|
|
7500
8426
|
|
|
8427
|
+
function positionOpenDetailsPanel(widgetId: string): void {
|
|
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
|
+
}
|
|
8435
|
+
getWidgetDetailsRef(widgetId)?.positionBesideShape?.(rect);
|
|
8436
|
+
}
|
|
8437
|
+
|
|
7501
8438
|
async function openWidgetDetailsPanel(widgetId: string): Promise<void> {
|
|
8439
|
+
const openGen = ++widgetDetailsOpenGen;
|
|
8440
|
+
widgetDetailsOpenTargetId = widgetId;
|
|
7502
8441
|
const deepLinkId = resolveEmbeddedDeepLinkWidgetId();
|
|
7503
8442
|
if (embedded && deepLinkId && widgetId === deepLinkId) {
|
|
7504
8443
|
embeddedDeepLinkPanelDismissed = false;
|
|
7505
8444
|
}
|
|
8445
|
+
// One details panel at a time — close any other open widget first.
|
|
8446
|
+
for (const openId of [...openWidgetPanelIds]) {
|
|
8447
|
+
if (openId !== widgetId) {
|
|
8448
|
+
closeWidgetDetailsPanel(openId, { fromShapeSwitch: true });
|
|
8449
|
+
}
|
|
8450
|
+
}
|
|
7506
8451
|
const isNewPanel = !openWidgetPanelIds.includes(widgetId);
|
|
7507
8452
|
if (isNewPanel) {
|
|
7508
8453
|
ensurePanelState(widgetId);
|
|
7509
8454
|
if (focusedPanelWidgetId) {
|
|
7510
8455
|
captureFocusedPanelSnapshot();
|
|
7511
8456
|
}
|
|
8457
|
+
if (!panelInstanceKeyByWidgetId[widgetId]) {
|
|
8458
|
+
panelInstanceKeyByWidgetId = {
|
|
8459
|
+
...panelInstanceKeyByWidgetId,
|
|
8460
|
+
[widgetId]: widgetId,
|
|
8461
|
+
};
|
|
8462
|
+
}
|
|
7512
8463
|
openWidgetPanelIds = [...openWidgetPanelIds, widgetId];
|
|
7513
8464
|
}
|
|
7514
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
|
+
}
|
|
8474
|
+
if (openGen !== widgetDetailsOpenGen || widgetDetailsOpenTargetId !== widgetId) {
|
|
8475
|
+
console.log('[CreatorApp] Skip stale Widget Details open:', widgetId?.substring(0, 8));
|
|
8476
|
+
return;
|
|
8477
|
+
}
|
|
7515
8478
|
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
7516
8479
|
// WidgetDetails is dynamically imported — wait for the module, then for bind:this refs.
|
|
7517
8480
|
// Only force-open the requested panel; other open panels keep their own show state.
|
|
@@ -7522,8 +8485,10 @@ Requirements:
|
|
|
7522
8485
|
continue;
|
|
7523
8486
|
}
|
|
7524
8487
|
const ref = getWidgetDetailsRef(widgetId);
|
|
8488
|
+
if (openGen !== widgetDetailsOpenGen || widgetDetailsOpenTargetId !== widgetId) return;
|
|
7525
8489
|
if (ref) {
|
|
7526
8490
|
ref.openWidgetDetails();
|
|
8491
|
+
positionOpenDetailsPanel(widgetId);
|
|
7527
8492
|
ensurePanelEditorData(widgetId);
|
|
7528
8493
|
break;
|
|
7529
8494
|
}
|
|
@@ -7532,20 +8497,8 @@ Requirements:
|
|
|
7532
8497
|
if (!getWidgetDetailsRef(widgetId)) {
|
|
7533
8498
|
console.warn('[CreatorApp] WidgetDetails ref not ready after open retries:', widgetId?.substring(0, 8));
|
|
7534
8499
|
}
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
if (openId === widgetId) continue;
|
|
7538
|
-
const other = getWidgetDetailsRef(openId);
|
|
7539
|
-
if (other && !other.getShowWidgetDetails?.()) {
|
|
7540
|
-
other.openWidgetDetails();
|
|
7541
|
-
ensurePanelEditorData(openId);
|
|
7542
|
-
}
|
|
7543
|
-
}
|
|
7544
|
-
openWidgetPanelIds.forEach((id, index) => {
|
|
7545
|
-
setTimeout(() => {
|
|
7546
|
-
void scheduleWidgetSchemaLoad(id);
|
|
7547
|
-
}, index * 150);
|
|
7548
|
-
});
|
|
8500
|
+
void scheduleWidgetSchemaLoad(widgetId);
|
|
8501
|
+
ensurePanelEditorData(widgetId);
|
|
7549
8502
|
ensureAllOpenPanelSchemasLoaded();
|
|
7550
8503
|
healStuckPanelSchemaLoadingFlags();
|
|
7551
8504
|
retryStalePanelSchemaLoads();
|
|
@@ -7561,6 +8514,7 @@ Requirements:
|
|
|
7561
8514
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
7562
8515
|
let missing = false;
|
|
7563
8516
|
for (const openId of openWidgetPanelIds) {
|
|
8517
|
+
if (focusedPanelWidgetId && openId !== focusedPanelWidgetId) continue;
|
|
7564
8518
|
const ref = getWidgetDetailsRef(openId);
|
|
7565
8519
|
if (ref) {
|
|
7566
8520
|
ref.openWidgetDetails();
|
|
@@ -7579,6 +8533,51 @@ Requirements:
|
|
|
7579
8533
|
}
|
|
7580
8534
|
}
|
|
7581
8535
|
|
|
8536
|
+
/**
|
|
8537
|
+
* Convert UX: first Generate creates the DB widget. Keep Widget Details mounted
|
|
8538
|
+
* by swapping the panel id in place (same {#each} key) instead of close → open.
|
|
8539
|
+
*/
|
|
8540
|
+
function migrateOpenPanelWidgetId(fromId: string, toId: string): void {
|
|
8541
|
+
if (!fromId || !toId || fromId === toId) return;
|
|
8542
|
+
const instanceKey = panelInstanceKeyByWidgetId[fromId] ?? fromId;
|
|
8543
|
+
const nextKeys = { ...panelInstanceKeyByWidgetId };
|
|
8544
|
+
delete nextKeys[fromId];
|
|
8545
|
+
nextKeys[toId] = instanceKey;
|
|
8546
|
+
panelInstanceKeyByWidgetId = nextKeys;
|
|
8547
|
+
|
|
8548
|
+
const draftZ = panelZIndexByWidgetId[fromId];
|
|
8549
|
+
const { [fromId]: _z, ...restZ } = panelZIndexByWidgetId;
|
|
8550
|
+
panelZIndexByWidgetId =
|
|
8551
|
+
draftZ !== undefined ? { ...restZ, [toId]: draftZ } : restZ;
|
|
8552
|
+
|
|
8553
|
+
const draftRef = widgetDetailsRefs[fromId];
|
|
8554
|
+
const { [fromId]: _r, ...restRefs } = widgetDetailsRefs;
|
|
8555
|
+
widgetDetailsRefs = draftRef ? { ...restRefs, [toId]: draftRef } : restRefs;
|
|
8556
|
+
|
|
8557
|
+
const draftChat = chatMethodsByWidgetId[fromId];
|
|
8558
|
+
if (draftChat) {
|
|
8559
|
+
const { [fromId]: _c, ...restChat } = chatMethodsByWidgetId;
|
|
8560
|
+
chatMethodsByWidgetId = { ...restChat, [toId]: draftChat };
|
|
8561
|
+
if (focusedPanelWidgetId === toId || selectedWidgetId === toId) {
|
|
8562
|
+
chatMethods = draftChat;
|
|
8563
|
+
}
|
|
8564
|
+
}
|
|
8565
|
+
|
|
8566
|
+
const replaced = openWidgetPanelIds.map((id) => (id === fromId ? toId : id));
|
|
8567
|
+
openWidgetPanelIds = replaced.includes(toId) ? replaced : [...replaced, toId];
|
|
8568
|
+
if (focusedPanelWidgetId === fromId) focusedPanelWidgetId = toId;
|
|
8569
|
+
if (selectedWidgetId === fromId) selectedWidgetId = toId;
|
|
8570
|
+
if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
|
|
8571
|
+
if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
|
|
8572
|
+
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
8573
|
+
console.log(
|
|
8574
|
+
'[CreatorApp] Migrated Widget Details panel',
|
|
8575
|
+
fromId.substring(0, 16),
|
|
8576
|
+
'→',
|
|
8577
|
+
toId.substring(0, 8),
|
|
8578
|
+
);
|
|
8579
|
+
}
|
|
8580
|
+
|
|
7582
8581
|
function closeWidgetDetailsPanel(
|
|
7583
8582
|
widgetId: string,
|
|
7584
8583
|
options: { fromShapeSwitch?: boolean } = {},
|
|
@@ -7593,12 +8592,20 @@ Requirements:
|
|
|
7593
8592
|
embeddedDeepLinkPanelDismissed = true;
|
|
7594
8593
|
embeddedDeepLinkRequestGen++;
|
|
7595
8594
|
}
|
|
8595
|
+
if (!options.fromShapeSwitch && widgetDetailsOpenTargetId === widgetId) {
|
|
8596
|
+
widgetDetailsOpenTargetId = null;
|
|
8597
|
+
}
|
|
8598
|
+
if (detailsSessionWidgetId === widgetId) {
|
|
8599
|
+
detailsSessionWidgetId = null;
|
|
8600
|
+
}
|
|
7596
8601
|
getWidgetDetailsRef(widgetId)?.closeWidgetDetails({ silent: true });
|
|
7597
8602
|
openWidgetPanelIds = openWidgetPanelIds.filter((id) => id !== widgetId);
|
|
7598
8603
|
const { [widgetId]: _z, ...restZ } = panelZIndexByWidgetId;
|
|
7599
8604
|
panelZIndexByWidgetId = restZ;
|
|
7600
8605
|
const { [widgetId]: _removedRef, ...remainingRefs } = widgetDetailsRefs;
|
|
7601
8606
|
widgetDetailsRefs = remainingRefs;
|
|
8607
|
+
const { [widgetId]: _k, ...restKeys } = panelInstanceKeyByWidgetId;
|
|
8608
|
+
panelInstanceKeyByWidgetId = restKeys;
|
|
7602
8609
|
if (focusedPanelWidgetId === widgetId) {
|
|
7603
8610
|
const nextId = openWidgetPanelIds.at(-1) ?? null;
|
|
7604
8611
|
if (nextId) {
|
|
@@ -7620,21 +8627,26 @@ Requirements:
|
|
|
7620
8627
|
}
|
|
7621
8628
|
openWidgetPanelIds = [];
|
|
7622
8629
|
widgetDetailsRefs = {};
|
|
8630
|
+
panelInstanceKeyByWidgetId = {};
|
|
7623
8631
|
focusedPanelWidgetId = null;
|
|
7624
8632
|
panelZIndexByWidgetId = {};
|
|
8633
|
+
detailsSessionWidgetId = null;
|
|
8634
|
+
widgetDetailsOpenTargetId = null;
|
|
7625
8635
|
showWidgetDetails = false;
|
|
7626
8636
|
}
|
|
7627
8637
|
|
|
7628
8638
|
/** Active panel for chat/codegen — falls back to selected widget's ref. */
|
|
7629
8639
|
$: widgetDetails = focusedPanelWidgetId
|
|
7630
|
-
?
|
|
8640
|
+
? getWidgetDetailsRef(focusedPanelWidgetId)
|
|
7631
8641
|
: selectedWidgetId
|
|
7632
|
-
?
|
|
8642
|
+
? getWidgetDetailsRef(selectedWidgetId)
|
|
7633
8643
|
: null;
|
|
7634
8644
|
|
|
7635
8645
|
function getWidgetDetailsRef(widgetId?: string | null): any {
|
|
7636
8646
|
const id = widgetId ?? focusedPanelWidgetId ?? selectedWidgetId;
|
|
7637
|
-
|
|
8647
|
+
if (!id) return null;
|
|
8648
|
+
const instanceKey = panelInstanceKeyByWidgetId[id] ?? id;
|
|
8649
|
+
return widgetDetailsRefs[instanceKey] ?? widgetDetailsRefs[id] ?? null;
|
|
7638
8650
|
}
|
|
7639
8651
|
|
|
7640
8652
|
// Widgets Panel sizing — dynamically adapts to auth panel height
|
|
@@ -7892,11 +8904,10 @@ Requirements:
|
|
|
7892
8904
|
// If the token expired during generation, we delayed logout to avoid disrupting the operation.
|
|
7893
8905
|
// Now that the operation is done, proceed with logout.
|
|
7894
8906
|
$: if (pendingTokenExpiryLogout && !isGeneratingCode && Object.keys(generatingWidgetIds).length === 0) {
|
|
7895
|
-
console.log('WidgetCreator:
|
|
8907
|
+
console.log('WidgetCreator: Token expired after operations — keeping canvas, prompting re-auth');
|
|
7896
8908
|
pendingTokenExpiryLogout = false;
|
|
7897
8909
|
tokenExpiryWarningShown = false;
|
|
7898
|
-
|
|
7899
|
-
authStatus = 'Token expired. Please login again.';
|
|
8910
|
+
authStatus = 'Session expired. Sign in again to keep saving.';
|
|
7900
8911
|
}
|
|
7901
8912
|
|
|
7902
8913
|
|
|
@@ -7978,31 +8989,89 @@ Requirements:
|
|
|
7978
8989
|
return [];
|
|
7979
8990
|
}
|
|
7980
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
|
+
|
|
7981
9008
|
/** Flatten schema-shaped content items to runtime values for preview postMessage. */
|
|
7982
9009
|
function flattenContentItemsForPreview(items: any[]): any[] {
|
|
7983
9010
|
return items.map((item) => {
|
|
7984
9011
|
if (!item || typeof item !== 'object') return item;
|
|
7985
9012
|
const flat: Record<string, unknown> = { id: item.id, title: item.title };
|
|
7986
|
-
|
|
7987
|
-
|
|
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
|
+
}
|
|
7988
9019
|
const props = item.properties;
|
|
7989
9020
|
if (Array.isArray(props)) {
|
|
7990
9021
|
for (const prop of props) {
|
|
7991
9022
|
const name = prop?.name || prop?.id;
|
|
7992
9023
|
if (!name) continue;
|
|
7993
|
-
if (
|
|
7994
|
-
|
|
7995
|
-
|
|
9024
|
+
if (flat[name] !== undefined && flat[name] !== '') continue;
|
|
9025
|
+
const picked = pickLiveContentValue(prop.value, prop.defaultValue);
|
|
9026
|
+
if (picked !== '') flat[name] = picked;
|
|
7996
9027
|
}
|
|
7997
9028
|
}
|
|
7998
|
-
for (const [key, value] of Object.entries(item)) {
|
|
7999
|
-
if (key === 'properties' || key === 'inputController') continue;
|
|
8000
|
-
if (flat[key] === undefined && value !== undefined) flat[key] = value;
|
|
8001
|
-
}
|
|
8002
9029
|
return flat;
|
|
8003
9030
|
});
|
|
8004
9031
|
}
|
|
8005
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
|
+
|
|
9042
|
+
function contentItemHasProperties(item: any): boolean {
|
|
9043
|
+
return Array.isArray(item?.properties) && item.properties.length > 0;
|
|
9044
|
+
}
|
|
9045
|
+
|
|
9046
|
+
/** New items from the editor arrive flat ({id, caption, image}) — reattach schema fields so the form stays editable. */
|
|
9047
|
+
function attachSchemaProperties(item: any, template: any): any {
|
|
9048
|
+
if (!item || typeof item !== 'object') return item;
|
|
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) => {
|
|
9055
|
+
const key = prop.id || prop.name;
|
|
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);
|
|
9061
|
+
return {
|
|
9062
|
+
...prop,
|
|
9063
|
+
value: picked
|
|
9064
|
+
};
|
|
9065
|
+
});
|
|
9066
|
+
return {
|
|
9067
|
+
...(templateHasProps ? JSON.parse(JSON.stringify(template)) : {}),
|
|
9068
|
+
...item,
|
|
9069
|
+
id: item.id,
|
|
9070
|
+
title: item.title || template?.title,
|
|
9071
|
+
properties: nextProperties
|
|
9072
|
+
};
|
|
9073
|
+
}
|
|
9074
|
+
|
|
8006
9075
|
function mergeContentItemsWithSchema(savedItems: any[], schemaItems: any[]): any[] {
|
|
8007
9076
|
if (!Array.isArray(schemaItems) || schemaItems.length === 0) {
|
|
8008
9077
|
return Array.isArray(savedItems) ? [...savedItems] : [];
|
|
@@ -8010,14 +9079,17 @@ Requirements:
|
|
|
8010
9079
|
if (!Array.isArray(savedItems) || savedItems.length === 0) {
|
|
8011
9080
|
return JSON.parse(JSON.stringify(schemaItems));
|
|
8012
9081
|
}
|
|
9082
|
+
const template = schemaItems.find(contentItemHasProperties) || schemaItems[0];
|
|
8013
9083
|
const byId = new Map(savedItems.map((item) => [item.id, item]));
|
|
8014
9084
|
const merged = schemaItems.map((schemaItem) => {
|
|
8015
9085
|
const saved = byId.get(schemaItem.id);
|
|
8016
|
-
|
|
9086
|
+
const clonedSchema = JSON.parse(JSON.stringify(schemaItem));
|
|
9087
|
+
if (!saved) return clonedSchema;
|
|
9088
|
+
return attachSchemaProperties({ ...clonedSchema, ...saved }, clonedSchema);
|
|
8017
9089
|
});
|
|
8018
9090
|
for (const savedItem of savedItems) {
|
|
8019
9091
|
if (!schemaItems.some((schemaItem) => schemaItem.id === savedItem.id)) {
|
|
8020
|
-
merged.push(savedItem);
|
|
9092
|
+
merged.push(attachSchemaProperties(savedItem, template));
|
|
8021
9093
|
}
|
|
8022
9094
|
}
|
|
8023
9095
|
return merged;
|
|
@@ -8244,8 +9316,26 @@ Requirements:
|
|
|
8244
9316
|
getLivePublishedCommitShaForPanel(widgetId)
|
|
8245
9317
|
?? getWidgetLastPublishCommitSha(widgetId);
|
|
8246
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
|
+
}
|
|
8247
9337
|
|
|
8248
|
-
// Known up-to-date: HEAD matches live
|
|
9338
|
+
// Known up-to-date: HEAD matches live.
|
|
8249
9339
|
if (headSha && liveSha && commitsMatch(headSha, liveSha)) {
|
|
8250
9340
|
return {
|
|
8251
9341
|
disabled: true,
|
|
@@ -8255,12 +9345,8 @@ Requirements:
|
|
|
8255
9345
|
};
|
|
8256
9346
|
}
|
|
8257
9347
|
|
|
8258
|
-
//
|
|
8259
|
-
if (
|
|
8260
|
-
pendingPublish
|
|
8261
|
-
|| (headSha && liveSha && !commitsMatch(headSha, liveSha))
|
|
8262
|
-
|| (headSha && !liveSha)
|
|
8263
|
-
) {
|
|
9348
|
+
// After a live version exists, enable vN+1 when HEAD is proven different.
|
|
9349
|
+
if (headSha && liveSha && !commitsMatch(headSha, liveSha)) {
|
|
8264
9350
|
return {
|
|
8265
9351
|
disabled: false,
|
|
8266
9352
|
label: `Publish new v${nextVersion}`,
|
|
@@ -8311,6 +9397,9 @@ Requirements:
|
|
|
8311
9397
|
function publishNeedsSaveFirstForPanel(widgetId: string): boolean {
|
|
8312
9398
|
const ps = getPanelDisplayState(widgetId);
|
|
8313
9399
|
const headSha = resolvePublishHeadCommitId(widgetId, ps);
|
|
9400
|
+
// Compiled preview / known code means Publish is allowed — don't nag "save first"
|
|
9401
|
+
// just because GitLab HEAD hasn't been probed yet.
|
|
9402
|
+
if (panelHasPublishableCode(widgetId, ps)) return false;
|
|
8314
9403
|
return (
|
|
8315
9404
|
!isPublishing &&
|
|
8316
9405
|
!generatingWidgetIds[widgetId] &&
|
|
@@ -8399,7 +9488,7 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
|
|
|
8399
9488
|
}
|
|
8400
9489
|
if (publishWidgetId) {
|
|
8401
9490
|
console.log('[Publish] Refreshing canvas widget screenshot after successful publish');
|
|
8402
|
-
setTimeout(() => handleRefreshScreenshot(publishWidgetId), 2000);
|
|
9491
|
+
setTimeout(() => handleRefreshScreenshot(publishWidgetId, { quiet: true }), 2000);
|
|
8403
9492
|
}
|
|
8404
9493
|
if (embedded && onPublishComplete && publishWidgetId) {
|
|
8405
9494
|
onPublishComplete({
|
|
@@ -8586,6 +9675,11 @@ $: if (lastPublishedVersion !== null && isHistoryOpen) {
|
|
|
8586
9675
|
// Runs reactively when selectedWidgetId + canvasRef + conversationsClient are ready.
|
|
8587
9676
|
let autoPromptSyncedForWidgetId: string | null = null;
|
|
8588
9677
|
$: if (selectedWidgetId && canvasRef && conversationsClient && messagesClient && autoPromptSyncedForWidgetId !== selectedWidgetId) {
|
|
9678
|
+
// Draft convert panels have no DB widget — the prompt sync (and any DB
|
|
9679
|
+
// conversation shell) happens in finalizeDraftConversion on first send.
|
|
9680
|
+
if (isDraftConvertId(selectedWidgetId)) {
|
|
9681
|
+
autoPromptSyncedForWidgetId = selectedWidgetId;
|
|
9682
|
+
} else {
|
|
8589
9683
|
autoPromptSyncedForWidgetId = selectedWidgetId;
|
|
8590
9684
|
const widgetIdSync = selectedWidgetId;
|
|
8591
9685
|
const widgetSync = getWidgetById(widgetIdSync);
|
|
@@ -8634,6 +9728,7 @@ $: if (selectedWidgetId && canvasRef && conversationsClient && messagesClient &&
|
|
|
8634
9728
|
}
|
|
8635
9729
|
}).catch(() => {});
|
|
8636
9730
|
}
|
|
9731
|
+
}
|
|
8637
9732
|
}
|
|
8638
9733
|
|
|
8639
9734
|
// Show "Confirm Generation" dialog for converted widgets that have a prompt + repo
|
|
@@ -8642,6 +9737,11 @@ $: if (selectedWidgetId && canvasRef && conversationsClient && messagesClient &&
|
|
|
8642
9737
|
// Uses setTimeout to allow async prompt loading to finish before evaluating.
|
|
8643
9738
|
let autoGenDialogCheckedForWidgetId: string | null = null;
|
|
8644
9739
|
$: if (selectedWidgetId && autoGenDialogCheckedForWidgetId !== selectedWidgetId) {
|
|
9740
|
+
// Draft convert panels: chat is already prefilled with the convert prompt —
|
|
9741
|
+
// the confirm dialog would be redundant and targets a non-existent widget.
|
|
9742
|
+
if (isDraftConvertId(selectedWidgetId)) {
|
|
9743
|
+
autoGenDialogCheckedForWidgetId = selectedWidgetId;
|
|
9744
|
+
} else {
|
|
8645
9745
|
autoGenDialogCheckedForWidgetId = selectedWidgetId;
|
|
8646
9746
|
const capturedWidgetId = selectedWidgetId;
|
|
8647
9747
|
setTimeout(() => {
|
|
@@ -8664,6 +9764,7 @@ $: if (selectedWidgetId && autoGenDialogCheckedForWidgetId !== selectedWidgetId)
|
|
|
8664
9764
|
console.log('[AutoGenDialog] Conditions not met:', { hasRepo, hasNoCode, hasPreview, hasPrompt, lastCommitId, lastPublishedVersion });
|
|
8665
9765
|
}
|
|
8666
9766
|
}, 1500);
|
|
9767
|
+
}
|
|
8667
9768
|
}
|
|
8668
9769
|
|
|
8669
9770
|
// Auto-save prompt text to the durable draft when it changes (debounced 500ms)
|
|
@@ -8783,6 +9884,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
8783
9884
|
hasValidToken = false;
|
|
8784
9885
|
inlinePanelView = null;
|
|
8785
9886
|
selectedWidget = null;
|
|
9887
|
+
if (typeof window !== 'undefined') {
|
|
9888
|
+
window.location.href = '/login';
|
|
9889
|
+
}
|
|
8786
9890
|
}
|
|
8787
9891
|
|
|
8788
9892
|
// decodeJWT imported from ./pageHelpers.ts
|
|
@@ -8799,11 +9903,12 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
8799
9903
|
|
|
8800
9904
|
// if the decoded token has an expiration field, update the token status
|
|
8801
9905
|
if (decoded && decoded.exp) {
|
|
8802
|
-
// calculate the token time left
|
|
8803
9906
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
8804
|
-
|
|
9907
|
+
const expUnix = decoded.exp;
|
|
9908
|
+
tokenTimeLeft = expUnix - currentTime;
|
|
8805
9909
|
tokenExpired = tokenTimeLeft <= 0;
|
|
8806
|
-
|
|
9910
|
+
// API calls must follow the real JWT, never the sessionBannerTest clock.
|
|
9911
|
+
hasValidToken = decoded.exp - currentTime > 0;
|
|
8807
9912
|
|
|
8808
9913
|
// log the token expiration status
|
|
8809
9914
|
if (tokenExpired) {
|
|
@@ -8851,33 +9956,26 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
8851
9956
|
// If there are active operations (generating, loading preview), defer logout
|
|
8852
9957
|
// and show a warning instead of abruptly cutting off the operation
|
|
8853
9958
|
function checkTokenExpiration() {
|
|
9959
|
+
updateTokenStatus();
|
|
8854
9960
|
if (currentUserToken) {
|
|
8855
|
-
if (
|
|
8856
|
-
// Check if there are active operations that would be disrupted by logout
|
|
9961
|
+
if (tokenExpired || tokenTimeLeft <= 0) {
|
|
8857
9962
|
const hasActiveOperations = isGeneratingCode || (showPreviewModal && previewLoading);
|
|
8858
9963
|
|
|
8859
9964
|
if (hasActiveOperations) {
|
|
8860
|
-
// Don't logout yet — defer until operations complete
|
|
8861
9965
|
pendingTokenExpiryLogout = true;
|
|
8862
|
-
|
|
8863
9966
|
if (!tokenExpiryWarningShown) {
|
|
8864
9967
|
tokenExpiryWarningShown = true;
|
|
8865
|
-
console.warn('WidgetCreator.checkTokenExpiration: Token expired but active operations in progress —
|
|
8866
|
-
authStatus = '
|
|
8867
|
-
showToast('warning', 'Session expiring', {
|
|
8868
|
-
description: 'Your login token has expired. You will be logged out after the current operation completes.',
|
|
8869
|
-
duration: 8000
|
|
8870
|
-
});
|
|
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.';
|
|
8871
9970
|
}
|
|
8872
9971
|
return false;
|
|
8873
9972
|
}
|
|
8874
9973
|
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
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
|
+
}
|
|
8881
9979
|
return false;
|
|
8882
9980
|
}
|
|
8883
9981
|
}
|
|
@@ -9032,8 +10130,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9032
10130
|
|
|
9033
10131
|
const panelRef = getWidgetDetailsRef(widgetId);
|
|
9034
10132
|
const isDetailsOpen = openWidgetPanelIds.includes(widgetId) && (panelRef?.getShowWidgetDetails() ?? false);
|
|
9035
|
-
if (isDetailsOpen
|
|
9036
|
-
|
|
10133
|
+
if (isDetailsOpen) {
|
|
10134
|
+
if (focusedPanelWidgetId !== widgetId) {
|
|
10135
|
+
focusWidgetPanel(widgetId);
|
|
10136
|
+
}
|
|
10137
|
+
console.log('[WidgetCreator] Same widget details already open, skipping reload:', widgetId.substring(0, 8));
|
|
9037
10138
|
return;
|
|
9038
10139
|
}
|
|
9039
10140
|
const widget = getWidgetById(widgetId);
|
|
@@ -9046,6 +10147,24 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9046
10147
|
});
|
|
9047
10148
|
return;
|
|
9048
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
|
+
|
|
9049
10168
|
console.log('[WidgetCreator] Widget Image clicked, opening widget details panel:', widgetId);
|
|
9050
10169
|
|
|
9051
10170
|
// Explicit user intent — cancel any in-flight deep-link auto-open and allow this panel.
|
|
@@ -9200,7 +10319,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9200
10319
|
/**
|
|
9201
10320
|
* Called when a widget shape is about to be deleted from canvas.
|
|
9202
10321
|
* Step 1: Confirm removing the shape from canvas.
|
|
9203
|
-
* 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.
|
|
9204
10324
|
* Returns true to proceed with canvas shape deletion, false to cancel.
|
|
9205
10325
|
*/
|
|
9206
10326
|
async function handleWidgetShapeDeleting(widgetId: string): Promise<boolean> {
|
|
@@ -9213,7 +10333,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9213
10333
|
|
|
9214
10334
|
if (!result.proceed) return false;
|
|
9215
10335
|
|
|
9216
|
-
|
|
10336
|
+
if (result.deleteFromDb) {
|
|
10337
|
+
await deleteWidgetFromDb(widgetId);
|
|
10338
|
+
} else {
|
|
10339
|
+
showToast('info', 'Shape removed from canvas', { description: `Widget "${widgetName}" kept in database` });
|
|
10340
|
+
}
|
|
9217
10341
|
return true;
|
|
9218
10342
|
}
|
|
9219
10343
|
|
|
@@ -9269,14 +10393,31 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9269
10393
|
console.log('[WidgetCreator] Canvas ready — skipping JSON hydrate (new widget)');
|
|
9270
10394
|
lastSavedCanvasObjectCount = 0;
|
|
9271
10395
|
markCanvasStateLoaded();
|
|
9272
|
-
|
|
10396
|
+
// F3: New-widget sessions stay EMPTY — the WidgetShape is created only
|
|
10397
|
+
// by "Convert to Widget", never auto-placed on canvas ready.
|
|
10398
|
+
if (selectedWidget?.id && !didForceCreateNewWidget) {
|
|
9273
10399
|
placeSelectedWidgetShapeOnCanvas();
|
|
9274
10400
|
}
|
|
9275
10401
|
return;
|
|
9276
10402
|
}
|
|
9277
|
-
const { widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
10403
|
+
const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
10404
|
+
if (!loaded) {
|
|
10405
|
+
console.log('[WidgetCreator] Canvas ready — JSON load deferred until canvasId is resolved');
|
|
10406
|
+
return;
|
|
10407
|
+
}
|
|
9278
10408
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
9279
10409
|
await maybeAutoRestoreWidgetShapes();
|
|
10410
|
+
const primaryId =
|
|
10411
|
+
canvasPrimaryWidgetId ||
|
|
10412
|
+
canvasRef.getPrimaryWidgetId?.() ||
|
|
10413
|
+
selectedWidgetId;
|
|
10414
|
+
if (primaryId && canvasRef && !isConvertOrDraftPanelOpen()) {
|
|
10415
|
+
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
10416
|
+
const found = canvasRef.selectWidgetShape(primaryId);
|
|
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');
|
|
10420
|
+
}
|
|
9280
10421
|
}
|
|
9281
10422
|
|
|
9282
10423
|
/**
|
|
@@ -9364,12 +10505,22 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9364
10505
|
|
|
9365
10506
|
try {
|
|
9366
10507
|
if (canvasesClient) {
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
10508
|
+
let canvasContent: any = null;
|
|
10509
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
10510
|
+
const canvasResponse = await canvasesClient.getCanvasRaw({ id: canvasId });
|
|
10511
|
+
const canvasJson = await canvasResponse.raw.json();
|
|
10512
|
+
const canvasRecord = canvasJson?.data || canvasJson;
|
|
10513
|
+
if (loadEpoch !== canvasLoadEpoch || ignoreIncomingCanvasLoads) {
|
|
10514
|
+
console.log('[CanvasDebug] Ignoring stale getCanvas result', { loadEpoch, canvasLoadEpoch });
|
|
10515
|
+
return emptyResult;
|
|
10516
|
+
}
|
|
10517
|
+
canvasContent = canvasRecord?.canvasContent ?? canvasRecord?.canvas_content ?? null;
|
|
10518
|
+
if (canvasContent) break;
|
|
10519
|
+
console.log('[CanvasDebug] getCanvas returned no canvasContent, retry', attempt + 1);
|
|
10520
|
+
if (attempt < 2) {
|
|
10521
|
+
await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
|
|
10522
|
+
}
|
|
9371
10523
|
}
|
|
9372
|
-
const canvasContent = canvasRecord?.canvasContent;
|
|
9373
10524
|
|
|
9374
10525
|
if (canvasContent) {
|
|
9375
10526
|
const parsed =
|
|
@@ -9412,6 +10563,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9412
10563
|
}
|
|
9413
10564
|
markCanvasStateLoaded();
|
|
9414
10565
|
if (canvasShapesLoaded) canvasApiLoadError = null;
|
|
10566
|
+
if (objectCount > 0) canvasShapesMissingWarning = false;
|
|
9415
10567
|
isLoadCooldownActive = true;
|
|
9416
10568
|
setTimeout(() => { isLoadCooldownActive = false; }, 3000);
|
|
9417
10569
|
lastSavedCanvasObjectCount = objectCount;
|
|
@@ -9569,179 +10721,330 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9569
10721
|
}
|
|
9570
10722
|
|
|
9571
10723
|
/**
|
|
9572
|
-
*
|
|
9573
|
-
*
|
|
9574
|
-
*
|
|
9575
|
-
*
|
|
9576
|
-
*
|
|
9577
|
-
*
|
|
10724
|
+
* Convert UX v2 — clicking "Convert to Widget" no longer creates anything in
|
|
10725
|
+
* the DB. It only:
|
|
10726
|
+
* 1. keeps the sketch on the canvas (the caller groups it via onGroupSelection),
|
|
10727
|
+
* 2. opens a DRAFT WidgetDetails panel under a synthetic id,
|
|
10728
|
+
* 3. prefills the panel chat with the captured screenshot + CONVERT_PROMPT.
|
|
10729
|
+
* The widget record, component, repository, and shape replacement all happen
|
|
10730
|
+
* in finalizeDraftConversion() when the user sends the first prompt.
|
|
9578
10731
|
*/
|
|
9579
10732
|
async function handleConvertToWidget(payload: WidgetConversionPayload) {
|
|
9580
|
-
console.log('[WidgetCreator] Convert to widget:', payload);
|
|
10733
|
+
console.log('[WidgetCreator] Convert to widget (draft, v2):', payload);
|
|
9581
10734
|
|
|
9582
10735
|
if (!$userSession.user || !hasValidToken) {
|
|
9583
10736
|
showToast('error', 'Please login first');
|
|
9584
10737
|
return;
|
|
9585
10738
|
}
|
|
9586
|
-
|
|
9587
10739
|
if (isWidgetOperationInProgress) {
|
|
9588
10740
|
console.warn('[WidgetCreator] Another widget operation in progress, skipping convert');
|
|
9589
10741
|
showToast('warning', 'Please wait for the current operation to finish');
|
|
9590
10742
|
return;
|
|
9591
10743
|
}
|
|
9592
10744
|
|
|
10745
|
+
const screenshotValid =
|
|
10746
|
+
!!payload.screenshot && payload.screenshot.startsWith('data:image/');
|
|
10747
|
+
if (!screenshotValid) {
|
|
10748
|
+
console.error(
|
|
10749
|
+
'[WidgetCreator] Convert screenshot capture FAILED (length:',
|
|
10750
|
+
payload.screenshot?.length ?? 0,
|
|
10751
|
+
') — keeping original shapes so the sketch is not lost'
|
|
10752
|
+
);
|
|
10753
|
+
showToast('warning', 'Screenshot could not be captured', {
|
|
10754
|
+
description: 'Fix the selection (zoom out / deselect) and try Convert again.',
|
|
10755
|
+
duration: 10000
|
|
10756
|
+
});
|
|
10757
|
+
return;
|
|
10758
|
+
}
|
|
10759
|
+
|
|
10760
|
+
const draftId = `${DRAFT_CONVERT_ID_PREFIX}${Date.now()}`;
|
|
10761
|
+
const draftName = payload.frameName && payload.frameName !== 'image'
|
|
10762
|
+
? `Converted ${payload.frameName}`
|
|
10763
|
+
: `ConvertedW-${Date.now()}`;
|
|
10764
|
+
draftConvertScreenshots.set(draftId, payload.screenshot);
|
|
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;
|
|
10768
|
+
|
|
10769
|
+
// Group the sketch into one editable object and keep it on canvas — the
|
|
10770
|
+
// user may keep editing it (ungroup, move, restyle) and re-convert later.
|
|
10771
|
+
// The returned id identifies the group for the later shape replacement.
|
|
10772
|
+
if (canvasRef?.onGroupSelection) {
|
|
10773
|
+
try {
|
|
10774
|
+
const groupId = await canvasRef.onGroupSelection();
|
|
10775
|
+
if (groupId) draftConvertObjectIds.set(draftId, groupId);
|
|
10776
|
+
} catch (groupErr) {
|
|
10777
|
+
console.warn('[WidgetCreator] Group selection failed — continuing with raw selection:', groupErr);
|
|
10778
|
+
}
|
|
10779
|
+
}
|
|
10780
|
+
|
|
10781
|
+
// Re-convert of a group that already has a draft: reopen that draft
|
|
10782
|
+
// panel instead of piling up duplicate drafts for the same sketch.
|
|
10783
|
+
const canvasObjectId = draftConvertObjectIds.get(draftId);
|
|
10784
|
+
const existingDraftId = canvasObjectId
|
|
10785
|
+
? [...draftConvertObjectIds.entries()].find(([id, objId]) => id !== draftId && objId === canvasObjectId)?.[0]
|
|
10786
|
+
: undefined;
|
|
10787
|
+
if (existingDraftId) {
|
|
10788
|
+
draftConvertObjectIds.delete(draftId);
|
|
10789
|
+
draftConvertScreenshots.delete(draftId);
|
|
10790
|
+
console.log('[WidgetCreator] Convert re-opened existing draft for group:', existingDraftId);
|
|
10791
|
+
await openWidgetDetailsPanel(existingDraftId);
|
|
10792
|
+
// Refresh the stored screenshot so finalize uses the latest sketch look
|
|
10793
|
+
draftConvertScreenshots.set(existingDraftId, payload.screenshot);
|
|
10794
|
+
authStatus = 'Widget draft ready — send the prompt to generate.';
|
|
10795
|
+
setTimeout(() => {
|
|
10796
|
+
if (!finalizingDraftConversions.size) {
|
|
10797
|
+
suppressWidgetDetailsClose = false;
|
|
10798
|
+
}
|
|
10799
|
+
}, 1500);
|
|
10800
|
+
return;
|
|
10801
|
+
}
|
|
10802
|
+
|
|
9593
10803
|
try {
|
|
9594
10804
|
isWidgetOperationInProgress = true;
|
|
10805
|
+
authStatus = 'Preparing widget draft...';
|
|
10806
|
+
|
|
10807
|
+
// Draft panel snapshot (no widget record in DB). WidgetDetails is
|
|
10808
|
+
// null-safe for a missing widget — the panel shows the draft name.
|
|
10809
|
+
await openWidgetDetailsPanel(draftId);
|
|
10810
|
+
embeddedDeepLinkAutoOpenDone = true;
|
|
10811
|
+
patchPanelSnapshot(draftId, {
|
|
10812
|
+
codeGenerationPrompt: CONVERT_PROMPT,
|
|
10813
|
+
promptImages: [payload.screenshot],
|
|
10814
|
+
});
|
|
10815
|
+
|
|
10816
|
+
// Prefill the chat draft — sending the message starts the real conversion.
|
|
10817
|
+
// Set the globals too: if the Chat component registers later (async mount),
|
|
10818
|
+
// restoreChatDraftFromStorage reapplies this exact draft to the input.
|
|
10819
|
+
loadPromptGeneration += 1;
|
|
10820
|
+
promptImages = [payload.screenshot];
|
|
10821
|
+
codeGenerationPrompt = CONVERT_PROMPT;
|
|
10822
|
+
pendingChatDraftText = CONVERT_PROMPT;
|
|
10823
|
+
chatDraftRestoredForWidgetId = null;
|
|
10824
|
+
savePromptForWidget(draftId);
|
|
10825
|
+
const panelChat = getChatMethodsForWidget(draftId);
|
|
10826
|
+
if (panelChat) {
|
|
10827
|
+
panelChat.setDraftText?.(CONVERT_PROMPT);
|
|
10828
|
+
panelChat.addImageAttachment?.(payload.screenshot, 'design-screenshot.png');
|
|
10829
|
+
console.log(
|
|
10830
|
+
'[Convert] Draft chat prefilled (no auto-start):',
|
|
10831
|
+
draftId,
|
|
10832
|
+
'| prompt:',
|
|
10833
|
+
CONVERT_PROMPT,
|
|
10834
|
+
'| screenshot attached: true'
|
|
10835
|
+
);
|
|
10836
|
+
} else {
|
|
10837
|
+
console.warn('[Convert] Chat methods not ready — draft stays in panel snapshot for restore:', draftId);
|
|
10838
|
+
}
|
|
10839
|
+
|
|
10840
|
+
authStatus = 'Widget draft ready — send the prompt to generate.';
|
|
10841
|
+
showToast('info', 'Widget draft created', {
|
|
10842
|
+
description: 'Edit the sketch freely. Sending the prompt creates the widget.'
|
|
10843
|
+
});
|
|
10844
|
+
} catch (error) {
|
|
10845
|
+
console.error('[WidgetCreator] Convert to widget (draft) error:', error);
|
|
10846
|
+
draftConvertObjectIds.delete(draftId);
|
|
10847
|
+
draftConvertScreenshots.delete(draftId);
|
|
10848
|
+
authStatus = toErrorMessage(error, 'Failed to prepare widget draft.');
|
|
10849
|
+
showToast('error', 'Failed to prepare widget draft');
|
|
10850
|
+
} finally {
|
|
10851
|
+
isWidgetOperationInProgress = false;
|
|
10852
|
+
setTimeout(() => {
|
|
10853
|
+
if (!finalizingDraftConversions.size) {
|
|
10854
|
+
suppressWidgetDetailsClose = false;
|
|
10855
|
+
}
|
|
10856
|
+
}, 1500);
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10859
|
+
|
|
10860
|
+
/**
|
|
10861
|
+
* Runs on the FIRST prompt send of a draft conversion: creates the widget
|
|
10862
|
+
* record + component + repository, replaces the grouped sketch on canvas with
|
|
10863
|
+
* a WidgetShape screenshot, and repoints panel state from draftId → widgetId.
|
|
10864
|
+
*/
|
|
10865
|
+
async function finalizeDraftConversion(draftId: string, prompt: string): Promise<string | null> {
|
|
10866
|
+
if (!draftId.startsWith(DRAFT_CONVERT_ID_PREFIX)) return null;
|
|
10867
|
+
if (finalizingDraftConversions.has(draftId)) return null;
|
|
10868
|
+
finalizingDraftConversions.add(draftId);
|
|
10869
|
+
// Keep Widget Details open while the canvas sketch is replaced (selection
|
|
10870
|
+
// change would otherwise close the panel, then we'd remount it).
|
|
10871
|
+
suppressWidgetDetailsClose = true;
|
|
10872
|
+
|
|
10873
|
+
try {
|
|
10874
|
+
const screenshot = draftConvertScreenshots.get(draftId) ?? '';
|
|
10875
|
+
const canvasObjectId = draftConvertObjectIds.get(draftId) ?? '';
|
|
10876
|
+
const draftPanelState = getPanelState(draftId);
|
|
10877
|
+
const draftName = getWidgetById(draftId)?.name
|
|
10878
|
+
?? `ConvertedW-${Date.now()}`;
|
|
10879
|
+
|
|
9595
10880
|
authStatus = 'Creating widget from design...';
|
|
9596
10881
|
const idempotencyKey = crypto?.randomUUID?.() ?? `${Date.now()}-convert`;
|
|
9597
|
-
|
|
9598
|
-
// 1. Create widget record
|
|
9599
|
-
const convertedName = payload.frameName && payload.frameName !== 'image'
|
|
9600
|
-
? `Converted ${payload.frameName}`
|
|
9601
|
-
: `ConvertedW-${Date.now()}`;
|
|
9602
10882
|
const response = await widgetsClient.createWidget(
|
|
9603
10883
|
{
|
|
9604
10884
|
newWidget: {
|
|
9605
|
-
name:
|
|
10885
|
+
name: draftName,
|
|
9606
10886
|
type: 'widget',
|
|
9607
|
-
|
|
10887
|
+
// Canvas sketch snapshot is stored on the widget for future reference
|
|
10888
|
+
content: { source: 'canvas_convert', prompt },
|
|
9608
10889
|
isDraft: true,
|
|
9609
|
-
|
|
10890
|
+
description: draftName,
|
|
9610
10891
|
...(canvasId ? { canvasId } : {}),
|
|
9611
10892
|
...embeddedWidgetProjectContext()
|
|
9612
10893
|
}
|
|
9613
10894
|
},
|
|
9614
|
-
{
|
|
9615
|
-
headers: { 'Idempotency-Key': idempotencyKey }
|
|
9616
|
-
}
|
|
10895
|
+
{ headers: { 'Idempotency-Key': idempotencyKey } }
|
|
9617
10896
|
);
|
|
9618
10897
|
|
|
9619
10898
|
if (!response || response.error || response.message) {
|
|
9620
|
-
console.error('[WidgetCreator] Widget create
|
|
9621
|
-
authStatus = `Create widget failed: ${response?.message || response?.error || 'Unknown error'}`;
|
|
10899
|
+
console.error('[WidgetCreator] Widget create on first send failed:', response);
|
|
9622
10900
|
showToast('error', 'Failed to create widget from design');
|
|
9623
|
-
return;
|
|
10901
|
+
return null;
|
|
9624
10902
|
}
|
|
9625
10903
|
|
|
9626
10904
|
const newWidget = response.data || response;
|
|
9627
|
-
if (newWidget?.id) {
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
canvasId,
|
|
9633
|
-
createComponentRequest: {
|
|
9634
|
-
name: payload.frameName || 'Untitled Component',
|
|
9635
|
-
widget_id: newWidget.id,
|
|
9636
|
-
is_widget: true,
|
|
9637
|
-
}
|
|
9638
|
-
});
|
|
9639
|
-
console.log('[WidgetCreator] Component record created for widget', newWidget.id);
|
|
9640
|
-
} catch (compErr) {
|
|
9641
|
-
console.warn('[WidgetCreator] Failed to create component record (non-blocking):', compErr);
|
|
9642
|
-
}
|
|
9643
|
-
}
|
|
9644
|
-
|
|
9645
|
-
// 3. Add to widgets list FIRST so PropsPanel can resolve the name
|
|
9646
|
-
widgets = [{ ...newWidget }, ...widgets];
|
|
9647
|
-
|
|
9648
|
-
// 4. Replace original shape with Widget Image on canvas
|
|
9649
|
-
if (canvasRef) {
|
|
9650
|
-
try {
|
|
9651
|
-
await canvasRef.replaceObjectWithWidgetImage(
|
|
9652
|
-
payload.frameId,
|
|
9653
|
-
newWidget.id,
|
|
9654
|
-
payload.screenshot
|
|
9655
|
-
);
|
|
9656
|
-
console.log('[WidgetCreator] Shape replaced with Widget Image');
|
|
9657
|
-
setTimeout(() => {
|
|
9658
|
-
const json = canvasRef?.saveCanvas();
|
|
9659
|
-
if (json) handleCanvasAutoSave(json);
|
|
9660
|
-
}, 500);
|
|
9661
|
-
} catch (replaceErr) {
|
|
9662
|
-
console.warn('[WidgetCreator] Failed to replace shape (falling back to link):', replaceErr);
|
|
9663
|
-
canvasRef.linkFrameToWidget(payload.frameId, newWidget.id);
|
|
9664
|
-
}
|
|
9665
|
-
}
|
|
10905
|
+
if (!newWidget?.id) {
|
|
10906
|
+
showToast('warning', 'Widget created but no ID returned');
|
|
10907
|
+
return null;
|
|
10908
|
+
}
|
|
10909
|
+
const widgetId: string = newWidget.id;
|
|
9666
10910
|
|
|
9667
|
-
|
|
10911
|
+
// Component record (canvas → widget bridge) — non-blocking.
|
|
10912
|
+
if (canvasId && componentsClient) {
|
|
9668
10913
|
try {
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
10914
|
+
await componentsClient.createComponent({
|
|
10915
|
+
canvasId,
|
|
10916
|
+
createComponentRequest: {
|
|
10917
|
+
name: draftName,
|
|
10918
|
+
widget_id: widgetId,
|
|
10919
|
+
is_widget: true,
|
|
10920
|
+
}
|
|
10921
|
+
});
|
|
10922
|
+
console.log('[WidgetCreator] Component record created for widget', widgetId);
|
|
10923
|
+
} catch (compErr) {
|
|
10924
|
+
console.warn('[WidgetCreator] Failed to create component record (non-blocking):', compErr);
|
|
9677
10925
|
}
|
|
10926
|
+
}
|
|
9678
10927
|
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
console.log('[WidgetCreator] Canvas widget marked for auto code generation:', newWidget.id);
|
|
9682
|
-
|
|
9683
|
-
await selectWidget(newWidget);
|
|
9684
|
-
|
|
9685
|
-
// 7. Override prompt with screenshot AFTER selectWidget (which resets prompt)
|
|
9686
|
-
promptImages = [payload.screenshot];
|
|
9687
|
-
codeGenerationPrompt = payload.frameName
|
|
9688
|
-
? `Design: "${payload.frameName}"\n\n${IMAGE_WIDGET_PROMPT}`
|
|
9689
|
-
: IMAGE_WIDGET_PROMPT;
|
|
9690
|
-
savePromptForWidget(newWidget.id);
|
|
10928
|
+
// Add to widgets list so getWidgetById resolves the real widget from now on.
|
|
10929
|
+
widgets = [{ ...newWidget }, ...widgets.filter((w) => w.id !== draftId)];
|
|
9691
10930
|
|
|
9692
|
-
|
|
10931
|
+
// Replace the grouped sketch with a WidgetShape screenshot. On failure
|
|
10932
|
+
// fall back to linking the sketch object to the widget id.
|
|
10933
|
+
if (canvasRef && canvasObjectId) {
|
|
9693
10934
|
try {
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
console.warn('[WidgetCreator] Failed to save conversion prompt to DB (non-blocking):', convErr);
|
|
10935
|
+
await canvasRef.replaceObjectWithWidgetImage(canvasObjectId, widgetId, screenshot);
|
|
10936
|
+
console.log('[WidgetCreator] Sketch replaced with WidgetShape screenshot');
|
|
10937
|
+
// Flush the canvas AFTER the WidgetShape image fields are set
|
|
10938
|
+
// (synchronous in replaceObjectWithWidgetImage). saveNow bypasses
|
|
10939
|
+
// the autosave debounce so a quick refresh can't lose the replace.
|
|
10940
|
+
setTimeout(() => {
|
|
10941
|
+
canvasRef?.saveNow?.();
|
|
10942
|
+
}, 800);
|
|
10943
|
+
} catch (replaceErr) {
|
|
10944
|
+
console.warn('[WidgetCreator] Sketch replace failed (falling back to link):', replaceErr);
|
|
10945
|
+
canvasRef.linkFrameToWidget(canvasObjectId, widgetId);
|
|
10946
|
+
setTimeout(() => {
|
|
10947
|
+
canvasRef?.saveNow?.();
|
|
10948
|
+
}, 800);
|
|
9709
10949
|
}
|
|
10950
|
+
}
|
|
9710
10951
|
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
skipAutoGenConfirmWidgetIds.add(autoGenWidgetId);
|
|
9719
|
-
patchPanelSnapshot(autoGenWidgetId, {
|
|
9720
|
-
promptImages: [payload.screenshot],
|
|
9721
|
-
codeGenerationPrompt: codeGenerationPrompt,
|
|
9722
|
-
});
|
|
9723
|
-
await tick();
|
|
9724
|
-
console.log('[AutoGenerate] Convert flow — starting codegen immediately:', autoGenWidgetId);
|
|
9725
|
-
void generateCode({ widgetId: autoGenWidgetId });
|
|
10952
|
+
// Auto-create repository so generation can start right away.
|
|
10953
|
+
try {
|
|
10954
|
+
const repoId = await ensureWidgetRepository(newWidget, { silent: true });
|
|
10955
|
+
if (repoId) {
|
|
10956
|
+
newWidget.repositoryId = repoId;
|
|
10957
|
+
widgets = widgets.map((w) => w.id === widgetId ? { ...w, repositoryId: repoId } : w);
|
|
10958
|
+
console.log('[WidgetCreator] Repository auto-created for converted widget:', repoId);
|
|
9726
10959
|
}
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
10960
|
+
} catch (repoErr) {
|
|
10961
|
+
console.warn('[WidgetCreator] Auto-create repo failed (non-blocking):', repoErr);
|
|
10962
|
+
}
|
|
10963
|
+
|
|
10964
|
+
// Migrate draft panel state → real widget panel state, then reopen the
|
|
10965
|
+
// panel under the real id so Chat binds to the widget's conversation.
|
|
10966
|
+
// Persist the real widget's first conversation + user message BEFORE the
|
|
10967
|
+
// panel remounts: the freshly mounted Chat then activates this
|
|
10968
|
+
// conversation (best-conversation = one with messages) instead of
|
|
10969
|
+
// starting empty after a close/reopen.
|
|
10970
|
+
await ensureCodegenUserMessagePersisted(widgetId, prompt, [
|
|
10971
|
+
...(draftPanelState.promptImages ?? []),
|
|
10972
|
+
screenshot,
|
|
10973
|
+
].filter(Boolean));
|
|
10974
|
+
// NOTE: never copy currentChatConversationId from the draft — the draft
|
|
10975
|
+
// chat's conversation is a local UUID that doesn't exist in the DB, and
|
|
10976
|
+
// migrating it would point codegen/summaries at a phantom conversation.
|
|
10977
|
+
const { currentChatConversationId: _draftConvId, ...draftPanelStateClean } =
|
|
10978
|
+
draftPanelState as Record<string, unknown>;
|
|
10979
|
+
patchPanelSnapshot(widgetId, {
|
|
10980
|
+
...draftPanelStateClean,
|
|
10981
|
+
lastPublishedVersion: null,
|
|
10982
|
+
lastCommitId: null,
|
|
10983
|
+
hasGeneratedCode: false,
|
|
10984
|
+
publishHistory: [],
|
|
10985
|
+
widgetDetailsStep: 'create',
|
|
10986
|
+
} as Partial<WidgetPanelSnapshot>);
|
|
10987
|
+
migrateOpenPanelWidgetId(draftId, widgetId);
|
|
10988
|
+
widgetPanelSnapshots = Object.fromEntries(
|
|
10989
|
+
Object.entries(widgetPanelSnapshots).filter(([key]) => key !== draftId),
|
|
10990
|
+
);
|
|
10991
|
+
draftConvertObjectIds.delete(draftId);
|
|
10992
|
+
draftConvertScreenshots.delete(draftId);
|
|
10993
|
+
await selectWidget(getWidgetById(widgetId) ?? newWidget, {
|
|
10994
|
+
skipEditorContextReset: true,
|
|
10995
|
+
skipPlaceShape: true,
|
|
10996
|
+
});
|
|
10997
|
+
focusWidgetPanel(widgetId);
|
|
10998
|
+
await tick();
|
|
10999
|
+
const migratedRef = getWidgetDetailsRef(widgetId);
|
|
11000
|
+
if (migratedRef?.getShowWidgetDetails?.()) {
|
|
11001
|
+
console.log('[CreatorApp] Draft→widget: Widget Details stayed open — keep current position');
|
|
9732
11002
|
} else {
|
|
9733
|
-
|
|
9734
|
-
showToast('warning', 'Widget created but no ID returned');
|
|
11003
|
+
await openWidgetDetailsPanel(widgetId);
|
|
9735
11004
|
}
|
|
11005
|
+
loadPromptGeneration += 1;
|
|
11006
|
+
const migrationPrompt = draftPanelState.codeGenerationPrompt || prompt;
|
|
11007
|
+
const migrationImages = draftPanelState.promptImages?.length
|
|
11008
|
+
? draftPanelState.promptImages
|
|
11009
|
+
: (screenshot ? [screenshot] : []);
|
|
11010
|
+
codeGenerationPrompt = migrationPrompt;
|
|
11011
|
+
promptImages = migrationImages;
|
|
11012
|
+
// Carry the user's typed text + sketch attachment into the real widget's
|
|
11013
|
+
// chat draft (restoreChatDraftFromStorage consumes these on Chat ready).
|
|
11014
|
+
pendingChatDraftText = prompt;
|
|
11015
|
+
chatDraftRestoredForWidgetId = null;
|
|
11016
|
+
savePromptForWidget(widgetId);
|
|
11017
|
+
|
|
11018
|
+
authStatus = 'Widget created from design.';
|
|
11019
|
+
showToast('success', 'Widget created', {
|
|
11020
|
+
description: `"${draftName}" is ready — generation started`
|
|
11021
|
+
});
|
|
11022
|
+
return widgetId;
|
|
9736
11023
|
} catch (error) {
|
|
9737
|
-
console.error('[WidgetCreator]
|
|
9738
|
-
|
|
9739
|
-
|
|
11024
|
+
console.error('[WidgetCreator] finalizeDraftConversion error:', error);
|
|
11025
|
+
showToast('error', 'Failed to finalize widget conversion');
|
|
11026
|
+
return null;
|
|
9740
11027
|
} finally {
|
|
9741
|
-
|
|
11028
|
+
suppressWidgetDetailsClose = false;
|
|
11029
|
+
finalizingDraftConversions.delete(draftId);
|
|
9742
11030
|
}
|
|
9743
11031
|
}
|
|
9744
11032
|
|
|
11033
|
+
/** Draft panel id helper — true when the id is a synthetic convert draft. */
|
|
11034
|
+
function isDraftConvertId(widgetId: string | null | undefined): boolean {
|
|
11035
|
+
return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
|
|
11036
|
+
}
|
|
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
|
+
|
|
9745
11048
|
|
|
9746
11049
|
/**
|
|
9747
11050
|
* Creates or recreates a repository for a specific widget.
|
|
@@ -9842,6 +11145,30 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9842
11145
|
} catch (error) {
|
|
9843
11146
|
if (error instanceof ResponseError) {
|
|
9844
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
|
+
}
|
|
9845
11172
|
if (status === 404 || status === 403) {
|
|
9846
11173
|
console.warn('Repository reference is stale or forbidden; it will be recreated.', {
|
|
9847
11174
|
repoId,
|
|
@@ -10033,7 +11360,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10033
11360
|
}}
|
|
10034
11361
|
onresize={() => {
|
|
10035
11362
|
if (focusedPanelWidgetId) {
|
|
10036
|
-
getWidgetDetailsRef(focusedPanelWidgetId)?.
|
|
11363
|
+
getWidgetDetailsRef(focusedPanelWidgetId)?.keepPanelInViewport?.();
|
|
10037
11364
|
}
|
|
10038
11365
|
}}
|
|
10039
11366
|
/>
|
|
@@ -10190,6 +11517,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10190
11517
|
</div>
|
|
10191
11518
|
{/if}
|
|
10192
11519
|
|
|
11520
|
+
|
|
10193
11521
|
<!-- Widgetic Canvas Editor - Full width background (lazy-loaded) -->
|
|
10194
11522
|
<div class="wc-canvas-area flex-1 min-h-0 relative">
|
|
10195
11523
|
<!-- Embedded: Debug toggle lives in site AppHeader (avoids overlapping Props Panel). -->
|
|
@@ -10219,6 +11547,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10219
11547
|
<svelte:component this={WidgeticCanvas}
|
|
10220
11548
|
bind:this={canvasRef}
|
|
10221
11549
|
showContainerFrame={!embedded}
|
|
11550
|
+
bind:wireframeMode={canvasWireframeMode}
|
|
11551
|
+
advancedFeatures={canvasAdvancedFeatures}
|
|
10222
11552
|
onReady={handleCanvasReady}
|
|
10223
11553
|
onAutoSave={handleCanvasAutoSave}
|
|
10224
11554
|
onBeforeUnmount={(json) => {
|
|
@@ -10234,9 +11564,20 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10234
11564
|
onWidgetShapeDeleting={handleWidgetShapeDeleting}
|
|
10235
11565
|
onWidgetRename={handleWidgetRename}
|
|
10236
11566
|
onRefreshScreenshot={handleRefreshScreenshot}
|
|
11567
|
+
onSetPrimaryWidget={handleSetPrimaryWidget}
|
|
10237
11568
|
getWidgetName={getWidgetNameById}
|
|
10238
11569
|
isWidgetKnown={isWidgetKnownById}
|
|
10239
|
-
onNonWidgetSelected={() => {
|
|
11570
|
+
onNonWidgetSelected={() => {
|
|
11571
|
+
if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
|
|
11572
|
+
console.log('[CreatorApp] Skip Widget Details close — convert/draft panel is open');
|
|
11573
|
+
return;
|
|
11574
|
+
}
|
|
11575
|
+
const active = canvasRef?.getSelectedObject?.() ?? canvasRef?.selectedObject;
|
|
11576
|
+
if (active?._isWidgetImage && active?._widgetId) {
|
|
11577
|
+
return;
|
|
11578
|
+
}
|
|
11579
|
+
widgetDetails?.closeWidgetDetails();
|
|
11580
|
+
}}
|
|
10240
11581
|
panZoomPosition="TC"
|
|
10241
11582
|
showSaveButton={true}
|
|
10242
11583
|
autosaveDelayMs={import.meta.env.DEV ? 15000 : 5000}
|
|
@@ -10534,7 +11875,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10534
11875
|
|
|
10535
11876
|
<!-- ═══ PANEL 3: Widget Details — one floating panel per opened widget ═══ -->
|
|
10536
11877
|
{#if WidgetDetails}
|
|
10537
|
-
{#each openWidgetPanelIds as panelWidgetId, panelIndex (panelWidgetId)}
|
|
11878
|
+
{#each openWidgetPanelIds as panelWidgetId, panelIndex (panelInstanceKeyByWidgetId[panelWidgetId] ?? panelWidgetId)}
|
|
11879
|
+
{@const panelInstanceKey = panelInstanceKeyByWidgetId[panelWidgetId] ?? panelWidgetId}
|
|
10538
11880
|
{@const panelWidget = widgetsById.get(panelWidgetId) ?? null}
|
|
10539
11881
|
{@const isPanelFocused = focusedPanelWidgetId === panelWidgetId}
|
|
10540
11882
|
{@const ps = (panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
|
|
@@ -10542,7 +11884,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10542
11884
|
{@const panelHasCompileErrorState = (panelDisplayRevision, panelHasCompileError(panelWidgetId))}
|
|
10543
11885
|
{@const panelHasInfraPreviewError = (panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
|
|
10544
11886
|
<svelte:component this={WidgetDetails}
|
|
10545
|
-
bind:this={widgetDetailsRefs[
|
|
11887
|
+
bind:this={widgetDetailsRefs[panelInstanceKey]}
|
|
10546
11888
|
userSession={$userSession}
|
|
10547
11889
|
{hasValidToken}
|
|
10548
11890
|
selectedWidgetId={panelWidgetId}
|
|
@@ -10563,7 +11905,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10563
11905
|
{isPublishing}
|
|
10564
11906
|
{publishWidgetStatus}
|
|
10565
11907
|
publishNeedsSaveFirst={publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
10566
|
-
lastPublishedVersion={ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null)}
|
|
11908
|
+
lastPublishedVersion={isDraftConvertId(panelWidgetId) ? null : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
|
|
10567
11909
|
widgetJsPath={ps.selectedWidgetJsPath ?? panelWidget?.jsPath ?? null}
|
|
10568
11910
|
{isGeneratingInOtherTab}
|
|
10569
11911
|
{isPublishingInOtherTab}
|
|
@@ -10591,7 +11933,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10591
11933
|
isFocusedPanel={isPanelFocused}
|
|
10592
11934
|
panelTopOffset={widgetsPanelTopOffset}
|
|
10593
11935
|
panelBottomMargin={embedded ? 24 : widgetsPanelBottomMargin}
|
|
10594
|
-
fillViewport={
|
|
11936
|
+
fillViewport={false}
|
|
10595
11937
|
currentStep={ps.widgetDetailsStep}
|
|
10596
11938
|
on:focusPanel={() => focusWidgetPanel(panelWidgetId)}
|
|
10597
11939
|
on:stepChange={({ detail }) => {
|
|
@@ -10639,6 +11981,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10639
11981
|
}
|
|
10640
11982
|
// Keep canvas WidgetShape thumbnail aligned with the live Worker preview.
|
|
10641
11983
|
scheduleCanvasShapePreviewSync(previewWidgetId);
|
|
11984
|
+
if (detail?.step === 'create' || ps.widgetDetailsStep === 'create') {
|
|
11985
|
+
applyPanelCompositionToPreview(previewWidgetId);
|
|
11986
|
+
}
|
|
10642
11987
|
}}
|
|
10643
11988
|
on:widgetReady={({ detail }) => {
|
|
10644
11989
|
if (detail.widgetId !== panelWidgetId) return;
|
|
@@ -10649,17 +11994,26 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10649
11994
|
on:buildFromRepoSuccess={({ detail }) => {
|
|
10650
11995
|
console.log('[WidgetCreator] Build from repo succeeded for widget:', detail.widgetId);
|
|
10651
11996
|
if (detail.widgetId !== panelWidgetId) return;
|
|
10652
|
-
|
|
11997
|
+
const repoHead =
|
|
11998
|
+
detail.repoHeadCommitSha ||
|
|
11999
|
+
detail.repo_head_commit_sha ||
|
|
12000
|
+
null;
|
|
12001
|
+
const hasSha = typeof repoHead === 'string' && repoHead.length > 0;
|
|
12002
|
+
if (hasSha) {
|
|
12003
|
+
saveCommitIdForWidget(detail.widgetId, repoHead);
|
|
12004
|
+
}
|
|
12005
|
+
// Always persist hasGeneratedCode on the snapshot so Publish re-renders
|
|
12006
|
+
// even when Worker cache hits without a SHA and the panel is unfocused.
|
|
12007
|
+
patchPanelSnapshot(detail.widgetId, {
|
|
12008
|
+
panelPreviewCompileError: null,
|
|
12009
|
+
hasGeneratedCode: true,
|
|
12010
|
+
...(hasSha ? { lastCommitId: repoHead } : {}),
|
|
12011
|
+
});
|
|
10653
12012
|
if (detail.widgetId === panelWidgetId) {
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
detail.
|
|
10658
|
-
null;
|
|
10659
|
-
if (repoHead && typeof repoHead === 'string') {
|
|
10660
|
-
saveCommitIdForWidget(detail.widgetId, repoHead);
|
|
10661
|
-
patchPanelSnapshot(detail.widgetId, { lastCommitId: repoHead, hasGeneratedCode: true });
|
|
10662
|
-
if (isPanelFocused) lastCommitId = repoHead;
|
|
12013
|
+
hasGeneratedCode = true;
|
|
12014
|
+
if (hasSha) lastCommitId = repoHead;
|
|
12015
|
+
if (!hasSha) {
|
|
12016
|
+
void reconcileWidgetHeadCommit(detail.widgetId);
|
|
10663
12017
|
}
|
|
10664
12018
|
if (isPanelFocused && generateCodeStatus?.includes('preview build failed')) {
|
|
10665
12019
|
generateCodeStatus = 'Code generation completed! Preview ready.';
|
|
@@ -10780,6 +12134,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10780
12134
|
console.log('[WidgetCreator] Generate Now clicked for widget:', detail.widgetId);
|
|
10781
12135
|
const widgetId = detail.widgetId;
|
|
10782
12136
|
const ps = getPanelDisplayState(widgetId);
|
|
12137
|
+
if (ps.lastGenerationFailed) {
|
|
12138
|
+
void handleChatRetryMessage(
|
|
12139
|
+
lastGenerationPrompt || ps.codeGenerationPrompt,
|
|
12140
|
+
widgetId,
|
|
12141
|
+
);
|
|
12142
|
+
return;
|
|
12143
|
+
}
|
|
10783
12144
|
const hasCode = !!(ps.lastCommitId || ps.lastPublishedVersion !== null);
|
|
10784
12145
|
const compileErr = getPanelPreviewCompileError(widgetId);
|
|
10785
12146
|
if (hasCode && compileErr) {
|
|
@@ -10792,6 +12153,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10792
12153
|
on:widgetDetailsToggle={({ detail }) => {
|
|
10793
12154
|
const isClosing = detail?.open === false || detail?.showWidgetDetails === false;
|
|
10794
12155
|
if (isClosing) {
|
|
12156
|
+
if (suppressWidgetDetailsClose) {
|
|
12157
|
+
console.log('[CreatorApp] Ignoring Widget Details close during draft→widget migrate');
|
|
12158
|
+
return;
|
|
12159
|
+
}
|
|
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.
|
|
10795
12163
|
if (isPanelFocused && selectedWidgetId) {
|
|
10796
12164
|
savePromptForWidget(selectedWidgetId);
|
|
10797
12165
|
captureFocusedPanelSnapshot();
|
|
@@ -10821,21 +12189,19 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10821
12189
|
>
|
|
10822
12190
|
<svelte:fragment slot="code-generation">
|
|
10823
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))))}
|
|
10824
12193
|
<div
|
|
10825
|
-
class="parallel-panel-left-slot-ct flex flex-col
|
|
12194
|
+
class="parallel-panel-left-slot-ct flex flex-col shrink-0 overflow-visible"
|
|
10826
12195
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
10827
12196
|
>
|
|
10828
|
-
<!-- Sticky header: title + model selectors (debug mode)
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
{#if debugMode}
|
|
12197
|
+
<!-- Sticky header: title + model selectors (debug mode).
|
|
12198
|
+
z-20 keeps it above Chat's inline error banner (z-10) so the
|
|
12199
|
+
banner scrolls under the dropdowns instead of covering them. -->
|
|
12200
|
+
{#if debugMode}
|
|
12201
|
+
<div class="generation-chat-sticky-header shrink-0 z-20 bg-white border-b border-gray-200">
|
|
10834
12202
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
10835
|
-
<div class="generation-models-header flex items-center gap-2 px-2 py-
|
|
10836
|
-
<span class="text-[10px] font-semibold text-gray-500 uppercase tracking-wide">Models</span>
|
|
10837
|
-
</div>
|
|
10838
|
-
<div class="model-selectors-bar flex items-center gap-3 px-2 py-1.5 bg-gray-50">
|
|
12203
|
+
<div class="generation-models-header flex items-center gap-2 px-2 py-1.5 bg-gray-50">
|
|
12204
|
+
<span class="text-[10px] font-semibold text-gray-500 uppercase tracking-wide shrink-0">Models</span>
|
|
10839
12205
|
<div class="planner-model-selector flex items-center gap-1.5 flex-1 min-w-0">
|
|
10840
12206
|
<label class="text-[10px] text-gray-500 font-medium whitespace-nowrap">Planner:</label>
|
|
10841
12207
|
<select
|
|
@@ -10869,13 +12235,14 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10869
12235
|
</select>
|
|
10870
12236
|
</div>
|
|
10871
12237
|
</div>
|
|
10872
|
-
{/if}
|
|
10873
12238
|
</div>
|
|
12239
|
+
{/if}
|
|
10874
12240
|
|
|
12241
|
+
<div class="generation-scroll-ct flex flex-col shrink-0 overflow-visible">
|
|
10875
12242
|
<!-- Chat-based Code Generation Panel -->
|
|
10876
|
-
{#key
|
|
10877
|
-
<Chat class="code-generation-chat"
|
|
10878
|
-
style="height: {
|
|
12243
|
+
{#key panelInstanceKey}
|
|
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;"
|
|
10879
12246
|
resizeExpandsContainer={true}
|
|
10880
12247
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
10881
12248
|
showLogs={debugMode}
|
|
@@ -10905,11 +12272,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10905
12272
|
onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
|
|
10906
12273
|
onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
|
|
10907
12274
|
onUploadChatFile={handleChatUploadFile}
|
|
10908
|
-
onTakeScreenshot={handleChatScreenshot}
|
|
12275
|
+
onTakeScreenshot={() => handleChatScreenshot(panelWidgetId)}
|
|
10909
12276
|
onLoadConversations={handleLoadConversations}
|
|
10910
12277
|
onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
|
|
10911
12278
|
onCreateConversation={handleCreateConversation}
|
|
10912
|
-
onSaveMessage={
|
|
12279
|
+
onSaveMessage={(conversationId, content, messageType, attachments) =>
|
|
12280
|
+
handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
|
|
12281
|
+
}
|
|
10913
12282
|
onRestoreCheckpointBackend={handleRestoreCheckpoint}
|
|
10914
12283
|
onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
|
|
10915
12284
|
onDraftChange={() => handleChatDraftChange(panelWidgetId)}
|
|
@@ -10984,41 +12353,34 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10984
12353
|
</div>
|
|
10985
12354
|
{/if}
|
|
10986
12355
|
</div>
|
|
10987
|
-
</div>
|
|
12356
|
+
</div> <!-- generation-scroll-ct -->
|
|
12357
|
+
</div> <!-- parallel-panel-left-slot-ct -->
|
|
10988
12358
|
</svelte:fragment>
|
|
10989
12359
|
|
|
10990
12360
|
<svelte:fragment slot="publish-widget">
|
|
10991
12361
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
10992
12362
|
<div
|
|
10993
|
-
class="parallel-panel-publish-slot-ct"
|
|
12363
|
+
class="parallel-panel-publish-slot-ct shrink-0"
|
|
10994
12364
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
10995
12365
|
>
|
|
10996
12366
|
<!-- ===== Publish Widget Section ===== -->
|
|
10997
|
-
<div class="publish-widget-section flex flex-col gap-
|
|
10998
|
-
<!-- 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">
|
|
10999
12368
|
<div class="publish-widget-section-header flex items-center justify-between gap-2">
|
|
11000
|
-
<span class="text-
|
|
11001
|
-
<!-- Current published version badge — shown when widget has been published -->
|
|
12369
|
+
<span class="text-xs font-semibold text-emerald-800">🚀 Publish</span>
|
|
11002
12370
|
{#if ps.lastPublishedVersion !== null}
|
|
11003
|
-
<span class="text-
|
|
12371
|
+
<span class="text-[10px] font-mono bg-emerald-700 text-white px-1.5 py-0.5 rounded">
|
|
11004
12372
|
live: v{ps.lastPublishedVersion}
|
|
11005
12373
|
</span>
|
|
11006
12374
|
{/if}
|
|
11007
12375
|
</div>
|
|
11008
12376
|
|
|
11009
|
-
|
|
11010
|
-
<p class="text-xs text-emerald-700 leading-relaxed">
|
|
11011
|
-
Publish when you’re happy with the preview. You’ll get a stable embed URL for your site.
|
|
11012
|
-
</p>
|
|
11013
|
-
|
|
11014
|
-
<!-- Publish Button + Build Status -->
|
|
11015
|
-
<div class="publish-widget-section-actions flex flex-col gap-2 items-center">
|
|
11016
|
-
<!-- Publish Widget Button — triggers production build via Trigger.dev -->
|
|
12377
|
+
<div class="publish-widget-section-actions flex flex-col gap-1 items-center">
|
|
11017
12378
|
<Button
|
|
11018
12379
|
onclick={() => runPublishForPanel(panelWidgetId)}
|
|
11019
|
-
disabled={isPublishDisabledForPanel(panelWidgetId)}
|
|
12380
|
+
disabled={(panelDisplayRevision, isPublishDisabledForPanel(panelWidgetId))}
|
|
11020
12381
|
title={getPublishDisabledReasonForPanel(panelWidgetId) ?? undefined}
|
|
11021
|
-
|
|
12382
|
+
size="sm"
|
|
12383
|
+
class={`publish-widget-btn flex justify-center items-center border text-white w-full h-8 text-xs
|
|
11022
12384
|
${isPublishing && publishingWidgetId === panelWidgetId
|
|
11023
12385
|
? 'bg-emerald-500 border-emerald-500 opacity-80 cursor-not-allowed'
|
|
11024
12386
|
: isPublishDisabledForPanel(panelWidgetId)
|
|
@@ -11033,7 +12395,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11033
12395
|
<span class="mr-1 inline-block">⏳</span>
|
|
11034
12396
|
<span>Publishing...</span>
|
|
11035
12397
|
{:else}
|
|
11036
|
-
<span>{getPublishButtonLabelForPanel(panelWidgetId)}</span>
|
|
12398
|
+
<span>{(panelDisplayRevision, getPublishButtonLabelForPanel(panelWidgetId))}</span>
|
|
11037
12399
|
{#if isPublishingInOtherTab}
|
|
11038
12400
|
<!-- svelte-ignore a11y-missing-attribute -->
|
|
11039
12401
|
<span class="text-xs opacity-70 ml-1" title="Publish is running in another tab">⏳</span>
|
|
@@ -11047,24 +12409,23 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11047
12409
|
</Button>
|
|
11048
12410
|
|
|
11049
12411
|
{#if isPublishDisabledForPanel(panelWidgetId) && shouldShowPublishDisabledHintForPanel(panelWidgetId)}
|
|
11050
|
-
<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">
|
|
11051
12413
|
{getPublishDisabledReasonForPanel(panelWidgetId)}
|
|
11052
12414
|
</p>
|
|
11053
12415
|
{/if}
|
|
11054
12416
|
|
|
11055
12417
|
{#if ps.lastPublishedVersion !== null}
|
|
11056
|
-
<a href="/test-widget?widgetId={panelWidgetId}&version={ps.lastPublishedVersion}" target="_blank" rel="noopener noreferrer" class="
|
|
11057
|
-
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} ↗
|
|
11058
12420
|
</a>
|
|
11059
12421
|
{/if}
|
|
11060
12422
|
|
|
11061
|
-
<!-- Save-first hint — shown when changes aren't saved yet -->
|
|
11062
12423
|
{#if publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
11063
|
-
<p class="text-
|
|
12424
|
+
<p class="text-[10px] text-center text-amber-700">
|
|
11064
12425
|
{#if !resolvePublishHeadCommitId(panelWidgetId, ps)}
|
|
11065
|
-
|
|
12426
|
+
Save the widget before publishing.
|
|
11066
12427
|
{:else}
|
|
11067
|
-
|
|
12428
|
+
Save your changes before publishing.
|
|
11068
12429
|
{/if}
|
|
11069
12430
|
</p>
|
|
11070
12431
|
{/if}
|
|
@@ -11077,7 +12438,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11077
12438
|
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
11078
12439
|
togglePublishHistory();
|
|
11079
12440
|
}}
|
|
11080
|
-
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"
|
|
11081
12442
|
>
|
|
11082
12443
|
<span class="font-medium">📋 Version History</span>
|
|
11083
12444
|
<span class="text-emerald-400">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
|
|
@@ -11140,44 +12501,36 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11140
12501
|
<svelte:fragment slot="composition-editor">
|
|
11141
12502
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
11142
12503
|
<div
|
|
11143
|
-
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"
|
|
11144
12505
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
11145
12506
|
>
|
|
11146
12507
|
<!-- ===== Composition Editor (Step 2: Edit) ===== -->
|
|
11147
12508
|
{#if ps.lastCommitId || ps.lastPublishedVersion !== null}
|
|
11148
|
-
<div class="composition-editor-section flex flex-col
|
|
12509
|
+
<div class="composition-editor-section flex flex-col flex-1 min-h-0 h-full">
|
|
11149
12510
|
|
|
11150
12511
|
{#if panelHasUnpublishedChanges}
|
|
11151
|
-
<div class="edit-unpublished-banner-ct mx-3 mt-2 px-
|
|
11152
|
-
<p class="edit-unpublished-banner-text leading-
|
|
11153
|
-
<strong class="font-semibold">Newer
|
|
11154
|
-
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>
|
|
11155
12515
|
<span class="font-mono">{formatCommitShaShort(ps.lastCommitId)}</span>
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
Edit preview shows <strong>Live v{ps.lastPublishedVersion ?? '—'}</strong> with your composition settings.
|
|
11159
|
-
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>
|
|
11160
12518
|
</p>
|
|
11161
|
-
<
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
<span>🚀 Publish to Live</span>
|
|
11175
|
-
{/if}
|
|
11176
|
-
</Button>
|
|
11177
|
-
{#if publishNeedsSaveFirstForPanel(panelWidgetId)}
|
|
11178
|
-
<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>
|
|
11179
12532
|
{/if}
|
|
11180
|
-
</
|
|
12533
|
+
</Button>
|
|
11181
12534
|
</div>
|
|
11182
12535
|
{/if}
|
|
11183
12536
|
|
|
@@ -11339,14 +12692,17 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11339
12692
|
</button>
|
|
11340
12693
|
</div>
|
|
11341
12694
|
{:else if ps.compositionDesignSchema || ps.compositionContentSchema}
|
|
11342
|
-
<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">
|
|
11343
12696
|
{#key `${panelWidgetId}-${ps.selectedComposition?.id ?? 'new'}`}
|
|
11344
12697
|
<PropsEditor
|
|
11345
12698
|
class="composition-props-editor flex-1 min-h-0 min-w-0"
|
|
11346
12699
|
designSchema={ps.compositionDesignSchema}
|
|
11347
12700
|
contentSchema={buildPropsEditorContentSchema(ps.compositionContentSchema, ps.liveContentItems)}
|
|
11348
12701
|
initialDesignValues={ps.liveDesignValues}
|
|
11349
|
-
initialContentValues={
|
|
12702
|
+
initialContentValues={{
|
|
12703
|
+
...ps.liveContentValues,
|
|
12704
|
+
items: contentItemsToEditorValueMap(ps.liveContentItems)
|
|
12705
|
+
}}
|
|
11350
12706
|
width="100%"
|
|
11351
12707
|
height="100%"
|
|
11352
12708
|
showDragHandle={false}
|
|
@@ -11453,9 +12809,11 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11453
12809
|
</div>
|
|
11454
12810
|
|
|
11455
12811
|
{#if lastPublishedVersion !== null}
|
|
11456
|
-
{@const embedSrc =
|
|
11457
|
-
|
|
11458
|
-
|
|
12812
|
+
{@const embedSrc = publishedWidgetHtmlUrl(
|
|
12813
|
+
selectedWidgetId,
|
|
12814
|
+
lastPublishedVersion,
|
|
12815
|
+
selectedComposition?.id
|
|
12816
|
+
)}
|
|
11459
12817
|
{@const embedCodeResult = generateEmbedCode(embedFormat, {
|
|
11460
12818
|
embedSrc,
|
|
11461
12819
|
compositionId: selectedComposition?.id ?? null,
|
|
@@ -11623,7 +12981,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11623
12981
|
<Dialog.Title>Delete Widget Shape</Dialog.Title>
|
|
11624
12982
|
<Dialog.Description>
|
|
11625
12983
|
{#if deleteDialogStep === 'confirm-remove'}
|
|
11626
|
-
Remove <strong>"{deleteDialogWidgetName}"</strong>
|
|
12984
|
+
Remove <strong>"{deleteDialogWidgetName}"</strong> from the canvas? This also deletes the associated widget.
|
|
11627
12985
|
{:else}
|
|
11628
12986
|
Also delete the widget <strong>"{deleteDialogWidgetName}"</strong> from the database?
|
|
11629
12987
|
{/if}
|
|
@@ -11641,8 +12999,8 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11641
12999
|
type="button"
|
|
11642
13000
|
class="px-4 py-2 text-sm rounded-lg bg-red-600 hover:bg-red-700 text-white cursor-pointer"
|
|
11643
13001
|
onclick={() => handleDeleteDialogResponse('remove-and-ask-db')}
|
|
11644
|
-
>
|
|
11645
|
-
{:else}
|
|
13002
|
+
>Delete</button>
|
|
13003
|
+
{:else if ASK_DELETE_FROM_DB_CONFIRM}
|
|
11646
13004
|
<button
|
|
11647
13005
|
type="button"
|
|
11648
13006
|
class="px-4 py-2 text-sm rounded-lg border border-gray-300 hover:bg-gray-100 cursor-pointer"
|