@widgetic/creator 0.3.55 → 0.3.57

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.
@@ -67,6 +67,7 @@
67
67
  isInfrastructurePreviewError
68
68
  } from './pageHelpers.js';
69
69
  import { uploadChatFileToWidgeticApi } from './utils/widgeticChatUpload.js';
70
+ import { persistWidgetThumbnail } from './utils/persistWidgetThumbnail.js';
70
71
  import { nativeScrollbarPadding } from './utils/nativeScrollbarPadding.js';
71
72
  import { streamOperationUpdates } from './utils/operationStream.js';
72
73
  import { generateEmbedCode, EMBED_FORMATS, EMBED_FORMAT_LABELS, type EmbedFormat } from './utils/embedCode.js';
@@ -81,6 +82,14 @@
81
82
  import { setEnableLocalCache, isLocalCacheEnabled } from './localCacheGate.js';
82
83
  import type { CreatorAppProps } from './creator-types.js';
83
84
 
85
+ function resolveCreatorWorkflowStep(
86
+ step?: CreatorAppProps['initialStep'] | 'create' | 'edit' | 'embed' | null
87
+ ): 'create' | 'edit' | 'embed' {
88
+ if (!canvasAdvancedFeatures) return 'create';
89
+ if (step === 'edit' || step === 'embed') return step;
90
+ return 'create';
91
+ }
92
+
84
93
  /** Embedded mode props (site integration) */
85
94
  export let embedded: CreatorAppProps['embedded'] = false;
86
95
  export let canvasId: CreatorAppProps['canvasId'] = null;
@@ -928,10 +937,6 @@
928
937
  codegenWidgetId = finalizedWidgetId;
929
938
  const newPanelChat = getChatMethodsForWidget(finalizedWidgetId);
930
939
  newPanelChat?.setDraftText?.('');
931
- const hydrateImages = fallbackChatImageAttachments(finalizedWidgetId);
932
- if (hydrateImages.length > 0) {
933
- newPanelChat?.updateLastUserMessageAttachments?.(hydrateImages);
934
- }
935
940
  }
936
941
 
937
942
  const panelChat = getChatMethodsForWidget(codegenWidgetId);
@@ -1251,31 +1256,6 @@
1251
1256
  * picked, which may differ from convos[0] when several empty conversations
1252
1257
  * exist (feedback c: chat looked empty while messages went elsewhere).
1253
1258
  */
1254
- function userMessageImpliesAttachedFile(content: string): boolean {
1255
- return /imaginea ata[sș]at[ăa]|attached (reference )?(image|document|file|pdf|video)|reference image|din imagine|ata[sș]at[ăa]?\s+(document|pdf|fi[sș]ier|video)|attached document|starting from the attached image/i.test(
1256
- content || '',
1257
- );
1258
- }
1259
-
1260
- function fallbackChatImageAttachments(widgetId: string): Array<{
1261
- id: string;
1262
- url: string;
1263
- file_type: string;
1264
- file_name: string;
1265
- }> {
1266
- const urls = uniquePersistImageUrls([
1267
- ...(getPanelState(widgetId).promptImages ?? []),
1268
- ...(widgetId === focusedPanelWidgetId ? promptImages : []),
1269
- (typeof canvasRef?.getWidgetShapeDataUrl === 'function' ? canvasRef.getWidgetShapeDataUrl(widgetId) : '') || '',
1270
- ]);
1271
- return urls.map((url, i) => ({
1272
- id: `hydrate_img_${i}`,
1273
- url,
1274
- file_type: guessAttachmentFileType(url, 'design-screenshot.png', 'image/png'),
1275
- file_name: 'design-screenshot.png',
1276
- }));
1277
- }
1278
-
1279
1259
  function guessAttachmentFileType(url: string, fileName?: string | null, fallback?: string | null): string {
1280
1260
  if (fallback && String(fallback).includes('/')) return String(fallback);
1281
1261
  const hint = `${url || ''} ${fileName || ''}`;
@@ -1297,7 +1277,26 @@
1297
1277
  else if (url.startsWith('data:image/')) dataUrls.push(url);
1298
1278
  }
1299
1279
  // Same screenshot often exists as CDN URL + compressed data URL — keep HTTPS only.
