@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.
@@ -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
- const panelChat = getChatMethodsForWidget(widgetId);
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(widgetId, { promptImages: panelPromptImages });
772
- if (widgetId === focusedPanelWidgetId) {
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(widgetId).catch(() => {
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 + widgetId);
783
- localStorage.removeItem(CHAT_DRAFT_STORAGE_PREFIX + widgetId);
784
- localStorage.removeItem(PROMPT_TEXT_STORAGE_PREFIX + widgetId);
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
- : widgetId === focusedPanelWidgetId
865
+ : codegenWidgetId === focusedPanelWidgetId
795
866
  ? promptImages
796
- : getPanelState(widgetId).promptImages;
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
- /** Persist the user prompt to DB only when generation actually starts. */
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
- let convos = await handleLoadConversations(widgetId);
899
- let convId = convos?.[0]?.id;
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
- console.log('[Chat→Backend] Loaded conversations for widget:', contextId, json?.data?.length || 0);
978
- return json?.data || [];
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
- widgetId: contextId,
1070
- title
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
- console.log('[Chat→Backend] Created conversation:', conversation?.id);
1075
- return conversation;
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 ahead = ordered.find((candidate) => !commitsMatch(candidate, liveSha));
1383
- if (ahead) return ahead;
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
- /** Default chat container height (px) per Widget Details panel. */
1439
- const DEFAULT_CHAT_CONTAINER_HEIGHT = 550;
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
- liveContentItems = event.detail.items;
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 || 'deepseek-v4-flash';
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: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', provider: 'DeepSeek', description: 'Frontier coding model, 1M context' },
2724
- { id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', provider: 'DeepSeek', description: 'Fast and cost-efficient' },
2725
- { id: 'openai-gpt-5.4', name: 'GPT-5.4 (OpenAI)', provider: 'OpenAI', description: 'Affordable model for coding' },
2726
- { id: 'openai-gpt-5.5', name: 'GPT-5.5 (OpenAI)', provider: 'OpenAI', description: 'Frontier model for complex work' },
2727
- { id: 'openai-codex-3.2', name: 'Codex 3.2 (OpenAI)', provider: 'OpenAI', description: 'Coding-specialized model' },
2728
- { id: 'gemini-3-flash', name: 'Gemini 3 Flash (Google)', provider: 'Google', description: 'Google\'s fast Gemini model' },
2729
- { id: 'anthropic-claude-sonnet-4.6', name: 'Claude Sonnet 4.6 (Anthropic)', provider: 'Anthropic', description: 'Best balance of intelligence, speed, and cost' },
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 || 'openai-gpt-5.5';
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 (isProduction || !debugMode) return undefined;
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 = userSelectedPlannerModelId || defaultPlannerModelId;
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] Creating empty canvas + Untitled widget for site New widget');
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 — Untitled widget without new canvas row');
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: 'The Untitled widget was still created. Avoid using the previous canvas shapes.',
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
- await createNewWidgetRecord();
3731
- const canvasReady = await waitForCanvasReadyToPlaceShape();
3732
- if (canvasReady) {
3733
- placeSelectedWidgetShapeOnCanvas();
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
- const deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
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 open — user dismissed panel:',
4181
- deepLinkWidgetId.substring(0, 8)
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
- // Auto-open only once per Creator mount (initial My widgets → Edit). Shape/sidebar clicks open explicitly.
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 (!openWidgetPanelIds.includes(deepLinkWidgetId) && deepLinkOpenStillValid()) {
4209
- await openWidgetDetailsPanel(deepLinkWidgetId);
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
- // Open Widget Details immediately — do not wait on canvas JSON load
4249
- // (large canvases can keep "Loading canvas…" up and leave the panel hidden).
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
- await openWidgetDetailsPanel(target.id);
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
- if (embedded && resolveEmbeddedDeepLinkWidgetId()) {
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 && resolveEmbeddedDeepLinkWidgetId()) {
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
- // Background probe: reconcile local lastCommitId with GitLab HEAD so the Edit banner
4650
- // ("Newer code than Live") is accurate even when the client has no cached commit SHA
4651
- // (fresh session, different machine, or codegen happened outside this tab).
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
- /** Models that accept image_url parts in codegen (DeepSeek = false on API). */
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
- targetWidgetId === focusedPanelWidgetId
6011
+ codegenTargetWidgetId === focusedPanelWidgetId
5646
6012
  ? codeGenerationPrompt
5647
- : getPanelState(targetWidgetId).codeGenerationPrompt;
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
- targetWidgetId.substring(0, 8),
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, [targetWidgetId]: true };
5676
- isGeneratingCode = targetWidgetId === focusedPanelWidgetId;
5677
- setCrossTabGenerating(true, targetWidgetId);
5678
- patchCodegenUiState(targetWidgetId, { generateCodeStatus: 'Generating mock code...' });
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 { [targetWidgetId]: _done, ...rest } = generatingWidgetIds;
6046
+ const { [codegenTargetWidgetId]: _done, ...rest } = generatingWidgetIds;
5681
6047
  generatingWidgetIds = rest;
5682
6048
  isGeneratingCode = !!(focusedPanelWidgetId && generatingWidgetIds[focusedPanelWidgetId]);
5683
- setCrossTabGenerating(false, targetWidgetId);
5684
- patchCodegenUiState(targetWidgetId, { generateCodeStatus: 'Mock code generation complete!' });
5685
- if (targetWidgetId === focusedPanelWidgetId) hasChangesToSave = true;
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: targetWidgetId,
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
- // Site header Debug button needs initial ON/OFF state (embed is a separate Svelte tree).
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
- return chatMethodsByWidgetId[widgetId] ?? (widgetId === selectedWidgetId ? chatMethods ?? null : null);
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: false,
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
- // After {#each} remount, other panels may have lost open state — re-open without stacking extras.
7536
- for (const openId of openWidgetPanelIds) {
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
- ? widgetDetailsRefs[focusedPanelWidgetId]
8114
+ ? getWidgetDetailsRef(focusedPanelWidgetId)
7631
8115
  : selectedWidgetId
7632
- ? widgetDetailsRefs[selectedWidgetId]
8116
+ ? getWidgetDetailsRef(selectedWidgetId)
7633
8117
  : null;
7634
8118
 
7635
8119
  function getWidgetDetailsRef(widgetId?: string | null): any {
7636
8120
  const id = widgetId ?? focusedPanelWidgetId ?? selectedWidgetId;
7637
- return id ? widgetDetailsRefs[id] : null;
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
- if (prop.defaultValue !== undefined && flat[name] === undefined) {
7994
- flat[name] = prop.defaultValue;
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
- return saved ? { ...JSON.parse(JSON.stringify(schemaItem)), ...saved } : JSON.parse(JSON.stringify(schemaItem));
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
- // Newer than live, OR codegen just finished (pending) enable Publish new.
8259
- if (
8260
- pendingPublish
8261
- || (headSha && liveSha && !commitsMatch(headSha, liveSha))
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 && focusedPanelWidgetId === widgetId) {
9036
- console.log('[WidgetCreator] Same widget shape re-clicked and panel focused, skipping reload:', widgetId);
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
- if (selectedWidget?.id) {
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
- const canvasRecord = await canvasesClient.getCanvas({ id: canvasId });
9368
- if (loadEpoch !== canvasLoadEpoch || ignoreIncomingCanvasLoads) {
9369
- console.log('[CanvasDebug] Ignoring stale getCanvas result', { loadEpoch, canvasLoadEpoch });
9370
- return emptyResult;
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
- * Called when user clicks "Convert to Widget" on a canvas shape.
9573
- * 1. Creates a widget record via API
9574
- * 2. Creates a component record linking canvas widget
9575
- * 3. Replaces the original shape with a Widget Image (screenshot)
9576
- * 4. Pre-fills promptImages + codeGenerationPrompt for WidgetDetails
9577
- * 5. Opens WidgetDetails panel
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: convertedName,
10282
+ name: draftName,
9606
10283
  type: 'widget',
9607
- content: payload.canvasData,
10284
+ // Canvas sketch snapshot is stored on the widget for future reference
10285
+ content: { source: 'canvas_convert', prompt },
9608
10286
  isDraft: true,
9609
- ...(payload.frameName ? { description: payload.frameName } : {}),
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 from frame failed:', response);
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
- // 2. Create component record (canvas → widget bridge)
9629
- if (canvasId && componentsClient) {
9630
- try {
9631
- await componentsClient.createComponent({
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
- // 5. Auto-create repository so the widget is ready for code generation
10308
+ // Component record (canvas widget bridge) non-blocking.
10309
+ if (canvasId && componentsClient) {
9668
10310
  try {
9669
- const repoId = await ensureWidgetRepository(newWidget, { silent: true });
9670
- if (repoId) {
9671
- newWidget.repositoryId = repoId;
9672
- widgets = widgets.map((w) => w.id === newWidget.id ? { ...w, repositoryId: repoId } : w);
9673
- console.log('[WidgetCreator] Repository auto-created for converted widget:', repoId);
9674
- }
9675
- } catch (repoErr) {
9676
- console.warn('[WidgetCreator] Auto-create repo failed (non-blocking):', repoErr);
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
- // 6. Mark for auto code generation BEFORE selectWidget
9680
- pendingAutoGenerateWidgetId = newWidget.id;
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
- // 7b. Persist the initial prompt as first message in a DB conversation
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
- if (conversationsClient && messagesClient) {
9695
- const convTitle = payload.frameName
9696
- ? `Convert: ${payload.frameName}`
9697
- : 'Widget conversion';
9698
- const conv = await handleCreateConversation(newWidget.id, convTitle);
9699
- if (conv?.id) {
9700
- const promptContent = promptImages.length > 0
9701
- ? `[image:design-screenshot]\n\n${codeGenerationPrompt}`
9702
- : codeGenerationPrompt;
9703
- await handleSaveMessage(conv.id, promptContent, 'user');
9704
- console.log('[WidgetCreator] Saved initial conversion prompt to DB conversation:', conv.id);
9705
- }
9706
- }
9707
- } catch (convErr) {
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
- // 8. Open WidgetDetails panel
9712
- await openWidgetDetailsPanel(newWidget.id);
9713
-
9714
- // 9. Auto-start codegen (convert flow — no confirm dialog)
9715
- if (pendingAutoGenerateWidgetId && pendingAutoGenerateWidgetId === newWidget.id) {
9716
- const autoGenWidgetId = pendingAutoGenerateWidgetId;
9717
- pendingAutoGenerateWidgetId = null;
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
- authStatus = 'Widget created from design.';
9729
- showToast('success', 'Widget created', {
9730
- description: `"${payload.frameName}" converted to widget`
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
- authStatus = 'Widget created but response missing id.';
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] Convert to widget error:', error);
9738
- authStatus = toErrorMessage(error, 'Failed to convert frame to widget.');
9739
- showToast('error', 'Failed to convert frame to widget');
10414
+ console.error('[WidgetCreator] finalizeDraftConversion error:', error);
10415
+ showToast('error', 'Failed to finalize widget conversion');
10416
+ return null;
9740
10417
  } finally {
9741
- isWidgetOperationInProgress = false;
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)?.centerDetailsPanel();
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={() => { widgetDetails?.closeWidgetDetails(); }}
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[panelWidgetId]}
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={embedded}
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
- patchPanelSnapshot(detail.widgetId, { panelPreviewCompileError: null });
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
- if (isPanelFocused) hasGeneratedCode = true;
10655
- const repoHead =
10656
- detail.repoHeadCommitSha ||
10657
- detail.repo_head_commit_sha ||
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
- <div class="generation-chat-sticky-header sticky top-0 z-10 bg-white border-b border-gray-200">
10830
- <div class="generation-chat-title flex items-center gap-2 px-3 py-1.5 bg-gray-50">
10831
- <h4 class="text-xs font-semibold text-gray-700">Widget Generation Chat</h4>
10832
- </div>
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-0.5 bg-gray-100 border-t border-gray-200">
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 panelWidgetId}
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 = selectedComposition
11457
- ? `${apiRoot ?? ''}/widgets/${selectedWidgetId}/v${lastPublishedVersion}/widget.html?compositionId=${selectedComposition.id}`
11458
- : `${apiRoot ?? ''}/widgets/${selectedWidgetId}/v${lastPublishedVersion}/widget.html`}
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,