@widgetic/creator 0.3.49 → 0.3.51

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