This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. .env.example +5 -0
  2. .gitignore +23 -38
  3. Dockerfile +5 -2
  4. README.md +3 -12
  5. app/(public)/layout.tsx +0 -15
  6. app/(public)/page.tsx +0 -44
  7. app/(public)/projects/page.tsx +0 -13
  8. app/actions/auth.ts +0 -18
  9. app/actions/projects.ts +0 -63
  10. app/api/ask-ai/route.ts +0 -503
  11. app/api/auth/route.ts +0 -86
  12. app/api/me/projects/[namespace]/[repoId]/images/route.ts +0 -111
  13. app/api/me/projects/[namespace]/[repoId]/route.ts +0 -273
  14. app/api/me/projects/route.ts +0 -124
  15. app/api/me/route.ts +0 -25
  16. app/api/re-design/route.ts +0 -39
  17. app/auth/callback/page.tsx +0 -72
  18. app/auth/page.tsx +0 -28
  19. app/favicon.ico +0 -0
  20. app/layout.tsx +0 -112
  21. app/projects/[namespace]/[repoId]/page.tsx +0 -42
  22. app/projects/new/page.tsx +0 -5
  23. assets/globals.css +0 -146
  24. assets/logo.svg +0 -316
  25. components.json +0 -21
  26. components/contexts/app-context.tsx +0 -57
  27. components/contexts/user-context.tsx +0 -8
  28. components/editor/ask-ai/follow-up-tooltip.tsx +0 -36
  29. components/editor/ask-ai/index.tsx +0 -444
  30. components/editor/ask-ai/re-imagine.tsx +0 -146
  31. components/editor/ask-ai/selected-files.tsx +0 -47
  32. components/editor/ask-ai/selected-html-element.tsx +0 -57
  33. components/editor/ask-ai/settings.tsx +0 -202
  34. components/editor/ask-ai/uploader.tsx +0 -202
  35. components/editor/deploy-button/content.tsx +0 -111
  36. components/editor/deploy-button/index.tsx +0 -79
  37. components/editor/footer/index.tsx +0 -128
  38. components/editor/header/index.tsx +0 -69
  39. components/editor/history/index.tsx +0 -73
  40. components/editor/index.tsx +0 -386
  41. components/editor/pages/index.tsx +0 -30
  42. components/editor/pages/page.tsx +0 -82
  43. components/editor/preview/index.tsx +0 -231
  44. components/editor/save-button/index.tsx +0 -76
  45. components/iframe-detector.tsx +0 -75
  46. components/iframe-warning-modal.tsx +0 -61
  47. components/invite-friends/index.tsx +0 -85
  48. components/login-modal/index.tsx +0 -62
  49. components/magic-ui/grid-pattern.tsx +0 -69
  50. components/my-projects/index.tsx +0 -57