1300
- return httpsUrls.length > 0 ? httpsUrls : dataUrls;
1280
+ // If only data URLs remain, keep the first one so convert does not triple the bubble.
1281
+ if (httpsUrls.length > 0) return httpsUrls;
1282
+ return dataUrls.slice(0, 1);
1283
+ }
1284
+
1285
+ async function persistWidgetListThumbnail(widgetId: string, imageDataUrl: string): Promise<void> {
1286
+ if (!widgetsClient || !widgetId || !imageDataUrl) return;
1287
+ try {
1288
+ const compact = imageDataUrl.startsWith('data:image/')
1289
+ ? await compressImageDataUrlForCodegen(imageDataUrl, 384, 180000)
1290
+ : imageDataUrl;
1291
+ const thumbnailUrl = await persistWidgetThumbnail(widgetsClient, widgetId, compact);
1292
+ if (thumbnailUrl) {
1293
+ widgets = widgets.map((w) =>
1294
+ w.id === widgetId ? { ...w, thumbnailUrl, thumbnail_url: thumbnailUrl } : w
1295
+ );
1296
+ }
1297
+ } catch (error) {
1298
+ console.warn('[CreatorApp] persist list thumbnail failed', widgetId.slice(0, 8), error);
1299
+ }
1301
1300
  }
1302
1301
 
1303
1302
  async function ensureCodegenUserMessagePersisted(
@@ -1400,6 +1399,14 @@
1400
1399
  .filter((url: string | undefined): url is string => !!url),
1401
1400
  );
1402
1401
  const existingHasRemoteImage = [...existingUrls].some((url) => /^https?:\/\//i.test(url));
1402
+ // Chat already persisted CDN attachments for this turn — do not merge data URLs/canvas shots.
1403
+ if (targetUser && existingHasRemoteImage) {
1404
+ if (convId !== visibleConversationId) {
1405
+ await panelChat?.selectConversation?.(convId);
1406
+ patchPanelSnapshot(widgetId, { currentChatConversationId: convId });
1407
+ }
1408
+ return;
1409
+ }
1403
1410
  const missingPersist = persistAttachments.filter((att) => {
1404
1411
  if (att.url && existingUrls.has(att.url)) return false;
1405
1412
  if (existingHasRemoteImage && typeof att.url === 'string' && att.url.startsWith('data:')) return false;
@@ -1627,14 +1634,6 @@
1627
1634
  created_at: content.created_at || content.createdAt || a.created_at || a.createdAt || new Date().toISOString(),
1628
1635
  };
1629
1636
  }).filter((a: { url: string }) => !!a.url);
