enzostvs HF Staff commited on
Commit
c10f8f8
·
1 Parent(s): de2f961
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app/(public)/layout.tsx +15 -0
  2. app/(public)/page.tsx +193 -0
  3. app/(public)/projects/page.tsx +12 -0
  4. app/actions/auth.ts +18 -0
  5. app/actions/projects.ts +79 -0
  6. app/api/ask/route.ts +541 -0
  7. app/api/auth/route.ts +86 -0
  8. app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +141 -0
  9. app/api/me/projects/[namespace]/[repoId]/images/route.ts +109 -0
  10. app/api/me/projects/[namespace]/[repoId]/route.ts +235 -0
  11. app/api/me/projects/route.ts +104 -0
  12. app/api/me/route.ts +25 -0
  13. app/api/proxy/route.ts +246 -0
  14. app/api/re-design/route.ts +39 -0
  15. app/auth/callback/page.tsx +74 -0
  16. app/auth/page.tsx +28 -0
  17. app/globals.css +0 -26
  18. app/layout.tsx +94 -12
  19. app/page.tsx +0 -103
  20. app/projects/[namespace]/[repoId]/page.tsx +10 -0
  21. app/projects/new/page.tsx +5 -0
  22. assets/globals.css +371 -0
  23. assets/logo.svg +316 -0
  24. assets/space.svg +7 -0
  25. components.json +21 -0
  26. components/animated-blobs/index.tsx +34 -0
  27. components/animated-text/index.tsx +123 -0
  28. components/contexts/app-context.tsx +52 -0
  29. components/contexts/login-context.tsx +61 -0
  30. components/contexts/pro-context.tsx +48 -0
  31. components/contexts/tanstack-query-context.tsx +31 -0
  32. components/contexts/user-context.tsx +8 -0
  33. components/editor/ask-ai/index.tsx +259 -0
  34. components/editor/ask-ai/loading.tsx +32 -0
  35. components/editor/ask-ai/prompt-builder/content-modal.tsx +196 -0
  36. components/editor/ask-ai/prompt-builder/index.tsx +73 -0
  37. components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +58 -0
  38. components/editor/ask-ai/prompt-builder/themes.tsx +48 -0
  39. components/editor/ask-ai/re-imagine.tsx +169 -0
  40. components/editor/ask-ai/selected-files.tsx +47 -0
  41. components/editor/ask-ai/selected-html-element.tsx +57 -0
  42. components/editor/ask-ai/selector.tsx +41 -0
  43. components/editor/ask-ai/settings.tsx +220 -0
  44. components/editor/ask-ai/uploader.tsx +165 -0
  45. components/editor/header/index.tsx +86 -0
  46. components/editor/header/switch-tab.tsx +58 -0
  47. components/editor/history/index.tsx +91 -0
  48. components/editor/index.tsx +106 -0
  49. components/editor/pages/index.tsx +24 -0
  50. components/editor/pages/page.tsx +56 -0
