This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. .astro/content-assets.mjs +1 -0
  2. .astro/content-modules.mjs +1 -0
  3. .astro/data-store.json +1 -0
  4. .astro/settings.json +5 -0
  5. .astro/types.d.ts +1 -0
  6. .bolt/config.json +3 -0
  7. .codesandbox/Dockerfile +1 -0
  8. .gitignore +15 -32
  9. .vscode/extensions.json +4 -0
  10. .vscode/launch.json +11 -0
  11. README.md +48 -29
  12. app/(public)/layout.tsx +1 -1
  13. app/(public)/page.tsx +43 -4
  14. app/(public)/projects/page.tsx +13 -0
  15. app/[namespace]/[repoId]/page.tsx +0 -28
  16. app/actions/projects.ts +40 -24
  17. app/api/{ask → ask-ai}/route.ts +135 -350
  18. app/api/auth/login-url/route.ts +0 -23
  19. app/api/auth/logout/route.ts +0 -25
  20. app/api/auth/route.ts +1 -21
  21. app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -190
  22. app/api/me/projects/[namespace]/[repoId]/images/route.ts +0 -113
  23. app/api/me/projects/[namespace]/[repoId]/route.ts +162 -112
  24. app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -64
  25. app/api/me/projects/route.ts +92 -73
  26. app/api/me/route.ts +1 -22
  27. app/auth/callback/page.tsx +42 -67
  28. app/layout.tsx +30 -50
  29. app/new/page.tsx +0 -14
  30. app/projects/[namespace]/[repoId]/page.tsx +40 -0
  31. app/projects/new/page.tsx +5 -0
  32. app/sitemap.ts +0 -28
  33. assets/deepseek.svg +0 -1
  34. assets/globals.css +0 -225
  35. assets/kimi.svg +0 -1
  36. assets/qwen.svg +0 -1
  37. assets/zai.svg +0 -13
  38. astro.config.mjs +30 -0
  39. components.json +1 -1
  40. components/animated-blobs/index.tsx +0 -34
  41. components/animated-text/index.tsx +0 -123
  42. components/contexts/app-context.tsx +10 -6
  43. components/contexts/login-context.tsx +0 -62
  44. components/contexts/pro-context.tsx +0 -48
  45. components/editor/ask-ai/fake-ask.tsx +0 -97
  46. components/editor/ask-ai/follow-up-tooltip.tsx +36 -0
  47. components/editor/ask-ai/index.tsx +342 -199
  48. components/editor/ask-ai/loading.tsx +0 -68
  49. components/editor/ask-ai/prompt-builder/content-modal.tsx +0 -196
  50. components/editor/ask-ai/prompt-builder/index.tsx +0 -68
.astro/content-assets.mjs ADDED
@@ -0,0 +1 @@
 
 
1
+ export default new Map();
.astro/content-modules.mjs ADDED
@@ -0,0 +1 @@
 
 
1
+ export default new Map();
.astro/data-store.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.10.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://blueprintrak.com\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"i18n\":{\"defaultLocale\":\"ar\",\"locales\":[\"ar\",\"en\"],\"routing\":{\"prefixDefaultLocale\":false,\"redirectToDefaultLocale\":true,\"fallbackType\":\"redirect\"}},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false},\"legacy\":{\"collections\":false}}"]
.astro/settings.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "_variables": {
3
+ "lastUpdateCheck": 1751112608444
4
+ }
5
+ }
.astro/types.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="astro/client" />
.bolt/config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "template": "astro"
3
+ }
.codesandbox/Dockerfile ADDED
@@ -0,0 +1 @@
 
 
1
+ FROM node:18-bullseye
.gitignore CHANGED
@@ -1,41 +1,24 @@
1
- # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 
2
 
