dr-data commited on
Commit
8e395e9
·
1 Parent(s): dcd5e1d

Update DeepSite v2 for Hugging Face Spaces deployment

Browse files
.gitignore CHANGED
@@ -39,3 +39,25 @@ yarn-error.log*
39
  # typescript
40
  *.tsbuildinfo
41
  next-env.d.ts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  # typescript
40
  *.tsbuildinfo
41
  next-env.d.ts
42
+
43
+ # Documentation files
44
+ *.md
45
+ !README.md
46
+ CLAUDE.md
47
+ EMERGENCY_FIX_SUMMARY.md
48
+ GITHUB_REPO_SETUP.md
49
+ PREVIEW_*.md
50
+ SMOOTH_*.md
51
+ TRUE_*.md
52
+ TYPESCRIPT_*.md
53
+ ZERO_*.md
54
+
55
+ # Test files
56
+ test-*.js
57
+ test-*.mjs
58
+ test-*.html
59
+ debug-*.html
60
+
61
+ # Backup files
62
+ *.backup
63
+ *.complex-backup
app/api/test-direct/route.ts ADDED
File without changes
app/api/test-minimal/route.ts ADDED
File without changes
app/api/test-working/route.ts ADDED
File without changes
assets/globals.css CHANGED
@@ -138,40 +138,73 @@
138
  @apply !bg-neutral-900;
139
  }
140
 
141
- /* ZERO-FLASH dual iframe system for ultra-smooth preview updates */
142
- #preview-iframe-1, #preview-iframe-2 {
143
- transition: opacity 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
144
- transform 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
145
- /* Prevent flash of unstyled content with white background */
146
- background: #ffffff;
147
- /* Hardware acceleration for smoother transitions */
148
- will-change: opacity, transform;
 
 
 
 
 
149
  transform: translateZ(0);
 
 
 
 
 
 
 
 
 
 
 
 
150
  z-index: 10;
151
  }
152
 
153
- /* Smooth swapping transitions */
154
- #preview-iframe-1.swapping, #preview-iframe-2.swapping {
155
- transition: opacity 150ms ease-out, transform 150ms ease-out;
 
156
  }
157
 
158
- /* Legacy iframe support (fallback) */
159
- #preview-iframe {
160
- transition: opacity 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
161
- background: #ffffff;
162
- will-change: opacity;
 
 
 
 
 
163
  transform: translateZ(0);
164
  }
165
 
166
- /* Zero-flash loading states */
167
- #preview-iframe-1:not([src]):not([srcdoc]),
168
- #preview-iframe-2:not([src]):not([srcdoc]) {
169
- opacity: 0;
170
  }
171
 
172
- #preview-iframe-1[srcdoc],
173
- #preview-iframe-2[srcdoc] {
174
- opacity: inherit; /* Respect the active/inactive state */
 
 
 
 
 
 
 
 
 
 
175
  }
176
 
177
  /* Ultra-smooth fade-in animation for new content with anti-flash */
@@ -245,3 +278,142 @@
245
  .matched-line {
246
  @apply bg-sky-500/30;
247
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  @apply !bg-neutral-900;
139
  }
140
 
141
+ /* 🚀 BULLETPROOF ZERO-FLASH PREVIEW SYSTEM */
142
+
143
+ /* Main preview iframe with anti-flash protection */
144
+ #preview-iframe {
145
+ /* Bulletproof anti-flash transitions */
146
+ transition: opacity 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
147
+
148
+ /* Prevent any flash with guaranteed white background */
149
+ background-color: #ffffff !important;
150
+ background-image: none !important;
151
+
152
+ /* Hardware acceleration for ultra-smooth performance */
153
+ will-change: opacity;
154
  transform: translateZ(0);
155
+ backface-visibility: hidden;
156
+
157
+ /* Ensure immediate visibility */
158
+ opacity: 1;
159
+
160
+ /* Smooth scrolling for content updates */
161
+ scroll-behavior: smooth;
162
+
163
+ /* Prevent layout shifts */
164
+ min-height: 100%;
165
+
166
+ /* Z-index management */
167
  z-index: 10;
168
  }
169
 
170
+ /* Loading state - fade in smoothly */
171
+ #preview-iframe.loading {
172
+ opacity: 0.7;
173
+ transition: opacity 150ms ease-out;
174
  }
175
 
176
+ /* Enhanced container for smooth updates */
177
+ .smooth-preview-container {
178
+ /* Prevent layout jumps and overflow flashes */
179
+ overflow: hidden;
180
+ position: relative;
181
+
182
+ /* Ensure smooth transitions */
183
+ will-change: contents;
184
+
185
+ /* Hardware acceleration */
186
  transform: translateZ(0);
187
  }
188
 
189
+ /* Support for smooth element transitions within iframes */
190
+ .smooth-element-transition {
191
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
192
+ will-change: opacity, transform;
193
  }
194
 
195
+ .smooth-content-update {
196
+ animation: smoothContentPulse 0.4s ease-in-out;
197
+ }
198
+
199
+ @keyframes smoothContentPulse {
200
+ 0%, 100% {
201
+ transform: scale(1);
202
+ opacity: 1;
203
+ }
204
+ 50% {
205
+ transform: scale(1.01);
206
+ opacity: 0.95;
207
+ }
208
  }
209
 
210
  /* Ultra-smooth fade-in animation for new content with anti-flash */
 
278
  .matched-line {
279
  @apply bg-sky-500/30;
280
  }
281
+
282
+ /* 🎨 ENHANCED IFRAME CONTENT TRANSITIONS */
283
+
284
+ /* Global styles that will be injected into iframe content */
285
+ .iframe-smooth-transitions * {
286
+ /* Smooth transitions for all content changes */
287
+ transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1),
288
+ color 200ms cubic-bezier(0.4, 0, 0.2, 1),
289
+ background-color 200ms cubic-bezier(0.4, 0, 0.2, 1),
290
+ border-color 200ms cubic-bezier(0.4, 0, 0.2, 1),
291
+ transform 200ms cubic-bezier(0.4, 0, 0.2, 1) !important;
292
+
293
+ /* Hardware acceleration */
294
+ will-change: opacity, transform, background-color;
295
+ transform: translateZ(0);
296
+ }
297
+
298
+ /* Smooth element entrance animation */
299
+ .element-entering {
300
+ opacity: 0 !important;
301
+ transform: translateY(5px) scale(0.99) translateZ(0) !important;
302
+ transition: opacity 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
303
+ transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
304
+ }
305
+
306
+ .element-entering.entered {
307
+ opacity: 1 !important;
308
+ transform: translateY(0) scale(1) translateZ(0) !important;
309
+ }
310
+
311
+ /* Smooth element exit animation */
312
+ .element-leaving {
313
+ opacity: 1 !important;
314
+ transform: translateY(0) scale(1) translateZ(0) !important;
315
+ transition: opacity 250ms cubic-bezier(0.4, 0, 1, 1),
316
+ transform 250ms cubic-bezier(0.4, 0, 1, 1) !important;
317
+ }
318
+
319
+ .element-leaving.left {
320
+ opacity: 0 !important;
321
+ transform: translateY(-5px) scale(0.98) translateZ(0) !important;
322
+ }
323
+
324
+ /* Content change highlight effect */
325
+ .content-changed {
326
+ animation: contentHighlight 400ms cubic-bezier(0.4, 0, 0.2, 1) !important;
327
+ transform: translateZ(0);
328
+ }
329
+
330
+ @keyframes contentHighlight {
331
+ 0%, 100% {
332
+ background-color: transparent !important;
333
+ box-shadow: none !important;
334
+ }
335
+ 30%, 70% {
336
+ background-color: rgba(59, 130, 246, 0.08) !important;
337
+ box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.1) !important;
338
+ }
339
+ }
340
+
341
+ /* Smooth morphing indicator */
342
+ .morphing-element {
343
+ position: relative;
344
+ }
345
+
346
+ .morphing-element::before {
347
+ content: '';
348
+ position: absolute;
349
+ top: -1px;
350
+ left: -1px;
351
+ right: -1px;
352
+ bottom: -1px;
353
+ background: linear-gradient(90deg,
354
+ transparent,
355
+ rgba(59, 130, 246, 0.1),
356
+ transparent
357
+ );
358
+ border-radius: 3px;
359
+ opacity: 0;
360
+ animation: morphingGlow 600ms ease-out;
361
+ pointer-events: none;
362
+ z-index: 1000;
363
+ }
364
+
365
+ @keyframes morphingGlow {
366
+ 0% { opacity: 0; transform: translateX(-100%); }
367
+ 50% { opacity: 1; transform: translateX(0); }
368
+ 100% { opacity: 0; transform: translateX(100%); }
369
+ }
370
+
371
+ /* Ultra-smooth text content updates */
372
+ .text-morphing {
373
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1) !important;
374
+ }
375
+
376
+ .text-morphing.changing {
377
+ opacity: 0.7;
378
+ transform: translateY(1px) translateZ(0);
379
+ }
380
+
381
+ /* ========================================
382
+ SMOOTH PREVIEW TRANSITIONS
383
+ ======================================== */
384
+
385
+ /* Basic iframe transition for content changes */
386
+ iframe {
387
+ transition: opacity 0.3s ease-in-out, transform 0.2s ease-out;
388
+ will-change: opacity;
389
+ }
390
+
391
+ /* Smooth loading state */
392
+ .preview-updating {
393
+ opacity: 0.8;
394
+ transition: opacity 0.2s ease-in-out;
395
+ }
396
+
397
+ /* Prevent flash during iframe content updates */
398
+ #preview-iframe-1,
399
+ #preview-iframe-2 {
400
+ background: white;
401
+ transition: opacity 0.2s ease-in-out;
402
+ }
403
+
404
+ /* Loading indicator for preview updates */
405
+ .preview-loading-indicator {
406
+ transition: opacity 0.2s ease-in-out;
407
+ }
408
+
409
+ /* Hardware acceleration for smooth transitions */
410
+ .preview-container {
411
+ transform: translateZ(0);
412
+ will-change: transform;
413
+ }
414
+
415
+ /* Prevent content jump during updates */
416
+ .preview-iframe-wrapper {
417
+ position: relative;
418
+ overflow: hidden;
419
+ }
components/editor/ask-ai/index.tsx CHANGED
@@ -176,31 +176,109 @@ export function AskAI({
176
  signal: abortController.signal,
177
  });