app/(public)/layout.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Navigation from "@/components/public/navigation";
2
+
3
+ export default async function PublicLayout({
4
+ children,
5
+ }: Readonly<{
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}
13
+ </div>
14
+ );
15
+ }
app/(public)/page.tsx ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
app/(public)/projects/page.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ if (!ok) {
8
+ return <NotLogged />;
9
+ }
10
+
11
+ return <MyProjects projects={projects} />;
12
+ }
app/actions/auth.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+
3
+ import { headers } from "next/headers";
4
+
5
+ export async function getAuth() {
6
+ const authList = await headers();
7
+ const host = authList.get("host") ?? "localhost:3000";
8
+ const url = host.includes("/spaces/enzostvs")
9
+ ? "enzostvs-deepsite.hf.space"
10
+ : host;
11
+ const redirect_uri =
12
+ `${host.includes("localhost") ? "http://" : "https://"}` +
13
+ url +
14
+ "/auth/callback";
15
+
16
+ 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`;
17
+ return loginRedirectUrl;
18
+ }
app/actions/projects.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
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
+
15
+ if (user instanceof NextResponse || !user) {
16
+ return {
17
+ ok: false,
18
+ projects: [],
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: true,
32
+ // isEmpty: true,
33
+ // projects: [],
34
+ // };
35
+ // }
36
+
37
+ // const mappedProjects = []
38
+
39
+ // for (const project of projects) {
40
+ // const space = await spaceInfo({
41
+ // name: project.space_id,
42
+ // accessToken: user.token as string,
43
+ // additionalFields: ["author", "cardData"],
44
+ // });
45
+ // if (!space.private) {
46
+ // mappedProjects.push({
47
+ // ...project,
48
+ // name: space.name,
49
+ // cardData: space.cardData,
50
+ // });
51
+ // }
52
+ // }
53
+ const projects = [];
54
+ // get user spaces from Hugging Face
55
+ for await (const space of listSpaces({
56
+ accessToken: user.token as string,
57
+ additionalFields: ["author", "cardData"],
58
+ search: {
59
+ owner: user.name,
60
+ }
61
+ })) {
62
+ if (
63
+ !space.private &&
64
+ space.sdk === "static" &&
65
+ Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
66
+ (
67
+ ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
68
+ ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
69
+ )
70
+ ) {
71
+ projects.push(space);
72
+ }
73
+ }
74
+
75
+ return {
76
+ ok: true,
77
+ projects,
78
+ };
79
+ }
app/api/ask/route.ts ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { NextRequest } from "next/server";
3
+ import { NextResponse } from "next/server";
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
+ NEW_PAGE_END,
14
+ NEW_PAGE_START,
15
+ REPLACE_END,
16
+ SEARCH_START,
17
+ UPDATE_PAGE_START,
18
+ UPDATE_PAGE_END,
19
+ } from "@/lib/prompts";
20
+ import MY_TOKEN_KEY from "@/lib/get-cookie-name";
21
+ import { Page } from "@/types";
22
+ import { uploadFiles } from "@huggingface/hub";
23
+ import { isAuthenticated } from "@/lib/auth";
24
+ import { getBestProvider } from "@/lib/best-provider";
25
+ import { rewritePrompt } from "@/lib/rewrite-prompt";
26
+
27
+ const ipAddresses = new Map();
28
+
29
+ export async function POST(request: NextRequest) {
30
+ const authHeaders = await headers();
31
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
32
+
33
+ const body = await request.json();
34
+ const { prompt, provider, model, redesignMarkdown, enhancedSettings } = body;
35
+
36
+ if (!model || (!prompt && !redesignMarkdown)) {
37
+ return NextResponse.json(
38
+ { ok: false, error: "Missing required fields" },
39
+ { status: 400 }
40
+ );
41
+ }
42
+
43
+ const selectedModel = MODELS.find(
44
+ (m) => m.value === model || m.label === model
45
+ );
46
+
47
+ if (!selectedModel) {
48
+ return NextResponse.json(
49
+ { ok: false, error: "Invalid model selected" },
50
+ { status: 400 }
51
+ );
52
+ }
53
+
54
+ if (!selectedModel.providers.includes(provider) && provider !== "auto") {
55
+ return NextResponse.json(
56
+ {
57
+ ok: false,
58
+ error: `The selected model does not support the ${provider} provider.`,
59
+ openSelectProvider: true,
60
+ },
61
+ { status: 400 }
62
+ );
63
+ }
64
+
65
+ let token = userToken;
66
+ let billTo: string | null = null;
67
+
68
+ /**
69
+ * Handle local usage token, this bypass the need for a user token
70
+ * and allows local testing without authentication.
71
+ * This is useful for development and testing purposes.
72
+ */
73
+ if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
74
+ token = process.env.HF_TOKEN;
75
+ }
76
+
77
+ const ip = authHeaders.get("x-forwarded-for")?.includes(",")
78
+ ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
79
+ : authHeaders.get("x-forwarded-for");
80
+
81
+ if (!token) {
82
+ ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
83
+ if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
84
+ return NextResponse.json(
85
+ {
86
+ ok: false,
87
+ openLogin: true,
88
+ message: "Log In to continue using the service",
89
+ },
90
+ { status: 429 }
91
+ );
92
+ }
93
+
94
+ token = process.env.DEFAULT_HF_TOKEN as string;
95
+ billTo = "huggingface";
96
+ }
97
+
98
+ const selectedProvider = await getBestProvider(selectedModel.value, provider)
99
+
100
+ let rewrittenPrompt = prompt;
101
+
102
+ if (enhancedSettings.isActive) {
103
+ rewrittenPrompt = await rewritePrompt(prompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider);
104
+ }
105
+
106
+ console.log(rewrittenPrompt);
107
+
108
+ try {
109
+ const encoder = new TextEncoder();
110
+ const stream = new TransformStream();
111
+ const writer = stream.writable.getWriter();
112
+
113
+ const response = new NextResponse(stream.readable, {
114
+ headers: {
115
+ "Content-Type": "text/plain; charset=utf-8",
116
+ "Cache-Control": "no-cache",
117
+ Connection: "keep-alive",
118
+ },
119
+ });
120
+
121
+ (async () => {
122
+ // let completeResponse = "";
123
+ try {
124
+ const client = new InferenceClient(token);
125
+ const chatCompletion = client.chatCompletionStream(
126
+ {
127
+ model: selectedModel.value,
128
+ provider: selectedProvider,
129
+ messages: [
130
+ {
131
+ role: "system",
132
+ content: INITIAL_SYSTEM_PROMPT,
133
+ },
134
+ {
135
+ role: "user",
136
+ content: `${rewritePrompt}${redesignMarkdown ? `\n\nHere is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : ""}`
137
+ },
138
+ ],
139
+ max_tokens: selectedProvider.max_tokens,
140
+ },
141
+ billTo ? { billTo } : {}
142
+ );
143
+
144
+ while (true) {
145
+ const { done, value } = await chatCompletion.next()
146
+ if (done) {
147
+ break;
148
+ }
149
+
150
+ const chunk = value.choices[0]?.delta?.content;
151
+ if (chunk) {
152
+ await writer.write(encoder.encode(chunk));
153
+ }
154
+ }
155
+
156
+ // Explicitly close the writer after successful completion
157
+ await writer.close();
158
+ } catch (error: any) {
159
+ if (error.message?.includes("exceeded your monthly included credits")) {
160
+ await writer.write(
161
+ encoder.encode(
162
+ JSON.stringify({
163
+ ok: false,
164
+ openProModal: true,
165
+ message: error.message,
166
+ })
167
+ )
168
+ );
169
+ } else if (error?.message?.includes("inference provider information")) {
170
+ await writer.write(
171
+ encoder.encode(
172
+ JSON.stringify({
173
+ ok: false,
174
+ openSelectProvider: true,
175
+ message: error.message,
176
+ })
177
+ )
178
+ );
179
+ }
180
+ else {
181
+ await writer.write(
182
+ encoder.encode(
183
+ JSON.stringify({
184
+ ok: false,
185
+ message:
186
+ error.message ||
187
+ "An error occurred while processing your request.",
188
+ })
189
+ )
190
+ );
191
+ }
192
+ } finally {
193
+ // Ensure the writer is always closed, even if already closed
194
+ try {
195
+ await writer?.close();
196
+ } catch {
197
+ // Ignore errors when closing the writer as it might already be closed
198
+ }
199
+ }
200
+ })();
201
+
202
+ return response;
203
+ } catch (error: any) {
204
+ return NextResponse.json(
205
+ {
206
+ ok: false,
207
+ openSelectProvider: true,
208
+ message:
209
+ error?.message || "An error occurred while processing your request.",
210
+ },
211
+ { status: 500 }
212
+ );
213
+ }
214
+ }
215
+
216
+ export async function PUT(request: NextRequest) {
217
+ const user = await isAuthenticated();
218
+ if (user instanceof NextResponse || !user) {
219
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
220
+ }
221
+
222
+ const authHeaders = await headers();
223
+
224
+ const body = await request.json();
225
+ const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId } =
226
+ body;
227
+
228
+ if (!prompt || pages.length === 0 || !repoId) {
229
+ return NextResponse.json(
230
+ { ok: false, error: "Missing required fields" },
231
+ { status: 400 }
232
+ );
233
+ }
234
+
235
+ const selectedModel = MODELS.find(
236
+ (m) => m.value === model || m.label === model
237
+ );
238
+ if (!selectedModel) {
239
+ return NextResponse.json(
240
+ { ok: false, error: "Invalid model selected" },
241
+ { status: 400 }
242
+ );
243
+ }
244
+
245
+ let token = user.token as string;
246
+ let billTo: string | null = null;
247
+
248
+ /**
249
+ * Handle local usage token, this bypass the need for a user token
250
+ * and allows local testing without authentication.
251
+ * This is useful for development and testing purposes.
252
+ */
253
+ if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
254
+ token = process.env.HF_TOKEN;
255
+ }
256
+
257
+ const ip = authHeaders.get("x-forwarded-for")?.includes(",")
258
+ ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
259
+ : authHeaders.get("x-forwarded-for");
260
+
261
+ if (!token) {
262
+ ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
263
+ if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
264
+ return NextResponse.json(
265
+ {
266
+ ok: false,
267
+ openLogin: true,
268
+ message: "Log In to continue using the service",
269
+ },
270
+ { status: 429 }
271
+ );
272
+ }
273
+
274
+ token = process.env.DEFAULT_HF_TOKEN as string;
275
+ billTo = "huggingface";
276
+ }
277
+
278
+ const client = new InferenceClient(token);
279
+
280
+ const selectedProvider = await getBestProvider(selectedModel.value, provider)
281
+
282
+ try {
283
+ const response = await client.chatCompletion(
284
+ {
285
+ model: selectedModel.value,
286
+ provider: selectedProvider,
287
+ messages: [
288
+ {
289
+ role: "system",
290
+ content: FOLLOW_UP_SYSTEM_PROMPT,
291
+ },
292
+ {
293
+ role: "user",
294
+ content: previousPrompts
295
+ ? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
296
+ : "You are modifying the HTML file based on the user's request.",
297
+ },
298
+ {
299
+ role: "assistant",
300
+
301
+ content: `${
302
+ selectedElementHtml
303
+ ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
304
+ : ""
305
+ }. 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")}.` : ""}`,
306
+ },
307
+ {
308
+ role: "user",
309
+ content: prompt,
310
+ },
311
+ ],
312
+ ...(selectedProvider.id !== "sambanova"
313
+ ? {
314
+ max_tokens: selectedProvider.max_tokens,
315
+ }
316
+ : {}),
317
+ },
318
+ billTo ? { billTo } : {}
319
+ );
320
+
321
+ const chunk = response.choices[0]?.message?.content;
322
+ if (!chunk) {
323
+ return NextResponse.json(
324
+ { ok: false, message: "No content returned from the model" },
325
+ { status: 400 }
326
+ );
327
+ }
328
+
329
+ if (chunk) {
330
+ const updatedLines: number[][] = [];
331
+ let newHtml = "";
332
+ const updatedPages = [...(pages || [])];
333
+
334
+ 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');
335
+ let updatePageMatch;
336
+
337
+ while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
338
+ const [, pagePath, pageContent] = updatePageMatch;
339
+
340
+ const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
341
+ if (pageIndex !== -1) {
342
+ let pageHtml = updatedPages[pageIndex].html;
343
+
344
+ let processedContent = pageContent;
345
+ const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
346
+ if (htmlMatch) {
347
+ processedContent = htmlMatch[1];
348
+ }
349
+ let position = 0;
350
+ let moreBlocks = true;
351
+
352
+ while (moreBlocks) {
353
+ const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
354
+ if (searchStartIndex === -1) {
355
+ moreBlocks = false;
356
+ continue;
357
+ }
358
+
359
+ const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
360
+ if (dividerIndex === -1) {
361
+ moreBlocks = false;
362
+ continue;
363
+ }
364
+
365
+ const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
366
+ if (replaceEndIndex === -1) {
367
+ moreBlocks = false;
368
+ continue;
369
+ }
370
+
371
+ const searchBlock = processedContent.substring(
372
+ searchStartIndex + SEARCH_START.length,
373
+ dividerIndex
374
+ );
375
+ const replaceBlock = processedContent.substring(
376
+ dividerIndex + DIVIDER.length,
377
+ replaceEndIndex
378
+ );
379
+
380
+ if (searchBlock.trim() === "") {
381
+ pageHtml = `${replaceBlock}\n${pageHtml}`;
382
+ updatedLines.push([1, replaceBlock.split("\n").length]);
383
+ } else {
384
+ const blockPosition = pageHtml.indexOf(searchBlock);
385
+ if (blockPosition !== -1) {
386
+ const beforeText = pageHtml.substring(0, blockPosition);
387
+ const startLineNumber = beforeText.split("\n").length;
388
+ const replaceLines = replaceBlock.split("\n").length;
389
+ const endLineNumber = startLineNumber + replaceLines - 1;
390
+
391
+ updatedLines.push([startLineNumber, endLineNumber]);
392
+ pageHtml = pageHtml.replace(searchBlock, replaceBlock);
393
+ }
394
+ }
395
+
396
+ position = replaceEndIndex + REPLACE_END.length;
397
+ }
398
+
399
+ updatedPages[pageIndex].html = pageHtml;
400
+
401
+ if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
402
+ newHtml = pageHtml;
403
+ }
404
+ }
405
+ }
406
+
407
+ 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');
408
+ let newPageMatch;
409
+
410
+ while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
411
+ const [, pagePath, pageContent] = newPageMatch;
412
+
413
+ let pageHtml = pageContent;
414
+ const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
415
+ if (htmlMatch) {
416
+ pageHtml = htmlMatch[1];
417
+ }
418
+
419
+ const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
420
+
421
+ if (existingPageIndex !== -1) {
422
+ updatedPages[existingPageIndex] = {
423
+ path: pagePath,
424
+ html: pageHtml.trim()
425
+ };
426
+ } else {
427
+ updatedPages.push({
428
+ path: pagePath,
429
+ html: pageHtml.trim()
430
+ });
431
+ }
432
+ }
433
+
434
+ if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
435
+ let position = 0;
436
+ let moreBlocks = true;
437
+
438
+ while (moreBlocks) {
439
+ const searchStartIndex = chunk.indexOf(SEARCH_START, position);
440
+ if (searchStartIndex === -1) {
441
+ moreBlocks = false;
442
+ continue;
443
+ }
444
+
445
+ const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
446
+ if (dividerIndex === -1) {
447
+ moreBlocks = false;
448
+ continue;
449
+ }
450
+
451
+ const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
452
+ if (replaceEndIndex === -1) {
453
+ moreBlocks = false;
454
+ continue;
455
+ }
456
+
457
+ const searchBlock = chunk.substring(
458
+ searchStartIndex + SEARCH_START.length,
459
+ dividerIndex
460
+ );
461
+ const replaceBlock = chunk.substring(
462
+ dividerIndex + DIVIDER.length,
463
+ replaceEndIndex
464
+ );
465
+
466
+ if (searchBlock.trim() === "") {
467
+ newHtml = `${replaceBlock}\n${newHtml}`;
468
+ updatedLines.push([1, replaceBlock.split("\n").length]);
469
+ } else {
470
+ const blockPosition = newHtml.indexOf(searchBlock);
471
+ if (blockPosition !== -1) {
472
+ const beforeText = newHtml.substring(0, blockPosition);
473
+ const startLineNumber = beforeText.split("\n").length;
474
+ const replaceLines = replaceBlock.split("\n").length;
475
+ const endLineNumber = startLineNumber + replaceLines - 1;
476
+
477
+ updatedLines.push([startLineNumber, endLineNumber]);
478
+ newHtml = newHtml.replace(searchBlock, replaceBlock);
479
+ }
480
+ }
481
+
482
+ position = replaceEndIndex + REPLACE_END.length;
483
+ }
484
+
485
+ // Update the main HTML if it's the index page
486
+ const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
487
+ if (mainPageIndex !== -1) {
488
+ updatedPages[mainPageIndex].html = newHtml;
489
+ }
490
+ }
491
+
492
+ const files: File[] = [];
493
+ updatedPages.forEach((page: Page) => {
494
+ const file = new File([page.html], page.path, { type: "text/html" });
495
+ files.push(file);
496
+ });
497
+
498
+ uploadFiles({
499
+ repo: {
500
+ type: "space",
501
+ name: repoId,
502
+ },
503
+ files,
504
+ commitTitle: prompt,
505
+ accessToken: user.token as string,
506
+ });
507
+
508
+ return NextResponse.json({
509
+ ok: true,
510
+ updatedLines,
511
+ pages: updatedPages,
512
+ });
513
+ } else {
514
+ return NextResponse.json(
515
+ { ok: false, message: "No content returned from the model" },
516
+ { status: 400 }
517
+ );
518
+ }
519
+ } catch (error: any) {
520
+ if (error.message?.includes("exceeded your monthly included credits")) {
521
+ return NextResponse.json(
522
+ {
523
+ ok: false,
524
+ openProModal: true,
525
+ message: error.message,
526
+ },
527
+ { status: 402 }
528
+ );
529
+ }
530
+ return NextResponse.json(
531
+ {
532
+ ok: false,
533
+ openSelectProvider: true,
534
+ message:
535
+ error.message || "An error occurred while processing your request.",
536
+ },
537
+ { status: 500 }
538
+ );
539
+ }
540
+ }
541
+
app/api/auth/route.ts ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ export async function POST(req: NextRequest) {
4
+ const body = await req.json();
5
+ const { code } = body;
6
+
7
+ if (!code) {
8
+ return NextResponse.json(
9
+ { error: "Code is required" },
10
+ {
11
+ status: 400,
12
+ headers: {
13
+ "Content-Type": "application/json",
14
+ },
15
+ }
16
+ );
17
+ }
18
+
19
+ const Authorization = `Basic ${Buffer.from(
20
+ `${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
21
+ ).toString("base64")}`;
22
+
23
+ const host =
24
+ req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
25
+
26
+ const url = host.includes("/spaces/enzostvs")
27
+ ? "enzostvs-deepsite.hf.space"
28
+ : host;
29
+ const redirect_uri =
30
+ `${host.includes("localhost") ? "http://" : "https://"}` +
31
+ url +
32
+ "/auth/callback";
33
+ const request_auth = await fetch("https://huggingface.co/oauth/token", {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "application/x-www-form-urlencoded",
37
+ Authorization,
38
+ },
39
+ body: new URLSearchParams({
40
+ grant_type: "authorization_code",
41
+ code,
42
+ redirect_uri,
43
+ }),
44
+ });
45
+
46
+ const response = await request_auth.json();
47
+ if (!response.access_token) {
48
+ return NextResponse.json(
49
+ { error: "Failed to retrieve access token" },
50
+ {
51
+ status: 400,
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ },
55
+ }
56
+ );
57
+ }
58
+
59
+ const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
60
+ headers: {
61
+ Authorization: `Bearer ${response.access_token}`,
62
+ },
63
+ });
64
+
65
+ if (!userResponse.ok) {
66
+ return NextResponse.json(
67
+ { user: null, errCode: userResponse.status },
68
+ { status: userResponse.status }
69
+ );
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,
81
+ headers: {
82
+ "Content-Type": "application/json",
83
+ },
84
+ }
85
+ );
86
+ }
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, listFiles, 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 }: {
11
+ params: Promise<{
12
+ namespace: string;
13
+ repoId: string;
14
+ commitId: string;
15
+ }>
16
+ }
17
+ ) {
18
+ const user = await isAuthenticated();
19
+
20
+ if (user instanceof NextResponse || !user) {
21
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
22
+ }
23
+
24
+ await dbConnect();
25
+ const param = await params;
26
+ const { namespace, repoId, commitId } = param;
27
+
28
+ const project = await Project.findOne({
29
+ user_id: user.id,
30
+ space_id: `${namespace}/${repoId}`,
31
+ }).lean();
32
+
33
+ if (!project) {
34
+ return NextResponse.json(
35
+ { ok: false, error: "Project not found" },
36
+ { status: 404 }
37
+ );
38
+ }
39
+
40
+ try {
41
+ const repo: RepoDesignation = {
42
+ type: "space",
43
+ name: `${namespace}/${repoId}`,
44
+ };
45
+
46
+ // Fetch files from the specific commit
47
+ const files: File[] = [];
48
+ const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
49
+
50
+ // Get all files from the specific commit
51
+ for await (const fileInfo of listFiles({
52
+ repo,
53
+ accessToken: user.token as string,
54
+ revision: commitId,
55
+ })) {
56
+ const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
57
+
58
+ if (allowedExtensions.includes(fileExtension || "")) {
59
+ // Fetch the file content from the specific commit
60
+ const response = await fetch(
61
+ `https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
62
+ );
63
+
64
+ if (response.ok) {
65
+ const content = await response.text();
66
+ let mimeType = "text/plain";
67
+
68
+ switch (fileExtension) {
69
+ case "html":
70
+ mimeType = "text/html";
71
+ break;
72
+ case "css":
73
+ mimeType = "text/css";
74
+ break;
75
+ case "js":
76
+ mimeType = "application/javascript";
77
+ break;
78
+ case "json":
79
+ mimeType = "application/json";
80
+ break;
81
+ case "md":
82
+ mimeType = "text/markdown";
83
+ break;
84
+ }
85
+
86
+ const file = new File([content], fileInfo.path, { type: mimeType });
87
+ files.push(file);
88
+ }
89
+ }
90
+ }
91
+
92
+ if (files.length === 0) {
93
+ return NextResponse.json(
94
+ { ok: false, error: "No files found in the specified commit" },
95
+ { status: 404 }
96
+ );
97
+ }
98
+
99
+ // Upload the files to the main branch with a promotion commit message
100
+ await uploadFiles({
101
+ repo,
102
+ files,
103
+ accessToken: user.token as string,
104
+ commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
105
+ commitDescription: `Promoted commit ${commitId} to main branch`,
106
+ });
107
+
108
+ return NextResponse.json(
109
+ {
110
+ ok: true,
111
+ message: "Version promoted successfully",
112
+ promotedCommit: commitId,
113
+ filesPromoted: files.length
114
+ },
115
+ { status: 200 }
116
+ );
117
+
118
+ } catch (error: any) {
119
+ console.error("Error promoting version:", error);
120
+
121
+ // Handle specific HuggingFace API errors
122
+ if (error.statusCode === 404) {
123
+ return NextResponse.json(
124
+ { ok: false, error: "Commit not found" },
125
+ { status: 404 }
126
+ );
127
+ }
128
+
129
+ if (error.statusCode === 403) {
130
+ return NextResponse.json(
131
+ { ok: false, error: "Access denied to repository" },
132
+ { status: 403 }
133
+ );
134
+ }
135
+
136
+ return NextResponse.json(
137
+ { ok: false, error: error.message || "Failed to promote version" },
138
+ { status: 500 }
139
+ );
140
+ }
141
+ }
app/api/me/projects/[namespace]/[repoId]/images/route.ts ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { 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
+
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
+ 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
+
28
+ if (!project) {
29
+ return NextResponse.json(
30
+ {
31
+ ok: false,
32
+ error: "Project not found",
33
+ },
34
+ { status: 404 }
35
+ );
36
+ }
37
+
38
+ // Parse the FormData to get the images
39
+ const formData = await req.formData();
40
+ const imageFiles = formData.getAll("images") as File[];
41
+
42
+ if (!imageFiles || imageFiles.length === 0) {
43
+ return NextResponse.json(
44
+ {
45
+ ok: false,
46
+ error: "At least one image file is required under the 'images' key",
47
+ },
48
+ { status: 400 }
49
+ );
50
+ }
51
+
52
+ const files: File[] = [];
53
+ for (const file of imageFiles) {
54
+ if (!(file instanceof File)) {
55
+ return NextResponse.json(
56
+ {
57
+ ok: false,
58
+ error: "Invalid file format - all items under 'images' key must be files",
59
+ },
60
+ { status: 400 }
61
+ );
62
+ }
63
+
64
+ if (!file.type.startsWith('image/')) {
65
+ return NextResponse.json(
66
+ {
67
+ ok: false,
68
+ error: `File ${file.name} is not an image`,
69
+ },
70
+ { status: 400 }
71
+ );
72
+ }
73
+
74
+ // Create File object with images/ folder prefix
75
+ const fileName = `images/${file.name}`;
76
+ const processedFile = new File([file], fileName, { type: file.type });
77
+ files.push(processedFile);
78
+ }
79
+
80
+ // Upload files to HuggingFace space
81
+ const repo: RepoDesignation = {
82
+ type: "space",
83
+ name: `${namespace}/${repoId}`,
84
+ };
85
+
86
+ await uploadFiles({
87
+ repo,
88
+ files,
89
+ accessToken: user.token as string,
90
+ commitTitle: `Upload ${files.length} image(s)`,
91
+ });
92
+
93
+ return NextResponse.json({
94
+ ok: true,
95
+ message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
96
+ uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
97
+ }, { status: 200 });
98
+
99
+ } catch (error) {
100
+ console.error('Error uploading images:', error);
101
+ return NextResponse.json(
102
+ {
103
+ ok: false,
104
+ error: "Failed to upload images",
105
+ },
106
+ { status: 500 }
107
+ );
108
+ }
109
+ }
app/api/me/projects/[namespace]/[repoId]/route.ts ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, spaceInfo, uploadFiles, listFiles, deleteRepo, listCommits } from "@huggingface/hub";
3
+
4
+ import { isAuthenticated } from "@/lib/auth";
5
+ import Project from "@/models/Project";
6
+ import dbConnect from "@/lib/mongodb";
7
+ import { Commit, Page } from "@/types";
8
+
9
+ export async function DELETE(
10
+ req: NextRequest,
11
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
12
+ ) {
13
+ const user = await isAuthenticated();
14
+
15
+ if (user instanceof NextResponse || !user) {
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
+
28
+ if (!project) {
29
+ return NextResponse.json(
30
+ { ok: false, error: "Project not found" },
31
+ { status: 404 }
32
+ );
33
+ }
34
+
35
+ try {
36
+ const space = await spaceInfo({
37
+ name: `${namespace}/${repoId}`,
38
+ accessToken: user.token as string,
39
+ additionalFields: ["author"],
40
+ });
41
+
42
+ if (!space || space.sdk !== "static") {
43
+ return NextResponse.json(
44
+ { ok: false, error: "Space is not a static space." },
45
+ { status: 404 }
46
+ );
47
+ }
48
+
49
+ if (space.author !== user.name) {
50
+ return NextResponse.json(
51
+ { ok: false, error: "Space does not belong to the authenticated user." },
52
+ { status: 403 }
53
+ );
54
+ }
55
+
56
+ if (space.private) {
57
+ return NextResponse.json(
58
+ { ok: false, error: "Your space must be public to access it." },
59
+ { status: 403 }
60
+ );
61
+ }
62
+
63
+ const repo: RepoDesignation = {
64
+ type: "space",
65
+ name: `${namespace}/${repoId}`,
66
+ };
67
+
68
+ await deleteRepo({
69
+ repo,
70
+ accessToken: user.token as string,
71
+ });
72
+
73
+ await Project.deleteOne({
74
+ user_id: user.id,
75
+ space_id: `${namespace}/${repoId}`,
76
+ });
77
+
78
+ return NextResponse.json({ ok: true }, { status: 200 });
79
+ } catch (error: any) {
80
+ return NextResponse.json(
81
+ { ok: false, error: error.message },
82
+ { status: 500 }
83
+ );
84
+ }
85
+ }
86
+
87
+ export async function GET(
88
+ req: NextRequest,
89
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
90
+ ) {
91
+ const user = await isAuthenticated();
92
+
93
+ if (user instanceof NextResponse || !user) {
94
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
95
+ }
96
+
97
+ await dbConnect();
98
+ const param = await params;
99
+ const { namespace, repoId } = param;
100
+
101
+ const project = await Project.findOne({
102
+ user_id: user.id,
103
+ space_id: `${namespace}/${repoId}`,
104
+ }).lean();
105
+ if (!project) {
106
+ return NextResponse.json(
107
+ {
108
+ ok: false,
109
+ error: "Project not found",
110
+ },
111
+ { status: 404 }
112
+ );
113
+ }
114
+ try {
115
+ const space = await spaceInfo({
116
+ name: namespace + "/" + repoId,
117
+ accessToken: user.token as string,
118
+ additionalFields: ["author"],
119
+ });
120
+
121
+ if (!space || space.sdk !== "static") {
122
+ return NextResponse.json(
123
+ {
124
+ ok: false,
125
+ error: "Space is not a static space",
126
+ },
127
+ { status: 404 }
128
+ );
129
+ }
130
+ if (space.author !== user.name) {
131
+ return NextResponse.json(
132
+ {
133
+ ok: false,
134
+ error: "Space does not belong to the authenticated user",
135
+ },
136
+ { status: 403 }
137
+ );
138
+ }
139
+ if (space.private) {
140
+ return NextResponse.json(
141
+ {
142
+ ok: false,
143
+ error: "Space must be public to access it",
144
+ },
145
+ { status: 403 }
146
+ );
147
+ }
148
+
149
+ const repo: RepoDesignation = {
150
+ type: "space",
151
+ name: `${namespace}/${repoId}`,
152
+ };
153
+
154
+ const htmlFiles: Page[] = [];
155
+ const files: string[] = [];
156
+
157
+ const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
158
+
159
+ for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
160
+ if (fileInfo.path.endsWith(".html")) {
161
+ const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`);
162
+ if (res.ok) {
163
+ const html = await res.text();
164
+ if (fileInfo.path === "index.html") {
165
+ htmlFiles.unshift({
166
+ path: fileInfo.path,
167
+ html,
168
+ });
169
+ } else {
170
+ htmlFiles.push({
171
+ path: fileInfo.path,
172
+ html,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ if (fileInfo.type === "directory" && fileInfo.path === "images") {
178
+ for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
179
+ if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
180
+ files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ const commits: Commit[] = [];
186
+ for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
187
+ if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
188
+ continue;
189
+ }
190
+ commits.push({
191
+ title: commit.title,
192
+ oid: commit.oid,
193
+ date: commit.date,
194
+ });
195
+ }
196
+
197
+ if (htmlFiles.length === 0) {
198
+ return NextResponse.json(
199
+ {
200
+ ok: false,
201
+ error: "No HTML files found",
202
+ },
203
+ { status: 404 }
204
+ );
205
+ }
206
+
207
+ return NextResponse.json(
208
+ {
209
+ project,
210
+ pages: htmlFiles,
211
+ files,
212
+ commits,
213
+ ok: true,
214
+ },
215
+ { status: 200 }
216
+ );
217
+
218
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
+ } catch (error: any) {
220
+ if (error.statusCode === 404) {
221
+ await Project.deleteOne({
222
+ user_id: user.id,
223
+ space_id: `${namespace}/${repoId}`,
224
+ });
225
+ return NextResponse.json(
226
+ { error: "Space not found", ok: false },
227
+ { status: 404 }
228
+ );
229
+ }
230
+ return NextResponse.json(
231
+ { error: error.message, ok: false },
232
+ { status: 500 }
233
+ );
234
+ }
235
+ }
app/api/me/projects/route.ts ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, createRepo, listCommits, 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 { Commit, Page } from "@/types";
8
+ import { COLORS } from "@/lib/utils";
9
+
10
+ export async function POST(
11
+ req: NextRequest,
12
+ ) {
13
+ const user = await isAuthenticated();
14
+ if (user instanceof NextResponse || !user) {
15
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
16
+ }
17
+
18
+ await dbConnect();
19
+ const { title: titleFromRequest, pages, prompt } = await req.json();
20
+
21
+ const title = titleFromRequest ?? "DeepSite Project";
22
+
23
+ const formattedTitle = title
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, "-")
26
+ .split("-")
27
+ .filter(Boolean)
28
+ .join("-")
29
+ .slice(0, 96);
30
+
31
+ const repo: RepoDesignation = {
32
+ type: "space",
33
+ name: `${user.name}/${formattedTitle}`,
34
+ };
35
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
36
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
37
+ const README = `---
38
+ title: ${title}
39
+ colorFrom: ${colorFrom}
40
+ colorTo: ${colorTo}
41
+ emoji: 🐳
42
+ sdk: static
43
+ pinned: false
44
+ tags:
45
+ - deepsite-v3
46
+ ---
47
+
48
+ # Welcome to your new DeepSite project!
49
+ This project was created with [DeepSite](https://deepsite.hf.co).
50
+ `;
51
+
52
+ const files: File[] = [];
53
+ const readmeFile = new File([README], "README.md", { type: "text/markdown" });
54
+ files.push(readmeFile);
55
+ pages.forEach((page: Page) => {
56
+ const file = new File([page.html], page.path, { type: "text/html" });
57
+ files.push(file);
58
+ });
59
+
60
+ try {
61
+ const { repoUrl } = await createRepo({
62
+ repo,
63
+ accessToken: user.token as string,
64
+ });
65
+ await uploadFiles({
66
+ repo,
67
+ files,
68
+ accessToken: user.token as string,
69
+ commitTitle: prompt ?? "Redesign my website"
70
+ });
71
+
72
+ const path = repoUrl.split("/").slice(-2).join("/");
73
+ const project = await Project.create({
74
+ user_id: user.id,
75
+ space_id: path,
76
+ });
77
+
78
+ const commits: Commit[] = [];
79
+ for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
80
+ if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
81
+ continue;
82
+ }
83
+ commits.push({
84
+ title: commit.title,
85
+ oid: commit.oid,
86
+ date: commit.date,
87
+ });
88
+ }
89
+
90
+ let newProject = {
91
+ files,
92
+ pages,
93
+ commits,
94
+ project,
95
+ }
96
+
97
+ return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
98
+ } catch (err: any) {
99
+ return NextResponse.json(
100
+ { error: err.message, ok: false },
101
+ { status: 500 }
102
+ );
103
+ }
104
+ }
app/api/me/route.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { headers } from "next/headers";
2
+ import { NextResponse } from "next/server";
3
+
4
+ export async function GET() {
5
+ const authHeaders = await headers();
6
+ const token = authHeaders.get("Authorization");
7
+ if (!token) {
8
+ return NextResponse.json({ user: null, errCode: 401 }, { status: 401 });
9
+ }
10
+
11
+ const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
12
+ headers: {
13
+ Authorization: `${token}`,
14
+ },
15
+ });
16
+
17
+ if (!userResponse.ok) {
18
+ return NextResponse.json(
19
+ { user: null, errCode: userResponse.status },
20
+ { status: userResponse.status }
21
+ );
22
+ }
23
+ const user = await userResponse.json();
24
+ return NextResponse.json({ user, errCode: null }, { status: 200 });
25
+ }
app/api/proxy/route.ts ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { isAuthenticated } from "@/lib/auth";
3
+
4
+ export async function GET(req: NextRequest) {
5
+ const user: any = await isAuthenticated();
6
+
7
+ if (user instanceof NextResponse || !user) {
8
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
9
+ }
10
+
11
+ const { searchParams } = new URL(req.url);
12
+ const spaceId = searchParams.get('spaceId');
13
+ const commitId = searchParams.get('commitId');
14
+ const path = searchParams.get('path') || '/';
15
+
16
+ if (!spaceId) {
17
+ return NextResponse.json({ error: "spaceId parameter required" }, { status: 400 });
18
+ }
19
+
20
+ try {
21
+ const spaceDomain = `${spaceId.replace("/", "-")}${commitId !== null? `--rev-${commitId.slice(0, 7)}` : ""}.static.hf.space`;
22
+ const targetUrl = `https://${spaceDomain}${path}`;
23
+
24
+ const response = await fetch(targetUrl, {
25
+ headers: {
26
+ 'User-Agent': req.headers.get('user-agent') || '',
27
+ },
28
+ });
29
+
30
+ if (!response.ok) {
31
+ console.error('Failed to fetch from HF space:', response.status, response.statusText);
32
+ return NextResponse.json({
33
+ error: "Failed to fetch content",
34
+ details: `${response.status} ${response.statusText}`,
35
+ targetUrl
36
+ }, { status: response.status });
37
+ }
38
+
39
+ let content = await response.text();
40
+ const contentType = response.headers.get('content-type') || 'text/html';
41
+
42
+ // Rewrite relative URLs to go through the proxy
43
+ if (contentType.includes('text/html')) {
44
+ const baseUrl = `https://${spaceDomain}`;
45
+
46
+ // Fix relative URLs in href attributes
47
+ content = content.replace(/href="([^"]+)"/g, (match, url) => {
48
+ if (url.startsWith('/') && !url.startsWith('//')) {
49
+ // Relative URL starting with /
50
+ return `href="${baseUrl}${url}"`;
51
+ } else if (!url.includes('://') && !url.startsWith('#') && !url.startsWith('mailto:') && !url.startsWith('tel:')) {
52
+ // Relative URL not starting with /
53
+ return `href="${baseUrl}/${url}"`;
54
+ }
55
+ return match;
56
+ });
57
+
58
+ // Fix relative URLs in src attributes
59
+ content = content.replace(/src="([^"]+)"/g, (match, url) => {
60
+ if (url.startsWith('/') && !url.startsWith('//')) {
61
+ return `src="${baseUrl}${url}"`;
62
+ } else if (!url.includes('://')) {
63
+ return `src="${baseUrl}/${url}"`;
64
+ }
65
+ return match;
66
+ });
67
+
68
+ // Add base tag to ensure relative URLs work correctly
69
+ const baseTag = `<base href="${baseUrl}/">`;
70
+ if (content.includes('<head>')) {
71
+ content = content.replace('<head>', `<head>${baseTag}`);
72
+ } else if (content.includes('<html>')) {
73
+ content = content.replace('<html>', `<html><head>${baseTag}</head>`);
74
+ } else {
75
+ content = `<head>${baseTag}</head>` + content;
76
+ }
77
+ }
78
+
79
+ const injectedScript = `
80
+ <script>
81
+ // Add event listeners and communicate with parent
82
+ document.addEventListener('DOMContentLoaded', function() {
83
+ console.log('DOM loaded, setting up communication');
84
+ let hoveredElement = null;
85
+ let isEditModeEnabled = false;
86
+
87
+ document.addEventListener('mouseover', function(event) {
88
+ if (event.target !== document.body && event.target !== document.documentElement) {
89
+ hoveredElement = event.target;
90
+ console.log('Element hovered:', event.target.tagName, 'Edit mode:', isEditModeEnabled);
91
+
92
+ const rect = event.target.getBoundingClientRect();
93
+ const message = {
94
+ type: 'ELEMENT_HOVERED',
95
+ data: {
96
+ tagName: event.target.tagName,
97
+ rect: {
98
+ top: rect.top,
99
+ left: rect.left,
100
+ width: rect.width,
101
+ height: rect.height
102
+ },
103
+ element: event.target.outerHTML.substring(0, 200)
104
+ }
105
+ };
106
+ console.log('Sending message to parent:', message);
107
+ parent.postMessage(message, '*');
108
+ }
109
+ });
110
+
111
+ document.addEventListener('mouseout', function(event) {
112
+ hoveredElement = null;
113
+
114
+ parent.postMessage({
115
+ type: 'ELEMENT_MOUSE_OUT'
116
+ }, '*');
117
+ });
118
+
119
+ // Handle clicks - prevent default only in edit mode
120
+ document.addEventListener('click', function(event) {
121
+ // Only prevent default if edit mode is enabled
122
+ if (isEditModeEnabled) {
123
+ event.preventDefault();
124
+ event.stopPropagation();
125
+
126
+ const rect = event.target.getBoundingClientRect();
127
+ parent.postMessage({
128
+ type: 'ELEMENT_CLICKED',
129
+ data: {
130
+ tagName: event.target.tagName,
131
+ rect: {
132
+ top: rect.top,
133
+ left: rect.left,
134
+ width: rect.width,
135
+ height: rect.height
136
+ },
137
+ element: event.target.outerHTML.substring(0, 200)
138
+ }
139
+ }, '*');
140
+ } else {
141
+ // In non-edit mode, handle link clicks to maintain proxy context
142
+ const link = event.target.closest('a');
143
+ if (link && link.href) {
144
+ event.preventDefault();
145
+
146
+ const url = new URL(link.href);
147
+
148
+ // If it's an external link (different domain than the space), open in new tab
149
+ if (url.hostname !== '${spaceDomain}') {
150
+ window.open(link.href, '_blank');
151
+ } else {
152
+ // For internal links within the space, navigate through the proxy
153
+ // Extract the path and query parameters from the original link
154
+ const targetPath = url.pathname + url.search + url.hash;
155
+
156
+ // Get current proxy URL parameters
157
+ const currentUrl = new URL(window.location.href);
158
+ const spaceId = currentUrl.searchParams.get('spaceId') || '';
159
+ const commitId = currentUrl.searchParams.get('commitId') || '';
160
+
161
+ // Construct new proxy URL with the target path
162
+ const proxyUrl = '/api/proxy/?' +
163
+ 'spaceId=' + encodeURIComponent(spaceId) +
164
+ (commitId ? '&commitId=' + encodeURIComponent(commitId) : '') +
165
+ '&path=' + encodeURIComponent(targetPath);
166
+
167
+ // Navigate to the new URL through the parent window
168
+ parent.postMessage({
169
+ type: 'NAVIGATE_TO_PROXY',
170
+ data: {
171
+ proxyUrl: proxyUrl,
172
+ targetPath: targetPath
173
+ }
174
+ }, '*');
175
+ }
176
+ }
177
+ }
178
+ });
179
+
180
+ // Prevent form submissions when in edit mode
181
+ document.addEventListener('submit', function(event) {
182
+ if (isEditModeEnabled) {
183
+ event.preventDefault();
184
+ event.stopPropagation();
185
+ }
186
+ });
187
+
188
+ // Prevent other navigation events when in edit mode
189
+ document.addEventListener('keydown', function(event) {
190
+ if (isEditModeEnabled && event.key === 'Enter' && (event.target.tagName === 'A' || event.target.tagName === 'BUTTON')) {
191
+ event.preventDefault();
192
+ event.stopPropagation();
193
+ }
194
+ });
195
+
196
+ // Listen for messages from parent
197
+ window.addEventListener('message', function(event) {
198
+ console.log('Iframe received message from parent:', event.data);
199
+ if (event.data.type === 'ENABLE_EDIT_MODE') {
200
+ console.log('Enabling edit mode');
201
+ isEditModeEnabled = true;
202
+ document.body.style.userSelect = 'none';
203
+ document.body.style.pointerEvents = 'auto';
204
+ } else if (event.data.type === 'DISABLE_EDIT_MODE') {
205
+ console.log('Disabling edit mode');
206
+ isEditModeEnabled = false;
207
+ document.body.style.userSelect = '';
208
+ document.body.style.pointerEvents = '';
209
+ }
210
+ });
211
+
212
+ // Notify parent that script is ready
213
+ parent.postMessage({
214
+ type: 'PROXY_SCRIPT_READY'
215
+ }, '*');
216
+ });
217
+ </script>
218
+ `;
219
+
220
+ let modifiedContent;
221
+ if (content.includes('</body>')) {
222
+ modifiedContent = content.replace(
223
+ /<\/body>/i,
224
+ `${injectedScript}</body>`
225
+ );
226
+ } else {
227
+ modifiedContent = content + injectedScript;
228
+ }
229
+
230
+ return new NextResponse(modifiedContent, {
231
+ headers: {
232
+ 'Content-Type': contentType,
233
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
234
+ 'X-Frame-Options': 'SAMEORIGIN',
235
+ },
236
+ });
237
+
238
+ } catch (error) {
239
+ console.error('Proxy error:', error);
240
+ return NextResponse.json({
241
+ error: "Proxy request failed",
242
+ details: error instanceof Error ? error.message : String(error),
243
+ spaceId
244
+ }, { status: 500 });
245
+ }
246
+ }
app/api/re-design/route.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ export async function PUT(request: NextRequest) {
4
+ const body = await request.json();
5
+ const { url } = body;
6
+
7
+ if (!url) {
8
+ return NextResponse.json({ error: "URL is required" }, { status: 400 });
9
+ }
10
+
11
+ try {
12
+ const response = await fetch(
13
+ `https://r.jina.ai/${encodeURIComponent(url)}`,
14
+ {
15
+ method: "POST",
16
+ }
17
+ );
18
+ if (!response.ok) {
19
+ return NextResponse.json(
20
+ { error: "Failed to fetch redesign" },
21
+ { status: 500 }
22
+ );
23
+ }
24
+ const markdown = await response.text();
25
+ return NextResponse.json(
26
+ {
27
+ ok: true,
28
+ markdown,
29
+ },
30
+ { status: 200 }
31
+ );
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ } catch (error: any) {
34
+ return NextResponse.json(
35
+ { error: error.message || "An error occurred" },
36
+ { status: 500 }
37
+ );
38
+ }
39
+ }
app/auth/callback/page.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import Link from "next/link";
3
+ import { useUser } from "@/hooks/useUser";
4
+ 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
+ export default function AuthCallback({
10
+ searchParams,
11
+ }: {
12
+ searchParams: Promise<{ code: string }>;
13
+ }) {
14
+ const [showButton, setShowButton] = useState(false);
15
+ const { code } = use(searchParams);
16
+ const { loginFromCode } = useUser();
17
+
18
+ useMount(async () => {
19
+ if (code) {
20
+ await loginFromCode(code);
21
+ }
22
+ });
23
+
24
+ useTimeoutFn(() => setShowButton(true), 7000);
25
+
26
+ return (
27
+ <div className="h-screen flex flex-col justify-center items-center bg-neutral-950 z-1 relative">
28
+ <div className="background__noisy" />
29
+ <div className="relative max-w-4xl py-10 flex items-center justify-center w-full">
30
+ <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">
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
+ <AnimatedBlobs />
71
+ </div>
72
+ </div>
73
+ );
74
+ }
app/auth/page.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { redirect } from "next/navigation";
2
+ import { Metadata } from "next";
3
+
4
+ import { getAuth } from "@/app/actions/auth";
5
+
6
+ export const revalidate = 1;
7
+
8
+ export const metadata: Metadata = {
9
+ robots: "noindex, nofollow",
10
+ };
11
+
12
+ export default async function Auth() {
13
+ const loginRedirectUrl = await getAuth();
14
+ if (loginRedirectUrl) {
15
+ redirect(loginRedirectUrl);
16
+ }
17
+
18
+ return (
19
+ <div className="p-4">
20
+ <div className="border bg-red-500/10 border-red-500/20 text-red-500 px-5 py-3 rounded-lg">
21
+ <h1 className="text-xl font-bold">Error</h1>
22
+ <p className="text-sm">
23
+ An error occurred while trying to log in. Please try again later.
24
+ </p>
25
+ </div>
26
+ </div>
27
+ );
28
+ }
app/globals.css DELETED
@@ -1,26 +0,0 @@
1
- @import "tailwindcss";
2
-
3
- :root {
4
- --background: #ffffff;
5
- --foreground: #171717;
6
- }
7
-
8
- @theme inline {
9
- --color-background: var(--background);
10
- --color-foreground: var(--foreground);
11
- --font-sans: var(--font-geist-sans);
12
- --font-mono: var(--font-geist-mono);
13
- }
14
-
15
- @media (prefers-color-scheme: dark) {
16
- :root {
17
- --background: #0a0a0a;
18
- --foreground: #ededed;
19
- }
20
- }
21
-
22
- body {
23
- background: var(--background);
24
- color: var(--foreground);
25
- font-family: Arial, Helvetica, sans-serif;
26
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/layout.tsx CHANGED
@@ -1,33 +1,115 @@
1
- import type { Metadata } from "next";
2
- import { Geist, Geist_Mono } from "next/font/google";
3
- import "./globals.css";
 
 
4
 
5
- const geistSans = Geist({
6
- variable: "--font-geist-sans",
 
 
 
 
 
 
 
 
 
 
7
  subsets: ["latin"],
8
  });
9
 
10
- const geistMono = Geist_Mono({
11
- variable: "--font-geist-mono",
12
  subsets: ["latin"],
 
13
  });
14
 
15
  export const metadata: Metadata = {
16
- title: "Create Next App",
17
- description: "Generated by create next app",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  };
19
 
20
- export default function RootLayout({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  children,
22
  }: Readonly<{
23
  children: React.ReactNode;
24
  }>) {
 
25
  return (
26
  <html lang="en">
 
 
 
 
 
27
  <body
28
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29
  >
30
- {children}
 
 
 
 
 
 
 
 
31
  </body>
32
  </html>
33
  );
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
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",
19
  subsets: ["latin"],
20
  });
