This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. README.md +2 -8
  2. app/(public)/layout.tsx +1 -1
  3. app/(public)/page.tsx +31 -180
  4. app/(public)/projects/page.tsx +8 -4
  5. app/actions/projects.ts +40 -24
  6. app/api/{ask → ask-ai}/route.ts +121 -330
  7. app/api/auth/login-url/route.ts +0 -23
  8. app/api/auth/logout/route.ts +0 -25
  9. app/api/auth/route.ts +1 -21
  10. app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -190
  11. app/api/me/projects/[namespace]/[repoId]/images/route.ts +0 -113
  12. app/api/me/projects/[namespace]/[repoId]/route.ts +162 -112
  13. app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -64
  14. app/api/me/projects/route.ts +92 -73
  15. app/api/me/route.ts +1 -22
  16. app/auth/callback/page.tsx +42 -67
  17. app/layout.tsx +8 -16
  18. app/projects/[namespace]/[repoId]/page.tsx +31 -1
  19. app/projects/new/page.tsx +2 -2
  20. assets/deepseek.svg +0 -1
  21. assets/globals.css +0 -225
  22. assets/kimi.svg +0 -1
  23. assets/qwen.svg +0 -1
  24. components.json +1 -1
  25. components/animated-blobs/index.tsx +0 -34
  26. components/animated-text/index.tsx +0 -123
  27. components/contexts/app-context.tsx +10 -6
  28. components/contexts/login-context.tsx +0 -62
  29. components/contexts/pro-context.tsx +0 -48
  30. components/editor/ask-ai/fake-ask.tsx +0 -97
  31. components/editor/ask-ai/follow-up-tooltip.tsx +36 -0
  32. components/editor/ask-ai/index.tsx +347 -198
  33. components/editor/ask-ai/loading.tsx +0 -68
  34. components/editor/ask-ai/prompt-builder/content-modal.tsx +0 -196
  35. components/editor/ask-ai/prompt-builder/index.tsx +0 -68
  36. components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +0 -58
  37. components/editor/ask-ai/prompt-builder/themes.tsx +0 -48
  38. components/editor/ask-ai/re-imagine.tsx +4 -10
  39. components/editor/ask-ai/selected-files.tsx +0 -47
  40. components/editor/ask-ai/selector.tsx +0 -41
  41. components/editor/ask-ai/settings.tsx +146 -171
  42. components/editor/ask-ai/uploader.tsx +0 -165
  43. components/editor/deploy-button/index.tsx +173 -0
  44. components/editor/footer/index.tsx +127 -0
  45. components/editor/header/index.tsx +48 -92
  46. components/editor/header/switch-tab.tsx +0 -58
  47. components/editor/history-notification/index.tsx +0 -119
  48. components/editor/history/index.tsx +30 -66
  49. components/editor/index.tsx +316 -127
  50. components/editor/live-preview/index.tsx +0 -165
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: DeepSite v3
3
  emoji: 🐳
4
  colorFrom: blue
5
  colorTo: blue
@@ -11,12 +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
- - deepseek-ai/DeepSeek-V3.2-Exp
20
  ---
21
 
22
  # DeepSite 🐳
@@ -25,4 +19,4 @@ DeepSite is a coding platform powered by DeepSeek AI, designed to make coding sm
25
 
26
  ## How to use it locally
27
 