3
- # dependencies
4
- /node_modules
5
- /.pnp
6
- .pnp.*
7
- .yarn/*
8
- !.yarn/patches
9
- !.yarn/plugins
10
- !.yarn/releases
11
- !.yarn/versions
12
-
13
- # testing
14
- /coverage
15
-
16
- # next.js
17
- /.next/
18
- /out/
19
-
20
- # production
21
- /build
22
 
23
- # misc
24
- .DS_Store
25
- *.pem
26
 
27
- # debug
28
  npm-debug.log*
29
  yarn-debug.log*
30
  yarn-error.log*
31
- .pnpm-debug.log*
32
 
33
- # env files (can opt-in for committing if needed)
34
- .env*
 
35
 
36
- # vercel
37
- .vercel
38
 
39
- # typescript
40
- *.tsbuildinfo
41
- next-env.d.ts
 
1
+ # build output
2
+ dist/
3
 
4
+ # generated types
5
+ .astro/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ # dependencies
8
+ node_modules/
 
9
 
10
+ # logs
11
  npm-debug.log*
12
  yarn-debug.log*
13
  yarn-error.log*
14
+ pnpm-debug.log*
15
 
16
+ # environment variables
17
+ .env
18
+ .env.production
19
 
20
+ # macOS-specific files
21
+ .DS_Store
22
 
23
+ # jetbrains setting folder
24
+ .idea/
 
.vscode/extensions.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "recommendations": ["astro-build.astro-vscode"],
3
+ "unwantedRecommendations": []
4
+ }
.vscode/launch.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "command": "./node_modules/.bin/astro dev",
6
+ "name": "Development server",
7
+ "request": "launch",
8
+ "type": "node-terminal"
9
+ }
10
+ ]
11
+ }
README.md CHANGED
@@ -1,29 +1,48 @@
1
- ---
2
- title: DeepSite v3
3
- emoji: 🐳
4
- colorFrom: blue
5
- colorTo: blue
6
- sdk: docker
7
- pinned: true
8
- app_port: 3000
9
- license: mit
10
- short_description: Generate any application 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
+ # Astro Starter Kit: Basics
2
+
3
+ ```sh
4
+ npm create astro@latest -- --template basics
5
+ ```
6
+
7
+ [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
8
+ [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
9
+ [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
10
+
11
+ > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
12
+
13
+ ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
14
+
15
+ ## 🚀 Project Structure
16
+
17
+ Inside of your Astro project, you'll see the following folders and files:
18
+
19
+ ```text
20
+ /
21
+ ├── public/
22
+ │ └── favicon.svg
23
+ ├── src/
24
+ │ ├── layouts/
25
+ │ │ └── Layout.astro
26
+ │ └── pages/
27
+ │ └── index.astro
28
+ └── package.json
29
+ ```
30
+
31
+ To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
32
+
33
+ ## 🧞 Commands
34
+
35
+ All commands are run from the root of the project, from a terminal:
36
+
37
+ | Command | Action |
38
+ | :------------------------ | :----------------------------------------------- |
39
+ | `npm install` | Installs dependencies |
40
+ | `npm run dev` | Starts local dev server at `localhost:4321` |
41
+ | `npm run build` | Build your production site to `./dist/` |
42
+ | `npm run preview` | Preview your build locally, before deploying |
43
+ | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
44
+ | `npm run astro -- --help` | Get help using the Astro CLI |
45
+
46
+ ## 👀 Want to learn more?
47
+
48
+ Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
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/api/{ask → ask-ai}/route.ts RENAMED
@@ -4,29 +4,16 @@ import { NextResponse } from "next/server";
4
  import { headers } from "next/headers";
5
  import { InferenceClient } from "@huggingface/inference";
6
 
7
- import { MODELS } from "@/lib/providers";
8
  import {
9
  DIVIDER,
10
  FOLLOW_UP_SYSTEM_PROMPT,
11
  INITIAL_SYSTEM_PROMPT,
12
  MAX_REQUESTS_PER_IP,
13
- NEW_PAGE_END,
14
- NEW_PAGE_START,
15
  REPLACE_END,
16
  SEARCH_START,
17
- UPDATE_PAGE_START,
18
- UPDATE_PAGE_END,
19
- PROMPT_FOR_PROJECT_NAME,
20
  } from "@/lib/prompts";
21
- import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
22
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
23
- import { Page } from "@/types";
24
- import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
25
- import { isAuthenticated } from "@/lib/auth";
26
- import { getBestProvider } from "@/lib/best-provider";
27
- // import { rewritePrompt } from "@/lib/rewrite-prompt";
28
- import { COLORS } from "@/lib/utils";
29
- import { templates } from "@/lib/templates";
30
 
31
  const ipAddresses = new Map();
32
 
@@ -35,7 +22,7 @@ export async function POST(request: NextRequest) {
35
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
36
 
37
  const body = await request.json();
38
- const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
39
 
40
  if (!model || (!prompt && !redesignMarkdown)) {
41
  return NextResponse.json(
@@ -47,7 +34,6 @@ export async function POST(request: NextRequest) {
47
  const selectedModel = MODELS.find(
48
  (m) => m.value === model || m.label === model
49
  );
50
-
51
  if (!selectedModel) {
52
  return NextResponse.json(
53
  { ok: false, error: "Invalid model selected" },
@@ -55,8 +41,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,19 +85,19 @@ 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();
102
  const stream = new TransformStream();
103
  const writer = stream.writable.getWriter();
104
 
 
105
  const response = new NextResponse(stream.readable, {
106
  headers: {
107
  "Content-Type": "text/plain; charset=utf-8",
@@ -111,52 +107,75 @@ export async function POST(request: NextRequest) {
111
  });
112
 
113
  (async () => {
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
  }
151
 
152
  const chunk = value.choices[0]?.delta?.content;
153
  if (chunk) {
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 +187,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 +200,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,38 +219,22 @@ export async function POST(request: NextRequest) {
216
  }
217
 
218
  export async function PUT(request: NextRequest) {
219
- console.log("PUT request received");
220
- const user = await isAuthenticated();
221
- if (user instanceof NextResponse || !user) {
222
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
223
- }
224
-
225
  const authHeaders = await headers();
 
226
 
227
  const body = await request.json();
228
- const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew, enhancedSettings } =
229
- body;
230
-
231
- let repoId = repoIdFromBody;
232
 
233
- if (!prompt || pages.length === 0) {
234
  return NextResponse.json(
235
  { ok: false, error: "Missing required fields" },
236
  { status: 400 }
237
  );
238
  }
239
 
240
- const selectedModel = MODELS.find(
241
- (m) => m.value === model || m.label === model
242
- );
243
- if (!selectedModel) {
244
- return NextResponse.json(
245
- { ok: false, error: "Invalid model selected" },
246
- { status: 400 }
247
- );
248
- }
249
 
250
- let token = user.token as string;
251
  let billTo: string | null = null;
252
 
253
  /**
@@ -282,80 +269,52 @@ export async function PUT(request: NextRequest) {
282
 
283
  const client = new InferenceClient(token);
284
 
285
- const escapeRegExp = (string: string) => {
286
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
287
- };
288
-
289
- const createFlexibleHtmlRegex = (searchBlock: string) => {
290
- let searchRegex = escapeRegExp(searchBlock)
291
- .replace(/\s+/g, '\\s*')
292
- .replace(/>\s*</g, '>\\s*<')
293
- .replace(/\s*>/g, '\\s*>');
294
-
295
- return new RegExp(searchRegex, 'g');
296
- };
297
-
298
- const selectedProvider = await getBestProvider(selectedModel.value, provider)
299
 
300
  try {
301
- const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
302
- const userContext = "You are modifying the HTML file based on the user's request.";
303
-
304
- // Send all pages without filtering
305
- const allPages = pages || [];
306
- const pagesContext = allPages
307
- .map((p: Page) => `- ${p.path}\n${p.html}`)
308
- .join("\n\n");
309
-
310
- const assistantContext = `${
311
- selectedElementHtml
312
- ? `\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.`
313
- : ""
314
- }. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
315
-
316
- const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
317
- const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
318
- const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
319
-
320
- const chatCompletion = client.chatCompletionStream(
321
  {
322
  model: selectedModel.value,
323
- provider: selectedProvider.provider,
324
  messages: [
325
  {
326
  role: "system",
327
- content: systemPrompt,
328
  },
329
  {
330
  role: "user",
331
- content: userContext,
 
 
332
  },
333
  {
334
  role: "assistant",
335
- content: assistantContext,
 
 
 
 
 
336
  },
337
  {
338
  role: "user",
339
  content: prompt,
340
  },
341
  ],
342
- ...providerConfig,
 
 
 
 
343
  },
344
  billTo ? { billTo } : {}
345
  );
346
 
347
- let chunk = "";
348
- while (true) {
349
- const { done, value } = await chatCompletion.next();
350
- if (done) {
351
- break;
352
- }
353
-
354
- const deltaContent = value.choices[0]?.delta?.content;
355
- if (deltaContent) {
356
- chunk += deltaContent;
357
- }
358
- }
359
  if (!chunk) {
360
  return NextResponse.json(
361
  { ok: false, message: "No content returned from the model" },
@@ -365,234 +324,61 @@ export async function PUT(request: NextRequest) {
365
 
366
  if (chunk) {
367
  const updatedLines: number[][] = [];
368
- let newHtml = "";
369
- const updatedPages = [...(pages || [])];
370
-
371
- const updatePageRegex = new RegExp(`${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
372
- let updatePageMatch;
373
-
374
- while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
375
- const [, pagePath, pageContent] = updatePageMatch;
376
-
377
- const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
378
- if (pageIndex !== -1) {
379
- let pageHtml = updatedPages[pageIndex].html;
380
-
381
- let processedContent = pageContent;
382
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
383
- if (htmlMatch) {
384
- processedContent = htmlMatch[1];
385
- }
386
- let position = 0;
387
- let moreBlocks = true;
388
-
389
- while (moreBlocks) {
390
- const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
391
- if (searchStartIndex === -1) {
392
- moreBlocks = false;
393
- continue;
394
- }
395
-
396
- const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
397
- if (dividerIndex === -1) {
398
- moreBlocks = false;
399
- continue;
400
- }
401
-
402
- const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
403
- if (replaceEndIndex === -1) {
404
- moreBlocks = false;
405
- continue;
406
- }
407
-
408
- const searchBlock = processedContent.substring(
409
- searchStartIndex + SEARCH_START.length,
410
- dividerIndex
411
- );
412
- const replaceBlock = processedContent.substring(
413
- dividerIndex + DIVIDER.length,
414
- replaceEndIndex
415
- );
416
-
417
- if (searchBlock.trim() === "") {
418
- pageHtml = `${replaceBlock}\n${pageHtml}`;
419
- updatedLines.push([1, replaceBlock.split("\n").length]);
420
- } else {
421
- const regex = createFlexibleHtmlRegex(searchBlock);
422
- const match = regex.exec(pageHtml);
423
-
424
- if (match) {
425
- const matchedText = match[0];
426
- const beforeText = pageHtml.substring(0, match.index);
427
- const startLineNumber = beforeText.split("\n").length;
428
- const replaceLines = replaceBlock.split("\n").length;
429
- const endLineNumber = startLineNumber + replaceLines - 1;
430
-
431
- updatedLines.push([startLineNumber, endLineNumber]);
432
- pageHtml = pageHtml.replace(matchedText, replaceBlock);
433
- }
434
- }
435
-
436
- position = replaceEndIndex + REPLACE_END.length;
437
- }
438
-
439
- updatedPages[pageIndex].html = pageHtml;
440
-
441
- if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
442
- newHtml = pageHtml;
443
- }
444
  }
445
- }
446
 
447
- const newPageRegex = new RegExp(`${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
448
- let newPageMatch;
449
-
450
- while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
451
- const [, pagePath, pageContent] = newPageMatch;
452
-
453
- let pageHtml = pageContent;
454
- const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
455
- if (htmlMatch) {
456
- pageHtml = htmlMatch[1];
457
  }
458
-
459
- const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
460
-
461
- if (existingPageIndex !== -1) {
462
- updatedPages[existingPageIndex] = {
463
- path: pagePath,
464
- html: pageHtml.trim()
465
- };
466
- } else {
467
- updatedPages.push({
468
- path: pagePath,
469
- html: pageHtml.trim()
470
- });
471
- }
472
- }
473
 
474
- if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
475
- let position = 0;
476
- let moreBlocks = true;
477
-
478
- while (moreBlocks) {
479
- const searchStartIndex = chunk.indexOf(SEARCH_START, position);
480
- if (searchStartIndex === -1) {
481
- moreBlocks = false;
482
- continue;
483
- }
484
-
485
- const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
486
- if (dividerIndex === -1) {
487
- moreBlocks = false;
488
- continue;
489
- }
490
-
491
- const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
492
- if (replaceEndIndex === -1) {
493
- moreBlocks = false;
494
- continue;
495
- }
496
 
497
- const searchBlock = chunk.substring(
498
- searchStartIndex + SEARCH_START.length,
499
- dividerIndex
500
- );
501
- const replaceBlock = chunk.substring(
502
- dividerIndex + DIVIDER.length,
503
- replaceEndIndex
504
- );
505
 
506
- if (searchBlock.trim() === "") {
507
- newHtml = `${replaceBlock}\n${newHtml}`;
508
- updatedLines.push([1, replaceBlock.split("\n").length]);
509
- } else {
510
- const regex = createFlexibleHtmlRegex(searchBlock);
511
- const match = regex.exec(newHtml);
512
-
513
- if (match) {
514
- const matchedText = match[0];
515
- const beforeText = newHtml.substring(0, match.index);
516
- const startLineNumber = beforeText.split("\n").length;
517
- const replaceLines = replaceBlock.split("\n").length;
518
- const endLineNumber = startLineNumber + replaceLines - 1;
519
-
520
- updatedLines.push([startLineNumber, endLineNumber]);
521
- newHtml = newHtml.replace(matchedText, replaceBlock);
522
- }
523
  }
524
-
525
- position = replaceEndIndex + REPLACE_END.length;
526
- }
527
-
528
- // Update the main HTML if it's the index page
529
- const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
530
- if (mainPageIndex !== -1) {
531
- updatedPages[mainPageIndex].html = newHtml;
532
  }
533
- }
534
-
535
- const files: File[] = [];
536
- updatedPages.forEach((page: Page) => {
537
- const file = new File([page.html], page.path, { type: "text/html" });
538
- files.push(file);
539
- });
540
 
541
- if (isNew) {
542
- const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
543
- const formattedTitle = projectName?.toLowerCase()
544
- .replace(/[^a-z0-9]+/g, "-")
545
- .split("-")
546
- .filter(Boolean)
547
- .join("-")
548
- .slice(0, 96);
549
- const repo: RepoDesignation = {
550
- type: "space",
551
- name: `${user.name}/${formattedTitle}`,
552
- };
553
- const { repoUrl} = await createRepo({
554
- repo,
555
- accessToken: user.token as string,
556
- });
557
- repoId = repoUrl.split("/").slice(-2).join("/");
558
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
559
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
560
- const README = `---
561
- title: ${projectName}
562
- colorFrom: ${colorFrom}
563
- colorTo: ${colorTo}
564
- emoji: 🐳
565
- sdk: static
566
- pinned: false
567
- tags:
568
- - deepsite-v3
569
- ---
570
-
571
- # Welcome to your new DeepSite project!
572
- This project was created with [DeepSite](https://deepsite.hf.co).
573
- `;
574
- files.push(new File([README], "README.md", { type: "text/markdown" }));
575
  }
576
 
577
- const response = await uploadFiles({
578
- repo: {
579
- type: "space",
580
- name: repoId,
581
- },
582
- files,
583
- commitTitle: prompt,
584
- accessToken: user.token as string,
585
- });
586
-
587
  return NextResponse.json({
588
  ok: true,
 
589
  updatedLines,
590
- pages: updatedPages,
591
- repoId,
592
- commit: {
593
- ...response.commit,
594
- title: prompt,
595
- }
596
  });
597
  } else {
598
  return NextResponse.json(
@@ -622,4 +408,3 @@ This project was created with [DeepSite](https://deepsite.hf.co).
622
  );
623
  }
624
  }
625
-
 
4
  import { headers } from "next/headers";
5
  import { InferenceClient } from "@huggingface/inference";
6
 
7
+ import { MODELS, PROVIDERS } from "@/lib/providers";
8
  import {
9
  DIVIDER,
10
  FOLLOW_UP_SYSTEM_PROMPT,
11
  INITIAL_SYSTEM_PROMPT,
12
  MAX_REQUESTS_PER_IP,
 
 
13
  REPLACE_END,
14
  SEARCH_START,
 
 
 
15
  } from "@/lib/prompts";
 
16
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
 
 
 
 
 
 
 
17
 
18
  const ipAddresses = new Map();
19
 
 
22
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
23
 
24
  const body = await request.json();
25
+ const { prompt, provider, model, redesignMarkdown, html } = body;
26
 
27
  if (!model || (!prompt && !redesignMarkdown)) {
28
  return NextResponse.json(
 
34
  const selectedModel = MODELS.find(
35
  (m) => m.value === model || m.label === model
36
  );
 
37
  if (!selectedModel) {
38
  return NextResponse.json(
39
  { ok: false, error: "Invalid model selected" },
 
41
  );
42
  }
43
 
44
+ if (!selectedModel.providers.includes(provider) && provider !== "auto") {
45
+ return NextResponse.json(
46
+ {
47
+ ok: false,
48
+ error: `The selected model does not support the ${provider} provider.`,
49
+ openSelectProvider: true,
50
+ },
51
+ { status: 400 }
52
+ );
53
+ }
54
+
55
+ let token = userToken;
56
  let billTo: string | null = null;
57
 
58
  /**
 
85
  billTo = "huggingface";
86
  }
87
 
88
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
89
+ const selectedProvider =
90
+ provider === "auto"
91
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
92
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
 
 
93
 
94
  try {
95
+ // Create a stream response
96
  const encoder = new TextEncoder();
97
  const stream = new TransformStream();
98
  const writer = stream.writable.getWriter();
99
 
100
+ // Start the response
101
  const response = new NextResponse(stream.readable, {
102
  headers: {
103
  "Content-Type": "text/plain; charset=utf-8",
 
107
  });
108
 
109
  (async () => {
110
+ let completeResponse = "";
111
  try {
112
  const client = new InferenceClient(token);
 
 
 
 
 
 
 
 
113
  const chatCompletion = client.chatCompletionStream(
114
  {
115
  model: selectedModel.value,
116
+ provider: selectedProvider.id as any,
117
  messages: [
118
  {
119
  role: "system",
120
+ content: INITIAL_SYSTEM_PROMPT,
121
  },
122
  {
123
  role: "user",
124
+ content: redesignMarkdown
125
+ ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
126
+ : html
127
+ ? `Here is my current HTML code:\n\n\`\`\`html\n${html}\n\`\`\`\n\nNow, please create a new design based on this HTML.`
128
+ : prompt,
129
  },
130
  ],
131
+ max_tokens: selectedProvider.max_tokens,
132
  },
133
  billTo ? { billTo } : {}
134
  );
135
 
136
  while (true) {
137
+ const { done, value } = await chatCompletion.next();
138
  if (done) {
139
  break;
140
  }
141
 
142
  const chunk = value.choices[0]?.delta?.content;
143
  if (chunk) {
144
+ let newChunk = chunk;
145
+ if (!selectedModel?.isThinker) {
146
+ if (provider !== "sambanova") {
147
+ await writer.write(encoder.encode(chunk));
148
+ completeResponse += chunk;
149
+
150
+ if (completeResponse.includes("</html>")) {
151
+ break;
152
+ }
153
+ } else {
154
+ if (chunk.includes("</html>")) {
155
+ newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
156
+ }
157
+ completeResponse += newChunk;
158
+ await writer.write(encoder.encode(newChunk));
159
+ if (newChunk.includes("</html>")) {
160
+ break;
161
+ }
162
+ }
163
+ } else {
164
+ const lastThinkTagIndex =
165
+ completeResponse.lastIndexOf("</think>");
166
+ completeResponse += newChunk;
167
+ await writer.write(encoder.encode(newChunk));
168
+ if (lastThinkTagIndex !== -1) {
169
+ const afterLastThinkTag = completeResponse.slice(
170
+ lastThinkTagIndex + "</think>".length
171
+ );
172
+ if (afterLastThinkTag.includes("</html>")) {
173
+ break;
174
+ }
175
+ }
176
+ }
177
  }
178
  }
 
 
 
179
  } catch (error: any) {
180
  if (error.message?.includes("exceeded your monthly included credits")) {
181
  await writer.write(
 
187
  })
188
  )
189
  );
190
+ } else {
 
 
 
 
 
 
 
 
 
 
 
191
  await writer.write(
192
  encoder.encode(
193
  JSON.stringify({
 
200
  );
201
  }
202
  } finally {
203
+ await writer?.close();
 
 
 
 
 
204
  }
205
  })();
206
 
 
219
  }
220
 
221
  export async function PUT(request: NextRequest) {
 
 
 
 
 
 
222
  const authHeaders = await headers();
223
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
224
 
225
  const body = await request.json();
226
+ const { prompt, html, previousPrompt, provider, selectedElementHtml } = body;
 
 
 
227
 
228
+ if (!prompt || !html) {
229
  return NextResponse.json(
230
  { ok: false, error: "Missing required fields" },
231
  { status: 400 }
232
  );
233
  }
234
 
235
+ const selectedModel = MODELS[0];
 
 
 
 
 
 
 
 
236
 
237
+ let token = userToken;
238
  let billTo: string | null = null;
239
 
240
  /**
 
269
 
270
  const client = new InferenceClient(token);
271
 
272
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
273
+ const selectedProvider =
274
+ provider === "auto"
275
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
276
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
 
 
 
 
 
 
 
 
 
277
 
278
  try {
279
+ const response = await client.chatCompletion(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  {
281
  model: selectedModel.value,
282
+ provider: selectedProvider.id as any,
283
  messages: [
284
  {
285
  role: "system",
286
+ content: FOLLOW_UP_SYSTEM_PROMPT,
287
  },
288
  {
289
  role: "user",
290
+ content: previousPrompt
291
+ ? previousPrompt
292
+ : "You are modifying the HTML file based on the user's request.",
293
  },
294
  {
295
  role: "assistant",
296
+
297
+ content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${
298
+ selectedElementHtml
299
+ ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
300
+ : ""
301
+ }`,
302
  },
303
  {
304
  role: "user",
305
  content: prompt,
306
  },
307
  ],
308
+ ...(selectedProvider.id !== "sambanova"
309
+ ? {
310
+ max_tokens: selectedProvider.max_tokens,
311
+ }
312
+ : {}),
313
  },
314
  billTo ? { billTo } : {}
315
  );
316
 
317
+ const chunk = response.choices[0]?.message?.content;
 
 
 
 
 
 
 
 
 
 
 
318
  if (!chunk) {
319
  return NextResponse.json(
320
  { ok: false, message: "No content returned from the model" },
 
324
 
325
  if (chunk) {
326
  const updatedLines: number[][] = [];
327
+ let newHtml = html;
328
+ let position = 0;
329
+ let moreBlocks = true;
330
+
331
+ while (moreBlocks) {
332
+ const searchStartIndex = chunk.indexOf(SEARCH_START, position);
333
+ if (searchStartIndex === -1) {
334
+ moreBlocks = false;
335
+ continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  }
 
337
 
338
+ const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
339
+ if (dividerIndex === -1) {
340
+ moreBlocks = false;
341
+ continue;
 
 
 
 
 
 
342
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
+ const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
345
+ if (replaceEndIndex === -1) {
346
+ moreBlocks = false;
347
+ continue;
348
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ const searchBlock = chunk.substring(
351
+ searchStartIndex + SEARCH_START.length,
352
+ dividerIndex
353
+ );
354
+ const replaceBlock = chunk.substring(
355
+ dividerIndex + DIVIDER.length,
356
+ replaceEndIndex
357
+ );
358
 
359
+ if (searchBlock.trim() === "") {
360
+ newHtml = `${replaceBlock}\n${newHtml}`;
361
+ updatedLines.push([1, replaceBlock.split("\n").length]);
362
+ } else {
363
+ const blockPosition = newHtml.indexOf(searchBlock);
364
+ if (blockPosition !== -1) {
365
+ const beforeText = newHtml.substring(0, blockPosition);
366
+ const startLineNumber = beforeText.split("\n").length;
367
+ const replaceLines = replaceBlock.split("\n").length;
368
+ const endLineNumber = startLineNumber + replaceLines - 1;
369
+
370
+ updatedLines.push([startLineNumber, endLineNumber]);
371
+ newHtml = newHtml.replace(searchBlock, replaceBlock);
 
 
 
 
372
  }
 
 
 
 
 
 
 
 
373
  }
 
 
 
 
 
 
 
374
 
375
+ position = replaceEndIndex + REPLACE_END.length;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
 
 
 
 
 
 
 
 
 
 
 
378
  return NextResponse.json({
379
  ok: true,
380
+ html: newHtml,
381
  updatedLines,
 
 
 
 
 
 
382
  });
383
  } else {
384
  return NextResponse.json(
 
408
  );
409
  }
410
  }
 
app/api/auth/login-url/route.ts DELETED
@@ -1,23 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
-
3
- export async function GET(req: NextRequest) {
4
- const host = req.headers.get("host") ?? "localhost:3000";
5
-
6
- let url: string;
7
- if (host.includes("localhost")) {
8
- url = host;
9
- } else if (host.includes("hf.space") || host.includes("/spaces/enzostvs")) {
10
- url = "enzostvs-deepsite.hf.space";
11
- } else {
12
- url = "deepsite.hf.co";
13
- }
14
-
15
- const redirect_uri =
16
- `${host.includes("localhost") ? "http://" : "https://"}` +
17
- url +
18
- "/auth/callback";
19
-
20
- const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
21
-
22
- return NextResponse.json({ loginUrl: loginRedirectUrl });
23
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/auth/logout/route.ts DELETED
@@ -1,25 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import MY_TOKEN_KEY from "@/lib/get-cookie-name";
3
-
4
- export async function POST() {
5
- const cookieName = MY_TOKEN_KEY();
6
- const isProduction = process.env.NODE_ENV === "production";
7
-
8
- const response = NextResponse.json(
9
- { message: "Logged out successfully" },
10
- { status: 200 }
11
- );
12
-
13
- // Clear the HTTP-only cookie
14
- const cookieOptions = [
15
- `${cookieName}=`,
16
- "Max-Age=0",
17
- "Path=/",
18
- "HttpOnly",
19
- ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
20
- ].join("; ");
21
-
22
- response.headers.set("Set-Cookie", cookieOptions);
23
-
24
- return response;
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/auth/route.ts CHANGED
@@ -1,5 +1,4 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import MY_TOKEN_KEY from "@/lib/get-cookie-name";
3
 
4
  export async function POST(req: NextRequest) {
5
  const body = await req.json();
@@ -71,17 +70,11 @@ export async function POST(req: NextRequest) {
71
  }
72
  const user = await userResponse.json();
73
 
74
- const cookieName = MY_TOKEN_KEY();
75
- const isProduction = process.env.NODE_ENV === "production";
76
-
77
- // Create response with user data
78
- const nextResponse = NextResponse.json(
79
  {
80
  access_token: response.access_token,
81
  expires_in: response.expires_in,
82
  user,
83
- // Include fallback flag for iframe contexts
84
- useLocalStorageFallback: true,
85
  },
86
  {
87
  status: 200,
@@ -90,17 +83,4 @@ export async function POST(req: NextRequest) {
90
  },
91
  }
92
  );
93
-
94
- // Set HTTP-only cookie with proper attributes for iframe support
95
- const cookieOptions = [
96
- `${cookieName}=${response.access_token}`,
97
- `Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided
98
- "Path=/",
99
- "HttpOnly",
100
- ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
101
- ].join("; ");
102
-
103
- nextResponse.headers.set("Set-Cookie", cookieOptions);
104
-
105
- return nextResponse;
106
  }
 
1
  import { NextRequest, NextResponse } from "next/server";
 
2
 
3
  export async function POST(req: NextRequest) {
4
  const body = await req.json();
 
70
  }
71
  const user = await userResponse.json();
72
 
73
+ return NextResponse.json(
 
 
 
 
74
  {
75
  access_token: response.access_token,
76
  expires_in: response.expires_in,
77
  user,
 
 
78
  },
79
  {
80
  status: 200,
 
83
  },
84
  }
85
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts DELETED
@@ -1,190 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import { Page } from "@/types";
6
-
7
- export async function POST(
8
- req: NextRequest,
9
- { params }: {
10
- params: Promise<{
11
- namespace: string;
12
- repoId: string;
13
- commitId: string;
14
- }>
15
- }
16
- ) {
17
- const user = await isAuthenticated();
18
-
19
- if (user instanceof NextResponse || !user) {
20
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
21
- }
22
-
23
- const param = await params;
24
- const { namespace, repoId, commitId } = param;
25
-
26
- try {
27
- const repo: RepoDesignation = {
28
- type: "space",
29
- name: `${namespace}/${repoId}`,
30
- };
31
-
32
- const space = await spaceInfo({
33
- name: `${namespace}/${repoId}`,
34
- accessToken: user.token as string,
35
- additionalFields: ["author"],
36
- });
37
-
38
- if (!space || space.sdk !== "static") {
39
- return NextResponse.json(
40
- { ok: false, error: "Space is not a static space." },
41
- { status: 404 }
42
- );
43
- }
44
-
45
- if (space.author !== user.name) {
46
- return NextResponse.json(
47
- { ok: false, error: "Space does not belong to the authenticated user." },
48
- { status: 403 }
49
- );
50
- }
51
-
52
- // Fetch files from the specific commit
53
- const files: File[] = [];
54
- const pages: Page[] = [];
55
- const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
56
- const commitFilePaths: Set<string> = new Set();
57
-
58
- // Get all files from the specific commit
59
- for await (const fileInfo of listFiles({
60
- repo,
61
- accessToken: user.token as string,
62
- revision: commitId,
63
- })) {
64
- const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
65
-
66
- if (allowedExtensions.includes(fileExtension || "")) {
67
- commitFilePaths.add(fileInfo.path);
68
-
69
- // Fetch the file content from the specific commit
70
- const response = await fetch(
71
- `https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
72
- );
73
-
74
- if (response.ok) {
75
- const content = await response.text();
76
- let mimeType = "text/plain";
77
-
78
- switch (fileExtension) {
79
- case "html":
80
- mimeType = "text/html";
81
- // Add HTML files to pages array for client-side setPages
82
- pages.push({
83
- path: fileInfo.path,
84
- html: content,
85
- });
86
- break;
87
- case "css":
88
- mimeType = "text/css";
89
- break;
90
- case "js":
91
- mimeType = "application/javascript";
92
- break;
93
- case "json":
94
- mimeType = "application/json";
95
- break;
96
- case "md":
97
- mimeType = "text/markdown";
98
- break;
99
- }
100
-
101
- const file = new File([content], fileInfo.path, { type: mimeType });
102
- files.push(file);
103
- }
104
- }
105
- }
106
-
107
- // Get files currently in main branch to identify files to delete
108
- const mainBranchFilePaths: Set<string> = new Set();
109
- for await (const fileInfo of listFiles({
110
- repo,
111
- accessToken: user.token as string,
112
- revision: "main",
113
- })) {
114
- const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
115
-
116
- if (allowedExtensions.includes(fileExtension || "")) {
117
- mainBranchFilePaths.add(fileInfo.path);
118
- }
119
- }
120
-
121
- // Identify files to delete (exist in main but not in commit)
122
- const filesToDelete: string[] = [];
123
- for (const mainFilePath of mainBranchFilePaths) {
124
- if (!commitFilePaths.has(mainFilePath)) {
125
- filesToDelete.push(mainFilePath);
126
- }
127
- }
128
-
129
- if (files.length === 0 && filesToDelete.length === 0) {
130
- return NextResponse.json(
131
- { ok: false, error: "No files found in the specified commit and no files to delete" },
132
- { status: 404 }
133
- );
134
- }
135
-
136
- // Delete files that exist in main but not in the commit being promoted
137
- if (filesToDelete.length > 0) {
138
- await deleteFiles({
139
- repo,
140
- paths: filesToDelete,
141
- accessToken: user.token as string,
142
- commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
143
- commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
144
- });
145
- }
146
-
147
- // Upload the files to the main branch with a promotion commit message
148
- if (files.length > 0) {
149
- await uploadFiles({
150
- repo,
151
- files,
152
- accessToken: user.token as string,
153
- commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
154
- commitDescription: `Promoted commit ${commitId} to main branch`,
155
- });
156
- }
157
-
158
- return NextResponse.json(
159
- {
160
- ok: true,
161
- message: "Version promoted successfully",
162
- promotedCommit: commitId,
163
- pages: pages,
164
- },
165
- { status: 200 }
166
- );
167
-
168
- } catch (error: any) {
169
-
170
- // Handle specific HuggingFace API errors
171
- if (error.statusCode === 404) {
172
- return NextResponse.json(
173
- { ok: false, error: "Commit not found" },
174
- { status: 404 }
175
- );
176
- }
177
-
178
- if (error.statusCode === 403) {
179
- return NextResponse.json(
180
- { ok: false, error: "Access denied to repository" },
181
- { status: 403 }
182
- );
183
- }
184
-
185
- return NextResponse.json(
186
- { ok: false, error: error.message || "Failed to promote version" },
187
- { status: 500 }
188
- );
189
- }
190
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/images/route.ts DELETED
@@ -1,113 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import Project from "@/models/Project";
6
- import dbConnect from "@/lib/mongodb";
7
-
8
- export async function POST(
9
- req: NextRequest,
10
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
11
- ) {
12
- try {
13
- const user = await isAuthenticated();
14
-
15
- if (user instanceof NextResponse || !user) {
16
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
17
- }
18
-
19
- const param = await params;
20
- const { namespace, repoId } = param;
21
-
22
- const space = await spaceInfo({
23
- name: `${namespace}/${repoId}`,
24
- accessToken: user.token as string,
25
- additionalFields: ["author"],
26
- });
27
-
28
- if (!space || space.sdk !== "static") {
29
- return NextResponse.json(
30
- { ok: false, error: "Space is not a static space." },
31
- { status: 404 }
32
- );
33
- }
34
-
35
- if (space.author !== user.name) {
36
- return NextResponse.json(
37
- { ok: false, error: "Space does not belong to the authenticated user." },
38
- { status: 403 }
39
- );
40
- }
41
-
42
- // Parse the FormData to get the images
43
- const formData = await req.formData();
44
- const imageFiles = formData.getAll("images") as File[];
45
-
46
- if (!imageFiles || imageFiles.length === 0) {
47
- return NextResponse.json(
48
- {
49
- ok: false,
50
- error: "At least one image file is required under the 'images' key",
51
- },
52
- { status: 400 }
53
- );
54
- }
55
-
56
- const files: File[] = [];
57
- for (const file of imageFiles) {
58
- if (!(file instanceof File)) {
59
- return NextResponse.json(
60
- {
61
- ok: false,
62
- error: "Invalid file format - all items under 'images' key must be files",
63
- },
64
- { status: 400 }
65
- );
66
- }
67
-
68
- if (!file.type.startsWith('image/')) {
69
- return NextResponse.json(
70
- {
71
- ok: false,
72
- error: `File ${file.name} is not an image`,
73
- },
74
- { status: 400 }
75
- );
76
- }
77
-
78
- // Create File object with images/ folder prefix
79
- const fileName = `images/${file.name}`;
80
- const processedFile = new File([file], fileName, { type: file.type });
81
- files.push(processedFile);
82
- }
83
-
84
- // Upload files to HuggingFace space
85
- const repo: RepoDesignation = {
86
- type: "space",
87
- name: `${namespace}/${repoId}`,
88
- };
89
-
90
- await uploadFiles({
91
- repo,
92
- files,
93
- accessToken: user.token as string,
94
- commitTitle: `Upload ${files.length} image(s)`,
95
- });
96
-
97
- return NextResponse.json({
98
- ok: true,
99
- message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
100
- uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
101
- }, { status: 200 });
102
-
103
- } catch (error) {
104
- console.error('Error uploading images:', error);
105
- return NextResponse.json(
106
- {
107
- ok: false,
108
- error: "Failed to upload images",
109
- },
110
- { status: 500 }
111
- );
112
- }
113
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/route.ts CHANGED
@@ -1,10 +1,12 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, listFiles, deleteRepo, listCommits, downloadFile } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
- import { Commit, Page } from "@/types";
 
 
6
 
7
- export async function DELETE(
8
  req: NextRequest,
9
  { params }: { params: Promise<{ namespace: string; repoId: string }> }
10
  ) {
@@ -14,63 +16,24 @@ export async function DELETE(
14
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
15
  }
16
 
 
17
  const param = await params;
18
  const { namespace, repoId } = param;
19
 
20
- try {
21
- const space = await spaceInfo({
22
- name: `${namespace}/${repoId}`,
23
- accessToken: user.token as string,
24
- additionalFields: ["author"],
25
- });
26
-
27
- if (!space || space.sdk !== "static") {
28
- return NextResponse.json(
29
- { ok: false, error: "Space is not a static space." },
30
- { status: 404 }
31
- );
32
- }
33
-
34
- if (space.author !== user.name) {
35
- return NextResponse.json(
36
- { ok: false, error: "Space does not belong to the authenticated user." },
37
- { status: 403 }
38
- );
39
- }
40
-
41
- const repo: RepoDesignation = {
42
- type: "space",
43
- name: `${namespace}/${repoId}`,
44
- };
45
-
46
- await deleteRepo({
47
- repo,
48
- accessToken: user.token as string,
49
- });
50
-
51
-
52
- return NextResponse.json({ ok: true }, { status: 200 });
53
- } catch (error: any) {
54
  return NextResponse.json(
55
- { ok: false, error: error.message },
56
- { status: 500 }
 
 
 
57
  );
58
  }
59
- }
60
-
61
- export async function GET(
62
- req: NextRequest,
63
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
64
- ) {
65
- const user = await isAuthenticated();
66
-
67
- if (user instanceof NextResponse || !user) {
68
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
69
- }
70
-
71
- const param = await params;
72
- const { namespace, repoId } = param;
73
-
74
  try {
75
  const space = await spaceInfo({
76
  name: namespace + "/" + repoId,
@@ -97,75 +60,26 @@ export async function GET(
97
  );
98
  }
99
 
100
- const repo: RepoDesignation = {
101
- type: "space",
102
- name: `${namespace}/${repoId}`,
103
- };
104
-
105
- const htmlFiles: Page[] = [];
106
- const files: string[] = [];
107
-
108
- const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
109
-
110
- for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
111
- if (fileInfo.path.endsWith(".html")) {
112
- const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
113
- const html = await blob?.text();
114
- if (!html) {
115
- continue;
116
- }
117
- if (fileInfo.path === "index.html") {
118
- htmlFiles.unshift({
119
- path: fileInfo.path,
120
- html,
121
- });
122
- } else {
123
- htmlFiles.push({
124
- path: fileInfo.path,
125
- html,
126
- });
127
- }
128
- }
129
- if (fileInfo.type === "directory" && fileInfo.path === "images") {
130
- for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
131
- if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
132
- files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
133
- }
134
- }
135
- }
136
- }
137
- const commits: Commit[] = [];
138
- for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
139
- if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
140
- continue;
141
- }
142
- commits.push({
143
- title: commit.title,
144
- oid: commit.oid,
145
- date: commit.date,
146
- });
147
- }
148
-
149
- if (htmlFiles.length === 0) {
150
  return NextResponse.json(
151
  {
152
  ok: false,
153
- error: "No HTML files found",
154
  },
155
  { status: 404 }
156
  );
157
  }
 
 
 
 
158
  return NextResponse.json(
159
  {
160
  project: {
161
- id: space.id,
162
- space_id: space.name,
163
- private: space.private,
164
- _updatedAt: space.updatedAt,
165
  },
166
- pages: htmlFiles,
167
- files,
168
- commits,
169
  ok: true,
170
  },
171
  { status: 200 }
@@ -174,6 +88,10 @@ export async function GET(
174
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
  } catch (error: any) {
176
  if (error.statusCode === 404) {
 
 
 
 
177
  return NextResponse.json(
178
  { error: "Space not found", ok: false },
179
  { status: 404 }
@@ -185,3 +103,135 @@ export async function GET(
185
  );
186
  }
187
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, spaceInfo, uploadFile } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
+ import Project from "@/models/Project";
6
+ import dbConnect from "@/lib/mongodb";
7
+ import { getPTag } from "@/lib/utils";
8
 
9
+ export async function GET(
10
  req: NextRequest,
11
  { params }: { params: Promise<{ namespace: string; repoId: string }> }
12
  ) {
 
16
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
17
  }
18
 
19
+ await dbConnect();
20
  const param = await params;
21
  const { namespace, repoId } = param;
22
 
23
+ const project = await Project.findOne({
24
+ user_id: user.id,
25
+ space_id: `${namespace}/${repoId}`,
26
+ }).lean();
27
+ if (!project) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  return NextResponse.json(
29
+ {
30
+ ok: false,
31
+ error: "Project not found",
32
+ },
33
+ { status: 404 }
34
  );
35
  }
36
+ const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  try {
38
  const space = await spaceInfo({
39
  name: namespace + "/" + repoId,
 
60
  );
61
  }
62
 
63
+ const response = await fetch(space_url);
64
+ if (!response.ok) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  return NextResponse.json(
66
  {
67
  ok: false,
68
+ error: "Failed to fetch space HTML",
69
  },
70
  { status: 404 }
71
  );
72
  }
73
+ let html = await response.text();
74
+ // remove the last p tag including this url https://enzostvs-deepsite.hf.space
75
+ html = html.replace(getPTag(namespace + "/" + repoId), "");
76
+
77
  return NextResponse.json(
78
  {
79
  project: {
80
+ ...project,
81
+ html,
 
 
82
  },
 
 
 
83
  ok: true,
84
  },
85
  { status: 200 }
 
88
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
  } catch (error: any) {
90
  if (error.statusCode === 404) {
91
+ await Project.deleteOne({
92
+ user_id: user.id,
93
+ space_id: `${namespace}/${repoId}`,
94
+ });
95
  return NextResponse.json(
96
  { error: "Space not found", ok: false },
97
  { status: 404 }
 
103
  );
104
  }
105
  }
106
+
107
+ export async function PUT(
108
+ req: NextRequest,
109
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
110
+ ) {
111
+ const user = await isAuthenticated();
112
+
113
+ if (user instanceof NextResponse || !user) {
114
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
115
+ }
116
+
117
+ await dbConnect();
118
+ const param = await params;
119
+ const { namespace, repoId } = param;
120
+ const { html, prompts } = await req.json();
121
+
122
+ const project = await Project.findOne({
123
+ user_id: user.id,
124
+ space_id: `${namespace}/${repoId}`,
125
+ }).lean();
126
+ if (!project) {
127
+ return NextResponse.json(
128
+ {
129
+ ok: false,
130
+ error: "Project not found",
131
+ },
132
+ { status: 404 }
133
+ );
134
+ }
135
+
136
+ const repo: RepoDesignation = {
137
+ type: "space",
138
+ name: `${namespace}/${repoId}`,
139
+ };
140
+
141
+ const newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
142
+ const file = new File([newHtml], "index.html", { type: "text/html" });
143
+ await uploadFile({
144
+ repo,
145
+ file,
146
+ accessToken: user.token as string,
147
+ commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
148
+ });
149
+
150
+ await Project.updateOne(
151
+ { user_id: user.id, space_id: `${namespace}/${repoId}` },
152
+ {
153
+ $set: {
154
+ prompts: [
155
+ ...(project && "prompts" in project ? project.prompts : []),
156
+ ...prompts,
157
+ ],
158
+ },
159
+ }
160
+ );
161
+ return NextResponse.json({ ok: true }, { status: 200 });
162
+ }
163
+
164
+ export async function POST(
165
+ req: NextRequest,
166
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
167
+ ) {
168
+ const user = await isAuthenticated();
169
+
170
+ if (user instanceof NextResponse || !user) {
171
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
172
+ }
173
+
174
+ await dbConnect();
175
+ const param = await params;
176
+ const { namespace, repoId } = param;
177
+
178
+ const space = await spaceInfo({
179
+ name: namespace + "/" + repoId,
180
+ accessToken: user.token as string,
181
+ additionalFields: ["author"],
182
+ });
183
+
184
+ if (!space || space.sdk !== "static") {
185
+ return NextResponse.json(
186
+ {
187
+ ok: false,
188
+ error: "Space is not a static space",
189
+ },
190
+ { status: 404 }
191
+ );
192
+ }
193
+ if (space.author !== user.name) {
194
+ return NextResponse.json(
195
+ {
196
+ ok: false,
197
+ error: "Space does not belong to the authenticated user",
198
+ },
199
+ { status: 403 }
200
+ );
201
+ }
202
+
203
+ const project = await Project.findOne({
204
+ user_id: user.id,
205
+ space_id: `${namespace}/${repoId}`,
206
+ }).lean();
207
+ if (project) {
208
+ // redirect to the project page if it already exists
209
+ return NextResponse.json(
210
+ {
211
+ ok: false,
212
+ error: "Project already exists",
213
+ redirect: `/projects/${namespace}/${repoId}`,
214
+ },
215
+ { status: 400 }
216
+ );
217
+ }
218
+
219
+ const newProject = new Project({
220
+ user_id: user.id,
221
+ space_id: `${namespace}/${repoId}`,
222
+ prompts: [],
223
+ });
224
+
225
+ await newProject.save();
226
+ return NextResponse.json(
227
+ {
228
+ ok: true,
229
+ project: {
230
+ id: newProject._id,
231
+ space_id: newProject.space_id,
232
+ prompts: newProject.prompts,
233
+ },
234
+ },
235
+ { status: 201 }
236
+ );
237
+ }
app/api/me/projects/[namespace]/[repoId]/save/route.ts DELETED
@@ -1,64 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { uploadFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import { Page } from "@/types";
6
-
7
- export async function PUT(
8
- req: NextRequest,
9
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
10
- ) {
11
- const user = await isAuthenticated();
12
- if (user instanceof NextResponse || !user) {
13
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
14
- }
15
-
16
- const param = await params;
17
- const { namespace, repoId } = param;
18
- const { pages, commitTitle = "Manual changes saved" } = await req.json();
19
-
20
- if (!pages || !Array.isArray(pages) || pages.length === 0) {
21
- return NextResponse.json(
22
- { ok: false, error: "Pages are required" },
23
- { status: 400 }
24
- );
25
- }
26
-
27
- try {
28
- // Prepare files for upload
29
- const files: File[] = [];
30
- pages.forEach((page: Page) => {
31
- const file = new File([page.html], page.path, { type: "text/html" });
32
- files.push(file);
33
- });
34
-
35
- // Upload files to HuggingFace Hub
36
- const response = await uploadFiles({
37
- repo: {
38
- type: "space",
39
- name: `${namespace}/${repoId}`,
40
- },
41
- files,
42
- commitTitle,
43
- accessToken: user.token as string,
44
- });
45
-
46
- return NextResponse.json({
47
- ok: true,
48
- pages,
49
- commit: {
50
- ...response.commit,
51
- title: commitTitle,
52
- }
53
- });
54
- } catch (error: any) {
55
- console.error("Error saving manual changes:", error);
56
- return NextResponse.json(
57
- {
58
- ok: false,
59
- error: error.message || "Failed to save changes",
60
- },
61
- { status: 500 }
62
- );
63
- }
64
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/route.ts CHANGED
@@ -1,107 +1,126 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
- import { Commit, Page } from "@/types";
6
- import { COLORS } from "@/lib/utils";
 
 
 
 
 
 
 
 
7
 
8
- export async function POST(
9
- req: NextRequest,
10
- ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  const user = await isAuthenticated();
 
12
  if (user instanceof NextResponse || !user) {
13
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
14
  }
15
 
16
- const { title: titleFromRequest, pages, prompt } = await req.json();
 
 
 
 
 
 
 
 
 
17
 
18
- const title = titleFromRequest ?? "DeepSite Project";
 
 
19
 
20
- const formattedTitle = title
21
- .toLowerCase()
22
- .replace(/[^a-z0-9]+/g, "-")
23
- .split("-")
24
- .filter(Boolean)
25
- .join("-")
26
- .slice(0, 96);
27
 
28
- const repo: RepoDesignation = {
29
- type: "space",
30
- name: `${user.name}/${formattedTitle}`,
31
- };
32
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
33
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
34
- const README = `---
35
- title: ${title}
 
 
 
 
 
 
36
  colorFrom: ${colorFrom}
37
  colorTo: ${colorTo}
38
- emoji: 🐳
39
  sdk: static
40
  pinned: false
41
  tags:
42
- - deepsite-v3
43
  ---
44
 
45
- # Welcome to your new DeepSite project!
46
- This project was created with [DeepSite](https://deepsite.hf.co).
47
- `;
48
 
49
- const files: File[] = [];
50
- const readmeFile = new File([README], "README.md", { type: "text/markdown" });
51
- files.push(readmeFile);
52
- pages.forEach((page: Page) => {
53
- const file = new File([page.html], page.path, { type: "text/html" });
54
- files.push(file);
55
- });
56
-
57
- try {
58
- const { repoUrl} = await createRepo({
59
- repo,
60
- accessToken: user.token as string,
61
  });
62
- const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
63
  await uploadFiles({
64
  repo,
65
  files,
66
  accessToken: user.token as string,
67
- commitTitle
68
  });
69
-
70
  const path = repoUrl.split("/").slice(-2).join("/");
71
-
72
- const commits: Commit[] = [];
73
- for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
74
- if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
75
- continue;
76
- }
77
- commits.push({
78
- title: commit.title,
79
- oid: commit.oid,
80
- date: commit.date,
81
- });
82
- }
83
-
84
- const space = await spaceInfo({
85
- name: repo.name,
86
- accessToken: user.token as string,
87
  });
88
-
89
- let newProject = {
90
- files,
91
- pages,
92
- commits,
93
- project: {
94
- id: space.id,
95
- space_id: space.name,
96
- _updatedAt: space.updatedAt,
97
- }
98
- }
99
-
100
- return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
101
  } catch (err: any) {
102
  return NextResponse.json(
103
  { error: err.message, ok: false },
104
  { status: 500 }
105
  );
106
  }
107
- }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
+ import Project from "@/models/Project";
6
+ import dbConnect from "@/lib/mongodb";
7
+ import { COLORS, getPTag } from "@/lib/utils";
8
+ // import type user
9
+ export async function GET() {
10
+ const user = await isAuthenticated();
11
+
12
+ if (user instanceof NextResponse || !user) {
13
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
14
+ }
15
 
16
+ await dbConnect();
17
+
18
+ const projects = await Project.find({
19
+ user_id: user?.id,
20
+ })
21
+ .sort({ _createdAt: -1 })
22
+ .limit(100)
23
+ .lean();
24
+ if (!projects) {
25
+ return NextResponse.json(
26
+ {
27
+ ok: false,
28
+ projects: [],
29
+ },
30
+ { status: 404 }
31
+ );
32
+ }
33
+ return NextResponse.json(
34
+ {
35
+ ok: true,
36
+ projects,
37
+ },
38
+ { status: 200 }
39
+ );
40
+ }
41
+
42
+ /**
43
+ * This API route creates a new project in Hugging Face Spaces.
44
+ * It requires an Authorization header with a valid token and a JSON body with the project details.
45
+ */
46
+ export async function POST(request: NextRequest) {
47
  const user = await isAuthenticated();
48
+
49
  if (user instanceof NextResponse || !user) {
50
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
51
  }
52
 
53
+ const { title, html, prompts } = await request.json();
54
+
55
+ if (!title || !html) {
56
+ return NextResponse.json(
57
+ { message: "Title and HTML content are required.", ok: false },
58
+ { status: 400 }
59
+ );
60
+ }
61
+
62
+ await dbConnect();
63
 
64
+ try {
65
+ let readme = "";
66
+ let newHtml = html;
67
 
68
+ const newTitle = title
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, "-")
71
+ .split("-")
72
+ .filter(Boolean)
73
+ .join("-")
74
+ .slice(0, 96);
75
 
76
+ const repo: RepoDesignation = {
77
+ type: "space",
78
+ name: `${user.name}/${newTitle}`,
79
+ };
80
+
81
+ const { repoUrl } = await createRepo({
82
+ repo,
83
+ accessToken: user.token as string,
84
+ });
85
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
86
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
87
+ readme = `---
88
+ title: ${newTitle}
89
+ emoji: 🐳
90
  colorFrom: ${colorFrom}
91
  colorTo: ${colorTo}
 
92
  sdk: static
93
  pinned: false
94
  tags:
95
+ - deepsite
96
  ---
97
 
98
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
 
 
99
 
100
+ newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
101
+ const file = new File([newHtml], "index.html", { type: "text/html" });
102
+ const readmeFile = new File([readme], "README.md", {
103
+ type: "text/markdown",
 
 
 
 
 
 
 
 
104
  });
105
+ const files = [file, readmeFile];
106
  await uploadFiles({
107
  repo,
108
  files,
109
  accessToken: user.token as string,
110
+ commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
111
  });
 
112
  const path = repoUrl.split("/").slice(-2).join("/");
113
+ const project = await Project.create({
114
+ user_id: user.id,
115
+ space_id: path,
116
+ prompts,
 
 
 
 
 
 
 
 
 
 
 
 
117
  });
118
+ return NextResponse.json({ project, path, ok: true }, { status: 201 });
119
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
 
 
 
 
 
 
 
 
 
 
 
120
  } catch (err: any) {
121
  return NextResponse.json(
122
  { error: err.message, ok: false },
123
  { status: 500 }
124
  );
125
  }
126
+ }
app/api/me/route.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { listSpaces } from "@huggingface/hub";
2
  import { headers } from "next/headers";
3
  import { NextResponse } from "next/server";
4
 
@@ -22,25 +21,5 @@ export async function GET() {
22
  );
23
  }
24
  const user = await userResponse.json();
25
- const projects = [];
26
- for await (const space of listSpaces({
27
- accessToken: token.replace("Bearer ", "") as string,
28
- additionalFields: ["author", "cardData"],
29
- search: {
30
- owner: user.name,
31
- }
32
- })) {
33
- if (
34
- space.sdk === "static" &&
35
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
36
- (
37
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
38
- ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
39
- )
40
- ) {
41
- projects.push(space);
42
- }
43
- }
44
-
45
- return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
46
  }
 
 
1
  import { headers } from "next/headers";
2
  import { NextResponse } from "next/server";
3
 
 
21
  );
22
  }
23
  const user = await userResponse.json();
24
+ return NextResponse.json({ user, errCode: null }, { status: 200 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
app/auth/callback/page.tsx CHANGED
@@ -5,92 +5,67 @@ import { use, useState } from "react";
5
  import { useMount, useTimeoutFn } from "react-use";
6
 
7
  import { Button } from "@/components/ui/button";
8
- import { AnimatedBlobs } from "@/components/animated-blobs";
9
- import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
10
  export default function AuthCallback({
11
  searchParams,
12
  }: {
13
  searchParams: Promise<{ code: string }>;
14
  }) {
15
  const [showButton, setShowButton] = useState(false);
16
- const [isPopupAuth, setIsPopupAuth] = useState(false);
17
  const { code } = use(searchParams);
18
  const { loginFromCode } = useUser();
19
- const { postMessage } = useBroadcastChannel("auth", () => {});
20
 
21
  useMount(async () => {
22
  if (code) {
23
- const isPopup = window.opener || window.parent !== window;
24
- setIsPopupAuth(isPopup);
25
-
26
- if (isPopup) {
27
- postMessage({
28
- type: "user-oauth",
29
- code: code,
30
- });
31
-
32
- setTimeout(() => {
33
- if (window.opener) {
34
- window.close();
35
- }
36
- }, 1000);
37
- } else {
38
- await loginFromCode(code);
39
- }
40
  }
41
  });
42
 
43
- useTimeoutFn(() => setShowButton(true), 7000);
 
 
 
44
 
45
  return (
46
- <div className="h-screen flex flex-col justify-center items-center bg-neutral-950 z-1 relative">
47
- <div className="background__noisy" />
48
- <div className="relative max-w-4xl py-10 flex items-center justify-center w-full">
49
- <div className="max-w-lg mx-auto !rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
50
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
51
- <div className="flex items-center justify-center -space-x-4 mb-3">
52
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
53
- 🚀
54
- </div>
55
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
56
- 👋
57
- </div>
58
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
59
- 🙌
60
- </div>
61
  </div>
62
- <p className="text-xl font-semibold text-neutral-950">
63
- {isPopupAuth
64
- ? "Authentication Complete!"
65
- : "Login In Progress..."}
66
- </p>
67
- <p className="text-sm text-neutral-500 mt-1.5">
68
- {isPopupAuth
69
- ? "You can now close this tab and return to the previous page."
70
- : "Wait a moment while we log you in with your code."}
 
 
 
 
 
 
 
 
 
 
71
  </p>
72
- </header>
73
- <main className="space-y-4 p-6">
74
- <div>
75
- <p className="text-sm text-neutral-700 mb-4 max-w-xs">
76
- If you are not redirected automatically in the next 5 seconds,
77
- please click the button below
 
 
 
78
  </p>
79
- {showButton ? (
80
- <Link href="/">
81
- <Button variant="black" className="relative">
82
- Go to Home
83
- </Button>
84
- </Link>
85
- ) : (
86
- <p className="text-xs text-neutral-500">
87
- Please wait, we are logging you in...
88
- </p>
89
- )}
90
- </div>
91
- </main>
92
- </div>
93
- <AnimatedBlobs />
94
  </div>
95
  </div>
96
  );
 
5
  import { useMount, useTimeoutFn } from "react-use";
6
 
7
  import { Button } from "@/components/ui/button";
 
 
8
  export default function AuthCallback({
9
  searchParams,
10
  }: {
11
  searchParams: Promise<{ code: string }>;
12
  }) {
13
  const [showButton, setShowButton] = useState(false);
 
14
  const { code } = use(searchParams);
15
  const { loginFromCode } = useUser();
 
16
 
17
  useMount(async () => {
18
  if (code) {
19
+ await loginFromCode(code);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
  });
22
 
23
+ useTimeoutFn(
24
+ () => setShowButton(true),
25
+ 7000 // Show button after 5 seconds
26
+ );
27
 
28
  return (
29
+ <div className="h-screen flex flex-col justify-center items-center">
30
+ <div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
31
+ <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
32
+ <div className="flex items-center justify-center -space-x-4 mb-3">
33
+ <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
34
+ 🚀
 
 
 
 
 
 
 
 
 
35
  </div>
36
+ <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
37
+ 👋
38
+ </div>
39
+ <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
40
+ 🙌
41
+ </div>
42
+ </div>
43
+ <p className="text-xl font-semibold text-neutral-950">
44
+ Login In Progress...
45
+ </p>
46
+ <p className="text-sm text-neutral-500 mt-1.5">
47
+ Wait a moment while we log you in with your code.
48
+ </p>
49
+ </header>
50
+ <main className="space-y-4 p-6">
51
+ <div>
52
+ <p className="text-sm text-neutral-700 mb-4 max-w-xs">
53
+ If you are not redirected automatically in the next 5 seconds,
54
+ please click the button below
55
  </p>
56
+ {showButton ? (
57
+ <Link href="/">
58
+ <Button variant="black" className="relative">
59
+ Go to Home
60
+ </Button>
61
+ </Link>
62
+ ) : (
63
+ <p className="text-xs text-neutral-500">
64
+ Please wait, we are logging you in...
65
  </p>
66
+ )}
67
+ </div>
68
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
69
  </div>
70
  </div>
71
  );
app/layout.tsx CHANGED
@@ -2,18 +2,14 @@
2
  import type { Metadata, Viewport } from "next";
3
  import { Inter, PT_Sans } from "next/font/google";
4
  import { cookies } from "next/headers";
5
- import Script from "next/script";
6
 
 
7
  import "@/assets/globals.css";
8
  import { Toaster } from "@/components/ui/sonner";
9
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
10
  import { apiServer } from "@/lib/api";
11
- import IframeDetector from "@/components/iframe-detector";
12
  import AppContext from "@/components/contexts/app-context";
13
- import TanstackContext from "@/components/contexts/tanstack-query-context";
14
- import { LoginProvider } from "@/components/contexts/login-context";
15
- import { ProProvider } from "@/components/contexts/pro-context";
16
- import { generateSEO, generateStructuredData } from "@/lib/seo";
17
 
18
  const inter = Inter({
19
  variable: "--font-inter-sans",
@@ -27,12 +23,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 +58,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,16 +69,16 @@ 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
 
@@ -76,35 +88,8 @@ export default async function RootLayout({
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"
@@ -113,15 +98,10 @@ export default async function RootLayout({
113
  <body
114
  className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
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
 
14
  const inter = Inter({
15
  variable: "--font-inter-sans",
 
23
  });
24
 
25
  export const metadata: Metadata = {
26
+ title: "DeepSite | Build with AI ✨",
27
+ description:
28
+ "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.",
29
+ openGraph: {
30
+ title: "DeepSite | Build with AI ✨",
31
+ description:
32
+ "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.",
33
+ url: "https://deepsite.hf.co",
34
+ siteName: "DeepSite",
35
+ images: [
36
+ {
37
+ url: "https://deepsite.hf.co/banner.png",
38
+ width: 1200,
39
+ height: 630,
40
+ alt: "DeepSite Open Graph Image",
41
+ },
42
+ ],
43
+ },
44
+ twitter: {
45
+ card: "summary_large_image",
46
  title: "DeepSite | Build with AI ✨",
47
  description:
48
  "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.",
49
+ images: ["https://deepsite.hf.co/banner.png"],
50
+ },
51
  appleWebApp: {
52
  capable: true,
53
  title: "DeepSite",
 
58
  shortcut: "/logo.svg",
59
  apple: "/logo.svg",
60
  },
 
 
 
61
  };
62
 
63
  export const viewport: Viewport = {
 
69
  async function getMe() {
70
  const cookieStore = await cookies();
71
  const token = cookieStore.get(MY_TOKEN_KEY())?.value;
72
+ if (!token) return { user: null, errCode: null };
73
  try {
74
  const res = await apiServer.get("/me", {
75
  headers: {
76
  Authorization: `Bearer ${token}`,
77
  },
78
  });
79
+ return { user: res.data.user, errCode: null };
80
  } catch (err: any) {
81
+ return { user: null, errCode: err.status };
82
  }
83
  }
84
 
 
88
  children: React.ReactNode;
89
  }>) {
90
  const data = await getMe();
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  return (
92
  <html lang="en">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  <Script
94
  defer
95
  data-domain="deepsite.hf.co"
 
98
  <body
99
  className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
100
  >
 
101
  <Toaster richColors position="bottom-center" />
102
+ <TanstackProvider>
103
+ <AppContext me={data}>{children}</AppContext>
104
+ </TanstackProvider>
 
 
 
 
105
  </body>
106
  </html>
107
  );
app/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,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cookies } from "next/headers";
2
+ import { redirect } from "next/navigation";
3
+
4
+ import { apiServer } from "@/lib/api";
5
+ import MY_TOKEN_KEY from "@/lib/get-cookie-name";
6
+ import { AppEditor } from "@/components/editor";
7
+
8
+ async function getProject(namespace: string, repoId: string) {
9
+ // TODO replace with a server action
10
+ const cookieStore = await cookies();
11
+ const token = cookieStore.get(MY_TOKEN_KEY())?.value;
12
+ if (!token) return {};
13
+ try {
14
+ const { data } = await apiServer.get(
15
+ `/me/projects/${namespace}/${repoId}`,
16
+ {
17
+ headers: {
18
+ Authorization: `Bearer ${token}`,
19
+ },
20
+ }
21
+ );
22
+
23
+ return data.project;
24
+ } catch {
25
+ return {};
26
+ }
27
+ }
28
+
29
+ export default async function ProjectNamespacePage({
30
+ params,
31
+ }: {
32
+ params: Promise<{ namespace: string; repoId: string }>;
33
+ }) {
34
+ const { namespace, repoId } = await params;
35
+ const project = await getProject(namespace, repoId);
36
+ if (!project?.html) {
37
+ redirect("/projects");
38
+ }
39
+ return <AppEditor project={project} />;
40
+ }
app/projects/new/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { AppEditor } from "@/components/editor";
2
+
3
+ export default function ProjectsNewPage() {
4
+ return <AppEditor />;
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
astro.config.mjs ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+ import { defineConfig } from 'astro/config';
3
+ import tailwind from '@astrojs/tailwind';
4
+ import sitemap from '@astrojs/sitemap';
5
+
6
+ // https://astro.build/config
7
+ export default defineConfig({
8
+ site: 'https://blueprintrak.com',
9
+ integrations: [
10
+ tailwind({
11
+ applyBaseStyles: false,
12
+ }),
13
+ sitemap({
14
+ i18n: {
15
+ defaultLocale: 'ar',
16
+ locales: {
17
+ ar: 'ar-AE',
18
+ en: 'en-US'
19
+ }
20
+ }
21
+ })
22
+ ],
23
+ i18n: {
24
+ defaultLocale: 'ar',
25
+ locales: ['ar', 'en'],
26
+ routing: {
27
+ prefixDefaultLocale: false
28
+ }
29
+ }
30
+ });
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,144 +1,270 @@
1
- import { useRef, useState } from "react";
 
 
2
  import classNames from "classnames";
3
- import { ArrowUp, ChevronDown, CircleStop, Dice6 } from "lucide-react";
4
- import { useLocalStorage, useUpdateEffect, useMount } from "react-use";
5
  import { toast } from "sonner";
 
 
 
6
 
7
- import { useAi } from "@/hooks/useAi";
8
- import { useEditor } from "@/hooks/useEditor";
9
- import { EnhancedSettings, Project } from "@/types";
10
- import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
11
- import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
12
- import { AiLoading } from "@/components/editor/ask-ai/loading";
13
  import { Button } from "@/components/ui/button";
14
- import { Uploader } from "@/components/editor/ask-ai/uploader";
 
 
 
 
15
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
16
- import { Selector } from "@/components/editor/ask-ai/selector";
17
- import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
18
- import { useUser } from "@/hooks/useUser";
19
- import { useLoginModal } from "@/components/contexts/login-context";
20
- import { Settings } from "./settings";
21
- import { useProModal } from "@/components/contexts/pro-context";
22
- import { MAX_FREE_PROJECTS } from "@/lib/utils";
23
- import { PROMPTS_FOR_AI } from "@/lib/prompts";
24
 
25
- export const AskAi = ({
26
- project,
27
- isNew,
28
  onScrollToBottom,
 
 
 
 
 
 
 
 
29
  }: {
30
- project?: Project;
31
- files?: string[];
32
- isNew?: boolean;
33
- onScrollToBottom?: () => void;
34
- }) => {
35
- const { user, projects } = useUser();
36
- const { isSameHtml, isUploading, pages, isLoadingProject } = useEditor();
37
- const {
38
- isAiWorking,
39
- isThinking,
40
- selectedFiles,
41
- setSelectedFiles,
42
- selectedElement,
43
- setSelectedElement,
44
- setIsThinking,
45
- callAiNewProject,
46
- callAiFollowUp,
47
- setModel,
48
- selectedModel,
49
- audio: hookAudio,
50
- cancelRequest,
51
- } = useAi(onScrollToBottom);
52
- const { openLoginModal } = useLoginModal();
53
- const { openProModal } = useProModal();
54
  const [openProvider, setOpenProvider] = useState(false);
55
  const [providerError, setProviderError] = useState("");
56
- const refThink = useRef<HTMLDivElement>(null);
57
-
58
- const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
59
- useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
60
- isActive: false,
61
- primaryColor: undefined,
62
- secondaryColor: undefined,
63
- theme: undefined,
64
- });
65
- const [promptStorage, , removePromptStorage] = useLocalStorage("prompt", "");
66
-
67
- const [isFollowUp, setIsFollowUp] = useState(true);
68
- const [prompt, setPrompt] = useState(
69
- promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
70
- );
71
- const [think, setThink] = useState("");
72
  const [openThink, setOpenThink] = useState(false);
73
- const [randomPromptLoading, setRandomPromptLoading] = useState(false);
74
-
75
- useMount(() => {
76
- if (promptStorage && promptStorage.trim() !== "") {
77
- callAi();
78
- }
79
- });
80
 
81
  const callAi = async (redesignMarkdown?: string) => {
82
- removePromptStorage();
83
- if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
84
- return openProModal([]);
85
  if (isAiWorking) return;
86
  if (!redesignMarkdown && !prompt.trim()) return;
 
 
 
 
 
87
 
88
- if (isFollowUp && !redesignMarkdown && !isSameHtml) {
89
- if (!user) return openLoginModal({ prompt });
90
- const result = await callAiFollowUp(prompt, enhancedSettings, isNew);
91
 
92
- if (result?.error) {
93
- handleError(result.error, result.message);
94
- return;
95
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- if (result?.success) {
98
- setPrompt("");
99
- }
100
- } else {
101
- const result = await callAiNewProject(
102
- prompt,
103
- enhancedSettings,
104
- redesignMarkdown,
105
- !!user
106
- );
107
 
108
- if (result?.error) {
109
- handleError(result.error, result.message);
110
- return;
111
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- if (result?.success) {
114
- setPrompt("");
115
- // if (selectedModel?.isThinker) {
116
- // setModel(MODELS[0].value);
117
- // }
 
 
 
118
  }
119
  }
120
  };
121
 
122
- const handleError = (error: string, message?: string) => {
123
- switch (error) {
124
- case "login_required":
125
- openLoginModal();
126
- break;
127
- case "provider_required":
128
- setOpenProvider(true);
129
- setProviderError(message || "");
130
- break;
131
- case "pro_required":
132
- openProModal([]);
133
- break;
134
- case "api_error":
135
- toast.error(message || "An error occurred");
136
- break;
137
- case "network_error":
138
- toast.error(message || "Network error occurred");
139
- break;
140
- default:
141
- toast.error("An unexpected error occurred");
142
  }
143
  };
144
 
@@ -148,19 +274,19 @@ export const AskAi = ({
148
  }
149
  }, [think]);
150
 
151
- const randomPrompt = () => {
152
- setRandomPromptLoading(true);
153
- setTimeout(() => {
154
- setPrompt(
155
- PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
156
- );
157
- setRandomPromptLoading(false);
158
- }, 400);
159
- };
160
 
161
  return (
162
- <div className="p-3 w-full">
163
- <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
164
  {think && (
165
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
166
  <header
@@ -198,13 +324,6 @@ export const AskAi = ({
198
  </main>
199
  </div>
200
  )}
201
- <SelectedFiles
202
- files={selectedFiles}
203
- isAiWorking={isAiWorking}
204
- onDelete={(file) =>
205
- setSelectedFiles(selectedFiles.filter((f) => f !== file))
206
- }
207
- />
208
  {selectedElement && (
209
  <div className="px-4 pt-3">
210
  <SelectedHtmlElement
@@ -215,47 +334,36 @@ export const AskAi = ({
215
  </div>
216
  )}
217
  <div className="w-full relative flex items-center justify-between">
218
- {(isAiWorking || isUploading || isThinking || isLoadingProject) && (
219
- <div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
220
- <AiLoading
221
- text={
222
- isLoadingProject
223
- ? "Fetching your project..."
224
- : isUploading
225
- ? "Uploading images..."
226
- : isAiWorking && !isSameHtml
227
- ? "DeepSite is working..."
228
- : "DeepSite is thinking..."
229
- }
230
- />
231
- {isAiWorking && (
232
- <Button
233
- size="iconXs"
234
- variant="outline"
235
- className="!rounded-md mr-0.5"
236
- onClick={cancelRequest}
237
- >
238
- <CircleStop className="size-4" />
239
- </Button>
240
- )}
241
  </div>
242
  )}
243
- <textarea
244
- disabled={
245
- isAiWorking || isUploading || isThinking || isLoadingProject
246
- }
247
  className={classNames(
248
- "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
249
  {
250
- "!pt-2.5":
251
- selectedElement &&
252
- !(isAiWorking || isUploading || isThinking),
253
  }
254
  )}
255
  placeholder={
256
  selectedElement
257
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
258
- : isFollowUp && (!isSameHtml || pages?.length > 1)
259
  ? "Ask DeepSite for edits"
260
  : "Ask DeepSite anything..."
261
  }
@@ -267,56 +375,91 @@ export const AskAi = ({
267
  }
268
  }}
269
  />
270
- {isNew && !isAiWorking && isSameHtml && (
271
- <Button
272
- size="iconXs"
273
- variant="outline"
274
- className="!rounded-md -translate-y-2 -translate-x-4"
275
- onClick={() => randomPrompt()}
276
- >
277
- <Dice6
278
- className={classNames("size-4", {
279
- "animate-spin animation-duration-500": randomPromptLoading,
280
- })}
281
- />
282
- </Button>
283
- )}
284
  </div>
285
- <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
286
- <div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
287
- <PromptBuilder
288
- enhancedSettings={enhancedSettings!}
289
- setEnhancedSettings={setEnhancedSettings}
290
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  <Settings
 
 
 
 
292
  open={openProvider}
293
  error={providerError}
294
  isFollowUp={!isSameHtml && isFollowUp}
295
  onClose={setOpenProvider}
296
  />
297
- {!isNew && <Uploader project={project} />}
298
- {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
299
- {!isNew && !isSameHtml && <Selector />}
300
- </div>
301
- <div className="flex items-center justify-end gap-2">
302
  <Button
303
  size="iconXs"
304
- variant="outline"
305
- className="!rounded-md"
306
- disabled={
307
- isAiWorking || isUploading || isThinking || !prompt.trim()
308
- }
309
  onClick={() => callAi()}
310
  >
311
  <ArrowUp className="size-4" />
312
  </Button>
313
  </div>
314
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  </div>
316
- <audio ref={hookAudio} id="audio" className="hidden">
317
  <source src="/success.mp3" type="audio/mpeg" />
318
  Your browser does not support the audio element.
319
  </audio>
320
  </div>
321
  );
322
- };
 
1
+ "use client";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { useState, useRef, useMemo } from "react";
4
  import classNames from "classnames";
 
 
5
  import { toast } from "sonner";
6
+ import { useLocalStorage, useUpdateEffect } from "react-use";
7
+ import { ArrowUp, ChevronDown, Crosshair } from "lucide-react";
8
+ import { FaStopCircle } from "react-icons/fa";
9
 
10
+ import ProModal from "@/components/pro-modal";
 
 
 
 
 
11
  import { Button } from "@/components/ui/button";
12
+ import { MODELS } from "@/lib/providers";
13
+ import { HtmlHistory } from "@/types";
14
+ import { InviteFriends } from "@/components/invite-friends";
15
+ import { Settings } from "@/components/editor/ask-ai/settings";
16
+ import { LoginModal } from "@/components/login-modal";
17
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
18
+ import Loading from "@/components/loading";
19
+ import { Checkbox } from "@/components/ui/checkbox";
20
+ import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
21
+ import { TooltipContent } from "@radix-ui/react-tooltip";
22
+ import { SelectedHtmlElement } from "./selected-html-element";
23
+ import { FollowUpTooltip } from "./follow-up-tooltip";
24
+ import { isTheSameHtml } from "@/lib/compare-html-diff";
 
25
 
26
+ export function AskAI({
27
+ html,
28
+ setHtml,
29
  onScrollToBottom,
30
+ isAiWorking,
31
+ setisAiWorking,
32
+ isEditableModeEnabled = false,
33
+ selectedElement,
34
+ setSelectedElement,
35
+ setIsEditableModeEnabled,
36
+ onNewPrompt,
37
+ onSuccess,
38
  }: {
39
+ html: string;
40
+ setHtml: (html: string) => void;
41
+ onScrollToBottom: () => void;
42
+ isAiWorking: boolean;
43
+ onNewPrompt: (prompt: string) => void;
44
+ htmlHistory?: HtmlHistory[];
45
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
46
+ onSuccess: (h: string, p: string, n?: number[][]) => void;
47
+ isEditableModeEnabled: boolean;
48
+ setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
49
+ selectedElement?: HTMLElement | null;
50
+ setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
51
+ }) {
52
+ const refThink = useRef<HTMLDivElement | null>(null);
53
+ const audio = useRef<HTMLAudioElement | null>(null);
54
+
55
+ const [open, setOpen] = useState(false);
56
+ const [prompt, setPrompt] = useState("");
57
+ const [hasAsked, setHasAsked] = useState(false);
58
+ const [previousPrompt, setPreviousPrompt] = useState("");
59
+ const [provider, setProvider] = useLocalStorage("provider", "auto");
60
+ const [model, setModel] = useLocalStorage("model", MODELS[0].value);
 
 
61
  const [openProvider, setOpenProvider] = useState(false);
62
  const [providerError, setProviderError] = useState("");
63
+ const [openProModal, setOpenProModal] = useState(false);
64
+ const [think, setThink] = useState<string | undefined>(undefined);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  const [openThink, setOpenThink] = useState(false);
66
+ const [isThinking, setIsThinking] = useState(true);
67
+ const [controller, setController] = useState<AbortController | null>(null);
68
+ const [isFollowUp, setIsFollowUp] = useState(true);
 
 
 
 
69
 
70
  const callAi = async (redesignMarkdown?: string) => {
 
 
 
71
  if (isAiWorking) return;
72
  if (!redesignMarkdown && !prompt.trim()) return;
73
+ setisAiWorking(true);
74
+ setProviderError("");
75
+ setThink("");
76
+ setOpenThink(false);
77
+ setIsThinking(true);
78
 
79
+ let contentResponse = "";
80
+ let thinkResponse = "";
81
+ let lastRenderTime = 0;
82
 
83
+ const abortController = new AbortController();
84
+ setController(abortController);
85
+ try {
86
+ onNewPrompt(prompt);
87
+ if (isFollowUp && !redesignMarkdown && !isSameHtml) {
88
+ const selectedElementHtml = selectedElement
89
+ ? selectedElement.outerHTML
90
+ : "";
91
+ const request = await fetch("/api/ask-ai", {
92
+ method: "PUT",
93
+ body: JSON.stringify({
94
+ prompt,
95
+ provider,
96
+ previousPrompt,
97
+ model,
98
+ html,
99
+ selectedElementHtml,
100
+ }),
101
+ headers: {
102
+ "Content-Type": "application/json",
103
+ "x-forwarded-for": window.location.hostname,
104
+ },
105
+ signal: abortController.signal,
106
+ });
107
+ if (request && request.body) {
108
+ const res = await request.json();
109
+ if (!request.ok) {
110
+ if (res.openLogin) {
111
+ setOpen(true);
112
+ } else if (res.openSelectProvider) {
113
+ setOpenProvider(true);
114
+ setProviderError(res.message);
115
+ } else if (res.openProModal) {
116
+ setOpenProModal(true);
117
+ } else {
118
+ toast.error(res.message);
119
+ }
120
+ setisAiWorking(false);
121
+ return;
122
+ }
123
+ setHtml(res.html);
124
+ toast.success("AI responded successfully");
125
+ setPreviousPrompt(prompt);
126
+ setPrompt("");
127
+ setisAiWorking(false);
128
+ onSuccess(res.html, prompt, res.updatedLines);
129
+ if (audio.current) audio.current.play();
130
+ }
131
+ } else {
132
+ const request = await fetch("/api/ask-ai", {
133
+ method: "POST",
134
+ body: JSON.stringify({
135
+ prompt,
136
+ provider,
137
+ model,
138
+ html: isSameHtml ? "" : html,
139
+ redesignMarkdown,
140
+ }),
141
+ headers: {
142
+ "Content-Type": "application/json",
143
+ "x-forwarded-for": window.location.hostname,
144
+ },
145
+ signal: abortController.signal,
146
+ });
147
+ if (request && request.body) {
148
+ const reader = request.body.getReader();
149
+ const decoder = new TextDecoder("utf-8");
150
+ const selectedModel = MODELS.find(
151
+ (m: { value: string }) => m.value === model
152
+ );
153
+ let contentThink: string | undefined = undefined;
154
+ const read = async () => {
155
+ const { done, value } = await reader.read();
156
+ if (done) {
157
+ const isJson =
158
+ contentResponse.trim().startsWith("{") &&
159
+ contentResponse.trim().endsWith("}");
160
+ const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
161
+ if (jsonResponse && !jsonResponse.ok) {
162
+ if (jsonResponse.openLogin) {
163
+ setOpen(true);
164
+ } else if (jsonResponse.openSelectProvider) {
165
+ setOpenProvider(true);
166
+ setProviderError(jsonResponse.message);
167
+ } else if (jsonResponse.openProModal) {
168
+ setOpenProModal(true);
169
+ } else {
170
+ toast.error(jsonResponse.message);
171
+ }
172
+ setisAiWorking(false);
173
+ return;
174
+ }
175
 
176
+ toast.success("AI responded successfully");
177
+ setPreviousPrompt(prompt);
178
+ setPrompt("");
179
+ setisAiWorking(false);
180
+ setHasAsked(true);
181
+ setModel(MODELS[0].value);
182
+ if (audio.current) audio.current.play();
 
 
 
183
 
184
+ // Now we have the complete HTML including </html>, so set it to be sure
185
+ const finalDoc = contentResponse.match(
186
+ /<!DOCTYPE html>[\s\S]*<\/html>/
187
+ )?.[0];
188
+ if (finalDoc) {
189
+ setHtml(finalDoc);
190
+ }
191
+ onSuccess(finalDoc ?? contentResponse, prompt);
192
+
193
+ return;
194
+ }
195
+
196
+ const chunk = decoder.decode(value, { stream: true });
197
+ thinkResponse += chunk;
198
+ if (selectedModel?.isThinker) {
199
+ const thinkMatch = thinkResponse.match(/<think>[\s\S]*/)?.[0];
200
+ if (thinkMatch && !thinkResponse?.includes("</think>")) {
201
+ if ((contentThink?.length ?? 0) < 3) {
202
+ setOpenThink(true);
203
+ }
204
+ setThink(thinkMatch.replace("<think>", "").trim());
205
+ contentThink += chunk;
206
+ return read();
207
+ }
208
+ }
209
+
210
+ contentResponse += chunk;
211
+
212
+ const newHtml = contentResponse.match(
213
+ /<!DOCTYPE html>[\s\S]*/
214
+ )?.[0];
215
+ if (newHtml) {
216
+ setIsThinking(false);
217
+ let partialDoc = newHtml;
218
+ if (
219
+ partialDoc.includes("<head>") &&
220
+ !partialDoc.includes("</head>")
221
+ ) {
222
+ partialDoc += "\n</head>";
223
+ }
224
+ if (
225
+ partialDoc.includes("<body") &&
226
+ !partialDoc.includes("</body>")
227
+ ) {
228
+ partialDoc += "\n</body>";
229
+ }
230
+ if (!partialDoc.includes("</html>")) {
231
+ partialDoc += "\n</html>";
232
+ }
233
+
234
+ // Throttle the re-renders to avoid flashing/flicker
235
+ const now = Date.now();
236
+ if (now - lastRenderTime > 300) {
237
+ setHtml(partialDoc);
238
+ lastRenderTime = now;
239
+ }
240
+
241
+ if (partialDoc.length > 200) {
242
+ onScrollToBottom();
243
+ }
244
+ }
245
+ read();
246
+ };
247
 
248
+ read();
249
+ }
250
+ }
251
+ } catch (error: any) {
252
+ setisAiWorking(false);
253
+ toast.error(error.message);
254
+ if (error.openLogin) {
255
+ setOpen(true);
256
  }
257
  }
258
  };
259
 
260
+ const stopController = () => {
261
+ if (controller) {
262
+ controller.abort();
263
+ setController(null);
264
+ setisAiWorking(false);
265
+ setThink("");
266
+ setOpenThink(false);
267
+ setIsThinking(false);
 
 
 
 
 
 
 
 
 
 
 
 
268
  }
269
  };
270
 
 
274
  }
275
  }, [think]);
276
 
277
+ useUpdateEffect(() => {
278
+ if (!isThinking) {
279
+ setOpenThink(false);
280
+ }
281
+ }, [isThinking]);
282
+
283
+ const isSameHtml = useMemo(() => {
284
+ return isTheSameHtml(html);
285
+ }, [html]);
286
 
287
  return (
288
+ <div className="px-3">
289
+ <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">
290
  {think && (
291
  <div className="w-full border-b border-neutral-700 relative overflow-hidden">
292
  <header
 
324
  </main>
325
  </div>
326
  )}
 
 
 
 
 
 
 
327
  {selectedElement && (
328
  <div className="px-4 pt-3">
329
  <SelectedHtmlElement
 
334
  </div>
335
  )}
336
  <div className="w-full relative flex items-center justify-between">
337
+ {isAiWorking && (
338
+ <div className="absolute bg-neutral-800 rounded-lg bottom-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-center justify-between max-lg:text-sm">
339
+ <div className="flex items-center justify-start gap-2">
340
+ <Loading overlay={false} className="!size-4" />
341
+ <p className="text-neutral-400 text-sm">
342
+ AI is {isThinking ? "thinking" : "coding"}...{" "}
343
+ </p>
344
+ </div>
345
+ <div
346
+ 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"
347
+ onClick={stopController}
348
+ >
349
+ <FaStopCircle />
350
+ Stop generation
351
+ </div>
 
 
 
 
 
 
 
 
352
  </div>
353
  )}
354
+ <input
355
+ type="text"
356
+ disabled={isAiWorking}
 
357
  className={classNames(
358
+ "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
359
  {
360
+ "!pt-2.5": selectedElement && !isAiWorking,
 
 
361
  }
362
  )}
363
  placeholder={
364
  selectedElement
365
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
366
+ : hasAsked
367
  ? "Ask DeepSite for edits"
368
  : "Ask DeepSite anything..."
369
  }
 
375
  }
376
  }}
377
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  </div>
379
+ <div className="flex items-center justify-between gap-2 px-4 pb-3">
380
+ <div className="flex-1 flex items-center justify-start gap-1.5">
381
+ <ReImagine onRedesign={(md) => callAi(md)} />
382
+ {!isSameHtml && (
383
+ <Tooltip>
384
+ <TooltipTrigger asChild>
385
+ <Button
386
+ size="xs"
387
+ variant={isEditableModeEnabled ? "default" : "outline"}
388
+ onClick={() => {
389
+ setIsEditableModeEnabled?.(!isEditableModeEnabled);
390
+ }}
391
+ className={classNames("h-[28px]", {
392
+ "!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
393
+ !isEditableModeEnabled,
394
+ })}
395
+ >
396
+ <Crosshair className="size-4" />
397
+ Edit
398
+ </Button>
399
+ </TooltipTrigger>
400
+ <TooltipContent
401
+ align="start"
402
+ className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
403
+ >
404
+ Select an element on the page to ask DeepSite edit it
405
+ directly.
406
+ </TooltipContent>
407
+ </Tooltip>
408
+ )}
409
+ <InviteFriends />
410
+ </div>
411
+ <div className="flex items-center justify-end gap-2">
412
  <Settings
413
+ provider={provider as string}
414
+ model={model as string}
415
+ onChange={setProvider}
416
+ onModelChange={setModel}
417
  open={openProvider}
418
  error={providerError}
419
  isFollowUp={!isSameHtml && isFollowUp}
420
  onClose={setOpenProvider}
421
  />
 
 
 
 
 
422
  <Button
423
  size="iconXs"
424
+ disabled={isAiWorking || !prompt.trim()}
 
 
 
 
425
  onClick={() => callAi()}
426
  >
427
  <ArrowUp className="size-4" />
428
  </Button>
429
  </div>
430
  </div>
431
+ <LoginModal open={open} onClose={() => setOpen(false)} html={html} />
432
+ <ProModal
433
+ html={html}
434
+ open={openProModal}
435
+ onClose={() => setOpenProModal(false)}
436
+ />
437
+ {!isSameHtml && (
438
+ <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">
439
+ <label
440
+ htmlFor="diff-patch-checkbox"
441
+ className="flex items-center gap-1.5 cursor-pointer"
442
+ >
443
+ <Checkbox
444
+ id="diff-patch-checkbox"
445
+ checked={isFollowUp}
446
+ onCheckedChange={(e) => {
447
+ if (e === true && !isSameHtml) {
448
+ setModel(MODELS[0].value);
449
+ }
450
+ setIsFollowUp(e === true);
451
+ }}
452
+ />
453
+ Diff-Patch Update
454
+ </label>
455
+ <FollowUpTooltip />
456
+ </div>
457
+ )}
458
  </div>
459
+ <audio ref={audio} id="audio" className="hidden">
460
  <source src="/success.mp3" type="audio/mpeg" />
461
  Your browser does not support the audio element.
462
  </audio>
463
  </div>
464
  );
465
+ }
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
- };