@widgetic/creator 0.3.50 → 0.3.53

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);
@@ -105,6 +111,21 @@
105
111
 
106
112
  function notifyHostWidgetName(widget: { id?: string | null; name?: string | null } | null | undefined): void {
107
113
  if (!embedded || !widget?.id) return;
114
+ const primaryId = (canvasPrimaryWidgetId || '').trim();
115
+ if (!primaryId) {
116
+ console.log('[CreatorApp] Skip host title — primary widget unknown');
117
+ emitCanvasWidgetsList('skip-host-title');
118
+ return;
119
+ }
120
+ if (widget.id !== primaryId) {
121
+ console.log(
122
+ '[CreatorApp] Skip host title — not primary',
123
+ widget.id.substring(0, 8),
124
+ primaryId.substring(0, 8)
125
+ );
126
+ emitCanvasWidgetsList('skip-host-title');
127
+ return;
128
+ }
108
129
  const name = (widget.name || '').trim() || 'Untitled Widget';
109
130
  console.log('[CreatorApp] Notify host widget name:', widget.id, name);
110
131
  onWidgetRenamed?.({ widgetId: widget.id, name });
@@ -132,11 +153,18 @@
132
153
  const HOST_REQUEST_PUBLISH_EVENT = 'widgetic:host-request-publish';
133
154
  const HOST_TOGGLE_DEBUG_EVENT = 'widgetic:host-toggle-debug';
134
155
  const HOST_TOGGLE_WIREFRAME_EVENT = 'widgetic:host-toggle-wireframe';
156
+ const HOST_TOGGLE_ADVANCED_EVENT = 'widgetic:host-toggle-advanced';
135
157
  const HOST_OPEN_WIDGET_DETAILS_EVENT = 'widgetic:host-open-widget-details';
136
158
  const HOST_DUPLICATE_WIDGET_EVENT = 'widgetic:host-duplicate-widget';
137
159
  const HOST_DELETE_WIDGET_EVENT = 'widgetic:host-delete-widget';
160
+ const HOST_SAVE_CANVAS_EVENT = 'widgetic:host-save-canvas';
161
+ const HOST_SET_PRIMARY_WIDGET_EVENT = 'widgetic:host-set-primary-widget';
162
+ const HOST_REQUEST_CANVAS_WIDGETS_EVENT = 'widgetic:host-request-canvas-widgets';
163
+ const HOST_MOVE_CANVAS_FOLDER_EVENT = 'widgetic:host-move-canvas-folder';
138
164
  const CREATOR_DEBUG_CHANGED_EVENT = 'widgetic:creator-debug-changed';
139
165
  const CREATOR_WIREFRAME_CHANGED_EVENT = 'widgetic:creator-wireframe-changed';
166
+ const CREATOR_ADVANCED_CHANGED_EVENT = 'widgetic:creator-advanced-changed';
167
+ const CREATOR_CANVAS_WIDGETS_EVENT = 'widgetic:creator-canvas-widgets';
140
168
 
141
169
  /** Site AppHeader rename — embed is a second Svelte runtime, so mount props do not update. */
142
170
  async function applyHostWidgetName(widgetId: string | null | undefined, newName: string): Promise<void> {
@@ -238,7 +266,7 @@
238
266
  /**
239
267
  * WIDGETIC EDITOR — PropsEditor for design/content property editing
240
268
  */
241
- import { PropsEditor, setFileBrowserUploadConfig } from '@widgetic/editor';
269
+ import { PropsEditor, setFileBrowserUploadConfig, setGoogleFontsApiKey } from '@widgetic/editor';
242
270
 
243
271
  // API root: embedded hosts pass apiUrl; standalone uses VITE_API_URL / PUBLIC_API_URL.
244
272
  // If unset, @widgetic/api-sdk falls back to BASE_PATH = dev-api.widgetic.com (OpenAPI default).
@@ -316,9 +344,41 @@
316
344
  : new UploadsApi({ accessToken: token, middleware: [idempotencyKeyMiddleware] });
317
345
 
318
346
  authenticatedClientsToken = token;
347
+ try {
348
+ setWsAuthToken(token);
349
+ } catch {
350
+ /* websocket store may not be ready yet */
351
+ }
319
352
  syncFileBrowserUploadConfigForCreator();
320
353
  }
321
354
 
355
+ function applySiteSessionToCreator(): boolean {
356
+ const synced = syncSessionFromSiteAuth();
357
+ if (!synced) return false;
358
+ try {
359
+ const sessionStr = localStorage.getItem('session');
360
+ const session = sessionStr ? JSON.parse(sessionStr) : null;
361
+ currentUserToken =
362
+ session?.accessToken ||
363
+ session?.access_token ||
364
+ localStorage.getItem('jwt') ||
365
+ currentUserToken;
366
+ } catch {
367
+ currentUserToken = localStorage.getItem('jwt') || currentUserToken;
368
+ }
369
+ if (currentUserToken) {
370
+ authenticatedClientsToken = null;
371
+ initializeAuthenticatedClients(currentUserToken);
372
+ }
373
+ updateTokenStatus();
374
+ console.log('[Auth] Creator SDK clients re-bound from site session');
375
+ return hasValidToken;
376
+ }
377
+
378
+ function handleSiteSessionExtended(): void {
379
+ applySiteSessionToCreator();
380
+ }
381
+
322
382
  function syncFileBrowserUploadConfigForCreator() {
323
383
  if (!authenticatedClientsToken) {
324
384
  setFileBrowserUploadConfig(null);
@@ -509,6 +569,7 @@
509
569
  let linkedWidgetIds: Set<string> = new Set();
510
570
  function refreshLinkedWidgetIds() {
511
571
  linkedWidgetIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set();
572
+ emitCanvasWidgetsList('linked-ids');
512
573
  }
513
574
 
514
575
  // Delete Widget Shape confirmation dialog state
@@ -517,6 +578,8 @@
517
578
  let deleteDialogWidgetId = '';
518
579
  let deleteDialogStep: 'confirm-remove' | 'confirm-delete-db' = 'confirm-remove';
519
580
  let deleteDialogResolve: ((result: { proceed: boolean; deleteFromDb: boolean }) => void) | null = null;
581
+ /** Keep second-step UI/code; MVP deletes the DB widget after the first confirm. */
582
+ const ASK_DELETE_FROM_DB_CONFIRM = false;
520
583
 
521
584
  function showDeleteWidgetDialog(widgetId: string, widgetName: string): Promise<{ proceed: boolean; deleteFromDb: boolean }> {
522
585
  return new Promise((resolve) => {
@@ -535,10 +598,15 @@
535
598
  if (deleteDialogResolve) deleteDialogResolve({ proceed: false, deleteFromDb: false });
536
599
  break;
537
600
  case 'remove-and-ask-db':
538
- if (deleteDialogResolve) deleteDialogResolve({ proceed: true, deleteFromDb: false });
539
- deleteDialogResolve = null;
540
- deleteDialogStep = 'confirm-delete-db';
541
- return;
601
+ if (ASK_DELETE_FROM_DB_CONFIRM) {
602
+ if (deleteDialogResolve) deleteDialogResolve({ proceed: true, deleteFromDb: false });
603
+ deleteDialogResolve = null;
604
+ deleteDialogStep = 'confirm-delete-db';
605
+ return;
606
+ }
607
+ deleteDialogOpen = false;
608
+ if (deleteDialogResolve) deleteDialogResolve({ proceed: true, deleteFromDb: true });
609
+ break;
542
610
  case 'keep-in-db':
543
611
  deleteDialogOpen = false;
544
612
  showToast('info', 'Shape removed', { description: 'Widget kept in database' });
@@ -559,6 +627,7 @@
559
627
  if (!widgetsClient || !hasValidToken) return;
560
628
  try {
561
629
  console.log('[WidgetCreator] Deleting widget from DB:', widgetId);
630
+ closeWidgetDetailsPanel(widgetId);
562
631
  await widgetsClient.deleteWidget({ id: widgetId });
563
632
  widgets = widgets.filter((w: any) => w.id !== widgetId);
564
633
  if (selectedWidgetId === widgetId) {
@@ -663,6 +732,8 @@
663
732
  const draftConvertScreenshots = new Map<string, string>();
664
733
  /** True while a draft conversion is being finalized (widget creation + shape replace). */
665
734
  const finalizingDraftConversions = new Set<string>();
735
+ /** Keep Widget Details on this convert/draft widget until the user closes the panel. */
736
+ let detailsSessionWidgetId: string | null = null;
666
737
 
667
738
  // Chat component reference and context (focused panel)
668
739
  let chatMethods: {
@@ -681,10 +752,13 @@
681
752
  markWidgetHeadAtLatestCommit?: () => void;
682
753
  updateLatestCodegenStatusMessage?: (content: string) => void;
683
754
  reloadConversationMessages?: () => Promise<void>;
684
- /** Select an existing conversation in the chat UI (keep DB thread and visible chat in sync). */
685
- selectConversation?: (conversationId: string) => void;
755
+ selectConversation?: (conversationId: string) => void | Promise<void>;
756
+ updateLastUserMessageAttachments?: (attachments: Array<{ id?: string; url: string; file_type?: string; file_name?: string | null }>) => void;
686
757
  } | null = null;
687
758
  let chatMethodsByWidgetId: Record<string, NonNullable<typeof chatMethods>> = {};
759
+ /** Conversation `completed` flag per widget — ActionBar Complete / Completed. */
760
+ let conversationCompletedByWidgetId: Record<string, boolean> = {};
761
+ let conversationsByWidgetId: Record<string, any[]> = {};
688
762
  let pendingChatDraftText = '';
689
763
  let chatDraftRestoredForWidgetId: string | null = null;
690
764
  /**
@@ -704,7 +778,7 @@
704
778
 
705
779
  /** Prompt mentions a reference image but chat has no attachment (common UX mistake). */
706
780
  function promptImpliesVisionReference(prompt: string): boolean {
707
- return /\b(attached|attachment|reference\s+image|screenshot|see\s+image|cu\s+imagine|atașat|atasat)\b/i.test(
781
+ return /(attached\s+image|image\s+attached|screenshot|reference\s+image|see\s+(the\s+)?image|cu\s+imagine|imagine[aă]?\s+ata[sș]|ata[sș]at[ăa]?\s+imagine)/i.test(
708
782
  prompt,
709
783
  );
710
784
  }
@@ -713,6 +787,9 @@
713
787
  file?: File,
714
788
  uploadedUrl?: string,
715
789
  ): Promise<string | null> {
790
+ if (file && !file.type.startsWith('image/')) {
791
+ return null;
792
+ }
716
793
  if (uploadedUrl && /^https?:\/\//i.test(uploadedUrl)) {
717
794
  try {
718
795
  const response = await fetch(uploadedUrl);
@@ -740,7 +817,7 @@
740
817
 
741
818
  async function handleChatUploadFile(
742
819
  file: File,
743
- options?: { onProgress?: (percentage: number) => void; source?: string },
820
+ options?: { onProgress?: (percentage: number) => void; source?: string; widgetId?: string },
744
821
  ) {
745
822
  if (!authenticatedClientsToken) {
746
823
  throw new Error('Upload client not initialized');
@@ -750,6 +827,7 @@
750
827
  const uploadBasePath =
751
828
  apiBasePath ||
752
829
  `${(apiRoot || import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/+$/, '')}/v1`;
830
+ const persistWidgetId = options?.widgetId || selectedWidgetId || undefined;
753
831
  const result = await uploadChatFileToWidgeticApi(
754
832
  file,
755
833
  {
@@ -757,7 +835,7 @@
757
835
  accessToken: authenticatedClientsToken,
758
836
  contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
759
837
  source: options?.source || 'chat-upload',
760
- widgetId: selectedWidgetId || undefined,
838
+ widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
761
839
  conversationId: currentChatConversationId || undefined,
762
840
  },
763
841
  { onProgress: options?.onProgress },
@@ -816,6 +894,7 @@
816
894
 
817
895
  const panelChat = getChatMethodsForWidget(codegenWidgetId);
818
896
  const cdnUploadIds = (attachments || [])
897
+ .filter((attachment) => attachment.uploadId && attachment.fileType.startsWith('image/'))
819
898
  .map((attachment) => attachment.uploadId)
820
899
  .filter((id): id is string => Boolean(id));
821
900
 
@@ -866,24 +945,58 @@
866
945
  ? promptImages
867
946
  : getPanelState(codegenWidgetId).promptImages;
868
947
  const hasImages = effectivePromptImages.length > 0 || cdnUploadIds.length > 0;
869
- if (!hasImages && promptImpliesVisionReference(content)) {
948
+ const hasNonImageFiles = (attachments || []).some(
949
+ (attachment) => attachment.fileType && !attachment.fileType.startsWith('image/'),
950
+ );
951
+ if (!hasImages && !hasNonImageFiles && promptImpliesVisionReference(content)) {
952
+ const missingImageError =
953
+ 'Promptul menționează o imagine atașată, dar în chat nu e niciun attachment. Folosește butonul Upload.';
870
954
  showToast('warning', 'Lipsește imaginea de referință', {
871
- description:
872
- 'Promptul menționează o imagine atașată, dar în chat nu e niciun attachment. Folosește butonul Upload sau ?attachStairsRef=1.',
955
+ description: missingImageError,
873
956
  duration: 10000,
874
957
  });
958
+ patchCodegenUiState(codegenWidgetId, {
959
+ lastGenerationFailed: true,
960
+ lastGenerationError: missingImageError,
961
+ generateCodeStatus: '❌ Missing reference image',
962
+ });
963
+ pushCodegenFailureToChat(codegenWidgetId, missingImageError);
875
964
  return;
876
965
  }
877
- // Pass upload IDs / images explicitly — clearDraft already wiped Chat's local attachments.
966
+ const persistFileAttachments = (attachments || [])
967
+ .filter((attachment) =>
968
+ !!attachment.file
969
+ || (attachment.url && (attachment.url.startsWith('blob:') || attachment.url.startsWith('data:') || /^https?:\/\//i.test(attachment.url))),
970
+ )
971
+ .map((attachment, i) => ({
972
+ url: attachment.url,
973
+ file: attachment.file,
974
+ file_type: guessAttachmentFileType(attachment.url || '', attachment.fileName, attachment.fileType),
975
+ file_name: attachment.fileName,
976
+ file_size: attachment.fileSize,
977
+ display_order: i + 1,
978
+ }));
979
+ // Chat.onSaveMessage already persisted this turn. generateCode keeps one
980
+ // ensureCodegenUserMessagePersisted after imagesToSend is finalized.
878
981
  await generateCode({
879
982
  prompt: content,
880
983
  fromChat: true,
881
984
  widgetId: codegenWidgetId,
882
985
  attachmentIds: cdnUploadIds.length > 0 ? cdnUploadIds : undefined,
883
986
  promptImagesOverride: effectivePromptImages.length > 0 ? effectivePromptImages : undefined,
987
+ persistFileAttachments: persistFileAttachments.length > 0 ? persistFileAttachments : undefined,
884
988
  });
885
989
  }
886
990
 
991
+ function pushCodegenFailureToChat(widgetId: string, errorText: string) {
992
+ const panelChat = getChatMethodsForWidget(widgetId);
993
+ const content = /^Code generation failed/i.test(errorText)
994
+ ? errorText
995
+ : `Code generation failed: ${errorText}`;
996
+ panelChat?.addAssistantMessage?.(content);
997
+ console.log('[Chat] Codegen failure bubble:', content.slice(0, 160));
998
+ }
999
+
887
1000
  /** Retry failed codegen without duplicating the user bubble in chat. */
888
1001
  async function handleChatRetryMessage(content: string, sourceWidgetId?: string) {
889
1002
  const widgetId = sourceWidgetId ?? selectedWidgetId;
@@ -894,7 +1007,53 @@
894
1007
  ':',
895
1008
  content.substring(0, 80),
896
1009
  );
897
- await generateCode({ prompt: content, fromChat: true, widgetId });
1010
+ const widget = getWidgetById(widgetId) || (selectedWidget?.id === widgetId ? selectedWidget : null);
1011
+ if (widget) {
1012
+ await ensureWidgetRepository(widget, { forceRecreate: true, silent: true });
1013
+ }
1014
+ const panelChat = getChatMethodsForWidget(widgetId);
1015
+ const draftUploadIds = panelChat?.getAttachmentUploadIds?.() ?? [];
1016
+ const draftImageUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
1017
+
1018
+ // Re-use attachments from previous user messages in the conversation
1019
+ // so that Retry doesn't lose PDFs/files that were uploaded with the original prompt.
1020
+ let retryFileAttachments: Array<{ url: string; file_type: string; file_name: string | null; file_size: number | null }> = [];
1021
+ if (draftUploadIds.length === 0 && draftImageUrls.length === 0) {
1022
+ try {
1023
+ const panelState = getPanelState(widgetId);
1024
+ const convId = panelChat?.getConversationId?.() ?? panelState.currentChatConversationId ?? null;
1025
+ if (convId) {
1026
+ const convMessages = await handleLoadMessages(convId, widgetId);
1027
+ // Find most-recent user message that has persisted attachments
1028
+ const userMsgsWithAtt = (convMessages || [])
1029
+ .filter((m: any) => (m.messageType || m.message_type) === 'user' && (m.attachments || []).length > 0);
1030
+ if (userMsgsWithAtt.length > 0) {
1031
+ const latestWithAtt = userMsgsWithAtt[userMsgsWithAtt.length - 1];
1032
+ retryFileAttachments = (latestWithAtt.attachments || [])
1033
+ .filter((a: any) => a.url && /^https?:\/\//i.test(a.url))
1034
+ .map((a: any) => ({
1035
+ url: a.url,
1036
+ file_type: a.file_type || 'application/octet-stream',
1037
+ file_name: a.file_name || null,
1038
+ file_size: a.file_size || null,
1039
+ }));
1040
+ console.log('[Chat→Retry] Re-using', retryFileAttachments.length, 'attachment(s) from previous user message for retry');
1041
+ }
1042
+ }
1043
+ } catch (retryAttErr) {
1044
+ console.warn('[Chat→Retry] Could not load previous attachments for retry:', retryAttErr);
1045
+ }
1046
+ }
1047
+
1048
+ await generateCode({
1049
+ prompt: content,
1050
+ fromChat: true,
1051
+ widgetId,
1052
+ forceRecreateRepo: true,
1053
+ attachmentIds: draftUploadIds.length > 0 ? draftUploadIds : undefined,
1054
+ promptImagesOverride: draftImageUrls.length > 0 ? draftImageUrls : undefined,
1055
+ persistFileAttachments: retryFileAttachments.length > 0 ? retryFileAttachments : undefined,
1056
+ });
898
1057
  }
899
1058
 
900
1059
  function handleChatConversationChange(conversationId: string | null, sourceWidgetId?: string) {
@@ -905,6 +1064,86 @@
905
1064
  currentChatConversationId = conversationId;
906
1065
  }
907
1066
  console.log('[Chat] Active conversation changed for', widgetId.substring(0, 8), ':', conversationId || 'none');
1067
+ const conv = (conversationsByWidgetId[widgetId] || []).find((c) => c.id === conversationId);
1068
+ if (conv) {
1069
+ const completed = !!(conv.completed ?? conv.is_completed ?? conv.isCompleted);
1070
+ conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: completed };
1071
+ }
1072
+ }
1073
+
1074
+ function isConversationRecordCompleted(conversation: any): boolean {
1075
+ return !!(conversation?.completed ?? conversation?.is_completed ?? conversation?.isCompleted);
1076
+ }
1077
+
1078
+ function isUuid(value: string | null | undefined): value is string {
1079
+ return !!value && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
1080
+ }
1081
+
1082
+ function resolvePersistableConversationId(widgetId: string, preferredId?: string | null): string | null {
1083
+ const knownIds = (conversationsByWidgetId[widgetId] || [])
1084
+ .map((c) => c.id)
1085
+ .filter((id) => isUuid(id));
1086
+ const fromChat = getChatMethodsForWidget(widgetId)?.getConversationId?.();
1087
+ const fromFocus = widgetId === focusedPanelWidgetId ? currentChatConversationId : null;
1088
+ const preferredKnown = [preferredId, fromChat, fromFocus].find((id) => id && knownIds.includes(id));
1089
+ if (preferredKnown) return preferredKnown;
1090
+ if (knownIds[0]) return knownIds[0];
1091
+ if (isUuid(preferredId)) return preferredId;
1092
+ if (isUuid(fromChat)) return fromChat;
1093
+ return isUuid(fromFocus) ? fromFocus : null;
1094
+ }
1095
+
1096
+ async function handleChatCompletionToggle(
1097
+ widgetId: string,
1098
+ detail?: { conversationId?: string | null; completed?: boolean },
1099
+ ) {
1100
+ const next = typeof detail?.completed === 'boolean'
1101
+ ? detail.completed
1102
+ : !conversationCompletedByWidgetId[widgetId];
1103
+ conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: next };
1104
+ let conversationId = resolvePersistableConversationId(widgetId, detail?.conversationId);
1105
+ console.log('[Chat] Complete toggle', widgetId.substring(0, 8), next ? 'completed' : 'active', conversationId);
1106
+ if (!conversationsClient) {
1107
+ console.warn('[Chat] Complete toggle skipped persist — missing conversations client');
1108
+ return;
1109
+ }
1110
+ try {
1111
+ if (!conversationId) {
1112
+ const created = await handleCreateConversation(widgetId, 'Widget conversation');
1113
+ conversationId = created?.id || null;
1114
+ console.log('[Chat] Complete created conversation for persist', conversationId);
1115
+ }
1116
+ if (!conversationId || !isUuid(conversationId)) {
1117
+ throw new Error('No persistable conversation id');
1118
+ }
1119
+ try {
1120
+ await conversationsClient.updateConversation({
1121
+ conversationId,
1122
+ updateConversationRequest: { completed: next },
1123
+ });
1124
+ } catch (updateError) {
1125
+ const listed = await handleLoadConversations(widgetId);
1126
+ const fallbackId = (listed || []).map((c) => c.id).find((id) => isUuid(id) && id !== conversationId);
1127
+ if (!fallbackId) throw updateError;
1128
+ console.warn('[Chat] Complete retry with loaded conversation', fallbackId);
1129
+ conversationId = fallbackId;
1130
+ await conversationsClient.updateConversation({
1131
+ conversationId,
1132
+ updateConversationRequest: { completed: next },
1133
+ });
1134
+ }
1135
+ conversationsByWidgetId = {
1136
+ ...conversationsByWidgetId,
1137
+ [widgetId]: (conversationsByWidgetId[widgetId] || []).map((c) =>
1138
+ c.id === conversationId ? { ...c, completed: next } : c,
1139
+ ),
1140
+ };
1141
+ console.log('[Chat] Conversation marked', next ? 'completed' : 'active', conversationId);
1142
+ } catch (error) {
1143
+ console.error('[Chat] Failed to persist conversation completed:', error);
1144
+ showToast('error', 'Could not update conversation status');
1145
+ conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: !next };
1146
+ }
908
1147
  }
909
1148
 
910
1149
  /** Sync unsent chat draft + attachments to the durable per-widget draft (or clear when empty). */
@@ -970,7 +1209,43 @@
970
1209
  * picked, which may differ from convos[0] when several empty conversations
971
1210
  * exist (feedback c: chat looked empty while messages went elsewhere).
972
1211
  */
973
- async function ensureCodegenUserMessagePersisted(widgetId: string, prompt: string): Promise<void> {
1212
+ function guessAttachmentFileType(url: string, fileName?: string | null, fallback?: string | null): string {
1213
+ if (fallback && String(fallback).includes('/')) return String(fallback);
1214
+ const hint = `${url || ''} ${fileName || ''}`;
1215
+ if (/\.pdf(\?|$)/i.test(hint)) return 'application/pdf';
1216
+ if (/\.(mp4|webm|mov)(\?|$)/i.test(hint)) return 'video/mp4';
1217
+ if (/\.(png|jpe?g|gif|webp|svg)(\?|$)/i.test(hint)) return 'image/png';
1218
+ if (fallback) return String(fallback);
1219
+ return 'application/octet-stream';
1220
+ }
1221
+
1222
+ function uniquePersistImageUrls(urls: string[]): string[] {
1223
+ const seen = new Set<string>();
1224
+ const httpsUrls: string[] = [];
1225
+ const dataUrls: string[] = [];
1226
+ for (const url of urls) {
1227
+ if (typeof url !== 'string' || url.length < 20 || seen.has(url)) continue;
1228
+ seen.add(url);
1229
+ if (/^https?:\/\//i.test(url)) httpsUrls.push(url);
1230
+ else if (url.startsWith('data:image/')) dataUrls.push(url);
1231
+ }
1232
+ // Same screenshot often exists as CDN URL + compressed data URL — keep HTTPS only.
1233
+ return httpsUrls.length > 0 ? httpsUrls : dataUrls;
1234
+ }
1235
+
1236
+ async function ensureCodegenUserMessagePersisted(
1237
+ widgetId: string,
1238
+ prompt: string,
1239
+ imageUrls?: string[],
1240
+ fileAttachments?: Array<{
1241
+ url?: string;
1242
+ file?: File;
1243
+ file_type?: string;
1244
+ file_name?: string | null;
1245
+ file_size?: number | null;
1246
+ display_order?: number;
1247
+ }>,
1248
+ ): Promise<void> {
974
1249
  if (!conversationsClient || !messagesClient) return;
975
1250
  try {
976
1251
  const panelChat = getChatMethodsForWidget(widgetId);
@@ -990,20 +1265,123 @@
990
1265
  convId = conv?.id;
991
1266
  }
992
1267
  if (!convId) return;
993
- if (convId !== visibleConversationId) {
994
- // Point the panel chat at the conversation that will hold the messages
995
- // so a later reopen shows the same thread the codegen used.
996
- panelChat?.selectConversation?.(convId);
997
- patchPanelSnapshot(widgetId, { currentChatConversationId: convId });
998
- }
999
1268
 
1000
- const messages = await handleLoadMessages(convId);
1001
- const hasUserMessage = messages?.some(
1269
+ const persistImages = uniquePersistImageUrls([
1270
+ ...(imageUrls || []),
1271
+ ...(getPanelState(widgetId).promptImages ?? []),
1272
+ ]);
1273
+ const compressedPersistImages: string[] = [];
1274
+ for (const url of persistImages) {
1275
+ if (typeof url === 'string' && url.startsWith('data:image/')) {
1276
+ compressedPersistImages.push(await compressImageDataUrlForCodegen(url));
1277
+ } else if (typeof url === 'string' && /^https?:\/\//i.test(url)) {
1278
+ compressedPersistImages.push(url);
1279
+ }
1280
+ }
1281
+ const persistFileItems = (fileAttachments || [])
1282
+ .filter((att) =>
1283
+ (typeof att.url === 'string' && (att.url.startsWith('blob:') || att.url.startsWith('data:') || /^https?:\/\//i.test(att.url)))
1284
+ || !!(att as { file?: File }).file,
1285
+ )
1286
+ .map((att, i) => ({
1287
+ url: att.url || '',
1288
+ file: (att as { file?: File }).file,
1289
+ file_type: guessAttachmentFileType(att.url || '', att.file_name, att.file_type),
1290
+ file_name: att.file_name || (att as { file?: File }).file?.name || `attachment-${i + 1}`,
1291
+ file_size: att.file_size && att.file_size > 0 ? att.file_size : (att as { file?: File }).file?.size,
1292
+ display_order: att.display_order ?? i + 1,
1293
+ }));
1294
+ if (compressedPersistImages.length === 0 && persistFileItems.length === 0) {
1295
+ console.warn('[Codegen] Persist user message has no attachment URLs for widget', widgetId?.substring(0, 8));
1296
+ }
1297
+ const fileHasImage = persistFileItems.some((att) => {
1298
+ const type = String(att.file_type || '');
1299
+ const url = String(att.url || '');
1300
+ return type.startsWith('image/') || url.startsWith('data:image/') || /\.(png|jpe?g|gif|webp|svg)(\?|$)/i.test(url);
1301
+ });
1302
+ const imagePersistItems = fileHasImage
1303
+ ? []
1304
+ : compressedPersistImages.map((url, i) => ({
1305
+ url,
1306
+ file_type: guessAttachmentFileType(url, 'design-screenshot.png', 'image/png'),
1307
+ file_name: 'design-screenshot.png',
1308
+ display_order: i + 1,
1309
+ }));
1310
+ const persistAttachments = [...persistFileItems, ...imagePersistItems].map((att, i) => ({
1311
+ ...att,
1312
+ display_order: i + 1,
1313
+ }));
1314
+ const convMessages = await handleLoadMessages(convId, widgetId);
1315
+ const userMessages = (convMessages || []).filter(
1002
1316
  (m) => (m.messageType || (m as any).message_type) === 'user'
1003
1317
  );
1004
- if (!hasUserMessage) {
1005
- await handleSaveMessage(convId, buildUserPromptMessageContent(prompt), 'user');
1006
- console.log('[Codegen] Persisted initial user prompt on generate:', convId);
1318
+ const promptNeedle = (prompt || '').trim();
1319
+ const targetUser =
1320
+ (promptNeedle
1321
+ ? [...userMessages].reverse().find((m) => {
1322
+ const text = String(m.messageContent || (m as any).message_content || '').trim();
1323
+ return (
1324
+ text === promptNeedle ||
1325
+ text.endsWith(promptNeedle) ||
1326
+ (promptNeedle.length >= 12 && text.includes(promptNeedle.slice(0, 80)))
1327
+ );
1328
+ })
1329
+ : null) || userMessages[userMessages.length - 1] || null;
1330
+ const existingUrls = new Set(
1331
+ (targetUser?.attachments || [])
1332
+ .map((a: { url?: string }) => a.url)
1333
+ .filter((url: string | undefined): url is string => !!url),
1334
+ );
1335
+ const existingHasRemoteImage = [...existingUrls].some((url) => /^https?:\/\//i.test(url));
1336
+ const missingPersist = persistAttachments.filter((att) => {
1337
+ if (att.url && existingUrls.has(att.url)) return false;
1338
+ if (existingHasRemoteImage && typeof att.url === 'string' && att.url.startsWith('data:')) return false;
1339
+ if (existingHasRemoteImage && String(att.file_type || '').startsWith('image/')) return false;
1340
+ return true;
1341
+ });
1342
+ let savedAttachmentCount = targetUser?.attachments?.length || 0;
1343
+ if (!targetUser) {
1344
+ const saved = await handleSaveMessage(
1345
+ convId,
1346
+ buildUserPromptMessageContent(prompt),
1347
+ 'user',
1348
+ persistAttachments.length > 0 ? persistAttachments : undefined,
1349
+ widgetId,
1350
+ );
1351
+ const savedAtt = saved?.attachments || saved?.data?.attachments || [];
1352
+ savedAttachmentCount = Array.isArray(savedAtt) ? savedAtt.length : 0;
1353
+ console.log('[Codegen] Persisted initial user prompt on generate:', convId, 'attachments:', persistAttachments.length, 'saved:', savedAttachmentCount);
1354
+ if (Array.isArray(savedAtt) && savedAtt.length > 0) {
1355
+ panelChat?.updateLastUserMessageAttachments?.(savedAtt);
1356
+ }
1357
+ } else if (missingPersist.length > 0) {
1358
+ const merged = [...(targetUser.attachments || []), ...missingPersist];
1359
+ const apiAtt = await mapChatAttachmentsForApi(merged, widgetId);
1360
+ if (apiAtt.length > 0) {
1361
+ try {
1362
+ await messagesClient.updateMessageRaw({
1363
+ conversationId: convId,
1364
+ messageId: targetUser.id,
1365
+ updateMessageRequest: { attachments: apiAtt },
1366
+ });
1367
+ savedAttachmentCount = apiAtt.length;
1368
+ console.log('[Codegen] Saved missing attachments on user message:', targetUser.id, apiAtt.length);
1369
+ } catch (updateErr) {
1370
+ console.warn('[Codegen] Failed to PATCH user message attachments:', updateErr);
1371
+ }
1372
+ panelChat?.updateLastUserMessageAttachments?.(apiAtt);
1373
+ } else {
1374
+ console.warn('[Codegen] mapChatAttachmentsForApi returned 0 items — attachments not stored');
1375
+ }
1376
+ }
1377
+ if (persistAttachments.length > 0 && savedAttachmentCount === 0) {
1378
+ console.error('[Codegen] User prompt saved without attachments — chat reopen will show placeholder');
1379
+ }
1380
+ if (convId !== visibleConversationId) {
1381
+ await panelChat?.selectConversation?.(convId);
1382
+ patchPanelSnapshot(widgetId, { currentChatConversationId: convId });
1383
+ } else {
1384
+ console.log('[Codegen] Keep local chat attachments after persist; skip reload wipe');
1007
1385
  }
1008
1386
  } catch (err) {
1009
1387
  console.warn('[Codegen] Could not persist user prompt before generation:', err);
@@ -1028,18 +1406,27 @@
1028
1406
  console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
1029
1407
  }
1030
1408
 
1031
- /** Screenshot: capture widget preview iframe and return data URL */
1032
- async function handleChatScreenshot(): Promise<string | null> {
1033
- if (!widgetDetails?.hasValidPreview()) {
1034
- console.log('[Chat→Screenshot] Preview not ready');
1409
+ /** Screenshot: capture this panel's widget preview iframe and return data URL */
1410
+ async function handleChatScreenshot(widgetId?: string | null): Promise<string | null> {
1411
+ const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
1412
+ const panelRef = getWidgetDetailsRef(targetId);
1413
+ const iframe =
1414
+ (panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null) ||
1415
+ (document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement | null);
1416
+
1417
+ if (!iframe?.contentWindow) {
1418
+ console.log('[Chat→Screenshot] Preview iframe not ready', {
1419
+ targetId: targetId?.substring(0, 8),
1420
+ hasValidPreview: panelRef?.hasValidPreview?.() ?? false,
1421
+ });
1035
1422
  return null;
1036
1423
  }
1037
- const iframe = document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement;
1038
- if (!iframe?.contentWindow) return null;
1039
1424
 
1425
+ console.log('[Chat→Screenshot] Requesting screenshot from preview iframe', targetId?.substring(0, 8));
1040
1426
  return new Promise<string | null>((resolve) => {
1041
1427
  const timeout = setTimeout(() => {
1042
1428
  window.removeEventListener('message', handler);
1429
+ console.warn('[Chat→Screenshot] Timed out waiting for widgetic:screenshot');
1043
1430
  resolve(null);
1044
1431
  }, 8000);
1045
1432
 
@@ -1087,6 +1474,13 @@
1087
1474
  });
1088
1475
  const json = await response.raw.json();
1089
1476
  const conversations = normalizeConversationRecords(json);
1477
+ conversationsByWidgetId = { ...conversationsByWidgetId, [contextId]: conversations };
1478
+ if (conversations[0]) {
1479
+ conversationCompletedByWidgetId = {
1480
+ ...conversationCompletedByWidgetId,
1481
+ [contextId]: isConversationRecordCompleted(conversations[0]),
1482
+ };
1483
+ }
1090
1484
  console.log('[Chat→Backend] Loaded conversations for widget:', contextId, conversations.length);
1091
1485
  return conversations;
1092
1486
  } catch (error) {
@@ -1139,26 +1533,40 @@
1139
1533
  const json = await response.raw.json();
1140
1534
  const messages = json?.data || [];
1141
1535
  console.log('[Chat→Backend] Loaded messages for conversation:', conversationId, messages.length);
1142
- const mappedMessages = messages.map((m: any) => ({
1536
+ const mappedMessages = messages.map((m: any) => {
1537
+ const rawAttachments = m.attachments || m.message_contents || m.messageContents || [];
1538
+ const attachments = (Array.isArray(rawAttachments) ? rawAttachments : []).map((a: any, idx: number) => {
1539
+ const nested = a?.contents || a?.content;
1540
+ const content = Array.isArray(nested) ? (nested[0] || {}) : (nested || a || {});
1541
+ const url = content.url || content.public_url || content.publicUrl || content.file_url || a.url || '';
1542
+ let fileType = content.file_type || content.fileType || a.file_type || a.fileType || '';
1543
+ const fileName = content.file_name || content.fileName || a.file_name || a.fileName || null;
1544
+ if (!fileType || !String(fileType).includes('/')) {
1545
+ fileType = guessAttachmentFileType(url, fileName, fileType);
1546
+ }
1547
+ return {
1548
+ id: content.id || a.id || `att_${idx}`,
1549
+ url,
1550
+ file_type: fileType,
1551
+ file_name: fileName,
1552
+ file_size: content.file_size ?? content.fileSize ?? a.file_size ?? a.fileSize ?? null,
1553
+ display_order: a.display_order ?? a.displayOrder ?? content.display_order ?? idx + 1,
1554
+ created_at: content.created_at || content.createdAt || a.created_at || a.createdAt || new Date().toISOString(),
1555
+ };
1556
+ }).filter((a: { url: string }) => !!a.url);
1557
+ return {
1143
1558
  id: m.id,
1144
1559
  conversationId: m.conversation_id || m.conversationId || conversationId,
1145
1560
  messageContent: m.message_content || m.messageContent || '',
1146
1561
  messageType: m.message_type || m.messageType || 'user',
1147
- attachments: (m.attachments || []).map((a: any, idx: number) => ({
1148
- id: a.id || `att_${idx}`,
1149
- url: a.url || '',
1150
- file_type: a.file_type || a.fileType || 'image/png',
1151
- file_name: a.file_name || a.fileName || null,
1152
- file_size: a.file_size ?? a.fileSize ?? null,
1153
- display_order: a.display_order ?? a.displayOrder ?? idx + 1,
1154
- created_at: a.created_at || a.createdAt || new Date().toISOString(),
1155
- })),
1562
+ attachments,
1156
1563
  isCommit: m.is_commit || m.isCommit || false,
1157
1564
  userId: m.user_id || m.userId || '',
1158
1565
  createdAt: new Date(m.created_at || m.createdAt),
1159
1566
  updatedAt: new Date(m.updated_at || m.updatedAt || m.created_at || m.createdAt),
1160
1567
  rolledBackAt: m.rolled_back_at || m.rolledBackAt || null,
1161
- }));
1568
+ };
1569
+ });
1162
1570
  const withHead = markWidgetHeadOnLoadedMessages(mappedMessages);
1163
1571
  const syncWidgetId = widgetIdForSync ?? selectedWidgetId;
1164
1572
  if (syncWidgetId) {
@@ -1188,6 +1596,15 @@
1188
1596
  const conversation = json?.data || json;
1189
1597
  const conversationId = conversation?.id || conversation?.conversation_id || conversation?.conversationId;
1190
1598
  console.log('[Chat→Backend] Created conversation:', conversationId);
1599
+ if (conversationId && contextId) {
1600
+ conversationsByWidgetId = {
1601
+ ...conversationsByWidgetId,
1602
+ [contextId]: [
1603
+ { ...conversation, id: conversationId },
1604
+ ...(conversationsByWidgetId[contextId] || []).filter((c) => c.id !== conversationId),
1605
+ ],
1606
+ };
1607
+ }
1191
1608
  return conversationId ? { ...conversation, id: conversationId } : conversation;
1192
1609
  } catch (error) {
1193
1610
  console.error('[Chat→Backend] Failed to create conversation:', error);
@@ -1195,48 +1612,96 @@
1195
1612
  }
1196
1613
  }
1197
1614
 
1198
- /** Map chat attachment objects to API content items (compressed data URLs for images). */
1615
+ function dataUrlToUploadFile(dataUrl: string, fileName: string): File | null {
1616
+ try {
1617
+ const comma = dataUrl.indexOf(',');
1618
+ if (comma < 0) return null;
1619
+ const header = dataUrl.slice(0, comma);
1620
+ const mimeMatch = header.match(/data:([^;,]+)/);
1621
+ const mimeType = mimeMatch?.[1] || 'application/octet-stream';
1622
+ const binary = atob(dataUrl.slice(comma + 1));
1623
+ const bytes = new Uint8Array(binary.length);
1624
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1625
+ return new File([bytes], fileName, { type: mimeType });
1626
+ } catch (err) {
1627
+ console.warn('[Chat→Backend] Could not convert data URL to File:', err);
1628
+ return null;
1629
+ }
1630
+ }
1631
+
1632
+ /** Map chat attachments to API items. Inline data/blob files are uploaded to user storage (CDN URL). */
1199
1633
  async function mapChatAttachmentsForApi(
1200
1634
  attachments?: Array<{
1201
1635
  url?: string;
1636
+ file?: File;
1202
1637
  file_type?: string;
1203
1638
  file_name?: string | null;
1204
1639
  file_size?: number | null;
1205
1640
  display_order?: number;
1206
1641
  }>,
1642
+ persistWidgetId?: string | null,
1207
1643
  ): Promise<Array<{ url: string; file_type: string; file_name?: string; file_size?: number; display_order?: number }>> {
1208
1644
  if (!attachments?.length) return [];
1209
1645
  const items: Array<{ url: string; file_type: string; file_name?: string; file_size?: number; display_order?: number }> = [];
1210
1646
  for (let i = 0; i < attachments.length; i++) {
1211
1647
  const att = attachments[i];
1212
1648
  let url = att.url || '';
1213
- if (!url) continue;
1214
- if (url.startsWith('blob:')) {
1649
+ const fileName = att.file_name || att.file?.name || `attachment-${i + 1}`;
1650
+ if (att.file instanceof File && (!url || url.startsWith('blob:') || url.startsWith('data:'))) {
1215
1651
  try {
1216
- const blob = await fetch(url).then((r) => r.blob());
1217
- url = await new Promise<string>((resolve, reject) => {
1218
- const reader = new FileReader();
1219
- reader.onload = () => resolve(String(reader.result));
1220
- reader.onerror = () => reject(reader.error);
1221
- reader.readAsDataURL(blob);
1652
+ const uploaded = await handleChatUploadFile(att.file, {
1653
+ source: 'chat-message-persist',
1654
+ widgetId: persistWidgetId || undefined,
1655
+ });
1656
+ url = uploaded.url;
1657
+ console.log('[Chat→Backend] Uploaded chat File to user storage:', uploaded.id, url.slice(0, 80));
1658
+ items.push({
1659
+ url,
1660
+ file_type: guessAttachmentFileType(url, uploaded.fileName || fileName, uploaded.fileType || att.file_type || att.file.type),
1661
+ file_name: uploaded.fileName || fileName,
1662
+ file_size: uploaded.fileSize,
1663
+ display_order: att.display_order ?? i + 1,
1222
1664
  });
1665
+ continue;
1223
1666
  } catch (err) {
1224
- console.warn('[Chat→Backend] Could not convert blob attachment to data URL:', err);
1667
+ console.warn('[Chat→Backend] Chat File upload failed, skipping attachment:', err);
1225
1668
  continue;
1226
1669
  }
1227
1670
  }
1228
- if (url.startsWith('data:image/')) {
1229
- url = await compressImageDataUrlForCodegen(url, 960, 80_000);
1230
- // Oversized inline payloads often 500 on remote create_message RPC — skip attachment.
1231
- if (url.length > 100_000) {
1232
- console.warn('[Chat→Backend] Skipping oversized data-URL attachment for message save');
1671
+ if (!url) continue;
1672
+ if (url.startsWith('blob:') || url.startsWith('data:')) {
1673
+ try {
1674
+ let file: File | null = null;
1675
+ if (url.startsWith('blob:')) {
1676
+ const blob = await fetch(url).then((r) => r.blob());
1677
+ file = new File([blob], fileName, { type: blob.type || att.file_type || 'application/octet-stream' });
1678
+ } else {
1679
+ file = dataUrlToUploadFile(url, fileName);
1680
+ }
1681
+ if (!file) continue;
1682
+ const uploaded = await handleChatUploadFile(file, {
1683
+ source: 'chat-message-persist',
1684
+ widgetId: persistWidgetId || undefined,
1685
+ });
1686
+ url = uploaded.url;
1687
+ console.log('[Chat→Backend] Uploaded chat file to user storage:', uploaded.id, url.slice(0, 80));
1688
+ items.push({
1689
+ url,
1690
+ file_type: guessAttachmentFileType(url, uploaded.fileName || fileName, uploaded.fileType || att.file_type),
1691
+ file_name: uploaded.fileName || fileName,
1692
+ file_size: uploaded.fileSize,
1693
+ display_order: att.display_order ?? i + 1,
1694
+ });
1695
+ continue;
1696
+ } catch (err) {
1697
+ console.warn('[Chat→Backend] Chat file upload failed, skipping attachment:', err);
1233
1698
  continue;
1234
1699
  }
1235
1700
  }
1236
1701
  items.push({
1237
1702
  url,
1238
- file_type: att.file_type || 'image/png',
1239
- file_name: att.file_name || `attachment-${i + 1}.png`,
1703
+ file_type: guessAttachmentFileType(url, fileName, att.file_type),
1704
+ file_name: fileName,
1240
1705
  file_size: att.file_size ?? undefined,
1241
1706
  display_order: att.display_order ?? i + 1,
1242
1707
  });
@@ -1251,11 +1716,13 @@
1251
1716
  messageType: string,
1252
1717
  attachments?: Array<{
1253
1718
  url?: string;
1719
+ file?: File;
1254
1720
  file_type?: string;
1255
1721
  file_name?: string | null;
1256
1722
  file_size?: number | null;
1257
1723
  display_order?: number;
1258
1724
  }>,
1725
+ persistWidgetId?: string | null,
1259
1726
  ): Promise<any> {
1260
1727
  try {
1261
1728
  if (!messagesClient) {
@@ -1268,7 +1735,17 @@
1268
1735
  return null;
1269
1736
  }
1270
1737
  const apiAttachments =
1271
- messageType === 'user' ? await mapChatAttachmentsForApi(attachments) : [];
1738
+ messageType === 'user' ? await mapChatAttachmentsForApi(
1739
+ (attachments || []).map((att, i) => ({
1740
+ url: att.url,
1741
+ file: att.file,
1742
+ file_type: guessAttachmentFileType(att.url || '', att.file_name, att.file_type),
1743
+ file_name: att.file_name || att.file?.name || `attachment-${i + 1}`,
1744
+ file_size: att.file_size && att.file_size > 0 ? att.file_size : att.file?.size,
1745
+ display_order: att.display_order ?? i + 1,
1746
+ })),
1747
+ persistWidgetId,
1748
+ ) : [];
1272
1749
  const createPayload = {
1273
1750
  message_content: content,
1274
1751
  message_type: messageType as 'user' | 'assistant',
@@ -1283,25 +1760,18 @@
1283
1760
  });
1284
1761
  const json = await response.raw.json();
1285
1762
  console.log('[Chat→Backend] Saved message to conversation:', conversationId);
1286
- return json?.data || json;
1763
+ const saved = json?.data || json;
1764
+ const savedAtt = saved?.attachments;
1765
+ return {
1766
+ ...saved,
1767
+ attachments: (Array.isArray(savedAtt) && savedAtt.length > 0) ? savedAtt : apiAttachments,
1768
+ };
1287
1769
  } catch (withAttachmentsError) {
1288
- // Remote RPC sometimes 500s on large/strict attachment payloads — retry text-only
1289
- // so codegen can continue (vision uses attachmentIds separately).
1290
- if (apiAttachments.length === 0) throw withAttachmentsError;
1291
- console.warn(
1292
- '[Chat→Backend] Save with attachments failed, retrying text-only:',
1770
+ console.error(
1771
+ '[Chat→Backend] Save with attachments failed — not falling back to text-only:',
1293
1772
  withAttachmentsError,
1294
1773
  );
1295
- const response = await messagesClient.createMessageRaw({
1296
- conversationId,
1297
- createMessageRequest: {
1298
- ...createPayload,
1299
- attachments: undefined,
1300
- },
1301
- });
1302
- const json = await response.raw.json();
1303
- console.log('[Chat→Backend] Saved message (text-only fallback):', conversationId);
1304
- return json?.data || json;
1774
+ throw withAttachmentsError;
1305
1775
  }
1306
1776
  } catch (error) {
1307
1777
  console.error('[Chat→Backend] Failed to save message:', error);
@@ -1417,6 +1887,11 @@
1417
1887
  let publishingWidgetId: string | null = null;
1418
1888
  let publishWidgetStatus: string | null = null;
1419
1889
 
1890
+ $: if (isPublishing && !publishingWidgetId) {
1891
+ console.warn('[Publish] Clearing stuck isPublishing without publishingWidgetId');
1892
+ isPublishing = false;
1893
+ }
1894
+
1420
1895
  // Cross-tab operation guards — detect when another tab is generating/publishing on the same widget
1421
1896
  let isGeneratingInOtherTab = false;
1422
1897
  let isPublishingInOtherTab = false;
@@ -1496,6 +1971,9 @@
1496
1971
  );
1497
1972
  const sessionAhead = sessionOrdered.find((candidate) => !commitsMatch(candidate, liveSha));
1498
1973
  if (sessionAhead) return sessionAhead;
1974
+ // localStorage HEAD written at codegen complete — must win over live SHA
1975
+ // even before snap/live.lastCommitId propagate (reopen currently "fixed" this).
1976
+ if (stored && !commitsMatch(stored, liveSha)) return stored;
1499
1977
  const sessionMatch = sessionOrdered.find((candidate) => commitsMatch(candidate, liveSha));
1500
1978
  if (sessionMatch) return sessionMatch;
1501
1979
  if (stored && commitsMatch(stored, liveSha)) return stored;
@@ -1554,18 +2032,21 @@
1554
2032
  let editingCompositionName = '';
1555
2033
  let deletingCompositionId: string | null = null;
1556
2034
 
1557
- /** Minimum chat height; flex-1 grows to fill leftover space above Publish. */
1558
- const DEFAULT_CHAT_CONTAINER_HEIGHT = 280;
2035
+ /** Explicit chat box height (px). Drag grows this and pushes Publish down. */
2036
+ const CHAT_DEFAULT_HEIGHT = 550;
1559
2037
  const CHAT_MIN_HEIGHT = 260;
1560
- const CHAT_MAX_HEIGHT = 1400;
2038
+ const CHAT_MAX_HEIGHT = 2000;
1561
2039
 
1562
2040
  function getChatContainerHeightForPanel(widgetId: string): number {
1563
- return getPanelState(widgetId).chatContainerHeight ?? DEFAULT_CHAT_CONTAINER_HEIGHT;
2041
+ const stored = getPanelState(widgetId).chatContainerHeight;
2042
+ if (stored == null || stored <= 0) return CHAT_DEFAULT_HEIGHT;
2043
+ return Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, stored));
1564
2044
  }
1565
2045
 
1566
2046
  function handleChatMessagesResize(widgetId: string, deltaPx: number): void {
1567
2047
  const current = getChatContainerHeightForPanel(widgetId);
1568
2048
  const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
2049
+ console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
1569
2050
  patchPanelSnapshot(widgetId, { chatContainerHeight: next });
1570
2051
  }
1571
2052
 
@@ -1627,6 +2108,50 @@
1627
2108
  return null;
1628
2109
  }
1629
2110
 
2111
+ /** If the widget only has titleFont, expose subtitleFont in Edit (same FontIC). */
2112
+ function ensureSubtitleFontInDesignSchema(schema: unknown): unknown {
2113
+ if (!schema) return schema;
2114
+ try {
2115
+ const clone = JSON.parse(JSON.stringify(schema));
2116
+ const groups = Array.isArray(clone) ? clone : clone.componentGroups;
2117
+ if (!Array.isArray(groups) || groups.length === 0) return schema;
2118
+ const allProps = groups.flatMap((group: { components?: Array<{ properties?: unknown[] }> }) =>
2119
+ (group.components || []).flatMap((component) => component.properties || [])
2120
+ );
2121
+ const hasSubtitle = allProps.some((prop: { name?: string; id?: string }) =>
2122
+ /subtitleFont/i.test(String(prop?.name || prop?.id || ''))
2123
+ );
2124
+ if (hasSubtitle) return clone;
2125
+ const titleProp = allProps.find((prop: { name?: string; id?: string; inputController?: { type?: string } }) => {
2126
+ const key = String(prop?.name || prop?.id || '');
2127
+ const type = String(prop?.inputController?.type || '').toLowerCase();
2128
+ return type === 'font' && /titleFont|^font$/i.test(key);
2129
+ });
2130
+ if (!titleProp || typeof titleProp !== 'object') return clone;
2131
+ const firstComponent = groups[0]?.components?.[0];
2132
+ if (!firstComponent) return clone;
2133
+ firstComponent.properties = Array.isArray(firstComponent.properties) ? firstComponent.properties : [];
2134
+ const subtitleProp = {
2135
+ ...JSON.parse(JSON.stringify(titleProp)),
2136
+ id: 'subtitleFont',
2137
+ name: 'subtitleFont',
2138
+ description: 'Subtitle Font',
2139
+ };
2140
+ const titleIndex = firstComponent.properties.findIndex(
2141
+ (prop: { name?: string; id?: string }) =>
2142
+ (prop?.name || prop?.id) === ((titleProp as { name?: string; id?: string }).name
2143
+ || (titleProp as { name?: string; id?: string }).id)
2144
+ );
2145
+ if (titleIndex >= 0) firstComponent.properties.splice(titleIndex + 1, 0, subtitleProp);
2146
+ else firstComponent.properties.push(subtitleProp);
2147
+ console.log('[CompositionEditor] Added subtitleFont FontIC next to titleFont');
2148
+ return Array.isArray(schema) ? clone : clone;
2149
+ } catch (error) {
2150
+ console.warn('[CompositionEditor] subtitleFont schema inject skipped:', error);
2151
+ return schema;
2152
+ }
2153
+ }
2154
+
1630
2155
  function normalizeContentSchemaFromApiResponse(contentRes: unknown): any {
1631
2156
  if (!contentRes) return null;
1632
2157
  const payload = (contentRes as { data?: unknown })?.data ?? contentRes;
@@ -1756,14 +2281,20 @@
1756
2281
  }
1757
2282
 
1758
2283
  /** Load design & content schemas for a widget — per-panel; never wipes other panels. */
1759
- async function loadWidgetSchemas(widgetId: string): Promise<void> {
2284
+ async function loadWidgetSchemas(widgetId: string, options?: { force?: boolean }): Promise<void> {
1760
2285
  if (!widgetsClient || !hasValidToken) {
1761
2286
  patchPanelSnapshot(widgetId, { compositionSchemasLoading: false, schemaLoadStartedAt: null });
1762
2287
  return;
1763
2288
  }
1764
2289
 
2290
+ if (options?.force) {
2291
+ widgetSchemaCache.delete(widgetId);
2292
+ cancelPanelSchemaLoad(widgetId);
2293
+ abandonSchemaLoadAttempt(widgetId);
2294
+ }
2295
+
1765
2296
  const existingLoad = schemaLoadPromises.get(widgetId);
1766
- if (existingLoad) {
2297
+ if (existingLoad && !options?.force) {
1767
2298
  const snap = getPanelState(widgetId);
1768
2299
  if (!isStalePanelSchemaLoading(widgetId, snap)) return existingLoad;
1769
2300
  cancelPanelSchemaLoad(widgetId);
@@ -1779,7 +2310,7 @@
1779
2310
  || existing.compositionContentSchema
1780
2311
  );
1781
2312
 
1782
- if (hasCachedSchemas) {
2313
+ if (hasCachedSchemas && !options?.force) {
1783
2314
  const designSchema = cached?.design ?? existing.compositionDesignSchema;
1784
2315
  const contentSchema = cached?.content ?? existing.compositionContentSchema;
1785
2316
  const schemaError = cached?.error ?? existing.compositionSchemasError;
@@ -1874,7 +2405,9 @@
1874
2405
  'Content schema',
1875
2406
  );
1876
2407
 
1877
- designSchema = normalizeDesignSchemaFromApiResponse(designRes);
2408
+ designSchema = ensureSubtitleFontInDesignSchema(
2409
+ normalizeDesignSchemaFromApiResponse(designRes)
2410
+ );
1878
2411
  contentSchema = normalizeContentSchemaFromApiResponse(contentRes);
1879
2412
 
1880
2413
  if (!designSchema && !contentSchema) {
@@ -1924,7 +2457,11 @@
1924
2457
  compositionSchemasError = schemaError;
1925
2458
  }
1926
2459
  if (designSchema || contentSchema) {
1927
- queueMicrotask(() => loadCompositionsForPanel(widgetId));
2460
+ queueMicrotask(() => {
2461
+ void loadCompositionsForPanel(widgetId).then(() => {
2462
+ applyPanelCompositionToPreview(widgetId);
2463
+ });
2464
+ });
1928
2465
  }
1929
2466
  } finally {
1930
2467
  schemaLoadInFlight.delete(widgetId);
@@ -1962,20 +2499,32 @@
1962
2499
  return values;
1963
2500
  }
1964
2501
 
1965
- /** After codegen, push design.json defaults into Create preview (Worker HTML uses var() fallbacks only). */
2502
+ /** After codegen, push design.json + content.json defaults into Create preview. */
1966
2503
  async function applyDesignSchemaDefaultsToPreview(widgetId: string) {
1967
2504
  if (!widgetId || !widgetsClient) return;
1968
2505
  await scheduleWidgetSchemaLoad(widgetId);
1969
2506
  const schema = getPanelDisplayState(widgetId).compositionDesignSchema;
1970
2507
  const defaults = extractDesignDefaultsFromSchema(schema);
1971
- if (Object.keys(defaults).length === 0) return;
1972
- if (widgetId === focusedPanelWidgetId) {
1973
- liveDesignValues = defaults;
1974
- } else {
1975
- patchPanelSnapshot(widgetId, { liveDesignValues: defaults });
2508
+ if (Object.keys(defaults).length > 0) {
2509
+ if (widgetId === focusedPanelWidgetId) {
2510
+ liveDesignValues = defaults;
2511
+ } else {
2512
+ patchPanelSnapshot(widgetId, { liveDesignValues: defaults });
2513
+ }
2514
+ getWidgetDetailsRef(widgetId)?.sendMessageToPreview({ type: 'widgetic:update', design: flattenDesignValuesForPreview(defaults) });
2515
+ console.log('[Create Preview] Applied design schema defaults:', Object.keys(defaults));
2516
+ }
2517
+ const contentSchema = getPanelDisplayState(widgetId).compositionContentSchema;
2518
+ const schemaItems = contentSchema?.contentItems ?? contentSchema?.data?.contentItems;
2519
+ if (Array.isArray(schemaItems) && schemaItems.length > 0) {
2520
+ const existing = getPanelDisplayState(widgetId).liveContentItems || [];
2521
+ const merged = mergeContentItemsWithSchema(existing, schemaItems);
2522
+ patchPanelSnapshot(widgetId, { liveContentItems: merged });
2523
+ if (widgetId === focusedPanelWidgetId) liveContentItems = merged;
1976
2524
  }
1977
- getWidgetDetailsRef(widgetId)?.sendMessageToPreview({ type: 'widgetic:update', design: defaults });
1978
- console.log('[Create Preview] Applied design schema defaults:', Object.keys(defaults));
2525
+ applyPanelCompositionToPreview(widgetId);
2526
+ setTimeout(() => applyPanelCompositionToPreview(widgetId), 250);
2527
+ setTimeout(() => applyPanelCompositionToPreview(widgetId), 800);
1979
2528
  }
1980
2529
 
1981
2530
  /** Toggle composition editor panel open/closed */
@@ -2045,9 +2594,20 @@
2045
2594
  });
2046
2595
  }
2047
2596
  if (event.detail.step === 'create') {
2048
- if (Object.keys(ps.liveDesignValues).length > 0) {
2049
- panelRef?.sendMessageToPreview({ type: 'widgetic:update', design: ps.liveDesignValues });
2597
+ let psCreate = getPanelDisplayState(panelWidgetId);
2598
+ if (!psCreate.compositionContentSchema && !psCreate.compositionSchemasLoading) {
2599
+ void scheduleWidgetSchemaLoad(panelWidgetId);
2050
2600
  }
2601
+ if (!psCreate.liveContentItems || psCreate.liveContentItems.length === 0) {
2602
+ const schemaItems = psCreate.compositionContentSchema?.contentItems
2603
+ ?? psCreate.compositionContentSchema?.data?.contentItems;
2604
+ if (Array.isArray(schemaItems) && schemaItems.length > 0) {
2605
+ const seeded = JSON.parse(JSON.stringify(schemaItems));
2606
+ patchPanelSnapshot(panelWidgetId, { liveContentItems: seeded });
2607
+ if (panelWidgetId === focusedPanelWidgetId) liveContentItems = seeded;
2608
+ }
2609
+ }
2610
+ applyPanelCompositionToPreview(panelWidgetId);
2051
2611
  if (panelWidgetId && pendingCreatePreviewDefaultsWidgetId === panelWidgetId) {
2052
2612
  pendingCreatePreviewDefaultsWidgetId = null;
2053
2613
  if (panelWidgetId === focusedPanelWidgetId) {
@@ -2089,15 +2649,22 @@
2089
2649
  const versionedUrl = `${cdnBase}/widgets/${widgetId}/v${version}/widget.html`;
2090
2650
 
2091
2651
  if (jsPath) {
2652
+ const trimmed = jsPath.replace(/\/+$/, '');
2653
+ let resolved = trimmed
2654
+ .replace(/\/widget\.js$/i, '/widget.html')
2655
+ .replace(/\.js$/i, '.html');
2656
+ if (!/\.html$/i.test(resolved)) {
2657
+ resolved = `${resolved}/widget.html`;
2658
+ }
2659
+ if (resolved.startsWith('/')) {
2660
+ resolved = `${cdnBase}${resolved}`;
2661
+ }
2092
2662
  const jsPathVersionMatch = jsPath.match(/\/v(\d+)(?:\/|$)/);
2093
2663
  const jsPathVersion = jsPathVersionMatch ? parseInt(jsPathVersionMatch[1], 10) : null;
2094
- if (jsPathVersion === version) {
2095
- const trimmed = jsPath.replace(/\/+$/, '');
2096
- let resolved = /\.html$/i.test(trimmed) ? trimmed : `${trimmed}/widget.html`;
2097
- // Bare pathname (e.g. from older saves) must be resolved against CDN, not localhost
2098
- if (resolved.startsWith('/')) {
2099
- resolved = `${cdnBase}${resolved}`;
2100
- }
2664
+ const jsPathWidgetId = (resolved.match(/\/widgets\/([^/]+)\//i) || [])[1] || null;
2665
+ const pointsAtOtherWidget = !!(jsPathWidgetId && jsPathWidgetId !== widgetId);
2666
+ if (pointsAtOtherWidget || jsPathVersion === version || /\/widgets\//i.test(resolved)) {
2667
+ console.log('[Publish] Using js_path artifact URL', resolved, pointsAtOtherWidget ? '(source widget)' : '');
2101
2668
  return resolved;
2102
2669
  }
2103
2670
  }
@@ -2128,6 +2695,68 @@
2128
2695
  let suppressNextBuildFromRepoChatMessage = false;
2129
2696
  let _isRestoringCheckpoint = false;
2130
2697
 
2698
+ function mapFontWeightToCss(weight: string | undefined): string {
2699
+ const name = String(weight || 'Regular').toLowerCase().replace(/[\s_-]/g, '');
2700
+ const table: Record<string, string> = {
2701
+ thin: '100', hairline: '100',
2702
+ extralight: '200', ultralight: '200',
2703
+ light: '300',
2704
+ regular: '400', normal: '400',
2705
+ medium: '500',
2706
+ semibold: '600', demibold: '600',
2707
+ bold: '700',
2708
+ extrabold: '800', ultrabold: '800',
2709
+ black: '900', heavy: '900',
2710
+ };
2711
+ if (/^\d{3}$/.test(name)) return name;
2712
+ return table[name] || '400';
2713
+ }
2714
+
2715
+ function mapAspectRatioToCss(raw: string): string {
2716
+ const key = raw.trim().toLowerCase();
2717
+ const table: Record<string, string> = {
2718
+ square: '1/1',
2719
+ '1:1': '1/1',
2720
+ '1/1': '1/1',
2721
+ landscape: '4/3',
2722
+ '4:3': '4/3',
2723
+ '4/3': '4/3',
2724
+ wide: '16/9',
2725
+ '16:9': '16/9',
2726
+ '16/9': '16/9',
2727
+ portrait: '3/4',
2728
+ '3:4': '3/4',
2729
+ '3/4': '3/4',
2730
+ };
2731
+ return table[key] || raw;
2732
+ }
2733
+
2734
+ /** CSS custom properties for widgetic:update — FontIC objects and dropdown {value} become strings. */
2735
+ function flattenDesignValuesForPreview(design: Record<string, unknown>): Record<string, string> {
2736
+ const out: Record<string, string> = {};
2737
+ for (const [key, value] of Object.entries(design || {})) {
2738
+ if (value && typeof value === 'object' && 'family' in (value as Record<string, unknown>)) {
2739
+ const font = value as { family?: string; weight?: string };
2740
+ const family = String(font.family || 'Inter');
2741
+ out[key] = `"${family}", sans-serif`;
2742
+ out[`${key}Family`] = family;
2743
+ out[`${key}Weight`] = mapFontWeightToCss(font.weight);
2744
+ continue;
2745
+ }
2746
+ if (value && typeof value === 'object' && 'value' in (value as Record<string, unknown>)) {
2747
+ out[key] = mapAspectRatioToCss(String((value as { value?: unknown }).value ?? ''));
2748
+ continue;
2749
+ }
2750
+ if (typeof value === 'string') {
2751
+ out[key] = /aspect/i.test(key) ? mapAspectRatioToCss(value) : value;
2752
+ continue;
2753
+ }
2754
+ if (value == null) continue;
2755
+ out[key] = String(value);
2756
+ }
2757
+ return out;
2758
+ }
2759
+
2131
2760
  /** Handle property change from PropsEditor — send to widget preview iframe */
2132
2761
  function handleEditorPropertyChange(event: CustomEvent<{ schema: string; propertyId: string; value: unknown; opacity?: number; itemId?: string }>) {
2133
2762
  const { schema, propertyId, value, itemId } = event.detail;
@@ -2137,7 +2766,7 @@
2137
2766
  liveDesignValues = { ...liveDesignValues, [propertyId]: value };
2138
2767
  getWidgetDetailsRef(focusedPanelWidgetId)?.sendMessageToPreview({
2139
2768
  type: 'widgetic:update',
2140
- design: liveDesignValues
2769
+ design: flattenDesignValuesForPreview(liveDesignValues)
2141
2770
  });
2142
2771
  } else if (schema === 'content') {
2143
2772
  if (itemId) {
@@ -2320,11 +2949,16 @@
2320
2949
 
2321
2950
  /** Push composition overlay to a specific panel preview iframe. */
2322
2951
  function applyPanelCompositionToPreview(widgetId: string): void {
2323
- const ps = getPanelDisplayState(widgetId);
2324
2952
  const panelRef = getWidgetDetailsRef(widgetId);
2325
2953
  if (!panelRef) return;
2954
+ const step = widgetId === focusedPanelWidgetId ? widgetDetailsStep : 'create';
2955
+ if (step === 'create') {
2956
+ console.log('[Preview] Skip composition overlay on Create step', widgetId?.substring(0, 8));
2957
+ return;
2958
+ }
2959
+ const ps = getPanelDisplayState(widgetId);
2326
2960
  if (Object.keys(ps.liveDesignValues).length > 0) {
2327
- panelRef.sendMessageToPreview({ type: 'widgetic:update', design: ps.liveDesignValues });
2961
+ panelRef.sendMessageToPreview({ type: 'widgetic:update', design: flattenDesignValuesForPreview(ps.liveDesignValues as Record<string, unknown>) });
2328
2962
  }
2329
2963
  if (Object.keys(ps.liveContentValues).length > 0) {
2330
2964
  panelRef.sendMessageToPreview({ type: 'widgetic:update', content: ps.liveContentValues });
@@ -2586,6 +3220,10 @@
2586
3220
  /** Send current composition design/content values to the preview iframe via postMessage.
2587
3221
  * Called on: composition select, iframe ready, and step switch to Edit/Embed. */
2588
3222
  function applyCompositionToPreview() {
3223
+ if (widgetDetailsStep === 'create') {
3224
+ console.log('[Preview] Skip composition overlay on Create step');
3225
+ return;
3226
+ }
2589
3227
  if (Object.keys(liveDesignValues).length > 0) {
2590
3228
  widgetDetails?.sendMessageToPreview({ type: 'widgetic:update', design: liveDesignValues });
2591
3229
  }
@@ -2593,7 +3231,10 @@
2593
3231
  widgetDetails?.sendMessageToPreview({ type: 'widgetic:update', content: liveContentValues });
2594
3232
  }
2595
3233
  if (liveContentItems.length > 0) {
2596
- widgetDetails?.sendMessageToPreview({ type: 'widgetic:update', contentItems: liveContentItems });
3234
+ widgetDetails?.sendMessageToPreview({
3235
+ type: 'widgetic:update',
3236
+ contentItems: flattenContentItemsForPreview(liveContentItems)
3237
+ });
2597
3238
  }
2598
3239
  }
2599
3240
 
@@ -3003,6 +3644,8 @@
3003
3644
  }
3004
3645
 
3005
3646
  let canvasWireframeMode = true;
3647
+ /** Extra canvas chrome — default off. Not tied to debugMode. */
3648
+ let canvasAdvancedFeatures = !!initialAdvancedFeatures;
3006
3649
  let canvasPrimaryWidgetId: string | null = null;
3007
3650
 
3008
3651
  function emitCreatorWireframeChanged() {
@@ -3020,6 +3663,21 @@
3020
3663
  emitCreatorWireframeChanged();
3021
3664
  }
3022
3665
 
3666
+ function emitCreatorAdvancedChanged() {
3667
+ if (typeof window === 'undefined') return;
3668
+ window.dispatchEvent(
3669
+ new CustomEvent(CREATOR_ADVANCED_CHANGED_EVENT, {
3670
+ detail: { advancedFeatures: canvasAdvancedFeatures }
3671
+ })
3672
+ );
3673
+ }
3674
+
3675
+ function handleHostToggleAdvancedEvent() {
3676
+ canvasAdvancedFeatures = !canvasAdvancedFeatures;
3677
+ console.log('[CreatorApp] Advanced canvas features:', canvasAdvancedFeatures ? 'ON' : 'OFF');
3678
+ emitCreatorAdvancedChanged();
3679
+ }
3680
+
3023
3681
  function resolveHostTargetWidgetId(): string | null {
3024
3682
  const id = focusedPanelWidgetId || selectedWidgetId;
3025
3683
  if (!id || isDraftConvertId(id)) return null;
@@ -3039,19 +3697,21 @@
3039
3697
  }
3040
3698
 
3041
3699
  async function handleHostDuplicateWidgetEvent(): Promise<void> {
3042
- const widgetId = resolveHostTargetWidgetId();
3043
- if (!widgetId || !canvasRef) {
3700
+ const widgetId = canvasPrimaryWidgetId || resolveHostTargetWidgetId();
3701
+ if (!widgetId || isDraftConvertId(widgetId)) {
3044
3702
  showToast('warning', 'Select a widget first', {
3045
- description: 'Click the widget on the canvas, then Duplicate.',
3703
+ description: 'Duplicate from the header copies the widget in the title.',
3046
3704
  });
3047
3705
  return;
3048
3706
  }
3049
- canvasRef.selectWidgetShape?.(widgetId);
3050
- if (typeof canvasRef.duplicateSelectedObjects === 'function') {
3051
- await canvasRef.duplicateSelectedObjects();
3052
- return;
3707
+ console.log('[CreatorApp] Host Duplicate — primary widget', widgetId.substring(0, 8));
3708
+ if (canvasRef?.selectWidgetShape && typeof canvasRef.duplicateSelectedObjects === 'function') {
3709
+ if (canvasRef.selectWidgetShape(widgetId)) {
3710
+ await canvasRef.duplicateSelectedObjects();
3711
+ return;
3712
+ }
3053
3713
  }
3054
- showToast('error', 'Cannot duplicate widget', { description: 'Canvas duplicate is not available' });
3714
+ await handleWidgetShapeDuplicated(widgetId, null);
3055
3715
  }
3056
3716
 
3057
3717
  async function handleHostDeleteWidgetEvent(): Promise<void> {
@@ -3071,6 +3731,15 @@
3071
3731
  }
3072
3732
  }
3073
3733
 
3734
+ function emitCreatorSaveState(dirty: boolean): void {
3735
+ if (typeof window === 'undefined' || !embedded) return;
3736
+ window.dispatchEvent(new CustomEvent('widgetic:creator-save-state', { detail: { dirty } }));
3737
+ }
3738
+
3739
+ function handleHostSaveCanvasEvent(): void {
3740
+ canvasRef?.saveNow?.();
3741
+ }
3742
+
3074
3743
  async function persistPrimaryWidgetId(widgetId: string) {
3075
3744
  if (!widgetId) return;
3076
3745
  canvasPrimaryWidgetId = widgetId;
@@ -3086,7 +3755,6 @@
3086
3755
  }
3087
3756
  }
3088
3757
  const widget = getWidgetById(widgetId);
3089
- if (widget) notifyHostWidgetName(widget);
3090
3758
  if (embedded && typeof window !== 'undefined') {
3091
3759
  try {
3092
3760
  const url = new URL(window.location.href);
@@ -3098,12 +3766,110 @@
3098
3766
  console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
3099
3767
  }
3100
3768
  }
3769
+ if (widget) notifyHostWidgetName(widget);
3770
+ emitCanvasWidgetsList('primary-changed');
3771
+ }
3772
+
3773
+ function emitCanvasWidgetsList(reason: string) {
3774
+ if (!embedded || typeof window === 'undefined') return;
3775
+ const ids = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
3776
+ const items: { id: string; name: string }[] = [];
3777
+ for (const id of ids) {
3778
+ const widget = getWidgetById(id);
3779
+ items.push({
3780
+ id,
3781
+ name: (widget?.name || '').trim() || 'Untitled Widget'
3782
+ });
3783
+ }
3784
+ if (canvasPrimaryWidgetId && !items.some((item) => item.id === canvasPrimaryWidgetId)) {
3785
+ const primary = getWidgetById(canvasPrimaryWidgetId);
3786
+ if (primary?.id) {
3787
+ items.unshift({
3788
+ id: primary.id,
3789
+ name: (primary.name || '').trim() || 'Untitled Widget'
3790
+ });
3791
+ }
3792
+ }
3793
+ const primaryWidgetId = canvasPrimaryWidgetId || '';
3794
+ console.log('[CreatorApp] Emit canvas widgets for header', reason, items.length, primaryWidgetId);
3795
+ window.dispatchEvent(new CustomEvent(CREATOR_CANVAS_WIDGETS_EVENT, {
3796
+ detail: { widgets: items, primaryWidgetId }
3797
+ }));
3798
+ }
3799
+
3800
+ function handleHostRequestCanvasWidgetsEvent() {
3801
+ emitCanvasWidgetsList('host-request');
3802
+ }
3803
+
3804
+ function handleHostSetPrimaryWidgetEvent(event: Event) {
3805
+ const widgetId = (event as CustomEvent).detail?.widgetId;
3806
+ if (!widgetId || typeof widgetId !== 'string') {
3807
+ console.warn('[CreatorApp] Host set primary ignored — missing widgetId');
3808
+ return;
3809
+ }
3810
+ console.log('[CreatorApp] Host set primary widget', widgetId.substring(0, 8));
3811
+ canvasRef?.setPrimaryWidgetShape?.(widgetId);
3812
+ canvasRef?.selectWidgetShape?.(widgetId);
3813
+ void persistPrimaryWidgetId(widgetId);
3101
3814
  }
3102
3815
 
3103
3816
  function handleSetPrimaryWidget(widgetId: string) {
3104
3817
  void persistPrimaryWidgetId(widgetId);
3105
3818
  }
3106
3819
 
3820
+ async function hydrateCanvasPrimaryWidget(linkedIds?: string[]): Promise<string | null> {
3821
+ const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
3822
+ if (!targetCanvasId) return canvasPrimaryWidgetId || null;
3823
+ let primaryId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
3824
+ if (!primaryId) {
3825
+ const fromLinks = (linkedIds || []).filter((id) => id && !isDraftConvertId(id) && !String(id).startsWith('draft'));
3826
+ const fromList = widgets
3827
+ .filter((w: any) => {
3828
+ const cid = w.canvasId || w.canvas_id;
3829
+ return cid === targetCanvasId && w.id && !isDraftConvertId(w.id);
3830
+ })
3831
+ .map((w: any) => w.id);
3832
+ primaryId = fromLinks[0] || fromList[0] || null;
3833
+ if (primaryId) {
3834
+ console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
3835
+ await persistPrimaryWidgetId(primaryId);
3836
+ }
3837
+ }
3838
+ if (primaryId) {
3839
+ const widget = getWidgetById(primaryId);
3840
+ if (widget) notifyHostWidgetName(widget);
3841
+ emitCanvasWidgetsList('hydrate-primary');
3842
+ }
3843
+ return primaryId || canvasPrimaryWidgetId || null;
3844
+ }
3845
+
3846
+ async function handleHostMoveCanvasFolderEvent(event: Event) {
3847
+ const folderId = (event as CustomEvent).detail?.folderId;
3848
+ if (!folderId || typeof folderId !== 'string' || !widgetsClient) return;
3849
+ const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
3850
+ let primaryId = canvasPrimaryWidgetId || (targetCanvasId ? await resolvePrimaryWidgetIdForCanvas(targetCanvasId) : null);
3851
+ if (!primaryId) {
3852
+ primaryId = await hydrateCanvasPrimaryWidget();
3853
+ }
3854
+ const ids = new Set<string>();
3855
+ if (primaryId) ids.add(primaryId);
3856
+ for (const w of widgets) {
3857
+ const cid = (w as any).canvasId || (w as any).canvas_id;
3858
+ if (targetCanvasId && cid === targetCanvasId && w.id) ids.add(w.id);
3859
+ }
3860
+ console.log('[CreatorApp] Move canvas widgets to folder', folderId, [...ids].map((id) => id.substring(0, 8)));
3861
+ for (const id of ids) {
3862
+ try {
3863
+ await widgetsClient.updateWidget({
3864
+ id,
3865
+ updateWidgetRequest: { projectId: folderId, projectFolder: folderId }
3866
+ });
3867
+ } catch (err) {
3868
+ console.warn('[CreatorApp] Failed to move widget to folder', id.substring(0, 8), err);
3869
+ }
3870
+ }
3871
+ }
3872
+
3107
3873
  // Preview Modal State Variables
3108
3874
  let previewIframe: HTMLIFrameElement | null = null;
3109
3875
  const { DEV: isDevMode } = import.meta.env;
@@ -3608,18 +4374,11 @@
3608
4374
  const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
3609
4375
  await syncWidgetsWithCanvas(widgetIdsOnCanvas);
3610
4376
  await maybeAutoRestoreWidgetShapes();
3611
- // Backfill widget name + thumbnail on shapes that were saved before those
3612
- // fields existed on WidgetShape (fixes "Widget" placeholder on old canvases).
3613
4377
  backfillWidgetShapeMetadata();
3614
- if (loaded && canvasRef) {
3615
- const primaryId =
3616
- canvasPrimaryWidgetId ||
3617
- canvasRef.getPrimaryWidgetId?.() ||
3618
- selectedWidgetId;
3619
- if (primaryId) {
3620
- canvasRef.setPrimaryWidgetShape?.(primaryId);
3621
- canvasRef.selectWidgetShape(primaryId);
3622
- }
4378
+ const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
4379
+ if (loaded && canvasRef && primaryId && !isConvertOrDraftPanelOpen()) {
4380
+ canvasRef.setPrimaryWidgetShape?.(primaryId);
4381
+ canvasRef.selectWidgetShape(primaryId);
3623
4382
  }
3624
4383
  } catch (error) {
3625
4384
  console.error('Widget Creator: Load canvases error:', error);
@@ -4481,6 +5240,11 @@
4481
5240
  if (embeddedDeepLinkAutoOpenDone) {
4482
5241
  return;
4483
5242
  }
5243
+ if (isConvertOrDraftPanelOpen()) {
5244
+ console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
5245
+ embeddedDeepLinkAutoOpenDone = true;
5246
+ return;
5247
+ }
4484
5248
 
4485
5249
  let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
4486
5250
  // Canvas-only deep-link: no ?widget_id= / initialWidgetId — select canvases.primary_widget_id.
@@ -4878,20 +5642,19 @@
4878
5642
  }
4879
5643
  }
4880
5644
 
4881
- /** After hydrate, cross-check GitLab HEAD if we don't have a fresh local commit.
4882
- * Widget: if HEAD sha differs from the local lastCommitId, promote it so the Edit banner
4883
- * ("Newer code than Live") can detect unpublished code even without recent codegen locally. */
5645
+ /** After hydrate, probe GitLab HEAD only when this widget has no codegen commit yet.
5646
+ * Do not promote HEAD over lastCommitId — that marks a new publish version on every Details open. */
4884
5647
  async function reconcileWidgetHeadCommit(widgetId: string): Promise<void> {
4885
5648
  const currentPanelLastCommit = getPanelState(widgetId).lastCommitId;
4886
- const publishSha = getWidgetLastPublishCommitSha(widgetId);
4887
- // If we already have a lastCommitId that differs from the publish SHA, no probe needed.
4888
- if (currentPanelLastCommit && publishSha && !commitsMatch(currentPanelLastCommit, publishSha)) {
5649
+ if (currentPanelLastCommit) {
5650
+ console.log(
5651
+ `[Version] Skip HEAD probe for "${widgetId}": lastCommitId already ${currentPanelLastCommit.substring(0, 8)}`
5652
+ );
4889
5653
  return;
4890
5654
  }
4891
5655
  const headSha = await fetchRepoHeadCommitSha(widgetId);
4892
5656
  if (!headSha) return;
4893
- if (currentPanelLastCommit && commitsMatch(currentPanelLastCommit, headSha)) return;
4894
- console.log(`[Version] Repo HEAD probe for "${widgetId}": ${headSha.substring(0, 8)} (was ${currentPanelLastCommit?.substring(0, 8) ?? 'null'})`);
5657
+ console.log(`[Version] Repo HEAD probe for "${widgetId}": ${headSha.substring(0, 8)} (was null)`);
4895
5658
  patchPanelSnapshot(widgetId, { lastCommitId: headSha, hasGeneratedCode: true });
4896
5659
  saveCommitIdForWidget(widgetId, headSha);
4897
5660
  if (widgetId === focusedPanelWidgetId) {
@@ -5164,9 +5927,11 @@
5164
5927
  // Ensure we have an id
5165
5928
  const newWidget = response.data || response; // handle envelope or direct
5166
5929
  if (newWidget?.id) {
5167
- // Optimistically add to list and select
5168
5930
  widgets = [{ ...newWidget }, ...widgets];
5169
5931
  selectWidget(newWidget);
5932
+ if (!canvasPrimaryWidgetId) {
5933
+ void persistPrimaryWidgetId(newWidget.id);
5934
+ }
5170
5935
  authStatus = 'Widget created.';
5171
5936
  } else {
5172
5937
  authStatus = 'Widget created but response missing id.';
@@ -5247,11 +6012,7 @@
5247
6012
 
5248
6013
  const status = (op?.status || '').toLowerCase();
5249
6014
  if (status === 'completed' || status === 'failed' || status === 'cancelled') {
5250
- if (status === 'failed') {
5251
- settle('reject', new Error(op?.error || 'Operation failed.'));
5252
- } else {
5253
- settle('resolve', op);
5254
- }
6015
+ settle('resolve', op);
5255
6016
  return;
5256
6017
  }
5257
6018
 
@@ -5290,11 +6051,7 @@
5290
6051
  signal
5291
6052
  })
5292
6053
  .then((finalOp) => {
5293
- if (String(finalOp?.status || '').toLowerCase() === 'failed') {
5294
- settle('reject', new Error(finalOp?.error || 'Operation failed.'));
5295
- } else {
5296
- settle('resolve', finalOp as OperationStatus);
5297
- }
6054
+ settle('resolve', finalOp as OperationStatus);
5298
6055
  })
5299
6056
  .catch((streamError) => {
5300
6057
  if (settled || signal?.aborted) return;
@@ -5822,11 +6579,22 @@
5822
6579
  const quiet = !!options.quiet;
5823
6580
  console.log('[handleRefreshScreenshot] Capturing screenshot via postMessage for widget:', widgetId, quiet ? '(quiet)' : '');
5824
6581
 
6582
+ if (isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
6583
+ console.log('[handleRefreshScreenshot] Skip iframe capture — convert/draft panel is open:', widgetId.substring(0, 8));
6584
+ return;
6585
+ }
6586
+
5825
6587
  // Quiet canvas-thumbnail sync must never force-open Widget Details.
5826
- // Manual refresh may open the panel so the preview iframe exists.
6588
+ // Convert drafts capture from the canvas shape, never by opening a panel.
5827
6589
  if (!openWidgetPanelIds.includes(widgetId)) {
5828
- if (quiet) {
5829
- console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
6590
+ if (quiet || isConvertOrDraftPanelOpen()) {
6591
+ const shapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
6592
+ if (shapeDataUrl && canvasRef) {
6593
+ console.log('[handleRefreshScreenshot] Using canvas shape screenshot (panel closed):', widgetId.substring(0, 8));
6594
+ canvasRef.updateWidgetImage(widgetId, shapeDataUrl);
6595
+ } else {
6596
+ console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
6597
+ }
5830
6598
  return;
5831
6599
  }
5832
6600
  // Manual "Refresh Widget Preview" always opens the panel so the iframe exists.
@@ -5836,8 +6604,9 @@
5836
6604
  }
5837
6605
 
5838
6606
  const panelRef = getWidgetDetailsRef(widgetId);
5839
- if (!panelRef?.hasValidPreview()) {
5840
- console.log('[handleRefreshScreenshot] Preview not ready or has error — skipping empty screenshot capture');
6607
+ const iframe = panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null;
6608
+ if (!iframe?.contentWindow) {
6609
+ console.log('[handleRefreshScreenshot] Preview iframe not ready — skipping capture');
5841
6610
  if (!quiet) {
5842
6611
  showToast('error', 'No valid preview available', { description: 'Generate code first to capture a screenshot.', duration: 5000 });
5843
6612
  }
@@ -5848,14 +6617,6 @@
5848
6617
  showToast('info', 'Capturing widget screenshot...', { duration: 3000 });
5849
6618
  }
5850
6619
 
5851
- const iframe = panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null;
5852
- if (!iframe?.contentWindow) {
5853
- if (!quiet) {
5854
- showToast('error', 'No preview iframe available', { description: 'Generate code first to capture a screenshot.', duration: 5000 });
5855
- }
5856
- return;
5857
- }
5858
-
5859
6620
  try {
5860
6621
  const result = await new Promise<{ data: string | null; error: string | null }>((resolve) => {
5861
6622
  const timeout = setTimeout(() => {
@@ -5982,10 +6743,21 @@ Requirements:
5982
6743
  fromChat?: boolean;
5983
6744
  widgetId?: string;
5984
6745
  compileFixOnly?: boolean;
6746
+ /** Recreate GitLab repo when the previous turn failed with missing repository. */
6747
+ forceRecreateRepo?: boolean;
5985
6748
  /** CDN upload row IDs captured before chat clearDraft. */
5986
6749
  attachmentIds?: string[];
5987
6750
  /** Image URLs / data URLs captured before chat clearDraft. */
5988
6751
  promptImagesOverride?: string[];
6752
+ /** Chat files (PDF, video, images) to store on the user message before codegen. */
6753
+ persistFileAttachments?: Array<{
6754
+ url?: string;
6755
+ file?: File;
6756
+ file_type?: string;
6757
+ file_name?: string | null;
6758
+ file_size?: number | null;
6759
+ display_order?: number;
6760
+ }>;
5989
6761
  } = {}) {
5990
6762
  const targetWidgetId = options.widgetId ?? selectedWidgetId;
5991
6763
  if (!targetWidgetId) return;
@@ -6062,8 +6834,10 @@ Requirements:
6062
6834
  fromChat: options.fromChat,
6063
6835
  widgetId: codegenTargetWidgetId,
6064
6836
  compileFixOnly: options.compileFixOnly,
6837
+ forceRecreateRepo: options.forceRecreateRepo,
6065
6838
  attachmentIds: options.attachmentIds,
6066
6839
  promptImagesOverride: options.promptImagesOverride,
6840
+ persistFileAttachments: options.persistFileAttachments,
6067
6841
  });
6068
6842
  }
6069
6843
 
@@ -6082,8 +6856,17 @@ Requirements:
6082
6856
  fromChat?: boolean;
6083
6857
  widgetId?: string;
6084
6858
  compileFixOnly?: boolean;
6859
+ forceRecreateRepo?: boolean;
6085
6860
  attachmentIds?: string[];
6086
6861
  promptImagesOverride?: string[];
6862
+ persistFileAttachments?: Array<{
6863
+ url?: string;
6864
+ file?: File;
6865
+ file_type?: string;
6866
+ file_name?: string | null;
6867
+ file_size?: number | null;
6868
+ display_order?: number;
6869
+ }>;
6087
6870
  } = {},
6088
6871
  ) {
6089
6872
  const generationWidgetId = options.widgetId ?? selectedWidgetId;
@@ -6106,7 +6889,6 @@ Requirements:
6106
6889
  lastHarnessChatLine = '';
6107
6890
 
6108
6891
  // Show user prompt in chat when triggered outside Chat (confirm dialog / Generate Now).
6109
- // DB write happens here too — not on widget select (so "Edit Prompt First" stays editable).
6110
6892
  if (panelChat && !options.fromChat) {
6111
6893
  const snapImages =
6112
6894
  generationWidgetId === focusedPanelWidgetId ? promptImages : panelSnap.promptImages;
@@ -6120,8 +6902,7 @@ Requirements:
6120
6902
  : undefined;
6121
6903
  panelChat.addUserMessage(prompt, codegenImageAttachments);
6122
6904
  }
6123
- await ensureCodegenUserMessagePersisted(generationWidgetId, prompt);
6124
-
6905
+
6125
6906
  isGeneratingCode = generationWidgetId === focusedPanelWidgetId;
6126
6907
  setCrossTabGenerating(true, generationWidgetId);
6127
6908
  lastGenerationPrompt = prompt;
@@ -6132,7 +6913,7 @@ Requirements:
6132
6913
  ? 'Processing queued code generation request...'
6133
6914
  : 'Preparing code generation...',
6134
6915
  });
6135
-
6916
+
6136
6917
  // Keep preview tab active so user sees the "Generating..." overlay.
6137
6918
  // Console logs are still accessible via the Console tab button.
6138
6919
  getWidgetDetailsRef(generationWidgetId)?.setInlinePanelView('preview');
@@ -6141,22 +6922,52 @@ Requirements:
6141
6922
  console.log('Widget Creator: Starting code generation for widget:', generationWidgetId);
6142
6923
 
6143
6924
  try {
6144
- const generationWidget = getWidgetById(generationWidgetId);
6145
- const repoId = generationWidget
6146
- ? await ensureWidgetRepository(generationWidget)
6147
- : panelSnap.currentRepositoryId;
6925
+ const generationWidget =
6926
+ getWidgetById(generationWidgetId) ||
6927
+ (selectedWidget?.id === generationWidgetId ? selectedWidget : null);
6928
+ let repoId: string | null = null;
6929
+ try {
6930
+ repoId = generationWidget
6931
+ ? await ensureWidgetRepository(generationWidget, {
6932
+ forceRecreate: !!options.forceRecreateRepo,
6933
+ silent: true,
6934
+ })
6935
+ : panelSnap.currentRepositoryId;
6936
+ if (!repoId && generationWidget) {
6937
+ console.warn('[CodeGen] Repository missing — retrying create for widget', generationWidgetId.substring(0, 8));
6938
+ repoId = await ensureWidgetRepository(generationWidget, { forceRecreate: true, silent: true });
6939
+ }
6940
+ } catch (repoError) {
6941
+ if (repoError instanceof ResponseError && repoError.response?.status === 401) {
6942
+ const sessionError = 'Session expired. Sign in again, then retry.';
6943
+ patchCodegenUiState(generationWidgetId, {
6944
+ generateCodeStatus: '❌ Session expired.',
6945
+ lastGenerationFailed: true,
6946
+ lastGenerationError: sessionError,
6947
+ });
6948
+ showToast('error', 'Session expired', {
6949
+ description: sessionError,
6950
+ duration: 5000
6951
+ });
6952
+ pushCodegenFailureToChat(generationWidgetId, sessionError);
6953
+ return;
6954
+ }
6955
+ throw repoError;
6956
+ }
6148
6957
  console.log('Widget Creator: Repository resolved for code generation:', repoId);
6149
6958
 
6150
6959
  if (!repoId) {
6960
+ const missingRepoError = 'Unable to resolve repository for this widget.';
6151
6961
  patchCodegenUiState(generationWidgetId, {
6152
6962
  generateCodeStatus: '❌ Repository is required to generate code.',
6153
6963
  lastGenerationFailed: true,
6154
- lastGenerationError: 'Unable to resolve repository for this widget.',
6964
+ lastGenerationError: missingRepoError,
6155
6965
  });
6156
6966
  showToast('error', 'Missing repository', {
6157
- description: 'Unable to resolve repository for this widget.',
6967
+ description: missingRepoError,
6158
6968
  duration: 5000
6159
6969
  });
6970
+ pushCodegenFailureToChat(generationWidgetId, missingRepoError);
6160
6971
  return;
6161
6972
  }
6162
6973
 
@@ -6182,7 +6993,7 @@ Requirements:
6182
6993
  // Drop invalid entries (empty strings, non-data-URLs) so a failed capture
6183
6994
  // can never silently become a vision-less generation request.
6184
6995
  panelPromptImages = panelPromptImages.filter(
6185
- (url) => typeof url === 'string' && url.startsWith('data:image/') && url.length > 100,
6996
+ (url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
6186
6997
  );
6187
6998
  const chatAttachmentUrls = panelChat?.getAttachmentDataUrls?.() ?? [];
6188
6999
  const chatUploadIds = [
@@ -6190,9 +7001,12 @@ Requirements:
6190
7001
  ...(panelChat?.getAttachmentUploadIds?.() ?? []),
6191
7002
  ].filter((id, index, arr) => id && arr.indexOf(id) === index);
6192
7003
  if (chatAttachmentUrls.length > 0 && !(options.promptImagesOverride?.length)) {
6193
- panelPromptImages = chatAttachmentUrls.filter(
6194
- (url) => typeof url === 'string' && url.startsWith('data:image/') && url.length > 100,
7004
+ const fromChat = chatAttachmentUrls.filter(
7005
+ (url) => typeof url === 'string' && (url.startsWith('data:image/') || /^https?:\/\//i.test(url)) && url.length > 20,
6195
7006
  );
7007
+ if (fromChat.length > 0) {
7008
+ panelPromptImages = fromChat;
7009
+ }
6196
7010
  }
6197
7011
  let imagesToSend: string[] | undefined;
6198
7012
  let attachmentIdsToSend: string[] | undefined;
@@ -6250,6 +7064,18 @@ Requirements:
6250
7064
  });
6251
7065
  }
6252
7066
 
7067
+ const persistImageUrls = uniquePersistImageUrls([
7068
+ ...(imagesToSend || []),
7069
+ ...(panelPromptImages || []),
7070
+ ...(options.promptImagesOverride || []),
7071
+ ]);
7072
+ await ensureCodegenUserMessagePersisted(
7073
+ generationWidgetId,
7074
+ prompt,
7075
+ persistImageUrls,
7076
+ options.persistFileAttachments,
7077
+ );
7078
+
6253
7079
  // Clear unsent draft/attachments once payload is captured — prevents restore on widget return
6254
7080
  clearPromptDraftForWidget(generationWidgetId);
6255
7081
 
@@ -6405,6 +7231,11 @@ Requirements:
6405
7231
  hasGeneratedCode: true,
6406
7232
  ...(completedCommitSha ? { lastCommitId: completedCommitSha } : {}),
6407
7233
  });
7234
+ if (isFocusedPanel && completedCommitSha) {
7235
+ lastCommitId = completedCommitSha;
7236
+ hasGeneratedCode = true;
7237
+ }
7238
+ panelDisplayRevision += 1;
6408
7239
  console.log(
6409
7240
  '[Publish] Codegen completed — pendingPublishAfterCodegen=true',
6410
7241
  generationWidgetId.substring(0, 8),
@@ -6449,6 +7280,11 @@ Requirements:
6449
7280
  }
6450
7281
  })();
6451
7282
  }
7283
+ if (generationWidgetId) {
7284
+ console.log('[CompositionEditor] Reloading schemas after codegen', generationWidgetId.substring(0, 8));
7285
+ void loadWidgetSchemas(generationWidgetId, { force: true });
7286
+ }
7287
+
6452
7288
  const completedRepoId = resolveRepositoryIdForWidget(generationWidgetId);
6453
7289
  if (completedRepoId) {
6454
7290
  stampWidgetRepositoryId(generationWidgetId, completedRepoId);
@@ -6548,8 +7384,9 @@ Requirements:
6548
7384
  );
6549
7385
  setTimeout(async () => {
6550
7386
  await panelChat?.reloadConversationMessages?.();
6551
- }, 1500);
6552
- setTimeout(() => handleRefreshScreenshot(generationWidgetId), 3000);
7387
+ panelChat?.markWidgetHeadAtLatestCommit?.();
7388
+ }, 2500);
7389
+ setTimeout(() => handleRefreshScreenshot(generationWidgetId, { quiet: true }), 3000);
6553
7390
  } else {
6554
7391
  panelChat?.updateLatestCodegenStatusMessage?.(
6555
7392
  `Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
@@ -6621,6 +7458,7 @@ Requirements:
6621
7458
  });
6622
7459
  clearActiveOperationId(); // Clear persisted operationId on error
6623
7460
  showToast('error', 'Code generation error', { description: message, duration: 6000 });
7461
+ pushCodegenFailureToChat(generationWidgetId, message);
6624
7462
  } finally {
6625
7463
  const { [generationWidgetId]: _done, ...restFinally } = generatingWidgetIds;
6626
7464
  generatingWidgetIds = restFinally;
@@ -6678,7 +7516,11 @@ Requirements:
6678
7516
  commitSha?: string | null
6679
7517
  ): void {
6680
7518
  const jsPath = resolvePublishedJsPath(widgetId, version, artifactUrl);
6681
- const normalizedCommit = commitSha ? normalizeCommitSha(commitSha) : null;
7519
+ const prevSnap = getPanelState(widgetId);
7520
+ const normalizedCommit =
7521
+ normalizeCommitSha(commitSha)
7522
+ || normalizeCommitSha(prevSnap.lastCommitId)
7523
+ || normalizeCommitSha(loadCommitIdForWidget(widgetId));
6682
7524
 
6683
7525
  widgets = widgets.map((w) =>
6684
7526
  w.id === widgetId
@@ -6695,7 +7537,6 @@ Requirements:
6695
7537
  : w
6696
7538
  );
6697
7539
 
6698
- const prevSnap = getPanelState(widgetId);
6699
7540
  const optimisticHistoryEntry = normalizedCommit
6700
7541
  ? {
6701
7542
  version,
@@ -6726,6 +7567,9 @@ Requirements:
6726
7567
  saveCommitIdForWidget(widgetId, normalizedCommit);
6727
7568
  }
6728
7569
 
7570
+ console.log('[CompositionEditor] Reloading schemas after publish', widgetId.substring(0, 8));
7571
+ void loadWidgetSchemas(widgetId, { force: true });
7572
+
6729
7573
  if (selectedWidgetId === widgetId) {
6730
7574
  lastPublishedVersion = version;
6731
7575
  selectedWidgetJsPath = jsPath;
@@ -6920,6 +7764,16 @@ Requirements:
6920
7764
 
6921
7765
  // Restore session on mount with expiration check
6922
7766
  onMount(() => {
7767
+ const googleFontsKey =
7768
+ (typeof import.meta !== 'undefined' &&
7769
+ (import.meta.env?.PUBLIC_GOOGLE_FONTS_API_KEY || import.meta.env?.VITE_GOOGLE_FONTS_API_KEY)) ||
7770
+ '';
7771
+ if (googleFontsKey) {
7772
+ setGoogleFontsApiKey(String(googleFontsKey));
7773
+ console.log('[CreatorApp] Google Fonts API key set for FontIC');
7774
+ } else {
7775
+ console.warn('[CreatorApp] No PUBLIC_GOOGLE_FONTS_API_KEY / VITE_GOOGLE_FONTS_API_KEY — FontIC uses fallback catalog');
7776
+ }
6923
7777
  setWebsocketApiUrl(apiRoot ?? null);
6924
7778
  if (embedded) {
6925
7779
  syncSessionFromSiteAuth();
@@ -6965,6 +7819,7 @@ Requirements:
6965
7819
 
6966
7820
  // Cross-tab operation sync: listen for localStorage changes from other tabs
6967
7821
  window.addEventListener('storage', handleCrossTabStorageEvent);
7822
+ window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
6968
7823
 
6969
7824
  // Clean up cross-tab flags when tab is closed/refreshed
6970
7825
  window.addEventListener('beforeunload', () => {
@@ -7058,14 +7913,20 @@ Requirements:
7058
7913
  // Cross-tab cleanup: remove flags so other tabs don't stay blocked
7059
7914
  if (typeof window !== 'undefined') {
7060
7915
  window.removeEventListener('storage', handleCrossTabStorageEvent);
7916
+ window.removeEventListener('widgetic:session-extended', handleSiteSessionExtended);
7061
7917
  window.removeEventListener('click', handleCanvasDropdownOutsideClick);
7062
7918
  window.removeEventListener(HOST_WIDGET_RENAMED_EVENT, handleHostWidgetRenamedEvent);
7063
7919
  window.removeEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
7064
7920
  window.removeEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
7065
7921
  window.removeEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
7922
+ window.removeEventListener(HOST_TOGGLE_ADVANCED_EVENT, handleHostToggleAdvancedEvent);
7066
7923
  window.removeEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
7067
7924
  window.removeEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
7068
7925
  window.removeEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
7926
+ window.removeEventListener(HOST_SAVE_CANVAS_EVENT, handleHostSaveCanvasEvent);
7927
+ window.removeEventListener(HOST_SET_PRIMARY_WIDGET_EVENT, handleHostSetPrimaryWidgetEvent);
7928
+ window.removeEventListener(HOST_REQUEST_CANVAS_WIDGETS_EVENT, handleHostRequestCanvasWidgetsEvent);
7929
+ window.removeEventListener(HOST_MOVE_CANVAS_FOLDER_EVENT, handleHostMoveCanvasFolderEvent);
7069
7930
  for (const wId of Object.keys(generatingWidgetIds)) {
7070
7931
  setCrossTabGenerating(false, wId);
7071
7932
  }
@@ -7091,11 +7952,18 @@ Requirements:
7091
7952
  window.addEventListener(HOST_REQUEST_PUBLISH_EVENT, handleHostRequestPublishEvent);
7092
7953
  window.addEventListener(HOST_TOGGLE_DEBUG_EVENT, handleHostToggleDebugEvent);
7093
7954
  window.addEventListener(HOST_TOGGLE_WIREFRAME_EVENT, handleHostToggleWireframeEvent);
7955
+ window.addEventListener(HOST_TOGGLE_ADVANCED_EVENT, handleHostToggleAdvancedEvent);
7094
7956
  window.addEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
7095
7957
  window.addEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
7096
7958
  window.addEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
7959
+ window.addEventListener(HOST_SAVE_CANVAS_EVENT, handleHostSaveCanvasEvent);
7960
+ window.addEventListener(HOST_SET_PRIMARY_WIDGET_EVENT, handleHostSetPrimaryWidgetEvent);
7961
+ window.addEventListener(HOST_REQUEST_CANVAS_WIDGETS_EVENT, handleHostRequestCanvasWidgetsEvent);
7962
+ window.addEventListener(HOST_MOVE_CANVAS_FOLDER_EVENT, handleHostMoveCanvasFolderEvent);
7097
7963
  emitCreatorDebugChanged();
7098
7964
  emitCreatorWireframeChanged();
7965
+ emitCreatorAdvancedChanged();
7966
+ emitCreatorSaveState(false);
7099
7967
  });
7100
7968
 
7101
7969
  // Capture-phase Escape: close lightbox before Canvas can deselect → close WidgetDetails
@@ -7133,8 +8001,6 @@ Requirements:
7133
8001
  if (file) {
7134
8002
  const base64 = await processImageFile(file);
7135
8003
  if (base64) {
7136
- promptImages = [...promptImages, base64];
7137
- savePromptImagesToSession();
7138
8004
  chatMethods?.addImageAttachment?.(base64, file.name || 'pasted-image.png');
7139
8005
  showToast('success', 'Image pasted to prompt', { duration: 2000 });
7140
8006
  }
@@ -7488,7 +8354,7 @@ Requirements:
7488
8354
  compositionSchemasLoading,
7489
8355
  compositionSchemasError,
7490
8356
  schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
7491
- chatContainerHeight: focusedSnap?.chatContainerHeight ?? DEFAULT_CHAT_CONTAINER_HEIGHT,
8357
+ chatContainerHeight: focusedSnap?.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
7492
8358
  panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
7493
8359
  };
7494
8360
  }
@@ -7541,7 +8407,7 @@ Requirements:
7541
8407
  compositionSchemasLoading: false,
7542
8408
  compositionSchemasError: null,
7543
8409
  schemaLoadStartedAt: null,
7544
- chatContainerHeight: DEFAULT_CHAT_CONTAINER_HEIGHT,
8410
+ chatContainerHeight: 550,
7545
8411
  panelPreviewCompileError: null,
7546
8412
  };
7547
8413
  }
@@ -7567,16 +8433,18 @@ Requirements:
7567
8433
  const snapPublishReady =
7568
8434
  snap.publishHistoryResolved && snap.publishHistoryResolvedForWidgetId === widgetId;
7569
8435
  const livePublishMatchesPanel = live.publishHistoryResolvedForWidgetId === widgetId;
7570
- const resolvedHead = resolvePanelHeadCommitId(widgetId, snap, live);
8436
+ const resolvedHead = isDraftConvertId(widgetId) ? null : resolvePanelHeadCommitId(widgetId, snap, live);
7571
8437
  return {
7572
8438
  ...snap,
7573
8439
  ...live,
7574
- // Prefer HEAD ahead of live publish SHA (localStorage may be newer than stale publish globals).
7575
- lastCommitId: resolvedHead,
7576
- // Never let focused globals wipe a true flag already on the panel snapshot / storage.
7577
- hasGeneratedCode: !!(live.hasGeneratedCode || snap.hasGeneratedCode || resolvedHead),
8440
+ lastCommitId: isDraftConvertId(widgetId) ? null : resolvedHead,
8441
+ hasGeneratedCode: isDraftConvertId(widgetId)
8442
+ ? false
8443
+ : !!(live.hasGeneratedCode || snap.hasGeneratedCode || resolvedHead),
7578
8444
  pendingPublishAfterCodegen: !!(snap.pendingPublishAfterCodegen || live.pendingPublishAfterCodegen),
7579
- lastPublishedVersion: live.lastPublishedVersion ?? snap.lastPublishedVersion,
8445
+ lastPublishedVersion: isDraftConvertId(widgetId)
8446
+ ? null
8447
+ : (live.lastPublishedVersion ?? snap.lastPublishedVersion),
7580
8448
  currentRepositoryId: live.currentRepositoryId ?? snap.currentRepositoryId,
7581
8449
  selectedWidgetJsPath: live.selectedWidgetJsPath ?? snap.selectedWidgetJsPath,
7582
8450
  // Once publish history loaded into this panel's snapshot, prefer it over stale globals.
@@ -7595,7 +8463,7 @@ Requirements:
7595
8463
  : livePublishMatchesPanel
7596
8464
  ? live.publishHistoryResolvedForWidgetId
7597
8465
  : snap.publishHistoryResolvedForWidgetId,
7598
- chatContainerHeight: snap.chatContainerHeight ?? DEFAULT_CHAT_CONTAINER_HEIGHT,
8466
+ chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
7599
8467
  compositionDesignSchema: resolvedDesignSchema,
7600
8468
  compositionContentSchema: resolvedContentSchema,
7601
8469
  compositionSchemasLoading: schemaLoading,
@@ -7613,7 +8481,7 @@ Requirements:
7613
8481
  ...snap,
7614
8482
  lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
7615
8483
  hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
7616
- chatContainerHeight: snap.chatContainerHeight ?? DEFAULT_CHAT_CONTAINER_HEIGHT,
8484
+ chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
7617
8485
  compositionDesignSchema: resolvedDesignSchema,
7618
8486
  compositionContentSchema: resolvedContentSchema,
7619
8487
  compositionSchemasError: resolvedSchemaError,
@@ -7903,6 +8771,22 @@ Requirements:
7903
8771
  selectedWidget = widget;
7904
8772
  saveSelectedWidgetId(widgetId);
7905
8773
  notifyHostWidgetName(widget);
8774
+ if (isDraftConvertId(widgetId)) {
8775
+ lastPublishedVersion = null;
8776
+ lastCommitId = null;
8777
+ hasGeneratedCode = false;
8778
+ publishHistory = [];
8779
+ publishHistoryResolved = true;
8780
+ publishHistoryResolvedForWidgetId = widgetId;
8781
+ widgetDetailsStep = 'create';
8782
+ patchPanelSnapshot(widgetId, {
8783
+ lastPublishedVersion: null,
8784
+ lastCommitId: null,
8785
+ hasGeneratedCode: false,
8786
+ publishHistory: [],
8787
+ widgetDetailsStep: 'create',
8788
+ });
8789
+ }
7906
8790
  if (widget?.id && canvasRef) {
7907
8791
  canvasRef.selectWidgetShape(widget.id);
7908
8792
  }
@@ -7920,8 +8804,20 @@ Requirements:
7920
8804
  }
7921
8805
 
7922
8806
  function positionOpenDetailsPanel(widgetId: string): void {
7923
- const rect = canvasRef?.getWidgetShapeScreenRect?.(widgetId) ?? null;
7924
- getWidgetDetailsRef(widgetId)?.positionBesideShape?.(rect);
8807
+ const apply = () => {
8808
+ let rect = canvasRef?.getWidgetShapeScreenRect?.(widgetId) ?? null;
8809
+ if (!rect) {
8810
+ const objectId = draftConvertObjectIds.get(widgetId);
8811
+ if (objectId && canvasRef?.getObjectScreenRect) {
8812
+ rect = canvasRef.getObjectScreenRect(objectId);
8813
+ }
8814
+ }
8815
+ getWidgetDetailsRef(widgetId)?.positionBesideShape?.(rect);
8816
+ };
8817
+ apply();
8818
+ if (typeof requestAnimationFrame === 'function') {
8819
+ requestAnimationFrame(apply);
8820
+ }
7925
8821
  }
7926
8822
 
7927
8823
  async function openWidgetDetailsPanel(widgetId: string): Promise<void> {
@@ -7952,6 +8848,14 @@ Requirements:
7952
8848
  openWidgetPanelIds = [...openWidgetPanelIds, widgetId];
7953
8849
  }
7954
8850
  focusWidgetPanel(widgetId);
8851
+ if (isDraftConvertId(widgetId)) {
8852
+ detailsSessionWidgetId = widgetId;
8853
+ }
8854
+ const alreadyMounted = getWidgetDetailsRef(widgetId);
8855
+ if (alreadyMounted && !alreadyMounted.getShowWidgetDetails?.()) {
8856
+ alreadyMounted.openWidgetDetails();
8857
+ positionOpenDetailsPanel(widgetId);
8858
+ }
7955
8859
  if (openGen !== widgetDetailsOpenGen || widgetDetailsOpenTargetId !== widgetId) {
7956
8860
  console.log('[CreatorApp] Skip stale Widget Details open:', widgetId?.substring(0, 8));
7957
8861
  return;
@@ -8048,6 +8952,8 @@ Requirements:
8048
8952
  openWidgetPanelIds = replaced.includes(toId) ? replaced : [...replaced, toId];
8049
8953
  if (focusedPanelWidgetId === fromId) focusedPanelWidgetId = toId;
8050
8954
  if (selectedWidgetId === fromId) selectedWidgetId = toId;
8955
+ if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
8956
+ if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
8051
8957
  showWidgetDetails = openWidgetPanelIds.length > 0;
8052
8958
  console.log(
8053
8959
  '[CreatorApp] Migrated Widget Details panel',
@@ -8074,6 +8980,9 @@ Requirements:
8074
8980
  if (!options.fromShapeSwitch && widgetDetailsOpenTargetId === widgetId) {
8075
8981
  widgetDetailsOpenTargetId = null;
8076
8982
  }
8983
+ if (detailsSessionWidgetId === widgetId) {
8984
+ detailsSessionWidgetId = null;
8985
+ }
8077
8986
  getWidgetDetailsRef(widgetId)?.closeWidgetDetails({ silent: true });
8078
8987
  openWidgetPanelIds = openWidgetPanelIds.filter((id) => id !== widgetId);
8079
8988
  const { [widgetId]: _z, ...restZ } = panelZIndexByWidgetId;
@@ -8106,6 +9015,8 @@ Requirements:
8106
9015
  panelInstanceKeyByWidgetId = {};
8107
9016
  focusedPanelWidgetId = null;
8108
9017
  panelZIndexByWidgetId = {};
9018
+ detailsSessionWidgetId = null;
9019
+ widgetDetailsOpenTargetId = null;
8109
9020
  showWidgetDetails = false;
8110
9021
  }
8111
9022
 
@@ -8378,11 +9289,10 @@ Requirements:
8378
9289
  // If the token expired during generation, we delayed logout to avoid disrupting the operation.
8379
9290
  // Now that the operation is done, proceed with logout.
8380
9291
  $: if (pendingTokenExpiryLogout && !isGeneratingCode && Object.keys(generatingWidgetIds).length === 0) {
8381
- console.log('WidgetCreator: Deferred logout — active operations completed, proceeding with logout');
9292
+ console.log('WidgetCreator: Token expired after operations — keeping canvas, prompting re-auth');
8382
9293
  pendingTokenExpiryLogout = false;
8383
9294
  tokenExpiryWarningShown = false;
8384
- handleLogout();
8385
- authStatus = 'Token expired. Please login again.';
9295
+ authStatus = 'Session expired. Sign in again to keep saving.';
8386
9296
  }
8387
9297
 
8388
9298
 
@@ -8464,35 +9374,56 @@ Requirements:
8464
9374
  return [];
8465
9375
  }
8466
9376
 
9377
+ function unwrapContentMediaValue(raw: unknown): unknown {
9378
+ if (raw && typeof raw === 'object' && 'url' in (raw as Record<string, unknown>)) {
9379
+ return (raw as { url?: string }).url ?? '';
9380
+ }
9381
+ return raw;
9382
+ }
9383
+
9384
+ function pickLiveContentValue(...candidates: unknown[]): unknown {
9385
+ for (const candidate of candidates) {
9386
+ const unwrapped = unwrapContentMediaValue(candidate);
9387
+ if (unwrapped === undefined || unwrapped === null || unwrapped === '') continue;
9388
+ return unwrapped;
9389
+ }
9390
+ return '';
9391
+ }
9392
+
8467
9393
  /** Flatten schema-shaped content items to runtime values for preview postMessage. */
8468
9394
  function flattenContentItemsForPreview(items: any[]): any[] {
8469
9395
  return items.map((item) => {
8470
9396
  if (!item || typeof item !== 'object') return item;
8471
9397
  const flat: Record<string, unknown> = { id: item.id, title: item.title };
8472
- if (item.imageUrl) flat.imageUrl = item.imageUrl;
8473
- if (item.description) flat.description = item.description;
9398
+ for (const [key, value] of Object.entries(item)) {
9399
+ if (key === 'properties' || key === 'inputController' || key === 'id' || key === 'title') continue;
9400
+ const unwrapped = unwrapContentMediaValue(value);
9401
+ if (unwrapped === undefined) continue;
9402
+ flat[key] = unwrapped;
9403
+ }
8474
9404
  const props = item.properties;
8475
9405
  if (Array.isArray(props)) {
8476
9406
  for (const prop of props) {
8477
9407
  const name = prop?.name || prop?.id;
8478
9408
  if (!name) continue;
8479
- const raw = prop.value !== undefined ? prop.value : prop.defaultValue;
8480
- if (raw === undefined || flat[name] !== undefined) continue;
8481
- if (raw && typeof raw === 'object' && 'url' in raw) {
8482
- flat[name] = (raw as { url?: string }).url ?? '';
8483
- } else {
8484
- flat[name] = raw;
8485
- }
9409
+ if (flat[name] !== undefined && flat[name] !== '') continue;
9410
+ const picked = pickLiveContentValue(prop.value, prop.defaultValue);
9411
+ if (picked !== '') flat[name] = picked;
8486
9412
  }
8487
9413
  }
8488
- for (const [key, value] of Object.entries(item)) {
8489
- if (key === 'properties' || key === 'inputController') continue;
8490
- if (flat[key] === undefined && value !== undefined) flat[key] = value;
8491
- }
8492
9414
  return flat;
8493
9415
  });
8494
9416
  }
8495
9417
 
9418
+ function contentItemsToEditorValueMap(items: any[]): Record<string, Record<string, unknown>> {
9419
+ const map: Record<string, Record<string, unknown>> = {};
9420
+ for (const item of flattenContentItemsForPreview(items || [])) {
9421
+ if (!item?.id) continue;
9422
+ map[String(item.id)] = item as Record<string, unknown>;
9423
+ }
9424
+ return map;
9425
+ }
9426
+
8496
9427
  function contentItemHasProperties(item: any): boolean {
8497
9428
  return Array.isArray(item?.properties) && item.properties.length > 0;
8498
9429
  }
@@ -8500,22 +9431,28 @@ Requirements:
8500
9431
  /** New items from the editor arrive flat ({id, caption, image}) — reattach schema fields so the form stays editable. */
8501
9432
  function attachSchemaProperties(item: any, template: any): any {
8502
9433
  if (!item || typeof item !== 'object') return item;
8503
- if (contentItemHasProperties(item)) return item;
8504
- if (!contentItemHasProperties(template)) return item;
8505
- const clonedTemplate = JSON.parse(JSON.stringify(template));
8506
- const nextProperties = clonedTemplate.properties.map((prop: any) => {
9434
+ const templateHasProps = contentItemHasProperties(template);
9435
+ if (!contentItemHasProperties(item) && !templateHasProps) return item;
9436
+ const sourceProperties = contentItemHasProperties(item)
9437
+ ? item.properties
9438
+ : JSON.parse(JSON.stringify(template.properties));
9439
+ const nextProperties = sourceProperties.map((prop: any) => {
8507
9440
  const key = prop.id || prop.name;
8508
- const incoming = key != null ? item[key] : undefined;
9441
+ const hasOwn = key != null && Object.prototype.hasOwnProperty.call(item, key);
9442
+ const incoming = hasOwn ? item[key] : undefined;
9443
+ const picked = hasOwn
9444
+ ? (unwrapContentMediaValue(incoming) ?? '')
9445
+ : pickLiveContentValue(prop.value, prop.defaultValue);
8509
9446
  return {
8510
9447
  ...prop,
8511
- value: incoming !== undefined ? incoming : (prop.defaultValue ?? prop.value)
9448
+ value: picked
8512
9449
  };
8513
9450
  });
8514
9451
  return {
8515
- ...clonedTemplate,
9452
+ ...(templateHasProps ? JSON.parse(JSON.stringify(template)) : {}),
8516
9453
  ...item,
8517
9454
  id: item.id,
8518
- title: item.title || clonedTemplate.title,
9455
+ title: item.title || template?.title,
8519
9456
  properties: nextProperties
8520
9457
  };
8521
9458
  }
@@ -8683,20 +9620,11 @@ Requirements:
8683
9620
  const effectiveCanvasId = getEffectiveCanvasId();
8684
9621
  const panelIsPublishing = isPublishing && publishingWidgetId === widgetId;
8685
9622
  const anotherPanelIsPublishing = isPublishing && publishingWidgetId !== null && publishingWidgetId !== widgetId;
8686
- const stuckGlobalPublishing = isPublishing && !publishingWidgetId;
8687
9623
  const nextVersion = (ps.lastPublishedVersion ?? 0) + 1;
8688
9624
 
8689
9625
  if (panelIsPublishing) {
8690
9626
  return { disabled: true, label: 'Publishing...', reason: 'Publishing in progress…', showDisabledHint: false };
8691
9627
  }
8692
- if (stuckGlobalPublishing) {
8693
- return {
8694
- disabled: true,
8695
- label: getPublishButtonLabelFallback(ps),
8696
- reason: 'Publish state is busy — try again in a moment.',
8697
- showDisabledHint: true,
8698
- };
8699
- }
8700
9628
  if (anotherPanelIsPublishing) {
8701
9629
  return {
8702
9630
  disabled: true,
@@ -8705,7 +9633,7 @@ Requirements:
8705
9633
  showDisabledHint: true,
8706
9634
  };
8707
9635
  }
8708
- if (isPublishingInOtherTab) {
9636
+ if (checkCrossTabPublishing(widgetId)) {
8709
9637
  return {
8710
9638
  disabled: true,
8711
9639
  label: getPublishButtonLabelFallback(ps),
@@ -8764,8 +9692,10 @@ Requirements:
8764
9692
  getLivePublishedCommitShaForPanel(widgetId)
8765
9693
  ?? getWidgetLastPublishCommitSha(widgetId);
8766
9694
  const pendingPublish = !!ps.pendingPublishAfterCodegen;
9695
+ const unpublishedAhead = hasUnpublishedChangesForPanel(widgetId)
9696
+ || !!(ps.lastCommitId && liveSha && !commitsMatch(ps.lastCommitId, liveSha));
8767
9697
 
8768
- // Known up-to-date: HEAD matches live (pending flag ignored when proven equal).
9698
+ // Known up-to-date: HEAD matches live. Must win over leftover pendingPublish flags.
8769
9699
  if (headSha && liveSha && commitsMatch(headSha, liveSha)) {
8770
9700
  return {
8771
9701
  disabled: true,
@@ -8775,10 +9705,24 @@ Requirements:
8775
9705
  };
8776
9706
  }
8777
9707
 
8778
- // After a live version exists, enable vN+1 only when codegen just finished
8779
- // or HEAD is proven different from the live SHA. Do not enable just because
8780
- // live SHA is still hydrating (that looked like "always publish v2").
8781
- if (pendingPublish || (headSha && liveSha && !commitsMatch(headSha, liveSha))) {
9708
+ if (pendingPublish || unpublishedAhead) {
9709
+ console.log('[Publish] Gate: unpublished HEAD — enable', widgetId.substring(0, 8), {
9710
+ pendingPublish,
9711
+ unpublishedAhead,
9712
+ headSha: headSha?.substring(0, 8),
9713
+ displayHead: ps.lastCommitId?.substring(0, 8),
9714
+ liveSha: liveSha?.substring(0, 8),
9715
+ });
9716
+ return {
9717
+ disabled: false,
9718
+ label: `Publish new v${nextVersion}`,
9719
+ reason: null,
9720
+ showDisabledHint: false,
9721
+ };
9722
+ }
9723
+
9724
+ // After a live version exists, enable vN+1 when HEAD is proven different.
9725
+ if (headSha && liveSha && !commitsMatch(headSha, liveSha)) {
8782
9726
  return {
8783
9727
  disabled: false,
8784
9728
  label: `Publish new v${nextVersion}`,
@@ -8920,7 +9864,7 @@ $: if ($widgetPublishStatus.timestamp && $widgetPublishStatus.widgetId) {
8920
9864
  }
8921
9865
  if (publishWidgetId) {
8922
9866
  console.log('[Publish] Refreshing canvas widget screenshot after successful publish');
8923
- setTimeout(() => handleRefreshScreenshot(publishWidgetId), 2000);
9867
+ setTimeout(() => handleRefreshScreenshot(publishWidgetId, { quiet: true }), 2000);
8924
9868
  }
8925
9869
  if (embedded && onPublishComplete && publishWidgetId) {
8926
9870
  onPublishComplete({
@@ -9316,6 +10260,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9316
10260
  hasValidToken = false;
9317
10261
  inlinePanelView = null;
9318
10262
  selectedWidget = null;
10263
+ if (typeof window !== 'undefined') {
10264
+ window.location.href = '/login';
10265
+ }
9319
10266
  }
9320
10267
 
9321
10268
  // decodeJWT imported from ./pageHelpers.ts
@@ -9332,11 +10279,12 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9332
10279
 
9333
10280
  // if the decoded token has an expiration field, update the token status
9334
10281
  if (decoded && decoded.exp) {
9335
- // calculate the token time left
9336
10282
  const currentTime = Math.floor(Date.now() / 1000);
9337
- tokenTimeLeft = decoded.exp - currentTime;
10283
+ const expUnix = decoded.exp;
10284
+ tokenTimeLeft = expUnix - currentTime;
9338
10285
  tokenExpired = tokenTimeLeft <= 0;
9339
- hasValidToken = !tokenExpired;
10286
+ // API calls must follow the real JWT, never the sessionBannerTest clock.
10287
+ hasValidToken = decoded.exp - currentTime > 0;
9340
10288
 
9341
10289
  // log the token expiration status
9342
10290
  if (tokenExpired) {
@@ -9384,33 +10332,26 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9384
10332
  // If there are active operations (generating, loading preview), defer logout
9385
10333
  // and show a warning instead of abruptly cutting off the operation
9386
10334
  function checkTokenExpiration() {
10335
+ updateTokenStatus();
9387
10336
  if (currentUserToken) {
9388
- if (isTokenExpired(currentUserToken)) {
9389
- // Check if there are active operations that would be disrupted by logout
10337
+ if (tokenExpired || tokenTimeLeft <= 0) {
9390
10338
  const hasActiveOperations = isGeneratingCode || (showPreviewModal && previewLoading);
9391
10339
 
9392
10340
  if (hasActiveOperations) {
9393
- // Don't logout yet — defer until operations complete
9394
10341
  pendingTokenExpiryLogout = true;
9395
-
9396
10342
  if (!tokenExpiryWarningShown) {
9397
10343
  tokenExpiryWarningShown = true;
9398
- console.warn('WidgetCreator.checkTokenExpiration: Token expired but active operations in progress — deferring logout');
9399
- authStatus = 'Token expired — finishing current operation before logout...';
9400
- showToast('warning', 'Session expiring', {
9401
- description: 'Your login token has expired. You will be logged out after the current operation completes.',
9402
- duration: 8000
9403
- });
10344
+ console.warn('WidgetCreator.checkTokenExpiration: Token expired but active operations in progress — keeping the workspace open');
10345
+ authStatus = 'Session expired — finish this operation, then sign in again to save.';
9404
10346
  }
9405
10347
  return false;
9406
10348
  }
9407
10349
 
9408
- // No active operations (or deferred logout is now safe) — proceed with logout
9409
- console.log('WidgetCreator.checkTokenExpiration: Token expired, logging out automatically');
9410
- pendingTokenExpiryLogout = false;
9411
- tokenExpiryWarningShown = false;
9412
- handleLogout();
9413
- authStatus = 'Token expired. Please login again.';
10350
+ if (!tokenExpiryWarningShown) {
10351
+ tokenExpiryWarningShown = true;
10352
+ console.log('WidgetCreator.checkTokenExpiration: Token expired — waiting for site session handler');
10353
+ authStatus = 'Session expired.';
10354
+ }
9414
10355
  return false;
9415
10356
  }
9416
10357
  }
@@ -9582,6 +10523,24 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9582
10523
  });
9583
10524
  return;
9584
10525
  }
10526
+ const lockedPanelVisible = !!(
10527
+ detailsSessionWidgetId
10528
+ && openWidgetPanelIds.includes(detailsSessionWidgetId)
10529
+ && (getWidgetDetailsRef(detailsSessionWidgetId)?.getShowWidgetDetails?.() ?? false)
10530
+ );
10531
+ if (
10532
+ detailsSessionWidgetId
10533
+ && detailsSessionWidgetId !== widgetId
10534
+ && lockedPanelVisible
10535
+ ) {
10536
+ console.log('[WidgetCreator] Skip widget-shape click — convert session locked to', detailsSessionWidgetId.substring(0, 8));
10537
+ return;
10538
+ }
10539
+ if (lockedPanelVisible && isConvertOrDraftPanelOpen() && !isDraftConvertId(widgetId)) {
10540
+ console.log('[WidgetCreator] Skip widget-shape click — convert/draft panel is open:', widgetId.substring(0, 8));
10541
+ return;
10542
+ }
10543
+
9585
10544
  console.log('[WidgetCreator] Widget Image clicked, opening widget details panel:', widgetId);
9586
10545
 
9587
10546
  // Explicit user intent — cancel any in-flight deep-link auto-open and allow this panel.
@@ -9595,8 +10554,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9595
10554
  }
9596
10555
  }
9597
10556
 
9598
- // Keep address-bar widget_id aligned with the shape (prod embed props are frozen at mount).
9599
- if (typeof window !== 'undefined' && embedded) {
10557
+ // Keep address-bar widget_id on the canvas primary (duplicate/select must not steal it).
10558
+ if (typeof window !== 'undefined' && embedded && canvasPrimaryWidgetId && widgetId === canvasPrimaryWidgetId) {
9600
10559
  try {
9601
10560
  const url = new URL(window.location.href);
9602
10561
  if (url.searchParams.get('widget_id') !== widgetId) {
@@ -9646,7 +10605,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9646
10605
  id: sourceWidgetId,
9647
10606
  duplicateWidgetRequest: { canvas_id: canvasId } as any
9648
10607
  });
9649
- const newWidget = response?.data;
10608
+ let newWidget = response?.data;
9650
10609
 
9651
10610
  if (!newWidget?.id) {
9652
10611
  console.error('[WidgetCreator] duplicateWidget returned no data', response);
@@ -9672,37 +10631,94 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9672
10631
  }
9673
10632
 
9674
10633
  // Re-link the cloned canvas shape to the new widget (also clears _isDuplicating on clone)
9675
- canvasRef?.linkObjectToWidget(clonedObject, newWidget.id, newWidget.name ?? null);
10634
+ if (clonedObject) {
10635
+ canvasRef?.linkObjectToWidget(clonedObject, newWidget.id, newWidget.name ?? null);
10636
+ } else if (canvasRef?.addWidgetShape) {
10637
+ canvasRef.addWidgetShape(newWidget.id, newWidget.name, 0, 1, null);
10638
+ }
9676
10639
  setTimeout(() => {
9677
10640
  const json = canvasRef?.saveCanvas();
9678
10641
  if (json) handleCanvasAutoSave(json);
9679
10642
  }, 500);
9680
10643
 
9681
- // Add to beginning of list (newest first) and auto-select
9682
10644
  widgets = [{ ...newWidget }, ...widgets];
9683
10645
 
9684
- // Set flag BEFORE selectWidget so loadWidget skips the build-from-repo attempt
9685
- // (the fork isn't ready yet — we handle it below after a delay)
9686
- pendingDuplicateBuildWidgetId = newWidget.id;
9687
- selectWidget(newWidget);
10646
+ const sourceWidget = getWidgetById(sourceWidgetId);
10647
+ const sourceJsPath =
10648
+ ((sourceWidget as { jsPath?: string | null; js_path?: string | null } | null)?.jsPath
10649
+ ?? (sourceWidget as { js_path?: string | null } | null)?.js_path)
10650
+ ?? null;
10651
+ const duplicateJsPath =
10652
+ sourceJsPath
10653
+ ?? ((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
10654
+ ?? (newWidget as { js_path?: string | null }).js_path);
10655
+ if (sourceWidget && duplicateJsPath) {
10656
+ Object.assign(newWidget, {
10657
+ jsPath: duplicateJsPath,
10658
+ js_path: duplicateJsPath,
10659
+ version: (newWidget as { version?: number }).version ?? (sourceWidget as { version?: number }).version,
10660
+ isDraft: (newWidget as { isDraft?: boolean }).isDraft
10661
+ ?? (sourceWidget as { isDraft?: boolean; is_draft?: boolean }).isDraft
10662
+ ?? (sourceWidget as { is_draft?: boolean }).is_draft,
10663
+ lastCpgCommitSha:
10664
+ (newWidget as { lastCpgCommitSha?: string | null }).lastCpgCommitSha
10665
+ ?? (sourceWidget as { lastCpgCommitSha?: string | null }).lastCpgCommitSha,
10666
+ lastPublishCommitSha:
10667
+ (newWidget as { lastPublishCommitSha?: string | null }).lastPublishCommitSha
10668
+ ?? (sourceWidget as { lastPublishCommitSha?: string | null; last_publish_commit_sha?: string | null }).lastPublishCommitSha
10669
+ ?? (sourceWidget as { last_publish_commit_sha?: string | null }).last_publish_commit_sha,
10670
+ });
10671
+ widgets = widgets.map((w) => (w.id === newWidget.id ? { ...newWidget } : w));
10672
+ }
10673
+
10674
+ const sourceCommit = loadCommitIdForWidget(sourceWidgetId);
10675
+ if (sourceCommit) {
10676
+ saveCommitIdForWidget(newWidget.id, sourceCommit);
10677
+ }
10678
+
10679
+ const sourceHasCode =
10680
+ !!normalizeRepositoryId(newWidget)
10681
+ || !!((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
10682
+ ?? (newWidget as { js_path?: string | null }).js_path)
10683
+ || resolvePublishedVersionFromWidget(newWidget) !== null
10684
+ || !!sourceCommit;
10685
+
10686
+ if (!canvasPrimaryWidgetId) {
10687
+ canvasPrimaryWidgetId = sourceWidgetId;
10688
+ }
10689
+
10690
+ // Duplicate stays a sibling on the canvas — do not make it the primary widget.
10691
+ await selectWidget(newWidget, { skipPlaceShape: true });
10692
+ notifyHostWidgetName(getWidgetById(canvasPrimaryWidgetId) ?? getWidgetById(sourceWidgetId));
9688
10693
 
9689
- const newRepoId = normalizeRepositoryId(newWidget);
9690
10694
  showToast('success', 'Widget duplicated', {
9691
- description: `"${newWidget.name}" created — compiling code…`
10695
+ description: sourceHasCode
10696
+ ? `"${newWidget.name}" created with existing code`
10697
+ : `"${newWidget.name}" created`
9692
10698
  });
9693
10699
 
9694
- // Re-fetch widget from API to ensure repo_id and other fields are current
9695
10700
  setTimeout(() => handleWidgetDataChanged(newWidget.id), 1500);
9696
10701
 
9697
- // Wait for GitLab fork to complete, then compile from repo
9698
- // GitLab forks are async — files need time to be copied
10702
+ if (sourceHasCode) {
10703
+ await openWidgetDetailsPanel(newWidget.id);
10704
+ await tick();
10705
+ getWidgetDetailsRef(newWidget.id)?.startPreviewLoadingPublic();
10706
+ try {
10707
+ await getWidgetDetailsRef(newWidget.id)?.retryPreviewIfEmpty?.();
10708
+ } catch (previewErr) {
10709
+ console.warn('[WidgetCreator] Duplicate preview fallback failed:', previewErr);
10710
+ }
10711
+ return;
10712
+ }
10713
+
10714
+ pendingDuplicateBuildWidgetId = newWidget.id;
9699
10715
  setTimeout(async () => {
9700
10716
  if (selectedWidgetId !== newWidget.id) return;
9701
10717
  pendingDuplicateBuildWidgetId = null;
9702
10718
  try {
9703
10719
  await handleWidgetDataChanged(newWidget.id);
9704
10720
  const freshWidget = getWidgetById(newWidget.id);
9705
- let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : newRepoId;
10721
+ let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(newWidget);
9706
10722
 
9707
10723
  if (!repoId) {
9708
10724
  console.log('[WidgetCreator] Duplicate has no repo after re-fetch, creating one...');
@@ -9736,7 +10752,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9736
10752
  /**
9737
10753
  * Called when a widget shape is about to be deleted from canvas.
9738
10754
  * Step 1: Confirm removing the shape from canvas.
9739
- * Step 2: Ask if user also wants to delete the widget from DB.
10755
+ * Step 2 (ASK_DELETE_FROM_DB_CONFIRM): Ask if user also wants to delete the widget from DB.
10756
+ * MVP: first confirm also deletes the associated widget from the database.
9740
10757
  * Returns true to proceed with canvas shape deletion, false to cancel.
9741
10758
  */
9742
10759
  async function handleWidgetShapeDeleting(widgetId: string): Promise<boolean> {
@@ -9749,7 +10766,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9749
10766
 
9750
10767
  if (!result.proceed) return false;
9751
10768
 
9752
- showToast('info', 'Shape removed from canvas', { description: `Widget "${widgetName}" — check dialog for DB deletion` });
10769
+ closeWidgetDetailsPanel(widgetId);
10770
+
10771
+ if (result.deleteFromDb) {
10772
+ await deleteWidgetFromDb(widgetId);
10773
+ } else {
10774
+ showToast('info', 'Shape removed from canvas', { description: `Widget "${widgetName}" kept in database` });
10775
+ }
9753
10776
  return true;
9754
10777
  }
9755
10778
 
@@ -9772,7 +10795,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9772
10795
  if (selectedWidget?.id === widgetId) {
9773
10796
  selectedWidget = { ...selectedWidget, name: newName };
9774
10797
  }
9775
- onWidgetRenamed?.({ widgetId, name: newName });
10798
+ notifyHostWidgetName({ id: widgetId, name: newName });
10799
+ emitCanvasWidgetsList('widget-renamed');
9776
10800
  showToast('success', `Widget renamed to "${newName}"`);
9777
10801
  } catch (error) {
9778
10802
  console.error('[WidgetCreator] Failed to rename widget:', error);
@@ -9819,14 +10843,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
9819
10843
  }
9820
10844
  await syncWidgetsWithCanvas(widgetIdsOnCanvas);
9821
10845
  await maybeAutoRestoreWidgetShapes();
9822
- const primaryId =
9823
- canvasPrimaryWidgetId ||
9824
- canvasRef.getPrimaryWidgetId?.() ||
9825
- selectedWidgetId;
9826
- if (primaryId && canvasRef) {
10846
+ const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
10847
+ if (primaryId && canvasRef && !isConvertOrDraftPanelOpen()) {
9827
10848
  canvasRef.setPrimaryWidgetShape?.(primaryId);
9828
10849
  const found = canvasRef.selectWidgetShape(primaryId);
9829
10850
  console.log('[CreatorApp] Primary widget shape after canvas load:', primaryId, found);
10851
+ } else if (isConvertOrDraftPanelOpen()) {
10852
+ console.log('[CreatorApp] Skip primary shape select — convert/draft panel is open');
9830
10853
  }
9831
10854
  }
9832
10855
 
@@ -10017,6 +11040,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10017
11040
  const widgetIdsOnCanvas = extractWidgetIdsFromCanvasContent(parsed);
10018
11041
  await canvasRef.loadCanvas(saved);
10019
11042
  markCanvasStateLoaded();
11043
+ canvasApiLoadError = null;
10020
11044
  isLoadCooldownActive = true;
10021
11045
  setTimeout(() => { isLoadCooldownActive = false; }, 3000);
10022
11046
  lastSavedCanvasObjectCount = objectCount;
@@ -10100,6 +11124,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10100
11124
 
10101
11125
  lastSavedCanvasObjectCount = objectCount;
10102
11126
  try { sessionStorage.setItem(CANVAS_OBJ_COUNT_KEY, String(objectCount)); } catch {}
11127
+ emitCreatorSaveState(false);
10103
11128
 
10104
11129
  // Save to durable L1 fallback (always, even with 0 objects for legitimate clears)
10105
11130
  saveCanvasState(canvasId, json);
@@ -10173,6 +11198,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10173
11198
  : `ConvertedW-${Date.now()}`;
10174
11199
  draftConvertScreenshots.set(draftId, payload.screenshot);
10175
11200
  draftConvertObjectIds.set(draftId, payload.frameId);
11201
+ // Grouping deselects the sketch; that must not close the draft or open the primary widget.
11202
+ suppressWidgetDetailsClose = true;
10176
11203
 
10177
11204
  // Group the sketch into one editable object and keep it on canvas — the
10178
11205
  // user may keep editing it (ungroup, move, restyle) and re-convert later.
@@ -10200,6 +11227,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10200
11227
  // Refresh the stored screenshot so finalize uses the latest sketch look
10201
11228
  draftConvertScreenshots.set(existingDraftId, payload.screenshot);
10202
11229
  authStatus = 'Widget draft ready — send the prompt to generate.';
11230
+ setTimeout(() => {
11231
+ if (!finalizingDraftConversions.size) {
11232
+ suppressWidgetDetailsClose = false;
11233
+ }
11234
+ }, 1500);
10203
11235
  return;
10204
11236
  }
10205
11237
 
@@ -10210,6 +11242,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10210
11242
  // Draft panel snapshot (no widget record in DB). WidgetDetails is
10211
11243
  // null-safe for a missing widget — the panel shows the draft name.
10212
11244
  await openWidgetDetailsPanel(draftId);
11245
+ embeddedDeepLinkAutoOpenDone = true;
10213
11246
  patchPanelSnapshot(draftId, {
10214
11247
  codeGenerationPrompt: CONVERT_PROMPT,
10215
11248
  promptImages: [payload.screenshot],
@@ -10251,6 +11284,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10251
11284
  showToast('error', 'Failed to prepare widget draft');
10252
11285
  } finally {
10253
11286
  isWidgetOperationInProgress = false;
11287
+ setTimeout(() => {
11288
+ if (!finalizingDraftConversions.size) {
11289
+ suppressWidgetDetailsClose = false;
11290
+ }
11291
+ }, 1500);
10254
11292
  }
10255
11293
  }
10256
11294
 
@@ -10325,6 +11363,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10325
11363
  // Add to widgets list so getWidgetById resolves the real widget from now on.
10326
11364
  widgets = [{ ...newWidget }, ...widgets.filter((w) => w.id !== draftId)];
10327
11365
 
11366
+ if (canvasId) {
11367
+ const existingPrimary = await resolvePrimaryWidgetIdForCanvas(canvasId);
11368
+ if (!existingPrimary) {
11369
+ await persistPrimaryWidgetId(widgetId);
11370
+ }
11371
+ }
11372
+
10328
11373
  // Replace the grouped sketch with a WidgetShape screenshot. On failure
10329
11374
  // fall back to linking the sketch object to the widget id.
10330
11375
  if (canvasRef && canvasObjectId) {
@@ -10364,7 +11409,10 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10364
11409
  // panel remounts: the freshly mounted Chat then activates this
10365
11410
  // conversation (best-conversation = one with messages) instead of
10366
11411
  // starting empty after a close/reopen.
10367
- await ensureCodegenUserMessagePersisted(widgetId, prompt);
11412
+ await ensureCodegenUserMessagePersisted(widgetId, prompt, [
11413
+ ...(draftPanelState.promptImages ?? []),
11414
+ screenshot,
11415
+ ].filter(Boolean));
10368
11416
  // NOTE: never copy currentChatConversationId from the draft — the draft
10369
11417
  // chat's conversation is a local UUID that doesn't exist in the DB, and
10370
11418
  // migrating it would point codegen/summaries at a phantom conversation.
@@ -10372,6 +11420,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10372
11420
  draftPanelState as Record<string, unknown>;
10373
11421
  patchPanelSnapshot(widgetId, {
10374
11422
  ...draftPanelStateClean,
11423
+ lastPublishedVersion: null,
11424
+ lastCommitId: null,
11425
+ hasGeneratedCode: false,
11426
+ publishHistory: [],
11427
+ widgetDetailsStep: 'create',
10375
11428
  } as Partial<WidgetPanelSnapshot>);
10376
11429
  migrateOpenPanelWidgetId(draftId, widgetId);
10377
11430
  widgetPanelSnapshots = Object.fromEntries(
@@ -10387,8 +11440,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10387
11440
  await tick();
10388
11441
  const migratedRef = getWidgetDetailsRef(widgetId);
10389
11442
  if (migratedRef?.getShowWidgetDetails?.()) {
10390
- console.log('[CreatorApp] Draft→widget: Widget Details stayed open, updating in place');
10391
- positionOpenDetailsPanel(widgetId);
11443
+ console.log('[CreatorApp] Draft→widget: Widget Details stayed open — keep current position');
10392
11444
  } else {
10393
11445
  await openWidgetDetailsPanel(widgetId);
10394
11446
  }
@@ -10425,6 +11477,16 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10425
11477
  return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
10426
11478
  }
10427
11479
 
11480
+ function isConvertOrDraftPanelOpen(): boolean {
11481
+ return (
11482
+ isDraftConvertId(focusedPanelWidgetId)
11483
+ || isDraftConvertId(widgetDetailsOpenTargetId)
11484
+ || isDraftConvertId(selectedWidgetId)
11485
+ || openWidgetPanelIds.some((id) => isDraftConvertId(id))
11486
+ || (!!detailsSessionWidgetId && openWidgetPanelIds.includes(detailsSessionWidgetId))
11487
+ );
11488
+ }
11489
+
10428
11490
 
10429
11491
  /**
10430
11492
  * Creates or recreates a repository for a specific widget.
@@ -10525,6 +11587,30 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10525
11587
  } catch (error) {
10526
11588
  if (error instanceof ResponseError) {
10527
11589
  const status = error.response?.status;
11590
+ if (status === 401) {
11591
+ console.warn('[Auth] Repository verify got 401 — rebinding session and retrying once', {
11592
+ repoId
11593
+ });
11594
+ applySiteSessionToCreator();
11595
+ try {
11596
+ const retryDetails = await agentClient.agentRepositoriesRepoIdGet({ repoId });
11597
+ if (retryDetails?.id) return true;
11598
+ } catch (retryError) {
11599
+ if (retryError instanceof ResponseError && retryError.response?.status === 401) {
11600
+ if (!silent) {
11601
+ showToast('warning', 'Session expired', {
11602
+ description: 'Sign in again, then retry. The widget repository is still linked.',
11603
+ duration: 6000
11604
+ });
11605
+ }
11606
+ if (typeof window !== 'undefined') {
11607
+ window.dispatchEvent(new CustomEvent('widgetic:session-needs-reauth'));
11608
+ }
11609
+ throw retryError;
11610
+ }
11611
+ }
11612
+ return false;
11613
+ }
10528
11614
  if (status === 404 || status === 403) {
10529
11615
  console.warn('Repository reference is stale or forbidden; it will be recreated.', {
10530
11616
  repoId,
@@ -10873,6 +11959,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10873
11959
  </div>
10874
11960
  {/if}
10875
11961
 
11962
+
10876
11963
  <!-- Widgetic Canvas Editor - Full width background (lazy-loaded) -->
10877
11964
  <div class="wc-canvas-area flex-1 min-h-0 relative">
10878
11965
  <!-- Embedded: Debug toggle lives in site AppHeader (avoids overlapping Props Panel). -->
@@ -10903,6 +11990,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10903
11990
  bind:this={canvasRef}
10904
11991
  showContainerFrame={!embedded}
10905
11992
  bind:wireframeMode={canvasWireframeMode}
11993
+ advancedFeatures={canvasAdvancedFeatures}
10906
11994
  onReady={handleCanvasReady}
10907
11995
  onAutoSave={handleCanvasAutoSave}
10908
11996
  onBeforeUnmount={(json) => {
@@ -10914,6 +12002,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10914
12002
  }}
10915
12003
  onConvertToWidget={handleConvertToWidget}
10916
12004
  onWidgetImageClick={handleWidgetImageClick}
12005
+ onCanvasDirty={() => emitCreatorSaveState(true)}
10917
12006
  onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
10918
12007
  onWidgetShapeDeleting={handleWidgetShapeDeleting}
10919
12008
  onWidgetRename={handleWidgetRename}
@@ -10922,8 +12011,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10922
12011
  getWidgetName={getWidgetNameById}
10923
12012
  isWidgetKnown={isWidgetKnownById}
10924
12013
  onNonWidgetSelected={() => {
10925
- if (suppressWidgetDetailsClose) {
10926
- console.log('[CreatorApp] Skip Widget Details close — canvas selection changed during draft finalize');
12014
+ if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
12015
+ console.log('[CreatorApp] Skip Widget Details close — convert/draft panel is open');
10927
12016
  return;
10928
12017
  }
10929
12018
  const active = canvasRef?.getSelectedObject?.() ?? canvasRef?.selectedObject;
@@ -11259,7 +12348,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11259
12348
  {isPublishing}
11260
12349
  {publishWidgetStatus}
11261
12350
  publishNeedsSaveFirst={publishNeedsSaveFirstForPanel(panelWidgetId)}
11262
- lastPublishedVersion={ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null)}
12351
+ lastPublishedVersion={isDraftConvertId(panelWidgetId) ? null : (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
11263
12352
  widgetJsPath={ps.selectedWidgetJsPath ?? panelWidget?.jsPath ?? null}
11264
12353
  {isGeneratingInOtherTab}
11265
12354
  {isPublishingInOtherTab}
@@ -11335,6 +12424,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11335
12424
  }
11336
12425
  // Keep canvas WidgetShape thumbnail aligned with the live Worker preview.
11337
12426
  scheduleCanvasShapePreviewSync(previewWidgetId);
12427
+ if (detail?.step === 'create' || ps.widgetDetailsStep === 'create') {
12428
+ applyPanelCompositionToPreview(previewWidgetId);
12429
+ }
11338
12430
  }}
11339
12431
  on:widgetReady={({ detail }) => {
11340
12432
  if (detail.widgetId !== panelWidgetId) return;
@@ -11349,21 +12441,15 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11349
12441
  detail.repoHeadCommitSha ||
11350
12442
  detail.repo_head_commit_sha ||
11351
12443
  null;
11352
- const hasSha = typeof repoHead === 'string' && repoHead.length > 0;
11353
- if (hasSha) {
11354
- saveCommitIdForWidget(detail.widgetId, repoHead);
11355
- }
11356
- // Always persist hasGeneratedCode on the snapshot so Publish re-renders
11357
- // even when Worker cache hits without a SHA and the panel is unfocused.
12444
+ // Rebuild is the same saved tree — do not promote GitLab HEAD into lastCommitId
12445
+ // or Publish will offer vN+1 without a new codegen commit.
11358
12446
  patchPanelSnapshot(detail.widgetId, {
11359
12447
  panelPreviewCompileError: null,
11360
12448
  hasGeneratedCode: true,
11361
- ...(hasSha ? { lastCommitId: repoHead } : {}),
11362
12449
  });
11363
12450
  if (detail.widgetId === panelWidgetId) {
11364
12451
  hasGeneratedCode = true;
11365
- if (hasSha) lastCommitId = repoHead;
11366
- if (!hasSha) {
12452
+ if (!getPanelState(detail.widgetId).lastCommitId && !loadCommitIdForWidget(detail.widgetId)) {
11367
12453
  void reconcileWidgetHeadCommit(detail.widgetId);
11368
12454
  }
11369
12455
  if (isPanelFocused && generateCodeStatus?.includes('preview build failed')) {
@@ -11485,6 +12571,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11485
12571
  console.log('[WidgetCreator] Generate Now clicked for widget:', detail.widgetId);
11486
12572
  const widgetId = detail.widgetId;
11487
12573
  const ps = getPanelDisplayState(widgetId);
12574
+ if (ps.lastGenerationFailed) {
12575
+ void handleChatRetryMessage(
12576
+ lastGenerationPrompt || ps.codeGenerationPrompt,
12577
+ widgetId,
12578
+ );
12579
+ return;
12580
+ }
11488
12581
  const hasCode = !!(ps.lastCommitId || ps.lastPublishedVersion !== null);
11489
12582
  const compileErr = getPanelPreviewCompileError(widgetId);
11490
12583
  if (hasCode && compileErr) {
@@ -11501,10 +12594,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11501
12594
  console.log('[CreatorApp] Ignoring Widget Details close during draft→widget migrate');
11502
12595
  return;
11503
12596
  }
11504
- if (widgetDetailsOpenTargetId && widgetDetailsOpenTargetId !== panelWidgetId) {
11505
- console.log('[CreatorApp] Ignoring close from previous details panel:', panelWidgetId?.substring(0, 8));
11506
- return;
11507
- }
12597
+ // After convert, the panel id changes but the X is still the visible
12598
+ // panel — always close it. Stale-target ignore left the child hidden
12599
+ // while openWidgetPanelIds stayed set, so nothing would reopen.
11508
12600
  if (isPanelFocused && selectedWidgetId) {
11509
12601
  savePromptForWidget(selectedWidgetId);
11510
12602
  captureFocusedPanelSnapshot();
@@ -11534,8 +12626,9 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11534
12626
  >
11535
12627
  <svelte:fragment slot="code-generation">
11536
12628
  <!-- svelte-ignore a11y_no_static_element_interactions -->
12629
+ {@const chatUserHeight = (panelDisplayRevision, Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, (ps.chatContainerHeight > 0 ? ps.chatContainerHeight : CHAT_DEFAULT_HEIGHT))))}
11537
12630
  <div
11538
- class="parallel-panel-left-slot-ct flex flex-col flex-1 min-h-0 overflow-hidden h-full"
12631
+ class="parallel-panel-left-slot-ct flex flex-col shrink-0 overflow-visible"
11539
12632
  onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
11540
12633
  >
11541
12634
  <!-- Sticky header: title + model selectors (debug mode).
@@ -11582,11 +12675,11 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11582
12675
  </div>
11583
12676
  {/if}
11584
12677
 
11585
- <div class="generation-scroll-ct flex flex-col flex-1 min-h-0 overflow-y-auto scrollbar-thin">
12678
+ <div class="generation-scroll-ct flex flex-col shrink-0 overflow-visible">
11586
12679
  <!-- Chat-based Code Generation Panel -->
11587
12680
  {#key panelInstanceKey}
11588
- <Chat class="code-generation-chat flex-1 w-full shrink-0"
11589
- style="min-height: {ps.chatContainerHeight}px;"
12681
+ <Chat class="code-generation-chat w-full shrink-0"
12682
+ style="height: {chatUserHeight}px; min-height: {CHAT_MIN_HEIGHT}px; flex: 0 0 {chatUserHeight}px;"
11590
12683
  resizeExpandsContainer={true}
11591
12684
  onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
11592
12685
  showLogs={debugMode}
@@ -11616,11 +12709,15 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11616
12709
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
11617
12710
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
11618
12711
  onUploadChatFile={handleChatUploadFile}
11619
- onTakeScreenshot={handleChatScreenshot}
12712
+ onTakeScreenshot={() => handleChatScreenshot(panelWidgetId)}
12713
+ isCompleted={!!conversationCompletedByWidgetId[panelWidgetId]}
12714
+ onCompletionToggle={(detail) => handleChatCompletionToggle(panelWidgetId, detail)}
11620
12715
  onLoadConversations={handleLoadConversations}
11621
12716
  onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
11622
12717
  onCreateConversation={handleCreateConversation}
11623
- onSaveMessage={handleSaveMessage}
12718
+ onSaveMessage={(conversationId, content, messageType, attachments) =>
12719
+ handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
12720
+ }
11624
12721
  onRestoreCheckpointBackend={handleRestoreCheckpoint}
11625
12722
  onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
11626
12723
  onDraftChange={() => handleChatDraftChange(panelWidgetId)}
@@ -11695,42 +12792,39 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11695
12792
  </div>
11696
12793
  {/if}
11697
12794
  </div>
12795
+ </div> <!-- generation-scroll-ct -->
12796
+ </div> <!-- parallel-panel-left-slot-ct -->
12797
+ </svelte:fragment>
12798
+
12799
+ <svelte:fragment slot="publish-widget">
11698
12800
  <!-- svelte-ignore a11y_no_static_element_interactions -->
11699
12801
  <div
11700
- class="parallel-panel-publish-slot-ct shrink-0 mt-2"
12802
+ class="parallel-panel-publish-slot-ct shrink-0"
11701
12803
  onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
11702
12804
  >
11703
12805
  <!-- ===== Publish Widget Section ===== -->
11704
- <div class="publish-widget-section flex flex-col gap-3 rounded border border-emerald-200 bg-emerald-50 p-3 mt-1">
11705
- <!-- Section Header -->
12806
+ <div class="publish-widget-section flex flex-col gap-1 rounded border border-emerald-200 bg-emerald-50 px-2 py-1.5 mt-1 shrink-0">
11706
12807
  <div class="publish-widget-section-header flex items-center justify-between gap-2">
11707
- <span class="text-sm font-semibold text-emerald-800">🚀 Publish Widget</span>
11708
- <!-- Current published version badge — shown when widget has been published -->
12808
+ <span class="text-xs font-semibold text-emerald-800">🚀 Publish</span>
11709
12809
  {#if ps.lastPublishedVersion !== null}
11710
- <span class="text-xs font-mono bg-emerald-700 text-white px-1.5 py-0.5 rounded">
12810
+ <span class="text-[10px] font-mono bg-emerald-700 text-white px-1.5 py-0.5 rounded">
11711
12811
  live: v{ps.lastPublishedVersion}
11712
12812
  </span>
11713
12813
  {/if}
11714
12814
  </div>
11715
12815
 
11716
- <!-- Description -->
11717
- <p class="text-xs text-emerald-700 leading-relaxed">
11718
- Publish when you’re happy with the preview. You’ll get a stable embed URL for your site.
11719
- </p>
11720
-
11721
- <!-- Publish Button + Build Status -->
11722
- <div class="publish-widget-section-actions flex flex-col gap-2 items-center">
11723
- <!-- Publish Widget Button — triggers production build via Trigger.dev -->
12816
+ <div class="publish-widget-section-actions flex flex-col gap-1 items-center">
11724
12817
  <Button
11725
12818
  onclick={() => runPublishForPanel(panelWidgetId)}
11726
- disabled={isPublishDisabledForPanel(panelWidgetId)}
12819
+ disabled={(panelDisplayRevision, isPublishDisabledForPanel(panelWidgetId))}
11727
12820
  title={getPublishDisabledReasonForPanel(panelWidgetId) ?? undefined}
11728
- class={`publish-widget-btn flex justify-center items-center border text-white w-full
12821
+ size="sm"
12822
+ class={`publish-widget-btn flex justify-center items-center border text-white w-full h-8 text-xs
11729
12823
  ${isPublishing && publishingWidgetId === panelWidgetId
11730
12824
  ? 'bg-emerald-500 border-emerald-500 opacity-80 cursor-not-allowed'
11731
12825
  : isPublishDisabledForPanel(panelWidgetId)
11732
- ? 'bg-emerald-600 border-emerald-600 opacity-35 cursor-not-allowed'
11733
- : 'bg-emerald-600 border-emerald-600 hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'
12826
+ ? 'bg-emerald-600 border-emerald-600 opacity-40 cursor-not-allowed'
12827
+ : '!opacity-100 bg-emerald-600 border-emerald-600 hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'
11734
12828
  } rounded-lg`}
11735
12829
  >
11736
12830
  {#if isPublishing && publishingWidgetId === panelWidgetId && publishWidgetStatus === 'Building...'}
@@ -11740,7 +12834,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
11740
12834
  <span class="mr-1 inline-block">⏳</span>
11741
12835
  <span>Publishing...</span>
11742
12836
  {:else}
11743
- <span>{getPublishButtonLabelForPanel(panelWidgetId)}</span>
12837
+ <span>{(panelDisplayRevision, getPublishButtonLabelForPanel(panelWidgetId))}</span>
11744
12838
  {#if isPublishingInOtherTab}
11745
12839
  <!-- svelte-ignore a11y-missing-attribute -->
11746
12840
  <span class="text-xs opacity-70 ml-1" title="Publish is running in another tab">⏳</span>
@@ -11753,25 +12847,24 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
11753
12847
  {/if}
11754
12848
  </Button>
11755
12849
 
11756
- {#if isPublishDisabledForPanel(panelWidgetId) && shouldShowPublishDisabledHintForPanel(panelWidgetId)}
11757
- <p class="publish-widget-disabled-hint text-xs text-center text-emerald-800/80 leading-relaxed px-1">
11758
- {getPublishDisabledReasonForPanel(panelWidgetId)}
12850
+ {#if isPublishDisabledForPanel(panelWidgetId)}
12851
+ <p class="publish-widget-disabled-hint text-[10px] text-center text-emerald-800/80 leading-snug px-1">
12852
+ {getPublishDisabledReasonForPanel(panelWidgetId) || getPublishButtonLabelForPanel(panelWidgetId)}
11759
12853
  </p>
11760
12854
  {/if}
11761
12855
 
11762
12856
  {#if ps.lastPublishedVersion !== null}
11763
- <a href="/test-widget?widgetId={panelWidgetId}&version={ps.lastPublishedVersion}&url={encodeURIComponent(publishedWidgetHtmlUrl(panelWidgetId, ps.lastPublishedVersion))}" target="_blank" rel="noopener noreferrer" class="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">
11764
- Open Published Widget (v{ps.lastPublishedVersion}) ↗
12857
+ <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">
12858
+ Open published v{ps.lastPublishedVersion} ↗
11765
12859
  </a>
11766
12860
  {/if}
11767
12861
 
11768
- <!-- Save-first hint — shown when changes aren't saved yet -->
11769
12862
  {#if publishNeedsSaveFirstForPanel(panelWidgetId)}
11770
- <p class="text-xs text-center text-amber-700">
12863
+ <p class="text-[10px] text-center text-amber-700">
11771
12864
  {#if !resolvePublishHeadCommitId(panelWidgetId, ps)}
11772
- 💾 Generate &amp; save the widget before publishing.
12865
+ Save the widget before publishing.
11773
12866
  {:else}
11774
- 💾 Save your changes before publishing.
12867
+ Save your changes before publishing.
11775
12868
  {/if}
11776
12869
  </p>
11777
12870
  {/if}
@@ -11784,7 +12877,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
11784
12877
  if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
11785
12878
  togglePublishHistory();
11786
12879
  }}
11787
- 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"
12880
+ class="publish-history-toggle flex items-center justify-between w-full text-[10px] text-emerald-700 hover:text-emerald-900 py-0.5 px-0.5 rounded transition-colors"
11788
12881
  >
11789
12882
  <span class="font-medium">📋 Version History</span>
11790
12883
  <span class="text-emerald-400">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
@@ -11842,53 +12935,41 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
11842
12935
 
11843
12936
  </div> <!-- end of publish-widget-section -->
11844
12937
  </div>
11845
- </div> <!-- generation-scroll-ct -->
11846
- </div> <!-- parallel-panel-left-slot-ct -->
11847
12938
  </svelte:fragment>
11848
12939
 
11849
- <svelte:fragment slot="publish-widget"></svelte:fragment>
11850
-
11851
12940
  <svelte:fragment slot="composition-editor">
11852
12941
  <!-- svelte-ignore a11y_no_static_element_interactions -->
11853
12942
  <div
11854
- class="parallel-panel-composition-slot-ct flex flex-col h-full min-h-0"
12943
+ class="parallel-panel-composition-slot-ct flex flex-col h-full min-h-0 min-w-0 overflow-y-auto overflow-x-hidden scrollbar-thin"
11855
12944
  onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
11856
12945
  >
11857
12946
  <!-- ===== Composition Editor (Step 2: Edit) ===== -->
11858
12947
  {#if ps.lastCommitId || ps.lastPublishedVersion !== null}
11859
- <div class="composition-editor-section flex flex-col h-full min-h-0">
12948
+ <div class="composition-editor-section flex flex-col flex-1 min-h-0 h-full">
11860
12949
 
11861
12950
  {#if panelHasUnpublishedChanges}
11862
- <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">
11863
- <p class="edit-unpublished-banner-text leading-relaxed">
11864
- <strong class="font-semibold">Newer code than Live v{ps.lastPublishedVersion ?? '—'}.</strong>
11865
- Widget is published, but latest commit
12951
+ <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">
12952
+ <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)}.">
12953
+ <strong class="font-semibold">Newer than Live v{ps.lastPublishedVersion ?? '—'}</strong>
11866
12954
  <span class="font-mono">{formatCommitShaShort(ps.lastCommitId)}</span>
11867
- is ahead of the live artifact
11868
- (<span class="font-mono">{formatCommitShaShort(getLivePublishedCommitShaForPanel(panelWidgetId))}</span>).
11869
- Edit preview shows <strong>Live v{ps.lastPublishedVersion ?? '—'}</strong> with your composition settings.
11870
- Publish to promote the latest code to Live — this preview will refresh automatically.
12955
+ ahead of
12956
+ <span class="font-mono">{formatCommitShaShort(getLivePublishedCommitShaForPanel(panelWidgetId))}</span>
11871
12957
  </p>
11872
- <div class="edit-unpublished-banner-actions flex items-center gap-2 flex-wrap">
11873
- <Button
11874
- onclick={() => runPublishForPanel(panelWidgetId)}
11875
- disabled={isPublishDisabledForPanel(panelWidgetId)}
11876
- title={getPublishDisabledReasonForPanel(panelWidgetId) ?? undefined}
11877
- 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
11878
- {isPublishDisabledForPanel(panelWidgetId)
11879
- ? 'bg-emerald-600/40 border-emerald-600/40 text-white cursor-not-allowed'
11880
- : 'bg-emerald-600 border-emerald-600 text-white hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'}"
11881
- >
11882
- {#if isPublishing && publishingWidgetId === panelWidgetId}
11883
- <span>⏳ Publishing…</span>
11884
- {:else}
11885
- <span>🚀 Publish to Live</span>
11886
- {/if}
11887
- </Button>
11888
- {#if publishNeedsSaveFirstForPanel(panelWidgetId)}
11889
- <span class="text-amber-700">Generate &amp; save code before publishing.</span>
12958
+ <Button
12959
+ onclick={() => runPublishForPanel(panelWidgetId)}
12960
+ disabled={isPublishDisabledForPanel(panelWidgetId)}
12961
+ title={getPublishDisabledReasonForPanel(panelWidgetId) ?? undefined}
12962
+ 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
12963
+ {isPublishDisabledForPanel(panelWidgetId)
12964
+ ? 'bg-emerald-600/40 border-emerald-600/40 text-white cursor-not-allowed'
12965
+ : 'bg-emerald-600 border-emerald-600 text-white hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'}"
12966
+ >
12967
+ {#if isPublishing && publishingWidgetId === panelWidgetId}
12968
+ <span>⏳</span>
12969
+ {:else}
12970
+ <span>🚀 Publish</span>
11890
12971
  {/if}
11891
- </div>
12972
+ </Button>
11892
12973
  </div>
11893
12974
  {/if}
11894
12975
 
@@ -12050,14 +13131,17 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
12050
13131
  </button>
12051
13132
  </div>
12052
13133
  {:else if ps.compositionDesignSchema || ps.compositionContentSchema}
12053
- <div class="composition-editor-props-ct flex flex-1 min-h-0 flex-col overflow-hidden p-2">
13134
+ <div class="composition-editor-props-ct flex flex-col flex-1 min-h-[550px] max-h-[800px] overflow-y-auto overflow-x-hidden p-2">
12054
13135
  {#key `${panelWidgetId}-${ps.selectedComposition?.id ?? 'new'}`}
12055
13136
  <PropsEditor
12056
13137
  class="composition-props-editor flex-1 min-h-0 min-w-0"
12057
13138
  designSchema={ps.compositionDesignSchema}
12058
13139
  contentSchema={buildPropsEditorContentSchema(ps.compositionContentSchema, ps.liveContentItems)}
12059
13140
  initialDesignValues={ps.liveDesignValues}
12060
- initialContentValues={ps.liveContentValues}
13141
+ initialContentValues={{
13142
+ ...ps.liveContentValues,
13143
+ items: contentItemsToEditorValueMap(ps.liveContentItems)
13144
+ }}
12061
13145
  width="100%"
12062
13146
  height="100%"
12063
13147
  showDragHandle={false}
@@ -12336,7 +13420,7 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
12336
13420
  <Dialog.Title>Delete Widget Shape</Dialog.Title>
12337
13421
  <Dialog.Description>
12338
13422
  {#if deleteDialogStep === 'confirm-remove'}
12339
- Remove <strong>"{deleteDialogWidgetName}"</strong> shape from the canvas?
13423
+ Remove <strong>"{deleteDialogWidgetName}"</strong> from the canvas? This also deletes the associated widget.
12340
13424
  {:else}
12341
13425
  Also delete the widget <strong>"{deleteDialogWidgetName}"</strong> from the database?
12342
13426
  {/if}
@@ -12354,8 +13438,8 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
12354
13438
  type="button"
12355
13439
  class="px-4 py-2 text-sm rounded-lg bg-red-600 hover:bg-red-700 text-white cursor-pointer"
12356
13440
  onclick={() => handleDeleteDialogResponse('remove-and-ask-db')}
12357
- >Remove from Canvas</button>
12358
- {:else}
13441
+ >Delete</button>
13442
+ {:else if ASK_DELETE_FROM_DB_CONFIRM}
12359
13443
  <button
12360
13444
  type="button"
12361
13445
  class="px-4 py-2 text-sm rounded-lg border border-gray-300 hover:bg-gray-100 cursor-pointer"