28
- 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,29 +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 { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
22
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
23
- import { Page } from "@/types";
24
- import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
25
- import { isAuthenticated } from "@/lib/auth";
26
- import { getBestProvider } from "@/lib/best-provider";
27
- import { rewritePrompt } from "@/lib/rewrite-prompt";
28
- import { COLORS } from "@/lib/utils";
29
- import { templates } from "@/lib/templates";
30
 
31
  const ipAddresses = new Map();
32
 
@@ -35,7 +22,7 @@ export async function POST(request: NextRequest) {
35
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
36
 
37
  const body = await request.json();
38
- const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
39
 
40
  if (!model || (!prompt && !redesignMarkdown)) {
41
  return NextResponse.json(
@@ -47,7 +34,6 @@ export async function POST(request: NextRequest) {
47
  const selectedModel = MODELS.find(
48
  (m) => m.value === model || m.label === model
49
  );
50
-
51
  if (!selectedModel) {
52
  return NextResponse.json(
53
  { ok: false, error: "Invalid model selected" },
@@ -66,8 +52,7 @@ export async function POST(request: NextRequest) {
66
  );
67
  }
68
 
69
- let token: string | null = null;
70
- if (userToken) token = userToken;
71
  let billTo: string | null = null;
72
 
73
  /**
@@ -100,19 +85,19 @@ export async function POST(request: NextRequest) {
100
  billTo = "huggingface";
101
  }
102
 
103
- const selectedProvider = await getBestProvider(selectedModel.value, provider)
104
-
105
- let rewrittenPrompt = redesignMarkdown ? `Here 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.` : prompt;
106
-
107
- if (enhancedSettings.isActive) {
108
- // rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
109
- }
110
 
111
  try {
 
112
  const encoder = new TextEncoder();
113
  const stream = new TransformStream();
114
  const writer = stream.writable.getWriter();
115
 
 
116
  const response = new NextResponse(stream.readable, {
117
  headers: {
118
  "Content-Type": "text/plain; charset=utf-8",
@@ -122,58 +107,75 @@ export async function POST(request: NextRequest) {
122
  });
123
 
124
  (async () => {
125
- // let completeResponse = "";
126
  try {
127
  const client = new InferenceClient(token);
128
-
129
- // Calculate dynamic max_tokens based on provider and input size
130
- const systemPrompt = INITIAL_SYSTEM_PROMPT + (enhancedSettings.isActive ? `
131
- Here are some examples of designs that you can inspire from:
132
- ${templates.map((template) => `- ${template}`).join("\n")}
133
- IMPORTANT: Use the templates as inspiration, but do not copy them exactly.
134
- Try to create a unique design, based on the templates, but not exactly like them, mostly depending on the user's prompt. These are just examples, do not copy them exactly.
135
- ` : "");
136
-
137
- const userPrompt = rewrittenPrompt;
138
- const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
139
- const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
140
- const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
141
-
142
  const chatCompletion = client.chatCompletionStream(
143
  {
144
  model: selectedModel.value,
145
- provider: selectedProvider.provider,
146
  messages: [
147
  {
148
  role: "system",
149
- content: systemPrompt,
150
  },
151
  {
152
  role: "user",
153
- content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
154
- 2. I want to use the following secondary color: ${enhancedSettings.secondaryColor} (eg: bg-${enhancedSettings.secondaryColor}-500).
155
- 3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
 
 
156
  },
157
  ],
158
- ...providerConfig,
159
  },
160
  billTo ? { billTo } : {}
161
  );
162
 
163
  while (true) {
164
- const { done, value } = await chatCompletion.next()
165
  if (done) {
166
  break;
167
  }
168
 
169
  const chunk = value.choices[0]?.delta?.content;
170
  if (chunk) {
171
- await writer.write(encoder.encode(chunk));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
  }
174
-
175
- // Explicitly close the writer after successful completion
176
- await writer.close();
177
  } catch (error: any) {
178
  if (error.message?.includes("exceeded your monthly included credits")) {
179
  await writer.write(
@@ -185,18 +187,7 @@ Try to create a unique design, based on the templates, but not exactly like them
185
  })
186
  )
187
  );
188
- } else if (error?.message?.includes("inference provider information")) {
189
- await writer.write(
190
- encoder.encode(
191
- JSON.stringify({
192
- ok: false,
193
- openSelectProvider: true,
194
- message: error.message,
195
- })
196
- )
197
- );
198
- }
199
- else {
200
  await writer.write(
201
  encoder.encode(
202
  JSON.stringify({
@@ -209,12 +200,7 @@ Try to create a unique design, based on the templates, but not exactly like them
209
  );
210
  }
211
  } finally {
212
- // Ensure the writer is always closed, even if already closed
213
- try {
214
- await writer?.close();
215
- } catch {
216
- // Ignore errors when closing the writer as it might already be closed
217
- }
218
  }
219
  })();
220
 
@@ -233,20 +219,14 @@ Try to create a unique design, based on the templates, but not exactly like them
233
  }
234
 
235
  export async function PUT(request: NextRequest) {
236
- const user = await isAuthenticated();
237
- if (user instanceof NextResponse || !user) {
238
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
239
- }
240
-
241
  const authHeaders = await headers();
 
242
 
243
  const body = await request.json();
244
- const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew, enhancedSettings } =
245
  body;
246
 
247
- let repoId = repoIdFromBody;
248
-
249
- if (!prompt || pages.length === 0) {
250
  return NextResponse.json(
251
  { ok: false, error: "Missing required fields" },
252
  { status: 400 }
@@ -263,7 +243,7 @@ export async function PUT(request: NextRequest) {
263
  );
264
  }
265
 
266
- let token = user.token as string;
267
  let billTo: string | null = null;
268
 
269
  /**
@@ -298,62 +278,47 @@ export async function PUT(request: NextRequest) {
298
 
299
  const client = new InferenceClient(token);
300
 
301
- // Helper function to escape regex special characters
302
- const escapeRegExp = (string: string) => {
303
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
304
- };
305
-
306
- // Helper function to create flexible HTML regex that handles varying spaces
307
- const createFlexibleHtmlRegex = (searchBlock: string) => {
308
- let searchRegex = escapeRegExp(searchBlock)
309
- .replace(/\s+/g, '\\s*') // Allow any amount of whitespace where there are spaces
310
- .replace(/>\s*</g, '>\\s*<') // Allow spaces between HTML tags
311
- .replace(/\s*>/g, '\\s*>'); // Allow spaces before closing >
312
-
313
- return new RegExp(searchRegex, 'g');
314
- };
315
-
316
- const selectedProvider = await getBestProvider(selectedModel.value, provider)
317
 
318
  try {
319
- // Calculate dynamic max_tokens for PUT request
320
- const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
321
- const userContext = previousPrompts
322
- ? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
323
- : "You are modifying the HTML file based on the user's request.";
324
- const assistantContext = `${
325
- selectedElementHtml
326
- ? `\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.`
327
- : ""
328
- }. 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")}.` : ""}`;
329
-
330
- const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
331
- const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
332
- const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
333
-
334
  const response = await client.chatCompletion(
335
  {
336
  model: selectedModel.value,
337
- provider: selectedProvider.provider,
338
  messages: [
339
  {
340
  role: "system",
341
- content: systemPrompt,
342
  },
343
  {
344
  role: "user",
345
- content: userContext,
 
 
346
  },
347
  {
348
  role: "assistant",
349
- content: assistantContext,
 
 
 
 
 
350
  },
351
  {
352
  role: "user",
353
  content: prompt,
354
  },
355
  ],
356
- ...providerConfig,
 
 
 
 
357
  },
358
  billTo ? { billTo } : {}
359
  );
@@ -368,234 +333,61 @@ export async function PUT(request: NextRequest) {
368
 
369
  if (chunk) {
370
  const updatedLines: number[][] = [];
371
- let newHtml = "";
372
- const updatedPages = [...(pages || [])];
373
-
374
- 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');
375
- let updatePageMatch;
376
-
377
- while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
378
- const [, pagePath, pageContent] = updatePageMatch;
379
-
380
- const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
381
- if (pageIndex !== -1) {
382
- let pageHtml = updatedPages[pageIndex].html;
383
-
384
- let processedContent = pageContent;
385
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
386
- if (htmlMatch) {
387
- processedContent = htmlMatch[1];
388
- }
389
- let position = 0;
390
- let moreBlocks = true;
391
-
392
- while (moreBlocks) {
393
- const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
394
- if (searchStartIndex === -1) {
395
- moreBlocks = false;
396
- continue;
397
- }
398
-
399
- const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
400
- if (dividerIndex === -1) {
401
- moreBlocks = false;
402
- continue;
403
- }
404
-
405
- const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
406
- if (replaceEndIndex === -1) {
407
- moreBlocks = false;
408
- continue;
409
- }
410
-
411
- const searchBlock = processedContent.substring(
412
- searchStartIndex + SEARCH_START.length,
413
- dividerIndex
414
- );
415
- const replaceBlock = processedContent.substring(
416
- dividerIndex + DIVIDER.length,
417
- replaceEndIndex
418
- );
419
-
420
- if (searchBlock.trim() === "") {
421
- pageHtml = `${replaceBlock}\n${pageHtml}`;
422
- updatedLines.push([1, replaceBlock.split("\n").length]);
423
- } else {
424
- const regex = createFlexibleHtmlRegex(searchBlock);
425
- const match = regex.exec(pageHtml);
426
-
427
- if (match) {
428
- const matchedText = match[0];
429
- const beforeText = pageHtml.substring(0, match.index);
430
- const startLineNumber = beforeText.split("\n").length;
431
- const replaceLines = replaceBlock.split("\n").length;
432
- const endLineNumber = startLineNumber + replaceLines - 1;
433
-
434
- updatedLines.push([startLineNumber, endLineNumber]);
435
- pageHtml = pageHtml.replace(matchedText, replaceBlock);
436
- }
437
- }
438
-
439
- position = replaceEndIndex + REPLACE_END.length;
440
- }
441
-
442
- updatedPages[pageIndex].html = pageHtml;
443
-
444
- if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
445
- newHtml = pageHtml;
446
- }
447
  }
448
- }
449
 
450
- 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');
451
- let newPageMatch;
452
-
453
- while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
454
- const [, pagePath, pageContent] = newPageMatch;
455
-
456
- let pageHtml = pageContent;
457
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
458
- if (htmlMatch) {
459
- pageHtml = htmlMatch[1];
460
- }
461
-
462
- const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
463
-
464
- if (existingPageIndex !== -1) {
465
- updatedPages[existingPageIndex] = {
466
- path: pagePath,
467
- html: pageHtml.trim()
468
- };
469
- } else {
470
- updatedPages.push({
471
- path: pagePath,
472
- html: pageHtml.trim()
473
- });
474
  }
475
- }
476
-
477
- if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
478
- let position = 0;
479
- let moreBlocks = true;
480
-
481
- while (moreBlocks) {
482
- const searchStartIndex = chunk.indexOf(SEARCH_START, position);
483
- if (searchStartIndex === -1) {
484
- moreBlocks = false;
485
- continue;
486
- }
487
 
488
- const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
489
- if (dividerIndex === -1) {
490
- moreBlocks = false;
491
- continue;
492
- }
493
-
494
- const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
495
- if (replaceEndIndex === -1) {
496
- moreBlocks = false;
497
- continue;
498
- }
499
 
500
- const searchBlock = chunk.substring(
501
- searchStartIndex + SEARCH_START.length,
502
- dividerIndex
503
- );
504
- const replaceBlock = chunk.substring(
505
- dividerIndex + DIVIDER.length,
506
- replaceEndIndex
507
- );
508
 
509
- if (searchBlock.trim() === "") {
510
- newHtml = `${replaceBlock}\n${newHtml}`;
511
- updatedLines.push([1, replaceBlock.split("\n").length]);
512
- } else {
513
- const regex = createFlexibleHtmlRegex(searchBlock);
514
- const match = regex.exec(newHtml);
515
-
516
- if (match) {
517
- const matchedText = match[0];
518
- const beforeText = newHtml.substring(0, match.index);
519
- const startLineNumber = beforeText.split("\n").length;
520
- const replaceLines = replaceBlock.split("\n").length;
521
- const endLineNumber = startLineNumber + replaceLines - 1;
522
-
523
- updatedLines.push([startLineNumber, endLineNumber]);
524
- newHtml = newHtml.replace(matchedText, replaceBlock);
525
- }
526
  }
527
-
528
- position = replaceEndIndex + REPLACE_END.length;
529
  }
530
 
531
- // Update the main HTML if it's the index page
532
- const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
533
- if (mainPageIndex !== -1) {
534
- updatedPages[mainPageIndex].html = newHtml;
535
- }
536
  }
537
 
538
- const files: File[] = [];
539
- updatedPages.forEach((page: Page) => {
540
- const file = new File([page.html], page.path, { type: "text/html" });
541
- files.push(file);
542
- });
543
-
544
- if (isNew) {
545
- const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
546
- const formattedTitle = projectName?.toLowerCase()
547
- .replace(/[^a-z0-9]+/g, "-")
548
- .split("-")
549
- .filter(Boolean)
550
- .join("-")
551
- .slice(0, 96);
552
- const repo: RepoDesignation = {
553
- type: "space",
554
- name: `${user.name}/${formattedTitle}`,
555
- };
556
- const { repoUrl} = await createRepo({
557
- repo,
558
- accessToken: user.token as string,
559
- });
560
- repoId = repoUrl.split("/").slice(-2).join("/");
561
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
562
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
563
- const README = `---
564
- title: ${projectName}
565
- colorFrom: ${colorFrom}
566
- colorTo: ${colorTo}
567
- emoji: 🐳
568
- sdk: static
569
- pinned: false
570
- tags:
571
- - deepsite-v3
572
- ---
573
-
574
- # Welcome to your new DeepSite project!
575
- This project was created with [DeepSite](https://deepsite.hf.co).
576
- `;
577
- files.push(new File([README], "README.md", { type: "text/markdown" }));
578
- }
579
-
580
- const response = await uploadFiles({
581
- repo: {
582
- type: "space",
583
- name: repoId,
584
- },
585
- files,
586
- commitTitle: prompt,
587
- accessToken: user.token as string,
588
- });
589
-
590
  return NextResponse.json({
591
  ok: true,
 
592
  updatedLines,
593
- pages: updatedPages,
594
- repoId,
595
- commit: {
596
- ...response.commit,
597
- title: prompt,
598
- }
599
  });
600
  } else {
601
  return NextResponse.json(
@@ -625,4 +417,3 @@ This project was created with [DeepSite](https://deepsite.hf.co).
625
  );
626
  }
627
  }
628
-
 
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" },
 
52
  );
53
  }
54
 
55
+ let token = userToken;
 
56
  let billTo: string | null = null;
57
 
58
  /**
 
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",
120
+ content: INITIAL_SYSTEM_PROMPT,
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
  },
323
  billTo ? { billTo } : {}
324
  );
 
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/deepseek.svg DELETED
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
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/kimi.svg DELETED
assets/qwen.svg DELETED
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,62 +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
- prompt?: string;
17
- description?: string;
18
- }
19
-
20
- const LoginContext = createContext<LoginContextType | undefined>(undefined);
21
-
22
- export function LoginProvider({ children }: { children: ReactNode }) {
23
- const [isOpen, setIsOpen] = useState(false);
24
- const [modalOptions, setModalOptions] = useState<LoginModalOptions>({});
25
-
26
- const openLoginModal = (options: LoginModalOptions = {}) => {
27
- setModalOptions(options);
28
- setIsOpen(true);
29
- };
30
-
31
- const closeLoginModal = () => {
32
- setIsOpen(false);
33
- setModalOptions({});
34
- };
35
-
36
- const value = {
37
- isOpen,
38
- openLoginModal,
39
- closeLoginModal,
40
- };
41
-
42
- return (
43
- <LoginContext.Provider value={value}>
44
- {children}
45
- <LoginModal
46
- open={isOpen}
47
- onClose={setIsOpen}
48
- title={modalOptions.title}
49
- prompt={modalOptions.prompt}
50
- description={modalOptions.description}
51
- />
52
- </LoginContext.Provider>
53
- );
54
- }
55
-
56
- export function useLoginModal() {
57
- const context = useContext(LoginContext);
58
- if (context === undefined) {
59
- throw new Error("useLoginModal must be used within a LoginProvider");
60
- }
61
- return context;
62
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/fake-ask.tsx DELETED
@@ -1,97 +0,0 @@
1
- import { useState } from "react";
2
- import { useLocalStorage } from "react-use";
3
- import { ArrowUp, Dice6 } from "lucide-react";
4
- import { useRouter } from "next/navigation";
5
-
6
- import { Button } from "@/components/ui/button";
7
- import { PromptBuilder } from "./prompt-builder";
8
- import { EnhancedSettings } from "@/types";
9
- import { Settings } from "./settings";
10
- import classNames from "classnames";
11
- import { PROMPTS_FOR_AI } from "@/lib/prompts";
12
-
13
- export const FakeAskAi = () => {
14
- const router = useRouter();
15
- const [prompt, setPrompt] = useState("");
16
- const [openProvider, setOpenProvider] = useState(false);
17
- const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
18
- useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
19
- isActive: true,
20
- primaryColor: undefined,
21
- secondaryColor: undefined,
22
- theme: undefined,
23
- });
24
- const [, setPromptStorage] = useLocalStorage("prompt", "");
25
- const [randomPromptLoading, setRandomPromptLoading] = useState(false);
26
-
27
- const callAi = async () => {
28
- setPromptStorage(prompt);
29
- router.push("/projects/new");
30
- };
31
-
32
- const randomPrompt = () => {
33
- setRandomPromptLoading(true);
34
- setTimeout(() => {
35
- setPrompt(
36
- PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
37
- );
38
- setRandomPromptLoading(false);
39
- }, 400);
40
- };
41
-
42
- return (
43
- <div className="p-3 w-full max-w-xl mx-auto">
44
- <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">
45
- <div className="w-full relative flex items-start justify-between pr-4 pt-4">
46
- <textarea
47
- className="w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 px-4 pb-4 resize-none"
48
- placeholder="Ask DeepSite anything..."
49
- value={prompt}
50
- onChange={(e) => setPrompt(e.target.value)}
51
- onKeyDown={(e) => {
52
- if (e.key === "Enter" && !e.shiftKey) {
53
- callAi();
54
- }
55
- }}
56
- />
57
- <Button
58
- size="iconXs"
59
- variant="outline"
60
- className="!rounded-md"
61
- onClick={() => randomPrompt()}
62
- >
63
- <Dice6
64
- className={classNames("size-4", {
65
- "animate-spin animation-duration-500": randomPromptLoading,
66
- })}
67
- />
68
- </Button>
69
- </div>
70
- <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
71
- <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
72
- <PromptBuilder
73
- enhancedSettings={enhancedSettings!}
74
- setEnhancedSettings={setEnhancedSettings}
75
- />
76
- <Settings
77
- open={openProvider}
78
- isFollowUp={false}
79
- error=""
80
- onClose={setOpenProvider}
81
- />
82
- </div>
83
- <div className="flex items-center justify-end gap-2">
84
- <Button
85
- size="iconXs"
86
- variant="outline"
87
- className="!rounded-md"
88
- onClick={() => callAi()}
89
- >
90
- <ArrowUp className="size-4" />
91
- </Button>
92
- </div>
93
- </div>
94
- </div>
95
- </div>
96
- );
97
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,144 +1,276 @@
1
- import { useRef, useState } from "react";
 
 
2
  import classNames from "classnames";
3
- import { ArrowUp, ChevronDown, CircleStop, Dice6 } from "lucide-react";
4
- import { useLocalStorage, useUpdateEffect, useMount } from "react-use";
5
  import { toast } from "sonner";
 
 
 
6
 
7
- import { useAi } from "@/hooks/useAi";
8
- import { useEditor } from "@/hooks/useEditor";
9
- import { EnhancedSettings, Project } from "@/types";
10
- import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
11
- import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
12
- import { AiLoading } from "@/components/editor/ask-ai/loading";
13
  import { Button } from "@/components/ui/button";
14
- import { Uploader } from "@/components/editor/ask-ai/uploader";
 
 
 
 
15
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
16
- import { Selector } from "@/components/editor/ask-ai/selector";
17
- import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
18
- import { useUser } from "@/hooks/useUser";
19
- import { useLoginModal } from "@/components/contexts/login-context";
20
- import { Settings } from "./settings";
21
- import { useProModal } from "@/components/contexts/pro-context";
22
- import { MAX_FREE_PROJECTS } from "@/lib/utils";
23
- import { PROMPTS_FOR_AI } from "@/lib/prompts";
24
 
25
- export const AskAi = ({
26
- project,
27
- isNew,
28
  onScrollToBottom,
 
 
 
 
 
 
 
 
29
  }: {
30
- project?: Project;
31
- files?: string[];
32
- isNew?: boolean;
33
- onScrollToBottom?: () => void;
34
- }) => {
35
- const { user, projects } = useUser();
36
- const { isSameHtml, isUploading, pages, isLoadingProject } = useEditor();
37
- const {
38
- isAiWorking,
39
- isThinking,
40
- selectedFiles,
41
- setSelectedFiles,
42
- selectedElement,
43
- setSelectedElement,
44
- setIsThinking,
45
- callAiNewProject,
46
- callAiFollowUp,
47
- setModel,
48
- selectedModel,
49
- audio: hookAudio,
50
- cancelRequest,
51
- } = useAi(onScrollToBottom);
52
- const { openLoginModal } = useLoginModal();
53
- const { openProModal } = useProModal();
54
  const [openProvider, setOpenProvider] = useState(false);
55
  const [providerError, setProviderError] = useState("");
56
- const refThink = useRef<HTMLDivElement>(null);
57
-
58
- const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
59
- useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
60
- isActive: false,
61
- primaryColor: undefined,
62
- secondaryColor: undefined,
63
- theme: undefined,
64
- });
65
- const [promptStorage, , removePromptStorage] = useLocalStorage("prompt", "");
66
-
67
- const [isFollowUp, setIsFollowUp] = useState(true);
68
- const [prompt, setPrompt] = useState(
69
- promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
70
- );
71
- const [think, setThink] = useState("");
72
  const [openThink, setOpenThink] = useState(false);
73
- const [randomPromptLoading, setRandomPromptLoading] = useState(false);
 
 
74
 
75
- useMount(() => {
76
- if (promptStorage && promptStorage.trim() !== "") {
77
- callAi();
78
- }
79
- });
80
 
81
  const callAi = async (redesignMarkdown?: string) => {
82
- removePromptStorage();
83
- if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
84
- return openProModal([]);
85
  if (isAiWorking) return;
86
  if (!redesignMarkdown && !prompt.trim()) return;
 
 
 
 
 
87
 
88
- if (isFollowUp && !redesignMarkdown && !isSameHtml) {
89
- if (!user) return openLoginModal({ prompt });
90
- const result = await callAiFollowUp(prompt, enhancedSettings, isNew);
91
 
92
- if (result?.error) {
93
- handleError(result.error, result.message);
94
- return;
95
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- if (result?.success) {
98
- setPrompt("");
99
- }
100
- } else {
101
- const result = await callAiNewProject(
102
- prompt,
103
- enhancedSettings,
104
- redesignMarkdown,
105
- !!user
106
- );
107
 
108
- if (result?.error) {
109
- handleError(result.error, result.message);
110
- return;
111
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- if (result?.success) {
114
- setPrompt("");
115
- // if (selectedModel?.isThinker) {
116
- // setModel(MODELS[0].value);
117
- // }
 
 
 
118
  }
119
  }
120
  };
121
 
122
- const handleError = (error: string, message?: string) => {
123
- switch (error) {
124
- case "login_required":
125
- openLoginModal();
126
- break;
127
- case "provider_required":
128
- setOpenProvider(true);
129
- setProviderError(message || "");
130
- break;
131
- case "pro_required":
132
- openProModal([]);
133
- break;
134
- case "api_error":
135
- toast.error(message || "An error occurred");
136
- break;
137
- case "network_error":
138
- toast.error(message || "Network error occurred");
139
- break;
140
- default:
141
- toast.error("An unexpected error occurred");
142
  }
143
  };
144
 
@@ -148,19 +280,19 @@ export const AskAi = ({
148
  }
149
  }, [think]);
150
 
151
- const randomPrompt = () => {
152
- setRandomPromptLoading(true);
153
- setTimeout(() => {
154
- setPrompt(
155
- PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
156
- );
157
- setRandomPromptLoading(false);
158
- }, 400);
159
- };
160
 
161
  return (
162
- <div className="p-3 w-full">
163
- <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">
164
  {think && (
165
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
166
  <header
@@ -198,13 +330,6 @@ export const AskAi = ({
198
  </main>
199
  </div>
200
  )}
201
- <SelectedFiles
202
- files={selectedFiles}
203
- isAiWorking={isAiWorking}
204
- onDelete={(file) =>
205
- setSelectedFiles(selectedFiles.filter((f) => f !== file))
206
- }
207
- />
208
  {selectedElement && (
209
  <div className="px-4 pt-3">
210
  <SelectedHtmlElement
@@ -215,47 +340,36 @@ export const AskAi = ({
215
  </div>
216
  )}
217
  <div className="w-full relative flex items-center justify-between">
218
- {(isAiWorking || isUploading || isThinking || isLoadingProject) && (
219
- <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">
220
- <AiLoading
221
- text={
222
- isLoadingProject
223
- ? "Fetching your project..."
224
- : isUploading
225
- ? "Uploading images..."
226
- : isAiWorking && !isSameHtml
227
- ? "DeepSite is working..."
228
- : "DeepSite is thinking..."
229
- }
230
- />
231
- {isAiWorking && (
232
- <Button
233
- size="iconXs"
234
- variant="outline"
235
- className="!rounded-md mr-0.5"
236
- onClick={cancelRequest}
237
- >
238
- <CircleStop className="size-4" />
239
- </Button>
240
- )}
241
  </div>
242
  )}
243
- <textarea
244
- disabled={
245
- isAiWorking || isUploading || isThinking || isLoadingProject
246
- }
247
  className={classNames(
248
- "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
249
  {
250
- "!pt-2.5":
251
- selectedElement &&
252
- !(isAiWorking || isUploading || isThinking),
253
  }
254
  )}
255
  placeholder={
256
  selectedElement
257
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
258
- : isFollowUp && (!isSameHtml || pages?.length > 1)
259
  ? "Ask DeepSite for edits"
260
  : "Ask DeepSite anything..."
261
  }
@@ -267,56 +381,91 @@ export const AskAi = ({
267
  }
268
  }}
269
  />
270
- {isNew && !isAiWorking && isSameHtml && (
271
- <Button
272
- size="iconXs"
273
- variant="outline"
274
- className="!rounded-md -translate-y-2 -translate-x-4"
275
- onClick={() => randomPrompt()}
276
- >
277
- <Dice6
278
- className={classNames("size-4", {
279
- "animate-spin animation-duration-500": randomPromptLoading,
280
- })}
281
- />
282
- </Button>
283
- )}
284
  </div>
285
- <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
286
- <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
287
- <PromptBuilder
288
- enhancedSettings={enhancedSettings!}
289
- setEnhancedSettings={setEnhancedSettings}
290
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  <Settings
 
 
 
 
292
  open={openProvider}
293
  error={providerError}
294
  isFollowUp={!isSameHtml && isFollowUp}
295
  onClose={setOpenProvider}
296
  />
297
- {!isNew && <Uploader project={project} />}
298
- {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
299
- {!isNew && !isSameHtml && <Selector />}
300
- </div>
301
- <div className="flex items-center justify-end gap-2">
302
  <Button
303
  size="iconXs"
304
- variant="outline"
305
- className="!rounded-md"
306
- disabled={
307
- isAiWorking || isUploading || isThinking || !prompt.trim()
308
- }
309
  onClick={() => callAi()}
310
  >
311
  <ArrowUp className="size-4" />
312
  </Button>
313
  </div>
314
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  </div>
316
- <audio ref={hookAudio} id="audio" className="hidden">
317
  <source src="/success.mp3" type="audio/mpeg" />
318
  Your browser does not support the audio element.
319
  </audio>
320
  </div>
321
  );
322
- };
 
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
 
 
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
 
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
  }
 
381
  }
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,68 +0,0 @@
1
- "use client";
2
- import Loading from "@/components/loading";
3
- import { useState, useEffect } from "react";
4
- import { useInterval } from "react-use";
5
-
6
- const TEXTS = [
7
- "Teaching pixels to dance with style...",
8
- "AI is having a creative breakthrough...",
9
- "Channeling digital vibes into pure code...",
10
- "Summoning the website spirits...",
11
- "Brewing some algorithmic magic...",
12
- "Composing a symphony of divs and spans...",
13
- "Riding the wave of computational creativity...",
14
- "Aligning the stars for perfect design...",
15
- "Training circus animals to write CSS...",
16
- "Launching ideas into the digital stratosphere...",
17
- ];
18
-
19
- export const AiLoading = ({
20
- text,
21
- className,
22
- }: {
23
- text?: string;
24
- className?: string;
25
- }) => {
26
- const [selectedText, setSelectedText] = useState(
27
- text ?? TEXTS[0] // Start with first text to avoid hydration issues
28
- );
29
-
30
- // Set random text on client-side only to avoid hydration mismatch
31
- useEffect(() => {
32
- if (!text) {
33
- setSelectedText(TEXTS[Math.floor(Math.random() * TEXTS.length)]);
34
- }
35
- }, [text]);
36
-
37
- useInterval(() => {
38
- if (!text) {
39
- if (selectedText === TEXTS[TEXTS.length - 1]) {
40
- setSelectedText(TEXTS[0]);
41
- } else {
42
- setSelectedText(TEXTS[TEXTS.indexOf(selectedText) + 1]);
43
- }
44
- }
45
- }, 12000);
46
- return (
47
- <div className={`flex items-center justify-start gap-2 ${className}`}>
48
- <Loading overlay={false} className="!size-5 opacity-50" />
49
- <p className="text-neutral-400 text-sm">
50
- <span className="inline-flex">
51
- {selectedText.split("").map((char, index) => (
52
- <span
53
- key={index}
54
- className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
55
- style={{
56
- animationDelay: `${index * 0.1}s`,
57
- animationDuration: "1.3s",
58
- animationIterationCount: "infinite",
59
- }}
60
- >
61
- {char === " " ? "\u00A0" : char}
62
- </span>
63
- ))}
64
- </span>
65
- </p>
66
- </div>
67
- );
68
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,68 +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 { EnhancedSettings } from "@/types";
15
-
16
- export const PromptBuilder = ({
17
- enhancedSettings,
18
- setEnhancedSettings,
19
- }: {
20
- enhancedSettings: EnhancedSettings;
21
- setEnhancedSettings: (settings: EnhancedSettings) => void;
22
- }) => {
23
- const { globalAiLoading } = useAi();
24
- const { globalEditorLoading } = useEditor();
25
-
26
- const [open, setOpen] = useState(false);
27
- return (
28
- <>
29
- <Button
30
- size="xs"
31
- variant="outline"
32
- className="!rounded-md !border-white/10 !bg-gradient-to-r from-sky-400/15 to-purple-400/15 light-sweep hover:brightness-110"
33
- disabled={globalAiLoading || globalEditorLoading}
34
- onClick={() => {
35
- setOpen(true);
36
- }}
37
- >
38
- <WandSparkles className="size-3.5 text-sky-500 relative z-10" />
39
- <span className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text relative z-10">
40
- Enhance
41
- </span>
42
- </Button>
43
- <Dialog open={open} onOpenChange={() => setOpen(false)}>
44
- <DialogContent className="sm:max-w-xl !p-0 !rounded-3xl !bg-neutral-900 !border-neutral-800/80 !gap-0">
45
- <DialogTitle className="px-6 py-3.5 border-b border-neutral-800">
46
- <div className="flex items-center justify-start gap-2 text-neutral-200 text-base font-medium">
47
- <WandSparkles className="size-3.5" />
48
- <p>Enhance Prompt</p>
49
- </div>
50
- </DialogTitle>
51
- <ContentModal
52
- enhancedSettings={enhancedSettings}
53
- setEnhancedSettings={setEnhancedSettings}
54
- />
55
- <DialogFooter className="px-6 py-3.5 border-t border-neutral-800">
56
- <Button
57
- variant="bordered"
58
- size="default"
59
- onClick={() => setOpen(false)}
60
- >
61
- Close
62
- </Button>
63
- </DialogFooter>
64
- </DialogContent>
65
- </Dialog>
66
- </>
67
- );
68
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,8 +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
 
17
  export function ReImagine({
18
  onRedesign,
@@ -22,8 +20,6 @@ export function ReImagine({
22
  const [url, setUrl] = useState<string>("");
23
  const [open, setOpen] = useState(false);
24
  const [isLoading, setIsLoading] = useState(false);
25
- const { globalAiLoading } = useAi();
26
- const { globalEditorLoading } = useEditor();
27
 
28
  const checkIfUrlIsValid = (url: string) => {
29
  const urlPattern = new RegExp(
@@ -63,13 +59,11 @@ export function ReImagine({
63
  <form>
64
  <PopoverTrigger asChild>
65
  <Button
66
- size="xs"
67
- variant={open ? "default" : "outline"}
68
- className="!rounded-md"
69
- disabled={globalAiLoading || globalEditorLoading}
70
  >
71
- <Paintbrush className="size-3.5" />
72
- Redesign
73
  </Button>
74
  </PopoverTrigger>
75
  <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(
 
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,171 +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
- {selectedModel?.logo && (
81
- <Image
82
- src={selectedModel?.logo}
83
- alt={selectedModel.label}
84
- className={`size-3.5 ${open ? "" : "filter invert"}`}
85
- width={20}
86
- height={20}
87
- />
88
- )}
89
- <span className="truncate max-w-[120px]">
90
- {isMounted
91
- ? selectedModel?.label?.split(" ").join("-").toLowerCase()
92
- : "..."}
93
- </span>
94
- <ChevronDown className="size-3.5" />
95
- </Button>
96
- </PopoverTrigger>
97
- <PopoverContent
98
- className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
99
- align="center"
100
- >
101
- <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">
102
- Customize Settings
103
- </header>
104
- <main className="px-4 pt-5 pb-6 space-y-5">
105
- {error !== "" && (
106
- <p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
107
- {error}
108
- </p>
109
- )}
110
- <label className="block">
111
- <p className="text-neutral-300 text-sm mb-2.5">Choose a model</p>
112
- <Select defaultValue={model} onValueChange={setModel}>
113
- <SelectTrigger className="w-full">
114
- <SelectValue placeholder="Select a model" />
115
- </SelectTrigger>
116
- <SelectContent>
117
- <SelectGroup>
118
- <SelectLabel>Models</SelectLabel>
119
- {MODELS.map(
120
- ({
121
- value,
122
- label,
123
- isNew = false,
124
- isThinker = false,
125
- }: {
126
- value: string;
127
- label: string;
128
- isNew?: boolean;
129
- isThinker?: boolean;
130
- }) => (
131
- <SelectItem
132
- key={value}
133
- value={value}
134
- className=""
135
- disabled={isThinker && isFollowUp}
136
- >
137
- {label}
138
- {isNew && (
139
- <span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
140
- New
141
- </span>
142
- )}
143
- </SelectItem>
144
- )
145
- )}
146
- </SelectGroup>
147
- </SelectContent>
148
- </Select>
149
- </label>
150
- {isFollowUp && (
151
- <div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
152
- Note: You can&apos;t use a Thinker model for follow-up requests.
153
- We automatically switch to the default model for you.
154
- </div>
155
- )}
156
- <div className="flex flex-col gap-3">
157
- <div className="flex items-center justify-between">
158
- <div>
159
- <p className="text-neutral-300 text-sm mb-1.5">
160
- Use auto-provider
161
- </p>
162
- <p className="text-xs text-neutral-400/70">
163
- We&apos;ll automatically select the best provider for you
164
- based on your prompt.
165
- </p>
166
  </div>
167
- <div
168
- className={classNames(
169
- "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",
170
- {
171
- "!bg-sky-500": provider === "auto",
172
- }
173
- )}
174
- onClick={() => {
175
- const foundModel = MODELS.find(
176
- (m: { value: string }) => m.value === model
177
- );
178
- if (provider === "auto" && foundModel?.autoProvider) {
179
- setProvider(foundModel.autoProvider);
180
- } else {
181
- setProvider("auto");
182
- }
183
- }}
184
- >
185
  <div
186
  className={classNames(
187
- "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
188
  {
189
- "translate-x-4": provider === "auto",
190
  }
191
  )}
192
- />
193
- </div>
194
- </div>
195
- <label className="block">
196
- <p className="text-neutral-300 text-sm mb-2">
197
- Inference Provider
198
- </p>
199
- <div className="grid grid-cols-2 gap-1.5">
200
- {modelAvailableProviders.map((id: string) => (
201
- <Button
202
- key={id}
203
- variant={id === provider ? "default" : "secondary"}
204
- size="sm"
205
- onClick={() => {
206
- setProvider(id);
207
- }}
208
- >
209
- <Image
210
- src={`/providers/${id}.svg`}
211
- alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
212
- className="size-5 mr-2"
213
- width={20}
214
- height={20}
215
- />
216
- {PROVIDERS[id as keyof typeof PROVIDERS].name}
217
- {id === provider && (
218
- <CheckCheck className="ml-2 size-4 text-blue-500" />
219
  )}
220
- </Button>
221
- ))}
222
  </div>
223
- </label>
224
- </div>
225
- </main>
226
- </PopoverContent>
227
- </Popover>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  );
229
  }
 
 
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,113 +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
- <span className="max-lg:hidden">Start Vibe Coding</span>
105
- <span className="lg:hidden">Log In</span>
106
- <ArrowRight className="size-4" />
107
- </Button>
108
- )}
109
- </div>
110
  </div>
 
111
  </header>
112
  );
113
  }
 
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,152 +1,341 @@
1
  "use client";
2
- import { useMemo, useRef, useState, useEffect } from "react";
3
- import { useCopyToClipboard, useLocalStorage, 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
- import { Page } from "@/types";
22
-
23
- export const AppEditor = ({
24
- namespace,
25
- repoId,
26
- isNew = false,
27
- }: {
28
- namespace?: string;
29
- repoId?: string;
30
- isNew?: boolean;
31
- }) => {
32
- const {
33
- project,
34
- setPages,
35
- files,
36
- currentPageData,
37
- currentTab,
38
- currentCommit,
39
- hasUnsavedChanges,
40
- saveChanges,
41
- pages,
42
- } = useEditor(namespace, repoId);
43
- const livePreviewRef = useRef<LivePreviewRef>(null);
44
- const { isAiWorking } = useAi(undefined, livePreviewRef);
45
  const [, copyToClipboard] = useCopyToClipboard();
46
- const [showSavePopup, setShowSavePopup] = useState(false);
47
- const [pagesStorage, , removePagesStorage] = useLocalStorage<Page[]>("pages");
 
 
 
 
48
 
49
- const monacoRef = useRef<any>(null);
 
50
  const editor = useRef<HTMLDivElement>(null);
51
  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  useMount(() => {
54
- if (isNew && pagesStorage) {
55
- setPages(pagesStorage);
56
- removePagesStorage();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
  });
59
 
60
- useEffect(() => {
61
- if (hasUnsavedChanges && !isAiWorking && project?.name) {
62
- setShowSavePopup(true);
 
 
 
 
 
63
  } else {
64
- setShowSavePopup(false);
 
 
 
65
  }
66
- }, [hasUnsavedChanges, isAiWorking]);
 
 
 
 
67
 
68
  return (
69
- <section className="h-screen w-full bg-neutral-950 flex flex-col">
70
- <Header />
71
- <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full relative">
72
- <div
73
- ref={editor}
74
- className={classNames(
75
- "bg-neutral-900 relative flex h-full max-h-[calc(100dvh-47px)] w-full flex-col lg:max-w-[600px] transition-all duration-200",
76
- {
77
- "max-lg:hidden lg:!w-[0px] overflow-hidden":
78
- currentTab !== "chat",
79
- }
80
- )}
81
- >
82
- <ListPages />
83
- <CopyIcon
84
- className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
85
- onClick={() => {
86
- copyToClipboard(currentPageData.html);
87
- toast.success("HTML copied to clipboard!");
88
- }}
89
- />
90
- <Editor
91
- defaultLanguage="html"
92
- theme="vs-dark"
93
- loading={<Loading overlay={false} />}
94
- className="h-full absolute left-0 top-0 lg:min-w-[600px]"
95
- options={{
96
- colorDecorators: true,
97
- fontLigatures: true,
98
- theme: "vs-dark",
99
- minimap: { enabled: false },
100
- scrollbar: {
101
- horizontal: "hidden",
102
- },
103
- wordWrap: "on",
104
- readOnly: !!isAiWorking || !!currentCommit,
105
- readOnlyMessage: {
106
- value: currentCommit
107
- ? "You can't edit the code, as this is an old version of the project."
108
- : "Wait for DeepSite to finish working...",
109
- isTrusted: true,
110
- },
111
- }}
112
- value={currentPageData.html}
113
- onChange={(value) => {
114
- const newValue = value ?? "";
115
- setPages((prev) =>
116
- prev.map((page) =>
117
- page.path === currentPageData.path
118
- ? { ...page, html: newValue }
119
- : page
120
- )
121
- );
122
- }}
123
- onMount={(editor, monaco) => {
124
- editorRef.current = editor;
125
- monacoRef.current = monaco;
126
- }}
127
- />
128
- <AskAi
129
- project={project}
130
- files={files}
131
- isNew={isNew}
132
- onScrollToBottom={() => {
133
- editorRef.current?.revealLine(
134
- editorRef.current?.getModel()?.getLineCount() ?? 0
135
- );
136
- }}
137
- />
138
- </div>
139
- <Preview ref={livePreviewRef} isNew={isNew} />
140
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- {/* Save Changes Popup */}
143
- <SaveChangesPopup
144
- isOpen={showSavePopup}
145
- onClose={() => setShowSavePopup(false)}
146
- onSave={saveChanges}
147
- hasUnsavedChanges={hasUnsavedChanges}
148
- pages={pages}
149
- project={project}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  />
151
  </section>
152
  );
 
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-[90dvw] lg: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";