enzostvs HF Staff commited on
Commit
4fb6d26
·
1 Parent(s): e0eec0d

wip: remove readonly

Browse files
app/api/proxy/route.ts DELETED
@@ -1,240 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { isAuthenticated } from "@/lib/auth";
3
-
4
- export async function GET(req: NextRequest) {
5
- const user: any = await isAuthenticated();
6
-
7
- if (user instanceof NextResponse || !user) {
8
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
9
- }
10
-
11
- const { searchParams } = new URL(req.url);
12
- const spaceId = searchParams.get('spaceId');
13
- const commitId = searchParams.get('commitId');
14
- const path = searchParams.get('path') || '/';
15
-
16
- if (!spaceId) {
17
- return NextResponse.json({ error: "spaceId parameter required" }, { status: 400 });
18
- }
19
-
20
- try {
21
- const spaceDomain = `${spaceId.replace("/", "-")}${commitId !== null? `--rev-${commitId.slice(0, 7)}` : ""}.static.hf.space`;
22
- const targetUrl = `https://${spaceDomain}${path}`;
23
-
24
- console.log("targetUrl", targetUrl);
25
-
26
- const response = await fetch(targetUrl, {
27
- headers: {
28
- 'User-Agent': req.headers.get('user-agent') || '',
29
- },
30
- });
31
-
32
- if (!response.ok) {
33
- console.error('Failed to fetch from HF space:', response.status, response.statusText);
34
- return NextResponse.json({
35
- error: "Failed to fetch content",
36
- details: `${response.status} ${response.statusText}`,
37
- targetUrl
38
- }, { status: response.status });
39
- }
40
-
41
- let content = await response.text();
42
- const contentType = response.headers.get('content-type') || 'text/html';
43
-
44
- // Rewrite relative URLs to go through the proxy
45
- if (contentType.includes('text/html')) {
46
- const baseUrl = `https://${spaceDomain}`;
47
-
48
- // Fix relative URLs in href attributes
49
- content = content.replace(/href="([^"]+)"/g, (match, url) => {
50
- if (url.startsWith('/') && !url.startsWith('//')) {
51
- // Relative URL starting with /
52
- return `href="${baseUrl}${url}"`;
53
- } else if (!url.includes('://') && !url.startsWith('#') && !url.startsWith('mailto:') && !url.startsWith('tel:')) {
54
- // Relative URL not starting with /
55
- return `href="${baseUrl}/${url}"`;
56
- }
57
- return match;
58
- });
59
-
60
- // Fix relative URLs in src attributes
61
- content = content.replace(/src="([^"]+)"/g, (match, url) => {
62
- if (url.startsWith('/') && !url.startsWith('//')) {
63
- return `src="${baseUrl}${url}"`;
64
- } else if (!url.includes('://')) {
65
- return `src="${baseUrl}/${url}"`;
66
- }
67
- return match;
68
- });
69
-
70
- // Add base tag to ensure relative URLs work correctly
71
- const baseTag = `<base href="${baseUrl}/">`;
72
- if (content.includes('<head>')) {
73
- content = content.replace('<head>', `<head>${baseTag}`);
74
- } else if (content.includes('<html>')) {
75
- content = content.replace('<html>', `<html><head>${baseTag}</head>`);
76
- } else {
77
- content = `<head>${baseTag}</head>` + content;
78
- }
79
- }
80
-
81
- const injectedScript = `
82
- <script>
83
- // Add event listeners and communicate with parent
84
- document.addEventListener('DOMContentLoaded', function() {
85
- let hoveredElement = null;
86
- let isEditModeEnabled = false;
87
-
88
- document.addEventListener('mouseover', function(event) {
89
- if (event.target !== document.body && event.target !== document.documentElement) {
90
- hoveredElement = event.target;
91
-
92
- const rect = event.target.getBoundingClientRect();
93
- const message = {
94
- type: 'ELEMENT_HOVERED',
95
- data: {
96
- tagName: event.target.tagName,
97
- rect: {
98
- top: rect.top,
99
- left: rect.left,
100
- width: rect.width,
101
- height: rect.height
102
- },
103
- element: event.target.outerHTML
104
- }
105
- };
106
- parent.postMessage(message, '*');
107
- }
108
- });
109
-
110
- document.addEventListener('mouseout', function(event) {
111
- hoveredElement = null;
112
-
113
- parent.postMessage({
114
- type: 'ELEMENT_MOUSE_OUT'
115
- }, '*');
116
- });
117
-
118
- // Handle clicks - prevent default only in edit mode
119
- document.addEventListener('click', function(event) {
120
- // Only prevent default if edit mode is enabled
121
- if (isEditModeEnabled) {
122
- event.preventDefault();
123
- event.stopPropagation();
124
-
125
- const rect = event.target.getBoundingClientRect();
126
- parent.postMessage({
127
- type: 'ELEMENT_CLICKED',
128
- data: {
129
- tagName: event.target.tagName,
130
- rect: {
131
- top: rect.top,
132
- left: rect.left,
133
- width: rect.width,
134
- height: rect.height
135
- },
136
- element: event.target.outerHTML
137
- }
138
- }, '*');
139
- } else {
140
- // In non-edit mode, handle link clicks to maintain proxy context
141
- const link = event.target.closest('a');
142
- if (link && link.href) {
143
- event.preventDefault();
144
-
145
- const url = new URL(link.href);
146
-
147
- // If it's an external link (different domain than the space), open in new tab
148
- if (url.hostname !== '${spaceDomain}') {
149
- window.open(link.href, '_blank');
150
- } else {
151
- // For internal links within the space, navigate through the proxy
152
- // Extract the path and query parameters from the original link
153
- const targetPath = url.pathname + url.search + url.hash;
154
-
155
- // Get current proxy URL parameters
156
- const currentUrl = new URL(window.location.href);
157
- const spaceId = currentUrl.searchParams.get('spaceId') || '';
158
- const commitId = currentUrl.searchParams.get('commitId') || '';
159
-
160
- // Construct new proxy URL with the target path
161
- const proxyUrl = '/api/proxy/?' +
162
- 'spaceId=' + encodeURIComponent(spaceId) +
163
- (commitId ? '&commitId=' + encodeURIComponent(commitId) : '') +
164
- '&path=' + encodeURIComponent(targetPath);
165
-
166
- // Navigate to the new URL through the parent window
167
- parent.postMessage({
168
- type: 'NAVIGATE_TO_PROXY',
169
- data: {
170
- proxyUrl: proxyUrl,
171
- targetPath: targetPath
172
- }
173
- }, '*');
174
- }
175
- }
176
- }
177
- });
178
-
179
- // Prevent form submissions when in edit mode
180
- document.addEventListener('submit', function(event) {
181
- if (isEditModeEnabled) {
182
- event.preventDefault();
183
- event.stopPropagation();
184
- }
185
- });
186
-
187
- // Prevent other navigation events when in edit mode
188
- document.addEventListener('keydown', function(event) {
189
- if (isEditModeEnabled && event.key === 'Enter' && (event.target.tagName === 'A' || event.target.tagName === 'BUTTON')) {
190
- event.preventDefault();
191
- event.stopPropagation();
192
- }
193
- });
194
-
195
- // Listen for messages from parent
196
- window.addEventListener('message', function(event) {
197
- if (event.data.type === 'ENABLE_EDIT_MODE') {
198
- isEditModeEnabled = true;
199
- document.body.style.userSelect = 'none';
200
- document.body.style.pointerEvents = 'auto';
201
- } else if (event.data.type === 'DISABLE_EDIT_MODE') {
202
- isEditModeEnabled = false;
203
- document.body.style.userSelect = '';
204
- document.body.style.pointerEvents = '';
205
- }
206
- });
207
-
208
- // Notify parent that script is ready
209
- parent.postMessage({
210
- type: 'PROXY_SCRIPT_READY'
211
- }, '*');
212
- });
213
- </script>
214
- `;
215
-
216
- let modifiedContent;
217
- if (content.includes('</body>')) {
218
- modifiedContent = content.replace(
219
- /<\/body>/i,
220
- `${injectedScript}</body>`
221
- );
222
- } else {
223
- modifiedContent = content + injectedScript;
224
- }
225
-
226
- return new NextResponse(modifiedContent, {
227
- headers: {
228
- 'Content-Type': contentType,
229
- 'Cache-Control': 'no-cache, no-store, must-revalidate',
230
- },
231
- });
232
-
233
- } catch (error) {
234
- return NextResponse.json({
235
- error: "Proxy request failed",
236
- details: error instanceof Error ? error.message : String(error),
237
- spaceId
238
- }, { status: 500 });
239
- }
240
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/history-notification/index.tsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import classNames from "classnames";
5
+ import { Button } from "@/components/ui/button";
6
+ import Loading from "@/components/loading";
7
+ import {
8
+ History,
9
+ ChevronUp,
10
+ ChevronDown,
11
+ MousePointerClick,
12
+ } from "lucide-react";
13
+
14
+ interface HistoryNotificationProps {
15
+ /** Whether the historical version notification should be visible */
16
+ isVisible: boolean;
17
+ /** Whether the version promotion is in progress */
18
+ isPromotingVersion: boolean;
19
+ /** Function to promote the current historical version */
20
+ onPromoteVersion: () => void;
21
+ /** Function to go back to the current version */
22
+ onGoBackToCurrent: () => void;
23
+ /** Additional CSS classes */
24
+ className?: string;
25
+ }
26
+
27
+ export const HistoryNotification = ({
28
+ isVisible,
29
+ isPromotingVersion,
30
+ onPromoteVersion,
31
+ onGoBackToCurrent,
32
+ className,
33
+ }: HistoryNotificationProps) => {
34
+ const [isCollapsed, setIsCollapsed] = useState(false);
35
+
36
+ if (!isVisible) {
37
+ return null;
38
+ }
39
+
40
+ return (
41
+ <div
42
+ className={classNames(
43
+ "absolute bottom-4 left-4 z-10 bg-white/95 backdrop-blur-sm border border-neutral-200 rounded-xl shadow-lg transition-all duration-300 ease-in-out",
44
+ className
45
+ )}
46
+ >
47
+ {isCollapsed ? (
48
+ // Collapsed state
49
+ <div className="flex items-center gap-2 p-3">
50
+ <History className="size-4 text-neutral-600" />
51
+ <span className="text-xs text-neutral-600 font-medium">
52
+ Historical Version
53
+ </span>
54
+ <Button
55
+ variant="outline"
56
+ size="iconXs"
57
+ className="!rounded-md !border-neutral-200"
58
+ onClick={() => setIsCollapsed(false)}
59
+ >
60
+ <ChevronUp className="text-neutral-400 size-3" />
61
+ </Button>
62
+ </div>
63
+ ) : (
64
+ // Expanded state
65
+ <div className="p-4 max-w-sm w-full">
66
+ <div className="flex items-start gap-3">
67
+ <History className="size-4 text-neutral-600 translate-y-1.5" />
68
+ <div className="flex-1 min-w-0">
69
+ <div className="flex items-center justify-between mb-1">
70
+ <div className="flex items-center gap-2">
71
+ <p className="font-semibold text-sm text-neutral-800">
72
+ Historical Version
73
+ </p>
74
+ </div>
75
+ <Button
76
+ variant="outline"
77
+ size="iconXs"
78
+ className="!rounded-md !border-neutral-200"
79
+ onClick={() => setIsCollapsed(true)}
80
+ >
81
+ <ChevronDown className="text-neutral-400 size-3" />
82
+ </Button>
83
+ </div>
84
+ <p className="text-xs text-neutral-600 leading-relaxed mb-3">
85
+ You're viewing a previous version of this project. Promote this
86
+ version to make it current and deploy it live.
87
+ </p>
88
+ <div className="flex items-center gap-2">
89
+ <Button
90
+ size="xs"
91
+ variant="black"
92
+ className="!pr-3"
93
+ onClick={onPromoteVersion}
94
+ disabled={isPromotingVersion}
95
+ >
96
+ {isPromotingVersion ? (
97
+ <Loading overlay={false} />
98
+ ) : (
99
+ <MousePointerClick className="size-3" />
100
+ )}
101
+ Promote Version
102
+ </Button>
103
+ <Button
104
+ size="xs"
105
+ variant="outline"
106
+ className=" !text-neutral-600 !border-neutral-200"
107
+ disabled={isPromotingVersion}
108
+ onClick={onGoBackToCurrent}
109
+ >
110
+ Go back to current
111
+ </Button>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ )}
117
+ </div>
118
+ );
119
+ };
components/editor/index.tsx CHANGED
@@ -25,10 +25,15 @@ export const AppEditor = ({
25
  repoId?: string;
26
  isNew?: boolean;
27
  }) => {
28
- const { project, setPages, files, currentPageData, currentTab } = useEditor(
29
- namespace,
30
- repoId
31
- );
 
 
 
 
 
32
  const [, copyToClipboard] = useCopyToClipboard();
33
 
34
  const monacoRef = useRef<any>(null);
@@ -71,10 +76,11 @@ export const AppEditor = ({
71
  horizontal: "hidden",
72
  },
73
  wordWrap: "on",
74
- readOnly: true,
75
  readOnlyMessage: {
76
- value:
77
- "You can't edit the code, ask DeepSite to do it for you!",
 
78
  isTrusted: true,
79
  },
80
  }}
 
