This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. .gitattributes +11 -0
  2. README.md +2 -7
  3. app/(public)/layout.tsx +1 -1
  4. app/(public)/page.tsx +31 -180
  5. app/(public)/projects/page.tsx +8 -4
  6. app/actions/projects.ts +40 -24
  7. app/api/{ask → ask-ai}/route.ts +112 -298
  8. app/api/auth/login-url/route.ts +0 -23
  9. app/api/auth/logout/route.ts +0 -25
  10. app/api/auth/route.ts +1 -21
  11. app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -190
  12. app/api/me/projects/[namespace]/[repoId]/images/route.ts +0 -113
  13. app/api/me/projects/[namespace]/[repoId]/route.ts +162 -112
  14. app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -64
  15. app/api/me/projects/route.ts +92 -73
  16. app/api/me/route.ts +1 -22
  17. app/auth/callback/page.tsx +42 -67
  18. app/layout.tsx +8 -16
  19. app/projects/[namespace]/[repoId]/page.tsx +31 -1
  20. app/projects/new/page.tsx +2 -2
  21. assets/globals.css +0 -225
  22. components.json +1 -1
  23. components/animated-blobs/index.tsx +0 -34
  24. components/animated-text/index.tsx +0 -123
  25. components/contexts/app-context.tsx +10 -6
  26. components/contexts/login-context.tsx +0 -61
  27. components/contexts/pro-context.tsx +0 -48
  28. components/editor/ask-ai/follow-up-tooltip.tsx +36 -0
  29. components/editor/ask-ai/index.tsx +390 -179
  30. components/editor/ask-ai/loading.tsx +0 -59
  31. components/editor/ask-ai/prompt-builder/content-modal.tsx +0 -196
  32. components/editor/ask-ai/prompt-builder/index.tsx +0 -73
  33. components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +0 -58
  34. components/editor/ask-ai/prompt-builder/themes.tsx +0 -48
  35. components/editor/ask-ai/re-imagine.tsx +4 -27
  36. components/editor/ask-ai/selected-files.tsx +0 -47
  37. components/editor/ask-ai/selector.tsx +0 -41
  38. components/editor/ask-ai/settings.tsx +146 -162
  39. components/editor/ask-ai/uploader.tsx +0 -165
  40. components/editor/deploy-button/index.tsx +173 -0
  41. components/editor/footer/index.tsx +127 -0
  42. components/editor/header/index.tsx +48 -91
  43. components/editor/header/switch-tab.tsx +0 -58
  44. components/editor/history-notification/index.tsx +0 -119
  45. components/editor/history/index.tsx +30 -66
  46. components/editor/index.tsx +315 -128
  47. components/editor/live-preview/index.tsx +0 -165
  48. components/editor/pages/index.tsx +0 -24
  49. components/editor/pages/page.tsx +0 -56
  50. components/editor/preview/index.tsx +153 -335
.gitattributes ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ public/bowl1.jpg filter=lfs diff=lfs merge=lfs -text
2
+ public/bowl2.jpg filter=lfs diff=lfs merge=lfs -text
3
+ public/bowl3.jpg filter=lfs diff=lfs merge=lfs -text
4
+ public/local1.jpg filter=lfs diff=lfs merge=lfs -text
5
+ public/local2.jpg filter=lfs diff=lfs merge=lfs -text
6
+ public/local3.jpg filter=lfs diff=lfs merge=lfs -text
7
+ public/portada1.JPG filter=lfs diff=lfs merge=lfs -text
8
+ public/riva_way_negro.png filter=lfs diff=lfs merge=lfs -text
9
+ public/style1.jpg filter=lfs diff=lfs merge=lfs -text
10
+ public/style2.JPG filter=lfs diff=lfs merge=lfs -text
11
+ public/style3.JPG filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: DeepSite v3
3
  emoji: 🐳
4
  colorFrom: blue
5
  colorTo: blue
@@ -11,11 +11,6 @@ short_description: Generate any application with DeepSeek
11
  models:
12
  - deepseek-ai/DeepSeek-V3-0324
13
  - deepseek-ai/DeepSeek-R1-0528
14
- - Qwen/Qwen3-Coder-480B-A35B-Instruct
15
- - moonshotai/Kimi-K2-Instruct
16
- - moonshotai/Kimi-K2-Instruct-0905
17
- - deepseek-ai/DeepSeek-V3.1
18
- - deepseek-ai/DeepSeek-V3.1-Terminus
19
  ---
20
 
21
  # DeepSite 🐳
@@ -24,4 +19,4 @@ DeepSite is a coding platform powered by DeepSeek AI, designed to make coding sm
24
 
25
  ## How to use it locally
26
 
27
- Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
 
1
  ---
2
+ title: DeepSite v2
3
  emoji: 🐳
4
  colorFrom: blue
5
  colorTo: blue
 
11
  models:
12
  - deepseek-ai/DeepSeek-V3-0324
13
  - deepseek-ai/DeepSeek-R1-0528
 
 
 
 
 
14
  ---
15
 
16
  # DeepSite 🐳
 
19
 
20
  ## How to use it locally
21
 
22
+ Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
app/(public)/layout.tsx CHANGED
@@ -6,7 +6,7 @@ export default async function PublicLayout({
6
  children: React.ReactNode;
7
  }>) {
8
  return (
9
- <div className="h-screen bg-neutral-950 z-1 relative overflow-auto scroll-smooth">
10
  <div className="background__noisy" />
11
  <Navigation />
12
  {children}
 
6
  children: React.ReactNode;
7
  }>) {
8
  return (
9
+ <div className="min-h-screen bg-black z-1 relative">
10
  <div className="background__noisy" />
11
  <Navigation />
12
  {children}
app/(public)/page.tsx CHANGED
@@ -1,193 +1,44 @@
1
- // import { AskAi } from "@/components/space/ask-ai";
2
  import { redirect } from "next/navigation";
3
- import { AnimatedText } from "@/components/animated-text";
4
- import { AnimatedBlobs } from "@/components/animated-blobs";
5
-
6
  export default function Home() {
7
- redirect("/projects");
8
  return (
9
- <div className="">
10
  <header className="container mx-auto pt-20 px-6 relative flex flex-col items-center justify-center text-center">
11
- <div className="rounded-full border border-sky-100/10 bg-gradient-to-r from-sky-500/15 to-sky-sky-500/5 text-sm text-sky-300 px-3 py-1 max-w-max mx-auto mb-2">
12
- ✨ DeepSite v3 is out!
13
  </div>
14
- <h1 className="text-6xl lg:text-8xl font-semibold text-white font-mono max-w-4xl">
15
  Code your website with AI in seconds
16
  </h1>
17
- <AnimatedText className="text-xl lg:text-2xl text-neutral-300/80 mt-4 text-center max-w-2xl" />
18
- <div className="mt-14 max-w-2xl w-full mx-auto">{/* <AskAi /> */}</div>
19
- <AnimatedBlobs />
20
- </header>
21
-
22
- <div id="features" className="min-h-screen py-20 px-6 relative">
23
- <div className="container mx-auto"></div>
24
- <div className="text-center mb-16">
25
- <div className="rounded-full border border-neutral-100/10 bg-neutral-100/5 text-sm text-neutral-300 px-3 py-1 max-w-max mx-auto mb-4">
26
- 🚀 Powerful Features
27
- </div>
28
- <h2 className="text-4xl lg:text-6xl font-extrabold text-white font-mono mb-4">
29
- Everything you need
30
- </h2>
31
- <p className="text-lg lg:text-xl text-neutral-300/80 max-w-2xl mx-auto">
32
- Build, deploy, and scale your websites with cutting-edge features
33
- </p>
34
  </div>
35
-
36
- {/* Bento Grid */}
37
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
38
- {/* Multi Pages */}
39
- <div
40
- className="lg:row-span-2 relative p-8 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-105 hover:rotate-1 hover:-translate-y-2 hover:shadow-2xl hover:shadow-purple-500/20"
41
- style={{ transformStyle: "preserve-3d" }}
42
- >
43
- <div className="relative z-10">
44
- <div className="text-3xl lg:text-4xl mb-4">📄</div>
45
- <h3 className="text-2xl lg:text-3xl font-bold text-white font-mono mb-3">
46
- Multi Pages
47
- </h3>
48
- <p className="text-neutral-300/80 lg:text-lg mb-6">
49
- Create complex websites with multiple interconnected pages.
50
- Build everything from simple landing pages to full-featured web
51
- applications with dynamic routing and navigation.
52
- </p>
53
- <div className="flex flex-wrap gap-2">
54
- <span className="px-3 py-1 bg-purple-500/20 text-purple-300 rounded-full text-sm">
55
- Dynamic Routing
56
- </span>
57
- <span className="px-3 py-1 bg-blue-500/20 text-blue-300 rounded-full text-sm">
58
- Navigation
59
- </span>
60
- <span className="px-3 py-1 bg-green-500/20 text-green-300 rounded-full text-sm">
61
- SEO Ready
62
- </span>
63
- </div>
64
- </div>
65
- <div className="absolute -top-20 -right-20 w-40 h-40 bg-gradient-to-r from-purple-500 to-pink-500 opacity-20 blur-3xl rounded-full transition-all duration-700 ease-out group-hover:scale-[4] group-hover:opacity-30" />
66
- </div>
67
-
68
- {/* Auto Deploy */}
69
- <div
70
- className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-110 hover:-translate-y-4 hover:-rotate-3 hover:shadow-2xl hover:shadow-yellow-500/25"
71
- style={{ perspective: "1000px", transformStyle: "preserve-3d" }}
72
- >
73
- <div className="relative z-10">
74
- <div className="text-3xl mb-4">⚡</div>
75
- <h3 className="text-2xl font-bold text-white font-mono mb-3">
76
- Auto Deploy
77
- </h3>
78
- <p className="text-neutral-300/80 mb-4">
79
- Push your changes and watch them go live instantly. No complex
80
- CI/CD setup required.
81
- </p>
82
- </div>
83
- <div className="absolute -bottom-10 -right-10 w-32 h-32 bg-gradient-to-r from-yellow-500 to-orange-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
84
- </div>
85
-
86
- {/* Free Hosting */}
87
- <div className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-105 hover:rotate-2 hover:-translate-y-3 hover:shadow-xl hover:shadow-green-500/20">
88
- <div className="relative z-10">
89
- <div className="text-3xl mb-4">🌐</div>
90
- <h3 className="text-2xl font-bold text-white font-mono mb-3">
91
- Free Hosting
92
- </h3>
93
- <p className="text-neutral-300/80 mb-4">
94
- Host your websites for free with global CDN and lightning-fast
95
- performance.
96
- </p>
97
- </div>
98
- <div className="absolute -top-10 -left-10 w-32 h-32 bg-gradient-to-r from-green-500 to-emerald-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
99
- </div>
100
-
101
- {/* Open Source Models */}
102
- <div
103
- className="lg:col-span-2 md:col-span-2 relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-600 hover:scale-[1.02] hover:rotate-y-6 hover:-translate-y-1 hover:shadow-2xl hover:shadow-cyan-500/20"
104
- style={{ perspective: "1200px", transformStyle: "preserve-3d" }}
105
- >
106
- <div className="relative z-10">
107
- <div className="text-3xl mb-4">🔓</div>
108
- <h3 className="text-2xl font-bold text-white font-mono mb-3">
109
- Open Source Models
110
- </h3>
111
- <p className="text-neutral-300/80 mb-4">
112
- Powered by cutting-edge open source AI models. Transparent,
113
- customizable, and community-driven development.
114
- </p>
115
- <div className="flex flex-wrap gap-2">
116
- <span className="px-3 py-1 bg-cyan-500/20 text-cyan-300 rounded-full text-sm">
117
- Llama
118
- </span>
119
- <span className="px-3 py-1 bg-indigo-500/20 text-indigo-300 rounded-full text-sm">
120
- Mistral
121
- </span>
122
- <span className="px-3 py-1 bg-pink-500/20 text-pink-300 rounded-full text-sm">
123
- CodeLlama
124
- </span>
125
- </div>
126
- </div>
127
- <div className="absolute -bottom-10 right-10 w-32 h-32 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
128
- </div>
129
-
130
- {/* UX Focus */}
131
- <div
132
- className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-110 hover:rotate-3 hover:-translate-y-2 hover:rotate-x-6 hover:shadow-xl hover:shadow-rose-500/25"
133
- style={{ transformStyle: "preserve-3d" }}
134
- >
135
- <div className="relative z-10">
136
- <div className="text-3xl mb-4">✨</div>
137
- <h3 className="text-2xl font-bold text-white font-mono mb-3">
138
- Perfect UX
139
- </h3>
140
- <p className="text-neutral-300/80 mb-4">
141
- Intuitive interface designed for developers and non-developers
142
- alike.
143
- </p>
144
- </div>
145
- <div className="absolute -top-10 -right-10 w-32 h-32 bg-gradient-to-r from-rose-500 to-pink-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
146
- </div>
147
-
148
- {/* Hugging Face Integration */}
149
- <div
150
- className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-[1.08] hover:-rotate-2 hover:-translate-y-3 hover:rotate-y-8 hover:shadow-xl hover:shadow-amber-500/20"
151
- style={{ perspective: "800px" }}
152
- >
153
- <div className="relative z-10">
154
- <div className="text-3xl mb-4">🤗</div>
155
- <h3 className="text-2xl font-bold text-white font-mono mb-3">
156
- Hugging Face
157
- </h3>
158
- <p className="text-neutral-300/80 mb-4">
159
- Seamless integration with Hugging Face models and datasets for
160
- cutting-edge AI capabilities.
161
- </p>
162
- </div>
163
- <div className="absolute -top-10 -right-10 w-32 h-32 bg-gradient-to-r from-yellow-500 to-amber-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
164
- </div>
165
-
166
- {/* Performance */}
167
- <div
168
- className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-105 hover:rotate-1 hover:-translate-y-4 hover:rotate-x-8 hover:shadow-2xl hover:shadow-blue-500/25"
169
- style={{ transformStyle: "preserve-3d" }}
170
- >
171
- <div className="relative z-10">
172
- <div className="text-3xl mb-4">🚀</div>
173
- <h3 className="text-2xl font-bold text-white font-mono mb-3">
174
- Blazing Fast
175
- </h3>
176
- <p className="text-neutral-300/80 mb-4">
177
- Optimized performance with edge computing and smart caching.
178
- </p>
179
- </div>
180
- <div className="absolute -bottom-10 -right-10 w-32 h-32 bg-gradient-to-r from-blue-500 to-cyan-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
181
- </div>
182
  </div>
 
 
 
 
 
183
  </div>
184
-
185
- {/* Background Effects */}
186
- <div className="absolute inset-0 pointer-events-none -z-[1]">
187
- <div className="w-1/3 h-1/3 bg-gradient-to-r from-purple-500 to-pink-500 opacity-5 blur-3xl absolute top-20 left-10 rounded-full" />
188
- <div className="w-1/4 h-1/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-10 blur-3xl absolute bottom-20 right-20 rounded-full" />
189
- <div className="w-1/5 h-1/5 bg-gradient-to-r from-amber-500 to-rose-500 opacity-8 blur-3xl absolute top-1/2 left-1/3 rounded-full" />
 
 
 
190
  </div>
191
- </div>
192
  );
193
  }
 
1
+ import { AskAi } from "@/components/space/ask-ai";
2
  import { redirect } from "next/navigation";
 
 
 
3
  export default function Home() {
4
+ redirect("/projects/new");
5
  return (
6
+ <>
7
  <header className="container mx-auto pt-20 px-6 relative flex flex-col items-center justify-center text-center">
8
+ <div className="rounded-full border border-neutral-100/10 bg-neutral-100/5 text-xs text-neutral-300 px-3 py-1 max-w-max mx-auto mb-2">
9
+ ✨ DeepSite Public Beta
10
  </div>
11
+ <h1 className="text-8xl font-semibold text-white font-mono max-w-4xl">
12
  Code your website with AI in seconds
13
  </h1>
14
+ <p className="text-2xl text-neutral-300/80 mt-4 text-center max-w-2xl">
15
+ Vibe Coding has never been so easy.
16
+ </p>
17
+ <div className="mt-14 max-w-2xl w-full mx-auto">
18
+ <AskAi />
 
 
 
 
 
 
 
 
 
 
 
 
19
  </div>
20
+ <div className="absolute inset-0 pointer-events-none -z-[1]">
21
+ <div className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl rounded-full" />
22
+ <div className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10 transform rotate-12" />
23
+ <div className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10 rounded-3xl" />
24
+ <div className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3 rounded-lg transform -rotate-15" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  </div>
26
+ </header>
27
+ <div id="community" className="h-screen flex items-center justify-center">
28
+ <h1 className="text-7xl font-extrabold text-white font-mono">
29
+ Community Driven
30
+ </h1>
31
  </div>
32
+ <div id="deploy" className="h-screen flex items-center justify-center">
33
+ <h1 className="text-7xl font-extrabold text-white font-mono">
34
+ Deploy your website in seconds
35
+ </h1>
36
+ </div>
37
+ <div id="features" className="h-screen flex items-center justify-center">
38
+ <h1 className="text-7xl font-extrabold text-white font-mono">
39
+ Features that make you smile
40
+ </h1>
41
  </div>
42
+ </>
43
  );
44
  }
app/(public)/projects/page.tsx CHANGED
@@ -1,9 +1,13 @@
1
- import { getProjects } from "@/app/actions/projects";
 
2
  import { MyProjects } from "@/components/my-projects";
3
- import { NotLogged } from "@/components/not-logged/not-logged";
4
 
5
  export default async function ProjectsPage() {
6
- // const { ok, projects } = await getProjects();
 
 
 
7
 
8
- return <MyProjects />;
9
  }
 
1
+ import { redirect } from "next/navigation";
2
+
3
  import { MyProjects } from "@/components/my-projects";
4
+ import { getProjects } from "@/app/actions/projects";
5
 
6
  export default async function ProjectsPage() {
7
+ const { ok, projects } = await getProjects();
8
+ if (!ok) {
9
+ redirect("/");
10
+ }
11
 
12
+ return <MyProjects projects={projects} />;
13
  }
app/actions/projects.ts CHANGED
@@ -2,13 +2,13 @@
2
 
3
  import { isAuthenticated } from "@/lib/auth";
4
  import { NextResponse } from "next/server";
5
- import { listSpaces } from "@huggingface/hub";
6
- import { ProjectType } from "@/types";
 
7
 
8
  export async function getProjects(): Promise<{
9
  ok: boolean;
10
  projects: ProjectType[];
11
- isEmpty?: boolean;
12
  }> {
13
  const user = await isAuthenticated();
14
 
@@ -19,29 +19,45 @@ export async function getProjects(): Promise<{
19
  };
20
  }
21
 
22
- const projects = [];
23
- for await (const space of listSpaces({
24
- accessToken: user.token as string,
25
- additionalFields: ["author", "cardData"],
26
- search: {
27
- owner: user.name,
28
- }
29
- })) {
30
- if (
31
- !space.private &&
32
- space.sdk === "static" &&
33
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
34
- (
35
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
36
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
37
- )
38
- ) {
39
- projects.push(space);
40
- }
41
  }
42
-
43
  return {
44
  ok: true,
45
- projects,
46
  };
47
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import { isAuthenticated } from "@/lib/auth";
4
  import { NextResponse } from "next/server";
5
+ import dbConnect from "@/lib/mongodb";
6
+ import Project from "@/models/Project";
7
+ import { Project as ProjectType } from "@/types";
8
 
9
  export async function getProjects(): Promise<{
10
  ok: boolean;
11
  projects: ProjectType[];
 
12
  }> {
13
  const user = await isAuthenticated();
14
 
 
19
  };
20
  }
21
 
22
+ await dbConnect();
23
+ const projects = await Project.find({
24
+ user_id: user?.id,
25
+ })
26
+ .sort({ _createdAt: -1 })
27
+ .limit(100)
28
+ .lean();
29
+ if (!projects) {
30
+ return {
31
+ ok: false,
32
+ projects: [],
33
+ };
 
 
 
 
 
 
 
34
  }
 
35
  return {
36
  ok: true,
37
+ projects: JSON.parse(JSON.stringify(projects)) as ProjectType[],
38
  };
39
  }
40
+
41
+ export async function getProject(
42
+ namespace: string,
43
+ repoId: string
44
+ ): Promise<ProjectType | null> {
45
+ const user = await isAuthenticated();
46
+
47
+ if (user instanceof NextResponse || !user) {
48
+ return null;
49
+ }
50
+
51
+ await dbConnect();
52
+ const project = await Project.findOne({
53
+ user_id: user.id,
54
+ namespace,
55
+ repoId,
56
+ }).lean();
57
+
58
+ if (!project) {
59
+ return null;
60
+ }
61
+
62
+ return JSON.parse(JSON.stringify(project)) as ProjectType;
63
+ }
app/api/{ask → ask-ai}/route.ts RENAMED
@@ -4,27 +4,16 @@ import { NextResponse } from "next/server";
4
  import { headers } from "next/headers";
5
  import { InferenceClient } from "@huggingface/inference";
6
 
7
- import { MODELS } from "@/lib/providers";
8
  import {
9
  DIVIDER,
10
  FOLLOW_UP_SYSTEM_PROMPT,
11
  INITIAL_SYSTEM_PROMPT,
12
  MAX_REQUESTS_PER_IP,
13
- NEW_PAGE_END,
14
- NEW_PAGE_START,
15
  REPLACE_END,
16
  SEARCH_START,
17
- UPDATE_PAGE_START,
18
- UPDATE_PAGE_END,
19
- PROMPT_FOR_PROJECT_NAME,
20
  } from "@/lib/prompts";
21
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
22
- import { Page } from "@/types";
23
- import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
24
- import { isAuthenticated } from "@/lib/auth";
25
- import { getBestProvider } from "@/lib/best-provider";
26
- import { rewritePrompt } from "@/lib/rewrite-prompt";
27
- import { COLORS } from "@/lib/utils";
28
 
29
  const ipAddresses = new Map();
30
 