178
  if (request && request.body) {
179
- // Enhanced error checking and debugging
180
  if (!request.ok) {
181
  console.error('❌ Request failed:', {
182
  status: request.status,
183
  statusText: request.statusText,
 
184
  headers: Object.fromEntries(request.headers.entries())
185
  });
186
 
187
  try {
188
- const res = await request.json();
189
- console.error('❌ Error response:', res);
 
 
 
 
 
 
 
 
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  if (res.openLogin) {
192
  setOpen(true);
193
  } else if (res.openSelectProvider) {
194
  setOpenProvider(true);
195
- setProviderError(res.message);
196
  } else if (res.openProModal) {
197
  setOpenProModal(true);
198
  } else {
199
- toast.error(res.message || 'Unknown error occurred');
 
 
200
  }
201
  } catch (parseError) {
202
- console.error('❌ Failed to parse error response:', parseError);
203
- toast.error('Failed to process server response');
 
 
 
 
204
  }
205
  setisAiWorking(false);
206
  return;
 
176
  signal: abortController.signal,
177
  });
178
  if (request && request.body) {
179
+ // ROBUST ERROR HANDLING: Handle both JSON and HTML error responses
180
  if (!request.ok) {
181
  console.error('❌ Request failed:', {
182
  status: request.status,
183
  statusText: request.statusText,
184
+ url: request.url,
185
  headers: Object.fromEntries(request.headers.entries())
186
  });
187
 
188
  try {
189
+ // Get the full response as text first
190
+ const responseText = await request.text();
191
+ console.log('📄 Raw error response:', {
192
+ length: responseText.length,
193
+ contentType: request.headers.get('content-type'),
194
+ preview: responseText.substring(0, 500) + (responseText.length > 500 ? '...' : ''),
195
+ isHtml: responseText.trim().toLowerCase().startsWith('<!doctype html') || responseText.trim().toLowerCase().startsWith('<html')
196
+ });
197
+
198
+ let res;
199
 
200
+ // Check if response is HTML (common for server errors)
201
+ if (responseText.trim().toLowerCase().startsWith('<!doctype html') ||
202
+ responseText.trim().toLowerCase().startsWith('<html') ||
203
+ responseText.includes('<title>') ||
204
+ responseText.includes('Internal Server Error')) {
205
+
206
+ console.error('❌ HTML error page received instead of JSON');
207
+
208
+ // Extract error info from HTML if possible
209
+ let errorMessage = 'Server error occurred';
210
+ const titleMatch = responseText.match(/<title[^>]*>([^<]+)<\/title>/i);
211
+ if (titleMatch && titleMatch[1]) {
212
+ errorMessage = titleMatch[1].trim();
213
+ }
214
+
215
+ // Look for common error patterns
216
+ if (responseText.includes('Internal Server Error')) {
217
+ errorMessage = 'Internal Server Error - Please try again';
218
+ } else if (responseText.includes('Service Unavailable')) {
219
+ errorMessage = 'Service temporarily unavailable';
220
+ } else if (responseText.includes('Bad Gateway')) {
221
+ errorMessage = 'Gateway error - Please try again';
222
+ }
223
+
224
+ res = {
225
+ ok: false,
226
+ error: errorMessage,
227
+ message: errorMessage,
228
+ isHtmlError: true
229
+ };
230
+ } else {
231
+ // Try to parse as JSON
232
+ try {
233
+ res = JSON.parse(responseText);
234
+ console.error('❌ Error response (JSON):', res);
235
+ } catch (jsonError) {
236
+ console.error('❌ Failed to parse JSON, treating as text:', {
237
+ jsonError: (jsonError as Error).message || String(jsonError),
238
+ responseLength: responseText.length
239
+ });
240
+
241
+ // Create error object from text response
242
+ let errorMessage = responseText.trim();
243
+
244
+ // Clean up common error patterns
245
+ if (errorMessage.length > 200) {
246
+ errorMessage = errorMessage.substring(0, 200) + '...';
247
+ }
248
+
249
+ if (!errorMessage) {
250
+ errorMessage = `HTTP ${request.status}: ${request.statusText}`;
251
+ }
252
+
253
+ res = {
254
+ ok: false,
255
+ error: errorMessage,
256
+ message: errorMessage,
257
+ isTextError: true
258
+ };
259
+ }
260
+ }
261
+
262
+ // Handle different error types
263
  if (res.openLogin) {
264
  setOpen(true);
265
  } else if (res.openSelectProvider) {
266
  setOpenProvider(true);
267
+ setProviderError(res.message || res.error || 'Provider error');
268
  } else if (res.openProModal) {
269
  setOpenProModal(true);
270
  } else {
271
+ const errorMsg = res.message || res.error || 'Unknown error occurred';
272
+ console.error('❌ Showing error to user:', errorMsg);
273
+ toast.error(errorMsg);
274
  }
275
  } catch (parseError) {
276
+ console.error('❌ Complete failure to parse error response:', {
277
+ parseError: (parseError as Error).message || String(parseError),
278
+ status: request.status,
279
+ statusText: request.statusText
280
+ });
281
+ toast.error(`Request failed: ${request.status} ${request.statusText}`);
282
  }
283
  setisAiWorking(false);
284
  return;
components/editor/preview/index-simplified.tsx ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useUpdateEffect } from "react-use";
3
+ import { useMemo, useState, useRef, useEffect, forwardRef, useCallback } from "react";
4
+ import classNames from "classnames";
5
+ import { toast } from "sonner";
6
+
7
+ import { cn } from "@/lib/utils";
8
+ import { GridPattern } from "@/components/magic-ui/grid-pattern";
9
+ import { htmlTagToText } from "@/lib/html-tag-to-text";
10
+
11
+ export const Preview = forwardRef<
12
+ HTMLDivElement,
13
+ {
14
+ html: string;
15
+ isResizing: boolean;
16
+ isAiWorking: boolean;
17
+ device: "desktop" | "mobile";
18
+ currentTab: string;
19
+ iframeRef?: React.RefObject<HTMLIFrameElement | null>;
20
+ isEditableModeEnabled?: boolean;
21
+ onClickElement?: (element: HTMLElement) => void;
22
+ }
23
+ >(({
24
+ html,
25
+ isResizing,
26
+ isAiWorking,
27
+ device,
28
+ currentTab,
29
+ iframeRef,
30
+ isEditableModeEnabled,
31
+ onClickElement,
32
+ }, ref) => {
33
+ const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(null);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [displayHtml, setDisplayHtml] = useState(html);
36
+ const htmlUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
37
+ const prevHtmlRef = useRef(html);
38
+ const internalIframeRef = useRef<HTMLIFrameElement>(null);
39
+
40
+ // Use internal ref if external ref not provided
41
+ const currentIframeRef = iframeRef || internalIframeRef;
42
+
43
+ // Debug logging for initial state
44
+ useEffect(() => {
45
+ console.log('🚀 SIMPLIFIED Preview component mounted with HTML:', {
46
+ htmlLength: html.length,
47
+ htmlPreview: html.substring(0, 200) + '...',
48
+ displayHtmlLength: displayHtml.length
49
+ });
50
+ }, []);
51
+
52
+ // SIMPLIFIED: Reliable HTML update logic with optional smoothness
53
+ useEffect(() => {
54
+ console.log('🔄 SIMPLIFIED Preview update triggered:', {
55
+ htmlLength: html.length,
56
+ isAiWorking,
57
+ displayHtmlLength: displayHtml.length,
58
+ htmlChanged: html !== displayHtml,
59
+ htmlPreview: html.substring(0, 100) + '...'
60
+ });
61
+
62
+ // PRIORITY: Always ensure HTML updates work immediately
63
+ if (html !== displayHtml) {
64
+ console.log('📝 HTML changed! Updating display HTML immediately:', html.length, 'characters');
65
+
66
+ // Clear any pending timeout to prevent conflicts
67
+ if (htmlUpdateTimeoutRef.current) {
68
+ clearTimeout(htmlUpdateTimeoutRef.current);
69
+ }
70
+
71
+ // For AI working (streaming), add minimal smoothness
72
+ if (isAiWorking) {
73
+ console.log('🤖 AI working - adding minimal delay for smoothness');
74
+ setIsLoading(true);
75
+
76
+ htmlUpdateTimeoutRef.current = setTimeout(() => {
77
+ setDisplayHtml(html);
78
+ prevHtmlRef.current = html;
79
+ setIsLoading(false);
80
+ console.log('✅ Delayed update completed for AI streaming');
81
+ }, 100); // Very short delay for minimal smoothness
82
+ } else {
83
+ // Immediate update for manual changes
84
+ setDisplayHtml(html);
85
+ prevHtmlRef.current = html;
86
+ setIsLoading(false);
87
+ console.log('⚡ Immediate update completed for manual change');
88
+ }
89
+ }
90
+
91
+ console.log('✅ SIMPLIFIED Preview update completed, displayHtml length:', displayHtml.length);
92
+ }, [html, isAiWorking]);
93
+
94
+ // Add basic smooth transitions via CSS
95
+ useEffect(() => {
96
+ const iframe = currentIframeRef.current;
97
+ if (!iframe || !iframe.contentDocument) return;
98
+
99
+ const injectSmoothTransitions = () => {
100
+ const doc = iframe.contentDocument;
101
+ if (!doc) return;
102
+
103
+ let existingStyle = doc.getElementById('smooth-transitions');
104
+ if (existingStyle) return;
105
+
106
+ const style = doc.createElement('style');
107
+ style.id = 'smooth-transitions';
108
+ style.textContent = `
109
+ /* Smooth transitions for content changes */
110
+ * {
111
+ transition: opacity 0.15s ease, background-color 0.15s ease,
112
+ color 0.15s ease, border-color 0.15s ease !important;
113
+ }
114
+
115
+ /* Prevent flashing during updates */
116
+ body {
117
+ transition: opacity 0.1s ease !important;
118
+ }
119
+
120
+ /* Subtle animations for new content */
121
+ @keyframes contentFadeIn {
122
+ from { opacity: 0.8; }
123
+ to { opacity: 1; }
124
+ }
125
+
126
+ body.content-updating {
127
+ animation: contentFadeIn 0.2s ease-in-out;
128
+ }
129
+ `;
130
+ doc.head.appendChild(style);
131
+ console.log('✨ Smooth transitions injected into iframe');
132
+ };
133
+
134
+ // Inject on iframe load
135
+ iframe.addEventListener('load', injectSmoothTransitions);
136
+
137
+ // Also try to inject immediately if iframe is already loaded
138
+ if (iframe.contentDocument?.readyState === 'complete') {
139
+ injectSmoothTransitions();
140
+ }
141
+
142
+ return () => {
143
+ iframe.removeEventListener('load', injectSmoothTransitions);
144
+ };
145
+ }, [currentIframeRef]);
146
+
147
+ // Add content updating class for animations
148
+ useEffect(() => {
149
+ const iframe = currentIframeRef.current;
150
+ if (!iframe || !iframe.contentDocument) return;
151
+
152
+ const body = iframe.contentDocument.body;
153
+ if (!body) return;
154
+
155
+ body.classList.add('content-updating');
156
+
157
+ const timeout = setTimeout(() => {
158
+ body.classList.remove('content-updating');
159
+ }, 200);
160
+
161
+ return () => {
162
+ clearTimeout(timeout);
163
+ body.classList.remove('content-updating');
164
+ };
165
+ }, [displayHtml, currentIframeRef]);
166
+
167
+ // Cleanup timeout on unmount
168
+ useEffect(() => {
169
+ return () => {
170
+ if (htmlUpdateTimeoutRef.current) {
171
+ clearTimeout(htmlUpdateTimeoutRef.current);
172
+ }
173
+ };
174
+ }, []);
175
+
176
+ // Event handlers for editable mode
177
+ const handleMouseOver = (event: MouseEvent) => {
178
+ if (currentIframeRef?.current) {
179
+ const iframeDocument = currentIframeRef.current.contentDocument;
180
+ if (iframeDocument) {
181
+ const targetElement = event.target as HTMLElement;
182
+ if (
183
+ hoveredElement !== targetElement &&
184
+ targetElement !== iframeDocument.body
185
+ ) {
186
+ console.log("🎯 Edit mode: Element hovered", {
187
+ tagName: targetElement.tagName,
188
+ id: targetElement.id || 'no-id',
189
+ className: targetElement.className || 'no-class'
190
+ });
191
+
192
+ // Remove previous hover class
193
+ if (hoveredElement) {
194
+ hoveredElement.classList.remove("hovered-element");
195
+ }
196
+
197
+ setHoveredElement(targetElement);
198
+ targetElement.classList.add("hovered-element");
199
+ } else {
200
+ return setHoveredElement(null);
201
+ }
202
+ }
203
+ }
204
+ };
205
+
206
+ const handleMouseOut = () => {
207
+ setHoveredElement(null);
208
+ };
209
+
210
+ const handleClick = (event: MouseEvent) => {
211
+ console.log("🖱️ Edit mode: Click detected in iframe", {
212
+ target: event.target,
213
+ tagName: (event.target as HTMLElement)?.tagName,
214
+ isBody: event.target === currentIframeRef?.current?.contentDocument?.body,
215
+ hasOnClickElement: !!onClickElement
216
+ });
217
+
218
+ if (currentIframeRef?.current) {
219
+ const iframeDocument = currentIframeRef.current.contentDocument;
220
+ if (iframeDocument) {
221
+ const targetElement = event.target as HTMLElement;
222
+ if (targetElement !== iframeDocument.body) {
223
+ console.log("✅ Edit mode: Valid element clicked, calling onClickElement", {
224
+ tagName: targetElement.tagName,
225
+ id: targetElement.id || 'no-id',
226
+ className: targetElement.className || 'no-class',
227
+ textContent: targetElement.textContent?.substring(0, 50) + '...'
228
+ });
229
+
230
+ // Prevent default behavior to avoid navigation
231
+ event.preventDefault();
232
+ event.stopPropagation();
233
+
234
+ onClickElement?.(targetElement);
235
+ } else {
236
+ console.log("⚠️ Edit mode: Body clicked, ignoring");
237
+ }
238
+ } else {
239
+ console.error("❌ Edit mode: No iframe document available on click");
240
+ }
241
+ } else {
242
+ console.error("❌ Edit mode: No iframe ref available on click");
243
+ }
244
+ };
245
+
246
+ // Setup event listeners for editable mode
247
+ useUpdateEffect(() => {
248
+ const cleanupListeners = () => {
249
+ if (currentIframeRef?.current?.contentDocument) {
250
+ const iframeDocument = currentIframeRef.current.contentDocument;
251
+ iframeDocument.removeEventListener("mouseover", handleMouseOver);
252
+ iframeDocument.removeEventListener("mouseout", handleMouseOut);
253
+ iframeDocument.removeEventListener("click", handleClick);
254
+ console.log("🧹 Edit mode: Cleaned up iframe event listeners");
255
+ }
256
+ };
257
+
258
+ const setupListeners = () => {
259
+ try {
260
+ if (!currentIframeRef?.current) {
261
+ console.log("⚠️ Edit mode: No iframe ref available");
262
+ return;
263
+ }
264
+
265
+ const iframeDocument = currentIframeRef.current.contentDocument;
266
+ if (!iframeDocument) {
267
+ console.log("⚠️ Edit mode: No iframe content document available");
268
+ return;
269
+ }
270
+
271
+ // Clean up existing listeners first
272
+ cleanupListeners();
273
+
274
+ if (isEditableModeEnabled) {
275
+ console.log("🎯 Edit mode: Setting up iframe event listeners");
276
+ iframeDocument.addEventListener("mouseover", handleMouseOver);
277
+ iframeDocument.addEventListener("mouseout", handleMouseOut);
278
+ iframeDocument.addEventListener("click", handleClick);
279
+ console.log("✅ Edit mode: Event listeners added successfully");
280
+ } else {
281
+ console.log("🔇 Edit mode: Disabled, no listeners added");
282
+ }
283
+ } catch (error) {
284
+ console.error("❌ Edit mode: Error setting up listeners:", error);
285
+ }
286
+ };
287
+
288
+ // Add a small delay to ensure iframe is fully loaded
289
+ const timeoutId = setTimeout(setupListeners, 100);
290
+
291
+ // Clean up when component unmounts or dependencies change
292
+ return () => {
293
+ clearTimeout(timeoutId);
294
+ cleanupListeners();
295
+ };
296
+ }, [currentIframeRef, isEditableModeEnabled]);
297
+
298
+ const selectedElement = useMemo(() => {
299
+ if (!isEditableModeEnabled) return null;
300
+ if (!hoveredElement) return null;
301
+ return hoveredElement;
302
+ }, [hoveredElement, isEditableModeEnabled]);
303
+
304
+ return (
305
+ <div
306
+ ref={ref}
307
+ className={classNames(
308
+ "bg-white overflow-hidden relative flex-1 h-full",
309
+ {
310
+ "cursor-wait": isLoading && isAiWorking,
311
+ }
312
+ )}
313
+ onClick={(e) => {
314
+ e.stopPropagation();
315
+ }}
316
+ >
317
+ <GridPattern
318
+ width={20}
319
+ height={20}
320
+ x={-1}
321
+ y={-1}
322
+ strokeDasharray={"4 2"}
323
+ className={cn(
324
+ "[mask-image:radial-gradient(300px_circle_at_center,white,transparent)] z-0 absolute inset-0 h-full w-full fill-neutral-100 stroke-neutral-100"
325
+ )}
326
+ />
327
+
328
+ {/* Simplified loading overlay */}
329
+ {isLoading && isAiWorking && (
330
+ <div className="absolute inset-0 bg-black/5 backdrop-blur-[0.5px] transition-all duration-300 z-20 flex items-center justify-center">
331
+ <div className="bg-neutral-800/95 rounded-lg px-4 py-2 text-sm text-neutral-300 border border-neutral-700 shadow-lg">
332
+ <div className="flex items-center gap-2">
333
+ <div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
334
+ Updating preview...
335
+ </div>
336
+ </div>
337
+ </div>
338
+ )}
339
+
340
+ {/* Selected element indicator */}
341
+ {!isAiWorking && hoveredElement && selectedElement && (
342
+ <div className="absolute bottom-4 left-4 z-30">
343
+ <div className="bg-neutral-800/90 rounded-lg px-3 py-2 text-sm text-neutral-300 border border-neutral-700 shadow-lg">
344
+ <span className="font-medium">
345
+ {htmlTagToText(selectedElement.tagName.toLowerCase())}
346
+ </span>
347
+ {selectedElement.id && (
348
+ <span className="ml-2 text-neutral-400">#{selectedElement.id}</span>
349
+ )}
350
+ </div>
351
+ </div>
352
+ )}
353
+
354
+ {/* Simplified single iframe with CSS transitions */}
355
+ <iframe
356
+ id="preview-iframe"
357
+ ref={currentIframeRef}
358
+ title="output"
359
+ className={classNames(
360
+ "w-full select-none h-full transition-all duration-200 ease-out",
361
+ {
362
+ "pointer-events-none": isResizing || isAiWorking,
363
+ "opacity-95 scale-[0.999]": isLoading && isAiWorking,
364
+ "opacity-100 scale-100": !isLoading || !isAiWorking,
365
+ "bg-black": true,
366
+ "lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
367
+ device === "mobile",
368
+ "lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
369
+ device === "desktop",
370
+ }
371
+ )}
372
+ srcDoc={displayHtml}
373
+ onLoad={() => {
374
+ console.log('🎯 SIMPLIFIED Preview iframe loaded with HTML length:', displayHtml.length);
375
+ setIsLoading(false);
376
+ }}
377
+ />
378
+ </div>
379
+ );
380
+ });
381
+
382
+ Preview.displayName = "Preview";
components/editor/preview/index.tsx CHANGED
@@ -1,579 +1,511 @@
1
  "use client";
2
- import { useUpdateEffect } from "react-use";
3
- import { useMemo, useState, useRef, useEffect, forwardRef, useCallback } from "react";
4
- import classNames from "classnames";
5
- import { toast } from "sonner";
6
-
7
- import { cn } from "@/lib/utils";
8
- import { GridPattern } from "@/components/magic-ui/grid-pattern";
9
- import { htmlTagToText } from "@/lib/html-tag-to-text";
10
-
11
- export const Preview = forwardRef<
12
- HTMLDivElement,
13
- {
14
- html: string;
15
- isResizing: boolean;
16
- isAiWorking: boolean;
17
- device: "desktop" | "mobile";
18
- currentTab: string;
19
- iframeRef?: React.RefObject<HTMLIFrameElement | null>;
20
- isEditableModeEnabled?: boolean;
21
- onClickElement?: (element: HTMLElement) => void;
22
- }
23
- >(({
24
- html,
25
- isResizing,
26
- isAiWorking,
27
- device,
28
- currentTab,
29
- iframeRef,
30
- isEditableModeEnabled,
31
- onClickElement,
32
- }, ref) => {
33
- const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
34
- null
35
- );
36
- const [isLoading, setIsLoading] = useState(false);
37
- const [displayHtml, setDisplayHtml] = useState(html);
38
- const htmlUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
39
- const prevHtmlRef = useRef(html);
40
- const updateCountRef = useRef(0);
41
-
42
- // Debug logging for initial state
43
- useEffect(() => {
44
- console.log('🚀 Preview component mounted with HTML:', {
45
- htmlLength: html.length,
46
- htmlPreview: html.substring(0, 200) + '...',
47
- displayHtmlLength: displayHtml.length
48
- });
49
- }, []);
50
-
51
- // CRITICAL: Main HTML update logic - handles all HTML changes
52
- useEffect(() => {
53
- console.log('🔄 Preview update triggered:', {
54
- htmlLength: html.length,
55
- isAiWorking,
56
- displayHtmlLength: displayHtml.length,
57
- htmlChanged: html !== displayHtml,
58
- htmlPreview: html.substring(0, 100) + '...'
59
- });
60
-
61
- // Always update displayHtml when html prop changes
62
- if (html !== displayHtml) {
63
- console.log('📝 HTML changed! Updating displayHtml from', displayHtml.length, 'to', html.length, 'characters');
64
- setDisplayHtml(html);
65
- // Also update secondaryHtml to keep both iframes in sync
66
- setSecondaryHtml(html);
67
- prevHtmlRef.current = html;
68
- }
69
-
70
- // Clear any pending timeouts
71
- if (htmlUpdateTimeoutRef.current) {
72
- clearTimeout(htmlUpdateTimeoutRef.current);
73
- }
74
-
75
- // Enhanced updates during AI streaming for smoothness
76
- if (isAiWorking && html !== prevHtmlRef.current) {
77
- console.log('� AI working - scheduling enhanced smooth update');
78
- setIsLoading(true);
79
-
80
- htmlUpdateTimeoutRef.current = setTimeout(async () => {
81
- console.log('🎬 Executing enhanced streaming update');
82
-
83
- // Try seamless injection for smoother updates during streaming
84
- const injectionSuccess = injectContentSeamlessly(html);
85
-
86
- if (!injectionSuccess) {
87
- console.log('💫 Seamless injection not available, using direct update');
88
- // displayHtml is already updated above, so iframe will re-render
89
- }
90
-
91
- prevHtmlRef.current = html;
92
- setIsLoading(false);
93
- }, 200); // Shorter delay for more responsive updates
94
- } else {
95
- // Immediate update when AI is not working
96
- setIsLoading(false);
97
- }
98
-
99
- console.log('✅ Preview update completed, displayHtml length:', displayHtml.length);
100
- }, [html, isAiWorking]); // Simplified dependencies
101
-
102
- // DUAL IFRAME SYSTEM for ultra-smooth transitions when injection fails
103
- const [activeIframeIndex, setActiveIframeIndex] = useState(0);
104
- const [secondaryHtml, setSecondaryHtml] = useState(html); // Initialize with current HTML
105
- const iframe1Ref = useRef<HTMLIFrameElement>(null);
106
- const iframe2Ref = useRef<HTMLIFrameElement>(null);
107
- const [isSwapping, setIsSwapping] = useState(false);
108
-
109
- // Get the currently active iframe reference
110
- const getCurrentIframe = useCallback(() => {
111
- return activeIframeIndex === 0 ? iframe1Ref.current : iframe2Ref.current;
112
- }, [activeIframeIndex]);
113
-
114
- // Get the secondary (hidden) iframe reference
115
- const getSecondaryIframe = useCallback(() => {
116
- return activeIframeIndex === 0 ? iframe2Ref.current : iframe1Ref.current;
117
- }, [activeIframeIndex]);
118
-
119
- // Forward the active iframe ref to maintain backward compatibility
120
- useEffect(() => {
121
- if (iframeRef) {
122
- iframeRef.current = getCurrentIframe();
123
- }
124
- }, [activeIframeIndex, iframeRef, getCurrentIframe]);
125
-
126
- // Content buffering for ultra-smooth transitions
127
- const [contentBuffer, setContentBuffer] = useState<string[]>([]);
128
- const bufferTimeoutRef = useRef<NodeJS.Timeout | null>(null);
129
-
130
- // Content similarity checking to prevent flash on tiny changes
131
- const getContentSimilarity = (html1: string, html2: string) => {
132
- // Remove whitespace and normalize for comparison
133
- const normalize = (str: string) => str.replace(/\s+/g, ' ').trim();
134
- const normalized1 = normalize(html1);
135
- const normalized2 = normalize(html2);
136
-
137
- // Calculate similarity percentage
138
- const maxLength = Math.max(normalized1.length, normalized2.length);
139
- if (maxLength === 0) return 1;
140
-
141
- const distance = Math.abs(normalized1.length - normalized2.length);
142
- return 1 - (distance / maxLength);
143
- };
144
-
145
- // Buffer management for even smoother updates
146
- const addToBuffer = useCallback((newHtml: string) => {
147
- setContentBuffer(prev => {
148
- const updated = [...prev, newHtml];
149
- // Keep only last 3 versions for smooth transitions
150
- return updated.slice(-3);
151
- });
152
-
153
- // Auto-flush buffer after delay
154
- if (bufferTimeoutRef.current) {
155
- clearTimeout(bufferTimeoutRef.current);
156
- }
157
-
158
- bufferTimeoutRef.current = setTimeout(() => {
159
- const latestContent = contentBuffer[contentBuffer.length - 1];
160
- if (latestContent && latestContent !== displayHtml) {
161
- console.log('📦 Buffer: Auto-flushing to latest content');
162
- setDisplayHtml(latestContent);
163
- setContentBuffer([]);
164
- }
165
- }, 3000);
166
- }, [contentBuffer, displayHtml]);
167
-
168
- // Ultra-smooth dual iframe swapping for zero-flash transitions
169
- const swapIframes = useCallback((newHtml: string) => {
170
- const secondary = getSecondaryIframe();
171
- if (!secondary) return false;
172
-
173
- return new Promise<boolean>((resolve) => {
174
- console.log('🔄 Starting dual iframe swap for zero-flash transition');
175
- setIsSwapping(true);
176
-
177
- // Pre-load content in secondary iframe
178
- setSecondaryHtml(newHtml);
179
-
180
- // Wait for secondary iframe to load
181
- const handleSecondaryLoad = () => {
182
- console.log('✨ Secondary iframe loaded, executing seamless swap');
183
-
184
- // Smooth transition: fade out current, fade in secondary
185
- const current = getCurrentIframe();
186
- if (current) {
187
- current.style.opacity = '0';
188
- current.style.transform = 'scale(0.98)';
189
- }
190
-
191
- setTimeout(() => {
192
- // Swap active iframe index
193
- setActiveIframeIndex(prev => prev === 0 ? 1 : 0);
194
- setDisplayHtml(newHtml);
195
-
196
- // Fade in the new active iframe
197
- const newActive = secondary;
198
- newActive.style.opacity = '1';
199
- newActive.style.transform = 'scale(1)';
200
-
201
- setTimeout(() => {
202
- setIsSwapping(false);
203
- resolve(true);
204
- console.log('🎯 Dual iframe swap completed - zero flash achieved!');
205
- }, 200);
206
- }, 150);
207
- };
208
-
209
- secondary.addEventListener('load', handleSecondaryLoad, { once: true });
210
-
211
- // Fallback timeout
212
- setTimeout(() => {
213
- if (isSwapping) {
214
- setIsSwapping(false);
215
- resolve(false);
216
- console.log('⏰ Dual iframe swap timeout');
217
- }
218
- }, 3000);
219
- });
220
- }, [getCurrentIframe, getSecondaryIframe, isSwapping]);
221
-
222
- // REVOLUTIONARY: Zero-flash content injection system (no srcDoc updates!)
223
- const injectContentSeamlessly = useCallback((newHtml: string, forceUpdate = false) => {
224
- const iframe = getCurrentIframe();
225
- if (!iframe || !iframe.contentWindow || !iframe.contentDocument) {
226
- console.log('🚫 Iframe not ready for seamless injection');
227
- return false;
228
- }
229
-
230
- try {
231
- const doc = iframe.contentDocument;
232
- const currentHtml = doc.documentElement.outerHTML;
233
-
234
- // Skip if content is identical
235
- if (currentHtml === newHtml && !forceUpdate) {
236
- return false;
237
- }
238
-
239
- console.log('💉 Seamless content injection starting...', {
240
- htmlLength: newHtml.length,
241
- currentLength: currentHtml.length,
242
- forceUpdate
243
- });
244
-
245
- // SEAMLESS METHOD 1: Direct DOM manipulation (zero flash)
246
- // Create a temporary container to parse the new HTML
247
- const tempDiv = doc.createElement('div');
248
- tempDiv.innerHTML = newHtml;
249
-
250
- // Get the new body content
251
- const newBodyElement = tempDiv.querySelector('body');
252
- const newHeadElement = tempDiv.querySelector('head');
253
-
254
- if (newBodyElement) {
255
- // Smoothly replace body content without flash
256
- const currentBody = doc.body;
257
- if (currentBody) {
258
- // Copy styles and classes to maintain visual continuity
259
- const bodyStyles = currentBody.style.cssText;
260
- const bodyClass = currentBody.className;
261
-
262
- // Replace content seamlessly
263
- currentBody.innerHTML = newBodyElement.innerHTML;
264
- currentBody.style.cssText = bodyStyles;
265
- currentBody.className = bodyClass;
266
-
267
- console.log('✨ Body content injected seamlessly');
268
- }
269
- }
270
-
271
- if (newHeadElement) {
272
- // Update head content if needed (styles, meta tags)
273
- const currentHead = doc.head;
274
- if (currentHead) {
275
- // Only update if head content is significantly different
276
- const headDiff = Math.abs(newHeadElement.innerHTML.length - currentHead.innerHTML.length);
277
- if (headDiff > 100) {
278
- currentHead.innerHTML = newHeadElement.innerHTML;
279
- console.log('🧠 Head content updated seamlessly');
280
- }
281
- }
282
- }
283
-
284
- return true;
285
- } catch (error) {
286
- console.warn('⚠️ Seamless injection failed, falling back:', error);
287
- return false;
288
- }
289
- }, [getCurrentIframe]);
290
-
291
- // Cleanup timeout on unmount
292
- useEffect(() => {
293
- return () => {
294
- if (htmlUpdateTimeoutRef.current) {
295
- clearTimeout(htmlUpdateTimeoutRef.current);
296
- }
297
- };
298
- }, []);
299
-
300
- // add event listener to the iframe to track hovered elements (dual iframe compatible)
301
- const handleMouseOver = (event: MouseEvent) => {
302
- const activeIframe = getCurrentIframe();
303
- if (activeIframe) {
304
- const iframeDocument = activeIframe.contentDocument;
305
- if (iframeDocument) {
306
- const targetElement = event.target as HTMLElement;
307
- if (
308
- hoveredElement !== targetElement &&
309
- targetElement !== iframeDocument.body
310
- ) {
311
- console.log("🎯 Edit mode: Element hovered", {
312
- tagName: targetElement.tagName,
313
- id: targetElement.id || 'no-id',
314
- className: targetElement.className || 'no-class'
315
- });
316
-
317
- // Remove previous hover class
318
- if (hoveredElement) {
319
- hoveredElement.classList.remove("hovered-element");
320
- }
321
-
322
- setHoveredElement(targetElement);
323
- targetElement.classList.add("hovered-element");
324
- } else {
325
- return setHoveredElement(null);
326
- }
327
- }
328
- }
329
- };
330
-
331
- const handleMouseOut = () => {
332
- setHoveredElement(null);
333
- };
334
-
335
- const handleClick = (event: MouseEvent) => {
336
- const activeIframe = getCurrentIframe();
337
- console.log("🖱️ Edit mode: Click detected in iframe", {
338
- target: event.target,
339
- tagName: (event.target as HTMLElement)?.tagName,
340
- isBody: event.target === activeIframe?.contentDocument?.body,
341
- hasOnClickElement: !!onClickElement
342
- });
343
-
344
- if (activeIframe) {
345
- const iframeDocument = activeIframe.contentDocument;
346
- if (iframeDocument) {
347
- const targetElement = event.target as HTMLElement;
348
- if (targetElement !== iframeDocument.body) {
349
- console.log("✅ Edit mode: Valid element clicked, calling onClickElement", {
350
- tagName: targetElement.tagName,
351
- id: targetElement.id || 'no-id',
352
- className: targetElement.className || 'no-class',
353
- textContent: targetElement.textContent?.substring(0, 50) + '...'
354
- });
355
-
356
- // Prevent default behavior to avoid navigation
357
- event.preventDefault();
358
- event.stopPropagation();
359
-
360
- onClickElement?.(targetElement);
361
- } else {
362
- console.log("⚠️ Edit mode: Body clicked, ignoring");
363
- }
364
- } else {
365
- console.error("❌ Edit mode: No iframe document available on click");
366
- }
367
- } else {
368
- console.error("❌ Edit mode: No iframe ref available on click");
369
- }
370
- };
371
-
372
- useUpdateEffect(() => {
373
- const cleanupListeners = () => {
374
- if (iframeRef?.current?.contentDocument) {
375
- const iframeDocument = iframeRef.current.contentDocument;
376
- iframeDocument.removeEventListener("mouseover", handleMouseOver);
377
- iframeDocument.removeEventListener("mouseout", handleMouseOut);
378
- iframeDocument.removeEventListener("click", handleClick);
379
- console.log("🧹 Edit mode: Cleaned up iframe event listeners");
380
- }
381
- };
382
-
383
- const setupListeners = () => {
384
- try {
385
- if (!iframeRef?.current) {
386
- console.log("⚠️ Edit mode: No iframe ref available");
387
- return;
388
- }
389
-
390
- const iframeDocument = iframeRef.current.contentDocument;
391
- if (!iframeDocument) {
392
- console.log("⚠️ Edit mode: No iframe content document available");
393
- return;
394
- }
395
-
396
- // Clean up existing listeners first
397
- cleanupListeners();
398
-
399
- if (isEditableModeEnabled) {
400
- console.log("🎯 Edit mode: Setting up iframe event listeners");
401
- iframeDocument.addEventListener("mouseover", handleMouseOver);
402
- iframeDocument.addEventListener("mouseout", handleMouseOut);
403
- iframeDocument.addEventListener("click", handleClick);
404
- console.log(" Edit mode: Event listeners added successfully");
405
- } else {
406
- console.log("🔇 Edit mode: Disabled, no listeners added");
407
- }
408
- } catch (error) {
409
- console.error("❌ Edit mode: Error setting up listeners:", error);
410
- }
411
- };
412
-
413
- // Add a small delay to ensure iframe is fully loaded
414
- const timeoutId = setTimeout(setupListeners, 100);
415
-
416
- // Clean up when component unmounts or dependencies change
417
- return () => {
418
- clearTimeout(timeoutId);
419
- cleanupListeners();
420
- };
421
- }, [iframeRef, isEditableModeEnabled]);
422
-
423
- const selectedElement = useMemo(() => {
424
- if (!isEditableModeEnabled) return null;
425
- if (!hoveredElement) return null;
426
- return hoveredElement;
427
- }, [hoveredElement, isEditableModeEnabled]);
428
-
429
- return (
430
- <div
431
- ref={ref}
432
- className={classNames(
433
- "w-full border-l border-gray-900 h-full relative z-0 flex items-center justify-center",
434
- {
435
- "lg:border-l-0": currentTab === "preview",
436
- }
437
- )}
438
- >
439
- <GridPattern
440
- width={20}
441
- height={20}
442
- x={-1}
443
- y={-1}
444
- className={cn(
445
- "[mask-image:linear-gradient(0deg,white,rgba(255,255,255,0.6))] absolute inset-0 h-full w-full"
446
- )}
447
- />
448
- {/* Subtle loading indicator - only during major transitions */}
449
- {isLoading && isAiWorking && (
450
- <div className="absolute top-3 left-3 z-30">
451
- <div className="bg-neutral-900/90 backdrop-blur-sm rounded-full px-3 py-1 text-xs text-neutral-300 border border-neutral-600/30 shadow-sm">
452
- <div className="flex items-center gap-2">
453
- <div className="w-1.5 h-1.5 bg-blue-400 rounded-full animate-pulse"></div>
454
- <span className="font-medium">Updating...</span>
455
- </div>
456
- </div>
457
- </div>
458
- )}
459
- {/* Gentle progress indicator during AI work */}
460
- {isAiWorking && !isLoading && (
461
- <div className="absolute top-3 right-3 z-30">
462
- <div className="bg-neutral-900/95 backdrop-blur-sm rounded-full px-4 py-2 text-xs text-neutral-300 border border-neutral-600/50 shadow-lg">
463
- <div className="flex items-center gap-3">
464
- <div className="relative">
465
- <div className="w-2 h-2 bg-gradient-to-r from-green-400 to-emerald-400 rounded-full animate-pulse"></div>
466
- <div className="absolute inset-0 w-2 h-2 bg-gradient-to-r from-green-400 to-emerald-400 rounded-full animate-ping opacity-30"></div>
467
- </div>
468
- <span className="font-medium">AI generating...</span>
469
- </div>
470
- </div>
471
- </div>
472
- )}
473
- {!isEditableModeEnabled &&
474
- !isAiWorking &&
475
- hoveredElement &&
476
- selectedElement && (
477
- <div
478
- className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"
479
- style={{
480
- top: selectedElement.getBoundingClientRect().top + 24,
481
- left: selectedElement.getBoundingClientRect().left + 24,
482
- width: selectedElement.getBoundingClientRect().width,
483
- height: selectedElement.getBoundingClientRect().height,
484
- }}
485
- >
486
- <span className="bg-sky-500 rounded-t-md text-sm text-neutral-100 px-2 py-0.5 -translate-y-7 absolute top-0 left-0">
487
- {htmlTagToText(selectedElement.tagName.toLowerCase())}
488
- </span>
489
- </div>
490
- )}
491
- {/* DUAL IFRAME SYSTEM for zero-flash transitions */}
492
- {/* Primary iframe */}
493
- <iframe
494
- id="preview-iframe-1"
495
- ref={iframe1Ref}
496
- title="output-primary"
497
- className={classNames(
498
- "absolute inset-0 w-full select-none h-full transition-all duration-300 ease-out",
499
- {
500
- "pointer-events-none": isResizing || isAiWorking,
501
- "opacity-100": activeIframeIndex === 0,
502
- "opacity-0": activeIframeIndex !== 0,
503
- "bg-white": true,
504
- "lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
505
- device === "mobile",
506
- "lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
507
- currentTab !== "preview" && device === "desktop",
508
- }
509
- )}
510
- srcDoc={displayHtml}
511
- onLoad={() => {
512
- const activeIframe = iframe1Ref.current;
513
- console.log("🎬 Primary iframe loaded:", {
514
- isActive: activeIframeIndex === 0,
515
- hasContentWindow: !!activeIframe?.contentWindow,
516
- hasContentDocument: !!activeIframe?.contentDocument,
517
- htmlLength: displayHtml.length,
518
- srcDocPreview: displayHtml.substring(0, 100) + '...'
519
- });
520
-
521
- setIsLoading(false);
522
-
523
- if (activeIframe?.contentWindow?.document?.body) {
524
- activeIframe.contentWindow.document.body.scrollIntoView({
525
- block: isAiWorking ? "end" : "start",
526
- inline: "nearest",
527
- behavior: "smooth",
528
- });
529
- }
530
- }}
531
- key={`primary-${displayHtml.length}`} // Force re-render when content changes
532
- />
533
-
534
- {/* Secondary iframe for seamless swapping */}
535
- <iframe
536
- id="preview-iframe-2"
537
- ref={iframe2Ref}
538
- title="output-secondary"
539
- className={classNames(
540
- "absolute inset-0 w-full select-none h-full transition-all duration-300 ease-out",
541
- {
542
- "pointer-events-none": isResizing || isAiWorking,
543
- "opacity-100": activeIframeIndex === 1,
544
- "opacity-0": activeIframeIndex !== 1,
545
- "bg-white": true,
546
- "lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
547
- device === "mobile",
548
- "lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
549
- currentTab !== "preview" && device === "desktop",
550
- }
551
- )}
552
- srcDoc={secondaryHtml || displayHtml}
553
- onLoad={() => {
554
- const activeIframe = iframe2Ref.current;
555
- console.log("🎬 Secondary iframe loaded:", {
556
- isActive: activeIframeIndex === 1,
557
- hasContentWindow: !!activeIframe?.contentWindow,
558
- hasContentDocument: !!activeIframe?.contentDocument,
559
- htmlLength: secondaryHtml.length || displayHtml.length,
560
- srcDocPreview: (secondaryHtml || displayHtml).substring(0, 100) + '...'
561
- });
562
-
563
- setIsLoading(false);
564
-
565
- if (activeIframe?.contentWindow?.document?.body) {
566
- activeIframe.contentWindow.document.body.scrollIntoView({
567
- block: isAiWorking ? "end" : "start",
568
- inline: "nearest",
569
- behavior: "smooth",
570
- });
571
- }
572
- }}
573
- key={`secondary-${(secondaryHtml || displayHtml).length}`} // Force re-render when content changes
574
- />
575
- </div>
576
- );
577
- });
578
-
579
- Preview.displayName = "Preview";
 