25
  repoId?: string;
26
  isNew?: boolean;
27
  }) => {
28
+ const {
29
+ project,
30
+ setPages,
31
+ files,
32
+ currentPageData,
33
+ currentTab,
34
+ currentCommit,
35
+ } = useEditor(namespace, repoId);
36
+ const { isAiWorking } = useAi();
37
  const [, copyToClipboard] = useCopyToClipboard();
38
 
39
  const monacoRef = useRef<any>(null);
 
76
  horizontal: "hidden",
77
  },
78
  wordWrap: "on",
79
+ readOnly: !!isAiWorking || !!currentCommit,
80
  readOnlyMessage: {
81
+ value: currentCommit
82
+ ? "You can't edit the code, as this is an old version of the project."
83
+ : "Wait for DeepSite to finish working...",
84
  isTrusted: true,
85
  },
86
  }}
components/editor/preview/index.tsx CHANGED
@@ -14,13 +14,8 @@ import { AiLoading } from "../ask-ai/loading";
14
  import { defaultHTML } from "@/lib/consts";
15
  import { Button } from "@/components/ui/button";
16
  import { LivePreview } from "../live-preview";
17
- import {
18
- MousePointerClick,
19
- History,
20
- AlertCircle,
21
- ChevronDown,
22
- ChevronUp,
23
- } from "lucide-react";
24
  import { api } from "@/lib/api";
