@widgetic/creator 0.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/dist/CreatorApp.svelte +11821 -0
- package/dist/CreatorApp.svelte.d.ts +41 -0
- package/dist/components/EditableName.svelte +94 -0
- package/dist/components/EditableName.svelte.d.ts +27 -0
- package/dist/components/SelectorDropdown.svelte +238 -0
- package/dist/components/SelectorDropdown.svelte.d.ts +41 -0
- package/dist/components/WidgetDetails.svelte +3127 -0
- package/dist/components/WidgetDetails.svelte.d.ts +235 -0
- package/dist/components/index.d.ts +0 -0
- package/dist/components/index.js +4 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/creator-types.d.ts +64 -0
- package/dist/creator-types.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/localCacheGate.d.ts +7 -0
- package/dist/localCacheGate.js +29 -0
- package/dist/pageHelpers.d.ts +15 -0
- package/dist/pageHelpers.js +194 -0
- package/dist/stores/userSession.d.ts +7 -0
- package/dist/stores/userSession.js +32 -0
- package/dist/stores/websocketStore.d.ts +53 -0
- package/dist/stores/websocketStore.js +289 -0
- package/dist/syncSiteAuth.d.ts +9 -0
- package/dist/syncSiteAuth.js +53 -0
- package/dist/utils/creatorDraftStorage.d.ts +17 -0
- package/dist/utils/creatorDraftStorage.js +87 -0
- package/dist/utils/embedCode.d.ts +51 -0
- package/dist/utils/embedCode.js +69 -0
- package/dist/utils/models.d.ts +4 -0
- package/dist/utils/models.js +94 -0
- package/dist/utils/operationStream.d.ts +29 -0
- package/dist/utils/operationStream.js +116 -0
- package/dist/utils/prototypes.d.ts +0 -0
- package/dist/utils/prototypes.js +20 -0
- package/dist/utils/widgeticChatUpload.d.ts +26 -0
- package/dist/utils/widgeticChatUpload.js +148 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +38 -0
- package/package.json +124 -0
|
@@ -0,0 +1,3127 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* WidgetDetails.svelte
|
|
4
|
+
*
|
|
5
|
+
* Encapsulates Cloudflare Worker preview/iframe, code generation,
|
|
6
|
+
* publishing, and the inline panel (preview tab).
|
|
7
|
+
*
|
|
8
|
+
* Exported interface:
|
|
9
|
+
* Props: userSession, hasValidToken, selectedWidgetId, selectedWidget, etc.
|
|
10
|
+
* Events: generateCode, widgetReady, previewReady, widgetDetailsToggle, widgetDataChanged
|
|
11
|
+
* Functions: loadWidget(), refreshPreview(), enqueueAction(), openWidgetDetails(), closeWidgetDetails()
|
|
12
|
+
*/
|
|
13
|
+
import { onMount, onDestroy, afterUpdate, tick, createEventDispatcher } from 'svelte';
|
|
14
|
+
import { get } from 'svelte/store';
|
|
15
|
+
import { Button, showToast, GenerateLoader } from '@widgetic/design-system/components';
|
|
16
|
+
import {
|
|
17
|
+
AgentApi
|
|
18
|
+
} from '@widgetic/api-sdk';
|
|
19
|
+
import { maxCharsFromId } from '../constants.js';
|
|
20
|
+
import {
|
|
21
|
+
sanitizePreviewBuildError,
|
|
22
|
+
isInfrastructurePreviewError
|
|
23
|
+
} from '../pageHelpers.js';
|
|
24
|
+
import {
|
|
25
|
+
widgetPublishStatus,
|
|
26
|
+
codeGenerationStatus,
|
|
27
|
+
connectWebSocket,
|
|
28
|
+
setAuthToken as setWsAuthToken,
|
|
29
|
+
dynamicWorkerPreviewStatus
|
|
30
|
+
} from '../stores/websocketStore.js';
|
|
31
|
+
import { userSession as userSessionStore } from '../stores/userSession.js';
|
|
32
|
+
|
|
33
|
+
import { WidgetPreview } from '@widgetic/editor';
|
|
34
|
+
import EditableName from './EditableName.svelte';
|
|
35
|
+
|
|
36
|
+
const dispatch = createEventDispatcher();
|
|
37
|
+
|
|
38
|
+
function getAuthJwt(): string | undefined {
|
|
39
|
+
return userSession?.jwt ?? get(userSessionStore)?.jwt ?? undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
43
|
+
// PROPS (from parent)
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
interface WidgetSummary {
|
|
47
|
+
id?: string;
|
|
48
|
+
created_at?: string | Date;
|
|
49
|
+
createdAt?: string | Date;
|
|
50
|
+
updated_at?: string | Date;
|
|
51
|
+
updatedAt?: string | Date;
|
|
52
|
+
repositoryId?: string | null;
|
|
53
|
+
repository_id?: string | null;
|
|
54
|
+
repoId?: string | null;
|
|
55
|
+
name?: string;
|
|
56
|
+
lastCpgCommitSha?: string | null;
|
|
57
|
+
[extra: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type QueuedAction =
|
|
61
|
+
| { type: 'generate'; prompt: string }
|
|
62
|
+
| { type: 'save' };
|
|
63
|
+
|
|
64
|
+
export let userSession: { jwt?: string; user?: { id?: string } } | null = null;
|
|
65
|
+
export let hasValidToken: boolean = false;
|
|
66
|
+
export let selectedWidgetId: string | null = null;
|
|
67
|
+
export let selectedWidget: WidgetSummary | null = null;
|
|
68
|
+
export let isGeneratingCode: boolean = false;
|
|
69
|
+
|
|
70
|
+
// When true, suppresses auto-opening the base preview iframe on VM ready.
|
|
71
|
+
// Used for canvas-converted widgets where we want to skip the default Widget.html
|
|
72
|
+
// and jump straight to code generation.
|
|
73
|
+
export let skipBasePreview: boolean = false;
|
|
74
|
+
/** 'pending' = repo being created (show loader); 'failed' = show Create Repository; 'ready' = normal. */
|
|
75
|
+
export let repositorySetupStatus: 'pending' | 'ready' | 'failed' = 'ready';
|
|
76
|
+
export let generateCodeStatus: string | null = null;
|
|
77
|
+
export let isCheckingActiveGeneration: boolean = false;
|
|
78
|
+
export let lastCommitId: string | null = null;
|
|
79
|
+
export let debugMode: boolean = false;
|
|
80
|
+
export let forceMockReady: boolean = false;
|
|
81
|
+
export let debugModeAvailable: boolean = false;
|
|
82
|
+
|
|
83
|
+
// Code gen / save / publish state — passed through to parent's slot content.
|
|
84
|
+
// svelte-ignore export_let_unused — these are consumed by <svelte:fragment> slots in the parent.
|
|
85
|
+
export let lastGenerationFailed: boolean = false;
|
|
86
|
+
export let lastGenerationError: string | null = null;
|
|
87
|
+
export let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = 'idle';
|
|
88
|
+
export let hasChangesToSave: boolean = false;
|
|
89
|
+
export let isSaveDisabled: boolean = true;
|
|
90
|
+
export let isGenerateDisabled: boolean = true;
|
|
91
|
+
export let isPublishDisabled: boolean = true;
|
|
92
|
+
export let isPublishing: boolean = false;
|
|
93
|
+
export let publishWidgetStatus: string | null = null;
|
|
94
|
+
export let publishNeedsSaveFirst: boolean = false;
|
|
95
|
+
export let lastPublishedVersion: number | null = null;
|
|
96
|
+
export let widgetJsPath: string | null = null;
|
|
97
|
+
export let isGeneratingInOtherTab: boolean = false;
|
|
98
|
+
export let isPublishingInOtherTab: boolean = false;
|
|
99
|
+
export let isSettingLiveVersion: number | null = null;
|
|
100
|
+
export let isHistoryOpen: boolean = false;
|
|
101
|
+
export let isLoadingHistory: boolean = false;
|
|
102
|
+
export let publishHistory: any[] = [];
|
|
103
|
+
|
|
104
|
+
// API client passed from parent (already authenticated)
|
|
105
|
+
export let agentClient: AgentApi;
|
|
106
|
+
|
|
107
|
+
// Prompt state — passed through to parent's slot content.
|
|
108
|
+
export let codeGenerationPrompt: string = '';
|
|
109
|
+
export let promptImages: string[] = [];
|
|
110
|
+
export let imageInputRef: HTMLInputElement | null = null;
|
|
111
|
+
export let isDraggingImage: boolean = false;
|
|
112
|
+
export let MAX_PROMPT_IMAGES: number = 5;
|
|
113
|
+
export let selectedModel: { id: string; name: string; provider: string; description: string } | null = null;
|
|
114
|
+
export let availableModels: { id: string; name: string; provider: string; description: string }[] = [];
|
|
115
|
+
export let userSelectedModelId: string = '';
|
|
116
|
+
export let modelsLoading: boolean = false;
|
|
117
|
+
|
|
118
|
+
/** Published widget URL for Embed step (may include compositionId). Set by parent. */
|
|
119
|
+
export let embedPreviewUrl: string | null = null;
|
|
120
|
+
/** Published widget base URL for Edit step when live matches latest commit. Null → Worker preview. */
|
|
121
|
+
export let editPreviewUrl: string | null = null;
|
|
122
|
+
/** True when generated code is ahead of the live published artifact. */
|
|
123
|
+
export let hasUnpublishedChanges: boolean = false;
|
|
124
|
+
/** Host API root (embedded site passes PUBLIC_API_URL). Used for published artifact proxy URLs. */
|
|
125
|
+
export let apiUrl: string | null = null;
|
|
126
|
+
/** Host Widget Builder origin (site PUBLIC_WIDGET_BUILDER_URL). */
|
|
127
|
+
export let widgetBuilderUrl: string | null = null;
|
|
128
|
+
/**
|
|
129
|
+
* Viewport top inset for centering/clamping the floating panel (site header / creator top bar).
|
|
130
|
+
* Parent should pass the embedded-aware value; null keeps the legacy auth-panel estimate.
|
|
131
|
+
*/
|
|
132
|
+
export let panelTopOffset: number | null = null;
|
|
133
|
+
/** Viewport bottom inset when sizing the default panel height. */
|
|
134
|
+
export let panelBottomMargin: number | null = null;
|
|
135
|
+
/**
|
|
136
|
+
* When true (site embed), open the panel nearly full-viewport so Create/Edit
|
|
137
|
+
* chat + preview have room — not a narrow floating card over the canvas.
|
|
138
|
+
*/
|
|
139
|
+
export let fillViewport: boolean = false;
|
|
140
|
+
|
|
141
|
+
/** Stacking order when multiple Widget Details panels are open. */
|
|
142
|
+
export let panelZIndex = 100;
|
|
143
|
+
/** Cascade offset index (0 = centered, 1+ = shifted down-right). */
|
|
144
|
+
export let panelCascadeIndex = 0;
|
|
145
|
+
/** False when another panel has keyboard/chat focus. */
|
|
146
|
+
export let isFocusedPanel = true;
|
|
147
|
+
|
|
148
|
+
// Reference slot-only props so Svelte doesn't warn about unused exports
|
|
149
|
+
$: void [generateCodeStatus, lastGenerationFailed, lastGenerationError, saveStatus,
|
|
150
|
+
hasChangesToSave, isSaveDisabled, isGenerateDisabled, isPublishDisabled,
|
|
151
|
+
isPublishing, publishWidgetStatus, publishNeedsSaveFirst, lastPublishedVersion,
|
|
152
|
+
isGeneratingInOtherTab, isPublishingInOtherTab, isSettingLiveVersion,
|
|
153
|
+
isHistoryOpen, isLoadingHistory, publishHistory,
|
|
154
|
+
codeGenerationPrompt, promptImages, imageInputRef, isDraggingImage,
|
|
155
|
+
MAX_PROMPT_IMAGES, selectedModel, availableModels, userSelectedModelId, modelsLoading,
|
|
156
|
+
embedPreviewUrl, editPreviewUrl, hasUnpublishedChanges, apiUrl, panelTopOffset, panelBottomMargin];
|
|
157
|
+
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
159
|
+
// CLOUDFLARE WORKER CONFIG
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
161
|
+
// Prefer host-passed Widget Builder URL (site PUBLIC_WIDGET_BUILDER_URL) over package VITE_*.
|
|
162
|
+
$: widgetBuilderPublicBaseUrl = (
|
|
163
|
+
widgetBuilderUrl ||
|
|
164
|
+
import.meta.env.VITE_WIDGET_BUILDER_URL ||
|
|
165
|
+
import.meta.env.PUBLIC_WIDGET_BUILDER_URL ||
|
|
166
|
+
'https://wbuilder.widgetic.com'
|
|
167
|
+
).replace(/\/+$/, '');
|
|
168
|
+
/**
|
|
169
|
+
* In embedded site DEV, same-origin `/__widget_builder__` proxies to the remote Worker
|
|
170
|
+
* (see site vite.config). That is intentional — not a local wrangler instance.
|
|
171
|
+
* Direct public URL is used when the host is not the Vite app (or when URL is non-wbuilder).
|
|
172
|
+
*/
|
|
173
|
+
$: widgetBuilderFetchBaseUrl = (import.meta.env.DEV && widgetBuilderPublicBaseUrl.includes('wbuilder.widgetic.com'))
|
|
174
|
+
? '/__widget_builder__'
|
|
175
|
+
: widgetBuilderPublicBaseUrl;
|
|
176
|
+
const cdnPublicBaseUrl = (import.meta.env.VITE_CDN_URL || 'https://cdn.widgetic.com').replace(/\/+$/, '');
|
|
177
|
+
const cdnFetchBaseUrl = (import.meta.env.DEV && cdnPublicBaseUrl.includes('cdn.widgetic.com'))
|
|
178
|
+
? '/__cdn_widgets__'
|
|
179
|
+
: cdnPublicBaseUrl;
|
|
180
|
+
|
|
181
|
+
function buildWorkerPreviewUrl(widgetId: string, opts: { forDisplay?: boolean } = {}): string {
|
|
182
|
+
const base = opts.forDisplay ? widgetBuilderPublicBaseUrl : widgetBuilderFetchBaseUrl;
|
|
183
|
+
return `${base}/preview/${widgetId}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Map public preview URLs to same-origin fetch paths in dev (iframe embed). */
|
|
187
|
+
function toPreviewFetchUrl(url: string): string {
|
|
188
|
+
if (!import.meta.env.DEV) return url;
|
|
189
|
+
if (url.startsWith('/__widget_builder__') || url.startsWith('/__cdn_widgets__')) return url;
|
|
190
|
+
// Published artifact path without host — route through Vite CDN proxy in dev
|
|
191
|
+
if (url.startsWith('/widgets/')) {
|
|
192
|
+
return `${cdnFetchBaseUrl}${url}`;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const parsed = new URL(url, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
196
|
+
// Dev iframe src is often absolute same-origin proxy (localhost:5174/__cdn_widgets__/...)
|
|
197
|
+
if (parsed.pathname.startsWith('/__cdn_widgets__/')) {
|
|
198
|
+
return `${cdnFetchBaseUrl}${parsed.pathname.replace('/__cdn_widgets__', '')}${parsed.search}`;
|
|
199
|
+
}
|
|
200
|
+
if (parsed.pathname.startsWith('/__widget_builder__/')) {
|
|
201
|
+
return `${widgetBuilderFetchBaseUrl}${parsed.pathname.replace('/__widget_builder__', '')}${parsed.search}`;
|
|
202
|
+
}
|
|
203
|
+
const workerOrigin = new URL(widgetBuilderPublicBaseUrl).origin;
|
|
204
|
+
if (parsed.origin === workerOrigin && parsed.pathname.startsWith('/preview/')) {
|
|
205
|
+
return `${widgetBuilderFetchBaseUrl}${parsed.pathname}${parsed.search}`;
|
|
206
|
+
}
|
|
207
|
+
const cdnOrigin = new URL(cdnPublicBaseUrl).origin;
|
|
208
|
+
if (parsed.origin === cdnOrigin && parsed.pathname.startsWith('/widgets/')) {
|
|
209
|
+
return `${cdnFetchBaseUrl}${parsed.pathname}${parsed.search}`;
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// keep as-is
|
|
213
|
+
}
|
|
214
|
+
return url;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function toPreviewDisplayUrl(fetchUrl: string): string {
|
|
218
|
+
if (fetchUrl.startsWith('/__widget_builder__')) {
|
|
219
|
+
return `${widgetBuilderPublicBaseUrl}${fetchUrl.replace('/__widget_builder__', '')}`;
|
|
220
|
+
}
|
|
221
|
+
if (fetchUrl.startsWith('/__cdn_widgets__')) {
|
|
222
|
+
return `${cdnPublicBaseUrl}${fetchUrl.replace('/__cdn_widgets__', '')}`;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const parsed = new URL(fetchUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
226
|
+
if (parsed.pathname.startsWith('/__cdn_widgets__/')) {
|
|
227
|
+
return `${cdnPublicBaseUrl}${parsed.pathname.replace('/__cdn_widgets__', '')}${parsed.search}`;
|
|
228
|
+
}
|
|
229
|
+
if (parsed.pathname.startsWith('/__widget_builder__/')) {
|
|
230
|
+
return `${widgetBuilderPublicBaseUrl}${parsed.pathname.replace('/__widget_builder__', '')}${parsed.search}`;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// keep as-is
|
|
234
|
+
}
|
|
235
|
+
return fetchUrl;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function toWorkerPreviewFetchUrl(url: string): string {
|
|
239
|
+
return toPreviewFetchUrl(url);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function toWorkerPreviewDisplayUrl(fetchUrl: string): string {
|
|
243
|
+
return toPreviewDisplayUrl(fetchUrl);
|
|
244
|
+
}
|
|
245
|
+
$: apiBaseUrl = (
|
|
246
|
+
apiUrl ||
|
|
247
|
+
import.meta.env.VITE_API_URL ||
|
|
248
|
+
import.meta.env.PUBLIC_API_URL ||
|
|
249
|
+
(import.meta.env.DEV ? 'http://localhost:3000' : '')
|
|
250
|
+
).replace(/\/+$/, '');
|
|
251
|
+
|
|
252
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
253
|
+
// INTERNAL STATE — Widget Session
|
|
254
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
255
|
+
|
|
256
|
+
let currentRepositoryId: string | null = null;
|
|
257
|
+
let queuedActions: QueuedAction[] = [];
|
|
258
|
+
let isProcessingQueuedActions = false;
|
|
259
|
+
let previewLoadedForWidgetId: string | null = null;
|
|
260
|
+
|
|
261
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
262
|
+
// INTERNAL STATE — Step Navigation
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
264
|
+
type WidgetStep = 'create' | 'edit' | 'embed';
|
|
265
|
+
export let currentStep: WidgetStep = 'create';
|
|
266
|
+
|
|
267
|
+
const STEPS: { id: WidgetStep; label: string; number: number }[] = [
|
|
268
|
+
{ id: 'create', label: 'Create', number: 1 },
|
|
269
|
+
{ id: 'edit', label: 'Edit', number: 2 },
|
|
270
|
+
{ id: 'embed', label: 'Embed', number: 3 },
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
$: hasWidgetCode = !!lastCommitId || lastPublishedVersion !== null;
|
|
274
|
+
|
|
275
|
+
/** Published widgets must never show the draft "No Code Yet" empty state. */
|
|
276
|
+
$: if (hasWidgetCode && previewError === 'NO_CODE_YET') {
|
|
277
|
+
previewError = lastPublishedVersion !== null
|
|
278
|
+
? 'Published version is not available. Use Rebuild code or republish.'
|
|
279
|
+
: 'Failed to compile widget code from repository.';
|
|
280
|
+
if (lastPublishedVersion !== null) {
|
|
281
|
+
publishedArtifactReachable = false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
$: canEditStep = lastPublishedVersion !== null && !isGeneratingCode;
|
|
285
|
+
$: canEmbedStep = lastPublishedVersion !== null && !isGeneratingCode;
|
|
286
|
+
|
|
287
|
+
// Auto-switch back to Create when code generation starts
|
|
288
|
+
let _justFinishedGenerating = false;
|
|
289
|
+
$: if (isGeneratingCode && currentStep !== 'create') {
|
|
290
|
+
currentStep = 'create';
|
|
291
|
+
switchPreviewUrlForStep('create');
|
|
292
|
+
}
|
|
293
|
+
// Track when generation finishes so we DON'T auto-switch to Edit
|
|
294
|
+
$: if (isGeneratingCode) {
|
|
295
|
+
_justFinishedGenerating = true;
|
|
296
|
+
previewError = null;
|
|
297
|
+
previewHasRuntimeError = false;
|
|
298
|
+
previewRuntimeErrorStatus = null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Default step on widget switch: always open on Create
|
|
302
|
+
let _lastAutoStepWidgetId: string | null = null;
|
|
303
|
+
$: if (selectedWidgetId && selectedWidgetId !== _lastAutoStepWidgetId && !isGeneratingCode) {
|
|
304
|
+
_lastAutoStepWidgetId = selectedWidgetId;
|
|
305
|
+
_lastGoToStepTimestamp = Date.now();
|
|
306
|
+
if (_justFinishedGenerating) {
|
|
307
|
+
_justFinishedGenerating = false;
|
|
308
|
+
}
|
|
309
|
+
currentStep = 'create';
|
|
310
|
+
dispatch('stepChange', { step: 'create' });
|
|
311
|
+
const createBase = normalizePreviewBaseUrl(
|
|
312
|
+
toPreviewFetchUrl(resolvePreviewUrlForStep('create') ?? '')
|
|
313
|
+
);
|
|
314
|
+
if (createBase) _lastEditPreviewBase = createBase;
|
|
315
|
+
switchPreviewUrlForStep('create');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// New / unpublished widgets: keep Edit & Embed visible as progress, but stay on Create.
|
|
319
|
+
$: if (!isGeneratingCode && !canEditStep && currentStep === 'edit') {
|
|
320
|
+
currentStep = 'create';
|
|
321
|
+
dispatch('stepChange', { step: 'create' });
|
|
322
|
+
switchPreviewUrlForStep('create');
|
|
323
|
+
}
|
|
324
|
+
$: if (!isGeneratingCode && !canEmbedStep && currentStep === 'embed') {
|
|
325
|
+
currentStep = canEditStep ? 'edit' : 'create';
|
|
326
|
+
dispatch('stepChange', { step: currentStep });
|
|
327
|
+
switchPreviewUrlForStep(currentStep);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function canNavigateToStep(step: WidgetStep): boolean {
|
|
331
|
+
if (step === 'create') return true;
|
|
332
|
+
if (step === 'edit') return canEditStep;
|
|
333
|
+
if (step === 'embed') return canEmbedStep;
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function syncPreviewDisplayUrlForStep(step: WidgetStep): void {
|
|
338
|
+
const resolved = resolvePreviewUrlForStep(step);
|
|
339
|
+
if (!resolved) return;
|
|
340
|
+
directPreviewUrl = toPreviewDisplayUrl(toPreviewFetchUrl(resolved));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isWorkerPreviewStep(step: WidgetStep): boolean {
|
|
344
|
+
return step === 'create';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function goToStep(step: WidgetStep) {
|
|
348
|
+
if (!canNavigateToStep(step)) return;
|
|
349
|
+
|
|
350
|
+
const previousStep = currentStep;
|
|
351
|
+
_lastGoToStepTimestamp = Date.now();
|
|
352
|
+
_stepSwitchInProgress = true;
|
|
353
|
+
|
|
354
|
+
const prevBase = normalizePreviewBaseUrl(iframeSrc);
|
|
355
|
+
const nextResolved = resolvePreviewUrlForStep(step);
|
|
356
|
+
const nextBase = nextResolved
|
|
357
|
+
? normalizePreviewBaseUrl(toPreviewFetchUrl(nextResolved))
|
|
358
|
+
: null;
|
|
359
|
+
const samePreviewArtifact =
|
|
360
|
+
!!prevBase && !!nextBase && prevBase === nextBase && !!iframeSrc;
|
|
361
|
+
const crossingPreviewFamily =
|
|
362
|
+
(isWorkerPreviewStep(previousStep) && !isWorkerPreviewStep(step))
|
|
363
|
+
|| (!isWorkerPreviewStep(previousStep) && isWorkerPreviewStep(step));
|
|
364
|
+
|
|
365
|
+
currentStep = step;
|
|
366
|
+
dispatch('stepChange', { step });
|
|
367
|
+
|
|
368
|
+
// URL bar always reflects the active step (Worker on Create, CDN on Edit/Embed)
|
|
369
|
+
syncPreviewDisplayUrlForStep(step);
|
|
370
|
+
|
|
371
|
+
// Update base trackers so reactive blocks don't re-trigger on this step change
|
|
372
|
+
if (nextBase) {
|
|
373
|
+
if (step === 'embed') _lastEmbedPreviewBase = nextBase;
|
|
374
|
+
if (step === 'edit') _lastEditPreviewBase = nextBase;
|
|
375
|
+
if (step === 'create') _lastEditPreviewBase = nextBase;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!samePreviewArtifact || crossingPreviewFamily) {
|
|
379
|
+
if (step === 'create' && crossingPreviewFamily && selectedWidgetId) {
|
|
380
|
+
const workerUrl = buildWorkerPreviewUrl(selectedWidgetId);
|
|
381
|
+
dynamicWorkerPreviewUrl = workerUrl;
|
|
382
|
+
syncPreviewDisplayUrlForStep('create');
|
|
383
|
+
void forceReloadPreviewIframe(workerUrl);
|
|
384
|
+
} else {
|
|
385
|
+
switchPreviewUrlForStep(step, { forceReload: crossingPreviewFamily });
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
previewLoading = false;
|
|
389
|
+
previewHydrating = false;
|
|
390
|
+
if (step === 'edit' || step === 'embed') {
|
|
391
|
+
if (previewWidgetReady) {
|
|
392
|
+
dispatch('previewLoaded', { step });
|
|
393
|
+
} else {
|
|
394
|
+
syncPreviewLoadingWithIframe();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
void tick().then(() => {
|
|
400
|
+
_stepSwitchInProgress = false;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Resolve the canonical preview URL for a workflow step (without cache buster). */
|
|
405
|
+
function resolvePreviewUrlForStep(step: WidgetStep): string | null {
|
|
406
|
+
if (step === 'create') {
|
|
407
|
+
return dynamicWorkerPreviewUrl;
|
|
408
|
+
}
|
|
409
|
+
// Use published URL unless probe already confirmed it missing.
|
|
410
|
+
// Waiting for publishedArtifactReachable === true left Edit/Embed on the idle
|
|
411
|
+
// "Preview will appear after code generation" state when the probe never ran.
|
|
412
|
+
if (step === 'edit') {
|
|
413
|
+
if (editPreviewUrl && publishedArtifactReachable !== false) {
|
|
414
|
+
return editPreviewUrl;
|
|
415
|
+
}
|
|
416
|
+
return dynamicWorkerPreviewUrl;
|
|
417
|
+
}
|
|
418
|
+
if (step === 'embed') {
|
|
419
|
+
if (embedPreviewUrl && publishedArtifactReachable !== false) {
|
|
420
|
+
return embedPreviewUrl;
|
|
421
|
+
}
|
|
422
|
+
return dynamicWorkerPreviewUrl;
|
|
423
|
+
}
|
|
424
|
+
return dynamicWorkerPreviewUrl;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let compositionHydrationTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
428
|
+
|
|
429
|
+
export function beginCompositionHydration(): void {
|
|
430
|
+
previewHydrating = true;
|
|
431
|
+
previewLoading = true;
|
|
432
|
+
if (compositionHydrationTimeout) clearTimeout(compositionHydrationTimeout);
|
|
433
|
+
compositionHydrationTimeout = setTimeout(() => {
|
|
434
|
+
console.warn('[WidgetDetails] Composition hydration safety timeout — revealing preview');
|
|
435
|
+
finishCompositionHydration();
|
|
436
|
+
}, 3000);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function finishCompositionHydration(): void {
|
|
440
|
+
if (compositionHydrationTimeout) {
|
|
441
|
+
clearTimeout(compositionHydrationTimeout);
|
|
442
|
+
compositionHydrationTimeout = null;
|
|
443
|
+
}
|
|
444
|
+
previewHydrating = false;
|
|
445
|
+
previewLoading = false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Canonical published/worker URL for equality checks (dev proxy vs CDN public URL must match). */
|
|
449
|
+
function normalizePreviewBaseUrl(url: string | null): string | null {
|
|
450
|
+
if (!url) return null;
|
|
451
|
+
try {
|
|
452
|
+
const fetchUrl = toPreviewFetchUrl(url);
|
|
453
|
+
const displayUrl = toPreviewDisplayUrl(fetchUrl);
|
|
454
|
+
const parsed = new URL(displayUrl);
|
|
455
|
+
parsed.searchParams.delete('_t');
|
|
456
|
+
// Edit + Embed load the same published widget.html; composition is applied via postMessage
|
|
457
|
+
parsed.searchParams.delete('compositionId');
|
|
458
|
+
return parsed.toString();
|
|
459
|
+
} catch {
|
|
460
|
+
return url;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let lastAppliedPreviewFullUrl: string | null = null;
|
|
465
|
+
let lastAppliedPreviewAt = 0;
|
|
466
|
+
|
|
467
|
+
function previewFullUrlsMatch(a: string | null, b: string | null): boolean {
|
|
468
|
+
if (!a || !b) return false;
|
|
469
|
+
try {
|
|
470
|
+
return new URL(a, typeof window !== 'undefined' ? window.location.origin : 'http://localhost').href
|
|
471
|
+
=== new URL(b, typeof window !== 'undefined' ? window.location.origin : 'http://localhost').href;
|
|
472
|
+
} catch {
|
|
473
|
+
return a === b;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function applyPreviewUrlToIframe(
|
|
478
|
+
url: string | null,
|
|
479
|
+
cacheBuster?: number,
|
|
480
|
+
opts: { forceReload?: boolean; skipHydration?: boolean } = {}
|
|
481
|
+
) {
|
|
482
|
+
if (!url) return;
|
|
483
|
+
|
|
484
|
+
const fetchUrl = toPreviewFetchUrl(url);
|
|
485
|
+
const nextBase = normalizePreviewBaseUrl(fetchUrl);
|
|
486
|
+
const currentBase = normalizePreviewBaseUrl(iframeSrc);
|
|
487
|
+
if (!opts.forceReload && nextBase && currentBase && nextBase === currentBase && iframeSrc) {
|
|
488
|
+
directPreviewUrl = toPreviewDisplayUrl(fetchUrl);
|
|
489
|
+
lastPreviewUrlFetchTime = Date.now();
|
|
490
|
+
if (currentStep === 'edit' || currentStep === 'embed') {
|
|
491
|
+
// Same published artifact — never remount iframe; sync composition when already ready
|
|
492
|
+
if (previewWidgetReady) {
|
|
493
|
+
previewLoading = false;
|
|
494
|
+
previewHydrating = false;
|
|
495
|
+
dispatch('previewLoaded', { step: currentStep });
|
|
496
|
+
} else {
|
|
497
|
+
beginCompositionHydration();
|
|
498
|
+
setupPreviewFallbackTimeout();
|
|
499
|
+
syncPreviewLoadingWithIframe();
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
previewLoading = false;
|
|
503
|
+
previewHydrating = false;
|
|
504
|
+
setupPreviewFallbackTimeout();
|
|
505
|
+
syncPreviewLoadingWithIframe();
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
resetPreviewMessageBridge();
|
|
511
|
+
resetConsoleLogsForNewPreviewRun();
|
|
512
|
+
if (!opts.skipHydration && (currentStep === 'edit' || currentStep === 'embed')) {
|
|
513
|
+
beginCompositionHydration();
|
|
514
|
+
}
|
|
515
|
+
directPreviewUrl = toPreviewDisplayUrl(fetchUrl);
|
|
516
|
+
let nextFull = fetchUrl;
|
|
517
|
+
try {
|
|
518
|
+
const parsed = new URL(fetchUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
519
|
+
parsed.searchParams.set('_t', (cacheBuster ?? Date.now()).toString());
|
|
520
|
+
nextFull = parsed.toString();
|
|
521
|
+
} catch {
|
|
522
|
+
nextFull = fetchUrl;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
if (
|
|
527
|
+
previewFullUrlsMatch(nextFull, lastAppliedPreviewFullUrl)
|
|
528
|
+
&& now - lastAppliedPreviewAt < 200
|
|
529
|
+
) {
|
|
530
|
+
directPreviewUrl = toPreviewDisplayUrl(fetchUrl);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
lastAppliedPreviewFullUrl = nextFull;
|
|
534
|
+
lastAppliedPreviewAt = now;
|
|
535
|
+
|
|
536
|
+
// Single navigation via WidgetPreview src binding — do not also set previewIframe.src directly
|
|
537
|
+
iframeSrc = nextFull;
|
|
538
|
+
lastPreviewUrlFetchTime = Date.now();
|
|
539
|
+
if (currentStep === 'create') {
|
|
540
|
+
previewLoading = true;
|
|
541
|
+
}
|
|
542
|
+
setupPreviewFallbackTimeout();
|
|
543
|
+
syncPreviewLoadingWithIframe();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** If iframe already loaded before onload handler attached, clear the loading overlay immediately. */
|
|
547
|
+
function syncPreviewLoadingWithIframe(): void {
|
|
548
|
+
requestAnimationFrame(() => {
|
|
549
|
+
if (!previewLoading || !previewIframe || !iframeSrc) return;
|
|
550
|
+
// Must match the exact target URL (incl. _t) — base-only match caused a false load on stale iframe content
|
|
551
|
+
if (!previewFullUrlsMatch(iframeSrc, previewIframe.src)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (!previewIframe.contentWindow) return;
|
|
555
|
+
try {
|
|
556
|
+
const loc = previewIframe.contentWindow.location.href;
|
|
557
|
+
if (loc && loc !== 'about:blank' && previewFullUrlsMatch(iframeSrc, loc)) {
|
|
558
|
+
handlePreviewIframeLoad();
|
|
559
|
+
}
|
|
560
|
+
} catch {
|
|
561
|
+
handlePreviewIframeLoad();
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Single-navigation cache-bust reload (refresh button, step switch). */
|
|
567
|
+
async function reloadPreviewIframeCacheBust(url: string): Promise<void> {
|
|
568
|
+
const fetchUrl = toPreviewFetchUrl(url);
|
|
569
|
+
const nextBase = normalizePreviewBaseUrl(fetchUrl);
|
|
570
|
+
const currentBase = normalizePreviewBaseUrl(iframeSrc);
|
|
571
|
+
const sameArtifact = !!nextBase && !!currentBase && nextBase === currentBase && !!iframeSrc;
|
|
572
|
+
|
|
573
|
+
previewError = null;
|
|
574
|
+
resetPreviewMessageBridge();
|
|
575
|
+
resetConsoleLogsForNewPreviewRun();
|
|
576
|
+
previewIframeGeneration += 1;
|
|
577
|
+
|
|
578
|
+
if (sameArtifact) {
|
|
579
|
+
// Soft refresh — one navigation, no hydration overlay flash
|
|
580
|
+
if (currentStep === 'create') {
|
|
581
|
+
previewLoading = true;
|
|
582
|
+
} else {
|
|
583
|
+
previewLoading = false;
|
|
584
|
+
previewHydrating = false;
|
|
585
|
+
}
|
|
586
|
+
applyPreviewUrlToIframe(fetchUrl, Date.now(), { forceReload: true, skipHydration: true });
|
|
587
|
+
setupPreviewFallbackTimeout();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
previewLoading = true;
|
|
592
|
+
if (currentStep === 'edit' || currentStep === 'embed') {
|
|
593
|
+
beginCompositionHydration();
|
|
594
|
+
}
|
|
595
|
+
applyPreviewUrlToIframe(fetchUrl, Date.now(), { forceReload: true, skipHydration: false });
|
|
596
|
+
setupPreviewFallbackTimeout();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Force iframe remount + cache-bust after Worker rebuild (avoids stale cached document). */
|
|
600
|
+
async function forceReloadPreviewIframe(url: string): Promise<void> {
|
|
601
|
+
const fetchUrl = toPreviewFetchUrl(url);
|
|
602
|
+
previewError = null;
|
|
603
|
+
previewLoading = true;
|
|
604
|
+
resetConsoleLogsForNewPreviewRun();
|
|
605
|
+
directPreviewUrl = toPreviewDisplayUrl(fetchUrl);
|
|
606
|
+
iframeSrc = 'about:blank';
|
|
607
|
+
previewIframeGeneration++;
|
|
608
|
+
await tick();
|
|
609
|
+
applyPreviewUrlToIframe(fetchUrl, Date.now(), { forceReload: true });
|
|
610
|
+
setupPreviewFallbackTimeout();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function switchPreviewUrlForStep(step: WidgetStep, opts: { forceReload?: boolean } = {}) {
|
|
614
|
+
const url = resolvePreviewUrlForStep(step);
|
|
615
|
+
if (url) {
|
|
616
|
+
applyPreviewUrlToIframe(url, Date.now(), {
|
|
617
|
+
forceReload: opts.forceReload,
|
|
618
|
+
skipHydration: opts.forceReload && step === 'create',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Track last step-switch timestamp to prevent reactive blocks from re-triggering immediately after goToStep
|
|
624
|
+
let _lastGoToStepTimestamp = 0;
|
|
625
|
+
let _stepSwitchInProgress = false;
|
|
626
|
+
|
|
627
|
+
// Embed step only: reload iframe when published base URL genuinely changes (not on step switch — goToStep handles that)
|
|
628
|
+
let _lastEmbedPreviewBase: string | null = null;
|
|
629
|
+
$: if (currentStep === 'embed' && embedPreviewUrl) {
|
|
630
|
+
const embedBase = normalizePreviewBaseUrl(toPreviewFetchUrl(embedPreviewUrl));
|
|
631
|
+
if (embedBase && embedBase !== _lastEmbedPreviewBase) {
|
|
632
|
+
_lastEmbedPreviewBase = embedBase;
|
|
633
|
+
applyPreviewUrlToIframe(embedPreviewUrl);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Edit step only: reload iframe when published base URL genuinely changes
|
|
638
|
+
let _lastEditPreviewBase: string | null = null;
|
|
639
|
+
/** False when HEAD probe shows published CDN artifact is missing (Edit/Embed fallback to Worker). */
|
|
640
|
+
let publishedArtifactReachable: boolean | null = null;
|
|
641
|
+
|
|
642
|
+
function isCdnArtifactMissingError(message: string | null): boolean {
|
|
643
|
+
if (!message) return false;
|
|
644
|
+
return /artifact not found|artifact missing|not available|CDN|404 Object not found|not publicly accessible/i.test(message);
|
|
645
|
+
}
|
|
646
|
+
$: if (currentStep === 'edit') {
|
|
647
|
+
const editSource = resolvePreviewUrlForStep('edit') ?? '';
|
|
648
|
+
const editBase = editSource ? normalizePreviewBaseUrl(toPreviewFetchUrl(editSource)) : null;
|
|
649
|
+
if (editBase && editBase !== _lastEditPreviewBase) {
|
|
650
|
+
_lastEditPreviewBase = editBase;
|
|
651
|
+
// Prop-driven URL changes must apply even right after step switch (parallel panels)
|
|
652
|
+
switchPreviewUrlForStep('edit', { forceReload: !_stepSwitchInProgress });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
657
|
+
// INTERNAL STATE — Preview / iframe
|
|
658
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
659
|
+
|
|
660
|
+
let directPreviewUrl: string | null = null;
|
|
661
|
+
let iframeSrc: string | null = null;
|
|
662
|
+
let previewCacheBuster = Date.now();
|
|
663
|
+
/** Incremented on forced reload — remounts preview iframe to bust browser cache. */
|
|
664
|
+
let previewIframeGeneration = 0;
|
|
665
|
+
let previewLoading = false;
|
|
666
|
+
let previewHydrating = false;
|
|
667
|
+
/** True after iframe sends widgetic:ready — postMessage updates must wait for this. */
|
|
668
|
+
let previewWidgetReady = false;
|
|
669
|
+
let pendingPreviewMessages: Record<string, unknown>[] = [];
|
|
670
|
+
let previewReadyFallbackTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
671
|
+
let hydrationSafetyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
672
|
+
let previewError: string | null = null;
|
|
673
|
+
let previewRetryCount = 0;
|
|
674
|
+
let previewHasRuntimeError = false;
|
|
675
|
+
let previewRuntimeErrorStatus: number | null = null;
|
|
676
|
+
/** Delay overlay so a subsequent widgetic:ready can cancel false positives. */
|
|
677
|
+
let pendingRuntimeErrorTimer: ReturnType<typeof setTimeout> | null = null;
|
|
678
|
+
|
|
679
|
+
function clearPendingRuntimeErrorOverlay(): void {
|
|
680
|
+
if (pendingRuntimeErrorTimer) {
|
|
681
|
+
clearTimeout(pendingRuntimeErrorTimer);
|
|
682
|
+
pendingRuntimeErrorTimer = null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function isBenignPreviewRuntimeError(message: string): boolean {
|
|
687
|
+
const msg = (message || '').trim();
|
|
688
|
+
if (!msg) return true;
|
|
689
|
+
return (
|
|
690
|
+
/NotAllowedError/i.test(msg) ||
|
|
691
|
+
/AbortError/i.test(msg) ||
|
|
692
|
+
/play\(\) (request was interrupted|failed)/i.test(msg) ||
|
|
693
|
+
/The play\(\) request was interrupted/i.test(msg) ||
|
|
694
|
+
/ResizeObserver loop/i.test(msg) ||
|
|
695
|
+
/^Script error\.?$/i.test(msg) ||
|
|
696
|
+
/user didn't interact|autoplay/i.test(msg)
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function looksLikeHardCompileFailure(message: string): boolean {
|
|
701
|
+
return /syntax|compile|parse error|Unexpected token|is not defined|Cannot find module|Failed to fetch dynamically imported|CSS|svelte/i.test(
|
|
702
|
+
message || '',
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
let showPreviewModal = false;
|
|
706
|
+
let previewModalRef: HTMLDivElement | null = null;
|
|
707
|
+
let widgetPreviewComponentRef: { getResizeLimits?: () => { maxWidth: number; maxHeight: number }; fitToContainer?: () => void } | undefined;
|
|
708
|
+
let previewIframe: HTMLIFrameElement | null = null;
|
|
709
|
+
let isLoadingDirectPreviewUrl = false;
|
|
710
|
+
let previewFetchRetryCount = 0;
|
|
711
|
+
let dwLoadTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
712
|
+
let lastPreviewUrlFetchTime = 0;
|
|
713
|
+
let previewSilentRetryCount = 0;
|
|
714
|
+
/** Monotonic counter — stale triggerBuildFromRepo runs must not overwrite a good preview. */
|
|
715
|
+
let buildFromRepoRunId = 0;
|
|
716
|
+
/** Background panels skip preview compile until the user focuses them. */
|
|
717
|
+
let previewBuildDeferred = false;
|
|
718
|
+
let wasFocusedPanel = isFocusedPanel;
|
|
719
|
+
let isRebuildingPreview = false;
|
|
720
|
+
|
|
721
|
+
/** Auto-rebuild attempt counter — reset each widget selection to guard against loops. */
|
|
722
|
+
let widgetNotFoundAutoRebuildAttempts = 0;
|
|
723
|
+
/**
|
|
724
|
+
* After a successful codegen Worker build, GitLab rebuilds overwrite the new HTML with
|
|
725
|
+
* a stale repo tree (commit not visible yet) and the iframe flashes new → old.
|
|
726
|
+
* Ignore force rebuild / widget-not-found auto-rebuild during this window.
|
|
727
|
+
*/
|
|
728
|
+
let codegenPreviewFreshUntil = 0;
|
|
729
|
+
|
|
730
|
+
function isCodegenPreviewFresh(): boolean {
|
|
731
|
+
return Date.now() < codegenPreviewFreshUntil;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** Call when codegen already compiled the Worker preview — do not rebuild from GitLab. */
|
|
735
|
+
export function markCodegenPreviewFresh(): void {
|
|
736
|
+
codegenPreviewFreshUntil = Date.now() + 30_000;
|
|
737
|
+
widgetNotFoundAutoRebuildAttempts = 1;
|
|
738
|
+
console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** True when the iframe is already showing this widget's Worker preview (not about:blank / error). */
|
|
742
|
+
export function hasLiveWorkerPreview(): boolean {
|
|
743
|
+
if (!selectedWidgetId) return false;
|
|
744
|
+
if (!iframeSrc || iframeSrc === 'about:blank') return false;
|
|
745
|
+
if (previewError || previewHasRuntimeError) return false;
|
|
746
|
+
const workerUrl = dynamicWorkerPreviewUrl || buildWorkerPreviewUrl(selectedWidgetId);
|
|
747
|
+
const currentBase = normalizePreviewBaseUrl(iframeSrc);
|
|
748
|
+
const workerBase = normalizePreviewBaseUrl(toPreviewFetchUrl(workerUrl));
|
|
749
|
+
return !!currentBase && !!workerBase && currentBase === workerBase;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Preview resize
|
|
753
|
+
const PREVIEW_MIN_WIDTH = 120;
|
|
754
|
+
const PREVIEW_MIN_HEIGHT = 80;
|
|
755
|
+
const PREVIEW_DEFAULT_WIDTH = 600;
|
|
756
|
+
const PREVIEW_DEFAULT_HEIGHT = 480;
|
|
757
|
+
let previewResizeWidth: number | null = PREVIEW_DEFAULT_WIDTH;
|
|
758
|
+
let previewResizeHeight: number | null = PREVIEW_DEFAULT_HEIGHT;
|
|
759
|
+
let previewAreaEl: HTMLElement | null = null;
|
|
760
|
+
let isResizingPreview = false;
|
|
761
|
+
|
|
762
|
+
$: isPreviewContentReady = !previewLoading && !previewHydrating && !previewError && !previewHasRuntimeError && (!!iframeSrc || !!getFallbackPreviewUrl());
|
|
763
|
+
|
|
764
|
+
const { DEV: isDevMode } = import.meta.env;
|
|
765
|
+
const shouldLogPreviewWarnings = Boolean(isDevMode);
|
|
766
|
+
|
|
767
|
+
// Cloudflare Dynamic Workers — preview URL from Worker (set via WebSocket event)
|
|
768
|
+
let dynamicWorkerPreviewUrl: string | null = null;
|
|
769
|
+
|
|
770
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
771
|
+
// INTERNAL STATE — Preview Readiness
|
|
772
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
773
|
+
|
|
774
|
+
let hasConnectedForDevServerCheck = false;
|
|
775
|
+
let hasAutoOpenedPreview = false;
|
|
776
|
+
|
|
777
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
778
|
+
// INTERNAL STATE — Inline Panel
|
|
779
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
780
|
+
|
|
781
|
+
let inlinePanelView: 'preview' | 'console' | null = 'preview';
|
|
782
|
+
|
|
783
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
784
|
+
// INTERNAL STATE — Console logs from widget iframe
|
|
785
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
786
|
+
|
|
787
|
+
interface ConsoleLogEntry {
|
|
788
|
+
timestamp: Date;
|
|
789
|
+
level: 'log' | 'warn' | 'error' | 'info';
|
|
790
|
+
message: string;
|
|
791
|
+
}
|
|
792
|
+
let consoleLogs: ConsoleLogEntry[] = [];
|
|
793
|
+
let consoleScrollEl: HTMLElement | null = null;
|
|
794
|
+
|
|
795
|
+
function handleWidgetConsoleMessage(event: MessageEvent) {
|
|
796
|
+
if (!event.data || typeof event.data !== 'object') return;
|
|
797
|
+
const { type, level, message, args } = event.data;
|
|
798
|
+
if (type !== 'widgetic:console') return;
|
|
799
|
+
const entry: ConsoleLogEntry = {
|
|
800
|
+
timestamp: new Date(),
|
|
801
|
+
level: level || 'log',
|
|
802
|
+
message: typeof message === 'string' ? message : JSON.stringify(args || message),
|
|
803
|
+
};
|
|
804
|
+
consoleLogs = [...consoleLogs, entry];
|
|
805
|
+
// Auto-scroll to bottom
|
|
806
|
+
requestAnimationFrame(() => {
|
|
807
|
+
if (consoleScrollEl) consoleScrollEl.scrollTop = consoleScrollEl.scrollHeight;
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function resetConsoleLogsForNewPreviewRun(): void {
|
|
812
|
+
consoleLogs = [];
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function formatRuntimeLogLine(entry: ConsoleLogEntry): string {
|
|
816
|
+
const time = entry.timestamp.toISOString().slice(11, 23);
|
|
817
|
+
return `[${time}] [${entry.level}] ${entry.message}`;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Latest preview console lines for codegen iteration (capped). */
|
|
821
|
+
export function getLatestRuntimeLogs(maxLines = 80): string[] {
|
|
822
|
+
if (consoleLogs.length === 0) return [];
|
|
823
|
+
return consoleLogs.slice(-maxLines).map(formatRuntimeLogLine);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
827
|
+
// INTERNAL STATE — Token auto-refresh for preview
|
|
828
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
829
|
+
|
|
830
|
+
const PREVIEW_TOKEN_REFRESH_MS = 25 * 60 * 1000;
|
|
831
|
+
let previewTokenRefreshInterval: ReturnType<typeof setInterval> | null = null;
|
|
832
|
+
const TOKEN_CHECK_INTERVAL_MS = 15 * 1000;
|
|
833
|
+
|
|
834
|
+
// Preview iframe timeout
|
|
835
|
+
const PREVIEW_FIRST_LOAD_TIMEOUT_MS = 90000;
|
|
836
|
+
let previewTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
837
|
+
let previewFallbackHandle: ReturnType<typeof setTimeout> | null = null;
|
|
838
|
+
let previewHealthCheckHandle: ReturnType<typeof setInterval> | null = null;
|
|
839
|
+
|
|
840
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
841
|
+
// INTERNAL STATE — Panel positioning (floating panel)
|
|
842
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
843
|
+
|
|
844
|
+
let showWidgetDetails = false;
|
|
845
|
+
let contentContainerEl: HTMLDivElement | null = null;
|
|
846
|
+
let hasScrollbar = false;
|
|
847
|
+
|
|
848
|
+
const authStatusEstimatedHeight = 140;
|
|
849
|
+
const legacyTopOffset = authStatusEstimatedHeight + 20;
|
|
850
|
+
const legacyBottomMargin = 24;
|
|
851
|
+
$: detailsPanelTopOffset = panelTopOffset ?? legacyTopOffset;
|
|
852
|
+
$: detailsPanelBottomMargin = panelBottomMargin ?? legacyBottomMargin;
|
|
853
|
+
|
|
854
|
+
const DETAILS_PANEL_DEFAULT_WIDTH = 1100;
|
|
855
|
+
const DETAILS_PANEL_MIN_WIDTH = 720;
|
|
856
|
+
const DETAILS_PANEL_MIN_HEIGHT = 500;
|
|
857
|
+
const DETAILS_PANEL_FILL_INSET = 16;
|
|
858
|
+
/** Left column share of the two-col layout (chat / PropsEditor). */
|
|
859
|
+
const LEFT_COL_DEFAULT_PERCENT = 58;
|
|
860
|
+
const LEFT_COL_MIN_PERCENT = 32;
|
|
861
|
+
const LEFT_COL_MAX_PERCENT = 72;
|
|
862
|
+
|
|
863
|
+
let detailsPanelX = -1;
|
|
864
|
+
let detailsPanelY = -1;
|
|
865
|
+
let isDraggingDetails = false;
|
|
866
|
+
let detailsDragStartX = 0;
|
|
867
|
+
let detailsDragStartY = 0;
|
|
868
|
+
let detailsInitialX = 0;
|
|
869
|
+
let detailsInitialY = 0;
|
|
870
|
+
|
|
871
|
+
let detailsPanelWidth = DETAILS_PANEL_DEFAULT_WIDTH;
|
|
872
|
+
let detailsPanelHeight = 0;
|
|
873
|
+
let isResizingDetails = false;
|
|
874
|
+
let detailsResizeStartX = 0;
|
|
875
|
+
let detailsResizeStartY = 0;
|
|
876
|
+
let detailsResizeStartWidth = 0;
|
|
877
|
+
let detailsResizeStartHeight = 0;
|
|
878
|
+
|
|
879
|
+
let leftColPercent = LEFT_COL_DEFAULT_PERCENT;
|
|
880
|
+
let isResizingColumns = false;
|
|
881
|
+
let columnResizeStartX = 0;
|
|
882
|
+
let columnResizeStartPercent = LEFT_COL_DEFAULT_PERCENT;
|
|
883
|
+
|
|
884
|
+
// Admin Delete Modal State — kept in this component since it's inside the panel template
|
|
885
|
+
let showAdminDeleteModal = false;
|
|
886
|
+
let adminDeleteWidget: WidgetSummary | null = null;
|
|
887
|
+
let adminKeyInput = '';
|
|
888
|
+
let adminDeleteLoading = false;
|
|
889
|
+
|
|
890
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
891
|
+
// EXPORTED FUNCTIONS (parent calls via bind:this)
|
|
892
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
893
|
+
|
|
894
|
+
function shouldDeferPreviewNetworkWork(force = false): boolean {
|
|
895
|
+
return !isFocusedPanel && !force;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** Probe whether a published artifact URL is reachable before loading it in the iframe. */
|
|
899
|
+
async function probePublishedArtifactUrl(url: string): Promise<boolean> {
|
|
900
|
+
const fetchUrl = toPreviewFetchUrl(url);
|
|
901
|
+
if (!fetchUrl) return false;
|
|
902
|
+
|
|
903
|
+
// Cross-origin CDN HEAD is blocked by CORS from the site origin; the iframe can still
|
|
904
|
+
// load the artifact. Skip the probe and let iframe onError report real failures.
|
|
905
|
+
try {
|
|
906
|
+
const absolute = new URL(fetchUrl, typeof window !== 'undefined' ? window.location.href : 'https://cdn.widgetic.com');
|
|
907
|
+
const pageOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
908
|
+
const isApiProxy = !!(apiBaseUrl && fetchUrl.startsWith(apiBaseUrl));
|
|
909
|
+
if (pageOrigin && absolute.origin !== pageOrigin && !isApiProxy) {
|
|
910
|
+
console.log('[WidgetDetails] Skipping cross-origin HEAD probe (CORS); loading in iframe:', absolute.origin);
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
} catch {
|
|
914
|
+
/* fall through to HEAD probe */
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
try {
|
|
918
|
+
const probeRes = await fetch(fetchUrl, { method: 'HEAD' });
|
|
919
|
+
if (probeRes.ok) return true;
|
|
920
|
+
console.warn('[WidgetDetails] Published artifact probe failed:', probeRes.status, fetchUrl);
|
|
921
|
+
} catch (probeErr) {
|
|
922
|
+
console.warn('[WidgetDetails] Published artifact probe error:', probeErr);
|
|
923
|
+
}
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Load published CDN preview without Worker probe (safe for background panels). */
|
|
928
|
+
async function loadPublishedPreviewForCurrentStep(): Promise<boolean> {
|
|
929
|
+
if (currentStep === 'edit' && editPreviewUrl) {
|
|
930
|
+
const reachable = await probePublishedArtifactUrl(editPreviewUrl);
|
|
931
|
+
publishedArtifactReachable = reachable;
|
|
932
|
+
if (!reachable) {
|
|
933
|
+
previewError = 'Published version is not available. Use Rebuild code.';
|
|
934
|
+
previewLoading = false;
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
_lastEditPreviewBase = null;
|
|
938
|
+
switchPreviewUrlForStep('edit', { forceReload: true });
|
|
939
|
+
previewBuildDeferred = false;
|
|
940
|
+
previewError = null;
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
if (currentStep === 'embed' && embedPreviewUrl) {
|
|
944
|
+
const reachable = await probePublishedArtifactUrl(embedPreviewUrl);
|
|
945
|
+
publishedArtifactReachable = reachable;
|
|
946
|
+
if (!reachable) {
|
|
947
|
+
previewError = 'Published version is not available. Use Rebuild code.';
|
|
948
|
+
previewLoading = false;
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
_lastEmbedPreviewBase = null;
|
|
952
|
+
applyPreviewUrlToIframe(embedPreviewUrl);
|
|
953
|
+
previewBuildDeferred = false;
|
|
954
|
+
previewError = null;
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** Probe Worker / build-from-repo pipeline without resetting panel state. */
|
|
961
|
+
async function runPreviewLoadPipeline(widgetId: string): Promise<void> {
|
|
962
|
+
if (shouldDeferPreviewNetworkWork()) {
|
|
963
|
+
console.log('[WidgetDetails] Deferring preview load (background panel):', widgetId.substring(0, 8));
|
|
964
|
+
previewBuildDeferred = true;
|
|
965
|
+
previewLoading = false;
|
|
966
|
+
previewError = null;
|
|
967
|
+
const publishedLoaded = await loadPublishedPreviewForCurrentStep();
|
|
968
|
+
if (!publishedLoaded) {
|
|
969
|
+
previewBuildDeferred = true;
|
|
970
|
+
}
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
previewBuildDeferred = false;
|
|
974
|
+
|
|
975
|
+
const workerPreviewUrl = buildWorkerPreviewUrl(widgetId);
|
|
976
|
+
const workerPreviewDisplayUrl = buildWorkerPreviewUrl(widgetId, { forDisplay: true });
|
|
977
|
+
dynamicWorkerPreviewUrl = workerPreviewUrl;
|
|
978
|
+
directPreviewUrl = workerPreviewDisplayUrl;
|
|
979
|
+
previewError = null;
|
|
980
|
+
previewLoading = true;
|
|
981
|
+
|
|
982
|
+
const capturedWidgetId = widgetId;
|
|
983
|
+
const probeResult = await probeWorkerReachable(workerPreviewUrl);
|
|
984
|
+
if (selectedWidgetId !== capturedWidgetId) return;
|
|
985
|
+
|
|
986
|
+
if (probeResult === 'unreachable') {
|
|
987
|
+
console.warn('[WidgetDetails] Worker unreachable at:', workerPreviewUrl);
|
|
988
|
+
previewLoading = false;
|
|
989
|
+
previewError = 'Widget Builder is not reachable. Make sure the Worker is running.';
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (probeResult === 'not-found') {
|
|
994
|
+
console.log('[WidgetDetails] Widget not on Worker — build-from-repo', {
|
|
995
|
+
widgetId: capturedWidgetId.substring(0, 8),
|
|
996
|
+
hasJwt: !!getAuthJwt(),
|
|
997
|
+
});
|
|
998
|
+
if (isGeneratingCode) {
|
|
999
|
+
previewLoading = true;
|
|
1000
|
+
previewError = null;
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
|
|
1004
|
+
if (!hasRepo) {
|
|
1005
|
+
// New widget: repo is still being created — do not call build-from-repo (400 noise).
|
|
1006
|
+
console.log('[WidgetDetails] No repository yet — skipping build-from-repo');
|
|
1007
|
+
previewLoading = false;
|
|
1008
|
+
previewError = 'NO_CODE_YET';
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
await triggerBuildFromRepo();
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
dynamicWorkerPreviewUrl = workerPreviewUrl;
|
|
1016
|
+
directPreviewUrl = workerPreviewUrl;
|
|
1017
|
+
previewLoading = true;
|
|
1018
|
+
switchPreviewUrlForStep(currentStep);
|
|
1019
|
+
console.log('[WidgetDetails] Loading preview iframe:', workerPreviewUrl, 'step:', currentStep);
|
|
1020
|
+
requestAnimationFrame(() => {
|
|
1021
|
+
setTimeout(fitPreviewToArea, 150);
|
|
1022
|
+
});
|
|
1023
|
+
dispatch('buildFromRepoSuccess', { widgetId: capturedWidgetId, source: 'worker-cache' });
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Resume preview after a background panel receives focus. */
|
|
1027
|
+
export async function resumeDeferredPreviewLoad(): Promise<void> {
|
|
1028
|
+
if (!previewBuildDeferred || !selectedWidgetId) return;
|
|
1029
|
+
previewBuildDeferred = false;
|
|
1030
|
+
await runPreviewLoadPipeline(selectedWidgetId);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Single entry point for loading/switching widgets.
|
|
1035
|
+
* Atomically resets ALL internal state and loads Worker preview for the new widget.
|
|
1036
|
+
*/
|
|
1037
|
+
export async function loadWidget(widgetId: string | null): Promise<void> {
|
|
1038
|
+
try {
|
|
1039
|
+
// === Full reset of all widget-scoped state ===
|
|
1040
|
+
consoleLogs = [];
|
|
1041
|
+
// Preview / iframe
|
|
1042
|
+
directPreviewUrl = null;
|
|
1043
|
+
dynamicWorkerPreviewUrl = null;
|
|
1044
|
+
iframeSrc = null;
|
|
1045
|
+
_lastEditPreviewBase = null;
|
|
1046
|
+
_lastEmbedPreviewBase = null;
|
|
1047
|
+
isLoadingDirectPreviewUrl = false;
|
|
1048
|
+
previewHasRuntimeError = false;
|
|
1049
|
+
previewRuntimeErrorStatus = null;
|
|
1050
|
+
previewHydrating = false;
|
|
1051
|
+
resetPreviewMessageBridge();
|
|
1052
|
+
clearPendingRuntimeErrorOverlay();
|
|
1053
|
+
// Keep previewLoading=true if it was set by widget selection (isWidgetLoading)
|
|
1054
|
+
if (!isWidgetLoading) {
|
|
1055
|
+
previewLoading = false;
|
|
1056
|
+
}
|
|
1057
|
+
isWidgetLoading = false;
|
|
1058
|
+
previewError = null;
|
|
1059
|
+
previewRetryCount = 0;
|
|
1060
|
+
lastWsPreviewSignature = null;
|
|
1061
|
+
previewIframeGeneration = 0;
|
|
1062
|
+
publishedArtifactReachable = null;
|
|
1063
|
+
previewLoadedForWidgetId = null;
|
|
1064
|
+
resetPreviewResizeSize();
|
|
1065
|
+
showPreviewModal = false;
|
|
1066
|
+
if (dwLoadTimeoutHandle) { clearTimeout(dwLoadTimeoutHandle); dwLoadTimeoutHandle = null; }
|
|
1067
|
+
|
|
1068
|
+
hasConnectedForDevServerCheck = false;
|
|
1069
|
+
hasAutoOpenedPreview = false;
|
|
1070
|
+
|
|
1071
|
+
// Inline panel — default to preview tab
|
|
1072
|
+
inlinePanelView = 'preview';
|
|
1073
|
+
|
|
1074
|
+
// Token auto-refresh
|
|
1075
|
+
stopPreviewTokenAutoRefresh();
|
|
1076
|
+
|
|
1077
|
+
// Read repository ID from the widget
|
|
1078
|
+
const widgetRepoId = normalizeRepositoryId(selectedWidget);
|
|
1079
|
+
if (widgetRepoId) currentRepositoryId = widgetRepoId;
|
|
1080
|
+
|
|
1081
|
+
// Do not auto-open here — parent owns panel visibility via openWidgetDetailsPanel / close.
|
|
1082
|
+
// Auto-open used to fight the close button (loadWidget after close reopened the panel).
|
|
1083
|
+
if (!widgetId) {
|
|
1084
|
+
closeWidgetDetails({ silent: true });
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
await tick();
|
|
1088
|
+
|
|
1089
|
+
console.log('WidgetDetails: loadWidget completed:', {
|
|
1090
|
+
widgetId,
|
|
1091
|
+
name: selectedWidget?.name,
|
|
1092
|
+
currentRepositoryId,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
dispatch('widgetReady', { widgetId: selectedWidgetId });
|
|
1096
|
+
|
|
1097
|
+
// If skipBasePreview is set, the caller will handle compiling via triggerBuildFromRepo
|
|
1098
|
+
// (e.g. duplicates waiting for GitLab fork, or converts waiting for code generation)
|
|
1099
|
+
if (skipBasePreview) {
|
|
1100
|
+
console.log('[WidgetDetails] skipBasePreview is set — deferring preview load to caller');
|
|
1101
|
+
previewLoading = true;
|
|
1102
|
+
previewError = null;
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (await loadPublishedPreviewForCurrentStep()) {
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Brand-new widget (no commit, not published): skip Worker HEAD / build-from-repo.
|
|
1111
|
+
// Probing localhost:5173/__widget_builder__/preview/... only produces expected 404 noise.
|
|
1112
|
+
const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
|
|
1113
|
+
if (!lastCommitId && lastPublishedVersion === null) {
|
|
1114
|
+
console.log('[WidgetDetails] No code yet — skipping Worker probe/build-from-repo', {
|
|
1115
|
+
widgetId: widgetId?.substring(0, 8),
|
|
1116
|
+
hasRepo,
|
|
1117
|
+
repositorySetupStatus,
|
|
1118
|
+
});
|
|
1119
|
+
previewLoading = false;
|
|
1120
|
+
previewError = 'NO_CODE_YET';
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
previewLoading = true;
|
|
1125
|
+
await runPreviewLoadPipeline(widgetId);
|
|
1126
|
+
if (widgetId) previewLoadedForWidgetId = widgetId;
|
|
1127
|
+
} catch (e) {
|
|
1128
|
+
console.warn('WidgetDetails.loadWidget error:', e);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** Refresh the preview iframe — called by parent after code generation/restore completes */
|
|
1133
|
+
/** Called by the parent right before refreshPreview after a successful publish, so that the
|
|
1134
|
+
* next preview load probes the CDN artifact instead of reusing the stale Worker URL. */
|
|
1135
|
+
export function invalidatePublishedArtifactCache(): void {
|
|
1136
|
+
publishedArtifactReachable = null;
|
|
1137
|
+
_lastEditPreviewBase = null;
|
|
1138
|
+
_lastEmbedPreviewBase = null;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
export async function refreshPreview(opts: { withRetry?: boolean; skipIfAlreadyShowing?: boolean } = {}): Promise<void> {
|
|
1142
|
+
if (opts.skipIfAlreadyShowing && hasLiveWorkerPreview()) {
|
|
1143
|
+
console.log('[WidgetDetails] refreshPreview skipped — Worker preview already showing');
|
|
1144
|
+
previewLoading = false;
|
|
1145
|
+
previewHydrating = false;
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
await refreshPreviewIframe(opts);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/** Trigger build-from-repo with retries — for duplicate/returning widgets.
|
|
1152
|
+
* Unlike loadWidget, this doesn't reset state. Just builds and updates preview.
|
|
1153
|
+
* When force is true, always recompiles from GitLab (evicts Worker cache) even if preview exists. */
|
|
1154
|
+
export async function triggerBuildFromRepo(options?: { force?: boolean }): Promise<void> {
|
|
1155
|
+
const forceRebuild = options?.force === true;
|
|
1156
|
+
if (!selectedWidgetId) return;
|
|
1157
|
+
if (shouldDeferPreviewNetworkWork(forceRebuild)) {
|
|
1158
|
+
console.log('[WidgetDetails] triggerBuildFromRepo deferred (background panel)');
|
|
1159
|
+
previewBuildDeferred = true;
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
|
|
1163
|
+
if (!hasRepo) {
|
|
1164
|
+
console.log('[WidgetDetails] triggerBuildFromRepo skipped — widget has no repository yet');
|
|
1165
|
+
previewLoading = false;
|
|
1166
|
+
previewError = 'NO_CODE_YET';
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const capturedWidgetId = selectedWidgetId;
|
|
1170
|
+
const runId = ++buildFromRepoRunId;
|
|
1171
|
+
if (forceRebuild) isRebuildingPreview = true;
|
|
1172
|
+
try {
|
|
1173
|
+
const workerPreviewUrl = buildWorkerPreviewUrl(capturedWidgetId);
|
|
1174
|
+
const workerPreviewDisplayUrl = buildWorkerPreviewUrl(capturedWidgetId, { forDisplay: true });
|
|
1175
|
+
|
|
1176
|
+
if (forceRebuild && isCodegenPreviewFresh()) {
|
|
1177
|
+
const freshProbe = await probeWorkerReachable(workerPreviewUrl);
|
|
1178
|
+
if (selectedWidgetId !== capturedWidgetId || runId !== buildFromRepoRunId) return;
|
|
1179
|
+
if (freshProbe === 'ok') {
|
|
1180
|
+
console.log('[WidgetDetails] triggerBuildFromRepo: skipping GitLab rebuild — codegen preview is fresh');
|
|
1181
|
+
dynamicWorkerPreviewUrl = workerPreviewUrl;
|
|
1182
|
+
directPreviewUrl = workerPreviewDisplayUrl;
|
|
1183
|
+
inlinePanelView = 'preview';
|
|
1184
|
+
await forceReloadPreviewIframe(workerPreviewUrl);
|
|
1185
|
+
dispatch('buildFromRepoSuccess', { widgetId: capturedWidgetId, source: 'codegen-fresh' });
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const isStaleRun = () =>
|
|
1191
|
+
!isFocusedPanel || selectedWidgetId !== capturedWidgetId || runId !== buildFromRepoRunId;
|
|
1192
|
+
|
|
1193
|
+
/** If Worker already has a preview (e.g. codegen finished while build-from-repo ran), load it. */
|
|
1194
|
+
const tryRecoverPreviewFromWorker = async (): Promise<boolean> => {
|
|
1195
|
+
if (isStaleRun()) return false;
|
|
1196
|
+
const probeResult = await probeWorkerReachable(workerPreviewUrl);
|
|
1197
|
+
if (isStaleRun()) return false;
|
|
1198
|
+
if (probeResult === 'ok') {
|
|
1199
|
+
dynamicWorkerPreviewUrl = workerPreviewUrl;
|
|
1200
|
+
directPreviewUrl = workerPreviewDisplayUrl;
|
|
1201
|
+
inlinePanelView = 'preview';
|
|
1202
|
+
await forceReloadPreviewIframe(workerPreviewUrl);
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
return false;
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
// Helper to load preview from Worker URL (always remount iframe — rebuild may change HTML at same URL)
|
|
1209
|
+
const loadPreviewFromWorker = async () => {
|
|
1210
|
+
dynamicWorkerPreviewUrl = workerPreviewUrl;
|
|
1211
|
+
directPreviewUrl = workerPreviewDisplayUrl;
|
|
1212
|
+
inlinePanelView = 'preview';
|
|
1213
|
+
await forceReloadPreviewIframe(workerPreviewUrl);
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// Step 1: Probe Worker — if widget is already compiled, just show preview (unless forced rebuild)
|
|
1217
|
+
const probeResult = await probeWorkerReachable(workerPreviewUrl);
|
|
1218
|
+
if (isStaleRun()) return;
|
|
1219
|
+
if (!forceRebuild && probeResult === 'ok') {
|
|
1220
|
+
console.log('[WidgetDetails] triggerBuildFromRepo: Worker already has widget, loading preview directly');
|
|
1221
|
+
dispatch('buildFromRepoSuccess', { widgetId: capturedWidgetId });
|
|
1222
|
+
loadPreviewFromWorker();
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (probeResult === 'unreachable') {
|
|
1226
|
+
console.warn('[WidgetDetails] triggerBuildFromRepo: Worker unreachable at', workerPreviewUrl);
|
|
1227
|
+
previewLoading = false;
|
|
1228
|
+
previewError = 'Widget Builder is not reachable. Make sure the Worker is running.';
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Skip build-from-repo while codegen is running — WS / refreshPreview will load when ready
|
|
1233
|
+
if (isGeneratingCode) {
|
|
1234
|
+
console.log('[WidgetDetails] triggerBuildFromRepo: codegen in progress — deferring build-from-repo');
|
|
1235
|
+
previewLoading = true;
|
|
1236
|
+
previewError = null;
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Step 2: Try build-from-repo with retries
|
|
1241
|
+
let lastBuildErrorMsg = '';
|
|
1242
|
+
const authJwt = getAuthJwt();
|
|
1243
|
+
if (authJwt && agentClient) {
|
|
1244
|
+
const maxRetries = 3;
|
|
1245
|
+
const retryDelayMs = 3000;
|
|
1246
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1247
|
+
if (isStaleRun()) return;
|
|
1248
|
+
try {
|
|
1249
|
+
const buildData = await agentClient.agentWidgetBuilderBuildFromRepoWidgetIdPost({
|
|
1250
|
+
widgetId: capturedWidgetId
|
|
1251
|
+
});
|
|
1252
|
+
if (isStaleRun()) return;
|
|
1253
|
+
|
|
1254
|
+
console.log('[WidgetDetails] triggerBuildFromRepo succeeded', { buildTimeMs: buildData?.data?.buildTimeMs ?? buildData?.buildTimeMs, attempt });
|
|
1255
|
+
previewError = null;
|
|
1256
|
+
previewLoading = false;
|
|
1257
|
+
dispatch('widgetDataChanged', { widgetId: capturedWidgetId });
|
|
1258
|
+
dispatch('buildFromRepoSuccess', {
|
|
1259
|
+
widgetId: capturedWidgetId,
|
|
1260
|
+
buildTimeMs: buildData?.data?.buildTimeMs ?? buildData?.buildTimeMs,
|
|
1261
|
+
repoHeadCommitSha:
|
|
1262
|
+
buildData?.data?.repoHeadCommitSha ??
|
|
1263
|
+
buildData?.data?.repo_head_commit_sha ??
|
|
1264
|
+
buildData?.repoHeadCommitSha,
|
|
1265
|
+
});
|
|
1266
|
+
if (isStaleRun()) return;
|
|
1267
|
+
loadPreviewFromWorker();
|
|
1268
|
+
return;
|
|
1269
|
+
} catch (buildErr: any) {
|
|
1270
|
+
let buildErrorCode = '';
|
|
1271
|
+
lastBuildErrorMsg = buildErr?.message || 'Build request failed';
|
|
1272
|
+
|
|
1273
|
+
// SDK ResponseError may carry API error payload
|
|
1274
|
+
const errBody = buildErr?.response?.value?.() ?? buildErr?.body ?? null;
|
|
1275
|
+
if (errBody && typeof errBody === 'object') {
|
|
1276
|
+
lastBuildErrorMsg = errBody?.error?.message || errBody?.message || lastBuildErrorMsg;
|
|
1277
|
+
buildErrorCode = errBody?.error?.code || errBody?.code || '';
|
|
1278
|
+
}
|
|
1279
|
+
lastBuildErrorMsg = sanitizePreviewBuildError(lastBuildErrorMsg);
|
|
1280
|
+
|
|
1281
|
+
if (buildErrorCode === 'NO_CODE_YET') {
|
|
1282
|
+
console.log('[WidgetDetails] triggerBuildFromRepo: repo exists but has no code yet — skipping retries');
|
|
1283
|
+
lastBuildErrorMsg = 'NO_CODE_YET';
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const status = buildErr?.response?.status ?? buildErr?.status;
|
|
1288
|
+
const isRetryable = status === 404 || status === 500 || !status;
|
|
1289
|
+
if (isRetryable && attempt < maxRetries) {
|
|
1290
|
+
console.log(`[WidgetDetails] triggerBuildFromRepo attempt ${attempt}/${maxRetries} failed (${lastBuildErrorMsg}), retrying in ${retryDelayMs}ms...`);
|
|
1291
|
+
await new Promise(r => setTimeout(r, retryDelayMs));
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
console.warn('[WidgetDetails] triggerBuildFromRepo giving up:', { status, error: lastBuildErrorMsg, attempts: attempt });
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
} else {
|
|
1299
|
+
lastBuildErrorMsg = agentClient
|
|
1300
|
+
? 'Not authenticated'
|
|
1301
|
+
: 'API client is not configured';
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Step 3: All builds failed — decide what to show
|
|
1305
|
+
if (isStaleRun()) return;
|
|
1306
|
+
|
|
1307
|
+
// Forced rebuild (user clicked Rebuild) must surface compile errors — never keep a stale iframe.
|
|
1308
|
+
const allowStalePreviewRecovery = !forceRebuild;
|
|
1309
|
+
|
|
1310
|
+
// Preview may have loaded via codegen/WS while build-from-repo was in flight
|
|
1311
|
+
if (allowStalePreviewRecovery && iframeSrc && !previewHasRuntimeError) {
|
|
1312
|
+
console.log('[WidgetDetails] triggerBuildFromRepo: build failed but preview iframe already loaded — keeping preview');
|
|
1313
|
+
previewError = null;
|
|
1314
|
+
previewLoading = false;
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (allowStalePreviewRecovery && (await tryRecoverPreviewFromWorker())) {
|
|
1319
|
+
console.log('[WidgetDetails] triggerBuildFromRepo: recovered preview from Worker after build-from-repo failure');
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (selectedWidgetId === capturedWidgetId) {
|
|
1324
|
+
// For published widgets, try loading via API proxy (handles both R2 and CDN)
|
|
1325
|
+
if (lastPublishedVersion && widgetJsPath && apiBaseUrl) {
|
|
1326
|
+
const proxyUrl = `${apiBaseUrl}/widgets/${capturedWidgetId}/v${lastPublishedVersion}/widget.html`;
|
|
1327
|
+
console.log('[WidgetDetails] triggerBuildFromRepo: build failed but widget is published — checking API proxy:', proxyUrl);
|
|
1328
|
+
try {
|
|
1329
|
+
const probeRes = await fetch(proxyUrl, { method: 'HEAD' });
|
|
1330
|
+
if (probeRes.ok) {
|
|
1331
|
+
console.log('[WidgetDetails] API proxy returned OK — loading published widget');
|
|
1332
|
+
previewError = null;
|
|
1333
|
+
previewLoading = true;
|
|
1334
|
+
iframeSrc = proxyUrl;
|
|
1335
|
+
setupPreviewFallbackTimeout();
|
|
1336
|
+
syncPreviewLoadingWithIframe();
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
console.warn('[WidgetDetails] API proxy returned', probeRes.status, '— artifact not on R2, showing generate button');
|
|
1340
|
+
} catch (probeErr) {
|
|
1341
|
+
console.warn('[WidgetDetails] API proxy probe failed:', probeErr);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
previewLoading = false;
|
|
1346
|
+
if (forceRebuild && iframeSrc) {
|
|
1347
|
+
iframeSrc = null;
|
|
1348
|
+
directPreviewUrl = null;
|
|
1349
|
+
}
|
|
1350
|
+
if (lastBuildErrorMsg === 'NO_CODE_YET') {
|
|
1351
|
+
if (lastPublishedVersion !== null) {
|
|
1352
|
+
previewError =
|
|
1353
|
+
'Published version is not available and source code could not be compiled. Use Rebuild code or republish.';
|
|
1354
|
+
publishedArtifactReachable = false;
|
|
1355
|
+
} else {
|
|
1356
|
+
previewError = 'NO_CODE_YET';
|
|
1357
|
+
}
|
|
1358
|
+
} else if (!lastCommitId && !lastPublishedVersion) {
|
|
1359
|
+
previewError = 'NO_CODE_YET';
|
|
1360
|
+
} else {
|
|
1361
|
+
previewError = sanitizePreviewBuildError(lastBuildErrorMsg || 'Failed to compile widget code from repository.');
|
|
1362
|
+
}
|
|
1363
|
+
notifyPreviewCompileErrorChanged();
|
|
1364
|
+
dispatch('buildFromRepoFailed', { widgetId: capturedWidgetId });
|
|
1365
|
+
console.warn('[WidgetDetails] triggerBuildFromRepo: all build attempts failed for widget', capturedWidgetId);
|
|
1366
|
+
}
|
|
1367
|
+
} finally {
|
|
1368
|
+
if (forceRebuild) isRebuildingPreview = false;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export function enqueueAction(action: QueuedAction): void {
|
|
1373
|
+
enqueueQueuedAction(action);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
export function getShowWidgetDetails(): boolean {
|
|
1377
|
+
return showWidgetDetails;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
export function getInlinePanelView(): 'preview' | 'console' | null {
|
|
1381
|
+
return inlinePanelView;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
export function setInlinePanelView(view: 'preview' | 'console' | null): void {
|
|
1385
|
+
inlinePanelView = view;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
export function hasValidPreview(): boolean {
|
|
1389
|
+
return !!iframeSrc && !previewError && !previewLoading;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
export function getPreviewIframe(): HTMLIFrameElement | null {
|
|
1393
|
+
return previewIframe;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
export function getCurrentRepositoryId(): string | null {
|
|
1397
|
+
return currentRepositoryId;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
export function setCurrentRepositoryId(repoId: string | null): void {
|
|
1401
|
+
currentRepositoryId = repoId;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
export function setDirectPreviewUrl(url: string | null): void {
|
|
1405
|
+
directPreviewUrl = url;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
export function setIframeSrc(src: string | null): void {
|
|
1409
|
+
iframeSrc = src;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
export function setPreviewHasRuntimeError(val: boolean): void {
|
|
1413
|
+
previewHasRuntimeError = val;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
export function setPreviewRuntimeErrorStatus(val: number | null): void {
|
|
1417
|
+
previewRuntimeErrorStatus = val;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
export function setHasAutoOpenedPreview(val: boolean): void {
|
|
1421
|
+
hasAutoOpenedPreview = val;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
export function setPreviewLoadingState(val: boolean): void {
|
|
1425
|
+
previewLoading = val;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/** Surface Worker compile failures after codegen (code may still be committed). Pass null to clear. */
|
|
1429
|
+
export function getPreviewError(): string | null {
|
|
1430
|
+
if (previewError) return previewError;
|
|
1431
|
+
if (previewHasRuntimeError) {
|
|
1432
|
+
return previewRuntimeErrorStatus
|
|
1433
|
+
? `Widget preview error (HTTP ${previewRuntimeErrorStatus})`
|
|
1434
|
+
: 'Widget preview compilation/runtime error';
|
|
1435
|
+
}
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function notifyPreviewCompileErrorChanged(): void {
|
|
1440
|
+
if (!selectedWidgetId) return;
|
|
1441
|
+
dispatch('previewCompileErrorChanged', {
|
|
1442
|
+
widgetId: selectedWidgetId,
|
|
1443
|
+
error: getPreviewError(),
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
export function setPreviewBuildError(message: string | null): void {
|
|
1448
|
+
if (!message) {
|
|
1449
|
+
previewError = null;
|
|
1450
|
+
notifyPreviewCompileErrorChanged();
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
previewLoading = false;
|
|
1454
|
+
const sanitized = sanitizePreviewBuildError(message);
|
|
1455
|
+
previewError = sanitized.length > 280 ? `${sanitized.slice(0, 280)}…` : sanitized;
|
|
1456
|
+
inlinePanelView = 'preview';
|
|
1457
|
+
console.warn('[WidgetDetails] Preview build failed after codegen:', sanitized);
|
|
1458
|
+
notifyPreviewCompileErrorChanged();
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
export function showNoCodeYetState(): void {
|
|
1462
|
+
previewLoading = false;
|
|
1463
|
+
previewError = 'NO_CODE_YET';
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
export function startPreviewLoadingPublic(): void {
|
|
1467
|
+
startPreviewLoading();
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function resetPreviewMessageBridge(): void {
|
|
1471
|
+
previewWidgetReady = false;
|
|
1472
|
+
pendingPreviewMessages = [];
|
|
1473
|
+
if (previewReadyFallbackTimeout) {
|
|
1474
|
+
clearTimeout(previewReadyFallbackTimeout);
|
|
1475
|
+
previewReadyFallbackTimeout = null;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function flushPendingPreviewMessages(): void {
|
|
1480
|
+
const target = previewIframe?.contentWindow;
|
|
1481
|
+
if (!target || pendingPreviewMessages.length === 0) return;
|
|
1482
|
+
for (const message of pendingPreviewMessages) {
|
|
1483
|
+
target.postMessage(message, '*');
|
|
1484
|
+
}
|
|
1485
|
+
pendingPreviewMessages = [];
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function scheduleFitPreviewToArea(): void {
|
|
1489
|
+
requestAnimationFrame(() => {
|
|
1490
|
+
fitPreviewToArea();
|
|
1491
|
+
widgetPreviewComponentRef?.nudgeIframePaint?.();
|
|
1492
|
+
setTimeout(fitPreviewToArea, 100);
|
|
1493
|
+
setTimeout(fitPreviewToArea, 350);
|
|
1494
|
+
setTimeout(fitPreviewToArea, 800);
|
|
1495
|
+
setTimeout(() => widgetPreviewComponentRef?.nudgeIframePaint?.(), 350);
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function markPreviewWidgetReady(): void {
|
|
1500
|
+
if (previewWidgetReady) return;
|
|
1501
|
+
previewWidgetReady = true;
|
|
1502
|
+
if (previewReadyFallbackTimeout) {
|
|
1503
|
+
clearTimeout(previewReadyFallbackTimeout);
|
|
1504
|
+
previewReadyFallbackTimeout = null;
|
|
1505
|
+
}
|
|
1506
|
+
// Widget is alive — never keep a runtime-error overlay over a working preview.
|
|
1507
|
+
clearPendingRuntimeErrorOverlay();
|
|
1508
|
+
if (previewHasRuntimeError) {
|
|
1509
|
+
console.log('[WidgetDetails] Clearing runtime error overlay — widgetic:ready');
|
|
1510
|
+
previewHasRuntimeError = false;
|
|
1511
|
+
previewRuntimeErrorStatus = null;
|
|
1512
|
+
if (previewError && /runtime error|HTTP 500|compilation/i.test(previewError)) {
|
|
1513
|
+
previewError = null;
|
|
1514
|
+
}
|
|
1515
|
+
notifyPreviewCompileErrorChanged();
|
|
1516
|
+
}
|
|
1517
|
+
flushPendingPreviewMessages();
|
|
1518
|
+
if (currentStep === 'edit' || currentStep === 'embed') {
|
|
1519
|
+
dispatch('previewLoaded', { step: currentStep });
|
|
1520
|
+
}
|
|
1521
|
+
// Emit previewReady so the parent app can clear the "…loading preview…" status.
|
|
1522
|
+
dispatch('previewReady', { widgetId: selectedWidgetId, step: currentStep });
|
|
1523
|
+
// Fill available preview area once the widget confirms it is ready.
|
|
1524
|
+
scheduleFitPreviewToArea();
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/** Send a postMessage to the widget preview iframe (for live property editing) */
|
|
1528
|
+
export function sendMessageToPreview(message: Record<string, unknown>): void {
|
|
1529
|
+
if (previewWidgetReady && previewIframe?.contentWindow) {
|
|
1530
|
+
previewIframe.contentWindow.postMessage(message, '*');
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
pendingPreviewMessages.push(message);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/** Set preview container dimensions from the compositions top bar W×H inputs */
|
|
1537
|
+
export function setPreviewDimensions(width: number, height: number): void {
|
|
1538
|
+
const w = Math.max(PREVIEW_MIN_WIDTH, width);
|
|
1539
|
+
const h = Math.max(PREVIEW_MIN_HEIGHT, height);
|
|
1540
|
+
previewResizeWidth = w;
|
|
1541
|
+
previewResizeHeight = h;
|
|
1542
|
+
clampPreviewToArea();
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/** Get current preview dimensions */
|
|
1546
|
+
export function getPreviewDimensions(): { width: number; height: number } {
|
|
1547
|
+
return {
|
|
1548
|
+
width: previewResizeWidth ?? PREVIEW_DEFAULT_WIDTH,
|
|
1549
|
+
height: previewResizeHeight ?? PREVIEW_DEFAULT_HEIGHT,
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
export function setupPreviewFallbackTimeoutPublic(): void {
|
|
1554
|
+
setupPreviewFallbackTimeout();
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
export function startTokenAutoRefreshPublic(): void {
|
|
1558
|
+
startPreviewTokenAutoRefresh();
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
export function fetchDirectPreviewUrlPublic(): Promise<string | null> {
|
|
1562
|
+
return fetchDirectPreviewUrl();
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1566
|
+
// HELPER FUNCTIONS
|
|
1567
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1568
|
+
|
|
1569
|
+
function normalizeRepositoryId(source: WidgetSummary | null): string | null {
|
|
1570
|
+
if (!source) return null;
|
|
1571
|
+
return source.repositoryId || source.repo_id || source.repoId || null;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1575
|
+
// PREVIEW TOKEN AUTO-REFRESH
|
|
1576
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1577
|
+
|
|
1578
|
+
function startPreviewTokenAutoRefresh() {
|
|
1579
|
+
stopPreviewTokenAutoRefresh();
|
|
1580
|
+
console.log('[TokenAutoRefresh] Started - checking every', TOKEN_CHECK_INTERVAL_MS / 1000, 'seconds');
|
|
1581
|
+
previewTokenRefreshInterval = setInterval(async () => {
|
|
1582
|
+
const isPreviewVisible = showPreviewModal || (showWidgetDetails && inlinePanelView === 'preview');
|
|
1583
|
+
if (!isPreviewVisible || !lastPreviewUrlFetchTime || !directPreviewUrl) return;
|
|
1584
|
+
const tokenAge = Date.now() - lastPreviewUrlFetchTime;
|
|
1585
|
+
if (tokenAge >= PREVIEW_TOKEN_REFRESH_MS) {
|
|
1586
|
+
console.warn(`[TokenAutoRefresh] Token EXPIRED — refreshing automatically...`);
|
|
1587
|
+
directPreviewUrl = null;
|
|
1588
|
+
lastPreviewUrlFetchTime = 0;
|
|
1589
|
+
try {
|
|
1590
|
+
const freshUrl = await fetchDirectPreviewUrl();
|
|
1591
|
+
if (freshUrl) {
|
|
1592
|
+
const cacheBuster = Date.now();
|
|
1593
|
+
const url = new URL(freshUrl);
|
|
1594
|
+
url.searchParams.set('_t', cacheBuster.toString());
|
|
1595
|
+
iframeSrc = url.toString();
|
|
1596
|
+
previewCacheBuster = cacheBuster;
|
|
1597
|
+
}
|
|
1598
|
+
} catch (err: any) {
|
|
1599
|
+
if (err?.response?.status === 429) {
|
|
1600
|
+
console.warn('[TokenAutoRefresh] Rate limited (429) — skipping');
|
|
1601
|
+
} else {
|
|
1602
|
+
console.error('[TokenAutoRefresh] Error refreshing token:', err);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}, TOKEN_CHECK_INTERVAL_MS);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function stopPreviewTokenAutoRefresh() {
|
|
1610
|
+
if (previewTokenRefreshInterval) {
|
|
1611
|
+
clearInterval(previewTokenRefreshInterval);
|
|
1612
|
+
previewTokenRefreshInterval = null;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function isPreviewUrlValid(): boolean {
|
|
1617
|
+
if (!directPreviewUrl || !lastPreviewUrlFetchTime) return false;
|
|
1618
|
+
const age = Date.now() - lastPreviewUrlFetchTime;
|
|
1619
|
+
return age < PREVIEW_TOKEN_REFRESH_MS;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1623
|
+
// WORKER HEALTH PROBE
|
|
1624
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1625
|
+
|
|
1626
|
+
async function probeWorkerReachable(url: string): Promise<'ok' | 'not-found' | 'unreachable'> {
|
|
1627
|
+
try {
|
|
1628
|
+
const controller = new AbortController();
|
|
1629
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
1630
|
+
const resp = await fetch(url, { method: 'HEAD', signal: controller.signal });
|
|
1631
|
+
clearTimeout(timeout);
|
|
1632
|
+
if (resp.ok) return 'ok';
|
|
1633
|
+
if (resp.status === 404) return 'not-found';
|
|
1634
|
+
// 5xx often means stale/broken Worker cache — rebuild from GitLab instead of showing unreachable.
|
|
1635
|
+
if (resp.status >= 500) {
|
|
1636
|
+
console.warn('[WidgetDetails] Worker preview probe server error — will rebuild from repo:', resp.status, url);
|
|
1637
|
+
return 'not-found';
|
|
1638
|
+
}
|
|
1639
|
+
console.warn('[WidgetDetails] Worker preview probe non-OK status:', resp.status, url);
|
|
1640
|
+
return 'unreachable';
|
|
1641
|
+
} catch {
|
|
1642
|
+
return 'unreachable';
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1647
|
+
// FETCH DIRECT PREVIEW URL
|
|
1648
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1649
|
+
|
|
1650
|
+
async function fetchDirectPreviewUrl(): Promise<string | null> {
|
|
1651
|
+
isLoadingDirectPreviewUrl = false;
|
|
1652
|
+
const stepUrl = resolvePreviewUrlForStep(currentStep);
|
|
1653
|
+
if (stepUrl) {
|
|
1654
|
+
directPreviewUrl = stepUrl;
|
|
1655
|
+
return stepUrl;
|
|
1656
|
+
}
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function getFallbackPreviewUrl(): string | null {
|
|
1661
|
+
if (forceMockReady) return 'about:blank';
|
|
1662
|
+
const stepUrl = resolvePreviewUrlForStep(currentStep);
|
|
1663
|
+
if (stepUrl) {
|
|
1664
|
+
try {
|
|
1665
|
+
const url = new URL(stepUrl);
|
|
1666
|
+
url.searchParams.set('_t', previewCacheBuster.toString());
|
|
1667
|
+
return url.toString();
|
|
1668
|
+
} catch {
|
|
1669
|
+
return stepUrl;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (directPreviewUrl && isPreviewUrlValid()) {
|
|
1673
|
+
const url = new URL(directPreviewUrl);
|
|
1674
|
+
url.searchParams.set('_t', previewCacheBuster.toString());
|
|
1675
|
+
return url.toString();
|
|
1676
|
+
}
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1681
|
+
// PREVIEW IFRAME HELPERS
|
|
1682
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1683
|
+
|
|
1684
|
+
/** @param opts.withRetry — enable auto-retry on not-found (use after restore/generate) */
|
|
1685
|
+
async function refreshPreviewIframe(opts: { withRetry?: boolean; _retryCount?: number } = {}) {
|
|
1686
|
+
const retryCount = opts._retryCount || 0;
|
|
1687
|
+
const withRetry = opts.withRetry || false;
|
|
1688
|
+
|
|
1689
|
+
console.log('WidgetDetails: Triggering preview refresh via src manipulation', retryCount > 0 ? `(retry ${retryCount})` : '');
|
|
1690
|
+
const newCacheBuster = Date.now();
|
|
1691
|
+
previewCacheBuster = newCacheBuster;
|
|
1692
|
+
|
|
1693
|
+
// Converted widgets may use skipBasePreview — worker URL is never set until we build it here.
|
|
1694
|
+
if (!dynamicWorkerPreviewUrl && selectedWidgetId) {
|
|
1695
|
+
const workerPreviewUrl = buildWorkerPreviewUrl(selectedWidgetId);
|
|
1696
|
+
const workerPreviewDisplayUrl = buildWorkerPreviewUrl(selectedWidgetId, { forDisplay: true });
|
|
1697
|
+
dynamicWorkerPreviewUrl = workerPreviewUrl;
|
|
1698
|
+
directPreviewUrl = workerPreviewDisplayUrl;
|
|
1699
|
+
console.log('[WidgetDetails] refreshPreview: initialized worker preview URL');
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// After publish (or any time the CDN artifact may have changed), edit/embed steps must
|
|
1703
|
+
// probe the published URL before falling back to the Worker preview. Otherwise the iframe
|
|
1704
|
+
// keeps the pre-publish Worker artifact and the user never sees the new published version.
|
|
1705
|
+
if (
|
|
1706
|
+
(currentStep === 'edit' || currentStep === 'embed')
|
|
1707
|
+
&& (editPreviewUrl || embedPreviewUrl)
|
|
1708
|
+
&& publishedArtifactReachable !== true
|
|
1709
|
+
) {
|
|
1710
|
+
const targetUrl = currentStep === 'edit' ? editPreviewUrl : embedPreviewUrl;
|
|
1711
|
+
if (targetUrl) {
|
|
1712
|
+
previewLoading = true;
|
|
1713
|
+
const reachable = await probePublishedArtifactUrl(targetUrl);
|
|
1714
|
+
publishedArtifactReachable = reachable;
|
|
1715
|
+
if (reachable) {
|
|
1716
|
+
_lastEditPreviewBase = null;
|
|
1717
|
+
_lastEmbedPreviewBase = null;
|
|
1718
|
+
previewError = null;
|
|
1719
|
+
await reloadPreviewIframeCacheBust(targetUrl);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
// If not reachable and we have retry budget, retry after a short delay (CDN propagation).
|
|
1723
|
+
if (withRetry && retryCount < 3) {
|
|
1724
|
+
const delay = (retryCount + 1) * 2000;
|
|
1725
|
+
console.log(`[WidgetDetails] Published artifact not reachable yet, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
|
|
1726
|
+
setTimeout(() => refreshPreviewIframe({ withRetry: true, _retryCount: retryCount + 1 }), delay);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
// Fall through to worker preview logic below
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const stepUrl = resolvePreviewUrlForStep(currentStep);
|
|
1734
|
+
if (stepUrl) {
|
|
1735
|
+
previewError = null;
|
|
1736
|
+
previewLoading = true;
|
|
1737
|
+
await reloadPreviewIframeCacheBust(stepUrl);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
if (dynamicWorkerPreviewUrl) {
|
|
1742
|
+
previewError = null;
|
|
1743
|
+
|
|
1744
|
+
const probeResult = await probeWorkerReachable(dynamicWorkerPreviewUrl);
|
|
1745
|
+
if (probeResult === 'unreachable') {
|
|
1746
|
+
previewLoading = false;
|
|
1747
|
+
previewError = 'Widget Builder is not reachable. Make sure the Worker is running.';
|
|
1748
|
+
iframeSrc = null;
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (probeResult === 'not-found') {
|
|
1752
|
+
if (withRetry && retryCount < 3) {
|
|
1753
|
+
const delay = (retryCount + 1) * 2000;
|
|
1754
|
+
console.log(`[WidgetDetails] Preview not found, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
|
|
1755
|
+
previewLoading = true;
|
|
1756
|
+
setTimeout(() => refreshPreviewIframe({ withRetry: true, _retryCount: retryCount + 1 }), delay);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
// Worker cache miss after codegen — compile from GitLab repo
|
|
1760
|
+
if (lastCommitId && !isGeneratingCode && !shouldDeferPreviewNetworkWork()) {
|
|
1761
|
+
console.log('[WidgetDetails] Preview not on Worker after retries — trying build-from-repo');
|
|
1762
|
+
await triggerBuildFromRepo();
|
|
1763
|
+
if (iframeSrc) return;
|
|
1764
|
+
}
|
|
1765
|
+
previewLoading = false;
|
|
1766
|
+
iframeSrc = null;
|
|
1767
|
+
if (lastCommitId) {
|
|
1768
|
+
previewError = previewError || 'Preview not ready yet. Click Rebuild or wait a moment.';
|
|
1769
|
+
}
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
try {
|
|
1774
|
+
await reloadPreviewIframeCacheBust(dynamicWorkerPreviewUrl);
|
|
1775
|
+
} catch {
|
|
1776
|
+
previewLoading = false;
|
|
1777
|
+
}
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
previewLoading = false;
|
|
1782
|
+
if (lastCommitId && !iframeSrc && !shouldDeferPreviewNetworkWork()) {
|
|
1783
|
+
previewError = previewError || 'Preview not ready yet. Click Rebuild to compile from GitLab.';
|
|
1784
|
+
await triggerBuildFromRepo();
|
|
1785
|
+
} else if (lastCommitId && !iframeSrc && shouldDeferPreviewNetworkWork()) {
|
|
1786
|
+
previewBuildDeferred = true;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
$: if (wasFocusedPanel && !isFocusedPanel) {
|
|
1791
|
+
buildFromRepoRunId++;
|
|
1792
|
+
}
|
|
1793
|
+
$: if (!wasFocusedPanel && isFocusedPanel && previewBuildDeferred && selectedWidgetId && showWidgetDetails) {
|
|
1794
|
+
void resumeDeferredPreviewLoad();
|
|
1795
|
+
}
|
|
1796
|
+
$: wasFocusedPanel = isFocusedPanel;
|
|
1797
|
+
|
|
1798
|
+
function resetPreviewResizeSize() {
|
|
1799
|
+
previewResizeWidth = PREVIEW_DEFAULT_WIDTH;
|
|
1800
|
+
previewResizeHeight = PREVIEW_DEFAULT_HEIGHT;
|
|
1801
|
+
requestAnimationFrame(fitPreviewToArea);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function startConcentricResize(event: PointerEvent, signX: number, signY: number) {
|
|
1805
|
+
event.preventDefault();
|
|
1806
|
+
event.stopPropagation();
|
|
1807
|
+
isResizingPreview = true;
|
|
1808
|
+
const startX = event.clientX;
|
|
1809
|
+
const startY = event.clientY;
|
|
1810
|
+
const target = event.currentTarget as HTMLElement;
|
|
1811
|
+
target.setPointerCapture(event.pointerId);
|
|
1812
|
+
const containerEl = target.closest('.widget-preview-modal-content-container') as HTMLElement | null;
|
|
1813
|
+
const maxWidth = containerEl?.clientWidth ?? 720;
|
|
1814
|
+
const maxHeight = containerEl?.clientHeight ?? 420;
|
|
1815
|
+
const startWidth = previewResizeWidth ?? maxWidth;
|
|
1816
|
+
const startHeight = previewResizeHeight ?? maxHeight;
|
|
1817
|
+
|
|
1818
|
+
function onPointerMove(e: PointerEvent) {
|
|
1819
|
+
const deltaX = (e.clientX - startX) * signX * 2;
|
|
1820
|
+
const deltaY = (e.clientY - startY) * signY * 2;
|
|
1821
|
+
previewResizeWidth = Math.max(PREVIEW_MIN_WIDTH, Math.min(maxWidth, startWidth + deltaX));
|
|
1822
|
+
previewResizeHeight = Math.max(PREVIEW_MIN_HEIGHT, Math.min(maxHeight, startHeight + deltaY));
|
|
1823
|
+
}
|
|
1824
|
+
function onPointerUp() {
|
|
1825
|
+
isResizingPreview = false;
|
|
1826
|
+
target.removeEventListener('pointermove', onPointerMove);
|
|
1827
|
+
target.removeEventListener('pointerup', onPointerUp);
|
|
1828
|
+
}
|
|
1829
|
+
target.addEventListener('pointermove', onPointerMove);
|
|
1830
|
+
target.addEventListener('pointerup', onPointerUp);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
let lastPreviewIframeLoadAt = 0;
|
|
1834
|
+
let lastPreviewIframeLoadSrc: string | null = null;
|
|
1835
|
+
|
|
1836
|
+
function handlePreviewIframeLoad(): void {
|
|
1837
|
+
if (iframeSrc === 'about:blank' || previewIframe?.src?.includes('about:blank')) {
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
const loadedSrc = previewIframe?.src ?? iframeSrc;
|
|
1841
|
+
const now = Date.now();
|
|
1842
|
+
if (
|
|
1843
|
+
loadedSrc
|
|
1844
|
+
&& previewFullUrlsMatch(loadedSrc, lastPreviewIframeLoadSrc)
|
|
1845
|
+
&& now - lastPreviewIframeLoadAt < 200
|
|
1846
|
+
) {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
lastPreviewIframeLoadSrc = loadedSrc;
|
|
1850
|
+
lastPreviewIframeLoadAt = now;
|
|
1851
|
+
if (previewTimeoutHandle) { clearTimeout(previewTimeoutHandle); previewTimeoutHandle = null; }
|
|
1852
|
+
clearPreviewHealthCheck();
|
|
1853
|
+
|
|
1854
|
+
previewLoading = false;
|
|
1855
|
+
if (dwLoadTimeoutHandle) clearTimeout(dwLoadTimeoutHandle);
|
|
1856
|
+
|
|
1857
|
+
if (currentStep === 'edit' || currentStep === 'embed') {
|
|
1858
|
+
// Only show hydration overlay when a step transition requested it — not on soft cache-bust refresh
|
|
1859
|
+
if (previewHydrating || previewLoading) {
|
|
1860
|
+
previewHydrating = true;
|
|
1861
|
+
previewWidgetReady = false;
|
|
1862
|
+
if (previewReadyFallbackTimeout) clearTimeout(previewReadyFallbackTimeout);
|
|
1863
|
+
previewReadyFallbackTimeout = setTimeout(() => {
|
|
1864
|
+
if (debugMode) {
|
|
1865
|
+
console.warn('[WidgetDetails] widgetic:ready fallback — applying composition anyway');
|
|
1866
|
+
}
|
|
1867
|
+
markPreviewWidgetReady();
|
|
1868
|
+
}, 2000);
|
|
1869
|
+
}
|
|
1870
|
+
} else {
|
|
1871
|
+
previewHydrating = false;
|
|
1872
|
+
previewWidgetReady = true;
|
|
1873
|
+
dispatch('previewReady', { widgetId: selectedWidgetId, step: currentStep });
|
|
1874
|
+
// Create step: iframe load does not go through markPreviewWidgetReady — still fit-to-area.
|
|
1875
|
+
scheduleFitPreviewToArea();
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function handlePreviewIframeError(): void {
|
|
1880
|
+
if (dwLoadTimeoutHandle) { clearTimeout(dwLoadTimeoutHandle); dwLoadTimeoutHandle = null; }
|
|
1881
|
+
previewLoading = false;
|
|
1882
|
+
previewError = 'Widget Builder is not reachable. Make sure the Worker is running.';
|
|
1883
|
+
iframeSrc = null;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function setupPreviewFallbackTimeout(): void {
|
|
1887
|
+
if (previewFallbackHandle) { clearTimeout(previewFallbackHandle); previewFallbackHandle = null; }
|
|
1888
|
+
if (previewHealthCheckHandle) { clearInterval(previewHealthCheckHandle); previewHealthCheckHandle = null; }
|
|
1889
|
+
previewFallbackHandle = setTimeout(() => {
|
|
1890
|
+
if ((previewLoading || previewHydrating) && (directPreviewUrl || iframeSrc)) {
|
|
1891
|
+
console.warn('[WidgetDetails] Preview fallback timeout — revealing iframe');
|
|
1892
|
+
previewLoading = false;
|
|
1893
|
+
previewError = null;
|
|
1894
|
+
if (previewHydrating) {
|
|
1895
|
+
previewHydrating = false;
|
|
1896
|
+
markPreviewWidgetReady();
|
|
1897
|
+
}
|
|
1898
|
+
if (previewTimeoutHandle) { clearTimeout(previewTimeoutHandle); previewTimeoutHandle = null; }
|
|
1899
|
+
clearPreviewHealthCheck();
|
|
1900
|
+
}
|
|
1901
|
+
}, 3000);
|
|
1902
|
+
previewHealthCheckHandle = setInterval(() => {
|
|
1903
|
+
if (!previewLoading && !previewHydrating) { clearPreviewHealthCheck(); return; }
|
|
1904
|
+
try {
|
|
1905
|
+
const iframe = previewIframe ?? document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement;
|
|
1906
|
+
if (!iframe || !iframeSrc) return;
|
|
1907
|
+
if (!previewFullUrlsMatch(iframeSrc, iframe.src)) return;
|
|
1908
|
+
if (iframe.contentWindow) {
|
|
1909
|
+
try {
|
|
1910
|
+
const _loc = iframe.contentWindow.location.href;
|
|
1911
|
+
if (_loc && _loc !== 'about:blank' && previewFullUrlsMatch(iframeSrc, _loc)) {
|
|
1912
|
+
handlePreviewIframeLoad();
|
|
1913
|
+
}
|
|
1914
|
+
} catch (_crossOriginErr) {
|
|
1915
|
+
// Cross-origin = document loaded at target URL
|
|
1916
|
+
handlePreviewIframeLoad();
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
} catch (err) { /* non-critical */ }
|
|
1920
|
+
}, 2000);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function clearPreviewHealthCheck(): void {
|
|
1924
|
+
if (previewFallbackHandle) { clearTimeout(previewFallbackHandle); previewFallbackHandle = null; }
|
|
1925
|
+
if (previewHealthCheckHandle) { clearInterval(previewHealthCheckHandle); previewHealthCheckHandle = null; }
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function startPreviewLoading(): void {
|
|
1929
|
+
previewLoading = true;
|
|
1930
|
+
previewHydrating = false;
|
|
1931
|
+
previewError = null;
|
|
1932
|
+
previewSilentRetryCount = 0;
|
|
1933
|
+
resetPreviewResizeSize();
|
|
1934
|
+
if (hydrationSafetyTimeout) { clearTimeout(hydrationSafetyTimeout); hydrationSafetyTimeout = null; }
|
|
1935
|
+
if (previewTimeoutHandle) clearTimeout(previewTimeoutHandle);
|
|
1936
|
+
previewTimeoutHandle = setTimeout(() => {
|
|
1937
|
+
if (!previewLoading) return;
|
|
1938
|
+
previewLoading = false;
|
|
1939
|
+
previewError = 'Preview timed out. The dev server may still be initializing.\nPlease wait a moment and click Retry.';
|
|
1940
|
+
}, PREVIEW_FIRST_LOAD_TIMEOUT_MS);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
/** Recompile widget from GitLab repo via Worker (Create step) or reload published preview (Edit/Embed). */
|
|
1944
|
+
async function retryPreview(): Promise<void> {
|
|
1945
|
+
previewRetryCount++;
|
|
1946
|
+
directPreviewUrl = null;
|
|
1947
|
+
iframeSrc = null;
|
|
1948
|
+
previewError = null;
|
|
1949
|
+
previewHasRuntimeError = false;
|
|
1950
|
+
previewRuntimeErrorStatus = null;
|
|
1951
|
+
startPreviewLoading();
|
|
1952
|
+
|
|
1953
|
+
if (currentStep !== 'create') {
|
|
1954
|
+
if (publishedArtifactReachable === false) {
|
|
1955
|
+
await triggerBuildFromRepo({ force: true });
|
|
1956
|
+
if (iframeSrc) return;
|
|
1957
|
+
}
|
|
1958
|
+
await refreshPreviewIframe();
|
|
1959
|
+
if (iframeSrc) return;
|
|
1960
|
+
previewLoading = false;
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// Create step: full rebuild from repo (sanitized) — not just iframe cache-bust
|
|
1965
|
+
await triggerBuildFromRepo({ force: true });
|
|
1966
|
+
if (iframeSrc) return;
|
|
1967
|
+
|
|
1968
|
+
// Fallback: try fetching direct preview URL
|
|
1969
|
+
const freshUrl = await fetchDirectPreviewUrl();
|
|
1970
|
+
if (freshUrl) {
|
|
1971
|
+
try {
|
|
1972
|
+
const url = new URL(freshUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
1973
|
+
url.searchParams.set('_t', Date.now().toString());
|
|
1974
|
+
iframeSrc = url.toString();
|
|
1975
|
+
setupPreviewFallbackTimeout();
|
|
1976
|
+
syncPreviewLoadingWithIframe();
|
|
1977
|
+
} catch (urlErr) {
|
|
1978
|
+
console.warn('[WidgetDetails] retryPreview: invalid preview URL', freshUrl, urlErr);
|
|
1979
|
+
previewLoading = false;
|
|
1980
|
+
previewError = 'Could not open preview URL. Try Rebuild again.';
|
|
1981
|
+
}
|
|
1982
|
+
} else if (!previewError) {
|
|
1983
|
+
previewLoading = false;
|
|
1984
|
+
previewError = 'Could not fetch the preview URL. The Worker may be unavailable.';
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1989
|
+
// QUEUED ACTIONS
|
|
1990
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1991
|
+
|
|
1992
|
+
function enqueueQueuedAction(action: QueuedAction): void {
|
|
1993
|
+
queuedActions = [...queuedActions, action];
|
|
1994
|
+
const description = action.type === 'generate'
|
|
1995
|
+
? 'Code generation will start shortly.'
|
|
1996
|
+
: 'Save will execute shortly.';
|
|
1997
|
+
showToast('info', action.type === 'generate' ? 'Code generation queued' : 'Save queued', { description, duration: 5000 });
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
async function processQueuedActions(): Promise<void> {
|
|
2001
|
+
if (isProcessingQueuedActions || queuedActions.length === 0) return;
|
|
2002
|
+
isProcessingQueuedActions = true;
|
|
2003
|
+
try {
|
|
2004
|
+
while (queuedActions.length > 0) {
|
|
2005
|
+
const currentAction = queuedActions[0];
|
|
2006
|
+
try {
|
|
2007
|
+
if (currentAction?.type === 'generate') {
|
|
2008
|
+
dispatch('generateCode', { prompt: currentAction.prompt, queued: true });
|
|
2009
|
+
} else if (currentAction?.type === 'save') {
|
|
2010
|
+
dispatch('saveWidget', { queued: true });
|
|
2011
|
+
}
|
|
2012
|
+
} catch (queueError) {
|
|
2013
|
+
console.error('Queued action failed', queueError);
|
|
2014
|
+
} finally {
|
|
2015
|
+
queuedActions = queuedActions.slice(1);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
} finally {
|
|
2019
|
+
isProcessingQueuedActions = false;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2024
|
+
// INLINE PANEL VIEW
|
|
2025
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2026
|
+
|
|
2027
|
+
function toggleInlinePanelView(view: 'preview' | 'console'): void {
|
|
2028
|
+
if (inlinePanelView === view) return;
|
|
2029
|
+
inlinePanelView = view;
|
|
2030
|
+
if (view === 'preview' && !iframeSrc && directPreviewUrl) {
|
|
2031
|
+
try {
|
|
2032
|
+
const url = new URL(directPreviewUrl);
|
|
2033
|
+
url.searchParams.set('_t', Date.now().toString());
|
|
2034
|
+
iframeSrc = url.toString();
|
|
2035
|
+
} catch (e) {
|
|
2036
|
+
console.error('toggleInlinePanelView: Failed to build iframe URL from directPreviewUrl', e);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2042
|
+
// PANEL POSITIONING & DRAG/RESIZE
|
|
2043
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2044
|
+
|
|
2045
|
+
function getDefaultDetailsPanelHeight(): number {
|
|
2046
|
+
if (typeof window === 'undefined') return 600;
|
|
2047
|
+
return window.innerHeight - detailsPanelTopOffset - detailsPanelBottomMargin;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
function clampPanelWidth(w: number): number {
|
|
2051
|
+
if (typeof window === 'undefined') return w;
|
|
2052
|
+
const maxWidth = window.innerWidth - 40;
|
|
2053
|
+
return Math.max(DETAILS_PANEL_MIN_WIDTH, Math.min(w, maxWidth));
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function resetDetailsPanelSize() {
|
|
2057
|
+
if (typeof window === 'undefined') {
|
|
2058
|
+
detailsPanelWidth = DETAILS_PANEL_DEFAULT_WIDTH;
|
|
2059
|
+
detailsPanelHeight = 0;
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
if (fillViewport) {
|
|
2063
|
+
detailsPanelWidth = Math.max(
|
|
2064
|
+
DETAILS_PANEL_MIN_WIDTH,
|
|
2065
|
+
window.innerWidth - DETAILS_PANEL_FILL_INSET * 2
|
|
2066
|
+
);
|
|
2067
|
+
detailsPanelHeight = Math.max(
|
|
2068
|
+
DETAILS_PANEL_MIN_HEIGHT,
|
|
2069
|
+
window.innerHeight - detailsPanelTopOffset - detailsPanelBottomMargin
|
|
2070
|
+
);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_DEFAULT_WIDTH);
|
|
2074
|
+
detailsPanelHeight = 0;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
export function centerDetailsPanel() {
|
|
2078
|
+
if (typeof window === 'undefined') return;
|
|
2079
|
+
resetDetailsPanelSize();
|
|
2080
|
+
if (fillViewport) {
|
|
2081
|
+
detailsPanelX = DETAILS_PANEL_FILL_INSET;
|
|
2082
|
+
detailsPanelY = detailsPanelTopOffset;
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
const cascade = panelCascadeIndex * 32;
|
|
2086
|
+
detailsPanelX = Math.max(0, Math.round((window.innerWidth - detailsPanelWidth) / 2) + cascade);
|
|
2087
|
+
detailsPanelY = detailsPanelTopOffset + cascade;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function startColumnResize(e: MouseEvent) {
|
|
2091
|
+
e.preventDefault();
|
|
2092
|
+
e.stopPropagation();
|
|
2093
|
+
isResizingColumns = true;
|
|
2094
|
+
columnResizeStartX = e.clientX;
|
|
2095
|
+
columnResizeStartPercent = leftColPercent;
|
|
2096
|
+
document.body.style.cursor = 'col-resize';
|
|
2097
|
+
document.body.style.userSelect = 'none';
|
|
2098
|
+
window.addEventListener('mousemove', onColumnResizeMove);
|
|
2099
|
+
addGlobalStopListeners(stopColumnResize);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function onColumnResizeMove(e: MouseEvent) {
|
|
2103
|
+
if (!isResizingColumns) return;
|
|
2104
|
+
const twoCol = document.querySelector('.widget-details-two-col') as HTMLElement | null;
|
|
2105
|
+
const width = twoCol?.clientWidth || detailsPanelWidth || DETAILS_PANEL_DEFAULT_WIDTH;
|
|
2106
|
+
if (width <= 0) return;
|
|
2107
|
+
const deltaPercent = ((e.clientX - columnResizeStartX) / width) * 100;
|
|
2108
|
+
leftColPercent = Math.min(
|
|
2109
|
+
LEFT_COL_MAX_PERCENT,
|
|
2110
|
+
Math.max(LEFT_COL_MIN_PERCENT, columnResizeStartPercent + deltaPercent)
|
|
2111
|
+
);
|
|
2112
|
+
// Keep preview iframe inside the shrinking right column (avoid clipped "cut off" widgets).
|
|
2113
|
+
requestAnimationFrame(() => {
|
|
2114
|
+
clampPreviewToArea();
|
|
2115
|
+
fitPreviewToArea();
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
function stopColumnResize() {
|
|
2120
|
+
if (!isResizingColumns) return;
|
|
2121
|
+
isResizingColumns = false;
|
|
2122
|
+
document.body.style.cursor = '';
|
|
2123
|
+
document.body.style.userSelect = '';
|
|
2124
|
+
window.removeEventListener('mousemove', onColumnResizeMove);
|
|
2125
|
+
window.removeEventListener('mouseup', stopColumnResize);
|
|
2126
|
+
window.removeEventListener('pointerup', stopColumnResize);
|
|
2127
|
+
window.removeEventListener('pointercancel', stopColumnResize);
|
|
2128
|
+
document.documentElement.removeEventListener('mouseleave', stopColumnResize);
|
|
2129
|
+
window.removeEventListener('blur', stopColumnResize);
|
|
2130
|
+
tick().then(() => requestAnimationFrame(() => fitPreviewToArea()));
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
let _hasBeenOpenedOnce = false;
|
|
2134
|
+
export function openWidgetDetails() {
|
|
2135
|
+
if (showWidgetDetails) return;
|
|
2136
|
+
resetDetailsPanelSize();
|
|
2137
|
+
if (fillViewport || !_hasBeenOpenedOnce || detailsPanelX < 0 || detailsPanelY < 0) {
|
|
2138
|
+
centerDetailsPanel();
|
|
2139
|
+
_hasBeenOpenedOnce = true;
|
|
2140
|
+
}
|
|
2141
|
+
showWidgetDetails = true;
|
|
2142
|
+
dispatch('widgetDetailsToggle', { open: true });
|
|
2143
|
+
|
|
2144
|
+
// Clamp preview after DOM is laid out
|
|
2145
|
+
tick().then(() => {
|
|
2146
|
+
requestAnimationFrame(() => {
|
|
2147
|
+
fitPreviewToArea();
|
|
2148
|
+
setTimeout(fitPreviewToArea, 200);
|
|
2149
|
+
});
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
if (selectedWidgetId) {
|
|
2153
|
+
if (iframeSrc && previewLoadedForWidgetId === selectedWidgetId) {
|
|
2154
|
+
console.log('[WidgetDetails] Reopened with existing preview, no reload needed');
|
|
2155
|
+
dispatch('buildFromRepoSuccess', { widgetId: selectedWidgetId, source: 'cached-preview' });
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
loadWidget(selectedWidgetId);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
export function closeWidgetDetails(options?: { silent?: boolean }) {
|
|
2163
|
+
showWidgetDetails = false;
|
|
2164
|
+
showPreviewModal = false;
|
|
2165
|
+
if (!options?.silent) {
|
|
2166
|
+
dispatch('widgetDetailsToggle', { open: false, showWidgetDetails: false });
|
|
2167
|
+
}
|
|
2168
|
+
// Keep iframeSrc, directPreviewUrl, previewLoading, previewError intact
|
|
2169
|
+
// so reopening the same widget shows the preview instantly without re-probing
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function handleCloseButtonClick(e: MouseEvent) {
|
|
2173
|
+
e.preventDefault();
|
|
2174
|
+
e.stopPropagation();
|
|
2175
|
+
console.log('[WidgetDetails] Close button clicked');
|
|
2176
|
+
closeWidgetDetails();
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function addGlobalStopListeners(stopFn: () => void) {
|
|
2180
|
+
window.addEventListener('mouseup', stopFn);
|
|
2181
|
+
window.addEventListener('pointerup', stopFn);
|
|
2182
|
+
window.addEventListener('pointercancel', stopFn);
|
|
2183
|
+
document.documentElement.addEventListener('mouseleave', stopFn);
|
|
2184
|
+
window.addEventListener('blur', stopFn);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
function removeGlobalStopListeners(stopFn: () => void) {
|
|
2188
|
+
window.removeEventListener('mouseup', stopFn);
|
|
2189
|
+
window.removeEventListener('pointerup', stopFn);
|
|
2190
|
+
window.removeEventListener('pointercancel', stopFn);
|
|
2191
|
+
document.documentElement.removeEventListener('mouseleave', stopFn);
|
|
2192
|
+
window.removeEventListener('blur', stopFn);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
function startDetailsResize(e: MouseEvent) {
|
|
2196
|
+
e.preventDefault();
|
|
2197
|
+
e.stopPropagation();
|
|
2198
|
+
isResizingDetails = true;
|
|
2199
|
+
detailsResizeStartX = e.clientX;
|
|
2200
|
+
detailsResizeStartY = e.clientY;
|
|
2201
|
+
detailsResizeStartWidth = detailsPanelWidth;
|
|
2202
|
+
detailsResizeStartHeight = detailsPanelHeight || getDefaultDetailsPanelHeight();
|
|
2203
|
+
document.body.style.cursor = 'nwse-resize';
|
|
2204
|
+
document.body.style.userSelect = 'none';
|
|
2205
|
+
window.addEventListener('mousemove', onDetailsResize);
|
|
2206
|
+
addGlobalStopListeners(stopDetailsResize);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
function onDetailsResize(e: MouseEvent) {
|
|
2210
|
+
if (!isResizingDetails) return;
|
|
2211
|
+
const deltaX = e.clientX - detailsResizeStartX;
|
|
2212
|
+
const deltaY = e.clientY - detailsResizeStartY;
|
|
2213
|
+
const maxWidth = window.innerWidth - detailsPanelX - 20;
|
|
2214
|
+
const maxHeight = window.innerHeight - detailsPanelY - 20;
|
|
2215
|
+
detailsPanelWidth = Math.min(maxWidth, Math.max(DETAILS_PANEL_MIN_WIDTH, detailsResizeStartWidth + deltaX));
|
|
2216
|
+
detailsPanelHeight = Math.min(maxHeight, Math.max(DETAILS_PANEL_MIN_HEIGHT, detailsResizeStartHeight + deltaY));
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function stopDetailsResize() {
|
|
2220
|
+
if (!isResizingDetails) return;
|
|
2221
|
+
isResizingDetails = false;
|
|
2222
|
+
document.body.style.cursor = '';
|
|
2223
|
+
document.body.style.userSelect = '';
|
|
2224
|
+
window.removeEventListener('mousemove', onDetailsResize);
|
|
2225
|
+
removeGlobalStopListeners(stopDetailsResize);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
function handlePanelFocusRequest() {
|
|
2229
|
+
// Always notify parent — raises z-index even when this panel is already the focused widget.
|
|
2230
|
+
dispatch('focusPanel');
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function clampDetailsPanelPosition(x: number, y: number): { x: number; y: number } {
|
|
2234
|
+
if (typeof window === 'undefined') return { x, y };
|
|
2235
|
+
const pad = 8;
|
|
2236
|
+
const panelH = detailsPanelHeight || getDefaultDetailsPanelHeight();
|
|
2237
|
+
const panelW = detailsPanelWidth || DETAILS_PANEL_DEFAULT_WIDTH;
|
|
2238
|
+
const minTop = detailsPanelTopOffset;
|
|
2239
|
+
const maxTop = Math.max(minTop, window.innerHeight - Math.min(panelH, window.innerHeight - pad) - pad);
|
|
2240
|
+
const maxLeft = Math.max(pad, window.innerWidth - Math.min(panelW, window.innerWidth - pad) - pad);
|
|
2241
|
+
return {
|
|
2242
|
+
x: Math.min(maxLeft, Math.max(pad, x)),
|
|
2243
|
+
y: Math.min(maxTop, Math.max(minTop, y)),
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
function startDetailsDrag(e: PointerEvent) {
|
|
2248
|
+
const target = e.target as HTMLElement | null;
|
|
2249
|
+
if (target?.closest('.step-button, .widget-details-close-bt, .widget-details-debug-buttons, button, a, input, select, textarea')) {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
handlePanelFocusRequest();
|
|
2253
|
+
e.preventDefault();
|
|
2254
|
+
e.stopPropagation();
|
|
2255
|
+
isDraggingDetails = true;
|
|
2256
|
+
detailsDragStartX = e.clientX;
|
|
2257
|
+
detailsDragStartY = e.clientY;
|
|
2258
|
+
detailsInitialX = detailsPanelX < 0 ? 0 : detailsPanelX;
|
|
2259
|
+
detailsInitialY = detailsPanelY < 0 ? detailsPanelTopOffset : detailsPanelY;
|
|
2260
|
+
document.body.style.cursor = 'grabbing';
|
|
2261
|
+
document.body.style.userSelect = 'none';
|
|
2262
|
+
try {
|
|
2263
|
+
(e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId);
|
|
2264
|
+
} catch {
|
|
2265
|
+
/* ignore capture failures */
|
|
2266
|
+
}
|
|
2267
|
+
window.addEventListener('pointermove', onDetailsDrag);
|
|
2268
|
+
window.addEventListener('pointerup', stopDetailsDrag);
|
|
2269
|
+
window.addEventListener('pointercancel', stopDetailsDrag);
|
|
2270
|
+
window.addEventListener('mousemove', onDetailsMouseDrag);
|
|
2271
|
+
addGlobalStopListeners(stopDetailsDrag);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function onDetailsDrag(e: PointerEvent) {
|
|
2275
|
+
if (!isDraggingDetails) return;
|
|
2276
|
+
const next = clampDetailsPanelPosition(
|
|
2277
|
+
detailsInitialX + (e.clientX - detailsDragStartX),
|
|
2278
|
+
detailsInitialY + (e.clientY - detailsDragStartY),
|
|
2279
|
+
);
|
|
2280
|
+
detailsPanelX = next.x;
|
|
2281
|
+
detailsPanelY = next.y;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function onDetailsMouseDrag(e: MouseEvent) {
|
|
2285
|
+
if (!isDraggingDetails) return;
|
|
2286
|
+
onDetailsDrag(e as unknown as PointerEvent);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function stopDetailsDrag() {
|
|
2290
|
+
if (!isDraggingDetails) return;
|
|
2291
|
+
isDraggingDetails = false;
|
|
2292
|
+
document.body.style.cursor = '';
|
|
2293
|
+
document.body.style.userSelect = '';
|
|
2294
|
+
window.removeEventListener('pointermove', onDetailsDrag);
|
|
2295
|
+
window.removeEventListener('pointerup', stopDetailsDrag);
|
|
2296
|
+
window.removeEventListener('pointercancel', stopDetailsDrag);
|
|
2297
|
+
window.removeEventListener('mousemove', onDetailsMouseDrag);
|
|
2298
|
+
removeGlobalStopListeners(stopDetailsDrag);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
// Svelte action: auto-focus element when mounted (for ESC key to work in modal)
|
|
2302
|
+
function focusOnMount(node: HTMLElement) {
|
|
2303
|
+
requestAnimationFrame(() => node.focus());
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2307
|
+
// REACTIVE BLOCKS
|
|
2308
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2309
|
+
|
|
2310
|
+
/** Expand preview to fill the available panel (fit-to-area). */
|
|
2311
|
+
function fitPreviewToArea() {
|
|
2312
|
+
widgetPreviewComponentRef?.fitToContainer?.();
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// Clamp preview to available stage inside WidgetPreview (handles + dimension badge padding)
|
|
2316
|
+
function clampPreviewToArea() {
|
|
2317
|
+
const limits = widgetPreviewComponentRef?.getResizeLimits?.();
|
|
2318
|
+
if (limits) {
|
|
2319
|
+
if (previewResizeWidth && previewResizeWidth > limits.maxWidth) {
|
|
2320
|
+
previewResizeWidth = limits.maxWidth;
|
|
2321
|
+
}
|
|
2322
|
+
if (previewResizeHeight && previewResizeHeight > limits.maxHeight) {
|
|
2323
|
+
previewResizeHeight = limits.maxHeight;
|
|
2324
|
+
}
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
if (!previewAreaEl) return;
|
|
2328
|
+
const chromePadX = 16;
|
|
2329
|
+
const chromePadY = 30;
|
|
2330
|
+
let areaW = previewAreaEl.clientWidth;
|
|
2331
|
+
let areaH = previewAreaEl.clientHeight;
|
|
2332
|
+
if (areaW === 0 && detailsPanelWidth > 0) {
|
|
2333
|
+
areaW = Math.round(detailsPanelWidth * 0.5 - 24);
|
|
2334
|
+
}
|
|
2335
|
+
if (areaH === 0 && detailsPanelHeight > 0) {
|
|
2336
|
+
areaH = Math.round(detailsPanelHeight - 120);
|
|
2337
|
+
} else if (areaH === 0) {
|
|
2338
|
+
areaH = Math.round((typeof window !== 'undefined' ? window.innerHeight : 600) - detailsPanelTopOffset - detailsPanelBottomMargin - 120);
|
|
2339
|
+
}
|
|
2340
|
+
const maxW = Math.max(PREVIEW_MIN_WIDTH, areaW - chromePadX);
|
|
2341
|
+
const maxH = Math.max(PREVIEW_MIN_HEIGHT, areaH - chromePadY);
|
|
2342
|
+
if (previewResizeWidth && previewResizeWidth > maxW) previewResizeWidth = maxW;
|
|
2343
|
+
if (previewResizeHeight && previewResizeHeight > maxH) previewResizeHeight = maxH;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
async function openDesktopPreview() {
|
|
2347
|
+
showPreviewModal = true;
|
|
2348
|
+
await tick();
|
|
2349
|
+
requestAnimationFrame(() => {
|
|
2350
|
+
widgetPreviewComponentRef?.fitToContainer?.();
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// Clamp when preview size changes
|
|
2355
|
+
$: if (previewAreaEl && (previewResizeWidth || previewResizeHeight)) {
|
|
2356
|
+
clampPreviewToArea();
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// Clamp when panel dimensions change
|
|
2360
|
+
$: void detailsPanelWidth, detailsPanelHeight, (() => {
|
|
2361
|
+
requestAnimationFrame(clampPreviewToArea);
|
|
2362
|
+
})();
|
|
2363
|
+
|
|
2364
|
+
// ResizeObserver: clamp on initial render and whenever the container resizes
|
|
2365
|
+
let previewAreaObserver: ResizeObserver | null = null;
|
|
2366
|
+
$: if (previewAreaEl) {
|
|
2367
|
+
previewAreaObserver?.disconnect();
|
|
2368
|
+
previewAreaObserver = new ResizeObserver(() => {
|
|
2369
|
+
requestAnimationFrame(fitPreviewToArea);
|
|
2370
|
+
});
|
|
2371
|
+
previewAreaObserver.observe(previewAreaEl);
|
|
2372
|
+
// Fit after layout settles (panel open, widget load)
|
|
2373
|
+
setTimeout(fitPreviewToArea, 100);
|
|
2374
|
+
setTimeout(fitPreviewToArea, 400);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
let prevShowWidgetDetails = false;
|
|
2378
|
+
$: if (showWidgetDetails && !prevShowWidgetDetails) {
|
|
2379
|
+
prevShowWidgetDetails = true;
|
|
2380
|
+
void tick().then(() => {
|
|
2381
|
+
requestAnimationFrame(() => {
|
|
2382
|
+
fitPreviewToArea();
|
|
2383
|
+
widgetPreviewComponentRef?.nudgeIframePaint?.();
|
|
2384
|
+
setTimeout(() => {
|
|
2385
|
+
fitPreviewToArea();
|
|
2386
|
+
widgetPreviewComponentRef?.nudgeIframePaint?.();
|
|
2387
|
+
}, 200);
|
|
2388
|
+
});
|
|
2389
|
+
});
|
|
2390
|
+
} else if (!showWidgetDetails) {
|
|
2391
|
+
prevShowWidgetDetails = false;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// Show loader immediately when a new widget is selected (before loadWidget is called)
|
|
2395
|
+
let prevSelectedWidgetId: string | null = null;
|
|
2396
|
+
let isWidgetLoading = false;
|
|
2397
|
+
$: if (selectedWidgetId && selectedWidgetId !== prevSelectedWidgetId) {
|
|
2398
|
+
prevSelectedWidgetId = selectedWidgetId;
|
|
2399
|
+
widgetNotFoundAutoRebuildAttempts = 0;
|
|
2400
|
+
codegenPreviewFreshUntil = 0;
|
|
2401
|
+
if (!iframeSrc) {
|
|
2402
|
+
previewLoading = true;
|
|
2403
|
+
isWidgetLoading = true;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// When debug mode is OFF, force preview (no console tab access)
|
|
2408
|
+
$: if (!debugMode && inlinePanelView !== 'preview') {
|
|
2409
|
+
inlinePanelView = 'preview';
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// When generation finishes without a preview URL, stop the loading spinner
|
|
2413
|
+
// But not during initial widget loading (loadWidget sets its own previewLoading flow)
|
|
2414
|
+
$: if (!isGeneratingCode && previewLoading && !iframeSrc && !isWidgetLoading) {
|
|
2415
|
+
previewLoading = false;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// Stop token auto-refresh when no preview is visible
|
|
2419
|
+
$: if (!showPreviewModal && !(showWidgetDetails && inlinePanelView === 'preview')) {
|
|
2420
|
+
stopPreviewTokenAutoRefresh();
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// Focus preview modal when opened; fit preview to modal stage
|
|
2424
|
+
$: if (showPreviewModal && previewModalRef) {
|
|
2425
|
+
requestAnimationFrame(() => {
|
|
2426
|
+
previewModalRef?.focus();
|
|
2427
|
+
widgetPreviewComponentRef?.fitToContainer?.();
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// Connect WebSocket as soon as widget is selected
|
|
2432
|
+
$: if (selectedWidgetId && !hasConnectedForDevServerCheck) {
|
|
2433
|
+
hasConnectedForDevServerCheck = true;
|
|
2434
|
+
const currentToken = getAuthJwt();
|
|
2435
|
+
if (currentToken) setWsAuthToken(currentToken);
|
|
2436
|
+
const userId = userSession?.user?.id;
|
|
2437
|
+
if (userId) {
|
|
2438
|
+
console.log('[WidgetDetails] Connecting WebSocket for user events');
|
|
2439
|
+
connectWebSocket(userId);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
let lastWsPreviewSignature: string | null = null;
|
|
2444
|
+
|
|
2445
|
+
async function applyWorkerPreviewFromEvent(workerUrl: string, buildTimeMs?: number | null): Promise<void> {
|
|
2446
|
+
const fetchUrl = toWorkerPreviewFetchUrl(workerUrl);
|
|
2447
|
+
const signature = `${fetchUrl}:${buildTimeMs ?? 'na'}:${selectedWidgetId ?? ''}`;
|
|
2448
|
+
if (signature === lastWsPreviewSignature) return;
|
|
2449
|
+
lastWsPreviewSignature = signature;
|
|
2450
|
+
|
|
2451
|
+
console.log('[WidgetDetails] Worker preview ready — force reloading iframe', {
|
|
2452
|
+
widgetId: selectedWidgetId,
|
|
2453
|
+
previewUrl: fetchUrl,
|
|
2454
|
+
displayUrl: toWorkerPreviewDisplayUrl(fetchUrl),
|
|
2455
|
+
buildTimeMs,
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
dynamicWorkerPreviewUrl = fetchUrl;
|
|
2459
|
+
directPreviewUrl = toWorkerPreviewDisplayUrl(fetchUrl);
|
|
2460
|
+
inlinePanelView = 'preview';
|
|
2461
|
+
await forceReloadPreviewIframe(fetchUrl);
|
|
2462
|
+
previewHydrating = false;
|
|
2463
|
+
previewError = null;
|
|
2464
|
+
previewHasRuntimeError = false;
|
|
2465
|
+
previewRuntimeErrorStatus = null;
|
|
2466
|
+
dispatch('widgetReady', { widgetId: selectedWidgetId });
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// Worker preview ready via WebSocket event (after codegen / Worker build)
|
|
2470
|
+
$: if ($dynamicWorkerPreviewStatus.widgetId === selectedWidgetId && $dynamicWorkerPreviewStatus.previewUrl) {
|
|
2471
|
+
void applyWorkerPreviewFromEvent(
|
|
2472
|
+
$dynamicWorkerPreviewStatus.previewUrl,
|
|
2473
|
+
$dynamicWorkerPreviewStatus.buildTimeMs,
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Process queued actions
|
|
2478
|
+
$: if (queuedActions.length > 0) {
|
|
2479
|
+
void processQueuedActions();
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// Sync key state back to parent so reactive blocks (isGenerateDisabled, etc.) stay correct
|
|
2483
|
+
$: dispatch('stateSync', {
|
|
2484
|
+
widgetId: selectedWidgetId,
|
|
2485
|
+
directPreviewUrl,
|
|
2486
|
+
iframeSrc,
|
|
2487
|
+
previewLoading,
|
|
2488
|
+
showPreviewModal,
|
|
2489
|
+
showWidgetDetails,
|
|
2490
|
+
inlinePanelView,
|
|
2491
|
+
hasAutoOpenedPreview,
|
|
2492
|
+
});
|
|
2493
|
+
|
|
2494
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2495
|
+
// LIFECYCLE
|
|
2496
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2497
|
+
|
|
2498
|
+
onMount(() => {
|
|
2499
|
+
// Worker-built widgets signal readiness via widgetic:ready (not sveltekit-hydration-complete)
|
|
2500
|
+
const handleWidgeticReady = (event: MessageEvent) => {
|
|
2501
|
+
if (event.data?.type !== 'widgetic:ready') return;
|
|
2502
|
+
console.log('[WidgetDetails] widgetic:ready received — applying queued composition updates');
|
|
2503
|
+
markPreviewWidgetReady();
|
|
2504
|
+
};
|
|
2505
|
+
window.addEventListener('message', handleWidgeticReady);
|
|
2506
|
+
|
|
2507
|
+
// Listen for SvelteKit hydration completion signal (legacy VM / published SvelteKit artifacts)
|
|
2508
|
+
const handleHydrationSignal = (event: MessageEvent) => {
|
|
2509
|
+
if (event.data?.type === 'sveltekit-hydration-complete') {
|
|
2510
|
+
previewHydrating = false;
|
|
2511
|
+
hydrationRetryCount = 0;
|
|
2512
|
+
if (hydrationSafetyTimeout) { clearTimeout(hydrationSafetyTimeout); hydrationSafetyTimeout = null; }
|
|
2513
|
+
markPreviewWidgetReady();
|
|
2514
|
+
}
|
|
2515
|
+
};
|
|
2516
|
+
window.addEventListener('message', handleHydrationSignal);
|
|
2517
|
+
|
|
2518
|
+
// Listen for runtime error signals from the preview iframe
|
|
2519
|
+
const handleIframeRuntimeError = (event: MessageEvent) => {
|
|
2520
|
+
if (event.data?.type !== 'sveltekit-runtime-error') return;
|
|
2521
|
+
|
|
2522
|
+
const runtimeMsg = String(event.data?.message || event.data?.error || '').trim();
|
|
2523
|
+
const runtimeSource = event.data?.source ? String(event.data.source) : '';
|
|
2524
|
+
const runtimeLine = event.data?.line != null ? Number(event.data.line) : null;
|
|
2525
|
+
|
|
2526
|
+
// Do not surface mid-generation / post-codegen races over a loading or good preview
|
|
2527
|
+
if (isGeneratingCode) {
|
|
2528
|
+
console.warn('[WidgetDetails] Ignoring runtime error during code generation:', runtimeMsg);
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
if (isCodegenPreviewFresh()) {
|
|
2532
|
+
console.warn('[WidgetDetails] Ignoring runtime error during codegen preview window:', runtimeMsg);
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
// Stale iframe (previous build) must not poison the active preview
|
|
2536
|
+
const activeWin = previewIframe?.contentWindow;
|
|
2537
|
+
if (activeWin && event.source && event.source !== activeWin) {
|
|
2538
|
+
console.warn('[WidgetDetails] Ignoring runtime error from inactive iframe');
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
if (isBenignPreviewRuntimeError(runtimeMsg)) {
|
|
2542
|
+
console.warn('[WidgetDetails] Ignoring benign preview runtime error:', runtimeMsg);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
// Widget already running — only overlay for hard compile/syntax failures
|
|
2546
|
+
if (previewWidgetReady && !looksLikeHardCompileFailure(runtimeMsg)) {
|
|
2547
|
+
console.warn(
|
|
2548
|
+
'[WidgetDetails] Ignoring non-fatal runtime error after widget ready (preview stays visible):',
|
|
2549
|
+
runtimeMsg,
|
|
2550
|
+
);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
clearPendingRuntimeErrorOverlay();
|
|
2555
|
+
const applyOverlay = () => {
|
|
2556
|
+
pendingRuntimeErrorTimer = null;
|
|
2557
|
+
// Ready arrived while we waited — keep the working preview.
|
|
2558
|
+
if (previewWidgetReady && !looksLikeHardCompileFailure(runtimeMsg)) {
|
|
2559
|
+
console.warn('[WidgetDetails] Cancelled runtime error overlay — widget became ready');
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
previewHasRuntimeError = true;
|
|
2563
|
+
previewRuntimeErrorStatus = 500;
|
|
2564
|
+
previewHydrating = false;
|
|
2565
|
+
previewLoading = false;
|
|
2566
|
+
if (runtimeMsg) {
|
|
2567
|
+
const locationSuffix =
|
|
2568
|
+
runtimeSource && runtimeLine
|
|
2569
|
+
? ` (${runtimeSource}:${runtimeLine})`
|
|
2570
|
+
: runtimeSource
|
|
2571
|
+
? ` (${runtimeSource})`
|
|
2572
|
+
: '';
|
|
2573
|
+
previewError = `${runtimeMsg}${locationSuffix}`;
|
|
2574
|
+
} else if (!previewError) {
|
|
2575
|
+
previewError = 'Widget preview runtime error (HTTP 500)';
|
|
2576
|
+
}
|
|
2577
|
+
notifyPreviewCompileErrorChanged();
|
|
2578
|
+
if (hydrationSafetyTimeout) {
|
|
2579
|
+
clearTimeout(hydrationSafetyTimeout);
|
|
2580
|
+
hydrationSafetyTimeout = null;
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
|
|
2584
|
+
// Brief delay: successful widgetic:ready often arrives right after a transient error.
|
|
2585
|
+
pendingRuntimeErrorTimer = setTimeout(applyOverlay, 1600);
|
|
2586
|
+
};
|
|
2587
|
+
window.addEventListener('message', handleIframeRuntimeError);
|
|
2588
|
+
|
|
2589
|
+
// Listen for expired preview token signal
|
|
2590
|
+
const handleTokenExpired = async (event: MessageEvent) => {
|
|
2591
|
+
if (event.data?.type === 'preview-token-expired') {
|
|
2592
|
+
directPreviewUrl = null;
|
|
2593
|
+
lastPreviewUrlFetchTime = 0;
|
|
2594
|
+
try {
|
|
2595
|
+
const freshUrl = await fetchDirectPreviewUrl();
|
|
2596
|
+
if (freshUrl) {
|
|
2597
|
+
const cacheBuster = Date.now();
|
|
2598
|
+
const url = new URL(freshUrl);
|
|
2599
|
+
url.searchParams.set('_t', cacheBuster.toString());
|
|
2600
|
+
iframeSrc = url.toString();
|
|
2601
|
+
previewCacheBuster = cacheBuster;
|
|
2602
|
+
}
|
|
2603
|
+
} catch (err) { console.error('Error refreshing preview token:', err); }
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
window.addEventListener('message', handleTokenExpired);
|
|
2607
|
+
|
|
2608
|
+
// Worker 404 page notifies parent when widget is not built yet.
|
|
2609
|
+
// If the widget already has a GitLab repo with committed code, kick off a rebuild
|
|
2610
|
+
// automatically — Worker cache is transient, we should not require the user to press
|
|
2611
|
+
// "Rebuild" whenever we lose the L1/L2 cache. (Reset happens on widget switch.)
|
|
2612
|
+
const handleWidgetNotFound = async (event: MessageEvent) => {
|
|
2613
|
+
if (event.data?.type !== 'widgetic:widget-not-found') return;
|
|
2614
|
+
if (isCodegenPreviewFresh()) {
|
|
2615
|
+
console.log('[WidgetDetails] Ignoring widget-not-found during codegen preview window');
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
if (dwLoadTimeoutHandle) { clearTimeout(dwLoadTimeoutHandle); dwLoadTimeoutHandle = null; }
|
|
2619
|
+
console.log('[WidgetDetails] Worker reports widget not found — checking if repo has code');
|
|
2620
|
+
dynamicWorkerPreviewUrl = null;
|
|
2621
|
+
directPreviewUrl = null;
|
|
2622
|
+
iframeSrc = null;
|
|
2623
|
+
previewError = null;
|
|
2624
|
+
isPreviewContentReady = false;
|
|
2625
|
+
|
|
2626
|
+
const capturedWidgetId = selectedWidgetId;
|
|
2627
|
+
const hasRepo = !!currentRepositoryId;
|
|
2628
|
+
if (hasRepo && capturedWidgetId && widgetNotFoundAutoRebuildAttempts === 0) {
|
|
2629
|
+
widgetNotFoundAutoRebuildAttempts = 1;
|
|
2630
|
+
console.log('[WidgetDetails] Widget missing from Worker — auto-triggering build-from-repo');
|
|
2631
|
+
previewLoading = true;
|
|
2632
|
+
try {
|
|
2633
|
+
await triggerBuildFromRepo({ force: true });
|
|
2634
|
+
} catch (err) {
|
|
2635
|
+
console.warn('[WidgetDetails] Auto rebuild after widget-not-found failed:', err);
|
|
2636
|
+
}
|
|
2637
|
+
} else {
|
|
2638
|
+
previewLoading = false;
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2641
|
+
window.addEventListener('message', handleWidgetNotFound);
|
|
2642
|
+
window.addEventListener('message', handleWidgetConsoleMessage);
|
|
2643
|
+
|
|
2644
|
+
return () => {
|
|
2645
|
+
window.removeEventListener('message', handleWidgeticReady);
|
|
2646
|
+
window.removeEventListener('message', handleHydrationSignal);
|
|
2647
|
+
window.removeEventListener('message', handleIframeRuntimeError);
|
|
2648
|
+
window.removeEventListener('message', handleTokenExpired);
|
|
2649
|
+
window.removeEventListener('message', handleWidgetNotFound);
|
|
2650
|
+
window.removeEventListener('message', handleWidgetConsoleMessage);
|
|
2651
|
+
clearPendingRuntimeErrorOverlay();
|
|
2652
|
+
resetPreviewMessageBridge();
|
|
2653
|
+
};
|
|
2654
|
+
});
|
|
2655
|
+
|
|
2656
|
+
afterUpdate(() => {
|
|
2657
|
+
if (contentContainerEl) {
|
|
2658
|
+
hasScrollbar = contentContainerEl.scrollHeight > contentContainerEl.clientHeight;
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
onDestroy(() => {
|
|
2663
|
+
previewAreaObserver?.disconnect();
|
|
2664
|
+
if (hydrationSafetyTimeout) clearTimeout(hydrationSafetyTimeout);
|
|
2665
|
+
if (dwLoadTimeoutHandle) clearTimeout(dwLoadTimeoutHandle);
|
|
2666
|
+
clearPendingRuntimeErrorOverlay();
|
|
2667
|
+
stopPreviewTokenAutoRefresh();
|
|
2668
|
+
clearPreviewHealthCheck();
|
|
2669
|
+
});
|
|
2670
|
+
</script>
|
|
2671
|
+
|
|
2672
|
+
<!-- ═══════════════════════════════════════════════════════════════════════ -->
|
|
2673
|
+
<!-- TEMPLATE: Widget Details Floating Panel -->
|
|
2674
|
+
<!-- ═══════════════════════════════════════════════════════════════════════ -->
|
|
2675
|
+
|
|
2676
|
+
{#if selectedWidgetId}
|
|
2677
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
2678
|
+
<div
|
|
2679
|
+
class="widget-details-floating-panel pointer-events-auto fixed overflow-hidden
|
|
2680
|
+
{showWidgetDetails ? 'flex flex-col' : 'hidden'}
|
|
2681
|
+
{fillViewport
|
|
2682
|
+
? 'bg-white rounded-none border-x border-b border-t-0 ring-0 shadow-none'
|
|
2683
|
+
: 'bg-white/95 backdrop-blur-sm shadow-2xl rounded-xl border'}
|
|
2684
|
+
{isFocusedPanel
|
|
2685
|
+
? (fillViewport ? 'border-blue-400' : 'border-blue-400 ring-2 ring-blue-200/80')
|
|
2686
|
+
: 'border-gray-300 opacity-[0.97]'}"
|
|
2687
|
+
style="left: {detailsPanelX}px; top: {detailsPanelY}px; width: {detailsPanelWidth}px; z-index: {panelZIndex};
|
|
2688
|
+
height: {detailsPanelHeight ? detailsPanelHeight + 'px' : `calc(100vh - ${detailsPanelTopOffset + detailsPanelBottomMargin}px)`};"
|
|
2689
|
+
onmousedown={handlePanelFocusRequest}
|
|
2690
|
+
>
|
|
2691
|
+
<!-- Widget Details Top Bar — title, steps, and close on one row -->
|
|
2692
|
+
<div class="widget-details-header-ct flex flex-col shrink-0 bg-gray-100 border-b border-gray-200">
|
|
2693
|
+
<div
|
|
2694
|
+
class="widget-details-drag-bar relative flex items-center gap-2 px-3 py-1.5 cursor-grab active:cursor-grabbing select-none min-h-[40px] touch-none"
|
|
2695
|
+
onpointerdown={startDetailsDrag}
|
|
2696
|
+
>
|
|
2697
|
+
<h3 class="widget-details-title text-sm font-semibold text-gray-700 truncate min-w-0 max-w-[28%] shrink" title={selectedWidget?.name || 'Widget Details'}>
|
|
2698
|
+
{selectedWidget?.name || 'Widget Details'}
|
|
2699
|
+
{#if !isFocusedPanel}
|
|
2700
|
+
<span class="text-gray-400 font-normal"> · bg</span>
|
|
2701
|
+
{/if}
|
|
2702
|
+
</h3>
|
|
2703
|
+
|
|
2704
|
+
<!-- Step Indicator (inline — same row as title) -->
|
|
2705
|
+
<div class="widget-details-step-indicator-row flex flex-1 justify-center min-w-0 px-1">
|
|
2706
|
+
<div class="widget-details-step-indicator flex items-center gap-1 flex-wrap justify-center">
|
|
2707
|
+
<!-- svelte-ignore a11y_consider_explicit_label -->
|
|
2708
|
+
<button
|
|
2709
|
+
type="button"
|
|
2710
|
+
onclick={() => goToStep('create')}
|
|
2711
|
+
class="step-button step-button-create flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium transition-all
|
|
2712
|
+
{currentStep === 'create'
|
|
2713
|
+
? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
|
|
2714
|
+
: 'text-gray-600 hover:bg-gray-200 border border-transparent cursor-pointer'}"
|
|
2715
|
+
>
|
|
2716
|
+
<span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold {currentStep === 'create' ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-700'}">1</span>
|
|
2717
|
+
<span class="step-label">Create</span>
|
|
2718
|
+
</button>
|
|
2719
|
+
<div class="step-connector w-4 h-px {canEditStep ? 'bg-gray-400' : 'bg-gray-200'}"></div>
|
|
2720
|
+
<!-- svelte-ignore a11y_consider_explicit_label -->
|
|
2721
|
+
<button
|
|
2722
|
+
type="button"
|
|
2723
|
+
onclick={() => goToStep('edit')}
|
|
2724
|
+
tabindex={canEditStep ? 0 : -1}
|
|
2725
|
+
aria-disabled={!canEditStep}
|
|
2726
|
+
title={canEditStep ? 'Edit composition' : 'Publish the widget to edit composition'}
|
|
2727
|
+
class="step-button step-button-edit flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium transition-all
|
|
2728
|
+
{currentStep === 'edit'
|
|
2729
|
+
? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
|
|
2730
|
+
: canEditStep
|
|
2731
|
+
? 'text-gray-600 hover:bg-gray-200 border border-transparent cursor-pointer'
|
|
2732
|
+
: 'text-gray-300 border border-transparent cursor-default pointer-events-none'}"
|
|
2733
|
+
>
|
|
2734
|
+
<span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold {currentStep === 'edit' ? 'bg-blue-600 text-white' : canEditStep ? 'bg-gray-300 text-gray-700' : 'bg-gray-200 text-gray-400'}">2</span>
|
|
2735
|
+
<span class="step-label">Edit</span>
|
|
2736
|
+
</button>
|
|
2737
|
+
<div class="step-connector w-4 h-px {canEmbedStep ? 'bg-gray-400' : 'bg-gray-200'}"></div>
|
|
2738
|
+
<!-- svelte-ignore a11y_consider_explicit_label -->
|
|
2739
|
+
<button
|
|
2740
|
+
type="button"
|
|
2741
|
+
onclick={() => goToStep('embed')}
|
|
2742
|
+
tabindex={canEmbedStep ? 0 : -1}
|
|
2743
|
+
aria-disabled={!canEmbedStep}
|
|
2744
|
+
title={canEmbedStep ? 'Embed the published widget' : 'Publish the widget to get embed code'}
|
|
2745
|
+
class="step-button step-button-embed flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium transition-all
|
|
2746
|
+
{currentStep === 'embed'
|
|
2747
|
+
? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
|
|
2748
|
+
: canEmbedStep
|
|
2749
|
+
? 'text-gray-600 hover:bg-gray-200 border border-transparent cursor-pointer'
|
|
2750
|
+
: 'text-gray-300 border border-transparent cursor-default pointer-events-none'}"
|
|
2751
|
+
>
|
|
2752
|
+
<span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold {currentStep === 'embed' ? 'bg-blue-600 text-white' : canEmbedStep ? 'bg-gray-300 text-gray-700' : 'bg-gray-200 text-gray-400'}">3</span>
|
|
2753
|
+
<span class="step-label">Embed</span>
|
|
2754
|
+
</button>
|
|
2755
|
+
</div>
|
|
2756
|
+
</div>
|
|
2757
|
+
|
|
2758
|
+
<div class="widget-details-debug-buttons flex items-center gap-1.5 shrink-0">
|
|
2759
|
+
<button
|
|
2760
|
+
type="button"
|
|
2761
|
+
onclick={handleCloseButtonClick}
|
|
2762
|
+
onmousedown={(e) => e.stopPropagation()}
|
|
2763
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
2764
|
+
class="widget-details-close-bt w-6 h-6 flex items-center justify-center rounded-lg hover:bg-gray-200 text-gray-500 hover:text-gray-700 cursor-pointer transition-colors text-sm"
|
|
2765
|
+
title="Close widget details"
|
|
2766
|
+
>
|
|
2767
|
+
✕
|
|
2768
|
+
</button>
|
|
2769
|
+
</div>
|
|
2770
|
+
</div>
|
|
2771
|
+
</div>
|
|
2772
|
+
|
|
2773
|
+
<!-- Widget Details Content: Two-column layout -->
|
|
2774
|
+
<div class="widget-details-content-wrapper relative m-2.5 min-w-0 min-h-0 flex-1">
|
|
2775
|
+
<div class="widget-details-two-col flex h-full min-h-0 gap-0">
|
|
2776
|
+
|
|
2777
|
+
<!-- ═══ LEFT COLUMN: Step Content (scrollable) ═══ -->
|
|
2778
|
+
<div
|
|
2779
|
+
class="widget-details-left-col flex flex-col min-w-0 min-h-0 relative"
|
|
2780
|
+
style="width: {leftColPercent}%; flex: 0 0 {leftColPercent}%;"
|
|
2781
|
+
>
|
|
2782
|
+
<div class="widget-details-left-border absolute inset-y-0 left-0 right-0 border border-gray-300 rounded-lg pointer-events-none z-10"></div>
|
|
2783
|
+
|
|
2784
|
+
{#if selectedWidgetId}
|
|
2785
|
+
<!-- Sticky Widget Info Bar (always visible above scrollable content) -->
|
|
2786
|
+
<div class="widget-info-sticky-bar flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-gray-50 rounded-t-lg shrink-0 gap-2 min-w-0 z-5">
|
|
2787
|
+
<div class="widget-info-name flex items-center min-w-0 gap-1 text-sm text-gray-700">
|
|
2788
|
+
<EditableName
|
|
2789
|
+
name={selectedWidget?.name || ''}
|
|
2790
|
+
placeholder="Unnamed"
|
|
2791
|
+
tooltipText="Double-click to rename"
|
|
2792
|
+
class="flex-1 min-w-0"
|
|
2793
|
+
on:rename={({ detail }) => dispatch('widgetRename', { widgetId: selectedWidgetId, newName: detail.newName })}
|
|
2794
|
+
/>
|
|
2795
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
2796
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
2797
|
+
<span class="ml-1 text-[10px] font-mono text-gray-400 cursor-pointer hover:text-gray-600 shrink-0" title="Click to copy widget ID" onclick={() => { navigator.clipboard.writeText(selectedWidgetId); showToast('success', 'Widget ID copied', { duration: 2000 }); }}>(id: {selectedWidgetId.substring(0, 8)}…)</span>
|
|
2798
|
+
</div>
|
|
2799
|
+
<div class="widget-info-meta flex items-center gap-2 shrink-0">
|
|
2800
|
+
{#if saveStatus === 'saving'}
|
|
2801
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 animate-pulse">Saving...</span>
|
|
2802
|
+
{:else if saveStatus === 'saved'}
|
|
2803
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded bg-green-100 text-green-700">Saved</span>
|
|
2804
|
+
{:else if saveStatus === 'error'}
|
|
2805
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-700">Save Error</span>
|
|
2806
|
+
{/if}
|
|
2807
|
+
<span class="text-[10px] text-gray-500">
|
|
2808
|
+
Commit: <span class="font-mono font-medium text-gray-600" title={lastCommitId || ''}>{lastCommitId ? lastCommitId.substring(0, 8) : '—'}</span>
|
|
2809
|
+
</span>
|
|
2810
|
+
{#if lastPublishedVersion !== null}
|
|
2811
|
+
<a href="/test-widget?widgetId={selectedWidgetId}&version={lastPublishedVersion}" target="_blank" rel="noopener noreferrer" class="text-[10px] px-1.5 py-0.5 rounded bg-emerald-50 border border-emerald-200 text-emerald-600 hover:bg-emerald-100 transition-colors whitespace-nowrap" title="Open published widget v{lastPublishedVersion}">Live: v{lastPublishedVersion} ↗</a>
|
|
2812
|
+
{/if}
|
|
2813
|
+
</div>
|
|
2814
|
+
</div>
|
|
2815
|
+
|
|
2816
|
+
<!-- Scrollable step content -->
|
|
2817
|
+
<div
|
|
2818
|
+
class="widget-details-left-content flex flex-col flex-1 min-h-0 relative rounded-b-lg {currentStep === 'edit' ? 'overflow-hidden' : 'overflow-y-auto'}"
|
|
2819
|
+
bind:this={contentContainerEl}
|
|
2820
|
+
>
|
|
2821
|
+
<div class="actions-panel widget-details-actions-panel relative flex flex-col flex-1 min-h-0">
|
|
2822
|
+
|
|
2823
|
+
<!-- Keep all steps mounted — switching tabs must not destroy chat / PropsEditor state -->
|
|
2824
|
+
<div class="widget-details-create-step flex flex-col gap-2 p-2 {currentStep === 'create' ? '' : 'hidden'}">
|
|
2825
|
+
<slot name="code-generation" />
|
|
2826
|
+
<slot name="publish-widget" />
|
|
2827
|
+
</div>
|
|
2828
|
+
<div class="widget-details-edit-step flex flex-col h-full min-h-0 overflow-hidden {currentStep === 'edit' ? '' : 'hidden'}">
|
|
2829
|
+
<slot name="composition-editor" />
|
|
2830
|
+
</div>
|
|
2831
|
+
<div class="widget-details-embed-step flex flex-col gap-2 p-2 {currentStep === 'embed' ? '' : 'hidden'}">
|
|
2832
|
+
<slot name="embed-section" />
|
|
2833
|
+
</div>
|
|
2834
|
+
|
|
2835
|
+
</div> <!-- end of actions-panel -->
|
|
2836
|
+
</div> <!-- end of widget-details-left-content -->
|
|
2837
|
+
{/if}
|
|
2838
|
+
|
|
2839
|
+
</div> <!-- end of widget-details-left-col -->
|
|
2840
|
+
|
|
2841
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
2842
|
+
<div
|
|
2843
|
+
class="widget-details-col-resizer w-2.5 shrink-0 cursor-col-resize relative z-20 group"
|
|
2844
|
+
title="Drag to resize chat / preview"
|
|
2845
|
+
onmousedown={startColumnResize}
|
|
2846
|
+
>
|
|
2847
|
+
<div class="widget-details-col-resizer-line absolute inset-y-3 left-1/2 w-0.5 -translate-x-1/2 rounded-full bg-gray-300 group-hover:bg-forest-green transition-colors"></div>
|
|
2848
|
+
</div>
|
|
2849
|
+
|
|
2850
|
+
<!-- ═══ RIGHT COLUMN: Widget Preview (always visible) ═══ -->
|
|
2851
|
+
<div
|
|
2852
|
+
class="widget-details-right-col flex flex-col min-w-[240px] min-h-0 relative flex-1"
|
|
2853
|
+
onmousedown={handlePanelFocusRequest}
|
|
2854
|
+
>
|
|
2855
|
+
<div class="widget-details-right-border absolute inset-0 border border-gray-300 rounded-lg pointer-events-none z-10"></div>
|
|
2856
|
+
<div class="widget-details-right-content flex flex-col h-full rounded-lg overflow-hidden bg-white">
|
|
2857
|
+
|
|
2858
|
+
<!-- Preview Header -->
|
|
2859
|
+
<div class="preview-header relative flex items-center justify-center px-3 py-1.5 border-b border-gray-200 shrink-0">
|
|
2860
|
+
<!-- Center: title + optional debug tabs -->
|
|
2861
|
+
<div class="preview-header-center flex items-center gap-2">
|
|
2862
|
+
<span class="text-xs font-semibold text-gray-600">Live Preview</span>
|
|
2863
|
+
{#if debugMode}
|
|
2864
|
+
<div class="preview-header-tabs flex items-center gap-1 ml-1">
|
|
2865
|
+
<button type="button" onclick={() => toggleInlinePanelView('preview')} title="Switch to preview" class="px-2 py-0.5 text-[10px] rounded border transition-colors cursor-pointer {inlinePanelView === 'preview' ? 'border-blue-400 bg-blue-50 text-blue-700 font-medium' : 'border-gray-200 text-gray-500 hover:bg-gray-50'}">Preview</button>
|
|
2866
|
+
<button type="button" onclick={() => toggleInlinePanelView('console')} title="Switch to console" class="px-2 py-0.5 text-[10px] rounded border transition-colors cursor-pointer {inlinePanelView === 'console' ? 'border-green-400 bg-green-50 text-green-700 font-medium' : 'border-gray-200 text-gray-500 hover:bg-gray-50'}">Console</button>
|
|
2867
|
+
</div>
|
|
2868
|
+
{/if}
|
|
2869
|
+
</div>
|
|
2870
|
+
<!-- Right: action buttons (Rebuild on Create step only — refresh lives in URL bar) -->
|
|
2871
|
+
<div class="preview-header-buttons absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
|
2872
|
+
{#if selectedWidgetId && !previewLoading && !isGeneratingCode && inlinePanelView !== 'console' && (currentStep === 'create' || previewError || publishedArtifactReachable === false)}
|
|
2873
|
+
<button
|
|
2874
|
+
type="button"
|
|
2875
|
+
onclick={() => retryPreview()}
|
|
2876
|
+
class="preview-rebuild-btn px-2 py-0.5 text-[10px] rounded border border-gray-200 text-gray-600 bg-white hover:bg-gray-50 cursor-pointer {previewError || previewHasRuntimeError || publishedArtifactReachable === false ? 'border-blue-400 bg-blue-50 text-blue-700' : ''}"
|
|
2877
|
+
title="Recompile widget code for preview"
|
|
2878
|
+
>{isRebuildingPreview ? '⏳ Rebuilding…' : '🔄 Rebuild'}</button>
|
|
2879
|
+
{/if}
|
|
2880
|
+
</div>
|
|
2881
|
+
</div>
|
|
2882
|
+
|
|
2883
|
+
<!-- Preview Content -->
|
|
2884
|
+
<div class="preview-content-ct flex flex-col flex-1 min-h-0 relative px-3 pt-3 pb-7 overflow-hidden" bind:this={previewAreaEl}>
|
|
2885
|
+
{#if !isFocusedPanel}
|
|
2886
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
2887
|
+
<div
|
|
2888
|
+
class="widget-details-preview-focus-overlay absolute inset-0 z-20 cursor-pointer"
|
|
2889
|
+
onmousedown={handlePanelFocusRequest}
|
|
2890
|
+
title="Click to focus this widget panel"
|
|
2891
|
+
></div>
|
|
2892
|
+
{/if}
|
|
2893
|
+
{#if iframeSrc || directPreviewUrl || showPreviewModal || ((isGeneratingCode || previewLoading) && inlinePanelView === 'preview')}
|
|
2894
|
+
<div class="widget-preview-wrapper flex-1 min-h-0 {showPreviewModal ? 'widget-preview-in-modal' : ''}{inlinePanelView === 'preview' || showPreviewModal ? '' : ' hidden'}">
|
|
2895
|
+
{#if showPreviewModal}
|
|
2896
|
+
<div class="widget-preview-modal-chrome">
|
|
2897
|
+
<div class="flex items-start justify-between gap-3 mb-2">
|
|
2898
|
+
<div class="min-w-0 flex-1 flex flex-col gap-1">
|
|
2899
|
+
{#if selectedWidget?.name}
|
|
2900
|
+
<span class="text-base font-semibold text-gray-800 truncate">{selectedWidget.name}</span>
|
|
2901
|
+
{/if}
|
|
2902
|
+
<p class="text-xs text-gray-500">Live preview below.</p>
|
|
2903
|
+
</div>
|
|
2904
|
+
<Button onclick={() => { showPreviewModal = false; }} variant="outline" size="sm" class="close-preview-modal-btn shrink-0 w-6 h-6 p-[3px] cursor-pointer border border-black text-md bg-white text-black hover:bg-gray-100 rounded-lg">X</Button>
|
|
2905
|
+
</div>
|
|
2906
|
+
</div>
|
|
2907
|
+
{/if}
|
|
2908
|
+
<WidgetPreview
|
|
2909
|
+
bind:this={widgetPreviewComponentRef}
|
|
2910
|
+
mode="controlled"
|
|
2911
|
+
enableResize={true}
|
|
2912
|
+
autoFitStage={showPreviewModal}
|
|
2913
|
+
src={iframeSrc || getFallbackPreviewUrl()}
|
|
2914
|
+
displayUrl={directPreviewUrl || iframeSrc}
|
|
2915
|
+
bind:resizeWidth={previewResizeWidth}
|
|
2916
|
+
bind:resizeHeight={previewResizeHeight}
|
|
2917
|
+
bind:iframeEl={previewIframe}
|
|
2918
|
+
iframeReloadKey={previewIframeGeneration}
|
|
2919
|
+
{previewLoading}
|
|
2920
|
+
{previewHydrating}
|
|
2921
|
+
{previewError}
|
|
2922
|
+
{previewRetryCount}
|
|
2923
|
+
{previewHasRuntimeError}
|
|
2924
|
+
{previewRuntimeErrorStatus}
|
|
2925
|
+
isContentReady={isPreviewContentReady}
|
|
2926
|
+
isLoadingUrl={isLoadingDirectPreviewUrl}
|
|
2927
|
+
{forceMockReady}
|
|
2928
|
+
{isGeneratingCode}
|
|
2929
|
+
{debugMode}
|
|
2930
|
+
contentHeight={showPreviewModal ? '100%' : '100%'}
|
|
2931
|
+
showUrlBar={true}
|
|
2932
|
+
showUrlBarButtons={true}
|
|
2933
|
+
on:refresh={() => { refreshPreviewIframe(); }}
|
|
2934
|
+
on:fitToArea={fitPreviewToArea}
|
|
2935
|
+
on:openInTab={() => { const url = directPreviewUrl || iframeSrc; if (url) window.open(url, '_blank'); }}
|
|
2936
|
+
on:desktopViewport={openDesktopPreview}
|
|
2937
|
+
on:iframeLoad={handlePreviewIframeLoad}
|
|
2938
|
+
on:iframeError={handlePreviewIframeError}
|
|
2939
|
+
on:retry={retryPreview}
|
|
2940
|
+
on:fixErrors={() => { if (showPreviewModal) showPreviewModal = false; dispatch('fixErrors'); }}
|
|
2941
|
+
on:viewErrors={() => { if (showPreviewModal) showPreviewModal = false; inlinePanelView = 'console'; }}
|
|
2942
|
+
on:retryPreview={() => { previewHasRuntimeError = false; previewRuntimeErrorStatus = null; refreshPreviewIframe(); }}
|
|
2943
|
+
/>
|
|
2944
|
+
</div>
|
|
2945
|
+
{:else}
|
|
2946
|
+
<div class="widget-preview-fallback relative flex items-center justify-center h-full rounded-lg bg-gray-50 overflow-hidden">
|
|
2947
|
+
{#if isGeneratingCode || previewLoading}
|
|
2948
|
+
<GenerateLoader
|
|
2949
|
+
title={isGeneratingCode ? 'Generating code…' : 'Building preview…'}
|
|
2950
|
+
size="md"
|
|
2951
|
+
/>
|
|
2952
|
+
{:else if previewError === 'NO_CODE_YET' && !hasWidgetCode}
|
|
2953
|
+
<div class="widget-preview-no-code flex flex-col items-center justify-center text-center p-4">
|
|
2954
|
+
{#if !normalizeRepositoryId(selectedWidget) && repositorySetupStatus !== 'failed'}
|
|
2955
|
+
<GenerateLoader
|
|
2956
|
+
title="Preparing widget…"
|
|
2957
|
+
subtitle="Setting up repository…"
|
|
2958
|
+
size="md"
|
|
2959
|
+
/>
|
|
2960
|
+
{:else if !normalizeRepositoryId(selectedWidget)}
|
|
2961
|
+
<div class="text-3xl mb-2">🖼️</div>
|
|
2962
|
+
<div class="font-medium text-sm text-red-600 mb-1">Widget has no repository</div>
|
|
2963
|
+
<div class="text-xs max-w-[200px] text-gray-500 mb-3">Repository creation failed previously. Click below to retry.</div>
|
|
2964
|
+
<button type="button" onclick={() => { dispatch('retryRepository', { widgetId: selectedWidgetId }); }} class="px-3 py-1.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer font-medium">🔧 Create Repository</button>
|
|
2965
|
+
{:else}
|
|
2966
|
+
<div class="text-3xl mb-2">🖼️</div>
|
|
2967
|
+
<div class="font-medium text-sm text-gray-700 mb-1">No Code Yet</div>
|
|
2968
|
+
<div class="text-xs max-w-[200px] text-gray-500 mb-3">Write a prompt and generate code to see preview here.</div>
|
|
2969
|
+
<button type="button" onclick={() => { dispatch('generateNow', { widgetId: selectedWidgetId }); }} class="generate-now-btn px-3 py-1.5 text-xs rounded border border-green-500 text-green-700 bg-white hover:bg-green-50 cursor-pointer font-medium">▶ Generate Now</button>
|
|
2970
|
+
{/if}
|
|
2971
|
+
</div>
|
|
2972
|
+
{:else if previewError && (lastPublishedVersion !== null || previewError !== 'NO_CODE_YET')}
|
|
2973
|
+
<div class="widget-preview-error-indicator flex flex-col items-center justify-center text-center p-4">
|
|
2974
|
+
<div class="text-3xl mb-2">⚠️</div>
|
|
2975
|
+
<div class="font-medium text-sm text-red-600 mb-1">Preview Unavailable</div>
|
|
2976
|
+
<div class="text-xs max-w-[200px] text-gray-600 mb-3">{previewError}</div>
|
|
2977
|
+
{#if !normalizeRepositoryId(selectedWidget)}
|
|
2978
|
+
<button type="button" onclick={() => { previewError = null; dispatch('retryRepository', { widgetId: selectedWidgetId }); }} class="px-3 py-1.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer font-medium">🔧 Create Repository</button>
|
|
2979
|
+
{:else}
|
|
2980
|
+
<div class="preview-error-actions flex flex-col gap-2 items-center">
|
|
2981
|
+
{#if isCdnArtifactMissingError(previewError) || isInfrastructurePreviewError(previewError)}
|
|
2982
|
+
<button type="button" onclick={() => { previewError = null; previewRetryCount++; void triggerBuildFromRepo({ force: true }); }} class="px-3 py-1.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer font-medium">🔄 Rebuild code</button>
|
|
2983
|
+
{:else if lastCommitId || lastPublishedVersion !== null}
|
|
2984
|
+
<button type="button" onclick={() => { dispatch('fixCompileError', { widgetId: selectedWidgetId, error: previewError }); }} class="fix-compile-error-btn px-3 py-1.5 text-xs rounded border border-amber-500 text-amber-800 bg-white hover:bg-amber-50 cursor-pointer font-medium">🔧 Fix Compile Error</button>
|
|
2985
|
+
{:else}
|
|
2986
|
+
<button type="button" onclick={() => { dispatch('generateNow', { widgetId: selectedWidgetId }); }} class="generate-now-btn px-3 py-1.5 text-xs rounded border border-green-500 text-green-700 bg-white hover:bg-green-50 cursor-pointer font-medium">▶ Generate Now</button>
|
|
2987
|
+
{/if}
|
|
2988
|
+
{#if !isCdnArtifactMissingError(previewError) && !isInfrastructurePreviewError(previewError)}
|
|
2989
|
+
<button type="button" onclick={() => { previewError = null; previewRetryCount++; triggerBuildFromRepo(); }} class="px-2 py-1 text-[10px] rounded border border-gray-300 text-gray-500 bg-white hover:bg-gray-50 cursor-pointer">🔄 Retry Build</button>
|
|
2990
|
+
{/if}
|
|
2991
|
+
</div>
|
|
2992
|
+
{/if}
|
|
2993
|
+
</div>
|
|
2994
|
+
{:else if isLoadingDirectPreviewUrl || previewLoading}
|
|
2995
|
+
<GenerateLoader title="Building Preview…" subtitle="Compiling widget code…" size="md" />
|
|
2996
|
+
{:else}
|
|
2997
|
+
<GenerateLoader
|
|
2998
|
+
class="widget-preview-idle-indicator text-gray-400"
|
|
2999
|
+
title=""
|
|
3000
|
+
subtitle="Preview will appear after code generation."
|
|
3001
|
+
size="md"
|
|
3002
|
+
/>
|
|
3003
|
+
{/if}
|
|
3004
|
+
</div>
|
|
3005
|
+
{/if}
|
|
3006
|
+
|
|
3007
|
+
<!-- Console Panel -->
|
|
3008
|
+
{#if inlinePanelView === 'console'}
|
|
3009
|
+
<div class="console-panel-ct flex flex-col h-full bg-gray-900 rounded-lg overflow-hidden">
|
|
3010
|
+
<div class="console-panel-header flex items-center justify-between px-3 py-1.5 bg-gray-800 border-b border-gray-700">
|
|
3011
|
+
<span class="text-[10px] font-mono text-gray-400">{consoleLogs.length} message{consoleLogs.length !== 1 ? 's' : ''}</span>
|
|
3012
|
+
<button type="button" onclick={() => { consoleLogs = []; }} class="text-[10px] text-gray-500 hover:text-gray-300 cursor-pointer">Clear</button>
|
|
3013
|
+
</div>
|
|
3014
|
+
<div class="console-panel-logs flex-1 overflow-y-auto p-2 font-mono text-[11px] leading-relaxed" bind:this={consoleScrollEl}>
|
|
3015
|
+
{#if consoleLogs.length === 0}
|
|
3016
|
+
<div class="console-panel-empty flex items-center justify-center h-full text-gray-600 text-xs">
|
|
3017
|
+
No console output yet. Widget logs will appear here.
|
|
3018
|
+
</div>
|
|
3019
|
+
{:else}
|
|
3020
|
+
{#each consoleLogs as entry}
|
|
3021
|
+
<div class="console-entry py-0.5 border-b border-gray-800 {entry.level === 'error' ? 'text-red-400' : entry.level === 'warn' ? 'text-yellow-400' : 'text-gray-300'}">
|
|
3022
|
+
<span class="text-gray-600 mr-2">{entry.timestamp.toLocaleTimeString()}</span>
|
|
3023
|
+
<span>{entry.message}</span>
|
|
3024
|
+
</div>
|
|
3025
|
+
{/each}
|
|
3026
|
+
{/if}
|
|
3027
|
+
</div>
|
|
3028
|
+
</div>
|
|
3029
|
+
{/if}
|
|
3030
|
+
</div>
|
|
3031
|
+
|
|
3032
|
+
</div> <!-- end of widget-details-right-content -->
|
|
3033
|
+
</div> <!-- end of widget-details-right-col -->
|
|
3034
|
+
|
|
3035
|
+
</div> <!-- end of widget-details-two-col -->
|
|
3036
|
+
|
|
3037
|
+
<!-- Modal backdrop — clicking outside closes -->
|
|
3038
|
+
{#if showPreviewModal}
|
|
3039
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
3040
|
+
<div
|
|
3041
|
+
bind:this={previewModalRef}
|
|
3042
|
+
class="widget-preview-modal-backdrop fixed inset-0 z-99998 bg-black/70"
|
|
3043
|
+
onclick={() => { showPreviewModal = false; }}
|
|
3044
|
+
onkeydown={(e) => { if (e.key === 'Escape') showPreviewModal = false; }}
|
|
3045
|
+
role="dialog"
|
|
3046
|
+
aria-modal="true"
|
|
3047
|
+
tabindex="-1"
|
|
3048
|
+
use:focusOnMount
|
|
3049
|
+
></div>
|
|
3050
|
+
{/if}
|
|
3051
|
+
|
|
3052
|
+
</div> <!-- end of widget-details-content-wrapper -->
|
|
3053
|
+
|
|
3054
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
3055
|
+
<div class="absolute bottom-0 right-0 w-4 h-4 cursor-nwse-resize z-10 group" onmousedown={startDetailsResize} title="Resize panel">
|
|
3056
|
+
<svg class="w-3 h-3 absolute bottom-0.5 right-0.5 text-gray-400 group-hover:text-gray-600 transition-colors" viewBox="0 0 6 6" fill="currentColor">
|
|
3057
|
+
<circle cx="5" cy="1" r="0.7"/>
|
|
3058
|
+
<circle cx="3" cy="3" r="0.7"/>
|
|
3059
|
+
<circle cx="5" cy="3" r="0.7"/>
|
|
3060
|
+
<circle cx="1" cy="5" r="0.7"/>
|
|
3061
|
+
<circle cx="3" cy="5" r="0.7"/>
|
|
3062
|
+
<circle cx="5" cy="5" r="0.7"/>
|
|
3063
|
+
</svg>
|
|
3064
|
+
</div>
|
|
3065
|
+
</div>
|
|
3066
|
+
{/if}
|
|
3067
|
+
|
|
3068
|
+
<style>
|
|
3069
|
+
.widget-details-left-content {
|
|
3070
|
+
overflow-y: auto;
|
|
3071
|
+
overflow-x: hidden;
|
|
3072
|
+
scrollbar-width: thin;
|
|
3073
|
+
scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
|
|
3074
|
+
}
|
|
3075
|
+
.widget-details-left-content::-webkit-scrollbar {
|
|
3076
|
+
width: 10px;
|
|
3077
|
+
background: transparent;
|
|
3078
|
+
}
|
|
3079
|
+
.widget-details-left-content::-webkit-scrollbar-track {
|
|
3080
|
+
background: transparent;
|
|
3081
|
+
}
|
|
3082
|
+
.widget-details-left-content::-webkit-scrollbar-thumb {
|
|
3083
|
+
background: rgba(0, 0, 0, 0.12);
|
|
3084
|
+
border-radius: 10px;
|
|
3085
|
+
border: 3px solid transparent;
|
|
3086
|
+
background-clip: content-box;
|
|
3087
|
+
}
|
|
3088
|
+
.widget-details-left-content::-webkit-scrollbar-thumb:hover {
|
|
3089
|
+
background: rgba(0, 0, 0, 0.22);
|
|
3090
|
+
border: 3px solid transparent;
|
|
3091
|
+
background-clip: content-box;
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
/* CSS-teleport: lift preview wrapper into a full-window modal overlay */
|
|
3095
|
+
:global(.widget-preview-in-modal) {
|
|
3096
|
+
position: fixed !important;
|
|
3097
|
+
z-index: 99999 !important;
|
|
3098
|
+
inset: 2rem !important;
|
|
3099
|
+
transform: none !important;
|
|
3100
|
+
width: auto !important;
|
|
3101
|
+
height: auto !important;
|
|
3102
|
+
background: white !important;
|
|
3103
|
+
border-radius: 0.75rem !important;
|
|
3104
|
+
border: 1px solid black !important;
|
|
3105
|
+
padding: 1rem 1.5rem 1.5rem !important;
|
|
3106
|
+
box-shadow: 0 25px 50px rgba(0,0,0,0.25) !important;
|
|
3107
|
+
display: flex !important;
|
|
3108
|
+
flex-direction: column !important;
|
|
3109
|
+
overflow: hidden !important;
|
|
3110
|
+
}
|
|
3111
|
+
:global(.widget-preview-in-modal .widget-preview-modal-chrome) {
|
|
3112
|
+
flex-shrink: 0 !important;
|
|
3113
|
+
}
|
|
3114
|
+
:global(.widget-preview-in-modal .preview-content-url-bar) {
|
|
3115
|
+
flex-shrink: 0 !important;
|
|
3116
|
+
}
|
|
3117
|
+
:global(.widget-preview-in-modal .preview-content-container) {
|
|
3118
|
+
flex: 1 !important;
|
|
3119
|
+
height: auto !important;
|
|
3120
|
+
min-height: 0 !important;
|
|
3121
|
+
overflow: visible !important;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
:global(.widget-details-right-content .widget-preview-wrapper) {
|
|
3125
|
+
overflow: visible !important;
|
|
3126
|
+
}
|
|
3127
|
+
</style>
|