@@ -33,7 +22,7 @@ export async function POST(request: NextRequest) {
33
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
34
 
35
  const body = await request.json();
36
- const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
37
 
38
  if (!model || (!prompt && !redesignMarkdown)) {
39
  return NextResponse.json(
@@ -45,7 +34,6 @@ export async function POST(request: NextRequest) {
45
  const selectedModel = MODELS.find(
46
  (m) => m.value === model || m.label === model
47
  );
48
-
49
  if (!selectedModel) {
50
  return NextResponse.json(
51
  { ok: false, error: "Invalid model selected" },
@@ -97,19 +85,19 @@ export async function POST(request: NextRequest) {
97
  billTo = "huggingface";
98
  }
99
 
100
- const selectedProvider = await getBestProvider(selectedModel.value, provider)
101
-
102
- let rewrittenPrompt = prompt;
103
-
104
- if (enhancedSettings.isActive) {
105
- rewrittenPrompt = await rewritePrompt(prompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider);
106
- }
107
 
108
  try {
 
109
  const encoder = new TextEncoder();
110
  const stream = new TransformStream();
111
  const writer = stream.writable.getWriter();
112
 
 
113
  const response = new NextResponse(stream.readable, {
114
  headers: {
115
  "Content-Type": "text/plain; charset=utf-8",
@@ -119,13 +107,13 @@ export async function POST(request: NextRequest) {
119
  });
120
 
121
  (async () => {
122
- // let completeResponse = "";
123
  try {
124
  const client = new InferenceClient(token);
125
  const chatCompletion = client.chatCompletionStream(
126
  {
127
  model: selectedModel.value,
128
- provider: selectedProvider.provider,
129
  messages: [
130
  {
131
  role: "system",
@@ -133,28 +121,61 @@ export async function POST(request: NextRequest) {
133
  },
134
  {
135
  role: "user",
136
- content: `${rewrittenPrompt}${redesignMarkdown ? `\n\nHere is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : ""} : ""}`
 
 
 
 
137
  },
138
  ],
139
- max_tokens: 65_536,
140
  },
141
  billTo ? { billTo } : {}
142
  );
143
 
144
  while (true) {
145
- const { done, value } = await chatCompletion.next()
146
  if (done) {
147
  break;
148
  }
149
 
150
  const chunk = value.choices[0]?.delta?.content;
151
  if (chunk) {
152
- await writer.write(encoder.encode(chunk));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  }
154
  }
155
-
156
- // Explicitly close the writer after successful completion
157
- await writer.close();
158
  } catch (error: any) {
159
  if (error.message?.includes("exceeded your monthly included credits")) {
160
  await writer.write(
@@ -166,18 +187,7 @@ export async function POST(request: NextRequest) {
166
  })
167
  )
168
  );
169
- } else if (error?.message?.includes("inference provider information")) {
170
- await writer.write(
171
- encoder.encode(
172
- JSON.stringify({
173
- ok: false,
174
- openSelectProvider: true,
175
- message: error.message,
176
- })
177
- )
178
- );
179
- }
180
- else {
181
  await writer.write(
182
  encoder.encode(
183
  JSON.stringify({
@@ -190,12 +200,7 @@ export async function POST(request: NextRequest) {
190
  );
191
  }
192
  } finally {
193
- // Ensure the writer is always closed, even if already closed
194
- try {
195
- await writer?.close();
196
- } catch {
197
- // Ignore errors when closing the writer as it might already be closed
198
- }
199
  }
200
  })();
201
 
@@ -214,20 +219,14 @@ export async function POST(request: NextRequest) {
214
  }
215
 
216
  export async function PUT(request: NextRequest) {
217
- const user = await isAuthenticated();
218
- if (user instanceof NextResponse || !user) {
219
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
220
- }
221
-
222
  const authHeaders = await headers();
 
223
 
224
  const body = await request.json();
225
- const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew, enhancedSettings } =
226
  body;
227
 
228
- let repoId = repoIdFromBody;
229
-
230
- if (!prompt || pages.length === 0) {
231
  return NextResponse.json(
232
  { ok: false, error: "Missing required fields" },
233
  { status: 400 }
@@ -244,7 +243,7 @@ export async function PUT(request: NextRequest) {
244
  );
245
  }
246
 
247
- let token = user.token as string;
248
  let billTo: string | null = null;
249
 
250
  /**
@@ -279,56 +278,45 @@ export async function PUT(request: NextRequest) {
279
 
280
  const client = new InferenceClient(token);
281
 
282
- // Helper function to escape regex special characters
283
- const escapeRegExp = (string: string) => {
284
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
285
- };
286
-
287
- // Helper function to create flexible HTML regex that handles varying spaces
288
- const createFlexibleHtmlRegex = (searchBlock: string) => {
289
- let searchRegex = escapeRegExp(searchBlock)
290
- .replace(/\s+/g, '\\s*') // Allow any amount of whitespace where there are spaces
291
- .replace(/>\s*</g, '>\\s*<') // Allow spaces between HTML tags
292
- .replace(/\s*>/g, '\\s*>'); // Allow spaces before closing >
293
-
294
- return new RegExp(searchRegex, 'g');
295
- };
296
-
297
- const selectedProvider = await getBestProvider(selectedModel.value, provider)
298
 
299
  try {
300
  const response = await client.chatCompletion(
301
  {
302
  model: selectedModel.value,
303
- provider: selectedProvider.provider,
304
  messages: [
305
  {
306
  role: "system",
307
- content: FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : ""),
308
  },
309
  {
310
  role: "user",
311
- content: previousPrompts
312
- ? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
313
  : "You are modifying the HTML file based on the user's request.",
314
  },
315
  {
316
  role: "assistant",
317
 
318
- content: `${
319
  selectedElementHtml
320
- ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
321
  : ""
322
- }. Current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}. ${files?.length > 0 ? `Current images: ${files?.map((f: string) => `- ${f}`).join("\n")}.` : ""}`,
323
  },
324
  {
325
  role: "user",
326
  content: prompt,
327
  },
328
  ],
329
- ...(selectedProvider.provider !== "sambanova"
330
  ? {
331
- max_tokens: 65_536,
332
  }
333
  : {}),
334
  },
@@ -345,234 +333,61 @@ export async function PUT(request: NextRequest) {
345
 
346
  if (chunk) {
347
  const updatedLines: number[][] = [];
348
- let newHtml = "";
349
- const updatedPages = [...(pages || [])];
350
-
351
- const updatePageRegex = new RegExp(`${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
352
- let updatePageMatch;
353
-
354
- while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
355
- const [, pagePath, pageContent] = updatePageMatch;
356
-
357
- const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
358
- if (pageIndex !== -1) {
359
- let pageHtml = updatedPages[pageIndex].html;
360
-
361
- let processedContent = pageContent;
362
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
363
- if (htmlMatch) {
364
- processedContent = htmlMatch[1];
365
- }
366
- let position = 0;
367
- let moreBlocks = true;
368
-
369
- while (moreBlocks) {
370
- const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
371
- if (searchStartIndex === -1) {
372
- moreBlocks = false;
373
- continue;
374
- }
375
-
376
- const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
377
- if (dividerIndex === -1) {
378
- moreBlocks = false;
379
- continue;
380
- }
381
-
382
- const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
383
- if (replaceEndIndex === -1) {
384
- moreBlocks = false;
385
- continue;
386
- }
387
-
388
- const searchBlock = processedContent.substring(
389
- searchStartIndex + SEARCH_START.length,
390
- dividerIndex
391
- );
392
- const replaceBlock = processedContent.substring(
393
- dividerIndex + DIVIDER.length,
394
- replaceEndIndex
395
- );
396
-
397
- if (searchBlock.trim() === "") {
398
- pageHtml = `${replaceBlock}\n${pageHtml}`;
399
- updatedLines.push([1, replaceBlock.split("\n").length]);
400
- } else {
401
- const regex = createFlexibleHtmlRegex(searchBlock);
402
- const match = regex.exec(pageHtml);
403
-
404
- if (match) {
405
- const matchedText = match[0];
406
- const beforeText = pageHtml.substring(0, match.index);
407
- const startLineNumber = beforeText.split("\n").length;
408
- const replaceLines = replaceBlock.split("\n").length;
409
- const endLineNumber = startLineNumber + replaceLines - 1;
410
-
411
- updatedLines.push([startLineNumber, endLineNumber]);
412
- pageHtml = pageHtml.replace(matchedText, replaceBlock);
413
- }
414
- }
415
-
416
- position = replaceEndIndex + REPLACE_END.length;
417
- }
418
-
419
- updatedPages[pageIndex].html = pageHtml;
420
-
421
- if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
422
- newHtml = pageHtml;
423
- }
424
  }
425
- }
426
 
427
- const newPageRegex = new RegExp(`${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
428
- let newPageMatch;
429
-
430
- while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
431
- const [, pagePath, pageContent] = newPageMatch;
432
-
433
- let pageHtml = pageContent;
434
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
435
- if (htmlMatch) {
436
- pageHtml = htmlMatch[1];
437
- }
438
-
439
- const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
440
-
441
- if (existingPageIndex !== -1) {
442
- updatedPages[existingPageIndex] = {
443
- path: pagePath,
444
- html: pageHtml.trim()
445
- };
446
- } else {
447
- updatedPages.push({
448
- path: pagePath,
449
- html: pageHtml.trim()
450
- });
451
  }
452
- }
453
-
454
- if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
455
- let position = 0;
456
- let moreBlocks = true;
457
-
458
- while (moreBlocks) {
459
- const searchStartIndex = chunk.indexOf(SEARCH_START, position);
460
- if (searchStartIndex === -1) {
461
- moreBlocks = false;
462
- continue;
463
- }
464
 
465
- const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
466
- if (dividerIndex === -1) {
467
- moreBlocks = false;
468
- continue;
469
- }
470
-
471
- const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
472
- if (replaceEndIndex === -1) {
473
- moreBlocks = false;
474
- continue;
475
- }
476
 
477
- const searchBlock = chunk.substring(
478
- searchStartIndex + SEARCH_START.length,
479
- dividerIndex
480
- );
481
- const replaceBlock = chunk.substring(
482
- dividerIndex + DIVIDER.length,
483
- replaceEndIndex
484
- );
485
 
486
- if (searchBlock.trim() === "") {
487
- newHtml = `${replaceBlock}\n${newHtml}`;
488
- updatedLines.push([1, replaceBlock.split("\n").length]);
489
- } else {
490
- const regex = createFlexibleHtmlRegex(searchBlock);
491
- const match = regex.exec(newHtml);
492
-
493
- if (match) {
494
- const matchedText = match[0];
495
- const beforeText = newHtml.substring(0, match.index);
496
- const startLineNumber = beforeText.split("\n").length;
497
- const replaceLines = replaceBlock.split("\n").length;
498
- const endLineNumber = startLineNumber + replaceLines - 1;
499
-
500
- updatedLines.push([startLineNumber, endLineNumber]);
501
- newHtml = newHtml.replace(matchedText, replaceBlock);
502
- }
503
  }
504
-
505
- position = replaceEndIndex + REPLACE_END.length;
506
  }
507
 
508
- // Update the main HTML if it's the index page
509
- const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
510
- if (mainPageIndex !== -1) {
511
- updatedPages[mainPageIndex].html = newHtml;
512
- }
513
  }
514
 
515
- const files: File[] = [];
516
- updatedPages.forEach((page: Page) => {
517
- const file = new File([page.html], page.path, { type: "text/html" });
518
- files.push(file);
519
- });
520
-
521
- if (isNew) {
522
- const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
523
- const formattedTitle = projectName?.toLowerCase()
524
- .replace(/[^a-z0-9]+/g, "-")
525
- .split("-")
526
- .filter(Boolean)
527
- .join("-")
528
- .slice(0, 96);
529
- const repo: RepoDesignation = {
530
- type: "space",
531
- name: `${user.name}/${formattedTitle}`,
532
- };
533
- const { repoUrl} = await createRepo({
534
- repo,
535
- accessToken: user.token as string,
536
- });
537
- repoId = repoUrl.split("/").slice(-2).join("/");
538
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
539
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
540
- const README = `---
541
- title: ${projectName}
542
- colorFrom: ${colorFrom}
543
- colorTo: ${colorTo}
544
- emoji: 🐳
545
- sdk: static
546
- pinned: false
547
- tags:
548
- - deepsite-v3
549
- ---
550
-
551
- # Welcome to your new DeepSite project!
552
- This project was created with [DeepSite](https://deepsite.hf.co).
553
- `;
554
- files.push(new File([README], "README.md", { type: "text/markdown" }));
555
- }
556
-
557
- const response = await uploadFiles({
558
- repo: {
559
- type: "space",
560
- name: repoId,
561
- },
562
- files,
563
- commitTitle: prompt,
564
- accessToken: user.token as string,
565
- });
566
-
567
  return NextResponse.json({
568
  ok: true,
 
569
  updatedLines,
570
- pages: updatedPages,
571
- repoId,
572
- commit: {
573
- ...response.commit,
574
- title: prompt,
575
- }
576
  });
577
  } else {
578
  return NextResponse.json(
@@ -602,4 +417,3 @@ This project was created with [DeepSite](https://deepsite.hf.co).
602
  );
603
  }
604
  }
605
-
 
4
  import { headers } from "next/headers";
5
  import { InferenceClient } from "@huggingface/inference";
6
 
7
+ import { MODELS, PROVIDERS } from "@/lib/providers";
8
  import {
9
  DIVIDER,
10
  FOLLOW_UP_SYSTEM_PROMPT,
11
  INITIAL_SYSTEM_PROMPT,
12
  MAX_REQUESTS_PER_IP,
 
 
13
  REPLACE_END,
14
  SEARCH_START,
 
 
 
15
  } from "@/lib/prompts";
16
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
 
 
 
 
 
 
17
 
18
  const ipAddresses = new Map();
19
 
 
22
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
23
 
24
  const body = await request.json();
25
+ const { prompt, provider, model, redesignMarkdown, html } = body;
26
 
27
  if (!model || (!prompt && !redesignMarkdown)) {
28
  return NextResponse.json(
 
34
  const selectedModel = MODELS.find(
35
  (m) => m.value === model || m.label === model
36
  );
 
37
  if (!selectedModel) {
38
  return NextResponse.json(
39
  { ok: false, error: "Invalid model selected" },
 
85
  billTo = "huggingface";
86
  }
87
 
88
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
89
+ const selectedProvider =
90
+ provider === "auto"
91
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
92
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
 
 
93
 
94
  try {
95
+ // Create a stream response
96
  const encoder = new TextEncoder();
97
  const stream = new TransformStream();
98
  const writer = stream.writable.getWriter();
99
 
100
+ // Start the response
101
  const response = new NextResponse(stream.readable, {
102
  headers: {
103
  "Content-Type": "text/plain; charset=utf-8",
 
107
  });
108
 
109
  (async () => {
110
+ let completeResponse = "";
111
  try {
112
  const client = new InferenceClient(token);
113
  const chatCompletion = client.chatCompletionStream(
114
  {
115
  model: selectedModel.value,
116
+ provider: selectedProvider.id as any,
117
  messages: [
118
  {
119
  role: "system",
 
121
  },
122
  {
123
  role: "user",
124
+ content: redesignMarkdown
125
+ ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
126
+ : html
127
+ ? `Here is my current HTML code:\n\n\`\`\`html\n${html}\n\`\`\`\n\nNow, please create a new design based on this HTML.`
128
+ : prompt,
129
  },
130
  ],
131
+ max_tokens: selectedProvider.max_tokens,
132
  },
133
  billTo ? { billTo } : {}
134
  );
135
 
136
  while (true) {
137
+ const { done, value } = await chatCompletion.next();
138
  if (done) {
139
  break;
140
  }
141
 
142
  const chunk = value.choices[0]?.delta?.content;
143
  if (chunk) {
144
+ let newChunk = chunk;
145
+ if (!selectedModel?.isThinker) {
146
+ if (provider !== "sambanova") {
147
+ await writer.write(encoder.encode(chunk));
148
+ completeResponse += chunk;
149
+
150
+ if (completeResponse.includes("</html>")) {
151
+ break;
152
+ }
153
+ } else {
154
+ if (chunk.includes("</html>")) {
155
+ newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
156
+ }
157
+ completeResponse += newChunk;
158
+ await writer.write(encoder.encode(newChunk));
159
+ if (newChunk.includes("</html>")) {
160
+ break;
161
+ }
162
+ }
163
+ } else {
164
+ const lastThinkTagIndex =
165
+ completeResponse.lastIndexOf("</think>");
166
+ completeResponse += newChunk;
167
+ await writer.write(encoder.encode(newChunk));
168
+ if (lastThinkTagIndex !== -1) {
169
+ const afterLastThinkTag = completeResponse.slice(
170
+ lastThinkTagIndex + "</think>".length
171
+ );
172
+ if (afterLastThinkTag.includes("</html>")) {
173
+ break;
174
+ }
175
+ }
176
+ }
177
  }
178
  }
 
 
 
179
  } catch (error: any) {
180
  if (error.message?.includes("exceeded your monthly included credits")) {
181
  await writer.write(
 
187
  })
188
  )
189
  );
190
+ } else {
 
 
 
 
 
 
 
 
 
 
 
191
  await writer.write(
192
  encoder.encode(
193
  JSON.stringify({
 
200
  );
201
  }
202
  } finally {
203
+ await writer?.close();
 
 
 
 
 
204
  }
205
  })();
206
 
 
219
  }
220
 
221
  export async function PUT(request: NextRequest) {
 
 
 
 
 
222
  const authHeaders = await headers();
223
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
224
 
225
  const body = await request.json();
226
+ const { prompt, html, previousPrompt, provider, selectedElementHtml, model } =
227
  body;
228
 
229
+ if (!prompt || !html) {
 
 
230
  return NextResponse.json(
231
  { ok: false, error: "Missing required fields" },
232
  { status: 400 }
 
243
  );
244
  }
245
 
246
+ let token = userToken;
247
  let billTo: string | null = null;
248
 
249
  /**
 
278
 
279
  const client = new InferenceClient(token);
280
 
281
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
282
+ const selectedProvider =
283
+ provider === "auto"
284
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
285
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
 
 
 
 
 
 
 
 
 
 
 
286
 
287
  try {
288
  const response = await client.chatCompletion(
289
  {
290
  model: selectedModel.value,
291
+ provider: selectedProvider.id as any,
292
  messages: [
293
  {
294
  role: "system",
295
+ content: FOLLOW_UP_SYSTEM_PROMPT,
296
  },
297
  {
298
  role: "user",
299
+ content: previousPrompt
300
+ ? previousPrompt
301
  : "You are modifying the HTML file based on the user's request.",
302
  },
303
  {
304
  role: "assistant",
305
 
306
+ content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${
307
  selectedElementHtml
308
+ ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
309
  : ""
310
+ }`,
311
  },
312
  {
313
  role: "user",
314
  content: prompt,
315
  },
316
  ],
317
+ ...(selectedProvider.id !== "sambanova"
318
  ? {
319
+ max_tokens: selectedProvider.max_tokens,
320
  }
321
  : {}),
322
  },
 
333
 
334
  if (chunk) {
335
  const updatedLines: number[][] = [];
336
+ let newHtml = html;
337
+ let position = 0;
338
+ let moreBlocks = true;
339
+
340
+ while (moreBlocks) {
341
+ const searchStartIndex = chunk.indexOf(SEARCH_START, position);
342
+ if (searchStartIndex === -1) {
343
+ moreBlocks = false;
344
+ continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  }
 
346
 
347
+ const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
348
+ if (dividerIndex === -1) {
349
+ moreBlocks = false;
350
+ continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  }
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
+ const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
354
+ if (replaceEndIndex === -1) {
355
+ moreBlocks = false;
356
+ continue;
357
+ }
 
 
 
 
 
 
358
 
359
+ const searchBlock = chunk.substring(
360
+ searchStartIndex + SEARCH_START.length,
361
+ dividerIndex
362
+ );
363
+ const replaceBlock = chunk.substring(
364
+ dividerIndex + DIVIDER.length,
365
+ replaceEndIndex
366
+ );
367
 
368
+ if (searchBlock.trim() === "") {
369
+ newHtml = `${replaceBlock}\n${newHtml}`;
370
+ updatedLines.push([1, replaceBlock.split("\n").length]);
371
+ } else {
372
+ const blockPosition = newHtml.indexOf(searchBlock);
373
+ if (blockPosition !== -1) {
374
+ const beforeText = newHtml.substring(0, blockPosition);
375
+ const startLineNumber = beforeText.split("\n").length;
376
+ const replaceLines = replaceBlock.split("\n").length;
377
+ const endLineNumber = startLineNumber + replaceLines - 1;
378
+
379
+ updatedLines.push([startLineNumber, endLineNumber]);
380
+ newHtml = newHtml.replace(searchBlock, replaceBlock);
 
 
 
 
381
  }
 
 
382
  }
383
 
384
+ position = replaceEndIndex + REPLACE_END.length;
 
 
 
 
385
  }
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  return NextResponse.json({
388
  ok: true,
389
+ html: newHtml,
390
  updatedLines,
 
 
 
 
 
 
391
  });
392
  } else {
393
  return NextResponse.json(
 
417
  );
418
  }
419
  }
 
app/api/auth/login-url/route.ts DELETED
@@ -1,23 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
-
3
- export async function GET(req: NextRequest) {
4
- const host = req.headers.get("host") ?? "localhost:3000";
5
-
6
- let url: string;
7
- if (host.includes("localhost")) {
8
- url = host;
9
- } else if (host.includes("hf.space") || host.includes("/spaces/enzostvs")) {
10
- url = "enzostvs-deepsite.hf.space";
11
- } else {
12
- url = "deepsite.hf.co";
13
- }
14
-
15
- const redirect_uri =
16
- `${host.includes("localhost") ? "http://" : "https://"}` +
17
- url +
18
- "/auth/callback";
19
-
20
- const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
21
-
22
- return NextResponse.json({ loginUrl: loginRedirectUrl });
23
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/auth/logout/route.ts DELETED
@@ -1,25 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import MY_TOKEN_KEY from "@/lib/get-cookie-name";
3
-
4
- export async function POST() {
5
- const cookieName = MY_TOKEN_KEY();
6
- const isProduction = process.env.NODE_ENV === "production";
7
-
8
- const response = NextResponse.json(
9
- { message: "Logged out successfully" },
10
- { status: 200 }
11
- );
12
-
13
- // Clear the HTTP-only cookie
14
- const cookieOptions = [
15
- `${cookieName}=`,
16
- "Max-Age=0",
17
- "Path=/",
18
- "HttpOnly",
19
- ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
20
- ].join("; ");
21
-
22
- response.headers.set("Set-Cookie", cookieOptions);
23
-
24
- return response;
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/auth/route.ts CHANGED
@@ -1,5 +1,4 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import MY_TOKEN_KEY from "@/lib/get-cookie-name";
3
 
4
  export async function POST(req: NextRequest) {
5
  const body = await req.json();
@@ -71,17 +70,11 @@ export async function POST(req: NextRequest) {
71
  }
72
  const user = await userResponse.json();
73
 
74
- const cookieName = MY_TOKEN_KEY();
75
- const isProduction = process.env.NODE_ENV === "production";
76
-
77
- // Create response with user data
78
- const nextResponse = NextResponse.json(
79
  {
80
  access_token: response.access_token,
81
  expires_in: response.expires_in,
82
  user,
83
- // Include fallback flag for iframe contexts
84
- useLocalStorageFallback: true,
85
  },
86
  {
87
  status: 200,
@@ -90,17 +83,4 @@ export async function POST(req: NextRequest) {
90
  },
91
  }
92
  );
93
-
94
- // Set HTTP-only cookie with proper attributes for iframe support
95
- const cookieOptions = [
96
- `${cookieName}=${response.access_token}`,
97
- `Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided
98
- "Path=/",
99
- "HttpOnly",
100
- ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
101
- ].join("; ");
102
-
103
- nextResponse.headers.set("Set-Cookie", cookieOptions);
104
-
105
- return nextResponse;
106
  }
 
1
  import { NextRequest, NextResponse } from "next/server";
 
2
 
3
  export async function POST(req: NextRequest) {
4
  const body = await req.json();
 
70
  }
71
  const user = await userResponse.json();
72
 
73
+ return NextResponse.json(
 
 
 
 
74
  {
75
  access_token: response.access_token,
76
  expires_in: response.expires_in,
77
  user,
 
 
78
  },
79
  {
80
  status: 200,
 
83
  },
84
  }
85
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts DELETED
@@ -1,190 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import { Page } from "@/types";
6
-
7
- export async function POST(
8
- req: NextRequest,
9
- { params }: {
10
- params: Promise<{
11
- namespace: string;
12
- repoId: string;
13
- commitId: string;
14
- }>
15
- }
16
- ) {
17
- const user = await isAuthenticated();
18
-
19
- if (user instanceof NextResponse || !user) {
20
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
21
- }
22
-
23
- const param = await params;
24
- const { namespace, repoId, commitId } = param;
25
-
26
- try {
27
- const repo: RepoDesignation = {
28
- type: "space",
29
- name: `${namespace}/${repoId}`,
30
- };
31
-
32
- const space = await spaceInfo({
33
- name: `${namespace}/${repoId}`,
34
- accessToken: user.token as string,
35
- additionalFields: ["author"],
36
- });
37
-
38
- if (!space || space.sdk !== "static") {
39
- return NextResponse.json(
40
- { ok: false, error: "Space is not a static space." },
41
- { status: 404 }
42
- );
43
- }
44
-
45
- if (space.author !== user.name) {
46
- return NextResponse.json(
47
- { ok: false, error: "Space does not belong to the authenticated user." },
48
- { status: 403 }
49
- );
50
- }
51
-
52
- // Fetch files from the specific commit
53
- const files: File[] = [];
54
- const pages: Page[] = [];
55
- const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
56
- const commitFilePaths: Set<string> = new Set();
57
-
58
- // Get all files from the specific commit
59
- for await (const fileInfo of listFiles({
60
- repo,
61
- accessToken: user.token as string,
62
- revision: commitId,
63
- })) {
64
- const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
65
-
66
- if (allowedExtensions.includes(fileExtension || "")) {
67
- commitFilePaths.add(fileInfo.path);
68
-
69
- // Fetch the file content from the specific commit
70
- const response = await fetch(
71
- `https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
72
- );
73
-
74
- if (response.ok) {
75
- const content = await response.text();
76
- let mimeType = "text/plain";
77
-
78
- switch (fileExtension) {
79
- case "html":
80
- mimeType = "text/html";
81
- // Add HTML files to pages array for client-side setPages
82
- pages.push({
83
- path: fileInfo.path,
84
- html: content,
85
- });
86
- break;
87
- case "css":
88
- mimeType = "text/css";
89
- break;
90
- case "js":
91
- mimeType = "application/javascript";
92
- break;
93
- case "json":
94
- mimeType = "application/json";
95
- break;
96
- case "md":
97
- mimeType = "text/markdown";
98
- break;
99
- }
100
-
101
- const file = new File([content], fileInfo.path, { type: mimeType });
102
- files.push(file);
103
- }
104
- }
105
- }
106
-
107
- // Get files currently in main branch to identify files to delete
108
- const mainBranchFilePaths: Set<string> = new Set();
109
- for await (const fileInfo of listFiles({
110
- repo,
111
- accessToken: user.token as string,
112
- revision: "main",
113
- })) {
114
- const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
115
-
116
- if (allowedExtensions.includes(fileExtension || "")) {
117
- mainBranchFilePaths.add(fileInfo.path);
118
- }
119
- }
120
-
121
- // Identify files to delete (exist in main but not in commit)
122
- const filesToDelete: string[] = [];
123
- for (const mainFilePath of mainBranchFilePaths) {
124
- if (!commitFilePaths.has(mainFilePath)) {
125
- filesToDelete.push(mainFilePath);
126
- }
127
- }
128
-
129
- if (files.length === 0 && filesToDelete.length === 0) {
130
- return NextResponse.json(
131
- { ok: false, error: "No files found in the specified commit and no files to delete" },
132
- { status: 404 }
133
- );
134
- }
135
-
136
- // Delete files that exist in main but not in the commit being promoted
137
- if (filesToDelete.length > 0) {
138
- await deleteFiles({
139
- repo,
140
- paths: filesToDelete,
141
- accessToken: user.token as string,
142
- commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
143
- commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
144
- });
145
- }
146
-
147
- // Upload the files to the main branch with a promotion commit message
148
- if (files.length > 0) {
149
- await uploadFiles({
150
- repo,
151
- files,
152
- accessToken: user.token as string,
153
- commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
154
- commitDescription: `Promoted commit ${commitId} to main branch`,
155
- });
156
- }
157
-
158
- return NextResponse.json(
159
- {
160
- ok: true,
161
- message: "Version promoted successfully",
162
- promotedCommit: commitId,
163
- pages: pages,
164
- },
165
- { status: 200 }
166
- );
167
-
168
- } catch (error: any) {
169
-
170
- // Handle specific HuggingFace API errors
171
- if (error.statusCode === 404) {
172
- return NextResponse.json(
173
- { ok: false, error: "Commit not found" },
174
- { status: 404 }
175
- );
176
- }
177
-
178
- if (error.statusCode === 403) {
179
- return NextResponse.json(
180
- { ok: false, error: "Access denied to repository" },
181
- { status: 403 }
182
- );
183
- }
184
-
185
- return NextResponse.json(
186
- { ok: false, error: error.message || "Failed to promote version" },
187
- { status: 500 }
188
- );
189
- }
190
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/images/route.ts DELETED
@@ -1,113 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import Project from "@/models/Project";
6
- import dbConnect from "@/lib/mongodb";
7
-
8
- export async function POST(
9
- req: NextRequest,
10
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
11
- ) {
12
- try {
13
- const user = await isAuthenticated();
14
-
15
- if (user instanceof NextResponse || !user) {
16
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
17
- }
18
-
19
- const param = await params;
20
- const { namespace, repoId } = param;
21
-
22
- const space = await spaceInfo({
23
- name: `${namespace}/${repoId}`,
24
- accessToken: user.token as string,
25
- additionalFields: ["author"],
26
- });
27
-
28
- if (!space || space.sdk !== "static") {
29
- return NextResponse.json(
30
- { ok: false, error: "Space is not a static space." },
31
- { status: 404 }
32
- );
33
- }
34
-
35
- if (space.author !== user.name) {
36
- return NextResponse.json(
37
- { ok: false, error: "Space does not belong to the authenticated user." },
38
- { status: 403 }
39
- );
40
- }
41
-
42
- // Parse the FormData to get the images
43
- const formData = await req.formData();
44
- const imageFiles = formData.getAll("images") as File[];
45
-
46
- if (!imageFiles || imageFiles.length === 0) {
47
- return NextResponse.json(
48
- {
49
- ok: false,
50
- error: "At least one image file is required under the 'images' key",
51
- },
52
- { status: 400 }
53
- );
54
- }
55
-
56
- const files: File[] = [];
57
- for (const file of imageFiles) {
58
- if (!(file instanceof File)) {
59
- return NextResponse.json(
60
- {
61
- ok: false,
62
- error: "Invalid file format - all items under 'images' key must be files",
63
- },
64
- { status: 400 }
65
- );
66
- }
67
-
68
- if (!file.type.startsWith('image/')) {
69
- return NextResponse.json(
70
- {
71
- ok: false,
72
- error: `File ${file.name} is not an image`,
73
- },
74
- { status: 400 }
75
- );
76
- }
77
-
78
- // Create File object with images/ folder prefix
79
- const fileName = `images/${file.name}`;
80
- const processedFile = new File([file], fileName, { type: file.type });
81
- files.push(processedFile);
82
- }
83
-
84
- // Upload files to HuggingFace space
85
- const repo: RepoDesignation = {
86
- type: "space",
87
- name: `${namespace}/${repoId}`,
88
- };
89
-
90
- await uploadFiles({
91
- repo,
92
- files,
93
- accessToken: user.token as string,
94
- commitTitle: `Upload ${files.length} image(s)`,
95
- });
96
-
97
- return NextResponse.json({
98
- ok: true,
99
- message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
100
- uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
101
- }, { status: 200 });
102
-
103
- } catch (error) {
104
- console.error('Error uploading images:', error);
105
- return NextResponse.json(
106
- {
107
- ok: false,
108
- error: "Failed to upload images",
109
- },
110
- { status: 500 }
111
- );
112
- }
113
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/route.ts CHANGED
@@ -1,10 +1,12 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, listFiles, deleteRepo, listCommits, downloadFile } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
- import { Commit, Page } from "@/types";
 
 
6
 
7
- export async function DELETE(
8
  req: NextRequest,
9
  { params }: { params: Promise<{ namespace: string; repoId: string }> }
10
  ) {
@@ -14,63 +16,24 @@ export async function DELETE(
14
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
15
  }
16
 
 
17
  const param = await params;
18
  const { namespace, repoId } = param;
19
 
20
- try {
21
- const space = await spaceInfo({
22
- name: `${namespace}/${repoId}`,
23
- accessToken: user.token as string,
24
- additionalFields: ["author"],
25
- });
26
-
27
- if (!space || space.sdk !== "static") {
28
- return NextResponse.json(
29
- { ok: false, error: "Space is not a static space." },
30
- { status: 404 }
31
- );
32
- }
33
-
34
- if (space.author !== user.name) {
35
- return NextResponse.json(
36
- { ok: false, error: "Space does not belong to the authenticated user." },
37
- { status: 403 }
38
- );
39
- }
40
-
41
- const repo: RepoDesignation = {
42
- type: "space",
43
- name: `${namespace}/${repoId}`,
44
- };
45
-
46
- await deleteRepo({
47
- repo,
48
- accessToken: user.token as string,
49
- });
50
-
51
-
52
- return NextResponse.json({ ok: true }, { status: 200 });
53
- } catch (error: any) {
54
  return NextResponse.json(
55
- { ok: false, error: error.message },
56
- { status: 500 }
 
 
 
57
  );
58
  }
59
- }
60
-
61
- export async function GET(
62
- req: NextRequest,
63
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
64
- ) {
65
- const user = await isAuthenticated();
66
-
67
- if (user instanceof NextResponse || !user) {
68
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
69
- }
70
-
71
- const param = await params;
72
- const { namespace, repoId } = param;
73
-
74
  try {
75
  const space = await spaceInfo({
76
  name: namespace + "/" + repoId,
@@ -97,75 +60,26 @@ export async function GET(
97
  );
98
  }
99
 
100
- const repo: RepoDesignation = {
101
- type: "space",
102
- name: `${namespace}/${repoId}`,
103
- };
104
-
105
- const htmlFiles: Page[] = [];
106
- const files: string[] = [];
107
-
108
- const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
109
-
110
- for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
111
- if (fileInfo.path.endsWith(".html")) {
112
- const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
113
- const html = await blob?.text();
114
- if (!html) {
115
- continue;
116
- }
117
- if (fileInfo.path === "index.html") {
118
- htmlFiles.unshift({
119
- path: fileInfo.path,
120
- html,
121
- });
122
- } else {
123
- htmlFiles.push({
124
- path: fileInfo.path,
125
- html,
126
- });
127
- }
128
- }
129
- if (fileInfo.type === "directory" && fileInfo.path === "images") {
130
- for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
131
- if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
132
- files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
133
- }
134
- }
135
- }
136
- }
137
- const commits: Commit[] = [];
138
- for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
139
- if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
140
- continue;
141
- }
142
- commits.push({
143
- title: commit.title,
144
- oid: commit.oid,
145
- date: commit.date,
146
- });
147
- }
148
-
149
- if (htmlFiles.length === 0) {
150
  return NextResponse.json(
151
  {
152
  ok: false,
153
- error: "No HTML files found",
154
  },
155
  { status: 404 }
156
  );
157
  }
 
 
 
 
158
  return NextResponse.json(
159
  {
160
  project: {
161
- id: space.id,
162
- space_id: space.name,
163
- private: space.private,
164
- _updatedAt: space.updatedAt,
165
  },
166
- pages: htmlFiles,
167
- files,
168
- commits,
169
  ok: true,
170
  },
171
  { status: 200 }
@@ -174,6 +88,10 @@ export async function GET(
174
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
  } catch (error: any) {
176
  if (error.statusCode === 404) {
 
 
 
 
177
  return NextResponse.json(
178
  { error: "Space not found", ok: false },
179
  { status: 404 }
@@ -185,3 +103,135 @@ export async function GET(
185
  );
186
  }
187
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, spaceInfo, uploadFile } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
+ import Project from "@/models/Project";
6
+ import dbConnect from "@/lib/mongodb";
7
+ import { getPTag } from "@/lib/utils";
8
 
9
+ export async function GET(
10
  req: NextRequest,
11
  { params }: { params: Promise<{ namespace: string; repoId: string }> }
12
  ) {
 
16
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
17
  }
18
 
19
+ await dbConnect();
20
  const param = await params;
21
  const { namespace, repoId } = param;
22
 
23
+ const project = await Project.findOne({
24
+ user_id: user.id,
25
+ space_id: `${namespace}/${repoId}`,
26
+ }).lean();
27
+ if (!project) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  return NextResponse.json(
29
+ {
30
+ ok: false,
31
+ error: "Project not found",
32
+ },
33
+ { status: 404 }
34
  );
35
  }
36
+ const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  try {
38
  const space = await spaceInfo({
39
  name: namespace + "/" + repoId,
 
60
  );
61
  }
62
 
63
+ const response = await fetch(space_url);
64
+ if (!response.ok) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  return NextResponse.json(
66
  {
67
  ok: false,
68
+ error: "Failed to fetch space HTML",
69
  },
70
  { status: 404 }
71
  );
72
  }
73
+ let html = await response.text();
74
+ // remove the last p tag including this url https://enzostvs-deepsite.hf.space
75
+ html = html.replace(getPTag(namespace + "/" + repoId), "");
76
+
77
  return NextResponse.json(
78
  {
79
  project: {
80
+ ...project,
81
+ html,
 
 
82
  },
 
 
 
83
  ok: true,
84
  },
85
  { status: 200 }
 
88
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
  } catch (error: any) {
90
  if (error.statusCode === 404) {
91
+ await Project.deleteOne({
92
+ user_id: user.id,
93
+ space_id: `${namespace}/${repoId}`,
94
+ });
95
  return NextResponse.json(
96
  { error: "Space not found", ok: false },
97
  { status: 404 }
 
103
  );
104
  }
105
  }
106
+
107
+ export async function PUT(
108
+ req: NextRequest,
109
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
110
+ ) {
111
+ const user = await isAuthenticated();
112
+
113
+ if (user instanceof NextResponse || !user) {
114
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
115
+ }
116
+
117
+ await dbConnect();
118
+ const param = await params;
119
+ const { namespace, repoId } = param;
120
+ const { html, prompts } = await req.json();
121
+
122
+ const project = await Project.findOne({
123
+ user_id: user.id,
124
+ space_id: `${namespace}/${repoId}`,
125
+ }).lean();
126
+ if (!project) {
127
+ return NextResponse.json(
128
+ {
129
+ ok: false,
130
+ error: "Project not found",
131
+ },
132
+ { status: 404 }
133
+ );
134
+ }
135
+
136
+ const repo: RepoDesignation = {
137
+ type: "space",
138
+ name: `${namespace}/${repoId}`,
139
+ };
140
+
141
+ const newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
142
+ const file = new File([newHtml], "index.html", { type: "text/html" });
143
+ await uploadFile({
144
+ repo,
145
+ file,
146
+ accessToken: user.token as string,
147
+ commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
148
+ });
149
+
150
+ await Project.updateOne(
151
+ { user_id: user.id, space_id: `${namespace}/${repoId}` },
152
+ {
153
+ $set: {
154
+ prompts: [
155
+ ...(project && "prompts" in project ? project.prompts : []),
156
+ ...prompts,
157
+ ],
158
+ },
159
+ }
160
+ );
161
+ return NextResponse.json({ ok: true }, { status: 200 });
162
+ }
163
+
164
+ export async function POST(
165
+ req: NextRequest,
166
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
167
+ ) {
168
+ const user = await isAuthenticated();
169
+
170
+ if (user instanceof NextResponse || !user) {
171
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
172
+ }
173
+
174
+ await dbConnect();
175
+ const param = await params;
176
+ const { namespace, repoId } = param;
177
+
178
+ const space = await spaceInfo({
179
+ name: namespace + "/" + repoId,
180
+ accessToken: user.token as string,
181
+ additionalFields: ["author"],
182
+ });
183
+
184
+ if (!space || space.sdk !== "static") {
185
+ return NextResponse.json(
186
+ {
187
+ ok: false,
188
+ error: "Space is not a static space",
189
+ },
190
+ { status: 404 }
191
+ );
192
+ }
193
+ if (space.author !== user.name) {
194
+ return NextResponse.json(
195
+ {
196
+ ok: false,
197
+ error: "Space does not belong to the authenticated user",
198
+ },
199
+ { status: 403 }
200
+ );
201
+ }
202
+
203
+ const project = await Project.findOne({
204
+ user_id: user.id,
205
+ space_id: `${namespace}/${repoId}`,
206
+ }).lean();
207
+ if (project) {
208
+ // redirect to the project page if it already exists
209
+ return NextResponse.json(
210
+ {
211
+ ok: false,
212
+ error: "Project already exists",
213
+ redirect: `/projects/${namespace}/${repoId}`,
214
+ },
215
+ { status: 400 }
216
+ );
217
+ }
218
+
219
+ const newProject = new Project({
220
+ user_id: user.id,
221
+ space_id: `${namespace}/${repoId}`,
222
+ prompts: [],
223
+ });
224
+
225
+ await newProject.save();
226
+ return NextResponse.json(
227
+ {
228
+ ok: true,
229
+ project: {
230
+ id: newProject._id,
231
+ space_id: newProject.space_id,
232
+ prompts: newProject.prompts,
233
+ },
234
+ },
235
+ { status: 201 }
236
+ );
237
+ }
app/api/me/projects/[namespace]/[repoId]/save/route.ts DELETED
@@ -1,64 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { uploadFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import { Page } from "@/types";
6
-
7
- export async function PUT(
8
- req: NextRequest,
9
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
10
- ) {
11
- const user = await isAuthenticated();
12
- if (user instanceof NextResponse || !user) {
13
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
14
- }
15
-
16
- const param = await params;
17
- const { namespace, repoId } = param;
18
- const { pages, commitTitle = "Manual changes saved" } = await req.json();
19
-
20
- if (!pages || !Array.isArray(pages) || pages.length === 0) {
21
- return NextResponse.json(
22
- { ok: false, error: "Pages are required" },
23
- { status: 400 }
24
- );
25
- }
26
-
27
- try {
28
- // Prepare files for upload
29
- const files: File[] = [];
30
- pages.forEach((page: Page) => {
31
- const file = new File([page.html], page.path, { type: "text/html" });
32
- files.push(file);
33
- });
34
-
35
- // Upload files to HuggingFace Hub
36
- const response = await uploadFiles({
37
- repo: {
38
- type: "space",
39
- name: `${namespace}/${repoId}`,
40
- },
41
- files,
42
- commitTitle,
43
- accessToken: user.token as string,
44
- });
45
-
46
- return NextResponse.json({
47
- ok: true,
48
- pages,
49
- commit: {
50
- ...response.commit,
51
- title: commitTitle,
52
- }
53
- });
54
- } catch (error: any) {
55
- console.error("Error saving manual changes:", error);
56
- return NextResponse.json(
57
- {
58
- ok: false,
59
- error: error.message || "Failed to save changes",
60
- },
61
- { status: 500 }
62
- );
63
- }
64
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/route.ts CHANGED
@@ -1,107 +1,126 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
- import { Commit, Page } from "@/types";
6
- import { COLORS } from "@/lib/utils";
 
 
 
 
 
 
 
 
7
 
8
- export async function POST(
9
- req: NextRequest,
10
- ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  const user = await isAuthenticated();
 
12
  if (user instanceof NextResponse || !user) {
13
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
14
  }
15
 
16
- const { title: titleFromRequest, pages, prompt } = await req.json();
 
 
 
 
 
 
 
 
 
17
 
18
- const title = titleFromRequest ?? "DeepSite Project";
 
 
19
 
20
- const formattedTitle = title
21
- .toLowerCase()
22
- .replace(/[^a-z0-9]+/g, "-")
23
- .split("-")
24
- .filter(Boolean)
25
- .join("-")
26
- .slice(0, 96);
27
 
28
- const repo: RepoDesignation = {
29
- type: "space",
30
- name: `${user.name}/${formattedTitle}`,
31
- };
32
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
33
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
34
- const README = `---
35
- title: ${title}
 
 
 
 
 
 
36
  colorFrom: ${colorFrom}
37
  colorTo: ${colorTo}
38
- emoji: 🐳
39
  sdk: static
40
  pinned: false
41
  tags:
42
- - deepsite-v3
43
  ---
44
 
45
- # Welcome to your new DeepSite project!
46
- This project was created with [DeepSite](https://deepsite.hf.co).
47
- `;
48
 
49
- const files: File[] = [];
50
- const readmeFile = new File([README], "README.md", { type: "text/markdown" });
51
- files.push(readmeFile);
52
- pages.forEach((page: Page) => {
53
- const file = new File([page.html], page.path, { type: "text/html" });
54
- files.push(file);
55
- });
56
-
57
- try {
58
- const { repoUrl} = await createRepo({
59
- repo,
60
- accessToken: user.token as string,
61
  });
62
- const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
63
  await uploadFiles({
64
  repo,
65
  files,
66
  accessToken: user.token as string,
67
- commitTitle
68
  });
69
-
70
  const path = repoUrl.split("/").slice(-2).join("/");
71
-
72
- const commits: Commit[] = [];
73
- for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
74
- if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
75
- continue;
76
- }
77
- commits.push({
78
- title: commit.title,
79
- oid: commit.oid,
80
- date: commit.date,
81
- });
82
- }
83
-
84
- const space = await spaceInfo({
85
- name: repo.name,
86
- accessToken: user.token as string,
87
  });
88
-
89
- let newProject = {
90
- files,
91
- pages,
92
- commits,
93
- project: {
94
- id: space.id,
95
- space_id: space.name,
96
- _updatedAt: space.updatedAt,
97
- }
98
- }
99
-
100
- return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
101
  } catch (err: any) {
102
  return NextResponse.json(
103
  { error: err.message, ok: false },
104
  { status: 500 }
105
  );
106
  }
107
- }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
+ import Project from "@/models/Project";
6
+ import dbConnect from "@/lib/mongodb";
7
+ import { COLORS, getPTag } from "@/lib/utils";
8
+ // import type user
9
+ export async function GET() {
10
+ const user = await isAuthenticated();
11
+
12
+ if (user instanceof NextResponse || !user) {
13
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
14
+ }
15
 
16
+ await dbConnect();
17
+
18
+ const projects = await Project.find({
19
+ user_id: user?.id,
20
+ })
21
+ .sort({ _createdAt: -1 })
22
+ .limit(100)
23
+ .lean();
24
+ if (!projects) {
25
+ return NextResponse.json(
26
+ {
27
+ ok: false,
28
+ projects: [],
29
+ },
30
+ { status: 404 }
31
+ );
32
+ }
33
+ return NextResponse.json(
34
+ {
35
+ ok: true,
36
+ projects,
37
+ },
38
+ { status: 200 }
39
+ );
40
+ }
41
+
42
+ /**
43
+ * This API route creates a new project in Hugging Face Spaces.
44
+ * It requires an Authorization header with a valid token and a JSON body with the project details.
45
+ */
46
+ export async function POST(request: NextRequest) {
47
  const user = await isAuthenticated();
48
+
49
  if (user instanceof NextResponse || !user) {
50
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
51
  }
52
 
53
+ const { title, html, prompts } = await request.json();
54
+
55
+ if (!title || !html) {
56
+ return NextResponse.json(
57
+ { message: "Title and HTML content are required.", ok: false },
58
+ { status: 400 }
59
+ );
60
+ }
61
+
62
+ await dbConnect();
63
 
64
+ try {
65
+ let readme = "";
66
+ let newHtml = html;
67
 
68
+ const newTitle = title
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, "-")
71
+ .split("-")
72
+ .filter(Boolean)
73
+ .join("-")
74
+ .slice(0, 96);
75
 
76
+ const repo: RepoDesignation = {
77
+ type: "space",
78
+ name: `${user.name}/${newTitle}`,
79
+ };
80
+
81
+ const { repoUrl } = await createRepo({
82
+ repo,
83
+ accessToken: user.token as string,
84
+ });
85
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
86
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
87
+ readme = `---
88
+ title: ${newTitle}
89
+ emoji: 🐳
90
  colorFrom: ${colorFrom}
91
  colorTo: ${colorTo}
 
92
  sdk: static
93
  pinned: false
94
  tags:
95
+ - deepsite
96
  ---
97
 
98
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
 
 
99
 
100
+ newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
101
+ const file = new File([newHtml], "index.html", { type: "text/html" });
102
+ const readmeFile = new File([readme], "README.md", {
103
+ type: "text/markdown",
 
 
 
 
 
 
 
 
104
  });
105
+ const files = [file, readmeFile];
106
  await uploadFiles({
107
  repo,
108
  files,
109
  accessToken: user.token as string,
110
+ commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
111
  });
 
112
  const path = repoUrl.split("/").slice(-2).join("/");
113
+ const project = await Project.create({
114
+ user_id: user.id,
115
+ space_id: path,
116
+ prompts,
 
 
 
 
 
 
 
 
 
 
 
 
117
  });
118
+ return NextResponse.json({ project, path, ok: true }, { status: 201 });
119
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
 
 
 
 
 
 
 
 
 
 
 
120
  } catch (err: any) {
121
  return NextResponse.json(
122
  { error: err.message, ok: false },
123
  { status: 500 }
124
  );
125
  }
126
+ }
app/api/me/route.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { listSpaces } from "@huggingface/hub";
2
  import { headers } from "next/headers";
3
  import { NextResponse } from "next/server";
4
 
@@ -22,25 +21,5 @@ export async function GET() {
22
  );
23
  }
24
  const user = await userResponse.json();
25
- const projects = [];
26
- for await (const space of listSpaces({
27
- accessToken: token.replace("Bearer ", "") as string,
28
- additionalFields: ["author", "cardData"],
29
- search: {
30
- owner: user.name,
31
- }
32
- })) {
33
- if (
34
- space.sdk === "static" &&
35
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
36
- (
37
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
38
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
39
- )
40
- ) {
41
- projects.push(space);
42
- }
43
- }
44
-
45
- return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
46
  }
 
 
1
  import { headers } from "next/headers";
2
  import { NextResponse } from "next/server";
3
 
 
21
  );
22
  }
23
  const user = await userResponse.json();
24
+ return NextResponse.json({ user, errCode: null }, { status: 200 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
app/auth/callback/page.tsx CHANGED
@@ -5,92 +5,67 @@ import { use, useState } from "react";
5
  import { useMount, useTimeoutFn } from "react-use";
6
 
7
  import { Button } from "@/components/ui/button";
8
- import { AnimatedBlobs } from "@/components/animated-blobs";
9
- import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
10
  export default function AuthCallback({
11
  searchParams,
12
  }: {
13
  searchParams: Promise<{ code: string }>;
14
  }) {
15
  const [showButton, setShowButton] = useState(false);
16
- const [isPopupAuth, setIsPopupAuth] = useState(false);
17
  const { code } = use(searchParams);
18
  const { loginFromCode } = useUser();
19
- const { postMessage } = useBroadcastChannel("auth", () => {});
20
 
21
  useMount(async () => {
22
  if (code) {
23
- const isPopup = window.opener || window.parent !== window;
24
- setIsPopupAuth(isPopup);
25
-
26
- if (isPopup) {
27
- postMessage({
28
- type: "user-oauth",
29
- code: code,
30
- });
31
-
32
- setTimeout(() => {
33
- if (window.opener) {
34
- window.close();
35
- }
36
- }, 1000);
37
- } else {
38
- await loginFromCode(code);
39
- }
40
  }
41
  });
42
 
43
- useTimeoutFn(() => setShowButton(true), 7000);
 
 
 
44
 
45
  return (
46
- <div className="h-screen flex flex-col justify-center items-center bg-neutral-950 z-1 relative">
47
- <div className="background__noisy" />
48
- <div className="relative max-w-4xl py-10 flex items-center justify-center w-full">
49
- <div className="max-w-lg mx-auto !rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
50
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
51
- <div className="flex items-center justify-center -space-x-4 mb-3">
52
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
53
- 🚀
54
- </div>
55
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
56
- 👋
57
- </div>
58
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
59
- 🙌
60
- </div>
61
  </div>
62
- <p className="text-xl font-semibold text-neutral-950">
63
- {isPopupAuth
64
- ? "Authentication Complete!"
65
- : "Login In Progress..."}
66
- </p>
67
- <p className="text-sm text-neutral-500 mt-1.5">
68
- {isPopupAuth
69
- ? "You can now close this tab and return to the previous page."
70
- : "Wait a moment while we log you in with your code."}
 
 
 
 
 
 
 
 
 
 
71
  </p>
72
- </header>
73
- <main className="space-y-4 p-6">
74
- <div>
75
- <p className="text-sm text-neutral-700 mb-4 max-w-xs">
76
- If you are not redirected automatically in the next 5 seconds,
77
- please click the button below
 
 
 
78
  </p>
79
- {showButton ? (
80
- <Link href="/">
81
- <Button variant="black" className="relative">
82
- Go to Home
83
- </Button>
84
- </Link>
85
- ) : (
86
- <p className="text-xs text-neutral-500">
87
- Please wait, we are logging you in...
88
- </p>
89
- )}
90
- </div>
91
- </main>
92
- </div>
93
- <AnimatedBlobs />
94
  </div>
95
  </div>
96
  );
 
5
  import { useMount, useTimeoutFn } from "react-use";
6
 
7
  import { Button } from "@/components/ui/button";
 
 
8
  export default function AuthCallback({
9
  searchParams,
10
  }: {
11
  searchParams: Promise<{ code: string }>;
12
  }) {
13
  const [showButton, setShowButton] = useState(false);
 
14
  const { code } = use(searchParams);
15
  const { loginFromCode } = useUser();
 
16
 
17
  useMount(async () => {
18
  if (code) {
19
+ await loginFromCode(code);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
  });
22
 
23
+ useTimeoutFn(
24
+ () => setShowButton(true),
25
+ 7000 // Show button after 5 seconds
26
+ );
27
 
28
  return (
29
+ <div className="h-screen flex flex-col justify-center items-center">
30
+ <div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
31
+ <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
32
+ <div className="flex items-center justify-center -space-x-4 mb-3">
33
+ <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
34
+ 🚀
 
 
 
 
 
 
 
 
 
35
  </div>
36
+ <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
37
+ 👋
38
+ </div>
39
+ <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
40
+ 🙌
41
+ </div>
42
+ </div>
43
+ <p className="text-xl font-semibold text-neutral-950">
44
+ Login In Progress...
45
+ </p>
46
+ <p className="text-sm text-neutral-500 mt-1.5">
47
+ Wait a moment while we log you in with your code.
48
+ </p>
49
+ </header>
50
+ <main className="space-y-4 p-6">
51
+ <div>
52
+ <p className="text-sm text-neutral-700 mb-4 max-w-xs">
53
+ If you are not redirected automatically in the next 5 seconds,
54
+ please click the button below
55
  </p>
56
+ {showButton ? (
57
+ <Link href="/">
58
+ <Button variant="black" className="relative">
59
+ Go to Home
60
+ </Button>
61
+ </Link>
62
+ ) : (
63
+ <p className="text-xs text-neutral-500">
64
+ Please wait, we are logging you in...
65
  </p>
66
+ )}
67
+ </div>
68
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
69
  </div>
70
  </div>
71
  );
app/layout.tsx CHANGED
@@ -2,17 +2,14 @@
2
  import type { Metadata, Viewport } from "next";
3
  import { Inter, PT_Sans } from "next/font/google";
4
  import { cookies } from "next/headers";
5
- import Script from "next/script";
6
 
 
7
  import "@/assets/globals.css";
8
  import { Toaster } from "@/components/ui/sonner";
9
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
10
  import { apiServer } from "@/lib/api";
11
- import IframeDetector from "@/components/iframe-detector";
12
  import AppContext from "@/components/contexts/app-context";
13
- import TanstackContext from "@/components/contexts/tanstack-query-context";
14
- import { LoginProvider } from "@/components/contexts/login-context";
15
- import { ProProvider } from "@/components/contexts/pro-context";
16
 
17
  const inter = Inter({
18
  variable: "--font-inter-sans",
@@ -72,16 +69,16 @@ export const viewport: Viewport = {
72
  async function getMe() {
73
  const cookieStore = await cookies();
74
  const token = cookieStore.get(MY_TOKEN_KEY())?.value;
75
- if (!token) return { user: null, projects: [], errCode: null };
76
  try {
77
  const res = await apiServer.get("/me", {
78
  headers: {
79
  Authorization: `Bearer ${token}`,
80
  },
81
  });
82
- return { user: res.data.user, projects: res.data.projects, errCode: null };
83
  } catch (err: any) {
84
- return { user: null, projects: [], errCode: err.status };
85
  }
86
  }
87
 
@@ -101,15 +98,10 @@ export default async function RootLayout({
101
  <body
102
  className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
103
  >
104
- <IframeDetector />
105
  <Toaster richColors position="bottom-center" />
106
- <TanstackContext>
107
- <AppContext me={data}>
108
- <LoginProvider>
109
- <ProProvider>{children}</ProProvider>
110
- </LoginProvider>
111
- </AppContext>
112
- </TanstackContext>
113
  </body>
114
  </html>
115
  );
 
2
  import type { Metadata, Viewport } from "next";
3
  import { Inter, PT_Sans } from "next/font/google";
4
  import { cookies } from "next/headers";
 
5
 
6
+ import TanstackProvider from "@/components/providers/tanstack-query-provider";
7
  import "@/assets/globals.css";
8
  import { Toaster } from "@/components/ui/sonner";
9
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
10
  import { apiServer } from "@/lib/api";
 
11
  import AppContext from "@/components/contexts/app-context";
12
+ import Script from "next/script";
 
 
13
 
14
  const inter = Inter({
15
  variable: "--font-inter-sans",
 
69
  async function getMe() {
70
  const cookieStore = await cookies();
71
  const token = cookieStore.get(MY_TOKEN_KEY())?.value;
72
+ if (!token) return { user: null, errCode: null };
73
  try {
74
  const res = await apiServer.get("/me", {
75
  headers: {
76
  Authorization: `Bearer ${token}`,
77
  },
78
  });
79
+ return { user: res.data.user, errCode: null };
80
  } catch (err: any) {
81
+ return { user: null, errCode: err.status };
82
  }
83
  }
84
 
 
98
  <body
99
  className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
100
  >
 
101
  <Toaster richColors position="bottom-center" />
102
+ <TanstackProvider>
103
+ <AppContext me={data}>{children}</AppContext>
104
+ </TanstackProvider>
 
 
 
 
105
  </body>
106
  </html>
107
  );
app/projects/[namespace]/[repoId]/page.tsx CHANGED
@@ -1,10 +1,40 @@
 
 
 
 
 
1
  import { AppEditor } from "@/components/editor";
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  export default async function ProjectNamespacePage({
4
  params,
5
  }: {
6
  params: Promise<{ namespace: string; repoId: string }>;
7
  }) {
8
  const { namespace, repoId } = await params;
9
- return <AppEditor namespace={namespace} repoId={repoId} />;
 
 
 
 
10
  }
 
1
+ import { cookies } from "next/headers";
2
+ import { redirect } from "next/navigation";
3
+
4
+ import { apiServer } from "@/lib/api";
5
+ import MY_TOKEN_KEY from "@/lib/get-cookie-name";
6
  import { AppEditor } from "@/components/editor";
7
 
8
+ async function getProject(namespace: string, repoId: string) {
9
+ // TODO replace with a server action
10
+ const cookieStore = await cookies();
11
+ const token = cookieStore.get(MY_TOKEN_KEY())?.value;
12
+ if (!token) return {};
13
+ try {
14
+ const { data } = await apiServer.get(
15
+ `/me/projects/${namespace}/${repoId}`,
16
+ {
17
+ headers: {
18
+ Authorization: `Bearer ${token}`,
19
+ },
20
+ }
21
+ );
22
+
23
+ return data.project;
24
+ } catch {
25
+ return {};
26
+ }
27
+ }
28
+
29
  export default async function ProjectNamespacePage({
30
  params,
31
  }: {
32
  params: Promise<{ namespace: string; repoId: string }>;
33
  }) {
34
  const { namespace, repoId } = await params;
35
+ const project = await getProject(namespace, repoId);
36
+ if (!project?.html) {
37
+ redirect("/projects");
38
+ }
39
+ return <AppEditor project={project} />;
40
  }
app/projects/new/page.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { AppEditor } from "@/components/editor";
2
 
3
- export default function NewProjectPage() {
4
- return <AppEditor isNew />;
5
  }
 
1
  import { AppEditor } from "@/components/editor";
2
 
3
+ export default function ProjectsNewPage() {
4
+ return <AppEditor />;
5
  }
assets/globals.css CHANGED
@@ -112,10 +112,6 @@
112
  --sidebar-ring: oklch(0.556 0 0);
113
  }
114
 
115
- body {
116
- @apply scroll-smooth
117
- }
118
-
119
  @layer base {
120
  * {
121
  @apply border-border outline-ring/50;
@@ -148,224 +144,3 @@ body {
148
  .matched-line {
149
  @apply bg-sky-500/30;
150
  }
151
-
152
- /* Fast liquid deformation animations */
153
- @keyframes liquidBlob1 {
154
- 0%, 100% {
155
- border-radius: 40% 60% 50% 50%;
156
- transform: scaleX(1) scaleY(1) rotate(0deg);
157
- }
158
- 12.5% {
159
- border-radius: 20% 80% 70% 30%;
160
- transform: scaleX(1.6) scaleY(0.4) rotate(25deg);
161
- }
162
- 25% {
163
- border-radius: 80% 20% 30% 70%;
164
- transform: scaleX(0.5) scaleY(2.1) rotate(-15deg);
165
- }
166
- 37.5% {
167
- border-radius: 30% 70% 80% 20%;
168
- transform: scaleX(1.8) scaleY(0.6) rotate(40deg);
169
- }
170
- 50% {
171
- border-radius: 70% 30% 20% 80%;
172
- transform: scaleX(0.4) scaleY(1.9) rotate(-30deg);
173
- }
174
- 62.5% {
175
- border-radius: 25% 75% 60% 40%;
176
- transform: scaleX(1.5) scaleY(0.7) rotate(55deg);
177
- }
178
- 75% {
179
- border-radius: 75% 25% 40% 60%;
180
- transform: scaleX(0.6) scaleY(1.7) rotate(-10deg);
181
- }
182
- 87.5% {
183
- border-radius: 50% 50% 75% 25%;
184
- transform: scaleX(1.3) scaleY(0.8) rotate(35deg);
185
- }
186
- }
187
-
188
- @keyframes liquidBlob2 {
189
- 0%, 100% {
190
- border-radius: 60% 40% 50% 50%;
191
- transform: scaleX(1) scaleY(1) rotate(12deg);
192
- }
193
- 16% {
194
- border-radius: 15% 85% 60% 40%;
195
- transform: scaleX(0.3) scaleY(2.3) rotate(50deg);
196
- }
197
- 32% {
198
- border-radius: 85% 15% 25% 75%;
199
- transform: scaleX(2.0) scaleY(0.5) rotate(-20deg);
200
- }
201
- 48% {
202
- border-radius: 30% 70% 85% 15%;
203
- transform: scaleX(0.4) scaleY(1.8) rotate(70deg);
204
- }
205
- 64% {
206
- border-radius: 70% 30% 15% 85%;
207
- transform: scaleX(1.9) scaleY(0.6) rotate(-35deg);
208
- }
209
- 80% {
210
- border-radius: 40% 60% 70% 30%;
211
- transform: scaleX(0.7) scaleY(1.6) rotate(45deg);
212
- }
213
- }
214
-
215
- @keyframes liquidBlob3 {
216
- 0%, 100% {
217
- border-radius: 50% 50% 40% 60%;
218
- transform: scaleX(1) scaleY(1) rotate(0deg);
219
- }
220
- 20% {
221
- border-radius: 10% 90% 75% 25%;
222
- transform: scaleX(2.2) scaleY(0.3) rotate(-45deg);
223
- }
224
- 40% {
225
- border-radius: 90% 10% 20% 80%;
226
- transform: scaleX(0.4) scaleY(2.5) rotate(60deg);
227
- }
228
- 60% {
229
- border-radius: 25% 75% 90% 10%;
230
- transform: scaleX(1.7) scaleY(0.5) rotate(-25deg);
231
- }
232
- 80% {
233
- border-radius: 75% 25% 10% 90%;
234
- transform: scaleX(0.6) scaleY(2.0) rotate(80deg);
235
- }
236
- }
237
-
238
- @keyframes liquidBlob4 {
239
- 0%, 100% {
240
- border-radius: 45% 55% 50% 50%;
241
- transform: scaleX(1) scaleY(1) rotate(-15deg);
242
- }
243
- 14% {
244
- border-radius: 90% 10% 65% 35%;
245
- transform: scaleX(0.2) scaleY(2.8) rotate(35deg);
246
- }
247
- 28% {
248
- border-radius: 10% 90% 20% 80%;
249
- transform: scaleX(2.4) scaleY(0.4) rotate(-50deg);
250
- }
251
- 42% {
252
- border-radius: 35% 65% 90% 10%;
253
- transform: scaleX(0.3) scaleY(2.1) rotate(70deg);
254
- }
255
- 56% {
256
- border-radius: 80% 20% 10% 90%;
257
- transform: scaleX(2.0) scaleY(0.5) rotate(-40deg);
258
- }
259
- 70% {
260
- border-radius: 20% 80% 55% 45%;
261
- transform: scaleX(0.5) scaleY(1.9) rotate(55deg);
262
- }
263
- 84% {
264
- border-radius: 65% 35% 80% 20%;
265
- transform: scaleX(1.6) scaleY(0.6) rotate(-25deg);
266
- }
267
- }
268
-
269
- /* Fast flowing movement animations */
270
- @keyframes liquidFlow1 {
271
- 0%, 100% { transform: translate(0, 0); }
272
- 16% { transform: translate(60px, -40px); }
273
- 32% { transform: translate(-45px, -70px); }
274
- 48% { transform: translate(80px, 25px); }
275
- 64% { transform: translate(-30px, 60px); }
276
- 80% { transform: translate(50px, -20px); }
277
- }
278
-
279
- @keyframes liquidFlow2 {
280
- 0%, 100% { transform: translate(0, 0); }
281
- 20% { transform: translate(-70px, 50px); }
282
- 40% { transform: translate(90px, -30px); }
283
- 60% { transform: translate(-40px, -55px); }
284
- 80% { transform: translate(65px, 35px); }
285
- }
286
-
287
- @keyframes liquidFlow3 {
288
- 0%, 100% { transform: translate(0, 0); }
289
- 12% { transform: translate(-50px, -60px); }
290
- 24% { transform: translate(40px, -20px); }
291
- 36% { transform: translate(-30px, 70px); }
292
- 48% { transform: translate(70px, 20px); }
293
- 60% { transform: translate(-60px, -35px); }
294
- 72% { transform: translate(35px, 55px); }
295
- 84% { transform: translate(-25px, -45px); }
296
- }
297
-
298
- @keyframes liquidFlow4 {
299
- 0%, 100% { transform: translate(0, 0); }
300
- 14% { transform: translate(50px, 60px); }
301
- 28% { transform: translate(-80px, -40px); }
302
- 42% { transform: translate(30px, -90px); }
303
- 56% { transform: translate(-55px, 45px); }
304
- 70% { transform: translate(75px, -25px); }
305
- 84% { transform: translate(-35px, 65px); }
306
- }
307
-
308
- /* Light sweep animation for buttons */
309
- @keyframes lightSweep {
310
- 0% {
311
- transform: translateX(-150%);
312
- opacity: 0;
313
- }
314
- 8% {
315
- opacity: 0.3;
316
- }
317
- 25% {
318
- opacity: 0.8;
319
- }
320
- 42% {
321
- opacity: 0.3;
322
- }
323
- 50% {
324
- transform: translateX(150%);
325
- opacity: 0;
326
- }
327
- 58% {
328
- opacity: 0.3;
329
- }
330
- 75% {
331
- opacity: 0.8;
332
- }
333
- 92% {
334
- opacity: 0.3;
335
- }
336
- 100% {
337
- transform: translateX(-150%);
338
- opacity: 0;
339
- }
340
- }
341
-
342
- .light-sweep {
343
- position: relative;
344
- overflow: hidden;
345
- }
346
-
347
- .light-sweep::before {
348
- content: '';
349
- position: absolute;
350
- top: 0;
351
- left: 0;
352
- right: 0;
353
- bottom: 0;
354
- width: 300%;
355
- background: linear-gradient(
356
- 90deg,
357
- transparent 0%,
358
- transparent 20%,
359
- rgba(56, 189, 248, 0.1) 35%,
360
- rgba(56, 189, 248, 0.2) 45%,
361
- rgba(255, 255, 255, 0.2) 50%,
362
- rgba(168, 85, 247, 0.2) 55%,
363
- rgba(168, 85, 247, 0.1) 65%,
364
- transparent 80%,
365
- transparent 100%
366
- );
367
- animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite;
368
- pointer-events: none;
369
- z-index: 1;
370
- filter: blur(1px);
371
- }
 
112
  --sidebar-ring: oklch(0.556 0 0);
113
  }
114
 
 
 
 
 
115
  @layer base {
116
  * {
117
  @apply border-border outline-ring/50;
 
144
  .matched-line {
145
  @apply bg-sky-500/30;
146
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components.json CHANGED
@@ -5,7 +5,7 @@
5
  "tsx": true,
6
  "tailwind": {
7
  "config": "",
8
- "css": "assets/globals.css",
9
  "baseColor": "neutral",
10
  "cssVariables": true,
11
  "prefix": ""
 
5
  "tsx": true,
6
  "tailwind": {
7
  "config": "",
8
+ "css": "app/globals.css",
9
  "baseColor": "neutral",
10
  "cssVariables": true,
11
  "prefix": ""
components/animated-blobs/index.tsx DELETED
@@ -1,34 +0,0 @@
1
- export function AnimatedBlobs() {
2
- return (
3
- <div className="absolute inset-0 pointer-events-none -z-[1]">
4
- <div
5
- className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl"
6
- style={{
7
- animation:
8
- "liquidBlob1 4s ease-in-out infinite, liquidFlow1 6s ease-in-out infinite",
9
- }}
10
- />
11
- <div
12
- className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10"
13
- style={{
14
- animation:
15
- "liquidBlob2 5s ease-in-out infinite, liquidFlow2 7s ease-in-out infinite",
16
- }}
17
- />
18
- <div
19
- className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10"
20
- style={{
21
- animation:
22
- "liquidBlob3 3.5s ease-in-out infinite, liquidFlow3 8s ease-in-out infinite",
23
- }}
24
- />
25
- <div
26
- className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3"
27
- style={{
28
- animation:
29
- "liquidBlob4 4.5s ease-in-out infinite, liquidFlow4 6.5s ease-in-out infinite",
30
- }}
31
- />
32
- </div>
33
- );
34
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/animated-text/index.tsx DELETED
@@ -1,123 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect } from "react";
4
-
5
- interface AnimatedTextProps {
6
- className?: string;
7
- }
8
-
9
- export function AnimatedText({ className = "" }: AnimatedTextProps) {
10
- const [displayText, setDisplayText] = useState("");
11
- const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
12
- const [isTyping, setIsTyping] = useState(true);
13
- const [showCursor, setShowCursor] = useState(true);
14
- const [lastTypedIndex, setLastTypedIndex] = useState(-1);
15
- const [animationComplete, setAnimationComplete] = useState(false);
16
-
17
- // Randomize suggestions on each component mount
18
- const [suggestions] = useState(() => {
19
- const baseSuggestions = [
20
- "create a stunning portfolio!",
21
- "build a tic tac toe game!",
22
- "design a website for my restaurant!",
23
- "make a sleek landing page!",
24
- "build an e-commerce store!",
25
- "create a personal blog!",
26
- "develop a modern dashboard!",
27
- "design a company website!",
28
- "build a todo app!",
29
- "create an online gallery!",
30
- "make a contact form!",
31
- "build a weather app!",
32
- ];
33
-
34
- // Fisher-Yates shuffle algorithm
35
- const shuffled = [...baseSuggestions];
36
- for (let i = shuffled.length - 1; i > 0; i--) {
37
- const j = Math.floor(Math.random() * (i + 1));
38
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
39
- }
40
-
41
- return shuffled;
42
- });
43
-
44
- useEffect(() => {
45
- if (animationComplete) return;
46
-
47
- let timeout: NodeJS.Timeout;
48
-
49
- const typeText = () => {
50
- const currentSuggestion = suggestions[currentSuggestionIndex];
51
-
52
- if (isTyping) {
53
- if (displayText.length < currentSuggestion.length) {
54
- setDisplayText(currentSuggestion.slice(0, displayText.length + 1));
55
- setLastTypedIndex(displayText.length);
56
- timeout = setTimeout(typeText, 80);
57
- } else {
58
- // Finished typing, wait then start erasing
59
- setLastTypedIndex(-1);
60
- timeout = setTimeout(() => {
61
- setIsTyping(false);
62
- }, 2000);
63
- }
64
- }
65
- };
66
-
67
- timeout = setTimeout(typeText, 100);
68
- return () => clearTimeout(timeout);
69
- }, [
70
- displayText,
71
- currentSuggestionIndex,
72
- isTyping,
73
- suggestions,
74
- animationComplete,
75
- ]);
76
-
77
- // Cursor blinking effect
78
- useEffect(() => {
79
- if (animationComplete) {
80
- setShowCursor(false);
81
- return;
82
- }
83
-
84
- const cursorInterval = setInterval(() => {
85
- setShowCursor((prev) => !prev);
86
- }, 600);
87
-
88
- return () => clearInterval(cursorInterval);
89
- }, [animationComplete]);
90
-
91
- useEffect(() => {
92
- if (lastTypedIndex >= 0) {
93
- const timeout = setTimeout(() => {
94
- setLastTypedIndex(-1);
95
- }, 400);
96
-
97
- return () => clearTimeout(timeout);
98
- }
99
- }, [lastTypedIndex]);
100
-
101
- return (
102
- <p className={`font-mono ${className}`}>
103
- Hey DeepSite,&nbsp;
104
- {displayText.split("").map((char, index) => (
105
- <span
106
- key={`${currentSuggestionIndex}-${index}`}
107
- className={`transition-colors duration-300 ${
108
- index === lastTypedIndex ? "text-neutral-100" : ""
109
- }`}
110
- >
111
- {char}
112
- </span>
113
- ))}
114
- <span
115
- className={`${
116
- showCursor ? "opacity-100" : "opacity-0"
117
- } transition-opacity`}
118
- >
119
- |
120
- </span>
121
- </p>
122
- );
123
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/contexts/app-context.tsx CHANGED
@@ -1,11 +1,12 @@
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
  "use client";
3
- import { useMount } from "react-use";
4
- import { toast } from "sonner";
5
- import { usePathname, useRouter } from "next/navigation";
6
 
7
  import { useUser } from "@/hooks/useUser";
8
- import { ProjectType, User } from "@/types";
 
 
 
 
9
  import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
10
 
11
  export default function AppContext({
@@ -15,7 +16,6 @@ export default function AppContext({
15
  children: React.ReactNode;
16
  me?: {
17
  user: User | null;
18
- projects: ProjectType[];
19
  errCode: number | null;
20
  };
21
  }) {
@@ -49,5 +49,9 @@ export default function AppContext({
49
  }
50
  });
51
 
52
- return children;
 
 
 
 
53
  }
 
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
  "use client";
 
 
 
3
 
4
  import { useUser } from "@/hooks/useUser";
5
+ import { usePathname, useRouter } from "next/navigation";
6
+ import { useMount } from "react-use";
7
+ import { UserContext } from "@/components/contexts/user-context";
8
+ import { User } from "@/types";
9
+ import { toast } from "sonner";
10
  import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
11
 
12
  export default function AppContext({
 
16
  children: React.ReactNode;
17
  me?: {
18
  user: User | null;
 
19
  errCode: number | null;
20
  };
21
  }) {
 
49
  }
50
  });
51
 
52
+ return (
53
+ <UserContext value={{ user, loading, logout } as any}>
54
+ {children}
55
+ </UserContext>
56
+ );
57
  }
components/contexts/login-context.tsx DELETED
@@ -1,61 +0,0 @@
1
- "use client";
2
-
3
- import React, { createContext, useContext, useState, ReactNode } from "react";
4
- import { LoginModal } from "@/components/login-modal";
5
- import { Page } from "@/types";
6
-
7
- interface LoginContextType {
8
- isOpen: boolean;
9
- openLoginModal: (options?: LoginModalOptions) => void;
10
- closeLoginModal: () => void;
11
- }
12
-
13
- interface LoginModalOptions {
14
- pages?: Page[];
15
- title?: string;
16
- description?: string;
17
- }
18
-
19
- const LoginContext = createContext<LoginContextType | undefined>(undefined);
20
-
21
- export function LoginProvider({ children }: { children: ReactNode }) {
22
- const [isOpen, setIsOpen] = useState(false);
23
- const [modalOptions, setModalOptions] = useState<LoginModalOptions>({});
24
-
25
- const openLoginModal = (options: LoginModalOptions = {}) => {
26
- setModalOptions(options);
27
- setIsOpen(true);
28
- };
29
-
30
- const closeLoginModal = () => {
31
- setIsOpen(false);
32
- setModalOptions({});
33
- };
34
-
35
- const value = {
36
- isOpen,
37
- openLoginModal,
38
- closeLoginModal,
39
- };
40
-
41
- return (
42
- <LoginContext.Provider value={value}>
43
- {children}
44
- <LoginModal
45
- open={isOpen}
46
- onClose={setIsOpen}
47
- pages={modalOptions.pages}
48
- title={modalOptions.title}
49
- description={modalOptions.description}
50
- />
51
- </LoginContext.Provider>
52
- );
53
- }
54
-
55
- export function useLoginModal() {
56
- const context = useContext(LoginContext);
57
- if (context === undefined) {
58
- throw new Error("useLoginModal must be used within a LoginProvider");
59
- }
60
- return context;
61
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/contexts/pro-context.tsx DELETED
@@ -1,48 +0,0 @@
1
- "use client";
2
-
3
- import React, { createContext, useContext, useState, ReactNode } from "react";
4
- import { ProModal } from "@/components/pro-modal";
5
- import { Page } from "@/types";
6
- import { useEditor } from "@/hooks/useEditor";
7
-
8
- interface ProContextType {
9
- isOpen: boolean;
10
- openProModal: (pages: Page[]) => void;
11
- closeProModal: () => void;
12
- }
13
-
14
- const ProContext = createContext<ProContextType | undefined>(undefined);
15
-
16
- export function ProProvider({ children }: { children: ReactNode }) {
17
- const [isOpen, setIsOpen] = useState(false);
18
- const { pages } = useEditor();
19
-
20
- const openProModal = () => {
21
- setIsOpen(true);
22
- };
23
-
24
- const closeProModal = () => {
25
- setIsOpen(false);
26
- };
27
-
28
- const value = {
29
- isOpen,
30
- openProModal,
31
- closeProModal,
32
- };
33
-
34
- return (
35
- <ProContext.Provider value={value}>
36
- {children}
37
- <ProModal open={isOpen} onClose={setIsOpen} pages={pages} />
38
- </ProContext.Provider>
39
- );
40
- }
41
-
42
- export function useProModal() {
43
- const context = useContext(ProContext);
44
- if (context === undefined) {
45
- throw new Error("useProModal must be used within a ProProvider");
46
- }
47
- return context;
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/follow-up-tooltip.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Popover,
3
+ PopoverContent,
4
+ PopoverTrigger,
5
+ } from "@/components/ui/popover";
6
+ import { Info } from "lucide-react";
7
+
8
+ export const FollowUpTooltip = () => {
9
+ return (
10
+ <Popover>
11
+ <PopoverTrigger asChild>
12
+ <Info className="size-3 text-neutral-300 cursor-pointer" />
13
+ </PopoverTrigger>
14
+ <PopoverContent
15
+ align="start"
16
+ className="!rounded-2xl !p-0 min-w-xs text-center overflow-hidden"
17
+ >
18
+ <header className="bg-neutral-950 px-4 py-3 border-b border-neutral-700/70">
19
+ <p className="text-base text-neutral-200 font-semibold">
20
+ ⚡ Faster, Smarter Updates
21
+ </p>
22
+ </header>
23
+ <main className="p-4">
24
+ <p className="text-neutral-300 text-sm">
25
+ Using the Diff-Patch system, allow DeepSite to intelligently update
26
+ your project without rewritting the entire codebase.
27
+ </p>
28
+ <p className="text-neutral-500 text-sm mt-2">
29
+ This means faster updates, less data usage, and a more efficient
30
+ development process.
31
+ </p>
32
+ </main>
33
+ </PopoverContent>
34
+ </Popover>
35
+ );
36
+ };
components/editor/ask-ai/index.tsx CHANGED
@@ -1,162 +1,335 @@
1
- import { useMemo, useState } from "react";
 
 
2
  import classNames from "classnames";
3
- import {
4
- ArrowUp,
5
- CircleStop,
6
- Pause,
7
- Plus,
8
- Square,
9
- StopCircle,
10
- } from "lucide-react";
11
- import { useLocalStorage } from "react-use";
12
  import { toast } from "sonner";
 
 
 
13
 
14
- import { useAi } from "@/hooks/useAi";
15
- import { useEditor } from "@/hooks/useEditor";
16
- import { isTheSameHtml } from "@/lib/compare-html-diff";
17
- import { EnhancedSettings, Project } from "@/types";
18
- import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
19
- import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
20
- import { AiLoading } from "@/components/editor/ask-ai/loading";
21
  import { Button } from "@/components/ui/button";
22
- import { Uploader } from "@/components/editor/ask-ai/uploader";
23
- import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
24
- import { Selector } from "@/components/editor/ask-ai/selector";
25
- import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
26
- import { useUser } from "@/hooks/useUser";
27
- import { useLoginModal } from "@/components/contexts/login-context";
28
- import { Settings } from "./settings";
29
- import { useProModal } from "@/components/contexts/pro-context";
30
  import { MODELS } from "@/lib/providers";
31
- import { MAX_FREE_PROJECTS } from "@/lib/utils";
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- export const AskAi = ({
34
- project,
35
- isNew,
36
  onScrollToBottom,
 
 
 
 
 
 
 
 
37
  }: {
38
- project?: Project;
39
- files?: string[];
40
- isNew?: boolean;
41
- onScrollToBottom?: () => void;
42
- }) => {
43
- const { user, projects } = useUser();
44
- const { isSameHtml, isUploading, pages, isLoadingProject } = useEditor();
45
- const {
46
- isAiWorking,
47
- isThinking,
48
- selectedFiles,
49
- setSelectedFiles,
50
- selectedElement,
51
- setSelectedElement,
52
- setIsThinking,
53
- callAiNewProject,
54
- callAiFollowUp,
55
- setModel,
56
- selectedModel,
57
- audio: hookAudio,
58
- cancelRequest,
59
- } = useAi(onScrollToBottom);
60
- const { openLoginModal } = useLoginModal();
61
- const { openProModal } = useProModal();
62
- const [openProvider, setOpenProvider] = useState(false);
63
- const [providerError, setProviderError] = useState("");
64
-
65
- const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
66
- useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
67
- isActive: true,
68
- primaryColor: undefined,
69
- secondaryColor: undefined,
70
- theme: undefined,
71
- });
72
 
73
- const [isFollowUp, setIsFollowUp] = useState(true);
74
  const [prompt, setPrompt] = useState("");
75
- const [think, setThink] = useState("");
 
 
 
 
 
 
 
76
  const [openThink, setOpenThink] = useState(false);
 
 
 
77
 
78
- const handleThink = (think: string) => {
79
- setThink(think);
80
- setIsThinking(true);
81
- setOpenThink(true);
82
- };
83
 
84
  const callAi = async (redesignMarkdown?: string) => {
85
- if (!user) return openLoginModal();
86
- if (!user.isPro && projects.length >= MAX_FREE_PROJECTS)
87
- return openProModal([]);
88
  if (isAiWorking) return;
89
  if (!redesignMarkdown && !prompt.trim()) return;
 
 
 
 
 
90
 
91
- if (isFollowUp && !redesignMarkdown && !isSameHtml) {
92
- const result = await callAiFollowUp(prompt, enhancedSettings, isNew);
 
93
 
94
- if (result?.error) {
95
- handleError(result.error, result.message);
96
- return;
97
- }
98
-
99
- if (result?.success) {
100
- setPrompt("");
101
- }
102
- } else {
103
- const result = await callAiNewProject(
104
- prompt,
105
- enhancedSettings,
106
- redesignMarkdown,
107
- handleThink,
108
- () => {
109
- setIsThinking(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  }
111
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- if (result?.error) {
114
- handleError(result.error, result.message);
115
- return;
116
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- if (result?.success) {
119
- setPrompt("");
120
- if (selectedModel?.isThinker) {
121
- setModel(MODELS[0].value);
 
 
 
 
122
  }
123
  }
 
 
 
 
 
 
124
  }
125
  };
126
 
127
- const handleError = (error: string, message?: string) => {
128
- switch (error) {
129
- case "login_required":
130
- openLoginModal();
131
- break;
132
- case "provider_required":
133
- setOpenProvider(true);
134
- setProviderError(message || "");
135
- break;
136
- case "pro_required":
137
- openProModal([]);
138
- break;
139
- case "api_error":
140
- toast.error(message || "An error occurred");
141
- break;
142
- case "network_error":
143
- toast.error(message || "Network error occurred");
144
- break;
145
- default:
146
- toast.error("An unexpected error occurred");
147
  }
148
  };
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  return (
151
- <div className="p-3 w-full">
152
- <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
153
- <SelectedFiles
154
- files={selectedFiles}
155
- isAiWorking={isAiWorking}
156
- onDelete={(file) =>
157
- setSelectedFiles(selectedFiles.filter((f) => f !== file))
158
- }
159
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  {selectedElement && (
161
  <div className="px-4 pt-3">
162
  <SelectedHtmlElement
@@ -167,47 +340,36 @@ export const AskAi = ({
167
  </div>
168
  )}
169
  <div className="w-full relative flex items-center justify-between">
170
- {(isAiWorking || isUploading || isThinking || isLoadingProject) && (
171
- <div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
172
- <AiLoading
173
- text={
174
- isLoadingProject
175
- ? "Fetching your project..."
176
- : isUploading
177
- ? "Uploading images..."
178
- : isAiWorking && !isSameHtml
179
- ? "DeepSite is working..."
180
- : "DeepSite is thinking..."
181
- }
182
- />
183
- {isAiWorking && (
184
- <Button
185
- size="iconXs"
186
- variant="outline"
187
- className="!rounded-md mr-0.5"
188
- onClick={cancelRequest}
189
- >
190
- <CircleStop className="size-4" />
191
- </Button>
192
- )}
193
  </div>
194
  )}
195
- <textarea
196
- disabled={
197
- isAiWorking || isUploading || isThinking || isLoadingProject
198
- }
199
  className={classNames(
200
- "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
201
  {
202
- "!pt-2.5":
203
- selectedElement &&
204
- !(isAiWorking || isUploading || isThinking),
205
  }
206
  )}
207
  placeholder={
208
  selectedElement
209
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
210
- : isFollowUp && (!isSameHtml || pages?.length > 1)
211
  ? "Ask DeepSite for edits"
212
  : "Ask DeepSite anything..."
213
  }
@@ -220,41 +382,90 @@ export const AskAi = ({
220
  }}
221
  />
222
  </div>
223
- <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
224
- <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
225
- <PromptBuilder
226
- enhancedSettings={enhancedSettings!}
227
- setEnhancedSettings={setEnhancedSettings}
228
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  <Settings
 
 
 
 
230
  open={openProvider}
231
  error={providerError}
232
  isFollowUp={!isSameHtml && isFollowUp}
233
  onClose={setOpenProvider}
234
  />
235
- {!isNew && <Uploader project={project} />}
236
- {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
237
- {!isNew && <Selector />}
238
- </div>
239
- <div className="flex items-center justify-end gap-2">
240
  <Button
241
  size="iconXs"
242
- variant="outline"
243
- className="!rounded-md"
244
- disabled={
245
- isAiWorking || isUploading || isThinking || !prompt.trim()
246
- }
247
  onClick={() => callAi()}
248
  >
249
  <ArrowUp className="size-4" />
250
  </Button>
251
  </div>
252
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  </div>
254
- <audio ref={hookAudio} id="audio" className="hidden">
255
  <source src="/success.mp3" type="audio/mpeg" />
256
  Your browser does not support the audio element.
257
  </audio>
258
  </div>
259
  );
260
- };
 
1
+ "use client";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { useState, useRef, useMemo } from "react";
4
  import classNames from "classnames";
 
 
 
 
 
 
 
 
 
5
  import { toast } from "sonner";
6
+ import { useLocalStorage, useUpdateEffect } from "react-use";
7
+ import { ArrowUp, ChevronDown, Crosshair } from "lucide-react";
8
+ import { FaStopCircle } from "react-icons/fa";
9
 
10
+ import ProModal from "@/components/pro-modal";
 
 
 
 
 
 
11
  import { Button } from "@/components/ui/button";
 
 
 
 
 
 
 
 
12
  import { MODELS } from "@/lib/providers";
13
+ import { HtmlHistory } from "@/types";
14
+ import { InviteFriends } from "@/components/invite-friends";
15
+ import { Settings } from "@/components/editor/ask-ai/settings";
16
+ import { LoginModal } from "@/components/login-modal";
17
+ import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
18
+ import Loading from "@/components/loading";
19
+ import { Checkbox } from "@/components/ui/checkbox";
20
+ import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
21
+ import { TooltipContent } from "@radix-ui/react-tooltip";
22
+ import { SelectedHtmlElement } from "./selected-html-element";
23
+ import { FollowUpTooltip } from "./follow-up-tooltip";
24
+ import { isTheSameHtml } from "@/lib/compare-html-diff";
25
 
26
+ export function AskAI({
27
+ html,
28
+ setHtml,
29
  onScrollToBottom,
30
+ isAiWorking,
31
+ setisAiWorking,
32
+ isEditableModeEnabled = false,
33
+ selectedElement,
34
+ setSelectedElement,
35
+ setIsEditableModeEnabled,
36
+ onNewPrompt,
37
+ onSuccess,
38
  }: {
39
+ html: string;
40
+ setHtml: (html: string) => void;
41
+ onScrollToBottom: () => void;
42
+ isAiWorking: boolean;
43
+ onNewPrompt: (prompt: string) => void;
44
+ htmlHistory?: HtmlHistory[];
45
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
46
+ onSuccess: (h: string, p: string, n?: number[][]) => void;
47
+ isEditableModeEnabled: boolean;
48
+ setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
49
+ selectedElement?: HTMLElement | null;
50
+ setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
51
+ }) {
52
+ const refThink = useRef<HTMLDivElement | null>(null);
53
+ const audio = useRef<HTMLAudioElement | null>(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ const [open, setOpen] = useState(false);
56
  const [prompt, setPrompt] = useState("");
57
+ const [hasAsked, setHasAsked] = useState(false);
58
+ const [previousPrompt, setPreviousPrompt] = useState("");
59
+ const [provider, setProvider] = useLocalStorage("provider", "auto");
60
+ const [model, setModel] = useLocalStorage("model", MODELS[0].value);
61
+ const [openProvider, setOpenProvider] = useState(false);
62
+ const [providerError, setProviderError] = useState("");
63
+ const [openProModal, setOpenProModal] = useState(false);
64
+ const [think, setThink] = useState<string | undefined>(undefined);
65
  const [openThink, setOpenThink] = useState(false);
66
+ const [isThinking, setIsThinking] = useState(true);
67
+ const [controller, setController] = useState<AbortController | null>(null);
68
+ const [isFollowUp, setIsFollowUp] = useState(true);
69
 
70
+ const selectedModel = useMemo(() => {
71
+ return MODELS.find((m: { value: string }) => m.value === model);
72
+ }, [model]);
 
 
73
 
74
  const callAi = async (redesignMarkdown?: string) => {
 
 
 
75
  if (isAiWorking) return;
76
  if (!redesignMarkdown && !prompt.trim()) return;
77
+ setisAiWorking(true);
78
+ setProviderError("");
79
+ setThink("");
80
+ setOpenThink(false);
81
+ setIsThinking(true);
82
 
83
+ let contentResponse = "";
84
+ let thinkResponse = "";
85
+ let lastRenderTime = 0;
86
 
87
+ const abortController = new AbortController();
88
+ setController(abortController);
89
+ try {
90
+ onNewPrompt(prompt);
91
+ if (isFollowUp && !redesignMarkdown && !isSameHtml) {
92
+ const selectedElementHtml = selectedElement
93
+ ? selectedElement.outerHTML
94
+ : "";
95
+ const request = await fetch("/api/ask-ai", {
96
+ method: "PUT",
97
+ body: JSON.stringify({
98
+ prompt,
99
+ provider,
100
+ previousPrompt,
101
+ model,
102
+ html,
103
+ selectedElementHtml,
104
+ }),
105
+ headers: {
106
+ "Content-Type": "application/json",
107
+ "x-forwarded-for": window.location.hostname,
108
+ },
109
+ signal: abortController.signal,
110
+ });
111
+ if (request && request.body) {
112
+ const res = await request.json();
113
+ if (!request.ok) {
114
+ if (res.openLogin) {
115
+ setOpen(true);
116
+ } else if (res.openSelectProvider) {
117
+ setOpenProvider(true);
118
+ setProviderError(res.message);
119
+ } else if (res.openProModal) {
120
+ setOpenProModal(true);
121
+ } else {
122
+ toast.error(res.message);
123
+ }
124
+ setisAiWorking(false);
125
+ return;
126
+ }
127
+ setHtml(res.html);
128
+ toast.success("AI responded successfully");
129
+ setPreviousPrompt(prompt);
130
+ setPrompt("");
131
+ setisAiWorking(false);
132
+ onSuccess(res.html, prompt, res.updatedLines);
133
+ if (audio.current) audio.current.play();
134
  }
135
+ } else {
136
+ const request = await fetch("/api/ask-ai", {
137
+ method: "POST",
138
+ body: JSON.stringify({
139
+ prompt,
140
+ provider,
141
+ model,
142
+ html: isSameHtml ? "" : html,
143
+ redesignMarkdown,
144
+ }),
145
+ headers: {
146
+ "Content-Type": "application/json",
147
+ "x-forwarded-for": window.location.hostname,
148
+ },
149
+ signal: abortController.signal,
150
+ });
151
+ if (request && request.body) {
152
+ const reader = request.body.getReader();
153
+ const decoder = new TextDecoder("utf-8");
154
+ const selectedModel = MODELS.find(
155
+ (m: { value: string }) => m.value === model
156
+ );
157
+ let contentThink: string | undefined = undefined;
158
+ const read = async () => {
159
+ const { done, value } = await reader.read();
160
+ if (done) {
161
+ const isJson =
162
+ contentResponse.trim().startsWith("{") &&
163
+ contentResponse.trim().endsWith("}");
164
+ const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
165
+ if (jsonResponse && !jsonResponse.ok) {
166
+ if (jsonResponse.openLogin) {
167
+ setOpen(true);
168
+ } else if (jsonResponse.openSelectProvider) {
169
+ setOpenProvider(true);
170
+ setProviderError(jsonResponse.message);
171
+ } else if (jsonResponse.openProModal) {
172
+ setOpenProModal(true);
173
+ } else {
174
+ toast.error(jsonResponse.message);
175
+ }
176
+ setisAiWorking(false);
177
+ return;
178
+ }
179
 
180
+ toast.success("AI responded successfully");
181
+ setPreviousPrompt(prompt);
182
+ setPrompt("");
183
+ setisAiWorking(false);
184
+ setHasAsked(true);
185
+ if (selectedModel?.isThinker) {
186
+ setModel(MODELS[0].value);
187
+ }
188
+ if (audio.current) audio.current.play();
189
+
190
+ // Now we have the complete HTML including </html>, so set it to be sure
191
+ const finalDoc = contentResponse.match(
192
+ /<!DOCTYPE html>[\s\S]*<\/html>/
193
+ )?.[0];
194
+ if (finalDoc) {
195
+ setHtml(finalDoc);
196
+ }
197
+ onSuccess(finalDoc ?? contentResponse, prompt);
198
+
199
+ return;
200
+ }
201
+
202
+ const chunk = decoder.decode(value, { stream: true });
203
+ thinkResponse += chunk;
204
+ if (selectedModel?.isThinker) {
205
+ const thinkMatch = thinkResponse.match(/<think>[\s\S]*/)?.[0];
206
+ if (thinkMatch && !thinkResponse?.includes("</think>")) {
207
+ if ((contentThink?.length ?? 0) < 3) {
208
+ setOpenThink(true);
209
+ }
210
+ setThink(thinkMatch.replace("<think>", "").trim());
211
+ contentThink += chunk;
212
+ return read();
213
+ }
214
+ }
215
+
216
+ contentResponse += chunk;
217
+
218
+ const newHtml = contentResponse.match(
219
+ /<!DOCTYPE html>[\s\S]*/
220
+ )?.[0];
221
+ if (newHtml) {
222
+ setIsThinking(false);
223
+ let partialDoc = newHtml;
224
+ if (
225
+ partialDoc.includes("<head>") &&
226
+ !partialDoc.includes("</head>")
227
+ ) {
228
+ partialDoc += "\n</head>";
229
+ }
230
+ if (
231
+ partialDoc.includes("<body") &&
232
+ !partialDoc.includes("</body>")
233
+ ) {
234
+ partialDoc += "\n</body>";
235
+ }
236
+ if (!partialDoc.includes("</html>")) {
237
+ partialDoc += "\n</html>";
238
+ }
239
+
240
+ // Throttle the re-renders to avoid flashing/flicker
241
+ const now = Date.now();
242
+ if (now - lastRenderTime > 300) {
243
+ setHtml(partialDoc);
244
+ lastRenderTime = now;
245
+ }
246
 
247
+ if (partialDoc.length > 200) {
248
+ onScrollToBottom();
249
+ }
250
+ }
251
+ read();
252
+ };
253
+
254
+ read();
255
  }
256
  }
257
+ } catch (error: any) {
258
+ setisAiWorking(false);
259
+ toast.error(error.message);
260
+ if (error.openLogin) {
261
+ setOpen(true);
262
+ }
263
  }
264
  };
265
 
266
+ const stopController = () => {
267
+ if (controller) {
268
+ controller.abort();
269
+ setController(null);
270
+ setisAiWorking(false);
271
+ setThink("");
272
+ setOpenThink(false);
273
+ setIsThinking(false);
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
275
  };
276
 
277
+ useUpdateEffect(() => {
278
+ if (refThink.current) {
279
+ refThink.current.scrollTop = refThink.current.scrollHeight;
280
+ }
281
+ }, [think]);
282
+
283
+ useUpdateEffect(() => {
284
+ if (!isThinking) {
285
+ setOpenThink(false);
286
+ }
287
+ }, [isThinking]);
288
+
289
+ const isSameHtml = useMemo(() => {
290
+ return isTheSameHtml(html);
291
+ }, [html]);
292
+
293
  return (
294
+ <div className="px-3">
295
+ <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-10 w-full group">
296
+ {think && (
297
+ <div className="w-full border-b border-neutral-700 relative overflow-hidden">
298
+ <header
299
+ className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
300
+ onClick={() => {
301
+ setOpenThink(!openThink);
302
+ }}
303
+ >
304
+ <p className="text-sm font-medium text-neutral-300 group-hover:text-neutral-200 transition-colors duration-200">
305
+ {isThinking ? "DeepSite is thinking..." : "DeepSite's plan"}
306
+ </p>
307
+ <ChevronDown
308
+ className={classNames(
309
+ "size-4 text-neutral-400 group-hover:text-neutral-300 transition-all duration-200",
310
+ {
311
+ "rotate-180": openThink,
312
+ }
313
+ )}
314
+ />
315
+ </header>
316
+ <main
317
+ ref={refThink}
318
+ className={classNames(
319
+ "overflow-y-auto transition-all duration-200 ease-in-out",
320
+ {
321
+ "max-h-[0px]": !openThink,
322
+ "min-h-[250px] max-h-[250px] border-t border-neutral-700":
323
+ openThink,
324
+ }
325
+ )}
326
+ >
327
+ <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
328
+ {think}
329
+ </p>
330
+ </main>
331
+ </div>
332
+ )}
333
  {selectedElement && (
334
  <div className="px-4 pt-3">
335
  <SelectedHtmlElement
 
340
  </div>
341
  )}
342
  <div className="w-full relative flex items-center justify-between">
343
+ {isAiWorking && (
344
+ <div className="absolute bg-neutral-800 rounded-lg bottom-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-center justify-between max-lg:text-sm">
345
+ <div className="flex items-center justify-start gap-2">
346
+ <Loading overlay={false} className="!size-4" />
347
+ <p className="text-neutral-400 text-sm">
348
+ AI is {isThinking ? "thinking" : "coding"}...{" "}
349
+ </p>
350
+ </div>
351
+ <div
352
+ className="text-xs text-neutral-400 px-1 py-0.5 rounded-md border border-neutral-600 flex items-center justify-center gap-1.5 bg-neutral-800 hover:brightness-110 transition-all duration-200 cursor-pointer"
353
+ onClick={stopController}
354
+ >
355
+ <FaStopCircle />
356
+ Stop generation
357
+ </div>
 
 
 
 
 
 
 
 
358
  </div>
359
  )}
360
+ <input
361
+ type="text"
362
+ disabled={isAiWorking}
 
363
  className={classNames(
364
+ "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
365
  {
366
+ "!pt-2.5": selectedElement && !isAiWorking,
 
 
367
  }
368
  )}
369
  placeholder={
370
  selectedElement
371
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
372
+ : hasAsked
373
  ? "Ask DeepSite for edits"
374
  : "Ask DeepSite anything..."
375
  }
 
382
  }}
383
  />
384
  </div>
385
+ <div className="flex items-center justify-between gap-2 px-4 pb-3">
386
+ <div className="flex-1 flex items-center justify-start gap-1.5">
387
+ <ReImagine onRedesign={(md) => callAi(md)} />
388
+ {!isSameHtml && (
389
+ <Tooltip>
390
+ <TooltipTrigger asChild>
391
+ <Button
392
+ size="xs"
393
+ variant={isEditableModeEnabled ? "default" : "outline"}
394
+ onClick={() => {
395
+ setIsEditableModeEnabled?.(!isEditableModeEnabled);
396
+ }}
397
+ className={classNames("h-[28px]", {
398
+ "!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
399
+ !isEditableModeEnabled,
400
+ })}
401
+ >
402
+ <Crosshair className="size-4" />
403
+ Edit
404
+ </Button>
405
+ </TooltipTrigger>
406
+ <TooltipContent
407
+ align="start"
408
+ className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
409
+ >
410
+ Select an element on the page to ask DeepSite edit it
411
+ directly.
412
+ </TooltipContent>
413
+ </Tooltip>
414
+ )}
415
+ <InviteFriends />
416
+ </div>
417
+ <div className="flex items-center justify-end gap-2">
418
  <Settings
419
+ provider={provider as string}
420
+ model={model as string}
421
+ onChange={setProvider}
422
+ onModelChange={setModel}
423
  open={openProvider}
424
  error={providerError}
425
  isFollowUp={!isSameHtml && isFollowUp}
426
  onClose={setOpenProvider}
427
  />
 
 
 
 
 
428
  <Button
429
  size="iconXs"
430
+ disabled={isAiWorking || !prompt.trim()}
 
 
 
 
431
  onClick={() => callAi()}
432
  >
433
  <ArrowUp className="size-4" />
434
  </Button>
435
  </div>
436
  </div>
437
+ <LoginModal open={open} onClose={() => setOpen(false)} html={html} />
438
+ <ProModal
439
+ html={html}
440
+ open={openProModal}
441
+ onClose={() => setOpenProModal(false)}
442
+ />
443
+ {!isSameHtml && (
444
+ <div className="absolute top-0 right-0 -translate-y-[calc(100%+8px)] select-none text-xs text-neutral-400 flex items-center justify-center gap-2 bg-neutral-800 border border-neutral-700 rounded-md p-1 pr-2.5">
445
+ <label
446
+ htmlFor="diff-patch-checkbox"
447
+ className="flex items-center gap-1.5 cursor-pointer"
448
+ >
449
+ <Checkbox
450
+ id="diff-patch-checkbox"
451
+ checked={isFollowUp}
452
+ onCheckedChange={(e) => {
453
+ if (e === true && !isSameHtml && selectedModel?.isThinker) {
454
+ setModel(MODELS[0].value);
455
+ }
456
+ setIsFollowUp(e === true);
457
+ }}
458
+ />
459
+ Diff-Patch Update
460
+ </label>
461
+ <FollowUpTooltip />
462
+ </div>
463
+ )}
464
  </div>
465
+ <audio ref={audio} id="audio" className="hidden">
466
  <source src="/success.mp3" type="audio/mpeg" />
467
  Your browser does not support the audio element.
468
  </audio>
469
  </div>
470
  );
471
+ }
components/editor/ask-ai/loading.tsx DELETED
@@ -1,59 +0,0 @@
1
- import Loading from "@/components/loading";
2
- import { useState } from "react";
3
- import { useInterval } from "react-use";
4
-
5
- const TEXTS = [
6
- "Teaching pixels to dance with style...",
7
- "AI is having a creative breakthrough...",
8
- "Channeling digital vibes into pure code...",
9
- "Summoning the website spirits...",
10
- "Brewing some algorithmic magic...",
11
- "Composing a symphony of divs and spans...",
12
- "Riding the wave of computational creativity...",
13
- "Aligning the stars for perfect design...",
14
- "Training circus animals to write CSS...",
15
- "Launching ideas into the digital stratosphere...",
16
- ];
17
-
18
- export const AiLoading = ({
19
- text,
20
- className,
21
- }: {
22
- text?: string;
23
- className?: string;
24
- }) => {
25
- const [selectedText, setSelectedText] = useState(
26
- text ?? TEXTS[Math.floor(Math.random() * TEXTS.length)]
27
- );
28
- useInterval(() => {
29
- if (!text) {
30
- if (selectedText === TEXTS[TEXTS.length - 1]) {
31
- setSelectedText(TEXTS[0]);
32
- } else {
33
- setSelectedText(TEXTS[TEXTS.indexOf(selectedText) + 1]);
34
- }
35
- }
36
- }, 12000);
37
- return (
38
- <div className={`flex items-center justify-start gap-2 ${className}`}>
39
- <Loading overlay={false} className="!size-5 opacity-50" />
40
- <p className="text-neutral-400 text-sm">
41
- <span className="inline-flex">
42
- {selectedText.split("").map((char, index) => (
43
- <span
44
- key={index}
45
- className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
46
- style={{
47
- animationDelay: `${index * 0.1}s`,
48
- animationDuration: "1.3s",
49
- animationIterationCount: "infinite",
50
- }}
51
- >
52
- {char === " " ? "\u00A0" : char}
53
- </span>
54
- ))}
55
- </span>
56
- </p>
57
- </div>
58
- );
59
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/prompt-builder/content-modal.tsx DELETED
@@ -1,196 +0,0 @@
1
- import classNames from "classnames";
2
- import { ChevronRight, RefreshCcw } from "lucide-react";
3
- import { useState } from "react";
4
- import { TailwindColors } from "./tailwind-colors";
5
- import { Switch } from "@/components/ui/switch";
6
- import { Button } from "@/components/ui/button";
7
- import { Themes } from "./themes";
8
- import { EnhancedSettings } from "@/types";
9
-
10
- export const ContentModal = ({
11
- enhancedSettings,
12
- setEnhancedSettings,
13
- }: {
14
- enhancedSettings: EnhancedSettings;
15
- setEnhancedSettings: (settings: EnhancedSettings) => void;
16
- }) => {
17
- const [collapsed, setCollapsed] = useState(["colors", "theme"]);
18
- return (
19
- <main className="overflow-x-hidden max-h-[50dvh] overflow-y-auto">
20
- <section className="w-full border-b border-neutral-800/80 px-6 py-3.5 sticky top-0 bg-neutral-900 z-10">
21
- <div className="flex items-center justify-between gap-3">
22
- <p className="text-base font-semibold text-neutral-200">
23
- Allow DeepSite to enhance your prompt
24
- </p>
25
- <Switch
26
- checked={enhancedSettings.isActive}
27
- onCheckedChange={() =>
28
- setEnhancedSettings({
29
- ...enhancedSettings,
30
- isActive: !enhancedSettings.isActive,
31
- })
32
- }
33
- />
34
- </div>
35
- <p className="text-sm text-neutral-500 mt-2">
36
- While using DeepSite enhanced prompt, you'll get better results. We'll
37
- add more details and features to your request.
38
- </p>
39
- <div className="text-sm text-sky-500 mt-3 bg-gradient-to-r from-sky-400/15 to-purple-400/15 rounded-md px-3 py-2 border border-white/10">
40
- <p className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text">
41
- You can also use the custom properties below to set specific
42
- information.
43
- </p>
44
- </div>
45
- </section>
46
- <section className="py-3.5 border-b border-neutral-800/80">
47
- <div
48
- className={classNames(
49
- "flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
50
- {
51
- "!text-neutral-200": collapsed.includes("colors"),
52
- }
53
- )}
54
- onClick={() =>
55
- setCollapsed((prev) => {
56
- if (prev.includes("colors")) {
57
- return prev.filter((item) => item !== "colors");
58
- }
59
- return [...prev, "colors"];
60
- })
61
- }
62
- >
63
- <ChevronRight className="size-4" />
64
- <p className="text-base font-semibold">Colors</p>
65
- </div>
66
- {collapsed.includes("colors") && (
67
- <div className="mt-4 space-y-4">
68
- <article className="w-full">
69
- <div className="flex items-center justify-start gap-2 px-5">
70
- <p className="text-xs font-medium uppercase text-neutral-400">
71
- Primary Color
72
- </p>
73
- <Button
74
- variant="bordered"
75
- size="xss"
76
- className={`${
77
- enhancedSettings.primaryColor ? "" : "opacity-0"
78
- }`}
79
- onClick={() =>
80
- setEnhancedSettings({
81
- ...enhancedSettings,
82
- primaryColor: undefined,
83
- })
84
- }
85
- >
86
- <RefreshCcw className="size-2.5" />
87
- Reset
88
- </Button>
89
- </div>
90
- <div className="text-muted-foreground text-sm mt-4">
91
- <TailwindColors
92
- value={enhancedSettings.primaryColor}
93
- onChange={(value) =>
94
- setEnhancedSettings({
95
- ...enhancedSettings,
96
- primaryColor: value,
97
- })
98
- }
99
- />
100
- </div>
101
- </article>
102
- <article className="w-full">
103
- <div className="flex items-center justify-start gap-2 px-5">
104
- <p className="text-xs font-medium uppercase text-neutral-400">
105
- Secondary Color
106
- </p>
107
- <Button
108
- variant="bordered"
109
- size="xss"
110
- className={`${
111
- enhancedSettings.secondaryColor ? "" : "opacity-0"
112
- }`}
113
- onClick={() =>
114
- setEnhancedSettings({
115
- ...enhancedSettings,
116
- secondaryColor: undefined,
117
- })
118
- }
119
- >
120
- <RefreshCcw className="size-2.5" />
121
- Reset
122
- </Button>
123
- </div>
124
- <div className="text-muted-foreground text-sm mt-4">
125
- <TailwindColors
126
- value={enhancedSettings.secondaryColor}
127
- onChange={(value) =>
128
- setEnhancedSettings({
129
- ...enhancedSettings,
130
- secondaryColor: value,
131
- })
132
- }
133
- />
134
- </div>
135
- </article>
136
- </div>
137
- )}
138
- </section>
139
- <section className="py-3.5 border-b border-neutral-800/80">
140
- <div
141
- className={classNames(
142
- "flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
143
- {
144
- "!text-neutral-200": collapsed.includes("theme"),
145
- }
146
- )}
147
- onClick={() =>
148
- setCollapsed((prev) => {
149
- if (prev.includes("theme")) {
150
- return prev.filter((item) => item !== "theme");
151
- }
152
- return [...prev, "theme"];
153
- })
154
- }
155
- >
156
- <ChevronRight className="size-4" />
157
- <p className="text-base font-semibold">Theme</p>
158
- </div>
159
- {collapsed.includes("theme") && (
160
- <article className="w-full mt-4">
161
- <div className="flex items-center justify-start gap-2 px-5">
162
- <p className="text-xs font-medium uppercase text-neutral-400">
163
- Theme
164
- </p>
165
- <Button
166
- variant="bordered"
167
- size="xss"
168
- className={`${enhancedSettings.theme ? "" : "opacity-0"}`}
169
- onClick={() =>
170
- setEnhancedSettings({
171
- ...enhancedSettings,
172
- theme: undefined,
173
- })
174
- }
175
- >
176
- <RefreshCcw className="size-2.5" />
177
- Reset
178
- </Button>
179
- </div>
180
- <div className="text-muted-foreground text-sm mt-4">
181
- <Themes
182
- value={enhancedSettings.theme}
183
- onChange={(value) =>
184
- setEnhancedSettings({
185
- ...enhancedSettings,
186
- theme: value,
187
- })
188
- }
189
- />
190
- </div>
191
- </article>
192
- )}
193
- </section>
194
- </main>
195
- );
196
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/prompt-builder/index.tsx DELETED
@@ -1,73 +0,0 @@
1
- import { useState } from "react";
2
- import { WandSparkles } from "lucide-react";
3
-
4
- import { Button } from "@/components/ui/button";
5
- import { useEditor } from "@/hooks/useEditor";
6
- import { useAi } from "@/hooks/useAi";
7
- import {
8
- Dialog,
9
- DialogContent,
10
- DialogFooter,
11
- DialogTitle,
12
- } from "@/components/ui/dialog";
13
- import { ContentModal } from "./content-modal";
14
- import { useLoginModal } from "@/components/contexts/login-context";
15
- import { useUser } from "@/hooks/useUser";
16
- import { EnhancedSettings } from "@/types";
17
-
18
- export const PromptBuilder = ({
19
- enhancedSettings,
20
- setEnhancedSettings,
21
- }: {
22
- enhancedSettings: EnhancedSettings;
23
- setEnhancedSettings: (settings: EnhancedSettings) => void;
24
- }) => {
25
- const { user } = useUser();
26
- const { openLoginModal } = useLoginModal();
27
- const { globalAiLoading } = useAi();
28
- const { globalEditorLoading } = useEditor();
29
-
30
- const [open, setOpen] = useState(false);
31
- return (
32
- <>
33
- <Button
34
- size="xs"
35
- variant="outline"
36
- className="!rounded-md !border-white/10 !bg-gradient-to-r from-sky-400/15 to-purple-400/15 light-sweep hover:brightness-110"
37
- disabled={globalAiLoading || globalEditorLoading}
38
- onClick={() => {
39
- if (!user) return openLoginModal();
40
- setOpen(true);
41
- }}
42
- >
43
- <WandSparkles className="size-3.5 text-sky-500 relative z-10" />
44
- <span className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text relative z-10">
45
- Enhance
46
- </span>
47
- </Button>
48
- <Dialog open={open} onOpenChange={() => setOpen(false)}>
49
- <DialogContent className="sm:max-w-xl !p-0 !rounded-3xl !bg-neutral-900 !border-neutral-800/80 !gap-0">
50
- <DialogTitle className="px-6 py-3.5 border-b border-neutral-800">
51
- <div className="flex items-center justify-start gap-2 text-neutral-200 text-base font-medium">
52
- <WandSparkles className="size-3.5" />
53
- <p>Enhance Prompt</p>
54
- </div>
55
- </DialogTitle>
56
- <ContentModal
57
- enhancedSettings={enhancedSettings}
58
- setEnhancedSettings={setEnhancedSettings}
59
- />
60
- <DialogFooter className="px-6 py-3.5 border-t border-neutral-800">
61
- <Button
62
- variant="bordered"
63
- size="default"
64
- onClick={() => setOpen(false)}
65
- >
66
- Close
67
- </Button>
68
- </DialogFooter>
69
- </DialogContent>
70
- </Dialog>
71
- </>
72
- );
73
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/prompt-builder/tailwind-colors.tsx DELETED
@@ -1,58 +0,0 @@
1
- import classNames from "classnames";
2
- import { useRef } from "react";
3
-
4
- import { TAILWIND_COLORS } from "@/lib/prompt-builder";
5
- import { useMount } from "react-use";
6
-
7
- export const TailwindColors = ({
8
- value,
9
- onChange,
10
- }: {
11
- value: string | undefined;
12
- onChange: (value: string) => void;
13
- }) => {
14
- const ref = useRef<HTMLDivElement>(null);
15
-
16
- useMount(() => {
17
- if (ref.current) {
18
- if (value) {
19
- const color = ref.current.querySelector(`[data-color="${value}"]`);
20
- if (color) {
21
- color.scrollIntoView({ inline: "center" });
22
- }
23
- }
24
- }
25
- });
26
- return (
27
- <div
28
- ref={ref}
29
- className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
30
- >
31
- {TAILWIND_COLORS.map((color) => (
32
- <div
33
- key={color}
34
- className={classNames(
35
- "flex flex-col items-center justify-center p-3 size-16 min-w-16 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
36
- {
37
- "!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
38
- value === color,
39
- }
40
- )}
41
- data-color={color}
42
- onClick={() => onChange(color)}
43
- >
44
- <div
45
- className={`w-4 h-4 min-w-4 min-h-4 rounded-xl ${
46
- ["white", "black"].includes(color)
47
- ? `bg-${color}`
48
- : `bg-${color}-500`
49
- }`}
50
- />
51
- <p className="text-xs capitalize text-neutral-200 truncate">
52
- {color}
53
- </p>
54
- </div>
55
- ))}
56
- </div>
57
- );
58
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/prompt-builder/themes.tsx DELETED
@@ -1,48 +0,0 @@
1
- import { Theme } from "@/types";
2
- import classNames from "classnames";
3
- import { Moon, Sun } from "lucide-react";
4
- import { useRef } from "react";
5
-
6
- export const Themes = ({
7
- value,
8
- onChange,
9
- }: {
10
- value: Theme;
11
- onChange: (value: Theme) => void;
12
- }) => {
13
- const ref = useRef<HTMLDivElement>(null);
14
-
15
- return (
16
- <div
17
- ref={ref}
18
- className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
19
- >
20
- <div
21
- className={classNames(
22
- "flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
23
- {
24
- "!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
25
- value === "light",
26
- }
27
- )}
28
- onClick={() => onChange("light")}
29
- >
30
- <Sun className="size-4 text-amber-500" />
31
- <p className="text-xs capitalize text-neutral-200 truncate">Light</p>
32
- </div>
33
- <div
34
- className={classNames(
35
- "flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
36
- {
37
- "!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
38
- value === "dark",
39
- }
40
- )}
41
- onClick={() => onChange("dark")}
42
- >
43
- <Moon className="size-4 text-indigo-500" />
44
- <p className="text-xs capitalize text-neutral-200 truncate">Dark</p>
45
- </div>
46
- </div>
47
- );
48
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/re-imagine.tsx CHANGED
@@ -11,10 +11,6 @@ import {
11
  import { Input } from "@/components/ui/input";
12
  import Loading from "@/components/loading";
13
  import { api } from "@/lib/api";
14
- import { useAi } from "@/hooks/useAi";
15
- import { useEditor } from "@/hooks/useEditor";
16
- import { useUser } from "@/hooks/useUser";
17
- import { useLoginModal } from "@/components/contexts/login-context";
18
 
19
  export function ReImagine({
20
  onRedesign,
@@ -24,10 +20,6 @@ export function ReImagine({
24
  const [url, setUrl] = useState<string>("");
25
  const [open, setOpen] = useState(false);
26
  const [isLoading, setIsLoading] = useState(false);
27
- const { globalAiLoading } = useAi();
28
- const { globalEditorLoading } = useEditor();
29
- const { user } = useUser();
30
- const { openLoginModal } = useLoginModal();
31
 
32
  const checkIfUrlIsValid = (url: string) => {
33
  const urlPattern = new RegExp(
@@ -62,31 +54,16 @@ export function ReImagine({
62
  setIsLoading(false);
63
  };
64
 
65
- if (!user)
66
- return (
67
- <Button
68
- size="xs"
69
- variant="outline"
70
- className="!rounded-md"
71
- onClick={() => openLoginModal()}
72
- >
73
- <Paintbrush className="size-3.5" />
74
- Redesign
75
- </Button>
76
- );
77
-
78
  return (
79
  <Popover open={open} onOpenChange={setOpen}>
80
  <form>
81
  <PopoverTrigger asChild>
82
  <Button
83
- size="xs"
84
- variant={open ? "default" : "outline"}
85
- className="!rounded-md"
86
- disabled={globalAiLoading || globalEditorLoading}
87
  >
88
- <Paintbrush className="size-3.5" />
89
- Redesign
90
  </Button>
91
  </PopoverTrigger>
92
  <PopoverContent
 
11
  import { Input } from "@/components/ui/input";
12
  import Loading from "@/components/loading";
13
  import { api } from "@/lib/api";
 
 
 
 
14
 
15
  export function ReImagine({
16
  onRedesign,
 
20
  const [url, setUrl] = useState<string>("");
21
  const [open, setOpen] = useState(false);
22
  const [isLoading, setIsLoading] = useState(false);
 
 
 
 
23
 
24
  const checkIfUrlIsValid = (url: string) => {
25
  const urlPattern = new RegExp(
 
54
  setIsLoading(false);
55
  };
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  return (
58
  <Popover open={open} onOpenChange={setOpen}>
59
  <form>
60
  <PopoverTrigger asChild>
61
  <Button
62
+ size="iconXs"
63
+ variant="outline"
64
+ className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
 
65
  >
66
+ <Paintbrush className="size-4" />
 
67
  </Button>
68
  </PopoverTrigger>
69
  <PopoverContent
components/editor/ask-ai/selected-files.tsx DELETED
@@ -1,47 +0,0 @@
1
- import Image from "next/image";
2
-
3
- import { Button } from "@/components/ui/button";
4
- import { Minus } from "lucide-react";
5
-
6
- export const SelectedFiles = ({
7
- files,
8
- isAiWorking,
9
- onDelete,
10
- }: {
11
- files: string[];
12
- isAiWorking: boolean;
13
- onDelete: (file: string) => void;
14
- }) => {
15
- if (files.length === 0) return null;
16
- return (
17
- <div className="px-4 pt-3">
18
- <div className="flex items-center justify-start gap-2">
19
- {files.map((file) => (
20
- <div
21
- key={file}
22
- className="flex items-center relative justify-start gap-2 p-1 bg-neutral-700 rounded-md"
23
- >
24
- <Image
25
- src={file}
26
- alt="uploaded image"
27
- className="size-12 rounded-md object-cover"
28
- width={40}
29
- height={40}
30
- />
31
- <Button
32
- size="iconXsss"
33
- variant="secondary"
34
- className={`absolute top-0.5 right-0.5 ${
35
- isAiWorking ? "opacity-50 !cursor-not-allowed" : ""
36
- }`}
37
- disabled={isAiWorking}
38
- onClick={() => onDelete(file)}
39
- >
40
- <Minus className="size-4" />
41
- </Button>
42
- </div>
43
- ))}
44
- </div>
45
- </div>
46
- );
47
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/selector.tsx DELETED
@@ -1,41 +0,0 @@
1
- import classNames from "classnames";
2
- import { Crosshair } from "lucide-react";
3
-
4
- import { Button } from "@/components/ui/button";
5
- import {
6
- Tooltip,
7
- TooltipContent,
8
- TooltipTrigger,
9
- } from "@/components/ui/tooltip";
10
- import { useAi } from "@/hooks/useAi";
11
- import { useEditor } from "@/hooks/useEditor";
12
-
13
- export const Selector = () => {
14
- const { globalEditorLoading } = useEditor();
15
- const { isEditableModeEnabled, setIsEditableModeEnabled, globalAiLoading } =
16
- useAi();
17
- return (
18
- <Tooltip>
19
- <TooltipTrigger asChild>
20
- <Button
21
- size="xs"
22
- variant={isEditableModeEnabled ? "default" : "outline"}
23
- onClick={() => {
24
- setIsEditableModeEnabled?.(!isEditableModeEnabled);
25
- }}
26
- disabled={globalAiLoading || globalEditorLoading}
27
- className="!rounded-md"
28
- >
29
- <Crosshair className="size-3.5" />
30
- Edit
31
- </Button>
32
- </TooltipTrigger>
33
- <TooltipContent
34
- align="start"
35
- className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
36
- >
37
- Select an element on the page to ask DeepSite edit it directly.
38
- </TooltipContent>
39
- </Tooltip>
40
- );
41
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/settings.tsx CHANGED
@@ -1,5 +1,6 @@
1
- "use client";
2
  import classNames from "classnames";
 
 
3
 
4
  import {
5
  Popover,
@@ -17,37 +18,29 @@ import {
17
  SelectTrigger,
18
  SelectValue,
19
  } from "@/components/ui/select";
20
- import { useMemo, useState, useEffect } from "react";
21
  import { useUpdateEffect } from "react-use";
22
  import Image from "next/image";
23
- import { Brain, CheckCheck, ChevronDown } from "lucide-react";
24
- import { useAi } from "@/hooks/useAi";
25
 
26
  export function Settings({
27
  open,
28
  onClose,
 
 
29
  error,
30
  isFollowUp = false,
 
 
31
  }: {
32
  open: boolean;
 
 
33
  error?: string;
34
  isFollowUp?: boolean;
35
  onClose: React.Dispatch<React.SetStateAction<boolean>>;
 
 
36
  }) {
37
- const {
38
- model,
39
- provider,
40
- setProvider,
41
- setModel,
42
- selectedModel,
43
- globalAiLoading,
44
- } = useAi();
45
- const [isMounted, setIsMounted] = useState(false);
46
-
47
- useEffect(() => {
48
- setIsMounted(true);
49
- }, []);
50
-
51
  const modelAvailableProviders = useMemo(() => {
52
  const availableProviders = MODELS.find(
53
  (m: { value: string }) => m.value === model
@@ -59,162 +52,153 @@ export function Settings({
59
  }, [model]);
60
 
61
  useUpdateEffect(() => {
62
- if (
63
- provider !== "auto" &&
64
- !modelAvailableProviders.includes(provider as string)
65
- ) {
66
- setProvider("auto");
67
  }
68
  }, [model, provider]);
69
 
70
  return (
71
- <Popover open={open} onOpenChange={onClose}>
72
- <PopoverTrigger asChild>
73
- <Button
74
- variant={open ? "default" : "outline"}
75
- className="!rounded-md"
76
- disabled={globalAiLoading}
77
- size="xs"
 
 
 
 
78
  >
79
- <Brain className="size-3.5" />
80
- <span className="truncate max-w-[120px]">
81
- {isMounted
82
- ? selectedModel?.label?.split(" ").join("-").toLowerCase()
83
- : "..."}
84
- </span>
85
- <ChevronDown className="size-3.5" />
86
- </Button>
87
- </PopoverTrigger>
88
- <PopoverContent
89
- className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
90
- align="center"
91
- >
92
- <header className="flex items-center justify-center text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
93
- Customize Settings
94
- </header>
95
- <main className="px-4 pt-5 pb-6 space-y-5">
96
- {error !== "" && (
97
- <p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
98
- {error}
99
- </p>
100
- )}
101
- <label className="block">
102
- <p className="text-neutral-300 text-sm mb-2.5">Choose a model</p>
103
- <Select defaultValue={model} onValueChange={setModel}>
104
- <SelectTrigger className="w-full">
105
- <SelectValue placeholder="Select a model" />
106
- </SelectTrigger>
107
- <SelectContent>
108
- <SelectGroup>
109
- <SelectLabel>Models</SelectLabel>
110
- {MODELS.map(
111
- ({
112
- value,
113
- label,
114
- isNew = false,
115
- isThinker = false,
116
- }: {
117
- value: string;
118
- label: string;
119
- isNew?: boolean;
120
- isThinker?: boolean;
121
- }) => (
122
- <SelectItem
123
- key={value}
124
- value={value}
125
- className=""
126
- disabled={isThinker && isFollowUp}
127
- >
128
- {label}
129
- {isNew && (
130
- <span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
131
- New
132
- </span>
133
- )}
134
- </SelectItem>
135
- )
136
- )}
137
- </SelectGroup>
138
- </SelectContent>
139
- </Select>
140
- </label>
141
- {isFollowUp && (
142
- <div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
143
- Note: You can&apos;t use a Thinker model for follow-up requests.
144
- We automatically switch to the default model for you.
145
- </div>
146
- )}
147
- <div className="flex flex-col gap-3">
148
- <div className="flex items-center justify-between">
149
- <div>
150
- <p className="text-neutral-300 text-sm mb-1.5">
151
- Use auto-provider
152
- </p>
153
- <p className="text-xs text-neutral-400/70">
154
- We&apos;ll automatically select the best provider for you
155
- based on your prompt.
156
- </p>
157
  </div>
158
- <div
159
- className={classNames(
160
- "bg-neutral-700 rounded-full min-w-10 w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
161
- {
162
- "!bg-sky-500": provider === "auto",
163
- }
164
- )}
165
- onClick={() => {
166
- const foundModel = MODELS.find(
167
- (m: { value: string }) => m.value === model
168
- );
169
- if (provider === "auto" && foundModel?.autoProvider) {
170
- setProvider(foundModel.autoProvider);
171
- } else {
172
- setProvider("auto");
173
- }
174
- }}
175
- >
176
  <div
177
  className={classNames(
178
- "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
179
  {
180
- "translate-x-4": provider === "auto",
181
  }
182
  )}
183
- />
184
- </div>
185
- </div>
186
- <label className="block">
187
- <p className="text-neutral-300 text-sm mb-2">
188
- Inference Provider
189
- </p>
190
- <div className="grid grid-cols-2 gap-1.5">
191
- {modelAvailableProviders.map((id: string) => (
192
- <Button
193
- key={id}
194
- variant={id === provider ? "default" : "secondary"}
195
- size="sm"
196
- onClick={() => {
197
- setProvider(id);
198
- }}
199
- >
200
- <Image
201
- src={`/providers/${id}.svg`}
202
- alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
203
- className="size-5 mr-2"
204
- width={20}
205
- height={20}
206
- />
207
- {PROVIDERS[id as keyof typeof PROVIDERS].name}
208
- {id === provider && (
209
- <CheckCheck className="ml-2 size-4 text-blue-500" />
210
  )}
211
- </Button>
212
- ))}
213
  </div>
214
- </label>
215
- </div>
216
- </main>
217
- </PopoverContent>
218
- </Popover>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  );
220
  }
 
 
1
  import classNames from "classnames";
2
+ import { PiGearSixFill } from "react-icons/pi";
3
+ import { RiCheckboxCircleFill } from "react-icons/ri";
4
 
5
  import {
6
  Popover,
 
18
  SelectTrigger,
19
  SelectValue,
20
  } from "@/components/ui/select";
21
+ import { useMemo } from "react";
22
  import { useUpdateEffect } from "react-use";
23
  import Image from "next/image";
 
 
24
 
25
  export function Settings({
26
  open,
27
  onClose,
28
+ provider,
29
+ model,
30
  error,
31
  isFollowUp = false,
32
+ onChange,
33
+ onModelChange,
34
  }: {
35
  open: boolean;
36
+ provider: string;
37
+ model: string;
38
  error?: string;
39
  isFollowUp?: boolean;
40
  onClose: React.Dispatch<React.SetStateAction<boolean>>;
41
+ onChange: (provider: string) => void;
42
+ onModelChange: (model: string) => void;
43
  }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  const modelAvailableProviders = useMemo(() => {
45
  const availableProviders = MODELS.find(
46
  (m: { value: string }) => m.value === model
 
52
  }, [model]);
53
 
54
  useUpdateEffect(() => {
55
+ if (provider !== "auto" && !modelAvailableProviders.includes(provider)) {
56
+ onChange("auto");
 
 
 
57
  }
58
  }, [model, provider]);
59
 
60
  return (
61
+ <div className="">
62
+ <Popover open={open} onOpenChange={onClose}>
63
+ <PopoverTrigger asChild>
64
+ <Button variant="black" size="sm">
65
+ <PiGearSixFill className="size-4" />
66
+ Settings
67
+ </Button>
68
+ </PopoverTrigger>
69
+ <PopoverContent
70
+ className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
71
+ align="center"
72
  >
73
+ <header className="flex items-center justify-center text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
74
+ Customize Settings
75
+ </header>
76
+ <main className="px-4 pt-5 pb-6 space-y-5">
77
+ {error !== "" && (
78
+ <p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
79
+ {error}
80
+ </p>
81
+ )}
82
+ <label className="block">
83
+ <p className="text-neutral-300 text-sm mb-2.5">
84
+ Choose a DeepSeek model
85
+ </p>
86
+ <Select defaultValue={model} onValueChange={onModelChange}>
87
+ <SelectTrigger className="w-full">
88
+ <SelectValue placeholder="Select a DeepSeek model" />
89
+ </SelectTrigger>
90
+ <SelectContent>
91
+ <SelectGroup>
92
+ <SelectLabel>DeepSeek models</SelectLabel>
93
+ {MODELS.map(
94
+ ({
95
+ value,
96
+ label,
97
+ isNew = false,
98
+ isThinker = false,
99
+ }: {
100
+ value: string;
101
+ label: string;
102
+ isNew?: boolean;
103
+ isThinker?: boolean;
104
+ }) => (
105
+ <SelectItem
106
+ key={value}
107
+ value={value}
108
+ className=""
109
+ disabled={isThinker && isFollowUp}
110
+ >
111
+ {label}
112
+ {isNew && (
113
+ <span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
114
+ New
115
+ </span>
116
+ )}
117
+ </SelectItem>
118
+ )
119
+ )}
120
+ </SelectGroup>
121
+ </SelectContent>
122
+ </Select>
123
+ </label>
124
+ {isFollowUp && (
125
+ <div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
126
+ Note: You can&apos;t use a Thinker model for follow-up requests.
127
+ We automatically switch to the default model for you.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  </div>
129
+ )}
130
+ <div className="flex flex-col gap-3">
131
+ <div className="flex items-center justify-between">
132
+ <div>
133
+ <p className="text-neutral-300 text-sm mb-1.5">
134
+ Use auto-provider
135
+ </p>
136
+ <p className="text-xs text-neutral-400/70">
137
+ We&apos;ll automatically select the best provider for you
138
+ based on your prompt.
139
+ </p>
140
+ </div>
 
 
 
 
 
 
141
  <div
142
  className={classNames(
143
+ "bg-neutral-700 rounded-full min-w-10 w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
144
  {
145
+ "!bg-sky-500": provider === "auto",
146
  }
147
  )}
148
+ onClick={() => {
149
+ const foundModel = MODELS.find(
150
+ (m: { value: string }) => m.value === model
151
+ );
152
+ if (provider === "auto" && foundModel?.autoProvider) {
153
+ onChange(foundModel.autoProvider);
154
+ } else {
155
+ onChange("auto");
156
+ }
157
+ }}
158
+ >
159
+ <div
160
+ className={classNames(
161
+ "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
162
+ {
163
+ "translate-x-4": provider === "auto",
164
+ }
 
 
 
 
 
 
 
 
 
 
165
  )}
166
+ />
167
+ </div>
168
  </div>
169
+ <label className="block">
170
+ <p className="text-neutral-300 text-sm mb-2">
171
+ Inference Provider
172
+ </p>
173
+ <div className="grid grid-cols-2 gap-1.5">
174
+ {modelAvailableProviders.map((id: string) => (
175
+ <Button
176
+ key={id}
177
+ variant={id === provider ? "default" : "secondary"}
178
+ size="sm"
179
+ onClick={() => {
180
+ onChange(id);
181
+ }}
182
+ >
183
+ <Image
184
+ src={`/providers/${id}.svg`}
185
+ alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
186
+ className="size-5 mr-2"
187
+ width={20}
188
+ height={20}
189
+ />
190
+ {PROVIDERS[id as keyof typeof PROVIDERS].name}
191
+ {id === provider && (
192
+ <RiCheckboxCircleFill className="ml-2 size-4 text-blue-500" />
193
+ )}
194
+ </Button>
195
+ ))}
196
+ </div>
197
+ </label>
198
+ </div>
199
+ </main>
200
+ </PopoverContent>
201
+ </Popover>
202
+ </div>
203
  );
204
  }
components/editor/ask-ai/uploader.tsx DELETED
@@ -1,165 +0,0 @@
1
- import { useRef, useState } from "react";
2
- import {
3
- CheckCircle,
4
- ImageIcon,
5
- Images,
6
- Link,
7
- Paperclip,
8
- Upload,
9
- } from "lucide-react";
10
- import Image from "next/image";
11
-
12
- import {
13
- Popover,
14
- PopoverContent,
15
- PopoverTrigger,
16
- } from "@/components/ui/popover";
17
- import { Button } from "@/components/ui/button";
18
- import { Project } from "@/types";
19
- import Loading from "@/components/loading";
20
- import { useUser } from "@/hooks/useUser";
21
- import { useEditor } from "@/hooks/useEditor";
22
- import { useAi } from "@/hooks/useAi";
23
- import { useLoginModal } from "@/components/contexts/login-context";
24
-
25
- export const Uploader = ({ project }: { project: Project | undefined }) => {
26
- const { user } = useUser();
27
- const { openLoginModal } = useLoginModal();
28
- const { uploadFiles, isUploading, files, globalEditorLoading } = useEditor();
29
- const { selectedFiles, setSelectedFiles, globalAiLoading } = useAi();
30
-
31
- const [open, setOpen] = useState(false);
32
- const fileInputRef = useRef<HTMLInputElement>(null);
33
-
34
- if (!user)
35
- return (
36
- <Button
37
- size="xs"
38
- variant="outline"
39
- className="!rounded-md"
40
- disabled={globalAiLoading || globalEditorLoading}
41
- onClick={() => openLoginModal()}
42
- >
43
- <Paperclip className="size-3.5" />
44
- Attach
45
- </Button>
46
- );
47
-
48
- return (
49
- <Popover open={open} onOpenChange={setOpen}>
50
- <form className="h-[24px]">
51
- <PopoverTrigger asChild>
52
- <Button
53
- size="xs"
54
- variant={open ? "default" : "outline"}
55
- className="!rounded-md"
56
- disabled={globalAiLoading || globalEditorLoading}
57
- >
58
- <Paperclip className="size-3.5" />
59
- Attach
60
- </Button>
61
- </PopoverTrigger>
62
- <PopoverContent
63
- align="start"
64
- className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
65
- >
66
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
67
- <div className="flex items-center justify-center -space-x-4 mb-3">
68
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
69
- 🎨
70
- </div>
71
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
72
- 🖼️
73
- </div>
74
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
75
- 💻
76
- </div>
77
- </div>
78
- <p className="text-xl font-semibold text-neutral-950">
79
- Add Custom Images
80
- </p>
81
- <p className="text-sm text-neutral-500 mt-1.5">
82
- Upload images to your project and use them with DeepSite!
83
- </p>
84
- </header>
85
- <main className="space-y-4 p-5">
86
- <div>
87
- <p className="text-xs text-left text-neutral-700 mb-2">
88
- Uploaded Images
89
- </p>
90
- {files?.length > 0 ? (
91
- <div className="grid grid-cols-4 gap-1 flex-wrap max-h-40 overflow-y-auto">
92
- {files.map((file: string) => (
93
- <div
94
- key={file}
95
- className="select-none relative cursor-pointer bg-white rounded-md border-[2px] border-white hover:shadow-2xl transition-all duration-300"
96
- onClick={() =>
97
- setSelectedFiles(
98
- selectedFiles.includes(file)
99
- ? selectedFiles.filter((f) => f !== file)
100
- : [...selectedFiles, file]
101
- )
102
- }
103
- >
104
- <Image
105
- src={file}
106
- alt="uploaded image"
107
- width={56}
108
- height={56}
109
- className="object-cover w-full rounded-sm aspect-square"
110
- />
111
- {selectedFiles.includes(file) && (
112
- <div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
113
- <CheckCircle className="size-6 text-neutral-100" />
114
- </div>
115
- )}
116
- </div>
117
- ))}
118
- </div>
119
- ) : (
120
- <p className="text-sm text-muted-foreground font-mono flex flex-col items-center gap-1 pt-2">
121
- <ImageIcon className="size-4" />
122
- No images uploaded yet
123
- </p>
124
- )}
125
- </div>
126
- <div>
127
- <p className="text-xs text-left text-neutral-700 mb-2">
128
- Or import images from your computer
129
- </p>
130
- <Button
131
- variant="black"
132
- onClick={() => fileInputRef.current?.click()}
133
- className="relative w-full"
134
- disabled={isUploading}
135
- >
136
- {isUploading ? (
137
- <>
138
- <Loading
139
- overlay={false}
140
- className="ml-2 size-4 animate-spin"
141
- />
142
- Uploading image(s)...
143
- </>
144
- ) : (
145
- <>
146
- <Upload className="size-4" />
147
- Upload Images
148
- </>
149
- )}
150
- </Button>
151
- <input
152
- ref={fileInputRef}
153
- type="file"
154
- className="hidden"
155
- multiple
156
- accept="image/*"
157
- onChange={(e) => uploadFiles(e.target.files, project!)}
158
- />
159
- </div>
160
- </main>
161
- </PopoverContent>
162
- </form>
163
- </Popover>
164
- );
165
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/deploy-button/index.tsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useState } from "react";
3
+ import { toast } from "sonner";
4
+ import Image from "next/image";
5
+ import { useRouter } from "next/navigation";
6
+ import { MdSave } from "react-icons/md";
7
+ import { Rocket } from "lucide-react";
8
+
9
+ import SpaceIcon from "@/assets/space.svg";
10
+ import Loading from "@/components/loading";
11
+ import { Button } from "@/components/ui/button";
12
+ import {
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from "@/components/ui/popover";
17
+ import { Input } from "@/components/ui/input";
18
+ import { api } from "@/lib/api";
19
+ import { LoginModal } from "@/components/login-modal";
20
+ import { useUser } from "@/hooks/useUser";
21
+
22
+ export function DeployButton({
23
+ html,
24
+ prompts,
25
+ }: {
26
+ html: string;
27
+ prompts: string[];
28
+ }) {
29
+ const router = useRouter();
30
+ const { user } = useUser();
31
+ const [loading, setLoading] = useState(false);
32
+ const [open, setOpen] = useState(false);
33
+
34
+ const [config, setConfig] = useState({
35
+ title: "",
36
+ });
37
+
38
+ const createSpace = async () => {
39
+ if (!config.title) {
40
+ toast.error("Please enter a title for your space.");
41
+ return;
42
+ }
43
+ setLoading(true);
44
+
45
+ try {
46
+ const res = await api.post("/me/projects", {
47
+ title: config.title,
48
+ html,
49
+ prompts,
50
+ });
51
+ if (res.data.ok) {
52
+ router.push(`/projects/${res.data.path}?deploy=true`);
53
+ } else {
54
+ toast.error(res?.data?.error || "Failed to create space");
55
+ }
56
+ } catch (err: any) {
57
+ toast.error(err.response?.data?.error || err.message);
58
+ } finally {
59
+ setLoading(false);
60
+ }
61
+ };
62
+
63
+ // TODO add a way to do not allow people to deploy if the html is broken.
64
+
65
+ return (
66
+ <div className="flex items-center justify-end gap-5">
67
+ <div className="relative flex items-center justify-end">
68
+ {user?.id ? (
69
+ <Popover>
70
+ <PopoverTrigger asChild>
71
+ <div>
72
+ <Button variant="default" className="max-lg:hidden !px-4">
73
+ <MdSave className="size-4" />
74
+ Deploy your Project
75
+ </Button>
76
+ <Button variant="default" size="sm" className="lg:hidden">
77
+ Deploy
78
+ </Button>
79
+ </div>
80
+ </PopoverTrigger>
81
+ <PopoverContent
82
+ className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
83
+ align="end"
84
+ >
85
+ <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
86
+ <div className="flex items-center justify-center -space-x-4 mb-3">
87
+ <div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
88
+ 🚀
89
+ </div>
90
+ <div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
91
+ <Image
92
+ src={SpaceIcon}
93
+ alt="Space Icon"
94
+ className="size-7"
95
+ />
96
+ </div>
97
+ <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
98
+ 👻
99
+ </div>
100
+ </div>
101
+ <p className="text-xl font-semibold text-neutral-950">
102
+ Deploy as Space!
103
+ </p>
104
+ <p className="text-sm text-neutral-500 mt-1.5">
105
+ Save and Deploy your project to a Space on the Hub. Spaces are
106
+ a way to share your project with the world.
107
+ </p>
108
+ </header>
109
+ <main className="space-y-4 p-6">
110
+ <div>
111
+ <p className="text-sm text-neutral-700 mb-2">
112
+ Choose a title for your space:
113
+ </p>
114
+ <Input
115
+ type="text"
116
+ placeholder="My Awesome Website"
117
+ value={config.title}
118
+ onChange={(e) =>
119
+ setConfig({ ...config, title: e.target.value })
120
+ }
121
+ className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
122
+ />
123
+ </div>
124
+ <div>
125
+ <p className="text-sm text-neutral-700 mb-2">
126
+ Then, let&apos;s deploy it!
127
+ </p>
128
+ <Button
129
+ variant="black"
130
+ onClick={createSpace}
131
+ className="relative w-full"
132
+ disabled={loading}
133
+ >
134
+ Deploy Space <Rocket className="size-4" />
135
+ {loading && (
136
+ <Loading className="ml-2 size-4 animate-spin" />
137
+ )}
138
+ </Button>
139
+ </div>
140
+ </main>
141
+ </PopoverContent>
142
+ </Popover>
143
+ ) : (
144
+ <>
145
+ <Button
146
+ variant="default"
147
+ className="max-lg:hidden !px-4"
148
+ onClick={() => setOpen(true)}
149
+ >
150
+ <MdSave className="size-4" />
151
+ Save your Project
152
+ </Button>
153
+ <Button
154
+ variant="default"
155
+ size="sm"
156
+ className="lg:hidden"
157
+ onClick={() => setOpen(true)}
158
+ >
159
+ Save
160
+ </Button>
161
+ </>
162
+ )}
163
+ <LoginModal
164
+ open={open}
165
+ onClose={() => setOpen(false)}
166
+ html={html}
167
+ title="Log In to save your Project"
168
+ description="Log In through your Hugging Face account to save your project and increase your monthly free limit."
169
+ />
170
+ </div>
171
+ </div>
172
+ );
173
+ }
components/editor/footer/index.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { FaMobileAlt } from "react-icons/fa";
3
+ import { HelpCircle, RefreshCcw, SparkleIcon } from "lucide-react";
4
+ import { FaLaptopCode } from "react-icons/fa6";
5
+ import { HtmlHistory } from "@/types";
6
+ import { Button } from "@/components/ui/button";
7
+ import { MdAdd } from "react-icons/md";
8
+ import { History } from "@/components/editor/history";
9
+ import { UserMenu } from "@/components/user-menu";
10
+ import { useUser } from "@/hooks/useUser";
11
+
12
+ const DEVICES = [
13
+ {
14
+ name: "desktop",
15
+ icon: FaLaptopCode,
16
+ },
17
+ {
18
+ name: "mobile",
19
+ icon: FaMobileAlt,
20
+ },
21
+ ];
22
+
23
+ export function Footer({
24
+ onReset,
25
+ htmlHistory,
26
+ setHtml,
27
+ device,
28
+ setDevice,
29
+ iframeRef,
30
+ }: {
31
+ onReset: () => void;
32
+ htmlHistory?: HtmlHistory[];
33
+ device: "desktop" | "mobile";
34
+ setHtml: (html: string) => void;
35
+ iframeRef?: React.RefObject<HTMLIFrameElement | null>;
36
+ setDevice: React.Dispatch<React.SetStateAction<"desktop" | "mobile">>;
37
+ }) {
38
+ const { user } = useUser();
39
+
40
+ const handleRefreshIframe = () => {
41
+ if (iframeRef?.current) {
42
+ const iframe = iframeRef.current;
43
+ const content = iframe.srcdoc;
44
+ iframe.srcdoc = "";
45
+ setTimeout(() => {
46
+ iframe.srcdoc = content;
47
+ }, 10);
48
+ }
49
+ };
50
+
51
+ return (
52
+ <footer className="border-t bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 py-2 flex items-center justify-between sticky bottom-0 z-20">
53
+ <div className="flex items-center gap-2">
54
+ {user &&
55
+ (user?.isLocalUse ? (
56
+ <>
57
+ <div className="max-w-max bg-amber-500/10 rounded-full px-3 py-1 text-amber-500 border border-amber-500/20 text-sm font-semibold">
58
+ Local Usage
59
+ </div>
60
+ </>
61
+ ) : (
62
+ <UserMenu className="!p-1 !pr-3 !h-auto" />
63
+ ))}
64
+ {user && <p className="text-neutral-700">|</p>}
65
+ <Button size="sm" variant="secondary" onClick={onReset}>
66
+ <MdAdd className="text-sm" />
67
+ New <span className="max-lg:hidden">Project</span>
68
+ </Button>
69
+ {htmlHistory && htmlHistory.length > 0 && (
70
+ <>
71
+ <p className="text-neutral-700">|</p>
72
+ <History history={htmlHistory} setHtml={setHtml} />
73
+ </>
74
+ )}
75
+ </div>
76
+ <div className="flex justify-end items-center gap-2.5">
77
+ <a
78
+ href="https://huggingface.co/spaces/victor/deepsite-gallery"
79
+ target="_blank"
80
+ >
81
+ <Button size="sm" variant="ghost">
82
+ <SparkleIcon className="size-3.5" />
83
+ <span className="max-lg:hidden">DeepSite Gallery</span>
84
+ </Button>
85
+ </a>
86
+ <a
87
+ target="_blank"
88
+ href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/157"
89
+ >
90
+ <Button size="sm" variant="outline">
91
+ <HelpCircle className="size-3.5" />
92
+ <span className="max-lg:hidden">Help</span>
93
+ </Button>
94
+ </a>
95
+ <Button size="sm" variant="outline" onClick={handleRefreshIframe}>
96
+ <RefreshCcw className="size-3.5" />
97
+ <span className="max-lg:hidden">Refresh Preview</span>
98
+ </Button>
99
+ <div className="flex items-center rounded-full p-0.5 bg-neutral-700/70 relative overflow-hidden z-0 max-lg:hidden gap-0.5">
100
+ <div
101
+ className={classNames(
102
+ "absolute left-0.5 top-0.5 rounded-full bg-white size-7 -z-[1] transition-all duration-200",
103
+ {
104
+ "translate-x-[calc(100%+2px)]": device === "mobile",
105
+ }
106
+ )}
107
+ />
108
+ {DEVICES.map((deviceItem) => (
109
+ <button
110
+ key={deviceItem.name}
111
+ className={classNames(
112
+ "rounded-full text-neutral-300 size-7 flex items-center justify-center cursor-pointer",
113
+ {
114
+ "!text-black": device === deviceItem.name,
115
+ "hover:bg-neutral-800": device !== deviceItem.name,
116
+ }
117
+ )}
118
+ onClick={() => setDevice(deviceItem.name as "desktop" | "mobile")}
119
+ >
120
+ <deviceItem.icon className="text-sm" />
121
+ </button>
122
+ ))}
123
+ </div>
124
+ </div>
125
+ </footer>
126
+ );
127
+ }
components/editor/header/index.tsx CHANGED
@@ -1,112 +1,69 @@
1
- import { ArrowRight, HelpCircle, RefreshCcw, Lock } from "lucide-react";
2
- import Image from "next/image";
3
- import Link from "next/link";
4
 
5
  import Logo from "@/assets/logo.svg";
 
6
  import { Button } from "@/components/ui/button";
7
- import { useUser } from "@/hooks/useUser";
8
- import { ProTag } from "@/components/pro-modal";
9
- import { UserMenu } from "@/components/user-menu";
10
- import { SwitchDevice } from "@/components/editor/switch-devide";
11
- import { SwitchTab } from "./switch-tab";
12
- import { History } from "@/components/editor/history";
13
- import { useEditor } from "@/hooks/useEditor";
14
- import {
15
- Tooltip,
16
- TooltipContent,
17
- TooltipTrigger,
18
- } from "@/components/ui/tooltip";
19
 
20
- export function Header() {
21
- const { project } = useEditor();
22
- const { user, openLoginWindow } = useUser();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  return (
24
- <header className="border-b bg-neutral-950 dark:border-neutral-800 grid grid-cols-3 lg:flex items-center max-lg:gap-3 justify-between z-20">
25
- <div className="flex items-center justify-between lg:max-w-[600px] lg:w-full py-2 px-2 lg:px-3 lg:pl-6 gap-3">
26
  <h1 className="text-neutral-900 dark:text-white text-lg lg:text-xl font-bold flex items-center justify-start">
27
  <Image
28
  src={Logo}
29
  alt="DeepSite Logo"
30
- className="size-8 invert-100 dark:invert-0"
31
  />
32
- <p className="ml-2 flex items-center justify-start max-lg:hidden">
33
  DeepSite
34
- {user?.isPro ? (
35
- <ProTag className="ml-2 !text-[10px]" />
36
- ) : (
37
- <span className="font-mono bg-gradient-to-r from-sky-500/20 to-sky-500/10 text-sky-500 rounded-full text-xs ml-2 px-1.5 py-0.5 border border-sky-500/20">
38
- {" "}
39
- v3
40
- </span>
41
- )}
42
  </p>
43
  </h1>
44
- <div className="flex items-center justify-end gap-2">
45
- <History />
46
- <SwitchTab />
47
- </div>
48
- </div>
49
- <div className="lg:hidden flex items-center justify-center whitespace-nowrap">
50
- <SwitchTab isMobile />
51
  </div>
52
- <div className="lg:w-full px-2 lg:px-3 py-2 flex items-center justify-end lg:justify-between lg:border-l lg:border-neutral-800">
53
- <div className="font-mono text-muted-foreground flex items-center gap-2">
54
- <SwitchDevice />
55
  <Button
56
- size="xs"
57
- variant="bordered"
58
- className="max-lg:hidden"
59
- onClick={() => {
60
- const iframe = document.getElementById(
61
- "preview-iframe"
62
- ) as HTMLIFrameElement;
63
- if (iframe) {
64
- iframe.src = iframe.src;
65
- }
66
- }}
67
  >
68
- <RefreshCcw className="size-3 mr-0.5" />
69
- Refresh Preview
70
  </Button>
71
- <Link
72
- href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/427"
73
- target="_blank"
74
- className="max-lg:hidden"
75
- >
76
- <Button size="xs" variant="bordered">
77
- <HelpCircle className="size-3 mr-0.5" />
78
- Help
79
- </Button>
80
- </Link>
81
- </div>
82
- <div className="flex items-center gap-2">
83
- {project?.private && (
84
- <Tooltip>
85
- <TooltipTrigger>
86
- <div className="max-lg:hidden flex items-center gap-1.5 bg-amber-500/10 backdrop-blur-sm px-3 py-1.5 rounded-full border border-amber-500/20 shadow-lg">
87
- <Lock className="w-3 h-3 text-amber-500" />
88
- <span className="text-amber-500 text-xs font-medium tracking-wide">
89
- Private Project
90
- </span>
91
- </div>
92
- </TooltipTrigger>
93
- <TooltipContent>
94
- <p className="text-xs">
95
- This project is private. Only you can see it.
96
- </p>
97
- </TooltipContent>
98
- </Tooltip>
99
- )}
100
- {user ? (
101
- <UserMenu className="!pl-1 !pr-3 !py-1 !h-auto" />
102
- ) : (
103
- <Button size="sm" onClick={openLoginWindow}>
104
- Start Vibe Coding
105
- <ArrowRight className="size-4" />
106
- </Button>
107
- )}
108
- </div>
109
  </div>
 
110
  </header>
111
  );
112
  }
 
1
+ import { ReactNode } from "react";
2
+ import { Eye, MessageCircleCode } from "lucide-react";
 
3
 
4
  import Logo from "@/assets/logo.svg";
5
+
6
  import { Button } from "@/components/ui/button";
7
+ import classNames from "classnames";
8
+ import Image from "next/image";
 
 
 
 
 
 
 
 
 
 
9
 
10
+ const TABS = [
11
+ {
12
+ value: "chat",
13
+ label: "Chat",
14
+ icon: MessageCircleCode,
15
+ },
16
+ {
17
+ value: "preview",
18
+ label: "Preview",
19
+ icon: Eye,
20
+ },
21
+ ];
22
+
23
+ export function Header({
24
+ tab,
25
+ onNewTab,
26
+ children,
27
+ }: {
28
+ tab: string;
29
+ onNewTab: (tab: string) => void;
30
+ children?: ReactNode;
31
+ }) {
32
  return (
33
+ <header className="border-b bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 lg:px-6 py-2 flex items-center max-lg:gap-3 justify-between lg:grid lg:grid-cols-3 z-20">
34
+ <div className="flex items-center justify-start gap-3">
35
  <h1 className="text-neutral-900 dark:text-white text-lg lg:text-xl font-bold flex items-center justify-start">
36
  <Image
37
  src={Logo}
38
  alt="DeepSite Logo"
39
+ className="size-6 lg:size-8 mr-2 invert-100 dark:invert-0"
40
  />
41
+ <p className="max-md:hidden flex items-center justify-start">
42
  DeepSite
43
+ <span className="font-mono bg-gradient-to-br from-sky-500 to-emerald-500 text-neutral-950 rounded-full text-xs ml-2 px-1.5 py-0.5">
44
+ {" "}
45
+ v2
46
+ </span>
 
 
 
 
47
  </p>
48
  </h1>
 
 
 
 
 
 
 
49
  </div>
50
+ <div className="flex items-center justify-start lg:justify-center gap-1 max-lg:pl-3 flex-1 max-lg:border-l max-lg:border-l-neutral-800">
51
+ {TABS.map((item) => (
 
52
  <Button
53
+ key={item.value}
54
+ variant={tab === item.value ? "secondary" : "ghost"}
55
+ className={classNames("", {
56
+ "opacity-60": tab !== item.value,
57
+ })}
58
+ size="sm"
59
+ onClick={() => onNewTab(item.value)}
 
 
 
 
60
  >
61
+ <item.icon className="size-4" />
62
+ <span className="hidden md:inline">{item.label}</span>
63
  </Button>
64
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  </div>
66
+ <div className="flex items-center justify-end gap-3">{children}</div>
67
  </header>
68
  );
69
  }
components/editor/header/switch-tab.tsx DELETED
@@ -1,58 +0,0 @@
1
- import {
2
- PanelLeftClose,
3
- PanelLeftOpen,
4
- Eye,
5
- MessageCircleCode,
6
- } from "lucide-react";
7
- import classNames from "classnames";
8
-
9
- import { Button } from "@/components/ui/button";
10
- import { useEditor } from "@/hooks/useEditor";
11
-
12
- const TABS = [
13
- {
14
- value: "chat",
15
- label: "Chat",
16
- icon: MessageCircleCode,
17
- },
18
- {
19
- value: "preview",
20
- label: "Preview",
21
- icon: Eye,
22
- },
23
- ];
24
-
25
- export const SwitchTab = ({ isMobile = false }: { isMobile?: boolean }) => {
26
- const { currentTab, setCurrentTab } = useEditor();
27
-
28
- if (isMobile) {
29
- return (
30
- <div className="flex items-center justify-center gap-1 bg-neutral-900 rounded-full p-1">
31
- {TABS.map((item) => (
32
- <Button
33
- key={item.value}
34
- variant={currentTab === item.value ? "default" : "ghost"}
35
- className={classNames("", {
36
- "opacity-60": currentTab !== item.value,
37
- })}
38
- size="sm"
39
- onClick={() => setCurrentTab(item.value)}
40
- >
41
- <item.icon className="size-4" />
42
- <span className="inline">{item.label}</span>
43
- </Button>
44
- ))}
45
- </div>
46
- );
47
- }
48
- return (
49
- <Button
50
- variant="ghost"
51
- size="iconXs"
52
- className="max-lg:hidden"
53
- onClick={() => setCurrentTab(currentTab === "chat" ? "preview" : "chat")}
54
- >
55
- {currentTab === "chat" ? <PanelLeftClose /> : <PanelLeftOpen />}
56
- </Button>
57
- );
58
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/history-notification/index.tsx DELETED
@@ -1,119 +0,0 @@
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/history/index.tsx CHANGED
@@ -1,32 +1,25 @@
1
  import { History as HistoryIcon } from "lucide-react";
2
- import { useState } from "react";
3
-
4
- import { Commit } from "@/types";
5
  import {
6
  Popover,
7
  PopoverContent,
8
  PopoverTrigger,
9
  } from "@/components/ui/popover";
10
  import { Button } from "@/components/ui/button";
11
- import { useEditor } from "@/hooks/useEditor";
12
- import classNames from "classnames";
13
-
14
- export function History() {
15
- const { commits, currentCommit, setCurrentCommit, project } = useEditor();
16
- const [open, setOpen] = useState(false);
17
-
18
- if (commits.length === 0) return null;
19
 
 
 
 
 
 
 
 
20
  return (
21
- <Popover open={open} onOpenChange={setOpen}>
22
  <PopoverTrigger asChild>
23
- <Button
24
- size="xs"
25
- variant={open ? "default" : "outline"}
26
- className="!rounded-md max-lg:hidden"
27
- >
28
- <HistoryIcon className="size-3.5" />
29
- {commits?.length} edit{commits?.length !== 1 ? "s" : ""}
30
  </Button>
31
  </PopoverTrigger>
32
  <PopoverContent
@@ -36,68 +29,39 @@ export function History() {
36
  <header className="text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
37
  History
38
  </header>
39
- <main className="space-y-3">
40
- {project?.private && (
41
- <div className="px-4 pt-3">
42
- <p className="text-amber-500 text-xs px-2 py-1 bg-amber-500/10 border border-amber-500/20 rounded-md">
43
- As this project is private, you can't see the history of
44
- changes.
45
- </p>
46
- </div>
47
- )}
48
  <ul className="max-h-[250px] overflow-y-auto">
49
- {commits?.map((item: Commit, index: number) => (
50
  <li
51
  key={index}
52
- className={classNames(
53
- "px-4 text-gray-200 py-2 border-b border-gray-800 last:border-0 space-y-1",
54
- {
55
- "bg-blue-500/10":
56
- currentCommit === item.oid ||
57
- (index === 0 && currentCommit === null),
58
- }
59
- )}
60
  >
61
- <p className="line-clamp-1 text-sm">{item.title}</p>
62
- <div className="w-full flex items-center justify-between gap-2">
63
- <p className="text-gray-500 text-[10px]">
64
- {new Date(item.date).toLocaleDateString("en-US", {
65
  month: "2-digit",
66
  day: "2-digit",
67
  year: "2-digit",
68
  }) +
69
  " " +
70
- new Date(item.date).toLocaleTimeString("en-US", {
71
  hour: "2-digit",
72
  minute: "2-digit",
73
  second: "2-digit",
74
  hour12: false,
75
  })}
76
- </p>
77
- {currentCommit === item.oid ||
78
- (index === 0 && currentCommit === null) ? (
79
- <span className="text-blue-500 bg-blue-500/10 border border-blue-500/20 rounded-full text-[10px] px-2 py-0.5">
80
- Current version
81
- </span>
82
- ) : (
83
- !project?.private && (
84
- <Button
85
- variant="link"
86
- size="xss"
87
- className="text-gray-400 hover:text-gray-200"
88
- onClick={() => {
89
- if (index === 0) {
90
- setCurrentCommit(null);
91
- } else {
92
- setCurrentCommit(item.oid);
93
- }
94
- }}
95
- >
96
- See version
97
- </Button>
98
- )
99
- )}
100
  </div>
 
 
 
 
 
 
 
 
 
101
  </li>
102
  ))}
103
  </ul>
 
1
  import { History as HistoryIcon } from "lucide-react";
2
+ import { HtmlHistory } from "@/types";
 
 
3
  import {
4
  Popover,
5
  PopoverContent,
6
  PopoverTrigger,
7
  } from "@/components/ui/popover";
8
  import { Button } from "@/components/ui/button";
 
 
 
 
 
 
 
 
9
 
10
+ export function History({
11
+ history,
12
+ setHtml,
13
+ }: {
14
+ history: HtmlHistory[];
15
+ setHtml: (html: string) => void;
16
+ }) {
17
  return (
18
+ <Popover>
19
  <PopoverTrigger asChild>
20
+ <Button variant="ghost" size="sm" className="max-lg:hidden">
21
+ <HistoryIcon className="size-4 text-neutral-300" />
22
+ {history?.length} edit{history.length !== 1 ? "s" : ""}
 
 
 
 
23
  </Button>
24
  </PopoverTrigger>
25
  <PopoverContent
 
29
  <header className="text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
30
  History
31
  </header>
32
+ <main className="px-4 space-y-3">
 
 
 
 
 
 
 
 
33
  <ul className="max-h-[250px] overflow-y-auto">
34
+ {history?.map((item, index) => (
35
  <li
36
  key={index}
37
+ className="text-gray-300 text-xs py-2 border-b border-gray-800 last:border-0 flex items-center justify-between gap-2"
 
 
 
 
 
 
 
38
  >
39
+ <div className="">
40
+ <span className="line-clamp-1">{item.prompt}</span>
41
+ <span className="text-gray-500 text-[10px]">
42
+ {new Date(item.createdAt).toLocaleDateString("en-US", {
43
  month: "2-digit",
44
  day: "2-digit",
45
  year: "2-digit",
46
  }) +
47
  " " +
48
+ new Date(item.createdAt).toLocaleTimeString("en-US", {
49
  hour: "2-digit",
50
  minute: "2-digit",
51
  second: "2-digit",
52
  hour12: false,
53
  })}
54
+ </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  </div>
56
+ <Button
57
+ variant="sky"
58
+ size="xs"
59
+ onClick={() => {
60
+ setHtml(item.html);
61
+ }}
62
+ >
63
+ Select
64
+ </Button>
65
  </li>
66
  ))}
67
  </ul>
components/editor/index.tsx CHANGED
@@ -1,154 +1,341 @@
1
  "use client";
2
- import { useMemo, useRef, useState, useEffect } from "react";
3
- import { useCopyToClipboard, useMount } from "react-use";
4
- import { CopyIcon } from "lucide-react";
5
  import { toast } from "sonner";
6
- import classNames from "classnames";
7
  import { editor } from "monaco-editor";
8
  import Editor from "@monaco-editor/react";
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- import { useEditor } from "@/hooks/useEditor";
11
  import { Header } from "@/components/editor/header";
12
- import { useAi } from "@/hooks/useAi";
13
  import { defaultHTML } from "@/lib/consts";
 
 
 
 
 
 
 
 
14
 
15
- import { ListPages } from "./pages";
16
- import { AskAi } from "./ask-ai";
17
- import { Preview } from "./preview";
18
- import { SaveChangesPopup } from "./save-changes-popup";
19
- import Loading from "../loading";
20
- import { LivePreviewRef } from "./live-preview";
21
-
22
- export const AppEditor = ({
23
- namespace,
24
- repoId,
25
- isNew = false,
26
- }: {
27
- namespace?: string;
28
- repoId?: string;
29
- isNew?: boolean;
30
- }) => {
31
- const {
32
- project,
33
- setPages,
34
- files,
35
- currentPageData,
36
- currentTab,
37
- currentCommit,
38
- hasUnsavedChanges,
39
- saveChanges,
40
- pages,
41
- } = useEditor(namespace, repoId);
42
- const livePreviewRef = useRef<LivePreviewRef>(null);
43
- const { isAiWorking } = useAi(undefined, livePreviewRef);
44
  const [, copyToClipboard] = useCopyToClipboard();
45
- const [showSavePopup, setShowSavePopup] = useState(false);
 
 
 
 
 
46
 
47
- const monacoRef = useRef<any>(null);
 
48
  const editor = useRef<HTMLDivElement>(null);
49
  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  useMount(() => {
52
- if (isNew) {
53
- setPages([
54
- {
55
- path: "index.html",
56
- html: defaultHTML,
 
 
 
 
 
57
  },
58
- ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  }
60
  });
61
 
62
- useEffect(() => {
63
- if (hasUnsavedChanges && !isAiWorking) {
64
- setShowSavePopup(true);
 
 
 
 
 
65
  } else {
66
- setShowSavePopup(false);
 
 
 
67
  }
68
- }, [hasUnsavedChanges, isAiWorking]);
 
 
 
 
69
 
70
  return (
71
- <section className="h-screen w-full bg-neutral-950 flex flex-col">
72
- <Header />
73
- <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full relative">
74
- <div
75
- ref={editor}
76
- className={classNames(
77
- "bg-neutral-900 relative flex h-full max-h-[calc(100dvh-47px)] w-full flex-col lg:max-w-[600px] transition-all duration-200",
78
- {
79
- "max-lg:hidden lg:!w-[0px] overflow-hidden":
80
- currentTab !== "chat",
81
- }
82
- )}
83
- >
84
- <ListPages />
85
- <CopyIcon
86
- className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
87
- onClick={() => {
88
- copyToClipboard(currentPageData.html);
89
- toast.success("HTML copied to clipboard!");
90
- }}
91
- />
92
- <Editor
93
- defaultLanguage="html"
94
- theme="vs-dark"
95
- loading={<Loading overlay={false} />}
96
- className="h-full absolute left-0 top-0 lg:min-w-[600px]"
97
- options={{
98
- colorDecorators: true,
99
- fontLigatures: true,
100
- theme: "vs-dark",
101
- minimap: { enabled: false },
102
- scrollbar: {
103
- horizontal: "hidden",
104
- },
105
- wordWrap: "on",
106
- readOnly: !!isAiWorking || !!currentCommit,
107
- readOnlyMessage: {
108
- value: currentCommit
109
- ? "You can't edit the code, as this is an old version of the project."
110
- : "Wait for DeepSite to finish working...",
111
- isTrusted: true,
112
- },
113
- }}
114
- value={currentPageData.html}
115
- onChange={(value) => {
116
- const newValue = value ?? "";
117
- setPages((prev) =>
118
- prev.map((page) =>
119
- page.path === currentPageData.path
120
- ? { ...page, html: newValue }
121
- : page
122
- )
123
- );
124
- }}
125
- onMount={(editor, monaco) => {
126
- editorRef.current = editor;
127
- monacoRef.current = monaco;
128
- }}
129
- />
130
- <AskAi
131
- project={project}
132
- files={files}
133
- isNew={isNew}
134
- onScrollToBottom={() => {
135
- editorRef.current?.revealLine(
136
- editorRef.current?.getModel()?.getLineCount() ?? 0
137
- );
138
- }}
139
- />
140
- </div>
141
- <Preview ref={livePreviewRef} isNew={isNew} />
142
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- {/* Save Changes Popup */}
145
- <SaveChangesPopup
146
- isOpen={showSavePopup}
147
- onClose={() => setShowSavePopup(false)}
148
- onSave={saveChanges}
149
- hasUnsavedChanges={hasUnsavedChanges}
150
- pages={pages}
151
- project={project}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  />
153
  </section>
154
  );
 
1
  "use client";
2
+ import { useRef, useState } from "react";
 
 
3
  import { toast } from "sonner";
 
4
  import { editor } from "monaco-editor";
5
  import Editor from "@monaco-editor/react";
6
+ import { CopyIcon } from "lucide-react";
7
+ import {
8
+ useCopyToClipboard,
9
+ useEvent,
10
+ useLocalStorage,
11
+ useMount,
12
+ useUnmount,
13
+ useUpdateEffect,
14
+ } from "react-use";
15
+ import classNames from "classnames";
16
+ import { useRouter, useSearchParams } from "next/navigation";
17
 
 
18
  import { Header } from "@/components/editor/header";
19
+ import { Footer } from "@/components/editor/footer";
20
  import { defaultHTML } from "@/lib/consts";
21
+ import { Preview } from "@/components/editor/preview";
22
+ import { useEditor } from "@/hooks/useEditor";
23
+ import { AskAI } from "@/components/editor/ask-ai";
24
+ import { DeployButton } from "./deploy-button";
25
+ import { Project } from "@/types";
26
+ import { SaveButton } from "./save-button";
27
+ import { LoadProject } from "../my-projects/load-project";
28
+ import { isTheSameHtml } from "@/lib/compare-html-diff";
29
 
30
+ export const AppEditor = ({ project }: { project?: Project | null }) => {
31
+ const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  const [, copyToClipboard] = useCopyToClipboard();
33
+ const { html, setHtml, htmlHistory, setHtmlHistory, prompts, setPrompts } =
34
+ useEditor(project?.html ?? (htmlStorage as string) ?? defaultHTML);
35
+ // get query params from URL
36
+ const searchParams = useSearchParams();
37
+ const router = useRouter();
38
+ const deploy = searchParams.get("deploy") === "true";
39
 
40
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
41
+ const preview = useRef<HTMLDivElement>(null);
42
  const editor = useRef<HTMLDivElement>(null);
43
  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
44
+ const resizer = useRef<HTMLDivElement>(null);
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ const monacoRef = useRef<any>(null);
47
+
48
+ const [currentTab, setCurrentTab] = useState("chat");
49
+ const [device, setDevice] = useState<"desktop" | "mobile">("desktop");
50
+ const [isResizing, setIsResizing] = useState(false);
51
+ const [isAiWorking, setIsAiWorking] = useState(false);
52
+ const [isEditableModeEnabled, setIsEditableModeEnabled] = useState(false);
53
+ const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
54
+ null
55
+ );
56
+
57
+ /**
58
+ * Resets the layout based on screen size
59
+ * - For desktop: Sets editor to 1/3 width and preview to 2/3
60
+ * - For mobile: Removes inline styles to let CSS handle it
61
+ */
62
+ const resetLayout = () => {
63
+ if (!editor.current || !preview.current) return;
64
+
65
+ // lg breakpoint is 1024px based on useBreakpoint definition and Tailwind defaults
66
+ if (window.innerWidth >= 1024) {
67
+ // Set initial 1/3 - 2/3 sizes for large screens, accounting for resizer width
68
+ const resizerWidth = resizer.current?.offsetWidth ?? 8; // w-2 = 0.5rem = 8px
69
+ const availableWidth = window.innerWidth - resizerWidth;
70
+ const initialEditorWidth = availableWidth / 3; // Editor takes 1/3 of space
71
+ const initialPreviewWidth = availableWidth - initialEditorWidth; // Preview takes 2/3
72
+ editor.current.style.width = `${initialEditorWidth}px`;
73
+ preview.current.style.width = `${initialPreviewWidth}px`;
74
+ } else {
75
+ // Remove inline styles for smaller screens, let CSS flex-col handle it
76
+ editor.current.style.width = "";
77
+ preview.current.style.width = "";
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Handles resizing when the user drags the resizer
83
+ * Ensures minimum widths are maintained for both panels
84
+ */
85
+ const handleResize = (e: MouseEvent) => {
86
+ if (!editor.current || !preview.current || !resizer.current) return;
87
+
88
+ const resizerWidth = resizer.current.offsetWidth;
89
+ const minWidth = 100; // Minimum width for editor/preview
90
+ const maxWidth = window.innerWidth - resizerWidth - minWidth;
91
+
92
+ const editorWidth = e.clientX;
93
+ const clampedEditorWidth = Math.max(
94
+ minWidth,
95
+ Math.min(editorWidth, maxWidth)
96
+ );
97
+ const calculatedPreviewWidth =
98
+ window.innerWidth - clampedEditorWidth - resizerWidth;
99
+
100
+ editor.current.style.width = `${clampedEditorWidth}px`;
101
+ preview.current.style.width = `${calculatedPreviewWidth}px`;
102
+ };
103
+
104
+ const handleMouseDown = () => {
105
+ setIsResizing(true);
106
+ document.addEventListener("mousemove", handleResize);
107
+ document.addEventListener("mouseup", handleMouseUp);
108
+ };
109
+
110
+ const handleMouseUp = () => {
111
+ setIsResizing(false);
112
+ document.removeEventListener("mousemove", handleResize);
113
+ document.removeEventListener("mouseup", handleMouseUp);
114
+ };
115
 
116
  useMount(() => {
117
+ if (deploy && project?._id) {
118
+ toast.success("Your project is deployed! 🎉", {
119
+ action: {
120
+ label: "See Project",
121
+ onClick: () => {
122
+ window.open(
123
+ `https://huggingface.co/spaces/${project?.space_id}`,
124
+ "_blank"
125
+ );
126
+ },
127
  },
128
+ });
129
+ router.replace(`/projects/${project?.space_id}`);
130
+ }
131
+ if (htmlStorage) {
132
+ removeHtmlStorage();
133
+ toast.warning("Previous HTML content restored from local storage.");
134
+ }
135
+
136
+ resetLayout();
137
+ if (!resizer.current) return;
138
+ resizer.current.addEventListener("mousedown", handleMouseDown);
139
+ window.addEventListener("resize", resetLayout);
140
+ });
141
+ useUnmount(() => {
142
+ document.removeEventListener("mousemove", handleResize);
143
+ document.removeEventListener("mouseup", handleMouseUp);
144
+ if (resizer.current) {
145
+ resizer.current.removeEventListener("mousedown", handleMouseDown);
146
+ }
147
+ window.removeEventListener("resize", resetLayout);
148
+ });
149
+
150
+ // Prevent accidental navigation away when AI is working or content has changed
151
+ useEvent("beforeunload", (e) => {
152
+ if (isAiWorking || !isTheSameHtml(html)) {
153
+ e.preventDefault();
154
+ return "";
155
  }
156
  });
157
 
158
+ useUpdateEffect(() => {
159
+ if (currentTab === "chat") {
160
+ // Reset editor width when switching to reasoning tab
161
+ resetLayout();
162
+ // re-add the event listener for resizing
163
+ if (resizer.current) {
164
+ resizer.current.addEventListener("mousedown", handleMouseDown);
165
+ }
166
  } else {
167
+ if (preview.current) {
168
+ // Reset preview width when switching to preview tab
169
+ preview.current.style.width = "100%";
170
+ }
171
  }
172
+ }, [currentTab]);
173
+
174
+ const handleEditorValidation = (markers: editor.IMarker[]) => {
175
+ console.log("Editor validation markers:", markers);
176
+ };
177
 
178
  return (
179
+ <section className="h-[100dvh] bg-neutral-950 flex flex-col">
180
+ <Header tab={currentTab} onNewTab={setCurrentTab}>
181
+ <LoadProject
182
+ onSuccess={(project: Project) => {
183
+ router.push(`/projects/${project.space_id}`);
184
+ }}
185
+ />
186
+ {project?._id ? (
187
+ <SaveButton html={html} prompts={prompts} />
188
+ ) : (
189
+ <DeployButton html={html} prompts={prompts} />
190
+ )}
191
+ </Header>
192
+ <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full max-lg:h-[calc(100%-82px)] relative">
193
+ {currentTab === "chat" && (
194
+ <>
195
+ <div
196
+ ref={editor}
197
+ className="bg-neutral-900 relative flex-1 overflow-hidden h-full flex flex-col gap-2 pb-3"
198
+ >
199
+ <CopyIcon
200
+ className="size-4 absolute top-2 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
201
+ onClick={() => {
202
+ copyToClipboard(html);
203
+ toast.success("HTML copied to clipboard!");
204
+ }}
205
+ />
206
+ <Editor
207
+ defaultLanguage="html"
208
+ theme="vs-dark"
209
+ className={classNames(
210
+ "h-full bg-neutral-900 transition-all duration-200 absolute left-0 top-0",
211
+ {
212
+ "pointer-events-none": isAiWorking,
213
+ }
214
+ )}
215
+ options={{
216
+ colorDecorators: true,
217
+ fontLigatures: true,
218
+ theme: "vs-dark",
219
+ minimap: { enabled: false },
220
+ scrollbar: {
221
+ horizontal: "hidden",
222
+ },
223
+ wordWrap: "on",
224
+ }}
225
+ value={html}
226
+ onChange={(value) => {
227
+ const newValue = value ?? "";
228
+ setHtml(newValue);
229
+ }}
230
+ onMount={(editor, monaco) => {
231
+ editorRef.current = editor;
232
+ monacoRef.current = monaco;
233
+ }}
234
+ onValidate={handleEditorValidation}
235
+ />
236
+ <AskAI
237
+ html={html}
238
+ setHtml={(newHtml: string) => {
239
+ setHtml(newHtml);
240
+ }}
241
+ htmlHistory={htmlHistory}
242
+ onSuccess={(
243
+ finalHtml: string,
244
+ p: string,
245
+ updatedLines?: number[][]
246
+ ) => {
247
+ const currentHistory = [...htmlHistory];
248
+ currentHistory.unshift({
249
+ html: finalHtml,
250
+ createdAt: new Date(),
251
+ prompt: p,
252
+ });
253
+ setHtmlHistory(currentHistory);
254
+ setSelectedElement(null);
255
+ // if xs or sm
256
+ if (window.innerWidth <= 1024) {
257
+ setCurrentTab("preview");
258
+ }
259
+ if (updatedLines && updatedLines?.length > 0) {
260
+ const decorations = updatedLines.map((line) => ({
261
+ range: new monacoRef.current.Range(
262
+ line[0],
263
+ 1,
264
+ line[1],
265
+ 1
266
+ ),
267
+ options: {
268
+ inlineClassName: "matched-line",
269
+ },
270
+ }));
271
+ setTimeout(() => {
272
+ editorRef?.current
273
+ ?.getModel()
274
+ ?.deltaDecorations([], decorations);
275
 
276
+ editorRef.current?.revealLine(updatedLines[0][0]);
277
+ }, 100);
278
+ }
279
+ }}
280
+ isAiWorking={isAiWorking}
281
+ setisAiWorking={setIsAiWorking}
282
+ onNewPrompt={(prompt: string) => {
283
+ setPrompts((prev) => [...prev, prompt]);
284
+ }}
285
+ onScrollToBottom={() => {
286
+ editorRef.current?.revealLine(
287
+ editorRef.current?.getModel()?.getLineCount() ?? 0
288
+ );
289
+ }}
290
+ isEditableModeEnabled={isEditableModeEnabled}
291
+ setIsEditableModeEnabled={setIsEditableModeEnabled}
292
+ selectedElement={selectedElement}
293
+ setSelectedElement={setSelectedElement}
294
+ />
295
+ </div>
296
+ <div
297
+ ref={resizer}
298
+ className="bg-neutral-800 hover:bg-sky-500 active:bg-sky-500 w-1.5 cursor-col-resize h-full max-lg:hidden"
299
+ />
300
+ </>
301
+ )}
302
+ <Preview
303
+ html={html}
304
+ isResizing={isResizing}
305
+ isAiWorking={isAiWorking}
306
+ ref={preview}
307
+ device={device}
308
+ currentTab={currentTab}
309
+ isEditableModeEnabled={isEditableModeEnabled}
310
+ iframeRef={iframeRef}
311
+ onClickElement={(element) => {
312
+ setIsEditableModeEnabled(false);
313
+ setSelectedElement(element);
314
+ setCurrentTab("chat");
315
+ }}
316
+ />
317
+ </main>
318
+ <Footer
319
+ onReset={() => {
320
+ if (isAiWorking) {
321
+ toast.warning("Please wait for the AI to finish working.");
322
+ return;
323
+ }
324
+ if (
325
+ window.confirm("You're about to reset the editor. Are you sure?")
326
+ ) {
327
+ setHtml(defaultHTML);
328
+ removeHtmlStorage();
329
+ editorRef.current?.revealLine(
330
+ editorRef.current?.getModel()?.getLineCount() ?? 0
331
+ );
332
+ }
333
+ }}
334
+ htmlHistory={htmlHistory}
335
+ setHtml={setHtml}
336
+ iframeRef={iframeRef}
337
+ device={device}
338
+ setDevice={setDevice}
339
  />
340
  </section>
341
  );
components/editor/live-preview/index.tsx DELETED
@@ -1,165 +0,0 @@
1
- "use client";
2
-
3
- import {
4
- useState,
5
- useEffect,
6
- useRef,
7
- forwardRef,
8
- useImperativeHandle,
9
- } from "react";
10
- import classNames from "classnames";
11
-
12
- import { Button } from "@/components/ui/button";
13
- import { Maximize, Minimize } from "lucide-react";
14
-
15
- interface LivePreviewProps {
16
- currentPageData: { path: string; html: string } | undefined;
17
- isAiWorking: boolean;
18
- defaultHTML: string;
19
- className?: string;
20
- }
21
-
22
- export interface LivePreviewRef {
23
- reset: () => void;
24
- }
25
-
26
- export const LivePreview = forwardRef<LivePreviewRef, LivePreviewProps>(
27
- ({ currentPageData, isAiWorking, defaultHTML, className }, ref) => {
28
- const [isMaximized, setIsMaximized] = useState(false);
29
- const [displayedHtml, setDisplayedHtml] = useState<string>("");
30
- const latestHtmlRef = useRef<string>("");
31
- const displayedHtmlRef = useRef<string>("");
32
- const intervalRef = useRef<NodeJS.Timeout | null>(null);
33
-
34
- const reset = () => {
35
- setIsMaximized(false);
36
- setDisplayedHtml("");
37
- latestHtmlRef.current = "";
38
- displayedHtmlRef.current = "";
39
- if (intervalRef.current) {
40
- clearInterval(intervalRef.current);
41
- intervalRef.current = null;
42
- }
43
- };
44
-
45
- useImperativeHandle(ref, () => ({
46
- reset,
47
- }));
48
-
49
- useEffect(() => {
50
- displayedHtmlRef.current = displayedHtml;
51
- }, [displayedHtml]);
52
-
53
- useEffect(() => {
54
- if (currentPageData?.html && currentPageData.html !== defaultHTML) {
55
- latestHtmlRef.current = currentPageData.html;
56
- }
57
- }, [currentPageData?.html, defaultHTML]);
58
-
59
- useEffect(() => {
60
- if (!currentPageData?.html || currentPageData.html === defaultHTML) {
61
- return;
62
- }
63
-
64
- if (!displayedHtml || !isAiWorking) {
65
- setDisplayedHtml(currentPageData.html);
66
- if (intervalRef.current) {
67
- clearInterval(intervalRef.current);
68
- intervalRef.current = null;
69
- }
70
- return;
71
- }
72
-
73
- if (isAiWorking && !intervalRef.current) {
74
- intervalRef.current = setInterval(() => {
75
- if (
76
- latestHtmlRef.current &&
77
- latestHtmlRef.current !== displayedHtmlRef.current
78
- ) {
79
- setDisplayedHtml(latestHtmlRef.current);
80
- }
81
- }, 3000);
82
- }
83
- }, [currentPageData?.html, defaultHTML, isAiWorking, displayedHtml]);
84
-
85
- useEffect(() => {
86
- if (!isAiWorking && intervalRef.current) {
87
- clearInterval(intervalRef.current);
88
- intervalRef.current = null;
89
- if (latestHtmlRef.current) {
90
- setDisplayedHtml(latestHtmlRef.current);
91
- }
92
- }
93
- }, [isAiWorking]);
94
-
95
- useEffect(() => {
96
- return () => {
97
- if (intervalRef.current) {
98
- clearInterval(intervalRef.current);
99
- intervalRef.current = null;
100
- }
101
- };
102
- }, []);
103
-
104
- if (!displayedHtml) {
105
- return null;
106
- }
107
-
108
- return (
109
- <div
110
- className={classNames(
111
- "absolute z-40 bg-white/95 backdrop-blur-sm border border-neutral-200 shadow-lg transition-all duration-500 ease-out transform scale-100 opacity-100 animate-in slide-in-from-bottom-4 zoom-in-95 rounded-xl",
112
- {
113
- "shadow-green-500/20 shadow-2xl border-green-200": isAiWorking,
114
- },
115
- className
116
- )}
117
- >
118
- <div
119
- className={classNames(
120
- "flex flex-col animate-in fade-in duration-300",
121
- isMaximized ? "w-[60dvw] h-[80dvh]" : "w-80 h-96"
122
- )}
123
- >
124
- <div className="flex items-center justify-between p-3 border-b border-neutral-200">
125
- <div className="flex items-center gap-2">
126
- <div className="size-2 bg-green-500 rounded-full animate-pulse shadow-sm shadow-green-500/50"></div>
127
- <span className="text-xs font-medium text-neutral-800">
128
- Live Preview
129
- </span>
130
- {isAiWorking && (
131
- <span className="text-xs text-green-600 font-medium animate-pulse">
132
- • Updating
133
- </span>
134
- )}
135
- </div>
136
- <div className="flex items-center gap-1">
137
- <Button
138
- variant="outline"
139
- size="iconXs"
140
- className="!rounded-md !border-neutral-200 hover:bg-neutral-50"
141
- onClick={() => setIsMaximized(!isMaximized)}
142
- >
143
- {isMaximized ? (
144
- <Minimize className="text-neutral-400 size-3" />
145
- ) : (
146
- <Maximize className="text-neutral-400 size-3" />
147
- )}
148
- </Button>
149
- </div>
150
- </div>
151
- <div className="flex-1 bg-black overflow-hidden relative rounded-b-xl">
152
- <iframe
153
- className="w-full h-full border-0"
154
- srcDoc={displayedHtml}
155
- sandbox="allow-scripts allow-same-origin"
156
- title="Live Preview"
157
- />
158
- </div>
159
- </div>
160
- </div>
161
- );
162
- }
163
- );
164
-
165
- LivePreview.displayName = "LivePreview";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/pages/index.tsx DELETED
@@ -1,24 +0,0 @@
1
- import { Page } from "@/types";
2
- import { ListPagesItem } from "./page";
3
- import { useEditor } from "@/hooks/useEditor";
4
-
5
- export function ListPages() {
6
- const { pages, setPages, currentPage, setCurrentPage } = useEditor();
7
- return (
8
- <div className="w-full flex items-center justify-start bg-neutral-950 overflow-auto flex-nowrap min-h-[45px]">
9
- {pages.map((page: Page, i: number) => (
10
- <ListPagesItem
11
- key={page.path ?? i}
12
- page={page}
13
- currentPage={currentPage}
14
- onSelectPage={setCurrentPage}
15
- onDeletePage={(path) => {
16
- setPages(pages.filter((page) => page.path !== path));
17
- setCurrentPage("index.html");
18
- }}
19
- index={i}
20
- />
21
- ))}
22
- </div>
23
- );
24
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/pages/page.tsx DELETED
@@ -1,56 +0,0 @@
1
- import classNames from "classnames";
2
- import { FileCode, XIcon } from "lucide-react";
3
-
4
- import { Button } from "@/components/ui/button";
5
- import { Page } from "@/types";
6
-
7
- export function ListPagesItem({
8
- page,
9
- currentPage,
10
- onSelectPage,
11
- onDeletePage,
12
- index,
13
- }: {
14
- page: Page;
15
- currentPage: string;
16
- onSelectPage: (path: string, newPath?: string) => void;
17
- onDeletePage: (path: string) => void;
18
- index: number;
19
- }) {
20
- return (
21
- <div
22
- key={index}
23
- className={classNames(
24
- "pl-6 pr-1 py-3 text-neutral-400 cursor-pointer text-sm hover:bg-neutral-900 flex items-center justify-center gap-1 group text-nowrap border-r border-neutral-800",
25
- {
26
- "bg-neutral-900 !text-white": currentPage === page.path,
27
- "!pr-6": index === 0, // Ensure the first item has padding on the right
28
- }
29
- )}
30
- onClick={() => onSelectPage(page.path)}
31
- title={page.path}
32
- >
33
- <FileCode className="size-4 mr-1" />
34
- {page.path}
35
- {index > 0 && (
36
- <Button
37
- size="iconXsss"
38
- variant="ghost"
39
- className="group-hover:opacity-100 opacity-0 !h-auto"
40
- onClick={(e) => {
41
- e.stopPropagation();
42
- if (
43
- window.confirm(
44
- "Are you sure you want to delete this page? This action cannot be undone."
45
- )
46
- ) {
47
- onDeletePage(page.path);
48
- }
49
- }}
50
- >
51
- <XIcon className="h-3 text-neutral-400 cursor-pointer hover:text-neutral-300" />
52
- </Button>
53
- )}
54
- </div>
55
- );
56
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/preview/index.tsx CHANGED
@@ -1,358 +1,176 @@
1
  "use client";
2
-
3
- import { useRef, useState, useEffect, forwardRef } from "react";
4
  import { useUpdateEffect } from "react-use";
 
5
  import classNames from "classnames";
 
6
 
7
  import { cn } from "@/lib/utils";
8
  import { GridPattern } from "@/components/magic-ui/grid-pattern";
9
- import { useEditor } from "@/hooks/useEditor";
10
- import { useAi } from "@/hooks/useAi";
11
  import { htmlTagToText } from "@/lib/html-tag-to-text";
12
- import { AnimatedBlobs } from "@/components/animated-blobs";
13
- import { AiLoading } from "../ask-ai/loading";
14
- import { defaultHTML } from "@/lib/consts";
15
- import { Button } from "@/components/ui/button";
16
- import { LivePreview, LivePreviewRef } 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";
22
-
23
- export const Preview = forwardRef<LivePreviewRef, { isNew: boolean }>(
24
- ({ isNew }, ref) => {
25
- const {
26
- project,
27
- device,
28
- isLoadingProject,
29
- currentTab,
30
- currentCommit,
31
- setCurrentCommit,
32
- currentPageData,
33
- pages,
34
- setPages,
35
- setCurrentPage,
36
- isSameHtml,
37
- } = useEditor();
38
- const {
39
- isEditableModeEnabled,
40
- setSelectedElement,
41
- isAiWorking,
42
- globalAiLoading,
43
- } = useAi();
44
-
45
- const iframeRef = useRef<HTMLIFrameElement>(null);
46
-
47
- const [hoveredElement, setHoveredElement] = useState<{
48
- tagName: string;
49
- rect: { top: number; left: number; width: number; height: number };
50
- } | null>(null);
51
- const [isPromotingVersion, setIsPromotingVersion] = useState(false);
52
- const [stableHtml, setStableHtml] = useState<string>("");
53
-
54
- useEffect(() => {
55
- if (!isAiWorking && !globalAiLoading && currentPageData?.html) {
56
- setStableHtml(currentPageData.html);
57
- }
58
- }, [isAiWorking, globalAiLoading, currentPageData?.html]);
59
-
60
- useEffect(() => {
61
- if (
62
- currentPageData?.html &&
63
- !stableHtml &&
64
- !isAiWorking &&
65
- !globalAiLoading
66
- ) {
67
- setStableHtml(currentPageData.html);
68
- }
69
- }, [currentPageData?.html, stableHtml, isAiWorking, globalAiLoading]);
70
 
71
- useUpdateEffect(() => {
72
- const cleanupListeners = () => {
73
- if (iframeRef?.current?.contentDocument) {
74
- const iframeDocument = iframeRef.current.contentDocument;
75
- iframeDocument.removeEventListener("mouseover", handleMouseOver);
76
- iframeDocument.removeEventListener("mouseout", handleMouseOut);
77
- iframeDocument.removeEventListener("click", handleClick);
78
- }
79
- };
80
-
81
- if (iframeRef?.current) {
82
- const iframeDocument = iframeRef.current.contentDocument;
83
- if (iframeDocument) {
84
- cleanupListeners();
85
-
86
- if (isEditableModeEnabled) {
87
- iframeDocument.addEventListener("mouseover", handleMouseOver);
88
- iframeDocument.addEventListener("mouseout", handleMouseOut);
89
- iframeDocument.addEventListener("click", handleClick);
90
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
  }
93
-
94
- return cleanupListeners;
95
- }, [iframeRef, isEditableModeEnabled]);
96
-
97
- const promoteVersion = async () => {
98
- setIsPromotingVersion(true);
99
- await api
100
- .post(
101
- `/me/projects/${project?.space_id}/commits/${currentCommit}/promote`
102
- )
103
- .then((res) => {
104
- if (res.data.ok) {
105
- setCurrentCommit(null);
106
- setPages(res.data.pages);
107
- setCurrentPage(res.data.pages[0].path);
108
- toast.success("Version promoted successfully");
109
- }
110
- })
111
- .catch((err) => {
112
- toast.error(err.response.data.error);
113
- });
114
- setIsPromotingVersion(false);
115
- };
116
-
117
- const handleMouseOver = (event: MouseEvent) => {
118
- if (iframeRef?.current) {
119
- const iframeDocument = iframeRef.current.contentDocument;
120
- if (iframeDocument) {
121
- const targetElement = event.target as HTMLElement;
122
- if (
123
- hoveredElement?.tagName !== targetElement.tagName ||
124
- hoveredElement?.rect.top !==
125
- targetElement.getBoundingClientRect().top ||
126
- hoveredElement?.rect.left !==
127
- targetElement.getBoundingClientRect().left ||
128
- hoveredElement?.rect.width !==
129
- targetElement.getBoundingClientRect().width ||
130
- hoveredElement?.rect.height !==
131
- targetElement.getBoundingClientRect().height
132
- ) {
133
- if (targetElement !== iframeDocument.body) {
134
- const rect = targetElement.getBoundingClientRect();
135
- setHoveredElement({
136
- tagName: targetElement.tagName,
137
- rect: {
138
- top: rect.top,
139
- left: rect.left,
140
- width: rect.width,
141
- height: rect.height,
142
- },
143
- });
144
- targetElement.classList.add("hovered-element");
145
- } else {
146
- return setHoveredElement(null);
147
- }
148
- }
149
  }
150
  }
151
- };
152
- const handleMouseOut = () => {
153
- setHoveredElement(null);
154
- };
155
- const handleClick = (event: MouseEvent) => {
156
- if (iframeRef?.current) {
157
  const iframeDocument = iframeRef.current.contentDocument;
158
- if (iframeDocument) {
159
- const targetElement = event.target as HTMLElement;
160
- if (targetElement !== iframeDocument.body) {
161
- setSelectedElement(targetElement);
162
- }
163
- }
164
  }
165
  };
166
 
167
- const handleCustomNavigation = (event: MouseEvent) => {
168
- if (iframeRef?.current) {
169
- const iframeDocument = iframeRef.current.contentDocument;
170
- if (iframeDocument) {
171
- const findClosestAnchor = (
172
- element: HTMLElement
173
- ): HTMLAnchorElement | null => {
174
- let current = element;
175
- while (current && current !== iframeDocument.body) {
176
- if (current.tagName === "A") {
177
- return current as HTMLAnchorElement;
178
- }
179
- current = current.parentElement as HTMLElement;
180
- }
181
- return null;
182
- };
183
-
184
- const anchorElement = findClosestAnchor(event.target as HTMLElement);
185
- if (anchorElement) {
186
- let href = anchorElement.getAttribute("href");
187
- if (href) {
188
- event.stopPropagation();
189
- event.preventDefault();
190
-
191
- if (href.includes("#") && !href.includes(".html")) {
192
- const targetElement = iframeDocument.querySelector(href);
193
- if (targetElement) {
194
- targetElement.scrollIntoView({ behavior: "smooth" });
195
- }
196
- return;
197
- }
198
 
199
- href = href.split(".html")[0] + ".html";
200
- const isPageExist = pages.some((page) => page.path === href);
201
- if (isPageExist) {
202
- setCurrentPage(href);
203
- }
204
- }
205
- }
206
  }
207
  }
208
- };
209
-
210
- return (
211
- <div
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  className={classNames(
213
- "bg-neutral-900/30 w-full h-[calc(100dvh-57px)] flex flex-col items-center justify-center relative z-1 lg:border-l border-neutral-800",
214
  {
215
- "max-lg:h-0": currentTab === "chat",
216
- "max-lg:h-full": currentTab === "preview",
 
 
 
217
  }
218
  )}
219
- >
220
- <GridPattern
221
- x={-1}
222
- y={-1}
223
- strokeDasharray={"4 2"}
224
- className={cn(
225
- "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)] opacity-40"
226
- )}
227
- />
228
- {!isAiWorking && hoveredElement && isEditableModeEnabled && (
229
- <div
230
- 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"
231
- style={{
232
- top: hoveredElement.rect.top,
233
- left: hoveredElement.rect.left,
234
- width: hoveredElement.rect.width,
235
- height: hoveredElement.rect.height,
236
- }}
237
- >
238
- <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">
239
- {htmlTagToText(hoveredElement.tagName.toLowerCase())}
240
- </span>
241
- </div>
242
- )}
243
- {isNew && !isLoadingProject && !globalAiLoading && isSameHtml ? (
244
- <iframe
245
- className={classNames(
246
- "w-full select-none transition-all duration-200 bg-black h-full",
247
- {
248
- "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]":
249
- device === "mobile",
250
- }
251
- )}
252
- srcDoc={defaultHTML}
253
- />
254
- ) : (isNew && globalAiLoading) || isLoadingProject ? (
255
- <div className="w-full h-full flex items-center justify-center relative">
256
- <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
257
- <AiLoading
258
- text={isLoadingProject ? "Fetching your project..." : undefined}
259
- className="flex-col"
260
- />
261
- <AnimatedBlobs />
262
- <AnimatedBlobs />
263
- </div>
264
- {!isLoadingProject && (
265
- <LivePreview
266
- ref={ref}
267
- currentPageData={currentPageData}
268
- isAiWorking={isAiWorking}
269
- defaultHTML={defaultHTML}
270
- className="bottom-4 left-4"
271
- />
272
- )}
273
- </div>
274
- ) : (
275
- <>
276
- <iframe
277
- id="preview-iframe"
278
- ref={iframeRef}
279
- className={classNames(
280
- "w-full select-none transition-all duration-200 bg-black h-full",
281
- {
282
- "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]":
283
- device === "mobile",
284
- }
285
- )}
286
- src={
287
- currentCommit
288
- ? `https://${project?.space_id?.replaceAll(
289
- "/",
290
- "-"
291
- )}--rev-${currentCommit.slice(0, 7)}.static.hf.space`
292
- : undefined
293
- }
294
- srcDoc={!currentCommit ? stableHtml : undefined}
295
- onLoad={
296
- !currentCommit
297
- ? () => {
298
- if (iframeRef?.current?.contentWindow?.document?.body) {
299
- iframeRef.current.contentWindow.document.body.scrollIntoView(
300
- {
301
- block: isAiWorking ? "end" : "start",
302
- inline: "nearest",
303
- behavior: isAiWorking ? "instant" : "smooth",
304
- }
305
- );
306
- }
307
- // add event listener to all links in the iframe to handle navigation
308
- if (iframeRef?.current?.contentWindow?.document) {
309
- const links =
310
- iframeRef.current.contentWindow.document.querySelectorAll(
311
- "a"
312
- );
313
- links.forEach((link) => {
314
- link.addEventListener(
315
- "click",
316
- handleCustomNavigation
317
- );
318
- });
319
- }
320
- }
321
- : undefined
322
- }
323
- sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
324
- 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"
325
- />
326
- <div
327
- className={classNames(
328
- "w-full h-full flex items-center justify-center absolute left-0 top-0 bg-black/40 backdrop-blur-lg transition-all duration-200",
329
- {
330
- "opacity-0 pointer-events-none": !globalAiLoading,
331
- }
332
- )}
333
- >
334
- <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
335
- <AiLoading
336
- text={
337
- isLoadingProject ? "Fetching your project..." : undefined
338
- }
339
- className="flex-col"
340
- />
341
- <AnimatedBlobs />
342
- <AnimatedBlobs />
343
- </div>
344
- </div>
345
- <HistoryNotification
346
- isVisible={!!currentCommit}
347
- isPromotingVersion={isPromotingVersion}
348
- onPromoteVersion={promoteVersion}
349
- onGoBackToCurrent={() => setCurrentCommit(null)}
350
- />
351
- </>
352
- )}
353
- </div>
354
- );
355
- }
356
- );
357
-
358
- Preview.displayName = "Preview";
 
1
  "use client";
 
 
2
  import { useUpdateEffect } from "react-use";
3
+ import { useMemo, useState } 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 = ({
12
+ html,
13
+ isResizing,
14
+ isAiWorking,
15
+ ref,
16
+ device,
17
+ currentTab,
18
+ iframeRef,
19
+ isEditableModeEnabled,
20
+ onClickElement,
21
+ }: {
22
+ html: string;
23
+ isResizing: boolean;
24
+ isAiWorking: boolean;
25
+ ref: React.RefObject<HTMLDivElement | null>;
26
+ iframeRef?: React.RefObject<HTMLIFrameElement | null>;
27
+ device: "desktop" | "mobile";
28
+ currentTab: string;
29
+ isEditableModeEnabled?: boolean;
30
+ onClickElement?: (element: HTMLElement) => void;
31
+ }) => {
32
+ const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
33
+ null
34
+ );
35
+
36
+ // add event listener to the iframe to track hovered elements
37
+ const handleMouseOver = (event: MouseEvent) => {
38
+ if (iframeRef?.current) {
39
+ const iframeDocument = iframeRef.current.contentDocument;
40
+ if (iframeDocument) {
41
+ const targetElement = event.target as HTMLElement;
42
+ if (
43
+ hoveredElement !== targetElement &&
44
+ targetElement !== iframeDocument.body
45
+ ) {
46
+ setHoveredElement(targetElement);
47
+ targetElement.classList.add("hovered-element");
48
+ } else {
49
+ return setHoveredElement(null);
50
  }
51
  }
52
+ }
53
+ };
54
+ const handleMouseOut = () => {
55
+ setHoveredElement(null);
56
+ };
57
+ const handleClick = (event: MouseEvent) => {
58
+ if (iframeRef?.current) {
59
+ const iframeDocument = iframeRef.current.contentDocument;
60
+ if (iframeDocument) {
61
+ const targetElement = event.target as HTMLElement;
62
+ if (targetElement !== iframeDocument.body) {
63
+ onClickElement?.(targetElement);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
  }
66
+ }
67
+ };
68
+
69
+ useUpdateEffect(() => {
70
+ const cleanupListeners = () => {
71
+ if (iframeRef?.current?.contentDocument) {
72
  const iframeDocument = iframeRef.current.contentDocument;
73
+ iframeDocument.removeEventListener("mouseover", handleMouseOver);
74
+ iframeDocument.removeEventListener("mouseout", handleMouseOut);
75
+ iframeDocument.removeEventListener("click", handleClick);
 
 
 
76
  }
77
  };
78
 
79
+ if (iframeRef?.current) {
80
+ const iframeDocument = iframeRef.current.contentDocument;
81
+ if (iframeDocument) {
82
+ // Clean up existing listeners first
83
+ cleanupListeners();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ if (isEditableModeEnabled) {
86
+ iframeDocument.addEventListener("mouseover", handleMouseOver);
87
+ iframeDocument.addEventListener("mouseout", handleMouseOut);
88
+ iframeDocument.addEventListener("click", handleClick);
 
 
 
89
  }
90
  }
91
+ }
92
+
93
+ // Clean up when component unmounts or dependencies change
94
+ return cleanupListeners;
95
+ }, [iframeRef, isEditableModeEnabled]);
96
+
97
+ const selectedElement = useMemo(() => {
98
+ if (!isEditableModeEnabled) return null;
99
+ if (!hoveredElement) return null;
100
+ return hoveredElement;
101
+ }, [hoveredElement, isEditableModeEnabled]);
102
+
103
+ return (
104
+ <div
105
+ ref={ref}
106
+ className={classNames(
107
+ "w-full border-l border-gray-900 h-full relative z-0 flex items-center justify-center",
108
+ {
109
+ "lg:p-4": currentTab !== "preview",
110
+ "max-lg:h-0": currentTab === "chat",
111
+ "max-lg:h-full": currentTab === "preview",
112
+ }
113
+ )}
114
+ onClick={(e) => {
115
+ if (isAiWorking) {
116
+ e.preventDefault();
117
+ e.stopPropagation();
118
+ toast.warning("Please wait for the AI to finish working.");
119
+ }
120
+ }}
121
+ >
122
+ <GridPattern
123
+ x={-1}
124
+ y={-1}
125
+ strokeDasharray={"4 2"}
126
+ className={cn(
127
+ "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)]"
128
+ )}
129
+ />
130
+ {!isAiWorking && hoveredElement && selectedElement && (
131
+ <div
132
+ 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"
133
+ style={{
134
+ top:
135
+ selectedElement.getBoundingClientRect().top +
136
+ (currentTab === "preview" ? 0 : 24),
137
+ left:
138
+ selectedElement.getBoundingClientRect().left +
139
+ (currentTab === "preview" ? 0 : 24),
140
+ width: selectedElement.getBoundingClientRect().width,
141
+ height: selectedElement.getBoundingClientRect().height,
142
+ }}
143
+ >
144
+ <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">
145
+ {htmlTagToText(selectedElement.tagName.toLowerCase())}
146
+ </span>
147
+ </div>
148
+ )}
149
+ <iframe
150
+ id="preview-iframe"
151
+ ref={iframeRef}
152
+ title="output"
153
  className={classNames(
154
+ "w-full select-none transition-all duration-200 bg-black h-full",
155
  {
156
+ "pointer-events-none": isResizing || isAiWorking,
157
+ "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]":
158
+ device === "mobile",
159
+ "lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
160
+ currentTab !== "preview" && device === "desktop",
161
  }
162
  )}
163
+ srcDoc={html}
164
+ onLoad={() => {
165
+ if (iframeRef?.current?.contentWindow?.document?.body) {
166
+ iframeRef.current.contentWindow.document.body.scrollIntoView({
167
+ block: isAiWorking ? "end" : "start",
168
+ inline: "nearest",
169
+ behavior: isAiWorking ? "instant" : "smooth",
170
+ });
171
+ }
172
+ }}
173
+ />
174
+ </div>
175
+ );
176
+ };