1630
- if (
1631
- attachments.length === 0
1632
- && (m.message_type || m.messageType || 'user') === 'user'
1633
- && userMessageImpliesAttachedFile(m.message_content || m.messageContent || '')
1634
- && widgetIdForSync
1635
- ) {
1636
- attachments = fallbackChatImageAttachments(widgetIdForSync);
1637
- }
1638
1637
  return {
1639
1638
  id: m.id,
1640
1639
  conversationId: m.conversation_id || m.conversationId || conversationId,
@@ -3592,12 +3591,12 @@
3592
3591
  const buildSha = import.meta.env.VITE_BUILD_SHA || 'local';
3593
3592
  const buildLabel = `v${appVersion} · ${buildSha}`;
3594
3593
 
3595
- // Default coder: host prop (site embed) wins. Fallback Flash for speed.
3594
+ // Default coder: host prop (site embed) wins. Fallback = first catalog model (DeepSeek V4.1 Flash on Baseten).
3596
3595
  const hostCoderModelId = (defaultCoderModelId || '').trim() || null;
3597
3596
  const hostPlannerRaw = (defaultPlannerModelId || '').trim();
3598
3597
  const hostPlannerModelId =
3599
3598
  !hostPlannerRaw || /^(none|skip)$/i.test(hostPlannerRaw) ? null : hostPlannerRaw;
3600
- let defaultModelId = hostCoderModelId || 'baseten-glm-5.3-fast';
3599
+ let defaultModelId = hostCoderModelId || 'baseten-deepseek-v4.1-flash';
3601
3600
 
3602
3601
  // Available LLM models for code generation
3603
3602
  interface LLMModelOption {
@@ -3608,18 +3607,18 @@
3608
3607
  supportsVision?: boolean;
3609
3608
  }
3610
3609
 
3611
- // Default fallback models in case API fails (one from each provider)
3610
+ // Default fallback models in case API fails (order mirrors llm-models.catalog.json)
3612
3611
  const fallbackModels: LLMModelOption[] = [
3612
+ { id: 'baseten-deepseek-v4.1-flash', name: 'DeepSeek V4.1 Flash (Baseten)', provider: 'Baseten', description: 'V4.1 Flash via Baseten Model APIs', supportsVision: true },
3613
3613
  { id: 'baseten-glm-5.3-fast', name: 'GLM 5.3 Fast (Baseten)', provider: 'Baseten', description: 'Lower-latency GLM 5.3 on Baseten Fast capacity', supportsVision: true },
3614
3614
  { id: 'baseten-glm-5.3-flash', name: 'GLM 5.3 Flash (Baseten)', provider: 'Baseten', description: 'Cheaper GLM via Baseten Model APIs', supportsVision: true },
3615
3615
  { id: 'zhipu-glm-5.3-flash', name: 'GLM 5.3 Flash (Z.ai)', provider: 'Zhipu', description: 'Cheapest vision + coding model', supportsVision: true },
3616
+ { id: 'deepseek-v4.1-flash', name: 'DeepSeek V4.1 Flash (DeepSeek API)', provider: 'DeepSeek', description: 'V4.1 Flash via DeepSeek direct API', supportsVision: true },
3616
3617
  { id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', provider: 'DeepSeek', description: 'Frontier coding model, 1M context', supportsVision: false },
3617
- { id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', provider: 'DeepSeek', description: 'Fast and cost-efficient', supportsVision: false },
3618
- { id: 'openai-gpt-5.4', name: 'GPT-5.4 (OpenAI)', provider: 'OpenAI', description: 'Affordable model for coding', supportsVision: true },
3619
- { id: 'openai-gpt-5.5', name: 'GPT-5.5 (OpenAI)', provider: 'OpenAI', description: 'Frontier model for complex work', supportsVision: true },
3618
+ { id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', provider: 'DeepSeek', description: 'Legacy id; routes to V4.1 Flash', supportsVision: true },
3620
3619
  { id: 'openai-codex-3.2', name: 'Codex 3.2 (OpenAI)', provider: 'OpenAI', description: 'Coding-specialized model', supportsVision: false },
3621
- { id: 'gemini-3-flash', name: 'Gemini 3 Flash (Google)', provider: 'Google', description: 'Google\'s fast Gemini model', supportsVision: true },
3622
- { id: 'anthropic-claude-sonnet-4.6', name: 'Claude Sonnet 4.6 (Anthropic)', provider: 'Anthropic', description: 'Best balance of intelligence, speed, and cost', supportsVision: true },
3620
+ { id: 'openai-gpt-5.5', name: 'GPT-5.5 (OpenAI)', provider: 'OpenAI', description: 'Frontier model for complex work', supportsVision: true },
3621
+ { id: 'gemini-3.8-flash', name: 'Gemini 3.8 Flash (Google)', provider: 'Google', description: 'Most capable Gemini Flash', supportsVision: true },
3623
3622
  ];
3624
3623
 
3625
3624
  // Models loaded from API (single source of truth)
@@ -3645,10 +3644,18 @@
3645
3644
  supportsVision: m.supportsVision === true,
3646
3645
  }));
3647
3646
 
3648
- // Model LIST + default coder come from GET /models (API / Doppler).
3647
+ // Model LIST from GET /models. Selection: host pin → first listed
3648
+ // (catalog puts DeepSeek V4.1 Flash Baseten first) → API default → fallback.
3649
3649
  const apiCoderId = (response as any).defaultModelId || (response as any).data?.defaultModelId;
3650
3650
  const firstListedId = availableModels[0]?.id;
3651
- defaultModelId = hostCoderModelId || apiCoderId || firstListedId || defaultModelId;
3651
+ const listedIds = new Set(availableModels.map((m) => m.id));
3652
+ const pickListed = (id: string | null | undefined) =>
3653
+ id && listedIds.has(id) ? id : null;
3654
+ defaultModelId =
3655
+ pickListed(hostCoderModelId) ||
3656
+ pickListed(firstListedId) ||
3657
+ pickListed(apiCoderId) ||
3658
+ defaultModelId;
3652
3659
  userSelectedModelId = defaultModelId;
3653
3660
  console.log('[WidgetCreator] Default coder model', {
3654
3661
  defaultModelId,
@@ -3724,6 +3731,11 @@
3724
3731
 
3725
3732
  // Get selected model object
3726
3733
  $: selectedModel = availableModels.find(m => m.id === selectedModelId) || availableModels[0];
3734
+ $: modelDropdownItems = availableModels.map((model) => ({
3735
+ id: model.id,
3736
+ name: model.name,
3737
+ badge: model.provider,
3738
+ }));
3727
3739
 
3728
3740
  // Handle model selection (only used in debug mode)
3729
3741
  function handleModelSelect(modelId: string) {
@@ -3794,8 +3806,17 @@
3794
3806
 
3795
3807
  function handleHostToggleAdvancedEvent() {
3796
3808
  canvasAdvancedFeatures = !canvasAdvancedFeatures;
3797
- // console.log('[CreatorApp] Advanced canvas features:', canvasAdvancedFeatures ? 'ON' : 'OFF');
3798
3809
  emitCreatorAdvancedChanged();
3810
+ console.log('[CreatorApp] Advanced', canvasAdvancedFeatures ? 'ON' : 'OFF',
3811
+ '— composition steps', canvasAdvancedFeatures ? 'visible' : 'hidden');
3812
+ if (!canvasAdvancedFeatures) {
3813
+ widgetDetailsStep = 'create';
3814
+ for (const panelId of Object.keys(widgetPanelSnapshots || {})) {
3815
+ if (getPanelState(panelId).widgetDetailsStep !== 'create') {
3816
+ patchPanelSnapshot(panelId, { widgetDetailsStep: 'create' });
3817
+ }
3818
+ }
3819
+ }
3799
3820
  }
3800
3821
 
3801
3822
  function resolveHostTargetWidgetId(): string | null {
@@ -5499,7 +5520,7 @@
5499
5520
  selectedWidgetId === deepLinkWidgetId && !!getWidgetById(deepLinkWidgetId);
5500
5521
  if (alreadyLinked) {
5501
5522
  if (embeddedInitialStepPending && initialStep) {
5502
- widgetDetailsStep = initialStep;
5523
+ widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
5503
5524
  embeddedInitialStepPending = false;
5504
5525
  }
5505
5526
  embeddedDeepLinkAutoOpenDone = true;
@@ -5550,7 +5571,7 @@
5550
5571
  canvasRef.selectWidgetShape(target.id);
5551
5572
  }
5552
5573
  if (embeddedInitialStepPending && initialStep) {
5553
- widgetDetailsStep = initialStep;
5574
+ widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
5554
5575
  embeddedInitialStepPending = false;
5555
5576
  }
5556
5577
 
@@ -5988,10 +6009,10 @@
5988
6009
  }
5989
6010
  if (!isGeneratingCode && !_isRestoringCheckpoint) {
5990
6011
  if (embeddedInitialStepPending && initialStep) {
5991
- widgetDetailsStep = initialStep;
6012
+ widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
5992
6013
  embeddedInitialStepPending = false;
5993
6014
  } else if (initialStep) {
5994
- widgetDetailsStep = initialStep;
6015
+ widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
5995
6016
  } else {
5996
6017
  widgetDetailsStep = 'create';
5997
6018
  }
@@ -6839,8 +6860,8 @@
6839
6860
  if (quiet || isConvertOrDraftPanelOpen()) {
6840
6861
  const shapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
6841
6862
  if (shapeDataUrl && canvasRef) {
6842
- // console.log('[handleRefreshScreenshot] Using canvas shape screenshot (panel closed):', widgetId.substring(0, 8));
6843
6863
  canvasRef.updateWidgetImage(widgetId, shapeDataUrl);
6864
+ void persistWidgetListThumbnail(widgetId, shapeDataUrl);
6844
6865
  } else {
6845
6866
  // console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
6846
6867
  }
@@ -6892,6 +6913,7 @@
6892
6913
  const json = canvasRef?.saveCanvas();
6893
6914
  if (json) handleCanvasAutoSave(json);
6894
6915
  }, 500);
6916
+ void persistWidgetListThumbnail(widgetId, result.data);
6895
6917
  if (!quiet) {
6896
6918
  showToast('success', 'Widget preview updated on canvas', { duration: 2000 });
6897
6919
  } else {
@@ -8080,9 +8102,10 @@ Requirements:
8080
8102
  selectedWidgetId = initialWidgetId;
8081
8103
  saveSelectedWidgetId(initialWidgetId);
8082
8104
  }
8083
- // Site deep-link: Edit widget → create, New Composition → edit (before widget hydrate).
8105
+ // Site deep-link: Edit widget / New Composition used to open Edit/Embed in Creator.
8106
+ // Canvas is Create-only now; /composition owns the editor.
8084
8107
  if (embedded && initialStep) {
8085
- widgetDetailsStep = initialStep;
8108
+ widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
8086
8109
  }
8087
8110
  if (embedded && projectId) {
8088
8111
  // console.log('[CreatorApp] Embedded project context:', {
@@ -8733,7 +8756,7 @@ Requirements:
8733
8756
  lastPublishedVersion: publishedVersion,
8734
8757
  selectedWidgetJsPath: widget?.jsPath ?? null,
8735
8758
  currentRepositoryId: normalizeRepositoryId(widget),
8736
- widgetDetailsStep: publishedVersion !== null ? 'edit' : 'create',
8759
+ widgetDetailsStep: resolveCreatorWorkflowStep(publishedVersion !== null ? 'edit' : 'create'),
8737
8760
  generateCodeStatus: null,
8738
8761
  lastGenerationFailed: false,
8739
8762
  lastGenerationError: null,
@@ -8949,12 +8972,18 @@ Requirements:
8949
8972
  const resolvedVersion = resolvePublishedVersionFromWidget(widget);
8950
8973
  const resolvedJsPath = ((widget as { jsPath?: string | null; js_path?: string | null })?.jsPath
8951
8974
  ?? (widget as { js_path?: string | null })?.js_path) as string | null;
8952
- if (resolvedVersion !== null && (snap.lastPublishedVersion === null || snap.widgetDetailsStep === 'create')) {
8975
+ if (resolvedVersion !== null && snap.lastPublishedVersion === null) {
8953
8976
  patchPanelSnapshot(widgetId, {
8954
8977
  lastPublishedVersion: resolvedVersion,
8955
8978
  selectedWidgetJsPath: snap.selectedWidgetJsPath ?? resolvedJsPath,
8956
- widgetDetailsStep: 'edit',
8979
+ ...(canvasAdvancedFeatures ? { widgetDetailsStep: 'edit' as const } : {}),
8957
8980
  });
8981
+ } else if (
8982
+ canvasAdvancedFeatures
8983
+ && resolvedVersion !== null
8984
+ && snap.widgetDetailsStep === 'create'
8985
+ ) {
8986
+ patchPanelSnapshot(widgetId, { widgetDetailsStep: 'edit' });
8958
8987
  }
8959
8988
  }
8960
8989
 
@@ -11889,6 +11918,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11889
11918
  }
11890
11919
  }
11891
11920
 
11921
+ if (screenshot) {
11922
+ void persistWidgetListThumbnail(widgetId, screenshot);
11923
+ }
11924
+
11892
11925
  // Auto-create repository so generation can start right away.
11893
11926
  try {
11894
11927
  const repoId = await ensureWidgetRepository(newWidget, { silent: true });
@@ -11907,11 +11940,15 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11907
11940
  // panel remounts: the freshly mounted Chat then activates this
11908
11941
  // conversation (best-conversation = one with messages) instead of
11909
11942
  // starting empty after a close/reopen.
11910
- await ensureCodegenUserMessagePersisted(widgetId, prompt, [
11911
- ...(draftPanelState.promptImages ?? []),
11912
- ...extraImageUrls,
11913
- screenshot,
11914
- ].filter(Boolean));
11943
+ await ensureCodegenUserMessagePersisted(
11944
+ widgetId,
11945
+ prompt,
11946
+ uniquePersistImageUrls([
11947
+ ...extraImageUrls,
11948
+ ...(draftPanelState.promptImages ?? []),
11949
+ screenshot,
11950
+ ]),
11951
+ );
11915
11952
  // NOTE: never copy currentChatConversationId from the draft — the draft
11916
11953
  // chat's conversation is a local UUID that doesn't exist in the DB, and
11917
11954
  // migrating it would point codegen/summaries at a phantom conversation.
@@ -12882,6 +12919,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12882
12919
  panelTopOffset={widgetsPanelTopOffset}
12883
12920
  panelBottomMargin={embedded ? 24 : widgetsPanelBottomMargin}
12884
12921
  fillViewport={false}
12922
+ showCompositionSteps={canvasAdvancedFeatures}
12885
12923
  currentStep={ps.widgetDetailsStep}
12886
12924
  on:focusPanel={() => focusWidgetPanel(panelWidgetId)}
12887
12925
  on:stepChange={({ detail }) => {
@@ -13209,22 +13247,21 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13209
13247
  <div class="generation-chat-sticky-header shrink-0 z-20 bg-white border-b border-gray-200">
13210
13248
  <!-- svelte-ignore a11y_label_has_associated_control -->
13211
13249
  <div class="generation-models-header flex items-center justify-end gap-2 px-2 py-1.5 bg-gray-50">
13212
- <span class="text-[10px] font-semibold text-gray-500 uppercase tracking-wide shrink-0">Model</span>
13213
13250
  <div class="coder-model-selector flex items-center justify-end min-w-0">
13214
- <select
13215
- class="coder-model-dropdown w-max max-w-full text-[11px] border border-gray-300 rounded px-1.5 py-0.5 bg-white text-gray-700 cursor-pointer focus:outline-none focus:border-blue-400"
13216
- style="width: {Math.max(18, (availableModels.reduce((longest, model) => model.name.length > longest.length ? model.name : longest, '').length) + 3)}ch"
13217
- value={userSelectedModelId}
13218
- onchange={(e) => handleModelSelect(e.currentTarget.value)}
13219
- >
13220
- {#if modelsLoading}
13221
- <option value="" disabled>Loading...</option>
13222
- {:else}
13223
- {#each availableModels as model (model.id)}
13224
- <option value={model.id}>{model.name}</option>
13225
- {/each}
13226
- {/if}
13227
- </select>
13251
+ <SelectorDropdown
13252
+ class="coder-model-dropdown"
13253
+ label="Model"
13254
+ labelPosition="inline"
13255
+ items={modelDropdownItems}
13256
+ selectedId={userSelectedModelId}
13257
+ placeholder={modelsLoading ? 'Loading…' : 'Select model'}
13258
+ nameWidth="240px"
13259
+ accentColor="#314F2F"
13260
+ showNewButton={false}
13261
+ allowRename={false}
13262
+ loading={modelsLoading}
13263
+ onselect={(item) => handleModelSelect(item.id)}
13264
+ />
13228
13265
  </div>
13229
13266
  </div>
13230
13267
  </div>
@@ -285,6 +285,12 @@
285
285
  // ═══════════════════════════════════════════════════════════════════════
286
286
  type WidgetStep = 'create' | 'edit' | 'embed';
287
287
  export let currentStep: WidgetStep = 'create';
288
+ /**
289
+ * Canvas Creator is code-only (chat + preview). Edit/Embed header steps stay
290
+ * off until composition editing lives on /composition. Keep the Edit/Embed
291
+ * panels mounted (CSS-hidden) so we can turn this back on without ripping slots.
292
+ */
293
+ export let showCompositionSteps: boolean = false;
288
294
  /** After Set live, Create preview stays on that published artifact until the next Worker build. */
289
295
  let pinnedPublishedPreviewUrl: string | null = null;
290
296
 
@@ -308,8 +314,15 @@
308
314
  previewError = null;
309
315
  if (!iframeSrc) previewLoading = true;
310
316
  }
311
- $: canEditStep = lastPublishedVersion !== null && !isGeneratingCode;
312
- $: canEmbedStep = lastPublishedVersion !== null && !isGeneratingCode;
317
+ $: canEditStep = showCompositionSteps && lastPublishedVersion !== null && !isGeneratingCode;
318
+ $: canEmbedStep = showCompositionSteps && lastPublishedVersion !== null && !isGeneratingCode;
319
+ $: activeWorkflowStep = showCompositionSteps ? currentStep : 'create';
320
+
321
+ $: if (!showCompositionSteps && currentStep !== 'create') {
322
+ currentStep = 'create';
323
+ dispatch('stepChange', { step: 'create' });
324
+ switchPreviewUrlForStep('create');
325
+ }
313
326
 
314
327
  // Auto-switch back to Create when code generation starts
315
328
  let _justFinishedGenerating = false;
@@ -358,6 +371,7 @@
358
371
 
359
372
  function canNavigateToStep(step: WidgetStep): boolean {
360
373
  if (step === 'create') return true;
374
+ if (!showCompositionSteps) return false;
361
375
  if (step === 'edit') return canEditStep;
362
376
  if (step === 'embed') return canEmbedStep;
363
377
  return false;
@@ -978,6 +992,7 @@
978
992
  let detailsDragBarEl: HTMLElement | null = null;
979
993
  let widgetDetailsNameCtEl: HTMLElement | null = null;
980
994
  let widgetDetailsStepsRowEl: HTMLElement | null = null;
995
+ let widgetDetailsDebugButtonsEl: HTMLElement | null = null;
981
996
  let headerNameMaxWidthPx = 96;
982
997
  let detailsDragStartX = 0;
983
998
  let detailsDragStartY = 0;
@@ -2818,10 +2833,14 @@
2818
2833
  }
2819
2834
 
2820
2835
  function updateHeaderNameMaxWidth() {
2821
- if (!widgetDetailsNameCtEl || !widgetDetailsStepsRowEl) return;
2836
+ if (!widgetDetailsNameCtEl) return;
2822
2837
  const nameRect = widgetDetailsNameCtEl.getBoundingClientRect();
2823
- const stepsRect = widgetDetailsStepsRowEl.getBoundingClientRect();
2824
- const available = Math.floor(stepsRect.left - nameRect.left - HEADER_ELEMENTS_GAP_PX);
2838
+ const rightEl = showCompositionSteps && widgetDetailsStepsRowEl
2839
+ ? widgetDetailsStepsRowEl
2840
+ : widgetDetailsDebugButtonsEl;
2841
+ if (!rightEl) return;
2842
+ const rightRect = rightEl.getBoundingClientRect();
2843
+ const available = Math.floor(rightRect.left - nameRect.left - HEADER_ELEMENTS_GAP_PX);
2825
2844
  const next = Math.max(HEADER_NAME_MIN_WIDTH_PX, available);
2826
2845
  if (next !== headerNameMaxWidthPx) {
2827
2846
  headerNameMaxWidthPx = next;
@@ -3359,6 +3378,7 @@
3359
3378
  }}
3360
3379
  />
3361
3380
  </div>
3381
+ {#if showCompositionSteps}
3362
3382
  <div
3363
3383
  class="widget-details-step-indicator-row pointer-events-none absolute left-1/2 top-1/2 z-0 flex -translate-x-1/2 -translate-y-1/2 justify-center"
3364
3384
  bind:this={widgetDetailsStepsRowEl}
@@ -3421,8 +3441,12 @@
3421
3441
  </HoverTooltip>
3422
3442
  </div>
3423
3443
  </div>
3444
+ {/if}
3424
3445
 
3425
- <div class="widget-details-debug-buttons relative z-[1] ml-auto flex items-center shrink-0">
3446
+ <div
3447
+ class="widget-details-debug-buttons relative z-[1] ml-auto flex items-center shrink-0"
3448
+ bind:this={widgetDetailsDebugButtonsEl}
3449
+ >
3426
3450
  <HoverTooltip text={previewColumnCollapsed ? 'Open live preview' : 'Close live preview'}>
3427
3451
  <button
3428
3452
  type="button"
@@ -3505,7 +3529,7 @@
3505
3529
  <div class="actions-panel widget-details-actions-panel relative flex flex-col flex-1 min-h-0">
3506
3530
 
3507
3531
  <!-- Keep all steps mounted — switching tabs must not destroy chat / PropsEditor state -->
3508
- <div class="widget-details-create-step flex flex-col flex-1 min-h-0 overflow-hidden overflow-x-hidden {currentStep === 'create' ? '' : 'hidden'}">
3532
+ <div class="widget-details-create-step flex flex-col flex-1 min-h-0 overflow-hidden overflow-x-hidden {activeWorkflowStep === 'create' ? '' : 'hidden'}">
3509
3533
  <div class="widget-details-create-models-ct shrink-0 empty:hidden">
3510
3534
  <slot name="code-generation-models" />
3511
3535
  </div>
@@ -3523,10 +3547,10 @@
3523
3547
  <slot name="publish-widget" />
3524
3548
  </div>
3525
3549
  </div>
3526
- <div class="widget-details-edit-step flex flex-col flex-1 min-h-0 overflow-hidden {currentStep === 'edit' ? '' : 'hidden'}">
3550
+ <div class="widget-details-edit-step flex flex-col flex-1 min-h-0 overflow-hidden {activeWorkflowStep === 'edit' ? '' : 'hidden'}">
3527
3551
  <slot name="composition-editor" />
3528
3552
  </div>
3529
- <div class="widget-details-embed-step flex flex-col flex-1 min-h-0 overflow-y-auto overflow-x-hidden scrollbar-thin gap-2 p-2 {currentStep === 'embed' ? '' : 'hidden'}">
3553
+ <div class="widget-details-embed-step flex flex-col flex-1 min-h-0 overflow-y-auto overflow-x-hidden scrollbar-thin gap-2 p-2 {activeWorkflowStep === 'embed' ? '' : 'hidden'}">
3530
3554
  <slot name="embed-section" />
3531
3555
  </div>
3532
3556
 
@@ -99,6 +99,11 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
99
99
  /** Cascade offset index (0 = centered, 1+ = shifted down-right). */ panelCascadeIndex?: number;
100
100
  /** False when another panel has keyboard/chat focus. */ isFocusedPanel?: boolean;
101
101
  currentStep?: "embed" | "create" | "edit";
102
+ /**
103
+ * Canvas Creator is code-only (chat + preview). Edit/Embed header steps stay
104
+ * off until composition editing lives on /composition. Keep the Edit/Embed
105
+ * panels mounted (CSS-hidden) so we can turn this back on without ripping slots.
106
+ */ showCompositionSteps?: boolean;
102
107
  beginCompositionHydration?: () => void;
103
108
  showPublishedArtifact?: (url: string | null) => void;
104
109
  finishCompositionHydration?: () => void;
@@ -6,7 +6,7 @@ export interface CreatorAppProps {
6
6
  initialWidgetId?: string | null;
7
7
  /** Select this composition when opening Edit step from site. */
8
8
  initialCompositionId?: string | null;
9
- /** Open Widget Details on this step when embedded from site (overrides auto step once). */
9
+ /** Open Widget Details on this step when embedded from site (overrides auto step once). Ignored while Creator composition steps are hidden. */
10
10
  initialStep?: 'create' | 'edit' | 'embed';
11
11
  /**
12
12
  * Site "+ New widget": create an empty canvas + Untitled widget instead of
@@ -35,6 +35,7 @@ export interface CreatorAppProps {
35
35
  /**
36
36
  * Extra canvas tools (undo/frame/image/library, zoom 1:1/fit/shortcuts).
37
37
  * Independent of debugMode — Debug is staging-only UI.
38
+ * Also shows Widget Details Edit/Embed steps while ON (dev testing).
38
39
  * Site owns the header toggle; embed uses CustomEvent after mount.
39
40
  */
40
41
  initialAdvancedFeatures?: boolean;
@@ -0,0 +1,5 @@
1
+ import type { WidgetsApi } from '@widgetic/api-sdk';
2
+ /**
3
+ * Upload a convert/preview screenshot to widgets.thumbnail_url via the API gateway.
4
+ */
5
+ export declare function persistWidgetThumbnail(widgetsClient: WidgetsApi | null | undefined, widgetId: string | null | undefined, imageDataUrl: string | null | undefined): Promise<string | null>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Upload a convert/preview screenshot to widgets.thumbnail_url via the API gateway.
3
+ */
4
+ export async function persistWidgetThumbnail(widgetsClient, widgetId, imageDataUrl) {
5
+ if (!widgetsClient || !widgetId || typeof imageDataUrl !== 'string')
6
+ return null;
7
+ if (!imageDataUrl.startsWith('data:image/'))
8
+ return null;
9
+ try {
10
+ const response = await widgetsClient.uploadWidgetThumbnailRaw({
11
+ id: widgetId,
12
+ uploadWidgetThumbnailRequest: { imageDataUrl },
13
+ });
14
+ const json = await response.raw.json();
15
+ const thumbnailUrl = json?.data?.thumbnailUrl ||
16
+ json?.data?.thumbnail_url ||
17
+ json?.thumbnailUrl ||
18
+ null;
19
+ if (thumbnailUrl) {
20
+ console.log('[persistWidgetThumbnail] stored', widgetId.slice(0, 8), thumbnailUrl.slice(0, 80));
21
+ }
22
+ return thumbnailUrl;
23
+ }
24
+ catch (error) {
25
+ console.warn('[persistWidgetThumbnail] failed', widgetId?.slice(0, 8), error);
26
+ return null;
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widgetic/creator",
3
- "version": "0.3.55",
3
+ "version": "0.3.57",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",