25
  import { toast } from "sonner";
26
  import Loading from "@/components/loading";
@@ -34,26 +29,20 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
34
  currentCommit,
35
  setCurrentCommit,
36
  currentPageData,
 
 
37
  } = useEditor();
38
  const {
39
  isEditableModeEnabled,
40
  setSelectedElement,
41
  isAiWorking,
 
42
  setIsEditableModeEnabled,
43
  } = useAi();
44
 
45
- const iframeSrc = project?.space_id
46
- ? `/api/proxy/?spaceId=${encodeURIComponent(project.space_id)}${
47
- currentCommit ? `&commitId=${currentCommit}` : ""
48
- }`
49
- : "";
50
-
51
  const iframeRef = useRef<HTMLIFrameElement>(null);
52
 
53
- // For private projects, use srcDoc instead of proxy URL
54
- const shouldUseCustomIframe = project?.private && currentPageData?.html;
55
-
56
- // Inject event handling script for private projects
57
  const injectInteractivityScript = (html: string) => {
58
  const interactivityScript = `
59
  <script>
@@ -112,6 +101,43 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
112
  element: event.target.outerHTML
113
  }
114
  }, '*');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  }
116
  });
117
 
@@ -146,7 +172,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
146
 
147
  // Notify parent that script is ready
148
  parent.postMessage({
149
- type: 'PROXY_SCRIPT_READY'
150
  }, '*');
151
  });
152
  </script>
@@ -163,9 +189,8 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
163
  tagName: string;
164
  rect: { top: number; left: number; width: number; height: number };
165
  } | null>(null);
166
- const [isHistoryNotificationCollapsed, setIsHistoryNotificationCollapsed] =
167
- useState(false);
168
  const [isPromotingVersion, setIsPromotingVersion] = useState(false);
 
169
 
170
  // Handle PostMessage communication with iframe
171
  useEffect(() => {
@@ -177,7 +202,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
177
 
178
  const { type, data } = event.data;
179
  switch (type) {
180
- case "PROXY_SCRIPT_READY":
181
  if (iframeRef.current?.contentWindow) {
182
  iframeRef.current.contentWindow.postMessage(
183
  {
@@ -210,10 +235,20 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
210
  setIsEditableModeEnabled(false);
211
  }
212
  break;
213
- case "NAVIGATE_TO_PROXY":
214
- // Handle navigation within the iframe while maintaining proxy context
215
- if (iframeRef.current && data.proxyUrl) {
216
- iframeRef.current.src = data.proxyUrl;
 
 
 
 
 
 
 
 
 
 
217
  }
218
  break;
219
  }
@@ -221,7 +256,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
221
 
222
  window.addEventListener("message", handleMessage);
223
  return () => window.removeEventListener("message", handleMessage);
224
- }, [setSelectedElement, isEditableModeEnabled]);
225
 
226
  // Send edit mode state to iframe and clear hover state when disabled
227
  useUpdateEffect(() => {
@@ -240,12 +275,26 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
240
  if (!isEditableModeEnabled) {
241
  setHoveredElement(null);
242
  }
243
- }, [
244
- isEditableModeEnabled,
245
- project?.space_id,
246
- shouldUseCustomIframe,
247
- currentPageData?.html,
248
- ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
  const promoteVersion = async () => {
251
  setIsPromotingVersion(true);
@@ -298,7 +347,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
298
  </span>
299
  </div>
300
  )}
301
- {isNew && !isAiWorking ? (
302
  <iframe
303
  className={classNames(
304
  "w-full select-none transition-all duration-200 bg-black h-full",
@@ -309,18 +358,11 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
309
  )}
310
  srcDoc={defaultHTML}
311
  />
312
- ) : iframeSrc === "" ||
313
- isLoadingProject ||
314
- (isAiWorking && iframeSrc == "") ||
315
- (shouldUseCustomIframe && !currentPageData?.html) ? (
316
  <div className="w-full h-full flex items-center justify-center relative">
317
  <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
318
  <AiLoading
319
- text={
320
- isAiWorking && iframeSrc === ""
321
- ? undefined
322
- : "Fetching your space..."
323
- }
324
  className="flex-col"
325
  />
326
  <AnimatedBlobs />
@@ -345,94 +387,28 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
345
  device === "mobile",
346
  }
347
  )}
348
- {...(shouldUseCustomIframe
349
- ? {
350
- srcDoc: injectInteractivityScript(
351
- currentPageData?.html || ""
352
- ),
353
- }
354
- : { src: iframeSrc })}
 
 
 
 
 
 
 
355
  allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"
356
  />
357
- <div
358
- className={classNames(
359
- "absolute bottom-4 left-4 z-10 bg-white/95 backdrop-blur-sm border border-neutral-200 rounded-xl shadow-lg transition-all duration-300 ease-in-out",
360
- {
361
- hidden: !currentCommit,
362
- }
363
- )}
364
- >
365
- {isHistoryNotificationCollapsed ? (
366
- // Collapsed state
367
- <div className="flex items-center gap-2 p-3">
368
- <History className="size-4 text-neutral-600" />
369
- <span className="text-xs text-neutral-600 font-medium">
370
- Historical Version
371
- </span>
372
- <Button
373
- variant="outline"
374
- size="iconXs"
375
- className="!rounded-md !border-neutral-200"
376
- onClick={() => setIsHistoryNotificationCollapsed(false)}
377
- >
378
- <ChevronUp className="text-neutral-400 size-3" />
379
- </Button>
380
- </div>
381
- ) : (
382
- // Expanded state
383
- <div className="p-4 max-w-sm w-full">
384
- <div className="flex items-start gap-3">
385
- <History className="size-4 text-neutral-600 translate-y-1.5" />
386
- <div className="flex-1 min-w-0">
387
- <div className="flex items-center justify-between mb-1">
388
- <div className="flex items-center gap-2">
389
- <p className="font-semibold text-sm text-neutral-800">
390
- Historical Version
391
- </p>
392
- </div>
393
- <Button
394
- variant="outline"
395
- size="iconXs"
396
- className="!rounded-md !border-neutral-200"
397
- onClick={() => setIsHistoryNotificationCollapsed(true)}
398
- >
399
- <ChevronDown className="text-neutral-400 size-3" />
400
- </Button>
401
- </div>
402
- <p className="text-xs text-neutral-600 leading-relaxed mb-3">
403
- You're viewing a previous version of this project. Promote
404
- this version to make it current and deploy it live.
405
- </p>
406
- <div className="flex items-center gap-2">
407
- <Button
408
- size="xs"
409
- variant="black"
410
- className="!pr-3"
411
- onClick={() => promoteVersion()}
412
- disabled={isPromotingVersion}
413
- >
414
- {isPromotingVersion ? (
415
- <Loading overlay={false} />
416
- ) : (
417
- <MousePointerClick className="size-3" />
418
- )}
419
- Promote Version
420
- </Button>
421
- <Button
422
- size="xs"
423
- variant="outline"
424
- className=" !text-neutral-600 !border-neutral-200"
425
- disabled={isPromotingVersion}
426
- onClick={() => setCurrentCommit(null)}
427
- >
428
- Go back to current
429
- </Button>
430
- </div>
431
- </div>
432
- </div>
433
- </div>
434
- )}
435
- </div>
436
  </>
437
  )}
438
  </div>
 
14
  import { defaultHTML } from "@/lib/consts";
15
  import { Button } from "@/components/ui/button";
16
  import { LivePreview } from "../live-preview";
17
+ import { HistoryNotification } from "../history-notification";
18
+ import { AlertCircle } from "lucide-react";
 
 
 
 
 
19
  import { api } from "@/lib/api";
20
  import { toast } from "sonner";
21
  import Loading from "@/components/loading";
 
29
  currentCommit,
30
  setCurrentCommit,
31
  currentPageData,
32
+ pages,
33
+ setCurrentPage,
34
  } = useEditor();
35
  const {
36
  isEditableModeEnabled,
37
  setSelectedElement,
38
  isAiWorking,
39
+ globalAiLoading,
40
  setIsEditableModeEnabled,
41
  } = useAi();
42
 
 
 
 
 
 
 
43
  const iframeRef = useRef<HTMLIFrameElement>(null);
44
 
45
+ // Inject event handling script
 
 
 
46
  const injectInteractivityScript = (html: string) => {
47
  const interactivityScript = `
48
  <script>
 
101
  element: event.target.outerHTML
102
  }