1
  "use client";
2
+ import { useUpdateEffect } from "react-use";
3
+ import { useMemo, useState, useRef, useEffect, forwardRef, useCallback } from "react";
4
+ import classNames from "classnames";
5
+ import { toast } from "sonner";
6
+
7
+ import { cn } from "@/lib/utils";
8
+ import { GridPattern } from "@/components/magic-ui/grid-pattern";
9
+ import { htmlTagToText } from "@/lib/html-tag-to-text";
10
+
11
+ export const Preview = forwardRef<
12
+ HTMLDivElement,
13
+ {
14
+ html: string;
15
+ isResizing: boolean;
16
+ isAiWorking: boolean;
17
+ device: "desktop" | "mobile";
18
+ currentTab: string;
19
+ iframeRef?: React.RefObject<HTMLIFrameElement | null>;
20
+ isEditableModeEnabled?: boolean;
21
+ onClickElement?: (element: HTMLElement) => void;
22
+ }
23
+ >(({
24
+ html,
25
+ isResizing,
26
+ isAiWorking,
27
+ device,
28
+ currentTab,
29
+ iframeRef,
30
+ isEditableModeEnabled,
31
+ onClickElement,
32
+ }, ref) => {
33
+ const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(null);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [displayHtml, setDisplayHtml] = useState(html);
36
+ const htmlUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
37
+ const prevHtmlRef = useRef(html);
38
+ const internalIframeRef = useRef<HTMLIFrameElement>(null);
39
+
40
+ // Add iframe key for force refresh when needed
41
+ const [iframeKey, setIframeKey] = useState(0);
42
+
43
+ // Use internal ref if external ref not provided
44
+ const currentIframeRef = iframeRef || internalIframeRef;
45
+
46
+ // Force refresh iframe if it seems stuck
47
+ const forceRefresh = useCallback(() => {
48
+ console.log('🔄 Force refreshing iframe');
49
+ setIframeKey(prev => prev + 1);
50
+ }, []);
51
+
52
+ // Monitor for stuck updates and force refresh if needed
53
+ useEffect(() => {
54
+ if (html !== displayHtml && !isAiWorking) {
55
+ const timeout = setTimeout(() => {
56
+ if (html !== displayHtml) {
57
+ console.log('⚠️ Preview seems stuck, force refreshing');
58
+ setDisplayHtml(html);
59
+ forceRefresh();
60
+ }
61
+ }, 2000);
62
+
63
+ return () => clearTimeout(timeout);
64
+ }
65
+ }, [html, displayHtml, isAiWorking, forceRefresh]);
66
+
67
+ // Debug logging for initial state and prop changes
68
+ useEffect(() => {
69
+ console.log('🚀 Preview component mounted/updated with HTML:', {
70
+ htmlLength: html.length,
71
+ displayHtmlLength: displayHtml.length,
72
+ htmlPreview: html.substring(0, 200) + '...',
73
+ isAiWorking,
74
+ device,
75
+ currentTab
76
+ });
77
+ }, [html, displayHtml, isAiWorking, device, currentTab]);
78
+
79
+ // CRITICAL: Reliable HTML update logic with debugging
80
+ useEffect(() => {
81
+ console.log('🔄 Preview update triggered:', {
82
+ htmlLength: html.length,
83
+ isAiWorking,
84
+ displayHtmlLength: displayHtml.length,
85
+ htmlChanged: html !== displayHtml,
86
+ prevHtmlLength: prevHtmlRef.current.length,
87
+ htmlPreview: html.substring(0, 100) + '...',
88
+ displayPreview: displayHtml.substring(0, 100) + '...'
89
+ });
90
+
91
+ // ALWAYS update when HTML prop changes - this is critical
92
+ if (html !== prevHtmlRef.current) {
93
+ console.log('📝 HTML prop changed! Forcing update:', {
94
+ from: prevHtmlRef.current.length,
95
+ to: html.length,
96
+ isAiWorking,
97
+ willUseDelay: isAiWorking
98
+ });
99
+
100
+ // Clear any pending timeout to prevent conflicts
101
+ if (htmlUpdateTimeoutRef.current) {
102
+ clearTimeout(htmlUpdateTimeoutRef.current);
103
+ console.log('⏰ Cleared pending timeout');
104
+ }
105
+
106
+ // Update ref immediately
107
+ prevHtmlRef.current = html;
108
+
109
+ // For AI working (streaming), add minimal smoothness
110
+ if (isAiWorking && html.length > 0) {
111
+ console.log('🤖 AI working - adding minimal delay for smoothness');
112
+ setIsLoading(true);
113
+
114
+ htmlUpdateTimeoutRef.current = setTimeout(() => {
115
+ console.log('⚡ Applying delayed HTML update:', html.length);
116
+ setDisplayHtml(html);
117
+ setIsLoading(false);
118
+ }, 100); // Very short delay for minimal smoothness
119
+ } else {
120
+ // Immediate update for manual changes or when AI stops
121
+ console.log('⚡ Applying immediate HTML update:', html.length);
122
+ setDisplayHtml(html);
123
+ setIsLoading(false);
124
+ }
125
+ } else if (html !== displayHtml) {
126
+ // Edge case: displayHtml is out of sync
127
+ console.log('🔧 Fixing displayHtml sync issue');
128
+ setDisplayHtml(html);
129
+ }
130
+
131
+ console.log('✅ Preview update completed');
132
+ }, [html, isAiWorking]);
133
+
134
+ // Enhanced smooth transitions via CSS injection
135
+ useEffect(() => {
136
+ const iframe = currentIframeRef.current;
137
+ if (!iframe) return;
138
+
139
+ const injectSmoothTransitions = () => {
140
+ const doc = iframe.contentDocument;
141
+ if (!doc) return;
142
+
143
+ let existingStyle = doc.getElementById('smooth-transitions');
144
+ if (existingStyle) {
145
+ console.log('🎨 Smooth transitions already injected, updating...');
146
+ existingStyle.remove();
147
+ }
148
+
149
+ const style = doc.createElement('style');
150
+ style.id = 'smooth-transitions';
151
+ style.textContent = `
152
+ /* Enhanced smooth transitions for zero-flash updates */
153
+ * {
154
+ transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
155
+ background-color 0.2s ease,
156
+ color 0.2s ease,
157
+ border-color 0.2s ease,
158
+ transform 0.2s ease !important;
159
+ }
160
+
161
+ /* Prevent flash during content updates */
162
+ body {
163
+ transition: opacity 0.15s ease !important;
164
+ will-change: opacity;
165
+ }
166
+
167
+ /* Smooth content updates */
168
+ .content-updating {
169
+ animation: contentUpdate 0.3s cubic-bezier(0.4, 0, 0.2, 1);
170
+ }
171
+
172
+ @keyframes contentUpdate {
173
+ 0% {
174
+ opacity: 0.9;
175
+ transform: translateY(1px);
176
+ }
177
+ 100% {
178
+ opacity: 1;
179
+ transform: translateY(0);
180
+ }
181
+ }
182
+
183
+ /* New element entrance */
184
+ .element-entering {
185
+ animation: elementEnter 0.4s cubic-bezier(0.4, 0, 0.2, 1);
186
+ }
187
+
188
+ @keyframes elementEnter {
189
+ 0% {
190
+ opacity: 0;
191
+ transform: translateY(8px) scale(0.98);
192
+ }
193
+ 100% {
194
+ opacity: 1;
195
+ transform: translateY(0) scale(1);
196
+ }
197
+ }
198
+
199
+ /* Subtle glow for changed content */
200
+ .content-changed {
201
+ animation: contentGlow 0.6s ease-in-out;
202
+ }
203
+
204
+ @keyframes contentGlow {
205
+ 0%, 100% {
206
+ box-shadow: none;
207
+ background-color: transparent;
208
+ }
209
+ 30% {
210
+ box-shadow: 0 0 20px rgba(59, 130, 246, 0.2);
211
+ background-color: rgba(59, 130, 246, 0.05);
212
+ }
213
+ }
214
+
215
+ /* Optimize rendering performance */
216
+ * {
217
+ backface-visibility: hidden;
218
+ -webkit-font-smoothing: antialiased;
219
+ }
220
+ `;
221
+ doc.head.appendChild(style);
222
+ console.log('✨ Enhanced smooth transitions injected into iframe');
223
+ };
224
+
225
+ // Inject on iframe load and when content changes
226
+ const handleLoad = () => {
227
+ setTimeout(injectSmoothTransitions, 10);
228
+ };
229
+
230
+ iframe.addEventListener('load', handleLoad);
231
+
232
+ // Also try to inject immediately if iframe is already loaded
233
+ if (iframe.contentDocument?.readyState === 'complete') {
234
+ injectSmoothTransitions();
235
+ } else {
236
+ // Try again after a short delay
237
+ setTimeout(() => {
238
+ if (iframe.contentDocument?.readyState === 'complete') {
239
+ injectSmoothTransitions();
240
+ }
241
+ }, 100);
242
+ }
243
+
244
+ return () => {
245
+ iframe.removeEventListener('load', handleLoad);
246
+ };
247
+ }, [currentIframeRef, iframeKey]);
248
+
249
+ // Enhanced content updating animations
250
+ useEffect(() => {
251
+ const iframe = currentIframeRef.current;
252
+ if (!iframe) return;
253
+
254
+ const addContentUpdateAnimation = () => {
255
+ const doc = iframe.contentDocument;
256
+ if (!doc || !doc.body) return;
257
+
258
+ const body = doc.body;
259
+
260
+ // Remove any existing animation classes
261
+ body.classList.remove('content-updating', 'content-changed');
262
+
263
+ // Add animation class
264
+ body.classList.add('content-updating');
265
+
266
+ console.log('🎬 Applied content update animation');
267
+
268
+ // Remove class after animation
269
+ const timeout = setTimeout(() => {
270
+ body.classList.remove('content-updating');
271
+ }, 300);
272
+
273
+ return () => {
274
+ clearTimeout(timeout);
275
+ body.classList.remove('content-updating', 'content-changed');
276
+ };
277
+ };
278
+
279
+ // Small delay to ensure iframe content is ready
280
+ const timeout = setTimeout(addContentUpdateAnimation, 50);
281
+
282
+ return () => {
283
+ clearTimeout(timeout);
284
+ };
285
+ }, [displayHtml, currentIframeRef]);
286
+
287
+ // Cleanup timeout on unmount
288
+ useEffect(() => {
289
+ return () => {
290
+ if (htmlUpdateTimeoutRef.current) {
291
+ clearTimeout(htmlUpdateTimeoutRef.current);
292
+ }
293
+ };
294
+ }, []);
295
+
296
+ // Event handlers for editable mode
297
+ const handleMouseOver = (event: MouseEvent) => {
298
+ if (currentIframeRef?.current) {
299
+ const iframeDocument = currentIframeRef.current.contentDocument;
300
+ if (iframeDocument) {
301
+ const targetElement = event.target as HTMLElement;
302
+ if (
303
+ hoveredElement !== targetElement &&
304
+ targetElement !== iframeDocument.body
305
+ ) {
306
+ console.log("🎯 Edit mode: Element hovered", {
307
+ tagName: targetElement.tagName,
308
+ id: targetElement.id || 'no-id',
309
+ className: targetElement.className || 'no-class'
310
+ });
311
+
312
+ // Remove previous hover class
313
+ if (hoveredElement) {
314
+ hoveredElement.classList.remove("hovered-element");
315
+ }
316
+
317
+ setHoveredElement(targetElement);
318
+ targetElement.classList.add("hovered-element");
319
+ } else {
320
+ return setHoveredElement(null);
321
+ }
322
+ }
323
+ }
324
+ };
325
+
326
+ const handleMouseOut = () => {
327
+ setHoveredElement(null);
328
+ };
329
+
330
+ const handleClick = (event: MouseEvent) => {
331
+ console.log("🖱️ Edit mode: Click detected in iframe", {
332
+ target: event.target,
333
+ tagName: (event.target as HTMLElement)?.tagName,
334
+ isBody: event.target === currentIframeRef?.current?.contentDocument?.body,
335
+ hasOnClickElement: !!onClickElement
336
+ });
337
+
338
+ if (currentIframeRef?.current) {
339
+ const iframeDocument = currentIframeRef.current.contentDocument;
340
+ if (iframeDocument) {
341
+ const targetElement = event.target as HTMLElement;
342
+ if (targetElement !== iframeDocument.body) {
343
+ console.log("✅ Edit mode: Valid element clicked, calling onClickElement", {
344
+ tagName: targetElement.tagName,
345
+ id: targetElement.id || 'no-id',
346
+ className: targetElement.className || 'no-class',
347
+ textContent: targetElement.textContent?.substring(0, 50) + '...'
348
+ });
349
+
350
+ // Prevent default behavior to avoid navigation
351
+ event.preventDefault();
352
+ event.stopPropagation();
353
+
354
+ onClickElement?.(targetElement);
355
+ } else {
356
+ console.log("⚠️ Edit mode: Body clicked, ignoring");
357
+ }
358
+ } else {
359
+ console.error("❌ Edit mode: No iframe document available on click");
360
+ }
361
+ } else {
362
+ console.error(" Edit mode: No iframe ref available on click");
363
+ }
364
+ };
365
+
366
+ // Setup event listeners for editable mode
367
+ useUpdateEffect(() => {
368
+ const cleanupListeners = () => {
369
+ if (currentIframeRef?.current?.contentDocument) {
370
+ const iframeDocument = currentIframeRef.current.contentDocument;
371
+ iframeDocument.removeEventListener("mouseover", handleMouseOver);
372
+ iframeDocument.removeEventListener("mouseout", handleMouseOut);
373
+ iframeDocument.removeEventListener("click", handleClick);
374
+ console.log("🧹 Edit mode: Cleaned up iframe event listeners");
375
+ }
376
+ };
377
+
378
+ const setupListeners = () => {
379
+ try {
380
+ if (!currentIframeRef?.current) {
381
+ console.log("⚠️ Edit mode: No iframe ref available");
382
+ return;
383
+ }
384
+
385
+ const iframeDocument = currentIframeRef.current.contentDocument;
386
+ if (!iframeDocument) {
387
+ console.log("⚠️ Edit mode: No iframe content document available");
388
+ return;
389
+ }
390
+
391
+ // Clean up existing listeners first
392
+ cleanupListeners();
393
+
394
+ if (isEditableModeEnabled) {
395
+ console.log("🎯 Edit mode: Setting up iframe event listeners");
396
+ iframeDocument.addEventListener("mouseover", handleMouseOver);
397
+ iframeDocument.addEventListener("mouseout", handleMouseOut);
398
+ iframeDocument.addEventListener("click", handleClick);
399
+ console.log("✅ Edit mode: Event listeners added successfully");
400
+ } else {
401
+ console.log("🔇 Edit mode: Disabled, no listeners added");
402
+ }
403
+ } catch (error) {
404
+ console.error(" Edit mode: Error setting up listeners:", error);
405
+ }
406
+ };
407
+
408
+ // Add a small delay to ensure iframe is fully loaded
409
+ const timeoutId = setTimeout(setupListeners, 100);
410
+
411
+ // Clean up when component unmounts or dependencies change
412
+ return () => {
413
+ clearTimeout(timeoutId);
414
+ cleanupListeners();
415
+ };
416
+ }, [currentIframeRef, isEditableModeEnabled]);
417
+
418
+ const selectedElement = useMemo(() => {
419
+ if (!isEditableModeEnabled) return null;
420
+ if (!hoveredElement) return null;
421
+ return hoveredElement;
422
+ }, [hoveredElement, isEditableModeEnabled]);
423
+
424
+ return (
425
+ <div
426
+ ref={ref}
427
+ className={classNames(
428
+ "bg-white overflow-hidden relative flex-1 h-full",
429
+ {
430
+ "cursor-wait": isLoading && isAiWorking,
431
+ }
432
+ )}
433
+ onClick={(e) => {
434
+ e.stopPropagation();
435
+ }}
436
+ >
437
+ <GridPattern
438
+ width={20}
439
+ height={20}
440
+ x={-1}
441
+ y={-1}
442
+ strokeDasharray={"4 2"}
443
+ className={cn(
444
+ "[mask-image:radial-gradient(300px_circle_at_center,white,transparent)] z-0 absolute inset-0 h-full w-full fill-neutral-100 stroke-neutral-100"
445
+ )}
446
+ />
447
+
448
+ {/* Simplified loading overlay */}
449
+ {isLoading && isAiWorking && (
450
+ <div className="absolute inset-0 bg-black/5 backdrop-blur-[0.5px] transition-all duration-300 z-20 flex items-center justify-center">
451
+ <div className="bg-neutral-800/95 rounded-lg px-4 py-2 text-sm text-neutral-300 border border-neutral-700 shadow-lg">
452
+ <div className="flex items-center gap-2">
453
+ <div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
454
+ Updating preview...
455
+ </div>
456
+ </div>
457
+ </div>
458
+ )}
459
+
460
+ {/* Selected element indicator */}
461
+ {!isAiWorking && hoveredElement && selectedElement && (
462
+ <div className="absolute bottom-4 left-4 z-30">
463
+ <div className="bg-neutral-800/90 rounded-lg px-3 py-2 text-sm text-neutral-300 border border-neutral-700 shadow-lg">
464
+ <span className="font-medium">
465
+ {htmlTagToText(selectedElement.tagName.toLowerCase())}
466
+ </span>
467
+ {selectedElement.id && (
468
+ <span className="ml-2 text-neutral-400">#{selectedElement.id}</span>
469
+ )}
470
+ </div>
471
+ </div>
472
+ )}
473
+
474
+ {/* Reliable iframe with force refresh capability */}
475
+ <iframe
476
+ key={iframeKey}
477
+ id="preview-iframe"
478
+ ref={currentIframeRef}
479
+ title="output"
480
+ className={classNames(
481
+ "w-full select-none h-full transition-all duration-200 ease-out",
482
+ {
483
+ "pointer-events-none": isResizing || isAiWorking,
484
+ "opacity-95 scale-[0.999]": isLoading && isAiWorking,
485
+ "opacity-100 scale-100": !isLoading || !isAiWorking,
486
+ "bg-black": true,
487
+ "lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
488
+ device === "mobile",
489
+ "lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
490
+ device === "desktop",
491
+ }
492
+ )}
493
+ srcDoc={displayHtml}
494
+ onLoad={() => {
495
+ console.log('🎯 Preview iframe loaded:', {
496
+ displayHtmlLength: displayHtml.length,
497
+ iframeKey,
498
+ hasContent: displayHtml.length > 0
499
+ });
500
+ setIsLoading(false);
501
+ }}
502
+ onError={(e) => {
503
+ console.error('❌ Iframe loading error:', e);
504
+ setIsLoading(false);
505
+ }}
506
+ />
507
+ </div>
508
+ );
509
+ });
510
+
511
+ Preview.displayName = "Preview";