21
 
22
+ const ptSans = PT_Sans({
23
+ variable: "--font-ptSans-mono",
24
  subsets: ["latin"],
25
+ weight: ["400", "700"],
26
  });
27
 
28
  export const metadata: Metadata = {
29
+ title: "DeepSite | Build with AI ✨",
30
+ description:
31
+ "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
32
+ openGraph: {
33
+ title: "DeepSite | Build with AI ✨",
34
+ description:
35
+ "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
36
+ url: "https://deepsite.hf.co",
37
+ siteName: "DeepSite",
38
+ images: [
39
+ {
40
+ url: "https://deepsite.hf.co/banner.png",
41
+ width: 1200,
42
+ height: 630,
43
+ alt: "DeepSite Open Graph Image",
44
+ },
45
+ ],
46
+ },
47
+ twitter: {
48
+ card: "summary_large_image",
49
+ title: "DeepSite | Build with AI ✨",
50
+ description:
51
+ "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
52
+ images: ["https://deepsite.hf.co/banner.png"],
53
+ },
54
+ appleWebApp: {
55
+ capable: true,
56
+ title: "DeepSite",
57
+ statusBarStyle: "black-translucent",
58
+ },
59
+ icons: {
60
+ icon: "/logo.svg",
61
+ shortcut: "/logo.svg",
62
+ apple: "/logo.svg",
63
+ },
64
+ };
65
+
66
+ export const viewport: Viewport = {
67
+ initialScale: 1,
68
+ maximumScale: 1,
69
+ themeColor: "#000000",
70
  };