103
  }, '*');
104
+ } else {
105
+ // Handle link clicks to navigate between pages
106
+ const link = event.target.closest('a');
107
+ if (link && link.href) {
108
+ event.preventDefault();
109
+
110
+ const url = new URL(link.href, window.location.href);
111
+
112
+ // Check if it's a relative link (same origin)
113
+ if (url.origin === window.location.origin || link.href.startsWith('/') || link.href.startsWith('./') || link.href.startsWith('../') || !link.href.includes('://')) {
114
+ // Extract the path from the link
115
+ let targetPath = link.getAttribute('href') || '';
116
+
117
+ // Handle relative paths
118
+ if (targetPath.startsWith('./')) {
119
+ targetPath = targetPath.substring(2);
120
+ } else if (targetPath.startsWith('/')) {
121
+ targetPath = targetPath.substring(1);
122
+ }
123
+
124
+ // If no extension, assume .html
125
+ if (!targetPath.includes('.') && !targetPath.includes('?') && !targetPath.includes('#')) {
126
+ targetPath = targetPath === '' ? 'index.html' : targetPath + '.html';
127
+ }
128
+
129
+ // Send message to parent to navigate to the page
130
+ parent.postMessage({
131
+ type: 'NAVIGATE_TO_PAGE',
132
+ data: {
133
+ targetPath: targetPath
134
+ }
135
+ }, '*');
136
+ } else {
137
+ // External link - open in new tab
138
+ window.open(link.href, '_blank');
139
+ }
140
+ }
141
  }
