@widgetic/creator 0.3.49 → 0.3.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CreatorApp.svelte
CHANGED
|
@@ -110,10 +110,33 @@
|
|
|
110
110
|
onWidgetRenamed?.({ widgetId: widget.id, name });
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Rewrite the embedded URL so a refresh resumes the current canvas instead of
|
|
115
|
+
* re-running the new-widget bootstrap (which creates a blank canvas every time).
|
|
116
|
+
* Uses history.replaceState — no navigation, no host remount.
|
|
117
|
+
*/
|
|
118
|
+
function syncEmbeddedCanvasUrl(canvasIdToSync: string): void {
|
|
119
|
+
if (!embedded || !canvasIdToSync) return;
|
|
120
|
+
try {
|
|
121
|
+
const url = new URL(window.location.href);
|
|
122
|
+
url.searchParams.set('canvas_id', canvasIdToSync);
|
|
123
|
+
url.searchParams.delete('new_widget');
|
|
124
|
+
window.history.replaceState(window.history.state, '', url.toString());
|
|
125
|
+
console.log('[CreatorApp] URL synced to canvas:', canvasIdToSync);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.warn('[CreatorApp] Failed to sync canvas URL:', err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
const HOST_WIDGET_RENAMED_EVENT = 'widgetic:host-widget-renamed';
|
|
114
132
|
const HOST_REQUEST_PUBLISH_EVENT = 'widgetic:host-request-publish';
|
|
115
133
|
const HOST_TOGGLE_DEBUG_EVENT = 'widgetic:host-toggle-debug';
|
|
134
|
+
const HOST_TOGGLE_WIREFRAME_EVENT = 'widgetic:host-toggle-wireframe';
|
|
135
|
+
const HOST_OPEN_WIDGET_DETAILS_EVENT = 'widgetic:host-open-widget-details';
|
|
136
|
+
const HOST_DUPLICATE_WIDGET_EVENT = 'widgetic:host-duplicate-widget';
|
|
137
|
+
const HOST_DELETE_WIDGET_EVENT = 'widgetic:host-delete-widget';
|
|
116
138
|
const CREATOR_DEBUG_CHANGED_EVENT = 'widgetic:creator-debug-changed';
|
|
139
|
+
const CREATOR_WIREFRAME_CHANGED_EVENT = 'widgetic:creator-wireframe-changed';
|
|
117
140
|
|
|
118
141
|
/** Site AppHeader rename — embed is a second Svelte runtime, so mount props do not update. */
|
|
119
142
|
async function applyHostWidgetName(widgetId: string | null | undefined, newName: string): Promise<void> {
|
|
@@ -347,7 +370,14 @@
|
|
|
347
370
|
let WidgetDetails: any;
|
|
348
371
|
let widgetDetailsRefs: Record<string, any> = {};
|
|
349
372
|
let openWidgetPanelIds: string[] = [];
|
|
373
|
+
/** Stable {#each} keys so draft → real widget does not remount Widget Details. */
|
|
374
|
+
let panelInstanceKeyByWidgetId: Record<string, string> = {};
|
|
350
375
|
let focusedPanelWidgetId: string | null = null;
|
|
376
|
+
/** Ignore WidgetDetails close events while swapping draft → real widget id. */
|
|
377
|
+
let suppressWidgetDetailsClose = false;
|
|
378
|
+
/** Latest user-requested details panel — delayed screenshot/deep-link must not reopen another widget. */
|
|
379
|
+
let widgetDetailsOpenGen = 0;
|
|
380
|
+
let widgetDetailsOpenTargetId: string | null = null;
|
|
351
381
|
|
|
352
382
|
// Lightweight static imports from canvas (excludes Canvas.svelte — 10K+ lines)
|
|
353
383
|
import { Tooltip } from '@widgetic/canvas/components';
|
|
@@ -600,6 +630,8 @@
|
|
|
600
630
|
const DEFAULT_PROMPT = 'Create a simple widget.';
|
|
601
631
|
const IMAGE_WIDGET_PROMPT =
|
|
602
632
|
'Create a widget from the attached image (short description inside).';
|
|
633
|
+
/** Prefilled prompt for Convert-to-Widget — user edits or presses Send to start. */
|
|
634
|
+
const CONVERT_PROMPT = 'Create a widget starting from the attached image';
|
|
603
635
|
let codeGenerationPrompt = DEFAULT_PROMPT;
|
|
604
636
|
let isGeneratingCode = false;
|
|
605
637
|
let generatingWidgetIds: Record<string, boolean> = {};
|
|
@@ -622,6 +654,16 @@
|
|
|
622
654
|
/** Widget targeted by the open confirm-generate dialog (may differ from focused panel). */
|
|
623
655
|
let confirmGenerateWidgetId: string | null = null;
|
|
624
656
|
|
|
657
|
+
// ─── Convert UX v2: draft panel state (no widget record until first Send) ────
|
|
658
|
+
/** Synthetic id for the draft convert panel — NOT a widget id in the DB. */
|
|
659
|
+
const DRAFT_CONVERT_ID_PREFIX = 'draft_convert_';
|
|
660
|
+
/** Canvas object ids (group or selection id) captured at convert time, per draft id. */
|
|
661
|
+
const draftConvertObjectIds = new Map<string, string>();
|
|
662
|
+
/** Captured sketch screenshot (data URL) per draft id — reused on first Send. */
|
|
663
|
+
const draftConvertScreenshots = new Map<string, string>();
|
|
664
|
+
/** True while a draft conversion is being finalized (widget creation + shape replace). */
|
|
665
|
+
const finalizingDraftConversions = new Set<string>();
|
|
666
|
+
|
|
625
667
|
// Chat component reference and context (focused panel)
|
|
626
668
|
let chatMethods: {
|
|
627
669
|
sendMessage: (content: string, attachments?: File[]) => Promise<void>;
|
|
@@ -639,10 +681,19 @@
|
|
|
639
681
|
markWidgetHeadAtLatestCommit?: () => void;
|
|
640
682
|
updateLatestCodegenStatusMessage?: (content: string) => void;
|
|
641
683
|
reloadConversationMessages?: () => Promise<void>;
|
|
684
|
+
/** Select an existing conversation in the chat UI (keep DB thread and visible chat in sync). */
|
|
685
|
+
selectConversation?: (conversationId: string) => void;
|
|
642
686
|
} | null = null;
|
|
643
687
|
let chatMethodsByWidgetId: Record<string, NonNullable<typeof chatMethods>> = {};
|
|
644
688
|
let pendingChatDraftText = '';
|
|
645
689
|
let chatDraftRestoredForWidgetId: string | null = null;
|
|
690
|
+
/**
|
|
691
|
+
* Monotonic generation counter for loadPromptForWidget. Each load increments it;
|
|
692
|
+
* when the async draft read resolves, a changed counter means another load or an
|
|
693
|
+
* explicit prompt set (e.g. convert flow) happened meanwhile — the stale result
|
|
694
|
+
* must NOT clobber the newer prompt state (race behind feedback b).
|
|
695
|
+
*/
|
|
696
|
+
let loadPromptGeneration = 0;
|
|
646
697
|
/** Skip draft→prompt sync while restoring chat draft (avoids clearing promptImages). */
|
|
647
698
|
let suppressChatDraftSync = false;
|
|
648
699
|
|
|
@@ -743,7 +794,27 @@
|
|
|
743
794
|
content.substring(0, 80),
|
|
744
795
|
);
|
|
745
796
|
|
|
746
|
-
|
|
797
|
+
// Convert UX v2: the first prompt send of a draft panel creates the widget
|
|
798
|
+
// (DB + component + repo), replaces the sketch with a WidgetShape, and
|
|
799
|
+
// repoints the panel to the real widget id. Generation continues below.
|
|
800
|
+
let codegenWidgetId = widgetId;
|
|
801
|
+
if (isDraftConvertId(widgetId)) {
|
|
802
|
+
const finalizedWidgetId = await finalizeDraftConversion(widgetId, content);
|
|
803
|
+
if (!finalizedWidgetId) {
|
|
804
|
+
console.warn('[Chat→CodeGen] Draft conversion failed — message not sent to codegen');
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
codegenWidgetId = finalizedWidgetId;
|
|
808
|
+
// Re-point the chat draft the user just typed onto the real widget panel.
|
|
809
|
+
const newPanelChat = getChatMethodsForWidget(finalizedWidgetId);
|
|
810
|
+
newPanelChat?.setDraftText?.(content);
|
|
811
|
+
const newPanelChatAttach = getPanelState(finalizedWidgetId).promptImages ?? [];
|
|
812
|
+
for (const imageDataUrl of newPanelChatAttach) {
|
|
813
|
+
newPanelChat?.addImageAttachment?.(imageDataUrl, 'design-screenshot.png');
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const panelChat = getChatMethodsForWidget(codegenWidgetId);
|
|
747
818
|
const cdnUploadIds = (attachments || [])
|
|
748
819
|
.map((attachment) => attachment.uploadId)
|
|
749
820
|
.filter((id): id is string => Boolean(id));
|
|
@@ -768,20 +839,20 @@
|
|
|
768
839
|
}
|
|
769
840
|
|
|
770
841
|
if (panelPromptImages.length > 0) {
|
|
771
|
-
patchPanelSnapshot(
|
|
772
|
-
if (
|
|
842
|
+
patchPanelSnapshot(codegenWidgetId, { promptImages: panelPromptImages });
|
|
843
|
+
if (codegenWidgetId === focusedPanelWidgetId) {
|
|
773
844
|
promptImages = panelPromptImages;
|
|
774
845
|
}
|
|
775
846
|
}
|
|
776
847
|
|
|
777
848
|
// Clear chat input immediately so thumbnails do not linger after Send
|
|
778
849
|
panelChat?.clearDraft?.();
|
|
779
|
-
void deleteWidgetDraft(
|
|
850
|
+
void deleteWidgetDraft(codegenWidgetId).catch(() => {
|
|
780
851
|
// Also drop the legacy localStorage copies if the durable clear fails.
|
|
781
852
|
try {
|
|
782
|
-
localStorage.removeItem(PROMPT_IMAGES_STORAGE_PREFIX +
|
|
783
|
-
localStorage.removeItem(CHAT_DRAFT_STORAGE_PREFIX +
|
|
784
|
-
localStorage.removeItem(PROMPT_TEXT_STORAGE_PREFIX +
|
|
853
|
+
localStorage.removeItem(PROMPT_IMAGES_STORAGE_PREFIX + codegenWidgetId);
|
|
854
|
+
localStorage.removeItem(CHAT_DRAFT_STORAGE_PREFIX + codegenWidgetId);
|
|
855
|
+
localStorage.removeItem(PROMPT_TEXT_STORAGE_PREFIX + codegenWidgetId);
|
|
785
856
|
localStorage.removeItem(PROMPT_IMAGES_STORAGE_KEY);
|
|
786
857
|
} catch {
|
|
787
858
|
/* ignore */
|
|
@@ -791,9 +862,9 @@
|
|
|
791
862
|
const effectivePromptImages =
|
|
792
863
|
panelPromptImages.length > 0
|
|
793
864
|
? panelPromptImages
|
|
794
|
-
:
|
|
865
|
+
: codegenWidgetId === focusedPanelWidgetId
|
|
795
866
|
? promptImages
|
|
796
|
-
: getPanelState(
|
|
867
|
+
: getPanelState(codegenWidgetId).promptImages;
|
|
797
868
|
const hasImages = effectivePromptImages.length > 0 || cdnUploadIds.length > 0;
|
|
798
869
|
if (!hasImages && promptImpliesVisionReference(content)) {
|
|
799
870
|
showToast('warning', 'Lipsește imaginea de referință', {
|
|
@@ -807,7 +878,7 @@
|
|
|
807
878
|
await generateCode({
|
|
808
879
|
prompt: content,
|
|
809
880
|
fromChat: true,
|
|
810
|
-
widgetId,
|
|
881
|
+
widgetId: codegenWidgetId,
|
|
811
882
|
attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
|
|
812
883
|
promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
|
|
813
884
|
});
|
|
@@ -891,12 +962,27 @@
|
|
|
891
962
|
: prompt;
|
|
892
963
|
}
|
|
893
964
|
|
|
894
|
-
/**
|
|
965
|
+
/**
|
|
966
|
+
* Persist the user prompt to DB only when generation actually starts.
|
|
967
|
+
* Writes into the conversation the panel Chat is currently showing (or the
|
|
968
|
+
* newest one) so the visible chat and the codegen context never diverge —
|
|
969
|
+
* a remounted Chat shows whatever conversation activateBestConversation
|
|
970
|
+
* picked, which may differ from convos[0] when several empty conversations
|
|
971
|
+
* exist (feedback c: chat looked empty while messages went elsewhere).
|
|
972
|
+
*/
|
|
895
973
|
async function ensureCodegenUserMessagePersisted(widgetId: string, prompt: string): Promise<void> {
|
|
896
974
|
if (!conversationsClient || !messagesClient) return;
|
|
897
975
|
try {
|
|
898
|
-
|
|
899
|
-
|
|
976
|
+
const panelChat = getChatMethodsForWidget(widgetId);
|
|
977
|
+
const visibleConversationId =
|
|
978
|
+
panelChat?.getConversationId?.() ??
|
|
979
|
+
getPanelState(widgetId).currentChatConversationId ??
|
|
980
|
+
null;
|
|
981
|
+
const convos = await handleLoadConversations(widgetId);
|
|
982
|
+
let convId =
|
|
983
|
+
(visibleConversationId && convos?.some((c) => c.id === visibleConversationId))
|
|
984
|
+
? visibleConversationId
|
|
985
|
+
: convos?.[0]?.id;
|
|
900
986
|
if (!convId) {
|
|
901
987
|
const widget = getWidgetById(widgetId);
|
|
902
988
|
const convTitle = widget?.name ? `Convert: ${widget.name}` : 'Widget conversion';
|
|
@@ -904,6 +990,12 @@
|
|
|
904
990
|
convId = conv?.id;
|
|
905
991
|
}
|
|
906
992
|
if (!convId) return;
|
|
993
|
+
if (convId !== visibleConversationId) {
|
|
994
|
+
// Point the panel chat at the conversation that will hold the messages
|
|
995
|
+
// so a later reopen shows the same thread the codegen used.
|
|
996
|
+
panelChat?.selectConversation?.(convId);
|
|
997
|
+
patchPanelSnapshot(widgetId, { currentChatConversationId: convId });
|
|
998
|
+
}
|
|
907
999
|
|
|
908
1000
|
const messages = await handleLoadMessages(convId);
|
|
909
1001
|
const hasUserMessage = messages?.some(
|
|
@@ -962,8 +1054,27 @@
|
|
|
962
1054
|
});
|
|
963
1055
|
}
|
|
964
1056
|
|
|
1057
|
+
function normalizeConversationRecords(json: any): any[] {
|
|
1058
|
+
const raw = json?.data ?? json?.conversations ?? json;
|
|
1059
|
+
const list = Array.isArray(raw)
|
|
1060
|
+
? raw
|
|
1061
|
+
: Array.isArray(raw?.data)
|
|
1062
|
+
? raw.data
|
|
1063
|
+
: [];
|
|
1064
|
+
return list
|
|
1065
|
+
.map((c: any) => ({
|
|
1066
|
+
...c,
|
|
1067
|
+
id: c?.id || c?.conversation_id || c?.conversationId,
|
|
1068
|
+
}))
|
|
1069
|
+
.filter((c: any) => !!c.id);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
965
1072
|
/** Load conversations for a widget from backend. Returns null when client/API is unavailable (not an empty list). */
|
|
966
1073
|
async function handleLoadConversations(contextId: string): Promise<any[] | null> {
|
|
1074
|
+
// Draft convert panels have no DB widget — return an empty list so the
|
|
1075
|
+
// Chat initializes cleanly (creates a local conversation) instead of
|
|
1076
|
+
// showing the "Could not load chat history" error after 4 retries.
|
|
1077
|
+
if (isDraftConvertId(contextId)) return [];
|
|
967
1078
|
try {
|
|
968
1079
|
if (!conversationsClient) {
|
|
969
1080
|
console.warn('[Chat→Backend] conversationsClient not initialized');
|
|
@@ -971,11 +1082,13 @@
|
|
|
971
1082
|
}
|
|
972
1083
|
const response = await conversationsClient.getConversationsByWidgetIdRaw({
|
|
973
1084
|
widgetId: contextId,
|
|
974
|
-
sort: 'created_at:desc'
|
|
1085
|
+
sort: 'created_at:desc',
|
|
1086
|
+
size: 100,
|
|
975
1087
|
});
|
|
976
1088
|
const json = await response.raw.json();
|
|
977
|
-
|
|
978
|
-
|
|
1089
|
+
const conversations = normalizeConversationRecords(json);
|
|
1090
|
+
console.log('[Chat→Backend] Loaded conversations for widget:', contextId, conversations.length);
|
|
1091
|
+
return conversations;
|
|
979
1092
|
} catch (error) {
|
|
980
1093
|
console.error('[Chat→Backend] Failed to load conversations:', error);
|
|
981
1094
|
return null;
|
|
@@ -1066,13 +1179,16 @@
|
|
|
1066
1179
|
return { id: crypto.randomUUID(), title };
|
|
1067
1180
|
}
|
|
1068
1181
|
const response = await conversationsClient.createConversationForWidgetRaw({
|
|
1069
|
-
|
|
1070
|
-
|
|
1182
|
+
createConversationForWidgetRequest: {
|
|
1183
|
+
widget_id: contextId,
|
|
1184
|
+
title,
|
|
1185
|
+
},
|
|
1071
1186
|
});
|
|
1072
1187
|
const json = await response.raw.json();
|
|
1073
1188
|
const conversation = json?.data || json;
|
|
1074
|
-
|
|
1075
|
-
|
|
1189
|
+
const conversationId = conversation?.id || conversation?.conversation_id || conversation?.conversationId;
|
|
1190
|
+
console.log('[Chat→Backend] Created conversation:', conversationId);
|
|
1191
|
+
return conversationId ? { ...conversation, id: conversationId } : conversation;
|
|
1076
1192
|
} catch (error) {
|
|
1077
1193
|
console.error('[Chat→Backend] Failed to create conversation:', error);
|
|
1078
1194
|
return { id: crypto.randomUUID(), title };
|
|
@@ -1358,10 +1474,6 @@
|
|
|
1358
1474
|
live: Partial<WidgetPanelSnapshot>
|
|
1359
1475
|
): string | null {
|
|
1360
1476
|
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
1477
|
|
|
1366
1478
|
const publishedVersion = live.lastPublishedVersion ?? snap.lastPublishedVersion ?? null;
|
|
1367
1479
|
let liveSha: string | null = null;
|
|
@@ -1379,8 +1491,15 @@
|
|
|
1379
1491
|
}
|
|
1380
1492
|
|
|
1381
1493
|
if (liveSha) {
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1494
|
+
const sessionOrdered = [live.lastCommitId, snap.lastCommitId].filter(
|
|
1495
|
+
(sha): sha is string => typeof sha === 'string' && sha.length > 0
|
|
1496
|
+
);
|
|
1497
|
+
const sessionAhead = sessionOrdered.find((candidate) => !commitsMatch(candidate, liveSha));
|
|
1498
|
+
if (sessionAhead) return sessionAhead;
|
|
1499
|
+
const sessionMatch = sessionOrdered.find((candidate) => commitsMatch(candidate, liveSha));
|
|
1500
|
+
if (sessionMatch) return sessionMatch;
|
|
1501
|
+
if (stored && commitsMatch(stored, liveSha)) return stored;
|
|
1502
|
+
return liveSha;
|
|
1384
1503
|
}
|
|
1385
1504
|
|
|
1386
1505
|
return live.lastCommitId ?? snap.lastCommitId ?? stored ?? null;
|
|
@@ -1435,8 +1554,8 @@
|
|
|
1435
1554
|
let editingCompositionName = '';
|
|
1436
1555
|
let deletingCompositionId: string | null = null;
|
|
1437
1556
|
|
|
1438
|
-
/**
|
|
1439
|
-
const DEFAULT_CHAT_CONTAINER_HEIGHT =
|
|
1557
|
+
/** Minimum chat height; flex-1 grows to fill leftover space above Publish. */
|
|
1558
|
+
const DEFAULT_CHAT_CONTAINER_HEIGHT = 280;
|
|
1440
1559
|
const CHAT_MIN_HEIGHT = 260;
|
|
1441
1560
|
const CHAT_MAX_HEIGHT = 1400;
|
|
1442
1561
|
|
|
@@ -2052,11 +2171,15 @@
|
|
|
2052
2171
|
|
|
2053
2172
|
/** Handle content items reorder/add/delete from PropsEditor */
|
|
2054
2173
|
function handleEditorContentItemsChanged(event: CustomEvent<{ items: unknown[] }>) {
|
|
2055
|
-
|
|
2174
|
+
const incoming = Array.isArray(event.detail.items) ? event.detail.items : [];
|
|
2175
|
+
const schemaItems = compositionContentSchema?.contentItems
|
|
2176
|
+
?? compositionContentSchema?.data?.contentItems
|
|
2177
|
+
?? [];
|
|
2178
|
+
liveContentItems = mergeContentItemsWithSchema(incoming, schemaItems);
|
|
2056
2179
|
console.log('[PropsEditor→Preview] Content items changed:', liveContentItems.length, 'items');
|
|
2057
2180
|
getWidgetDetailsRef(focusedPanelWidgetId)?.sendMessageToPreview({
|
|
2058
2181
|
type: 'widgetic:update',
|
|
2059
|
-
contentItems: liveContentItems
|
|
2182
|
+
contentItems: flattenContentItemsForPreview(liveContentItems)
|
|
2060
2183
|
});
|
|
2061
2184
|
compositionDirty = true;
|
|
2062
2185
|
syncFocusedPanelEditorSnapshot();
|
|
@@ -2708,7 +2831,7 @@
|
|
|
2708
2831
|
// Default coder: host prop (site embed) wins. Fallback Flash for speed.
|
|
2709
2832
|
const hostCoderModelId = (defaultCoderModelId || '').trim() || null;
|
|
2710
2833
|
const hostPlannerModelId = (defaultPlannerModelId || '').trim() || null;
|
|
2711
|
-
let defaultModelId = hostCoderModelId || '
|
|
2834
|
+
let defaultModelId = hostCoderModelId || 'zhipu-glm-5.3-flash';
|
|
2712
2835
|
|
|
2713
2836
|
// Available LLM models for code generation
|
|
2714
2837
|
interface LLMModelOption {
|
|
@@ -2716,17 +2839,19 @@
|
|
|
2716
2839
|
name: string;
|
|
2717
2840
|
provider: string;
|
|
2718
2841
|
description: string;
|
|
2842
|
+
supportsVision?: boolean;
|
|
2719
2843
|
}
|
|
2720
2844
|
|
|
2721
2845
|
// Default fallback models in case API fails (one from each provider)
|
|
2722
2846
|
const fallbackModels: LLMModelOption[] = [
|
|
2723
|
-
{ id: '
|
|
2724
|
-
{ id: 'deepseek-v4-
|
|
2725
|
-
{ id: '
|
|
2726
|
-
{ id: 'openai-gpt-5.
|
|
2727
|
-
{ id: 'openai-
|
|
2728
|
-
{ id: '
|
|
2729
|
-
{ id: '
|
|
2847
|
+
{ id: 'zhipu-glm-5.3-flash', name: 'GLM 5.3 Flash (Z.ai)', provider: 'Zhipu', description: 'Cheapest vision + coding model', supportsVision: true },
|
|
2848
|
+
{ id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', provider: 'DeepSeek', description: 'Frontier coding model, 1M context', supportsVision: false },
|
|
2849
|
+
{ id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', provider: 'DeepSeek', description: 'Fast and cost-efficient', supportsVision: false },
|
|
2850
|
+
{ id: 'openai-gpt-5.4', name: 'GPT-5.4 (OpenAI)', provider: 'OpenAI', description: 'Affordable model for coding', supportsVision: true },
|
|
2851
|
+
{ id: 'openai-gpt-5.5', name: 'GPT-5.5 (OpenAI)', provider: 'OpenAI', description: 'Frontier model for complex work', supportsVision: true },
|
|
2852
|
+
{ id: 'openai-codex-3.2', name: 'Codex 3.2 (OpenAI)', provider: 'OpenAI', description: 'Coding-specialized model', supportsVision: false },
|
|
2853
|
+
{ id: 'gemini-3-flash', name: 'Gemini 3 Flash (Google)', provider: 'Google', description: 'Google\'s fast Gemini model', supportsVision: true },
|
|
2854
|
+
{ 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
2855
|
];
|
|
2731
2856
|
|
|
2732
2857
|
// Models loaded from API (single source of truth)
|
|
@@ -2744,11 +2869,12 @@
|
|
|
2744
2869
|
// SDK returns { success, models } directly (not nested under data)
|
|
2745
2870
|
if (response.success && Array.isArray(response.models)) {
|
|
2746
2871
|
// Transform API response to match our interface
|
|
2747
|
-
availableModels = response.models.map((m: { id: string; displayName: string; provider: string; description: string }) => ({
|
|
2872
|
+
availableModels = response.models.map((m: { id: string; displayName: string; provider: string; description: string; supportsVision?: boolean }) => ({
|
|
2748
2873
|
id: m.id,
|
|
2749
2874
|
name: m.displayName,
|
|
2750
2875
|
provider: m.provider.charAt(0).toUpperCase() + m.provider.slice(1), // Capitalize provider
|
|
2751
|
-
description: m.description
|
|
2876
|
+
description: m.description,
|
|
2877
|
+
supportsVision: m.supportsVision === true,
|
|
2752
2878
|
}));
|
|
2753
2879
|
|
|
2754
2880
|
// Model LIST comes from API. UI defaults come from host props when provided.
|
|
@@ -2787,7 +2913,7 @@
|
|
|
2787
2913
|
|
|
2788
2914
|
// Planner model — separate model for prompt enhancement pre-call (debug mode only)
|
|
2789
2915
|
// Empty string = user chose "None (always skip)". Non-empty = preferred planner when auto-heuristics enable it.
|
|
2790
|
-
defaultPlannerModelId = hostPlannerModelId || '
|
|
2916
|
+
defaultPlannerModelId = hostPlannerModelId || 'zhipu-glm-5.3-flash';
|
|
2791
2917
|
let userSelectedPlannerModelId = defaultPlannerModelId;
|
|
2792
2918
|
|
|
2793
2919
|
// Debug ON → dropdown (starts at host default). Debug OFF → host/fallback default.
|
|
@@ -2801,14 +2927,24 @@
|
|
|
2801
2927
|
return false;
|
|
2802
2928
|
}
|
|
2803
2929
|
|
|
2804
|
-
function resolvePlannerModelIdForGeneration(prompt: string): string | undefined {
|
|
2805
|
-
if (
|
|
2806
|
-
if (userSelectedPlannerModelId === '') {
|
|
2930
|
+
function resolvePlannerModelIdForGeneration(prompt: string, coderIdForRequest?: string): string | undefined {
|
|
2931
|
+
if (debugMode && userSelectedPlannerModelId === '') {
|
|
2807
2932
|
console.log('[WidgetCreator] Planner skipped (manual None)');
|
|
2808
2933
|
return undefined;
|
|
2809
2934
|
}
|
|
2810
2935
|
|
|
2811
|
-
const preferredPlannerId =
|
|
2936
|
+
const preferredPlannerId = debugMode
|
|
2937
|
+
? (userSelectedPlannerModelId || defaultPlannerModelId)
|
|
2938
|
+
: defaultPlannerModelId;
|
|
2939
|
+
const selectedCoderId = debugMode ? (userSelectedModelId || defaultModelId) : defaultModelId;
|
|
2940
|
+
const coderId = coderIdForRequest || selectedCoderId;
|
|
2941
|
+
if (!preferredPlannerId || preferredPlannerId === selectedCoderId || preferredPlannerId === coderId) {
|
|
2942
|
+
console.log('[WidgetCreator] Planner skipped (same model as coder — one LLM call)', {
|
|
2943
|
+
planner: preferredPlannerId,
|
|
2944
|
+
coder: coderId,
|
|
2945
|
+
});
|
|
2946
|
+
return undefined;
|
|
2947
|
+
}
|
|
2812
2948
|
const widgetHasCode = !!(lastCommitId || lastPublishedVersion);
|
|
2813
2949
|
|
|
2814
2950
|
if (!widgetHasCode) {
|
|
@@ -2866,6 +3002,108 @@
|
|
|
2866
3002
|
toggleDebugMode();
|
|
2867
3003
|
}
|
|
2868
3004
|
|
|
3005
|
+
let canvasWireframeMode = true;
|
|
3006
|
+
let canvasPrimaryWidgetId: string | null = null;
|
|
3007
|
+
|
|
3008
|
+
function emitCreatorWireframeChanged() {
|
|
3009
|
+
if (typeof window === 'undefined') return;
|
|
3010
|
+
window.dispatchEvent(
|
|
3011
|
+
new CustomEvent(CREATOR_WIREFRAME_CHANGED_EVENT, {
|
|
3012
|
+
detail: { wireframeMode: canvasWireframeMode }
|
|
3013
|
+
})
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
function handleHostToggleWireframeEvent() {
|
|
3018
|
+
canvasWireframeMode = !canvasWireframeMode;
|
|
3019
|
+
console.log('[CreatorApp] Wireframe mode:', canvasWireframeMode ? 'ON' : 'OFF');
|
|
3020
|
+
emitCreatorWireframeChanged();
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
function resolveHostTargetWidgetId(): string | null {
|
|
3024
|
+
const id = focusedPanelWidgetId || selectedWidgetId;
|
|
3025
|
+
if (!id || isDraftConvertId(id)) return null;
|
|
3026
|
+
return id;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
function handleHostOpenWidgetDetailsEvent(): void {
|
|
3030
|
+
const widgetId = resolveHostTargetWidgetId();
|
|
3031
|
+
if (!widgetId) {
|
|
3032
|
+
showToast('warning', 'Select a widget first', {
|
|
3033
|
+
description: 'Click a widget on the canvas, then use Edit to open Widget Details.',
|
|
3034
|
+
});
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
console.log('[CreatorApp] Host Edit — opening Widget Details', widgetId.substring(0, 8));
|
|
3038
|
+
void openWidgetDetailsPanel(widgetId);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
async function handleHostDuplicateWidgetEvent(): Promise<void> {
|
|
3042
|
+
const widgetId = resolveHostTargetWidgetId();
|
|
3043
|
+
if (!widgetId || !canvasRef) {
|
|
3044
|
+
showToast('warning', 'Select a widget first', {
|
|
3045
|
+
description: 'Click the widget on the canvas, then Duplicate.',
|
|
3046
|
+
});
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
canvasRef.selectWidgetShape?.(widgetId);
|
|
3050
|
+
if (typeof canvasRef.duplicateSelectedObjects === 'function') {
|
|
3051
|
+
await canvasRef.duplicateSelectedObjects();
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
showToast('error', 'Cannot duplicate widget', { description: 'Canvas duplicate is not available' });
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
async function handleHostDeleteWidgetEvent(): Promise<void> {
|
|
3058
|
+
const widgetId = resolveHostTargetWidgetId();
|
|
3059
|
+
if (!widgetId) {
|
|
3060
|
+
showToast('warning', 'Select a widget first');
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
if (canvasRef?.selectWidgetShape?.(widgetId) && typeof canvasRef.deleteSelectedObjects === 'function') {
|
|
3064
|
+
await canvasRef.deleteSelectedObjects();
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
const widgetName = getWidgetById(widgetId)?.name || 'Widget';
|
|
3068
|
+
const result = await showDeleteWidgetDialog(widgetId, widgetName);
|
|
3069
|
+
if (result.proceed) {
|
|
3070
|
+
await deleteWidgetFromDb(widgetId);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
async function persistPrimaryWidgetId(widgetId: string) {
|
|
3075
|
+
if (!widgetId) return;
|
|
3076
|
+
canvasPrimaryWidgetId = widgetId;
|
|
3077
|
+
console.log('[CreatorApp] Set primary widget:', widgetId);
|
|
3078
|
+
if (canvasId && canvasesClient) {
|
|
3079
|
+
try {
|
|
3080
|
+
await canvasesClient.updateCanvas({
|
|
3081
|
+
id: canvasId,
|
|
3082
|
+
updateCanvasRequest: { primaryWidgetId: widgetId }
|
|
3083
|
+
});
|
|
3084
|
+
} catch (err) {
|
|
3085
|
+
console.warn('[CreatorApp] Failed to save primary_widget_id:', err);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
const widget = getWidgetById(widgetId);
|
|
3089
|
+
if (widget) notifyHostWidgetName(widget);
|
|
3090
|
+
if (embedded && typeof window !== 'undefined') {
|
|
3091
|
+
try {
|
|
3092
|
+
const url = new URL(window.location.href);
|
|
3093
|
+
if (url.searchParams.get('widget_id') !== widgetId) {
|
|
3094
|
+
url.searchParams.set('widget_id', widgetId);
|
|
3095
|
+
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
3096
|
+
}
|
|
3097
|
+
} catch (err) {
|
|
3098
|
+
console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
function handleSetPrimaryWidget(widgetId: string) {
|
|
3104
|
+
void persistPrimaryWidgetId(widgetId);
|
|
3105
|
+
}
|
|
3106
|
+
|
|
2869
3107
|
// Preview Modal State Variables
|
|
2870
3108
|
let previewIframe: HTMLIFrameElement | null = null;
|
|
2871
3109
|
const { DEV: isDevMode } = import.meta.env;
|
|
@@ -3325,7 +3563,7 @@
|
|
|
3325
3563
|
const getCanvasName = (c: any): string =>
|
|
3326
3564
|
c.persistenceKey || c.name || c.id?.substring(0, 8) || 'Untitled';
|
|
3327
3565
|
const hasCanvasContent = (c: any): boolean =>
|
|
3328
|
-
!!c.canvasContent;
|
|
3566
|
+
!!(c.canvasContent || c.canvas_content);
|
|
3329
3567
|
|
|
3330
3568
|
userCanvases = rows
|
|
3331
3569
|
.map((c: any) => ({
|
|
@@ -3367,12 +3605,22 @@
|
|
|
3367
3605
|
}
|
|
3368
3606
|
|
|
3369
3607
|
// Load canvas shapes first, then hydrate widget metadata from shapes + canvas_id
|
|
3370
|
-
const { widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
3608
|
+
const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
3371
3609
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
3372
3610
|
await maybeAutoRestoreWidgetShapes();
|
|
3373
3611
|
// Backfill widget name + thumbnail on shapes that were saved before those
|
|
3374
3612
|
// fields existed on WidgetShape (fixes "Widget" placeholder on old canvases).
|
|
3375
3613
|
backfillWidgetShapeMetadata();
|
|
3614
|
+
if (loaded && canvasRef) {
|
|
3615
|
+
const primaryId =
|
|
3616
|
+
canvasPrimaryWidgetId ||
|
|
3617
|
+
canvasRef.getPrimaryWidgetId?.() ||
|
|
3618
|
+
selectedWidgetId;
|
|
3619
|
+
if (primaryId) {
|
|
3620
|
+
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
3621
|
+
canvasRef.selectWidgetShape(primaryId);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3376
3624
|
} catch (error) {
|
|
3377
3625
|
console.error('Widget Creator: Load canvases error:', error);
|
|
3378
3626
|
const defaultError = 'Failed to load canvases';
|
|
@@ -3714,34 +3962,30 @@
|
|
|
3714
3962
|
selectedWidgetId = null;
|
|
3715
3963
|
selectedWidget = null;
|
|
3716
3964
|
beginBlankCanvasSession('site New widget');
|
|
3717
|
-
console.log('[CreatorApp]
|
|
3965
|
+
console.log('[CreatorApp] New widget: blank canvas only — widget record is created on Convert (F3)');
|
|
3718
3966
|
const clientsReady = await waitForAuthenticatedCanvasClient();
|
|
3719
3967
|
if (!clientsReady) {
|
|
3720
|
-
console.error('[CreatorApp] Auth/clients not ready —
|
|
3968
|
+
console.error('[CreatorApp] Auth/clients not ready — blank canvas without new canvas row');
|
|
3721
3969
|
} else {
|
|
3722
3970
|
const newCanvasId = await createNewCanvas({ skipSaveCurrent: true });
|
|
3723
3971
|
if (!newCanvasId) {
|
|
3724
3972
|
showToast('error', 'Could not create a blank canvas', {
|
|
3725
|
-
description: '
|
|
3973
|
+
description: 'Try reloading the page to get a fresh canvas.',
|
|
3726
3974
|
duration: 8000
|
|
3727
3975
|
});
|
|
3976
|
+
} else {
|
|
3977
|
+
// Replace `new_widget=1` with the concrete canvas_id so a page refresh
|
|
3978
|
+
// reloads THIS canvas (and its sketch) instead of re-running the
|
|
3979
|
+
// new-widget bootstrap and creating yet another blank canvas row.
|
|
3980
|
+
syncEmbeddedCanvasUrl(newCanvasId);
|
|
3728
3981
|
}
|
|
3729
3982
|
}
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
} else {
|
|
3735
|
-
canvasRef?.onClearAllObjects();
|
|
3736
|
-
relinkSelectedWidgetShape();
|
|
3737
|
-
}
|
|
3738
|
-
lastSavedCanvasObjectCount = canvasRef ? 1 : 0;
|
|
3983
|
+
// F3: New-widget flow starts with an EMPTY canvas — no widget record, no
|
|
3984
|
+
// WidgetShape, no auto-opened Widget Details. The widget record is created
|
|
3985
|
+
// only when the user sketches something and clicks "Convert to Widget".
|
|
3986
|
+
lastSavedCanvasObjectCount = 0;
|
|
3739
3987
|
markCanvasStateLoaded();
|
|
3740
3988
|
isLoadingWidgets = false;
|
|
3741
|
-
if (selectedWidgetId) {
|
|
3742
|
-
console.log('[CreatorApp] Opening Widget Details for new Untitled widget');
|
|
3743
|
-
await openWidgetDetailsPanel(selectedWidgetId);
|
|
3744
|
-
}
|
|
3745
3989
|
setTimeout(() => {
|
|
3746
3990
|
ignoreIncomingCanvasLoads = false;
|
|
3747
3991
|
console.log('[CreatorApp] New-widget canvas lock released');
|
|
@@ -3929,6 +4173,16 @@
|
|
|
3929
4173
|
|
|
3930
4174
|
/** Re-create widget shapes when canvas_content is empty but widgets exist for canvas_id. */
|
|
3931
4175
|
async function maybeAutoRestoreWidgetShapes(): Promise<void> {
|
|
4176
|
+
// F3: New-widget sessions start intentionally empty — never auto-restore
|
|
4177
|
+
// shapes for the freshly created Untitled widget.
|
|
4178
|
+
if (didForceCreateNewWidget) {
|
|
4179
|
+
canvasShapesMissingWarning = false;
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
if (!isCanvasStateLoaded) {
|
|
4183
|
+
console.log('[CanvasDebug] maybeAutoRestore skipped — canvas JSON not loaded yet');
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
3932
4186
|
if (lastSavedCanvasObjectCount !== 0 || widgets.length === 0) {
|
|
3933
4187
|
canvasShapesMissingWarning = false;
|
|
3934
4188
|
return;
|
|
@@ -4170,23 +4424,93 @@
|
|
|
4170
4424
|
return initialWidgetId ?? null;
|
|
4171
4425
|
}
|
|
4172
4426
|
|
|
4427
|
+
/**
|
|
4428
|
+
* Canvas-only embed deep-link (My widgets / canvas without widget_id):
|
|
4429
|
+
* read canvases.primary_widget_id so Creator can open that widget panel.
|
|
4430
|
+
*/
|
|
4431
|
+
async function resolvePrimaryWidgetIdForCanvas(targetCanvasId: string): Promise<string | null> {
|
|
4432
|
+
if (!canvasesClient || !targetCanvasId?.trim()) return null;
|
|
4433
|
+
try {
|
|
4434
|
+
const canvasRecord = await canvasesClient.getCanvas({ id: targetCanvasId });
|
|
4435
|
+
const record =
|
|
4436
|
+
(canvasRecord as { data?: Record<string, unknown> })?.data ??
|
|
4437
|
+
(canvasRecord as Record<string, unknown>);
|
|
4438
|
+
const raw =
|
|
4439
|
+
(record as { primaryWidgetId?: unknown })?.primaryWidgetId ??
|
|
4440
|
+
(record as { primary_widget_id?: unknown })?.primary_widget_id ??
|
|
4441
|
+
null;
|
|
4442
|
+
const primaryId = typeof raw === 'string' && raw.trim() ? raw.trim() : null;
|
|
4443
|
+
if (primaryId) {
|
|
4444
|
+
canvasPrimaryWidgetId = primaryId;
|
|
4445
|
+
console.log(
|
|
4446
|
+
'[CreatorApp] Resolved primary widget for canvas',
|
|
4447
|
+
targetCanvasId.substring(0, 8),
|
|
4448
|
+
'→',
|
|
4449
|
+
primaryId.substring(0, 8)
|
|
4450
|
+
);
|
|
4451
|
+
} else {
|
|
4452
|
+
console.log(
|
|
4453
|
+
'[CreatorApp] No primary_widget_id on canvas',
|
|
4454
|
+
targetCanvasId.substring(0, 8)
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
return primaryId;
|
|
4458
|
+
} catch (err) {
|
|
4459
|
+
console.warn(
|
|
4460
|
+
'[CreatorApp] Failed to resolve primary_widget_id for canvas',
|
|
4461
|
+
targetCanvasId.substring(0, 8),
|
|
4462
|
+
err
|
|
4463
|
+
);
|
|
4464
|
+
return null;
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4173
4468
|
/** Fetch widget by id from site deep-link (My widgets → Edit / New Composition). */
|
|
4174
4469
|
async function ensureEmbeddedWidgetDeepLink(): Promise<void> {
|
|
4175
4470
|
syncEmbeddedDeepLinkDismissTracking();
|
|
4176
|
-
|
|
4177
|
-
if (!embedded || !deepLinkWidgetId || !widgetsClient || !hasValidToken) return;
|
|
4471
|
+
if (!embedded || !widgetsClient || !hasValidToken) return;
|
|
4178
4472
|
if (embeddedDeepLinkPanelDismissed) {
|
|
4473
|
+
const dismissedId = resolveEmbeddedDeepLinkWidgetId();
|
|
4179
4474
|
console.log(
|
|
4180
|
-
'[CreatorApp] Skip embedded deep-link
|
|
4181
|
-
|
|
4475
|
+
'[CreatorApp] Skip embedded deep-link select — user dismissed panel:',
|
|
4476
|
+
dismissedId ? dismissedId.substring(0, 8) : '(none)'
|
|
4182
4477
|
);
|
|
4183
4478
|
return;
|
|
4184
4479
|
}
|
|
4185
|
-
//
|
|
4480
|
+
// Select primary widget once per Creator mount. Do not auto-open Widget Details.
|
|
4186
4481
|
if (embeddedDeepLinkAutoOpenDone) {
|
|
4187
4482
|
return;
|
|
4188
4483
|
}
|
|
4189
4484
|
|
|
4485
|
+
let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
|
|
4486
|
+
// Canvas-only deep-link: no ?widget_id= / initialWidgetId — select canvases.primary_widget_id.
|
|
4487
|
+
if (!deepLinkWidgetId && canvasesClient) {
|
|
4488
|
+
const targetCanvasId =
|
|
4489
|
+
(typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
|
|
4490
|
+
(typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null) ||
|
|
4491
|
+
(typeof routeCanvasId === 'string' && routeCanvasId.trim() ? routeCanvasId.trim() : null);
|
|
4492
|
+
if (targetCanvasId) {
|
|
4493
|
+
deepLinkWidgetId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
|
|
4494
|
+
if (embeddedDeepLinkPanelDismissed || embeddedDeepLinkAutoOpenDone) {
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
// Align address-bar widget_id so header / dismiss tracking match the opened panel.
|
|
4498
|
+
if (deepLinkWidgetId && typeof window !== 'undefined') {
|
|
4499
|
+
try {
|
|
4500
|
+
const url = new URL(window.location.href);
|
|
4501
|
+
if (url.searchParams.get('widget_id') !== deepLinkWidgetId) {
|
|
4502
|
+
url.searchParams.set('widget_id', deepLinkWidgetId);
|
|
4503
|
+
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
4504
|
+
syncEmbeddedDeepLinkDismissTracking();
|
|
4505
|
+
}
|
|
4506
|
+
} catch (err) {
|
|
4507
|
+
console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
if (!deepLinkWidgetId) return;
|
|
4513
|
+
|
|
4190
4514
|
const requestGen = ++embeddedDeepLinkRequestGen;
|
|
4191
4515
|
|
|
4192
4516
|
function deepLinkOpenStillValid(): boolean {
|
|
@@ -4205,8 +4529,8 @@
|
|
|
4205
4529
|
embeddedInitialStepPending = false;
|
|
4206
4530
|
}
|
|
4207
4531
|
embeddedDeepLinkAutoOpenDone = true;
|
|
4208
|
-
if (
|
|
4209
|
-
|
|
4532
|
+
if (canvasRef && deepLinkWidgetId) {
|
|
4533
|
+
canvasRef.selectWidgetShape(deepLinkWidgetId);
|
|
4210
4534
|
}
|
|
4211
4535
|
return;
|
|
4212
4536
|
}
|
|
@@ -4245,11 +4569,12 @@
|
|
|
4245
4569
|
}
|
|
4246
4570
|
|
|
4247
4571
|
embeddedDeepLinkAutoOpenDone = true;
|
|
4248
|
-
//
|
|
4249
|
-
|
|
4250
|
-
await selectWidget(target);
|
|
4572
|
+
// Select the primary widget (and its canvas shape) — do not open Widget Details.
|
|
4573
|
+
await selectWidget(target, { skipLoadWidget: true, skipPlaceShape: true });
|
|
4251
4574
|
if (!deepLinkOpenStillValid()) return;
|
|
4252
|
-
|
|
4575
|
+
if (canvasRef) {
|
|
4576
|
+
canvasRef.selectWidgetShape(target.id);
|
|
4577
|
+
}
|
|
4253
4578
|
if (embeddedInitialStepPending && initialStep) {
|
|
4254
4579
|
widgetDetailsStep = initialStep;
|
|
4255
4580
|
embeddedInitialStepPending = false;
|
|
@@ -4278,7 +4603,8 @@
|
|
|
4278
4603
|
*/
|
|
4279
4604
|
async function syncWidgetsWithCanvas(widgetIdsFromCanvasContent: string[] = []) {
|
|
4280
4605
|
if (!canvasId) {
|
|
4281
|
-
|
|
4606
|
+
// Widget-only or canvas-primary deep-link (ensure resolves primary when widget_id absent).
|
|
4607
|
+
if (embedded) {
|
|
4282
4608
|
await ensureEmbeddedWidgetDeepLink();
|
|
4283
4609
|
}
|
|
4284
4610
|
return;
|
|
@@ -4291,7 +4617,7 @@
|
|
|
4291
4617
|
if (shapeWidgetIds.length > 0) {
|
|
4292
4618
|
await mergeWidgetsLinkedOnCanvas(shapeWidgetIds);
|
|
4293
4619
|
}
|
|
4294
|
-
if (embedded
|
|
4620
|
+
if (embedded) {
|
|
4295
4621
|
await ensureEmbeddedWidgetDeepLink();
|
|
4296
4622
|
}
|
|
4297
4623
|
}
|
|
@@ -4312,7 +4638,7 @@
|
|
|
4312
4638
|
loadCurrentCanvas();
|
|
4313
4639
|
}
|
|
4314
4640
|
|
|
4315
|
-
async function selectWidget(widget: WidgetSummary | null, options: { skipLoadWidget?: boolean; skipEditorContextReset?: boolean } = {}) {
|
|
4641
|
+
async function selectWidget(widget: WidgetSummary | null, options: { skipLoadWidget?: boolean; skipEditorContextReset?: boolean; skipPlaceShape?: boolean } = {}) {
|
|
4316
4642
|
try {
|
|
4317
4643
|
// Save the current widget's prompt before switching away
|
|
4318
4644
|
if (selectedWidgetId) {
|
|
@@ -4367,7 +4693,7 @@
|
|
|
4367
4693
|
syncFileBrowserUploadConfigForCreator();
|
|
4368
4694
|
|
|
4369
4695
|
// Select the linked canvas shape; if none exists, create one (empty canvas + Untitled).
|
|
4370
|
-
if (widget?.id && canvasRef) {
|
|
4696
|
+
if (widget?.id && canvasRef && !options.skipPlaceShape) {
|
|
4371
4697
|
const found = canvasRef.selectWidgetShape(widget.id);
|
|
4372
4698
|
if (!found) {
|
|
4373
4699
|
const anyW = widget as unknown as { thumbnail_url?: string | null; thumbnailUrl?: string | null };
|
|
@@ -4566,10 +4892,11 @@
|
|
|
4566
4892
|
if (!headSha) return;
|
|
4567
4893
|
if (currentPanelLastCommit && commitsMatch(currentPanelLastCommit, headSha)) return;
|
|
4568
4894
|
console.log(`[Version] Repo HEAD probe for "${widgetId}": ${headSha.substring(0, 8)} (was ${currentPanelLastCommit?.substring(0, 8) ?? 'null'})`);
|
|
4569
|
-
patchPanelSnapshot(widgetId, { lastCommitId: headSha });
|
|
4895
|
+
patchPanelSnapshot(widgetId, { lastCommitId: headSha, hasGeneratedCode: true });
|
|
4570
4896
|
saveCommitIdForWidget(widgetId, headSha);
|
|
4571
4897
|
if (widgetId === focusedPanelWidgetId) {
|
|
4572
4898
|
lastCommitId = headSha;
|
|
4899
|
+
hasGeneratedCode = true;
|
|
4573
4900
|
}
|
|
4574
4901
|
}
|
|
4575
4902
|
|
|
@@ -4632,7 +4959,9 @@
|
|
|
4632
4959
|
lastPublishedVersion,
|
|
4633
4960
|
currentRepositoryId,
|
|
4634
4961
|
selectedWidgetJsPath,
|
|
4962
|
+
...(lastCommitId ? { hasGeneratedCode: true } : {}),
|
|
4635
4963
|
});
|
|
4964
|
+
if (lastCommitId) hasGeneratedCode = true;
|
|
4636
4965
|
if (lastPublishedVersion !== null) {
|
|
4637
4966
|
// Load history against the specific widget id (not selectedWidgetId) so the
|
|
4638
4967
|
// panel snapshot is guaranteed to be marked publishHistoryResolved even if the
|
|
@@ -4646,10 +4975,9 @@
|
|
|
4646
4975
|
publishHistoryResolvedForWidgetId: widget.id,
|
|
4647
4976
|
});
|
|
4648
4977
|
}
|
|
4649
|
-
//
|
|
4650
|
-
//
|
|
4651
|
-
|
|
4652
|
-
if (lastPublishedVersion !== null) {
|
|
4978
|
+
// Reconcile GitLab HEAD whenever we have a repo — unpublished widgets
|
|
4979
|
+
// also need lastCommitId so Create preview is not stuck on "No Code Yet".
|
|
4980
|
+
if (currentRepositoryId) {
|
|
4653
4981
|
void reconcileWidgetHeadCommit(widget.id);
|
|
4654
4982
|
}
|
|
4655
4983
|
}
|
|
@@ -5238,9 +5566,23 @@
|
|
|
5238
5566
|
* Returns the default prompt if no stored prompt is found.
|
|
5239
5567
|
*/
|
|
5240
5568
|
async function loadPromptForWidget(widgetId: string) {
|
|
5569
|
+
// Draft convert panels keep their prefilled prompt — nothing stored to load.
|
|
5570
|
+
if (isDraftConvertId(widgetId)) return;
|
|
5241
5571
|
try {
|
|
5572
|
+
// Guard against the convert-flow race: selectWidget fires this loader
|
|
5573
|
+
// without awaiting it; the convert flow then sets the real prompt +
|
|
5574
|
+
// screenshot right after `await selectWidget(...)` returns. The draft
|
|
5575
|
+
// read below resolves LATER and would clobber the fresh prompt with
|
|
5576
|
+
// DEFAULT_PROMPT (feedback b). Skip when a newer load for this widget
|
|
5577
|
+
// has already been superseded by an explicit prompt set.
|
|
5578
|
+
loadPromptGeneration += 1;
|
|
5579
|
+
const loadGeneration = loadPromptGeneration;
|
|
5242
5580
|
// Restore from the durable L1 draft store (legacy localStorage fallback)
|
|
5243
5581
|
const draft = await readWidgetDraft(widgetId);
|
|
5582
|
+
if (loadGeneration !== loadPromptGeneration) {
|
|
5583
|
+
console.log('[Prompt] Skipped stale load for widget', widgetId.substring(0, 8), '— a newer prompt state was applied');
|
|
5584
|
+
return;
|
|
5585
|
+
}
|
|
5244
5586
|
codeGenerationPrompt = draft?.promptText ?? DEFAULT_PROMPT;
|
|
5245
5587
|
pendingChatDraftText = draft?.chatDraft ?? '';
|
|
5246
5588
|
|
|
@@ -5383,14 +5725,20 @@
|
|
|
5383
5725
|
}
|
|
5384
5726
|
}
|
|
5385
5727
|
|
|
5386
|
-
/**
|
|
5728
|
+
/** Vision capability comes from GET /models (`supportsVision`), not from provider-name prefixes. */
|
|
5387
5729
|
function modelSupportsVisionClient(modelId: string): boolean {
|
|
5388
5730
|
if (!modelId) return false;
|
|
5731
|
+
const listed = availableModels.find((m) => m.id === modelId);
|
|
5732
|
+
if (listed && typeof listed.supportsVision === 'boolean') {
|
|
5733
|
+
return listed.supportsVision;
|
|
5734
|
+
}
|
|
5389
5735
|
if (modelId.startsWith('deepseek-')) return false;
|
|
5390
5736
|
return (
|
|
5391
5737
|
modelId.startsWith('openai-') ||
|
|
5392
5738
|
modelId.startsWith('gemini-') ||
|
|
5393
|
-
modelId.startsWith('anthropic-')
|
|
5739
|
+
modelId.startsWith('anthropic-') ||
|
|
5740
|
+
modelId.startsWith('zhipu-') ||
|
|
5741
|
+
modelId.startsWith('glm-')
|
|
5394
5742
|
);
|
|
5395
5743
|
}
|
|
5396
5744
|
|
|
@@ -5481,6 +5829,8 @@
|
|
|
5481
5829
|
console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
|
|
5482
5830
|
return;
|
|
5483
5831
|
}
|
|
5832
|
+
// Manual "Refresh Widget Preview" always opens the panel so the iframe exists.
|
|
5833
|
+
console.log('[handleRefreshScreenshot] Opening Widget Details for manual preview refresh:', widgetId.substring(0, 8));
|
|
5484
5834
|
await openWidgetDetailsPanel(widgetId);
|
|
5485
5835
|
await new Promise(r => setTimeout(r, 300));
|
|
5486
5836
|
}
|
|
@@ -5640,11 +5990,27 @@ Requirements:
|
|
|
5640
5990
|
const targetWidgetId = options.widgetId ?? selectedWidgetId;
|
|
5641
5991
|
if (!targetWidgetId) return;
|
|
5642
5992
|
|
|
5993
|
+
// Convert UX v2: a draft panel's first Generate (button / confirm dialog)
|
|
5994
|
+
// finalizes the conversion — creates widget + repo, replaces the sketch,
|
|
5995
|
+
// and repoints this call at the real widget id. Chat sends are finalized
|
|
5996
|
+
// in handleChatSendMessage before reaching this point.
|
|
5997
|
+
let codegenTargetWidgetId = targetWidgetId;
|
|
5998
|
+
if (isDraftConvertId(targetWidgetId)) {
|
|
5999
|
+
const panelPromptForDraft =
|
|
6000
|
+
targetWidgetId === focusedPanelWidgetId
|
|
6001
|
+
? codeGenerationPrompt
|
|
6002
|
+
: getPanelState(targetWidgetId).codeGenerationPrompt;
|
|
6003
|
+
const draftPrompt = options.prompt || panelPromptForDraft;
|
|
6004
|
+
const finalizedWidgetId = await finalizeDraftConversion(targetWidgetId, draftPrompt);
|
|
6005
|
+
if (!finalizedWidgetId) return;
|
|
6006
|
+
codegenTargetWidgetId = finalizedWidgetId;
|
|
6007
|
+
}
|
|
6008
|
+
|
|
5643
6009
|
// Use override prompt if provided (e.g. from Fix Errors button), otherwise use panel prompt
|
|
5644
6010
|
const panelPrompt =
|
|
5645
|
-
|
|
6011
|
+
codegenTargetWidgetId === focusedPanelWidgetId
|
|
5646
6012
|
? codeGenerationPrompt
|
|
5647
|
-
: getPanelState(
|
|
6013
|
+
: getPanelState(codegenTargetWidgetId).codeGenerationPrompt;
|
|
5648
6014
|
const promptSource = options.prompt || panelPrompt;
|
|
5649
6015
|
const trimmedPrompt = promptSource.trim();
|
|
5650
6016
|
if (trimmedPrompt.length < 10) {
|
|
@@ -5664,7 +6030,7 @@ Requirements:
|
|
|
5664
6030
|
: 'panel prompt';
|
|
5665
6031
|
console.log(
|
|
5666
6032
|
'[generateCode] Widget',
|
|
5667
|
-
|
|
6033
|
+
codegenTargetWidgetId.substring(0, 8),
|
|
5668
6034
|
'| prompt source:',
|
|
5669
6035
|
promptSourceLabel,
|
|
5670
6036
|
'| Length:',
|
|
@@ -5672,17 +6038,17 @@ Requirements:
|
|
|
5672
6038
|
);
|
|
5673
6039
|
|
|
5674
6040
|
if (forceMockReady) {
|
|
5675
|
-
generatingWidgetIds = { ...generatingWidgetIds, [
|
|
5676
|
-
isGeneratingCode =
|
|
5677
|
-
setCrossTabGenerating(true,
|
|
5678
|
-
patchCodegenUiState(
|
|
6041
|
+
generatingWidgetIds = { ...generatingWidgetIds, [codegenTargetWidgetId]: true };
|
|
6042
|
+
isGeneratingCode = codegenTargetWidgetId === focusedPanelWidgetId;
|
|
6043
|
+
setCrossTabGenerating(true, codegenTargetWidgetId);
|
|
6044
|
+
patchCodegenUiState(codegenTargetWidgetId, { generateCodeStatus: 'Generating mock code...' });
|
|
5679
6045
|
setTimeout(() => {
|
|
5680
|
-
const { [
|
|
6046
|
+
const { [codegenTargetWidgetId]: _done, ...rest } = generatingWidgetIds;
|
|
5681
6047
|
generatingWidgetIds = rest;
|
|
5682
6048
|
isGeneratingCode = !!(focusedPanelWidgetId && generatingWidgetIds[focusedPanelWidgetId]);
|
|
5683
|
-
setCrossTabGenerating(false,
|
|
5684
|
-
patchCodegenUiState(
|
|
5685
|
-
if (
|
|
6049
|
+
setCrossTabGenerating(false, codegenTargetWidgetId);
|
|
6050
|
+
patchCodegenUiState(codegenTargetWidgetId, { generateCodeStatus: 'Mock code generation complete!' });
|
|
6051
|
+
if (codegenTargetWidgetId === focusedPanelWidgetId) hasChangesToSave = true;
|
|
5686
6052
|
showToast('success', 'Mock Code Generated', {
|
|
5687
6053
|
description: 'This is a simulation. No actual code was generated.',
|
|
5688
6054
|
duration: 3000
|
|
@@ -5694,7 +6060,7 @@ Requirements:
|
|
|
5694
6060
|
await performCodeGeneration(trimmedPrompt, {
|
|
5695
6061
|
queued: false,
|
|
5696
6062
|
fromChat: options.fromChat,
|
|
5697
|
-
widgetId:
|
|
6063
|
+
widgetId: codegenTargetWidgetId,
|
|
5698
6064
|
compileFixOnly: options.compileFixOnly,
|
|
5699
6065
|
attachmentIds: options.attachmentIds,
|
|
5700
6066
|
promptImagesOverride: options.promptImagesOverride,
|
|
@@ -5813,13 +6179,20 @@ Requirements:
|
|
|
5813
6179
|
: generationWidgetId === focusedPanelWidgetId
|
|
5814
6180
|
? promptImages
|
|
5815
6181
|
: panelSnap.promptImages;
|
|
6182
|
+
// Drop invalid entries (empty strings, non-data-URLs) so a failed capture
|
|
6183
|
+
// can never silently become a vision-less generation request.
|
|
6184
|
+
panelPromptImages = panelPromptImages.filter(
|
|
6185
|
+
(url) => typeof url === 'string' && url.startsWith('data:image/') && url.length > 100,
|
|
6186
|
+
);
|
|
5816
6187
|
const chatAttachmentUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
|
|
5817
6188
|
const chatUploadIds = [
|
|
5818
6189
|
...(options.attachmentIds || []),
|
|
5819
6190
|
...(panelChat?.getAttachmentUploadIds?.() ?? []),
|
|
5820
6191
|
].filter((id, index, arr) => id && arr.indexOf(id) === index);
|
|
5821
6192
|
if (chatAttachmentUrls.length > 0 && !(options.promptImagesOverride?.length)) {
|
|
5822
|
-
panelPromptImages = chatAttachmentUrls
|
|
6193
|
+
panelPromptImages = chatAttachmentUrls.filter(
|
|
6194
|
+
(url) => typeof url === 'string' && url.startsWith('data:image/') && url.length > 100,
|
|
6195
|
+
);
|
|
5823
6196
|
}
|
|
5824
6197
|
let imagesToSend: string[] | undefined;
|
|
5825
6198
|
let attachmentIdsToSend: string[] | undefined;
|
|
@@ -5830,11 +6203,19 @@ Requirements:
|
|
|
5830
6203
|
imagesToSend = await Promise.all(
|
|
5831
6204
|
panelPromptImages.map((url) => compressImageDataUrlForCodegen(url)),
|
|
5832
6205
|
);
|
|
6206
|
+
imagesToSend = imagesToSend.filter(
|
|
6207
|
+
(url) => url.startsWith('data:image/') && url.length > 100,
|
|
6208
|
+
);
|
|
5833
6209
|
patchPanelSnapshot(generationWidgetId, { promptImages: imagesToSend });
|
|
5834
6210
|
if (generationWidgetId === focusedPanelWidgetId) {
|
|
5835
6211
|
promptImages = imagesToSend;
|
|
5836
6212
|
savePromptImagesToSession();
|
|
5837
6213
|
}
|
|
6214
|
+
} else if (promptImpliesVisionReference(prompt)) {
|
|
6215
|
+
console.warn(
|
|
6216
|
+
'[WidgetCreator] Codegen proceeding WITHOUT image although prompt references one — widget',
|
|
6217
|
+
generationWidgetId,
|
|
6218
|
+
);
|
|
5838
6219
|
}
|
|
5839
6220
|
let coderModelForRequest = selectedModelId || defaultModelId;
|
|
5840
6221
|
if (imagesToSend?.length || attachmentIdsToSend?.length) {
|
|
@@ -5850,8 +6231,11 @@ Requirements:
|
|
|
5850
6231
|
const visionFallback = modelSupportsVisionClient(defaultModelId)
|
|
5851
6232
|
? defaultModelId
|
|
5852
6233
|
: 'openai-gpt-5.4';
|
|
6234
|
+
console.log('[WidgetCreator] Coder vision fallback (request only, dropdown unchanged)', {
|
|
6235
|
+
from: coderModelForRequest,
|
|
6236
|
+
to: visionFallback,
|
|
6237
|
+
});
|
|
5853
6238
|
coderModelForRequest = visionFallback;
|
|
5854
|
-
userSelectedModelId = visionFallback;
|
|
5855
6239
|
const visionModel = availableModels.find((m) => m.id === visionFallback);
|
|
5856
6240
|
showToast('info', `Coder: ${visionModel?.name ?? visionFallback} (vision)`, {
|
|
5857
6241
|
description: 'Cu imagini atașate folosim automat un model vision — același ca pe server.',
|
|
@@ -5875,7 +6259,7 @@ Requirements:
|
|
|
5875
6259
|
panelChat?.getConversationId?.() ?? panelSnap.currentChatConversationId ?? currentChatConversationId;
|
|
5876
6260
|
const plannerModelIdForRequest = options.compileFixOnly
|
|
5877
6261
|
? undefined
|
|
5878
|
-
: resolvePlannerModelIdForGeneration(prompt);
|
|
6262
|
+
: resolvePlannerModelIdForGeneration(prompt, coderModelForRequest);
|
|
5879
6263
|
const runtimeLogsForRequest = collectRuntimeLogsForCodegen(generationWidgetId);
|
|
5880
6264
|
const response = await agentClient.agentCodeGeneratePost({
|
|
5881
6265
|
generateCodeRequest: {
|
|
@@ -6118,6 +6502,21 @@ Requirements:
|
|
|
6118
6502
|
liveContentValues = {};
|
|
6119
6503
|
}
|
|
6120
6504
|
|
|
6505
|
+
// Final chat message regardless of panel focus — without this, a
|
|
6506
|
+
// closed/unfocused panel never receives the terminal update and the
|
|
6507
|
+
// chat stays on the last progress line (e.g. stuck at 80%).
|
|
6508
|
+
panelChat?.addAssistantMessage(
|
|
6509
|
+
completedPreviewBuildErr
|
|
6510
|
+
? `Code saved${completedCommitSha ? ` (Commit: ${completedCommitSha.substring(0, 7)})` : ''}, but preview build failed:\n${completedPreviewBuildErr}\n\nRebuilding preview…`
|
|
6511
|
+
: `Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
|
|
6512
|
+
true
|
|
6513
|
+
);
|
|
6514
|
+
if (!completedPreviewBuildErr) {
|
|
6515
|
+
panelChat?.updateLatestCodegenStatusMessage?.(
|
|
6516
|
+
`Code generation completed!${completedCommitSha ? ` Commit: ${completedCommitSha.substring(0, 7)}` : ''}\nPreview is loading…`,
|
|
6517
|
+
);
|
|
6518
|
+
}
|
|
6519
|
+
|
|
6121
6520
|
setTimeout(async () => {
|
|
6122
6521
|
if (!getWidgetDetailsRef(generationWidgetId)) return;
|
|
6123
6522
|
const activePanelRef = getWidgetDetailsRef(generationWidgetId);
|
|
@@ -6238,6 +6637,15 @@ Requirements:
|
|
|
6238
6637
|
return (import.meta.env.VITE_CDN_URL || 'https://cdn.widgetic.com').replace(/\/+$/, '');
|
|
6239
6638
|
}
|
|
6240
6639
|
|
|
6640
|
+
/** Published artifact URL for embed/test page — CDN, never the API gateway origin. */
|
|
6641
|
+
function publishedWidgetHtmlUrl(widgetId: string, version: number, compositionId?: string | null): string {
|
|
6642
|
+
const base = `${publishedCdnBase()}/widgets/${widgetId}/v${version}/widget.html`;
|
|
6643
|
+
if (compositionId) {
|
|
6644
|
+
return `${base}?compositionId=${compositionId}`;
|
|
6645
|
+
}
|
|
6646
|
+
return base;
|
|
6647
|
+
}
|
|
6648
|
+
|
|
6241
6649
|
/** Canonical full CDN URL for a published widget artifact (never a bare pathname). */
|
|
6242
6650
|
function resolvePublishedJsPath(widgetId: string, version: number, artifactUrl?: string | null): string {
|
|
6243
6651
|
const cdnBase = publishedCdnBase();
|
|
@@ -6654,6 +7062,10 @@ Requirements:
|
|
|
6654
7062
|
window.removeEventListener(HOST_WIDGET_RENAMED_EVENT, handleHostWidgetRenamedEvent);
|
|
6655
7063
|
window.removeEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
|
|
6656
7064
|
window.removeEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
|
|
7065
|
+
window.removeEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
|
|
7066
|
+
window.removeEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
|
|
7067
|
+
window.removeEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
|
|
7068
|
+
window.removeEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
|
|
6657
7069
|
for (const wId of Object.keys(generatingWidgetIds)) {
|
|
6658
7070
|
setCrossTabGenerating(false, wId);
|
|
6659
7071
|
}
|
|
@@ -6678,8 +7090,12 @@ Requirements:
|
|
|
6678
7090
|
window.addEventListener(HOST_WIDGET_RENAMED_EVENT, handleHostWidgetRenamedEvent);
|
|
6679
7091
|
window.addEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
|
|
6680
7092
|
window.addEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
|
|
6681
|
-
|
|
7093
|
+
window.addEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
|
|
7094
|
+
window.addEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
|
|
7095
|
+
window.addEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
|
|
7096
|
+
window.addEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
|
|
6682
7097
|
emitCreatorDebugChanged();
|
|
7098
|
+
emitCreatorWireframeChanged();
|
|
6683
7099
|
});
|
|
6684
7100
|
|
|
6685
7101
|
// Capture-phase Escape: close lightbox before Canvas can deselect → close WidgetDetails
|
|
@@ -6794,7 +7210,12 @@ Requirements:
|
|
|
6794
7210
|
|
|
6795
7211
|
function getChatMethodsForWidget(widgetId: string | null | undefined) {
|
|
6796
7212
|
if (!widgetId) return chatMethods ?? null;
|
|
6797
|
-
|
|
7213
|
+
const instanceKey = panelInstanceKeyByWidgetId[widgetId] ?? widgetId;
|
|
7214
|
+
return (
|
|
7215
|
+
chatMethodsByWidgetId[widgetId] ??
|
|
7216
|
+
chatMethodsByWidgetId[instanceKey] ??
|
|
7217
|
+
(widgetId === selectedWidgetId ? chatMethods ?? null : null)
|
|
7218
|
+
);
|
|
6798
7219
|
}
|
|
6799
7220
|
|
|
6800
7221
|
function getOperationStageMessage(metadata?: Record<string, unknown>): string | null {
|
|
@@ -7108,7 +7529,7 @@ Requirements:
|
|
|
7108
7529
|
publishHistoryResolved: false,
|
|
7109
7530
|
publishHistoryResolvedForWidgetId: null,
|
|
7110
7531
|
pendingPublishAfterCodegen: false,
|
|
7111
|
-
hasGeneratedCode:
|
|
7532
|
+
hasGeneratedCode: !!loadCommitIdForWidget(widgetId),
|
|
7112
7533
|
liveDesignValues: {},
|
|
7113
7534
|
liveContentValues: {},
|
|
7114
7535
|
liveContentItems: [],
|
|
@@ -7498,20 +7919,43 @@ Requirements:
|
|
|
7498
7919
|
retryStalePanelSchemaLoads();
|
|
7499
7920
|
}
|
|
7500
7921
|
|
|
7922
|
+
function positionOpenDetailsPanel(widgetId: string): void {
|
|
7923
|
+
const rect = canvasRef?.getWidgetShapeScreenRect?.(widgetId) ?? null;
|
|
7924
|
+
getWidgetDetailsRef(widgetId)?.positionBesideShape?.(rect);
|
|
7925
|
+
}
|
|
7926
|
+
|
|
7501
7927
|
async function openWidgetDetailsPanel(widgetId: string): Promise<void> {
|
|
7928
|
+
const openGen = ++widgetDetailsOpenGen;
|
|
7929
|
+
widgetDetailsOpenTargetId = widgetId;
|
|
7502
7930
|
const deepLinkId = resolveEmbeddedDeepLinkWidgetId();
|
|
7503
7931
|
if (embedded && deepLinkId && widgetId === deepLinkId) {
|
|
7504
7932
|
embeddedDeepLinkPanelDismissed = false;
|
|
7505
7933
|
}
|
|
7934
|
+
// One details panel at a time — close any other open widget first.
|
|
7935
|
+
for (const openId of [...openWidgetPanelIds]) {
|
|
7936
|
+
if (openId !== widgetId) {
|
|
7937
|
+
closeWidgetDetailsPanel(openId, { fromShapeSwitch: true });
|
|
7938
|
+
}
|
|
7939
|
+
}
|
|
7506
7940
|
const isNewPanel = !openWidgetPanelIds.includes(widgetId);
|
|
7507
7941
|
if (isNewPanel) {
|
|
7508
7942
|
ensurePanelState(widgetId);
|
|
7509
7943
|
if (focusedPanelWidgetId) {
|
|
7510
7944
|
captureFocusedPanelSnapshot();
|
|
7511
7945
|
}
|
|
7946
|
+
if (!panelInstanceKeyByWidgetId[widgetId]) {
|
|
7947
|
+
panelInstanceKeyByWidgetId = {
|
|
7948
|
+
...panelInstanceKeyByWidgetId,
|
|
7949
|
+
[widgetId]: widgetId,
|
|
7950
|
+
};
|
|
7951
|
+
}
|
|
7512
7952
|
openWidgetPanelIds = [...openWidgetPanelIds, widgetId];
|
|
7513
7953
|
}
|
|
7514
7954
|
focusWidgetPanel(widgetId);
|
|
7955
|
+
if (openGen !== widgetDetailsOpenGen || widgetDetailsOpenTargetId !== widgetId) {
|
|
7956
|
+
console.log('[CreatorApp] Skip stale Widget Details open:', widgetId?.substring(0, 8));
|
|
7957
|
+
return;
|
|
7958
|
+
}
|
|
7515
7959
|
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
7516
7960
|
// WidgetDetails is dynamically imported — wait for the module, then for bind:this refs.
|
|
7517
7961
|
// Only force-open the requested panel; other open panels keep their own show state.
|
|
@@ -7522,8 +7966,10 @@ Requirements:
|
|
|
7522
7966
|
continue;
|
|
7523
7967
|
}
|
|
7524
7968
|
const ref = getWidgetDetailsRef(widgetId);
|
|
7969
|
+
if (openGen !== widgetDetailsOpenGen || widgetDetailsOpenTargetId !== widgetId) return;
|
|
7525
7970
|
if (ref) {
|
|
7526
7971
|
ref.openWidgetDetails();
|
|
7972
|
+
positionOpenDetailsPanel(widgetId);
|
|
7527
7973
|
ensurePanelEditorData(widgetId);
|
|
7528
7974
|
break;
|
|
7529
7975
|
}
|
|
@@ -7532,20 +7978,8 @@ Requirements:
|
|
|
7532
7978
|
if (!getWidgetDetailsRef(widgetId)) {
|
|
7533
7979
|
console.warn('[CreatorApp] WidgetDetails ref not ready after open retries:', widgetId?.substring(0, 8));
|
|
7534
7980
|
}
|
|
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
|
-
});
|
|
7981
|
+
void scheduleWidgetSchemaLoad(widgetId);
|
|
7982
|
+
ensurePanelEditorData(widgetId);
|
|
7549
7983
|
ensureAllOpenPanelSchemasLoaded();
|
|
7550
7984
|
healStuckPanelSchemaLoadingFlags();
|
|
7551
7985
|
retryStalePanelSchemaLoads();
|
|
@@ -7561,6 +7995,7 @@ Requirements:
|
|
|
7561
7995
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
7562
7996
|
let missing = false;
|
|
7563
7997
|
for (const openId of openWidgetPanelIds) {
|
|
7998
|
+
if (focusedPanelWidgetId && openId !== focusedPanelWidgetId) continue;
|
|
7564
7999
|
const ref = getWidgetDetailsRef(openId);
|
|
7565
8000
|
if (ref) {
|
|
7566
8001
|
ref.openWidgetDetails();
|
|
@@ -7579,6 +8014,49 @@ Requirements:
|
|
|
7579
8014
|
}
|
|
7580
8015
|
}
|
|
7581
8016
|
|
|
8017
|
+
/**
|
|
8018
|
+
* Convert UX: first Generate creates the DB widget. Keep Widget Details mounted
|
|
8019
|
+
* by swapping the panel id in place (same {#each} key) instead of close → open.
|
|
8020
|
+
*/
|
|
8021
|
+
function migrateOpenPanelWidgetId(fromId: string, toId: string): void {
|
|
8022
|
+
if (!fromId || !toId || fromId === toId) return;
|
|
8023
|
+
const instanceKey = panelInstanceKeyByWidgetId[fromId] ?? fromId;
|
|
8024
|
+
const nextKeys = { ...panelInstanceKeyByWidgetId };
|
|
8025
|
+
delete nextKeys[fromId];
|
|
8026
|
+
nextKeys[toId] = instanceKey;
|
|
8027
|
+
panelInstanceKeyByWidgetId = nextKeys;
|
|
8028
|
+
|
|
8029
|
+
const draftZ = panelZIndexByWidgetId[fromId];
|
|
8030
|
+
const { [fromId]: _z, ...restZ } = panelZIndexByWidgetId;
|
|
8031
|
+
panelZIndexByWidgetId =
|
|
8032
|
+
draftZ !== undefined ? { ...restZ, [toId]: draftZ } : restZ;
|
|
8033
|
+
|
|
8034
|
+
const draftRef = widgetDetailsRefs[fromId];
|
|
8035
|
+
const { [fromId]: _r, ...restRefs } = widgetDetailsRefs;
|
|
8036
|
+
widgetDetailsRefs = draftRef ? { ...restRefs, [toId]: draftRef } : restRefs;
|
|
8037
|
+
|
|
8038
|
+
const draftChat = chatMethodsByWidgetId[fromId];
|
|
8039
|
+
if (draftChat) {
|
|
8040
|
+
const { [fromId]: _c, ...restChat } = chatMethodsByWidgetId;
|
|
8041
|
+
chatMethodsByWidgetId = { ...restChat, [toId]: draftChat };
|
|
8042
|
+
if (focusedPanelWidgetId === toId || selectedWidgetId === toId) {
|
|
8043
|
+
chatMethods = draftChat;
|
|
8044
|
+
}
|
|
8045
|
+
}
|
|
8046
|
+
|
|
8047
|
+
const replaced = openWidgetPanelIds.map((id) => (id === fromId ? toId : id));
|
|
8048
|
+
openWidgetPanelIds = replaced.includes(toId) ? replaced : [...replaced, toId];
|
|
8049
|
+
if (focusedPanelWidgetId === fromId) focusedPanelWidgetId = toId;
|
|
8050
|
+
if (selectedWidgetId === fromId) selectedWidgetId = toId;
|
|
8051
|
+
showWidgetDetails = openWidgetPanelIds.length > 0;
|
|
8052
|
+
console.log(
|
|
8053
|
+
'[CreatorApp] Migrated Widget Details panel',
|
|
8054
|
+
fromId.substring(0, 16),
|
|
8055
|
+
'→',
|
|
8056
|
+
toId.substring(0, 8),
|
|
8057
|
+
);
|
|
8058
|
+
}
|
|
8059
|
+
|
|
7582
8060
|
function closeWidgetDetailsPanel(
|
|
7583
8061
|
widgetId: string,
|
|
7584
8062
|
options: { fromShapeSwitch?: boolean } = {},
|
|
@@ -7593,12 +8071,17 @@ Requirements:
|
|
|
7593
8071
|
embeddedDeepLinkPanelDismissed = true;
|
|
7594
8072
|
embeddedDeepLinkRequestGen++;
|
|
7595
8073
|
}
|
|
8074
|
+
if (!options.fromShapeSwitch && widgetDetailsOpenTargetId === widgetId) {
|
|
8075
|
+
widgetDetailsOpenTargetId = null;
|
|
8076
|
+
}
|
|
7596
8077
|
getWidgetDetailsRef(widgetId)?.closeWidgetDetails({ silent: true });
|
|
7597
8078
|
openWidgetPanelIds = openWidgetPanelIds.filter((id) => id !== widgetId);
|
|
7598
8079
|
const { [widgetId]: _z, ...restZ } = panelZIndexByWidgetId;
|
|
7599
8080
|
panelZIndexByWidgetId = restZ;
|
|
7600
8081
|
const { [widgetId]: _removedRef, ...remainingRefs } = widgetDetailsRefs;
|
|
7601
8082
|
widgetDetailsRefs = remainingRefs;
|
|
8083
|
+
const { [widgetId]: _k, ...restKeys } = panelInstanceKeyByWidgetId;
|
|
8084
|
+
panelInstanceKeyByWidgetId = restKeys;
|
|
7602
8085
|
if (focusedPanelWidgetId === widgetId) {
|
|
7603
8086
|
const nextId = openWidgetPanelIds.at(-1) ?? null;
|
|
7604
8087
|
if (nextId) {
|
|
@@ -7620,6 +8103,7 @@ Requirements:
|
|
|
7620
8103
|
}
|
|
7621
8104
|
openWidgetPanelIds = [];
|
|
7622
8105
|
widgetDetailsRefs = {};
|
|
8106
|
+
panelInstanceKeyByWidgetId = {};
|
|
7623
8107
|
focusedPanelWidgetId = null;
|
|
7624
8108
|
panelZIndexByWidgetId = {};
|
|
7625
8109
|
showWidgetDetails = false;
|
|
@@ -7627,14 +8111,16 @@ Requirements:
|
|
|
7627
8111
|
|
|
7628
8112
|
/** Active panel for chat/codegen — falls back to selected widget's ref. */
|
|
7629
8113
|
$: widgetDetails = focusedPanelWidgetId
|
|
7630
|
-
?
|
|
8114
|
+
? getWidgetDetailsRef(focusedPanelWidgetId)
|
|
7631
8115
|
: selectedWidgetId
|
|
7632
|
-
?
|
|
8116
|
+
? getWidgetDetailsRef(selectedWidgetId)
|
|
7633
8117
|
: null;
|
|
7634
8118
|
|
|
7635
8119
|
function getWidgetDetailsRef(widgetId?: string | null): any {
|
|
7636
8120
|
const id = widgetId ?? focusedPanelWidgetId ?? selectedWidgetId;
|
|
7637
|
-
|
|
8121
|
+
if (!id) return null;
|
|
8122
|
+
const instanceKey = panelInstanceKeyByWidgetId[id] ?? id;
|
|
8123
|
+
return widgetDetailsRefs[instanceKey] ?? widgetDetailsRefs[id] ?? null;
|
|
7638
8124
|
}
|
|
7639
8125
|
|
|
7640
8126
|
// Widgets Panel sizing — dynamically adapts to auth panel height
|
|
@@ -7990,8 +8476,12 @@ Requirements:
|
|
|
7990
8476
|
for (const prop of props) {
|
|
7991
8477
|
const name = prop?.name || prop?.id;
|
|
7992
8478
|
if (!name) continue;
|
|
7993
|
-
|
|
7994
|
-
|
|
8479
|
+
const raw = prop.value !== undefined ? prop.value : prop.defaultValue;
|
|
8480
|
+
if (raw === undefined || flat[name] !== undefined) continue;
|
|
8481
|
+
if (raw && typeof raw === 'object' && 'url' in raw) {
|
|
8482
|
+
flat[name] = (raw as { url?: string }).url ?? '';
|
|
8483
|
+
} else {
|
|
8484
|
+
flat[name] = raw;
|
|
7995
8485
|
}
|
|
7996
8486
|
}
|
|
7997
8487
|
}
|
|
@@ -8003,6 +8493,33 @@ Requirements:
|
|
|
8003
8493
|
});
|
|
8004
8494
|
}
|
|
8005
8495
|
|
|
8496
|
+
function contentItemHasProperties(item: any): boolean {
|
|
8497
|
+
return Array.isArray(item?.properties) && item.properties.length > 0;
|
|
8498
|
+
}
|
|
8499
|
+
|
|
8500
|
+
/** New items from the editor arrive flat ({id, caption, image}) — reattach schema fields so the form stays editable. */
|
|
8501
|
+
function attachSchemaProperties(item: any, template: any): any {
|
|
8502
|
+
if (!item || typeof item !== 'object') return item;
|
|
8503
|
+
if (contentItemHasProperties(item)) return item;
|
|
8504
|
+
if (!contentItemHasProperties(template)) return item;
|
|
8505
|
+
const clonedTemplate = JSON.parse(JSON.stringify(template));
|
|
8506
|
+
const nextProperties = clonedTemplate.properties.map((prop: any) => {
|
|
8507
|
+
const key = prop.id || prop.name;
|
|
8508
|
+
const incoming = key != null ? item[key] : undefined;
|
|
8509
|
+
return {
|
|
8510
|
+
...prop,
|
|
8511
|
+
value: incoming !== undefined ? incoming : (prop.defaultValue ?? prop.value)
|
|
8512
|
+
};
|
|
8513
|
+
});
|
|
8514
|
+
return {
|
|
8515
|
+
...clonedTemplate,
|
|
8516
|
+
...item,
|
|
8517
|
+
id: item.id,
|
|
8518
|
+
title: item.title || clonedTemplate.title,
|
|
8519
|
+
properties: nextProperties
|
|
8520
|
+
};
|
|
8521
|
+
}
|
|
8522
|
+
|
|
8006
8523
|
function mergeContentItemsWithSchema(savedItems: any[], schemaItems: any[]): any[] {
|
|
8007
8524
|
if (!Array.isArray(schemaItems) || schemaItems.length === 0) {
|
|
8008
8525
|
return Array.isArray(savedItems) ? [...savedItems] : [];
|
|
@@ -8010,14 +8527,17 @@ Requirements:
|
|
|
8010
8527
|
if (!Array.isArray(savedItems) || savedItems.length === 0) {
|
|
8011
8528
|
return JSON.parse(JSON.stringify(schemaItems));
|
|
8012
8529
|
}
|
|
8530
|
+
const template = schemaItems.find(contentItemHasProperties) || schemaItems[0];
|
|
8013
8531
|
const byId = new Map(savedItems.map((item) => [item.id, item]));
|
|
8014
8532
|
const merged = schemaItems.map((schemaItem) => {
|
|
8015
8533
|
const saved = byId.get(schemaItem.id);
|
|
8016
|
-
|
|
8534
|
+
const clonedSchema = JSON.parse(JSON.stringify(schemaItem));
|
|
8535
|
+
if (!saved) return clonedSchema;
|
|
8536
|
+
return attachSchemaProperties({ ...clonedSchema, ...saved }, clonedSchema);
|
|
8017
8537
|
});
|
|
8018
8538
|
for (const savedItem of savedItems) {
|
|
8019
8539
|
if (!schemaItems.some((schemaItem) => schemaItem.id === savedItem.id)) {
|
|
8020
|
-
merged.push(savedItem);
|
|
8540
|
+
merged.push(attachSchemaProperties(savedItem, template));
|
|
8021
8541
|
}
|
|
8022
8542
|
}
|
|
8023
8543
|
return merged;
|
|
@@ -8255,12 +8775,10 @@ Requirements:
|
|
|
8255
8775
|
};
|
|
8256
8776
|
}
|
|
8257
8777
|
|
|
8258
|
-
//
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|| (headSha && !liveSha)
|
|
8263
|
-
) {
|
|
8778
|
+
// After a live version exists, enable vN+1 only when codegen just finished
|
|
8779
|
+
// or HEAD is proven different from the live SHA. Do not enable just because
|
|
8780
|
+
// live SHA is still hydrating (that looked like "always publish v2").
|
|
8781
|
+
if (pendingPublish || (headSha && liveSha && !commitsMatch(headSha, liveSha))) {
|
|
8264
8782
|
return {
|
|
8265
8783
|
disabled: false,
|
|
8266
8784
|
label: `Publish new v${nextVersion}`,
|
|
@@ -8311,6 +8829,9 @@ Requirements:
|
|
|
8311
8829
|
function publishNeedsSaveFirstForPanel(widgetId: string): boolean {
|
|
8312
8830
|
const ps = getPanelDisplayState(widgetId);
|
|
8313
8831
|
const headSha = resolvePublishHeadCommitId(widgetId, ps);
|
|
8832
|
+
// Compiled preview / known code means Publish is allowed — don't nag "save first"
|
|
8833
|
+
// just because GitLab HEAD hasn't been probed yet.
|
|
8834
|
+
if (panelHasPublishableCode(widgetId, ps)) return false;
|
|
8314
8835
|
return (
|
|
8315
8836
|
!isPublishing &&
|
|
8316
8837
|
!generatingWidgetIds[widgetId] &&
|
|
@@ -8586,6 +9107,11 @@ $: if (lastPublishedVersion !== null && isHistoryOpen) {
|
|
|
8586
9107
|
// Runs reactively when selectedWidgetId + canvasRef + conversationsClient are ready.
|
|
8587
9108
|
let autoPromptSyncedForWidgetId: string | null = null;
|
|
8588
9109
|
$: if (selectedWidgetId && canvasRef && conversationsClient && messagesClient && autoPromptSyncedForWidgetId !== selectedWidgetId) {
|
|
9110
|
+
// Draft convert panels have no DB widget — the prompt sync (and any DB
|
|
9111
|
+
// conversation shell) happens in finalizeDraftConversion on first send.
|
|
9112
|
+
if (isDraftConvertId(selectedWidgetId)) {
|
|
9113
|
+
autoPromptSyncedForWidgetId = selectedWidgetId;
|
|
9114
|
+
} else {
|
|
8589
9115
|
autoPromptSyncedForWidgetId = selectedWidgetId;
|
|
8590
9116
|
const widgetIdSync = selectedWidgetId;
|
|
8591
9117
|
const widgetSync = getWidgetById(widgetIdSync);
|
|
@@ -8634,6 +9160,7 @@ $: if (selectedWidgetId && canvasRef && conversationsClient && messagesClient &&
|
|
|
8634
9160
|
}
|
|
8635
9161
|
}).catch(() => {});
|
|
8636
9162
|
}
|
|
9163
|
+
}
|
|
8637
9164
|
}
|
|
8638
9165
|
|
|
8639
9166
|
// Show "Confirm Generation" dialog for converted widgets that have a prompt + repo
|
|
@@ -8642,6 +9169,11 @@ $: if (selectedWidgetId && canvasRef && conversationsClient && messagesClient &&
|
|
|
8642
9169
|
// Uses setTimeout to allow async prompt loading to finish before evaluating.
|
|
8643
9170
|
let autoGenDialogCheckedForWidgetId: string | null = null;
|
|
8644
9171
|
$: if (selectedWidgetId && autoGenDialogCheckedForWidgetId !== selectedWidgetId) {
|
|
9172
|
+
// Draft convert panels: chat is already prefilled with the convert prompt —
|
|
9173
|
+
// the confirm dialog would be redundant and targets a non-existent widget.
|
|
9174
|
+
if (isDraftConvertId(selectedWidgetId)) {
|
|
9175
|
+
autoGenDialogCheckedForWidgetId = selectedWidgetId;
|
|
9176
|
+
} else {
|
|
8645
9177
|
autoGenDialogCheckedForWidgetId = selectedWidgetId;
|
|
8646
9178
|
const capturedWidgetId = selectedWidgetId;
|
|
8647
9179
|
setTimeout(() => {
|
|
@@ -8664,6 +9196,7 @@ $: if (selectedWidgetId && autoGenDialogCheckedForWidgetId !== selectedWidgetId)
|
|
|
8664
9196
|
console.log('[AutoGenDialog] Conditions not met:', { hasRepo, hasNoCode, hasPreview, hasPrompt, lastCommitId, lastPublishedVersion });
|
|
8665
9197
|
}
|
|
8666
9198
|
}, 1500);
|
|
9199
|
+
}
|
|
8667
9200
|
}
|
|
8668
9201
|
|
|
8669
9202
|
// Auto-save prompt text to the durable draft when it changes (debounced 500ms)
|
|
@@ -9032,8 +9565,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9032
9565
|
|
|
9033
9566
|
const panelRef = getWidgetDetailsRef(widgetId);
|
|
9034
9567
|
const isDetailsOpen = openWidgetPanelIds.includes(widgetId) && (panelRef?.getShowWidgetDetails() ?? false);
|
|
9035
|
-
if (isDetailsOpen
|
|
9036
|
-
|
|
9568
|
+
if (isDetailsOpen) {
|
|
9569
|
+
if (focusedPanelWidgetId !== widgetId) {
|
|
9570
|
+
focusWidgetPanel(widgetId);
|
|
9571
|
+
}
|
|
9572
|
+
console.log('[WidgetCreator] Same widget details already open, skipping reload:', widgetId.substring(0, 8));
|
|
9037
9573
|
return;
|
|
9038
9574
|
}
|
|
9039
9575
|
const widget = getWidgetById(widgetId);
|
|
@@ -9269,14 +9805,29 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9269
9805
|
console.log('[WidgetCreator] Canvas ready — skipping JSON hydrate (new widget)');
|
|
9270
9806
|
lastSavedCanvasObjectCount = 0;
|
|
9271
9807
|
markCanvasStateLoaded();
|
|
9272
|
-
|
|
9808
|
+
// F3: New-widget sessions stay EMPTY — the WidgetShape is created only
|
|
9809
|
+
// by "Convert to Widget", never auto-placed on canvas ready.
|
|
9810
|
+
if (selectedWidget?.id && !didForceCreateNewWidget) {
|
|
9273
9811
|
placeSelectedWidgetShapeOnCanvas();
|
|
9274
9812
|
}
|
|
9275
9813
|
return;
|
|
9276
9814
|
}
|
|
9277
|
-
const { widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
9815
|
+
const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
|
|
9816
|
+
if (!loaded) {
|
|
9817
|
+
console.log('[WidgetCreator] Canvas ready — JSON load deferred until canvasId is resolved');
|
|
9818
|
+
return;
|
|
9819
|
+
}
|
|
9278
9820
|
await syncWidgetsWithCanvas(widgetIdsOnCanvas);
|
|
9279
9821
|
await maybeAutoRestoreWidgetShapes();
|
|
9822
|
+
const primaryId =
|
|
9823
|
+
canvasPrimaryWidgetId ||
|
|
9824
|
+
canvasRef.getPrimaryWidgetId?.() ||
|
|
9825
|
+
selectedWidgetId;
|
|
9826
|
+
if (primaryId && canvasRef) {
|
|
9827
|
+
canvasRef.setPrimaryWidgetShape?.(primaryId);
|
|
9828
|
+
const found = canvasRef.selectWidgetShape(primaryId);
|
|
9829
|
+
console.log('[CreatorApp] Primary widget shape after canvas load:', primaryId, found);
|
|
9830
|
+
}
|
|
9280
9831
|
}
|
|
9281
9832
|
|
|
9282
9833
|
/**
|
|
@@ -9364,12 +9915,22 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9364
9915
|
|
|
9365
9916
|
try {
|
|
9366
9917
|
if (canvasesClient) {
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9918
|
+
let canvasContent: any = null;
|
|
9919
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
9920
|
+
const canvasResponse = await canvasesClient.getCanvasRaw({ id: canvasId });
|
|
9921
|
+
const canvasJson = await canvasResponse.raw.json();
|
|
9922
|
+
const canvasRecord = canvasJson?.data || canvasJson;
|
|
9923
|
+
if (loadEpoch !== canvasLoadEpoch || ignoreIncomingCanvasLoads) {
|
|
9924
|
+
console.log('[CanvasDebug] Ignoring stale getCanvas result', { loadEpoch, canvasLoadEpoch });
|
|
9925
|
+
return emptyResult;
|
|
9926
|
+
}
|
|
9927
|
+
canvasContent = canvasRecord?.canvasContent ?? canvasRecord?.canvas_content ?? null;
|
|
9928
|
+
if (canvasContent) break;
|
|
9929
|
+
console.log('[CanvasDebug] getCanvas returned no canvasContent, retry', attempt + 1);
|
|
9930
|
+
if (attempt < 2) {
|
|
9931
|
+
await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
|
|
9932
|
+
}
|
|
9371
9933
|
}
|
|
9372
|
-
const canvasContent = canvasRecord?.canvasContent;
|
|
9373
9934
|
|
|
9374
9935
|
if (canvasContent) {
|
|
9375
9936
|
const parsed =
|
|
@@ -9412,6 +9973,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9412
9973
|
}
|
|
9413
9974
|
markCanvasStateLoaded();
|
|
9414
9975
|
if (canvasShapesLoaded) canvasApiLoadError = null;
|
|
9976
|
+
if (objectCount > 0) canvasShapesMissingWarning = false;
|
|
9415
9977
|
isLoadCooldownActive = true;
|
|
9416
9978
|
setTimeout(() => { isLoadCooldownActive = false; }, 3000);
|
|
9417
9979
|
lastSavedCanvasObjectCount = objectCount;
|
|
@@ -9569,179 +10131,300 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
9569
10131
|
}
|
|
9570
10132
|
|
|
9571
10133
|
/**
|
|
9572
|
-
*
|
|
9573
|
-
*
|
|
9574
|
-
*
|
|
9575
|
-
*
|
|
9576
|
-
*
|
|
9577
|
-
*
|
|
10134
|
+
* Convert UX v2 — clicking "Convert to Widget" no longer creates anything in
|
|
10135
|
+
* the DB. It only:
|
|
10136
|
+
* 1. keeps the sketch on the canvas (the caller groups it via onGroupSelection),
|
|
10137
|
+
* 2. opens a DRAFT WidgetDetails panel under a synthetic id,
|
|
10138
|
+
* 3. prefills the panel chat with the captured screenshot + CONVERT_PROMPT.
|
|
10139
|
+
* The widget record, component, repository, and shape replacement all happen
|
|
10140
|
+
* in finalizeDraftConversion() when the user sends the first prompt.
|
|
9578
10141
|
*/
|
|
9579
10142
|
async function handleConvertToWidget(payload: WidgetConversionPayload) {
|
|
9580
|
-
console.log('[WidgetCreator] Convert to widget:', payload);
|
|
10143
|
+
console.log('[WidgetCreator] Convert to widget (draft, v2):', payload);
|
|
9581
10144
|
|
|
9582
10145
|
if (!$userSession.user || !hasValidToken) {
|
|
9583
10146
|
showToast('error', 'Please login first');
|
|
9584
10147
|
return;
|
|
9585
10148
|
}
|
|
9586
|
-
|
|
9587
10149
|
if (isWidgetOperationInProgress) {
|
|
9588
10150
|
console.warn('[WidgetCreator] Another widget operation in progress, skipping convert');
|
|
9589
10151
|
showToast('warning', 'Please wait for the current operation to finish');
|
|
9590
10152
|
return;
|
|
9591
10153
|
}
|
|
9592
10154
|
|
|
10155
|
+
const screenshotValid =
|
|
10156
|
+
!!payload.screenshot && payload.screenshot.startsWith('data:image/');
|
|
10157
|
+
if (!screenshotValid) {
|
|
10158
|
+
console.error(
|
|
10159
|
+
'[WidgetCreator] Convert screenshot capture FAILED (length:',
|
|
10160
|
+
payload.screenshot?.length ?? 0,
|
|
10161
|
+
') — keeping original shapes so the sketch is not lost'
|
|
10162
|
+
);
|
|
10163
|
+
showToast('warning', 'Screenshot could not be captured', {
|
|
10164
|
+
description: 'Fix the selection (zoom out / deselect) and try Convert again.',
|
|
10165
|
+
duration: 10000
|
|
10166
|
+
});
|
|
10167
|
+
return;
|
|
10168
|
+
}
|
|
10169
|
+
|
|
10170
|
+
const draftId = `${DRAFT_CONVERT_ID_PREFIX}${Date.now()}`;
|
|
10171
|
+
const draftName = payload.frameName && payload.frameName !== 'image'
|
|
10172
|
+
? `Converted ${payload.frameName}`
|
|
10173
|
+
: `ConvertedW-${Date.now()}`;
|
|
10174
|
+
draftConvertScreenshots.set(draftId, payload.screenshot);
|
|
10175
|
+
draftConvertObjectIds.set(draftId, payload.frameId);
|
|
10176
|
+
|
|
10177
|
+
// Group the sketch into one editable object and keep it on canvas — the
|
|
10178
|
+
// user may keep editing it (ungroup, move, restyle) and re-convert later.
|
|
10179
|
+
// The returned id identifies the group for the later shape replacement.
|
|
10180
|
+
if (canvasRef?.onGroupSelection) {
|
|
10181
|
+
try {
|
|
10182
|
+
const groupId = await canvasRef.onGroupSelection();
|
|
10183
|
+
if (groupId) draftConvertObjectIds.set(draftId, groupId);
|
|
10184
|
+
} catch (groupErr) {
|
|
10185
|
+
console.warn('[WidgetCreator] Group selection failed — continuing with raw selection:', groupErr);
|
|
10186
|
+
}
|
|
10187
|
+
}
|
|
10188
|
+
|
|
10189
|
+
// Re-convert of a group that already has a draft: reopen that draft
|
|
10190
|
+
// panel instead of piling up duplicate drafts for the same sketch.
|
|
10191
|
+
const canvasObjectId = draftConvertObjectIds.get(draftId);
|
|
10192
|
+
const existingDraftId = canvasObjectId
|
|
10193
|
+
? [...draftConvertObjectIds.entries()].find(([id, objId]) => id !== draftId && objId === canvasObjectId)?.[0]
|
|
10194
|
+
: undefined;
|
|
10195
|
+
if (existingDraftId) {
|
|
10196
|
+
draftConvertObjectIds.delete(draftId);
|
|
10197
|
+
draftConvertScreenshots.delete(draftId);
|
|
10198
|
+
console.log('[WidgetCreator] Convert re-opened existing draft for group:', existingDraftId);
|
|
10199
|
+
await openWidgetDetailsPanel(existingDraftId);
|
|
10200
|
+
// Refresh the stored screenshot so finalize uses the latest sketch look
|
|
10201
|
+
draftConvertScreenshots.set(existingDraftId, payload.screenshot);
|
|
10202
|
+
authStatus = 'Widget draft ready — send the prompt to generate.';
|
|
10203
|
+
return;
|
|
10204
|
+
}
|
|
10205
|
+
|
|
9593
10206
|
try {
|
|
9594
10207
|
isWidgetOperationInProgress = true;
|
|
10208
|
+
authStatus = 'Preparing widget draft...';
|
|
10209
|
+
|
|
10210
|
+
// Draft panel snapshot (no widget record in DB). WidgetDetails is
|
|
10211
|
+
// null-safe for a missing widget — the panel shows the draft name.
|
|
10212
|
+
await openWidgetDetailsPanel(draftId);
|
|
10213
|
+
patchPanelSnapshot(draftId, {
|
|
10214
|
+
codeGenerationPrompt: CONVERT_PROMPT,
|
|
10215
|
+
promptImages: [payload.screenshot],
|
|
10216
|
+
});
|
|
10217
|
+
|
|
10218
|
+
// Prefill the chat draft — sending the message starts the real conversion.
|
|
10219
|
+
// Set the globals too: if the Chat component registers later (async mount),
|
|
10220
|
+
// restoreChatDraftFromStorage reapplies this exact draft to the input.
|
|
10221
|
+
loadPromptGeneration += 1;
|
|
10222
|
+
promptImages = [payload.screenshot];
|
|
10223
|
+
codeGenerationPrompt = CONVERT_PROMPT;
|
|
10224
|
+
pendingChatDraftText = CONVERT_PROMPT;
|
|
10225
|
+
chatDraftRestoredForWidgetId = null;
|
|
10226
|
+
savePromptForWidget(draftId);
|
|
10227
|
+
const panelChat = getChatMethodsForWidget(draftId);
|
|
10228
|
+
if (panelChat) {
|
|
10229
|
+
panelChat.setDraftText?.(CONVERT_PROMPT);
|
|
10230
|
+
panelChat.addImageAttachment?.(payload.screenshot, 'design-screenshot.png');
|
|
10231
|
+
console.log(
|
|
10232
|
+
'[Convert] Draft chat prefilled (no auto-start):',
|
|
10233
|
+
draftId,
|
|
10234
|
+
'| prompt:',
|
|
10235
|
+
CONVERT_PROMPT,
|
|
10236
|
+
'| screenshot attached: true'
|
|
10237
|
+
);
|
|
10238
|
+
} else {
|
|
10239
|
+
console.warn('[Convert] Chat methods not ready — draft stays in panel snapshot for restore:', draftId);
|
|
10240
|
+
}
|
|
10241
|
+
|
|
10242
|
+
authStatus = 'Widget draft ready — send the prompt to generate.';
|
|
10243
|
+
showToast('info', 'Widget draft created', {
|
|
10244
|
+
description: 'Edit the sketch freely. Sending the prompt creates the widget.'
|
|
10245
|
+
});
|
|
10246
|
+
} catch (error) {
|
|
10247
|
+
console.error('[WidgetCreator] Convert to widget (draft) error:', error);
|
|
10248
|
+
draftConvertObjectIds.delete(draftId);
|
|
10249
|
+
draftConvertScreenshots.delete(draftId);
|
|
10250
|
+
authStatus = toErrorMessage(error, 'Failed to prepare widget draft.');
|
|
10251
|
+
showToast('error', 'Failed to prepare widget draft');
|
|
10252
|
+
} finally {
|
|
10253
|
+
isWidgetOperationInProgress = false;
|
|
10254
|
+
}
|
|
10255
|
+
}
|
|
10256
|
+
|
|
10257
|
+
/**
|
|
10258
|
+
* Runs on the FIRST prompt send of a draft conversion: creates the widget
|
|
10259
|
+
* record + component + repository, replaces the grouped sketch on canvas with
|
|
10260
|
+
* a WidgetShape screenshot, and repoints panel state from draftId → widgetId.
|
|
10261
|
+
*/
|
|
10262
|
+
async function finalizeDraftConversion(draftId: string, prompt: string): Promise<string | null> {
|
|
10263
|
+
if (!draftId.startsWith(DRAFT_CONVERT_ID_PREFIX)) return null;
|
|
10264
|
+
if (finalizingDraftConversions.has(draftId)) return null;
|
|
10265
|
+
finalizingDraftConversions.add(draftId);
|
|
10266
|
+
// Keep Widget Details open while the canvas sketch is replaced (selection
|
|
10267
|
+
// change would otherwise close the panel, then we'd remount it).
|
|
10268
|
+
suppressWidgetDetailsClose = true;
|
|
10269
|
+
|
|
10270
|
+
try {
|
|
10271
|
+
const screenshot = draftConvertScreenshots.get(draftId) ?? '';
|
|
10272
|
+
const canvasObjectId = draftConvertObjectIds.get(draftId) ?? '';
|
|
10273
|
+
const draftPanelState = getPanelState(draftId);
|
|
10274
|
+
const draftName = getWidgetById(draftId)?.name
|
|
10275
|
+
?? `ConvertedW-${Date.now()}`;
|
|
10276
|
+
|
|
9595
10277
|
authStatus = 'Creating widget from design...';
|
|
9596
10278
|
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
10279
|
const response = await widgetsClient.createWidget(
|
|
9603
10280
|
{
|
|
9604
10281
|
newWidget: {
|
|
9605
|
-
name:
|
|
10282
|
+
name: draftName,
|
|
9606
10283
|
type: 'widget',
|
|
9607
|
-
|
|
10284
|
+
// Canvas sketch snapshot is stored on the widget for future reference
|
|
10285
|
+
content: { source: 'canvas_convert', prompt },
|
|
9608
10286
|
isDraft: true,
|
|
9609
|
-
|
|
10287
|
+
description: draftName,
|
|
9610
10288
|
...(canvasId ? { canvasId } : {}),
|
|
9611
10289
|
...embeddedWidgetProjectContext()
|
|
9612
10290
|
}
|
|
9613
10291
|
},
|
|
9614
|
-
{
|
|
9615
|
-
headers: { 'Idempotency-Key': idempotencyKey }
|
|
9616
|
-
}
|
|
10292
|
+
{ headers: { 'Idempotency-Key': idempotencyKey } }
|
|
9617
10293
|
);
|
|
9618
10294
|
|
|
9619
10295
|
if (!response || response.error || response.message) {
|
|
9620
|
-
console.error('[WidgetCreator] Widget create
|
|
9621
|
-
authStatus = `Create widget failed: ${response?.message || response?.error || 'Unknown error'}`;
|
|
10296
|
+
console.error('[WidgetCreator] Widget create on first send failed:', response);
|
|
9622
10297
|
showToast('error', 'Failed to create widget from design');
|
|
9623
|
-
return;
|
|
10298
|
+
return null;
|
|
9624
10299
|
}
|
|
9625
10300
|
|
|
9626
10301
|
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
|
-
}
|
|
10302
|
+
if (!newWidget?.id) {
|
|
10303
|
+
showToast('warning', 'Widget created but no ID returned');
|
|
10304
|
+
return null;
|
|
10305
|
+
}
|
|
10306
|
+
const widgetId: string = newWidget.id;
|
|
9666
10307
|
|
|
9667
|
-
|
|
10308
|
+
// Component record (canvas → widget bridge) — non-blocking.
|
|
10309
|
+
if (canvasId && componentsClient) {
|
|
9668
10310
|
try {
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
10311
|
+
await componentsClient.createComponent({
|
|
10312
|
+
canvasId,
|
|
10313
|
+
createComponentRequest: {
|
|
10314
|
+
name: draftName,
|
|
10315
|
+
widget_id: widgetId,
|
|
10316
|
+
is_widget: true,
|
|
10317
|
+
}
|
|
10318
|
+
});
|
|
10319
|
+
console.log('[WidgetCreator] Component record created for widget', widgetId);
|
|
10320
|
+
} catch (compErr) {
|
|
10321
|
+
console.warn('[WidgetCreator] Failed to create component record (non-blocking):', compErr);
|
|
9677
10322
|
}
|
|
10323
|
+
}
|
|
9678
10324
|
|
|
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);
|
|
10325
|
+
// Add to widgets list so getWidgetById resolves the real widget from now on.
|
|
10326
|
+
widgets = [{ ...newWidget }, ...widgets.filter((w) => w.id !== draftId)];
|
|
9691
10327
|
|
|
9692
|
-
|
|
10328
|
+
// Replace the grouped sketch with a WidgetShape screenshot. On failure
|
|
10329
|
+
// fall back to linking the sketch object to the widget id.
|
|
10330
|
+
if (canvasRef && canvasObjectId) {
|
|
9693
10331
|
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);
|
|
10332
|
+
await canvasRef.replaceObjectWithWidgetImage(canvasObjectId, widgetId, screenshot);
|
|
10333
|
+
console.log('[WidgetCreator] Sketch replaced with WidgetShape screenshot');
|
|
10334
|
+
// Flush the canvas AFTER the WidgetShape image fields are set
|
|
10335
|
+
// (synchronous in replaceObjectWithWidgetImage). saveNow bypasses
|
|
10336
|
+
// the autosave debounce so a quick refresh can't lose the replace.
|
|
10337
|
+
setTimeout(() => {
|
|
10338
|
+
canvasRef?.saveNow?.();
|
|
10339
|
+
}, 800);
|
|
10340
|
+
} catch (replaceErr) {
|
|
10341
|
+
console.warn('[WidgetCreator] Sketch replace failed (falling back to link):', replaceErr);
|
|
10342
|
+
canvasRef.linkFrameToWidget(canvasObjectId, widgetId);
|
|
10343
|
+
setTimeout(() => {
|
|
10344
|
+
canvasRef?.saveNow?.();
|
|
10345
|
+
}, 800);
|
|
9709
10346
|
}
|
|
10347
|
+
}
|
|
9710
10348
|
|
|
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 });
|
|
10349
|
+
// Auto-create repository so generation can start right away.
|
|
10350
|
+
try {
|
|
10351
|
+
const repoId = await ensureWidgetRepository(newWidget, { silent: true });
|
|
10352
|
+
if (repoId) {
|
|
10353
|
+
newWidget.repositoryId = repoId;
|
|
10354
|
+
widgets = widgets.map((w) => w.id === widgetId ? { ...w, repositoryId: repoId } : w);
|
|
10355
|
+
console.log('[WidgetCreator] Repository auto-created for converted widget:', repoId);
|
|
9726
10356
|
}
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
10357
|
+
} catch (repoErr) {
|
|
10358
|
+
console.warn('[WidgetCreator] Auto-create repo failed (non-blocking):', repoErr);
|
|
10359
|
+
}
|
|
10360
|
+
|
|
10361
|
+
// Migrate draft panel state → real widget panel state, then reopen the
|
|
10362
|
+
// panel under the real id so Chat binds to the widget's conversation.
|
|
10363
|
+
// Persist the real widget's first conversation + user message BEFORE the
|
|
10364
|
+
// panel remounts: the freshly mounted Chat then activates this
|
|
10365
|
+
// conversation (best-conversation = one with messages) instead of
|
|
10366
|
+
// starting empty after a close/reopen.
|
|
10367
|
+
await ensureCodegenUserMessagePersisted(widgetId, prompt);
|
|
10368
|
+
// NOTE: never copy currentChatConversationId from the draft — the draft
|
|
10369
|
+
// chat's conversation is a local UUID that doesn't exist in the DB, and
|
|
10370
|
+
// migrating it would point codegen/summaries at a phantom conversation.
|
|
10371
|
+
const { currentChatConversationId: _draftConvId, ...draftPanelStateClean } =
|
|
10372
|
+
draftPanelState as Record<string, unknown>;
|
|
10373
|
+
patchPanelSnapshot(widgetId, {
|
|
10374
|
+
...draftPanelStateClean,
|
|
10375
|
+
} as Partial<WidgetPanelSnapshot>);
|
|
10376
|
+
migrateOpenPanelWidgetId(draftId, widgetId);
|
|
10377
|
+
widgetPanelSnapshots = Object.fromEntries(
|
|
10378
|
+
Object.entries(widgetPanelSnapshots).filter(([key]) => key !== draftId),
|
|
10379
|
+
);
|
|
10380
|
+
draftConvertObjectIds.delete(draftId);
|
|
10381
|
+
draftConvertScreenshots.delete(draftId);
|
|
10382
|
+
await selectWidget(getWidgetById(widgetId) ?? newWidget, {
|
|
10383
|
+
skipEditorContextReset: true,
|
|
10384
|
+
skipPlaceShape: true,
|
|
10385
|
+
});
|
|
10386
|
+
focusWidgetPanel(widgetId);
|
|
10387
|
+
await tick();
|
|
10388
|
+
const migratedRef = getWidgetDetailsRef(widgetId);
|
|
10389
|
+
if (migratedRef?.getShowWidgetDetails?.()) {
|
|
10390
|
+
console.log('[CreatorApp] Draft→widget: Widget Details stayed open, updating in place');
|
|
10391
|
+
positionOpenDetailsPanel(widgetId);
|
|
9732
10392
|
} else {
|
|
9733
|
-
|
|
9734
|
-
showToast('warning', 'Widget created but no ID returned');
|
|
10393
|
+
await openWidgetDetailsPanel(widgetId);
|
|
9735
10394
|
}
|
|
10395
|
+
loadPromptGeneration += 1;
|
|
10396
|
+
const migrationPrompt = draftPanelState.codeGenerationPrompt || prompt;
|
|
10397
|
+
const migrationImages = draftPanelState.promptImages?.length
|
|
10398
|
+
? draftPanelState.promptImages
|
|
10399
|
+
: (screenshot ? [screenshot] : []);
|
|
10400
|
+
codeGenerationPrompt = migrationPrompt;
|
|
10401
|
+
promptImages = migrationImages;
|
|
10402
|
+
// Carry the user's typed text + sketch attachment into the real widget's
|
|
10403
|
+
// chat draft (restoreChatDraftFromStorage consumes these on Chat ready).
|
|
10404
|
+
pendingChatDraftText = prompt;
|
|
10405
|
+
chatDraftRestoredForWidgetId = null;
|
|
10406
|
+
savePromptForWidget(widgetId);
|
|
10407
|
+
|
|
10408
|
+
authStatus = 'Widget created from design.';
|
|
10409
|
+
showToast('success', 'Widget created', {
|
|
10410
|
+
description: `"${draftName}" is ready — generation started`
|
|
10411
|
+
});
|
|
10412
|
+
return widgetId;
|
|
9736
10413
|
} catch (error) {
|
|
9737
|
-
console.error('[WidgetCreator]
|
|
9738
|
-
|
|
9739
|
-
|
|
10414
|
+
console.error('[WidgetCreator] finalizeDraftConversion error:', error);
|
|
10415
|
+
showToast('error', 'Failed to finalize widget conversion');
|
|
10416
|
+
return null;
|
|
9740
10417
|
} finally {
|
|
9741
|
-
|
|
10418
|
+
suppressWidgetDetailsClose = false;
|
|
10419
|
+
finalizingDraftConversions.delete(draftId);
|
|
9742
10420
|
}
|
|
9743
10421
|
}
|
|
9744
10422
|
|
|
10423
|
+
/** Draft panel id helper — true when the id is a synthetic convert draft. */
|
|
10424
|
+
function isDraftConvertId(widgetId: string | null | undefined): boolean {
|
|
10425
|
+
return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
|
|
10426
|
+
}
|
|
10427
|
+
|
|
9745
10428
|
|
|
9746
10429
|
/**
|
|
9747
10430
|
* Creates or recreates a repository for a specific widget.
|
|
@@ -10033,7 +10716,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10033
10716
|
}}
|
|
10034
10717
|
onresize={() => {
|
|
10035
10718
|
if (focusedPanelWidgetId) {
|
|
10036
|
-
getWidgetDetailsRef(focusedPanelWidgetId)?.
|
|
10719
|
+
getWidgetDetailsRef(focusedPanelWidgetId)?.keepPanelInViewport?.();
|
|
10037
10720
|
}
|
|
10038
10721
|
}}
|
|
10039
10722
|
/>
|
|
@@ -10219,6 +10902,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10219
10902
|
<svelte:component this={WidgeticCanvas}
|
|
10220
10903
|
bind:this={canvasRef}
|
|
10221
10904
|
showContainerFrame={!embedded}
|
|
10905
|
+
bind:wireframeMode={canvasWireframeMode}
|
|
10222
10906
|
onReady={handleCanvasReady}
|
|
10223
10907
|
onAutoSave={handleCanvasAutoSave}
|
|
10224
10908
|
onBeforeUnmount={(json) => {
|
|
@@ -10234,9 +10918,20 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10234
10918
|
onWidgetShapeDeleting={handleWidgetShapeDeleting}
|
|
10235
10919
|
onWidgetRename={handleWidgetRename}
|
|
10236
10920
|
onRefreshScreenshot={handleRefreshScreenshot}
|
|
10921
|
+
onSetPrimaryWidget={handleSetPrimaryWidget}
|
|
10237
10922
|
getWidgetName={getWidgetNameById}
|
|
10238
10923
|
isWidgetKnown={isWidgetKnownById}
|
|
10239
|
-
onNonWidgetSelected={() => {
|
|
10924
|
+
onNonWidgetSelected={() => {
|
|
10925
|
+
if (suppressWidgetDetailsClose) {
|
|
10926
|
+
console.log('[CreatorApp] Skip Widget Details close — canvas selection changed during draft finalize');
|
|
10927
|
+
return;
|
|
10928
|
+
}
|
|
10929
|
+
const active = canvasRef?.getSelectedObject?.() ?? canvasRef?.selectedObject;
|
|
10930
|
+
if (active?._isWidgetImage && active?._widgetId) {
|
|
10931
|
+
return;
|
|
10932
|
+
}
|
|
10933
|
+
widgetDetails?.closeWidgetDetails();
|
|
10934
|
+
}}
|
|
10240
10935
|
panZoomPosition="TC"
|
|
10241
10936
|
showSaveButton={true}
|
|
10242
10937
|
autosaveDelayMs={import.meta.env.DEV ? 15000 : 5000}
|
|
@@ -10534,7 +11229,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10534
11229
|
|
|
10535
11230
|
<!-- ═══ PANEL 3: Widget Details — one floating panel per opened widget ═══ -->
|
|
10536
11231
|
{#if WidgetDetails}
|
|
10537
|
-
{#each openWidgetPanelIds as panelWidgetId, panelIndex (panelWidgetId)}
|
|
11232
|
+
{#each openWidgetPanelIds as panelWidgetId, panelIndex (panelInstanceKeyByWidgetId[panelWidgetId] ?? panelWidgetId)}
|
|
11233
|
+
{@const panelInstanceKey = panelInstanceKeyByWidgetId[panelWidgetId] ?? panelWidgetId}
|
|
10538
11234
|
{@const panelWidget = widgetsById.get(panelWidgetId) ?? null}
|
|
10539
11235
|
{@const isPanelFocused = focusedPanelWidgetId === panelWidgetId}
|
|
10540
11236
|
{@const ps = (panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
|
|
@@ -10542,7 +11238,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10542
11238
|
{@const panelHasCompileErrorState = (panelDisplayRevision, panelHasCompileError(panelWidgetId))}
|
|
10543
11239
|
{@const panelHasInfraPreviewError = (panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
|
|
10544
11240
|
<svelte:component this={WidgetDetails}
|
|
10545
|
-
bind:this={widgetDetailsRefs[
|
|
11241
|
+
bind:this={widgetDetailsRefs[panelInstanceKey]}
|
|
10546
11242
|
userSession={$userSession}
|
|
10547
11243
|
{hasValidToken}
|
|
10548
11244
|
selectedWidgetId={panelWidgetId}
|
|
@@ -10591,7 +11287,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10591
11287
|
isFocusedPanel={isPanelFocused}
|
|
10592
11288
|
panelTopOffset={widgetsPanelTopOffset}
|
|
10593
11289
|
panelBottomMargin={embedded ? 24 : widgetsPanelBottomMargin}
|
|
10594
|
-
fillViewport={
|
|
11290
|
+
fillViewport={false}
|
|
10595
11291
|
currentStep={ps.widgetDetailsStep}
|
|
10596
11292
|
on:focusPanel={() => focusWidgetPanel(panelWidgetId)}
|
|
10597
11293
|
on:stepChange={({ detail }) => {
|
|
@@ -10649,17 +11345,26 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10649
11345
|
on:buildFromRepoSuccess={({ detail }) => {
|
|
10650
11346
|
console.log('[WidgetCreator] Build from repo succeeded for widget:', detail.widgetId);
|
|
10651
11347
|
if (detail.widgetId !== panelWidgetId) return;
|
|
10652
|
-
|
|
11348
|
+
const repoHead =
|
|
11349
|
+
detail.repoHeadCommitSha ||
|
|
11350
|
+
detail.repo_head_commit_sha ||
|
|
11351
|
+
null;
|
|
11352
|
+
const hasSha = typeof repoHead === 'string' && repoHead.length > 0;
|
|
11353
|
+
if (hasSha) {
|
|
11354
|
+
saveCommitIdForWidget(detail.widgetId, repoHead);
|
|
11355
|
+
}
|
|
11356
|
+
// Always persist hasGeneratedCode on the snapshot so Publish re-renders
|
|
11357
|
+
// even when Worker cache hits without a SHA and the panel is unfocused.
|
|
11358
|
+
patchPanelSnapshot(detail.widgetId, {
|
|
11359
|
+
panelPreviewCompileError: null,
|
|
11360
|
+
hasGeneratedCode: true,
|
|
11361
|
+
...(hasSha ? { lastCommitId: repoHead } : {}),
|
|
11362
|
+
});
|
|
10653
11363
|
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;
|
|
11364
|
+
hasGeneratedCode = true;
|
|
11365
|
+
if (hasSha) lastCommitId = repoHead;
|
|
11366
|
+
if (!hasSha) {
|
|
11367
|
+
void reconcileWidgetHeadCommit(detail.widgetId);
|
|
10663
11368
|
}
|
|
10664
11369
|
if (isPanelFocused && generateCodeStatus?.includes('preview build failed')) {
|
|
10665
11370
|
generateCodeStatus = 'Code generation completed! Preview ready.';
|
|
@@ -10792,6 +11497,14 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10792
11497
|
on:widgetDetailsToggle={({ detail }) => {
|
|
10793
11498
|
const isClosing = detail?.open === false || detail?.showWidgetDetails === false;
|
|
10794
11499
|
if (isClosing) {
|
|
11500
|
+
if (suppressWidgetDetailsClose) {
|
|
11501
|
+
console.log('[CreatorApp] Ignoring Widget Details close during draft→widget migrate');
|
|
11502
|
+
return;
|
|
11503
|
+
}
|
|
11504
|
+
if (widgetDetailsOpenTargetId && widgetDetailsOpenTargetId !== panelWidgetId) {
|
|
11505
|
+
console.log('[CreatorApp] Ignoring close from previous details panel:', panelWidgetId?.substring(0, 8));
|
|
11506
|
+
return;
|
|
11507
|
+
}
|
|
10795
11508
|
if (isPanelFocused && selectedWidgetId) {
|
|
10796
11509
|
savePromptForWidget(selectedWidgetId);
|
|
10797
11510
|
captureFocusedPanelSnapshot();
|
|
@@ -10822,20 +11535,17 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10822
11535
|
<svelte:fragment slot="code-generation">
|
|
10823
11536
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
10824
11537
|
<div
|
|
10825
|
-
class="parallel-panel-left-slot-ct flex flex-col min-h-0"
|
|
11538
|
+
class="parallel-panel-left-slot-ct flex flex-col flex-1 min-h-0 overflow-hidden h-full"
|
|
10826
11539
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
10827
11540
|
>
|
|
10828
|
-
<!-- Sticky header: title + model selectors (debug mode)
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
{#if debugMode}
|
|
11541
|
+
<!-- Sticky header: title + model selectors (debug mode).
|
|
11542
|
+
z-20 keeps it above Chat's inline error banner (z-10) so the
|
|
11543
|
+
banner scrolls under the dropdowns instead of covering them. -->
|
|
11544
|
+
{#if debugMode}
|
|
11545
|
+
<div class="generation-chat-sticky-header shrink-0 z-20 bg-white border-b border-gray-200">
|
|
10834
11546
|
<!-- 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">
|
|
11547
|
+
<div class="generation-models-header flex items-center gap-2 px-2 py-1.5 bg-gray-50">
|
|
11548
|
+
<span class="text-[10px] font-semibold text-gray-500 uppercase tracking-wide shrink-0">Models</span>
|
|
10839
11549
|
<div class="planner-model-selector flex items-center gap-1.5 flex-1 min-w-0">
|
|
10840
11550
|
<label class="text-[10px] text-gray-500 font-medium whitespace-nowrap">Planner:</label>
|
|
10841
11551
|
<select
|
|
@@ -10869,13 +11579,14 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10869
11579
|
</select>
|
|
10870
11580
|
</div>
|
|
10871
11581
|
</div>
|
|
10872
|
-
{/if}
|
|
10873
11582
|
</div>
|
|
11583
|
+
{/if}
|
|
10874
11584
|
|
|
11585
|
+
<div class="generation-scroll-ct flex flex-col flex-1 min-h-0 overflow-y-auto scrollbar-thin">
|
|
10875
11586
|
<!-- Chat-based Code Generation Panel -->
|
|
10876
|
-
{#key
|
|
10877
|
-
<Chat class="code-generation-chat"
|
|
10878
|
-
style="height: {ps.chatContainerHeight}px;"
|
|
11587
|
+
{#key panelInstanceKey}
|
|
11588
|
+
<Chat class="code-generation-chat flex-1 w-full shrink-0"
|
|
11589
|
+
style="min-height: {ps.chatContainerHeight}px;"
|
|
10879
11590
|
resizeExpandsContainer={true}
|
|
10880
11591
|
onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
|
|
10881
11592
|
showLogs={debugMode}
|
|
@@ -10984,13 +11695,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
|
|
|
10984
11695
|
</div>
|
|
10985
11696
|
{/if}
|
|
10986
11697
|
</div>
|
|
10987
|
-
</div>
|
|
10988
|
-
</svelte:fragment>
|
|
10989
|
-
|
|
10990
|
-
<svelte:fragment slot="publish-widget">
|
|
10991
11698
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
10992
11699
|
<div
|
|
10993
|
-
class="parallel-panel-publish-slot-ct"
|
|
11700
|
+
class="parallel-panel-publish-slot-ct shrink-0 mt-2"
|
|
10994
11701
|
onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
|
|
10995
11702
|
>
|
|
10996
11703
|
<!-- ===== Publish Widget Section ===== -->
|
|
@@ -11053,7 +11760,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11053
11760
|
{/if}
|
|
11054
11761
|
|
|
11055
11762
|
{#if ps.lastPublishedVersion !== null}
|
|
11056
|
-
<a href="/test-widget?widgetId={panelWidgetId}&version={ps.lastPublishedVersion}" target="_blank" rel="noopener noreferrer" class="flex justify-center items-center gap-1.5 w-full text-sm px-3 py-2 rounded-lg border border-emerald-200 bg-white text-emerald-700 hover:bg-emerald-50 transition-colors">
|
|
11763
|
+
<a href="/test-widget?widgetId={panelWidgetId}&version={ps.lastPublishedVersion}&url={encodeURIComponent(publishedWidgetHtmlUrl(panelWidgetId, ps.lastPublishedVersion))}" target="_blank" rel="noopener noreferrer" class="flex justify-center items-center gap-1.5 w-full text-sm px-3 py-2 rounded-lg border border-emerald-200 bg-white text-emerald-700 hover:bg-emerald-50 transition-colors">
|
|
11057
11764
|
Open Published Widget (v{ps.lastPublishedVersion}) ↗
|
|
11058
11765
|
</a>
|
|
11059
11766
|
{/if}
|
|
@@ -11135,8 +11842,12 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11135
11842
|
|
|
11136
11843
|
</div> <!-- end of publish-widget-section -->
|
|
11137
11844
|
</div>
|
|
11845
|
+
</div> <!-- generation-scroll-ct -->
|
|
11846
|
+
</div> <!-- parallel-panel-left-slot-ct -->
|
|
11138
11847
|
</svelte:fragment>
|
|
11139
11848
|
|
|
11849
|
+
<svelte:fragment slot="publish-widget"></svelte:fragment>
|
|
11850
|
+
|
|
11140
11851
|
<svelte:fragment slot="composition-editor">
|
|
11141
11852
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
11142
11853
|
<div
|
|
@@ -11453,9 +12164,11 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
|
|
|
11453
12164
|
</div>
|
|
11454
12165
|
|
|
11455
12166
|
{#if lastPublishedVersion !== null}
|
|
11456
|
-
{@const embedSrc =
|
|
11457
|
-
|
|
11458
|
-
|
|
12167
|
+
{@const embedSrc = publishedWidgetHtmlUrl(
|
|
12168
|
+
selectedWidgetId,
|
|
12169
|
+
lastPublishedVersion,
|
|
12170
|
+
selectedComposition?.id
|
|
12171
|
+
)}
|
|
11459
12172
|
{@const embedCodeResult = generateEmbedCode(embedFormat, {
|
|
11460
12173
|
embedSrc,
|
|
11461
12174
|
compositionId: selectedComposition?.id ?? null,
|