Spaces:
Running
Running
toto
#2
by
victor
HF Staff
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .env.example +5 -0
- .gitignore +23 -38
- Dockerfile +5 -2
- README.md +4 -13
- app/(public)/layout.tsx +0 -15
- app/(public)/page.tsx +0 -44
- app/(public)/projects/page.tsx +0 -33
- app/actions/auth.ts +0 -15
- app/api/ask-ai/route.ts +0 -411
- app/api/auth/route.ts +0 -82
- app/api/me/projects/[namespace]/[repoId]/route.ts +0 -235
- app/api/me/projects/route.ts +0 -126
- app/api/me/route.ts +0 -25
- app/api/re-design/route.ts +0 -39
- app/auth/callback/page.tsx +0 -72
- app/auth/page.tsx +0 -28
- app/favicon.ico +0 -0
- app/layout.tsx +0 -102
- app/projects/[namespace]/[repoId]/page.tsx +0 -40
- app/projects/new/page.tsx +0 -5
- assets/globals.css +0 -146
- assets/logo.svg +0 -316
- components.json +0 -21
- components/contexts/app-context.tsx +0 -57
- components/contexts/user-context.tsx +0 -8
- components/editor/ask-ai/follow-up-tooltip.tsx +0 -36
- components/editor/ask-ai/index.tsx +0 -474
- components/editor/ask-ai/re-imagine.tsx +0 -146
- components/editor/ask-ai/selected-html-element.tsx +0 -57
- components/editor/ask-ai/settings.tsx +0 -212
- components/editor/deploy-button/index.tsx +0 -171
- components/editor/footer/index.tsx +0 -118
- components/editor/header/index.tsx +0 -69
- components/editor/history/index.tsx +0 -72
- components/editor/preview/index.tsx +0 -172
- components/editor/save-button/index.tsx +0 -76
- components/invite-friends/index.tsx +0 -85
- components/login-modal/index.tsx +0 -61
- components/magic-ui/grid-pattern.tsx +0 -69
- components/my-projects/index.tsx +0 -57
- components/my-projects/load-project.tsx +0 -196
- components/my-projects/project-card.tsx +0 -74
- components/pro-modal/index.tsx +0 -92
- components/providers/tanstack-query-provider.tsx +0 -18
- components/public/navigation/index.tsx +0 -156
- components/space/ask-ai/index.tsx +0 -43
- components/ui/avatar.tsx +0 -53
- components/ui/button.tsx +0 -67
- components/ui/checkbox.tsx +0 -32
- components/ui/collapsible.tsx +0 -33
.env.example
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
OAUTH_CLIENT_ID=
|
2 |
+
OAUTH_CLIENT_SECRET=
|
3 |
+
APP_PORT=5173
|
4 |
+
REDIRECT_URI=http://localhost:5173/auth/login
|
5 |
+
DEFAULT_HF_TOKEN=
|
.gitignore
CHANGED
@@ -1,41 +1,26 @@
|
|
1 |
-
#
|
2 |
-
|
3 |
-
|
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 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
#
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
npm-debug.log*
|
5 |
yarn-debug.log*
|
6 |
yarn-error.log*
|
7 |
+
pnpm-debug.log*
|
8 |
+
lerna-debug.log*
|
9 |
+
|
10 |
+
node_modules
|
11 |
+
dist
|
12 |
+
dist-ssr
|
13 |
+
*.local
|
14 |
+
|
15 |
+
# Editor directories and files
|
16 |
+
.vscode/*
|
17 |
+
!.vscode/extensions.json
|
18 |
+
.idea
|
19 |
+
.DS_Store
|
20 |
+
*.suo
|
21 |
+
*.ntvs*
|
22 |
+
*.njsproj
|
23 |
+
*.sln
|
24 |
+
*.sw?
|
25 |
+
.env
|
26 |
+
.aider*
|
Dockerfile
CHANGED
@@ -1,6 +1,9 @@
|
|
1 |
-
|
|
|
|
|
2 |
USER root
|
3 |
|
|
|
4 |
USER 1000
|
5 |
WORKDIR /usr/src/app
|
6 |
# Copy package.json and package-lock.json to the container
|
@@ -13,7 +16,7 @@ RUN npm install
|
|
13 |
RUN npm run build
|
14 |
|
15 |
# Expose the application port (assuming your app runs on port 3000)
|
16 |
-
EXPOSE
|
17 |
|
18 |
# Start the application
|
19 |
CMD ["npm", "start"]
|
|
|
1 |
+
# Dockerfile
|
2 |
+
# Use an official Node.js runtime as the base image
|
3 |
+
FROM node:22.1.0
|
4 |
USER root
|
5 |
|
6 |
+
RUN apt-get update
|
7 |
USER 1000
|
8 |
WORKDIR /usr/src/app
|
9 |
# Copy package.json and package-lock.json to the container
|
|
|
16 |
RUN npm run build
|
17 |
|
18 |
# Expose the application port (assuming your app runs on port 3000)
|
19 |
+
EXPOSE 5173
|
20 |
|
21 |
# Start the application
|
22 |
CMD ["npm", "start"]
|
README.md
CHANGED
@@ -1,22 +1,13 @@
|
|
1 |
---
|
2 |
-
title: DeepSite
|
3 |
emoji: 🐳
|
4 |
colorFrom: blue
|
5 |
colorTo: blue
|
6 |
sdk: docker
|
7 |
pinned: true
|
8 |
-
app_port:
|
9 |
license: mit
|
10 |
-
short_description:
|
11 |
-
models:
|
12 |
-
- deepseek-ai/DeepSeek-V3-0324
|
13 |
-
- deepseek-ai/DeepSeek-R1-0528
|
14 |
---
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
DeepSite is a coding platform powered by DeepSeek AI, designed to make coding smarter and more efficient. Tailored for developers, data scientists, and AI engineers, it integrates generative AI into your coding projects to enhance creativity and productivity.
|
19 |
-
|
20 |
-
## How to use it locally
|
21 |
-
|
22 |
-
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
|
|
1 |
---
|
2 |
+
title: DeepSite
|
3 |
emoji: 🐳
|
4 |
colorFrom: blue
|
5 |
colorTo: blue
|
6 |
sdk: docker
|
7 |
pinned: true
|
8 |
+
app_port: 5173
|
9 |
license: mit
|
10 |
+
short_description: Imagine and Share in 1-Click
|
|
|
|
|
|
|
11 |
---
|
12 |
|
13 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
app/(public)/layout.tsx
DELETED
@@ -1,15 +0,0 @@
|
|
1 |
-
import Navigation from "@/components/public/navigation";
|
2 |
-
|
3 |
-
export default async function PublicLayout({
|
4 |
-
children,
|
5 |
-
}: Readonly<{
|
6 |
-
children: React.ReactNode;
|
7 |
-
}>) {
|
8 |
-
return (
|
9 |
-
<div className="min-h-screen bg-black z-1 relative">
|
10 |
-
<div className="background__noisy" />
|
11 |
-
<Navigation />
|
12 |
-
{children}
|
13 |
-
</div>
|
14 |
-
);
|
15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/(public)/page.tsx
DELETED
@@ -1,44 +0,0 @@
|
|
1 |
-
import { AskAi } from "@/components/space/ask-ai";
|
2 |
-
import { redirect } from "next/navigation";
|
3 |
-
export default function Home() {
|
4 |
-
redirect("/projects/new");
|
5 |
-
return (
|
6 |
-
<>
|
7 |
-
<header className="container mx-auto pt-20 px-6 relative flex flex-col items-center justify-center text-center">
|
8 |
-
<div className="rounded-full border border-neutral-100/10 bg-neutral-100/5 text-xs text-neutral-300 px-3 py-1 max-w-max mx-auto mb-2">
|
9 |
-
✨ DeepSite Public Beta
|
10 |
-
</div>
|
11 |
-
<h1 className="text-8xl font-semibold text-white font-mono max-w-4xl">
|
12 |
-
Code your website with AI in seconds
|
13 |
-
</h1>
|
14 |
-
<p className="text-2xl text-neutral-300/80 mt-4 text-center max-w-2xl">
|
15 |
-
Vibe Coding has never been so easy.
|
16 |
-
</p>
|
17 |
-
<div className="mt-14 max-w-2xl w-full mx-auto">
|
18 |
-
<AskAi />
|
19 |
-
</div>
|
20 |
-
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
21 |
-
<div className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl rounded-full" />
|
22 |
-
<div className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10 transform rotate-12" />
|
23 |
-
<div className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10 rounded-3xl" />
|
24 |
-
<div className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3 rounded-lg transform -rotate-15" />
|
25 |
-
</div>
|
26 |
-
</header>
|
27 |
-
<div id="community" className="h-screen flex items-center justify-center">
|
28 |
-
<h1 className="text-7xl font-extrabold text-white font-mono">
|
29 |
-
Community Driven
|
30 |
-
</h1>
|
31 |
-
</div>
|
32 |
-
<div id="deploy" className="h-screen flex items-center justify-center">
|
33 |
-
<h1 className="text-7xl font-extrabold text-white font-mono">
|
34 |
-
Deploy your website in seconds
|
35 |
-
</h1>
|
36 |
-
</div>
|
37 |
-
<div id="features" className="h-screen flex items-center justify-center">
|
38 |
-
<h1 className="text-7xl font-extrabold text-white font-mono">
|
39 |
-
Features that make you smile
|
40 |
-
</h1>
|
41 |
-
</div>
|
42 |
-
</>
|
43 |
-
);
|
44 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/(public)/projects/page.tsx
DELETED
@@ -1,33 +0,0 @@
|
|
1 |
-
import { cookies } from "next/headers";
|
2 |
-
import { redirect } from "next/navigation";
|
3 |
-
|
4 |
-
import { apiServer } from "@/lib/api";
|
5 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
6 |
-
import { MyProjects } from "@/components/my-projects";
|
7 |
-
|
8 |
-
async function getMyProjects() {
|
9 |
-
const cookieStore = await cookies();
|
10 |
-
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
11 |
-
if (!token) return { redirectUrl: true, projects: [] };
|
12 |
-
try {
|
13 |
-
const { data } = await apiServer.get("/me/projects", {
|
14 |
-
headers: {
|
15 |
-
Authorization: `Bearer ${token}`,
|
16 |
-
},
|
17 |
-
});
|
18 |
-
|
19 |
-
return {
|
20 |
-
projects: data.projects,
|
21 |
-
};
|
22 |
-
} catch {
|
23 |
-
return { projects: [] };
|
24 |
-
}
|
25 |
-
}
|
26 |
-
export default async function ProjectsPage() {
|
27 |
-
const { redirectUrl, projects } = await getMyProjects();
|
28 |
-
if (redirectUrl) {
|
29 |
-
redirect("/");
|
30 |
-
}
|
31 |
-
|
32 |
-
return <MyProjects projects={projects} />;
|
33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/actions/auth.ts
DELETED
@@ -1,15 +0,0 @@
|
|
1 |
-
"use server";
|
2 |
-
|
3 |
-
import { headers } from "next/headers";
|
4 |
-
|
5 |
-
export async function getAuth() {
|
6 |
-
const authList = await headers();
|
7 |
-
const host = authList.get("host") ?? "localhost:3000";
|
8 |
-
const redirect_uri =
|
9 |
-
`${host.includes("localhost") ? "http://" : "https://"}` +
|
10 |
-
host +
|
11 |
-
"/auth/callback";
|
12 |
-
|
13 |
-
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`;
|
14 |
-
return loginRedirectUrl;
|
15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/ask-ai/route.ts
DELETED
@@ -1,411 +0,0 @@
|
|
1 |
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
-
import type { NextRequest } from "next/server";
|
3 |
-
import { NextResponse } from "next/server";
|
4 |
-
import { headers } from "next/headers";
|
5 |
-
import { InferenceClient } from "@huggingface/inference";
|
6 |
-
|
7 |
-
import { MODELS, PROVIDERS } from "@/lib/providers";
|
8 |
-
import {
|
9 |
-
DIVIDER,
|
10 |
-
FOLLOW_UP_SYSTEM_PROMPT,
|
11 |
-
INITIAL_SYSTEM_PROMPT,
|
12 |
-
MAX_REQUESTS_PER_IP,
|
13 |
-
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 |
-
|
20 |
-
export async function POST(request: NextRequest) {
|
21 |
-
const authHeaders = await headers();
|
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(
|
29 |
-
{ ok: false, error: "Missing required fields" },
|
30 |
-
{ status: 400 }
|
31 |
-
);
|
32 |
-
}
|
33 |
-
|
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" },
|
40 |
-
{ status: 400 }
|
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 |
-
/**
|
59 |
-
* Handle local usage token, this bypass the need for a user token
|
60 |
-
* and allows local testing without authentication.
|
61 |
-
* This is useful for development and testing purposes.
|
62 |
-
*/
|
63 |
-
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
|
64 |
-
token = process.env.HF_TOKEN;
|
65 |
-
}
|
66 |
-
|
67 |
-
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
|
68 |
-
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
69 |
-
: authHeaders.get("x-forwarded-for");
|
70 |
-
|
71 |
-
if (!token) {
|
72 |
-
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
73 |
-
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
74 |
-
return NextResponse.json(
|
75 |
-
{
|
76 |
-
ok: false,
|
77 |
-
openLogin: true,
|
78 |
-
message: "Log In to continue using the service",
|
79 |
-
},
|
80 |
-
{ status: 429 }
|
81 |
-
);
|
82 |
-
}
|
83 |
-
|
84 |
-
token = process.env.DEFAULT_HF_TOKEN as string;
|
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",
|
104 |
-
"Cache-Control": "no-cache",
|
105 |
-
Connection: "keep-alive",
|
106 |
-
},
|
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(
|
182 |
-
encoder.encode(
|
183 |
-
JSON.stringify({
|
184 |
-
ok: false,
|
185 |
-
openProModal: true,
|
186 |
-
message: error.message,
|
187 |
-
})
|
188 |
-
)
|
189 |
-
);
|
190 |
-
} else {
|
191 |
-
await writer.write(
|
192 |
-
encoder.encode(
|
193 |
-
JSON.stringify({
|
194 |
-
ok: false,
|
195 |
-
openSelectProvider: true,
|
196 |
-
message:
|
197 |
-
error.message ||
|
198 |
-
"An error occurred while processing your request.",
|
199 |
-
})
|
200 |
-
)
|
201 |
-
);
|
202 |
-
}
|
203 |
-
} finally {
|
204 |
-
await writer?.close();
|
205 |
-
}
|
206 |
-
})();
|
207 |
-
|
208 |
-
return response;
|
209 |
-
} catch (error: any) {
|
210 |
-
return NextResponse.json(
|
211 |
-
{
|
212 |
-
ok: false,
|
213 |
-
openSelectProvider: true,
|
214 |
-
message:
|
215 |
-
error?.message || "An error occurred while processing your request.",
|
216 |
-
},
|
217 |
-
{ status: 500 }
|
218 |
-
);
|
219 |
-
}
|
220 |
-
}
|
221 |
-
|
222 |
-
export async function PUT(request: NextRequest) {
|
223 |
-
const authHeaders = await headers();
|
224 |
-
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
225 |
-
|
226 |
-
const body = await request.json();
|
227 |
-
const { prompt, html, previousPrompt, provider, selectedElementHtml } = body;
|
228 |
-
|
229 |
-
if (!prompt || !html) {
|
230 |
-
return NextResponse.json(
|
231 |
-
{ ok: false, error: "Missing required fields" },
|
232 |
-
{ status: 400 }
|
233 |
-
);
|
234 |
-
}
|
235 |
-
|
236 |
-
const selectedModel = MODELS[0];
|
237 |
-
|
238 |
-
let token = userToken;
|
239 |
-
let billTo: string | null = null;
|
240 |
-
|
241 |
-
/**
|
242 |
-
* Handle local usage token, this bypass the need for a user token
|
243 |
-
* and allows local testing without authentication.
|
244 |
-
* This is useful for development and testing purposes.
|
245 |
-
*/
|
246 |
-
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
|
247 |
-
token = process.env.HF_TOKEN;
|
248 |
-
}
|
249 |
-
|
250 |
-
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
|
251 |
-
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
252 |
-
: authHeaders.get("x-forwarded-for");
|
253 |
-
|
254 |
-
if (!token) {
|
255 |
-
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
256 |
-
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
257 |
-
return NextResponse.json(
|
258 |
-
{
|
259 |
-
ok: false,
|
260 |
-
openLogin: true,
|
261 |
-
message: "Log In to continue using the service",
|
262 |
-
},
|
263 |
-
{ status: 429 }
|
264 |
-
);
|
265 |
-
}
|
266 |
-
|
267 |
-
token = process.env.DEFAULT_HF_TOKEN as string;
|
268 |
-
billTo = "huggingface";
|
269 |
-
}
|
270 |
-
|
271 |
-
const client = new InferenceClient(token);
|
272 |
-
|
273 |
-
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
274 |
-
const selectedProvider =
|
275 |
-
provider === "auto"
|
276 |
-
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
277 |
-
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
278 |
-
|
279 |
-
try {
|
280 |
-
const response = await client.chatCompletion(
|
281 |
-
{
|
282 |
-
model: selectedModel.value,
|
283 |
-
provider: selectedProvider.id as any,
|
284 |
-
messages: [
|
285 |
-
{
|
286 |
-
role: "system",
|
287 |
-
content: FOLLOW_UP_SYSTEM_PROMPT,
|
288 |
-
},
|
289 |
-
{
|
290 |
-
role: "user",
|
291 |
-
content: previousPrompt
|
292 |
-
? previousPrompt
|
293 |
-
: "You are modifying the HTML file based on the user's request.",
|
294 |
-
},
|
295 |
-
{
|
296 |
-
role: "assistant",
|
297 |
-
|
298 |
-
content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${
|
299 |
-
selectedElementHtml
|
300 |
-
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
301 |
-
: ""
|
302 |
-
}`,
|
303 |
-
},
|
304 |
-
{
|
305 |
-
role: "user",
|
306 |
-
content: prompt,
|
307 |
-
},
|
308 |
-
],
|
309 |
-
...(selectedProvider.id !== "sambanova"
|
310 |
-
? {
|
311 |
-
max_tokens: selectedProvider.max_tokens,
|
312 |
-
}
|
313 |
-
: {}),
|
314 |
-
},
|
315 |
-
billTo ? { billTo } : {}
|
316 |
-
);
|
317 |
-
|
318 |
-
const chunk = response.choices[0]?.message?.content;
|
319 |
-
if (!chunk) {
|
320 |
-
return NextResponse.json(
|
321 |
-
{ ok: false, message: "No content returned from the model" },
|
322 |
-
{ status: 400 }
|
323 |
-
);
|
324 |
-
}
|
325 |
-
|
326 |
-
if (chunk) {
|
327 |
-
const updatedLines: number[][] = [];
|
328 |
-
let newHtml = html;
|
329 |
-
let position = 0;
|
330 |
-
let moreBlocks = true;
|
331 |
-
|
332 |
-
while (moreBlocks) {
|
333 |
-
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
334 |
-
if (searchStartIndex === -1) {
|
335 |
-
moreBlocks = false;
|
336 |
-
continue;
|
337 |
-
}
|
338 |
-
|
339 |
-
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
340 |
-
if (dividerIndex === -1) {
|
341 |
-
moreBlocks = false;
|
342 |
-
continue;
|
343 |
-
}
|
344 |
-
|
345 |
-
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
346 |
-
if (replaceEndIndex === -1) {
|
347 |
-
moreBlocks = false;
|
348 |
-
continue;
|
349 |
-
}
|
350 |
-
|
351 |
-
const searchBlock = chunk.substring(
|
352 |
-
searchStartIndex + SEARCH_START.length,
|
353 |
-
dividerIndex
|
354 |
-
);
|
355 |
-
const replaceBlock = chunk.substring(
|
356 |
-
dividerIndex + DIVIDER.length,
|
357 |
-
replaceEndIndex
|
358 |
-
);
|
359 |
-
|
360 |
-
if (searchBlock.trim() === "") {
|
361 |
-
newHtml = `${replaceBlock}\n${newHtml}`;
|
362 |
-
updatedLines.push([1, replaceBlock.split("\n").length]);
|
363 |
-
} else {
|
364 |
-
const blockPosition = newHtml.indexOf(searchBlock);
|
365 |
-
if (blockPosition !== -1) {
|
366 |
-
const beforeText = newHtml.substring(0, blockPosition);
|
367 |
-
const startLineNumber = beforeText.split("\n").length;
|
368 |
-
const replaceLines = replaceBlock.split("\n").length;
|
369 |
-
const endLineNumber = startLineNumber + replaceLines - 1;
|
370 |
-
|
371 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
372 |
-
newHtml = newHtml.replace(searchBlock, replaceBlock);
|
373 |
-
}
|
374 |
-
}
|
375 |
-
|
376 |
-
position = replaceEndIndex + REPLACE_END.length;
|
377 |
-
}
|
378 |
-
|
379 |
-
return NextResponse.json({
|
380 |
-
ok: true,
|
381 |
-
html: newHtml,
|
382 |
-
updatedLines,
|
383 |
-
});
|
384 |
-
} else {
|
385 |
-
return NextResponse.json(
|
386 |
-
{ ok: false, message: "No content returned from the model" },
|
387 |
-
{ status: 400 }
|
388 |
-
);
|
389 |
-
}
|
390 |
-
} catch (error: any) {
|
391 |
-
if (error.message?.includes("exceeded your monthly included credits")) {
|
392 |
-
return NextResponse.json(
|
393 |
-
{
|
394 |
-
ok: false,
|
395 |
-
openProModal: true,
|
396 |
-
message: error.message,
|
397 |
-
},
|
398 |
-
{ status: 402 }
|
399 |
-
);
|
400 |
-
}
|
401 |
-
return NextResponse.json(
|
402 |
-
{
|
403 |
-
ok: false,
|
404 |
-
openSelectProvider: true,
|
405 |
-
message:
|
406 |
-
error.message || "An error occurred while processing your request.",
|
407 |
-
},
|
408 |
-
{ status: 500 }
|
409 |
-
);
|
410 |
-
}
|
411 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/route.ts
DELETED
@@ -1,82 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
|
3 |
-
export async function POST(req: NextRequest) {
|
4 |
-
const body = await req.json();
|
5 |
-
const { code } = body;
|
6 |
-
|
7 |
-
if (!code) {
|
8 |
-
return NextResponse.json(
|
9 |
-
{ error: "Code is required" },
|
10 |
-
{
|
11 |
-
status: 400,
|
12 |
-
headers: {
|
13 |
-
"Content-Type": "application/json",
|
14 |
-
},
|
15 |
-
}
|
16 |
-
);
|
17 |
-
}
|
18 |
-
|
19 |
-
const Authorization = `Basic ${Buffer.from(
|
20 |
-
`${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
|
21 |
-
).toString("base64")}`;
|
22 |
-
|
23 |
-
const host =
|
24 |
-
req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
|
25 |
-
const redirect_uri =
|
26 |
-
`${host.includes("localhost") ? "http://" : "https://"}` +
|
27 |
-
host +
|
28 |
-
"/auth/callback";
|
29 |
-
const request_auth = await fetch("https://huggingface.co/oauth/token", {
|
30 |
-
method: "POST",
|
31 |
-
headers: {
|
32 |
-
"Content-Type": "application/x-www-form-urlencoded",
|
33 |
-
Authorization,
|
34 |
-
},
|
35 |
-
body: new URLSearchParams({
|
36 |
-
grant_type: "authorization_code",
|
37 |
-
code,
|
38 |
-
redirect_uri,
|
39 |
-
}),
|
40 |
-
});
|
41 |
-
|
42 |
-
const response = await request_auth.json();
|
43 |
-
if (!response.access_token) {
|
44 |
-
return NextResponse.json(
|
45 |
-
{ error: "Failed to retrieve access token" },
|
46 |
-
{
|
47 |
-
status: 400,
|
48 |
-
headers: {
|
49 |
-
"Content-Type": "application/json",
|
50 |
-
},
|
51 |
-
}
|
52 |
-
);
|
53 |
-
}
|
54 |
-
|
55 |
-
const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
|
56 |
-
headers: {
|
57 |
-
Authorization: `Bearer ${response.access_token}`,
|
58 |
-
},
|
59 |
-
});
|
60 |
-
|
61 |
-
if (!userResponse.ok) {
|
62 |
-
return NextResponse.json(
|
63 |
-
{ user: null, errCode: userResponse.status },
|
64 |
-
{ status: userResponse.status }
|
65 |
-
);
|
66 |
-
}
|
67 |
-
const user = await userResponse.json();
|
68 |
-
|
69 |
-
return NextResponse.json(
|
70 |
-
{
|
71 |
-
access_token: response.access_token,
|
72 |
-
expires_in: response.expires_in,
|
73 |
-
user,
|
74 |
-
},
|
75 |
-
{
|
76 |
-
status: 200,
|
77 |
-
headers: {
|
78 |
-
"Content-Type": "application/json",
|
79 |
-
},
|
80 |
-
}
|
81 |
-
);
|
82 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/route.ts
DELETED
@@ -1,235 +0,0 @@
|
|
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 |
-
) {
|
13 |
-
const user = await isAuthenticated();
|
14 |
-
|
15 |
-
if (user instanceof NextResponse || !user) {
|
16 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
17 |
-
}
|
18 |
-
|
19 |
-
await dbConnect();
|
20 |
-
const param = await params;
|
21 |
-
const { namespace, repoId } = param;
|
22 |
-
|
23 |
-
const project = await Project.findOne({
|
24 |
-
user_id: user.id,
|
25 |
-
space_id: `${namespace}/${repoId}`,
|
26 |
-
}).lean();
|
27 |
-
if (!project) {
|
28 |
-
return NextResponse.json(
|
29 |
-
{
|
30 |
-
ok: false,
|
31 |
-
error: "Project not found",
|
32 |
-
},
|
33 |
-
{ status: 404 }
|
34 |
-
);
|
35 |
-
}
|
36 |
-
const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`;
|
37 |
-
try {
|
38 |
-
const space = await spaceInfo({
|
39 |
-
name: namespace + "/" + repoId,
|
40 |
-
accessToken: user.token as string,
|
41 |
-
additionalFields: ["author"],
|
42 |
-
});
|
43 |
-
|
44 |
-
if (!space || space.sdk !== "static") {
|
45 |
-
return NextResponse.json(
|
46 |
-
{
|
47 |
-
ok: false,
|
48 |
-
error: "Space is not a static space",
|
49 |
-
},
|
50 |
-
{ status: 404 }
|
51 |
-
);
|
52 |
-
}
|
53 |
-
if (space.author !== user.name) {
|
54 |
-
return NextResponse.json(
|
55 |
-
{
|
56 |
-
ok: false,
|
57 |
-
error: "Space does not belong to the authenticated user",
|
58 |
-
},
|
59 |
-
{ status: 403 }
|
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 }
|
86 |
-
);
|
87 |
-
|
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 }
|
98 |
-
);
|
99 |
-
}
|
100 |
-
return NextResponse.json(
|
101 |
-
{ error: error.message, ok: false },
|
102 |
-
{ status: 500 }
|
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 |
-
return NextResponse.json(
|
209 |
-
{
|
210 |
-
ok: false,
|
211 |
-
error: "Project already exists",
|
212 |
-
},
|
213 |
-
{ status: 400 }
|
214 |
-
);
|
215 |
-
}
|
216 |
-
|
217 |
-
const newProject = new Project({
|
218 |
-
user_id: user.id,
|
219 |
-
space_id: `${namespace}/${repoId}`,
|
220 |
-
prompts: [],
|
221 |
-
});
|
222 |
-
|
223 |
-
await newProject.save();
|
224 |
-
return NextResponse.json(
|
225 |
-
{
|
226 |
-
ok: true,
|
227 |
-
project: {
|
228 |
-
id: newProject._id,
|
229 |
-
space_id: newProject.space_id,
|
230 |
-
prompts: newProject.prompts,
|
231 |
-
},
|
232 |
-
},
|
233 |
-
{ status: 201 }
|
234 |
-
);
|
235 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/route.ts
DELETED
@@ -1,126 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import Project from "@/models/Project";
|
6 |
-
import dbConnect from "@/lib/mongodb";
|
7 |
-
import { COLORS, 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
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
import { headers } from "next/headers";
|
2 |
-
import { NextResponse } from "next/server";
|
3 |
-
|
4 |
-
export async function GET() {
|
5 |
-
const authHeaders = await headers();
|
6 |
-
const token = authHeaders.get("Authorization");
|
7 |
-
if (!token) {
|
8 |
-
return NextResponse.json({ user: null, errCode: 401 }, { status: 401 });
|
9 |
-
}
|
10 |
-
|
11 |
-
const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
|
12 |
-
headers: {
|
13 |
-
Authorization: `${token}`,
|
14 |
-
},
|
15 |
-
});
|
16 |
-
|
17 |
-
if (!userResponse.ok) {
|
18 |
-
return NextResponse.json(
|
19 |
-
{ user: null, errCode: userResponse.status },
|
20 |
-
{ status: userResponse.status }
|
21 |
-
);
|
22 |
-
}
|
23 |
-
const user = await userResponse.json();
|
24 |
-
return NextResponse.json({ user, errCode: null }, { status: 200 });
|
25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/re-design/route.ts
DELETED
@@ -1,39 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
|
3 |
-
export async function PUT(request: NextRequest) {
|
4 |
-
const body = await request.json();
|
5 |
-
const { url } = body;
|
6 |
-
|
7 |
-
if (!url) {
|
8 |
-
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
9 |
-
}
|
10 |
-
|
11 |
-
try {
|
12 |
-
const response = await fetch(
|
13 |
-
`https://r.jina.ai/${encodeURIComponent(url)}`,
|
14 |
-
{
|
15 |
-
method: "POST",
|
16 |
-
}
|
17 |
-
);
|
18 |
-
if (!response.ok) {
|
19 |
-
return NextResponse.json(
|
20 |
-
{ error: "Failed to fetch redesign" },
|
21 |
-
{ status: 500 }
|
22 |
-
);
|
23 |
-
}
|
24 |
-
const markdown = await response.text();
|
25 |
-
return NextResponse.json(
|
26 |
-
{
|
27 |
-
ok: true,
|
28 |
-
markdown,
|
29 |
-
},
|
30 |
-
{ status: 200 }
|
31 |
-
);
|
32 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
33 |
-
} catch (error: any) {
|
34 |
-
return NextResponse.json(
|
35 |
-
{ error: error.message || "An error occurred" },
|
36 |
-
{ status: 500 }
|
37 |
-
);
|
38 |
-
}
|
39 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/auth/callback/page.tsx
DELETED
@@ -1,72 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
import Link from "next/link";
|
3 |
-
import { useUser } from "@/hooks/useUser";
|
4 |
-
import { use, useState } from "react";
|
5 |
-
import { useMount, useTimeoutFn } from "react-use";
|
6 |
-
|
7 |
-
import { Button } from "@/components/ui/button";
|
8 |
-
export default function AuthCallback({
|
9 |
-
searchParams,
|
10 |
-
}: {
|
11 |
-
searchParams: Promise<{ code: string }>;
|
12 |
-
}) {
|
13 |
-
const [showButton, setShowButton] = useState(false);
|
14 |
-
const { code } = use(searchParams);
|
15 |
-
const { loginFromCode } = useUser();
|
16 |
-
|
17 |
-
useMount(async () => {
|
18 |
-
if (code) {
|
19 |
-
await loginFromCode(code);
|
20 |
-
}
|
21 |
-
});
|
22 |
-
|
23 |
-
useTimeoutFn(
|
24 |
-
() => setShowButton(true),
|
25 |
-
7000 // Show button after 5 seconds
|
26 |
-
);
|
27 |
-
|
28 |
-
return (
|
29 |
-
<div className="h-screen flex flex-col justify-center items-center">
|
30 |
-
<div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
|
31 |
-
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
32 |
-
<div className="flex items-center justify-center -space-x-4 mb-3">
|
33 |
-
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
34 |
-
🚀
|
35 |
-
</div>
|
36 |
-
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
37 |
-
👋
|
38 |
-
</div>
|
39 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
40 |
-
🙌
|
41 |
-
</div>
|
42 |
-
</div>
|
43 |
-
<p className="text-xl font-semibold text-neutral-950">
|
44 |
-
Login In Progress...
|
45 |
-
</p>
|
46 |
-
<p className="text-sm text-neutral-500 mt-1.5">
|
47 |
-
Wait a moment while we log you in with your code.
|
48 |
-
</p>
|
49 |
-
</header>
|
50 |
-
<main className="space-y-4 p-6">
|
51 |
-
<div>
|
52 |
-
<p className="text-sm text-neutral-700 mb-4 max-w-xs">
|
53 |
-
If you are not redirected automatically in the next 5 seconds,
|
54 |
-
please click the button below
|
55 |
-
</p>
|
56 |
-
{showButton ? (
|
57 |
-
<Link href="/">
|
58 |
-
<Button variant="black" className="relative">
|
59 |
-
Go to Home
|
60 |
-
</Button>
|
61 |
-
</Link>
|
62 |
-
) : (
|
63 |
-
<p className="text-xs text-neutral-500">
|
64 |
-
Please wait, we are logging you in...
|
65 |
-
</p>
|
66 |
-
)}
|
67 |
-
</div>
|
68 |
-
</main>
|
69 |
-
</div>
|
70 |
-
</div>
|
71 |
-
);
|
72 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/auth/page.tsx
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
import { redirect } from "next/navigation";
|
2 |
-
import { Metadata } from "next";
|
3 |
-
|
4 |
-
import { getAuth } from "@/app/actions/auth";
|
5 |
-
|
6 |
-
export const revalidate = 1;
|
7 |
-
|
8 |
-
export const metadata: Metadata = {
|
9 |
-
robots: "noindex, nofollow",
|
10 |
-
};
|
11 |
-
|
12 |
-
export default async function Auth() {
|
13 |
-
const loginRedirectUrl = await getAuth();
|
14 |
-
if (loginRedirectUrl) {
|
15 |
-
redirect(loginRedirectUrl);
|
16 |
-
}
|
17 |
-
|
18 |
-
return (
|
19 |
-
<div className="p-4">
|
20 |
-
<div className="border bg-red-500/10 border-red-500/20 text-red-500 px-5 py-3 rounded-lg">
|
21 |
-
<h1 className="text-xl font-bold">Error</h1>
|
22 |
-
<p className="text-sm">
|
23 |
-
An error occurred while trying to log in. Please try again later.
|
24 |
-
</p>
|
25 |
-
</div>
|
26 |
-
</div>
|
27 |
-
);
|
28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/favicon.ico
DELETED
Binary file (25.9 kB)
|
|
app/layout.tsx
DELETED
@@ -1,102 +0,0 @@
|
|
1 |
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
-
import type { Metadata, Viewport } from "next";
|
3 |
-
import { Inter, PT_Sans } from "next/font/google";
|
4 |
-
import { cookies } from "next/headers";
|
5 |
-
|
6 |
-
import TanstackProvider from "@/components/providers/tanstack-query-provider";
|
7 |
-
import "@/assets/globals.css";
|
8 |
-
import { Toaster } from "@/components/ui/sonner";
|
9 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
10 |
-
import { apiServer } from "@/lib/api";
|
11 |
-
import AppContext from "@/components/contexts/app-context";
|
12 |
-
|
13 |
-
const inter = Inter({
|
14 |
-
variable: "--font-inter-sans",
|
15 |
-
subsets: ["latin"],
|
16 |
-
});
|
17 |
-
|
18 |
-
const ptSans = PT_Sans({
|
19 |
-
variable: "--font-ptSans-mono",
|
20 |
-
subsets: ["latin"],
|
21 |
-
weight: ["400", "700"],
|
22 |
-
});
|
23 |
-
|
24 |
-
export const metadata: Metadata = {
|
25 |
-
title: "DeepSite | Build with AI ✨",
|
26 |
-
description:
|
27 |
-
"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.",
|
28 |
-
openGraph: {
|
29 |
-
title: "DeepSite | Build with AI ✨",
|
30 |
-
description:
|
31 |
-
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
32 |
-
url: "https://deepsite.hf.co",
|
33 |
-
siteName: "DeepSite",
|
34 |
-
images: [
|
35 |
-
{
|
36 |
-
url: "https://deepsite.hf.co/banner.png",
|
37 |
-
width: 1200,
|
38 |
-
height: 630,
|
39 |
-
alt: "DeepSite Open Graph Image",
|
40 |
-
},
|
41 |
-
],
|
42 |
-
},
|
43 |
-
twitter: {
|
44 |
-
card: "summary_large_image",
|
45 |
-
title: "DeepSite | Build with AI ✨",
|
46 |
-
description:
|
47 |
-
"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.",
|
48 |
-
images: ["https://deepsite.hf.co/banner.png"],
|
49 |
-
},
|
50 |
-
appleWebApp: {
|
51 |
-
capable: true,
|
52 |
-
title: "DeepSite",
|
53 |
-
statusBarStyle: "black-translucent",
|
54 |
-
},
|
55 |
-
icons: {
|
56 |
-
icon: "/logo.svg",
|
57 |
-
shortcut: "/logo.svg",
|
58 |
-
apple: "/logo.svg",
|
59 |
-
},
|
60 |
-
};
|
61 |
-
|
62 |
-
export const viewport: Viewport = {
|
63 |
-
initialScale: 1,
|
64 |
-
maximumScale: 1,
|
65 |
-
themeColor: "#000000",
|
66 |
-
};
|
67 |
-
|
68 |
-
async function getMe() {
|
69 |
-
const cookieStore = await cookies();
|
70 |
-
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
71 |
-
if (!token) return { user: null, errCode: null };
|
72 |
-
try {
|
73 |
-
const res = await apiServer.get("/me", {
|
74 |
-
headers: {
|
75 |
-
Authorization: `Bearer ${token}`,
|
76 |
-
},
|
77 |
-
});
|
78 |
-
return { user: res.data.user, errCode: null };
|
79 |
-
} catch (err: any) {
|
80 |
-
return { user: null, errCode: err.status };
|
81 |
-
}
|
82 |
-
}
|
83 |
-
|
84 |
-
export default async function RootLayout({
|
85 |
-
children,
|
86 |
-
}: Readonly<{
|
87 |
-
children: React.ReactNode;
|
88 |
-
}>) {
|
89 |
-
const data = await getMe();
|
90 |
-
return (
|
91 |
-
<html lang="en">
|
92 |
-
<body
|
93 |
-
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
94 |
-
>
|
95 |
-
<Toaster richColors position="bottom-center" />
|
96 |
-
<TanstackProvider>
|
97 |
-
<AppContext me={data}>{children}</AppContext>
|
98 |
-
</TanstackProvider>
|
99 |
-
</body>
|
100 |
-
</html>
|
101 |
-
);
|
102 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/projects/[namespace]/[repoId]/page.tsx
DELETED
@@ -1,40 +0,0 @@
|
|
1 |
-
import { cookies } from "next/headers";
|
2 |
-
import { redirect } from "next/navigation";
|
3 |
-
|
4 |
-
import { apiServer } from "@/lib/api";
|
5 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
6 |
-
import { AppEditor } from "@/components/editor";
|
7 |
-
|
8 |
-
async function getProject(namespace: string, repoId: string) {
|
9 |
-
// TODO replace with a server action
|
10 |
-
const cookieStore = await cookies();
|
11 |
-
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
12 |
-
if (!token) return {};
|
13 |
-
try {
|
14 |
-
const { data } = await apiServer.get(
|
15 |
-
`/me/projects/${namespace}/${repoId}`,
|
16 |
-
{
|
17 |
-
headers: {
|
18 |
-
Authorization: `Bearer ${token}`,
|
19 |
-
},
|
20 |
-
}
|
21 |
-
);
|
22 |
-
|
23 |
-
return data.project;
|
24 |
-
} catch {
|
25 |
-
return {};
|
26 |
-
}
|
27 |
-
}
|
28 |
-
|
29 |
-
export default async function ProjectNamespacePage({
|
30 |
-
params,
|
31 |
-
}: {
|
32 |
-
params: Promise<{ namespace: string; repoId: string }>;
|
33 |
-
}) {
|
34 |
-
const { namespace, repoId } = await params;
|
35 |
-
const project = await getProject(namespace, repoId);
|
36 |
-
if (!project?.html) {
|
37 |
-
redirect("/projects");
|
38 |
-
}
|
39 |
-
return <AppEditor project={project} />;
|
40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/projects/new/page.tsx
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import { AppEditor } from "@/components/editor";
|
2 |
-
|
3 |
-
export default function ProjectsNewPage() {
|
4 |
-
return <AppEditor />;
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
assets/globals.css
DELETED
@@ -1,146 +0,0 @@
|
|
1 |
-
@import "tailwindcss";
|
2 |
-
@import "tw-animate-css";
|
3 |
-
|
4 |
-
@custom-variant dark (&:is(.dark *));
|
5 |
-
|
6 |
-
@theme inline {
|
7 |
-
--color-background: var(--background);
|
8 |
-
--color-foreground: var(--foreground);
|
9 |
-
--font-sans: var(--font-inter-sans);
|
10 |
-
--font-mono: var(--font-ptSans-mono);
|
11 |
-
--color-sidebar-ring: var(--sidebar-ring);
|
12 |
-
--color-sidebar-border: var(--sidebar-border);
|
13 |
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
14 |
-
--color-sidebar-accent: var(--sidebar-accent);
|
15 |
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
16 |
-
--color-sidebar-primary: var(--sidebar-primary);
|
17 |
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
18 |
-
--color-sidebar: var(--sidebar);
|
19 |
-
--color-chart-5: var(--chart-5);
|
20 |
-
--color-chart-4: var(--chart-4);
|
21 |
-
--color-chart-3: var(--chart-3);
|
22 |
-
--color-chart-2: var(--chart-2);
|
23 |
-
--color-chart-1: var(--chart-1);
|
24 |
-
--color-ring: var(--ring);
|
25 |
-
--color-input: var(--input);
|
26 |
-
--color-border: var(--border);
|
27 |
-
--color-destructive: var(--destructive);
|
28 |
-
--color-accent-foreground: var(--accent-foreground);
|
29 |
-
--color-accent: var(--accent);
|
30 |
-
--color-muted-foreground: var(--muted-foreground);
|
31 |
-
--color-muted: var(--muted);
|
32 |
-
--color-secondary-foreground: var(--secondary-foreground);
|
33 |
-
--color-secondary: var(--secondary);
|
34 |
-
--color-primary-foreground: var(--primary-foreground);
|
35 |
-
--color-primary: var(--primary);
|
36 |
-
--color-popover-foreground: var(--popover-foreground);
|
37 |
-
--color-popover: var(--popover);
|
38 |
-
--color-card-foreground: var(--card-foreground);
|
39 |
-
--color-card: var(--card);
|
40 |
-
--radius-sm: calc(var(--radius) - 4px);
|
41 |
-
--radius-md: calc(var(--radius) - 2px);
|
42 |
-
--radius-lg: var(--radius);
|
43 |
-
--radius-xl: calc(var(--radius) + 4px);
|
44 |
-
}
|
45 |
-
|
46 |
-
:root {
|
47 |
-
--radius: 0.625rem;
|
48 |
-
--background: oklch(1 0 0);
|
49 |
-
--foreground: oklch(0.145 0 0);
|
50 |
-
--card: oklch(1 0 0);
|
51 |
-
--card-foreground: oklch(0.145 0 0);
|
52 |
-
--popover: oklch(1 0 0);
|
53 |
-
--popover-foreground: oklch(0.145 0 0);
|
54 |
-
--primary: oklch(0.205 0 0);
|
55 |
-
--primary-foreground: oklch(0.985 0 0);
|
56 |
-
--secondary: oklch(0.97 0 0);
|
57 |
-
--secondary-foreground: oklch(0.205 0 0);
|
58 |
-
--muted: oklch(0.97 0 0);
|
59 |
-
--muted-foreground: oklch(0.556 0 0);
|
60 |
-
--accent: oklch(0.97 0 0);
|
61 |
-
--accent-foreground: oklch(0.205 0 0);
|
62 |
-
--destructive: oklch(0.577 0.245 27.325);
|
63 |
-
--border: oklch(0.922 0 0);
|
64 |
-
--input: oklch(0.922 0 0);
|
65 |
-
--ring: oklch(0.708 0 0);
|
66 |
-
--chart-1: oklch(0.646 0.222 41.116);
|
67 |
-
--chart-2: oklch(0.6 0.118 184.704);
|
68 |
-
--chart-3: oklch(0.398 0.07 227.392);
|
69 |
-
--chart-4: oklch(0.828 0.189 84.429);
|
70 |
-
--chart-5: oklch(0.769 0.188 70.08);
|
71 |
-
--sidebar: oklch(0.985 0 0);
|
72 |
-
--sidebar-foreground: oklch(0.145 0 0);
|
73 |
-
--sidebar-primary: oklch(0.205 0 0);
|
74 |
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
75 |
-
--sidebar-accent: oklch(0.97 0 0);
|
76 |
-
--sidebar-accent-foreground: oklch(0.205 0 0);
|
77 |
-
--sidebar-border: oklch(0.922 0 0);
|
78 |
-
--sidebar-ring: oklch(0.708 0 0);
|
79 |
-
}
|
80 |
-
|
81 |
-
.dark {
|
82 |
-
--background: oklch(0.145 0 0);
|
83 |
-
--foreground: oklch(0.985 0 0);
|
84 |
-
--card: oklch(0.205 0 0);
|
85 |
-
--card-foreground: oklch(0.985 0 0);
|
86 |
-
--popover: oklch(0.205 0 0);
|
87 |
-
--popover-foreground: oklch(0.985 0 0);
|
88 |
-
--primary: oklch(0.922 0 0);
|
89 |
-
--primary-foreground: oklch(0.205 0 0);
|
90 |
-
--secondary: oklch(0.269 0 0);
|
91 |
-
--secondary-foreground: oklch(0.985 0 0);
|
92 |
-
--muted: oklch(0.269 0 0);
|
93 |
-
--muted-foreground: oklch(0.708 0 0);
|
94 |
-
--accent: oklch(0.269 0 0);
|
95 |
-
--accent-foreground: oklch(0.985 0 0);
|
96 |
-
--destructive: oklch(0.704 0.191 22.216);
|
97 |
-
--border: oklch(1 0 0 / 10%);
|
98 |
-
--input: oklch(1 0 0 / 15%);
|
99 |
-
--ring: oklch(0.556 0 0);
|
100 |
-
--chart-1: oklch(0.488 0.243 264.376);
|
101 |
-
--chart-2: oklch(0.696 0.17 162.48);
|
102 |
-
--chart-3: oklch(0.769 0.188 70.08);
|
103 |
-
--chart-4: oklch(0.627 0.265 303.9);
|
104 |
-
--chart-5: oklch(0.645 0.246 16.439);
|
105 |
-
--sidebar: oklch(0.205 0 0);
|
106 |
-
--sidebar-foreground: oklch(0.985 0 0);
|
107 |
-
--sidebar-primary: oklch(0.488 0.243 264.376);
|
108 |
-
--sidebar-primary-foreground: oklch(0.985 0 0);
|
109 |
-
--sidebar-accent: oklch(0.269 0 0);
|
110 |
-
--sidebar-accent-foreground: oklch(0.985 0 0);
|
111 |
-
--sidebar-border: oklch(1 0 0 / 10%);
|
112 |
-
--sidebar-ring: oklch(0.556 0 0);
|
113 |
-
}
|
114 |
-
|
115 |
-
@layer base {
|
116 |
-
* {
|
117 |
-
@apply border-border outline-ring/50;
|
118 |
-
}
|
119 |
-
body {
|
120 |
-
@apply bg-background text-foreground;
|
121 |
-
}
|
122 |
-
html {
|
123 |
-
@apply scroll-smooth;
|
124 |
-
}
|
125 |
-
}
|
126 |
-
|
127 |
-
.background__noisy {
|
128 |
-
@apply bg-blend-normal pointer-events-none opacity-90;
|
129 |
-
background-size: 25ww auto;
|
130 |
-
background-image: url("/background_noisy.webp");
|
131 |
-
@apply fixed w-screen h-screen -z-1 top-0 left-0;
|
132 |
-
}
|
133 |
-
|
134 |
-
.monaco-editor .margin {
|
135 |
-
@apply !bg-neutral-900;
|
136 |
-
}
|
137 |
-
.monaco-editor .monaco-editor-background {
|
138 |
-
@apply !bg-neutral-900;
|
139 |
-
}
|
140 |
-
.monaco-editor .line-numbers {
|
141 |
-
@apply !text-neutral-500;
|
142 |
-
}
|
143 |
-
|
144 |
-
.matched-line {
|
145 |
-
@apply bg-sky-500/30;
|
146 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/logo.svg
DELETED
components.json
DELETED
@@ -1,21 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"$schema": "https://ui.shadcn.com/schema.json",
|
3 |
-
"style": "new-york",
|
4 |
-
"rsc": true,
|
5 |
-
"tsx": true,
|
6 |
-
"tailwind": {
|
7 |
-
"config": "",
|
8 |
-
"css": "app/globals.css",
|
9 |
-
"baseColor": "neutral",
|
10 |
-
"cssVariables": true,
|
11 |
-
"prefix": ""
|
12 |
-
},
|
13 |
-
"aliases": {
|
14 |
-
"components": "@/components",
|
15 |
-
"utils": "@/lib/utils",
|
16 |
-
"ui": "@/components/ui",
|
17 |
-
"lib": "@/lib",
|
18 |
-
"hooks": "@/hooks"
|
19 |
-
},
|
20 |
-
"iconLibrary": "lucide"
|
21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/contexts/app-context.tsx
DELETED
@@ -1,57 +0,0 @@
|
|
1 |
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
-
"use client";
|
3 |
-
|
4 |
-
import { useUser } from "@/hooks/useUser";
|
5 |
-
import { usePathname, useRouter } from "next/navigation";
|
6 |
-
import { useMount } from "react-use";
|
7 |
-
import { UserContext } from "@/components/contexts/user-context";
|
8 |
-
import { User } from "@/types";
|
9 |
-
import { toast } from "sonner";
|
10 |
-
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
11 |
-
|
12 |
-
export default function AppContext({
|
13 |
-
children,
|
14 |
-
me: initialData,
|
15 |
-
}: {
|
16 |
-
children: React.ReactNode;
|
17 |
-
me?: {
|
18 |
-
user: User | null;
|
19 |
-
errCode: number | null;
|
20 |
-
};
|
21 |
-
}) {
|
22 |
-
const { loginFromCode, user, logout, loading, errCode } =
|
23 |
-
useUser(initialData);
|
24 |
-
const pathname = usePathname();
|
25 |
-
const router = useRouter();
|
26 |
-
|
27 |
-
useMount(() => {
|
28 |
-
if (!initialData?.user && !user) {
|
29 |
-
if ([401, 403].includes(errCode as number)) {
|
30 |
-
logout();
|
31 |
-
} else if (pathname.includes("/spaces")) {
|
32 |
-
if (errCode) {
|
33 |
-
toast.error("An error occured while trying to log in");
|
34 |
-
}
|
35 |
-
// If we did not manage to log in (probs because api is down), we simply redirect to the home page
|
36 |
-
router.push("/");
|
37 |
-
}
|
38 |
-
}
|
39 |
-
});
|
40 |
-
|
41 |
-
const events: any = {};
|
42 |
-
|
43 |
-
useBroadcastChannel("auth", (message) => {
|
44 |
-
if (pathname.includes("/auth/callback")) return;
|
45 |
-
|
46 |
-
if (!message.code) return;
|
47 |
-
if (message.type === "user-oauth" && message?.code && !events.code) {
|
48 |
-
loginFromCode(message.code);
|
49 |
-
}
|
50 |
-
});
|
51 |
-
|
52 |
-
return (
|
53 |
-
<UserContext value={{ user, loading, logout } as any}>
|
54 |
-
{children}
|
55 |
-
</UserContext>
|
56 |
-
);
|
57 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/contexts/user-context.tsx
DELETED
@@ -1,8 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import { createContext } from "react";
|
4 |
-
import { User } from "@/types";
|
5 |
-
|
6 |
-
export const UserContext = createContext({
|
7 |
-
user: undefined as User | undefined,
|
8 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/follow-up-tooltip.tsx
DELETED
@@ -1,36 +0,0 @@
|
|
1 |
-
import {
|
2 |
-
Popover,
|
3 |
-
PopoverContent,
|
4 |
-
PopoverTrigger,
|
5 |
-
} from "@/components/ui/popover";
|
6 |
-
import { Info } from "lucide-react";
|
7 |
-
|
8 |
-
export const FollowUpTooltip = () => {
|
9 |
-
return (
|
10 |
-
<Popover>
|
11 |
-
<PopoverTrigger asChild>
|
12 |
-
<Info className="size-3 text-neutral-300 cursor-pointer" />
|
13 |
-
</PopoverTrigger>
|
14 |
-
<PopoverContent
|
15 |
-
align="start"
|
16 |
-
className="!rounded-2xl !p-0 min-w-xs text-center overflow-hidden"
|
17 |
-
>
|
18 |
-
<header className="bg-neutral-950 px-4 py-3 border-b border-neutral-700/70">
|
19 |
-
<p className="text-base text-neutral-200 font-semibold">
|
20 |
-
⚡ Faster, Smarter Updates
|
21 |
-
</p>
|
22 |
-
</header>
|
23 |
-
<main className="p-4">
|
24 |
-
<p className="text-neutral-300 text-sm">
|
25 |
-
Using the Diff-Patch system, allow DeepSite to intelligently update
|
26 |
-
your project without rewritting the entire codebase.
|
27 |
-
</p>
|
28 |
-
<p className="text-neutral-500 text-sm mt-2">
|
29 |
-
This means faster updates, less data usage, and a more efficient
|
30 |
-
development process.
|
31 |
-
</p>
|
32 |
-
</main>
|
33 |
-
</PopoverContent>
|
34 |
-
</Popover>
|
35 |
-
);
|
36 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/index.tsx
DELETED
@@ -1,474 +0,0 @@
|
|
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 |
-
// if (!request.ok) {
|
149 |
-
// const res = await request.json();
|
150 |
-
// if (res.openLogin) {
|
151 |
-
// setOpen(true);
|
152 |
-
// } else if (res.openSelectProvider) {
|
153 |
-
// setOpenProvider(true);
|
154 |
-
// setProviderError(res.message);
|
155 |
-
// } else if (res.openProModal) {
|
156 |
-
// setOpenProModal(true);
|
157 |
-
// } else {
|
158 |
-
// toast.error(res.message);
|
159 |
-
// }
|
160 |
-
// setisAiWorking(false);
|
161 |
-
// return;
|
162 |
-
// }
|
163 |
-
const reader = request.body.getReader();
|
164 |
-
const decoder = new TextDecoder("utf-8");
|
165 |
-
const selectedModel = MODELS.find(
|
166 |
-
(m: { value: string }) => m.value === model
|
167 |
-
);
|
168 |
-
let contentThink: string | undefined = undefined;
|
169 |
-
const read = async () => {
|
170 |
-
const { done, value } = await reader.read();
|
171 |
-
if (done) {
|
172 |
-
toast.success("AI responded successfully");
|
173 |
-
setPreviousPrompt(prompt);
|
174 |
-
setPrompt("");
|
175 |
-
setisAiWorking(false);
|
176 |
-
setHasAsked(true);
|
177 |
-
setModel(MODELS[0].value);
|
178 |
-
if (audio.current) audio.current.play();
|
179 |
-
|
180 |
-
// Now we have the complete HTML including </html>, so set it to be sure
|
181 |
-
const finalDoc = contentResponse.match(
|
182 |
-
/<!DOCTYPE html>[\s\S]*<\/html>/
|
183 |
-
)?.[0];
|
184 |
-
if (finalDoc) {
|
185 |
-
setHtml(finalDoc);
|
186 |
-
}
|
187 |
-
onSuccess(finalDoc ?? contentResponse, prompt);
|
188 |
-
|
189 |
-
return;
|
190 |
-
}
|
191 |
-
|
192 |
-
const chunk = decoder.decode(value, { stream: true });
|
193 |
-
try {
|
194 |
-
const res = JSON.parse(chunk);
|
195 |
-
if (res.openLogin) {
|
196 |
-
setOpen(true);
|
197 |
-
} else if (res.openSelectProvider) {
|
198 |
-
setOpenProvider(true);
|
199 |
-
setProviderError(res.message);
|
200 |
-
} else if (res.openProModal) {
|
201 |
-
setOpenProModal(true);
|
202 |
-
} else {
|
203 |
-
toast.error(res.message);
|
204 |
-
}
|
205 |
-
setisAiWorking(false);
|
206 |
-
return;
|
207 |
-
} catch {
|
208 |
-
thinkResponse += chunk;
|
209 |
-
if (selectedModel?.isThinker) {
|
210 |
-
const thinkMatch = thinkResponse.match(/<think>[\s\S]*/)?.[0];
|
211 |
-
if (thinkMatch && !thinkResponse?.includes("</think>")) {
|
212 |
-
if ((contentThink?.length ?? 0) < 3) {
|
213 |
-
setOpenThink(true);
|
214 |
-
}
|
215 |
-
setThink(thinkMatch.replace("<think>", "").trim());
|
216 |
-
contentThink += chunk;
|
217 |
-
return read();
|
218 |
-
}
|
219 |
-
}
|
220 |
-
|
221 |
-
contentResponse += chunk;
|
222 |
-
|
223 |
-
const newHtml = contentResponse.match(
|
224 |
-
/<!DOCTYPE html>[\s\S]*/
|
225 |
-
)?.[0];
|
226 |
-
if (newHtml) {
|
227 |
-
setIsThinking(false);
|
228 |
-
let partialDoc = newHtml;
|
229 |
-
if (
|
230 |
-
partialDoc.includes("<head>") &&
|
231 |
-
!partialDoc.includes("</head>")
|
232 |
-
) {
|
233 |
-
partialDoc += "\n</head>";
|
234 |
-
}
|
235 |
-
if (
|
236 |
-
partialDoc.includes("<body") &&
|
237 |
-
!partialDoc.includes("</body>")
|
238 |
-
) {
|
239 |
-
partialDoc += "\n</body>";
|
240 |
-
}
|
241 |
-
if (!partialDoc.includes("</html>")) {
|
242 |
-
partialDoc += "\n</html>";
|
243 |
-
}
|
244 |
-
|
245 |
-
// Throttle the re-renders to avoid flashing/flicker
|
246 |
-
const now = Date.now();
|
247 |
-
if (now - lastRenderTime > 300) {
|
248 |
-
setHtml(partialDoc);
|
249 |
-
lastRenderTime = now;
|
250 |
-
}
|
251 |
-
|
252 |
-
if (partialDoc.length > 200) {
|
253 |
-
onScrollToBottom();
|
254 |
-
}
|
255 |
-
}
|
256 |
-
read();
|
257 |
-
}
|
258 |
-
};
|
259 |
-
|
260 |
-
read();
|
261 |
-
}
|
262 |
-
}
|
263 |
-
} catch (error: any) {
|
264 |
-
setisAiWorking(false);
|
265 |
-
toast.error(error.message);
|
266 |
-
if (error.openLogin) {
|
267 |
-
setOpen(true);
|
268 |
-
}
|
269 |
-
}
|
270 |
-
};
|
271 |
-
|
272 |
-
const stopController = () => {
|
273 |
-
if (controller) {
|
274 |
-
controller.abort();
|
275 |
-
setController(null);
|
276 |
-
setisAiWorking(false);
|
277 |
-
setThink("");
|
278 |
-
setOpenThink(false);
|
279 |
-
setIsThinking(false);
|
280 |
-
}
|
281 |
-
};
|
282 |
-
|
283 |
-
useUpdateEffect(() => {
|
284 |
-
if (refThink.current) {
|
285 |
-
refThink.current.scrollTop = refThink.current.scrollHeight;
|
286 |
-
}
|
287 |
-
}, [think]);
|
288 |
-
|
289 |
-
useUpdateEffect(() => {
|
290 |
-
if (!isThinking) {
|
291 |
-
setOpenThink(false);
|
292 |
-
}
|
293 |
-
}, [isThinking]);
|
294 |
-
|
295 |
-
const isSameHtml = useMemo(() => {
|
296 |
-
return isTheSameHtml(html);
|
297 |
-
}, [html]);
|
298 |
-
|
299 |
-
return (
|
300 |
-
<div className="px-3">
|
301 |
-
<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">
|
302 |
-
{think && (
|
303 |
-
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
304 |
-
<header
|
305 |
-
className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
|
306 |
-
onClick={() => {
|
307 |
-
setOpenThink(!openThink);
|
308 |
-
}}
|
309 |
-
>
|
310 |
-
<p className="text-sm font-medium text-neutral-300 group-hover:text-neutral-200 transition-colors duration-200">
|
311 |
-
{isThinking ? "DeepSite is thinking..." : "DeepSite's plan"}
|
312 |
-
</p>
|
313 |
-
<ChevronDown
|
314 |
-
className={classNames(
|
315 |
-
"size-4 text-neutral-400 group-hover:text-neutral-300 transition-all duration-200",
|
316 |
-
{
|
317 |
-
"rotate-180": openThink,
|
318 |
-
}
|
319 |
-
)}
|
320 |
-
/>
|
321 |
-
</header>
|
322 |
-
<main
|
323 |
-
ref={refThink}
|
324 |
-
className={classNames(
|
325 |
-
"overflow-y-auto transition-all duration-200 ease-in-out",
|
326 |
-
{
|
327 |
-
"max-h-[0px]": !openThink,
|
328 |
-
"min-h-[250px] max-h-[250px] border-t border-neutral-700":
|
329 |
-
openThink,
|
330 |
-
}
|
331 |
-
)}
|
332 |
-
>
|
333 |
-
<p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
|
334 |
-
{think}
|
335 |
-
</p>
|
336 |
-
</main>
|
337 |
-
</div>
|
338 |
-
)}
|
339 |
-
{selectedElement && (
|
340 |
-
<div className="px-4 pt-3">
|
341 |
-
<SelectedHtmlElement
|
342 |
-
element={selectedElement}
|
343 |
-
isAiWorking={isAiWorking}
|
344 |
-
onDelete={() => setSelectedElement(null)}
|
345 |
-
/>
|
346 |
-
</div>
|
347 |
-
)}
|
348 |
-
<div className="w-full relative flex items-center justify-between">
|
349 |
-
{isAiWorking && (
|
350 |
-
<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">
|
351 |
-
<div className="flex items-center justify-start gap-2">
|
352 |
-
<Loading overlay={false} className="!size-4" />
|
353 |
-
<p className="text-neutral-400 text-sm">
|
354 |
-
AI is {isThinking ? "thinking" : "coding"}...{" "}
|
355 |
-
</p>
|
356 |
-
</div>
|
357 |
-
<div
|
358 |
-
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"
|
359 |
-
onClick={stopController}
|
360 |
-
>
|
361 |
-
<FaStopCircle />
|
362 |
-
Stop generation
|
363 |
-
</div>
|
364 |
-
</div>
|
365 |
-
)}
|
366 |
-
<input
|
367 |
-
type="text"
|
368 |
-
disabled={isAiWorking}
|
369 |
-
className={classNames(
|
370 |
-
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
|
371 |
-
{
|
372 |
-
"!pt-2.5": selectedElement && !isAiWorking,
|
373 |
-
}
|
374 |
-
)}
|
375 |
-
placeholder={
|
376 |
-
selectedElement
|
377 |
-
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
378 |
-
: hasAsked
|
379 |
-
? "Ask DeepSite for edits"
|
380 |
-
: "Ask DeepSite anything..."
|
381 |
-
}
|
382 |
-
value={prompt}
|
383 |
-
onChange={(e) => setPrompt(e.target.value)}
|
384 |
-
onKeyDown={(e) => {
|
385 |
-
if (e.key === "Enter" && !e.shiftKey) {
|
386 |
-
callAi();
|
387 |
-
}
|
388 |
-
}}
|
389 |
-
/>
|
390 |
-
</div>
|
391 |
-
<div className="flex items-center justify-between gap-2 px-4 pb-3">
|
392 |
-
<div className="flex-1 flex items-center justify-start gap-1.5">
|
393 |
-
<ReImagine onRedesign={(md) => callAi(md)} />
|
394 |
-
{!isSameHtml && (
|
395 |
-
<Tooltip>
|
396 |
-
<TooltipTrigger asChild>
|
397 |
-
<Button
|
398 |
-
size="xs"
|
399 |
-
variant={isEditableModeEnabled ? "default" : "outline"}
|
400 |
-
onClick={() => {
|
401 |
-
setIsEditableModeEnabled?.(!isEditableModeEnabled);
|
402 |
-
}}
|
403 |
-
className={classNames("h-[28px]", {
|
404 |
-
"!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
|
405 |
-
!isEditableModeEnabled,
|
406 |
-
})}
|
407 |
-
>
|
408 |
-
<Crosshair className="size-4" />
|
409 |
-
Edit
|
410 |
-
</Button>
|
411 |
-
</TooltipTrigger>
|
412 |
-
<TooltipContent
|
413 |
-
align="start"
|
414 |
-
className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
|
415 |
-
>
|
416 |
-
Select an element on the page to ask DeepSite edit it
|
417 |
-
directly.
|
418 |
-
</TooltipContent>
|
419 |
-
</Tooltip>
|
420 |
-
)}
|
421 |
-
<InviteFriends />
|
422 |
-
</div>
|
423 |
-
<div className="flex items-center justify-end gap-2">
|
424 |
-
<Settings
|
425 |
-
provider={provider as string}
|
426 |
-
model={model as string}
|
427 |
-
onChange={setProvider}
|
428 |
-
onModelChange={setModel}
|
429 |
-
open={openProvider}
|
430 |
-
error={providerError}
|
431 |
-
isFollowUp={!isSameHtml}
|
432 |
-
onClose={setOpenProvider}
|
433 |
-
/>
|
434 |
-
<Button
|
435 |
-
size="iconXs"
|
436 |
-
disabled={isAiWorking || !prompt.trim()}
|
437 |
-
onClick={() => callAi()}
|
438 |
-
>
|
439 |
-
<ArrowUp className="size-4" />
|
440 |
-
</Button>
|
441 |
-
</div>
|
442 |
-
</div>
|
443 |
-
<LoginModal open={open} onClose={() => setOpen(false)} html={html} />
|
444 |
-
<ProModal
|
445 |
-
html={html}
|
446 |
-
open={openProModal}
|
447 |
-
onClose={() => setOpenProModal(false)}
|
448 |
-
/>
|
449 |
-
{!isSameHtml && (
|
450 |
-
<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">
|
451 |
-
<label
|
452 |
-
htmlFor="diff-patch-checkbox"
|
453 |
-
className="flex items-center gap-1.5 cursor-pointer"
|
454 |
-
>
|
455 |
-
<Checkbox
|
456 |
-
id="diff-patch-checkbox"
|
457 |
-
checked={isFollowUp}
|
458 |
-
onCheckedChange={(e) => {
|
459 |
-
setIsFollowUp(e === true);
|
460 |
-
}}
|
461 |
-
/>
|
462 |
-
Diff-Patch Update
|
463 |
-
</label>
|
464 |
-
<FollowUpTooltip />
|
465 |
-
</div>
|
466 |
-
)}
|
467 |
-
</div>
|
468 |
-
<audio ref={audio} id="audio" className="hidden">
|
469 |
-
<source src="/success.mp3" type="audio/mpeg" />
|
470 |
-
Your browser does not support the audio element.
|
471 |
-
</audio>
|
472 |
-
</div>
|
473 |
-
);
|
474 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/re-imagine.tsx
DELETED
@@ -1,146 +0,0 @@
|
|
1 |
-
import { useState } from "react";
|
2 |
-
import { Paintbrush } from "lucide-react";
|
3 |
-
import { toast } from "sonner";
|
4 |
-
|
5 |
-
import { Button } from "@/components/ui/button";
|
6 |
-
import {
|
7 |
-
Popover,
|
8 |
-
PopoverContent,
|
9 |
-
PopoverTrigger,
|
10 |
-
} from "@/components/ui/popover";
|
11 |
-
import { Input } from "@/components/ui/input";
|
12 |
-
import Loading from "@/components/loading";
|
13 |
-
import { api } from "@/lib/api";
|
14 |
-
|
15 |
-
export function ReImagine({
|
16 |
-
onRedesign,
|
17 |
-
}: {
|
18 |
-
onRedesign: (md: string) => void;
|
19 |
-
}) {
|
20 |
-
const [url, setUrl] = useState<string>("");
|
21 |
-
const [open, setOpen] = useState(false);
|
22 |
-
const [isLoading, setIsLoading] = useState(false);
|
23 |
-
|
24 |
-
const checkIfUrlIsValid = (url: string) => {
|
25 |
-
const urlPattern = new RegExp(
|
26 |
-
/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,
|
27 |
-
"i"
|
28 |
-
);
|
29 |
-
return urlPattern.test(url);
|
30 |
-
};
|
31 |
-
|
32 |
-
const handleClick = async () => {
|
33 |
-
if (isLoading) return; // Prevent multiple clicks while loading
|
34 |
-
if (!url) {
|
35 |
-
toast.error("Please enter a URL.");
|
36 |
-
return;
|
37 |
-
}
|
38 |
-
if (!checkIfUrlIsValid(url)) {
|
39 |
-
toast.error("Please enter a valid URL.");
|
40 |
-
return;
|
41 |
-
}
|
42 |
-
setIsLoading(true);
|
43 |
-
const response = await api.put("/re-design", {
|
44 |
-
url: url.trim(),
|
45 |
-
});
|
46 |
-
if (response?.data?.ok) {
|
47 |
-
setOpen(false);
|
48 |
-
setUrl("");
|
49 |
-
onRedesign(response.data.markdown);
|
50 |
-
toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
|
51 |
-
} else {
|
52 |
-
toast.error(response?.data?.error || "Failed to redesign the site.");
|
53 |
-
}
|
54 |
-
setIsLoading(false);
|
55 |
-
};
|
56 |
-
|
57 |
-
return (
|
58 |
-
<Popover open={open} onOpenChange={setOpen}>
|
59 |
-
<form>
|
60 |
-
<PopoverTrigger asChild>
|
61 |
-
<Button
|
62 |
-
size="iconXs"
|
63 |
-
variant="outline"
|
64 |
-
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
65 |
-
>
|
66 |
-
<Paintbrush className="size-4" />
|
67 |
-
</Button>
|
68 |
-
</PopoverTrigger>
|
69 |
-
<PopoverContent
|
70 |
-
align="start"
|
71 |
-
className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
|
72 |
-
>
|
73 |
-
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
74 |
-
<div className="flex items-center justify-center -space-x-4 mb-3">
|
75 |
-
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
76 |
-
🎨
|
77 |
-
</div>
|
78 |
-
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
79 |
-
🥳
|
80 |
-
</div>
|
81 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
82 |
-
💎
|
83 |
-
</div>
|
84 |
-
</div>
|
85 |
-
<p className="text-xl font-semibold text-neutral-950">
|
86 |
-
Redesign your Site!
|
87 |
-
</p>
|
88 |
-
<p className="text-sm text-neutral-500 mt-1.5">
|
89 |
-
Try our new Redesign feature to give your site a fresh look.
|
90 |
-
</p>
|
91 |
-
</header>
|
92 |
-
<main className="space-y-4 p-6">
|
93 |
-
<div>
|
94 |
-
<p className="text-sm text-neutral-700 mb-2">
|
95 |
-
Enter your website URL to get started:
|
96 |
-
</p>
|
97 |
-
<Input
|
98 |
-
type="text"
|
99 |
-
placeholder="https://example.com"
|
100 |
-
value={url}
|
101 |
-
onChange={(e) => setUrl(e.target.value)}
|
102 |
-
onBlur={(e) => {
|
103 |
-
const inputUrl = e.target.value.trim();
|
104 |
-
if (!inputUrl) {
|
105 |
-
setUrl("");
|
106 |
-
return;
|
107 |
-
}
|
108 |
-
if (!checkIfUrlIsValid(inputUrl)) {
|
109 |
-
toast.error("Please enter a valid URL.");
|
110 |
-
return;
|
111 |
-
}
|
112 |
-
setUrl(inputUrl);
|
113 |
-
}}
|
114 |
-
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
115 |
-
/>
|
116 |
-
</div>
|
117 |
-
<div>
|
118 |
-
<p className="text-sm text-neutral-700 mb-2">
|
119 |
-
Then, let's redesign it!
|
120 |
-
</p>
|
121 |
-
<Button
|
122 |
-
variant="black"
|
123 |
-
onClick={handleClick}
|
124 |
-
className="relative w-full"
|
125 |
-
>
|
126 |
-
{isLoading ? (
|
127 |
-
<>
|
128 |
-
<Loading
|
129 |
-
overlay={false}
|
130 |
-
className="ml-2 size-4 animate-spin"
|
131 |
-
/>
|
132 |
-
Fetching your site...
|
133 |
-
</>
|
134 |
-
) : (
|
135 |
-
<>
|
136 |
-
Redesign <Paintbrush className="size-4" />
|
137 |
-
</>
|
138 |
-
)}
|
139 |
-
</Button>
|
140 |
-
</div>
|
141 |
-
</main>
|
142 |
-
</PopoverContent>
|
143 |
-
</form>
|
144 |
-
</Popover>
|
145 |
-
);
|
146 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/selected-html-element.tsx
DELETED
@@ -1,57 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { Code, XCircle } from "lucide-react";
|
3 |
-
|
4 |
-
import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible";
|
5 |
-
import { htmlTagToText } from "@/lib/html-tag-to-text";
|
6 |
-
|
7 |
-
export const SelectedHtmlElement = ({
|
8 |
-
element,
|
9 |
-
isAiWorking = false,
|
10 |
-
onDelete,
|
11 |
-
}: {
|
12 |
-
element: HTMLElement | null;
|
13 |
-
isAiWorking: boolean;
|
14 |
-
onDelete?: () => void;
|
15 |
-
}) => {
|
16 |
-
if (!element) return null;
|
17 |
-
|
18 |
-
const tagName = element.tagName.toLowerCase();
|
19 |
-
return (
|
20 |
-
<Collapsible
|
21 |
-
className={classNames(
|
22 |
-
"border border-neutral-700 rounded-xl p-1.5 pr-3 max-w-max hover:brightness-110 transition-all duration-200 ease-in-out !cursor-pointer",
|
23 |
-
{
|
24 |
-
"!cursor-pointer": !isAiWorking,
|
25 |
-
"opacity-50 !cursor-not-allowed": isAiWorking,
|
26 |
-
}
|
27 |
-
)}
|
28 |
-
disabled={isAiWorking}
|
29 |
-
onClick={() => {
|
30 |
-
if (!isAiWorking && onDelete) {
|
31 |
-
onDelete();
|
32 |
-
}
|
33 |
-
}}
|
34 |
-
>
|
35 |
-
<CollapsibleTrigger className="flex items-center justify-start gap-2 cursor-pointer">
|
36 |
-
<div className="rounded-lg bg-neutral-700 size-6 flex items-center justify-center">
|
37 |
-
<Code className="text-neutral-300 size-3.5" />
|
38 |
-
</div>
|
39 |
-
<p className="text-sm font-semibold text-neutral-300">
|
40 |
-
{element.textContent?.trim().split(/\s+/)[0]} {htmlTagToText(tagName)}
|
41 |
-
</p>
|
42 |
-
<XCircle className="text-neutral-300 size-4" />
|
43 |
-
</CollapsibleTrigger>
|
44 |
-
{/* <CollapsibleContent className="border-t border-neutral-700 pt-2 mt-2">
|
45 |
-
<div className="text-xs text-neutral-400">
|
46 |
-
<p>
|
47 |
-
<span className="font-semibold">ID:</span> {element.id || "No ID"}
|
48 |
-
</p>
|
49 |
-
<p>
|
50 |
-
<span className="font-semibold">Classes:</span>{" "}
|
51 |
-
{element.className || "No classes"}
|
52 |
-
</p>
|
53 |
-
</div>
|
54 |
-
</CollapsibleContent> */}
|
55 |
-
</Collapsible>
|
56 |
-
);
|
57 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/settings.tsx
DELETED
@@ -1,212 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { PiGearSixFill } from "react-icons/pi";
|
3 |
-
import { RiCheckboxCircleFill } from "react-icons/ri";
|
4 |
-
|
5 |
-
import {
|
6 |
-
Popover,
|
7 |
-
PopoverContent,
|
8 |
-
PopoverTrigger,
|
9 |
-
} from "@/components/ui/popover";
|
10 |
-
import { PROVIDERS, MODELS } from "@/lib/providers";
|
11 |
-
import { Button } from "@/components/ui/button";
|
12 |
-
import {
|
13 |
-
Select,
|
14 |
-
SelectContent,
|
15 |
-
SelectGroup,
|
16 |
-
SelectItem,
|
17 |
-
SelectLabel,
|
18 |
-
SelectTrigger,
|
19 |
-
SelectValue,
|
20 |
-
} from "@/components/ui/select";
|
21 |
-
import { useMemo } from "react";
|
22 |
-
import { useUpdateEffect } from "react-use";
|
23 |
-
import Image from "next/image";
|
24 |
-
|
25 |
-
export function Settings({
|
26 |
-
open,
|
27 |
-
onClose,
|
28 |
-
provider,
|
29 |
-
model,
|
30 |
-
error,
|
31 |
-
isFollowUp = false,
|
32 |
-
onChange,
|
33 |
-
onModelChange,
|
34 |
-
}: {
|
35 |
-
open: boolean;
|
36 |
-
provider: string;
|
37 |
-
model: string;
|
38 |
-
error?: string;
|
39 |
-
isFollowUp?: boolean;
|
40 |
-
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
41 |
-
onChange: (provider: string) => void;
|
42 |
-
onModelChange: (model: string) => void;
|
43 |
-
}) {
|
44 |
-
const modelAvailableProviders = useMemo(() => {
|
45 |
-
const availableProviders = MODELS.find(
|
46 |
-
(m: { value: string }) => m.value === model
|
47 |
-
)?.providers;
|
48 |
-
if (!availableProviders) return Object.keys(PROVIDERS);
|
49 |
-
return Object.keys(PROVIDERS).filter((id) =>
|
50 |
-
availableProviders.includes(id)
|
51 |
-
);
|
52 |
-
}, [model]);
|
53 |
-
|
54 |
-
useUpdateEffect(() => {
|
55 |
-
if (provider !== "auto" && !modelAvailableProviders.includes(provider)) {
|
56 |
-
onChange("auto");
|
57 |
-
}
|
58 |
-
}, [model, provider]);
|
59 |
-
|
60 |
-
return (
|
61 |
-
<div className="">
|
62 |
-
<Popover open={open} onOpenChange={onClose}>
|
63 |
-
<PopoverTrigger asChild>
|
64 |
-
<Button variant="black" size="sm">
|
65 |
-
<PiGearSixFill className="size-4" />
|
66 |
-
Settings
|
67 |
-
</Button>
|
68 |
-
</PopoverTrigger>
|
69 |
-
<PopoverContent
|
70 |
-
className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
|
71 |
-
align="center"
|
72 |
-
>
|
73 |
-
<header className="flex items-center justify-center text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
|
74 |
-
Customize Settings
|
75 |
-
</header>
|
76 |
-
<main className="px-4 pt-5 pb-6 space-y-5">
|
77 |
-
{/* <a
|
78 |
-
href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/74"
|
79 |
-
target="_blank"
|
80 |
-
className="w-full flex items-center justify-between text-neutral-300 bg-neutral-300/15 border border-neutral-300/15 pl-4 p-1.5 rounded-full text-sm font-medium hover:brightness-95"
|
81 |
-
>
|
82 |
-
How to use it locally?
|
83 |
-
<Button size="xs">See guide</Button>
|
84 |
-
</a> */}
|
85 |
-
{error !== "" && (
|
86 |
-
<p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
|
87 |
-
{error}
|
88 |
-
</p>
|
89 |
-
)}
|
90 |
-
<label className="block">
|
91 |
-
<p className="text-neutral-300 text-sm mb-2.5">
|
92 |
-
Choose a DeepSeek model
|
93 |
-
</p>
|
94 |
-
<Select defaultValue={model} onValueChange={onModelChange}>
|
95 |
-
<SelectTrigger className="w-full">
|
96 |
-
<SelectValue placeholder="Select a DeepSeek model" />
|
97 |
-
</SelectTrigger>
|
98 |
-
<SelectContent>
|
99 |
-
<SelectGroup>
|
100 |
-
<SelectLabel>DeepSeek models</SelectLabel>
|
101 |
-
{MODELS.map(
|
102 |
-
({
|
103 |
-
value,
|
104 |
-
label,
|
105 |
-
isNew = false,
|
106 |
-
isThinker = false,
|
107 |
-
}: {
|
108 |
-
value: string;
|
109 |
-
label: string;
|
110 |
-
isNew?: boolean;
|
111 |
-
isThinker?: boolean;
|
112 |
-
}) => (
|
113 |
-
<SelectItem
|
114 |
-
key={value}
|
115 |
-
value={value}
|
116 |
-
className=""
|
117 |
-
disabled={isThinker && isFollowUp}
|
118 |
-
>
|
119 |
-
{label}
|
120 |
-
{isNew && (
|
121 |
-
<span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
|
122 |
-
New
|
123 |
-
</span>
|
124 |
-
)}
|
125 |
-
</SelectItem>
|
126 |
-
)
|
127 |
-
)}
|
128 |
-
</SelectGroup>
|
129 |
-
</SelectContent>
|
130 |
-
</Select>
|
131 |
-
</label>
|
132 |
-
{isFollowUp && (
|
133 |
-
<div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
|
134 |
-
Note: You can't use a Thinker model for follow-up requests.
|
135 |
-
We automatically switch to the default model for you.
|
136 |
-
</div>
|
137 |
-
)}
|
138 |
-
<div className="flex flex-col gap-3">
|
139 |
-
<div className="flex items-center justify-between">
|
140 |
-
<div>
|
141 |
-
<p className="text-neutral-300 text-sm mb-1.5">
|
142 |
-
Use auto-provider
|
143 |
-
</p>
|
144 |
-
<p className="text-xs text-neutral-400/70">
|
145 |
-
We'll automatically select the best provider for you
|
146 |
-
based on your prompt.
|
147 |
-
</p>
|
148 |
-
</div>
|
149 |
-
<div
|
150 |
-
className={classNames(
|
151 |
-
"bg-neutral-700 rounded-full min-w-10 w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
|
152 |
-
{
|
153 |
-
"!bg-sky-500": provider === "auto",
|
154 |
-
}
|
155 |
-
)}
|
156 |
-
onClick={() => {
|
157 |
-
const foundModel = MODELS.find(
|
158 |
-
(m: { value: string }) => m.value === model
|
159 |
-
);
|
160 |
-
if (provider === "auto" && foundModel?.autoProvider) {
|
161 |
-
onChange(foundModel.autoProvider);
|
162 |
-
} else {
|
163 |
-
onChange("auto");
|
164 |
-
}
|
165 |
-
}}
|
166 |
-
>
|
167 |
-
<div
|
168 |
-
className={classNames(
|
169 |
-
"w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
|
170 |
-
{
|
171 |
-
"translate-x-4": provider === "auto",
|
172 |
-
}
|
173 |
-
)}
|
174 |
-
/>
|
175 |
-
</div>
|
176 |
-
</div>
|
177 |
-
<label className="block">
|
178 |
-
<p className="text-neutral-300 text-sm mb-2">
|
179 |
-
Inference Provider
|
180 |
-
</p>
|
181 |
-
<div className="grid grid-cols-2 gap-1.5">
|
182 |
-
{modelAvailableProviders.map((id: string) => (
|
183 |
-
<Button
|
184 |
-
key={id}
|
185 |
-
variant={id === provider ? "default" : "secondary"}
|
186 |
-
size="sm"
|
187 |
-
onClick={() => {
|
188 |
-
onChange(id);
|
189 |
-
}}
|
190 |
-
>
|
191 |
-
<Image
|
192 |
-
src={`/providers/${id}.svg`}
|
193 |
-
alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
|
194 |
-
className="size-5 mr-2"
|
195 |
-
width={20}
|
196 |
-
height={20}
|
197 |
-
/>
|
198 |
-
{PROVIDERS[id as keyof typeof PROVIDERS].name}
|
199 |
-
{id === provider && (
|
200 |
-
<RiCheckboxCircleFill className="ml-2 size-4 text-blue-500" />
|
201 |
-
)}
|
202 |
-
</Button>
|
203 |
-
))}
|
204 |
-
</div>
|
205 |
-
</label>
|
206 |
-
</div>
|
207 |
-
</main>
|
208 |
-
</PopoverContent>
|
209 |
-
</Popover>
|
210 |
-
</div>
|
211 |
-
);
|
212 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/deploy-button/index.tsx
DELETED
@@ -1,171 +0,0 @@
|
|
1 |
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
-
import { useState } from "react";
|
3 |
-
import { toast } from "sonner";
|
4 |
-
import Image from "next/image";
|
5 |
-
import { useRouter } from "next/navigation";
|
6 |
-
import { MdSave } from "react-icons/md";
|
7 |
-
import { Rocket } from "lucide-react";
|
8 |
-
|
9 |
-
import SpaceIcon from "@/assets/space.svg";
|
10 |
-
import Loading from "@/components/loading";
|
11 |
-
import { Button } from "@/components/ui/button";
|
12 |
-
import {
|
13 |
-
Popover,
|
14 |
-
PopoverContent,
|
15 |
-
PopoverTrigger,
|
16 |
-
} from "@/components/ui/popover";
|
17 |
-
import { Input } from "@/components/ui/input";
|
18 |
-
import { api } from "@/lib/api";
|
19 |
-
import { LoginModal } from "@/components/login-modal";
|
20 |
-
import { useUser } from "@/hooks/useUser";
|
21 |
-
|
22 |
-
export function DeployButton({
|
23 |
-
html,
|
24 |
-
prompts,
|
25 |
-
}: {
|
26 |
-
html: string;
|
27 |
-
prompts: string[];
|
28 |
-
}) {
|
29 |
-
const router = useRouter();
|
30 |
-
const { user } = useUser();
|
31 |
-
const [loading, setLoading] = useState(false);
|
32 |
-
const [open, setOpen] = useState(false);
|
33 |
-
|
34 |
-
const [config, setConfig] = useState({
|
35 |
-
title: "",
|
36 |
-
});
|
37 |
-
|
38 |
-
const createSpace = async () => {
|
39 |
-
if (!config.title) {
|
40 |
-
toast.error("Please enter a title for your space.");
|
41 |
-
return;
|
42 |
-
}
|
43 |
-
setLoading(true);
|
44 |
-
|
45 |
-
try {
|
46 |
-
const res = await api.post("/me/projects", {
|
47 |
-
title: config.title,
|
48 |
-
html,
|
49 |
-
prompts,
|
50 |
-
});
|
51 |
-
if (res.data.ok) {
|
52 |
-
router.push(`/projects/${res.data.path}?deploy=true`);
|
53 |
-
} else {
|
54 |
-
toast.error(res?.data?.error || "Failed to create space");
|
55 |
-
}
|
56 |
-
} catch (err: any) {
|
57 |
-
toast.error(err.response?.data?.error || err.message);
|
58 |
-
} finally {
|
59 |
-
setLoading(false);
|
60 |
-
}
|
61 |
-
};
|
62 |
-
|
63 |
-
return (
|
64 |
-
<div className="flex items-center justify-end gap-5">
|
65 |
-
<div className="relative flex items-center justify-end">
|
66 |
-
{user?.id ? (
|
67 |
-
<Popover>
|
68 |
-
<PopoverTrigger asChild>
|
69 |
-
<div>
|
70 |
-
<Button variant="default" className="max-lg:hidden !px-4">
|
71 |
-
<MdSave className="size-4" />
|
72 |
-
Save your Project
|
73 |
-
</Button>
|
74 |
-
<Button variant="default" size="sm" className="lg:hidden">
|
75 |
-
Deploy
|
76 |
-
</Button>
|
77 |
-
</div>
|
78 |
-
</PopoverTrigger>
|
79 |
-
<PopoverContent
|
80 |
-
className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
|
81 |
-
align="end"
|
82 |
-
>
|
83 |
-
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
84 |
-
<div className="flex items-center justify-center -space-x-4 mb-3">
|
85 |
-
<div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
86 |
-
🚀
|
87 |
-
</div>
|
88 |
-
<div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
|
89 |
-
<Image
|
90 |
-
src={SpaceIcon}
|
91 |
-
alt="Space Icon"
|
92 |
-
className="size-7"
|
93 |
-
/>
|
94 |
-
</div>
|
95 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
96 |
-
👻
|
97 |
-
</div>
|
98 |
-
</div>
|
99 |
-
<p className="text-xl font-semibold text-neutral-950">
|
100 |
-
Deploy as Space!
|
101 |
-
</p>
|
102 |
-
<p className="text-sm text-neutral-500 mt-1.5">
|
103 |
-
Save and Deploy your project to a Space on the Hub. Spaces are
|
104 |
-
a way to share your project with the world.
|
105 |
-
</p>
|
106 |
-
</header>
|
107 |
-
<main className="space-y-4 p-6">
|
108 |
-
<div>
|
109 |
-
<p className="text-sm text-neutral-700 mb-2">
|
110 |
-
Choose a title for your space:
|
111 |
-
</p>
|
112 |
-
<Input
|
113 |
-
type="text"
|
114 |
-
placeholder="My Awesome Website"
|
115 |
-
value={config.title}
|
116 |
-
onChange={(e) =>
|
117 |
-
setConfig({ ...config, title: e.target.value })
|
118 |
-
}
|
119 |
-
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
120 |
-
/>
|
121 |
-
</div>
|
122 |
-
<div>
|
123 |
-
<p className="text-sm text-neutral-700 mb-2">
|
124 |
-
Then, let's deploy it!
|
125 |
-
</p>
|
126 |
-
<Button
|
127 |
-
variant="black"
|
128 |
-
onClick={createSpace}
|
129 |
-
className="relative w-full"
|
130 |
-
disabled={loading}
|
131 |
-
>
|
132 |
-
Deploy Space <Rocket className="size-4" />
|
133 |
-
{loading && (
|
134 |
-
<Loading className="ml-2 size-4 animate-spin" />
|
135 |
-
)}
|
136 |
-
</Button>
|
137 |
-
</div>
|
138 |
-
</main>
|
139 |
-
</PopoverContent>
|
140 |
-
</Popover>
|
141 |
-
) : (
|
142 |
-
<>
|
143 |
-
<Button
|
144 |
-
variant="default"
|
145 |
-
className="max-lg:hidden !px-4"
|
146 |
-
onClick={() => setOpen(true)}
|
147 |
-
>
|
148 |
-
<MdSave className="size-4" />
|
149 |
-
Save your Project
|
150 |
-
</Button>
|
151 |
-
<Button
|
152 |
-
variant="default"
|
153 |
-
size="sm"
|
154 |
-
className="lg:hidden"
|
155 |
-
onClick={() => setOpen(true)}
|
156 |
-
>
|
157 |
-
Save
|
158 |
-
</Button>
|
159 |
-
</>
|
160 |
-
)}
|
161 |
-
<LoginModal
|
162 |
-
open={open}
|
163 |
-
onClose={() => setOpen(false)}
|
164 |
-
html={html}
|
165 |
-
title="Log In to save your Project"
|
166 |
-
description="Log In through your Hugging Face account to save your project and increase your monthly free limit."
|
167 |
-
/>
|
168 |
-
</div>
|
169 |
-
</div>
|
170 |
-
);
|
171 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/footer/index.tsx
DELETED
@@ -1,118 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { FaMobileAlt } from "react-icons/fa";
|
3 |
-
import { RefreshCcw, SparkleIcon } from "lucide-react";
|
4 |
-
import { FaLaptopCode } from "react-icons/fa6";
|
5 |
-
import { HtmlHistory } from "@/types";
|
6 |
-
import { Button } from "@/components/ui/button";
|
7 |
-
import { MdAdd } from "react-icons/md";
|
8 |
-
import { History } from "@/components/editor/history";
|
9 |
-
import { UserMenu } from "@/components/user-menu";
|
10 |
-
import { useUser } from "@/hooks/useUser";
|
11 |
-
|
12 |
-
const DEVICES = [
|
13 |
-
{
|
14 |
-
name: "desktop",
|
15 |
-
icon: FaLaptopCode,
|
16 |
-
},
|
17 |
-
{
|
18 |
-
name: "mobile",
|
19 |
-
icon: FaMobileAlt,
|
20 |
-
},
|
21 |
-
];
|
22 |
-
|
23 |
-
export function Footer({
|
24 |
-
onReset,
|
25 |
-
htmlHistory,
|
26 |
-
setHtml,
|
27 |
-
device,
|
28 |
-
setDevice,
|
29 |
-
iframeRef,
|
30 |
-
}: {
|
31 |
-
onReset: () => void;
|
32 |
-
htmlHistory?: HtmlHistory[];
|
33 |
-
device: "desktop" | "mobile";
|
34 |
-
setHtml: (html: string) => void;
|
35 |
-
iframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
36 |
-
setDevice: React.Dispatch<React.SetStateAction<"desktop" | "mobile">>;
|
37 |
-
}) {
|
38 |
-
const { user } = useUser();
|
39 |
-
|
40 |
-
const handleRefreshIframe = () => {
|
41 |
-
if (iframeRef?.current) {
|
42 |
-
const iframe = iframeRef.current;
|
43 |
-
const content = iframe.srcdoc;
|
44 |
-
iframe.srcdoc = "";
|
45 |
-
setTimeout(() => {
|
46 |
-
iframe.srcdoc = content;
|
47 |
-
}, 10);
|
48 |
-
}
|
49 |
-
};
|
50 |
-
|
51 |
-
return (
|
52 |
-
<footer className="border-t bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 py-2 flex items-center justify-between sticky bottom-0 z-20">
|
53 |
-
<div className="flex items-center gap-2">
|
54 |
-
{user &&
|
55 |
-
(user?.isLocalUse ? (
|
56 |
-
<>
|
57 |
-
<div className="max-w-max bg-amber-500/10 rounded-full px-3 py-1 text-amber-500 border border-amber-500/20 text-sm font-semibold">
|
58 |
-
Local Usage
|
59 |
-
</div>
|
60 |
-
</>
|
61 |
-
) : (
|
62 |
-
<UserMenu className="!p-1 !pr-3 !h-auto" />
|
63 |
-
))}
|
64 |
-
{user && <p className="text-neutral-700">|</p>}
|
65 |
-
<Button size="sm" variant="secondary" onClick={onReset}>
|
66 |
-
<MdAdd className="text-sm" />
|
67 |
-
New <span className="max-lg:hidden">Project</span>
|
68 |
-
</Button>
|
69 |
-
{htmlHistory && htmlHistory.length > 0 && (
|
70 |
-
<>
|
71 |
-
<p className="text-neutral-700">|</p>
|
72 |
-
<History history={htmlHistory} setHtml={setHtml} />
|
73 |
-
</>
|
74 |
-
)}
|
75 |
-
</div>
|
76 |
-
<div className="flex justify-end items-center gap-2.5">
|
77 |
-
<a
|
78 |
-
href="https://huggingface.co/spaces/victor/deepsite-gallery"
|
79 |
-
target="_blank"
|
80 |
-
>
|
81 |
-
<Button size="sm" variant="ghost">
|
82 |
-
<SparkleIcon className="size-3.5" />
|
83 |
-
<span className="max-lg:hidden">DeepSite Gallery</span>
|
84 |
-
</Button>
|
85 |
-
</a>
|
86 |
-
<Button size="sm" variant="outline" onClick={handleRefreshIframe}>
|
87 |
-
<RefreshCcw className="size-3.5" />
|
88 |
-
<span className="max-lg:hidden">Refresh Preview</span>
|
89 |
-
</Button>
|
90 |
-
<div className="flex items-center rounded-full p-0.5 bg-neutral-700/70 relative overflow-hidden z-0 max-lg:hidden gap-0.5">
|
91 |
-
<div
|
92 |
-
className={classNames(
|
93 |
-
"absolute left-0.5 top-0.5 rounded-full bg-white size-7 -z-[1] transition-all duration-200",
|
94 |
-
{
|
95 |
-
"translate-x-[calc(100%+2px)]": device === "mobile",
|
96 |
-
}
|
97 |
-
)}
|
98 |
-
/>
|
99 |
-
{DEVICES.map((deviceItem) => (
|
100 |
-
<button
|
101 |
-
key={deviceItem.name}
|
102 |
-
className={classNames(
|
103 |
-
"rounded-full text-neutral-300 size-7 flex items-center justify-center cursor-pointer",
|
104 |
-
{
|
105 |
-
"!text-black": device === deviceItem.name,
|
106 |
-
"hover:bg-neutral-800": device !== deviceItem.name,
|
107 |
-
}
|
108 |
-
)}
|
109 |
-
onClick={() => setDevice(deviceItem.name as "desktop" | "mobile")}
|
110 |
-
>
|
111 |
-
<deviceItem.icon className="text-sm" />
|
112 |
-
</button>
|
113 |
-
))}
|
114 |
-
</div>
|
115 |
-
</div>
|
116 |
-
</footer>
|
117 |
-
);
|
118 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/header/index.tsx
DELETED
@@ -1,69 +0,0 @@
|
|
1 |
-
import { ReactNode } from "react";
|
2 |
-
import { Eye, MessageCircleCode } from "lucide-react";
|
3 |
-
|
4 |
-
import Logo from "@/assets/logo.svg";
|
5 |
-
|
6 |
-
import { Button } from "@/components/ui/button";
|
7 |
-
import classNames from "classnames";
|
8 |
-
import Image from "next/image";
|
9 |
-
|
10 |
-
const TABS = [
|
11 |
-
{
|
12 |
-
value: "chat",
|
13 |
-
label: "Chat",
|
14 |
-
icon: MessageCircleCode,
|
15 |
-
},
|
16 |
-
{
|
17 |
-
value: "preview",
|
18 |
-
label: "Preview",
|
19 |
-
icon: Eye,
|
20 |
-
},
|
21 |
-
];
|
22 |
-
|
23 |
-
export function Header({
|
24 |
-
tab,
|
25 |
-
onNewTab,
|
26 |
-
children,
|
27 |
-
}: {
|
28 |
-
tab: string;
|
29 |
-
onNewTab: (tab: string) => void;
|
30 |
-
children?: ReactNode;
|
31 |
-
}) {
|
32 |
-
return (
|
33 |
-
<header className="border-b bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 lg:px-6 py-2 flex items-center max-lg:gap-3 justify-between lg:grid lg:grid-cols-3 z-20">
|
34 |
-
<div className="flex items-center justify-start gap-3">
|
35 |
-
<h1 className="text-neutral-900 dark:text-white text-lg lg:text-xl font-bold flex items-center justify-start">
|
36 |
-
<Image
|
37 |
-
src={Logo}
|
38 |
-
alt="DeepSite Logo"
|
39 |
-
className="size-6 lg:size-8 mr-2 invert-100 dark:invert-0"
|
40 |
-
/>
|
41 |
-
<p className="max-md:hidden flex items-center justify-start">
|
42 |
-
DeepSite
|
43 |
-
<span className="font-mono bg-gradient-to-br from-sky-500 to-emerald-500 text-neutral-950 rounded-full text-xs ml-2 px-1.5 py-0.5">
|
44 |
-
{" "}
|
45 |
-
v2
|
46 |
-
</span>
|
47 |
-
</p>
|
48 |
-
</h1>
|
49 |
-
</div>
|
50 |
-
<div className="flex items-center justify-start lg:justify-center gap-1 max-lg:pl-3 flex-1 max-lg:border-l max-lg:border-l-neutral-800">
|
51 |
-
{TABS.map((item) => (
|
52 |
-
<Button
|
53 |
-
key={item.value}
|
54 |
-
variant={tab === item.value ? "secondary" : "ghost"}
|
55 |
-
className={classNames("", {
|
56 |
-
"opacity-60": tab !== item.value,
|
57 |
-
})}
|
58 |
-
size="sm"
|
59 |
-
onClick={() => onNewTab(item.value)}
|
60 |
-
>
|
61 |
-
<item.icon className="size-4" />
|
62 |
-
<span className="hidden md:inline">{item.label}</span>
|
63 |
-
</Button>
|
64 |
-
))}
|
65 |
-
</div>
|
66 |
-
<div className="flex items-center justify-end gap-3">{children}</div>
|
67 |
-
</header>
|
68 |
-
);
|
69 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/history/index.tsx
DELETED
@@ -1,72 +0,0 @@
|
|
1 |
-
import { History as HistoryIcon } from "lucide-react";
|
2 |
-
import { HtmlHistory } from "@/types";
|
3 |
-
import {
|
4 |
-
Popover,
|
5 |
-
PopoverContent,
|
6 |
-
PopoverTrigger,
|
7 |
-
} from "@/components/ui/popover";
|
8 |
-
import { Button } from "@/components/ui/button";
|
9 |
-
|
10 |
-
export function History({
|
11 |
-
history,
|
12 |
-
setHtml,
|
13 |
-
}: {
|
14 |
-
history: HtmlHistory[];
|
15 |
-
setHtml: (html: string) => void;
|
16 |
-
}) {
|
17 |
-
return (
|
18 |
-
<Popover>
|
19 |
-
<PopoverTrigger asChild>
|
20 |
-
<Button variant="ghost" size="sm" className="max-lg:hidden">
|
21 |
-
<HistoryIcon className="size-4 text-neutral-300" />
|
22 |
-
{history?.length} edit{history.length !== 1 ? "s" : ""}
|
23 |
-
</Button>
|
24 |
-
</PopoverTrigger>
|
25 |
-
<PopoverContent
|
26 |
-
className="!rounded-2xl !p-0 overflow-hidden !bg-neutral-900"
|
27 |
-
align="start"
|
28 |
-
>
|
29 |
-
<header className="text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
|
30 |
-
History
|
31 |
-
</header>
|
32 |
-
<main className="px-4 space-y-3">
|
33 |
-
<ul className="max-h-[250px] overflow-y-auto">
|
34 |
-
{history?.map((item, index) => (
|
35 |
-
<li
|
36 |
-
key={index}
|
37 |
-
className="text-gray-300 text-xs py-2 border-b border-gray-800 last:border-0 flex items-center justify-between gap-2"
|
38 |
-
>
|
39 |
-
<div className="">
|
40 |
-
<span className="line-clamp-1">{item.prompt}</span>
|
41 |
-
<span className="text-gray-500 text-[10px]">
|
42 |
-
{new Date(item.createdAt).toLocaleDateString("en-US", {
|
43 |
-
month: "2-digit",
|
44 |
-
day: "2-digit",
|
45 |
-
year: "2-digit",
|
46 |
-
}) +
|
47 |
-
" " +
|
48 |
-
new Date(item.createdAt).toLocaleTimeString("en-US", {
|
49 |
-
hour: "2-digit",
|
50 |
-
minute: "2-digit",
|
51 |
-
second: "2-digit",
|
52 |
-
hour12: false,
|
53 |
-
})}
|
54 |
-
</span>
|
55 |
-
</div>
|
56 |
-
<Button
|
57 |
-
variant="sky"
|
58 |
-
size="xs"
|
59 |
-
onClick={() => {
|
60 |
-
setHtml(item.html);
|
61 |
-
}}
|
62 |
-
>
|
63 |
-
Select
|
64 |
-
</Button>
|
65 |
-
</li>
|
66 |
-
))}
|
67 |
-
</ul>
|
68 |
-
</main>
|
69 |
-
</PopoverContent>
|
70 |
-
</Popover>
|
71 |
-
);
|
72 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/preview/index.tsx
DELETED
@@ -1,172 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
import { useUpdateEffect } from "react-use";
|
3 |
-
import { useMemo, useState } from "react";
|
4 |
-
import classNames from "classnames";
|
5 |
-
import { toast } from "sonner";
|
6 |
-
|
7 |
-
import { cn } from "@/lib/utils";
|
8 |
-
import { GridPattern } from "@/components/magic-ui/grid-pattern";
|
9 |
-
import { htmlTagToText } from "@/lib/html-tag-to-text";
|
10 |
-
|
11 |
-
export const Preview = ({
|
12 |
-
html,
|
13 |
-
isResizing,
|
14 |
-
isAiWorking,
|
15 |
-
ref,
|
16 |
-
device,
|
17 |
-
currentTab,
|
18 |
-
iframeRef,
|
19 |
-
isEditableModeEnabled,
|
20 |
-
onClickElement,
|
21 |
-
}: {
|
22 |
-
html: string;
|
23 |
-
isResizing: boolean;
|
24 |
-
isAiWorking: boolean;
|
25 |
-
ref: React.RefObject<HTMLDivElement | null>;
|
26 |
-
iframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
27 |
-
device: "desktop" | "mobile";
|
28 |
-
currentTab: string;
|
29 |
-
isEditableModeEnabled?: boolean;
|
30 |
-
onClickElement?: (element: HTMLElement) => void;
|
31 |
-
}) => {
|
32 |
-
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
|
33 |
-
null
|
34 |
-
);
|
35 |
-
|
36 |
-
// add event listener to the iframe to track hovered elements
|
37 |
-
const handleMouseOver = (event: MouseEvent) => {
|
38 |
-
if (iframeRef?.current) {
|
39 |
-
const iframeDocument = iframeRef.current.contentDocument;
|
40 |
-
if (iframeDocument) {
|
41 |
-
const targetElement = event.target as HTMLElement;
|
42 |
-
if (
|
43 |
-
hoveredElement !== targetElement &&
|
44 |
-
targetElement !== iframeDocument.body
|
45 |
-
) {
|
46 |
-
setHoveredElement(targetElement);
|
47 |
-
targetElement.classList.add("hovered-element");
|
48 |
-
} else {
|
49 |
-
return setHoveredElement(null);
|
50 |
-
}
|
51 |
-
}
|
52 |
-
}
|
53 |
-
};
|
54 |
-
const handleMouseOut = () => {
|
55 |
-
setHoveredElement(null);
|
56 |
-
};
|
57 |
-
const handleClick = (event: MouseEvent) => {
|
58 |
-
if (iframeRef?.current) {
|
59 |
-
const iframeDocument = iframeRef.current.contentDocument;
|
60 |
-
if (iframeDocument) {
|
61 |
-
const targetElement = event.target as HTMLElement;
|
62 |
-
if (targetElement !== iframeDocument.body) {
|
63 |
-
onClickElement?.(targetElement);
|
64 |
-
}
|
65 |
-
}
|
66 |
-
}
|
67 |
-
};
|
68 |
-
|
69 |
-
useUpdateEffect(() => {
|
70 |
-
const cleanupListeners = () => {
|
71 |
-
if (iframeRef?.current?.contentDocument) {
|
72 |
-
const iframeDocument = iframeRef.current.contentDocument;
|
73 |
-
iframeDocument.removeEventListener("mouseover", handleMouseOver);
|
74 |
-
iframeDocument.removeEventListener("mouseout", handleMouseOut);
|
75 |
-
iframeDocument.removeEventListener("click", handleClick);
|
76 |
-
}
|
77 |
-
};
|
78 |
-
|
79 |
-
if (iframeRef?.current) {
|
80 |
-
const iframeDocument = iframeRef.current.contentDocument;
|
81 |
-
if (iframeDocument) {
|
82 |
-
// Clean up existing listeners first
|
83 |
-
cleanupListeners();
|
84 |
-
|
85 |
-
if (isEditableModeEnabled) {
|
86 |
-
iframeDocument.addEventListener("mouseover", handleMouseOver);
|
87 |
-
iframeDocument.addEventListener("mouseout", handleMouseOut);
|
88 |
-
iframeDocument.addEventListener("click", handleClick);
|
89 |
-
}
|
90 |
-
}
|
91 |
-
}
|
92 |
-
|
93 |
-
// Clean up when component unmounts or dependencies change
|
94 |
-
return cleanupListeners;
|
95 |
-
}, [iframeRef, isEditableModeEnabled]);
|
96 |
-
|
97 |
-
const selectedElement = useMemo(() => {
|
98 |
-
if (!isEditableModeEnabled) return null;
|
99 |
-
if (!hoveredElement) return null;
|
100 |
-
return hoveredElement;
|
101 |
-
}, [hoveredElement, isEditableModeEnabled]);
|
102 |
-
|
103 |
-
return (
|
104 |
-
<div
|
105 |
-
ref={ref}
|
106 |
-
className={classNames(
|
107 |
-
"w-full border-l border-gray-900 h-full relative z-0 flex items-center justify-center",
|
108 |
-
{
|
109 |
-
"lg:p-4": currentTab !== "preview",
|
110 |
-
"max-lg:h-0": currentTab === "chat",
|
111 |
-
"max-lg:h-full": currentTab === "preview",
|
112 |
-
}
|
113 |
-
)}
|
114 |
-
onClick={(e) => {
|
115 |
-
if (isAiWorking) {
|
116 |
-
e.preventDefault();
|
117 |
-
e.stopPropagation();
|
118 |
-
toast.warning("Please wait for the AI to finish working.");
|
119 |
-
}
|
120 |
-
}}
|
121 |
-
>
|
122 |
-
<GridPattern
|
123 |
-
x={-1}
|
124 |
-
y={-1}
|
125 |
-
strokeDasharray={"4 2"}
|
126 |
-
className={cn(
|
127 |
-
"[mask-image:radial-gradient(900px_circle_at_center,white,transparent)]"
|
128 |
-
)}
|
129 |
-
/>
|
130 |
-
{!isAiWorking && hoveredElement && selectedElement && (
|
131 |
-
<div
|
132 |
-
className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"
|
133 |
-
style={{
|
134 |
-
top: selectedElement.getBoundingClientRect().top + 24,
|
135 |
-
left: selectedElement.getBoundingClientRect().left + 24,
|
136 |
-
width: selectedElement.getBoundingClientRect().width,
|
137 |
-
height: selectedElement.getBoundingClientRect().height,
|
138 |
-
}}
|
139 |
-
>
|
140 |
-
<span className="bg-sky-500 rounded-t-md text-sm text-neutral-100 px-2 py-0.5 -translate-y-7 absolute top-0 left-0">
|
141 |
-
{htmlTagToText(selectedElement.tagName.toLowerCase())}
|
142 |
-
</span>
|
143 |
-
</div>
|
144 |
-
)}
|
145 |
-
<iframe
|
146 |
-
id="preview-iframe"
|
147 |
-
ref={iframeRef}
|
148 |
-
title="output"
|
149 |
-
className={classNames(
|
150 |
-
"w-full select-none transition-all duration-200 bg-black h-full",
|
151 |
-
{
|
152 |
-
"pointer-events-none": isResizing || isAiWorking,
|
153 |
-
"lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":
|
154 |
-
device === "mobile",
|
155 |
-
"lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
|
156 |
-
currentTab !== "preview" && device === "desktop",
|
157 |
-
}
|
158 |
-
)}
|
159 |
-
srcDoc={html}
|
160 |
-
onLoad={() => {
|
161 |
-
if (iframeRef?.current?.contentWindow?.document?.body) {
|
162 |
-
iframeRef.current.contentWindow.document.body.scrollIntoView({
|
163 |
-
block: isAiWorking ? "end" : "start",
|
164 |
-
inline: "nearest",
|
165 |
-
behavior: isAiWorking ? "instant" : "smooth",
|
166 |
-
});
|
167 |
-
}
|
168 |
-
}}
|
169 |
-
/>
|
170 |
-
</div>
|
171 |
-
);
|
172 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/save-button/index.tsx
DELETED
@@ -1,76 +0,0 @@
|
|
1 |
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
-
import { useState } from "react";
|
3 |
-
import { toast } from "sonner";
|
4 |
-
import { MdSave } from "react-icons/md";
|
5 |
-
import { useParams } from "next/navigation";
|
6 |
-
|
7 |
-
import Loading from "@/components/loading";
|
8 |
-
import { Button } from "@/components/ui/button";
|
9 |
-
import { api } from "@/lib/api";
|
10 |
-
|
11 |
-
export function SaveButton({
|
12 |
-
html,
|
13 |
-
prompts,
|
14 |
-
}: {
|
15 |
-
html: string;
|
16 |
-
prompts: string[];
|
17 |
-
}) {
|
18 |
-
// get params from URL
|
19 |
-
const { namespace, repoId } = useParams<{
|
20 |
-
namespace: string;
|
21 |
-
repoId: string;
|
22 |
-
}>();
|
23 |
-
const [loading, setLoading] = useState(false);
|
24 |
-
|
25 |
-
const updateSpace = async () => {
|
26 |
-
setLoading(true);
|
27 |
-
|
28 |
-
try {
|
29 |
-
const res = await api.put(`/me/projects/${namespace}/${repoId}`, {
|
30 |
-
html,
|
31 |
-
prompts,
|
32 |
-
});
|
33 |
-
if (res.data.ok) {
|
34 |
-
toast.success("Your space is updated! 🎉", {
|
35 |
-
action: {
|
36 |
-
label: "See Space",
|
37 |
-
onClick: () => {
|
38 |
-
window.open(
|
39 |
-
`https://huggingface.co/spaces/${namespace}/${repoId}`,
|
40 |
-
"_blank"
|
41 |
-
);
|
42 |
-
},
|
43 |
-
},
|
44 |
-
});
|
45 |
-
} else {
|
46 |
-
toast.error(res?.data?.error || "Failed to update space");
|
47 |
-
}
|
48 |
-
} catch (err: any) {
|
49 |
-
toast.error(err.response?.data?.error || err.message);
|
50 |
-
} finally {
|
51 |
-
setLoading(false);
|
52 |
-
}
|
53 |
-
};
|
54 |
-
|
55 |
-
return (
|
56 |
-
<>
|
57 |
-
<Button
|
58 |
-
variant="default"
|
59 |
-
className="max-lg:hidden !px-4"
|
60 |
-
onClick={updateSpace}
|
61 |
-
>
|
62 |
-
<MdSave className="size-4" />
|
63 |
-
Save your Project{" "}
|
64 |
-
{loading && <Loading className="ml-2 size-4 animate-spin" />}
|
65 |
-
</Button>
|
66 |
-
<Button
|
67 |
-
variant="default"
|
68 |
-
size="sm"
|
69 |
-
className="lg:hidden"
|
70 |
-
onClick={updateSpace}
|
71 |
-
>
|
72 |
-
Save {loading && <Loading className="ml-2 size-4 animate-spin" />}
|
73 |
-
</Button>
|
74 |
-
</>
|
75 |
-
);
|
76 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/invite-friends/index.tsx
DELETED
@@ -1,85 +0,0 @@
|
|
1 |
-
import { TiUserAdd } from "react-icons/ti";
|
2 |
-
import { Link } from "lucide-react";
|
3 |
-
import { FaXTwitter } from "react-icons/fa6";
|
4 |
-
import { useCopyToClipboard } from "react-use";
|
5 |
-
import { toast } from "sonner";
|
6 |
-
|
7 |
-
import { Button } from "@/components/ui/button";
|
8 |
-
import {
|
9 |
-
Dialog,
|
10 |
-
DialogContent,
|
11 |
-
DialogTitle,
|
12 |
-
DialogTrigger,
|
13 |
-
} from "@/components/ui/dialog";
|
14 |
-
|
15 |
-
export function InviteFriends() {
|
16 |
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
17 |
-
const [_, copyToClipboard] = useCopyToClipboard();
|
18 |
-
|
19 |
-
return (
|
20 |
-
<Dialog>
|
21 |
-
<form>
|
22 |
-
<DialogTrigger asChild>
|
23 |
-
<Button
|
24 |
-
size="iconXs"
|
25 |
-
variant="outline"
|
26 |
-
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
27 |
-
>
|
28 |
-
<TiUserAdd className="size-4" />
|
29 |
-
</Button>
|
30 |
-
</DialogTrigger>
|
31 |
-
<DialogContent className="sm:max-w-lg lg:!p-8 !rounded-3xl !bg-white !border-neutral-100">
|
32 |
-
<DialogTitle className="hidden" />
|
33 |
-
<main>
|
34 |
-
<div className="flex items-center justify-start -space-x-4 mb-5">
|
35 |
-
<div className="size-11 rounded-full bg-pink-300 shadow-2xs flex items-center justify-center text-2xl">
|
36 |
-
😎
|
37 |
-
</div>
|
38 |
-
<div className="size-11 rounded-full bg-amber-300 shadow-2xs flex items-center justify-center text-2xl z-2">
|
39 |
-
😇
|
40 |
-
</div>
|
41 |
-
<div className="size-11 rounded-full bg-sky-300 shadow-2xs flex items-center justify-center text-2xl">
|
42 |
-
😜
|
43 |
-
</div>
|
44 |
-
</div>
|
45 |
-
<p className="text-xl font-semibold text-neutral-950 max-w-[200px]">
|
46 |
-
Invite your friends to join us!
|
47 |
-
</p>
|
48 |
-
<p className="text-sm text-neutral-500 mt-2 max-w-sm">
|
49 |
-
Support us and share the love and let them know about our awesome
|
50 |
-
platform.
|
51 |
-
</p>
|
52 |
-
<div className="mt-4 space-x-3.5">
|
53 |
-
<a
|
54 |
-
href="https://x.com/intent/post?url=https://enzostvs-deepsite.hf.space/&text=Checkout%20this%20awesome%20Ai%20Tool!%20Vibe%20coding%20has%20never%20been%20so%20easy✨"
|
55 |
-
target="_blank"
|
56 |
-
rel="noopener noreferrer"
|
57 |
-
>
|
58 |
-
<Button
|
59 |
-
variant="lightGray"
|
60 |
-
size="sm"
|
61 |
-
className="!text-neutral-700"
|
62 |
-
>
|
63 |
-
<FaXTwitter className="size-4" />
|
64 |
-
Share on
|
65 |
-
</Button>
|
66 |
-
</a>
|
67 |
-
<Button
|
68 |
-
variant="lightGray"
|
69 |
-
size="sm"
|
70 |
-
className="!text-neutral-700"
|
71 |
-
onClick={() => {
|
72 |
-
copyToClipboard("https://enzostvs-deepsite.hf.space/");
|
73 |
-
toast.success("Invite link copied to clipboard!");
|
74 |
-
}}
|
75 |
-
>
|
76 |
-
<Link className="size-4" />
|
77 |
-
Copy Invite Link
|
78 |
-
</Button>
|
79 |
-
</div>
|
80 |
-
</main>
|
81 |
-
</DialogContent>
|
82 |
-
</form>
|
83 |
-
</Dialog>
|
84 |
-
);
|
85 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/login-modal/index.tsx
DELETED
@@ -1,61 +0,0 @@
|
|
1 |
-
import { useLocalStorage } from "react-use";
|
2 |
-
import { Button } from "@/components/ui/button";
|
3 |
-
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
4 |
-
import { useUser } from "@/hooks/useUser";
|
5 |
-
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
6 |
-
|
7 |
-
export const LoginModal = ({
|
8 |
-
open,
|
9 |
-
html,
|
10 |
-
onClose,
|
11 |
-
title = "Log In to use DeepSite for free",
|
12 |
-
description = "Log In through your Hugging Face account to continue using DeepSite and increase your monthly free limit.",
|
13 |
-
}: {
|
14 |
-
open: boolean;
|
15 |
-
html?: string;
|
16 |
-
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
17 |
-
title?: string;
|
18 |
-
description?: string;
|
19 |
-
}) => {
|
20 |
-
const { openLoginWindow } = useUser();
|
21 |
-
const [, setStorage] = useLocalStorage("html_content");
|
22 |
-
const handleClick = async () => {
|
23 |
-
if (html && !isTheSameHtml(html)) {
|
24 |
-
setStorage(html);
|
25 |
-
}
|
26 |
-
openLoginWindow();
|
27 |
-
onClose(false);
|
28 |
-
};
|
29 |
-
return (
|
30 |
-
<Dialog open={open} onOpenChange={onClose}>
|
31 |
-
<DialogContent className="sm:max-w-lg lg:!p-8 !rounded-3xl !bg-white !border-neutral-100">
|
32 |
-
<DialogTitle className="hidden" />
|
33 |
-
<main className="flex flex-col items-start text-left relative pt-2">
|
34 |
-
<div className="flex items-center justify-start -space-x-4 mb-5">
|
35 |
-
<div className="size-14 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-3xl opacity-50">
|
36 |
-
💪
|
37 |
-
</div>
|
38 |
-
<div className="size-16 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-4xl z-2">
|
39 |
-
😎
|
40 |
-
</div>
|
41 |
-
<div className="size-14 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-3xl opacity-50">
|
42 |
-
🙌
|
43 |
-
</div>
|
44 |
-
</div>
|
45 |
-
<p className="text-2xl font-bold text-neutral-950">{title}</p>
|
46 |
-
<p className="text-neutral-500 text-base mt-2 max-w-sm">
|
47 |
-
{description}
|
48 |
-
</p>
|
49 |
-
<Button
|
50 |
-
variant="black"
|
51 |
-
size="lg"
|
52 |
-
className="w-full !text-base !h-11 mt-8"
|
53 |
-
onClick={handleClick}
|
54 |
-
>
|
55 |
-
Log In to Continue
|
56 |
-
</Button>
|
57 |
-
</main>
|
58 |
-
</DialogContent>
|
59 |
-
</Dialog>
|
60 |
-
);
|
61 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/magic-ui/grid-pattern.tsx
DELETED
@@ -1,69 +0,0 @@
|
|
1 |
-
import { useId } from "react";
|
2 |
-
import { cn } from "@/lib/utils";
|
3 |
-
|
4 |
-
interface GridPatternProps extends React.SVGProps<SVGSVGElement> {
|
5 |
-
width?: number;
|
6 |
-
height?: number;
|
7 |
-
x?: number;
|
8 |
-
y?: number;
|
9 |
-
squares?: Array<[x: number, y: number]>;
|
10 |
-
strokeDasharray?: string;
|
11 |
-
className?: string;
|
12 |
-
[key: string]: unknown;
|
13 |
-
}
|
14 |
-
|
15 |
-
export function GridPattern({
|
16 |
-
width = 40,
|
17 |
-
height = 40,
|
18 |
-
x = -1,
|
19 |
-
y = -1,
|
20 |
-
strokeDasharray = "0",
|
21 |
-
squares,
|
22 |
-
className,
|
23 |
-
...props
|
24 |
-
}: GridPatternProps) {
|
25 |
-
const id = useId();
|
26 |
-
|
27 |
-
return (
|
28 |
-
<svg
|
29 |
-
aria-hidden="true"
|
30 |
-
className={cn(
|
31 |
-
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-neutral-700 -z-[1]",
|
32 |
-
className
|
33 |
-
)}
|
34 |
-
{...props}
|
35 |
-
>
|
36 |
-
<defs>
|
37 |
-
<pattern
|
38 |
-
id={id}
|
39 |
-
width={width}
|
40 |
-
height={height}
|
41 |
-
patternUnits="userSpaceOnUse"
|
42 |
-
x={x}
|
43 |
-
y={y}
|
44 |
-
>
|
45 |
-
<path
|
46 |
-
d={`M.5 ${height}V.5H${width}`}
|
47 |
-
fill="none"
|
48 |
-
strokeDasharray={strokeDasharray}
|
49 |
-
/>
|
50 |
-
</pattern>
|
51 |
-
</defs>
|
52 |
-
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
53 |
-
{squares && (
|
54 |
-
<svg x={x} y={y} className="overflow-visible">
|
55 |
-
{squares.map(([x, y]) => (
|
56 |
-
<rect
|
57 |
-
strokeWidth="0"
|
58 |
-
key={`${x}-${y}`}
|
59 |
-
width={width - 1}
|
60 |
-
height={height - 1}
|
61 |
-
x={x * width + 1}
|
62 |
-
y={y * height + 1}
|
63 |
-
/>
|
64 |
-
))}
|
65 |
-
</svg>
|
66 |
-
)}
|
67 |
-
</svg>
|
68 |
-
);
|
69 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/my-projects/index.tsx
DELETED
@@ -1,57 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
import { Plus } from "lucide-react";
|
3 |
-
import Link from "next/link";
|
4 |
-
import { useState } from "react";
|
5 |
-
|
6 |
-
import { useUser } from "@/hooks/useUser";
|
7 |
-
import { Project } from "@/types";
|
8 |
-
import { redirect } from "next/navigation";
|
9 |
-
import { ProjectCard } from "./project-card";
|
10 |
-
import { LoadProject } from "./load-project";
|
11 |
-
|
12 |
-
export function MyProjects({
|
13 |
-
projects: initialProjects,
|
14 |
-
}: {
|
15 |
-
projects: Project[];
|
16 |
-
}) {
|
17 |
-
const { user } = useUser();
|
18 |
-
if (!user) {
|
19 |
-
redirect("/");
|
20 |
-
}
|
21 |
-
const [projects, setProjects] = useState<Project[]>(initialProjects || []);
|
22 |
-
return (
|
23 |
-
<>
|
24 |
-
<section className="max-w-[86rem] py-12 px-4 mx-auto">
|
25 |
-
<header className="flex items-center justify-between max-lg:flex-col gap-4">
|
26 |
-
<div className="text-left">
|
27 |
-
<h1 className="text-3xl font-bold text-white">
|
28 |
-
<span className="capitalize">{user.fullname}</span>'s
|
29 |
-
DeepSite Projects
|
30 |
-
</h1>
|
31 |
-
<p className="text-muted-foreground text-base mt-1 max-w-xl">
|
32 |
-
Create, manage, and explore your DeepSite projects.
|
33 |
-
</p>
|
34 |
-
</div>
|
35 |
-
<LoadProject
|
36 |
-
fullXsBtn
|
37 |
-
onSuccess={(project: Project) => {
|
38 |
-
setProjects((prev) => [...prev, project]);
|
39 |
-
}}
|
40 |
-
/>
|
41 |
-
</header>
|
42 |
-
<div className="mt-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
43 |
-
<Link
|
44 |
-
href="/projects/new"
|
45 |
-
className="bg-neutral-900 rounded-xl h-44 flex items-center justify-center text-neutral-300 border border-neutral-800 hover:brightness-110 transition-all duration-200"
|
46 |
-
>
|
47 |
-
<Plus className="size-5 mr-1.5" />
|
48 |
-
Create Project
|
49 |
-
</Link>
|
50 |
-
{projects.map((project: Project) => (
|
51 |
-
<ProjectCard key={project._id} project={project} />
|
52 |
-
))}
|
53 |
-
</div>
|
54 |
-
</section>
|
55 |
-
</>
|
56 |
-
);
|
57 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/my-projects/load-project.tsx
DELETED
@@ -1,196 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
import { useState } from "react";
|
3 |
-
import { Import } from "lucide-react";
|
4 |
-
|
5 |
-
import { Project } from "@/types";
|
6 |
-
import { Button } from "@/components/ui/button";
|
7 |
-
import {
|
8 |
-
Dialog,
|
9 |
-
DialogContent,
|
10 |
-
DialogTitle,
|
11 |
-
DialogTrigger,
|
12 |
-
} from "@/components/ui/dialog";
|
13 |
-
import Loading from "@/components/loading";
|
14 |
-
import { Input } from "../ui/input";
|
15 |
-
import { toast } from "sonner";
|
16 |
-
import { api } from "@/lib/api";
|
17 |
-
import { useUser } from "@/hooks/useUser";
|
18 |
-
import { LoginModal } from "../login-modal";
|
19 |
-
|
20 |
-
export const LoadProject = ({
|
21 |
-
fullXsBtn = false,
|
22 |
-
onSuccess,
|
23 |
-
}: {
|
24 |
-
fullXsBtn?: boolean;
|
25 |
-
onSuccess: (project: Project) => void;
|
26 |
-
}) => {
|
27 |
-
const { user } = useUser();
|
28 |
-
|
29 |
-
const [openLoginModal, setOpenLoginModal] = useState(false);
|
30 |
-
const [open, setOpen] = useState(false);
|
31 |
-
const [url, setUrl] = useState<string>("");
|
32 |
-
const [isLoading, setIsLoading] = useState(false);
|
33 |
-
|
34 |
-
const checkIfUrlIsValid = (url: string) => {
|
35 |
-
// should match a hugging face spaces URL like: https://huggingface.co/spaces/username/project or https://hf.co/spaces/username/project
|
36 |
-
const urlPattern = new RegExp(
|
37 |
-
/^(https?:\/\/)?(huggingface\.co|hf\.co)\/spaces\/([\w-]+)\/([\w-]+)$/,
|
38 |
-
"i"
|
39 |
-
);
|
40 |
-
return urlPattern.test(url);
|
41 |
-
};
|
42 |
-
|
43 |
-
const handleClick = async () => {
|
44 |
-
if (isLoading) return; // Prevent multiple clicks while loading
|
45 |
-
if (!url) {
|
46 |
-
toast.error("Please enter a URL.");
|
47 |
-
return;
|
48 |
-
}
|
49 |
-
if (!checkIfUrlIsValid(url)) {
|
50 |
-
toast.error("Please enter a valid Hugging Face Spaces URL.");
|
51 |
-
return;
|
52 |
-
}
|
53 |
-
|
54 |
-
const [username, namespace] = url
|
55 |
-
.replace("https://huggingface.co/spaces/", "")
|
56 |
-
.replace("https://hf.co/spaces/", "")
|
57 |
-
.split("/");
|
58 |
-
|
59 |
-
setIsLoading(true);
|
60 |
-
try {
|
61 |
-
const response = await api.post(`/me/projects/${username}/${namespace}`);
|
62 |
-
console.log("response", response);
|
63 |
-
toast.success("Project imported successfully!");
|
64 |
-
setOpen(false);
|
65 |
-
setUrl("");
|
66 |
-
onSuccess(response.data.project);
|
67 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
68 |
-
} catch (error: any) {
|
69 |
-
toast.error(
|
70 |
-
error?.response?.data?.error ?? "Failed to import the project."
|
71 |
-
);
|
72 |
-
} finally {
|
73 |
-
setIsLoading(false);
|
74 |
-
}
|
75 |
-
};
|
76 |
-
|
77 |
-
return (
|
78 |
-
<>
|
79 |
-
{!user ? (
|
80 |
-
<>
|
81 |
-
<Button
|
82 |
-
variant="outline"
|
83 |
-
className="max-lg:hidden"
|
84 |
-
onClick={() => setOpenLoginModal(true)}
|
85 |
-
>
|
86 |
-
<Import className="size-4 mr-1.5" />
|
87 |
-
Load existing Project
|
88 |
-
</Button>
|
89 |
-
<Button
|
90 |
-
variant="outline"
|
91 |
-
size="sm"
|
92 |
-
className="lg:hidden"
|
93 |
-
onClick={() => setOpenLoginModal(true)}
|
94 |
-
>
|
95 |
-
{fullXsBtn && <Import className="size-3.5 mr-1" />}
|
96 |
-
Load
|
97 |
-
{fullXsBtn && " existing Project"}
|
98 |
-
</Button>
|
99 |
-
<LoginModal
|
100 |
-
open={openLoginModal}
|
101 |
-
onClose={setOpenLoginModal}
|
102 |
-
title="Log In to load your Project"
|
103 |
-
description="Log In through Hugging Face to load an existing project and increase your free limit!"
|
104 |
-
/>
|
105 |
-
</>
|
106 |
-
) : (
|
107 |
-
<Dialog open={open} onOpenChange={setOpen}>
|
108 |
-
<DialogTrigger asChild>
|
109 |
-
<div>
|
110 |
-
<Button variant="outline" className="max-lg:hidden">
|
111 |
-
<Import className="size-4 mr-1.5" />
|
112 |
-
Load existing Project
|
113 |
-
</Button>
|
114 |
-
<Button variant="outline" size="sm" className="lg:hidden">
|
115 |
-
{fullXsBtn && <Import className="size-3.5 mr-1" />}
|
116 |
-
Load
|
117 |
-
{fullXsBtn && " existing Project"}
|
118 |
-
</Button>
|
119 |
-
</div>
|
120 |
-
</DialogTrigger>
|
121 |
-
<DialogContent className="sm:max-w-md !p-0 !rounded-3xl !bg-white !border-neutral-100 overflow-hidden text-center">
|
122 |
-
<DialogTitle className="hidden" />
|
123 |
-
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
124 |
-
<div className="flex items-center justify-center -space-x-4 mb-3">
|
125 |
-
<div className="size-11 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-2xl opacity-50">
|
126 |
-
🎨
|
127 |
-
</div>
|
128 |
-
<div className="size-13 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-3xl z-2">
|
129 |
-
🥳
|
130 |
-
</div>
|
131 |
-
<div className="size-11 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-2xl opacity-50">
|
132 |
-
💎
|
133 |
-
</div>
|
134 |
-
</div>
|
135 |
-
<p className="text-2xl font-semibold text-neutral-950">
|
136 |
-
Import a Project
|
137 |
-
</p>
|
138 |
-
<p className="text-base text-neutral-500 mt-1.5">
|
139 |
-
Enter the URL of your Hugging Face Space to import an existing
|
140 |
-
project.
|
141 |
-
</p>
|
142 |
-
</header>
|
143 |
-
<main className="space-y-4 px-9 pb-9 pt-2">
|
144 |
-
<div>
|
145 |
-
<p className="text-sm text-neutral-700 mb-2">
|
146 |
-
Enter your Hugging Face Space
|
147 |
-
</p>
|
148 |
-
<Input
|
149 |
-
type="text"
|
150 |
-
placeholder="https://huggingface.com/spaces/username/project"
|
151 |
-
value={url}
|
152 |
-
onChange={(e) => setUrl(e.target.value)}
|
153 |
-
onBlur={(e) => {
|
154 |
-
const inputUrl = e.target.value.trim();
|
155 |
-
if (!inputUrl) {
|
156 |
-
setUrl("");
|
157 |
-
return;
|
158 |
-
}
|
159 |
-
if (!checkIfUrlIsValid(inputUrl)) {
|
160 |
-
toast.error("Please enter a valid URL.");
|
161 |
-
return;
|
162 |
-
}
|
163 |
-
setUrl(inputUrl);
|
164 |
-
}}
|
165 |
-
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
166 |
-
/>
|
167 |
-
</div>
|
168 |
-
<div>
|
169 |
-
<p className="text-sm text-neutral-700 mb-2">
|
170 |
-
Then, let's import it!
|
171 |
-
</p>
|
172 |
-
<Button
|
173 |
-
variant="black"
|
174 |
-
onClick={handleClick}
|
175 |
-
className="relative w-full"
|
176 |
-
>
|
177 |
-
{isLoading ? (
|
178 |
-
<>
|
179 |
-
<Loading
|
180 |
-
overlay={false}
|
181 |
-
className="ml-2 size-4 animate-spin"
|
182 |
-
/>
|
183 |
-
Fetching your Space...
|
184 |
-
</>
|
185 |
-
) : (
|
186 |
-
<>Import your Space</>
|
187 |
-
)}
|
188 |
-
</Button>
|
189 |
-
</div>
|
190 |
-
</main>
|
191 |
-
</DialogContent>
|
192 |
-
</Dialog>
|
193 |
-
)}
|
194 |
-
</>
|
195 |
-
);
|
196 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/my-projects/project-card.tsx
DELETED
@@ -1,74 +0,0 @@
|
|
1 |
-
import Link from "next/link";
|
2 |
-
import { formatDistance } from "date-fns";
|
3 |
-
import { EllipsisVertical, Settings } from "lucide-react";
|
4 |
-
|
5 |
-
import { Project } from "@/types";
|
6 |
-
import { Button } from "@/components/ui/button";
|
7 |
-
import {
|
8 |
-
DropdownMenu,
|
9 |
-
DropdownMenuContent,
|
10 |
-
DropdownMenuGroup,
|
11 |
-
DropdownMenuItem,
|
12 |
-
DropdownMenuTrigger,
|
13 |
-
} from "@/components/ui/dropdown-menu";
|
14 |
-
|
15 |
-
export function ProjectCard({ project }: { project: Project }) {
|
16 |
-
return (
|
17 |
-
<div className="text-neutral-200 space-y-4 group cursor-pointer">
|
18 |
-
<Link
|
19 |
-
href={`/projects/${project.space_id}`}
|
20 |
-
className="relative bg-neutral-900 rounded-2xl overflow-hidden h-44 w-full flex items-center justify-end flex-col px-3 border border-neutral-800"
|
21 |
-
>
|
22 |
-
<iframe
|
23 |
-
src={`https://${project.space_id.replace("/", "-")}.static.hf.space/`}
|
24 |
-
frameBorder="0"
|
25 |
-
className="absolute inset-0 w-full h-full top-0 left-0 group-hover:brightness-75 transition-all duration-200 pointer-events-none"
|
26 |
-
></iframe>
|
27 |
-
|
28 |
-
<Button
|
29 |
-
variant="default"
|
30 |
-
className="w-full transition-all duration-200 translate-y-full group-hover:-translate-y-3"
|
31 |
-
>
|
32 |
-
Open project
|
33 |
-
</Button>
|
34 |
-
</Link>
|
35 |
-
<div className="flex items-start justify-between gap-3">
|
36 |
-
<div>
|
37 |
-
<p className="text-neutral-200 text-base font-semibold line-clamp-1">
|
38 |
-
{project.space_id}
|
39 |
-
</p>
|
40 |
-
<p className="text-sm text-neutral-500">
|
41 |
-
Updated{" "}
|
42 |
-
{formatDistance(
|
43 |
-
new Date(project._updatedAt || Date.now()),
|
44 |
-
new Date(),
|
45 |
-
{
|
46 |
-
addSuffix: true,
|
47 |
-
}
|
48 |
-
)}
|
49 |
-
</p>
|
50 |
-
</div>
|
51 |
-
<DropdownMenu>
|
52 |
-
<DropdownMenuTrigger asChild>
|
53 |
-
<Button variant="ghost" size="icon">
|
54 |
-
<EllipsisVertical className="text-neutral-400 size-5 hover:text-neutral-300 transition-colors duration-200 cursor-pointer" />
|
55 |
-
</Button>
|
56 |
-
</DropdownMenuTrigger>
|
57 |
-
<DropdownMenuContent className="w-56" align="start">
|
58 |
-
<DropdownMenuGroup>
|
59 |
-
<a
|
60 |
-
href={`https://huggingface.co/spaces/${project.space_id}/settings`}
|
61 |
-
target="_blank"
|
62 |
-
>
|
63 |
-
<DropdownMenuItem>
|
64 |
-
<Settings className="size-4 text-neutral-100" />
|
65 |
-
Project Settings
|
66 |
-
</DropdownMenuItem>
|
67 |
-
</a>
|
68 |
-
</DropdownMenuGroup>
|
69 |
-
</DropdownMenuContent>
|
70 |
-
</DropdownMenu>
|
71 |
-
</div>
|
72 |
-
</div>
|
73 |
-
);
|
74 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/pro-modal/index.tsx
DELETED
@@ -1,92 +0,0 @@
|
|
1 |
-
import { useLocalStorage } from "react-use";
|
2 |
-
import { Button } from "@/components/ui/button";
|
3 |
-
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
4 |
-
import { CheckCheck } from "lucide-react";
|
5 |
-
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
6 |
-
|
7 |
-
export const ProModal = ({
|
8 |
-
open,
|
9 |
-
html,
|
10 |
-
onClose,
|
11 |
-
}: {
|
12 |
-
open: boolean;
|
13 |
-
html: string;
|
14 |
-
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
15 |
-
}) => {
|
16 |
-
const [, setStorage] = useLocalStorage("html_content");
|
17 |
-
const handleProClick = () => {
|
18 |
-
if (!isTheSameHtml(html)) {
|
19 |
-
setStorage(html);
|
20 |
-
}
|
21 |
-
window.open("https://huggingface.co/subscribe/pro?from=DeepSite", "_blank");
|
22 |
-
onClose(false);
|
23 |
-
};
|
24 |
-
return (
|
25 |
-
<Dialog open={open} onOpenChange={onClose}>
|
26 |
-
<DialogContent className="sm:max-w-lg lg:!p-8 !rounded-3xl !bg-white !border-neutral-100">
|
27 |
-
<DialogTitle className="hidden" />
|
28 |
-
<main className="flex flex-col items-start text-left relative pt-2">
|
29 |
-
<div className="flex items-center justify-start -space-x-4 mb-5">
|
30 |
-
<div className="size-14 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-3xl opacity-50">
|
31 |
-
🚀
|
32 |
-
</div>
|
33 |
-
<div className="size-16 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-4xl z-2">
|
34 |
-
🤩
|
35 |
-
</div>
|
36 |
-
<div className="size-14 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-3xl opacity-50">
|
37 |
-
🥳
|
38 |
-
</div>
|
39 |
-
</div>
|
40 |
-
<h2 className="text-2xl font-bold text-neutral-950">
|
41 |
-
Only $9 to enhance your possibilities
|
42 |
-
</h2>
|
43 |
-
<p className="text-neutral-500 text-base mt-2 max-w-sm">
|
44 |
-
It seems like you have reached the monthly free limit of DeepSite.
|
45 |
-
</p>
|
46 |
-
<hr className="bg-neutral-200 w-full max-w-[150px] my-6" />
|
47 |
-
<p className="text-lg mt-3 text-neutral-900 font-semibold">
|
48 |
-
Upgrade to a <ProTag className="mx-1" /> Account, and unlock your
|
49 |
-
DeepSite high quota access ⚡
|
50 |
-
</p>
|
51 |
-
<ul className="mt-3 space-y-1 text-neutral-500">
|
52 |
-
<li className="text-sm text-neutral-500 space-x-2 flex items-center justify-start gap-2 mb-3">
|
53 |
-
You'll also unlock some Hugging Face PRO features, like:
|
54 |
-
</li>
|
55 |
-
<li className="text-sm space-x-2 flex items-center justify-start gap-2">
|
56 |
-
<CheckCheck className="text-emerald-500 size-4" />
|
57 |
-
Get acces to thousands of AI app (ZeroGPU) with high quota
|
58 |
-
</li>
|
59 |
-
<li className="text-sm space-x-2 flex items-center justify-start gap-2">
|
60 |
-
<CheckCheck className="text-emerald-500 size-4" />
|
61 |
-
Get exclusive early access to new features and updates
|
62 |
-
</li>
|
63 |
-
<li className="text-sm space-x-2 flex items-center justify-start gap-2">
|
64 |
-
<CheckCheck className="text-emerald-500 size-4" />
|
65 |
-
Get free credits across all Inference Providers
|
66 |
-
</li>
|
67 |
-
<li className="text-sm text-neutral-500 space-x-2 flex items-center justify-start gap-2 mt-3">
|
68 |
-
... and lots more!
|
69 |
-
</li>
|
70 |
-
</ul>
|
71 |
-
<Button
|
72 |
-
variant="black"
|
73 |
-
size="lg"
|
74 |
-
className="w-full !text-base !h-11 mt-8"
|
75 |
-
onClick={handleProClick}
|
76 |
-
>
|
77 |
-
Subscribe to PRO ($9/month)
|
78 |
-
</Button>
|
79 |
-
</main>
|
80 |
-
</DialogContent>
|
81 |
-
</Dialog>
|
82 |
-
);
|
83 |
-
};
|
84 |
-
|
85 |
-
const ProTag = ({ className }: { className?: string }) => (
|
86 |
-
<span
|
87 |
-
className={`${className} bg-linear-to-br shadow-green-500/10 dark:shadow-green-500/20 inline-block -skew-x-12 border border-gray-200 from-pink-300 via-green-200 to-yellow-200 text-xs font-bold text-black shadow-lg rounded-md px-2.5 py-0.5`}
|
88 |
-
>
|
89 |
-
PRO
|
90 |
-
</span>
|
91 |
-
);
|
92 |
-
export default ProModal;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/providers/tanstack-query-provider.tsx
DELETED
@@ -1,18 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
4 |
-
|
5 |
-
export default function TanstackProvider({
|
6 |
-
children,
|
7 |
-
}: {
|
8 |
-
children: React.ReactNode;
|
9 |
-
}) {
|
10 |
-
const queryClient = new QueryClient();
|
11 |
-
|
12 |
-
return (
|
13 |
-
<QueryClientProvider client={queryClient}>
|
14 |
-
{children}
|
15 |
-
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
16 |
-
</QueryClientProvider>
|
17 |
-
);
|
18 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/public/navigation/index.tsx
DELETED
@@ -1,156 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import { useRef, useState } from "react";
|
4 |
-
import Image from "next/image";
|
5 |
-
import Link from "next/link";
|
6 |
-
import { useMount, useUnmount } from "react-use";
|
7 |
-
import classNames from "classnames";
|
8 |
-
|
9 |
-
import { Button } from "@/components/ui/button";
|
10 |
-
import Logo from "@/assets/logo.svg";
|
11 |
-
import { useUser } from "@/hooks/useUser";
|
12 |
-
import { UserMenu } from "@/components/user-menu";
|
13 |
-
|
14 |
-
const navigationLinks = [
|
15 |
-
{
|
16 |
-
name: "Create Website",
|
17 |
-
href: "/projects/new",
|
18 |
-
},
|
19 |
-
{
|
20 |
-
name: "Features",
|
21 |
-
href: "#features",
|
22 |
-
},
|
23 |
-
{
|
24 |
-
name: "Community",
|
25 |
-
href: "#community",
|
26 |
-
},
|
27 |
-
{
|
28 |
-
name: "Deploy",
|
29 |
-
href: "#deploy",
|
30 |
-
},
|
31 |
-
];
|
32 |
-
|
33 |
-
export default function Navigation() {
|
34 |
-
const { openLoginWindow, user } = useUser();
|
35 |
-
const [hash, setHash] = useState("");
|
36 |
-
|
37 |
-
const selectorRef = useRef<HTMLDivElement>(null);
|
38 |
-
const linksRef = useRef<HTMLLIElement[]>(
|
39 |
-
new Array(navigationLinks.length).fill(null)
|
40 |
-
);
|
41 |
-
const [isScrolled, setIsScrolled] = useState(false);
|
42 |
-
|
43 |
-
useMount(() => {
|
44 |
-
const handleScroll = () => {
|
45 |
-
const scrollTop = window.scrollY;
|
46 |
-
setIsScrolled(scrollTop > 100);
|
47 |
-
};
|
48 |
-
|
49 |
-
const initialHash = window.location.hash;
|
50 |
-
if (initialHash) {
|
51 |
-
setHash(initialHash);
|
52 |
-
calculateSelectorPosition(initialHash);
|
53 |
-
}
|
54 |
-
|
55 |
-
window.addEventListener("scroll", handleScroll);
|
56 |
-
});
|
57 |
-
|
58 |
-
useUnmount(() => {
|
59 |
-
window.removeEventListener("scroll", () => {});
|
60 |
-
});
|
61 |
-
|
62 |
-
const handleClick = (href: string) => {
|
63 |
-
setHash(href);
|
64 |
-
calculateSelectorPosition(href);
|
65 |
-
};
|
66 |
-
|
67 |
-
const calculateSelectorPosition = (href: string) => {
|
68 |
-
if (selectorRef.current && linksRef.current) {
|
69 |
-
const index = navigationLinks.findIndex((l) => l.href === href);
|
70 |
-
const targetLink = linksRef.current[index];
|
71 |
-
if (targetLink) {
|
72 |
-
const targetRect = targetLink.getBoundingClientRect();
|
73 |
-
selectorRef.current.style.left = targetRect.left + "px";
|
74 |
-
selectorRef.current.style.width = targetRect.width + "px";
|
75 |
-
}
|
76 |
-
}
|
77 |
-
};
|
78 |
-
|
79 |
-
return (
|
80 |
-
<div
|
81 |
-
className={classNames(
|
82 |
-
"sticky top-0 z-10 transition-all duration-200 backdrop-blur-md",
|
83 |
-
{
|
84 |
-
"bg-black/30": isScrolled,
|
85 |
-
}
|
86 |
-
)}
|
87 |
-
>
|
88 |
-
<nav className="grid grid-cols-2 p-4 container mx-auto">
|
89 |
-
<Link href="/" className="flex items-center gap-1">
|
90 |
-
<Image
|
91 |
-
src={Logo}
|
92 |
-
className="w-9 mr-1"
|
93 |
-
alt="DeepSite Logo"
|
94 |
-
width={64}
|
95 |
-
height={64}
|
96 |
-
/>
|
97 |
-
<p className="font-sans text-white text-xl font-bold">DeepSite</p>
|
98 |
-
</Link>
|
99 |
-
<ul className="items-center justify-center gap-6 hidden">
|
100 |
-
{navigationLinks.map((link) => (
|
101 |
-
<li
|
102 |
-
key={link.name}
|
103 |
-
ref={(el) => {
|
104 |
-
const index = navigationLinks.findIndex(
|
105 |
-
(l) => l.href === link.href
|
106 |
-
);
|
107 |
-
if (el && linksRef.current[index] !== el) {
|
108 |
-
linksRef.current[index] = el;
|
109 |
-
}
|
110 |
-
}}
|
111 |
-
className="inline-block font-sans text-sm"
|
112 |
-
>
|
113 |
-
<Link
|
114 |
-
href={link.href}
|
115 |
-
className={classNames(
|
116 |
-
"text-neutral-500 hover:text-primary transition-colors",
|
117 |
-
{
|
118 |
-
"text-primary": hash === link.href,
|
119 |
-
}
|
120 |
-
)}
|
121 |
-
onClick={() => {
|
122 |
-
handleClick(link.href);
|
123 |
-
}}
|
124 |
-
>
|
125 |
-
{link.name}
|
126 |
-
</Link>
|
127 |
-
</li>
|
128 |
-
))}
|
129 |
-
<div
|
130 |
-
ref={selectorRef}
|
131 |
-
className={classNames(
|
132 |
-
"h-1 absolute bottom-4 transition-all duration-200 flex items-center justify-center",
|
133 |
-
{
|
134 |
-
"opacity-0": !hash,
|
135 |
-
}
|
136 |
-
)}
|
137 |
-
>
|
138 |
-
<div className="size-1 bg-white rounded-full" />
|
139 |
-
</div>
|
140 |
-
</ul>
|
141 |
-
<div className="flex items-center justify-end gap-2">
|
142 |
-
{user ? (
|
143 |
-
<UserMenu className="!pl-3 !pr-4 !py-2 !h-auto !rounded-lg" />
|
144 |
-
) : (
|
145 |
-
<>
|
146 |
-
<Button variant="link" size={"sm"} onClick={openLoginWindow}>
|
147 |
-
Log In
|
148 |
-
</Button>
|
149 |
-
<Button size={"sm"}>Sign Up</Button>
|
150 |
-
</>
|
151 |
-
)}
|
152 |
-
</div>
|
153 |
-
</nav>
|
154 |
-
</div>
|
155 |
-
);
|
156 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/space/ask-ai/index.tsx
DELETED
@@ -1,43 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import { ArrowUp } from "lucide-react";
|
4 |
-
import { PiGearSixFill } from "react-icons/pi";
|
5 |
-
import { TiUserAdd } from "react-icons/ti";
|
6 |
-
|
7 |
-
import { Button } from "@/components/ui/button";
|
8 |
-
|
9 |
-
export const AskAi = () => {
|
10 |
-
return (
|
11 |
-
<>
|
12 |
-
<div className="bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent group">
|
13 |
-
<textarea
|
14 |
-
rows={3}
|
15 |
-
className="w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none mb-1"
|
16 |
-
placeholder="Ask DeepSite anything..."
|
17 |
-
onChange={() => {}}
|
18 |
-
onKeyDown={() => {}}
|
19 |
-
/>
|
20 |
-
<div className="flex items-center justify-between gap-2 px-4 pb-3">
|
21 |
-
<div className="flex-1 flex justify-start">
|
22 |
-
<Button
|
23 |
-
size="iconXs"
|
24 |
-
variant="outline"
|
25 |
-
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
26 |
-
>
|
27 |
-
<TiUserAdd className="size-4" />
|
28 |
-
</Button>
|
29 |
-
</div>
|
30 |
-
<div className="flex items-center justify-end gap-2">
|
31 |
-
<Button variant="black" size="sm">
|
32 |
-
<PiGearSixFill className="size-4" />
|
33 |
-
Settings
|
34 |
-
</Button>
|
35 |
-
<Button size="iconXs">
|
36 |
-
<ArrowUp className="size-4" />
|
37 |
-
</Button>
|
38 |
-
</div>
|
39 |
-
</div>
|
40 |
-
</div>
|
41 |
-
</>
|
42 |
-
);
|
43 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ui/avatar.tsx
DELETED
@@ -1,53 +0,0 @@
|
|
1 |
-
"use client"
|
2 |
-
|
3 |
-
import * as React from "react"
|
4 |
-
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
5 |
-
|
6 |
-
import { cn } from "@/lib/utils"
|
7 |
-
|
8 |
-
function Avatar({
|
9 |
-
className,
|
10 |
-
...props
|
11 |
-
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
12 |
-
return (
|
13 |
-
<AvatarPrimitive.Root
|
14 |
-
data-slot="avatar"
|
15 |
-
className={cn(
|
16 |
-
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
17 |
-
className
|
18 |
-
)}
|
19 |
-
{...props}
|
20 |
-
/>
|
21 |
-
)
|
22 |
-
}
|
23 |
-
|
24 |
-
function AvatarImage({
|
25 |
-
className,
|
26 |
-
...props
|
27 |
-
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
28 |
-
return (
|
29 |
-
<AvatarPrimitive.Image
|
30 |
-
data-slot="avatar-image"
|
31 |
-
className={cn("aspect-square size-full", className)}
|
32 |
-
{...props}
|
33 |
-
/>
|
34 |
-
)
|
35 |
-
}
|
36 |
-
|
37 |
-
function AvatarFallback({
|
38 |
-
className,
|
39 |
-
...props
|
40 |
-
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
41 |
-
return (
|
42 |
-
<AvatarPrimitive.Fallback
|
43 |
-
data-slot="avatar-fallback"
|
44 |
-
className={cn(
|
45 |
-
"bg-muted flex size-full items-center justify-center rounded-full",
|
46 |
-
className
|
47 |
-
)}
|
48 |
-
{...props}
|
49 |
-
/>
|
50 |
-
)
|
51 |
-
}
|
52 |
-
|
53 |
-
export { Avatar, AvatarImage, AvatarFallback }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ui/button.tsx
DELETED
@@ -1,67 +0,0 @@
|
|
1 |
-
import * as React from "react";
|
2 |
-
import { Slot } from "@radix-ui/react-slot";
|
3 |
-
import { cva, type VariantProps } from "class-variance-authority";
|
4 |
-
|
5 |
-
import { cn } from "@/lib/utils";
|
6 |
-
|
7 |
-
const buttonVariants = cva(
|
8 |
-
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-full text-sm font-sans font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
9 |
-
{
|
10 |
-
variants: {
|
11 |
-
variant: {
|
12 |
-
default:
|
13 |
-
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
14 |
-
destructive:
|
15 |
-
"bg-red-500 text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 [&_svg]:!text-white",
|
16 |
-
outline:
|
17 |
-
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
18 |
-
secondary:
|
19 |
-
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
20 |
-
ghost:
|
21 |
-
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
22 |
-
lightGray: "bg-neutral-200/60 hover:bg-neutral-200",
|
23 |
-
link: "text-primary underline-offset-4 hover:underline",
|
24 |
-
ghostDarker:
|
25 |
-
"text-white shadow-xs focus-visible:ring-black/40 bg-black/40 hover:bg-black/70",
|
26 |
-
black: "bg-neutral-950 text-neutral-300 hover:brightness-110",
|
27 |
-
sky: "bg-sky-500 text-white hover:brightness-110",
|
28 |
-
},
|
29 |
-
size: {
|
30 |
-
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
31 |
-
sm: "h-8 rounded-full text-[13px] gap-1.5 px-3",
|
32 |
-
lg: "h-10 rounded-full px-6 has-[>svg]:px-4",
|
33 |
-
icon: "size-9",
|
34 |
-
iconXs: "size-7",
|
35 |
-
iconXss: "size-6",
|
36 |
-
xs: "h-6 text-xs rounded-full pl-2 pr-2 gap-1",
|
37 |
-
},
|
38 |
-
},
|
39 |
-
defaultVariants: {
|
40 |
-
variant: "default",
|
41 |
-
size: "default",
|
42 |
-
},
|
43 |
-
}
|
44 |
-
);
|
45 |
-
|
46 |
-
function Button({
|
47 |
-
className,
|
48 |
-
variant,
|
49 |
-
size,
|
50 |
-
asChild = false,
|
51 |
-
...props
|
52 |
-
}: React.ComponentProps<"button"> &
|
53 |
-
VariantProps<typeof buttonVariants> & {
|
54 |
-
asChild?: boolean;
|
55 |
-
}) {
|
56 |
-
const Comp = asChild ? Slot : "button";
|
57 |
-
|
58 |
-
return (
|
59 |
-
<Comp
|
60 |
-
data-slot="button"
|
61 |
-
className={cn(buttonVariants({ variant, size, className }))}
|
62 |
-
{...props}
|
63 |
-
/>
|
64 |
-
);
|
65 |
-
}
|
66 |
-
|
67 |
-
export { Button, buttonVariants };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ui/checkbox.tsx
DELETED
@@ -1,32 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import * as React from "react";
|
4 |
-
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
5 |
-
import { CheckIcon } from "lucide-react";
|
6 |
-
|
7 |
-
import { cn } from "@/lib/utils";
|
8 |
-
|
9 |
-
function Checkbox({
|
10 |
-
className,
|
11 |
-
...props
|
12 |
-
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
13 |
-
return (
|
14 |
-
<CheckboxPrimitive.Root
|
15 |
-
data-slot="checkbox"
|
16 |
-
className={cn(
|
17 |
-
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-3.5 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
18 |
-
className
|
19 |
-
)}
|
20 |
-
{...props}
|
21 |
-
>
|
22 |
-
<CheckboxPrimitive.Indicator
|
23 |
-
data-slot="checkbox-indicator"
|
24 |
-
className="flex items-center justify-center text-current transition-none"
|
25 |
-
>
|
26 |
-
<CheckIcon className="size-3" />
|
27 |
-
</CheckboxPrimitive.Indicator>
|
28 |
-
</CheckboxPrimitive.Root>
|
29 |
-
);
|
30 |
-
}
|
31 |
-
|
32 |
-
export { Checkbox };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ui/collapsible.tsx
DELETED
@@ -1,33 +0,0 @@
|
|
1 |
-
"use client"
|
2 |
-
|
3 |
-
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
4 |
-
|
5 |
-
function Collapsible({
|
6 |
-
...props
|
7 |
-
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
8 |
-
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
9 |
-
}
|
10 |
-
|
11 |
-
function CollapsibleTrigger({
|
12 |
-
...props
|
13 |
-
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
14 |
-
return (
|
15 |
-
<CollapsiblePrimitive.CollapsibleTrigger
|
16 |
-
data-slot="collapsible-trigger"
|
17 |
-
{...props}
|
18 |
-
/>
|
19 |
-
)
|
20 |
-
}
|
21 |
-
|
22 |
-
function CollapsibleContent({
|
23 |
-
...props
|
24 |
-
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
25 |
-
return (
|
26 |
-
<CollapsiblePrimitive.CollapsibleContent
|
27 |
-
data-slot="collapsible-content"
|
28 |
-
{...props}
|
29 |
-
/>
|
30 |
-
)
|
31 |
-
}
|
32 |
-
|
33 |
-
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|