142
  });
143
 
 
172
 
173
  // Notify parent that script is ready
174
  parent.postMessage({
175
+ type: 'IFRAME_SCRIPT_READY'
176
  }, '*');
177
  });
178
  </script>
 
189
  tagName: string;
190
  rect: { top: number; left: number; width: number; height: number };
191
  } | null>(null);
 
 
192
  const [isPromotingVersion, setIsPromotingVersion] = useState(false);
193
+ const [stableHtml, setStableHtml] = useState<string>("");
194
 
195
  // Handle PostMessage communication with iframe
196
  useEffect(() => {
 
202
 
203
  const { type, data } = event.data;
204
  switch (type) {
205
+ case "IFRAME_SCRIPT_READY":
206
  if (iframeRef.current?.contentWindow) {
207
  iframeRef.current.contentWindow.postMessage(
208
  {
 
235
  setIsEditableModeEnabled(false);
236
  }
237
  break;
238
+ case "NAVIGATE_TO_PAGE":
239
+ // Handle navigation between pages by updating currentPageData
240
+ if (data.targetPath) {
241
+ // Find the page in the pages array
242
+ const targetPage = pages.find(
243
+ (page) => page.path === data.targetPath
244
+ );
245
+ if (targetPage) {
246
+ setCurrentPage(data.targetPath);
247
+ } else {
248
+ // If page doesn't exist, you might want to create it or show an error
249
+ console.warn(`Page not found: ${data.targetPath}`);
250
+ toast.error(`Page not found: ${data.targetPath}`);
251
+ }
252
  }
253
  break;
254
  }
 
256
 
257
  window.addEventListener("message", handleMessage);
258
  return () => window.removeEventListener("message", handleMessage);
259
+ }, [setSelectedElement, isEditableModeEnabled, pages, setCurrentPage]);
260
 
261
  // Send edit mode state to iframe and clear hover state when disabled
262
  useUpdateEffect(() => {
 
275
  if (!isEditableModeEnabled) {
276
  setHoveredElement(null);
277
  }
278
+ }, [isEditableModeEnabled, stableHtml]);
279
+
280
+ // Update stable HTML only when AI finishes working to prevent blinking
281
+ useEffect(() => {
282
+ if (!isAiWorking && !globalAiLoading && currentPageData?.html) {
283
+ setStableHtml(currentPageData.html);
284
+ }
285
+ }, [isAiWorking, globalAiLoading, currentPageData?.html]);
286
+
287
+ // Initialize stable HTML when component first loads
288
+ useEffect(() => {
289
+ if (
290
+ currentPageData?.html &&
291
+ !stableHtml &&
292
+ !isAiWorking &&
293
+ !globalAiLoading
294
+ ) {
295
+ setStableHtml(currentPageData.html);
296
+ }
297
+ }, [currentPageData?.html, stableHtml, isAiWorking, globalAiLoading]);
298
 
299
  const promoteVersion = async () => {
300
  setIsPromotingVersion(true);
 
347
  </span>
348
  </div>
349
  )}
350
+ {isNew && !isLoadingProject ? (
351
  <iframe
352
  className={classNames(
353
  "w-full select-none transition-all duration-200 bg-black h-full",
 
358
  )}
359
  srcDoc={defaultHTML}
360
  />
361
+ ) : isLoadingProject || globalAiLoading ? (
 
 
 
362
  <div className="w-full h-full flex items-center justify-center relative">
363
  <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
364
  <AiLoading
365
+ text={isLoadingProject ? "Fetching your project..." : undefined}
 
 
 
 
366
  className="flex-col"
367
  />
368
  <AnimatedBlobs />
 
387
  device === "mobile",
388
  }
389
  )}
