Upload ChatGPT Image 14 сент. 2025 г., 14_27_00.png

#415
This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. README.md +4 -11
  3. app/(public)/layout.tsx +1 -1
  4. app/(public)/page.tsx +43 -4
  5. app/(public)/projects/page.tsx +13 -0
  6. app/[namespace]/[repoId]/page.tsx +0 -28
  7. app/actions/projects.ts +40 -24
  8. app/actions/rewrite-prompt.ts +35 -0
  9. app/api/{ask → ask-ai}/route.ts +71 -217
  10. app/api/auth/login-url/route.ts +0 -23
  11. app/api/auth/logout/route.ts +0 -25
  12. app/api/auth/route.ts +1 -21
  13. app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -190
  14. app/api/me/projects/[namespace]/[repoId]/images/route.ts +14 -16
  15. app/api/me/projects/[namespace]/[repoId]/route.ts +179 -90
  16. app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -64
  17. app/api/me/projects/route.ts +95 -75
  18. app/api/me/route.ts +1 -22
  19. app/auth/callback/page.tsx +42 -67
  20. app/layout.tsx +33 -49
  21. app/new/page.tsx +0 -14
  22. app/projects/[namespace]/[repoId]/page.tsx +42 -0
  23. app/projects/new/page.tsx +5 -0
  24. app/sitemap.ts +0 -28
  25. assets/deepseek.svg +0 -1
  26. assets/globals.css +0 -225
  27. assets/kimi.svg +0 -1
  28. assets/qwen.svg +0 -1
  29. assets/zai.svg +0 -13
  30. components.json +1 -1
  31. components/animated-blobs/index.tsx +0 -34
  32. components/animated-text/index.tsx +0 -123
  33. components/contexts/app-context.tsx +10 -6
  34. components/contexts/login-context.tsx +0 -62
  35. components/contexts/pro-context.tsx +0 -48
  36. components/editor/ask-ai/fake-ask.tsx +0 -97
  37. components/editor/ask-ai/follow-up-tooltip.tsx +36 -0
  38. components/editor/ask-ai/index.tsx +320 -142
  39. components/editor/ask-ai/loading.tsx +0 -68
  40. components/editor/ask-ai/prompt-builder/content-modal.tsx +0 -196
  41. components/editor/ask-ai/prompt-builder/index.tsx +0 -68
  42. components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +0 -58
  43. components/editor/ask-ai/prompt-builder/themes.tsx +0 -48
  44. components/editor/ask-ai/re-imagine.tsx +4 -10
  45. components/editor/ask-ai/selector.tsx +0 -41
  46. components/editor/ask-ai/settings.tsx +143 -230
  47. components/editor/ask-ai/uploader.tsx +167 -129
  48. components/editor/deploy-button/content.tsx +111 -0
  49. components/editor/deploy-button/index.tsx +79 -0
  50. components/editor/footer/index.tsx +150 -0
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ public/ChatGPT[[:space:]]Image[[:space:]]14[[:space:]]сент.[[:space:]]2025[[:space:]]г.,[[:space:]]14_27_00.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: DeepSite v3
3
  emoji: 🐳
4
  colorFrom: blue
5
  colorTo: blue
@@ -7,23 +7,16 @@ sdk: docker
7
  pinned: true
8
  app_port: 3000
9
  license: mit
10
- short_description: Generate any application by Vibe Coding
11
  models:
12
  - deepseek-ai/DeepSeek-V3-0324
13
  - deepseek-ai/DeepSeek-R1-0528
14
- - deepseek-ai/DeepSeek-V3.1
15
- - deepseek-ai/DeepSeek-V3.1-Terminus
16
- - deepseek-ai/DeepSeek-V3.2-Exp
17
- - Qwen/Qwen3-Coder-480B-A35B-Instruct
18
- - moonshotai/Kimi-K2-Instruct
19
- - moonshotai/Kimi-K2-Instruct-0905
20
- - zai-org/GLM-4.6
21
  ---
22
 
23
  # DeepSite 🐳
24
 
25
- DeepSite is a Vibe Coding Platform 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.
26
 
27
  ## How to use it locally
28
 
29
- Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
 
1
  ---
2
+ title: DeepSite v2
3
  emoji: 🐳
4
  colorFrom: blue
5
  colorTo: blue
 
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)
app/(public)/layout.tsx CHANGED
@@ -6,7 +6,7 @@ export default async function PublicLayout({
6
  children: React.ReactNode;
7
  }>) {
8
  return (
9
- <div className="h-screen bg-neutral-950 z-1 relative overflow-auto scroll-smooth">
10
  <div className="background__noisy" />
11
  <Navigation />
12
  {children}
 
6
  children: React.ReactNode;
7
  }>) {
8
  return (
9
+ <div className="min-h-screen bg-black z-1 relative">
10
  <div className="background__noisy" />
11
  <Navigation />
12
  {children}
app/(public)/page.tsx CHANGED
@@ -1,5 +1,44 @@
1
- import { MyProjects } from "@/components/my-projects";
2
-
3
- export default async function HomePage() {
4
- return <MyProjects />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  }
 
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 ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/[namespace]/[repoId]/page.tsx DELETED
@@ -1,28 +0,0 @@
1
- import { AppEditor } from "@/components/editor";
2
- import { generateSEO } from "@/lib/seo";
3
- import { Metadata } from "next";
4
-
5
- export async function generateMetadata({
6
- params,
7
- }: {
8
- params: Promise<{ namespace: string; repoId: string }>;
9
- }): Promise<Metadata> {
10
- const { namespace, repoId } = await params;
11
-
12
- return generateSEO({
13
- title: `${namespace}/${repoId} - DeepSite Editor`,
14
- description: `Edit and build ${namespace}/${repoId} with AI-powered tools on DeepSite. Create stunning websites with no code required.`,
15
- path: `/${namespace}/${repoId}`,
16
- // Prevent indexing of individual project editor pages if they contain sensitive content
17
- noIndex: false, // Set to true if you want to keep project pages private
18
- });
19
- }
20
-
21
- export default async function ProjectNamespacePage({
22
- params,
23
- }: {
24
- params: Promise<{ namespace: string; repoId: string }>;
25
- }) {
26
- const { namespace, repoId } = await params;
27
- return <AppEditor namespace={namespace} repoId={repoId} />;
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/actions/projects.ts CHANGED
@@ -2,13 +2,13 @@
2
 
3
  import { isAuthenticated } from "@/lib/auth";
4
  import { NextResponse } from "next/server";
5
- import { listSpaces } from "@huggingface/hub";
6
- import { ProjectType } from "@/types";
 
7
 
8
  export async function getProjects(): Promise<{
9
  ok: boolean;
10
  projects: ProjectType[];
11
- isEmpty?: boolean;
12
  }> {
13
  const user = await isAuthenticated();
14
 
@@ -19,29 +19,45 @@ export async function getProjects(): Promise<{
19
  };
20
  }
21
 
22
- const projects = [];
23
- for await (const space of listSpaces({
24
- accessToken: user.token as string,
25
- additionalFields: ["author", "cardData"],
26
- search: {
27
- owner: user.name,
28
- }
29
- })) {
30
- if (
31
- !space.private &&
32
- space.sdk === "static" &&
33
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
34
- (
35
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
36
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
37
- )
38
- ) {
39
- projects.push(space);
40
- }
41
  }
42
-
43
  return {
44
  ok: true,
45
- projects,
46
  };
47
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import { isAuthenticated } from "@/lib/auth";
4
  import { NextResponse } from "next/server";
5
+ import dbConnect from "@/lib/mongodb";
6
+ import Project from "@/models/Project";
7
+ import { Project as ProjectType } from "@/types";
8
 
9
  export async function getProjects(): Promise<{
10
  ok: boolean;
11
  projects: ProjectType[];
 
12
  }> {
13
  const user = await isAuthenticated();
14
 
 
19
  };
20
  }
21
 
22
+ await dbConnect();
23
+ const projects = await Project.find({
24
+ user_id: user?.id,
25
+ })
26
+ .sort({ _createdAt: -1 })
27
+ .limit(100)
28
+ .lean();
29
+ if (!projects) {
30
+ return {
31
+ ok: false,
32
+ projects: [],
33
+ };
 
 
 
 
 
 
 
34
  }
 
35
  return {
36
  ok: true,
37
+ projects: JSON.parse(JSON.stringify(projects)) as ProjectType[],
38
  };
39
  }
40
+
41
+ export async function getProject(
42
+ namespace: string,
43
+ repoId: string
44
+ ): Promise<ProjectType | null> {
45
+ const user = await isAuthenticated();
46
+
47
+ if (user instanceof NextResponse || !user) {
48
+ return null;
49
+ }
50
+
51
+ await dbConnect();
52
+ const project = await Project.findOne({
53
+ user_id: user.id,
54
+ namespace,
55
+ repoId,
56
+ }).lean();
57
+
58
+ if (!project) {
59
+ return null;
60
+ }
61
+
62
+ return JSON.parse(JSON.stringify(project)) as ProjectType;
63
+ }
app/actions/rewrite-prompt.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InferenceClient } from "@huggingface/inference";
2
+
3
+ const START_REWRITE_PROMPT = ">>>>>>> START PROMPT >>>>>>";
4
+ const END_REWRITE_PROMPT = ">>>>>>> END PROMPT >>>>>>";
5
+
6
+ export const callAiRewritePrompt = async (prompt: string, { token, billTo }: { token: string, billTo?: string | null }) => {
7
+ const client = new InferenceClient(token);
8
+ const response = await client.chatCompletion(
9
+ {
10
+ model: "deepseek-ai/DeepSeek-V3.1",
11
+ provider: "novita",
12
+ messages: [{
13
+ role: "system",
14
+ content: `You are a helpful assistant that rewrites prompts to make them better. All the prompts will be about creating a website or app.
15
+ Try to make the prompt more detailed and specific to create a good UI/UX Design and good code.
16
+ Format the result by following this format:
17
+ ${START_REWRITE_PROMPT}
18
+ new prompt here
19
+ ${END_REWRITE_PROMPT}
20
+ If you don't rewrite the prompt, return the original prompt.
21
+ Make sure to return the prompt in the same language as the prompt you are given. Also IMPORTANT: Make sure to keep the original intent of the prompt. Improve it it needed, but don't change the original intent.
22
+ `
23
+ },{ role: "user", content: prompt }],
24
+ },
25
+ billTo ? { billTo } : {}
26
+ );
27
+
28
+ const responseContent = response.choices[0]?.message?.content;
29
+ if (!responseContent) {
30
+ return prompt;
31
+ }
32
+ const startIndex = responseContent.indexOf(START_REWRITE_PROMPT);
33
+ const endIndex = responseContent.indexOf(END_REWRITE_PROMPT);
34
+ return responseContent.substring(startIndex + START_REWRITE_PROMPT.length, endIndex);
35
+ };
app/api/{ask → ask-ai}/route.ts RENAMED
@@ -4,7 +4,7 @@ import { NextResponse } from "next/server";
4
  import { headers } from "next/headers";
5
  import { InferenceClient } from "@huggingface/inference";
6
 