.env.example ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ OAUTH_CLIENT_ID=
2
+ OAUTH_CLIENT_SECRET=
3
+ APP_PORT=5173
4
+ REDIRECT_URI=http://localhost:5173/auth/login
5
+ DEFAULT_HF_TOKEN=
.gitignore CHANGED
@@ -1,41 +1,26 @@
1
- # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
-
3
- # dependencies
4
- /node_modules
5
- /.pnp
6
- .pnp.*
7
- .yarn/*
8
- !.yarn/patches
9
- !.yarn/plugins
10
- !.yarn/releases
11
- !.yarn/versions
12
-
13
- # testing
14
- /coverage
15
-
16
- # next.js
17
- /.next/
18
- /out/
19
-
20
- # production
21
- /build
22
-
23
- # misc
24
- .DS_Store
25
- *.pem
26
-
27
- # debug
28
  npm-debug.log*
29
  yarn-debug.log*
30
  yarn-error.log*
31
- .pnpm-debug.log*
32
-
33
- # env files (can opt-in for committing if needed)
34
- .env*
35
-
36
- # vercel
37
- .vercel
38
-
39
- # typescript
40
- *.tsbuildinfo
41
- next-env.d.ts
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  npm-debug.log*
5
  yarn-debug.log*
6
  yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
25
+ .env
26
+ .aider*
Dockerfile CHANGED
@@ -1,6 +1,9 @@
1
- FROM node:20-alpine
 
 
2
  USER root
3
 
 
4
  USER 1000
5
  WORKDIR /usr/src/app
6
  # Copy package.json and package-lock.json to the container
@@ -13,7 +16,7 @@ RUN npm install
13
  RUN npm run build
14
 
15
  # Expose the application port (assuming your app runs on port 3000)
16
- EXPOSE 3000
17
 
18
  # Start the application
19
  CMD ["npm", "start"]
 
1
+ # Dockerfile
2
+ # Use an official Node.js runtime as the base image
3
+ FROM node:22.1.0
4
  USER root
5
 
6
+ RUN apt-get update
7
  USER 1000
8
  WORKDIR /usr/src/app
9
  # Copy package.json and package-lock.json to the container
 
16
  RUN npm run build
17
 
18
  # Expose the application port (assuming your app runs on port 3000)
19
+ EXPOSE 5173
20
 
21
  # Start the application
22
  CMD ["npm", "start"]
README.md CHANGED
@@ -1,22 +1,13 @@
1
  ---
2
- title: DeepSite v2
3
  emoji: 🐳
4
  colorFrom: blue
5
  colorTo: blue
6
  sdk: docker
7
  pinned: true
8
- app_port: 3000
9
  license: mit
10
  short_description: Generate any application with DeepSeek
11
- models:
12
- - deepseek-ai/DeepSeek-V3-0324
13
- - deepseek-ai/DeepSeek-R1-0528
14
  ---
15
 
16
- # DeepSite 🐳
17
-
18
- DeepSite is a coding platform powered by DeepSeek AI, designed to make coding smarter and more efficient. Tailored for developers, data scientists, and AI engineers, it integrates generative AI into your coding projects to enhance creativity and productivity.
19
-
20
- ## How to use it locally
21
-
22
- Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
 
1
  ---
2
+ title: DeepSite
3
  emoji: 🐳
4
  colorFrom: blue
5
  colorTo: blue
6
  sdk: docker
7
  pinned: true
8
+ app_port: 5173
9
  license: mit
10
  short_description: Generate any application with DeepSeek
 
 
 
11
  ---
12
 
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
app/(public)/layout.tsx DELETED
@@ -1,15 +0,0 @@
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="min-h-screen bg-black z-1 relative">
10
- <div className="background__noisy" />
11
- <Navigation />
12
- {children}
13
- </div>
14
- );
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/(public)/page.tsx DELETED
@@ -1,44 +0,0 @@
1
- import { AskAi } from "@/components/space/ask-ai";
2
- import { redirect } from "next/navigation";
3
- export default function Home() {
4
- redirect("/projects/new");
5
- return (
6
- <>
7
- <header className="container mx-auto pt-20 px-6 relative flex flex-col items-center justify-center text-center">
8
- <div className="rounded-full border border-neutral-100/10 bg-neutral-100/5 text-xs text-neutral-300 px-3 py-1 max-w-max mx-auto mb-2">
9
- ✨ DeepSite Public Beta
10
- </div>
11
- <h1 className="text-8xl font-semibold text-white font-mono max-w-4xl">
12
- Code your website with AI in seconds
13
- </h1>
14
- <p className="text-2xl text-neutral-300/80 mt-4 text-center max-w-2xl">
15
- Vibe Coding has never been so easy.
16
- </p>
17
- <div className="mt-14 max-w-2xl w-full mx-auto">
18
- <AskAi />
19
- </div>
20
- <div className="absolute inset-0 pointer-events-none -z-[1]">
21
- <div className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl rounded-full" />
22
- <div className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10 transform rotate-12" />
23
- <div className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10 rounded-3xl" />
24
- <div className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3 rounded-lg transform -rotate-15" />
25
- </div>
26
- </header>
27
- <div id="community" className="h-screen flex items-center justify-center">
28
- <h1 className="text-7xl font-extrabold text-white font-mono">
29
- Community Driven
30
- </h1>
31
- </div>
32
- <div id="deploy" className="h-screen flex items-center justify-center">
33
- <h1 className="text-7xl font-extrabold text-white font-mono">
34
- Deploy your website in seconds
35
- </h1>
36
- </div>
37
- <div id="features" className="h-screen flex items-center justify-center">
38
- <h1 className="text-7xl font-extrabold text-white font-mono">
39
- Features that make you smile
40
- </h1>
41
- </div>
42
- </>
43
- );
44
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/(public)/projects/page.tsx DELETED
@@ -1,13 +0,0 @@
1
- import { redirect } from "next/navigation";
2
-
3
- import { MyProjects } from "@/components/my-projects";
4
- import { getProjects } from "@/app/actions/projects";
5
-
6
- export default async function ProjectsPage() {
7
- const { ok, projects } = await getProjects();
8
- if (!ok) {
9
- redirect("/");
10
- }
11
-
12
- return <MyProjects projects={projects} />;
13
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/actions/auth.ts DELETED
@@ -1,18 +0,0 @@
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 DELETED
@@ -1,63 +0,0 @@
1
- "use server";
2
-
3
- import { isAuthenticated } from "@/lib/auth";
4
- import { NextResponse } from "next/server";
5
- import dbConnect from "@/lib/mongodb";
6
- import Project from "@/models/Project";
7
- import { Project as ProjectType } from "@/types";
8
-
9
- export async function getProjects(): Promise<{
10
- ok: boolean;
11
- projects: ProjectType[];
12
- }> {
13
- const user = await isAuthenticated();
14
-
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: false,
32
- projects: [],
33
- };
34
- }
35
- return {
36
- ok: true,
37
- projects: JSON.parse(JSON.stringify(projects)) as ProjectType[],
38
- };
39
- }
40
-
41
- export async function getProject(
42
- namespace: string,
43
- repoId: string
44
- ): Promise<ProjectType | null> {
45
- const user = await isAuthenticated();
46
-
47
- if (user instanceof NextResponse || !user) {
48
- return null;
49
- }
50
-
51
- await dbConnect();
52
- const project = await Project.findOne({
53
- user_id: user.id,
54
- namespace,
55
- repoId,
56
- }).lean();
57
-
58
- if (!project) {
59
- return null;
60
- }
61
-
62
- return JSON.parse(JSON.stringify(project)) as ProjectType;
63
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/ask-ai/route.ts DELETED
@@ -1,503 +0,0 @@
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
-
23
- const ipAddresses = new Map();
24
-
25
- export async function POST(request: NextRequest) {
26
- const authHeaders = await headers();
27
- const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
28
-
29
- const body = await request.json();
30
- const { prompt, provider, model, redesignMarkdown, previousPrompts, pages } = body;
31
-
32
- if (!model || (!prompt && !redesignMarkdown)) {
33
- return NextResponse.json(
34
- { ok: false, error: "Missing required fields" },
35
- { status: 400 }
36
- );
37
- }
38
-
39
- const selectedModel = MODELS.find(
40
- (m) => m.value === model || m.label === model
41
- );
42
-
43
- if (!selectedModel) {
44
- return NextResponse.json(
45
- { ok: false, error: "Invalid model selected" },
46
- { status: 400 }
47
- );
48
- }
49
-
50
- if (!selectedModel.providers.includes(provider) && provider !== "auto") {
51
- return NextResponse.json(
52
- {
53
- ok: false,
54
- error: `The selected model does not support the ${provider} provider.`,
55
- openSelectProvider: true,
56
- },
57
- { status: 400 }
58
- );
59
- }
60
-
61
- let token = userToken;
62
- let billTo: string | null = null;
63
-
64
- /**
65
- * Handle local usage token, this bypass the need for a user token
66
- * and allows local testing without authentication.
67
- * This is useful for development and testing purposes.
68
- */
69
- if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
70
- token = process.env.HF_TOKEN;
71
- }
72
-
73
- const ip = authHeaders.get("x-forwarded-for")?.includes(",")
74
- ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
75
- : authHeaders.get("x-forwarded-for");
76
-
77
- if (!token) {
78
- ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
79
- if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
80
- return NextResponse.json(
81
- {
82
- ok: false,
83
- openLogin: true,
84
- message: "Log In to continue using the service",
85
- },
86
- { status: 429 }
87
- );
88
- }
89
-
90
- token = process.env.DEFAULT_HF_TOKEN as string;
91
- billTo = "huggingface";
92
- }
93
-
94
- const DEFAULT_PROVIDER = PROVIDERS.novita;
95
- const selectedProvider =
96
- provider === "auto"
97
- ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
98
- : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
99
-
100
- try {
101
- const encoder = new TextEncoder();
102
- const stream = new TransformStream();
103
- const writer = stream.writable.getWriter();
104
-
105
- const response = new NextResponse(stream.readable, {
106
- headers: {
107
- "Content-Type": "text/plain; charset=utf-8",
108
- "Cache-Control": "no-cache",
109
- Connection: "keep-alive",
110
- },
111
- });
112
-
113
- (async () => {
114
- // let completeResponse = "";
115
- try {
116
- const client = new InferenceClient(token);
117
- const chatCompletion = client.chatCompletionStream(
118
- {
119
- model: selectedModel.value,
120
- provider: selectedProvider.id as any,
121
- messages: [
122
- {
123
- role: "system",
124
- content: INITIAL_SYSTEM_PROMPT,
125
- },
126
- ...(pages?.length > 1 ? [{
127
- role: "assistant",
128
- content: `Here are the current pages:\n\n${pages.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}\n\nNow, please create a new page based on this code. Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
129
- }] : []),
130
- {
131
- role: "user",
132
- content: redesignMarkdown
133
- ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
134
- : prompt,
135
- },
136
- ],
137
- max_tokens: selectedProvider.max_tokens,
138
- },
139
- billTo ? { billTo } : {}
140
- );
141
-
142
- while (true) {
143
- const { done, value } = await chatCompletion.next();
144
- if (done) {
145
- break;
146
- }
147
-
148
- const chunk = value.choices[0]?.delta?.content;
149
- if (chunk) {
150
- await writer.write(encoder.encode(chunk));
151
- }
152
- }
153
- } catch (error: any) {
154
- if (error.message?.includes("exceeded your monthly included credits")) {
155
- await writer.write(
156
- encoder.encode(
157
- JSON.stringify({
158
- ok: false,
159
- openProModal: true,
160
- message: error.message,
161
- })
162
- )
163
- );
164
- } else {
165
- await writer.write(
166
- encoder.encode(
167
- JSON.stringify({
168
- ok: false,
169
- message:
170
- error.message ||
171
- "An error occurred while processing your request.",
172
- })
173
- )
174
- );
175
- }
176
- } finally {
177
- await writer?.close();
178
- }
179
- })();
180
-
181
- return response;
182
- } catch (error: any) {
183
- return NextResponse.json(
184
- {
185
- ok: false,
186
- openSelectProvider: true,
187
- message:
188
- error?.message || "An error occurred while processing your request.",
189
- },
190
- { status: 500 }
191
- );
192
- }
193
- }
194
-
195
- export async function PUT(request: NextRequest) {
196
- const authHeaders = await headers();
197
- const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
198
-
199
- const body = await request.json();
200
- const { prompt, previousPrompt, provider, selectedElementHtml, model, pages, files } =
201
- body;
202
-
203
- if (!prompt || pages.length === 0) {
204
- return NextResponse.json(
205
- { ok: false, error: "Missing required fields" },
206
- { status: 400 }
207
- );
208
- }
209
-
210
- const selectedModel = MODELS.find(
211
- (m) => m.value === model || m.label === model
212
- );
213
- if (!selectedModel) {
214
- return NextResponse.json(
215
- { ok: false, error: "Invalid model selected" },
216
- { status: 400 }
217
- );
218
- }
219
-
220
- let token = userToken;
221
- let billTo: string | null = null;
222
-
223
- /**
224
- * Handle local usage token, this bypass the need for a user token
225
- * and allows local testing without authentication.
226
- * This is useful for development and testing purposes.
227
- */
228
- if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
229
- token = process.env.HF_TOKEN;
230
- }
231
-
232
- const ip = authHeaders.get("x-forwarded-for")?.includes(",")
233
- ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
234
- : authHeaders.get("x-forwarded-for");
235
-
236
- if (!token) {
237
- ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
238
- if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
239
- return NextResponse.json(
240
- {
241
- ok: false,
242
- openLogin: true,
243
- message: "Log In to continue using the service",
244
- },
245
- { status: 429 }
246
- );
247
- }
248
-
249
- token = process.env.DEFAULT_HF_TOKEN as string;
250
- billTo = "huggingface";
251
- }
252
-
253
- const client = new InferenceClient(token);
254
-
255
- const DEFAULT_PROVIDER = PROVIDERS.novita;
256
- const selectedProvider =
257
- provider === "auto"
258
- ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
259
- : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
260
-
261
- try {
262
- const response = await client.chatCompletion(
263
- {
264
- model: selectedModel.value,
265
- provider: selectedProvider.id as any,
266
- messages: [
267
- {
268
- role: "system",
269
- content: FOLLOW_UP_SYSTEM_PROMPT,
270
- },
271
- {
272
- role: "user",
273
- content: previousPrompt
274
- ? previousPrompt
275
- : "You are modifying the HTML file based on the user's request.",
276
- },
277
- {
278
- role: "assistant",
279
-
280
- content: `${
281
- selectedElementHtml
282
- ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
283
- : ""
284
- }. 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")}.` : ""}`,
285
- },
286
- {
287
- role: "user",
288
- content: prompt,
289
- },
290
- ],
291
- ...(selectedProvider.id !== "sambanova"
292
- ? {
293
- max_tokens: selectedProvider.max_tokens,
294
- }
295
- : {}),
296
- },
297
- billTo ? { billTo } : {}
298
- );
299
-
300
- const chunk = response.choices[0]?.message?.content;
301
- if (!chunk) {
302
- return NextResponse.json(
303
- { ok: false, message: "No content returned from the model" },
304
- { status: 400 }
305
- );
306
- }
307
-
308
- if (chunk) {
309
- const updatedLines: number[][] = [];
310
- let newHtml = "";
311
- const updatedPages = [...(pages || [])];
312
-
313
- 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');
314
- let updatePageMatch;
315
-
316
- while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
317
- const [, pagePath, pageContent] = updatePageMatch;
318
-
319
- const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
320
- if (pageIndex !== -1) {
321
- let pageHtml = updatedPages[pageIndex].html;
322
-
323
- let processedContent = pageContent;
324
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
325
- if (htmlMatch) {
326
- processedContent = htmlMatch[1];
327
- }
328
- let position = 0;
329
- let moreBlocks = true;
330
-
331
- while (moreBlocks) {
332
- const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
333
- if (searchStartIndex === -1) {
334
- moreBlocks = false;
335
- continue;
336
- }
337
-
338
- const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
339
- if (dividerIndex === -1) {
340
- moreBlocks = false;
341
- continue;
342
- }
343
-
344
- const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
345
- if (replaceEndIndex === -1) {
346
- moreBlocks = false;
347
- continue;
348
- }
349
-
350
- const searchBlock = processedContent.substring(
351
- searchStartIndex + SEARCH_START.length,
352
- dividerIndex
353
- );
354
- const replaceBlock = processedContent.substring(
355
- dividerIndex + DIVIDER.length,
356
- replaceEndIndex
357
- );
358
-
359
- if (searchBlock.trim() === "") {
360
- pageHtml = `${replaceBlock}\n${pageHtml}`;
361
- updatedLines.push([1, replaceBlock.split("\n").length]);
362
- } else {
363
- const blockPosition = pageHtml.indexOf(searchBlock);
364
- if (blockPosition !== -1) {
365
- const beforeText = pageHtml.substring(0, blockPosition);
366
- const startLineNumber = beforeText.split("\n").length;
367
- const replaceLines = replaceBlock.split("\n").length;
368
- const endLineNumber = startLineNumber + replaceLines - 1;
369
-
370
- updatedLines.push([startLineNumber, endLineNumber]);
371
- pageHtml = pageHtml.replace(searchBlock, replaceBlock);
372
- }
373
- }
374
-
375
- position = replaceEndIndex + REPLACE_END.length;
376
- }
377
-
378
- updatedPages[pageIndex].html = pageHtml;
379
-
380
- if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
381
- newHtml = pageHtml;
382
- }
383
- }
384
- }
385
-
386
- 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');
387
- let newPageMatch;
388
-
389
- while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
390
- const [, pagePath, pageContent] = newPageMatch;
391
-
392
- let pageHtml = pageContent;
393
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
394
- if (htmlMatch) {
395
- pageHtml = htmlMatch[1];
396
- }
397
-
398
- const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
399
-
400
- if (existingPageIndex !== -1) {
401
- updatedPages[existingPageIndex] = {
402
- path: pagePath,
403
- html: pageHtml.trim()
404
- };
405
- } else {
406
- updatedPages.push({
407
- path: pagePath,
408
- html: pageHtml.trim()
409
- });
410
- }
411
- }
412
-
413
- if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
414
- let position = 0;
415
- let moreBlocks = true;
416
-
417
- while (moreBlocks) {
418
- const searchStartIndex = chunk.indexOf(SEARCH_START, position);
419
- if (searchStartIndex === -1) {
420
- moreBlocks = false;
421
- continue;
422
- }
423
-
424
- const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
425
- if (dividerIndex === -1) {
426
- moreBlocks = false;
427
- continue;
428
- }
429
-
430
- const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
431
- if (replaceEndIndex === -1) {
432
- moreBlocks = false;
433
- continue;
434
- }
435
-
436
- const searchBlock = chunk.substring(
437
- searchStartIndex + SEARCH_START.length,
438
- dividerIndex
439
- );
440
- const replaceBlock = chunk.substring(
441
- dividerIndex + DIVIDER.length,
442
- replaceEndIndex
443
- );
444
-
445
- if (searchBlock.trim() === "") {
446
- newHtml = `${replaceBlock}\n${newHtml}`;
447
- updatedLines.push([1, replaceBlock.split("\n").length]);
448
- } else {
449
- const blockPosition = newHtml.indexOf(searchBlock);
450
- if (blockPosition !== -1) {
451
- const beforeText = newHtml.substring(0, blockPosition);
452
- const startLineNumber = beforeText.split("\n").length;
453
- const replaceLines = replaceBlock.split("\n").length;
454
- const endLineNumber = startLineNumber + replaceLines - 1;
455
-
456
- updatedLines.push([startLineNumber, endLineNumber]);
457
- newHtml = newHtml.replace(searchBlock, replaceBlock);
458
- }
459
- }
460
-
461
- position = replaceEndIndex + REPLACE_END.length;
462
- }
463
-
464
- // Update the main HTML if it's the index page
465
- const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
466
- if (mainPageIndex !== -1) {
467
- updatedPages[mainPageIndex].html = newHtml;
468
- }
469
- }
470
-
471
- return NextResponse.json({
472
- ok: true,
473
- updatedLines,
474
- pages: updatedPages,
475
- });
476
- } else {
477
- return NextResponse.json(
478
- { ok: false, message: "No content returned from the model" },
479
- { status: 400 }
480
- );
481
- }
482
- } catch (error: any) {
483
- if (error.message?.includes("exceeded your monthly included credits")) {
484
- return NextResponse.json(
485
- {
486
- ok: false,
487
- openProModal: true,
488
- message: error.message,
489
- },
490
- { status: 402 }
491
- );
492
- }
493
- return NextResponse.json(
494
- {
495
- ok: false,
496
- openSelectProvider: true,
497
- message:
498
- error.message || "An error occurred while processing your request.",
499
- },
500
- { status: 500 }
501
- );
502
- }
503
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/auth/route.ts DELETED
@@ -1,86 +0,0 @@
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]/images/route.ts DELETED
@@ -1,111 +0,0 @@
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
- // No longer need the ImageUpload interface since we're handling FormData with File objects
9
-
10
- export async function POST(
11
- req: NextRequest,
12
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
13
- ) {
14
- try {
15
- const user = await isAuthenticated();
16
-
17
- if (user instanceof NextResponse || !user) {
18
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
19
- }
20
-
21
- await dbConnect();
22
- const param = await params;
23
- const { namespace, repoId } = param;
24
-
25
- const project = await Project.findOne({
26
- user_id: user.id,
27
- space_id: `${namespace}/${repoId}`,
28
- }).lean();
29
-
30
- if (!project) {
31
- return NextResponse.json(
32
- {
33
- ok: false,
34
- error: "Project not found",
35
- },
36
- { status: 404 }
37
- );
38
- }
39
-
40
- // Parse the FormData to get the images
41
- const formData = await req.formData();
42
- const imageFiles = formData.getAll("images") as File[];
43
-
44
- if (!imageFiles || imageFiles.length === 0) {
45
- return NextResponse.json(
46
- {
47
- ok: false,
48
- error: "At least one image file is required under the 'images' key",
49
- },
50
- { status: 400 }
51
- );
52
- }
53
-
54
- const files: File[] = [];
55
- for (const file of imageFiles) {
56
- if (!(file instanceof File)) {
57
- return NextResponse.json(
58
- {
59
- ok: false,
60
- error: "Invalid file format - all items under 'images' key must be files",
61
- },
62
- { status: 400 }
63
- );
64
- }
65
-
66
- if (!file.type.startsWith('image/')) {
67
- return NextResponse.json(
68
- {
69
- ok: false,
70
- error: `File ${file.name} is not an image`,
71
- },
72
- { status: 400 }
73
- );
74
- }
75
-
76
- // Create File object with images/ folder prefix
77
- const fileName = `images/${file.name}`;
78
- const processedFile = new File([file], fileName, { type: file.type });
79
- files.push(processedFile);
80
- }
81
-
82
- // Upload files to HuggingFace space
83
- const repo: RepoDesignation = {
84
- type: "space",
85
- name: `${namespace}/${repoId}`,
86
- };
87
-
88
- await uploadFiles({
89
- repo,
90
- files,
91
- accessToken: user.token as string,
92
- commitTitle: `Upload ${files.length} image(s)`,
93
- });
94
-
95
- return NextResponse.json({
96
- ok: true,
97
- message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
98
- uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
99
- }, { status: 200 });
100
-
101
- } catch (error) {
102
- console.error('Error uploading images:', error);
103
- return NextResponse.json(
104
- {
105
- ok: false,
106
- error: "Failed to upload images",
107
- },
108
- { status: 500 }
109
- );
110
- }
111
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/route.ts DELETED
@@ -1,273 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, uploadFiles, listFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import Project from "@/models/Project";
6
- import dbConnect from "@/lib/mongodb";
7
- import { Page } from "@/types";
8
-
9
- export async function GET(
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
- if (!project) {
28
- return NextResponse.json(
29
- {
30
- ok: false,
31
- error: "Project not found",
32
- },
33
- { status: 404 }
34
- );
35
- }
36
- try {
37
- const space = await spaceInfo({
38
- name: namespace + "/" + repoId,
39
- accessToken: user.token as string,
40
- additionalFields: ["author"],
41
- });
42
-
43
- if (!space || space.sdk !== "static") {
44
- return NextResponse.json(
45
- {
46
- ok: false,
47
- error: "Space is not a static space",
48
- },
49
- { status: 404 }
50
- );
51
- }
52
- if (space.author !== user.name) {
53
- return NextResponse.json(
54
- {
55
- ok: false,
56
- error: "Space does not belong to the authenticated user",
57
- },
58
- { status: 403 }
59
- );
60
- }
61
-
62
- const repo: RepoDesignation = {
63
- type: "space",
64
- name: `${namespace}/${repoId}`,
65
- };
66
-
67
- const htmlFiles: Page[] = [];
68
- const images: string[] = [];
69
-
70
- const allowedImagesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
71
-
72
- for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
73
- if (fileInfo.path.endsWith(".html")) {
74
- const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`);
75
- if (res.ok) {
76
- const html = await res.text();
77
- if (fileInfo.path === "index.html") {
78
- htmlFiles.unshift({
79
- path: fileInfo.path,
80
- html,
81
- });
82
- } else {
83
- htmlFiles.push({
84
- path: fileInfo.path,
85
- html,
86
- });
87
- }
88
- }
89
- }
90
- if (fileInfo.type === "directory" && fileInfo.path === "images") {
91
- for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
92
- if (allowedImagesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
93
- images.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
94
- }
95
- }
96
- }
97
- }
98
-
99
- if (htmlFiles.length === 0) {
100
- return NextResponse.json(
101
- {
102
- ok: false,
103
- error: "No HTML files found",
104
- },
105
- { status: 404 }
106
- );
107
- }
108
-
109
- return NextResponse.json(
110
- {
111
- project: {
112
- ...project,
113
- pages: htmlFiles,
114
- images,
115
- },
116
- ok: true,
117
- },
118
- { status: 200 }
119
- );
120
-
121
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
- } catch (error: any) {
123
- if (error.statusCode === 404) {
124
- await Project.deleteOne({
125
- user_id: user.id,
126
- space_id: `${namespace}/${repoId}`,
127
- });
128
- return NextResponse.json(
129
- { error: "Space not found", ok: false },
130
- { status: 404 }
131
- );
132
- }
133
- return NextResponse.json(
134
- { error: error.message, ok: false },
135
- { status: 500 }
136
- );
137
- }
138
- }
139
-
140
- export async function PUT(
141
- req: NextRequest,
142
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
143
- ) {
144
- const user = await isAuthenticated();
145
-
146
- if (user instanceof NextResponse || !user) {
147
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
148
- }
149
-
150
- await dbConnect();
151
- const param = await params;
152
- const { namespace, repoId } = param;
153
- const { pages, prompts } = await req.json();
154
-
155
- const project = await Project.findOne({
156
- user_id: user.id,
157
- space_id: `${namespace}/${repoId}`,
158
- }).lean();
159
- if (!project) {
160
- return NextResponse.json(
161
- {
162
- ok: false,
163
- error: "Project not found",
164
- },
165
- { status: 404 }
166
- );
167
- }
168
-
169
- const repo: RepoDesignation = {
170
- type: "space",
171
- name: `${namespace}/${repoId}`,
172
- };
173
-
174
- const files: File[] = [];
175
- pages.forEach((page: Page) => {
176
- const file = new File([page.html], page.path, { type: "text/html" });
177
- files.push(file);
178
- });
179
- await uploadFiles({
180
- repo,
181
- files,
182
- accessToken: user.token as string,
183
- commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
184
- });
185
-
186
- await Project.updateOne(
187
- { user_id: user.id, space_id: `${namespace}/${repoId}` },
188
- {
189
- $set: {
190
- prompts: [
191
- ...(project && "prompts" in project ? project.prompts : []),
192
- ...prompts,
193
- ],
194
- },
195
- }
196
- );
197
- return NextResponse.json({ ok: true }, { status: 200 });
198
- }
199
-
200
- export async function POST(
201
- req: NextRequest,
202
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
203
- ) {
204
- const user = await isAuthenticated();
205
-
206
- if (user instanceof NextResponse || !user) {
207
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
208
- }
209
-
210
- await dbConnect();
211
- const param = await params;
212
- const { namespace, repoId } = param;
213
-
214
- const space = await spaceInfo({
215
- name: namespace + "/" + repoId,
216
- accessToken: user.token as string,
217
- additionalFields: ["author"],
218
- });
219
-
220
- if (!space || space.sdk !== "static") {
221
- return NextResponse.json(
222
- {
223
- ok: false,
224
- error: "Space is not a static space",
225
- },
226
- { status: 404 }
227
- );
228
- }
229
- if (space.author !== user.name) {
230
- return NextResponse.json(
231
- {
232
- ok: false,
233
- error: "Space does not belong to the authenticated user",
234
- },
235
- { status: 403 }
236
- );
237
- }
238
-
239
- const project = await Project.findOne({
240
- user_id: user.id,
241
- space_id: `${namespace}/${repoId}`,
242
- }).lean();
243
- if (project) {
244
- // redirect to the project page if it already exists
245
- return NextResponse.json(
246
- {
247
- ok: false,
248
- error: "Project already exists",
249
- redirect: `/projects/${namespace}/${repoId}`,
250
- },
251
- { status: 400 }
252
- );
253
- }
254
-
255
- const newProject = new Project({
256
- user_id: user.id,
257
- space_id: `${namespace}/${repoId}`,
258
- prompts: [],
259
- });
260
-
261
- await newProject.save();
262
- return NextResponse.json(
263
- {
264
- ok: true,
265
- project: {
266
- id: newProject._id,
267
- space_id: newProject.space_id,
268
- prompts: newProject.prompts,
269
- },
270
- },
271
- { status: 201 }
272
- );
273
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/route.ts DELETED
@@ -1,124 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import Project from "@/models/Project";
6
- import dbConnect from "@/lib/mongodb";
7
- import { COLORS } from "@/lib/utils";
8
- import { Page } from "@/types";
9
-
10
- export async function GET() {
11
- const user = await isAuthenticated();
12
-
13
- if (user instanceof NextResponse || !user) {
14
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
15
- }
16
-
17
- await dbConnect();
18
-
19
- const projects = await Project.find({
20
- user_id: user?.id,
21
- })
22
- .sort({ _createdAt: -1 })
23
- .limit(100)
24
- .lean();
25
- if (!projects) {
26
- return NextResponse.json(
27
- {
28
- ok: false,
29
- projects: [],
30
- },
31
- { status: 404 }
32
- );
33
- }
34
- return NextResponse.json(
35
- {
36
- ok: true,
37
- projects,
38
- },
39
- { status: 200 }
40
- );
41
- }
42
-
43
- export async function POST(request: NextRequest) {
44
- const user = await isAuthenticated();
45
-
46
- if (user instanceof NextResponse || !user) {
47
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
48
- }
49
-
50
- const { title, pages, prompts } = await request.json();
51
-
52
- if (!title || !pages || pages.length === 0) {
53
- return NextResponse.json(
54
- { message: "Title and HTML content are required.", ok: false },
55
- { status: 400 }
56
- );
57
- }
58
-
59
- await dbConnect();
60
-
61
- try {
62
- let readme = "";
63
-
64
- const newTitle = title
65
- .toLowerCase()
66
- .replace(/[^a-z0-9]+/g, "-")
67
- .split("-")
68
- .filter(Boolean)
69
- .join("-")
70
- .slice(0, 96);
71
-
72
- const repo: RepoDesignation = {
73
- type: "space",
74
- name: `${user.name}/${newTitle}`,
75
- };
76
-
77
- const { repoUrl } = await createRepo({
78
- repo,
79
- accessToken: user.token as string,
80
- });
81
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
82
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
83
- readme = `---
84
- title: ${newTitle}
85
- emoji: 🐳
86
- colorFrom: ${colorFrom}
87
- colorTo: ${colorTo}
88
- sdk: static
89
- pinned: false
90
- tags:
91
- - deepsite
92
- ---
93
-
94
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
95
-
96
- const readmeFile = new File([readme], "README.md", {
97
- type: "text/markdown",
98
- });
99
- const files = [readmeFile];
100
- pages.forEach((page: Page) => {
101
- const file = new File([page.html], page.path, { type: "text/html" });
102
- files.push(file);
103
- });
104
- await uploadFiles({
105
- repo,
106
- files,
107
- accessToken: user.token as string,
108
- commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
109
- });
110
- const path = repoUrl.split("/").slice(-2).join("/");
111
- const project = await Project.create({
112
- user_id: user.id,
113
- space_id: path,
114
- prompts,
115
- });
116
- return NextResponse.json({ project, path, ok: true }, { status: 201 });
117
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
- } catch (err: any) {
119
- return NextResponse.json(
120
- { error: err.message, ok: false },
121
- { status: 500 }
122
- );
123
- }
124
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/route.ts DELETED
@@ -1,25 +0,0 @@
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/re-design/route.ts DELETED
@@ -1,39 +0,0 @@
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 DELETED
@@ -1,72 +0,0 @@
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
- export default function AuthCallback({
9
- searchParams,
10
- }: {
11
- searchParams: Promise<{ code: string }>;
12
- }) {
13
- const [showButton, setShowButton] = useState(false);
14
- const { code } = use(searchParams);
15
- const { loginFromCode } = useUser();
16
-
17
- useMount(async () => {
18
- if (code) {
19
- await loginFromCode(code);
20
- }
21
- });
22
-
23
- useTimeoutFn(
24
- () => setShowButton(true),
25
- 7000 // Show button after 5 seconds
26
- );
27
-
28
- return (
29
- <div className="h-screen flex flex-col justify-center items-center">
30
- <div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
31
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
32
- <div className="flex items-center justify-center -space-x-4 mb-3">
33
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
34
- 🚀
35
- </div>
36
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
37
- 👋
38
- </div>
39
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
40
- 🙌
41
- </div>
42
- </div>
43
- <p className="text-xl font-semibold text-neutral-950">
44
- Login In Progress...
45
- </p>
46
- <p className="text-sm text-neutral-500 mt-1.5">
47
- Wait a moment while we log you in with your code.
48
- </p>
49
- </header>
50
- <main className="space-y-4 p-6">
51
- <div>
52
- <p className="text-sm text-neutral-700 mb-4 max-w-xs">
53
- If you are not redirected automatically in the next 5 seconds,
54
- please click the button below
55
- </p>
56
- {showButton ? (
57
- <Link href="/">
58
- <Button variant="black" className="relative">
59
- Go to Home
60
- </Button>
61
- </Link>
62
- ) : (
63
- <p className="text-xs text-neutral-500">
64
- Please wait, we are logging you in...
65
- </p>
66
- )}
67
- </div>
68
- </main>
69
- </div>
70
- </div>
71
- );
72
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/auth/page.tsx DELETED
@@ -1,28 +0,0 @@
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/favicon.ico DELETED
Binary file (25.9 kB)
 
app/layout.tsx DELETED
@@ -1,112 +0,0 @@
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
-
6
- import TanstackProvider from "@/components/providers/tanstack-query-provider";
7
- import "@/assets/globals.css";
8
- import { Toaster } from "@/components/ui/sonner";
9
- import MY_TOKEN_KEY from "@/lib/get-cookie-name";
10
- import { apiServer } from "@/lib/api";
11
- import AppContext from "@/components/contexts/app-context";
12
- import Script from "next/script";
13
- import IframeDetector from "@/components/iframe-detector";
14
-
15
- const inter = Inter({
16
- variable: "--font-inter-sans",
17
- subsets: ["latin"],
18
- });
19
-
20
- const ptSans = PT_Sans({
21
- variable: "--font-ptSans-mono",
22
- subsets: ["latin"],
23
- weight: ["400", "700"],
24
- });
25
-
26
- export const metadata: Metadata = {
27
- title: "DeepSite | Build with AI ✨",
28
- description:
29
- "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.",
30
- openGraph: {
31
- title: "DeepSite | Build with AI ✨",
32
- description:
33
- "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.",
34
- url: "https://deepsite.hf.co",
35
- siteName: "DeepSite",
36
- images: [
37
- {
38
- url: "https://deepsite.hf.co/banner.png",
39
- width: 1200,
40
- height: 630,
41
- alt: "DeepSite Open Graph Image",
42
- },
43
- ],
44
- },
45
- twitter: {
46
- card: "summary_large_image",
47
- title: "DeepSite | Build with AI ✨",
48
- description:
49
- "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.",
50
- images: ["https://deepsite.hf.co/banner.png"],
51
- },
52
- appleWebApp: {
53
- capable: true,
54
- title: "DeepSite",
55
- statusBarStyle: "black-translucent",
56
- },
57
- icons: {
58
- icon: "/logo.svg",
59
- shortcut: "/logo.svg",
60
- apple: "/logo.svg",
61
- },
62
- };
63
-
64
- export const viewport: Viewport = {
65
- initialScale: 1,
66
- maximumScale: 1,
67
- themeColor: "#000000",
68
- };
69
-
70
- async function getMe() {
71
- const cookieStore = await cookies();
72
- const token = cookieStore.get(MY_TOKEN_KEY())?.value;
73
- if (!token) return { user: null, errCode: null };
74
- try {
75
- const res = await apiServer.get("/me", {
76
- headers: {
77
- Authorization: `Bearer ${token}`,
78
- },
79
- });
80
- return { user: res.data.user, errCode: null };
81
- } catch (err: any) {
82
- return { user: null, errCode: err.status };
83
- }
84
- }
85
-
86
- // if domain isn't deepsite.hf.co or enzostvs-deepsite.hf.space redirect to deepsite.hf.co
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
- <TanstackProvider>
107
- <AppContext me={data}>{children}</AppContext>
108
- </TanstackProvider>
109
- </body>
110
- </html>
111
- );
112
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/projects/[namespace]/[repoId]/page.tsx DELETED
@@ -1,42 +0,0 @@
1
- import { cookies } from "next/headers";
2
- import { redirect } from "next/navigation";
3
-
4
- import { apiServer } from "@/lib/api";
5
- import MY_TOKEN_KEY from "@/lib/get-cookie-name";
6
- import { AppEditor } from "@/components/editor";
7
-
8
- async function getProject(namespace: string, repoId: string) {
9
- // TODO replace with a server action
10
- const cookieStore = await cookies();
11
- const token = cookieStore.get(MY_TOKEN_KEY())?.value;
12
- if (!token) return {};
13
- try {
14
- const { data } = await apiServer.get(
15
- `/me/projects/${namespace}/${repoId}`,
16
- {
17
- headers: {
18
- Authorization: `Bearer ${token}`,
19
- },
20
- }
21
- );
22
-
23
- return data.project;
24
- } catch {
25
- return {};
26
- }
27
- }
28
-
29
- export default async function ProjectNamespacePage({
30
- params,
31
- }: {
32
- params: Promise<{ namespace: string; repoId: string }>;
33
- }) {
34
- const { namespace, repoId } = await params;
35
- const data = await getProject(namespace, repoId);
36
- if (!data?.pages) {
37
- redirect("/projects");
38
- }
39
- return (
40
- <AppEditor project={data} pages={data.pages} images={data.images ?? []} />
41
- );
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/projects/new/page.tsx DELETED
@@ -1,5 +0,0 @@
1
- import { AppEditor } from "@/components/editor";
2
-
3
- export default function ProjectsNewPage() {
4
- return <AppEditor isNew />;
5
- }
 
 
 
 
 
 
assets/globals.css DELETED
@@ -1,146 +0,0 @@
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
- @layer base {
116
- * {
117
- @apply border-border outline-ring/50;
118
- }
119
- body {
120
- @apply bg-background text-foreground;
121
- }
122
- html {
123
- @apply scroll-smooth;
124
- }
125
- }
126
-
127
- .background__noisy {
128
- @apply bg-blend-normal pointer-events-none opacity-90;
129
- background-size: 25ww auto;
130
- background-image: url("/background_noisy.webp");
131
- @apply fixed w-screen h-screen -z-1 top-0 left-0;
132
- }
133
-
134
- .monaco-editor .margin {
135
- @apply !bg-neutral-900;
136
- }
137
- .monaco-editor .monaco-editor-background {
138
- @apply !bg-neutral-900;
139
- }
140
- .monaco-editor .line-numbers {
141
- @apply !text-neutral-500;
142
- }
143
-
144
- .matched-line {
145
- @apply bg-sky-500/30;
146
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/logo.svg DELETED
components.json DELETED
@@ -1,21 +0,0 @@
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": "app/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/contexts/app-context.tsx DELETED
@@ -1,57 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- "use client";
3
-
4
- import { useUser } from "@/hooks/useUser";
5
- import { usePathname, useRouter } from "next/navigation";
6
- import { useMount } from "react-use";
7
- import { UserContext } from "@/components/contexts/user-context";
8
- import { User } from "@/types";
9
- import { toast } from "sonner";
10
- import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
11
-
12
- export default function AppContext({
13
- children,
14
- me: initialData,
15
- }: {
16
- children: React.ReactNode;
17
- me?: {
18
- user: User | null;
19
- errCode: number | null;
20
- };
21
- }) {
22
- const { loginFromCode, user, logout, loading, errCode } =
23
- useUser(initialData);
24
- const pathname = usePathname();
25
- const router = useRouter();
26
-
27
- useMount(() => {
28
- if (!initialData?.user && !user) {
29
- if ([401, 403].includes(errCode as number)) {
30
- logout();
31
- } else if (pathname.includes("/spaces")) {
32
- if (errCode) {
33
- toast.error("An error occured while trying to log in");
34
- }
35
- // If we did not manage to log in (probs because api is down), we simply redirect to the home page
36
- router.push("/");
37
- }
38
- }
39
- });
40
-
41
- const events: any = {};
42
-
43
- useBroadcastChannel("auth", (message) => {
44
- if (pathname.includes("/auth/callback")) return;
45
-
46
- if (!message.code) return;
47
- if (message.type === "user-oauth" && message?.code && !events.code) {
48
- loginFromCode(message.code);
49
- }
50
- });
51
-
52
- return (
53
- <UserContext value={{ user, loading, logout } as any}>
54
- {children}
55
- </UserContext>
56
- );
57
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/contexts/user-context.tsx DELETED
@@ -1,8 +0,0 @@
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/follow-up-tooltip.tsx DELETED
@@ -1,36 +0,0 @@
1
- import {
2
- Popover,
3
- PopoverContent,
4
- PopoverTrigger,
5
- } from "@/components/ui/popover";
6
- import { Info } from "lucide-react";
7
-
8
- export const FollowUpTooltip = () => {
9
- return (
10
- <Popover>
11
- <PopoverTrigger asChild>
12
- <Info className="size-3 text-neutral-300 cursor-pointer" />
13
- </PopoverTrigger>
14
- <PopoverContent
15
- align="start"
16
- className="!rounded-2xl !p-0 min-w-xs text-center overflow-hidden"
17
- >
18
- <header className="bg-neutral-950 px-4 py-3 border-b border-neutral-700/70">
19
- <p className="text-base text-neutral-200 font-semibold">
20
- ⚡ Faster, Smarter Updates
21
- </p>
22
- </header>
23
- <main className="p-4">
24
- <p className="text-neutral-300 text-sm">
25
- Using the Diff-Patch system, allow DeepSite to intelligently update
26
- your project without rewritting the entire codebase.
27
- </p>
28
- <p className="text-neutral-500 text-sm mt-2">
29
- This means faster updates, less data usage, and a more efficient
30
- development process.
31
- </p>
32
- </main>
33
- </PopoverContent>
34
- </Popover>
35
- );
36
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/index.tsx DELETED
@@ -1,444 +0,0 @@
1
- "use client";
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { useState, useMemo, useRef } from "react";
4
- import classNames from "classnames";
5
- import { toast } from "sonner";
6
- import { useLocalStorage, useUpdateEffect } from "react-use";
7
- import { ArrowUp, ChevronDown, Crosshair } from "lucide-react";
8
- import { FaStopCircle } from "react-icons/fa";
9
-
10
- import ProModal from "@/components/pro-modal";
11
- import { Button } from "@/components/ui/button";
12
- import { MODELS } from "@/lib/providers";
13
- import { HtmlHistory, Page, Project } from "@/types";
14
- // import { InviteFriends } from "@/components/invite-friends";
15
- import { Settings } from "@/components/editor/ask-ai/settings";
16
- import { LoginModal } from "@/components/login-modal";
17
- import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
18
- import Loading from "@/components/loading";
19
- import { Checkbox } from "@/components/ui/checkbox";
20
- import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
21
- import { TooltipContent } from "@radix-ui/react-tooltip";
22
- import { SelectedHtmlElement } from "./selected-html-element";
23
- import { FollowUpTooltip } from "./follow-up-tooltip";
24
- import { isTheSameHtml } from "@/lib/compare-html-diff";
25
- import { useCallAi } from "@/hooks/useCallAi";
26
- import { SelectedFiles } from "./selected-files";
27
- import { Uploader } from "./uploader";
28
-
29
- export function AskAI({
30
- project,
31
- images,
32
- currentPage,
33
- previousPrompts,
34
- onScrollToBottom,
35
- isAiWorking,
36
- setisAiWorking,
37
- isEditableModeEnabled = false,
38
- pages,
39
- htmlHistory,
40
- selectedElement,
41
- setSelectedElement,
42
- selectedFiles,
43
- setSelectedFiles,
44
- setIsEditableModeEnabled,
45
- onNewPrompt,
46
- onSuccess,
47
- setPages,
48
- setCurrentPage,
49
- }: {
50
- project?: Project | null;
51
- currentPage: Page;
52
- images?: string[];
53
- pages: Page[];
54
- onScrollToBottom: () => void;
55
- previousPrompts: string[];
56
- isAiWorking: boolean;
57
- onNewPrompt: (prompt: string) => void;
58
- htmlHistory?: HtmlHistory[];
59
- setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
60
- isNew?: boolean;
61
- onSuccess: (page: Page[], p: string, n?: number[][]) => void;
62
- isEditableModeEnabled: boolean;
63
- setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
64
- selectedElement?: HTMLElement | null;
65
- setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
66
- selectedFiles: string[];
67
- setSelectedFiles: React.Dispatch<React.SetStateAction<string[]>>;
68
- setPages: React.Dispatch<React.SetStateAction<Page[]>>;
69
- setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
70
- }) {
71
- const refThink = useRef<HTMLDivElement | null>(null);
72
-
73
- const [open, setOpen] = useState(false);
74
- const [prompt, setPrompt] = useState("");
75
- const [previousPrompt, setPreviousPrompt] = useState("");
76
- const [provider, setProvider] = useLocalStorage("provider", "auto");
77
- const [model, setModel] = useLocalStorage("model", MODELS[0].value);
78
- const [openProvider, setOpenProvider] = useState(false);
79
- const [providerError, setProviderError] = useState("");
80
- const [openProModal, setOpenProModal] = useState(false);
81
- const [openThink, setOpenThink] = useState(false);
82
- const [isThinking, setIsThinking] = useState(true);
83
- const [think, setThink] = useState("");
84
- const [isFollowUp, setIsFollowUp] = useState(true);
85
- const [isUploading, setIsUploading] = useState(false);
86
- const [files, setFiles] = useState<string[]>(images ?? []);
87
-
88
- const {
89
- callAiNewProject,
90
- callAiFollowUp,
91
- callAiNewPage,
92
- stopController,
93
- audio: hookAudio,
94
- } = useCallAi({
95
- onNewPrompt,
96
- onSuccess,
97
- onScrollToBottom,
98
- setPages,
99
- setCurrentPage,
100
- currentPage,
101
- pages,
102
- isAiWorking,
103
- setisAiWorking,
104
- });
105
-
106
- const selectedModel = useMemo(() => {
107
- return MODELS.find((m: { value: string }) => m.value === model);
108
- }, [model]);
109
-
110
- const callAi = async (redesignMarkdown?: string) => {
111
- if (isAiWorking) return;
112
- if (!redesignMarkdown && !prompt.trim()) return;
113
-
114
- if (isFollowUp && !redesignMarkdown && !isSameHtml) {
115
- // Use follow-up function for existing projects
116
- const selectedElementHtml = selectedElement
117
- ? selectedElement.outerHTML
118
- : "";
119
-
120
- const result = await callAiFollowUp(
121
- prompt,
122
- model,
123
- provider,
124
- previousPrompt,
125
- selectedElementHtml,
126
- selectedFiles
127
- );
128
-
129
- if (result?.error) {
130
- handleError(result.error, result.message);
131
- return;
132
- }
133
-
134
- if (result?.success) {
135
- setPreviousPrompt(prompt);
136
- setPrompt("");
137
- }
138
- } else if (isFollowUp && pages.length > 1 && isSameHtml) {
139
- const result = await callAiNewPage(
140
- prompt,
141
- model,
142
- provider,
143
- currentPage.path,
144
- [
145
- ...(previousPrompts ?? []),
146
- ...(htmlHistory?.map((h) => h.prompt) ?? []),
147
- ]
148
- );
149
- if (result?.error) {
150
- handleError(result.error, result.message);
151
- return;
152
- }
153
-
154
- if (result?.success) {
155
- setPreviousPrompt(prompt);
156
- setPrompt("");
157
- }
158
- } else {
159
- const result = await callAiNewProject(
160
- prompt,
161
- model,
162
- provider,
163
- redesignMarkdown,
164
- handleThink,
165
- () => {
166
- setIsThinking(false);
167
- }
168
- );
169
-
170
- if (result?.error) {
171
- handleError(result.error, result.message);
172
- return;
173
- }
174
-
175
- if (result?.success) {
176
- setPreviousPrompt(prompt);
177
- setPrompt("");
178
- if (selectedModel?.isThinker) {
179
- setModel(MODELS[0].value);
180
- }
181
- }
182
- }
183
- };
184
-
185
- const handleThink = (think: string) => {
186
- setThink(think);
187
- setIsThinking(true);
188
- setOpenThink(true);
189
- };
190
-
191
- const handleError = (error: string, message?: string) => {
192
- switch (error) {
193
- case "login_required":
194
- setOpen(true);
195
- break;
196
- case "provider_required":
197
- setOpenProvider(true);
198
- setProviderError(message || "");
199
- break;
200
- case "pro_required":
201
- setOpenProModal(true);
202
- break;
203
- case "api_error":
204
- toast.error(message || "An error occurred");
205
- break;
206
- case "network_error":
207
- toast.error(message || "Network error occurred");
208
- break;
209
- default:
210
- toast.error("An unexpected error occurred");
211
- }
212
- };
213
-
214
- useUpdateEffect(() => {
215
- if (refThink.current) {
216
- refThink.current.scrollTop = refThink.current.scrollHeight;
217
- }
218
- }, [think]);
219
-
220
- useUpdateEffect(() => {
221
- if (!isThinking) {
222
- setOpenThink(false);
223
- }
224
- }, [isThinking]);
225
-
226
- const isSameHtml = useMemo(() => {
227
- return isTheSameHtml(currentPage.html);
228
- }, [currentPage.html]);
229
-
230
- return (
231
- <div className="px-3">
232
- <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-10 w-full group">
233
- {think && (
234
- <div className="w-full border-b border-neutral-700 relative overflow-hidden">
235
- <header
236
- className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
237
- onClick={() => {
238
- setOpenThink(!openThink);
239
- }}
240
- >
241
- <p className="text-sm font-medium text-neutral-300 group-hover:text-neutral-200 transition-colors duration-200">
242
- {isThinking ? "DeepSite is thinking..." : "DeepSite's plan"}
243
- </p>
244
- <ChevronDown
245
- className={classNames(
246
- "size-4 text-neutral-400 group-hover:text-neutral-300 transition-all duration-200",
247
- {
248
- "rotate-180": openThink,
249
- }
250
- )}
251
- />
252
- </header>
253
- <main
254
- ref={refThink}
255
- className={classNames(
256
- "overflow-y-auto transition-all duration-200 ease-in-out",
257
- {
258
- "max-h-[0px]": !openThink,
259
- "min-h-[250px] max-h-[250px] border-t border-neutral-700":
260
- openThink,
261
- }
262
- )}
263
- >
264
- <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
265
- {think}
266
- </p>
267
- </main>
268
- </div>
269
- )}
270
- <SelectedFiles
271
- files={selectedFiles}
272
- isAiWorking={isAiWorking}
273
- onDelete={(file) =>
274
- setSelectedFiles((prev) => prev.filter((f) => f !== file))
275
- }
276
- />
277
- {selectedElement && (
278
- <div className="px-4 pt-3">
279
- <SelectedHtmlElement
280
- element={selectedElement}
281
- isAiWorking={isAiWorking}
282
- onDelete={() => setSelectedElement(null)}
283
- />
284
- </div>
285
- )}
286
- <div className="w-full relative flex items-center justify-between">
287
- {(isAiWorking || isUploading) && (
288
- <div className="absolute bg-neutral-800 rounded-lg top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
289
- <div className="flex items-center justify-start gap-2">
290
- <Loading overlay={false} className="!size-4" />
291
- <p className="text-neutral-400 text-sm">
292
- {isUploading
293
- ? "Uploading images..."
294
- : `AI is ${isThinking ? "thinking" : "coding"}...`}
295
- </p>
296
- </div>
297
- {isAiWorking && (
298
- <div
299
- className="text-xs text-neutral-400 px-1 py-0.5 rounded-md border border-neutral-600 flex items-center justify-center gap-1.5 bg-neutral-800 hover:brightness-110 transition-all duration-200 cursor-pointer"
300
- onClick={stopController}
301
- >
302
- <FaStopCircle />
303
- Stop generation
304
- </div>
305
- )}
306
- </div>
307
- )}
308
- <textarea
309
- disabled={isAiWorking}
310
- className={classNames(
311
- "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
312
- {
313
- "!pt-2.5": selectedElement && !isAiWorking,
314
- }
315
- )}
316
- placeholder={
317
- selectedElement
318
- ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
319
- : isFollowUp && (!isSameHtml || pages?.length > 1)
320
- ? "Ask DeepSite for edits"
321
- : "Ask DeepSite anything..."
322
- }
323
- value={prompt}
324
- onChange={(e) => setPrompt(e.target.value)}
325
- onKeyDown={(e) => {
326
- if (e.key === "Enter" && !e.shiftKey) {
327
- callAi();
328
- }
329
- }}
330
- />
331
- </div>
332
- <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
333
- <div className="flex-1 flex items-center justify-start gap-1.5">
334
- <Uploader
335
- pages={pages}
336
- onLoading={setIsUploading}
337
- isLoading={isUploading}
338
- onFiles={setFiles}
339
- onSelectFile={(file) => {
340
- if (selectedFiles.includes(file)) {
341
- setSelectedFiles((prev) => prev.filter((f) => f !== file));
342
- } else {
343
- setSelectedFiles((prev) => [...prev, file]);
344
- }
345
- }}
346
- files={files}
347
- selectedFiles={selectedFiles}
348
- project={project}
349
- />
350
- <ReImagine onRedesign={(md) => callAi(md)} />
351
- {!isSameHtml && (
352
- <Tooltip>
353
- <TooltipTrigger asChild>
354
- <Button
355
- size="xs"
356
- variant={isEditableModeEnabled ? "default" : "outline"}
357
- onClick={() => {
358
- setIsEditableModeEnabled?.(!isEditableModeEnabled);
359
- }}
360
- className={classNames("h-[28px]", {
361
- "!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
362
- !isEditableModeEnabled,
363
- })}
364
- >
365
- <Crosshair className="size-4" />
366
- Edit
367
- </Button>
368
- </TooltipTrigger>
369
- <TooltipContent
370
- align="start"
371
- className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
372
- >
373
- Select an element on the page to ask DeepSite edit it
374
- directly.
375
- </TooltipContent>
376
- </Tooltip>
377
- )}
378
- {/* <InviteFriends /> */}
379
- </div>
380
- <div className="flex items-center justify-end gap-2">
381
- <Settings
382
- provider={provider as string}
383
- model={model as string}
384
- onChange={setProvider}
385
- onModelChange={setModel}
386
- open={openProvider}
387
- error={providerError}
388
- isFollowUp={!isSameHtml && isFollowUp}
389
- onClose={setOpenProvider}
390
- />
391
- <Button
392
- size="iconXs"
393
- disabled={isAiWorking || !prompt.trim()}
394
- onClick={() => callAi()}
395
- >
396
- <ArrowUp className="size-4" />
397
- </Button>
398
- </div>
399
- </div>
400
- <LoginModal open={open} onClose={() => setOpen(false)} pages={pages} />
401
- <ProModal
402
- pages={pages}
403
- open={openProModal}
404
- onClose={() => setOpenProModal(false)}
405
- />
406
- {pages.length === 1 && (
407
- <div className="border border-sky-500/20 bg-sky-500/40 hover:bg-sky-600 transition-all duration-200 text-sky-500 pl-2 pr-4 py-1.5 text-xs rounded-full absolute top-0 -translate-y-[calc(100%+8px)] left-0 max-w-max flex items-center justify-start gap-2">
408
- <span className="rounded-full text-[10px] font-semibold bg-white text-neutral-900 px-1.5 py-0.5">
409
- NEW
410
- </span>
411
- <p className="text-sm text-neutral-100">
412
- DeepSite can now create multiple pages at once. Try it!
413
- </p>
414
- </div>
415
- )}
416
- {!isSameHtml && (
417
- <div className="absolute top-0 right-0 -translate-y-[calc(100%+8px)] select-none text-xs text-neutral-400 flex items-center justify-center gap-2 bg-neutral-800 border border-neutral-700 rounded-md p-1 pr-2.5">
418
- <label
419
- htmlFor="diff-patch-checkbox"
420
- className="flex items-center gap-1.5 cursor-pointer"
421
- >
422
- <Checkbox
423
- id="diff-patch-checkbox"
424
- checked={isFollowUp}
425
- onCheckedChange={(e) => {
426
- if (e === true && !isSameHtml && selectedModel?.isThinker) {
427
- setModel(MODELS[0].value);
428
- }
429
- setIsFollowUp(e === true);
430
- }}
431
- />
432
- Diff-Patch Update
433
- </label>
434
- <FollowUpTooltip />
435
- </div>
436
- )}
437
- </div>
438
- <audio ref={hookAudio} id="audio" className="hidden">
439
- <source src="/success.mp3" type="audio/mpeg" />
440
- Your browser does not support the audio element.
441
- </audio>
442
- </div>
443
- );
444
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/re-imagine.tsx DELETED
@@ -1,146 +0,0 @@
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
-
15
- export function ReImagine({
16
- onRedesign,
17
- }: {
18
- onRedesign: (md: string) => void;
19
- }) {
20
- const [url, setUrl] = useState<string>("");
21
- const [open, setOpen] = useState(false);
22
- const [isLoading, setIsLoading] = useState(false);
23
-
24
- const checkIfUrlIsValid = (url: string) => {
25
- const urlPattern = new RegExp(
26
- /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,
27
- "i"
28
- );
29
- return urlPattern.test(url);
30
- };
31
-
32
- const handleClick = async () => {
33
- if (isLoading) return; // Prevent multiple clicks while loading
34
- if (!url) {
35
- toast.error("Please enter a URL.");
36
- return;
37
- }
38
- if (!checkIfUrlIsValid(url)) {
39
- toast.error("Please enter a valid URL.");
40
- return;
41
- }
42
- setIsLoading(true);
43
- const response = await api.put("/re-design", {
44
- url: url.trim(),
45
- });
46
- if (response?.data?.ok) {
47
- setOpen(false);
48
- setUrl("");
49
- onRedesign(response.data.markdown);
50
- toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
51
- } else {
52
- toast.error(response?.data?.error || "Failed to redesign the site.");
53
- }
54
- setIsLoading(false);
55
- };
56
-
57
- return (
58
- <Popover open={open} onOpenChange={setOpen}>
59
- <form>
60
- <PopoverTrigger asChild>
61
- <Button
62
- size="iconXs"
63
- variant="outline"
64
- className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
65
- >
66
- <Paintbrush className="size-4" />
67
- </Button>
68
- </PopoverTrigger>
69
- <PopoverContent
70
- align="start"
71
- className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
72
- >
73
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
74
- <div className="flex items-center justify-center -space-x-4 mb-3">
75
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
76
- 🎨
77
- </div>
78
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
79
- 🥳
80
- </div>
81
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
82
- 💎
83
- </div>
84
- </div>
85
- <p className="text-xl font-semibold text-neutral-950">
86
- Redesign your Site!
87
- </p>
88
- <p className="text-sm text-neutral-500 mt-1.5">
89
- Try our new Redesign feature to give your site a fresh look.
90
- </p>
91
- </header>
92
- <main className="space-y-4 p-6">
93
- <div>
94
- <p className="text-sm text-neutral-700 mb-2">
95
- Enter your website URL to get started:
96
- </p>
97
- <Input
98
- type="text"
99
- placeholder="https://example.com"
100
- value={url}
101
- onChange={(e) => setUrl(e.target.value)}
102
- onBlur={(e) => {
103
- const inputUrl = e.target.value.trim();
104
- if (!inputUrl) {
105
- setUrl("");
106
- return;
107
- }
108
- if (!checkIfUrlIsValid(inputUrl)) {
109
- toast.error("Please enter a valid URL.");
110
- return;
111
- }
112
- setUrl(inputUrl);
113
- }}
114
- className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
115
- />
116
- </div>
117
- <div>
118
- <p className="text-sm text-neutral-700 mb-2">
119
- Then, let&apos;s redesign it!
120
- </p>
121
- <Button
122
- variant="black"
123
- onClick={handleClick}
124
- className="relative w-full"
125
- >
126
- {isLoading ? (
127
- <>
128
- <Loading
129
- overlay={false}
130
- className="ml-2 size-4 animate-spin"
131
- />
132
- Fetching your site...
133
- </>
134
- ) : (
135
- <>
136
- Redesign <Paintbrush className="size-4" />
137
- </>
138
- )}
139
- </Button>
140
- </div>
141
- </main>
142
- </PopoverContent>
143
- </form>
144
- </Popover>
145
- );
146
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/selected-files.tsx DELETED
@@ -1,47 +0,0 @@
1
- import Image from "next/image";
2
-
3
- import { Button } from "@/components/ui/button";
4
- import { Minus } from "lucide-react";
5
-
6
- export const SelectedFiles = ({
7
- files,
8
- isAiWorking,
9
- onDelete,
10
- }: {
11
- files: string[];
12
- isAiWorking: boolean;
13
- onDelete: (file: string) => void;
14
- }) => {
15
- if (files.length === 0) return null;
16
- return (
17
- <div className="px-4 pt-3">
18
- <div className="flex items-center justify-start gap-2">
19
- {files.map((file) => (
20
- <div
21
- key={file}
22
- className="flex items-center relative justify-start gap-2 p-1 bg-neutral-700 rounded-md"
23
- >
24
- <Image
25
- src={file}
26
- alt="uploaded image"
27
- className="size-12 rounded-md object-cover"
28
- width={40}
29
- height={40}
30
- />
31
- <Button
32
- size="iconXsss"
33
- variant="secondary"
34
- className={`absolute top-0.5 right-0.5 ${
35
- isAiWorking ? "opacity-50 !cursor-not-allowed" : ""
36
- }`}
37
- disabled={isAiWorking}
38
- onClick={() => onDelete(file)}
39
- >
40
- <Minus className="size-4" />
41
- </Button>
42
- </div>
43
- ))}
44
- </div>
45
- </div>
46
- );
47
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/selected-html-element.tsx DELETED
@@ -1,57 +0,0 @@
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/settings.tsx DELETED
@@ -1,202 +0,0 @@
1
- import classNames from "classnames";
2
- import { PiGearSixFill } from "react-icons/pi";
3
- import { RiCheckboxCircleFill } from "react-icons/ri";
4
-
5
- import {
6
- Popover,
7
- PopoverContent,
8
- PopoverTrigger,
9
- } from "@/components/ui/popover";
10
- import { PROVIDERS, MODELS } from "@/lib/providers";
11
- import { Button } from "@/components/ui/button";
12
- import {
13
- Select,
14
- SelectContent,
15
- SelectGroup,
16
- SelectItem,
17
- SelectLabel,
18
- SelectTrigger,
19
- SelectValue,
20
- } from "@/components/ui/select";
21
- import { useMemo } from "react";
22
- import { useUpdateEffect } from "react-use";
23
- import Image from "next/image";
24
-
25
- export function Settings({
26
- open,
27
- onClose,
28
- provider,
29
- model,
30
- error,
31
- isFollowUp = false,
32
- onChange,
33
- onModelChange,
34
- }: {
35
- open: boolean;
36
- provider: string;
37
- model: string;
38
- error?: string;
39
- isFollowUp?: boolean;
40
- onClose: React.Dispatch<React.SetStateAction<boolean>>;
41
- onChange: (provider: string) => void;
42
- onModelChange: (model: string) => void;
43
- }) {
44
- const modelAvailableProviders = useMemo(() => {
45
- const availableProviders = MODELS.find(
46
- (m: { value: string }) => m.value === model
47
- )?.providers;
48
- if (!availableProviders) return Object.keys(PROVIDERS);
49
- return Object.keys(PROVIDERS).filter((id) =>
50
- availableProviders.includes(id)
51
- );
52
- }, [model]);
53
-
54
- useUpdateEffect(() => {
55
- if (provider !== "auto" && !modelAvailableProviders.includes(provider)) {
56
- onChange("auto");
57
- }
58
- }, [model, provider]);
59
-
60
- return (
61
- <div className="">
62
- <Popover open={open} onOpenChange={onClose}>
63
- <PopoverTrigger asChild>
64
- <Button variant="black" size="sm">
65
- <PiGearSixFill className="size-4" />
66
- Settings
67
- </Button>
68
- </PopoverTrigger>
69
- <PopoverContent
70
- className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
71
- align="center"
72
- >
73
- <header className="flex items-center justify-center text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
74
- Customize Settings
75
- </header>
76
- <main className="px-4 pt-5 pb-6 space-y-5">
77
- {error !== "" && (
78
- <p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
79
- {error}
80
- </p>
81
- )}
82
- <label className="block">
83
- <p className="text-neutral-300 text-sm mb-2.5">Choose a model</p>
84
- <Select defaultValue={model} onValueChange={onModelChange}>
85
- <SelectTrigger className="w-full">
86
- <SelectValue placeholder="Select a model" />
87
- </SelectTrigger>
88
- <SelectContent>
89
- <SelectGroup>
90
- <SelectLabel>Models</SelectLabel>
91
- {MODELS.map(
92
- ({
93
- value,
94
- label,
95
- isNew = false,
96
- isThinker = false,
97
- }: {
98
- value: string;
99
- label: string;
100
- isNew?: boolean;
101
- isThinker?: boolean;
102
- }) => (
103
- <SelectItem
104
- key={value}
105
- value={value}
106
- className=""
107
- disabled={isThinker && isFollowUp}
108
- >
109
- {label}
110
- {isNew && (
111
- <span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
112
- New
113
- </span>
114
- )}
115
- </SelectItem>
116
- )
117
- )}
118
- </SelectGroup>
119
- </SelectContent>
120
- </Select>
121
- </label>
122
- {isFollowUp && (
123
- <div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
124
- Note: You can&apos;t use a Thinker model for follow-up requests.
125
- We automatically switch to the default model for you.
126
- </div>
127
- )}
128
- <div className="flex flex-col gap-3">
129
- <div className="flex items-center justify-between">
130
- <div>
131
- <p className="text-neutral-300 text-sm mb-1.5">
132
- Use auto-provider
133
- </p>
134
- <p className="text-xs text-neutral-400/70">
135
- We&apos;ll automatically select the best provider for you
136
- based on your prompt.
137
- </p>
138
- </div>
139
- <div
140
- className={classNames(
141
- "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",
142
- {
143
- "!bg-sky-500": provider === "auto",
144
- }
145
- )}
146
- onClick={() => {
147
- const foundModel = MODELS.find(
148
- (m: { value: string }) => m.value === model
149
- );
150
- if (provider === "auto" && foundModel?.autoProvider) {
151
- onChange(foundModel.autoProvider);
152
- } else {
153
- onChange("auto");
154
- }
155
- }}
156
- >
157
- <div
158
- className={classNames(
159
- "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
160
- {
161
- "translate-x-4": provider === "auto",
162
- }
163
- )}
164
- />
165
- </div>
166
- </div>
167
- <label className="block">
168
- <p className="text-neutral-300 text-sm mb-2">
169
- Inference Provider
170
- </p>
171
- <div className="grid grid-cols-2 gap-1.5">
172
- {modelAvailableProviders.map((id: string) => (
173
- <Button
174
- key={id}
175
- variant={id === provider ? "default" : "secondary"}
176
- size="sm"
177
- onClick={() => {
178
- onChange(id);
179
- }}
180
- >
181
- <Image
182
- src={`/providers/${id}.svg`}
183
- alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
184
- className="size-5 mr-2"
185
- width={20}
186
- height={20}
187
- />
188
- {PROVIDERS[id as keyof typeof PROVIDERS].name}
189
- {id === provider && (
190
- <RiCheckboxCircleFill className="ml-2 size-4 text-blue-500" />
191
- )}
192
- </Button>
193
- ))}
194
- </div>
195
- </label>
196
- </div>
197
- </main>
198
- </PopoverContent>
199
- </Popover>
200
- </div>
201
- );
202
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/uploader.tsx DELETED
@@ -1,202 +0,0 @@
1
- import { useRef, useState } from "react";
2
- import { Images, Upload } from "lucide-react";
3
- import Image from "next/image";
4
-
5
- import {
6
- Popover,
7
- PopoverContent,
8
- PopoverTrigger,
9
- } from "@/components/ui/popover";
10
- import { Button } from "@/components/ui/button";
11
- import { Page, Project } from "@/types";
12
- import Loading from "@/components/loading";
13
- import { RiCheckboxCircleFill } from "react-icons/ri";
14
- import { useUser } from "@/hooks/useUser";
15
- import { LoginModal } from "@/components/login-modal";
16
- import { DeployButtonContent } from "../deploy-button/content";
17
-
18
- export const Uploader = ({
19
- pages,
20
- onLoading,
21
- isLoading,
22
- onFiles,
23
- onSelectFile,
24
- selectedFiles,
25
- files,
26
- project,
27
- }: {
28
- pages: Page[];
29
- onLoading: (isLoading: boolean) => void;
30
- isLoading: boolean;
31
- files: string[];
32
- onFiles: React.Dispatch<React.SetStateAction<string[]>>;
33
- onSelectFile: (file: string) => void;
34
- selectedFiles: string[];
35
- project?: Project | null;
36
- }) => {
37
- const { user } = useUser();
38
-
39
- const [open, setOpen] = useState(false);
40
- const fileInputRef = useRef<HTMLInputElement>(null);
41
-
42
- const uploadFiles = async (files: FileList | null) => {
43
- if (!files) return;
44
- if (!project) return;
45
-
46
- onLoading(true);
47
-
48
- const images = Array.from(files).filter((file) => {
49
- return file.type.startsWith("image/");
50
- });
51
-
52
- const data = new FormData();
53
- images.forEach((image) => {
54
- data.append("images", image);
55
- });
56
-
57
- const response = await fetch(
58
- `/api/me/projects/${project.space_id}/images`,
59
- {
60
- method: "POST",
61
- body: data,
62
- }
63
- );
64
- if (response.ok) {
65
- const data = await response.json();
66
- onFiles((prev) => [...prev, ...data.uploadedFiles]);
67
- }
68
- onLoading(false);
69
- };
70
-
71
- // TODO FIRST PUBLISH YOUR PROJECT TO UPLOAD IMAGES.
72
- return user?.id ? (
73
- <Popover open={open} onOpenChange={setOpen}>
74
- <form>
75
- <PopoverTrigger asChild>
76
- <Button
77
- size="iconXs"
78
- variant="outline"
79
- className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
80
- >
81
- <Images className="size-4" />
82
- </Button>
83
- </PopoverTrigger>
84
- <PopoverContent
85
- align="start"
86
- className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
87
- >
88
- {project?.space_id ? (
89
- <>
90
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
91
- <div className="flex items-center justify-center -space-x-4 mb-3">
92
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
93
- 🎨
94
- </div>
95
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
96
- 🖼️
97
- </div>
98
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
99
- 💻
100
- </div>
101
- </div>
102
- <p className="text-xl font-semibold text-neutral-950">
103
- Add Custom Images
104
- </p>
105
- <p className="text-sm text-neutral-500 mt-1.5">
106
- Upload images to your project and use them with DeepSite!
107
- </p>
108
- </header>
109
- <main className="space-y-4 p-5">
110
- <div>
111
- <p className="text-xs text-left text-neutral-700 mb-2">
112
- Uploaded Images
113
- </p>
114
- <div className="grid grid-cols-4 gap-1 flex-wrap max-h-40 overflow-y-auto">
115
- {files.map((file) => (
116
- <div
117
- key={file}
118
- className="select-none relative cursor-pointer bg-white rounded-md border-[2px] border-white hover:shadow-2xl transition-all duration-300"
119
- onClick={() => onSelectFile(file)}
120
- >
121
- <Image
122
- src={file}
123
- alt="uploaded image"
124
- width={56}
125
- height={56}
126
- className="object-cover w-full rounded-sm aspect-square"
127
- />
128
- {selectedFiles.includes(file) && (
129
- <div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
130
- <RiCheckboxCircleFill className="size-6 text-neutral-100" />
131
- </div>
132
- )}
133
- </div>
134
- ))}
135
- </div>
136
- </div>
137
- <div>
138
- <p className="text-xs text-left text-neutral-700 mb-2">
139
- Or import images from your computer
140
- </p>
141
- <Button
142
- variant="black"
143
- onClick={() => fileInputRef.current?.click()}
144
- className="relative w-full"
145
- >
146
- {isLoading ? (
147
- <>
148
- <Loading
149
- overlay={false}
150
- className="ml-2 size-4 animate-spin"
151
- />
152
- Uploading image(s)...
153
- </>
154
- ) : (
155
- <>
156
- <Upload className="size-4" />
157
- Upload Images
158
- </>
159
- )}
160
- </Button>
161
- <input
162
- ref={fileInputRef}
163
- type="file"
164
- className="hidden"
165
- multiple
166
- accept="image/*"
167
- onChange={(e) => uploadFiles(e.target.files)}
168
- />
169
- </div>
170
- </main>
171
- </>
172
- ) : (
173
- <DeployButtonContent
174
- pages={pages}
175
- prompts={[]}
176
- options={{
177
- description: "Publish your project first to add custom images.",
178
- }}
179
- />
180
- )}
181
- </PopoverContent>
182
- </form>
183
- </Popover>
184
- ) : (
185
- <>
186
- <Button
187
- size="iconXs"
188
- variant="outline"
189
- className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
190
- >
191
- <Images className="size-4" />
192
- </Button>
193
- <LoginModal
194
- open={open}
195
- onClose={() => setOpen(false)}
196
- pages={pages}
197
- title="Log In to add Custom Images"
198
- description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
199
- />
200
- </>
201
- );
202
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/deploy-button/content.tsx DELETED
@@ -1,111 +0,0 @@
1
- import { Rocket } from "lucide-react";
2
- import Image from "next/image";
3
-
4
- import Loading from "@/components/loading";
5
- import { Button } from "@/components/ui/button";
6
- import { Input } from "@/components/ui/input";
7
- import SpaceIcon from "@/assets/space.svg";
8
- import { Page } from "@/types";
9
- import { api } from "@/lib/api";
10
- import { toast } from "sonner";
11
- import { useState } from "react";
12
- import { useRouter } from "next/navigation";
13
-
14
- export const DeployButtonContent = ({
15
- pages,
16
- options,
17
- prompts,
18
- }: {
19
- pages: Page[];
20
- options?: {
21
- title?: string;
22
- description?: string;
23
- };
24
- prompts: string[];
25
- }) => {
26
- const router = useRouter();
27
- const [loading, setLoading] = useState(false);
28
-
29
- const [config, setConfig] = useState({
30
- title: "",
31
- });
32
-
33
- const createSpace = async () => {
34
- if (!config.title) {
35
- toast.error("Please enter a title for your space.");
36
- return;
37
- }
38
- setLoading(true);
39
-
40
- try {
41
- const res = await api.post("/me/projects", {
42
- title: config.title,
43
- pages,
44
- prompts,
45
- });
46
- if (res.data.ok) {
47
- router.push(`/projects/${res.data.path}?deploy=true`);
48
- } else {
49
- toast.error(res?.data?.error || "Failed to create space");
50
- }
51
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- } catch (err: any) {
53
- toast.error(err.response?.data?.error || err.message);
54
- } finally {
55
- setLoading(false);
56
- }
57
- };
58
-
59
- return (
60
- <>
61
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
62
- <div className="flex items-center justify-center -space-x-4 mb-3">
63
- <div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
64
- 🚀
65
- </div>
66
- <div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
67
- <Image src={SpaceIcon} alt="Space Icon" className="size-7" />
68
- </div>
69
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
70
- 👻
71
- </div>
72
- </div>
73
- <p className="text-xl font-semibold text-neutral-950">
74
- Publish as Space!
75
- </p>
76
- <p className="text-sm text-neutral-500 mt-1.5">
77
- {options?.description ??
78
- "Save and Publish your project to a Space on the Hub. Spaces are a way to share your project with the world."}
79
- </p>
80
- </header>
81
- <main className="space-y-4 p-6">
82
- <div>
83
- <p className="text-sm text-neutral-700 mb-2">
84
- Choose a title for your space:
85
- </p>
86
- <Input
87
- type="text"
88
- placeholder="My Awesome Website"
89
- value={config.title}
90
- onChange={(e) => setConfig({ ...config, title: e.target.value })}
91
- className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
92
- />
93
- </div>
94
- <div>
95
- <p className="text-sm text-neutral-700 mb-2">
96
- Then, let&apos;s publish it!
97
- </p>
98
- <Button
99
- variant="black"
100
- onClick={createSpace}
101
- className="relative w-full"
102
- disabled={loading}
103
- >
104
- Publish Space <Rocket className="size-4" />
105
- {loading && <Loading className="ml-2 size-4 animate-spin" />}
106
- </Button>
107
- </div>
108
- </main>
109
- </>
110
- );
111
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/deploy-button/index.tsx DELETED
@@ -1,79 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { useState } from "react";
3
- import { MdSave } from "react-icons/md";
4
-
5
- import { Button } from "@/components/ui/button";
6
- import {
7
- Popover,
8
- PopoverContent,
9
- PopoverTrigger,
10
- } from "@/components/ui/popover";
11
- import { LoginModal } from "@/components/login-modal";
12
- import { useUser } from "@/hooks/useUser";
13
- import { Page } from "@/types";
14
- import { DeployButtonContent } from "./content";
15
-
16
- export function DeployButton({
17
- pages,
18
- prompts,
19
- }: {
20
- pages: Page[];
21
- prompts: string[];
22
- }) {
23
- const { user } = useUser();
24
- const [open, setOpen] = useState(false);
25
-
26
- return (
27
- <div className="flex items-center justify-end gap-5">
28
- <div className="relative flex items-center justify-end">
29
- {user?.id ? (
30
- <Popover>
31
- <PopoverTrigger asChild>
32
- <div>
33
- <Button variant="default" className="max-lg:hidden !px-4">
34
- <MdSave className="size-4" />
35
- Publish your Project
36
- </Button>
37
- <Button variant="default" size="sm" className="lg:hidden">
38
- Publish
39
- </Button>
40
- </div>
41
- </PopoverTrigger>
42
- <PopoverContent
43
- className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
44
- align="end"
45
- >
46
- <DeployButtonContent pages={pages} prompts={prompts} />
47
- </PopoverContent>
48
- </Popover>
49
- ) : (
50
- <>
51
- <Button
52
- variant="default"
53
- className="max-lg:hidden !px-4"
54
- onClick={() => setOpen(true)}
55
- >
56
- <MdSave className="size-4" />
57
- Publish your Project
58
- </Button>
59
- <Button
60
- variant="default"
61
- size="sm"
62
- className="lg:hidden"
63
- onClick={() => setOpen(true)}
64
- >
65
- Publish
66
- </Button>
67
- </>
68
- )}
69
- <LoginModal
70
- open={open}
71
- onClose={() => setOpen(false)}
72
- pages={pages}
73
- title="Log In to publish your Project"
74
- description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
75
- />
76
- </div>
77
- </div>
78
- );
79
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/footer/index.tsx DELETED
@@ -1,128 +0,0 @@
1
- import classNames from "classnames";
2
- import { FaMobileAlt } from "react-icons/fa";
3
- import { HelpCircle, RefreshCcw, SparkleIcon } from "lucide-react";
4
- import { FaLaptopCode } from "react-icons/fa6";
5
- import { HtmlHistory, Page } from "@/types";
6
- import { Button } from "@/components/ui/button";
7
- import { MdAdd } from "react-icons/md";
8
- import { History } from "@/components/editor/history";
9
- import { UserMenu } from "@/components/user-menu";
10
- import { useUser } from "@/hooks/useUser";
11
- import Link from "next/link";
12
-
13
- const DEVICES = [
14
- {
15
- name: "desktop",
16
- icon: FaLaptopCode,
17
- },
18
- {
19
- name: "mobile",
20
- icon: FaMobileAlt,
21
- },
22
- ];
23
-
24
- export function Footer({
25
- htmlHistory,
26
- setPages,
27
- device,
28
- setDevice,
29
- iframeRef,
30
- }: {
31
- htmlHistory?: HtmlHistory[];
32
- device: "desktop" | "mobile";
33
- setPages: (pages: Page[]) => void;
34
- iframeRef?: React.RefObject<HTMLIFrameElement | null>;
35
- setDevice: React.Dispatch<React.SetStateAction<"desktop" | "mobile">>;
36
- }) {
37
- const { user } = useUser();
38
-
39
- const handleRefreshIframe = () => {
40
- if (iframeRef?.current) {
41
- const iframe = iframeRef.current;
42
- const content = iframe.srcdoc;
43
- iframe.srcdoc = "";
44
- setTimeout(() => {
45
- iframe.srcdoc = content;
46
- }, 10);
47
- }
48
- };
49
-
50
- return (
51
- <footer className="border-t bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 py-2 flex items-center justify-between sticky bottom-0 z-20">
52
- <div className="flex items-center gap-2">
53
- {user &&
54
- (user?.isLocalUse ? (
55
- <>
56
- <div className="max-w-max bg-amber-500/10 rounded-full px-3 py-1 text-amber-500 border border-amber-500/20 text-sm font-semibold">
57
- Local Usage
58
- </div>
59
- </>
60
- ) : (
61
- <UserMenu className="!p-1 !pr-3 !h-auto" />
62
- ))}
63
- {user && <p className="text-neutral-700">|</p>}
64
- <Link href="/projects/new">
65
- <Button size="sm" variant="secondary">
66
- <MdAdd className="text-sm" />
67
- New <span className="max-lg:hidden">Project</span>
68
- </Button>
69
- </Link>
70
- {htmlHistory && htmlHistory.length > 0 && (
71
- <>
72
- <p className="text-neutral-700">|</p>
73
- <History history={htmlHistory} setPages={setPages} />
74
- </>
75
- )}
76
- </div>
77
- <div className="flex justify-end items-center gap-2.5">
78
- <a
79
- href="https://huggingface.co/spaces/victor/deepsite-gallery"
80
- target="_blank"
81
- >
82
- <Button size="sm" variant="ghost">
83
- <SparkleIcon className="size-3.5" />
84
- <span className="max-lg:hidden">DeepSite Gallery</span>
85
- </Button>
86
- </a>
87
- <a
88
- target="_blank"
89
- href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/157"
90
- >
91
- <Button size="sm" variant="outline">
92
- <HelpCircle className="size-3.5" />
93
- <span className="max-lg:hidden">Help</span>
94
- </Button>
95
- </a>
96
- <Button size="sm" variant="outline" onClick={handleRefreshIframe}>
97
- <RefreshCcw className="size-3.5" />
98
- <span className="max-lg:hidden">Refresh Preview</span>
99
- </Button>
100
- <div className="flex items-center rounded-full p-0.5 bg-neutral-700/70 relative overflow-hidden z-0 max-lg:hidden gap-0.5">
101
- <div
102
- className={classNames(
103
- "absolute left-0.5 top-0.5 rounded-full bg-white size-7 -z-[1] transition-all duration-200",
104
- {
105
- "translate-x-[calc(100%+2px)]": device === "mobile",
106
- }
107
- )}
108
- />
109
- {DEVICES.map((deviceItem) => (
110
- <button
111
- key={deviceItem.name}
112
- className={classNames(
113
- "rounded-full text-neutral-300 size-7 flex items-center justify-center cursor-pointer",
114
- {
115
- "!text-black": device === deviceItem.name,
116
- "hover:bg-neutral-800": device !== deviceItem.name,
117
- }
118
- )}
119
- onClick={() => setDevice(deviceItem.name as "desktop" | "mobile")}
120
- >
121
- <deviceItem.icon className="text-sm" />
122
- </button>
123
- ))}
124
- </div>
125
- </div>
126
- </footer>
127
- );
128
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/header/index.tsx DELETED
@@ -1,69 +0,0 @@
1
- import { ReactNode } from "react";
2
- import { Eye, MessageCircleCode } from "lucide-react";
3
-
4
- import Logo from "@/assets/logo.svg";
5
-
6
- import { Button } from "@/components/ui/button";
7
- import classNames from "classnames";
8
- import Image from "next/image";
9
-
10
- const TABS = [
11
- {
12
- value: "chat",
13
- label: "Chat",
14
- icon: MessageCircleCode,
15
- },
16
- {
17
- value: "preview",
18
- label: "Preview",
19
- icon: Eye,
20
- },
21
- ];
22
-
23
- export function Header({
24
- tab,
25
- onNewTab,
26
- children,
27
- }: {
28
- tab: string;
29
- onNewTab: (tab: string) => void;
30
- children?: ReactNode;
31
- }) {
32
- return (
33
- <header className="border-b bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 lg:px-6 py-2 flex items-center max-lg:gap-3 justify-between lg:grid lg:grid-cols-3 z-20">
34
- <div className="flex items-center justify-start gap-3">
35
- <h1 className="text-neutral-900 dark:text-white text-lg lg:text-xl font-bold flex items-center justify-start">
36
- <Image
37
- src={Logo}
38
- alt="DeepSite Logo"
39
- className="size-6 lg:size-8 mr-2 invert-100 dark:invert-0"
40
- />
41
- <p className="max-md:hidden flex items-center justify-start">
42
- DeepSite
43
- <span className="font-mono bg-gradient-to-br from-sky-500 to-emerald-500 text-neutral-950 rounded-full text-xs ml-2 px-1.5 py-0.5">
44
- {" "}
45
- v2
46
- </span>
47
- </p>
48
- </h1>
49
- </div>
50
- <div className="flex items-center justify-start lg:justify-center gap-1 max-lg:pl-3 flex-1 max-lg:border-l max-lg:border-l-neutral-800">
51
- {TABS.map((item) => (
52
- <Button
53
- key={item.value}
54
- variant={tab === item.value ? "secondary" : "ghost"}
55
- className={classNames("", {
56
- "opacity-60": tab !== item.value,
57
- })}
58
- size="sm"
59
- onClick={() => onNewTab(item.value)}
60
- >
61
- <item.icon className="size-4" />
62
- <span className="hidden md:inline">{item.label}</span>
63
- </Button>
64
- ))}
65
- </div>
66
- <div className="flex items-center justify-end gap-3">{children}</div>
67
- </header>
68
- );
69
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/history/index.tsx DELETED
@@ -1,73 +0,0 @@
1
- import { History as HistoryIcon } from "lucide-react";
2
- import { HtmlHistory, Page } from "@/types";
3
- import {
4
- Popover,
5
- PopoverContent,
6
- PopoverTrigger,
7
- } from "@/components/ui/popover";
8
- import { Button } from "@/components/ui/button";
9
-
10
- export function History({
11
- history,
12
- setPages,
13
- }: {
14
- history: HtmlHistory[];
15
- setPages: (pages: Page[]) => void;
16
- }) {
17
- return (
18
- <Popover>
19
- <PopoverTrigger asChild>
20
- <Button variant="ghost" size="sm" className="max-lg:hidden">
21
- <HistoryIcon className="size-4 text-neutral-300" />
22
- {history?.length} edit{history.length !== 1 ? "s" : ""}
23
- </Button>
24
- </PopoverTrigger>
25
- <PopoverContent
26
- className="!rounded-2xl !p-0 overflow-hidden !bg-neutral-900"
27
- align="start"
28
- >
29
- <header className="text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
30
- History
31
- </header>
32
- <main className="px-4 space-y-3">
33
- <ul className="max-h-[250px] overflow-y-auto">
34
- {history?.map((item, index) => (
35
- <li
36
- key={index}
37
- className="text-gray-300 text-xs py-2 border-b border-gray-800 last:border-0 flex items-center justify-between gap-2"
38
- >
39
- <div className="">
40
- <span className="line-clamp-1">{item.prompt}</span>
41
- <span className="text-gray-500 text-[10px]">
42
- {new Date(item.createdAt).toLocaleDateString("en-US", {
43
- month: "2-digit",
44
- day: "2-digit",
45
- year: "2-digit",
46
- }) +
47
- " " +
48
- new Date(item.createdAt).toLocaleTimeString("en-US", {
49
- hour: "2-digit",
50
- minute: "2-digit",
51
- second: "2-digit",
52
- hour12: false,
53
- })}
54
- </span>
55
- </div>
56
- <Button
57
- variant="sky"
58
- size="xs"
59
- onClick={() => {
60
- console.log(item);
61
- setPages(item.pages);
62
- }}
63
- >
64
- Select
65
- </Button>
66
- </li>
67
- ))}
68
- </ul>
69
- </main>
70
- </PopoverContent>
71
- </Popover>
72
- );
73
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/index.tsx DELETED
@@ -1,386 +0,0 @@
1
- "use client";
2
- import { useMemo, useRef, useState } from "react";
3
- import { toast } from "sonner";
4
- import { editor } from "monaco-editor";
5
- import Editor from "@monaco-editor/react";
6
- import { CopyIcon } from "lucide-react";
7
- import {
8
- useCopyToClipboard,
9
- useEvent,
10
- useLocalStorage,
11
- useMount,
12
- useUnmount,
13
- useUpdateEffect,
14
- } from "react-use";
15
- import classNames from "classnames";
16
- import { useRouter, useSearchParams } from "next/navigation";
17
-
18
- import { Header } from "@/components/editor/header";
19
- import { Footer } from "@/components/editor/footer";
20
- import { defaultHTML } from "@/lib/consts";
21
- import { Preview } from "@/components/editor/preview";
22
- import { useEditor } from "@/hooks/useEditor";
23
- import { AskAI } from "@/components/editor/ask-ai";
24
- import { DeployButton } from "./deploy-button";
25
- import { Page, Project } from "@/types";
26
- import { SaveButton } from "./save-button";
27
- import { LoadProject } from "../my-projects/load-project";
28
- import { isTheSameHtml } from "@/lib/compare-html-diff";
29
- import { ListPages } from "./pages";
30
-
31
- export const AppEditor = ({
32
- project,
33
- pages: initialPages,
34
- images,
35
- isNew,
36
- }: {
37
- project?: Project | null;
38
- pages?: Page[];
39
- images?: string[];
40
- isNew?: boolean;
41
- }) => {
42
- const [htmlStorage, , removeHtmlStorage] = useLocalStorage("pages");
43
- const [, copyToClipboard] = useCopyToClipboard();
44
- const { htmlHistory, setHtmlHistory, prompts, setPrompts, pages, setPages } =
45
- useEditor(initialPages);
46
-
47
- const searchParams = useSearchParams();
48
- const router = useRouter();
49
- const deploy = searchParams.get("deploy") === "true";
50
-
51
- const iframeRef = useRef<HTMLIFrameElement | null>(null);
52
- const preview = useRef<HTMLDivElement>(null);
53
- const editor = useRef<HTMLDivElement>(null);
54
- const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
55
- const resizer = useRef<HTMLDivElement>(null);
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
- const monacoRef = useRef<any>(null);
58
-
59
- const [currentTab, setCurrentTab] = useState("chat");
60
- const [currentPage, setCurrentPage] = useState("index.html");
61
- const [device, setDevice] = useState<"desktop" | "mobile">("desktop");
62
- const [isResizing, setIsResizing] = useState(false);
63
- const [isAiWorking, setIsAiWorking] = useState(false);
64
- const [isEditableModeEnabled, setIsEditableModeEnabled] = useState(false);
65
- const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
66
- null
67
- );
68
- const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
69
-
70
- const resetLayout = () => {
71
- if (!editor.current || !preview.current) return;
72
-
73
- // lg breakpoint is 1024px based on useBreakpoint definition and Tailwind defaults
74
- if (window.innerWidth >= 1024) {
75
- // Set initial 1/3 - 2/3 sizes for large screens, accounting for resizer width
76
- const resizerWidth = resizer.current?.offsetWidth ?? 8; // w-2 = 0.5rem = 8px
77
- const availableWidth = window.innerWidth - resizerWidth;
78
- const initialEditorWidth = availableWidth / 3; // Editor takes 1/3 of space
79
- const initialPreviewWidth = availableWidth - initialEditorWidth; // Preview takes 2/3
80
- editor.current.style.width = `${initialEditorWidth}px`;
81
- preview.current.style.width = `${initialPreviewWidth}px`;
82
- } else {
83
- // Remove inline styles for smaller screens, let CSS flex-col handle it
84
- editor.current.style.width = "";
85
- preview.current.style.width = "";
86
- }
87
- };
88
-
89
- const handleResize = (e: MouseEvent) => {
90
- if (!editor.current || !preview.current || !resizer.current) return;
91
-
92
- const resizerWidth = resizer.current.offsetWidth;
93
- const minWidth = 100; // Minimum width for editor/preview
94
- const maxWidth = window.innerWidth - resizerWidth - minWidth;
95
-
96
- const editorWidth = e.clientX;
97
- const clampedEditorWidth = Math.max(
98
- minWidth,
99
- Math.min(editorWidth, maxWidth)
100
- );
101
- const calculatedPreviewWidth =
102
- window.innerWidth - clampedEditorWidth - resizerWidth;
103
-
104
- editor.current.style.width = `${clampedEditorWidth}px`;
105
- preview.current.style.width = `${calculatedPreviewWidth}px`;
106
- };
107
-
108
- const handleMouseDown = () => {
109
- setIsResizing(true);
110
- document.addEventListener("mousemove", handleResize);
111
- document.addEventListener("mouseup", handleMouseUp);
112
- };
113
-
114
- const handleMouseUp = () => {
115
- setIsResizing(false);
116
- document.removeEventListener("mousemove", handleResize);
117
- document.removeEventListener("mouseup", handleMouseUp);
118
- };
119
-
120
- useMount(() => {
121
- if (deploy && project?._id) {
122
- toast.success("Your project is deployed! 🎉", {
123
- action: {
124
- label: "See Project",
125
- onClick: () => {
126
- window.open(
127
- `https://huggingface.co/spaces/${project?.space_id}`,
128
- "_blank"
129
- );
130
- },
131
- },
132
- });
133
- router.replace(`/projects/${project?.space_id}`);
134
- }
135
- if (htmlStorage) {
136
- removeHtmlStorage();
137
- toast.warning("Previous HTML content restored from local storage.");
138
- }
139
-
140
- resetLayout();
141
- if (!resizer.current) return;
142
- resizer.current.addEventListener("mousedown", handleMouseDown);
143
- window.addEventListener("resize", resetLayout);
144
- });
145
- useUnmount(() => {
146
- document.removeEventListener("mousemove", handleResize);
147
- document.removeEventListener("mouseup", handleMouseUp);
148
- if (resizer.current) {
149
- resizer.current.removeEventListener("mousedown", handleMouseDown);
150
- }
151
- window.removeEventListener("resize", resetLayout);
152
- });
153
-
154
- // Prevent accidental navigation away when AI is working or content has changed
155
- useEvent("beforeunload", (e) => {
156
- if (isAiWorking || !isTheSameHtml(currentPageData?.html)) {
157
- e.preventDefault();
158
- return "";
159
- }
160
- });
161
-
162
- useUpdateEffect(() => {
163
- if (currentTab === "chat") {
164
- // Reset editor width when switching to reasoning tab
165
- resetLayout();
166
- // re-add the event listener for resizing
167
- if (resizer.current) {
168
- resizer.current.addEventListener("mousedown", handleMouseDown);
169
- }
170
- } else {
171
- if (preview.current) {
172
- // Reset preview width when switching to preview tab
173
- preview.current.style.width = "100%";
174
- }
175
- }
176
- }, [currentTab]);
177
-
178
- const handleEditorValidation = (markers: editor.IMarker[]) => {
179
- console.log("Editor validation markers:", markers);
180
- };
181
-
182
- const currentPageData = useMemo(() => {
183
- return (
184
- pages.find((page) => page.path === currentPage) ?? {
185
- path: "index.html",
186
- html: defaultHTML,
187
- }
188
- );
189
- }, [pages, currentPage]);
190
-
191
- return (
192
- <section className="h-[100dvh] bg-neutral-950 flex flex-col">
193
- <Header tab={currentTab} onNewTab={setCurrentTab}>
194
- <LoadProject
195
- onSuccess={(project: Project) => {
196
- router.push(`/projects/${project.space_id}`);
197
- }}
198
- />
199
- {/* for these buttons pass the whole pages */}
200
- {project?._id ? (
201
- <SaveButton pages={pages} prompts={prompts} />
202
- ) : (
203
- <DeployButton pages={pages} prompts={prompts} />
204
- )}
205
- </Header>
206
- <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full max-lg:h-[calc(100%-82px)] relative">
207
- {currentTab === "chat" && (
208
- <>
209
- <div
210
- ref={editor}
211
- className="bg-neutral-900 relative flex-1 overflow-hidden h-full flex flex-col gap-2 pb-3"
212
- >
213
- <ListPages
214
- pages={pages}
215
- currentPage={currentPage}
216
- onSelectPage={(path, newPath) => {
217
- if (newPath) {
218
- setPages((prev) =>
219
- prev.map((page) =>
220
- page.path === path ? { ...page, path: newPath } : page
221
- )
222
- );
223
- setCurrentPage(newPath);
224
- } else {
225
- setCurrentPage(path);
226
- }
227
- }}
228
- onDeletePage={(path) => {
229
- const newPages = pages.filter((page) => page.path !== path);
230
- setPages(newPages);
231
- if (currentPage === path) {
232
- setCurrentPage(newPages[0]?.path ?? "index.html");
233
- }
234
- }}
235
- onNewPage={() => {
236
- setPages((prev) => [
237
- ...prev,
238
- {
239
- path: `page-${prev.length + 1}.html`,
240
- html: defaultHTML,
241
- },
242
- ]);
243
- setCurrentPage(`page-${pages.length + 1}.html`);
244
- }}
245
- />
246
- <CopyIcon
247
- className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
248
- onClick={() => {
249
- copyToClipboard(currentPageData.html);
250
- toast.success("HTML copied to clipboard!");
251
- }}
252
- />
253
- <Editor
254
- defaultLanguage="html"
255
- theme="vs-dark"
256
- className={classNames(
257
- "h-full bg-neutral-900 transition-all duration-200 absolute left-0 top-0",
258
- {
259
- "pointer-events-none": isAiWorking,
260
- }
261
- )}
262
- options={{
263
- colorDecorators: true,
264
- fontLigatures: true,
265
- theme: "vs-dark",
266
- minimap: { enabled: false },
267
- scrollbar: {
268
- horizontal: "hidden",
269
- },
270
- wordWrap: "on",
271
- }}
272
- value={currentPageData.html}
273
- onChange={(value) => {
274
- const newValue = value ?? "";
275
- // setHtml(newValue);
276
- setPages((prev) =>
277
- prev.map((page) =>
278
- page.path === currentPageData.path
279
- ? { ...page, html: newValue }
280
- : page
281
- )
282
- );
283
- }}
284
- onMount={(editor, monaco) => {
285
- editorRef.current = editor;
286
- monacoRef.current = monaco;
287
- }}
288
- onValidate={handleEditorValidation}
289
- />
290
- <AskAI
291
- project={project}
292
- images={images}
293
- currentPage={currentPageData}
294
- htmlHistory={htmlHistory}
295
- previousPrompts={project?.prompts ?? []}
296
- onSuccess={(newPages, p: string) => {
297
- const currentHistory = [...htmlHistory];
298
- currentHistory.unshift({
299
- pages: newPages,
300
- createdAt: new Date(),
301
- prompt: p,
302
- });
303
- setHtmlHistory(currentHistory);
304
- setSelectedElement(null);
305
- setSelectedFiles([]);
306
- // if xs or sm
307
- if (window.innerWidth <= 1024) {
308
- setCurrentTab("preview");
309
- }
310
- // if (updatedLines && updatedLines?.length > 0) {
311
- // const decorations = updatedLines.map((line) => ({
312
- // range: new monacoRef.current.Range(
313
- // line[0],
314
- // 1,
315
- // line[1],
316
- // 1
317
- // ),
318
- // options: {
319
- // inlineClassName: "matched-line",
320
- // },
321
- // }));
322
- // setTimeout(() => {
323
- // editorRef?.current
324
- // ?.getModel()
325
- // ?.deltaDecorations([], decorations);
326
-
327
- // editorRef.current?.revealLine(updatedLines[0][0]);
328
- // }, 100);
329
- // }
330
- }}
331
- setPages={setPages}
332
- pages={pages}
333
- setCurrentPage={setCurrentPage}
334
- isAiWorking={isAiWorking}
335
- setisAiWorking={setIsAiWorking}
336
- onNewPrompt={(prompt: string) => {
337
- setPrompts((prev) => [...prev, prompt]);
338
- }}
339
- onScrollToBottom={() => {
340
- editorRef.current?.revealLine(
341
- editorRef.current?.getModel()?.getLineCount() ?? 0
342
- );
343
- }}
344
- isNew={isNew}
345
- isEditableModeEnabled={isEditableModeEnabled}
346
- setIsEditableModeEnabled={setIsEditableModeEnabled}
347
- selectedElement={selectedElement}
348
- setSelectedElement={setSelectedElement}
349
- setSelectedFiles={setSelectedFiles}
350
- selectedFiles={selectedFiles}
351
- />
352
- </div>
353
- <div
354
- ref={resizer}
355
- className="bg-neutral-800 hover:bg-sky-500 active:bg-sky-500 w-1.5 cursor-col-resize h-full max-lg:hidden"
356
- />
357
- </>
358
- )}
359
- <Preview
360
- html={currentPageData?.html}
361
- isResizing={isResizing}
362
- isAiWorking={isAiWorking}
363
- ref={preview}
364
- device={device}
365
- pages={pages}
366
- setCurrentPage={setCurrentPage}
367
- currentTab={currentTab}
368
- isEditableModeEnabled={isEditableModeEnabled}
369
- iframeRef={iframeRef}
370
- onClickElement={(element) => {
371
- setIsEditableModeEnabled(false);
372
- setSelectedElement(element);
373
- setCurrentTab("chat");
374
- }}
375
- />
376
- </main>
377
- <Footer
378
- htmlHistory={htmlHistory}
379
- setPages={setPages}
380
- iframeRef={iframeRef}
381
- device={device}
382
- setDevice={setDevice}
383
- />
384
- </section>
385
- );
386
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/pages/index.tsx DELETED
@@ -1,30 +0,0 @@
1
- import { Page } from "@/types";
2
- import { ListPagesItem } from "./page";
3
-
4
- export function ListPages({
5
- pages,
6
- currentPage,
7
- onSelectPage,
8
- onDeletePage,
9
- }: {
10
- pages: Array<Page>;
11
- currentPage: string;
12
- onSelectPage: (path: string, newPath?: string) => void;
13
- onNewPage: () => void;
14
- onDeletePage: (path: string) => void;
15
- }) {
16
- return (
17
- <div className="w-full flex items-center justify-start bg-neutral-950 overflow-auto flex-nowrap min-h-[44px]">
18
- {pages.map((page, i) => (
19
- <ListPagesItem
20
- key={i}
21
- page={page}
22
- currentPage={currentPage}
23
- onSelectPage={onSelectPage}
24
- onDeletePage={onDeletePage}
25
- index={i}
26
- />
27
- ))}
28
- </div>
29
- );
30
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/pages/page.tsx DELETED
@@ -1,82 +0,0 @@
1
- import classNames from "classnames";
2
- import { XIcon } from "lucide-react";
3
-
4
- import { Button } from "@/components/ui/button";
5
- import { Page } from "@/types";
6
-
7
- export function ListPagesItem({
8
- page,
9
- currentPage,
10
- onSelectPage,
11
- onDeletePage,
12
- index,
13
- }: {
14
- page: Page;
15
- currentPage: string;
16
- onSelectPage: (path: string, newPath?: string) => void;
17
- onDeletePage: (path: string) => void;
18
- index: number;
19
- }) {
20
- return (
21
- <div
22
- key={index}
23
- className={classNames(
24
- "pl-6 pr-1 py-3 text-neutral-400 cursor-pointer text-sm hover:bg-neutral-900 flex items-center justify-center gap-1 group text-nowrap border-r border-neutral-800",
25
- {
26
- "bg-neutral-900 !text-white": currentPage === page.path,
27
- "!pr-6": index === 0, // Ensure the first item has padding on the right
28
- }
29
- )}
30
- onClick={() => onSelectPage(page.path)}
31
- title={page.path}
32
- >
33
- {/* {index > 0 && (
34
- <Button
35
- size="iconXsss"
36
- variant="ghost"
37
- onClick={(e) => {
38
- e.stopPropagation();
39
- // open the window modal to edit the name page
40
- let newName = window.prompt(
41
- "Enter new name for the page:",
42
- page.path
43
- );
44
- if (newName && newName.trim() !== "") {
45
- newName = newName.toLowerCase();
46
- if (!newName.endsWith(".html")) {
47
- newName = newName.replace(/\.[^/.]+$/, "");
48
- newName = newName.replace(/\s+/g, "-");
49
- newName += ".html";
50
- }
51
- onSelectPage(page.path, newName);
52
- } else {
53
- window.alert("Page name cannot be empty.");
54
- }
55
- }}
56
- >
57
- <EditIcon className="!h-3.5 text-neutral-400 cursor-pointer hover:text-neutral-300" />
58
- </Button>
59
- )} */}
60
- {page.path}
61
- {index > 0 && (
62
- <Button
63
- size="iconXsss"
64
- variant="ghost"
65
- className="group-hover:opacity-100 opacity-0"
66
- onClick={(e) => {
67
- e.stopPropagation();
68
- if (
69
- window.confirm(
70
- "Are you sure you want to delete this page? This action cannot be undone."
71
- )
72
- ) {
73
- onDeletePage(page.path);
74
- }
75
- }}
76
- >
77
- <XIcon className="h-3 text-neutral-400 cursor-pointer hover:text-neutral-300" />
78
- </Button>
79
- )}
80
- </div>
81
- );
82
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/preview/index.tsx DELETED
@@ -1,231 +0,0 @@
1
- "use client";
2
- import { useUpdateEffect } from "react-use";
3
- import { useMemo, useState } from "react";
4
- import classNames from "classnames";
5
- import { toast } from "sonner";
6
- import { useThrottleFn } from "react-use";
7
-
8
- import { cn } from "@/lib/utils";
9
- import { GridPattern } from "@/components/magic-ui/grid-pattern";
10
- import { htmlTagToText } from "@/lib/html-tag-to-text";
11
- import { Page } from "@/types";
12
-
13
- export const Preview = ({
14
- html,
15
- isResizing,
16
- isAiWorking,
17
- ref,
18
- device,
19
- currentTab,
20
- iframeRef,
21
- pages,
22
- setCurrentPage,
23
- isEditableModeEnabled,
24
- onClickElement,
25
- }: {
26
- html: string;
27
- isResizing: boolean;
28
- isAiWorking: boolean;
29
- pages: Page[];
30
- setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
31
- ref: React.RefObject<HTMLDivElement | null>;
32
- iframeRef?: React.RefObject<HTMLIFrameElement | null>;
33
- device: "desktop" | "mobile";
34
- currentTab: string;
35
- isEditableModeEnabled?: boolean;
36
- onClickElement?: (element: HTMLElement) => void;
37
- }) => {
38
- const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
39
- null
40
- );
41
-
42
- const handleMouseOver = (event: MouseEvent) => {
43
- if (iframeRef?.current) {
44
- const iframeDocument = iframeRef.current.contentDocument;
45
- if (iframeDocument) {
46
- const targetElement = event.target as HTMLElement;
47
- if (
48
- hoveredElement !== targetElement &&
49
- targetElement !== iframeDocument.body
50
- ) {
51
- setHoveredElement(targetElement);
52
- targetElement.classList.add("hovered-element");
53
- } else {
54
- return setHoveredElement(null);
55
- }
56
- }
57
- }
58
- };
59
- const handleMouseOut = () => {
60
- setHoveredElement(null);
61
- };
62
- const handleClick = (event: MouseEvent) => {
63
- if (iframeRef?.current) {
64
- const iframeDocument = iframeRef.current.contentDocument;
65
- if (iframeDocument) {
66
- const targetElement = event.target as HTMLElement;
67
- if (targetElement !== iframeDocument.body) {
68
- onClickElement?.(targetElement);
69
- }
70
- }
71
- }
72
- };
73
- const handleCustomNavigation = (event: MouseEvent) => {
74
- if (iframeRef?.current) {
75
- const iframeDocument = iframeRef.current.contentDocument;
76
- if (iframeDocument) {
77
- const findClosestAnchor = (
78
- element: HTMLElement
79
- ): HTMLAnchorElement | null => {
80
- let current = element;
81
- while (current && current !== iframeDocument.body) {
82
- if (current.tagName === "A") {
83
- return current as HTMLAnchorElement;
84
- }
85
- current = current.parentElement as HTMLElement;
86
- }
87
- return null;
88
- };
89
-
90
- const anchorElement = findClosestAnchor(event.target as HTMLElement);
91
- if (anchorElement) {
92
- let href = anchorElement.getAttribute("href");
93
- if (href) {
94
- event.stopPropagation();
95
- event.preventDefault();
96
-
97
- if (href.includes("#") && !href.includes(".html")) {
98
- const targetElement = iframeDocument.querySelector(href);
99
- if (targetElement) {
100
- targetElement.scrollIntoView({ behavior: "smooth" });
101
- }
102
- return;
103
- }
104
-
105
- href = href.split(".html")[0] + ".html";
106
- const isPageExist = pages.some((page) => page.path === href);
107
- if (isPageExist) {
108
- setCurrentPage(href);
109
- }
110
- }
111
- }
112
- }
113
- }
114
- };
115
-
116
- useUpdateEffect(() => {
117
- const cleanupListeners = () => {
118
- if (iframeRef?.current?.contentDocument) {
119
- const iframeDocument = iframeRef.current.contentDocument;
120
- iframeDocument.removeEventListener("mouseover", handleMouseOver);
121
- iframeDocument.removeEventListener("mouseout", handleMouseOut);
122
- iframeDocument.removeEventListener("click", handleClick);
123
- }
124
- };
125
-
126
- if (iframeRef?.current) {
127
- const iframeDocument = iframeRef.current.contentDocument;
128
- if (iframeDocument) {
129
- cleanupListeners();
130
-
131
- if (isEditableModeEnabled) {
132
- iframeDocument.addEventListener("mouseover", handleMouseOver);
133
- iframeDocument.addEventListener("mouseout", handleMouseOut);
134
- iframeDocument.addEventListener("click", handleClick);
135
- }
136
- }
137
- }
138
-
139
- return cleanupListeners;
140
- }, [iframeRef, isEditableModeEnabled]);
141
-
142
- const selectedElement = useMemo(() => {
143
- if (!isEditableModeEnabled) return null;
144
- if (!hoveredElement) return null;
145
- return hoveredElement;
146
- }, [hoveredElement, isEditableModeEnabled]);
147
-
148
- const throttledHtml = useThrottleFn((html) => html, 1000, [html]);
149
-
150
- return (
151
- <div
152
- ref={ref}
153
- className={classNames(
154
- "w-full border-l border-gray-900 h-full relative z-0 flex items-center justify-center",
155
- {
156
- "lg:p-4": currentTab !== "preview",
157
- "max-lg:h-0": currentTab === "chat",
158
- "max-lg:h-full": currentTab === "preview",
159
- }
160
- )}
161
- onClick={(e) => {
162
- if (isAiWorking) {
163
- e.preventDefault();
164
- e.stopPropagation();
165
- toast.warning("Please wait for the AI to finish working.");
166
- }
167
- }}
168
- >
169
- <GridPattern
170
- x={-1}
171
- y={-1}
172
- strokeDasharray={"4 2"}
173
- className={cn(
174
- "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)]"
175
- )}
176
- />
177
- {!isAiWorking && hoveredElement && selectedElement && (
178
- <div
179
- className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"
180
- style={{
181
- top:
182
- selectedElement.getBoundingClientRect().top +
183
- (currentTab === "preview" ? 0 : 24),
184
- left:
185
- selectedElement.getBoundingClientRect().left +
186
- (currentTab === "preview" ? 0 : 24),
187
- width: selectedElement.getBoundingClientRect().width,
188
- height: selectedElement.getBoundingClientRect().height,
189
- }}
190
- >
191
- <span className="bg-sky-500 rounded-t-md text-sm text-neutral-100 px-2 py-0.5 -translate-y-7 absolute top-0 left-0">
192
- {htmlTagToText(selectedElement.tagName.toLowerCase())}
193
- </span>
194
- </div>
195
- )}
196
- <iframe
197
- id="preview-iframe"
198
- ref={iframeRef}
199
- title="output"
200
- className={classNames(
201
- "w-full select-none transition-all duration-200 bg-black h-full",
202
- {
203
- "pointer-events-none": isResizing || isAiWorking,
204
- "lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
205
- device === "mobile",
206
- "lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
207
- currentTab !== "preview" && device === "desktop",
208
- }
209
- )}
210
- srcDoc={isAiWorking ? (throttledHtml as string) : html}
211
- onLoad={() => {
212
- if (iframeRef?.current?.contentWindow?.document?.body) {
213
- iframeRef.current.contentWindow.document.body.scrollIntoView({
214
- block: isAiWorking ? "end" : "start",
215
- inline: "nearest",
216
- behavior: isAiWorking ? "instant" : "smooth",
217
- });
218
- }
219
- // add event listener to all links in the iframe to handle navigation
220
- if (iframeRef?.current?.contentWindow?.document) {
221
- const links =
222
- iframeRef.current.contentWindow.document.querySelectorAll("a");
223
- links.forEach((link) => {
224
- link.addEventListener("click", handleCustomNavigation);
225
- });
226
- }
227
- }}
228
- />
229
- </div>
230
- );
231
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/save-button/index.tsx DELETED
@@ -1,76 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { useState } from "react";
3
- import { toast } from "sonner";
4
- import { MdSave } from "react-icons/md";
5
- import { useParams } from "next/navigation";
6
-
7
- import Loading from "@/components/loading";
8
- import { Button } from "@/components/ui/button";
9
- import { api } from "@/lib/api";
10
- import { Page } from "@/types";
11
-
12
- export function SaveButton({
13
- pages,
14
- prompts,
15
- }: {
16
- pages: Page[];
17
- prompts: string[];
18
- }) {
19
- // get params from URL
20
- const { namespace, repoId } = useParams<{
21
- namespace: string;
22
- repoId: string;
23
- }>();
24
- const [loading, setLoading] = useState(false);
25
-
26
- const updateSpace = async () => {
27
- setLoading(true);
28
-
29
- try {
30
- const res = await api.put(`/me/projects/${namespace}/${repoId}`, {
31
- pages,
32
- prompts,
33
- });
34
- if (res.data.ok) {
35
- toast.success("Your space is updated! 🎉", {
36
- action: {
37
- label: "See Space",
38
- onClick: () => {
39
- window.open(
40
- `https://huggingface.co/spaces/${namespace}/${repoId}`,
41
- "_blank"
42
- );
43
- },
44
- },
45
- });
46
- } else {
47
- toast.error(res?.data?.error || "Failed to update space");
48
- }
49
- } catch (err: any) {
50
- toast.error(err.response?.data?.error || err.message);
51
- } finally {
52
- setLoading(false);
53
- }
54
- };
55
- return (
56
- <>
57
- <Button
58
- variant="default"
59
- className="max-lg:hidden !px-4 relative"
60
- onClick={updateSpace}
61
- >
62
- <MdSave className="size-4" />
63
- Publish your Project{" "}
64
- {loading && <Loading className="ml-2 size-4 animate-spin" />}
65
- </Button>
66
- <Button
67
- variant="default"
68
- size="sm"
69
- className="lg:hidden relative"
70
- onClick={updateSpace}
71
- >
72
- Publish {loading && <Loading className="ml-2 size-4 animate-spin" />}
73
- </Button>
74
- </>
75
- );
76
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/iframe-detector.tsx DELETED
@@ -1,75 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useState } from "react";
4
- import IframeWarningModal from "./iframe-warning-modal";
5
-
6
- export default function IframeDetector() {
7
- const [showWarning, setShowWarning] = useState(false);
8
-
9
- useEffect(() => {
10
- // Helper function to check if a hostname is from allowed domains
11
- const isAllowedDomain = (hostname: string) => {
12
- const host = hostname.toLowerCase();
13
- return (
14
- host.endsWith(".huggingface.co") ||
15
- host.endsWith(".hf.co") ||
16
- host === "huggingface.co" ||
17
- host === "hf.co"
18
- );
19
- };
20
-
21
- // Check if the current window is in an iframe
22
- const isInIframe = () => {
23
- try {
24
- return window.self !== window.top;
25
- } catch {
26
- // If we can't access window.top due to cross-origin restrictions,
27
- // we're likely in an iframe
28
- return true;
29
- }
30
- };
31
-
32
- // Additional check: compare window location with parent location
33
- const isEmbedded = () => {
34
- try {
35
- return window.location !== window.parent.location;
36
- } catch {
37
- // Cross-origin iframe
38
- return true;
39
- }
40
- };
41
-
42
- // Check if we're in an iframe from a non-allowed domain
43
- const shouldShowWarning = () => {
44
- if (!isInIframe() && !isEmbedded()) {
45
- return false; // Not in an iframe
46
- }
47
-
48
- try {
49
- // Try to get the parent's hostname
50
- const parentHostname = window.parent.location.hostname;
51
- return !isAllowedDomain(parentHostname);
52
- } catch {
53
- // Cross-origin iframe - try to get referrer instead
54
- try {
55
- if (document.referrer) {
56
- const referrerUrl = new URL(document.referrer);
57
- return !isAllowedDomain(referrerUrl.hostname);
58
- }
59
- } catch {
60
- // If we can't determine the parent domain, assume it's not allowed
61
- }
62
- return true;
63
- }
64
- };
65
-
66
- if (shouldShowWarning()) {
67
- // Show warning modal instead of redirecting immediately
68
- setShowWarning(true);
69
- }
70
- }, []);
71
-
72
- return (
73
- <IframeWarningModal isOpen={showWarning} onOpenChange={setShowWarning} />
74
- );
75
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/iframe-warning-modal.tsx DELETED
@@ -1,61 +0,0 @@
1
- "use client";
2
-
3
- import {
4
- Dialog,
5
- DialogContent,
6
- DialogDescription,
7
- DialogFooter,
8
- DialogHeader,
9
- DialogTitle,
10
- } from "@/components/ui/dialog";
11
- import { Button } from "@/components/ui/button";
12
- import { ExternalLink, AlertTriangle } from "lucide-react";
13
-
14
- interface IframeWarningModalProps {
15
- isOpen: boolean;
16
- onOpenChange: (open: boolean) => void;
17
- }
18
-
19
- export default function IframeWarningModal({
20
- isOpen,
21
- }: // onOpenChange,
22
- IframeWarningModalProps) {
23
- const handleVisitSite = () => {
24
- window.open("https://deepsite.hf.co", "_blank");
25
- };
26
-
27
- return (
28
- <Dialog open={isOpen} onOpenChange={() => {}}>
29
- <DialogContent className="sm:max-w-md">
30
- <DialogHeader>
31
- <div className="flex items-center gap-2">
32
- <AlertTriangle className="h-5 w-5 text-red-500" />
33
- <DialogTitle>Unauthorized Embedding</DialogTitle>
34
- </div>
35
- <DialogDescription className="text-left">
36
- You&apos;re viewing DeepSite through an unauthorized iframe. For the
37
- best experience and security, please visit the official website
38
- directly.
39
- </DialogDescription>
40
- </DialogHeader>
41
-
42
- <div className="bg-muted/50 rounded-lg p-4 space-y-2">
43
- <p className="text-sm font-medium">Why visit the official site?</p>
44
- <ul className="text-sm text-muted-foreground space-y-1">
45
- <li>• Better performance and security</li>
46
- <li>• Full functionality access</li>
47
- <li>• Latest features and updates</li>
48
- <li>• Proper authentication support</li>
49
- </ul>
50
- </div>
51
-
52
- <DialogFooter className="flex-col sm:flex-row gap-2">
53
- <Button onClick={handleVisitSite} className="w-full sm:w-auto">
54
- <ExternalLink className="mr-2 h-4 w-4" />
55
- Visit Deepsite.hf.co
56
- </Button>
57
- </DialogFooter>
58
- </DialogContent>
59
- </Dialog>
60
- );
61
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/invite-friends/index.tsx DELETED
@@ -1,85 +0,0 @@
1
- import { TiUserAdd } from "react-icons/ti";
2
- import { Link } from "lucide-react";
3
- import { FaXTwitter } from "react-icons/fa6";
4
- import { useCopyToClipboard } from "react-use";
5
- import { toast } from "sonner";
6
-
7
- import { Button } from "@/components/ui/button";
8
- import {
9
- Dialog,
10
- DialogContent,
11
- DialogTitle,
12
- DialogTrigger,
13
- } from "@/components/ui/dialog";
14
-
15
- export function InviteFriends() {
16
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- const [_, copyToClipboard] = useCopyToClipboard();
18
-
19
- return (
20
- <Dialog>
21
- <form>
22
- <DialogTrigger asChild>
23
- <Button
24
- size="iconXs"
25
- variant="outline"
26
- className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
27
- >
28
- <TiUserAdd className="size-4" />
29
- </Button>
30
- </DialogTrigger>
31
- <DialogContent className="sm:max-w-lg lg:!p-8 !rounded-3xl !bg-white !border-neutral-100">
32
- <DialogTitle className="hidden" />
33
- <main>
34
- <div className="flex items-center justify-start -space-x-4 mb-5">
35
- <div className="size-11 rounded-full bg-pink-300 shadow-2xs flex items-center justify-center text-2xl">
36
- 😎
37
- </div>
38
- <div className="size-11 rounded-full bg-amber-300 shadow-2xs flex items-center justify-center text-2xl z-2">
39
- 😇
40
- </div>
41
- <div className="size-11 rounded-full bg-sky-300 shadow-2xs flex items-center justify-center text-2xl">
42
- 😜
43
- </div>
44
- </div>
45
- <p className="text-xl font-semibold text-neutral-950 max-w-[200px]">
46
- Invite your friends to join us!
47
- </p>
48
- <p className="text-sm text-neutral-500 mt-2 max-w-sm">
49
- Support us and share the love and let them know about our awesome
50
- platform.
51
- </p>
52
- <div className="mt-4 space-x-3.5">
53
- <a
54
- href="https://x.com/intent/post?url=https://enzostvs-deepsite.hf.space/&text=Checkout%20this%20awesome%20Ai%20Tool!%20Vibe%20coding%20has%20never%20been%20so%20easy✨"
55
- target="_blank"
56
- rel="noopener noreferrer"
57
- >
58
- <Button
59
- variant="lightGray"
60
- size="sm"
61
- className="!text-neutral-700"
62
- >
63
- <FaXTwitter className="size-4" />
64
- Share on
65
- </Button>
66
- </a>
67
- <Button
68
- variant="lightGray"
69
- size="sm"
70
- className="!text-neutral-700"
71
- onClick={() => {
72
- copyToClipboard("https://enzostvs-deepsite.hf.space/");
73
- toast.success("Invite link copied to clipboard!");
74
- }}
75
- >
76
- <Link className="size-4" />
77
- Copy Invite Link
78
- </Button>
79
- </div>
80
- </main>
81
- </DialogContent>
82
- </form>
83
- </Dialog>
84
- );
85
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/login-modal/index.tsx DELETED
@@ -1,62 +0,0 @@
1
- import { useLocalStorage } from "react-use";
2
- import { Button } from "@/components/ui/button";
3
- import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
4
- import { useUser } from "@/hooks/useUser";
5
- import { isTheSameHtml } from "@/lib/compare-html-diff";
6
- import { Page } from "@/types";
7
-
8
- export const LoginModal = ({
9
- open,
10
- pages,
11
- onClose,
12
- title = "Log In to use DeepSite for free",
13
- description = "Log In through your Hugging Face account to continue using DeepSite and increase your monthly free limit.",
14
- }: {
15
- open: boolean;
16
- pages?: Page[];
17
- onClose: React.Dispatch<React.SetStateAction<boolean>>;
18
- title?: string;
19
- description?: string;
20
- }) => {
21
- const { openLoginWindow } = useUser();
22
- const [, setStorage] = useLocalStorage("pages");
23
- const handleClick = async () => {
24
- if (pages && !isTheSameHtml(pages[0].html)) {
25
- setStorage(pages);
26
- }
27
- openLoginWindow();
28
- onClose(false);
29
- };
30
- return (
31
- <Dialog open={open} onOpenChange={onClose}>
32
- <DialogContent className="sm:max-w-lg lg:!p-8 !rounded-3xl !bg-white !border-neutral-100">
33
- <DialogTitle className="hidden" />
34
- <main className="flex flex-col items-start text-left relative pt-2">
35
- <div className="flex items-center justify-start -space-x-4 mb-5">
36
- <div className="size-14 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-3xl opacity-50">
37
- 💪
38
- </div>
39
- <div className="size-16 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-4xl z-2">
40
- 😎
41
- </div>
42
- <div className="size-14 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-3xl opacity-50">
43
- 🙌
44
- </div>
45
- </div>
46
- <p className="text-2xl font-bold text-neutral-950">{title}</p>
47
- <p className="text-neutral-500 text-base mt-2 max-w-sm">
48
- {description}
49
- </p>
50
- <Button
51
- variant="black"
52
- size="lg"
53
- className="w-full !text-base !h-11 mt-8"
54
- onClick={handleClick}
55
- >
56
- Log In to Continue
57
- </Button>
58
- </main>
59
- </DialogContent>
60
- </Dialog>
61
- );
62
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/magic-ui/grid-pattern.tsx DELETED
@@ -1,69 +0,0 @@
1
- import { useId } from "react";
2
- import { cn } from "@/lib/utils";
3
-
4
- interface GridPatternProps extends React.SVGProps<SVGSVGElement> {
5
- width?: number;
6
- height?: number;
7
- x?: number;
8
- y?: number;
9
- squares?: Array<[x: number, y: number]>;
10
- strokeDasharray?: string;
11
- className?: string;
12
- [key: string]: unknown;
13
- }
14
-
15
- export function GridPattern({
16
- width = 40,
17
- height = 40,
18
- x = -1,
19
- y = -1,
20
- strokeDasharray = "0",
21
- squares,
22
- className,
23
- ...props
24
- }: GridPatternProps) {
25
- const id = useId();
26
-
27
- return (
28
- <svg
29
- aria-hidden="true"
30
- className={cn(
31
- "pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-neutral-700 -z-[1]",
32
- className
33
- )}
34
- {...props}
35
- >
36
- <defs>
37
- <pattern
38
- id={id}
39
- width={width}
40
- height={height}
41
- patternUnits="userSpaceOnUse"
42
- x={x}
43
- y={y}
44
- >
45
- <path
46
- d={`M.5 ${height}V.5H${width}`}
47
- fill="none"
48
- strokeDasharray={strokeDasharray}
49
- />
50
- </pattern>
51
- </defs>
52
- <rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
53
- {squares && (
54
- <svg x={x} y={y} className="overflow-visible">
55
- {squares.map(([x, y]) => (
56
- <rect
57
- strokeWidth="0"
58
- key={`${x}-${y}`}
59
- width={width - 1}
60
- height={height - 1}
61
- x={x * width + 1}
62
- y={y * height + 1}
63
- />
64
- ))}
65
- </svg>
66
- )}
67
- </svg>
68
- );
69
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/my-projects/index.tsx DELETED
@@ -1,57 +0,0 @@
1
- "use client";
2
- import { Plus } from "lucide-react";
3
- import Link from "next/link";
4
- import { useState } from "react";
5
-
6
- import { useUser } from "@/hooks/useUser";
7
- import { Project } from "@/types";
8
- import { redirect } from "next/navigation";
9
- import { ProjectCard } from "./project-card";
10
- import { LoadProject } from "./load-project";
11
-
12
- export function MyProjects({
13
- projects: initialProjects,
14
- }: {
15
- projects: Project[];
16
- }) {
17
- const { user } = useUser();
18
- if (!user) {
19
- redirect("/");
20
- }
21
- const [projects, setProjects] = useState<Project[]>(initialProjects || []);
22
- return (
23
- <>
24
- <section className="max-w-[86rem] py-12 px-4 mx-auto">
25
- <header className="flex items-center justify-between max-lg:flex-col gap-4">
26
- <div className="text-left">
27
- <h1 className="text-3xl font-bold text-white">
28
- <span className="capitalize">{user.fullname}</span>&apos;s
29
- DeepSite Projects
30
- </h1>
31
- <p className="text-muted-foreground text-base mt-1 max-w-xl">
32
- Create, manage, and explore your DeepSite projects.
33
- </p>
34
- </div>
35
- <LoadProject
36
- fullXsBtn
37
- onSuccess={(project: Project) => {
38
- setProjects((prev) => [...prev, project]);
39
- }}
40
- />
41
- </header>
42
- <div className="mt-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
43
- <Link
44
- href="/projects/new"
45
- className="bg-neutral-900 rounded-xl h-44 flex items-center justify-center text-neutral-300 border border-neutral-800 hover:brightness-110 transition-all duration-200"
46
- >
47
- <Plus className="size-5 mr-1.5" />
48
- Create Project
49
- </Link>
50
- {projects.map((project: Project) => (
51
- <ProjectCard key={project._id} project={project} />
52
- ))}
53
- </div>
54
- </section>
55
- </>
56
- );
57
- }