390
+ src={
391
+ currentCommit
392
+ ? `https://${project?.space_id?.replaceAll(
393
+ "/",
394
+ "-"
395
+ )}--rev-${currentCommit.slice(0, 7)}.static.hf.space`
396
+ : undefined
397
+ }
398
+ srcDoc={
399
+ !currentCommit
400
+ ? injectInteractivityScript(stableHtml || "")
401
+ : undefined
402
+ }
403
+ sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
404
  allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"
405
  />
406
+ <HistoryNotification
407
+ isVisible={!!currentCommit}
408
+ isPromotingVersion={isPromotingVersion}
409
+ onPromoteVersion={promoteVersion}
410
+ onGoBackToCurrent={() => setCurrentCommit(null)}
411
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  </>
413
  )}
414
  </div>
components/my-projects/project-card.tsx CHANGED
@@ -67,7 +67,10 @@ export function ProjectCard({
67
  ) : (
68
  <div className="absolute inset-0 w-full h-full overflow-hidden">
69
  <iframe
70
- src={`/api/proxy/?spaceId=${encodeURIComponent(project.name)}`}
 
 
 
71
  className="w-[1200px] h-[675px] border-0 origin-top-left"
72
  style={{
73
  transform: "scale(0.5)",
 
67
  ) : (
68
  <div className="absolute inset-0 w-full h-full overflow-hidden">
69
  <iframe
70
+ src={`https://${project.name.replaceAll(
71
+ "/",
72
+ "-"
73
+ )}.static.hf.space`}
74
  className="w-[1200px] h-[675px] border-0 origin-top-left"
75
  style={{
76
  transform: "scale(0.5)",