7
- import { MODELS } from "@/lib/providers";
8
  import {
9
  DIVIDER,
10
  FOLLOW_UP_SYSTEM_PROMPT,
@@ -16,17 +16,9 @@ import {
16
  SEARCH_START,
17
  UPDATE_PAGE_START,
18
  UPDATE_PAGE_END,
19
- PROMPT_FOR_PROJECT_NAME,
20
  } from "@/lib/prompts";
21
- import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
22
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
23
  import { Page } from "@/types";
24
- import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
25
- import { isAuthenticated } from "@/lib/auth";
26
- import { getBestProvider } from "@/lib/best-provider";
27
- // import { rewritePrompt } from "@/lib/rewrite-prompt";
28
- import { COLORS } from "@/lib/utils";
29
- import { templates } from "@/lib/templates";
30
 
31
  const ipAddresses = new Map();
32
 
@@ -35,7 +27,7 @@ export async function POST(request: NextRequest) {
35
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
36
 
37
  const body = await request.json();
38
- const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
39
 
40
  if (!model || (!prompt && !redesignMarkdown)) {
41
  return NextResponse.json(
@@ -55,8 +47,18 @@ export async function POST(request: NextRequest) {
55
  );
56
  }
57
 
58
- let token: string | null = null;
59
- if (userToken) token = userToken;
 
 
 
 
 
 
 
 
 
 
60
  let billTo: string | null = null;
61
 
62
  /**
@@ -89,13 +91,18 @@ export async function POST(request: NextRequest) {
89
  billTo = "huggingface";
90
  }
91
 
92
- const selectedProvider = await getBestProvider(selectedModel.value, provider)
 
 
 
 
93
 
94
- let rewrittenPrompt = redesignMarkdown ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : prompt;
95
 
96
- if (enhancedSettings.isActive) {
97
- // rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
98
- }
 
99
 
100
  try {
101
  const encoder = new TextEncoder();
@@ -114,37 +121,33 @@ export async function POST(request: NextRequest) {
114
  // let completeResponse = "";
115
  try {
116
  const client = new InferenceClient(token);
117
-
118
- const systemPrompt = INITIAL_SYSTEM_PROMPT;
119
-
120
- const userPrompt = rewrittenPrompt;
121
- const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
122
- const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
123
- const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
124
-
125
  const chatCompletion = client.chatCompletionStream(
126
  {
127
  model: selectedModel.value,
128
- provider: selectedProvider.provider,
129
  messages: [
130
  {
131
  role: "system",
132
- content: systemPrompt,
133
  },
 
 
 
 
134
  {
135
  role: "user",
136
- content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
137
- 2. I want to use the following secondary color: ${enhancedSettings.secondaryColor} (eg: bg-${enhancedSettings.secondaryColor}-500).
138
- 3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
139
  },
140
  ],
141
- ...providerConfig,
142
  },
143
  billTo ? { billTo } : {}
144
  );
145
 
146
  while (true) {
147
- const { done, value } = await chatCompletion.next()
148
  if (done) {
149
  break;
150
  }
@@ -154,9 +157,6 @@ export async function POST(request: NextRequest) {
154
  await writer.write(encoder.encode(chunk));
155
  }
156
  }
157
-
158
- // Explicitly close the writer after successful completion
159
- await writer.close();
160
  } catch (error: any) {
161
  if (error.message?.includes("exceeded your monthly included credits")) {
162
  await writer.write(
@@ -168,18 +168,7 @@ export async function POST(request: NextRequest) {
168
  })
169
  )
170
  );
171
- } else if (error?.message?.includes("inference provider information")) {
172
- await writer.write(
173
- encoder.encode(
174
- JSON.stringify({
175
- ok: false,
176
- openSelectProvider: true,
177
- message: error.message,
178
- })
179
- )
180
- );
181
- }
182
- else {
183
  await writer.write(
184
  encoder.encode(
185
  JSON.stringify({
@@ -192,12 +181,7 @@ export async function POST(request: NextRequest) {
192
  );
193
  }
194
  } finally {
195
- // Ensure the writer is always closed, even if already closed
196
- try {
197
- await writer?.close();
198
- } catch {
199
- // Ignore errors when closing the writer as it might already be closed
200
- }
201
  }
202
  })();
203
 
@@ -216,19 +200,13 @@ export async function POST(request: NextRequest) {
216
  }
217
 
218
  export async function PUT(request: NextRequest) {
219
- const user = await isAuthenticated();
220
- if (user instanceof NextResponse || !user) {
221
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
222
- }
223
-
224
  const authHeaders = await headers();
 
225
 
226
  const body = await request.json();
227
- const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew, enhancedSettings } =
228
  body;
229
 
230
- let repoId = repoIdFromBody;
231
-
232
  if (!prompt || pages.length === 0) {
233
  return NextResponse.json(
234
  { ok: false, error: "Missing required fields" },
@@ -246,7 +224,7 @@ export async function PUT(request: NextRequest) {
246
  );
247
  }
248
 
249
- let token = user.token as string;
250
  let billTo: string | null = null;
251
 
252
  /**
@@ -281,112 +259,52 @@ export async function PUT(request: NextRequest) {
281
 
282
  const client = new InferenceClient(token);
283
 
284
- const escapeRegExp = (string: string) => {
285
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
286
- };
287
-
288
- const createFlexibleHtmlRegex = (searchBlock: string) => {
289
- let searchRegex = escapeRegExp(searchBlock)
290
- .replace(/\s+/g, '\\s*')
291
- .replace(/>\s*</g, '>\\s*<')
292
- .replace(/\s*>/g, '\\s*>');
293
-
294
- return new RegExp(searchRegex, 'g');
295
- };
296
-
297
- const selectedProvider = await getBestProvider(selectedModel.value, provider)
298
 
299
  try {
300
- const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
301
- const userContext = "You are modifying the HTML file based on the user's request.";
302
-
303
- const getRelevantPages = (pages: Page[], prompt: string, maxPages: number = 2): Page[] => {
304
- if (pages.length <= maxPages) return pages;
305
-
306
- const indexPage = pages.find(p => p.path === '/' || p.path === '/index' || p.path === 'index');
307
- const otherPages = pages.filter(p => p !== indexPage);
308
-
309
- if (selectedElementHtml) {
310
- const elementKeywords = selectedElementHtml.toLowerCase().match(/class=["']([^"']*)["']|id=["']([^"']*)["']/g) || [];
311
- const relevantPages = otherPages.filter(page => {
312
- const pageContent = page.html.toLowerCase();
313
- return elementKeywords.some((keyword: string) => pageContent.includes(keyword.toLowerCase()));
314
- });
315
-
316
- return indexPage ? [indexPage, ...relevantPages.slice(0, maxPages - 1)] : relevantPages.slice(0, maxPages);
317
- }
318
-
319
- const keywords = prompt.toLowerCase().split(/\s+/).filter(word => word.length > 3);
320
- const scoredPages = otherPages.map(page => {
321
- const pageContent = (page.path + ' ' + page.html).toLowerCase();
322
- const score = keywords.reduce((acc, keyword) => {
323
- return acc + (pageContent.includes(keyword) ? 1 : 0);
324
- }, 0);
325
- return { page, score };
326
- });
327
-
328
- const topPages = scoredPages
329
- .sort((a, b) => b.score - a.score)
330
- .slice(0, maxPages - (indexPage ? 1 : 0))
331
- .map(item => item.page);
332
-
333
- return indexPage ? [indexPage, ...topPages] : topPages;
334
- };
335
-
336
- const relevantPages = getRelevantPages(pages || [], prompt);
337
- const pagesContext = relevantPages
338
- .map((p: Page) => `- ${p.path}\n${p.html}`)
339
- .join("\n\n");
340
-
341
- const assistantContext = `${
342
- selectedElementHtml
343
- ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
344
- : ""
345
- }. Current pages (${relevantPages.length}/${pages?.length || 0} shown): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
346
-
347
- const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
348
- const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
349
- const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
350
-
351
- const chatCompletion = client.chatCompletionStream(
352
  {
353
  model: selectedModel.value,
354
- provider: selectedProvider.provider,
355
  messages: [
356
  {
357
  role: "system",
358
- content: systemPrompt,
359
  },
360
  {
361
  role: "user",
362
- content: userContext,
 
 
363
  },
364
  {
365
  role: "assistant",
366
- content: assistantContext,
 
 
 
 
 
367
  },
368
  {
369
  role: "user",
370
  content: prompt,
371
  },
372
  ],
373
- ...providerConfig,
 
 
 
 
374
  },
375
  billTo ? { billTo } : {}
376
  );
377
 
378
- let chunk = "";
379
- while (true) {
380
- const { done, value } = await chatCompletion.next();
381
- if (done) {
382
- break;
383
- }
384
-
385
- const deltaContent = value.choices[0]?.delta?.content;
386
- if (deltaContent) {
387
- chunk += deltaContent;
388
- }
389
- }
390
  if (!chunk) {
391
  return NextResponse.json(
392
  { ok: false, message: "No content returned from the model" },
@@ -449,18 +367,15 @@ export async function PUT(request: NextRequest) {
449
  pageHtml = `${replaceBlock}\n${pageHtml}`;
450
  updatedLines.push([1, replaceBlock.split("\n").length]);
451
  } else {
452
- const regex = createFlexibleHtmlRegex(searchBlock);
453
- const match = regex.exec(pageHtml);
454
-
455
- if (match) {
456
- const matchedText = match[0];
457
- const beforeText = pageHtml.substring(0, match.index);
458
  const startLineNumber = beforeText.split("\n").length;
459
  const replaceLines = replaceBlock.split("\n").length;
460
  const endLineNumber = startLineNumber + replaceLines - 1;
461
 
462
  updatedLines.push([startLineNumber, endLineNumber]);
463
- pageHtml = pageHtml.replace(matchedText, replaceBlock);
464
  }
465
  }
466
 
@@ -538,18 +453,15 @@ export async function PUT(request: NextRequest) {
538
  newHtml = `${replaceBlock}\n${newHtml}`;
539
  updatedLines.push([1, replaceBlock.split("\n").length]);
540
  } else {
541
- const regex = createFlexibleHtmlRegex(searchBlock);
542
- const match = regex.exec(newHtml);
543
-
544
- if (match) {
545
- const matchedText = match[0];
546
- const beforeText = newHtml.substring(0, match.index);
547
  const startLineNumber = beforeText.split("\n").length;
548
  const replaceLines = replaceBlock.split("\n").length;
549
  const endLineNumber = startLineNumber + replaceLines - 1;
550
 
551
  updatedLines.push([startLineNumber, endLineNumber]);
552
- newHtml = newHtml.replace(matchedText, replaceBlock);
553
  }
554
  }
555
 
@@ -563,67 +475,10 @@ export async function PUT(request: NextRequest) {
563
  }
564
  }
565
 
566
- const files: File[] = [];
567
- updatedPages.forEach((page: Page) => {
568
- const file = new File([page.html], page.path, { type: "text/html" });
569
- files.push(file);
570
- });
571
-
572
- if (isNew) {
573
- const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
574
- const formattedTitle = projectName?.toLowerCase()
575
- .replace(/[^a-z0-9]+/g, "-")
576
- .split("-")
577
- .filter(Boolean)
578
- .join("-")
579
- .slice(0, 96);
580
- const repo: RepoDesignation = {
581
- type: "space",
582
- name: `${user.name}/${formattedTitle}`,
583
- };
584
- const { repoUrl} = await createRepo({
585
- repo,
586
- accessToken: user.token as string,
587
- });
588
- repoId = repoUrl.split("/").slice(-2).join("/");
589
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
590
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
591
- const README = `---
592
- title: ${projectName}
593
- colorFrom: ${colorFrom}
594
- colorTo: ${colorTo}
595
- emoji: 🐳
596
- sdk: static
597
- pinned: false
598
- tags:
599
- - deepsite-v3
600
- ---
601
-
602
- # Welcome to your new DeepSite project!
603
- This project was created with [DeepSite](https://deepsite.hf.co).
604
- `;
605
- files.push(new File([README], "README.md", { type: "text/markdown" }));
606
- }
607
-
608
- const response = await uploadFiles({
609
- repo: {
610
- type: "space",
611
- name: repoId,
612
- },
613
- files,
614
- commitTitle: prompt,
615
- accessToken: user.token as string,
616
- });
617
-
618
  return NextResponse.json({
619
  ok: true,
620
  updatedLines,
621
  pages: updatedPages,
622
- repoId,
623
- commit: {
624
- ...response.commit,
625
- title: prompt,
626
- }
627
  });
628
  } else {
629
  return NextResponse.json(
@@ -653,4 +508,3 @@ This project was created with [DeepSite](https://deepsite.hf.co).
653
  );
654
  }
655
  }
656
-
 
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,
 
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
 
 
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(
 
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
  /**
 
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
+ const rewrittenPrompt = prompt;
101
 
102
+ // if (prompt?.length < 240) {
103
+
104
+ //rewrittenPrompt = await callAiRewritePrompt(prompt, { token, billTo });
105
+ // }
106
 
107
  try {
108
  const encoder = new TextEncoder();
 
121
  // let completeResponse = "";
122
  try {
123
  const client = new InferenceClient(token);
 
 
 
 
 
 
 
 
124
  const chatCompletion = client.chatCompletionStream(
125
  {
126
  model: selectedModel.value,
127
+ provider: selectedProvider.id as any,
128
  messages: [
129
  {
130
  role: "system",
131
+ content: INITIAL_SYSTEM_PROMPT,
132
  },
133
+ ...(pages?.length > 1 ? [{
134
+ role: "assistant",
135
+ 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")}`
136
+ }] : []),
137
  {
138
  role: "user",
139
+ content: redesignMarkdown
140
+ ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
141
+ : rewrittenPrompt,
142
  },
143
  ],
144
+ max_tokens: selectedProvider.max_tokens,
145
  },
146
  billTo ? { billTo } : {}
147
  );
148
 
149
  while (true) {
150
+ const { done, value } = await chatCompletion.next();
151
  if (done) {
152
  break;
153
  }
 
157
  await writer.write(encoder.encode(chunk));
158
  }
159
  }
 
 
 
160
  } catch (error: any) {
161
  if (error.message?.includes("exceeded your monthly included credits")) {
162
  await writer.write(
 
168
  })
169
  )
170
  );
171
+ } else {
 
 
 
 
 
 
 
 
 
 
 
172
  await writer.write(
173
  encoder.encode(
174
  JSON.stringify({
 
181
  );
182
  }
183
  } finally {
184
+ await writer?.close();
 
 
 
 
 
185
  }
186
  })();
187
 
 
200
  }
201
 
202
  export async function PUT(request: NextRequest) {
 
 
 
 
 
203
  const authHeaders = await headers();
204
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
205
 
206
  const body = await request.json();
207
+ const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, } =
208
  body;
209
 
 
 
210
  if (!prompt || pages.length === 0) {
211
  return NextResponse.json(
212
  { ok: false, error: "Missing required fields" },
 
224
  );
225
  }
226
 
227
+ let token = userToken;
228
  let billTo: string | null = null;
229
 
230
  /**
 
259
 
260
  const client = new InferenceClient(token);
261
 
262
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
263
+ const selectedProvider =
264
+ provider === "auto"
265
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
266
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
 
 
 
 
 
 
 
 
 
267
 
268
  try {
269
+ const response = await client.chatCompletion(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  {
271
  model: selectedModel.value,
272
+ provider: selectedProvider.id as any,
273
  messages: [
274
  {
275
  role: "system",
276
+ content: FOLLOW_UP_SYSTEM_PROMPT,
277
  },
278
  {
279
  role: "user",
280
+ content: previousPrompts
281
+ ? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
282
+ : "You are modifying the HTML file based on the user's request.",
283
  },
284
  {
285
  role: "assistant",
286
+
287
+ content: `${
288
+ selectedElementHtml
289
+ ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
290
+ : ""
291
+ }. 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")}.` : ""}`,
292
  },
293
  {
294
  role: "user",
295
  content: prompt,
296
  },
297
  ],
298
+ ...(selectedProvider.id !== "sambanova"
299
+ ? {
300
+ max_tokens: selectedProvider.max_tokens,
301
+ }
302
+ : {}),
303
  },
304
  billTo ? { billTo } : {}
305
  );
306
 
307
+ const chunk = response.choices[0]?.message?.content;
 
 
 
 
 
 
 
 
 
 
 
308
  if (!chunk) {
309
  return NextResponse.json(
310
  { ok: false, message: "No content returned from the model" },
 
367
  pageHtml = `${replaceBlock}\n${pageHtml}`;
368
  updatedLines.push([1, replaceBlock.split("\n").length]);
369
  } else {
370
+ const blockPosition = pageHtml.indexOf(searchBlock);
371
+ if (blockPosition !== -1) {
372
+ const beforeText = pageHtml.substring(0, blockPosition);
 
 
 
373
  const startLineNumber = beforeText.split("\n").length;
374
  const replaceLines = replaceBlock.split("\n").length;
375
  const endLineNumber = startLineNumber + replaceLines - 1;
376
 
377
  updatedLines.push([startLineNumber, endLineNumber]);
378
+ pageHtml = pageHtml.replace(searchBlock, replaceBlock);
379
  }
380
  }
381
 
 
453
  newHtml = `${replaceBlock}\n${newHtml}`;
454
  updatedLines.push([1, replaceBlock.split("\n").length]);
455
  } else {
456
+ const blockPosition = newHtml.indexOf(searchBlock);
457
+ if (blockPosition !== -1) {
458
+ const beforeText = newHtml.substring(0, blockPosition);
 
 
 
459
  const startLineNumber = beforeText.split("\n").length;
460
  const replaceLines = replaceBlock.split("\n").length;
461
  const endLineNumber = startLineNumber + replaceLines - 1;
462
 
463
  updatedLines.push([startLineNumber, endLineNumber]);
464
+ newHtml = newHtml.replace(searchBlock, replaceBlock);
465
  }
466
  }
467
 
 
475
  }
476
  }
477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  return NextResponse.json({
479
  ok: true,
480
  updatedLines,
481
  pages: updatedPages,
 
 
 
 
 
482
  });
483
  } else {
484
  return NextResponse.json(
 
508
  );
509
  }
510
  }
 
app/api/auth/login-url/route.ts DELETED
@@ -1,23 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
-
3
- export async function GET(req: NextRequest) {
4
- const host = req.headers.get("host") ?? "localhost:3000";
5
-
6
- let url: string;
7
- if (host.includes("localhost")) {
8
- url = host;
9
- } else if (host.includes("hf.space") || host.includes("/spaces/enzostvs")) {
10
- url = "enzostvs-deepsite.hf.space";
11
- } else {
12
- url = "deepsite.hf.co";
13
- }
14
-
15
- const redirect_uri =
16
- `${host.includes("localhost") ? "http://" : "https://"}` +
17
- url +
18
- "/auth/callback";
19
-
20
- const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
21
-
22
- return NextResponse.json({ loginUrl: loginRedirectUrl });
23
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/auth/logout/route.ts DELETED
@@ -1,25 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import MY_TOKEN_KEY from "@/lib/get-cookie-name";
3
-
4
- export async function POST() {
5
- const cookieName = MY_TOKEN_KEY();
6
- const isProduction = process.env.NODE_ENV === "production";
7
-
8
- const response = NextResponse.json(
9
- { message: "Logged out successfully" },
10
- { status: 200 }
11
- );
12
-
13
- // Clear the HTTP-only cookie
14
- const cookieOptions = [
15
- `${cookieName}=`,
16
- "Max-Age=0",
17
- "Path=/",
18
- "HttpOnly",
19
- ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
20
- ].join("; ");
21
-
22
- response.headers.set("Set-Cookie", cookieOptions);
23
-
24
- return response;
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/auth/route.ts CHANGED
@@ -1,5 +1,4 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import MY_TOKEN_KEY from "@/lib/get-cookie-name";
3
 
4
  export async function POST(req: NextRequest) {
5
  const body = await req.json();
@@ -71,17 +70,11 @@ export async function POST(req: NextRequest) {
71
  }
72
  const user = await userResponse.json();
73
 
74
- const cookieName = MY_TOKEN_KEY();
75
- const isProduction = process.env.NODE_ENV === "production";
76
-
77
- // Create response with user data
78
- const nextResponse = NextResponse.json(
79
  {
80
  access_token: response.access_token,
81
  expires_in: response.expires_in,
82
  user,
83
- // Include fallback flag for iframe contexts
84
- useLocalStorageFallback: true,
85
  },
86
  {
87
  status: 200,
@@ -90,17 +83,4 @@ export async function POST(req: NextRequest) {
90
  },
91
  }
92
  );
93
-
94
- // Set HTTP-only cookie with proper attributes for iframe support
95
- const cookieOptions = [
96
- `${cookieName}=${response.access_token}`,
97
- `Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided
98
- "Path=/",
99
- "HttpOnly",
100
- ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
101
- ].join("; ");
102
-
103
- nextResponse.headers.set("Set-Cookie", cookieOptions);
104
-
105
- return nextResponse;
106
  }
 
1
  import { NextRequest, NextResponse } from "next/server";
 
2
 
3
  export async function POST(req: NextRequest) {
4
  const body = await req.json();
 
70
  }
71
  const user = await userResponse.json();
72
 
73
+ return NextResponse.json(
 
 
 
 
74
  {
75
  access_token: response.access_token,
76
  expires_in: response.expires_in,
77
  user,
 
 
78
  },
79
  {
80
  status: 200,
 
83
  },
84
  }
85
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts DELETED
@@ -1,190 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import { Page } from "@/types";
6
-
7
- export async function POST(
8
- req: NextRequest,
9
- { params }: {
10
- params: Promise<{
11
- namespace: string;
12
- repoId: string;
13
- commitId: string;
14
- }>
15
- }
16
- ) {
17
- const user = await isAuthenticated();
18
-
19
- if (user instanceof NextResponse || !user) {
20
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
21
- }
22
-
23
- const param = await params;
24
- const { namespace, repoId, commitId } = param;
25
-
26
- try {
27
- const repo: RepoDesignation = {
28
- type: "space",
29
- name: `${namespace}/${repoId}`,
30
- };
31
-
32
- const space = await spaceInfo({
33
- name: `${namespace}/${repoId}`,
34
- accessToken: user.token as string,
35
- additionalFields: ["author"],
36
- });
37
-
38
- if (!space || space.sdk !== "static") {
39
- return NextResponse.json(
40
- { ok: false, error: "Space is not a static space." },
41
- { status: 404 }
42
- );
43
- }
44
-
45
- if (space.author !== user.name) {
46
- return NextResponse.json(
47
- { ok: false, error: "Space does not belong to the authenticated user." },
48
- { status: 403 }
49
- );
50
- }
51
-
52
- // Fetch files from the specific commit
53
- const files: File[] = [];
54
- const pages: Page[] = [];
55
- const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
56
- const commitFilePaths: Set<string> = new Set();
57
-
58
- // Get all files from the specific commit
59
- for await (const fileInfo of listFiles({
60
- repo,
61
- accessToken: user.token as string,
62
- revision: commitId,
63
- })) {
64
- const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
65
-
66
- if (allowedExtensions.includes(fileExtension || "")) {
67
- commitFilePaths.add(fileInfo.path);
68
-
69
- // Fetch the file content from the specific commit
70
- const response = await fetch(
71
- `https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
72
- );
73
-
74
- if (response.ok) {
75
- const content = await response.text();
76
- let mimeType = "text/plain";
77
-
78
- switch (fileExtension) {
79
- case "html":
80
- mimeType = "text/html";
81
- // Add HTML files to pages array for client-side setPages
82
- pages.push({
83
- path: fileInfo.path,
84
- html: content,
85
- });
86
- break;
87
- case "css":
88
- mimeType = "text/css";
89
- break;
90
- case "js":
91
- mimeType = "application/javascript";
92
- break;
93
- case "json":
94
- mimeType = "application/json";
95
- break;
96
- case "md":
97
- mimeType = "text/markdown";
98
- break;
99
- }
100
-
101
- const file = new File([content], fileInfo.path, { type: mimeType });
102
- files.push(file);
103
- }
104
- }
105
- }
106
-
107
- // Get files currently in main branch to identify files to delete
108
- const mainBranchFilePaths: Set<string> = new Set();
109
- for await (const fileInfo of listFiles({
110
- repo,
111
- accessToken: user.token as string,
112
- revision: "main",
113
- })) {
114
- const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
115
-
116
- if (allowedExtensions.includes(fileExtension || "")) {
117
- mainBranchFilePaths.add(fileInfo.path);
118
- }
119
- }
120
-
121
- // Identify files to delete (exist in main but not in commit)
122
- const filesToDelete: string[] = [];
123
- for (const mainFilePath of mainBranchFilePaths) {
124
- if (!commitFilePaths.has(mainFilePath)) {
125
- filesToDelete.push(mainFilePath);
126
- }
127
- }
128
-
129
- if (files.length === 0 && filesToDelete.length === 0) {
130
- return NextResponse.json(
131
- { ok: false, error: "No files found in the specified commit and no files to delete" },
132
- { status: 404 }
133
- );
134
- }
135
-
136
- // Delete files that exist in main but not in the commit being promoted
137
- if (filesToDelete.length > 0) {
138
- await deleteFiles({
139
- repo,
140
- paths: filesToDelete,
141
- accessToken: user.token as string,
142
- commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
143
- commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
144
- });
145
- }
146
-
147
- // Upload the files to the main branch with a promotion commit message
148
- if (files.length > 0) {
149
- await uploadFiles({
150
- repo,
151
- files,
152
- accessToken: user.token as string,
153
- commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
154
- commitDescription: `Promoted commit ${commitId} to main branch`,
155
- });
156
- }
157
-
158
- return NextResponse.json(
159
- {
160
- ok: true,
161
- message: "Version promoted successfully",
162
- promotedCommit: commitId,
163
- pages: pages,
164
- },
165
- { status: 200 }
166
- );
167
-
168
- } catch (error: any) {
169
-
170
- // Handle specific HuggingFace API errors
171
- if (error.statusCode === 404) {
172
- return NextResponse.json(
173
- { ok: false, error: "Commit not found" },
174
- { status: 404 }
175
- );
176
- }
177
-
178
- if (error.statusCode === 403) {
179
- return NextResponse.json(
180
- { ok: false, error: "Access denied to repository" },
181
- { status: 403 }
182
- );
183
- }
184
-
185
- return NextResponse.json(
186
- { ok: false, error: error.message || "Failed to promote version" },
187
- { status: 500 }
188
- );
189
- }
190
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/images/route.ts CHANGED
@@ -1,10 +1,12 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import Project from "@/models/Project";
6
  import dbConnect from "@/lib/mongodb";
7
 
 
 
8
  export async function POST(
9
  req: NextRequest,
10
  { params }: { params: Promise<{ namespace: string; repoId: string }> }
@@ -16,26 +18,22 @@ export async function POST(
16
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
17
  }
18
 
 
19
  const param = await params;
20
  const { namespace, repoId } = param;
21
 
22
- const space = await spaceInfo({
23
- name: `${namespace}/${repoId}`,
24
- accessToken: user.token as string,
25
- additionalFields: ["author"],
26
- });
27
-
28
- if (!space || space.sdk !== "static") {
29
- return NextResponse.json(
30
- { ok: false, error: "Space is not a static space." },
31
- { status: 404 }
32
- );
33
- }
34
 
35
- if (space.author !== user.name) {
36
  return NextResponse.json(
37
- { ok: false, error: "Space does not belong to the authenticated user." },
38
- { status: 403 }
 
 
 
39
  );
40
  }
41
 
 
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 }> }
 
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
 
app/api/me/projects/[namespace]/[repoId]/route.ts CHANGED
@@ -1,10 +1,12 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, listFiles, deleteRepo, listCommits, downloadFile } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
- import { Commit, Page } from "@/types";
 
 
6
 
7
- export async function DELETE(
8
  req: NextRequest,
9
  { params }: { params: Promise<{ namespace: string; repoId: string }> }
10
  ) {
@@ -14,63 +16,23 @@ export async function DELETE(
14
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
15
  }
16
 
 
17
  const param = await params;
18
  const { namespace, repoId } = param;
19
 
20
- try {
21
- const space = await spaceInfo({
22
- name: `${namespace}/${repoId}`,
23
- accessToken: user.token as string,
24
- additionalFields: ["author"],
25
- });
26
-
27
- if (!space || space.sdk !== "static") {
28
- return NextResponse.json(
29
- { ok: false, error: "Space is not a static space." },
30
- { status: 404 }
31
- );
32
- }
33
-
34
- if (space.author !== user.name) {
35
- return NextResponse.json(
36
- { ok: false, error: "Space does not belong to the authenticated user." },
37
- { status: 403 }
38
- );
39
- }
40
-
41
- const repo: RepoDesignation = {
42
- type: "space",
43
- name: `${namespace}/${repoId}`,
44
- };
45
-
46
- await deleteRepo({
47
- repo,
48
- accessToken: user.token as string,
49
- });
50
-
51
-
52
- return NextResponse.json({ ok: true }, { status: 200 });
53
- } catch (error: any) {
54
  return NextResponse.json(
55
- { ok: false, error: error.message },
56
- { status: 500 }
 
 
 
57
  );
58
  }
59
- }
60
-
61
- export async function GET(
62
- req: NextRequest,
63
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
64
- ) {
65
- const user = await isAuthenticated();
66
-
67
- if (user instanceof NextResponse || !user) {
68
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
69
- }
70
-
71
- const param = await params;
72
- const { namespace, repoId } = param;
73
-
74
  try {
75
  const space = await spaceInfo({
76
  name: namespace + "/" + repoId,
@@ -103,49 +65,37 @@ export async function GET(
103
  };
104
 
105
  const htmlFiles: Page[] = [];
106
- const files: string[] = [];
107
 
108
- const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
109
 
110
  for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
111
  if (fileInfo.path.endsWith(".html")) {
112
- const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
113
- const html = await blob?.text();
114
- if (!html) {
115
- continue;
116
- }
117
- if (fileInfo.path === "index.html") {
118
- htmlFiles.unshift({
119
- path: fileInfo.path,
120
- html,
121
- });
122
- } else {
123
  htmlFiles.push({
124
  path: fileInfo.path,
125
- html,
126
- });
 
127
  }
128
  }
129
  if (fileInfo.type === "directory" && fileInfo.path === "images") {
130
  for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
131
- if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
132
- files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
133
  }
134
  }
135
  }
136
  }
137
- const commits: Commit[] = [];
138
- for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
139
- if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
140
- continue;
141
- }
142
- commits.push({
143
- title: commit.title,
144
- oid: commit.oid,
145
- date: commit.date,
146
- });
147
- }
148
-
149
  if (htmlFiles.length === 0) {
150
  return NextResponse.json(
151
  {
@@ -155,17 +105,14 @@ export async function GET(
155
  { status: 404 }
156
  );
157
  }
 
158
  return NextResponse.json(
159
  {
160
  project: {
161
- id: space.id,
162
- space_id: space.name,
163
- private: space.private,
164
- _updatedAt: space.updatedAt,
165
  },
166
- pages: htmlFiles,
167
- files,
168
- commits,
169
  ok: true,
170
  },
171
  { status: 200 }
@@ -174,6 +121,10 @@ export async function GET(
174
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
  } catch (error: any) {
176
  if (error.statusCode === 404) {
 
 
 
 
177
  return NextResponse.json(
178
  { error: "Space not found", ok: false },
179
  { status: 404 }
@@ -185,3 +136,141 @@ export async function GET(
185
  );
186
  }
187
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  ) {
 
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,
 
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
  {
 
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 }
 
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 }
 
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
+ const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
176
+ type: "text/plain",
177
+ });
178
+ files.push(promptsFile);
179
+ pages.forEach((page: Page) => {
180
+ const file = new File([page.html], page.path, { type: "text/html" });
181
+ files.push(file);
182
+ });
183
+ await uploadFiles({
184
+ repo,
185
+ files,
186
+ accessToken: user.token as string,
187
+ commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
188
+ });
189
+
190
+ await Project.updateOne(
191
+ { user_id: user.id, space_id: `${namespace}/${repoId}` },
192
+ {
193
+ $set: {
194
+ prompts: [
195
+ ...prompts,
196
+ ],
197
+ },
198
+ }
199
+ );
200
+ return NextResponse.json({ ok: true }, { status: 200 });
201
+ }
202
+
203
+ export async function POST(
204
+ req: NextRequest,
205
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
206
+ ) {
207
+ const user = await isAuthenticated();
208
+
209
+ if (user instanceof NextResponse || !user) {
210
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
211
+ }
212
+
213
+ await dbConnect();
214
+ const param = await params;
215
+ const { namespace, repoId } = param;
216
+
217
+ const space = await spaceInfo({
218
+ name: namespace + "/" + repoId,
219
+ accessToken: user.token as string,
220
+ additionalFields: ["author"],
221
+ });
222
+
223
+ if (!space || space.sdk !== "static") {
224
+ return NextResponse.json(
225
+ {
226
+ ok: false,
227
+ error: "Space is not a static space",
228
+ },
229
+ { status: 404 }
230
+ );
231
+ }
232
+ if (space.author !== user.name) {
233
+ return NextResponse.json(
234
+ {
235
+ ok: false,
236
+ error: "Space does not belong to the authenticated user",
237
+ },
238
+ { status: 403 }
239
+ );
240
+ }
241
+
242
+ const project = await Project.findOne({
243
+ user_id: user.id,
244
+ space_id: `${namespace}/${repoId}`,
245
+ }).lean();
246
+ if (project) {
247
+ // redirect to the project page if it already exists
248
+ return NextResponse.json(
249
+ {
250
+ ok: false,
251
+ error: "Project already exists",
252
+ redirect: `/projects/${namespace}/${repoId}`,
253
+ },
254
+ { status: 400 }
255
+ );
256
+ }
257
+
258
+ const newProject = new Project({
259
+ user_id: user.id,
260
+ space_id: `${namespace}/${repoId}`,
261
+ prompts: [],
262
+ });
263
+
264
+ await newProject.save();
265
+ return NextResponse.json(
266
+ {
267
+ ok: true,
268
+ project: {
269
+ id: newProject._id,
270
+ space_id: newProject.space_id,
271
+ prompts: newProject.prompts,
272
+ },
273
+ },
274
+ { status: 201 }
275
+ );
276
+ }
app/api/me/projects/[namespace]/[repoId]/save/route.ts DELETED
@@ -1,64 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { uploadFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import { Page } from "@/types";
6
-
7
- export async function PUT(
8
- req: NextRequest,
9
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
10
- ) {
11
- const user = await isAuthenticated();
12
- if (user instanceof NextResponse || !user) {
13
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
14
- }
15
-
16
- const param = await params;
17
- const { namespace, repoId } = param;
18
- const { pages, commitTitle = "Manual changes saved" } = await req.json();
19
-
20
- if (!pages || !Array.isArray(pages) || pages.length === 0) {
21
- return NextResponse.json(
22
- { ok: false, error: "Pages are required" },
23
- { status: 400 }
24
- );
25
- }
26
-
27
- try {
28
- // Prepare files for upload
29
- const files: File[] = [];
30
- pages.forEach((page: Page) => {
31
- const file = new File([page.html], page.path, { type: "text/html" });
32
- files.push(file);
33
- });
34
-
35
- // Upload files to HuggingFace Hub
36
- const response = await uploadFiles({
37
- repo: {
38
- type: "space",
39
- name: `${namespace}/${repoId}`,
40
- },
41
- files,
42
- commitTitle,
43
- accessToken: user.token as string,
44
- });
45
-
46
- return NextResponse.json({
47
- ok: true,
48
- pages,
49
- commit: {
50
- ...response.commit,
51
- title: commitTitle,
52
- }
53
- });
54
- } catch (error: any) {
55
- console.error("Error saving manual changes:", error);
56
- return NextResponse.json(
57
- {
58
- ok: false,
59
- error: error.message || "Failed to save changes",
60
- },
61
- { status: 500 }
62
- );
63
- }
64
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/route.ts CHANGED
@@ -1,107 +1,127 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
- import { Commit, Page } from "@/types";
 
6
  import { COLORS } from "@/lib/utils";
 
7
 
8
- export async function POST(
9
- req: NextRequest,
10
- ) {
11
  const user = await isAuthenticated();
 
12
  if (user instanceof NextResponse || !user) {
13
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
14
  }
15
 
16
- const { title: titleFromRequest, pages, prompt } = await req.json();
17
-
18
- const title = titleFromRequest ?? "DeepSite Project";
19
-
20
- const formattedTitle = title
21
- .toLowerCase()
22
- .replace(/[^a-z0-9]+/g, "-")
23
- .split("-")
24
- .filter(Boolean)
25
- .join("-")
26
- .slice(0, 96);
27
-
28
- const repo: RepoDesignation = {
29
- type: "space",
30
- name: `${user.name}/${formattedTitle}`,
31
- };
32
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
33
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
34
- const README = `---
35
- title: ${title}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  colorFrom: ${colorFrom}
37
  colorTo: ${colorTo}
38
- emoji: 🐳
39
  sdk: static
40
  pinned: false
41
  tags:
42
- - deepsite-v3
43
  ---
44
 
45
- # Welcome to your new DeepSite project!
46
- This project was created with [DeepSite](https://deepsite.hf.co).
47
- `;
48
 
49
- const files: File[] = [];
50
- const readmeFile = new File([README], "README.md", { type: "text/markdown" });
51
- files.push(readmeFile);
52
- pages.forEach((page: Page) => {
53
- const file = new File([page.html], page.path, { type: "text/html" });
54
- files.push(file);
55
- });
56
-
57
- try {
58
- const { repoUrl} = await createRepo({
59
- repo,
60
- accessToken: user.token as string,
61
  });
62
- const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
63
  await uploadFiles({
64
  repo,
65
  files,
66
  accessToken: user.token as string,
67
- commitTitle
68
  });
69
-
70
  const path = repoUrl.split("/").slice(-2).join("/");
71
-
72
- const commits: Commit[] = [];
73
- for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
74
- if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
75
- continue;
76
- }
77
- commits.push({
78
- title: commit.title,
79
- oid: commit.oid,
80
- date: commit.date,
81
- });
82
- }
83
-
84
- const space = await spaceInfo({
85
- name: repo.name,
86
- accessToken: user.token as string,
87
  });
88
-
89
- let newProject = {
90
- files,
91
- pages,
92
- commits,
93
- project: {
94
- id: space.id,
95
- space_id: space.name,
96
- _updatedAt: space.updatedAt,
97
- }
98
- }
99
-
100
- return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
101
  } catch (err: any) {
102
  return NextResponse.json(
103
  { error: err.message, ok: false },
104
  { status: 500 }
105
  );
106
  }
107
- }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
+ import Project from "@/models/Project";
6
+ import dbConnect from "@/lib/mongodb";
7
  import { COLORS } 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 promptsFile = new File([prompts.join("\n")], "prompts.txt", {
100
+ type: "text/plain",
101
+ });
102
+ const files = [readmeFile, promptsFile];
103
+ pages.forEach((page: Page) => {
104
+ const file = new File([page.html], page.path, { type: "text/html" });
105
+ files.push(file);
 
 
106
  });
 
107
  await uploadFiles({
108
  repo,
109
  files,
110
  accessToken: user.token as string,
111
+ commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
112
  });
 
113
  const path = repoUrl.split("/").slice(-2).join("/");
114
+ const project = await Project.create({
115
+ user_id: user.id,
116
+ space_id: path,
117
+ prompts,
 
 
 
 
 
 
 
 
 
 
 
 
118
  });
119
+ return NextResponse.json({ project, path, ok: true }, { status: 201 });
120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
 
 
 
 
 
 
 
 
 
 
 
121
  } catch (err: any) {
122
  return NextResponse.json(
123
  { error: err.message, ok: false },
124
  { status: 500 }
125
  );
126
  }
127
+ }
app/api/me/route.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { listSpaces } from "@huggingface/hub";
2
  import { headers } from "next/headers";
3
  import { NextResponse } from "next/server";
4
 
@@ -22,25 +21,5 @@ export async function GET() {
22
  );
23
  }
24
  const user = await userResponse.json();
25
- const projects = [];
26
- for await (const space of listSpaces({
27
- accessToken: token.replace("Bearer ", "") as string,
28
- additionalFields: ["author", "cardData"],
29
- search: {
30
- owner: user.name,
31
- }
32
- })) {
33
- if (
34
- space.sdk === "static" &&
35
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
36
- (
37
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
38
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
39
- )
40
- ) {
41
- projects.push(space);
42
- }
43
- }
44
-
45
- return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
46
  }
 
 
1
  import { headers } from "next/headers";
2
  import { NextResponse } from "next/server";
3
 
 
21
  );
22
  }
23
  const user = await userResponse.json();
24
+ return NextResponse.json({ user, errCode: null }, { status: 200 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
app/auth/callback/page.tsx CHANGED
@@ -5,92 +5,67 @@ import { use, useState } from "react";
5
  import { useMount, useTimeoutFn } from "react-use";
6
 
7
  import { Button } from "@/components/ui/button";
8
- import { AnimatedBlobs } from "@/components/animated-blobs";
9
- import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
10
  export default function AuthCallback({
11
  searchParams,
12
  }: {
13
  searchParams: Promise<{ code: string }>;
14
  }) {
15
  const [showButton, setShowButton] = useState(false);
16
- const [isPopupAuth, setIsPopupAuth] = useState(false);
17
  const { code } = use(searchParams);
18
  const { loginFromCode } = useUser();
19
- const { postMessage } = useBroadcastChannel("auth", () => {});
20
 
21
  useMount(async () => {
22
  if (code) {
23
- const isPopup = window.opener || window.parent !== window;
24
- setIsPopupAuth(isPopup);
25
-
26
- if (isPopup) {
27
- postMessage({
28
- type: "user-oauth",
29
- code: code,
30
- });
31
-
32
- setTimeout(() => {
33
- if (window.opener) {
34
- window.close();
35
- }
36
- }, 1000);
37
- } else {
38
- await loginFromCode(code);
39
- }
40
  }
41
  });
42
 
43
- useTimeoutFn(() => setShowButton(true), 7000);
 
 
 
44
 
45
  return (
46
- <div className="h-screen flex flex-col justify-center items-center bg-neutral-950 z-1 relative">
47
- <div className="background__noisy" />
48
- <div className="relative max-w-4xl py-10 flex items-center justify-center w-full">
49
- <div className="max-w-lg mx-auto !rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
50
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
51
- <div className="flex items-center justify-center -space-x-4 mb-3">
52
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
53
- 🚀
54
- </div>
55
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
56
- 👋
57
- </div>
58
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
59
- 🙌
60
- </div>
61
  </div>
62
- <p className="text-xl font-semibold text-neutral-950">
63
- {isPopupAuth
64
- ? "Authentication Complete!"
65
- : "Login In Progress..."}
66
- </p>
67
- <p className="text-sm text-neutral-500 mt-1.5">
68
- {isPopupAuth
69
- ? "You can now close this tab and return to the previous page."
70
- : "Wait a moment while we log you in with your code."}
 
 
 
 
 
 
 
 
 
 
71
  </p>
72
- </header>
73
- <main className="space-y-4 p-6">
74
- <div>
75
- <p className="text-sm text-neutral-700 mb-4 max-w-xs">
76
- If you are not redirected automatically in the next 5 seconds,
77
- please click the button below
 
 
 
78
  </p>
79
- {showButton ? (
80
- <Link href="/">
81
- <Button variant="black" className="relative">
82
- Go to Home
83
- </Button>
84
- </Link>
85
- ) : (
86
- <p className="text-xs text-neutral-500">
87
- Please wait, we are logging you in...
88
- </p>
89
- )}
90
- </div>
91
- </main>
92
- </div>
93
- <AnimatedBlobs />
94
  </div>
95
  </div>
96
  );
 
5
  import { useMount, useTimeoutFn } from "react-use";
6
 
7
  import { Button } from "@/components/ui/button";
 
 
8
  export default function AuthCallback({
9
  searchParams,
10
  }: {
11
  searchParams: Promise<{ code: string }>;
12
  }) {
13
  const [showButton, setShowButton] = useState(false);
 
14
  const { code } = use(searchParams);
15
  const { loginFromCode } = useUser();
 
16
 
17
  useMount(async () => {
18
  if (code) {
19
+ await loginFromCode(code);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
  });
22
 
23
+ useTimeoutFn(
24
+ () => setShowButton(true),
25
+ 7000 // Show button after 5 seconds
26
+ );
27
 
28
  return (
29
+ <div className="h-screen flex flex-col justify-center items-center">
30
+ <div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
31
+ <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
32
+ <div className="flex items-center justify-center -space-x-4 mb-3">
33
+ <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
34
+ 🚀
 
 
 
 
 
 
 
 
 
35
  </div>
36
+ <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
37
+ 👋
38
+ </div>
39
+ <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
40
+ 🙌
41
+ </div>
42
+ </div>
43
+ <p className="text-xl font-semibold text-neutral-950">
44
+ Login In Progress...
45
+ </p>
46
+ <p className="text-sm text-neutral-500 mt-1.5">
47
+ Wait a moment while we log you in with your code.
48
+ </p>
49
+ </header>
50
+ <main className="space-y-4 p-6">
51
+ <div>
52
+ <p className="text-sm text-neutral-700 mb-4 max-w-xs">
53
+ If you are not redirected automatically in the next 5 seconds,
54
+ please click the button below
55
  </p>
56
+ {showButton ? (
57
+ <Link href="/">
58
+ <Button variant="black" className="relative">
59
+ Go to Home
60
+ </Button>
61
+ </Link>
62
+ ) : (
63
+ <p className="text-xs text-neutral-500">
64
+ Please wait, we are logging you in...
65
  </p>
66
+ )}
67
+ </div>
68
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
69
  </div>
70
  </div>
71
  );
app/layout.tsx CHANGED
@@ -2,18 +2,15 @@
2
  import type { Metadata, Viewport } from "next";
3
  import { Inter, PT_Sans } from "next/font/google";
4
  import { cookies } from "next/headers";
5
- import Script from "next/script";
6
 
 
7
  import "@/assets/globals.css";
8
  import { Toaster } from "@/components/ui/sonner";
9
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
10
  import { apiServer } from "@/lib/api";
11
- import IframeDetector from "@/components/iframe-detector";
12
  import AppContext from "@/components/contexts/app-context";
13
- import TanstackContext from "@/components/contexts/tanstack-query-context";
14
- import { LoginProvider } from "@/components/contexts/login-context";
15
- import { ProProvider } from "@/components/contexts/pro-context";
16
- import { generateSEO, generateStructuredData } from "@/lib/seo";
17
 
18
  const inter = Inter({
19
  variable: "--font-inter-sans",
@@ -27,12 +24,31 @@ const ptSans = PT_Sans({
27
  });
28
 
29
  export const metadata: Metadata = {
30
- ...generateSEO({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- path: "/",
35
- }),
36
  appleWebApp: {
37
  capable: true,
38
  title: "DeepSite",
@@ -43,9 +59,6 @@ export const metadata: Metadata = {
43
  shortcut: "/logo.svg",
44
  apple: "/logo.svg",
45
  },
46
- verification: {
47
- google: process.env.GOOGLE_SITE_VERIFICATION,
48
- },
49
  };
50
 
51
  export const viewport: Viewport = {
@@ -57,54 +70,29 @@ export const viewport: Viewport = {
57
  async function getMe() {
58
  const cookieStore = await cookies();
59
  const token = cookieStore.get(MY_TOKEN_KEY())?.value;
60
- if (!token) return { user: null, projects: [], errCode: null };
61
  try {
62
  const res = await apiServer.get("/me", {
63
  headers: {
64
  Authorization: `Bearer ${token}`,
65
  },
66
  });
67
- return { user: res.data.user, projects: res.data.projects, errCode: null };
68
  } catch (err: any) {
69
- return { user: null, projects: [], errCode: err.status };
70
  }
71
  }
72
 
 
 
73
  export default async function RootLayout({
74
  children,
75
  }: Readonly<{
76
  children: React.ReactNode;
77
  }>) {
78
  const data = await getMe();
79
-
80
- // Generate structured data
81
- const structuredData = generateStructuredData("WebApplication", {
82
- name: "DeepSite",
83
- description: "Build websites with AI, no code required",
84
- url: "https://deepsite.hf.co",
85
- });
86
-
87
- const organizationData = generateStructuredData("Organization", {
88
- name: "DeepSite",
89
- url: "https://deepsite.hf.co",
90
- });
91
-
92
  return (
93
  <html lang="en">
94
- <head>
95
- <script
96
- type="application/ld+json"
97
- dangerouslySetInnerHTML={{
98
- __html: JSON.stringify(structuredData),
99
- }}
100
- />
101
- <script
102
- type="application/ld+json"
103
- dangerouslySetInnerHTML={{
104
- __html: JSON.stringify(organizationData),
105
- }}
106
- />
107
- </head>
108
  <Script
109
  defer
110
  data-domain="deepsite.hf.co"
@@ -115,13 +103,9 @@ export default async function RootLayout({
115
  >
116
  <IframeDetector />
117
  <Toaster richColors position="bottom-center" />
118
- <TanstackContext>
119
- <AppContext me={data}>
120
- <LoginProvider>
121
- <ProProvider>{children}</ProProvider>
122
- </LoginProvider>
123
- </AppContext>
124
- </TanstackContext>
125
  </body>
126
  </html>
127
  );
 
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",
 
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",
 
59
  shortcut: "/logo.svg",
60
  apple: "/logo.svg",
61
  },
 
 
 
62
  };
63
 
64
  export const viewport: Viewport = {
 
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"
 
103
  >
104
  <IframeDetector />
105
  <Toaster richColors position="bottom-center" />
106
+ <TanstackProvider>
107
+ <AppContext me={data}>{children}</AppContext>
108
+ </TanstackProvider>
 
 
 
 
109
  </body>
110
  </html>
111
  );
app/new/page.tsx DELETED
@@ -1,14 +0,0 @@
1
- import { AppEditor } from "@/components/editor";
2
- import { Metadata } from "next";
3
- import { generateSEO } from "@/lib/seo";
4
-
5
- export const metadata: Metadata = generateSEO({
6
- title: "Create New Project - DeepSite",
7
- description:
8
- "Start building your next website with AI. Create a new project on DeepSite and experience the power of AI-driven web development.",
9
- path: "/new",
10
- });
11
-
12
- export default function NewProjectPage() {
13
- return <AppEditor isNew />;
14
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/projects/[namespace]/[repoId]/page.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { AppEditor } from "@/components/editor";
2
+
3
+ export default function ProjectsNewPage() {
4
+ return <AppEditor isNew />;
5
+ }
app/sitemap.ts DELETED
@@ -1,28 +0,0 @@
1
- import { MetadataRoute } from 'next';
2
-
3
- export default function sitemap(): MetadataRoute.Sitemap {
4
- const baseUrl = 'https://deepsite.hf.co';
5
-
6
- return [
7
- {
8
- url: baseUrl,
9
- lastModified: new Date(),
10
- changeFrequency: 'daily',
11
- priority: 1,
12
- },
13
- {
14
- url: `${baseUrl}/new`,
15
- lastModified: new Date(),
16
- changeFrequency: 'weekly',
17
- priority: 0.8,
18
- },
19
- {
20
- url: `${baseUrl}/auth`,
21
- lastModified: new Date(),
22
- changeFrequency: 'monthly',
23
- priority: 0.5,
24
- },
25
- // Note: Dynamic project routes will be handled by Next.js automatically
26
- // but you can add specific high-priority project pages here if needed
27
- ];
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/deepseek.svg DELETED
assets/globals.css CHANGED
@@ -112,10 +112,6 @@
112
  --sidebar-ring: oklch(0.556 0 0);
113
  }
114
 
115
- body {
116
- @apply scroll-smooth
117
- }
118
-
119
  @layer base {
120
  * {
121
  @apply border-border outline-ring/50;
@@ -148,224 +144,3 @@ body {
148
  .matched-line {
149
  @apply bg-sky-500/30;
150
  }
151
-
152
- /* Fast liquid deformation animations */
153
- @keyframes liquidBlob1 {
154
- 0%, 100% {
155
- border-radius: 40% 60% 50% 50%;
156
- transform: scaleX(1) scaleY(1) rotate(0deg);
157
- }
158
- 12.5% {
159
- border-radius: 20% 80% 70% 30%;
160
- transform: scaleX(1.6) scaleY(0.4) rotate(25deg);
161
- }
162
- 25% {
163
- border-radius: 80% 20% 30% 70%;
164
- transform: scaleX(0.5) scaleY(2.1) rotate(-15deg);
165
- }
166
- 37.5% {
167
- border-radius: 30% 70% 80% 20%;
168
- transform: scaleX(1.8) scaleY(0.6) rotate(40deg);
169
- }
170
- 50% {
171
- border-radius: 70% 30% 20% 80%;
172
- transform: scaleX(0.4) scaleY(1.9) rotate(-30deg);
173
- }
174
- 62.5% {
175
- border-radius: 25% 75% 60% 40%;
176
- transform: scaleX(1.5) scaleY(0.7) rotate(55deg);
177
- }
178
- 75% {
179
- border-radius: 75% 25% 40% 60%;
180
- transform: scaleX(0.6) scaleY(1.7) rotate(-10deg);
181
- }
182
- 87.5% {
183
- border-radius: 50% 50% 75% 25%;
184
- transform: scaleX(1.3) scaleY(0.8) rotate(35deg);
185
- }
186
- }
187
-
188
- @keyframes liquidBlob2 {
189
- 0%, 100% {
190
- border-radius: 60% 40% 50% 50%;
191
- transform: scaleX(1) scaleY(1) rotate(12deg);
192
- }
193
- 16% {
194
- border-radius: 15% 85% 60% 40%;
195
- transform: scaleX(0.3) scaleY(2.3) rotate(50deg);
196
- }
197
- 32% {
198
- border-radius: 85% 15% 25% 75%;
199
- transform: scaleX(2.0) scaleY(0.5) rotate(-20deg);
200
- }
201
- 48% {
202
- border-radius: 30% 70% 85% 15%;
203
- transform: scaleX(0.4) scaleY(1.8) rotate(70deg);
204
- }
205
- 64% {
206
- border-radius: 70% 30% 15% 85%;
207
- transform: scaleX(1.9) scaleY(0.6) rotate(-35deg);
208
- }
209
- 80% {
210
- border-radius: 40% 60% 70% 30%;
211
- transform: scaleX(0.7) scaleY(1.6) rotate(45deg);
212
- }
213
- }
214
-
215
- @keyframes liquidBlob3 {
216
- 0%, 100% {
217
- border-radius: 50% 50% 40% 60%;
218
- transform: scaleX(1) scaleY(1) rotate(0deg);
219
- }
220
- 20% {
221
- border-radius: 10% 90% 75% 25%;
222
- transform: scaleX(2.2) scaleY(0.3) rotate(-45deg);
223
- }
224
- 40% {
225
- border-radius: 90% 10% 20% 80%;
226
- transform: scaleX(0.4) scaleY(2.5) rotate(60deg);
227
- }
228
- 60% {
229
- border-radius: 25% 75% 90% 10%;
230
- transform: scaleX(1.7) scaleY(0.5) rotate(-25deg);
231
- }
232
- 80% {
233
- border-radius: 75% 25% 10% 90%;
234
- transform: scaleX(0.6) scaleY(2.0) rotate(80deg);
235
- }
236
- }
237
-
238
- @keyframes liquidBlob4 {
239
- 0%, 100% {
240
- border-radius: 45% 55% 50% 50%;
241
- transform: scaleX(1) scaleY(1) rotate(-15deg);
242
- }
243
- 14% {
244
- border-radius: 90% 10% 65% 35%;
245
- transform: scaleX(0.2) scaleY(2.8) rotate(35deg);
246
- }
247
- 28% {
248
- border-radius: 10% 90% 20% 80%;
249
- transform: scaleX(2.4) scaleY(0.4) rotate(-50deg);
250
- }
251
- 42% {
252
- border-radius: 35% 65% 90% 10%;
253
- transform: scaleX(0.3) scaleY(2.1) rotate(70deg);
254
- }
255
- 56% {
256
- border-radius: 80% 20% 10% 90%;
257
- transform: scaleX(2.0) scaleY(0.5) rotate(-40deg);
258
- }
259
- 70% {
260
- border-radius: 20% 80% 55% 45%;
261
- transform: scaleX(0.5) scaleY(1.9) rotate(55deg);
262
- }
263
- 84% {
264
- border-radius: 65% 35% 80% 20%;
265
- transform: scaleX(1.6) scaleY(0.6) rotate(-25deg);
266
- }
267
- }
268
-
269
- /* Fast flowing movement animations */
270
- @keyframes liquidFlow1 {
271
- 0%, 100% { transform: translate(0, 0); }
272
- 16% { transform: translate(60px, -40px); }
273
- 32% { transform: translate(-45px, -70px); }
274
- 48% { transform: translate(80px, 25px); }
275
- 64% { transform: translate(-30px, 60px); }
276
- 80% { transform: translate(50px, -20px); }
277
- }
278
-
279
- @keyframes liquidFlow2 {
280
- 0%, 100% { transform: translate(0, 0); }
281
- 20% { transform: translate(-70px, 50px); }
282
- 40% { transform: translate(90px, -30px); }
283
- 60% { transform: translate(-40px, -55px); }
284
- 80% { transform: translate(65px, 35px); }
285
- }
286
-
287
- @keyframes liquidFlow3 {
288
- 0%, 100% { transform: translate(0, 0); }
289
- 12% { transform: translate(-50px, -60px); }
290
- 24% { transform: translate(40px, -20px); }
291
- 36% { transform: translate(-30px, 70px); }
292
- 48% { transform: translate(70px, 20px); }
293
- 60% { transform: translate(-60px, -35px); }
294
- 72% { transform: translate(35px, 55px); }
295
- 84% { transform: translate(-25px, -45px); }
296
- }
297
-
298
- @keyframes liquidFlow4 {
299
- 0%, 100% { transform: translate(0, 0); }
300
- 14% { transform: translate(50px, 60px); }
301
- 28% { transform: translate(-80px, -40px); }
302
- 42% { transform: translate(30px, -90px); }
303
- 56% { transform: translate(-55px, 45px); }
304
- 70% { transform: translate(75px, -25px); }
305
- 84% { transform: translate(-35px, 65px); }
306
- }
307
-
308
- /* Light sweep animation for buttons */
309
- @keyframes lightSweep {
310
- 0% {
311
- transform: translateX(-150%);
312
- opacity: 0;
313
- }
314
- 8% {
315
- opacity: 0.3;
316
- }
317
- 25% {
318
- opacity: 0.8;
319
- }
320
- 42% {
321
- opacity: 0.3;
322
- }
323
- 50% {
324
- transform: translateX(150%);
325
- opacity: 0;
326
- }
327
- 58% {
328
- opacity: 0.3;
329
- }
330
- 75% {
331
- opacity: 0.8;
332
- }
333
- 92% {
334
- opacity: 0.3;
335
- }
336
- 100% {
337
- transform: translateX(-150%);
338
- opacity: 0;
339
- }
340
- }
341
-
342
- .light-sweep {
343
- position: relative;
344
- overflow: hidden;
345
- }
346
-
347
- .light-sweep::before {
348
- content: '';
349
- position: absolute;
350
- top: 0;
351
- left: 0;
352
- right: 0;
353
- bottom: 0;
354
- width: 300%;
355
- background: linear-gradient(
356
- 90deg,
357
- transparent 0%,
358
- transparent 20%,
359
- rgba(56, 189, 248, 0.1) 35%,
360
- rgba(56, 189, 248, 0.2) 45%,
361
- rgba(255, 255, 255, 0.2) 50%,
362
- rgba(168, 85, 247, 0.2) 55%,
363
- rgba(168, 85, 247, 0.1) 65%,
364
- transparent 80%,
365
- transparent 100%
366
- );
367
- animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite;
368
- pointer-events: none;
369
- z-index: 1;
370
- filter: blur(1px);
371
- }
 
112
  --sidebar-ring: oklch(0.556 0 0);
113
  }
114
 
 
 
 
 
115
  @layer base {
116
  * {
117
  @apply border-border outline-ring/50;
 
144
  .matched-line {
145
  @apply bg-sky-500/30;
146
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/kimi.svg DELETED
assets/qwen.svg DELETED
assets/zai.svg DELETED
components.json CHANGED
@@ -5,7 +5,7 @@
5
  "tsx": true,
6
  "tailwind": {
7
  "config": "",
8
- "css": "assets/globals.css",
9
  "baseColor": "neutral",
10
  "cssVariables": true,
11
  "prefix": ""
 
5
  "tsx": true,
6
  "tailwind": {
7
  "config": "",
8
+ "css": "app/globals.css",
9
  "baseColor": "neutral",
10
  "cssVariables": true,
11
  "prefix": ""
components/animated-blobs/index.tsx DELETED
@@ -1,34 +0,0 @@
1
- export function AnimatedBlobs() {
2
- return (
3
- <div className="absolute inset-0 pointer-events-none -z-[1]">
4
- <div
5
- className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl"
6
- style={{
7
- animation:
8
- "liquidBlob1 4s ease-in-out infinite, liquidFlow1 6s ease-in-out infinite",
9
- }}
10
- />
11
- <div
12
- className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10"
13
- style={{
14
- animation:
15
- "liquidBlob2 5s ease-in-out infinite, liquidFlow2 7s ease-in-out infinite",
16
- }}
17
- />
18
- <div
19
- className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10"
20
- style={{
21
- animation:
22
- "liquidBlob3 3.5s ease-in-out infinite, liquidFlow3 8s ease-in-out infinite",
23
- }}
24
- />
25
- <div
26
- className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3"
27
- style={{
28
- animation:
29
- "liquidBlob4 4.5s ease-in-out infinite, liquidFlow4 6.5s ease-in-out infinite",
30
- }}
31
- />
32
- </div>
33
- );
34
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/animated-text/index.tsx DELETED
@@ -1,123 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect } from "react";
4
-
5
- interface AnimatedTextProps {
6
- className?: string;
7
- }
8
-
9
- export function AnimatedText({ className = "" }: AnimatedTextProps) {
10
- const [displayText, setDisplayText] = useState("");
11
- const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
12
- const [isTyping, setIsTyping] = useState(true);
13
- const [showCursor, setShowCursor] = useState(true);
14
- const [lastTypedIndex, setLastTypedIndex] = useState(-1);
15
- const [animationComplete, setAnimationComplete] = useState(false);
16
-
17
- // Randomize suggestions on each component mount
18
- const [suggestions] = useState(() => {
19
- const baseSuggestions = [
20
- "create a stunning portfolio!",
21
- "build a tic tac toe game!",
22
- "design a website for my restaurant!",
23
- "make a sleek landing page!",
24
- "build an e-commerce store!",
25
- "create a personal blog!",
26
- "develop a modern dashboard!",
27
- "design a company website!",
28
- "build a todo app!",
29
- "create an online gallery!",
30
- "make a contact form!",
31
- "build a weather app!",
32
- ];
33
-
34
- // Fisher-Yates shuffle algorithm
35
- const shuffled = [...baseSuggestions];
36
- for (let i = shuffled.length - 1; i > 0; i--) {
37
- const j = Math.floor(Math.random() * (i + 1));
38
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
39
- }
40
-
41
- return shuffled;
42
- });
43
-
44
- useEffect(() => {
45
- if (animationComplete) return;
46
-
47
- let timeout: NodeJS.Timeout;
48
-
49
- const typeText = () => {
50
- const currentSuggestion = suggestions[currentSuggestionIndex];
51
-
52
- if (isTyping) {
53
- if (displayText.length < currentSuggestion.length) {
54
- setDisplayText(currentSuggestion.slice(0, displayText.length + 1));
55
- setLastTypedIndex(displayText.length);
56
- timeout = setTimeout(typeText, 80);
57
- } else {
58
- // Finished typing, wait then start erasing
59
- setLastTypedIndex(-1);
60
- timeout = setTimeout(() => {
61
- setIsTyping(false);
62
- }, 2000);
63
- }
64
- }
65
- };
66
-
67
- timeout = setTimeout(typeText, 100);
68
- return () => clearTimeout(timeout);
69
- }, [
70
- displayText,
71
- currentSuggestionIndex,
72
- isTyping,
73
- suggestions,
74
- animationComplete,
75
- ]);
76
-
77
- // Cursor blinking effect
78
- useEffect(() => {
79
- if (animationComplete) {
80
- setShowCursor(false);
81
- return;
82
- }
83
-
84
- const cursorInterval = setInterval(() => {
85
- setShowCursor((prev) => !prev);
86
- }, 600);
87
-
88
- return () => clearInterval(cursorInterval);
89
- }, [animationComplete]);
90
-
91
- useEffect(() => {
92
- if (lastTypedIndex >= 0) {
93
- const timeout = setTimeout(() => {
94
- setLastTypedIndex(-1);
95
- }, 400);
96
-
97
- return () => clearTimeout(timeout);
98
- }
99
- }, [lastTypedIndex]);
100
-
101
- return (
102
- <p className={`font-mono ${className}`}>
103
- Hey DeepSite,&nbsp;
104
- {displayText.split("").map((char, index) => (
105
- <span
106
- key={`${currentSuggestionIndex}-${index}`}
107
- className={`transition-colors duration-300 ${
108
- index === lastTypedIndex ? "text-neutral-100" : ""
109
- }`}
110
- >
111
- {char}
112
- </span>
113
- ))}
114
- <span
115
- className={`${
116
- showCursor ? "opacity-100" : "opacity-0"
117
- } transition-opacity`}
118
- >
119
- |
120
- </span>
121
- </p>
122
- );
123
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/contexts/app-context.tsx CHANGED
@@ -1,11 +1,12 @@
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
  "use client";
3
- import { useMount } from "react-use";
4
- import { toast } from "sonner";
5
- import { usePathname, useRouter } from "next/navigation";
6
 
7
  import { useUser } from "@/hooks/useUser";
8
- import { ProjectType, User } from "@/types";
 
 
 
 
9
  import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
10
 
11
  export default function AppContext({
@@ -15,7 +16,6 @@ export default function AppContext({
15
  children: React.ReactNode;
16
  me?: {
17
  user: User | null;
18
- projects: ProjectType[];
19
  errCode: number | null;
20
  };
21
  }) {
@@ -49,5 +49,9 @@ export default function AppContext({
49
  }
50
  });
51
 
52
- return children;
 
 
 
 
53
  }
 
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
  "use client";
 
 
 
3
 
4
  import { useUser } from "@/hooks/useUser";
5
+ import { usePathname, useRouter } from "next/navigation";
6
+ import { useMount } from "react-use";
7
+ import { UserContext } from "@/components/contexts/user-context";
8
+ import { User } from "@/types";
9
+ import { toast } from "sonner";
10
  import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
11
 
12
  export default function AppContext({
 
16
  children: React.ReactNode;
17
  me?: {
18
  user: User | null;
 
19
  errCode: number | null;
20
  };
21
  }) {
 
49
  }
50
  });
51
 
52
+ return (
53
+ <UserContext value={{ user, loading, logout } as any}>
54
+ {children}
55
+ </UserContext>
56
+ );
57
  }
components/contexts/login-context.tsx DELETED
@@ -1,62 +0,0 @@
1
- "use client";
2
-
3
- import React, { createContext, useContext, useState, ReactNode } from "react";
4
- import { LoginModal } from "@/components/login-modal";
5
- import { Page } from "@/types";
6
-
7
- interface LoginContextType {
8
- isOpen: boolean;
9
- openLoginModal: (options?: LoginModalOptions) => void;
10
- closeLoginModal: () => void;
11
- }
12
-
13
- interface LoginModalOptions {
14
- pages?: Page[];
15
- title?: string;
16
- prompt?: string;
17
- description?: string;
18
- }
19
-
20
- const LoginContext = createContext<LoginContextType | undefined>(undefined);
21
-
22
- export function LoginProvider({ children }: { children: ReactNode }) {
23
- const [isOpen, setIsOpen] = useState(false);
24
- const [modalOptions, setModalOptions] = useState<LoginModalOptions>({});
25
-
26
- const openLoginModal = (options: LoginModalOptions = {}) => {
27
- setModalOptions(options);
28
- setIsOpen(true);
29
- };
30
-
31
- const closeLoginModal = () => {
32
- setIsOpen(false);
33
- setModalOptions({});
34
- };
35
-
36
- const value = {
37
- isOpen,
38
- openLoginModal,
39
- closeLoginModal,
40
- };
41
-
42
- return (
43
- <LoginContext.Provider value={value}>
44
- {children}
45
- <LoginModal
46
- open={isOpen}
47
- onClose={setIsOpen}
48
- title={modalOptions.title}
49
- prompt={modalOptions.prompt}
50
- description={modalOptions.description}
51
- />
52
- </LoginContext.Provider>
53
- );
54
- }
55
-
56
- export function useLoginModal() {
57
- const context = useContext(LoginContext);
58
- if (context === undefined) {
59
- throw new Error("useLoginModal must be used within a LoginProvider");
60
- }
61
- return context;
62
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/contexts/pro-context.tsx DELETED
@@ -1,48 +0,0 @@
1
- "use client";
2
-
3
- import React, { createContext, useContext, useState, ReactNode } from "react";
4
- import { ProModal } from "@/components/pro-modal";
5
- import { Page } from "@/types";
6
- import { useEditor } from "@/hooks/useEditor";
7
-
8
- interface ProContextType {
9
- isOpen: boolean;
10
- openProModal: (pages: Page[]) => void;
11
- closeProModal: () => void;
12
- }
13
-
14
- const ProContext = createContext<ProContextType | undefined>(undefined);
15
-
16
- export function ProProvider({ children }: { children: ReactNode }) {
17
- const [isOpen, setIsOpen] = useState(false);
18
- const { pages } = useEditor();
19
-
20
- const openProModal = () => {
21
- setIsOpen(true);
22
- };
23
-
24
- const closeProModal = () => {
25
- setIsOpen(false);
26
- };
27
-
28
- const value = {
29
- isOpen,
30
- openProModal,
31
- closeProModal,
32
- };
33
-
34
- return (
35
- <ProContext.Provider value={value}>
36
- {children}
37
- <ProModal open={isOpen} onClose={setIsOpen} pages={pages} />
38
- </ProContext.Provider>
39
- );
40
- }
41
-
42
- export function useProModal() {
43
- const context = useContext(ProContext);
44
- if (context === undefined) {
45
- throw new Error("useProModal must be used within a ProProvider");
46
- }
47
- return context;
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/fake-ask.tsx DELETED
@@ -1,97 +0,0 @@
1
- import { useState } from "react";
2
- import { useLocalStorage } from "react-use";
3
- import { ArrowUp, Dice6 } from "lucide-react";
4
- import { useRouter } from "next/navigation";
5
-
6
- import { Button } from "@/components/ui/button";
7
- import { PromptBuilder } from "./prompt-builder";
8
- import { EnhancedSettings } from "@/types";
9
- import { Settings } from "./settings";
10
- import classNames from "classnames";
11
- import { PROMPTS_FOR_AI } from "@/lib/prompts";
12
-
13
- export const FakeAskAi = () => {
14
- const router = useRouter();
15
- const [prompt, setPrompt] = useState("");
16
- const [openProvider, setOpenProvider] = useState(false);
17
- const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
18
- useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
19
- isActive: true,
20
- primaryColor: undefined,
21
- secondaryColor: undefined,
22
- theme: undefined,
23
- });
24
- const [, setPromptStorage] = useLocalStorage("prompt", "");
25
- const [randomPromptLoading, setRandomPromptLoading] = useState(false);
26
-
27
- const callAi = async () => {
28
- setPromptStorage(prompt);
29
- router.push("/new");
30
- };
31
-
32
- const randomPrompt = () => {
33
- setRandomPromptLoading(true);
34
- setTimeout(() => {
35
- setPrompt(
36
- PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
37
- );
38
- setRandomPromptLoading(false);
39
- }, 400);
40
- };
41
-
42
- return (
43
- <div className="p-3 w-full max-w-xl mx-auto">
44
- <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
45
- <div className="w-full relative flex items-start justify-between pr-4 pt-4">
46
- <textarea
47
- className="w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 px-4 pb-4 resize-none"
48
- placeholder="Ask DeepSite anything..."
49
- value={prompt}
50
- onChange={(e) => setPrompt(e.target.value)}
51
- onKeyDown={(e) => {
52
- if (e.key === "Enter" && !e.shiftKey) {
53
- callAi();
54
- }
55
- }}
56
- />
57
- <Button
58
- size="iconXs"
59
- variant="outline"
60
- className="!rounded-md"
61
- onClick={() => randomPrompt()}
62
- >
63
- <Dice6
64
- className={classNames("size-4", {
65
- "animate-spin animation-duration-500": randomPromptLoading,
66
- })}
67
- />
68
- </Button>
69
- </div>
70
- <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
71
- <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
72
- <PromptBuilder
73
- enhancedSettings={enhancedSettings!}
74
- setEnhancedSettings={setEnhancedSettings}
75
- />
76
- <Settings
77
- open={openProvider}
78
- isFollowUp={false}
79
- error=""
80
- onClose={setOpenProvider}
81
- />
82
- </div>
83
- <div className="flex items-center justify-end gap-2">
84
- <Button
85
- size="iconXs"
86
- variant="outline"
87
- className="!rounded-md"
88
- onClick={() => callAi()}
89
- >
90
- <ArrowUp className="size-4" />
91
- </Button>
92
- </div>
93
- </div>
94
- </div>
95
- </div>
96
- );
97
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/follow-up-tooltip.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Popover,
3
+ PopoverContent,
4
+ PopoverTrigger,
5
+ } from "@/components/ui/popover";
6
+ import { Info } from "lucide-react";
7
+
8
+ export const FollowUpTooltip = () => {
9
+ return (
10
+ <Popover>
11
+ <PopoverTrigger asChild>
12
+ <Info className="size-3 text-neutral-300 cursor-pointer" />
13
+ </PopoverTrigger>
14
+ <PopoverContent
15
+ align="start"
16
+ className="!rounded-2xl !p-0 min-w-xs text-center overflow-hidden"
17
+ >
18
+ <header className="bg-neutral-950 px-4 py-3 border-b border-neutral-700/70">
19
+ <p className="text-base text-neutral-200 font-semibold">
20
+ ⚡ Faster, Smarter Updates
21
+ </p>
22
+ </header>
23
+ <main className="p-4">
24
+ <p className="text-neutral-300 text-sm">
25
+ Using the Diff-Patch system, allow DeepSite to intelligently update
26
+ your project without rewritting the entire codebase.
27
+ </p>
28
+ <p className="text-neutral-500 text-sm mt-2">
29
+ This means faster updates, less data usage, and a more efficient
30
+ development process.
31
+ </p>
32
+ </main>
33
+ </PopoverContent>
34
+ </Popover>
35
+ );
36
+ };
components/editor/ask-ai/index.tsx CHANGED
@@ -1,94 +1,150 @@
1
- import { useRef, useState } from "react";
 
 
2
  import classNames from "classnames";
3
- import { ArrowUp, ChevronDown, CircleStop, Dice6 } from "lucide-react";
4
- import { useLocalStorage, useUpdateEffect, useMount } from "react-use";
5
  import { toast } from "sonner";
 
 
 
6
 
7
- import { useAi } from "@/hooks/useAi";
8
- import { useEditor } from "@/hooks/useEditor";
9
- import { EnhancedSettings, Project } from "@/types";
10
- import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
11
- import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
12
- import { AiLoading } from "@/components/editor/ask-ai/loading";
13
  import { Button } from "@/components/ui/button";
14
- import { Uploader } from "@/components/editor/ask-ai/uploader";
 
 
 
 
15
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
16
- import { Selector } from "@/components/editor/ask-ai/selector";
17
- import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
18
- import { useUser } from "@/hooks/useUser";
19
- import { useLoginModal } from "@/components/contexts/login-context";
20
- import { Settings } from "./settings";
21
- import { useProModal } from "@/components/contexts/pro-context";
22
- import { MAX_FREE_PROJECTS } from "@/lib/utils";
23
- import { PROMPTS_FOR_AI } from "@/lib/prompts";
 
 
24
 
25
- export const AskAi = ({
26
- project,
27
  isNew,
 
 
 
 
28
  onScrollToBottom,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }: {
30
- project?: Project;
31
- files?: string[];
 
 
 
 
 
 
 
 
32
  isNew?: boolean;
33
- onScrollToBottom?: () => void;
34
- }) => {
35
- const { user, projects } = useUser();
36
- const { isSameHtml, isUploading, pages, isLoadingProject } = useEditor();
37
- const {
38
- isAiWorking,
39
- isThinking,
40
- selectedFiles,
41
- setSelectedFiles,
42
- selectedElement,
43
- setSelectedElement,
44
- setIsThinking,
45
- callAiNewProject,
46
- callAiFollowUp,
47
- setModel,
48
- selectedModel,
49
- audio: hookAudio,
50
- cancelRequest,
51
- } = useAi(onScrollToBottom);
52
- const { openLoginModal } = useLoginModal();
53
- const { openProModal } = useProModal();
54
  const [openProvider, setOpenProvider] = useState(false);
55
  const [providerError, setProviderError] = useState("");
56
- const refThink = useRef<HTMLDivElement>(null);
57
-
58
- const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
59
- useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
60
- isActive: false,
61
- primaryColor: undefined,
62
- secondaryColor: undefined,
63
- theme: undefined,
64
- });
65
- const [promptStorage, , removePromptStorage] = useLocalStorage("prompt", "");
66
-
67
- const [isFollowUp, setIsFollowUp] = useState(true);
68
- const [prompt, setPrompt] = useState(
69
- promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
70
- );
71
- const [think, setThink] = useState("");
72
  const [openThink, setOpenThink] = useState(false);
73
- const [randomPromptLoading, setRandomPromptLoading] = useState(false);
 
 
 
 
74
 
75
- useMount(() => {
76
- if (promptStorage && promptStorage.trim() !== "") {
77
- callAi();
78
- }
 
 
 
 
 
 
 
 
 
 
 
 
79
  });
80
 
 
 
 
 
81
  const callAi = async (redesignMarkdown?: string) => {
82
- removePromptStorage();
83
- if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
84
- return openProModal([]);
85
  if (isAiWorking) return;
86
  if (!redesignMarkdown && !prompt.trim()) return;
87
 
88
  if (isFollowUp && !redesignMarkdown && !isSameHtml) {
89
- if (!user) return openLoginModal({ prompt });
90
- const result = await callAiFollowUp(prompt, enhancedSettings, isNew);
 
 
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  if (result?.error) {
93
  handleError(result.error, result.message);
94
  return;
@@ -100,9 +156,13 @@ export const AskAi = ({
100
  } else {
101
  const result = await callAiNewProject(
102
  prompt,
103
- enhancedSettings,
 
104
  redesignMarkdown,
105
- !!user
 
 
 
106
  );
107
 
108
  if (result?.error) {
@@ -112,24 +172,30 @@ export const AskAi = ({
112
 
113
  if (result?.success) {
114
  setPrompt("");
115
- // if (selectedModel?.isThinker) {
116
- // setModel(MODELS[0].value);
117
- // }
118
  }
119
  }
120
  };
121
 
 
 
 
 
 
 
122
  const handleError = (error: string, message?: string) => {
123
  switch (error) {
124
  case "login_required":
125
- openLoginModal();
126
  break;
127
  case "provider_required":
128
  setOpenProvider(true);
129
  setProviderError(message || "");
130
  break;
131
  case "pro_required":
132
- openProModal([]);
133
  break;
134
  case "api_error":
135
  toast.error(message || "An error occurred");
@@ -148,19 +214,19 @@ export const AskAi = ({
148
  }
149
  }, [think]);
150
 
151
- const randomPrompt = () => {
152
- setRandomPromptLoading(true);
153
- setTimeout(() => {
154
- setPrompt(
155
- PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
156
- );
157
- setRandomPromptLoading(false);
158
- }, 400);
159
- };
160
 
161
  return (
162
- <div className="p-3 w-full">
163
- <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
164
  {think && (
165
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
166
  <header
@@ -202,7 +268,7 @@ export const AskAi = ({
202
  files={selectedFiles}
203
  isAiWorking={isAiWorking}
204
  onDelete={(file) =>
205
- setSelectedFiles(selectedFiles.filter((f) => f !== file))
206
  }
207
  />
208
  {selectedElement && (
@@ -215,41 +281,92 @@ export const AskAi = ({
215
  </div>
216
  )}
217
  <div className="w-full relative flex items-center justify-between">
218
- {(isAiWorking || isUploading || isThinking || isLoadingProject) && (
219
  <div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
220
- <AiLoading
221
- text={
222
- isLoadingProject
223
- ? "Fetching your project..."
224
- : isUploading
225
- ? "Uploading images..."
226
- : isAiWorking && !isSameHtml
227
- ? "DeepSite is working..."
228
- : "DeepSite is thinking..."
229
- }
230
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  {isAiWorking && (
232
- <Button
233
- size="iconXs"
234
- variant="outline"
235
- className="!rounded-md mr-0.5"
236
- onClick={cancelRequest}
237
  >
238
- <CircleStop className="size-4" />
239
- </Button>
 
240
  )}
241
  </div>
242
  )}
243
  <textarea
244
- disabled={
245
- isAiWorking || isUploading || isThinking || isLoadingProject
246
- }
247
  className={classNames(
248
  "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
249
  {
250
- "!pt-2.5":
251
- selectedElement &&
252
- !(isAiWorking || isUploading || isThinking),
253
  }
254
  )}
255
  placeholder={
@@ -267,51 +384,112 @@ export const AskAi = ({
267
  }
268
  }}
269
  />
270
- {isNew && !isAiWorking && isSameHtml && (
271
- <Button
272
- size="iconXs"
273
- variant="outline"
274
- className="!rounded-md -translate-y-2 -translate-x-4"
275
- onClick={() => randomPrompt()}
276
- >
277
- <Dice6
278
- className={classNames("size-4", {
279
- "animate-spin animation-duration-500": randomPromptLoading,
280
- })}
281
- />
282
- </Button>
283
- )}
284
  </div>
285
  <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
286
- <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
287
- <PromptBuilder
288
- enhancedSettings={enhancedSettings!}
289
- setEnhancedSettings={setEnhancedSettings}
 
 
 
 
 
 
 
 
 
 
 
 
290
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  <Settings
 
 
 
 
292
  open={openProvider}
293
  error={providerError}
294
  isFollowUp={!isSameHtml && isFollowUp}
295
  onClose={setOpenProvider}
296
  />
297
- {!isNew && <Uploader project={project} />}
298
- {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
299
- {!isNew && !isSameHtml && <Selector />}
300
- </div>
301
- <div className="flex items-center justify-end gap-2">
302
  <Button
303
  size="iconXs"
304
- variant="outline"
305
- className="!rounded-md"
306
- disabled={
307
- isAiWorking || isUploading || isThinking || !prompt.trim()
308
- }
309
  onClick={() => callAi()}
310
  >
311
  <ArrowUp className="size-4" />
312
  </Button>
313
  </div>
314
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  </div>
316
  <audio ref={hookAudio} id="audio" className="hidden">
317
  <source src="/success.mp3" type="audio/mpeg" />
@@ -319,4 +497,4 @@ export const AskAi = ({
319
  </audio>
320
  </div>
321
  );
322
- };
 
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
  isNew,
31
+ project,
32
+ images,
33
+ currentPage,
34
+ previousPrompts,
35
  onScrollToBottom,
36
+ isAiWorking,
37
+ setisAiWorking,
38
+ isEditableModeEnabled = false,
39
+ pages,
40
+ htmlHistory,
41
+ selectedElement,
42
+ setSelectedElement,
43
+ selectedFiles,
44
+ setSelectedFiles,
45
+ setIsEditableModeEnabled,
46
+ onNewPrompt,
47
+ onSuccess,
48
+ setPages,
49
+ setCurrentPage,
50
  }: {
51
+ project?: Project | null;
52
+ currentPage: Page;
53
+ images?: string[];
54
+ pages: Page[];
55
+ onScrollToBottom: () => void;
56
+ previousPrompts: string[];
57
+ isAiWorking: boolean;
58
+ onNewPrompt: (prompt: string) => void;
59
+ htmlHistory?: HtmlHistory[];
60
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
61
  isNew?: boolean;
62
+ onSuccess: (page: Page[], p: string, n?: number[][]) => void;
63
+ isEditableModeEnabled: boolean;
64
+ setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
65
+ selectedElement?: HTMLElement | null;
66
+ setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
67
+ selectedFiles: string[];
68
+ setSelectedFiles: React.Dispatch<React.SetStateAction<string[]>>;
69
+ setPages: React.Dispatch<React.SetStateAction<Page[]>>;
70
+ setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
71
+ }) {
72
+ const refThink = useRef<HTMLDivElement | null>(null);
73
+
74
+ const [open, setOpen] = useState(false);
75
+ const [prompt, setPrompt] = 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
+ previousPrompts,
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
+ setPrompt("");
136
+ }
137
+ } else if (isFollowUp && pages.length > 1 && isSameHtml) {
138
+ const result = await callAiNewPage(
139
+ prompt,
140
+ model,
141
+ provider,
142
+ currentPage.path,
143
+ [
144
+ ...(previousPrompts ?? []),
145
+ ...(htmlHistory?.map((h) => h.prompt) ?? []),
146
+ ]
147
+ );
148
  if (result?.error) {
149
  handleError(result.error, result.message);
150
  return;
 
156
  } else {
157
  const result = await callAiNewProject(
158
  prompt,
159
+ model,
160
+ provider,
161
  redesignMarkdown,
162
+ handleThink,
163
+ () => {
164
+ setIsThinking(false);
165
+ }
166
  );
167
 
168
  if (result?.error) {
 
172
 
173
  if (result?.success) {
174
  setPrompt("");
175
+ if (selectedModel?.isThinker) {
176
+ setModel(MODELS[0].value);
177
+ }
178
  }
179
  }
180
  };
181
 
182
+ const handleThink = (think: string) => {
183
+ setThink(think);
184
+ setIsThinking(true);
185
+ setOpenThink(true);
186
+ };
187
+
188
  const handleError = (error: string, message?: string) => {
189
  switch (error) {
190
  case "login_required":
191
+ setOpen(true);
192
  break;
193
  case "provider_required":
194
  setOpenProvider(true);
195
  setProviderError(message || "");
196
  break;
197
  case "pro_required":
198
+ setOpenProModal(true);
199
  break;
200
  case "api_error":
201
  toast.error(message || "An error occurred");
 
214
  }
215
  }, [think]);
216
 
217
+ useUpdateEffect(() => {
218
+ if (!isThinking) {
219
+ setOpenThink(false);
220
+ }
221
+ }, [isThinking]);
222
+
223
+ const isSameHtml = useMemo(() => {
224
+ return isTheSameHtml(currentPage.html);
225
+ }, [currentPage.html]);
226
 
227
  return (
228
+ <div className="px-3">
229
+ <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">
230
  {think && (
231
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
232
  <header
 
268
  files={selectedFiles}
269
  isAiWorking={isAiWorking}
270
  onDelete={(file) =>
271
+ setSelectedFiles((prev) => prev.filter((f) => f !== file))
272
  }
273
  />
274
  {selectedElement && (
 
281
  </div>
282
  )}
283
  <div className="w-full relative flex items-center justify-between">
284
+ {(isAiWorking || isUploading) && (
285
  <div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
286
+ <div className="flex items-center justify-start gap-2">
287
+ <Loading overlay={false} className="!size-4 opacity-50" />
288
+ <p className="text-neutral-400 text-sm">
289
+ {isUploading ? (
290
+ "Uploading images..."
291
+ ) : isAiWorking && !isSameHtml ? (
292
+ "AI is working..."
293
+ ) : (
294
+ <span className="inline-flex">
295
+ {[
296
+ "D",
297
+ "e",
298
+ "e",
299
+ "p",
300
+ "S",
301
+ "i",
302
+ "t",
303
+ "e",
304
+ " ",
305
+ "i",
306
+ "s",
307
+ " ",
308
+ "T",
309
+ "h",
310
+ "i",
311
+ "n",
312
+ "k",
313
+ "i",
314
+ "n",
315
+ "g",
316
+ ".",
317
+ ".",
318
+ ".",
319
+ " ",
320
+ "W",
321
+ "a",
322
+ "i",
323
+ "t",
324
+ " ",
325
+ "a",
326
+ " ",
327
+ "m",
328
+ "o",
329
+ "m",
330
+ "e",
331
+ "n",
332
+ "t",
333
+ ".",
334
+ ".",
335
+ ".",
336
+ ].map((char, index) => (
337
+ <span
338
+ key={index}
339
+ className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
340
+ style={{
341
+ animationDelay: `${index * 0.1}s`,
342
+ animationDuration: "1.3s",
343
+ animationIterationCount: "infinite",
344
+ }}
345
+ >
346
+ {char === " " ? "\u00A0" : char}
347
+ </span>
348
+ ))}
349
+ </span>
350
+ )}
351
+ </p>
352
+ </div>
353
  {isAiWorking && (
354
+ <div
355
+ 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"
356
+ onClick={stopController}
 
 
357
  >
358
+ <FaStopCircle />
359
+ Stop generation
360
+ </div>
361
  )}
362
  </div>
363
  )}
364
  <textarea
365
+ disabled={isAiWorking}
 
 
366
  className={classNames(
367
  "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
368
  {
369
+ "!pt-2.5": selectedElement && !isAiWorking,
 
 
370
  }
371
  )}
372
  placeholder={
 
384
  }
385
  }}
386
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  </div>
388
  <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
389
+ <div className="flex-1 flex items-center justify-start gap-1.5">
390
+ <Uploader
391
+ pages={pages}
392
+ onLoading={setIsUploading}
393
+ isLoading={isUploading}
394
+ onFiles={setFiles}
395
+ onSelectFile={(file) => {
396
+ if (selectedFiles.includes(file)) {
397
+ setSelectedFiles((prev) => prev.filter((f) => f !== file));
398
+ } else {
399
+ setSelectedFiles((prev) => [...prev, file]);
400
+ }
401
+ }}
402
+ files={files}
403
+ selectedFiles={selectedFiles}
404
+ project={project}
405
  />
406
+ {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
407
+ {!isSameHtml && (
408
+ <Tooltip>
409
+ <TooltipTrigger asChild>
410
+ <Button
411
+ size="xs"
412
+ variant={isEditableModeEnabled ? "default" : "outline"}
413
+ onClick={() => {
414
+ setIsEditableModeEnabled?.(!isEditableModeEnabled);
415
+ }}
416
+ className={classNames("h-[28px]", {
417
+ "!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
418
+ !isEditableModeEnabled,
419
+ })}
420
+ >
421
+ <Crosshair className="size-4" />
422
+ Edit
423
+ </Button>
424
+ </TooltipTrigger>
425
+ <TooltipContent
426
+ align="start"
427
+ className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
428
+ >
429
+ Select an element on the page to ask DeepSite edit it
430
+ directly.
431
+ </TooltipContent>
432
+ </Tooltip>
433
+ )}
434
+ {/* <InviteFriends /> */}
435
+ </div>
436
+ <div className="flex items-center justify-end gap-2">
437
  <Settings
438
+ provider={provider as string}
439
+ model={model as string}
440
+ onChange={setProvider}
441
+ onModelChange={setModel}
442
  open={openProvider}
443
  error={providerError}
444
  isFollowUp={!isSameHtml && isFollowUp}
445
  onClose={setOpenProvider}
446
  />
 
 
 
 
 
447
  <Button
448
  size="iconXs"
449
+ disabled={isAiWorking || !prompt.trim()}
 
 
 
 
450
  onClick={() => callAi()}
451
  >
452
  <ArrowUp className="size-4" />
453
  </Button>
454
  </div>
455
  </div>
456
+ <LoginModal open={open} onClose={() => setOpen(false)} pages={pages} />
457
+ <ProModal
458
+ pages={pages}
459
+ open={openProModal}
460
+ onClose={() => setOpenProModal(false)}
461
+ />
462
+ {pages.length === 1 && (
463
+ <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">
464
+ <span className="rounded-full text-[10px] font-semibold bg-white text-neutral-900 px-1.5 py-0.5">
465
+ NEW
466
+ </span>
467
+ <p className="text-sm text-neutral-100">
468
+ DeepSite can now create multiple pages at once. Try it!
469
+ </p>
470
+ </div>
471
+ )}
472
+ {!isSameHtml && (
473
+ <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">
474
+ <label
475
+ htmlFor="diff-patch-checkbox"
476
+ className="flex items-center gap-1.5 cursor-pointer"
477
+ >
478
+ <Checkbox
479
+ id="diff-patch-checkbox"
480
+ checked={isFollowUp}
481
+ onCheckedChange={(e) => {
482
+ if (e === true && !isSameHtml && selectedModel?.isThinker) {
483
+ setModel(MODELS[0].value);
484
+ }
485
+ setIsFollowUp(e === true);
486
+ }}
487
+ />
488
+ Diff-Patch Update
489
+ </label>
490
+ <FollowUpTooltip />
491
+ </div>
492
+ )}
493
  </div>
494
  <audio ref={hookAudio} id="audio" className="hidden">
495
  <source src="/success.mp3" type="audio/mpeg" />
 
497
  </audio>
498
  </div>
499
  );
500
+ }
components/editor/ask-ai/loading.tsx DELETED
@@ -1,68 +0,0 @@
1
- "use client";
2
- import Loading from "@/components/loading";
3
- import { useState, useEffect } from "react";
4
- import { useInterval } from "react-use";
5
-
6
- const TEXTS = [
7
- "Teaching pixels to dance with style...",
8
- "AI is having a creative breakthrough...",
9
- "Channeling digital vibes into pure code...",
10
- "Summoning the website spirits...",
11
- "Brewing some algorithmic magic...",
12
- "Composing a symphony of divs and spans...",
13
- "Riding the wave of computational creativity...",
14
- "Aligning the stars for perfect design...",
15
- "Training circus animals to write CSS...",
16
- "Launching ideas into the digital stratosphere...",
17
- ];
18
-
19
- export const AiLoading = ({
20
- text,
21
- className,
22
- }: {
23
- text?: string;
24
- className?: string;
25
- }) => {
26
- const [selectedText, setSelectedText] = useState(
27
- text ?? TEXTS[0] // Start with first text to avoid hydration issues
28
- );
29
-
30
- // Set random text on client-side only to avoid hydration mismatch
31
- useEffect(() => {
32
- if (!text) {
33
- setSelectedText(TEXTS[Math.floor(Math.random() * TEXTS.length)]);
34
- }
35
- }, [text]);
36
-
37
- useInterval(() => {
38
- if (!text) {
39
- if (selectedText === TEXTS[TEXTS.length - 1]) {
40
- setSelectedText(TEXTS[0]);
41
- } else {
42
- setSelectedText(TEXTS[TEXTS.indexOf(selectedText) + 1]);
43
- }
44
- }
45
- }, 12000);
46
- return (
47
- <div className={`flex items-center justify-start gap-2 ${className}`}>
48
- <Loading overlay={false} className="!size-5 opacity-50" />
49
- <p className="text-neutral-400 text-sm">
50
- <span className="inline-flex">
51
- {selectedText.split("").map((char, index) => (
52
- <span
53
- key={index}
54
- className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
55
- style={{
56
- animationDelay: `${index * 0.1}s`,
57
- animationDuration: "1.3s",
58
- animationIterationCount: "infinite",
59
- }}
60
- >
61
- {char === " " ? "\u00A0" : char}
62
- </span>
63
- ))}
64
- </span>
65
- </p>
66
- </div>
67
- );
68
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/prompt-builder/content-modal.tsx DELETED
@@ -1,196 +0,0 @@
1
- import classNames from "classnames";
2
- import { ChevronRight, RefreshCcw } from "lucide-react";
3
- import { useState } from "react";
4
- import { TailwindColors } from "./tailwind-colors";
5
- import { Switch } from "@/components/ui/switch";
6
- import { Button } from "@/components/ui/button";
7
- import { Themes } from "./themes";
8
- import { EnhancedSettings } from "@/types";
9
-
10
- export const ContentModal = ({
11
- enhancedSettings,
12
- setEnhancedSettings,
13
- }: {
14
- enhancedSettings: EnhancedSettings;
15
- setEnhancedSettings: (settings: EnhancedSettings) => void;
16
- }) => {
17
- const [collapsed, setCollapsed] = useState(["colors", "theme"]);
18
- return (
19
- <main className="overflow-x-hidden max-h-[50dvh] overflow-y-auto">
20
- <section className="w-full border-b border-neutral-800/80 px-6 py-3.5 sticky top-0 bg-neutral-900 z-10">
21
- <div className="flex items-center justify-between gap-3">
22
- <p className="text-base font-semibold text-neutral-200">
23
- Allow DeepSite to enhance your prompt
24
- </p>
25
- <Switch
26
- checked={enhancedSettings.isActive}
27
- onCheckedChange={() =>
28
- setEnhancedSettings({
29
- ...enhancedSettings,
30
- isActive: !enhancedSettings.isActive,
31
- })
32
- }
33
- />
34
- </div>
35
- <p className="text-sm text-neutral-500 mt-2">
36
- While using DeepSite enhanced prompt, you'll get better results. We'll
37
- add more details and features to your request.
38
- </p>
39
- <div className="text-sm text-sky-500 mt-3 bg-gradient-to-r from-sky-400/15 to-purple-400/15 rounded-md px-3 py-2 border border-white/10">
40
- <p className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text">
41
- You can also use the custom properties below to set specific
42
- information.
43
- </p>
44
- </div>
45
- </section>
46
- <section className="py-3.5 border-b border-neutral-800/80">
47
- <div
48
- className={classNames(
49
- "flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
50
- {
51
- "!text-neutral-200": collapsed.includes("colors"),
52
- }
53
- )}
54
- onClick={() =>
55
- setCollapsed((prev) => {
56
- if (prev.includes("colors")) {
57
- return prev.filter((item) => item !== "colors");
58
- }
59
- return [...prev, "colors"];
60
- })
61
- }
62
- >
63
- <ChevronRight className="size-4" />
64
- <p className="text-base font-semibold">Colors</p>
65
- </div>
66
- {collapsed.includes("colors") && (
67
- <div className="mt-4 space-y-4">
68
- <article className="w-full">
69
- <div className="flex items-center justify-start gap-2 px-5">
70
- <p className="text-xs font-medium uppercase text-neutral-400">
71
- Primary Color
72
- </p>
73
- <Button
74
- variant="bordered"
75
- size="xss"
76
- className={`${
77
- enhancedSettings.primaryColor ? "" : "opacity-0"
78
- }`}
79
- onClick={() =>
80
- setEnhancedSettings({
81
- ...enhancedSettings,
82
- primaryColor: undefined,
83
- })
84
- }
85
- >
86
- <RefreshCcw className="size-2.5" />
87
- Reset
88
- </Button>
89
- </div>
90
- <div className="text-muted-foreground text-sm mt-4">
91
- <TailwindColors
92
- value={enhancedSettings.primaryColor}
93
- onChange={(value) =>
94
- setEnhancedSettings({
95
- ...enhancedSettings,
96
- primaryColor: value,
97
- })
98
- }
99
- />
100
- </div>
101
- </article>
102
- <article className="w-full">
103
- <div className="flex items-center justify-start gap-2 px-5">
104
- <p className="text-xs font-medium uppercase text-neutral-400">
105
- Secondary Color
106
- </p>
107
- <Button
108
- variant="bordered"
109
- size="xss"
110
- className={`${
111
- enhancedSettings.secondaryColor ? "" : "opacity-0"
112
- }`}
113
- onClick={() =>
114
- setEnhancedSettings({
115
- ...enhancedSettings,
116
- secondaryColor: undefined,
117
- })
118
- }
119
- >
120
- <RefreshCcw className="size-2.5" />
121
- Reset
122
- </Button>
123
- </div>
124
- <div className="text-muted-foreground text-sm mt-4">
125
- <TailwindColors
126
- value={enhancedSettings.secondaryColor}
127
- onChange={(value) =>
128
- setEnhancedSettings({
129
- ...enhancedSettings,
130
- secondaryColor: value,
131
- })
132
- }
133
- />
134
- </div>
135
- </article>
136
- </div>
137
- )}
138
- </section>
139
- <section className="py-3.5 border-b border-neutral-800/80">
140
- <div
141
- className={classNames(
142
- "flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
143
- {
144
- "!text-neutral-200": collapsed.includes("theme"),
145
- }
146
- )}
147
- onClick={() =>
148
- setCollapsed((prev) => {
149
- if (prev.includes("theme")) {
150
- return prev.filter((item) => item !== "theme");
151
- }
152
- return [...prev, "theme"];
153
- })
154
- }
155
- >
156
- <ChevronRight className="size-4" />
157
- <p className="text-base font-semibold">Theme</p>
158
- </div>
159
- {collapsed.includes("theme") && (
160
- <article className="w-full mt-4">
161
- <div className="flex items-center justify-start gap-2 px-5">
162
- <p className="text-xs font-medium uppercase text-neutral-400">
163
- Theme
164
- </p>
165
- <Button
166
- variant="bordered"
167
- size="xss"
168
- className={`${enhancedSettings.theme ? "" : "opacity-0"}`}
169
- onClick={() =>
170
- setEnhancedSettings({
171
- ...enhancedSettings,
172
- theme: undefined,
173
- })
174
- }
175
- >
176
- <RefreshCcw className="size-2.5" />
177
- Reset
178
- </Button>
179
- </div>
180
- <div className="text-muted-foreground text-sm mt-4">
181
- <Themes
182
- value={enhancedSettings.theme}
183
- onChange={(value) =>
184
- setEnhancedSettings({
185
- ...enhancedSettings,
186
- theme: value,
187
- })
188
- }
189
- />
190
- </div>
191
- </article>
192
- )}
193
- </section>
194
- </main>
195
- );
196
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/prompt-builder/index.tsx DELETED
@@ -1,68 +0,0 @@
1
- import { useState } from "react";
2
- import { WandSparkles } from "lucide-react";
3
-
4
- import { Button } from "@/components/ui/button";
5
- import { useEditor } from "@/hooks/useEditor";
6
- import { useAi } from "@/hooks/useAi";
7
- import {
8
- Dialog,
9
- DialogContent,
10
- DialogFooter,
11
- DialogTitle,
12
- } from "@/components/ui/dialog";
13
- import { ContentModal } from "./content-modal";
14
- import { EnhancedSettings } from "@/types";
15
-
16
- export const PromptBuilder = ({
17
- enhancedSettings,
18
- setEnhancedSettings,
19
- }: {
20
- enhancedSettings: EnhancedSettings;
21
- setEnhancedSettings: (settings: EnhancedSettings) => void;
22
- }) => {
23
- const { globalAiLoading } = useAi();
24
- const { globalEditorLoading } = useEditor();
25
-
26
- const [open, setOpen] = useState(false);
27
- return (
28
- <>
29
- <Button
30
- size="xs"
31
- variant="outline"
32
- className="!rounded-md !border-white/10 !bg-gradient-to-r from-sky-400/15 to-purple-400/15 light-sweep hover:brightness-110"
33
- disabled={globalAiLoading || globalEditorLoading}
34
- onClick={() => {
35
- setOpen(true);
36
- }}
37
- >
38
- <WandSparkles className="size-3.5 text-sky-500 relative z-10" />
39
- <span className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text relative z-10">
40
- Enhance
41
- </span>
42
- </Button>
43
- <Dialog open={open} onOpenChange={() => setOpen(false)}>
44
- <DialogContent className="sm:max-w-xl !p-0 !rounded-3xl !bg-neutral-900 !border-neutral-800/80 !gap-0">
45
- <DialogTitle className="px-6 py-3.5 border-b border-neutral-800">
46
- <div className="flex items-center justify-start gap-2 text-neutral-200 text-base font-medium">
47
- <WandSparkles className="size-3.5" />
48
- <p>Enhance Prompt</p>
49
- </div>
50
- </DialogTitle>
51
- <ContentModal
52
- enhancedSettings={enhancedSettings}
53
- setEnhancedSettings={setEnhancedSettings}
54
- />
55
- <DialogFooter className="px-6 py-3.5 border-t border-neutral-800">
56
- <Button
57
- variant="bordered"
58
- size="default"
59
- onClick={() => setOpen(false)}
60
- >
61
- Close
62
- </Button>
63
- </DialogFooter>
64
- </DialogContent>
65
- </Dialog>
66
- </>
67
- );
68
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/prompt-builder/tailwind-colors.tsx DELETED
@@ -1,58 +0,0 @@
1
- import classNames from "classnames";
2
- import { useRef } from "react";
3
-
4
- import { TAILWIND_COLORS } from "@/lib/prompt-builder";
5
- import { useMount } from "react-use";
6
-
7
- export const TailwindColors = ({
8
- value,
9
- onChange,
10
- }: {
11
- value: string | undefined;
12
- onChange: (value: string) => void;
13
- }) => {
14
- const ref = useRef<HTMLDivElement>(null);
15
-
16
- useMount(() => {
17
- if (ref.current) {
18
- if (value) {
19
- const color = ref.current.querySelector(`[data-color="${value}"]`);
20
- if (color) {
21
- color.scrollIntoView({ inline: "center" });
22
- }
23
- }
24
- }
25
- });
26
- return (
27
- <div
28
- ref={ref}
29
- className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
30
- >
31
- {TAILWIND_COLORS.map((color) => (
32
- <div
33
- key={color}
34
- className={classNames(
35
- "flex flex-col items-center justify-center p-3 size-16 min-w-16 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
36
- {
37
- "!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
38
- value === color,
39
- }
40
- )}
41
- data-color={color}
42
- onClick={() => onChange(color)}
43
- >
44
- <div
45
- className={`w-4 h-4 min-w-4 min-h-4 rounded-xl ${
46
- ["white", "black"].includes(color)
47
- ? `bg-${color}`
48
- : `bg-${color}-500`
49
- }`}
50
- />
51
- <p className="text-xs capitalize text-neutral-200 truncate">
52
- {color}
53
- </p>
54
- </div>
55
- ))}
56
- </div>
57
- );
58
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/prompt-builder/themes.tsx DELETED
@@ -1,48 +0,0 @@
1
- import { Theme } from "@/types";
2
- import classNames from "classnames";
3
- import { Moon, Sun } from "lucide-react";
4
- import { useRef } from "react";
5
-
6
- export const Themes = ({
7
- value,
8
- onChange,
9
- }: {
10
- value: Theme;
11
- onChange: (value: Theme) => void;
12
- }) => {
13
- const ref = useRef<HTMLDivElement>(null);
14
-
15
- return (
16
- <div
17
- ref={ref}
18
- className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
19
- >
20
- <div
21
- className={classNames(
22
- "flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
23
- {
24
- "!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
25
- value === "light",
26
- }
27
- )}
28
- onClick={() => onChange("light")}
29
- >
30
- <Sun className="size-4 text-amber-500" />
31
- <p className="text-xs capitalize text-neutral-200 truncate">Light</p>
32
- </div>
33
- <div
34
- className={classNames(
35
- "flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
36
- {
37
- "!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
38
- value === "dark",
39
- }
40
- )}
41
- onClick={() => onChange("dark")}
42
- >
43
- <Moon className="size-4 text-indigo-500" />
44
- <p className="text-xs capitalize text-neutral-200 truncate">Dark</p>
45
- </div>
46
- </div>
47
- );
48
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/re-imagine.tsx CHANGED
@@ -11,8 +11,6 @@ import {
11
  import { Input } from "@/components/ui/input";
12
  import Loading from "@/components/loading";
13
  import { api } from "@/lib/api";
14
- import { useAi } from "@/hooks/useAi";
15
- import { useEditor } from "@/hooks/useEditor";
16
 
17
  export function ReImagine({
18
  onRedesign,
@@ -22,8 +20,6 @@ export function ReImagine({
22
  const [url, setUrl] = useState<string>("");
23
  const [open, setOpen] = useState(false);
24
  const [isLoading, setIsLoading] = useState(false);
25
- const { globalAiLoading } = useAi();
26
- const { globalEditorLoading } = useEditor();
27
 
28
  const checkIfUrlIsValid = (url: string) => {
29
  const urlPattern = new RegExp(
@@ -63,13 +59,11 @@ export function ReImagine({
63
  <form>
64
  <PopoverTrigger asChild>
65
  <Button
66
- size="xs"
67
- variant={open ? "default" : "outline"}
68
- className="!rounded-md"
69
- disabled={globalAiLoading || globalEditorLoading}
70
  >
71
- <Paintbrush className="size-3.5" />
72
- Redesign
73
  </Button>
74
  </PopoverTrigger>
75
  <PopoverContent
 
11
  import { Input } from "@/components/ui/input";
12
  import Loading from "@/components/loading";
13
  import { api } from "@/lib/api";
 
 
14
 
15
  export function ReImagine({
16
  onRedesign,
 
20
  const [url, setUrl] = useState<string>("");
21
  const [open, setOpen] = useState(false);
22
  const [isLoading, setIsLoading] = useState(false);
 
 
23
 
24
  const checkIfUrlIsValid = (url: string) => {
25
  const urlPattern = new RegExp(
 
59
  <form>
60
  <PopoverTrigger asChild>
61
  <Button
62
+ size="iconXs"
63
+ variant="outline"
64
+ className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
 
65
  >
66
+ <Paintbrush className="size-4" />
 
67
  </Button>
68
  </PopoverTrigger>
69
  <PopoverContent
components/editor/ask-ai/selector.tsx DELETED
@@ -1,41 +0,0 @@
1
- import classNames from "classnames";
2
- import { Crosshair } from "lucide-react";
3
-
4
- import { Button } from "@/components/ui/button";
5
- import {
6
- Tooltip,
7
- TooltipContent,
8
- TooltipTrigger,
9
- } from "@/components/ui/tooltip";
10
- import { useAi } from "@/hooks/useAi";
11
- import { useEditor } from "@/hooks/useEditor";
12
-
13
- export const Selector = () => {
14
- const { globalEditorLoading } = useEditor();
15
- const { isEditableModeEnabled, setIsEditableModeEnabled, globalAiLoading } =
16
- useAi();
17
- return (
18
- <Tooltip>
19
- <TooltipTrigger asChild>
20
- <Button
21
- size="xs"
22
- variant={isEditableModeEnabled ? "default" : "outline"}
23
- onClick={() => {
24
- setIsEditableModeEnabled?.(!isEditableModeEnabled);
25
- }}
26
- disabled={globalAiLoading || globalEditorLoading}
27
- className="!rounded-md"
28
- >
29
- <Crosshair className="size-3.5" />
30
- Edit
31
- </Button>
32
- </TooltipTrigger>
33
- <TooltipContent
34
- align="start"
35
- className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
36
- >
37
- Select an element on the page to ask DeepSite edit it directly.
38
- </TooltipContent>
39
- </Tooltip>
40
- );
41
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/settings.tsx CHANGED
@@ -1,5 +1,6 @@
1
- "use client";
2
  import classNames from "classnames";
 
 
3
 
4
  import {
5
  Popover,
@@ -17,273 +18,185 @@ import {
17
  SelectTrigger,
18
  SelectValue,
19
  } from "@/components/ui/select";
20
- import { useMemo, useState, useEffect } from "react";
21
  import { useUpdateEffect } from "react-use";
22
  import Image from "next/image";
23
- import { Brain, BrainIcon, CheckCheck, ChevronDown } from "lucide-react";
24
- import { useAi } from "@/hooks/useAi";
25
- import { getProviders } from "@/lib/get-providers";
26
- import Loading from "@/components/loading";
27
 
28
  export function Settings({
29
  open,
30
  onClose,
 
 
31
  error,
32
  isFollowUp = false,
 
 
33
  }: {
34
  open: boolean;
 
 
35
  error?: string;
36
  isFollowUp?: boolean;
37
  onClose: React.Dispatch<React.SetStateAction<boolean>>;
 
 
38
  }) {
39
- const {
40
- model,
41
- provider,
42
- setProvider,
43
- setModel,
44
- selectedModel,
45
- globalAiLoading,
46
- } = useAi();
47
- const [isMounted, setIsMounted] = useState(false);
48
- const [loadingProviders, setLoadingProviders] = useState(false);
49
-
50
- useEffect(() => {
51
- setIsMounted(true);
52
- }, []);
53
-
54
- // const modelAvailableProviders = useMemo(() => {
55
- // const availableProviders = MODELS.find(
56
- // (m: { value: string }) => m.value === model
57
- // )?.providers;
58
- // if (!availableProviders) return Object.keys(PROVIDERS);
59
- // return Object.keys(PROVIDERS).filter((id) =>
60
- // availableProviders.includes(id)
61
- // );
62
- // }, [model]);
63
 
64
  useUpdateEffect(() => {
65
- if (provider !== "auto" && !providers.includes(provider as string)) {
66
- setProvider("auto");
67
  }
68
  }, [model, provider]);
69
 
70
- const formattedModels = useMemo(() => {
71
- const lists: ((typeof MODELS)[0] | { isCategory: true; name: string })[] =
72
- [];
73
- const keys = new Set<string>();
74
- MODELS.forEach((model) => {
75
- if (!keys.has(model.companyName)) {
76
- lists.push({
77
- isCategory: true,
78
- name: model.companyName,
79
- logo: model.logo,
80
- });
81
- keys.add(model.companyName);
82
- }
83
- lists.push(model);
84
- });
85
- return lists;
86
- }, [MODELS]);
87
-
88
- const [providers, setProviders] = useState<any[]>([]);
89
- const [failedImages, setFailedImages] = useState<Set<string>>(new Set());
90
-
91
- useEffect(() => {
92
- const loadProviders = async () => {
93
- setLoadingProviders(true);
94
- if (!model) {
95
- setProviders([]);
96
- return;
97
- }
98
- try {
99
- const result = await getProviders(model);
100
- setProviders(result);
101
- } catch (error) {
102
- console.error("Failed to load providers:", error);
103
- setProviders([]);
104
- } finally {
105
- setLoadingProviders(false);
106
- }
107
- };
108
-
109
- loadProviders();
110
- }, [model]);
111
-
112
- const handleImageError = (providerId: string) => {
113
- setFailedImages((prev) => new Set([...prev, providerId]));
114
- };
115
-
116
  return (
117
- <Popover open={open} onOpenChange={onClose}>
118
- <PopoverTrigger asChild>
119
- <Button
120
- variant={open ? "default" : "outline"}
121
- className="!rounded-md"
122
- disabled={globalAiLoading || loadingProviders}
123
- size="xs"
 
 
 
 
124
  >
125
- {/* <Brain className="size-3.5" /> */}
126
- {selectedModel?.logo && (
127
- <Image
128
- src={selectedModel?.logo}
129
- alt={selectedModel.label}
130
- className={`size-3.5 ${open ? "" : "filter invert"}`}
131
- width={20}
132
- height={20}
133
- />
134
- )}
135
- <span className="truncate max-w-[120px]">
136
- {isMounted
137
- ? selectedModel?.label?.split(" ").join("-").toLowerCase()
138
- : "..."}
139
- </span>
140
- <ChevronDown className="size-3.5" />
141
- </Button>
142
- </PopoverTrigger>
143
- <PopoverContent
144
- className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
145
- align="center"
146
- >
147
- <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">
148
- Customize Settings
149
- </header>
150
- <main className="px-4 pt-5 pb-6 space-y-5">
151
- {error !== "" && (
152
- <p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
153
- {error}
154
- </p>
155
- )}
156
- <label className="block">
157
- <p className="text-neutral-300 text-sm mb-2.5">Choose a model</p>
158
- <Select defaultValue={model} onValueChange={setModel}>
159
- <SelectTrigger className="w-full">
160
- <SelectValue placeholder="Select a model" />
161
- </SelectTrigger>
162
- <SelectContent>
163
- <SelectGroup>
164
- {formattedModels.map((item: any) => {
165
- if ("isCategory" in item) {
166
- return (
167
- <SelectLabel
168
- key={item.name}
169
- className="flex items-center gap-1"
170
  >
171
- {item.name}
172
- </SelectLabel>
173
- );
174
- }
175
- const {
176
- value,
177
- label,
178
- isNew = false,
179
- isThinker = false,
180
- } = item;
181
- return (
182
- <SelectItem
183
- key={value}
184
- value={value}
185
- className=""
186
- disabled={isThinker && isFollowUp}
187
- >
188
- {label}
189
- {isNew && (
190
- <span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
191
- New
192
- </span>
193
- )}
194
- </SelectItem>
195
- );
196
- })}
197
- </SelectGroup>
198
- </SelectContent>
199
- </Select>
200
- </label>
201
- {/* {isFollowUp && (
202
- <div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
203
- Note: You can&apos;t use a Thinker model for follow-up requests.
204
- We automatically switch to the default model for you.
205
- </div>
206
- )} */}
207
- <div className="flex flex-col gap-3">
208
- <div className="flex items-center justify-between">
209
- <div>
210
- <p className="text-neutral-300 text-sm mb-1.5">
211
- Use auto-provider
212
- </p>
213
- <p className="text-xs text-neutral-400/70">
214
- We&apos;ll automatically select the best provider for you
215
- based on your prompt.
216
- </p>
217
  </div>
218
- <div
219
- className={classNames(
220
- "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",
221
- {
222
- "!bg-sky-500": provider === "auto",
223
- }
224
- )}
225
- onClick={() => {
226
- const foundModel = MODELS.find(
227
- (m: { value: string }) => m.value === model
228
- );
229
- if (provider === "auto" && foundModel?.autoProvider) {
230
- setProvider(foundModel.autoProvider);
231
- } else {
232
- setProvider("auto");
233
- }
234
- }}
235
- >
236
  <div
237
  className={classNames(
238
- "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
239
  {
240
- "translate-x-4": provider === "auto",
241
  }
242
  )}
243
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  </div>
245
- </div>
246
- <label className="block">
247
- <p className="text-neutral-300 text-sm mb-2">
248
- Inference Provider
249
- </p>
250
- <div className="grid grid-cols-2 gap-1.5 relative">
251
- {loadingProviders ? (
252
- <Loading overlay={false} />
253
- ) : (
254
- providers.map((id: string) => (
255
  <Button
256
  key={id}
257
  variant={id === provider ? "default" : "secondary"}
258
  size="sm"
259
  onClick={() => {
260
- setProvider(id);
261
  }}
262
  >
263
- {failedImages.has(id) ? (
264
- <BrainIcon className="size-4 mr-2" />
265
- ) : (
266
- <Image
267
- src={`/providers/${id}.svg`}
268
- alt={id}
269
- className="size-5 mr-2"
270
- width={20}
271
- height={20}
272
- onError={() => handleImageError(id)}
273
- />
274
- )}
275
- {PROVIDERS?.[id as keyof typeof PROVIDERS]?.name || id}
276
  {id === provider && (
277
- <CheckCheck className="ml-2 size-4 text-blue-500" />
278
  )}
279
  </Button>
280
- ))
281
- )}
282
- </div>
283
- </label>
284
- </div>
285
- </main>
286
- </PopoverContent>
287
- </Popover>
288
  );
289
  }
 
 
1
  import classNames from "classnames";
2
+ import { PiGearSixFill } from "react-icons/pi";
3
+ import { RiCheckboxCircleFill } from "react-icons/ri";
4
 
5
  import {
6
  Popover,
 
18
  SelectTrigger,
19
  SelectValue,
20
  } from "@/components/ui/select";
21
+ import { useMemo } from "react";
22
  import { useUpdateEffect } from "react-use";
23
  import Image from "next/image";
 
 
 
 
24
 
25
  export function Settings({
26
  open,
27
  onClose,
28
+ provider,
29
+ model,
30
  error,
31
  isFollowUp = false,
32
+ onChange,
33
+ onModelChange,
34
  }: {
35
  open: boolean;
36
+ provider: string;
37
+ model: string;
38
  error?: string;
39
  isFollowUp?: boolean;
40
  onClose: React.Dispatch<React.SetStateAction<boolean>>;
41
+ onChange: (provider: string) => void;
42
+ onModelChange: (model: string) => void;
43
  }) {
44
+ const modelAvailableProviders = useMemo(() => {
45
+ const availableProviders = MODELS.find(
46
+ (m: { value: string }) => m.value === model
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 CHANGED
@@ -1,12 +1,5 @@
1
  import { useRef, useState } from "react";
2
- import {
3
- CheckCircle,
4
- ImageIcon,
5
- Images,
6
- Link,
7
- Paperclip,
8
- Upload,
9
- } from "lucide-react";
10
  import Image from "next/image";
11
 
12
  import {
@@ -15,151 +8,196 @@ import {
15
  PopoverTrigger,
16
  } from "@/components/ui/popover";
17
  import { Button } from "@/components/ui/button";
18
- import { Project } from "@/types";
19
  import Loading from "@/components/loading";
 
20
  import { useUser } from "@/hooks/useUser";
21
- import { useEditor } from "@/hooks/useEditor";
22
- import { useAi } from "@/hooks/useAi";
23
- import { useLoginModal } from "@/components/contexts/login-context";
24
 
25
- export const Uploader = ({ project }: { project: Project | undefined }) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  const { user } = useUser();
27
- const { openLoginModal } = useLoginModal();
28
- const { uploadFiles, isUploading, files, globalEditorLoading } = useEditor();
29
- const { selectedFiles, setSelectedFiles, globalAiLoading } = useAi();
30
 
31
  const [open, setOpen] = useState(false);
32
  const fileInputRef = useRef<HTMLInputElement>(null);
33
 
34
- if (!user)
35
- return (
36
- <Button
37
- size="xs"
38
- variant="outline"
39
- className="!rounded-md"
40
- disabled={globalAiLoading || globalEditorLoading}
41
- onClick={() => openLoginModal()}
42
- >
43
- <Paperclip className="size-3.5" />
44
- Attach
45
- </Button>
 
 
 
 
 
 
 
 
 
46
  );
 
 
 
 
 
 
47
 
48
- return (
 
49
  <Popover open={open} onOpenChange={setOpen}>
50
- <form className="h-[24px]">
51
  <PopoverTrigger asChild>
52
  <Button
53
- size="xs"
54
- variant={open ? "default" : "outline"}
55
- className="!rounded-md"
56
- disabled={globalAiLoading || globalEditorLoading}
57
  >
58
- <Paperclip className="size-3.5" />
59
- Attach
60
  </Button>
61
  </PopoverTrigger>
62
  <PopoverContent
63
  align="start"
64
  className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
65
  >
66
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
67
- <div className="flex items-center justify-center -space-x-4 mb-3">
68
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
69
- 🎨
70
- </div>
71
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
72
- 🖼️
73
- </div>
74
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
75
- 💻
76
- </div>
77
- </div>
78
- <p className="text-xl font-semibold text-neutral-950">
79
- Add Custom Images
80
- </p>
81
- <p className="text-sm text-neutral-500 mt-1.5">
82
- Upload images to your project and use them with DeepSite!
83
- </p>
84
- </header>
85
- <main className="space-y-4 p-5">
86
- <div>
87
- <p className="text-xs text-left text-neutral-700 mb-2">
88
- Uploaded Images
89
- </p>
90
- {files?.length > 0 ? (
91
- <div className="grid grid-cols-4 gap-1 flex-wrap max-h-40 overflow-y-auto">
92
- {files.map((file: string) => (
93
- <div
94
- key={file}
95
- className="select-none relative cursor-pointer bg-white rounded-md border-[2px] border-white hover:shadow-2xl transition-all duration-300"
96
- onClick={() =>
97
- setSelectedFiles(
98
- selectedFiles.includes(file)
99
- ? selectedFiles.filter((f) => f !== file)
100
- : [...selectedFiles, file]
101
- )
102
- }
103
- >
104
- <Image
105
- src={file}
106
- alt="uploaded image"
107
- width={56}
108
- height={56}
109
- className="object-cover w-full rounded-sm aspect-square"
110
- />
111
- {selectedFiles.includes(file) && (
112
- <div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
113
- <CheckCircle className="size-6 text-neutral-100" />
114
- </div>
115
- )}
116
- </div>
117
- ))}
118
  </div>
119
- ) : (
120
- <p className="text-sm text-muted-foreground font-mono flex flex-col items-center gap-1 pt-2">
121
- <ImageIcon className="size-4" />
122
- No images uploaded yet
 
123
  </p>
124
- )}
125
- </div>
126
- <div>
127
- <p className="text-xs text-left text-neutral-700 mb-2">
128
- Or import images from your computer
129
- </p>
130
- <Button
131
- variant="black"
132
- onClick={() => fileInputRef.current?.click()}
133
- className="relative w-full"
134
- disabled={isUploading}
135
- >
136
- {isUploading ? (
137
- <>
138
- <Loading
139
- overlay={false}
140
- className="ml-2 size-4 animate-spin"
141
- />
142
- Uploading image(s)...
143
- </>
144
- ) : (
145
- <>
146
- <Upload className="size-4" />
147
- Upload Images
148
- </>
149
- )}
150
- </Button>
151
- <input
152
- ref={fileInputRef}
153
- type="file"
154
- className="hidden"
155
- multiple
156
- accept="image/*"
157
- onChange={(e) => uploadFiles(e.target.files, project!)}
158
- />
159
- </div>
160
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  </PopoverContent>
162
  </form>
163
  </Popover>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  );
165
  };
 
1
  import { useRef, useState } from "react";
2
+ import { Images, Upload } from "lucide-react";
 
 
 
 
 
 
 
3
  import Image from "next/image";
4
 
5
  import {
 
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
+ onClick={() => setOpen(true)}
191
+ >
192
+ <Images className="size-4" />
193
+ </Button>
194
+ <LoginModal
195
+ open={open}
196
+ onClose={() => setOpen(false)}
197
+ pages={pages}
198
+ title="Log In to add Custom Images"
199
+ description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
200
+ />
201
+ </>
202
  );
203
  };
components/editor/deploy-button/content.tsx ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { FaMobileAlt } from "react-icons/fa";
3
+ import { HelpCircle, LogIn, 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
+ import { useLocalStorage } from "react-use";
13
+ import { isTheSameHtml } from "@/lib/compare-html-diff";
14
+
15
+ const DEVICES = [
16
+ {
17
+ name: "desktop",
18
+ icon: FaLaptopCode,
19
+ },
20
+ {
21
+ name: "mobile",
22
+ icon: FaMobileAlt,
23
+ },
24
+ ];
25
+
26
+ export function Footer({
27
+ pages,
28
+ isNew = false,
29
+ htmlHistory,
30
+ setPages,
31
+ device,
32
+ setDevice,
33
+ iframeRef,
34
+ }: {
35
+ pages: Page[];
36
+ isNew?: boolean;
37
+ htmlHistory?: HtmlHistory[];
38
+ device: "desktop" | "mobile";
39
+ setPages: (pages: Page[]) => void;
40
+ iframeRef?: React.RefObject<HTMLIFrameElement | null>;
41
+ setDevice: React.Dispatch<React.SetStateAction<"desktop" | "mobile">>;
42
+ }) {
43
+ const { user, openLoginWindow } = useUser();
44
+
45
+ const handleRefreshIframe = () => {
46
+ if (iframeRef?.current) {
47
+ const iframe = iframeRef.current;
48
+ const content = iframe.srcdoc;
49
+ iframe.srcdoc = "";
50
+ setTimeout(() => {
51
+ iframe.srcdoc = content;
52
+ }, 10);
53
+ }
54
+ };
55
+
56
+ const [, setStorage] = useLocalStorage("pages");
57
+ const handleClick = async () => {
58
+ if (pages && !isTheSameHtml(pages[0].html)) {
59
+ setStorage(pages);
60
+ }
61
+ openLoginWindow();
62
+ };
63
+
64
+ return (
65
+ <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">
66
+ <div className="flex items-center gap-2">
67
+ {user ? (
68
+ user?.isLocalUse ? (
69
+ <>
70
+ <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">
71
+ Local Usage
72
+ </div>
73
+ </>
74
+ ) : (
75
+ <UserMenu className="!p-1 !pr-3 !h-auto" />
76
+ )
77
+ ) : (
78
+ <Button size="sm" variant="default" onClick={handleClick}>
79
+ <LogIn className="text-sm" />
80
+ Log In
81
+ </Button>
82
+ )}
83
+ {user && !isNew && <p className="text-neutral-700">|</p>}
84
+ {!isNew && (
85
+ <Link href="/projects/new">
86
+ <Button size="sm" variant="secondary">
87
+ <MdAdd className="text-sm" />
88
+ New <span className="max-lg:hidden">Project</span>
89
+ </Button>
90
+ </Link>
91
+ )}
92
+ {htmlHistory && htmlHistory.length > 0 && (
93
+ <>
94
+ <p className="text-neutral-700">|</p>
95
+ <History history={htmlHistory} setPages={setPages} />
96
+ </>
97
+ )}
98
+ </div>
99
+ <div className="flex justify-end items-center gap-2.5">
100
+ <a
101
+ href="https://huggingface.co/spaces/victor/deepsite-gallery"
102
+ target="_blank"
103
+ >
104
+ <Button size="sm" variant="ghost">
105
+ <SparkleIcon className="size-3.5" />
106
+ <span className="max-lg:hidden">DeepSite Gallery</span>
107
+ </Button>
108
+ </a>
109
+ <a
110
+ target="_blank"
111
+ href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/157"
112
+ >
113
+ <Button size="sm" variant="outline">
114
+ <HelpCircle className="size-3.5" />
115
+ <span className="max-lg:hidden">Help</span>
116
+ </Button>
117
+ </a>
118
+ <Button size="sm" variant="outline" onClick={handleRefreshIframe}>
119
+ <RefreshCcw className="size-3.5" />
120
+ <span className="max-lg:hidden">Refresh Preview</span>
121
+ </Button>
122
+ <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">
123
+ <div
124
+ className={classNames(
125
+ "absolute left-0.5 top-0.5 rounded-full bg-white size-7 -z-[1] transition-all duration-200",
126
+ {
127
+ "translate-x-[calc(100%+2px)]": device === "mobile",
128
+ }
129
+ )}
130
+ />
131
+ {DEVICES.map((deviceItem) => (
132
+ <button
133
+ key={deviceItem.name}
134
+ className={classNames(
135
+ "rounded-full text-neutral-300 size-7 flex items-center justify-center cursor-pointer",
136
+ {
137
+ "!text-black": device === deviceItem.name,
138
+ "hover:bg-neutral-800": device !== deviceItem.name,
139
+ }
140
+ )}
141
+ onClick={() => setDevice(deviceItem.name as "desktop" | "mobile")}
142
+ >
143
+ <deviceItem.icon className="text-sm" />
144
+ </button>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ </footer>
149
+ );
150
+ }