71
 
72
+ async function getMe() {
73
+ const cookieStore = await cookies();
74
+ const token = cookieStore.get(MY_TOKEN_KEY())?.value;
75
+ if (!token) return { user: null, 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, errCode: null };
83
+ } catch (err: any) {
84
+ return { user: null, errCode: err.status };
85
+ }
86
+ }
87
+
88
+ export default async function RootLayout({
89
  children,
90
  }: Readonly<{
91
  children: React.ReactNode;
92
  }>) {
93
+ const data = await getMe();
94
  return (
95
  <html lang="en">
96
+ <Script
97
+ defer
98
+ data-domain="deepsite.hf.co"
99
+ src="https://plausible.io/js/script.js"
100
+ ></Script>
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
  );
app/page.tsx DELETED
@@ -1,103 +0,0 @@
1
- import Image from "next/image";
2
-
3
- export default function Home() {
4
- return (
5
- <div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
6
- <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
7
- <Image
8
- className="dark:invert"
9
- src="/next.svg"
10
- alt="Next.js logo"
11
- width={180}
12
- height={38}
13
- priority
14
- />
15
- <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
16
- <li className="mb-2 tracking-[-.01em]">
17
- Get started by editing{" "}
18
- <code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
19
- app/page.tsx
20
- </code>
21
- .
22
- </li>
23
- <li className="tracking-[-.01em]">
24
- Save and see your changes instantly.
25
- </li>
26
- </ol>
27
-
28
- <div className="flex gap-4 items-center flex-col sm:flex-row">
29
- <a
30
- className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
31
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
32
- target="_blank"
33
- rel="noopener noreferrer"
34
- >
35
- <Image
36
- className="dark:invert"
37
- src="/vercel.svg"
38
- alt="Vercel logomark"
39
- width={20}
40
- height={20}
41
- />
42
- Deploy now
43
- </a>
44
- <a
45
- className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
46
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
47
- target="_blank"
48
- rel="noopener noreferrer"
49
- >
50
- Read our docs
51
- </a>
52
- </div>
53
- </main>
54
- <footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
55
- <a
56
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
57
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
58
- target="_blank"
59
- rel="noopener noreferrer"
60
- >
61
- <Image
62
- aria-hidden
63
- src="/file.svg"
64
- alt="File icon"
65
- width={16}
66
- height={16}
67
- />
68
- Learn
69
- </a>
70
- <a
71
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
72
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
73
- target="_blank"
74
- rel="noopener noreferrer"
75
- >
76
- <Image
77
- aria-hidden
78
- src="/window.svg"
79
- alt="Window icon"
80
- width={16}
81
- height={16}
82
- />
83
- Examples
84
- </a>
85
- <a
86
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
87
- href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
88
- target="_blank"
89
- rel="noopener noreferrer"
90
- >
91
- <Image
92
- aria-hidden
93
- src="/globe.svg"
94
- alt="Globe icon"
95
- width={16}
96
- height={16}
97
- />
98
- Go to nextjs.org →
99
- </a>
100
- </footer>
101
- </div>
102
- );
103
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/projects/[namespace]/[repoId]/page.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
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
+ }
app/projects/new/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { AppEditor } from "@/components/editor";
2
+
3
+ export default function NewProjectPage() {
4
+ return <AppEditor isNew />;
5
+ }
assets/globals.css ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ @theme inline {
7
+ --color-background: var(--background);
8
+ --color-foreground: var(--foreground);
9
+ --font-sans: var(--font-inter-sans);
10
+ --font-mono: var(--font-ptSans-mono);
11
+ --color-sidebar-ring: var(--sidebar-ring);
12
+ --color-sidebar-border: var(--sidebar-border);
13
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
+ --color-sidebar-accent: var(--sidebar-accent);
15
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
+ --color-sidebar-primary: var(--sidebar-primary);
17
+ --color-sidebar-foreground: var(--sidebar-foreground);
18
+ --color-sidebar: var(--sidebar);
19
+ --color-chart-5: var(--chart-5);
20
+ --color-chart-4: var(--chart-4);
21
+ --color-chart-3: var(--chart-3);
22
+ --color-chart-2: var(--chart-2);
23
+ --color-chart-1: var(--chart-1);
24
+ --color-ring: var(--ring);
25
+ --color-input: var(--input);
26
+ --color-border: var(--border);
27
+ --color-destructive: var(--destructive);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-muted-foreground: var(--muted-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-secondary-foreground: var(--secondary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-popover: var(--popover);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-card: var(--card);
40
+ --radius-sm: calc(var(--radius) - 4px);
41
+ --radius-md: calc(var(--radius) - 2px);
42
+ --radius-lg: var(--radius);
43
+ --radius-xl: calc(var(--radius) + 4px);
44
+ }
45
+
46
+ :root {
47
+ --radius: 0.625rem;
48
+ --background: oklch(1 0 0);
49
+ --foreground: oklch(0.145 0 0);
50
+ --card: oklch(1 0 0);
51
+ --card-foreground: oklch(0.145 0 0);
52
+ --popover: oklch(1 0 0);
53
+ --popover-foreground: oklch(0.145 0 0);
54
+ --primary: oklch(0.205 0 0);
55
+ --primary-foreground: oklch(0.985 0 0);
56
+ --secondary: oklch(0.97 0 0);
57
+ --secondary-foreground: oklch(0.205 0 0);
58
+ --muted: oklch(0.97 0 0);
59
+ --muted-foreground: oklch(0.556 0 0);
60
+ --accent: oklch(0.97 0 0);
61
+ --accent-foreground: oklch(0.205 0 0);
62
+ --destructive: oklch(0.577 0.245 27.325);
63
+ --border: oklch(0.922 0 0);
64
+ --input: oklch(0.922 0 0);
65
+ --ring: oklch(0.708 0 0);
66
+ --chart-1: oklch(0.646 0.222 41.116);
67
+ --chart-2: oklch(0.6 0.118 184.704);
68
+ --chart-3: oklch(0.398 0.07 227.392);
69
+ --chart-4: oklch(0.828 0.189 84.429);
70
+ --chart-5: oklch(0.769 0.188 70.08);
71
+ --sidebar: oklch(0.985 0 0);
72
+ --sidebar-foreground: oklch(0.145 0 0);
73
+ --sidebar-primary: oklch(0.205 0 0);
74
+ --sidebar-primary-foreground: oklch(0.985 0 0);
75
+ --sidebar-accent: oklch(0.97 0 0);
76
+ --sidebar-accent-foreground: oklch(0.205 0 0);
77
+ --sidebar-border: oklch(0.922 0 0);
78
+ --sidebar-ring: oklch(0.708 0 0);
79
+ }
80
+
81
+ .dark {
82
+ --background: oklch(0.145 0 0);
83
+ --foreground: oklch(0.985 0 0);
84
+ --card: oklch(0.205 0 0);
85
+ --card-foreground: oklch(0.985 0 0);
86
+ --popover: oklch(0.205 0 0);
87
+ --popover-foreground: oklch(0.985 0 0);
88
+ --primary: oklch(0.922 0 0);
89
+ --primary-foreground: oklch(0.205 0 0);
90
+ --secondary: oklch(0.269 0 0);
91
+ --secondary-foreground: oklch(0.985 0 0);
92
+ --muted: oklch(0.269 0 0);
93
+ --muted-foreground: oklch(0.708 0 0);
94
+ --accent: oklch(0.269 0 0);
95
+ --accent-foreground: oklch(0.985 0 0);
96
+ --destructive: oklch(0.704 0.191 22.216);
97
+ --border: oklch(1 0 0 / 10%);
98
+ --input: oklch(1 0 0 / 15%);
99
+ --ring: oklch(0.556 0 0);
100
+ --chart-1: oklch(0.488 0.243 264.376);
101
+ --chart-2: oklch(0.696 0.17 162.48);
102
+ --chart-3: oklch(0.769 0.188 70.08);
103
+ --chart-4: oklch(0.627 0.265 303.9);
104
+ --chart-5: oklch(0.645 0.246 16.439);
105
+ --sidebar: oklch(0.205 0 0);
106
+ --sidebar-foreground: oklch(0.985 0 0);
107
+ --sidebar-primary: oklch(0.488 0.243 264.376);
108
+ --sidebar-primary-foreground: oklch(0.985 0 0);
109
+ --sidebar-accent: oklch(0.269 0 0);
110
+ --sidebar-accent-foreground: oklch(0.985 0 0);
111
+ --sidebar-border: oklch(1 0 0 / 10%);
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;
122
+ }
123
+ body {
124
+ @apply bg-background text-foreground;
125
+ }
126
+ html {
127
+ @apply scroll-smooth;
128
+ }
129
+ }
130
+
131
+ .background__noisy {
132
+ @apply bg-blend-normal pointer-events-none opacity-90;
133
+ background-size: 25ww auto;
134
+ background-image: url("/background_noisy.webp");
135
+ @apply fixed w-screen h-screen -z-1 top-0 left-0;
136
+ }
137
+
138
+ .monaco-editor .margin {
139
+ @apply !bg-neutral-900;
140
+ }
141
+ .monaco-editor .monaco-editor-background {
142
+ @apply !bg-neutral-900;
143
+ }
144
+ .monaco-editor .line-numbers {
145
+ @apply !text-neutral-500;
146
+ }
147
+
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
+ }
assets/logo.svg ADDED
assets/space.svg ADDED
components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "assets/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
components/animated-blobs/index.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { User } from "@/types";
9
+ import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
10
+
11
+ export default function AppContext({
12
+ children,
13
+ me: initialData,
14
+ }: {
15
+ children: React.ReactNode;
16
+ me?: {
17
+ user: User | null;
18
+ errCode: number | null;
19
+ };
20
+ }) {
21
+ const { loginFromCode, user, logout, loading, errCode } =
22
+ useUser(initialData);
23
+ const pathname = usePathname();
24
+ const router = useRouter();
25
+
26
+ useMount(() => {
27
+ if (!initialData?.user && !user) {
28
+ if ([401, 403].includes(errCode as number)) {
29
+ logout();
30
+ } else if (pathname.includes("/spaces")) {
31
+ if (errCode) {
32
+ toast.error("An error occured while trying to log in");
33
+ }
34
+ // If we did not manage to log in (probs because api is down), we simply redirect to the home page
35
+ router.push("/");
36
+ }
37
+ }
38
+ });
39
+
40
+ const events: any = {};
41
+
42
+ useBroadcastChannel("auth", (message) => {
43
+ if (pathname.includes("/auth/callback")) return;
44
+
45
+ if (!message.code) return;
46
+ if (message.type === "user-oauth" && message?.code && !events.code) {
47
+ loginFromCode(message.code);
48
+ }
49
+ });
50
+
51
+ return children;
52
+ }
components/contexts/login-context.tsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { createContext, useContext, useState, ReactNode } from "react";
4
+ import { LoginModal } from "@/components/login-modal";
5
+ import { Page } from "@/types";
6
+
7
+ interface LoginContextType {
8
+ isOpen: boolean;
9
+ openLoginModal: (options?: LoginModalOptions) => void;
10
+ closeLoginModal: () => void;
11
+ }
12
+
13
+ interface LoginModalOptions {
14
+ pages?: Page[];
15
+ title?: string;
16
+ description?: string;
17
+ }
18
+
19
+ const LoginContext = createContext<LoginContextType | undefined>(undefined);
20
+
21
+ export function LoginProvider({ children }: { children: ReactNode }) {
22
+ const [isOpen, setIsOpen] = useState(false);
23
+ const [modalOptions, setModalOptions] = useState<LoginModalOptions>({});
24
+
25
+ const openLoginModal = (options: LoginModalOptions = {}) => {
26
+ setModalOptions(options);
27
+ setIsOpen(true);
28
+ };
29
+
30
+ const closeLoginModal = () => {
31
+ setIsOpen(false);
32
+ setModalOptions({});
33
+ };
34
+
35
+ const value = {
36
+ isOpen,
37
+ openLoginModal,
38
+ closeLoginModal,
39
+ };
40
+
41
+ return (
42
+ <LoginContext.Provider value={value}>
43
+ {children}
44
+ <LoginModal
45
+ open={isOpen}
46
+ onClose={setIsOpen}
47
+ pages={modalOptions.pages}
48
+ title={modalOptions.title}
49
+ description={modalOptions.description}
50
+ />
51
+ </LoginContext.Provider>
52
+ );
53
+ }
54
+
55
+ export function useLoginModal() {
56
+ const context = useContext(LoginContext);
57
+ if (context === undefined) {
58
+ throw new Error("useLoginModal must be used within a LoginProvider");
59
+ }
60
+ return context;
61
+ }
components/contexts/pro-context.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/contexts/tanstack-query-context.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5
+ import { useState } from "react";
6
+
7
+ export default function TanstackContext({
8
+ children,
9
+ }: {
10
+ children: React.ReactNode;
11
+ }) {
12
+ // Create QueryClient instance only once using useState with a function
13
+ const [queryClient] = useState(
14
+ () =>
15
+ new QueryClient({
16
+ defaultOptions: {
17
+ queries: {
18
+ staleTime: 60 * 1000, // 1 minute
19
+ refetchOnWindowFocus: false,
20
+ },
21
+ },
22
+ })
23
+ );
24
+
25
+ return (
26
+ <QueryClientProvider client={queryClient}>
27
+ {children}
28
+ <ReactQueryDevtools initialIsOpen={false} />
29
+ </QueryClientProvider>
30
+ );
31
+ }
components/contexts/user-context.tsx ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { createContext } from "react";
4
+ import { User } from "@/types";
5
+
6
+ export const UserContext = createContext({
7
+ user: undefined as User | undefined,
8
+ });
components/editor/ask-ai/index.tsx ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo, useState } from "react";
2
+ import classNames from "classnames";
3
+ import {
4
+ ArrowUp,
5
+ CircleStop,
6
+ Pause,
7
+ Plus,
8
+ Square,
9
+ StopCircle,
10
+ } from "lucide-react";
11
+ import { useLocalStorage } from "react-use";
12
+ import { toast } from "sonner";
13
+
14
+ import { useAi } from "@/hooks/useAi";
15
+ import { useEditor } from "@/hooks/useEditor";
16
+ import { isTheSameHtml } from "@/lib/compare-html-diff";
17
+ import { EnhancedSettings, Project } from "@/types";
18
+ import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
19
+ import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
20
+ import { AiLoading } from "@/components/editor/ask-ai/loading";
21
+ import { Button } from "@/components/ui/button";
22
+ import { Uploader } from "@/components/editor/ask-ai/uploader";
23
+ import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
24
+ import { Selector } from "@/components/editor/ask-ai/selector";
25
+ import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
26
+ import { useUser } from "@/hooks/useUser";
27
+ import { useLoginModal } from "@/components/contexts/login-context";
28
+ import { Settings } from "./settings";
29
+ import { useProModal } from "@/components/contexts/pro-context";
30
+ import { MODELS } from "@/lib/providers";
31
+
32
+ export const AskAi = ({
33
+ project,
34
+ isNew,
35
+ onScrollToBottom,
36
+ }: {
37
+ project?: Project;
38
+ files?: string[];
39
+ isNew?: boolean;
40
+ onScrollToBottom?: () => void;
41
+ }) => {
42
+ const { user } = useUser();
43
+ const { currentPageData, isUploading, pages, isLoadingProject } = useEditor();
44
+ const {
45
+ isAiWorking,
46
+ isThinking,
47
+ selectedFiles,
48
+ setSelectedFiles,
49
+ selectedElement,
50
+ setSelectedElement,
51
+ setIsThinking,
52
+ callAiNewProject,
53
+ callAiFollowUp,
54
+ setModel,
55
+ selectedModel,
56
+ audio: hookAudio,
57
+ cancelRequest,
58
+ } = useAi(onScrollToBottom);
59
+ const { openLoginModal } = useLoginModal();
60
+ const { openProModal } = useProModal();
61
+ const [openProvider, setOpenProvider] = useState(false);
62
+ const [providerError, setProviderError] = useState("");
63
+
64
+ const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
65
+ useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
66
+ isActive: true,
67
+ primaryColor: undefined,
68
+ secondaryColor: undefined,
69
+ theme: undefined,
70
+ });
71
+
72
+ const [isFollowUp, setIsFollowUp] = useState(true);
73
+ const [prompt, setPrompt] = useState("");
74
+ const [think, setThink] = useState("");
75
+ const [openThink, setOpenThink] = useState(false);
76
+
77
+ const isSameHtml = useMemo(() => {
78
+ return isTheSameHtml(currentPageData.html);
79
+ }, [currentPageData.html]);
80
+
81
+ const handleThink = (think: string) => {
82
+ setThink(think);
83
+ setIsThinking(true);
84
+ setOpenThink(true);
85
+ };
86
+
87
+ const callAi = async (redesignMarkdown?: string) => {
88
+ if (!user) return openLoginModal();
89
+ if (isAiWorking) return;
90
+ if (!redesignMarkdown && !prompt.trim()) return;
91
+
92
+ if (isFollowUp && !redesignMarkdown && !isSameHtml) {
93
+ const result = await callAiFollowUp(prompt, enhancedSettings);
94
+
95
+ if (result?.error) {
96
+ handleError(result.error, result.message);
97
+ return;
98
+ }
99
+
100
+ if (result?.success) {
101
+ setPrompt("");
102
+ }
103
+ } else {
104
+ const result = await callAiNewProject(
105
+ prompt,
106
+ enhancedSettings,
107
+ redesignMarkdown,
108
+ handleThink,
109
+ () => {
110
+ setIsThinking(false);
111
+ }
112
+ );
113
+
114
+ if (result?.error) {
115
+ handleError(result.error, result.message);
116
+ return;
117
+ }
118
+
119
+ if (result?.success) {
120
+ setPrompt("");
121
+ if (selectedModel?.isThinker) {
122
+ setModel(MODELS[0].value);
123
+ }
124
+ }
125
+ }
126
+ };
127
+
128
+ const handleError = (error: string, message?: string) => {
129
+ switch (error) {
130
+ case "login_required":
131
+ openLoginModal();
132
+ break;
133
+ case "provider_required":
134
+ setOpenProvider(true);
135
+ setProviderError(message || "");
136
+ break;
137
+ case "pro_required":
138
+ openProModal([]);
139
+ break;
140
+ case "api_error":
141
+ toast.error(message || "An error occurred");
142
+ break;
143
+ case "network_error":
144
+ toast.error(message || "Network error occurred");
145
+ break;
146
+ default:
147
+ toast.error("An unexpected error occurred");
148
+ }
149
+ };
150
+
151
+ return (
152
+ <div className="p-3 w-full">
153
+ <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">
154
+ <SelectedFiles
155
+ files={selectedFiles}
156
+ isAiWorking={isAiWorking}
157
+ onDelete={(file) =>
158
+ setSelectedFiles(selectedFiles.filter((f) => f !== file))
159
+ }
160
+ />
161
+ {selectedElement && (
162
+ <div className="px-4 pt-3">
163
+ <SelectedHtmlElement
164
+ element={selectedElement}
165
+ isAiWorking={isAiWorking}
166
+ onDelete={() => setSelectedElement(null)}
167
+ />
168
+ </div>
169
+ )}
170
+ <div className="w-full relative flex items-center justify-between">
171
+ {(isAiWorking || isUploading || isThinking) && (
172
+ <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">
173
+ <AiLoading
174
+ text={
175
+ isUploading
176
+ ? "Uploading images..."
177
+ : isAiWorking && !isSameHtml
178
+ ? "DeepSite is working..."
179
+ : "DeepSite is thinking..."
180
+ }
181
+ />
182
+ {isAiWorking && (
183
+ <Button
184
+ size="iconXs"
185
+ variant="outline"
186
+ className="!rounded-md mr-0.5"
187
+ onClick={cancelRequest}
188
+ >
189
+ <CircleStop className="size-4" />
190
+ </Button>
191
+ )}
192
+ </div>
193
+ )}
194
+ <textarea
195
+ disabled={
196
+ isAiWorking || isUploading || isThinking || isLoadingProject
197
+ }
198
+ className={classNames(
199
+ "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
200
+ {
201
+ "!pt-2.5":
202
+ selectedElement &&
203
+ !(isAiWorking || isUploading || isThinking),
204
+ }
205
+ )}
206
+ placeholder={
207
+ selectedElement
208
+ ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
209
+ : isFollowUp && (!isSameHtml || pages?.length > 1)
210
+ ? "Ask DeepSite for edits"
211
+ : "Ask DeepSite anything..."
212
+ }
213
+ value={prompt}
214
+ onChange={(e) => setPrompt(e.target.value)}
215
+ onKeyDown={(e) => {
216
+ if (e.key === "Enter" && !e.shiftKey) {
217
+ callAi();
218
+ }
219
+ }}
220
+ />
221
+ </div>
222
+ <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
223
+ <div className="flex-1 flex items-center justify-start gap-1.5">
224
+ <PromptBuilder
225
+ enhancedSettings={enhancedSettings!}
226
+ setEnhancedSettings={setEnhancedSettings}
227
+ />
228
+ <Settings
229
+ open={openProvider}
230
+ error={providerError}
231
+ isFollowUp={!isSameHtml && isFollowUp}
232
+ onClose={setOpenProvider}
233
+ />
234
+ {!isNew && <Uploader project={project} />}
235
+ {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
236
+ {!isNew && <Selector />}
237
+ </div>
238
+ <div className="flex items-center justify-end gap-2">
239
+ <Button
240
+ size="iconXs"
241
+ variant="outline"
242
+ className="!rounded-md"
243
+ disabled={
244
+ isAiWorking || isUploading || isThinking || !prompt.trim()
245
+ }
246
+ onClick={() => callAi()}
247
+ >
248
+ <ArrowUp className="size-4" />
249
+ </Button>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ <audio ref={hookAudio} id="audio" className="hidden">
254
+ <source src="/success.mp3" type="audio/mpeg" />
255
+ Your browser does not support the audio element.
256
+ </audio>
257
+ </div>
258
+ );
259
+ };
components/editor/ask-ai/loading.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Loading from "@/components/loading";
2
+
3
+ export const AiLoading = ({
4
+ text = "Ai is working...",
5
+ className,
6
+ }: {
7
+ text?: string;
8
+ className?: string;
9
+ }) => {
10
+ return (
11
+ <div className={`flex items-center justify-start gap-2 ${className}`}>
12
+ <Loading overlay={false} className="!size-4 opacity-50" />
13
+ <p className="text-neutral-400 text-sm">
14
+ <span className="inline-flex">
15
+ {text.split("").map((char, index) => (
16
+ <span
17
+ key={index}
18
+ className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
19
+ style={{
20
+ animationDelay: `${index * 0.1}s`,
21
+ animationDuration: "1.3s",
22
+ animationIterationCount: "infinite",
23
+ }}
24
+ >
25
+ {char === " " ? "\u00A0" : char}
26
+ </span>
27
+ ))}
28
+ </span>
29
+ </p>
30
+ </div>
31
+ );
32
+ };
components/editor/ask-ai/prompt-builder/content-modal.tsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { EnhancedSettings } from ".";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Themes } from "./themes";
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 ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { WandSparkles } from "lucide-react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import { useEditor } from "@/hooks/useEditor";
6
+ import { useAi } from "@/hooks/useAi";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogFooter,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import { ContentModal } from "./content-modal";
14
+ import { useLoginModal } from "@/components/contexts/login-context";
15
+ import { useUser } from "@/hooks/useUser";
16
+ import { EnhancedSettings } from "@/types";
17
+
18
+ export const PromptBuilder = ({
19
+ enhancedSettings,
20
+ setEnhancedSettings,
21
+ }: {
22
+ enhancedSettings: EnhancedSettings;
23
+ setEnhancedSettings: (settings: EnhancedSettings) => void;
24
+ }) => {
25
+ const { user } = useUser();
26
+ const { openLoginModal } = useLoginModal();
27
+ const { globalAiLoading } = useAi();
28
+ const { globalEditorLoading } = useEditor();
29
+
30
+ const [open, setOpen] = useState(false);
31
+ return (
32
+ <>
33
+ <Button
34
+ size="xs"
35
+ variant="outline"
36
+ className="!rounded-md !border-white/10 !bg-gradient-to-r from-sky-400/15 to-purple-400/15 light-sweep hover:brightness-110"
37
+ disabled={globalAiLoading || globalEditorLoading}
38
+ onClick={() => {
39
+ if (!user) return openLoginModal();
40
+ setOpen(true);
41
+ }}
42
+ >
43
+ <WandSparkles className="size-3.5 text-sky-500 relative z-10" />
44
+ <span className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text relative z-10">
45
+ Enhance
46
+ </span>
47
+ </Button>
48
+ <Dialog open={open} onOpenChange={() => setOpen(false)}>
49
+ <DialogContent className="sm:max-w-xl !p-0 !rounded-3xl !bg-neutral-900 !border-neutral-800/80 !gap-0">
50
+ <DialogTitle className="px-6 py-3.5 border-b border-neutral-800">
51
+ <div className="flex items-center justify-start gap-2 text-neutral-200 text-base font-medium">
52
+ <WandSparkles className="size-3.5" />
53
+ <p>Enhance Prompt</p>
54
+ </div>
55
+ </DialogTitle>
56
+ <ContentModal
57
+ enhancedSettings={enhancedSettings}
58
+ setEnhancedSettings={setEnhancedSettings}
59
+ />
60
+ <DialogFooter className="px-6 py-3.5 border-t border-neutral-800">
61
+ <Button
62
+ variant="bordered"
63
+ size="default"
64
+ onClick={() => setOpen(false)}
65
+ >
66
+ Close
67
+ </Button>
68
+ </DialogFooter>
69
+ </DialogContent>
70
+ </Dialog>
71
+ </>
72
+ );
73
+ };
components/editor/ask-ai/prompt-builder/tailwind-colors.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Paintbrush } from "lucide-react";
3
+ import { toast } from "sonner";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Popover,
8
+ PopoverContent,
9
+ PopoverTrigger,
10
+ } from "@/components/ui/popover";
11
+ import { Input } from "@/components/ui/input";
12
+ import Loading from "@/components/loading";
13
+ import { api } from "@/lib/api";
14
+ import { useAi } from "@/hooks/useAi";
15
+ import { useEditor } from "@/hooks/useEditor";
16
+ import { useUser } from "@/hooks/useUser";
17
+ import { useLoginModal } from "@/components/contexts/login-context";
18
+
19
+ export function ReImagine({
20
+ onRedesign,
21
+ }: {
22
+ onRedesign: (md: string) => void;
23
+ }) {
24
+ const [url, setUrl] = useState<string>("");
25
+ const [open, setOpen] = useState(false);
26
+ const [isLoading, setIsLoading] = useState(false);
27
+ const { globalAiLoading } = useAi();
28
+ const { globalEditorLoading } = useEditor();
29
+ const { user } = useUser();
30
+ const { openLoginModal } = useLoginModal();
31
+
32
+ const checkIfUrlIsValid = (url: string) => {
33
+ const urlPattern = new RegExp(
34
+ /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,
35
+ "i"
36
+ );
37
+ return urlPattern.test(url);
38
+ };
39
+
40
+ const handleClick = async () => {
41
+ if (isLoading) return; // Prevent multiple clicks while loading
42
+ if (!url) {
43
+ toast.error("Please enter a URL.");
44
+ return;
45
+ }
46
+ if (!checkIfUrlIsValid(url)) {
47
+ toast.error("Please enter a valid URL.");
48
+ return;
49
+ }
50
+ setIsLoading(true);
51
+ const response = await api.put("/re-design", {
52
+ url: url.trim(),
53
+ });
54
+ if (response?.data?.ok) {
55
+ setOpen(false);
56
+ setUrl("");
57
+ onRedesign(response.data.markdown);
58
+ toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
59
+ } else {
60
+ toast.error(response?.data?.error || "Failed to redesign the site.");
61
+ }
62
+ setIsLoading(false);
63
+ };
64
+
65
+ if (!user)
66
+ return (
67
+ <Button
68
+ size="xs"
69
+ variant="outline"
70
+ className="!rounded-md"
71
+ onClick={() => openLoginModal()}
72
+ >
73
+ <Paintbrush className="size-3.5" />
74
+ Redesign
75
+ </Button>
76
+ );
77
+
78
+ return (
79
+ <Popover open={open} onOpenChange={setOpen}>
80
+ <form>
81
+ <PopoverTrigger asChild>
82
+ <Button
83
+ size="xs"
84
+ variant={open ? "default" : "outline"}
85
+ className="!rounded-md"
86
+ disabled={globalAiLoading || globalEditorLoading}
87
+ >
88
+ <Paintbrush className="size-3.5" />
89
+ Redesign
90
+ </Button>
91
+ </PopoverTrigger>
92
+ <PopoverContent
93
+ align="start"
94
+ className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
95
+ >
96
+ <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
97
+ <div className="flex items-center justify-center -space-x-4 mb-3">
98
+ <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
99
+ 🎨
100
+ </div>
101
+ <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
102
+ 🥳
103
+ </div>
104
+ <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
105
+ 💎
106
+ </div>
107
+ </div>
108
+ <p className="text-xl font-semibold text-neutral-950">
109
+ Redesign your Site!
110
+ </p>
111
+ <p className="text-sm text-neutral-500 mt-1.5">
112
+ Try our new Redesign feature to give your site a fresh look.
113
+ </p>
114
+ </header>
115
+ <main className="space-y-4 p-6">
116
+ <div>
117
+ <p className="text-sm text-neutral-700 mb-2">
118
+ Enter your website URL to get started:
119
+ </p>
120
+ <Input
121
+ type="text"
122
+ placeholder="https://example.com"
123
+ value={url}
124
+ onChange={(e) => setUrl(e.target.value)}
125
+ onBlur={(e) => {
126
+ const inputUrl = e.target.value.trim();
127
+ if (!inputUrl) {
128
+ setUrl("");
129
+ return;
130
+ }
131
+ if (!checkIfUrlIsValid(inputUrl)) {
132
+ toast.error("Please enter a valid URL.");
133
+ return;
134
+ }
135
+ setUrl(inputUrl);
136
+ }}
137
+ className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
138
+ />
139
+ </div>
140
+ <div>
141
+ <p className="text-sm text-neutral-700 mb-2">
142
+ Then, let&apos;s redesign it!
143
+ </p>
144
+ <Button
145
+ variant="black"
146
+ onClick={handleClick}
147
+ className="relative w-full"
148
+ >
149
+ {isLoading ? (
150
+ <>
151
+ <Loading
152
+ overlay={false}
153
+ className="ml-2 size-4 animate-spin"
154
+ />
155
+ Fetching your site...
156
+ </>
157
+ ) : (
158
+ <>
159
+ Redesign <Paintbrush className="size-4" />
160
+ </>
161
+ )}
162
+ </Button>
163
+ </div>
164
+ </main>
165
+ </PopoverContent>
166
+ </form>
167
+ </Popover>
168
+ );
169
+ }
components/editor/ask-ai/selected-files.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/selected-html-element.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { Code, XCircle } from "lucide-react";
3
+
4
+ import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible";
5
+ import { htmlTagToText } from "@/lib/html-tag-to-text";
6
+
7
+ export const SelectedHtmlElement = ({
8
+ element,
9
+ isAiWorking = false,
10
+ onDelete,
11
+ }: {
12
+ element: HTMLElement | null;
13
+ isAiWorking: boolean;
14
+ onDelete?: () => void;
15
+ }) => {
16
+ if (!element) return null;
17
+
18
+ const tagName = element.tagName.toLowerCase();
19
+ return (
20
+ <Collapsible
21
+ className={classNames(
22
+ "border border-neutral-700 rounded-xl p-1.5 pr-3 max-w-max hover:brightness-110 transition-all duration-200 ease-in-out !cursor-pointer",
23
+ {
24
+ "!cursor-pointer": !isAiWorking,
25
+ "opacity-50 !cursor-not-allowed": isAiWorking,
26
+ }
27
+ )}
28
+ disabled={isAiWorking}
29
+ onClick={() => {
30
+ if (!isAiWorking && onDelete) {
31
+ onDelete();
32
+ }
33
+ }}
34
+ >
35
+ <CollapsibleTrigger className="flex items-center justify-start gap-2 cursor-pointer">
36
+ <div className="rounded-lg bg-neutral-700 size-6 flex items-center justify-center">
37
+ <Code className="text-neutral-300 size-3.5" />
38
+ </div>
39
+ <p className="text-sm font-semibold text-neutral-300">
40
+ {element.textContent?.trim().split(/\s+/)[0]} {htmlTagToText(tagName)}
41
+ </p>
42
+ <XCircle className="text-neutral-300 size-4" />
43
+ </CollapsibleTrigger>
44
+ {/* <CollapsibleContent className="border-t border-neutral-700 pt-2 mt-2">
45
+ <div className="text-xs text-neutral-400">
46
+ <p>
47
+ <span className="font-semibold">ID:</span> {element.id || "No ID"}
48
+ </p>
49
+ <p>
50
+ <span className="font-semibold">Classes:</span>{" "}
51
+ {element.className || "No classes"}
52
+ </p>
53
+ </div>
54
+ </CollapsibleContent> */}
55
+ </Collapsible>
56
+ );
57
+ };
components/editor/ask-ai/selector.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import classNames from "classnames";
3
+
4
+ import {
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ } from "@/components/ui/popover";
9
+ import { PROVIDERS, MODELS } from "@/lib/providers";
10
+ import { Button } from "@/components/ui/button";
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectGroup,
15
+ SelectItem,
16
+ SelectLabel,
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
54
+ )?.providers;
55
+ if (!availableProviders) return Object.keys(PROVIDERS);
56
+ return Object.keys(PROVIDERS).filter((id) =>
57
+ availableProviders.includes(id)
58
+ );
59
+ }, [model]);
60
+
61
+ useUpdateEffect(() => {
62
+ if (
63
+ provider !== "auto" &&
64
+ !modelAvailableProviders.includes(provider as string)
65
+ ) {
66
+ setProvider("auto");
67
+ }
68
+ }, [model, provider]);
69
+
70
+ return (
71
+ <Popover open={open} onOpenChange={onClose}>
72
+ <PopoverTrigger asChild>
73
+ <Button
74
+ variant={open ? "default" : "outline"}
75
+ className="!rounded-md"
76
+ disabled={globalAiLoading}
77
+ size="xs"
78
+ >
79
+ <Brain className="size-3.5" />
80
+ <span className="truncate max-w-[120px]">
81
+ {isMounted
82
+ ? selectedModel?.label?.split(" ").join("-").toLowerCase()
83
+ : "..."}
84
+ </span>
85
+ <ChevronDown className="size-3.5" />
86
+ </Button>
87
+ </PopoverTrigger>
88
+ <PopoverContent
89
+ className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
90
+ align="center"
91
+ >
92
+ <header className="flex items-center justify-center text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
93
+ Customize Settings
94
+ </header>
95
+ <main className="px-4 pt-5 pb-6 space-y-5">
96
+ {error !== "" && (
97
+ <p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
98
+ {error}
99
+ </p>
100
+ )}
101
+ <label className="block">
102
+ <p className="text-neutral-300 text-sm mb-2.5">Choose a model</p>
103
+ <Select defaultValue={model} onValueChange={setModel}>
104
+ <SelectTrigger className="w-full">
105
+ <SelectValue placeholder="Select a model" />
106
+ </SelectTrigger>
107
+ <SelectContent>
108
+ <SelectGroup>
109
+ <SelectLabel>Models</SelectLabel>
110
+ {MODELS.map(
111
+ ({
112
+ value,
113
+ label,
114
+ isNew = false,
115
+ isThinker = false,
116
+ }: {
117
+ value: string;
118
+ label: string;
119
+ isNew?: boolean;
120
+ isThinker?: boolean;
121
+ }) => (
122
+ <SelectItem
123
+ key={value}
124
+ value={value}
125
+ className=""
126
+ disabled={isThinker && isFollowUp}
127
+ >
128
+ {label}
129
+ {isNew && (
130
+ <span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
131
+ New
132
+ </span>
133
+ )}
134
+ </SelectItem>
135
+ )
136
+ )}
137
+ </SelectGroup>
138
+ </SelectContent>
139
+ </Select>
140
+ </label>
141
+ {isFollowUp && (
142
+ <div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
143
+ Note: You can&apos;t use a Thinker model for follow-up requests.
144
+ We automatically switch to the default model for you.
145
+ </div>
146
+ )}
147
+ <div className="flex flex-col gap-3">
148
+ <div className="flex items-center justify-between">
149
+ <div>
150
+ <p className="text-neutral-300 text-sm mb-1.5">
151
+ Use auto-provider
152
+ </p>
153
+ <p className="text-xs text-neutral-400/70">
154
+ We&apos;ll automatically select the best provider for you
155
+ based on your prompt.
156
+ </p>
157
+ </div>
158
+ <div
159
+ className={classNames(
160
+ "bg-neutral-700 rounded-full min-w-10 w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
161
+ {
162
+ "!bg-sky-500": provider === "auto",
163
+ }
164
+ )}
165
+ onClick={() => {
166
+ const foundModel = MODELS.find(
167
+ (m: { value: string }) => m.value === model
168
+ );
169
+ if (provider === "auto" && foundModel?.autoProvider) {
170
+ setProvider(foundModel.autoProvider);
171
+ } else {
172
+ setProvider("auto");
173
+ }
174
+ }}
175
+ >
176
+ <div
177
+ className={classNames(
178
+ "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
179
+ {
180
+ "translate-x-4": provider === "auto",
181
+ }
182
+ )}
183
+ />
184
+ </div>
185
+ </div>
186
+ <label className="block">
187
+ <p className="text-neutral-300 text-sm mb-2">
188
+ Inference Provider
189
+ </p>
190
+ <div className="grid grid-cols-2 gap-1.5">
191
+ {modelAvailableProviders.map((id: string) => (
192
+ <Button
193
+ key={id}
194
+ variant={id === provider ? "default" : "secondary"}
195
+ size="sm"
196
+ onClick={() => {
197
+ setProvider(id);
198
+ }}
199
+ >
200
+ <Image
201
+ src={`/providers/${id}.svg`}
202
+ alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
203
+ className="size-5 mr-2"
204
+ width={20}
205
+ height={20}
206
+ />
207
+ {PROVIDERS[id as keyof typeof PROVIDERS].name}
208
+ {id === provider && (
209
+ <CheckCheck className="ml-2 size-4 text-blue-500" />
210
+ )}
211
+ </Button>
212
+ ))}
213
+ </div>
214
+ </label>
215
+ </div>
216
+ </main>
217
+ </PopoverContent>
218
+ </Popover>
219
+ );
220
+ }
components/editor/ask-ai/uploader.tsx ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/header/index.tsx ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ArrowRight, HelpCircle, RefreshCcw } 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
+
14
+ export function Header() {
15
+ const { user, openLoginWindow } = useUser();
16
+ return (
17
+ <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">
18
+ <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">
19
+ <h1 className="text-neutral-900 dark:text-white text-lg lg:text-xl font-bold flex items-center justify-start">
20
+ <Image
21
+ src={Logo}
22
+ alt="DeepSite Logo"
23
+ className="size-8 invert-100 dark:invert-0"
24
+ />
25
+ <p className="ml-2 flex items-center justify-start max-lg:hidden">
26
+ DeepSite
27
+ {user?.isPro ? (
28
+ <ProTag className="ml-2 !text-[10px]" />
29
+ ) : (
30
+ <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">
31
+ {" "}
32
+ v3
33
+ </span>
34
+ )}
35
+ </p>
36
+ </h1>
37
+ <div className="flex items-center justify-end gap-2">
38
+ <History />
39
+ <SwitchTab />
40
+ </div>
41
+ </div>
42
+ <div className="lg:hidden flex items-center justify-center whitespace-nowrap">
43
+ <SwitchTab isMobile />
44
+ </div>
45
+ <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">
46
+ <div className="font-mono text-muted-foreground flex items-center gap-2">
47
+ <SwitchDevice />
48
+ <Button
49
+ size="xs"
50
+ variant="bordered"
51
+ className="max-lg:hidden"
52
+ onClick={() => {
53
+ const iframe = document.getElementById(
54
+ "preview-iframe"
55
+ ) as HTMLIFrameElement;
56
+ if (iframe) {
57
+ iframe.src = iframe.src;
58
+ }
59
+ }}
60
+ >
61
+ <RefreshCcw className="size-3 mr-0.5" />
62
+ Refresh Preview
63
+ </Button>
64
+ <Link
65
+ href="https://huggingface.co/enzostvs/deepsite"
66
+ target="_blank"
67
+ className="max-lg:hidden"
68
+ >
69
+ <Button size="xs" variant="bordered">
70
+ <HelpCircle className="size-3 mr-0.5" />
71
+ Help
72
+ </Button>
73
+ </Link>
74
+ </div>
75
+ {user ? (
76
+ <UserMenu className="!pl-1 !pr-3 !py-1 !h-auto" />
77
+ ) : (
78
+ <Button size="sm" onClick={openLoginWindow}>
79
+ Access to my Account
80
+ <ArrowRight className="size-4" />
81
+ </Button>
82
+ )}
83
+ </div>
84
+ </header>
85
+ );
86
+ }
components/editor/header/switch-tab.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/index.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 } = 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
33
+ className="!rounded-2xl !p-0 overflow-hidden !bg-neutral-900"
34
+ align="start"
35
+ >
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
+ <ul className="max-h-[250px] overflow-y-auto">
41
+ {commits?.map((item: Commit, index: number) => (
42
+ <li
43
+ key={index}
44
+ className={classNames(
45
+ "px-4 text-gray-200 py-2 border-b border-gray-800 last:border-0 space-y-1",
46
+ {
47
+ "bg-blue-500/10": currentCommit === item.oid,
48
+ }
49
+ )}
50
+ >
51
+ <p className="line-clamp-1 text-sm">{item.title}</p>
52
+ <div className="w-full flex items-center justify-between gap-2">
53
+ <p className="text-gray-500 text-[10px]">
54
+ {new Date(item.date).toLocaleDateString("en-US", {
55
+ month: "2-digit",
56
+ day: "2-digit",
57
+ year: "2-digit",
58
+ }) +
59
+ " " +
60
+ new Date(item.date).toLocaleTimeString("en-US", {
61
+ hour: "2-digit",
62
+ minute: "2-digit",
63
+ second: "2-digit",
64
+ hour12: false,
65
+ })}
66
+ </p>
67
+ {currentCommit !== item.oid ? (
68
+ <Button
69
+ variant="link"
70
+ size="xss"
71
+ className="text-gray-400 hover:text-gray-200"
72
+ onClick={() => {
73
+ setCurrentCommit(item.oid);
74
+ }}
75
+ >
76
+ See version
77
+ </Button>
78
+ ) : (
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
+ </div>
84
+ </li>
85
+ ))}
86
+ </ul>
87
+ </main>
88
+ </PopoverContent>
89
+ </Popover>
90
+ );
91
+ }
components/editor/index.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useMemo, useRef, useState } from "react";
3
+ import { useCopyToClipboard } 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
+
14
+ import { ListPages } from "./pages";
15
+ import { AskAi } from "./ask-ai";
16
+ import { Preview } from "./preview";
17
+ import Loading from "../loading";
18
+
19
+ export const AppEditor = ({
20
+ namespace,
21
+ repoId,
22
+ isNew = false,
23
+ }: {
24
+ namespace?: string;
25
+ repoId?: string;
26
+ isNew?: boolean;
27
+ }) => {
28
+ const { project, setPages, files, currentPageData, currentTab } = useEditor(
29
+ namespace,
30
+ repoId
31
+ );
32
+ const [, copyToClipboard] = useCopyToClipboard();
33
+
34
+ const monacoRef = useRef<any>(null);
35
+ const editor = useRef<HTMLDivElement>(null);
36
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
37
+
38
+ return (
39
+ <section className="h-screen w-full bg-neutral-950 flex flex-col">
40
+ <Header />
41
+ <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full relative">
42
+ <div
43
+ ref={editor}
44
+ className={classNames(
45
+ "bg-neutral-900 relative flex h-full max-h-[calc(100dvh-47px)] w-full flex-col lg:max-w-[600px] transition-all duration-200",
46
+ {
47
+ "max-lg:hidden lg:!w-[0px] overflow-hidden":
48
+ currentTab !== "chat",
49
+ }
50
+ )}
51
+ >
52
+ <ListPages />
53
+ <CopyIcon
54
+ className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
55
+ onClick={() => {
56
+ copyToClipboard(currentPageData.html);
57
+ toast.success("HTML copied to clipboard!");
58
+ }}
59
+ />
60
+ <Editor
61
+ defaultLanguage="html"
62
+ theme="vs-dark"
63
+ loading={<Loading overlay={false} />}
64
+ className="h-full absolute left-0 top-0 lg:min-w-[600px]"
65
+ options={{
66
+ colorDecorators: true,
67
+ fontLigatures: true,
68
+ theme: "vs-dark",
69
+ minimap: { enabled: false },
70
+ scrollbar: {
71
+ horizontal: "hidden",
72
+ },
73
+ wordWrap: "on",
74
+ }}
75
+ value={currentPageData.html}
76
+ onChange={(value) => {
77
+ const newValue = value ?? "";
78
+ setPages((prev) =>
79
+ prev.map((page) =>
80
+ page.path === currentPageData.path
81
+ ? { ...page, html: newValue }
82
+ : page
83
+ )
84
+ );
85
+ }}
86
+ onMount={(editor, monaco) => {
87
+ editorRef.current = editor;
88
+ monacoRef.current = monaco;
89
+ }}
90
+ />
91
+ <AskAi
92
+ project={project}
93
+ files={files}
94
+ isNew={isNew}
95
+ onScrollToBottom={() => {
96
+ editorRef.current?.revealLine(
97
+ editorRef.current?.getModel()?.getLineCount() ?? 0
98
+ );
99
+ }}
100
+ />
101
+ </div>
102
+ <Preview isNew={isNew} />
103
+ </main>
104
+ </section>
105
+ );
106
+ };
components/editor/pages/index.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Page } from "@/types";
2
+ import { ListPagesItem } from "./page";
3
+ import { useEditor } from "@/hooks/useEditor";
4
+
5
+ export function ListPages() {
6
+ const { pages, setPages, currentPage, setCurrentPage } = useEditor();
7
+ return (
8
+ <div className="w-full flex items-center justify-start bg-neutral-950 overflow-auto flex-nowrap">
9
+ {pages.map((page: Page, i: number) => (
10
+ <ListPagesItem
11
+ key={page.path ?? i}
12
+ page={page}
13
+ currentPage={currentPage}
14
+ onSelectPage={setCurrentPage}
15
+ onDeletePage={(path) => {
16
+ setPages(pages.filter((page) => page.path !== path));
17
+ setCurrentPage("index.html");
18
+ }}
19
+ index={i}
20
+ />
21
+ ))}
22
+ </div>
23
+ );
24
+ }
components/editor/pages/page.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { FileCode, XIcon } from "lucide-react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import { Page } from "@/types";
6
+
7
+ export function ListPagesItem({
8
+ page,
9
+ currentPage,
10
+ onSelectPage,
11
+ onDeletePage,
12
+ index,
13
+ }: {
14
+ page: Page;
15
+ currentPage: string;
16
+ onSelectPage: (path: string, newPath?: string) => void;
17
+ onDeletePage: (path: string) => void;
18
+ index: number;
19
+ }) {
20
+ return (
21
+ <div
22
+ key={index}
23
+ className={classNames(
24
+ "pl-6 pr-1 py-4 text-neutral-400 cursor-pointer text-sm hover:bg-neutral-900 flex items-center justify-center gap-1 group text-nowrap border-r border-neutral-800",
25
+ {
26
+ "bg-neutral-900 !text-white border-b": currentPage === page.path,
27
+ "!pr-6": index === 0, // Ensure the first item has padding on the right
28
+ }
29
+ )}
30
+ onClick={() => onSelectPage(page.path)}
31
+ title={page.path}
32
+ >
33
+ <FileCode className="size-4 mr-1" />
34
+ {page.path}
35
+ {index > 0 && (
36
+ <Button
37
+ size="iconXsss"
38
+ variant="ghost"
39
+ className="group-hover:opacity-100 opacity-0 !h-auto"
40
+ onClick={(e) => {
41
+ e.stopPropagation();
42
+ if (
43
+ window.confirm(
44
+ "Are you sure you want to delete this page? This action cannot be undone."
45
+ )
46
+ ) {
47
+ onDeletePage(page.path);
48
+ }
49
+ }}
50
+ >
51
+ <XIcon className="h-3 text-neutral-400 cursor-pointer hover:text-neutral-300" />
52
+ </Button>
53
+ )}
54
+ </div>
55
+ );
56
+ }