Spaces:
Running
Running
Upload 11 files
#312
by
SebaVergara420
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .gitattributes +11 -0
- README.md +2 -7
- app/(public)/layout.tsx +1 -1
- app/(public)/page.tsx +31 -180
- app/(public)/projects/page.tsx +8 -4
- app/actions/projects.ts +40 -24
- app/api/{ask → ask-ai}/route.ts +112 -298
- app/api/auth/login-url/route.ts +0 -23
- app/api/auth/logout/route.ts +0 -25
- app/api/auth/route.ts +1 -21
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -190
- app/api/me/projects/[namespace]/[repoId]/images/route.ts +0 -113
- app/api/me/projects/[namespace]/[repoId]/route.ts +162 -112
- app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -64
- app/api/me/projects/route.ts +92 -73
- app/api/me/route.ts +1 -22
- app/auth/callback/page.tsx +42 -67
- app/layout.tsx +8 -16
- app/projects/[namespace]/[repoId]/page.tsx +31 -1
- app/projects/new/page.tsx +2 -2
- assets/globals.css +0 -225
- components.json +1 -1
- components/animated-blobs/index.tsx +0 -34
- components/animated-text/index.tsx +0 -123
- components/contexts/app-context.tsx +10 -6
- components/contexts/login-context.tsx +0 -61
- components/contexts/pro-context.tsx +0 -48
- components/editor/ask-ai/follow-up-tooltip.tsx +36 -0
- components/editor/ask-ai/index.tsx +390 -179
- components/editor/ask-ai/loading.tsx +0 -59
- components/editor/ask-ai/prompt-builder/content-modal.tsx +0 -196
- components/editor/ask-ai/prompt-builder/index.tsx +0 -73
- components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +0 -58
- components/editor/ask-ai/prompt-builder/themes.tsx +0 -48
- components/editor/ask-ai/re-imagine.tsx +4 -27
- components/editor/ask-ai/selected-files.tsx +0 -47
- components/editor/ask-ai/selector.tsx +0 -41
- components/editor/ask-ai/settings.tsx +146 -162
- components/editor/ask-ai/uploader.tsx +0 -165
- components/editor/deploy-button/index.tsx +173 -0
- components/editor/footer/index.tsx +127 -0
- components/editor/header/index.tsx +48 -91
- components/editor/header/switch-tab.tsx +0 -58
- components/editor/history-notification/index.tsx +0 -119
- components/editor/history/index.tsx +30 -66
- components/editor/index.tsx +315 -128
- components/editor/live-preview/index.tsx +0 -165
- components/editor/pages/index.tsx +0 -24
- components/editor/pages/page.tsx +0 -56
- components/editor/preview/index.tsx +153 -335
.gitattributes
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
public/bowl1.jpg filter=lfs diff=lfs merge=lfs -text
|
2 |
+
public/bowl2.jpg filter=lfs diff=lfs merge=lfs -text
|
3 |
+
public/bowl3.jpg filter=lfs diff=lfs merge=lfs -text
|
4 |
+
public/local1.jpg filter=lfs diff=lfs merge=lfs -text
|
5 |
+
public/local2.jpg filter=lfs diff=lfs merge=lfs -text
|
6 |
+
public/local3.jpg filter=lfs diff=lfs merge=lfs -text
|
7 |
+
public/portada1.JPG filter=lfs diff=lfs merge=lfs -text
|
8 |
+
public/riva_way_negro.png filter=lfs diff=lfs merge=lfs -text
|
9 |
+
public/style1.jpg filter=lfs diff=lfs merge=lfs -text
|
10 |
+
public/style2.JPG filter=lfs diff=lfs merge=lfs -text
|
11 |
+
public/style3.JPG filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
---
|
2 |
-
title: DeepSite
|
3 |
emoji: 🐳
|
4 |
colorFrom: blue
|
5 |
colorTo: blue
|
@@ -11,11 +11,6 @@ short_description: Generate any application with DeepSeek
|
|
11 |
models:
|
12 |
- deepseek-ai/DeepSeek-V3-0324
|
13 |
- deepseek-ai/DeepSeek-R1-0528
|
14 |
-
- Qwen/Qwen3-Coder-480B-A35B-Instruct
|
15 |
-
- moonshotai/Kimi-K2-Instruct
|
16 |
-
- moonshotai/Kimi-K2-Instruct-0905
|
17 |
-
- deepseek-ai/DeepSeek-V3.1
|
18 |
-
- deepseek-ai/DeepSeek-V3.1-Terminus
|
19 |
---
|
20 |
|
21 |
# DeepSite 🐳
|
@@ -24,4 +19,4 @@ DeepSite is a coding platform powered by DeepSeek AI, designed to make coding sm
|
|
24 |
|
25 |
## How to use it locally
|
26 |
|
27 |
-
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
|
|
1 |
---
|
2 |
+
title: DeepSite v2
|
3 |
emoji: 🐳
|
4 |
colorFrom: blue
|
5 |
colorTo: blue
|
|
|
11 |
models:
|
12 |
- deepseek-ai/DeepSeek-V3-0324
|
13 |
- deepseek-ai/DeepSeek-R1-0528
|
|
|
|
|
|
|
|
|
|
|
14 |
---
|
15 |
|
16 |
# DeepSite 🐳
|
|
|
19 |
|
20 |
## How to use it locally
|
21 |
|
22 |
+
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
app/(public)/layout.tsx
CHANGED
@@ -6,7 +6,7 @@ export default async function PublicLayout({
|
|
6 |
children: React.ReactNode;
|
7 |
}>) {
|
8 |
return (
|
9 |
-
<div className="h-screen bg-
|
10 |
<div className="background__noisy" />
|
11 |
<Navigation />
|
12 |
{children}
|
|
|
6 |
children: React.ReactNode;
|
7 |
}>) {
|
8 |
return (
|
9 |
+
<div className="min-h-screen bg-black z-1 relative">
|
10 |
<div className="background__noisy" />
|
11 |
<Navigation />
|
12 |
{children}
|
app/(public)/page.tsx
CHANGED
@@ -1,193 +1,44 @@
|
|
1 |
-
|
2 |
import { redirect } from "next/navigation";
|
3 |
-
import { AnimatedText } from "@/components/animated-text";
|
4 |
-
import { AnimatedBlobs } from "@/components/animated-blobs";
|
5 |
-
|
6 |
export default function Home() {
|
7 |
-
redirect("/projects");
|
8 |
return (
|
9 |
-
|
10 |
<header className="container mx-auto pt-20 px-6 relative flex flex-col items-center justify-center text-center">
|
11 |
-
<div className="rounded-full border border-
|
12 |
-
✨ DeepSite
|
13 |
</div>
|
14 |
-
<h1 className="text-
|
15 |
Code your website with AI in seconds
|
16 |
</h1>
|
17 |
-
<
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
<div id="features" className="min-h-screen py-20 px-6 relative">
|
23 |
-
<div className="container mx-auto"></div>
|
24 |
-
<div className="text-center mb-16">
|
25 |
-
<div className="rounded-full border border-neutral-100/10 bg-neutral-100/5 text-sm text-neutral-300 px-3 py-1 max-w-max mx-auto mb-4">
|
26 |
-
🚀 Powerful Features
|
27 |
-
</div>
|
28 |
-
<h2 className="text-4xl lg:text-6xl font-extrabold text-white font-mono mb-4">
|
29 |
-
Everything you need
|
30 |
-
</h2>
|
31 |
-
<p className="text-lg lg:text-xl text-neutral-300/80 max-w-2xl mx-auto">
|
32 |
-
Build, deploy, and scale your websites with cutting-edge features
|
33 |
-
</p>
|
34 |
</div>
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
<div
|
40 |
-
className="lg:row-span-2 relative p-8 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-105 hover:rotate-1 hover:-translate-y-2 hover:shadow-2xl hover:shadow-purple-500/20"
|
41 |
-
style={{ transformStyle: "preserve-3d" }}
|
42 |
-
>
|
43 |
-
<div className="relative z-10">
|
44 |
-
<div className="text-3xl lg:text-4xl mb-4">📄</div>
|
45 |
-
<h3 className="text-2xl lg:text-3xl font-bold text-white font-mono mb-3">
|
46 |
-
Multi Pages
|
47 |
-
</h3>
|
48 |
-
<p className="text-neutral-300/80 lg:text-lg mb-6">
|
49 |
-
Create complex websites with multiple interconnected pages.
|
50 |
-
Build everything from simple landing pages to full-featured web
|
51 |
-
applications with dynamic routing and navigation.
|
52 |
-
</p>
|
53 |
-
<div className="flex flex-wrap gap-2">
|
54 |
-
<span className="px-3 py-1 bg-purple-500/20 text-purple-300 rounded-full text-sm">
|
55 |
-
Dynamic Routing
|
56 |
-
</span>
|
57 |
-
<span className="px-3 py-1 bg-blue-500/20 text-blue-300 rounded-full text-sm">
|
58 |
-
Navigation
|
59 |
-
</span>
|
60 |
-
<span className="px-3 py-1 bg-green-500/20 text-green-300 rounded-full text-sm">
|
61 |
-
SEO Ready
|
62 |
-
</span>
|
63 |
-
</div>
|
64 |
-
</div>
|
65 |
-
<div className="absolute -top-20 -right-20 w-40 h-40 bg-gradient-to-r from-purple-500 to-pink-500 opacity-20 blur-3xl rounded-full transition-all duration-700 ease-out group-hover:scale-[4] group-hover:opacity-30" />
|
66 |
-
</div>
|
67 |
-
|
68 |
-
{/* Auto Deploy */}
|
69 |
-
<div
|
70 |
-
className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-110 hover:-translate-y-4 hover:-rotate-3 hover:shadow-2xl hover:shadow-yellow-500/25"
|
71 |
-
style={{ perspective: "1000px", transformStyle: "preserve-3d" }}
|
72 |
-
>
|
73 |
-
<div className="relative z-10">
|
74 |
-
<div className="text-3xl mb-4">⚡</div>
|
75 |
-
<h3 className="text-2xl font-bold text-white font-mono mb-3">
|
76 |
-
Auto Deploy
|
77 |
-
</h3>
|
78 |
-
<p className="text-neutral-300/80 mb-4">
|
79 |
-
Push your changes and watch them go live instantly. No complex
|
80 |
-
CI/CD setup required.
|
81 |
-
</p>
|
82 |
-
</div>
|
83 |
-
<div className="absolute -bottom-10 -right-10 w-32 h-32 bg-gradient-to-r from-yellow-500 to-orange-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
|
84 |
-
</div>
|
85 |
-
|
86 |
-
{/* Free Hosting */}
|
87 |
-
<div className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-105 hover:rotate-2 hover:-translate-y-3 hover:shadow-xl hover:shadow-green-500/20">
|
88 |
-
<div className="relative z-10">
|
89 |
-
<div className="text-3xl mb-4">🌐</div>
|
90 |
-
<h3 className="text-2xl font-bold text-white font-mono mb-3">
|
91 |
-
Free Hosting
|
92 |
-
</h3>
|
93 |
-
<p className="text-neutral-300/80 mb-4">
|
94 |
-
Host your websites for free with global CDN and lightning-fast
|
95 |
-
performance.
|
96 |
-
</p>
|
97 |
-
</div>
|
98 |
-
<div className="absolute -top-10 -left-10 w-32 h-32 bg-gradient-to-r from-green-500 to-emerald-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
|
99 |
-
</div>
|
100 |
-
|
101 |
-
{/* Open Source Models */}
|
102 |
-
<div
|
103 |
-
className="lg:col-span-2 md:col-span-2 relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-600 hover:scale-[1.02] hover:rotate-y-6 hover:-translate-y-1 hover:shadow-2xl hover:shadow-cyan-500/20"
|
104 |
-
style={{ perspective: "1200px", transformStyle: "preserve-3d" }}
|
105 |
-
>
|
106 |
-
<div className="relative z-10">
|
107 |
-
<div className="text-3xl mb-4">🔓</div>
|
108 |
-
<h3 className="text-2xl font-bold text-white font-mono mb-3">
|
109 |
-
Open Source Models
|
110 |
-
</h3>
|
111 |
-
<p className="text-neutral-300/80 mb-4">
|
112 |
-
Powered by cutting-edge open source AI models. Transparent,
|
113 |
-
customizable, and community-driven development.
|
114 |
-
</p>
|
115 |
-
<div className="flex flex-wrap gap-2">
|
116 |
-
<span className="px-3 py-1 bg-cyan-500/20 text-cyan-300 rounded-full text-sm">
|
117 |
-
Llama
|
118 |
-
</span>
|
119 |
-
<span className="px-3 py-1 bg-indigo-500/20 text-indigo-300 rounded-full text-sm">
|
120 |
-
Mistral
|
121 |
-
</span>
|
122 |
-
<span className="px-3 py-1 bg-pink-500/20 text-pink-300 rounded-full text-sm">
|
123 |
-
CodeLlama
|
124 |
-
</span>
|
125 |
-
</div>
|
126 |
-
</div>
|
127 |
-
<div className="absolute -bottom-10 right-10 w-32 h-32 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
|
128 |
-
</div>
|
129 |
-
|
130 |
-
{/* UX Focus */}
|
131 |
-
<div
|
132 |
-
className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-110 hover:rotate-3 hover:-translate-y-2 hover:rotate-x-6 hover:shadow-xl hover:shadow-rose-500/25"
|
133 |
-
style={{ transformStyle: "preserve-3d" }}
|
134 |
-
>
|
135 |
-
<div className="relative z-10">
|
136 |
-
<div className="text-3xl mb-4">✨</div>
|
137 |
-
<h3 className="text-2xl font-bold text-white font-mono mb-3">
|
138 |
-
Perfect UX
|
139 |
-
</h3>
|
140 |
-
<p className="text-neutral-300/80 mb-4">
|
141 |
-
Intuitive interface designed for developers and non-developers
|
142 |
-
alike.
|
143 |
-
</p>
|
144 |
-
</div>
|
145 |
-
<div className="absolute -top-10 -right-10 w-32 h-32 bg-gradient-to-r from-rose-500 to-pink-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
|
146 |
-
</div>
|
147 |
-
|
148 |
-
{/* Hugging Face Integration */}
|
149 |
-
<div
|
150 |
-
className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-[1.08] hover:-rotate-2 hover:-translate-y-3 hover:rotate-y-8 hover:shadow-xl hover:shadow-amber-500/20"
|
151 |
-
style={{ perspective: "800px" }}
|
152 |
-
>
|
153 |
-
<div className="relative z-10">
|
154 |
-
<div className="text-3xl mb-4">🤗</div>
|
155 |
-
<h3 className="text-2xl font-bold text-white font-mono mb-3">
|
156 |
-
Hugging Face
|
157 |
-
</h3>
|
158 |
-
<p className="text-neutral-300/80 mb-4">
|
159 |
-
Seamless integration with Hugging Face models and datasets for
|
160 |
-
cutting-edge AI capabilities.
|
161 |
-
</p>
|
162 |
-
</div>
|
163 |
-
<div className="absolute -top-10 -right-10 w-32 h-32 bg-gradient-to-r from-yellow-500 to-amber-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
|
164 |
-
</div>
|
165 |
-
|
166 |
-
{/* Performance */}
|
167 |
-
<div
|
168 |
-
className="relative p-6 rounded-2xl border border-neutral-100/10 bg-neutral-900/50 backdrop-blur-sm overflow-hidden group hover:border-neutral-100/20 transition-all duration-500 hover:scale-105 hover:rotate-1 hover:-translate-y-4 hover:rotate-x-8 hover:shadow-2xl hover:shadow-blue-500/25"
|
169 |
-
style={{ transformStyle: "preserve-3d" }}
|
170 |
-
>
|
171 |
-
<div className="relative z-10">
|
172 |
-
<div className="text-3xl mb-4">🚀</div>
|
173 |
-
<h3 className="text-2xl font-bold text-white font-mono mb-3">
|
174 |
-
Blazing Fast
|
175 |
-
</h3>
|
176 |
-
<p className="text-neutral-300/80 mb-4">
|
177 |
-
Optimized performance with edge computing and smart caching.
|
178 |
-
</p>
|
179 |
-
</div>
|
180 |
-
<div className="absolute -bottom-10 -right-10 w-32 h-32 bg-gradient-to-r from-blue-500 to-cyan-500 opacity-20 blur-2xl rounded-full transition-all duration-700 ease-out group-hover:scale-[5] group-hover:opacity-35" />
|
181 |
-
</div>
|
182 |
</div>
|
|
|
|
|
|
|
|
|
|
|
183 |
</div>
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
|
|
|
|
|
|
190 |
</div>
|
191 |
-
|
192 |
);
|
193 |
}
|
|
|
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
CHANGED
@@ -1,9 +1,13 @@
|
|
1 |
-
import {
|
|
|
2 |
import { MyProjects } from "@/components/my-projects";
|
3 |
-
import {
|
4 |
|
5 |
export default async function ProjectsPage() {
|
6 |
-
|
|
|
|
|
|
|
7 |
|
8 |
-
return <MyProjects />;
|
9 |
}
|
|
|
1 |
+
import { redirect } from "next/navigation";
|
2 |
+
|
3 |
import { MyProjects } from "@/components/my-projects";
|
4 |
+
import { getProjects } from "@/app/actions/projects";
|
5 |
|
6 |
export default async function ProjectsPage() {
|
7 |
+
const { ok, projects } = await getProjects();
|
8 |
+
if (!ok) {
|
9 |
+
redirect("/");
|
10 |
+
}
|
11 |
|
12 |
+
return <MyProjects projects={projects} />;
|
13 |
}
|
app/actions/projects.ts
CHANGED
@@ -2,13 +2,13 @@
|
|
2 |
|
3 |
import { isAuthenticated } from "@/lib/auth";
|
4 |
import { NextResponse } from "next/server";
|
5 |
-
import
|
6 |
-
import
|
|
|
7 |
|
8 |
export async function getProjects(): Promise<{
|
9 |
ok: boolean;
|
10 |
projects: ProjectType[];
|
11 |
-
isEmpty?: boolean;
|
12 |
}> {
|
13 |
const user = await isAuthenticated();
|
14 |
|
@@ -19,29 +19,45 @@ export async function getProjects(): Promise<{
|
|
19 |
};
|
20 |
}
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
(
|
35 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
36 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
37 |
-
)
|
38 |
-
) {
|
39 |
-
projects.push(space);
|
40 |
-
}
|
41 |
}
|
42 |
-
|
43 |
return {
|
44 |
ok: true,
|
45 |
-
projects,
|
46 |
};
|
47 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
import { isAuthenticated } from "@/lib/auth";
|
4 |
import { NextResponse } from "next/server";
|
5 |
+
import dbConnect from "@/lib/mongodb";
|
6 |
+
import Project from "@/models/Project";
|
7 |
+
import { Project as ProjectType } from "@/types";
|
8 |
|
9 |
export async function getProjects(): Promise<{
|
10 |
ok: boolean;
|
11 |
projects: ProjectType[];
|
|
|
12 |
}> {
|
13 |
const user = await isAuthenticated();
|
14 |
|
|
|
19 |
};
|
20 |
}
|
21 |
|
22 |
+
await dbConnect();
|
23 |
+
const projects = await Project.find({
|
24 |
+
user_id: user?.id,
|
25 |
+
})
|
26 |
+
.sort({ _createdAt: -1 })
|
27 |
+
.limit(100)
|
28 |
+
.lean();
|
29 |
+
if (!projects) {
|
30 |
+
return {
|
31 |
+
ok: false,
|
32 |
+
projects: [],
|
33 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
}
|
|
|
35 |
return {
|
36 |
ok: true,
|
37 |
+
projects: JSON.parse(JSON.stringify(projects)) as ProjectType[],
|
38 |
};
|
39 |
}
|
40 |
+
|
41 |
+
export async function getProject(
|
42 |
+
namespace: string,
|
43 |
+
repoId: string
|
44 |
+
): Promise<ProjectType | null> {
|
45 |
+
const user = await isAuthenticated();
|
46 |
+
|
47 |
+
if (user instanceof NextResponse || !user) {
|
48 |
+
return null;
|
49 |
+
}
|
50 |
+
|
51 |
+
await dbConnect();
|
52 |
+
const project = await Project.findOne({
|
53 |
+
user_id: user.id,
|
54 |
+
namespace,
|
55 |
+
repoId,
|
56 |
+
}).lean();
|
57 |
+
|
58 |
+
if (!project) {
|
59 |
+
return null;
|
60 |
+
}
|
61 |
+
|
62 |
+
return JSON.parse(JSON.stringify(project)) as ProjectType;
|
63 |
+
}
|
app/api/{ask → ask-ai}/route.ts
RENAMED
@@ -4,27 +4,16 @@ import { NextResponse } from "next/server";
|
|
4 |
import { headers } from "next/headers";
|
5 |
import { InferenceClient } from "@huggingface/inference";
|
6 |
|
7 |
-
import { MODELS } from "@/lib/providers";
|
8 |
import {
|
9 |
DIVIDER,
|
10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
11 |
INITIAL_SYSTEM_PROMPT,
|
12 |
MAX_REQUESTS_PER_IP,
|
13 |
-
NEW_PAGE_END,
|
14 |
-
NEW_PAGE_START,
|
15 |
REPLACE_END,
|
16 |
SEARCH_START,
|
17 |
-
UPDATE_PAGE_START,
|
18 |
-
UPDATE_PAGE_END,
|
19 |
-
PROMPT_FOR_PROJECT_NAME,
|
20 |
} from "@/lib/prompts";
|
21 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
22 |
-
import { Page } from "@/types";
|
23 |
-
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
24 |
-
import { isAuthenticated } from "@/lib/auth";
|
25 |
-
import { getBestProvider } from "@/lib/best-provider";
|
26 |
-
import { rewritePrompt } from "@/lib/rewrite-prompt";
|
27 |
-
import { COLORS } from "@/lib/utils";
|
28 |
|
29 |
const ipAddresses = new Map();
|
30 |
|
@@ -33,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|
33 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
34 |
|
35 |
const body = await request.json();
|
36 |
-
const { prompt, provider, model, redesignMarkdown,
|
37 |
|
38 |
if (!model || (!prompt && !redesignMarkdown)) {
|
39 |
return NextResponse.json(
|
@@ -45,7 +34,6 @@ export async function POST(request: NextRequest) {
|
|
45 |
const selectedModel = MODELS.find(
|
46 |
(m) => m.value === model || m.label === model
|
47 |
);
|
48 |
-
|
49 |
if (!selectedModel) {
|
50 |
return NextResponse.json(
|
51 |
{ ok: false, error: "Invalid model selected" },
|
@@ -97,19 +85,19 @@ export async function POST(request: NextRequest) {
|
|
97 |
billTo = "huggingface";
|
98 |
}
|
99 |
|
100 |
-
const
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
rewrittenPrompt = await rewritePrompt(prompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider);
|
106 |
-
}
|
107 |
|
108 |
try {
|
|
|
109 |
const encoder = new TextEncoder();
|
110 |
const stream = new TransformStream();
|
111 |
const writer = stream.writable.getWriter();
|
112 |
|
|
|
113 |
const response = new NextResponse(stream.readable, {
|
114 |
headers: {
|
115 |
"Content-Type": "text/plain; charset=utf-8",
|
@@ -119,13 +107,13 @@ export async function POST(request: NextRequest) {
|
|
119 |
});
|
120 |
|
121 |
(async () => {
|
122 |
-
|
123 |
try {
|
124 |
const client = new InferenceClient(token);
|
125 |
const chatCompletion = client.chatCompletionStream(
|
126 |
{
|
127 |
model: selectedModel.value,
|
128 |
-
provider: selectedProvider.
|
129 |
messages: [
|
130 |
{
|
131 |
role: "system",
|
@@ -133,28 +121,61 @@ export async function POST(request: NextRequest) {
|
|
133 |
},
|
134 |
{
|
135 |
role: "user",
|
136 |
-
content:
|
|
|
|
|
|
|
|
|
137 |
},
|
138 |
],
|
139 |
-
max_tokens:
|
140 |
},
|
141 |
billTo ? { billTo } : {}
|
142 |
);
|
143 |
|
144 |
while (true) {
|
145 |
-
const { done, value } = await chatCompletion.next()
|
146 |
if (done) {
|
147 |
break;
|
148 |
}
|
149 |
|
150 |
const chunk = value.choices[0]?.delta?.content;
|
151 |
if (chunk) {
|
152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
}
|
154 |
}
|
155 |
-
|
156 |
-
// Explicitly close the writer after successful completion
|
157 |
-
await writer.close();
|
158 |
} catch (error: any) {
|
159 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
160 |
await writer.write(
|
@@ -166,18 +187,7 @@ export async function POST(request: NextRequest) {
|
|
166 |
})
|
167 |
)
|
168 |
);
|
169 |
-
} else
|
170 |
-
await writer.write(
|
171 |
-
encoder.encode(
|
172 |
-
JSON.stringify({
|
173 |
-
ok: false,
|
174 |
-
openSelectProvider: true,
|
175 |
-
message: error.message,
|
176 |
-
})
|
177 |
-
)
|
178 |
-
);
|
179 |
-
}
|
180 |
-
else {
|
181 |
await writer.write(
|
182 |
encoder.encode(
|
183 |
JSON.stringify({
|
@@ -190,12 +200,7 @@ export async function POST(request: NextRequest) {
|
|
190 |
);
|
191 |
}
|
192 |
} finally {
|
193 |
-
|
194 |
-
try {
|
195 |
-
await writer?.close();
|
196 |
-
} catch {
|
197 |
-
// Ignore errors when closing the writer as it might already be closed
|
198 |
-
}
|
199 |
}
|
200 |
})();
|
201 |
|
@@ -214,20 +219,14 @@ export async function POST(request: NextRequest) {
|
|
214 |
}
|
215 |
|
216 |
export async function PUT(request: NextRequest) {
|
217 |
-
const user = await isAuthenticated();
|
218 |
-
if (user instanceof NextResponse || !user) {
|
219 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
220 |
-
}
|
221 |
-
|
222 |
const authHeaders = await headers();
|
|
|
223 |
|
224 |
const body = await request.json();
|
225 |
-
const { prompt,
|
226 |
body;
|
227 |
|
228 |
-
|
229 |
-
|
230 |
-
if (!prompt || pages.length === 0) {
|
231 |
return NextResponse.json(
|
232 |
{ ok: false, error: "Missing required fields" },
|
233 |
{ status: 400 }
|
@@ -244,7 +243,7 @@ export async function PUT(request: NextRequest) {
|
|
244 |
);
|
245 |
}
|
246 |
|
247 |
-
let token =
|
248 |
let billTo: string | null = null;
|
249 |
|
250 |
/**
|
@@ -279,56 +278,45 @@ export async function PUT(request: NextRequest) {
|
|
279 |
|
280 |
const client = new InferenceClient(token);
|
281 |
|
282 |
-
|
283 |
-
const
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
// Helper function to create flexible HTML regex that handles varying spaces
|
288 |
-
const createFlexibleHtmlRegex = (searchBlock: string) => {
|
289 |
-
let searchRegex = escapeRegExp(searchBlock)
|
290 |
-
.replace(/\s+/g, '\\s*') // Allow any amount of whitespace where there are spaces
|
291 |
-
.replace(/>\s*</g, '>\\s*<') // Allow spaces between HTML tags
|
292 |
-
.replace(/\s*>/g, '\\s*>'); // Allow spaces before closing >
|
293 |
-
|
294 |
-
return new RegExp(searchRegex, 'g');
|
295 |
-
};
|
296 |
-
|
297 |
-
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
298 |
|
299 |
try {
|
300 |
const response = await client.chatCompletion(
|
301 |
{
|
302 |
model: selectedModel.value,
|
303 |
-
provider: selectedProvider.
|
304 |
messages: [
|
305 |
{
|
306 |
role: "system",
|
307 |
-
content: FOLLOW_UP_SYSTEM_PROMPT
|
308 |
},
|
309 |
{
|
310 |
role: "user",
|
311 |
-
content:
|
312 |
-
?
|
313 |
: "You are modifying the HTML file based on the user's request.",
|
314 |
},
|
315 |
{
|
316 |
role: "assistant",
|
317 |
|
318 |
-
content:
|
319 |
selectedElementHtml
|
320 |
-
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n
|
321 |
: ""
|
322 |
-
}
|
323 |
},
|
324 |
{
|
325 |
role: "user",
|
326 |
content: prompt,
|
327 |
},
|
328 |
],
|
329 |
-
...(selectedProvider.
|
330 |
? {
|
331 |
-
max_tokens:
|
332 |
}
|
333 |
: {}),
|
334 |
},
|
@@ -345,234 +333,61 @@ export async function PUT(request: NextRequest) {
|
|
345 |
|
346 |
if (chunk) {
|
347 |
const updatedLines: number[][] = [];
|
348 |
-
let newHtml =
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
358 |
-
if (pageIndex !== -1) {
|
359 |
-
let pageHtml = updatedPages[pageIndex].html;
|
360 |
-
|
361 |
-
let processedContent = pageContent;
|
362 |
-
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
363 |
-
if (htmlMatch) {
|
364 |
-
processedContent = htmlMatch[1];
|
365 |
-
}
|
366 |
-
let position = 0;
|
367 |
-
let moreBlocks = true;
|
368 |
-
|
369 |
-
while (moreBlocks) {
|
370 |
-
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
371 |
-
if (searchStartIndex === -1) {
|
372 |
-
moreBlocks = false;
|
373 |
-
continue;
|
374 |
-
}
|
375 |
-
|
376 |
-
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
377 |
-
if (dividerIndex === -1) {
|
378 |
-
moreBlocks = false;
|
379 |
-
continue;
|
380 |
-
}
|
381 |
-
|
382 |
-
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
383 |
-
if (replaceEndIndex === -1) {
|
384 |
-
moreBlocks = false;
|
385 |
-
continue;
|
386 |
-
}
|
387 |
-
|
388 |
-
const searchBlock = processedContent.substring(
|
389 |
-
searchStartIndex + SEARCH_START.length,
|
390 |
-
dividerIndex
|
391 |
-
);
|
392 |
-
const replaceBlock = processedContent.substring(
|
393 |
-
dividerIndex + DIVIDER.length,
|
394 |
-
replaceEndIndex
|
395 |
-
);
|
396 |
-
|
397 |
-
if (searchBlock.trim() === "") {
|
398 |
-
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
399 |
-
updatedLines.push([1, replaceBlock.split("\n").length]);
|
400 |
-
} else {
|
401 |
-
const regex = createFlexibleHtmlRegex(searchBlock);
|
402 |
-
const match = regex.exec(pageHtml);
|
403 |
-
|
404 |
-
if (match) {
|
405 |
-
const matchedText = match[0];
|
406 |
-
const beforeText = pageHtml.substring(0, match.index);
|
407 |
-
const startLineNumber = beforeText.split("\n").length;
|
408 |
-
const replaceLines = replaceBlock.split("\n").length;
|
409 |
-
const endLineNumber = startLineNumber + replaceLines - 1;
|
410 |
-
|
411 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
412 |
-
pageHtml = pageHtml.replace(matchedText, replaceBlock);
|
413 |
-
}
|
414 |
-
}
|
415 |
-
|
416 |
-
position = replaceEndIndex + REPLACE_END.length;
|
417 |
-
}
|
418 |
-
|
419 |
-
updatedPages[pageIndex].html = pageHtml;
|
420 |
-
|
421 |
-
if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
|
422 |
-
newHtml = pageHtml;
|
423 |
-
}
|
424 |
}
|
425 |
-
}
|
426 |
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
const [, pagePath, pageContent] = newPageMatch;
|
432 |
-
|
433 |
-
let pageHtml = pageContent;
|
434 |
-
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
435 |
-
if (htmlMatch) {
|
436 |
-
pageHtml = htmlMatch[1];
|
437 |
-
}
|
438 |
-
|
439 |
-
const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
440 |
-
|
441 |
-
if (existingPageIndex !== -1) {
|
442 |
-
updatedPages[existingPageIndex] = {
|
443 |
-
path: pagePath,
|
444 |
-
html: pageHtml.trim()
|
445 |
-
};
|
446 |
-
} else {
|
447 |
-
updatedPages.push({
|
448 |
-
path: pagePath,
|
449 |
-
html: pageHtml.trim()
|
450 |
-
});
|
451 |
}
|
452 |
-
}
|
453 |
-
|
454 |
-
if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
|
455 |
-
let position = 0;
|
456 |
-
let moreBlocks = true;
|
457 |
-
|
458 |
-
while (moreBlocks) {
|
459 |
-
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
460 |
-
if (searchStartIndex === -1) {
|
461 |
-
moreBlocks = false;
|
462 |
-
continue;
|
463 |
-
}
|
464 |
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
472 |
-
if (replaceEndIndex === -1) {
|
473 |
-
moreBlocks = false;
|
474 |
-
continue;
|
475 |
-
}
|
476 |
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
501 |
-
newHtml = newHtml.replace(matchedText, replaceBlock);
|
502 |
-
}
|
503 |
}
|
504 |
-
|
505 |
-
position = replaceEndIndex + REPLACE_END.length;
|
506 |
}
|
507 |
|
508 |
-
|
509 |
-
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
510 |
-
if (mainPageIndex !== -1) {
|
511 |
-
updatedPages[mainPageIndex].html = newHtml;
|
512 |
-
}
|
513 |
}
|
514 |
|
515 |
-
const files: File[] = [];
|
516 |
-
updatedPages.forEach((page: Page) => {
|
517 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
518 |
-
files.push(file);
|
519 |
-
});
|
520 |
-
|
521 |
-
if (isNew) {
|
522 |
-
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
523 |
-
const formattedTitle = projectName?.toLowerCase()
|
524 |
-
.replace(/[^a-z0-9]+/g, "-")
|
525 |
-
.split("-")
|
526 |
-
.filter(Boolean)
|
527 |
-
.join("-")
|
528 |
-
.slice(0, 96);
|
529 |
-
const repo: RepoDesignation = {
|
530 |
-
type: "space",
|
531 |
-
name: `${user.name}/${formattedTitle}`,
|
532 |
-
};
|
533 |
-
const { repoUrl} = await createRepo({
|
534 |
-
repo,
|
535 |
-
accessToken: user.token as string,
|
536 |
-
});
|
537 |
-
repoId = repoUrl.split("/").slice(-2).join("/");
|
538 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
539 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
540 |
-
const README = `---
|
541 |
-
title: ${projectName}
|
542 |
-
colorFrom: ${colorFrom}
|
543 |
-
colorTo: ${colorTo}
|
544 |
-
emoji: 🐳
|
545 |
-
sdk: static
|
546 |
-
pinned: false
|
547 |
-
tags:
|
548 |
-
- deepsite-v3
|
549 |
-
---
|
550 |
-
|
551 |
-
# Welcome to your new DeepSite project!
|
552 |
-
This project was created with [DeepSite](https://deepsite.hf.co).
|
553 |
-
`;
|
554 |
-
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
555 |
-
}
|
556 |
-
|
557 |
-
const response = await uploadFiles({
|
558 |
-
repo: {
|
559 |
-
type: "space",
|
560 |
-
name: repoId,
|
561 |
-
},
|
562 |
-
files,
|
563 |
-
commitTitle: prompt,
|
564 |
-
accessToken: user.token as string,
|
565 |
-
});
|
566 |
-
|
567 |
return NextResponse.json({
|
568 |
ok: true,
|
|
|
569 |
updatedLines,
|
570 |
-
pages: updatedPages,
|
571 |
-
repoId,
|
572 |
-
commit: {
|
573 |
-
...response.commit,
|
574 |
-
title: prompt,
|
575 |
-
}
|
576 |
});
|
577 |
} else {
|
578 |
return NextResponse.json(
|
@@ -602,4 +417,3 @@ This project was created with [DeepSite](https://deepsite.hf.co).
|
|
602 |
);
|
603 |
}
|
604 |
}
|
605 |
-
|
|
|
4 |
import { headers } from "next/headers";
|
5 |
import { InferenceClient } from "@huggingface/inference";
|
6 |
|
7 |
+
import { MODELS, PROVIDERS } from "@/lib/providers";
|
8 |
import {
|
9 |
DIVIDER,
|
10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
11 |
INITIAL_SYSTEM_PROMPT,
|
12 |
MAX_REQUESTS_PER_IP,
|
|
|
|
|
13 |
REPLACE_END,
|
14 |
SEARCH_START,
|
|
|
|
|
|
|
15 |
} from "@/lib/prompts";
|
16 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
const ipAddresses = new Map();
|
19 |
|
|
|
22 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
23 |
|
24 |
const body = await request.json();
|
25 |
+
const { prompt, provider, model, redesignMarkdown, html } = body;
|
26 |
|
27 |
if (!model || (!prompt && !redesignMarkdown)) {
|
28 |
return NextResponse.json(
|
|
|
34 |
const selectedModel = MODELS.find(
|
35 |
(m) => m.value === model || m.label === model
|
36 |
);
|
|
|
37 |
if (!selectedModel) {
|
38 |
return NextResponse.json(
|
39 |
{ ok: false, error: "Invalid model selected" },
|
|
|
85 |
billTo = "huggingface";
|
86 |
}
|
87 |
|
88 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
89 |
+
const selectedProvider =
|
90 |
+
provider === "auto"
|
91 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
92 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
|
|
|
|
93 |
|
94 |
try {
|
95 |
+
// Create a stream response
|
96 |
const encoder = new TextEncoder();
|
97 |
const stream = new TransformStream();
|
98 |
const writer = stream.writable.getWriter();
|
99 |
|
100 |
+
// Start the response
|
101 |
const response = new NextResponse(stream.readable, {
|
102 |
headers: {
|
103 |
"Content-Type": "text/plain; charset=utf-8",
|
|
|
107 |
});
|
108 |
|
109 |
(async () => {
|
110 |
+
let completeResponse = "";
|
111 |
try {
|
112 |
const client = new InferenceClient(token);
|
113 |
const chatCompletion = client.chatCompletionStream(
|
114 |
{
|
115 |
model: selectedModel.value,
|
116 |
+
provider: selectedProvider.id as any,
|
117 |
messages: [
|
118 |
{
|
119 |
role: "system",
|
|
|
121 |
},
|
122 |
{
|
123 |
role: "user",
|
124 |
+
content: redesignMarkdown
|
125 |
+
? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
|
126 |
+
: html
|
127 |
+
? `Here is my current HTML code:\n\n\`\`\`html\n${html}\n\`\`\`\n\nNow, please create a new design based on this HTML.`
|
128 |
+
: prompt,
|
129 |
},
|
130 |
],
|
131 |
+
max_tokens: selectedProvider.max_tokens,
|
132 |
},
|
133 |
billTo ? { billTo } : {}
|
134 |
);
|
135 |
|
136 |
while (true) {
|
137 |
+
const { done, value } = await chatCompletion.next();
|
138 |
if (done) {
|
139 |
break;
|
140 |
}
|
141 |
|
142 |
const chunk = value.choices[0]?.delta?.content;
|
143 |
if (chunk) {
|
144 |
+
let newChunk = chunk;
|
145 |
+
if (!selectedModel?.isThinker) {
|
146 |
+
if (provider !== "sambanova") {
|
147 |
+
await writer.write(encoder.encode(chunk));
|
148 |
+
completeResponse += chunk;
|
149 |
+
|
150 |
+
if (completeResponse.includes("</html>")) {
|
151 |
+
break;
|
152 |
+
}
|
153 |
+
} else {
|
154 |
+
if (chunk.includes("</html>")) {
|
155 |
+
newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
|
156 |
+
}
|
157 |
+
completeResponse += newChunk;
|
158 |
+
await writer.write(encoder.encode(newChunk));
|
159 |
+
if (newChunk.includes("</html>")) {
|
160 |
+
break;
|
161 |
+
}
|
162 |
+
}
|
163 |
+
} else {
|
164 |
+
const lastThinkTagIndex =
|
165 |
+
completeResponse.lastIndexOf("</think>");
|
166 |
+
completeResponse += newChunk;
|
167 |
+
await writer.write(encoder.encode(newChunk));
|
168 |
+
if (lastThinkTagIndex !== -1) {
|
169 |
+
const afterLastThinkTag = completeResponse.slice(
|
170 |
+
lastThinkTagIndex + "</think>".length
|
171 |
+
);
|
172 |
+
if (afterLastThinkTag.includes("</html>")) {
|
173 |
+
break;
|
174 |
+
}
|
175 |
+
}
|
176 |
+
}
|
177 |
}
|
178 |
}
|
|
|
|
|
|
|
179 |
} catch (error: any) {
|
180 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
181 |
await writer.write(
|
|
|
187 |
})
|
188 |
)
|
189 |
);
|
190 |
+
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
await writer.write(
|
192 |
encoder.encode(
|
193 |
JSON.stringify({
|
|
|
200 |
);
|
201 |
}
|
202 |
} finally {
|
203 |
+
await writer?.close();
|
|
|
|
|
|
|
|
|
|
|
204 |
}
|
205 |
})();
|
206 |
|
|
|
219 |
}
|
220 |
|
221 |
export async function PUT(request: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
222 |
const authHeaders = await headers();
|
223 |
+
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
224 |
|
225 |
const body = await request.json();
|
226 |
+
const { prompt, html, previousPrompt, provider, selectedElementHtml, model } =
|
227 |
body;
|
228 |
|
229 |
+
if (!prompt || !html) {
|
|
|
|
|
230 |
return NextResponse.json(
|
231 |
{ ok: false, error: "Missing required fields" },
|
232 |
{ status: 400 }
|
|
|
243 |
);
|
244 |
}
|
245 |
|
246 |
+
let token = userToken;
|
247 |
let billTo: string | null = null;
|
248 |
|
249 |
/**
|
|
|
278 |
|
279 |
const client = new InferenceClient(token);
|
280 |
|
281 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
282 |
+
const selectedProvider =
|
283 |
+
provider === "auto"
|
284 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
285 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
286 |
|
287 |
try {
|
288 |
const response = await client.chatCompletion(
|
289 |
{
|
290 |
model: selectedModel.value,
|
291 |
+
provider: selectedProvider.id as any,
|
292 |
messages: [
|
293 |
{
|
294 |
role: "system",
|
295 |
+
content: FOLLOW_UP_SYSTEM_PROMPT,
|
296 |
},
|
297 |
{
|
298 |
role: "user",
|
299 |
+
content: previousPrompt
|
300 |
+
? previousPrompt
|
301 |
: "You are modifying the HTML file based on the user's request.",
|
302 |
},
|
303 |
{
|
304 |
role: "assistant",
|
305 |
|
306 |
+
content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${
|
307 |
selectedElementHtml
|
308 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
309 |
: ""
|
310 |
+
}`,
|
311 |
},
|
312 |
{
|
313 |
role: "user",
|
314 |
content: prompt,
|
315 |
},
|
316 |
],
|
317 |
+
...(selectedProvider.id !== "sambanova"
|
318 |
? {
|
319 |
+
max_tokens: selectedProvider.max_tokens,
|
320 |
}
|
321 |
: {}),
|
322 |
},
|
|
|
333 |
|
334 |
if (chunk) {
|
335 |
const updatedLines: number[][] = [];
|
336 |
+
let newHtml = html;
|
337 |
+
let position = 0;
|
338 |
+
let moreBlocks = true;
|
339 |
+
|
340 |
+
while (moreBlocks) {
|
341 |
+
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
342 |
+
if (searchStartIndex === -1) {
|
343 |
+
moreBlocks = false;
|
344 |
+
continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
345 |
}
|
|
|
346 |
|
347 |
+
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
348 |
+
if (dividerIndex === -1) {
|
349 |
+
moreBlocks = false;
|
350 |
+
continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
351 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
352 |
|
353 |
+
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
354 |
+
if (replaceEndIndex === -1) {
|
355 |
+
moreBlocks = false;
|
356 |
+
continue;
|
357 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
358 |
|
359 |
+
const searchBlock = chunk.substring(
|
360 |
+
searchStartIndex + SEARCH_START.length,
|
361 |
+
dividerIndex
|
362 |
+
);
|
363 |
+
const replaceBlock = chunk.substring(
|
364 |
+
dividerIndex + DIVIDER.length,
|
365 |
+
replaceEndIndex
|
366 |
+
);
|
367 |
|
368 |
+
if (searchBlock.trim() === "") {
|
369 |
+
newHtml = `${replaceBlock}\n${newHtml}`;
|
370 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
371 |
+
} else {
|
372 |
+
const blockPosition = newHtml.indexOf(searchBlock);
|
373 |
+
if (blockPosition !== -1) {
|
374 |
+
const beforeText = newHtml.substring(0, blockPosition);
|
375 |
+
const startLineNumber = beforeText.split("\n").length;
|
376 |
+
const replaceLines = replaceBlock.split("\n").length;
|
377 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
378 |
+
|
379 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
380 |
+
newHtml = newHtml.replace(searchBlock, replaceBlock);
|
|
|
|
|
|
|
|
|
381 |
}
|
|
|
|
|
382 |
}
|
383 |
|
384 |
+
position = replaceEndIndex + REPLACE_END.length;
|
|
|
|
|
|
|
|
|
385 |
}
|
386 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
387 |
return NextResponse.json({
|
388 |
ok: true,
|
389 |
+
html: newHtml,
|
390 |
updatedLines,
|
|
|
|
|
|
|
|
|
|
|
|
|
391 |
});
|
392 |
} else {
|
393 |
return NextResponse.json(
|
|
|
417 |
);
|
418 |
}
|
419 |
}
|
|
app/api/auth/login-url/route.ts
DELETED
@@ -1,23 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
|
3 |
-
export async function GET(req: NextRequest) {
|
4 |
-
const host = req.headers.get("host") ?? "localhost:3000";
|
5 |
-
|
6 |
-
let url: string;
|
7 |
-
if (host.includes("localhost")) {
|
8 |
-
url = host;
|
9 |
-
} else if (host.includes("hf.space") || host.includes("/spaces/enzostvs")) {
|
10 |
-
url = "enzostvs-deepsite.hf.space";
|
11 |
-
} else {
|
12 |
-
url = "deepsite.hf.co";
|
13 |
-
}
|
14 |
-
|
15 |
-
const redirect_uri =
|
16 |
-
`${host.includes("localhost") ? "http://" : "https://"}` +
|
17 |
-
url +
|
18 |
-
"/auth/callback";
|
19 |
-
|
20 |
-
const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
|
21 |
-
|
22 |
-
return NextResponse.json({ loginUrl: loginRedirectUrl });
|
23 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/logout/route.ts
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
import { NextResponse } from "next/server";
|
2 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
3 |
-
|
4 |
-
export async function POST() {
|
5 |
-
const cookieName = MY_TOKEN_KEY();
|
6 |
-
const isProduction = process.env.NODE_ENV === "production";
|
7 |
-
|
8 |
-
const response = NextResponse.json(
|
9 |
-
{ message: "Logged out successfully" },
|
10 |
-
{ status: 200 }
|
11 |
-
);
|
12 |
-
|
13 |
-
// Clear the HTTP-only cookie
|
14 |
-
const cookieOptions = [
|
15 |
-
`${cookieName}=`,
|
16 |
-
"Max-Age=0",
|
17 |
-
"Path=/",
|
18 |
-
"HttpOnly",
|
19 |
-
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
20 |
-
].join("; ");
|
21 |
-
|
22 |
-
response.headers.set("Set-Cookie", cookieOptions);
|
23 |
-
|
24 |
-
return response;
|
25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/route.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
3 |
|
4 |
export async function POST(req: NextRequest) {
|
5 |
const body = await req.json();
|
@@ -71,17 +70,11 @@ export async function POST(req: NextRequest) {
|
|
71 |
}
|
72 |
const user = await userResponse.json();
|
73 |
|
74 |
-
|
75 |
-
const isProduction = process.env.NODE_ENV === "production";
|
76 |
-
|
77 |
-
// Create response with user data
|
78 |
-
const nextResponse = NextResponse.json(
|
79 |
{
|
80 |
access_token: response.access_token,
|
81 |
expires_in: response.expires_in,
|
82 |
user,
|
83 |
-
// Include fallback flag for iframe contexts
|
84 |
-
useLocalStorageFallback: true,
|
85 |
},
|
86 |
{
|
87 |
status: 200,
|
@@ -90,17 +83,4 @@ export async function POST(req: NextRequest) {
|
|
90 |
},
|
91 |
}
|
92 |
);
|
93 |
-
|
94 |
-
// Set HTTP-only cookie with proper attributes for iframe support
|
95 |
-
const cookieOptions = [
|
96 |
-
`${cookieName}=${response.access_token}`,
|
97 |
-
`Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided
|
98 |
-
"Path=/",
|
99 |
-
"HttpOnly",
|
100 |
-
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
101 |
-
].join("; ");
|
102 |
-
|
103 |
-
nextResponse.headers.set("Set-Cookie", cookieOptions);
|
104 |
-
|
105 |
-
return nextResponse;
|
106 |
}
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
2 |
|
3 |
export async function POST(req: NextRequest) {
|
4 |
const body = await req.json();
|
|
|
70 |
}
|
71 |
const user = await userResponse.json();
|
72 |
|
73 |
+
return NextResponse.json(
|
|
|
|
|
|
|
|
|
74 |
{
|
75 |
access_token: response.access_token,
|
76 |
expires_in: response.expires_in,
|
77 |
user,
|
|
|
|
|
78 |
},
|
79 |
{
|
80 |
status: 200,
|
|
|
83 |
},
|
84 |
}
|
85 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
}
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
DELETED
@@ -1,190 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
export async function POST(
|
8 |
-
req: NextRequest,
|
9 |
-
{ params }: {
|
10 |
-
params: Promise<{
|
11 |
-
namespace: string;
|
12 |
-
repoId: string;
|
13 |
-
commitId: string;
|
14 |
-
}>
|
15 |
-
}
|
16 |
-
) {
|
17 |
-
const user = await isAuthenticated();
|
18 |
-
|
19 |
-
if (user instanceof NextResponse || !user) {
|
20 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
21 |
-
}
|
22 |
-
|
23 |
-
const param = await params;
|
24 |
-
const { namespace, repoId, commitId } = param;
|
25 |
-
|
26 |
-
try {
|
27 |
-
const repo: RepoDesignation = {
|
28 |
-
type: "space",
|
29 |
-
name: `${namespace}/${repoId}`,
|
30 |
-
};
|
31 |
-
|
32 |
-
const space = await spaceInfo({
|
33 |
-
name: `${namespace}/${repoId}`,
|
34 |
-
accessToken: user.token as string,
|
35 |
-
additionalFields: ["author"],
|
36 |
-
});
|
37 |
-
|
38 |
-
if (!space || space.sdk !== "static") {
|
39 |
-
return NextResponse.json(
|
40 |
-
{ ok: false, error: "Space is not a static space." },
|
41 |
-
{ status: 404 }
|
42 |
-
);
|
43 |
-
}
|
44 |
-
|
45 |
-
if (space.author !== user.name) {
|
46 |
-
return NextResponse.json(
|
47 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
48 |
-
{ status: 403 }
|
49 |
-
);
|
50 |
-
}
|
51 |
-
|
52 |
-
// Fetch files from the specific commit
|
53 |
-
const files: File[] = [];
|
54 |
-
const pages: Page[] = [];
|
55 |
-
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
56 |
-
const commitFilePaths: Set<string> = new Set();
|
57 |
-
|
58 |
-
// Get all files from the specific commit
|
59 |
-
for await (const fileInfo of listFiles({
|
60 |
-
repo,
|
61 |
-
accessToken: user.token as string,
|
62 |
-
revision: commitId,
|
63 |
-
})) {
|
64 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
65 |
-
|
66 |
-
if (allowedExtensions.includes(fileExtension || "")) {
|
67 |
-
commitFilePaths.add(fileInfo.path);
|
68 |
-
|
69 |
-
// Fetch the file content from the specific commit
|
70 |
-
const response = await fetch(
|
71 |
-
`https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
|
72 |
-
);
|
73 |
-
|
74 |
-
if (response.ok) {
|
75 |
-
const content = await response.text();
|
76 |
-
let mimeType = "text/plain";
|
77 |
-
|
78 |
-
switch (fileExtension) {
|
79 |
-
case "html":
|
80 |
-
mimeType = "text/html";
|
81 |
-
// Add HTML files to pages array for client-side setPages
|
82 |
-
pages.push({
|
83 |
-
path: fileInfo.path,
|
84 |
-
html: content,
|
85 |
-
});
|
86 |
-
break;
|
87 |
-
case "css":
|
88 |
-
mimeType = "text/css";
|
89 |
-
break;
|
90 |
-
case "js":
|
91 |
-
mimeType = "application/javascript";
|
92 |
-
break;
|
93 |
-
case "json":
|
94 |
-
mimeType = "application/json";
|
95 |
-
break;
|
96 |
-
case "md":
|
97 |
-
mimeType = "text/markdown";
|
98 |
-
break;
|
99 |
-
}
|
100 |
-
|
101 |
-
const file = new File([content], fileInfo.path, { type: mimeType });
|
102 |
-
files.push(file);
|
103 |
-
}
|
104 |
-
}
|
105 |
-
}
|
106 |
-
|
107 |
-
// Get files currently in main branch to identify files to delete
|
108 |
-
const mainBranchFilePaths: Set<string> = new Set();
|
109 |
-
for await (const fileInfo of listFiles({
|
110 |
-
repo,
|
111 |
-
accessToken: user.token as string,
|
112 |
-
revision: "main",
|
113 |
-
})) {
|
114 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
115 |
-
|
116 |
-
if (allowedExtensions.includes(fileExtension || "")) {
|
117 |
-
mainBranchFilePaths.add(fileInfo.path);
|
118 |
-
}
|
119 |
-
}
|
120 |
-
|
121 |
-
// Identify files to delete (exist in main but not in commit)
|
122 |
-
const filesToDelete: string[] = [];
|
123 |
-
for (const mainFilePath of mainBranchFilePaths) {
|
124 |
-
if (!commitFilePaths.has(mainFilePath)) {
|
125 |
-
filesToDelete.push(mainFilePath);
|
126 |
-
}
|
127 |
-
}
|
128 |
-
|
129 |
-
if (files.length === 0 && filesToDelete.length === 0) {
|
130 |
-
return NextResponse.json(
|
131 |
-
{ ok: false, error: "No files found in the specified commit and no files to delete" },
|
132 |
-
{ status: 404 }
|
133 |
-
);
|
134 |
-
}
|
135 |
-
|
136 |
-
// Delete files that exist in main but not in the commit being promoted
|
137 |
-
if (filesToDelete.length > 0) {
|
138 |
-
await deleteFiles({
|
139 |
-
repo,
|
140 |
-
paths: filesToDelete,
|
141 |
-
accessToken: user.token as string,
|
142 |
-
commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
|
143 |
-
commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
|
144 |
-
});
|
145 |
-
}
|
146 |
-
|
147 |
-
// Upload the files to the main branch with a promotion commit message
|
148 |
-
if (files.length > 0) {
|
149 |
-
await uploadFiles({
|
150 |
-
repo,
|
151 |
-
files,
|
152 |
-
accessToken: user.token as string,
|
153 |
-
commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
|
154 |
-
commitDescription: `Promoted commit ${commitId} to main branch`,
|
155 |
-
});
|
156 |
-
}
|
157 |
-
|
158 |
-
return NextResponse.json(
|
159 |
-
{
|
160 |
-
ok: true,
|
161 |
-
message: "Version promoted successfully",
|
162 |
-
promotedCommit: commitId,
|
163 |
-
pages: pages,
|
164 |
-
},
|
165 |
-
{ status: 200 }
|
166 |
-
);
|
167 |
-
|
168 |
-
} catch (error: any) {
|
169 |
-
|
170 |
-
// Handle specific HuggingFace API errors
|
171 |
-
if (error.statusCode === 404) {
|
172 |
-
return NextResponse.json(
|
173 |
-
{ ok: false, error: "Commit not found" },
|
174 |
-
{ status: 404 }
|
175 |
-
);
|
176 |
-
}
|
177 |
-
|
178 |
-
if (error.statusCode === 403) {
|
179 |
-
return NextResponse.json(
|
180 |
-
{ ok: false, error: "Access denied to repository" },
|
181 |
-
{ status: 403 }
|
182 |
-
);
|
183 |
-
}
|
184 |
-
|
185 |
-
return NextResponse.json(
|
186 |
-
{ ok: false, error: error.message || "Failed to promote version" },
|
187 |
-
{ status: 500 }
|
188 |
-
);
|
189 |
-
}
|
190 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/images/route.ts
DELETED
@@ -1,113 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import Project from "@/models/Project";
|
6 |
-
import dbConnect from "@/lib/mongodb";
|
7 |
-
|
8 |
-
export async function POST(
|
9 |
-
req: NextRequest,
|
10 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
11 |
-
) {
|
12 |
-
try {
|
13 |
-
const user = await isAuthenticated();
|
14 |
-
|
15 |
-
if (user instanceof NextResponse || !user) {
|
16 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
17 |
-
}
|
18 |
-
|
19 |
-
const param = await params;
|
20 |
-
const { namespace, repoId } = param;
|
21 |
-
|
22 |
-
const space = await spaceInfo({
|
23 |
-
name: `${namespace}/${repoId}`,
|
24 |
-
accessToken: user.token as string,
|
25 |
-
additionalFields: ["author"],
|
26 |
-
});
|
27 |
-
|
28 |
-
if (!space || space.sdk !== "static") {
|
29 |
-
return NextResponse.json(
|
30 |
-
{ ok: false, error: "Space is not a static space." },
|
31 |
-
{ status: 404 }
|
32 |
-
);
|
33 |
-
}
|
34 |
-
|
35 |
-
if (space.author !== user.name) {
|
36 |
-
return NextResponse.json(
|
37 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
38 |
-
{ status: 403 }
|
39 |
-
);
|
40 |
-
}
|
41 |
-
|
42 |
-
// Parse the FormData to get the images
|
43 |
-
const formData = await req.formData();
|
44 |
-
const imageFiles = formData.getAll("images") as File[];
|
45 |
-
|
46 |
-
if (!imageFiles || imageFiles.length === 0) {
|
47 |
-
return NextResponse.json(
|
48 |
-
{
|
49 |
-
ok: false,
|
50 |
-
error: "At least one image file is required under the 'images' key",
|
51 |
-
},
|
52 |
-
{ status: 400 }
|
53 |
-
);
|
54 |
-
}
|
55 |
-
|
56 |
-
const files: File[] = [];
|
57 |
-
for (const file of imageFiles) {
|
58 |
-
if (!(file instanceof File)) {
|
59 |
-
return NextResponse.json(
|
60 |
-
{
|
61 |
-
ok: false,
|
62 |
-
error: "Invalid file format - all items under 'images' key must be files",
|
63 |
-
},
|
64 |
-
{ status: 400 }
|
65 |
-
);
|
66 |
-
}
|
67 |
-
|
68 |
-
if (!file.type.startsWith('image/')) {
|
69 |
-
return NextResponse.json(
|
70 |
-
{
|
71 |
-
ok: false,
|
72 |
-
error: `File ${file.name} is not an image`,
|
73 |
-
},
|
74 |
-
{ status: 400 }
|
75 |
-
);
|
76 |
-
}
|
77 |
-
|
78 |
-
// Create File object with images/ folder prefix
|
79 |
-
const fileName = `images/${file.name}`;
|
80 |
-
const processedFile = new File([file], fileName, { type: file.type });
|
81 |
-
files.push(processedFile);
|
82 |
-
}
|
83 |
-
|
84 |
-
// Upload files to HuggingFace space
|
85 |
-
const repo: RepoDesignation = {
|
86 |
-
type: "space",
|
87 |
-
name: `${namespace}/${repoId}`,
|
88 |
-
};
|
89 |
-
|
90 |
-
await uploadFiles({
|
91 |
-
repo,
|
92 |
-
files,
|
93 |
-
accessToken: user.token as string,
|
94 |
-
commitTitle: `Upload ${files.length} image(s)`,
|
95 |
-
});
|
96 |
-
|
97 |
-
return NextResponse.json({
|
98 |
-
ok: true,
|
99 |
-
message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
|
100 |
-
uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
|
101 |
-
}, { status: 200 });
|
102 |
-
|
103 |
-
} catch (error) {
|
104 |
-
console.error('Error uploading images:', error);
|
105 |
-
return NextResponse.json(
|
106 |
-
{
|
107 |
-
ok: false,
|
108 |
-
error: "Failed to upload images",
|
109 |
-
},
|
110 |
-
{ status: 500 }
|
111 |
-
);
|
112 |
-
}
|
113 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/route.ts
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, spaceInfo,
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import
|
|
|
|
|
6 |
|
7 |
-
export async function
|
8 |
req: NextRequest,
|
9 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
10 |
) {
|
@@ -14,63 +16,24 @@ export async function DELETE(
|
|
14 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
15 |
}
|
16 |
|
|
|
17 |
const param = await params;
|
18 |
const { namespace, repoId } = param;
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
});
|
26 |
-
|
27 |
-
if (!space || space.sdk !== "static") {
|
28 |
-
return NextResponse.json(
|
29 |
-
{ ok: false, error: "Space is not a static space." },
|
30 |
-
{ status: 404 }
|
31 |
-
);
|
32 |
-
}
|
33 |
-
|
34 |
-
if (space.author !== user.name) {
|
35 |
-
return NextResponse.json(
|
36 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
37 |
-
{ status: 403 }
|
38 |
-
);
|
39 |
-
}
|
40 |
-
|
41 |
-
const repo: RepoDesignation = {
|
42 |
-
type: "space",
|
43 |
-
name: `${namespace}/${repoId}`,
|
44 |
-
};
|
45 |
-
|
46 |
-
await deleteRepo({
|
47 |
-
repo,
|
48 |
-
accessToken: user.token as string,
|
49 |
-
});
|
50 |
-
|
51 |
-
|
52 |
-
return NextResponse.json({ ok: true }, { status: 200 });
|
53 |
-
} catch (error: any) {
|
54 |
return NextResponse.json(
|
55 |
-
{
|
56 |
-
|
|
|
|
|
|
|
57 |
);
|
58 |
}
|
59 |
-
}
|
60 |
-
|
61 |
-
export async function GET(
|
62 |
-
req: NextRequest,
|
63 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
64 |
-
) {
|
65 |
-
const user = await isAuthenticated();
|
66 |
-
|
67 |
-
if (user instanceof NextResponse || !user) {
|
68 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
69 |
-
}
|
70 |
-
|
71 |
-
const param = await params;
|
72 |
-
const { namespace, repoId } = param;
|
73 |
-
|
74 |
try {
|
75 |
const space = await spaceInfo({
|
76 |
name: namespace + "/" + repoId,
|
@@ -97,75 +60,26 @@ export async function GET(
|
|
97 |
);
|
98 |
}
|
99 |
|
100 |
-
const
|
101 |
-
|
102 |
-
name: `${namespace}/${repoId}`,
|
103 |
-
};
|
104 |
-
|
105 |
-
const htmlFiles: Page[] = [];
|
106 |
-
const files: string[] = [];
|
107 |
-
|
108 |
-
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
|
109 |
-
|
110 |
-
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
111 |
-
if (fileInfo.path.endsWith(".html")) {
|
112 |
-
const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
|
113 |
-
const html = await blob?.text();
|
114 |
-
if (!html) {
|
115 |
-
continue;
|
116 |
-
}
|
117 |
-
if (fileInfo.path === "index.html") {
|
118 |
-
htmlFiles.unshift({
|
119 |
-
path: fileInfo.path,
|
120 |
-
html,
|
121 |
-
});
|
122 |
-
} else {
|
123 |
-
htmlFiles.push({
|
124 |
-
path: fileInfo.path,
|
125 |
-
html,
|
126 |
-
});
|
127 |
-
}
|
128 |
-
}
|
129 |
-
if (fileInfo.type === "directory" && fileInfo.path === "images") {
|
130 |
-
for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
131 |
-
if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
|
132 |
-
files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
|
133 |
-
}
|
134 |
-
}
|
135 |
-
}
|
136 |
-
}
|
137 |
-
const commits: Commit[] = [];
|
138 |
-
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
139 |
-
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
|
140 |
-
continue;
|
141 |
-
}
|
142 |
-
commits.push({
|
143 |
-
title: commit.title,
|
144 |
-
oid: commit.oid,
|
145 |
-
date: commit.date,
|
146 |
-
});
|
147 |
-
}
|
148 |
-
|
149 |
-
if (htmlFiles.length === 0) {
|
150 |
return NextResponse.json(
|
151 |
{
|
152 |
ok: false,
|
153 |
-
error: "
|
154 |
},
|
155 |
{ status: 404 }
|
156 |
);
|
157 |
}
|
|
|
|
|
|
|
|
|
158 |
return NextResponse.json(
|
159 |
{
|
160 |
project: {
|
161 |
-
|
162 |
-
|
163 |
-
private: space.private,
|
164 |
-
_updatedAt: space.updatedAt,
|
165 |
},
|
166 |
-
pages: htmlFiles,
|
167 |
-
files,
|
168 |
-
commits,
|
169 |
ok: true,
|
170 |
},
|
171 |
{ status: 200 }
|
@@ -174,6 +88,10 @@ export async function GET(
|
|
174 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
175 |
} catch (error: any) {
|
176 |
if (error.statusCode === 404) {
|
|
|
|
|
|
|
|
|
177 |
return NextResponse.json(
|
178 |
{ error: "Space not found", ok: false },
|
179 |
{ status: 404 }
|
@@ -185,3 +103,135 @@ export async function GET(
|
|
185 |
);
|
186 |
}
|
187 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { RepoDesignation, spaceInfo, uploadFile } from "@huggingface/hub";
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
+
import Project from "@/models/Project";
|
6 |
+
import dbConnect from "@/lib/mongodb";
|
7 |
+
import { getPTag } from "@/lib/utils";
|
8 |
|
9 |
+
export async function GET(
|
10 |
req: NextRequest,
|
11 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
12 |
) {
|
|
|
16 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
17 |
}
|
18 |
|
19 |
+
await dbConnect();
|
20 |
const param = await params;
|
21 |
const { namespace, repoId } = param;
|
22 |
|
23 |
+
const project = await Project.findOne({
|
24 |
+
user_id: user.id,
|
25 |
+
space_id: `${namespace}/${repoId}`,
|
26 |
+
}).lean();
|
27 |
+
if (!project) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
return NextResponse.json(
|
29 |
+
{
|
30 |
+
ok: false,
|
31 |
+
error: "Project not found",
|
32 |
+
},
|
33 |
+
{ status: 404 }
|
34 |
);
|
35 |
}
|
36 |
+
const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
try {
|
38 |
const space = await spaceInfo({
|
39 |
name: namespace + "/" + repoId,
|
|
|
60 |
);
|
61 |
}
|
62 |
|
63 |
+
const response = await fetch(space_url);
|
64 |
+
if (!response.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
return NextResponse.json(
|
66 |
{
|
67 |
ok: false,
|
68 |
+
error: "Failed to fetch space HTML",
|
69 |
},
|
70 |
{ status: 404 }
|
71 |
);
|
72 |
}
|
73 |
+
let html = await response.text();
|
74 |
+
// remove the last p tag including this url https://enzostvs-deepsite.hf.space
|
75 |
+
html = html.replace(getPTag(namespace + "/" + repoId), "");
|
76 |
+
|
77 |
return NextResponse.json(
|
78 |
{
|
79 |
project: {
|
80 |
+
...project,
|
81 |
+
html,
|
|
|
|
|
82 |
},
|
|
|
|
|
|
|
83 |
ok: true,
|
84 |
},
|
85 |
{ status: 200 }
|
|
|
88 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
89 |
} catch (error: any) {
|
90 |
if (error.statusCode === 404) {
|
91 |
+
await Project.deleteOne({
|
92 |
+
user_id: user.id,
|
93 |
+
space_id: `${namespace}/${repoId}`,
|
94 |
+
});
|
95 |
return NextResponse.json(
|
96 |
{ error: "Space not found", ok: false },
|
97 |
{ status: 404 }
|
|
|
103 |
);
|
104 |
}
|
105 |
}
|
106 |
+
|
107 |
+
export async function PUT(
|
108 |
+
req: NextRequest,
|
109 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
110 |
+
) {
|
111 |
+
const user = await isAuthenticated();
|
112 |
+
|
113 |
+
if (user instanceof NextResponse || !user) {
|
114 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
115 |
+
}
|
116 |
+
|
117 |
+
await dbConnect();
|
118 |
+
const param = await params;
|
119 |
+
const { namespace, repoId } = param;
|
120 |
+
const { html, prompts } = await req.json();
|
121 |
+
|
122 |
+
const project = await Project.findOne({
|
123 |
+
user_id: user.id,
|
124 |
+
space_id: `${namespace}/${repoId}`,
|
125 |
+
}).lean();
|
126 |
+
if (!project) {
|
127 |
+
return NextResponse.json(
|
128 |
+
{
|
129 |
+
ok: false,
|
130 |
+
error: "Project not found",
|
131 |
+
},
|
132 |
+
{ status: 404 }
|
133 |
+
);
|
134 |
+
}
|
135 |
+
|
136 |
+
const repo: RepoDesignation = {
|
137 |
+
type: "space",
|
138 |
+
name: `${namespace}/${repoId}`,
|
139 |
+
};
|
140 |
+
|
141 |
+
const newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
|
142 |
+
const file = new File([newHtml], "index.html", { type: "text/html" });
|
143 |
+
await uploadFile({
|
144 |
+
repo,
|
145 |
+
file,
|
146 |
+
accessToken: user.token as string,
|
147 |
+
commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
|
148 |
+
});
|
149 |
+
|
150 |
+
await Project.updateOne(
|
151 |
+
{ user_id: user.id, space_id: `${namespace}/${repoId}` },
|
152 |
+
{
|
153 |
+
$set: {
|
154 |
+
prompts: [
|
155 |
+
...(project && "prompts" in project ? project.prompts : []),
|
156 |
+
...prompts,
|
157 |
+
],
|
158 |
+
},
|
159 |
+
}
|
160 |
+
);
|
161 |
+
return NextResponse.json({ ok: true }, { status: 200 });
|
162 |
+
}
|
163 |
+
|
164 |
+
export async function POST(
|
165 |
+
req: NextRequest,
|
166 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
167 |
+
) {
|
168 |
+
const user = await isAuthenticated();
|
169 |
+
|
170 |
+
if (user instanceof NextResponse || !user) {
|
171 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
172 |
+
}
|
173 |
+
|
174 |
+
await dbConnect();
|
175 |
+
const param = await params;
|
176 |
+
const { namespace, repoId } = param;
|
177 |
+
|
178 |
+
const space = await spaceInfo({
|
179 |
+
name: namespace + "/" + repoId,
|
180 |
+
accessToken: user.token as string,
|
181 |
+
additionalFields: ["author"],
|
182 |
+
});
|
183 |
+
|
184 |
+
if (!space || space.sdk !== "static") {
|
185 |
+
return NextResponse.json(
|
186 |
+
{
|
187 |
+
ok: false,
|
188 |
+
error: "Space is not a static space",
|
189 |
+
},
|
190 |
+
{ status: 404 }
|
191 |
+
);
|
192 |
+
}
|
193 |
+
if (space.author !== user.name) {
|
194 |
+
return NextResponse.json(
|
195 |
+
{
|
196 |
+
ok: false,
|
197 |
+
error: "Space does not belong to the authenticated user",
|
198 |
+
},
|
199 |
+
{ status: 403 }
|
200 |
+
);
|
201 |
+
}
|
202 |
+
|
203 |
+
const project = await Project.findOne({
|
204 |
+
user_id: user.id,
|
205 |
+
space_id: `${namespace}/${repoId}`,
|
206 |
+
}).lean();
|
207 |
+
if (project) {
|
208 |
+
// redirect to the project page if it already exists
|
209 |
+
return NextResponse.json(
|
210 |
+
{
|
211 |
+
ok: false,
|
212 |
+
error: "Project already exists",
|
213 |
+
redirect: `/projects/${namespace}/${repoId}`,
|
214 |
+
},
|
215 |
+
{ status: 400 }
|
216 |
+
);
|
217 |
+
}
|
218 |
+
|
219 |
+
const newProject = new Project({
|
220 |
+
user_id: user.id,
|
221 |
+
space_id: `${namespace}/${repoId}`,
|
222 |
+
prompts: [],
|
223 |
+
});
|
224 |
+
|
225 |
+
await newProject.save();
|
226 |
+
return NextResponse.json(
|
227 |
+
{
|
228 |
+
ok: true,
|
229 |
+
project: {
|
230 |
+
id: newProject._id,
|
231 |
+
space_id: newProject.space_id,
|
232 |
+
prompts: newProject.prompts,
|
233 |
+
},
|
234 |
+
},
|
235 |
+
{ status: 201 }
|
236 |
+
);
|
237 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/save/route.ts
DELETED
@@ -1,64 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { uploadFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
export async function PUT(
|
8 |
-
req: NextRequest,
|
9 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
10 |
-
) {
|
11 |
-
const user = await isAuthenticated();
|
12 |
-
if (user instanceof NextResponse || !user) {
|
13 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
-
}
|
15 |
-
|
16 |
-
const param = await params;
|
17 |
-
const { namespace, repoId } = param;
|
18 |
-
const { pages, commitTitle = "Manual changes saved" } = await req.json();
|
19 |
-
|
20 |
-
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
21 |
-
return NextResponse.json(
|
22 |
-
{ ok: false, error: "Pages are required" },
|
23 |
-
{ status: 400 }
|
24 |
-
);
|
25 |
-
}
|
26 |
-
|
27 |
-
try {
|
28 |
-
// Prepare files for upload
|
29 |
-
const files: File[] = [];
|
30 |
-
pages.forEach((page: Page) => {
|
31 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
32 |
-
files.push(file);
|
33 |
-
});
|
34 |
-
|
35 |
-
// Upload files to HuggingFace Hub
|
36 |
-
const response = await uploadFiles({
|
37 |
-
repo: {
|
38 |
-
type: "space",
|
39 |
-
name: `${namespace}/${repoId}`,
|
40 |
-
},
|
41 |
-
files,
|
42 |
-
commitTitle,
|
43 |
-
accessToken: user.token as string,
|
44 |
-
});
|
45 |
-
|
46 |
-
return NextResponse.json({
|
47 |
-
ok: true,
|
48 |
-
pages,
|
49 |
-
commit: {
|
50 |
-
...response.commit,
|
51 |
-
title: commitTitle,
|
52 |
-
}
|
53 |
-
});
|
54 |
-
} catch (error: any) {
|
55 |
-
console.error("Error saving manual changes:", error);
|
56 |
-
return NextResponse.json(
|
57 |
-
{
|
58 |
-
ok: false,
|
59 |
-
error: error.message || "Failed to save changes",
|
60 |
-
},
|
61 |
-
{ status: 500 }
|
62 |
-
);
|
63 |
-
}
|
64 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/route.ts
CHANGED
@@ -1,107 +1,126 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import {
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import
|
6 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
const user = await isAuthenticated();
|
|
|
12 |
if (user instanceof NextResponse || !user) {
|
13 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
}
|
15 |
|
16 |
-
const { title
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
-
|
|
|
|
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
colorFrom: ${colorFrom}
|
37 |
colorTo: ${colorTo}
|
38 |
-
emoji: 🐳
|
39 |
sdk: static
|
40 |
pinned: false
|
41 |
tags:
|
42 |
-
- deepsite
|
43 |
---
|
44 |
|
45 |
-
|
46 |
-
This project was created with [DeepSite](https://deepsite.hf.co).
|
47 |
-
`;
|
48 |
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
54 |
-
files.push(file);
|
55 |
-
});
|
56 |
-
|
57 |
-
try {
|
58 |
-
const { repoUrl} = await createRepo({
|
59 |
-
repo,
|
60 |
-
accessToken: user.token as string,
|
61 |
});
|
62 |
-
const
|
63 |
await uploadFiles({
|
64 |
repo,
|
65 |
files,
|
66 |
accessToken: user.token as string,
|
67 |
-
commitTitle
|
68 |
});
|
69 |
-
|
70 |
const path = repoUrl.split("/").slice(-2).join("/");
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
continue;
|
76 |
-
}
|
77 |
-
commits.push({
|
78 |
-
title: commit.title,
|
79 |
-
oid: commit.oid,
|
80 |
-
date: commit.date,
|
81 |
-
});
|
82 |
-
}
|
83 |
-
|
84 |
-
const space = await spaceInfo({
|
85 |
-
name: repo.name,
|
86 |
-
accessToken: user.token as string,
|
87 |
});
|
88 |
-
|
89 |
-
|
90 |
-
files,
|
91 |
-
pages,
|
92 |
-
commits,
|
93 |
-
project: {
|
94 |
-
id: space.id,
|
95 |
-
space_id: space.name,
|
96 |
-
_updatedAt: space.updatedAt,
|
97 |
-
}
|
98 |
-
}
|
99 |
-
|
100 |
-
return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
|
101 |
} catch (err: any) {
|
102 |
return NextResponse.json(
|
103 |
{ error: err.message, ok: false },
|
104 |
{ status: 500 }
|
105 |
);
|
106 |
}
|
107 |
-
}
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
+
import Project from "@/models/Project";
|
6 |
+
import dbConnect from "@/lib/mongodb";
|
7 |
+
import { COLORS, getPTag } from "@/lib/utils";
|
8 |
+
// import type user
|
9 |
+
export async function GET() {
|
10 |
+
const user = await isAuthenticated();
|
11 |
+
|
12 |
+
if (user instanceof NextResponse || !user) {
|
13 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
+
}
|
15 |
|
16 |
+
await dbConnect();
|
17 |
+
|
18 |
+
const projects = await Project.find({
|
19 |
+
user_id: user?.id,
|
20 |
+
})
|
21 |
+
.sort({ _createdAt: -1 })
|
22 |
+
.limit(100)
|
23 |
+
.lean();
|
24 |
+
if (!projects) {
|
25 |
+
return NextResponse.json(
|
26 |
+
{
|
27 |
+
ok: false,
|
28 |
+
projects: [],
|
29 |
+
},
|
30 |
+
{ status: 404 }
|
31 |
+
);
|
32 |
+
}
|
33 |
+
return NextResponse.json(
|
34 |
+
{
|
35 |
+
ok: true,
|
36 |
+
projects,
|
37 |
+
},
|
38 |
+
{ status: 200 }
|
39 |
+
);
|
40 |
+
}
|
41 |
+
|
42 |
+
/**
|
43 |
+
* This API route creates a new project in Hugging Face Spaces.
|
44 |
+
* It requires an Authorization header with a valid token and a JSON body with the project details.
|
45 |
+
*/
|
46 |
+
export async function POST(request: NextRequest) {
|
47 |
const user = await isAuthenticated();
|
48 |
+
|
49 |
if (user instanceof NextResponse || !user) {
|
50 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
51 |
}
|
52 |
|
53 |
+
const { title, html, prompts } = await request.json();
|
54 |
+
|
55 |
+
if (!title || !html) {
|
56 |
+
return NextResponse.json(
|
57 |
+
{ message: "Title and HTML content are required.", ok: false },
|
58 |
+
{ status: 400 }
|
59 |
+
);
|
60 |
+
}
|
61 |
+
|
62 |
+
await dbConnect();
|
63 |
|
64 |
+
try {
|
65 |
+
let readme = "";
|
66 |
+
let newHtml = html;
|
67 |
|
68 |
+
const newTitle = title
|
69 |
+
.toLowerCase()
|
70 |
+
.replace(/[^a-z0-9]+/g, "-")
|
71 |
+
.split("-")
|
72 |
+
.filter(Boolean)
|
73 |
+
.join("-")
|
74 |
+
.slice(0, 96);
|
75 |
|
76 |
+
const repo: RepoDesignation = {
|
77 |
+
type: "space",
|
78 |
+
name: `${user.name}/${newTitle}`,
|
79 |
+
};
|
80 |
+
|
81 |
+
const { repoUrl } = await createRepo({
|
82 |
+
repo,
|
83 |
+
accessToken: user.token as string,
|
84 |
+
});
|
85 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
86 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
87 |
+
readme = `---
|
88 |
+
title: ${newTitle}
|
89 |
+
emoji: 🐳
|
90 |
colorFrom: ${colorFrom}
|
91 |
colorTo: ${colorTo}
|
|
|
92 |
sdk: static
|
93 |
pinned: false
|
94 |
tags:
|
95 |
+
- deepsite
|
96 |
---
|
97 |
|
98 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
|
|
|
|
|
99 |
|
100 |
+
newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
|
101 |
+
const file = new File([newHtml], "index.html", { type: "text/html" });
|
102 |
+
const readmeFile = new File([readme], "README.md", {
|
103 |
+
type: "text/markdown",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
});
|
105 |
+
const files = [file, readmeFile];
|
106 |
await uploadFiles({
|
107 |
repo,
|
108 |
files,
|
109 |
accessToken: user.token as string,
|
110 |
+
commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
|
111 |
});
|
|
|
112 |
const path = repoUrl.split("/").slice(-2).join("/");
|
113 |
+
const project = await Project.create({
|
114 |
+
user_id: user.id,
|
115 |
+
space_id: path,
|
116 |
+
prompts,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
});
|
118 |
+
return NextResponse.json({ project, path, ok: true }, { status: 201 });
|
119 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
} catch (err: any) {
|
121 |
return NextResponse.json(
|
122 |
{ error: err.message, ok: false },
|
123 |
{ status: 500 }
|
124 |
);
|
125 |
}
|
126 |
+
}
|
app/api/me/route.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
import { listSpaces } from "@huggingface/hub";
|
2 |
import { headers } from "next/headers";
|
3 |
import { NextResponse } from "next/server";
|
4 |
|
@@ -22,25 +21,5 @@ export async function GET() {
|
|
22 |
);
|
23 |
}
|
24 |
const user = await userResponse.json();
|
25 |
-
|
26 |
-
for await (const space of listSpaces({
|
27 |
-
accessToken: token.replace("Bearer ", "") as string,
|
28 |
-
additionalFields: ["author", "cardData"],
|
29 |
-
search: {
|
30 |
-
owner: user.name,
|
31 |
-
}
|
32 |
-
})) {
|
33 |
-
if (
|
34 |
-
space.sdk === "static" &&
|
35 |
-
Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
|
36 |
-
(
|
37 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
38 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
39 |
-
)
|
40 |
-
) {
|
41 |
-
projects.push(space);
|
42 |
-
}
|
43 |
-
}
|
44 |
-
|
45 |
-
return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
|
46 |
}
|
|
|
|
|
1 |
import { headers } from "next/headers";
|
2 |
import { NextResponse } from "next/server";
|
3 |
|
|
|
21 |
);
|
22 |
}
|
23 |
const user = await userResponse.json();
|
24 |
+
return NextResponse.json({ user, errCode: null }, { status: 200 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}
|
app/auth/callback/page.tsx
CHANGED
@@ -5,92 +5,67 @@ import { use, useState } from "react";
|
|
5 |
import { useMount, useTimeoutFn } from "react-use";
|
6 |
|
7 |
import { Button } from "@/components/ui/button";
|
8 |
-
import { AnimatedBlobs } from "@/components/animated-blobs";
|
9 |
-
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
10 |
export default function AuthCallback({
|
11 |
searchParams,
|
12 |
}: {
|
13 |
searchParams: Promise<{ code: string }>;
|
14 |
}) {
|
15 |
const [showButton, setShowButton] = useState(false);
|
16 |
-
const [isPopupAuth, setIsPopupAuth] = useState(false);
|
17 |
const { code } = use(searchParams);
|
18 |
const { loginFromCode } = useUser();
|
19 |
-
const { postMessage } = useBroadcastChannel("auth", () => {});
|
20 |
|
21 |
useMount(async () => {
|
22 |
if (code) {
|
23 |
-
|
24 |
-
setIsPopupAuth(isPopup);
|
25 |
-
|
26 |
-
if (isPopup) {
|
27 |
-
postMessage({
|
28 |
-
type: "user-oauth",
|
29 |
-
code: code,
|
30 |
-
});
|
31 |
-
|
32 |
-
setTimeout(() => {
|
33 |
-
if (window.opener) {
|
34 |
-
window.close();
|
35 |
-
}
|
36 |
-
}, 1000);
|
37 |
-
} else {
|
38 |
-
await loginFromCode(code);
|
39 |
-
}
|
40 |
}
|
41 |
});
|
42 |
|
43 |
-
useTimeoutFn(
|
|
|
|
|
|
|
44 |
|
45 |
return (
|
46 |
-
<div className="h-screen flex flex-col justify-center items-center
|
47 |
-
<div className="
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
53 |
-
🚀
|
54 |
-
</div>
|
55 |
-
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
56 |
-
👋
|
57 |
-
</div>
|
58 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
59 |
-
🙌
|
60 |
-
</div>
|
61 |
</div>
|
62 |
-
<
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
</p>
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
78 |
</p>
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
Go to Home
|
83 |
-
</Button>
|
84 |
-
</Link>
|
85 |
-
) : (
|
86 |
-
<p className="text-xs text-neutral-500">
|
87 |
-
Please wait, we are logging you in...
|
88 |
-
</p>
|
89 |
-
)}
|
90 |
-
</div>
|
91 |
-
</main>
|
92 |
-
</div>
|
93 |
-
<AnimatedBlobs />
|
94 |
</div>
|
95 |
</div>
|
96 |
);
|
|
|
5 |
import { useMount, useTimeoutFn } from "react-use";
|
6 |
|
7 |
import { Button } from "@/components/ui/button";
|
|
|
|
|
8 |
export default function AuthCallback({
|
9 |
searchParams,
|
10 |
}: {
|
11 |
searchParams: Promise<{ code: string }>;
|
12 |
}) {
|
13 |
const [showButton, setShowButton] = useState(false);
|
|
|
14 |
const { code } = use(searchParams);
|
15 |
const { loginFromCode } = useUser();
|
|
|
16 |
|
17 |
useMount(async () => {
|
18 |
if (code) {
|
19 |
+
await loginFromCode(code);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
});
|
22 |
|
23 |
+
useTimeoutFn(
|
24 |
+
() => setShowButton(true),
|
25 |
+
7000 // Show button after 5 seconds
|
26 |
+
);
|
27 |
|
28 |
return (
|
29 |
+
<div className="h-screen flex flex-col justify-center items-center">
|
30 |
+
<div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
|
31 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
32 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
33 |
+
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
34 |
+
🚀
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
</div>
|
36 |
+
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
37 |
+
👋
|
38 |
+
</div>
|
39 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
40 |
+
🙌
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
<p className="text-xl font-semibold text-neutral-950">
|
44 |
+
Login In Progress...
|
45 |
+
</p>
|
46 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
47 |
+
Wait a moment while we log you in with your code.
|
48 |
+
</p>
|
49 |
+
</header>
|
50 |
+
<main className="space-y-4 p-6">
|
51 |
+
<div>
|
52 |
+
<p className="text-sm text-neutral-700 mb-4 max-w-xs">
|
53 |
+
If you are not redirected automatically in the next 5 seconds,
|
54 |
+
please click the button below
|
55 |
</p>
|
56 |
+
{showButton ? (
|
57 |
+
<Link href="/">
|
58 |
+
<Button variant="black" className="relative">
|
59 |
+
Go to Home
|
60 |
+
</Button>
|
61 |
+
</Link>
|
62 |
+
) : (
|
63 |
+
<p className="text-xs text-neutral-500">
|
64 |
+
Please wait, we are logging you in...
|
65 |
</p>
|
66 |
+
)}
|
67 |
+
</div>
|
68 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
</div>
|
70 |
</div>
|
71 |
);
|
app/layout.tsx
CHANGED
@@ -2,17 +2,14 @@
|
|
2 |
import type { Metadata, Viewport } from "next";
|
3 |
import { Inter, PT_Sans } from "next/font/google";
|
4 |
import { cookies } from "next/headers";
|
5 |
-
import Script from "next/script";
|
6 |
|
|
|
7 |
import "@/assets/globals.css";
|
8 |
import { Toaster } from "@/components/ui/sonner";
|
9 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
10 |
import { apiServer } from "@/lib/api";
|
11 |
-
import IframeDetector from "@/components/iframe-detector";
|
12 |
import AppContext from "@/components/contexts/app-context";
|
13 |
-
import
|
14 |
-
import { LoginProvider } from "@/components/contexts/login-context";
|
15 |
-
import { ProProvider } from "@/components/contexts/pro-context";
|
16 |
|
17 |
const inter = Inter({
|
18 |
variable: "--font-inter-sans",
|
@@ -72,16 +69,16 @@ export const viewport: Viewport = {
|
|
72 |
async function getMe() {
|
73 |
const cookieStore = await cookies();
|
74 |
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
75 |
-
if (!token) return { user: null,
|
76 |
try {
|
77 |
const res = await apiServer.get("/me", {
|
78 |
headers: {
|
79 |
Authorization: `Bearer ${token}`,
|
80 |
},
|
81 |
});
|
82 |
-
return { user: res.data.user,
|
83 |
} catch (err: any) {
|
84 |
-
return { user: null,
|
85 |
}
|
86 |
}
|
87 |
|
@@ -101,15 +98,10 @@ export default async function RootLayout({
|
|
101 |
<body
|
102 |
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
103 |
>
|
104 |
-
<IframeDetector />
|
105 |
<Toaster richColors position="bottom-center" />
|
106 |
-
<
|
107 |
-
<AppContext me={data}>
|
108 |
-
|
109 |
-
<ProProvider>{children}</ProProvider>
|
110 |
-
</LoginProvider>
|
111 |
-
</AppContext>
|
112 |
-
</TanstackContext>
|
113 |
</body>
|
114 |
</html>
|
115 |
);
|
|
|
2 |
import type { Metadata, Viewport } from "next";
|
3 |
import { Inter, PT_Sans } from "next/font/google";
|
4 |
import { cookies } from "next/headers";
|
|
|
5 |
|
6 |
+
import TanstackProvider from "@/components/providers/tanstack-query-provider";
|
7 |
import "@/assets/globals.css";
|
8 |
import { Toaster } from "@/components/ui/sonner";
|
9 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
10 |
import { apiServer } from "@/lib/api";
|
|
|
11 |
import AppContext from "@/components/contexts/app-context";
|
12 |
+
import Script from "next/script";
|
|
|
|
|
13 |
|
14 |
const inter = Inter({
|
15 |
variable: "--font-inter-sans",
|
|
|
69 |
async function getMe() {
|
70 |
const cookieStore = await cookies();
|
71 |
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
72 |
+
if (!token) return { user: null, errCode: null };
|
73 |
try {
|
74 |
const res = await apiServer.get("/me", {
|
75 |
headers: {
|
76 |
Authorization: `Bearer ${token}`,
|
77 |
},
|
78 |
});
|
79 |
+
return { user: res.data.user, errCode: null };
|
80 |
} catch (err: any) {
|
81 |
+
return { user: null, errCode: err.status };
|
82 |
}
|
83 |
}
|
84 |
|
|
|
98 |
<body
|
99 |
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
100 |
>
|
|
|
101 |
<Toaster richColors position="bottom-center" />
|
102 |
+
<TanstackProvider>
|
103 |
+
<AppContext me={data}>{children}</AppContext>
|
104 |
+
</TanstackProvider>
|
|
|
|
|
|
|
|
|
105 |
</body>
|
106 |
</html>
|
107 |
);
|
app/projects/[namespace]/[repoId]/page.tsx
CHANGED
@@ -1,10 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import { AppEditor } from "@/components/editor";
|
2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
export default async function ProjectNamespacePage({
|
4 |
params,
|
5 |
}: {
|
6 |
params: Promise<{ namespace: string; repoId: string }>;
|
7 |
}) {
|
8 |
const { namespace, repoId } = await params;
|
9 |
-
|
|
|
|
|
|
|
|
|
10 |
}
|
|
|
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
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import { AppEditor } from "@/components/editor";
|
2 |
|
3 |
-
export default function
|
4 |
-
return <AppEditor
|
5 |
}
|
|
|
1 |
import { AppEditor } from "@/components/editor";
|
2 |
|
3 |
+
export default function ProjectsNewPage() {
|
4 |
+
return <AppEditor />;
|
5 |
}
|
assets/globals.css
CHANGED
@@ -112,10 +112,6 @@
|
|
112 |
--sidebar-ring: oklch(0.556 0 0);
|
113 |
}
|
114 |
|
115 |
-
body {
|
116 |
-
@apply scroll-smooth
|
117 |
-
}
|
118 |
-
|
119 |
@layer base {
|
120 |
* {
|
121 |
@apply border-border outline-ring/50;
|
@@ -148,224 +144,3 @@ body {
|
|
148 |
.matched-line {
|
149 |
@apply bg-sky-500/30;
|
150 |
}
|
151 |
-
|
152 |
-
/* Fast liquid deformation animations */
|
153 |
-
@keyframes liquidBlob1 {
|
154 |
-
0%, 100% {
|
155 |
-
border-radius: 40% 60% 50% 50%;
|
156 |
-
transform: scaleX(1) scaleY(1) rotate(0deg);
|
157 |
-
}
|
158 |
-
12.5% {
|
159 |
-
border-radius: 20% 80% 70% 30%;
|
160 |
-
transform: scaleX(1.6) scaleY(0.4) rotate(25deg);
|
161 |
-
}
|
162 |
-
25% {
|
163 |
-
border-radius: 80% 20% 30% 70%;
|
164 |
-
transform: scaleX(0.5) scaleY(2.1) rotate(-15deg);
|
165 |
-
}
|
166 |
-
37.5% {
|
167 |
-
border-radius: 30% 70% 80% 20%;
|
168 |
-
transform: scaleX(1.8) scaleY(0.6) rotate(40deg);
|
169 |
-
}
|
170 |
-
50% {
|
171 |
-
border-radius: 70% 30% 20% 80%;
|
172 |
-
transform: scaleX(0.4) scaleY(1.9) rotate(-30deg);
|
173 |
-
}
|
174 |
-
62.5% {
|
175 |
-
border-radius: 25% 75% 60% 40%;
|
176 |
-
transform: scaleX(1.5) scaleY(0.7) rotate(55deg);
|
177 |
-
}
|
178 |
-
75% {
|
179 |
-
border-radius: 75% 25% 40% 60%;
|
180 |
-
transform: scaleX(0.6) scaleY(1.7) rotate(-10deg);
|
181 |
-
}
|
182 |
-
87.5% {
|
183 |
-
border-radius: 50% 50% 75% 25%;
|
184 |
-
transform: scaleX(1.3) scaleY(0.8) rotate(35deg);
|
185 |
-
}
|
186 |
-
}
|
187 |
-
|
188 |
-
@keyframes liquidBlob2 {
|
189 |
-
0%, 100% {
|
190 |
-
border-radius: 60% 40% 50% 50%;
|
191 |
-
transform: scaleX(1) scaleY(1) rotate(12deg);
|
192 |
-
}
|
193 |
-
16% {
|
194 |
-
border-radius: 15% 85% 60% 40%;
|
195 |
-
transform: scaleX(0.3) scaleY(2.3) rotate(50deg);
|
196 |
-
}
|
197 |
-
32% {
|
198 |
-
border-radius: 85% 15% 25% 75%;
|
199 |
-
transform: scaleX(2.0) scaleY(0.5) rotate(-20deg);
|
200 |
-
}
|
201 |
-
48% {
|
202 |
-
border-radius: 30% 70% 85% 15%;
|
203 |
-
transform: scaleX(0.4) scaleY(1.8) rotate(70deg);
|
204 |
-
}
|
205 |
-
64% {
|
206 |
-
border-radius: 70% 30% 15% 85%;
|
207 |
-
transform: scaleX(1.9) scaleY(0.6) rotate(-35deg);
|
208 |
-
}
|
209 |
-
80% {
|
210 |
-
border-radius: 40% 60% 70% 30%;
|
211 |
-
transform: scaleX(0.7) scaleY(1.6) rotate(45deg);
|
212 |
-
}
|
213 |
-
}
|
214 |
-
|
215 |
-
@keyframes liquidBlob3 {
|
216 |
-
0%, 100% {
|
217 |
-
border-radius: 50% 50% 40% 60%;
|
218 |
-
transform: scaleX(1) scaleY(1) rotate(0deg);
|
219 |
-
}
|
220 |
-
20% {
|
221 |
-
border-radius: 10% 90% 75% 25%;
|
222 |
-
transform: scaleX(2.2) scaleY(0.3) rotate(-45deg);
|
223 |
-
}
|
224 |
-
40% {
|
225 |
-
border-radius: 90% 10% 20% 80%;
|
226 |
-
transform: scaleX(0.4) scaleY(2.5) rotate(60deg);
|
227 |
-
}
|
228 |
-
60% {
|
229 |
-
border-radius: 25% 75% 90% 10%;
|
230 |
-
transform: scaleX(1.7) scaleY(0.5) rotate(-25deg);
|
231 |
-
}
|
232 |
-
80% {
|
233 |
-
border-radius: 75% 25% 10% 90%;
|
234 |
-
transform: scaleX(0.6) scaleY(2.0) rotate(80deg);
|
235 |
-
}
|
236 |
-
}
|
237 |
-
|
238 |
-
@keyframes liquidBlob4 {
|
239 |
-
0%, 100% {
|
240 |
-
border-radius: 45% 55% 50% 50%;
|
241 |
-
transform: scaleX(1) scaleY(1) rotate(-15deg);
|
242 |
-
}
|
243 |
-
14% {
|
244 |
-
border-radius: 90% 10% 65% 35%;
|
245 |
-
transform: scaleX(0.2) scaleY(2.8) rotate(35deg);
|
246 |
-
}
|
247 |
-
28% {
|
248 |
-
border-radius: 10% 90% 20% 80%;
|
249 |
-
transform: scaleX(2.4) scaleY(0.4) rotate(-50deg);
|
250 |
-
}
|
251 |
-
42% {
|
252 |
-
border-radius: 35% 65% 90% 10%;
|
253 |
-
transform: scaleX(0.3) scaleY(2.1) rotate(70deg);
|
254 |
-
}
|
255 |
-
56% {
|
256 |
-
border-radius: 80% 20% 10% 90%;
|
257 |
-
transform: scaleX(2.0) scaleY(0.5) rotate(-40deg);
|
258 |
-
}
|
259 |
-
70% {
|
260 |
-
border-radius: 20% 80% 55% 45%;
|
261 |
-
transform: scaleX(0.5) scaleY(1.9) rotate(55deg);
|
262 |
-
}
|
263 |
-
84% {
|
264 |
-
border-radius: 65% 35% 80% 20%;
|
265 |
-
transform: scaleX(1.6) scaleY(0.6) rotate(-25deg);
|
266 |
-
}
|
267 |
-
}
|
268 |
-
|
269 |
-
/* Fast flowing movement animations */
|
270 |
-
@keyframes liquidFlow1 {
|
271 |
-
0%, 100% { transform: translate(0, 0); }
|
272 |
-
16% { transform: translate(60px, -40px); }
|
273 |
-
32% { transform: translate(-45px, -70px); }
|
274 |
-
48% { transform: translate(80px, 25px); }
|
275 |
-
64% { transform: translate(-30px, 60px); }
|
276 |
-
80% { transform: translate(50px, -20px); }
|
277 |
-
}
|
278 |
-
|
279 |
-
@keyframes liquidFlow2 {
|
280 |
-
0%, 100% { transform: translate(0, 0); }
|
281 |
-
20% { transform: translate(-70px, 50px); }
|
282 |
-
40% { transform: translate(90px, -30px); }
|
283 |
-
60% { transform: translate(-40px, -55px); }
|
284 |
-
80% { transform: translate(65px, 35px); }
|
285 |
-
}
|
286 |
-
|
287 |
-
@keyframes liquidFlow3 {
|
288 |
-
0%, 100% { transform: translate(0, 0); }
|
289 |
-
12% { transform: translate(-50px, -60px); }
|
290 |
-
24% { transform: translate(40px, -20px); }
|
291 |
-
36% { transform: translate(-30px, 70px); }
|
292 |
-
48% { transform: translate(70px, 20px); }
|
293 |
-
60% { transform: translate(-60px, -35px); }
|
294 |
-
72% { transform: translate(35px, 55px); }
|
295 |
-
84% { transform: translate(-25px, -45px); }
|
296 |
-
}
|
297 |
-
|
298 |
-
@keyframes liquidFlow4 {
|
299 |
-
0%, 100% { transform: translate(0, 0); }
|
300 |
-
14% { transform: translate(50px, 60px); }
|
301 |
-
28% { transform: translate(-80px, -40px); }
|
302 |
-
42% { transform: translate(30px, -90px); }
|
303 |
-
56% { transform: translate(-55px, 45px); }
|
304 |
-
70% { transform: translate(75px, -25px); }
|
305 |
-
84% { transform: translate(-35px, 65px); }
|
306 |
-
}
|
307 |
-
|
308 |
-
/* Light sweep animation for buttons */
|
309 |
-
@keyframes lightSweep {
|
310 |
-
0% {
|
311 |
-
transform: translateX(-150%);
|
312 |
-
opacity: 0;
|
313 |
-
}
|
314 |
-
8% {
|
315 |
-
opacity: 0.3;
|
316 |
-
}
|
317 |
-
25% {
|
318 |
-
opacity: 0.8;
|
319 |
-
}
|
320 |
-
42% {
|
321 |
-
opacity: 0.3;
|
322 |
-
}
|
323 |
-
50% {
|
324 |
-
transform: translateX(150%);
|
325 |
-
opacity: 0;
|
326 |
-
}
|
327 |
-
58% {
|
328 |
-
opacity: 0.3;
|
329 |
-
}
|
330 |
-
75% {
|
331 |
-
opacity: 0.8;
|
332 |
-
}
|
333 |
-
92% {
|
334 |
-
opacity: 0.3;
|
335 |
-
}
|
336 |
-
100% {
|
337 |
-
transform: translateX(-150%);
|
338 |
-
opacity: 0;
|
339 |
-
}
|
340 |
-
}
|
341 |
-
|
342 |
-
.light-sweep {
|
343 |
-
position: relative;
|
344 |
-
overflow: hidden;
|
345 |
-
}
|
346 |
-
|
347 |
-
.light-sweep::before {
|
348 |
-
content: '';
|
349 |
-
position: absolute;
|
350 |
-
top: 0;
|
351 |
-
left: 0;
|
352 |
-
right: 0;
|
353 |
-
bottom: 0;
|
354 |
-
width: 300%;
|
355 |
-
background: linear-gradient(
|
356 |
-
90deg,
|
357 |
-
transparent 0%,
|
358 |
-
transparent 20%,
|
359 |
-
rgba(56, 189, 248, 0.1) 35%,
|
360 |
-
rgba(56, 189, 248, 0.2) 45%,
|
361 |
-
rgba(255, 255, 255, 0.2) 50%,
|
362 |
-
rgba(168, 85, 247, 0.2) 55%,
|
363 |
-
rgba(168, 85, 247, 0.1) 65%,
|
364 |
-
transparent 80%,
|
365 |
-
transparent 100%
|
366 |
-
);
|
367 |
-
animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
368 |
-
pointer-events: none;
|
369 |
-
z-index: 1;
|
370 |
-
filter: blur(1px);
|
371 |
-
}
|
|
|
112 |
--sidebar-ring: oklch(0.556 0 0);
|
113 |
}
|
114 |
|
|
|
|
|
|
|
|
|
115 |
@layer base {
|
116 |
* {
|
117 |
@apply border-border outline-ring/50;
|
|
|
144 |
.matched-line {
|
145 |
@apply bg-sky-500/30;
|
146 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components.json
CHANGED
@@ -5,7 +5,7 @@
|
|
5 |
"tsx": true,
|
6 |
"tailwind": {
|
7 |
"config": "",
|
8 |
-
"css": "
|
9 |
"baseColor": "neutral",
|
10 |
"cssVariables": true,
|
11 |
"prefix": ""
|
|
|
5 |
"tsx": true,
|
6 |
"tailwind": {
|
7 |
"config": "",
|
8 |
+
"css": "app/globals.css",
|
9 |
"baseColor": "neutral",
|
10 |
"cssVariables": true,
|
11 |
"prefix": ""
|
components/animated-blobs/index.tsx
DELETED
@@ -1,34 +0,0 @@
|
|
1 |
-
export function AnimatedBlobs() {
|
2 |
-
return (
|
3 |
-
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
4 |
-
<div
|
5 |
-
className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl"
|
6 |
-
style={{
|
7 |
-
animation:
|
8 |
-
"liquidBlob1 4s ease-in-out infinite, liquidFlow1 6s ease-in-out infinite",
|
9 |
-
}}
|
10 |
-
/>
|
11 |
-
<div
|
12 |
-
className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10"
|
13 |
-
style={{
|
14 |
-
animation:
|
15 |
-
"liquidBlob2 5s ease-in-out infinite, liquidFlow2 7s ease-in-out infinite",
|
16 |
-
}}
|
17 |
-
/>
|
18 |
-
<div
|
19 |
-
className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10"
|
20 |
-
style={{
|
21 |
-
animation:
|
22 |
-
"liquidBlob3 3.5s ease-in-out infinite, liquidFlow3 8s ease-in-out infinite",
|
23 |
-
}}
|
24 |
-
/>
|
25 |
-
<div
|
26 |
-
className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3"
|
27 |
-
style={{
|
28 |
-
animation:
|
29 |
-
"liquidBlob4 4.5s ease-in-out infinite, liquidFlow4 6.5s ease-in-out infinite",
|
30 |
-
}}
|
31 |
-
/>
|
32 |
-
</div>
|
33 |
-
);
|
34 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/animated-text/index.tsx
DELETED
@@ -1,123 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import { useState, useEffect } from "react";
|
4 |
-
|
5 |
-
interface AnimatedTextProps {
|
6 |
-
className?: string;
|
7 |
-
}
|
8 |
-
|
9 |
-
export function AnimatedText({ className = "" }: AnimatedTextProps) {
|
10 |
-
const [displayText, setDisplayText] = useState("");
|
11 |
-
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
|
12 |
-
const [isTyping, setIsTyping] = useState(true);
|
13 |
-
const [showCursor, setShowCursor] = useState(true);
|
14 |
-
const [lastTypedIndex, setLastTypedIndex] = useState(-1);
|
15 |
-
const [animationComplete, setAnimationComplete] = useState(false);
|
16 |
-
|
17 |
-
// Randomize suggestions on each component mount
|
18 |
-
const [suggestions] = useState(() => {
|
19 |
-
const baseSuggestions = [
|
20 |
-
"create a stunning portfolio!",
|
21 |
-
"build a tic tac toe game!",
|
22 |
-
"design a website for my restaurant!",
|
23 |
-
"make a sleek landing page!",
|
24 |
-
"build an e-commerce store!",
|
25 |
-
"create a personal blog!",
|
26 |
-
"develop a modern dashboard!",
|
27 |
-
"design a company website!",
|
28 |
-
"build a todo app!",
|
29 |
-
"create an online gallery!",
|
30 |
-
"make a contact form!",
|
31 |
-
"build a weather app!",
|
32 |
-
];
|
33 |
-
|
34 |
-
// Fisher-Yates shuffle algorithm
|
35 |
-
const shuffled = [...baseSuggestions];
|
36 |
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
37 |
-
const j = Math.floor(Math.random() * (i + 1));
|
38 |
-
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
39 |
-
}
|
40 |
-
|
41 |
-
return shuffled;
|
42 |
-
});
|
43 |
-
|
44 |
-
useEffect(() => {
|
45 |
-
if (animationComplete) return;
|
46 |
-
|
47 |
-
let timeout: NodeJS.Timeout;
|
48 |
-
|
49 |
-
const typeText = () => {
|
50 |
-
const currentSuggestion = suggestions[currentSuggestionIndex];
|
51 |
-
|
52 |
-
if (isTyping) {
|
53 |
-
if (displayText.length < currentSuggestion.length) {
|
54 |
-
setDisplayText(currentSuggestion.slice(0, displayText.length + 1));
|
55 |
-
setLastTypedIndex(displayText.length);
|
56 |
-
timeout = setTimeout(typeText, 80);
|
57 |
-
} else {
|
58 |
-
// Finished typing, wait then start erasing
|
59 |
-
setLastTypedIndex(-1);
|
60 |
-
timeout = setTimeout(() => {
|
61 |
-
setIsTyping(false);
|
62 |
-
}, 2000);
|
63 |
-
}
|
64 |
-
}
|
65 |
-
};
|
66 |
-
|
67 |
-
timeout = setTimeout(typeText, 100);
|
68 |
-
return () => clearTimeout(timeout);
|
69 |
-
}, [
|
70 |
-
displayText,
|
71 |
-
currentSuggestionIndex,
|
72 |
-
isTyping,
|
73 |
-
suggestions,
|
74 |
-
animationComplete,
|
75 |
-
]);
|
76 |
-
|
77 |
-
// Cursor blinking effect
|
78 |
-
useEffect(() => {
|
79 |
-
if (animationComplete) {
|
80 |
-
setShowCursor(false);
|
81 |
-
return;
|
82 |
-
}
|
83 |
-
|
84 |
-
const cursorInterval = setInterval(() => {
|
85 |
-
setShowCursor((prev) => !prev);
|
86 |
-
}, 600);
|
87 |
-
|
88 |
-
return () => clearInterval(cursorInterval);
|
89 |
-
}, [animationComplete]);
|
90 |
-
|
91 |
-
useEffect(() => {
|
92 |
-
if (lastTypedIndex >= 0) {
|
93 |
-
const timeout = setTimeout(() => {
|
94 |
-
setLastTypedIndex(-1);
|
95 |
-
}, 400);
|
96 |
-
|
97 |
-
return () => clearTimeout(timeout);
|
98 |
-
}
|
99 |
-
}, [lastTypedIndex]);
|
100 |
-
|
101 |
-
return (
|
102 |
-
<p className={`font-mono ${className}`}>
|
103 |
-
Hey DeepSite,
|
104 |
-
{displayText.split("").map((char, index) => (
|
105 |
-
<span
|
106 |
-
key={`${currentSuggestionIndex}-${index}`}
|
107 |
-
className={`transition-colors duration-300 ${
|
108 |
-
index === lastTypedIndex ? "text-neutral-100" : ""
|
109 |
-
}`}
|
110 |
-
>
|
111 |
-
{char}
|
112 |
-
</span>
|
113 |
-
))}
|
114 |
-
<span
|
115 |
-
className={`${
|
116 |
-
showCursor ? "opacity-100" : "opacity-0"
|
117 |
-
} transition-opacity`}
|
118 |
-
>
|
119 |
-
|
|
120 |
-
</span>
|
121 |
-
</p>
|
122 |
-
);
|
123 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/contexts/app-context.tsx
CHANGED
@@ -1,11 +1,12 @@
|
|
1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
"use client";
|
3 |
-
import { useMount } from "react-use";
|
4 |
-
import { toast } from "sonner";
|
5 |
-
import { usePathname, useRouter } from "next/navigation";
|
6 |
|
7 |
import { useUser } from "@/hooks/useUser";
|
8 |
-
import {
|
|
|
|
|
|
|
|
|
9 |
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
10 |
|
11 |
export default function AppContext({
|
@@ -15,7 +16,6 @@ export default function AppContext({
|
|
15 |
children: React.ReactNode;
|
16 |
me?: {
|
17 |
user: User | null;
|
18 |
-
projects: ProjectType[];
|
19 |
errCode: number | null;
|
20 |
};
|
21 |
}) {
|
@@ -49,5 +49,9 @@ export default function AppContext({
|
|
49 |
}
|
50 |
});
|
51 |
|
52 |
-
return
|
|
|
|
|
|
|
|
|
53 |
}
|
|
|
1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
"use client";
|
|
|
|
|
|
|
3 |
|
4 |
import { useUser } from "@/hooks/useUser";
|
5 |
+
import { usePathname, useRouter } from "next/navigation";
|
6 |
+
import { useMount } from "react-use";
|
7 |
+
import { UserContext } from "@/components/contexts/user-context";
|
8 |
+
import { User } from "@/types";
|
9 |
+
import { toast } from "sonner";
|
10 |
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
11 |
|
12 |
export default function AppContext({
|
|
|
16 |
children: React.ReactNode;
|
17 |
me?: {
|
18 |
user: User | null;
|
|
|
19 |
errCode: number | null;
|
20 |
};
|
21 |
}) {
|
|
|
49 |
}
|
50 |
});
|
51 |
|
52 |
+
return (
|
53 |
+
<UserContext value={{ user, loading, logout } as any}>
|
54 |
+
{children}
|
55 |
+
</UserContext>
|
56 |
+
);
|
57 |
}
|
components/contexts/login-context.tsx
DELETED
@@ -1,61 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import React, { createContext, useContext, useState, ReactNode } from "react";
|
4 |
-
import { LoginModal } from "@/components/login-modal";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
interface LoginContextType {
|
8 |
-
isOpen: boolean;
|
9 |
-
openLoginModal: (options?: LoginModalOptions) => void;
|
10 |
-
closeLoginModal: () => void;
|
11 |
-
}
|
12 |
-
|
13 |
-
interface LoginModalOptions {
|
14 |
-
pages?: Page[];
|
15 |
-
title?: string;
|
16 |
-
description?: string;
|
17 |
-
}
|
18 |
-
|
19 |
-
const LoginContext = createContext<LoginContextType | undefined>(undefined);
|
20 |
-
|
21 |
-
export function LoginProvider({ children }: { children: ReactNode }) {
|
22 |
-
const [isOpen, setIsOpen] = useState(false);
|
23 |
-
const [modalOptions, setModalOptions] = useState<LoginModalOptions>({});
|
24 |
-
|
25 |
-
const openLoginModal = (options: LoginModalOptions = {}) => {
|
26 |
-
setModalOptions(options);
|
27 |
-
setIsOpen(true);
|
28 |
-
};
|
29 |
-
|
30 |
-
const closeLoginModal = () => {
|
31 |
-
setIsOpen(false);
|
32 |
-
setModalOptions({});
|
33 |
-
};
|
34 |
-
|
35 |
-
const value = {
|
36 |
-
isOpen,
|
37 |
-
openLoginModal,
|
38 |
-
closeLoginModal,
|
39 |
-
};
|
40 |
-
|
41 |
-
return (
|
42 |
-
<LoginContext.Provider value={value}>
|
43 |
-
{children}
|
44 |
-
<LoginModal
|
45 |
-
open={isOpen}
|
46 |
-
onClose={setIsOpen}
|
47 |
-
pages={modalOptions.pages}
|
48 |
-
title={modalOptions.title}
|
49 |
-
description={modalOptions.description}
|
50 |
-
/>
|
51 |
-
</LoginContext.Provider>
|
52 |
-
);
|
53 |
-
}
|
54 |
-
|
55 |
-
export function useLoginModal() {
|
56 |
-
const context = useContext(LoginContext);
|
57 |
-
if (context === undefined) {
|
58 |
-
throw new Error("useLoginModal must be used within a LoginProvider");
|
59 |
-
}
|
60 |
-
return context;
|
61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/contexts/pro-context.tsx
DELETED
@@ -1,48 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import React, { createContext, useContext, useState, ReactNode } from "react";
|
4 |
-
import { ProModal } from "@/components/pro-modal";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
import { useEditor } from "@/hooks/useEditor";
|
7 |
-
|
8 |
-
interface ProContextType {
|
9 |
-
isOpen: boolean;
|
10 |
-
openProModal: (pages: Page[]) => void;
|
11 |
-
closeProModal: () => void;
|
12 |
-
}
|
13 |
-
|
14 |
-
const ProContext = createContext<ProContextType | undefined>(undefined);
|
15 |
-
|
16 |
-
export function ProProvider({ children }: { children: ReactNode }) {
|
17 |
-
const [isOpen, setIsOpen] = useState(false);
|
18 |
-
const { pages } = useEditor();
|
19 |
-
|
20 |
-
const openProModal = () => {
|
21 |
-
setIsOpen(true);
|
22 |
-
};
|
23 |
-
|
24 |
-
const closeProModal = () => {
|
25 |
-
setIsOpen(false);
|
26 |
-
};
|
27 |
-
|
28 |
-
const value = {
|
29 |
-
isOpen,
|
30 |
-
openProModal,
|
31 |
-
closeProModal,
|
32 |
-
};
|
33 |
-
|
34 |
-
return (
|
35 |
-
<ProContext.Provider value={value}>
|
36 |
-
{children}
|
37 |
-
<ProModal open={isOpen} onClose={setIsOpen} pages={pages} />
|
38 |
-
</ProContext.Provider>
|
39 |
-
);
|
40 |
-
}
|
41 |
-
|
42 |
-
export function useProModal() {
|
43 |
-
const context = useContext(ProContext);
|
44 |
-
if (context === undefined) {
|
45 |
-
throw new Error("useProModal must be used within a ProProvider");
|
46 |
-
}
|
47 |
-
return context;
|
48 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/follow-up-tooltip.tsx
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
Popover,
|
3 |
+
PopoverContent,
|
4 |
+
PopoverTrigger,
|
5 |
+
} from "@/components/ui/popover";
|
6 |
+
import { Info } from "lucide-react";
|
7 |
+
|
8 |
+
export const FollowUpTooltip = () => {
|
9 |
+
return (
|
10 |
+
<Popover>
|
11 |
+
<PopoverTrigger asChild>
|
12 |
+
<Info className="size-3 text-neutral-300 cursor-pointer" />
|
13 |
+
</PopoverTrigger>
|
14 |
+
<PopoverContent
|
15 |
+
align="start"
|
16 |
+
className="!rounded-2xl !p-0 min-w-xs text-center overflow-hidden"
|
17 |
+
>
|
18 |
+
<header className="bg-neutral-950 px-4 py-3 border-b border-neutral-700/70">
|
19 |
+
<p className="text-base text-neutral-200 font-semibold">
|
20 |
+
⚡ Faster, Smarter Updates
|
21 |
+
</p>
|
22 |
+
</header>
|
23 |
+
<main className="p-4">
|
24 |
+
<p className="text-neutral-300 text-sm">
|
25 |
+
Using the Diff-Patch system, allow DeepSite to intelligently update
|
26 |
+
your project without rewritting the entire codebase.
|
27 |
+
</p>
|
28 |
+
<p className="text-neutral-500 text-sm mt-2">
|
29 |
+
This means faster updates, less data usage, and a more efficient
|
30 |
+
development process.
|
31 |
+
</p>
|
32 |
+
</main>
|
33 |
+
</PopoverContent>
|
34 |
+
</Popover>
|
35 |
+
);
|
36 |
+
};
|
components/editor/ask-ai/index.tsx
CHANGED
@@ -1,162 +1,335 @@
|
|
1 |
-
|
|
|
|
|
2 |
import classNames from "classnames";
|
3 |
-
import {
|
4 |
-
ArrowUp,
|
5 |
-
CircleStop,
|
6 |
-
Pause,
|
7 |
-
Plus,
|
8 |
-
Square,
|
9 |
-
StopCircle,
|
10 |
-
} from "lucide-react";
|
11 |
-
import { useLocalStorage } from "react-use";
|
12 |
import { toast } from "sonner";
|
|
|
|
|
|
|
13 |
|
14 |
-
import
|
15 |
-
import { useEditor } from "@/hooks/useEditor";
|
16 |
-
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
17 |
-
import { EnhancedSettings, Project } from "@/types";
|
18 |
-
import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
|
19 |
-
import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
|
20 |
-
import { AiLoading } from "@/components/editor/ask-ai/loading";
|
21 |
import { Button } from "@/components/ui/button";
|
22 |
-
import { Uploader } from "@/components/editor/ask-ai/uploader";
|
23 |
-
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
24 |
-
import { Selector } from "@/components/editor/ask-ai/selector";
|
25 |
-
import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
|
26 |
-
import { useUser } from "@/hooks/useUser";
|
27 |
-
import { useLoginModal } from "@/components/contexts/login-context";
|
28 |
-
import { Settings } from "./settings";
|
29 |
-
import { useProModal } from "@/components/contexts/pro-context";
|
30 |
import { MODELS } from "@/lib/providers";
|
31 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
-
export
|
34 |
-
|
35 |
-
|
36 |
onScrollToBottom,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
}: {
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
callAiNewProject,
|
54 |
-
callAiFollowUp,
|
55 |
-
setModel,
|
56 |
-
selectedModel,
|
57 |
-
audio: hookAudio,
|
58 |
-
cancelRequest,
|
59 |
-
} = useAi(onScrollToBottom);
|
60 |
-
const { openLoginModal } = useLoginModal();
|
61 |
-
const { openProModal } = useProModal();
|
62 |
-
const [openProvider, setOpenProvider] = useState(false);
|
63 |
-
const [providerError, setProviderError] = useState("");
|
64 |
-
|
65 |
-
const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
|
66 |
-
useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
|
67 |
-
isActive: true,
|
68 |
-
primaryColor: undefined,
|
69 |
-
secondaryColor: undefined,
|
70 |
-
theme: undefined,
|
71 |
-
});
|
72 |
|
73 |
-
const [
|
74 |
const [prompt, setPrompt] = useState("");
|
75 |
-
const [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
const [openThink, setOpenThink] = useState(false);
|
|
|
|
|
|
|
77 |
|
78 |
-
const
|
79 |
-
|
80 |
-
|
81 |
-
setOpenThink(true);
|
82 |
-
};
|
83 |
|
84 |
const callAi = async (redesignMarkdown?: string) => {
|
85 |
-
if (!user) return openLoginModal();
|
86 |
-
if (!user.isPro && projects.length >= MAX_FREE_PROJECTS)
|
87 |
-
return openProModal([]);
|
88 |
if (isAiWorking) return;
|
89 |
if (!redesignMarkdown && !prompt.trim()) return;
|
|
|
|
|
|
|
|
|
|
|
90 |
|
91 |
-
|
92 |
-
|
|
|
93 |
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
}
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
|
|
|
|
|
|
|
|
122 |
}
|
123 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
}
|
125 |
};
|
126 |
|
127 |
-
const
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
break;
|
136 |
-
case "pro_required":
|
137 |
-
openProModal([]);
|
138 |
-
break;
|
139 |
-
case "api_error":
|
140 |
-
toast.error(message || "An error occurred");
|
141 |
-
break;
|
142 |
-
case "network_error":
|
143 |
-
toast.error(message || "Network error occurred");
|
144 |
-
break;
|
145 |
-
default:
|
146 |
-
toast.error("An unexpected error occurred");
|
147 |
}
|
148 |
};
|
149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
return (
|
151 |
-
<div className="
|
152 |
-
<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-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
{selectedElement && (
|
161 |
<div className="px-4 pt-3">
|
162 |
<SelectedHtmlElement
|
@@ -167,47 +340,36 @@ export const AskAi = ({
|
|
167 |
</div>
|
168 |
)}
|
169 |
<div className="w-full relative flex items-center justify-between">
|
170 |
-
{
|
171 |
-
<div className="absolute bg-neutral-800
|
172 |
-
<
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
size="iconXs"
|
186 |
-
variant="outline"
|
187 |
-
className="!rounded-md mr-0.5"
|
188 |
-
onClick={cancelRequest}
|
189 |
-
>
|
190 |
-
<CircleStop className="size-4" />
|
191 |
-
</Button>
|
192 |
-
)}
|
193 |
</div>
|
194 |
)}
|
195 |
-
<
|
196 |
-
|
197 |
-
|
198 |
-
}
|
199 |
className={classNames(
|
200 |
-
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4
|
201 |
{
|
202 |
-
"!pt-2.5":
|
203 |
-
selectedElement &&
|
204 |
-
!(isAiWorking || isUploading || isThinking),
|
205 |
}
|
206 |
)}
|
207 |
placeholder={
|
208 |
selectedElement
|
209 |
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
210 |
-
:
|
211 |
? "Ask DeepSite for edits"
|
212 |
: "Ask DeepSite anything..."
|
213 |
}
|
@@ -220,41 +382,90 @@ export const AskAi = ({
|
|
220 |
}}
|
221 |
/>
|
222 |
</div>
|
223 |
-
<div className="flex items-center justify-between gap-2 px-4 pb-3
|
224 |
-
<div className="flex-1 flex items-center justify-start gap-1.5
|
225 |
-
<
|
226 |
-
|
227 |
-
|
228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
<Settings
|
|
|
|
|
|
|
|
|
230 |
open={openProvider}
|
231 |
error={providerError}
|
232 |
isFollowUp={!isSameHtml && isFollowUp}
|
233 |
onClose={setOpenProvider}
|
234 |
/>
|
235 |
-
{!isNew && <Uploader project={project} />}
|
236 |
-
{isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
|
237 |
-
{!isNew && <Selector />}
|
238 |
-
</div>
|
239 |
-
<div className="flex items-center justify-end gap-2">
|
240 |
<Button
|
241 |
size="iconXs"
|
242 |
-
|
243 |
-
className="!rounded-md"
|
244 |
-
disabled={
|
245 |
-
isAiWorking || isUploading || isThinking || !prompt.trim()
|
246 |
-
}
|
247 |
onClick={() => callAi()}
|
248 |
>
|
249 |
<ArrowUp className="size-4" />
|
250 |
</Button>
|
251 |
</div>
|
252 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
253 |
</div>
|
254 |
-
<audio ref={
|
255 |
<source src="/success.mp3" type="audio/mpeg" />
|
256 |
Your browser does not support the audio element.
|
257 |
</audio>
|
258 |
</div>
|
259 |
);
|
260 |
-
}
|
|
|
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 selectedModel = useMemo(() => {
|
71 |
+
return MODELS.find((m: { value: string }) => m.value === model);
|
72 |
+
}, [model]);
|
|
|
|
|
73 |
|
74 |
const callAi = async (redesignMarkdown?: string) => {
|
|
|
|
|
|
|
75 |
if (isAiWorking) return;
|
76 |
if (!redesignMarkdown && !prompt.trim()) return;
|
77 |
+
setisAiWorking(true);
|
78 |
+
setProviderError("");
|
79 |
+
setThink("");
|
80 |
+
setOpenThink(false);
|
81 |
+
setIsThinking(true);
|
82 |
|
83 |
+
let contentResponse = "";
|
84 |
+
let thinkResponse = "";
|
85 |
+
let lastRenderTime = 0;
|
86 |
|
87 |
+
const abortController = new AbortController();
|
88 |
+
setController(abortController);
|
89 |
+
try {
|
90 |
+
onNewPrompt(prompt);
|
91 |
+
if (isFollowUp && !redesignMarkdown && !isSameHtml) {
|
92 |
+
const selectedElementHtml = selectedElement
|
93 |
+
? selectedElement.outerHTML
|
94 |
+
: "";
|
95 |
+
const request = await fetch("/api/ask-ai", {
|
96 |
+
method: "PUT",
|
97 |
+
body: JSON.stringify({
|
98 |
+
prompt,
|
99 |
+
provider,
|
100 |
+
previousPrompt,
|
101 |
+
model,
|
102 |
+
html,
|
103 |
+
selectedElementHtml,
|
104 |
+
}),
|
105 |
+
headers: {
|
106 |
+
"Content-Type": "application/json",
|
107 |
+
"x-forwarded-for": window.location.hostname,
|
108 |
+
},
|
109 |
+
signal: abortController.signal,
|
110 |
+
});
|
111 |
+
if (request && request.body) {
|
112 |
+
const res = await request.json();
|
113 |
+
if (!request.ok) {
|
114 |
+
if (res.openLogin) {
|
115 |
+
setOpen(true);
|
116 |
+
} else if (res.openSelectProvider) {
|
117 |
+
setOpenProvider(true);
|
118 |
+
setProviderError(res.message);
|
119 |
+
} else if (res.openProModal) {
|
120 |
+
setOpenProModal(true);
|
121 |
+
} else {
|
122 |
+
toast.error(res.message);
|
123 |
+
}
|
124 |
+
setisAiWorking(false);
|
125 |
+
return;
|
126 |
+
}
|
127 |
+
setHtml(res.html);
|
128 |
+
toast.success("AI responded successfully");
|
129 |
+
setPreviousPrompt(prompt);
|
130 |
+
setPrompt("");
|
131 |
+
setisAiWorking(false);
|
132 |
+
onSuccess(res.html, prompt, res.updatedLines);
|
133 |
+
if (audio.current) audio.current.play();
|
134 |
}
|
135 |
+
} else {
|
136 |
+
const request = await fetch("/api/ask-ai", {
|
137 |
+
method: "POST",
|
138 |
+
body: JSON.stringify({
|
139 |
+
prompt,
|
140 |
+
provider,
|
141 |
+
model,
|
142 |
+
html: isSameHtml ? "" : html,
|
143 |
+
redesignMarkdown,
|
144 |
+
}),
|
145 |
+
headers: {
|
146 |
+
"Content-Type": "application/json",
|
147 |
+
"x-forwarded-for": window.location.hostname,
|
148 |
+
},
|
149 |
+
signal: abortController.signal,
|
150 |
+
});
|
151 |
+
if (request && request.body) {
|
152 |
+
const reader = request.body.getReader();
|
153 |
+
const decoder = new TextDecoder("utf-8");
|
154 |
+
const selectedModel = MODELS.find(
|
155 |
+
(m: { value: string }) => m.value === model
|
156 |
+
);
|
157 |
+
let contentThink: string | undefined = undefined;
|
158 |
+
const read = async () => {
|
159 |
+
const { done, value } = await reader.read();
|
160 |
+
if (done) {
|
161 |
+
const isJson =
|
162 |
+
contentResponse.trim().startsWith("{") &&
|
163 |
+
contentResponse.trim().endsWith("}");
|
164 |
+
const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
|
165 |
+
if (jsonResponse && !jsonResponse.ok) {
|
166 |
+
if (jsonResponse.openLogin) {
|
167 |
+
setOpen(true);
|
168 |
+
} else if (jsonResponse.openSelectProvider) {
|
169 |
+
setOpenProvider(true);
|
170 |
+
setProviderError(jsonResponse.message);
|
171 |
+
} else if (jsonResponse.openProModal) {
|
172 |
+
setOpenProModal(true);
|
173 |
+
} else {
|
174 |
+
toast.error(jsonResponse.message);
|
175 |
+
}
|
176 |
+
setisAiWorking(false);
|
177 |
+
return;
|
178 |
+
}
|
179 |
|
180 |
+
toast.success("AI responded successfully");
|
181 |
+
setPreviousPrompt(prompt);
|
182 |
+
setPrompt("");
|
183 |
+
setisAiWorking(false);
|
184 |
+
setHasAsked(true);
|
185 |
+
if (selectedModel?.isThinker) {
|
186 |
+
setModel(MODELS[0].value);
|
187 |
+
}
|
188 |
+
if (audio.current) audio.current.play();
|
189 |
+
|
190 |
+
// Now we have the complete HTML including </html>, so set it to be sure
|
191 |
+
const finalDoc = contentResponse.match(
|
192 |
+
/<!DOCTYPE html>[\s\S]*<\/html>/
|
193 |
+
)?.[0];
|
194 |
+
if (finalDoc) {
|
195 |
+
setHtml(finalDoc);
|
196 |
+
}
|
197 |
+
onSuccess(finalDoc ?? contentResponse, prompt);
|
198 |
+
|
199 |
+
return;
|
200 |
+
}
|
201 |
+
|
202 |
+
const chunk = decoder.decode(value, { stream: true });
|
203 |
+
thinkResponse += chunk;
|
204 |
+
if (selectedModel?.isThinker) {
|
205 |
+
const thinkMatch = thinkResponse.match(/<think>[\s\S]*/)?.[0];
|
206 |
+
if (thinkMatch && !thinkResponse?.includes("</think>")) {
|
207 |
+
if ((contentThink?.length ?? 0) < 3) {
|
208 |
+
setOpenThink(true);
|
209 |
+
}
|
210 |
+
setThink(thinkMatch.replace("<think>", "").trim());
|
211 |
+
contentThink += chunk;
|
212 |
+
return read();
|
213 |
+
}
|
214 |
+
}
|
215 |
+
|
216 |
+
contentResponse += chunk;
|
217 |
+
|
218 |
+
const newHtml = contentResponse.match(
|
219 |
+
/<!DOCTYPE html>[\s\S]*/
|
220 |
+
)?.[0];
|
221 |
+
if (newHtml) {
|
222 |
+
setIsThinking(false);
|
223 |
+
let partialDoc = newHtml;
|
224 |
+
if (
|
225 |
+
partialDoc.includes("<head>") &&
|
226 |
+
!partialDoc.includes("</head>")
|
227 |
+
) {
|
228 |
+
partialDoc += "\n</head>";
|
229 |
+
}
|
230 |
+
if (
|
231 |
+
partialDoc.includes("<body") &&
|
232 |
+
!partialDoc.includes("</body>")
|
233 |
+
) {
|
234 |
+
partialDoc += "\n</body>";
|
235 |
+
}
|
236 |
+
if (!partialDoc.includes("</html>")) {
|
237 |
+
partialDoc += "\n</html>";
|
238 |
+
}
|
239 |
+
|
240 |
+
// Throttle the re-renders to avoid flashing/flicker
|
241 |
+
const now = Date.now();
|
242 |
+
if (now - lastRenderTime > 300) {
|
243 |
+
setHtml(partialDoc);
|
244 |
+
lastRenderTime = now;
|
245 |
+
}
|
246 |
|
247 |
+
if (partialDoc.length > 200) {
|
248 |
+
onScrollToBottom();
|
249 |
+
}
|
250 |
+
}
|
251 |
+
read();
|
252 |
+
};
|
253 |
+
|
254 |
+
read();
|
255 |
}
|
256 |
}
|
257 |
+
} catch (error: any) {
|
258 |
+
setisAiWorking(false);
|
259 |
+
toast.error(error.message);
|
260 |
+
if (error.openLogin) {
|
261 |
+
setOpen(true);
|
262 |
+
}
|
263 |
}
|
264 |
};
|
265 |
|
266 |
+
const stopController = () => {
|
267 |
+
if (controller) {
|
268 |
+
controller.abort();
|
269 |
+
setController(null);
|
270 |
+
setisAiWorking(false);
|
271 |
+
setThink("");
|
272 |
+
setOpenThink(false);
|
273 |
+
setIsThinking(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
274 |
}
|
275 |
};
|
276 |
|
277 |
+
useUpdateEffect(() => {
|
278 |
+
if (refThink.current) {
|
279 |
+
refThink.current.scrollTop = refThink.current.scrollHeight;
|
280 |
+
}
|
281 |
+
}, [think]);
|
282 |
+
|
283 |
+
useUpdateEffect(() => {
|
284 |
+
if (!isThinking) {
|
285 |
+
setOpenThink(false);
|
286 |
+
}
|
287 |
+
}, [isThinking]);
|
288 |
+
|
289 |
+
const isSameHtml = useMemo(() => {
|
290 |
+
return isTheSameHtml(html);
|
291 |
+
}, [html]);
|
292 |
+
|
293 |
return (
|
294 |
+
<div className="px-3">
|
295 |
+
<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">
|
296 |
+
{think && (
|
297 |
+
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
298 |
+
<header
|
299 |
+
className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
|
300 |
+
onClick={() => {
|
301 |
+
setOpenThink(!openThink);
|
302 |
+
}}
|
303 |
+
>
|
304 |
+
<p className="text-sm font-medium text-neutral-300 group-hover:text-neutral-200 transition-colors duration-200">
|
305 |
+
{isThinking ? "DeepSite is thinking..." : "DeepSite's plan"}
|
306 |
+
</p>
|
307 |
+
<ChevronDown
|
308 |
+
className={classNames(
|
309 |
+
"size-4 text-neutral-400 group-hover:text-neutral-300 transition-all duration-200",
|
310 |
+
{
|
311 |
+
"rotate-180": openThink,
|
312 |
+
}
|
313 |
+
)}
|
314 |
+
/>
|
315 |
+
</header>
|
316 |
+
<main
|
317 |
+
ref={refThink}
|
318 |
+
className={classNames(
|
319 |
+
"overflow-y-auto transition-all duration-200 ease-in-out",
|
320 |
+
{
|
321 |
+
"max-h-[0px]": !openThink,
|
322 |
+
"min-h-[250px] max-h-[250px] border-t border-neutral-700":
|
323 |
+
openThink,
|
324 |
+
}
|
325 |
+
)}
|
326 |
+
>
|
327 |
+
<p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
|
328 |
+
{think}
|
329 |
+
</p>
|
330 |
+
</main>
|
331 |
+
</div>
|
332 |
+
)}
|
333 |
{selectedElement && (
|
334 |
<div className="px-4 pt-3">
|
335 |
<SelectedHtmlElement
|
|
|
340 |
</div>
|
341 |
)}
|
342 |
<div className="w-full relative flex items-center justify-between">
|
343 |
+
{isAiWorking && (
|
344 |
+
<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">
|
345 |
+
<div className="flex items-center justify-start gap-2">
|
346 |
+
<Loading overlay={false} className="!size-4" />
|
347 |
+
<p className="text-neutral-400 text-sm">
|
348 |
+
AI is {isThinking ? "thinking" : "coding"}...{" "}
|
349 |
+
</p>
|
350 |
+
</div>
|
351 |
+
<div
|
352 |
+
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"
|
353 |
+
onClick={stopController}
|
354 |
+
>
|
355 |
+
<FaStopCircle />
|
356 |
+
Stop generation
|
357 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
358 |
</div>
|
359 |
)}
|
360 |
+
<input
|
361 |
+
type="text"
|
362 |
+
disabled={isAiWorking}
|
|
|
363 |
className={classNames(
|
364 |
+
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
|
365 |
{
|
366 |
+
"!pt-2.5": selectedElement && !isAiWorking,
|
|
|
|
|
367 |
}
|
368 |
)}
|
369 |
placeholder={
|
370 |
selectedElement
|
371 |
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
372 |
+
: hasAsked
|
373 |
? "Ask DeepSite for edits"
|
374 |
: "Ask DeepSite anything..."
|
375 |
}
|
|
|
382 |
}}
|
383 |
/>
|
384 |
</div>
|
385 |
+
<div className="flex items-center justify-between gap-2 px-4 pb-3">
|
386 |
+
<div className="flex-1 flex items-center justify-start gap-1.5">
|
387 |
+
<ReImagine onRedesign={(md) => callAi(md)} />
|
388 |
+
{!isSameHtml && (
|
389 |
+
<Tooltip>
|
390 |
+
<TooltipTrigger asChild>
|
391 |
+
<Button
|
392 |
+
size="xs"
|
393 |
+
variant={isEditableModeEnabled ? "default" : "outline"}
|
394 |
+
onClick={() => {
|
395 |
+
setIsEditableModeEnabled?.(!isEditableModeEnabled);
|
396 |
+
}}
|
397 |
+
className={classNames("h-[28px]", {
|
398 |
+
"!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
|
399 |
+
!isEditableModeEnabled,
|
400 |
+
})}
|
401 |
+
>
|
402 |
+
<Crosshair className="size-4" />
|
403 |
+
Edit
|
404 |
+
</Button>
|
405 |
+
</TooltipTrigger>
|
406 |
+
<TooltipContent
|
407 |
+
align="start"
|
408 |
+
className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
|
409 |
+
>
|
410 |
+
Select an element on the page to ask DeepSite edit it
|
411 |
+
directly.
|
412 |
+
</TooltipContent>
|
413 |
+
</Tooltip>
|
414 |
+
)}
|
415 |
+
<InviteFriends />
|
416 |
+
</div>
|
417 |
+
<div className="flex items-center justify-end gap-2">
|
418 |
<Settings
|
419 |
+
provider={provider as string}
|
420 |
+
model={model as string}
|
421 |
+
onChange={setProvider}
|
422 |
+
onModelChange={setModel}
|
423 |
open={openProvider}
|
424 |
error={providerError}
|
425 |
isFollowUp={!isSameHtml && isFollowUp}
|
426 |
onClose={setOpenProvider}
|
427 |
/>
|
|
|
|
|
|
|
|
|
|
|
428 |
<Button
|
429 |
size="iconXs"
|
430 |
+
disabled={isAiWorking || !prompt.trim()}
|
|
|
|
|
|
|
|
|
431 |
onClick={() => callAi()}
|
432 |
>
|
433 |
<ArrowUp className="size-4" />
|
434 |
</Button>
|
435 |
</div>
|
436 |
</div>
|
437 |
+
<LoginModal open={open} onClose={() => setOpen(false)} html={html} />
|
438 |
+
<ProModal
|
439 |
+
html={html}
|
440 |
+
open={openProModal}
|
441 |
+
onClose={() => setOpenProModal(false)}
|
442 |
+
/>
|
443 |
+
{!isSameHtml && (
|
444 |
+
<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">
|
445 |
+
<label
|
446 |
+
htmlFor="diff-patch-checkbox"
|
447 |
+
className="flex items-center gap-1.5 cursor-pointer"
|
448 |
+
>
|
449 |
+
<Checkbox
|
450 |
+
id="diff-patch-checkbox"
|
451 |
+
checked={isFollowUp}
|
452 |
+
onCheckedChange={(e) => {
|
453 |
+
if (e === true && !isSameHtml && selectedModel?.isThinker) {
|
454 |
+
setModel(MODELS[0].value);
|
455 |
+
}
|
456 |
+
setIsFollowUp(e === true);
|
457 |
+
}}
|
458 |
+
/>
|
459 |
+
Diff-Patch Update
|
460 |
+
</label>
|
461 |
+
<FollowUpTooltip />
|
462 |
+
</div>
|
463 |
+
)}
|
464 |
</div>
|
465 |
+
<audio ref={audio} id="audio" className="hidden">
|
466 |
<source src="/success.mp3" type="audio/mpeg" />
|
467 |
Your browser does not support the audio element.
|
468 |
</audio>
|
469 |
</div>
|
470 |
);
|
471 |
+
}
|
components/editor/ask-ai/loading.tsx
DELETED
@@ -1,59 +0,0 @@
|
|
1 |
-
import Loading from "@/components/loading";
|
2 |
-
import { useState } from "react";
|
3 |
-
import { useInterval } from "react-use";
|
4 |
-
|
5 |
-
const TEXTS = [
|
6 |
-
"Teaching pixels to dance with style...",
|
7 |
-
"AI is having a creative breakthrough...",
|
8 |
-
"Channeling digital vibes into pure code...",
|
9 |
-
"Summoning the website spirits...",
|
10 |
-
"Brewing some algorithmic magic...",
|
11 |
-
"Composing a symphony of divs and spans...",
|
12 |
-
"Riding the wave of computational creativity...",
|
13 |
-
"Aligning the stars for perfect design...",
|
14 |
-
"Training circus animals to write CSS...",
|
15 |
-
"Launching ideas into the digital stratosphere...",
|
16 |
-
];
|
17 |
-
|
18 |
-
export const AiLoading = ({
|
19 |
-
text,
|
20 |
-
className,
|
21 |
-
}: {
|
22 |
-
text?: string;
|
23 |
-
className?: string;
|
24 |
-
}) => {
|
25 |
-
const [selectedText, setSelectedText] = useState(
|
26 |
-
text ?? TEXTS[Math.floor(Math.random() * TEXTS.length)]
|
27 |
-
);
|
28 |
-
useInterval(() => {
|
29 |
-
if (!text) {
|
30 |
-
if (selectedText === TEXTS[TEXTS.length - 1]) {
|
31 |
-
setSelectedText(TEXTS[0]);
|
32 |
-
} else {
|
33 |
-
setSelectedText(TEXTS[TEXTS.indexOf(selectedText) + 1]);
|
34 |
-
}
|
35 |
-
}
|
36 |
-
}, 12000);
|
37 |
-
return (
|
38 |
-
<div className={`flex items-center justify-start gap-2 ${className}`}>
|
39 |
-
<Loading overlay={false} className="!size-5 opacity-50" />
|
40 |
-
<p className="text-neutral-400 text-sm">
|
41 |
-
<span className="inline-flex">
|
42 |
-
{selectedText.split("").map((char, index) => (
|
43 |
-
<span
|
44 |
-
key={index}
|
45 |
-
className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
|
46 |
-
style={{
|
47 |
-
animationDelay: `${index * 0.1}s`,
|
48 |
-
animationDuration: "1.3s",
|
49 |
-
animationIterationCount: "infinite",
|
50 |
-
}}
|
51 |
-
>
|
52 |
-
{char === " " ? "\u00A0" : char}
|
53 |
-
</span>
|
54 |
-
))}
|
55 |
-
</span>
|
56 |
-
</p>
|
57 |
-
</div>
|
58 |
-
);
|
59 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/content-modal.tsx
DELETED
@@ -1,196 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { ChevronRight, RefreshCcw } from "lucide-react";
|
3 |
-
import { useState } from "react";
|
4 |
-
import { TailwindColors } from "./tailwind-colors";
|
5 |
-
import { Switch } from "@/components/ui/switch";
|
6 |
-
import { Button } from "@/components/ui/button";
|
7 |
-
import { Themes } from "./themes";
|
8 |
-
import { EnhancedSettings } from "@/types";
|
9 |
-
|
10 |
-
export const ContentModal = ({
|
11 |
-
enhancedSettings,
|
12 |
-
setEnhancedSettings,
|
13 |
-
}: {
|
14 |
-
enhancedSettings: EnhancedSettings;
|
15 |
-
setEnhancedSettings: (settings: EnhancedSettings) => void;
|
16 |
-
}) => {
|
17 |
-
const [collapsed, setCollapsed] = useState(["colors", "theme"]);
|
18 |
-
return (
|
19 |
-
<main className="overflow-x-hidden max-h-[50dvh] overflow-y-auto">
|
20 |
-
<section className="w-full border-b border-neutral-800/80 px-6 py-3.5 sticky top-0 bg-neutral-900 z-10">
|
21 |
-
<div className="flex items-center justify-between gap-3">
|
22 |
-
<p className="text-base font-semibold text-neutral-200">
|
23 |
-
Allow DeepSite to enhance your prompt
|
24 |
-
</p>
|
25 |
-
<Switch
|
26 |
-
checked={enhancedSettings.isActive}
|
27 |
-
onCheckedChange={() =>
|
28 |
-
setEnhancedSettings({
|
29 |
-
...enhancedSettings,
|
30 |
-
isActive: !enhancedSettings.isActive,
|
31 |
-
})
|
32 |
-
}
|
33 |
-
/>
|
34 |
-
</div>
|
35 |
-
<p className="text-sm text-neutral-500 mt-2">
|
36 |
-
While using DeepSite enhanced prompt, you'll get better results. We'll
|
37 |
-
add more details and features to your request.
|
38 |
-
</p>
|
39 |
-
<div className="text-sm text-sky-500 mt-3 bg-gradient-to-r from-sky-400/15 to-purple-400/15 rounded-md px-3 py-2 border border-white/10">
|
40 |
-
<p className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text">
|
41 |
-
You can also use the custom properties below to set specific
|
42 |
-
information.
|
43 |
-
</p>
|
44 |
-
</div>
|
45 |
-
</section>
|
46 |
-
<section className="py-3.5 border-b border-neutral-800/80">
|
47 |
-
<div
|
48 |
-
className={classNames(
|
49 |
-
"flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
|
50 |
-
{
|
51 |
-
"!text-neutral-200": collapsed.includes("colors"),
|
52 |
-
}
|
53 |
-
)}
|
54 |
-
onClick={() =>
|
55 |
-
setCollapsed((prev) => {
|
56 |
-
if (prev.includes("colors")) {
|
57 |
-
return prev.filter((item) => item !== "colors");
|
58 |
-
}
|
59 |
-
return [...prev, "colors"];
|
60 |
-
})
|
61 |
-
}
|
62 |
-
>
|
63 |
-
<ChevronRight className="size-4" />
|
64 |
-
<p className="text-base font-semibold">Colors</p>
|
65 |
-
</div>
|
66 |
-
{collapsed.includes("colors") && (
|
67 |
-
<div className="mt-4 space-y-4">
|
68 |
-
<article className="w-full">
|
69 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
70 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
71 |
-
Primary Color
|
72 |
-
</p>
|
73 |
-
<Button
|
74 |
-
variant="bordered"
|
75 |
-
size="xss"
|
76 |
-
className={`${
|
77 |
-
enhancedSettings.primaryColor ? "" : "opacity-0"
|
78 |
-
}`}
|
79 |
-
onClick={() =>
|
80 |
-
setEnhancedSettings({
|
81 |
-
...enhancedSettings,
|
82 |
-
primaryColor: undefined,
|
83 |
-
})
|
84 |
-
}
|
85 |
-
>
|
86 |
-
<RefreshCcw className="size-2.5" />
|
87 |
-
Reset
|
88 |
-
</Button>
|
89 |
-
</div>
|
90 |
-
<div className="text-muted-foreground text-sm mt-4">
|
91 |
-
<TailwindColors
|
92 |
-
value={enhancedSettings.primaryColor}
|
93 |
-
onChange={(value) =>
|
94 |
-
setEnhancedSettings({
|
95 |
-
...enhancedSettings,
|
96 |
-
primaryColor: value,
|
97 |
-
})
|
98 |
-
}
|
99 |
-
/>
|
100 |
-
</div>
|
101 |
-
</article>
|
102 |
-
<article className="w-full">
|
103 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
104 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
105 |
-
Secondary Color
|
106 |
-
</p>
|
107 |
-
<Button
|
108 |
-
variant="bordered"
|
109 |
-
size="xss"
|
110 |
-
className={`${
|
111 |
-
enhancedSettings.secondaryColor ? "" : "opacity-0"
|
112 |
-
}`}
|
113 |
-
onClick={() =>
|
114 |
-
setEnhancedSettings({
|
115 |
-
...enhancedSettings,
|
116 |
-
secondaryColor: undefined,
|
117 |
-
})
|
118 |
-
}
|
119 |
-
>
|
120 |
-
<RefreshCcw className="size-2.5" />
|
121 |
-
Reset
|
122 |
-
</Button>
|
123 |
-
</div>
|
124 |
-
<div className="text-muted-foreground text-sm mt-4">
|
125 |
-
<TailwindColors
|
126 |
-
value={enhancedSettings.secondaryColor}
|
127 |
-
onChange={(value) =>
|
128 |
-
setEnhancedSettings({
|
129 |
-
...enhancedSettings,
|
130 |
-
secondaryColor: value,
|
131 |
-
})
|
132 |
-
}
|
133 |
-
/>
|
134 |
-
</div>
|
135 |
-
</article>
|
136 |
-
</div>
|
137 |
-
)}
|
138 |
-
</section>
|
139 |
-
<section className="py-3.5 border-b border-neutral-800/80">
|
140 |
-
<div
|
141 |
-
className={classNames(
|
142 |
-
"flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
|
143 |
-
{
|
144 |
-
"!text-neutral-200": collapsed.includes("theme"),
|
145 |
-
}
|
146 |
-
)}
|
147 |
-
onClick={() =>
|
148 |
-
setCollapsed((prev) => {
|
149 |
-
if (prev.includes("theme")) {
|
150 |
-
return prev.filter((item) => item !== "theme");
|
151 |
-
}
|
152 |
-
return [...prev, "theme"];
|
153 |
-
})
|
154 |
-
}
|
155 |
-
>
|
156 |
-
<ChevronRight className="size-4" />
|
157 |
-
<p className="text-base font-semibold">Theme</p>
|
158 |
-
</div>
|
159 |
-
{collapsed.includes("theme") && (
|
160 |
-
<article className="w-full mt-4">
|
161 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
162 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
163 |
-
Theme
|
164 |
-
</p>
|
165 |
-
<Button
|
166 |
-
variant="bordered"
|
167 |
-
size="xss"
|
168 |
-
className={`${enhancedSettings.theme ? "" : "opacity-0"}`}
|
169 |
-
onClick={() =>
|
170 |
-
setEnhancedSettings({
|
171 |
-
...enhancedSettings,
|
172 |
-
theme: undefined,
|
173 |
-
})
|
174 |
-
}
|
175 |
-
>
|
176 |
-
<RefreshCcw className="size-2.5" />
|
177 |
-
Reset
|
178 |
-
</Button>
|
179 |
-
</div>
|
180 |
-
<div className="text-muted-foreground text-sm mt-4">
|
181 |
-
<Themes
|
182 |
-
value={enhancedSettings.theme}
|
183 |
-
onChange={(value) =>
|
184 |
-
setEnhancedSettings({
|
185 |
-
...enhancedSettings,
|
186 |
-
theme: value,
|
187 |
-
})
|
188 |
-
}
|
189 |
-
/>
|
190 |
-
</div>
|
191 |
-
</article>
|
192 |
-
)}
|
193 |
-
</section>
|
194 |
-
</main>
|
195 |
-
);
|
196 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/index.tsx
DELETED
@@ -1,73 +0,0 @@
|
|
1 |
-
import { useState } from "react";
|
2 |
-
import { WandSparkles } from "lucide-react";
|
3 |
-
|
4 |
-
import { Button } from "@/components/ui/button";
|
5 |
-
import { useEditor } from "@/hooks/useEditor";
|
6 |
-
import { useAi } from "@/hooks/useAi";
|
7 |
-
import {
|
8 |
-
Dialog,
|
9 |
-
DialogContent,
|
10 |
-
DialogFooter,
|
11 |
-
DialogTitle,
|
12 |
-
} from "@/components/ui/dialog";
|
13 |
-
import { ContentModal } from "./content-modal";
|
14 |
-
import { useLoginModal } from "@/components/contexts/login-context";
|
15 |
-
import { useUser } from "@/hooks/useUser";
|
16 |
-
import { EnhancedSettings } from "@/types";
|
17 |
-
|
18 |
-
export const PromptBuilder = ({
|
19 |
-
enhancedSettings,
|
20 |
-
setEnhancedSettings,
|
21 |
-
}: {
|
22 |
-
enhancedSettings: EnhancedSettings;
|
23 |
-
setEnhancedSettings: (settings: EnhancedSettings) => void;
|
24 |
-
}) => {
|
25 |
-
const { user } = useUser();
|
26 |
-
const { openLoginModal } = useLoginModal();
|
27 |
-
const { globalAiLoading } = useAi();
|
28 |
-
const { globalEditorLoading } = useEditor();
|
29 |
-
|
30 |
-
const [open, setOpen] = useState(false);
|
31 |
-
return (
|
32 |
-
<>
|
33 |
-
<Button
|
34 |
-
size="xs"
|
35 |
-
variant="outline"
|
36 |
-
className="!rounded-md !border-white/10 !bg-gradient-to-r from-sky-400/15 to-purple-400/15 light-sweep hover:brightness-110"
|
37 |
-
disabled={globalAiLoading || globalEditorLoading}
|
38 |
-
onClick={() => {
|
39 |
-
if (!user) return openLoginModal();
|
40 |
-
setOpen(true);
|
41 |
-
}}
|
42 |
-
>
|
43 |
-
<WandSparkles className="size-3.5 text-sky-500 relative z-10" />
|
44 |
-
<span className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text relative z-10">
|
45 |
-
Enhance
|
46 |
-
</span>
|
47 |
-
</Button>
|
48 |
-
<Dialog open={open} onOpenChange={() => setOpen(false)}>
|
49 |
-
<DialogContent className="sm:max-w-xl !p-0 !rounded-3xl !bg-neutral-900 !border-neutral-800/80 !gap-0">
|
50 |
-
<DialogTitle className="px-6 py-3.5 border-b border-neutral-800">
|
51 |
-
<div className="flex items-center justify-start gap-2 text-neutral-200 text-base font-medium">
|
52 |
-
<WandSparkles className="size-3.5" />
|
53 |
-
<p>Enhance Prompt</p>
|
54 |
-
</div>
|
55 |
-
</DialogTitle>
|
56 |
-
<ContentModal
|
57 |
-
enhancedSettings={enhancedSettings}
|
58 |
-
setEnhancedSettings={setEnhancedSettings}
|
59 |
-
/>
|
60 |
-
<DialogFooter className="px-6 py-3.5 border-t border-neutral-800">
|
61 |
-
<Button
|
62 |
-
variant="bordered"
|
63 |
-
size="default"
|
64 |
-
onClick={() => setOpen(false)}
|
65 |
-
>
|
66 |
-
Close
|
67 |
-
</Button>
|
68 |
-
</DialogFooter>
|
69 |
-
</DialogContent>
|
70 |
-
</Dialog>
|
71 |
-
</>
|
72 |
-
);
|
73 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/tailwind-colors.tsx
DELETED
@@ -1,58 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { useRef } from "react";
|
3 |
-
|
4 |
-
import { TAILWIND_COLORS } from "@/lib/prompt-builder";
|
5 |
-
import { useMount } from "react-use";
|
6 |
-
|
7 |
-
export const TailwindColors = ({
|
8 |
-
value,
|
9 |
-
onChange,
|
10 |
-
}: {
|
11 |
-
value: string | undefined;
|
12 |
-
onChange: (value: string) => void;
|
13 |
-
}) => {
|
14 |
-
const ref = useRef<HTMLDivElement>(null);
|
15 |
-
|
16 |
-
useMount(() => {
|
17 |
-
if (ref.current) {
|
18 |
-
if (value) {
|
19 |
-
const color = ref.current.querySelector(`[data-color="${value}"]`);
|
20 |
-
if (color) {
|
21 |
-
color.scrollIntoView({ inline: "center" });
|
22 |
-
}
|
23 |
-
}
|
24 |
-
}
|
25 |
-
});
|
26 |
-
return (
|
27 |
-
<div
|
28 |
-
ref={ref}
|
29 |
-
className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
|
30 |
-
>
|
31 |
-
{TAILWIND_COLORS.map((color) => (
|
32 |
-
<div
|
33 |
-
key={color}
|
34 |
-
className={classNames(
|
35 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-16 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
36 |
-
{
|
37 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
38 |
-
value === color,
|
39 |
-
}
|
40 |
-
)}
|
41 |
-
data-color={color}
|
42 |
-
onClick={() => onChange(color)}
|
43 |
-
>
|
44 |
-
<div
|
45 |
-
className={`w-4 h-4 min-w-4 min-h-4 rounded-xl ${
|
46 |
-
["white", "black"].includes(color)
|
47 |
-
? `bg-${color}`
|
48 |
-
: `bg-${color}-500`
|
49 |
-
}`}
|
50 |
-
/>
|
51 |
-
<p className="text-xs capitalize text-neutral-200 truncate">
|
52 |
-
{color}
|
53 |
-
</p>
|
54 |
-
</div>
|
55 |
-
))}
|
56 |
-
</div>
|
57 |
-
);
|
58 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/themes.tsx
DELETED
@@ -1,48 +0,0 @@
|
|
1 |
-
import { Theme } from "@/types";
|
2 |
-
import classNames from "classnames";
|
3 |
-
import { Moon, Sun } from "lucide-react";
|
4 |
-
import { useRef } from "react";
|
5 |
-
|
6 |
-
export const Themes = ({
|
7 |
-
value,
|
8 |
-
onChange,
|
9 |
-
}: {
|
10 |
-
value: Theme;
|
11 |
-
onChange: (value: Theme) => void;
|
12 |
-
}) => {
|
13 |
-
const ref = useRef<HTMLDivElement>(null);
|
14 |
-
|
15 |
-
return (
|
16 |
-
<div
|
17 |
-
ref={ref}
|
18 |
-
className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
|
19 |
-
>
|
20 |
-
<div
|
21 |
-
className={classNames(
|
22 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
23 |
-
{
|
24 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
25 |
-
value === "light",
|
26 |
-
}
|
27 |
-
)}
|
28 |
-
onClick={() => onChange("light")}
|
29 |
-
>
|
30 |
-
<Sun className="size-4 text-amber-500" />
|
31 |
-
<p className="text-xs capitalize text-neutral-200 truncate">Light</p>
|
32 |
-
</div>
|
33 |
-
<div
|
34 |
-
className={classNames(
|
35 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
36 |
-
{
|
37 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
38 |
-
value === "dark",
|
39 |
-
}
|
40 |
-
)}
|
41 |
-
onClick={() => onChange("dark")}
|
42 |
-
>
|
43 |
-
<Moon className="size-4 text-indigo-500" />
|
44 |
-
<p className="text-xs capitalize text-neutral-200 truncate">Dark</p>
|
45 |
-
</div>
|
46 |
-
</div>
|
47 |
-
);
|
48 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/re-imagine.tsx
CHANGED
@@ -11,10 +11,6 @@ import {
|
|
11 |
import { Input } from "@/components/ui/input";
|
12 |
import Loading from "@/components/loading";
|
13 |
import { api } from "@/lib/api";
|
14 |
-
import { useAi } from "@/hooks/useAi";
|
15 |
-
import { useEditor } from "@/hooks/useEditor";
|
16 |
-
import { useUser } from "@/hooks/useUser";
|
17 |
-
import { useLoginModal } from "@/components/contexts/login-context";
|
18 |
|
19 |
export function ReImagine({
|
20 |
onRedesign,
|
@@ -24,10 +20,6 @@ export function ReImagine({
|
|
24 |
const [url, setUrl] = useState<string>("");
|
25 |
const [open, setOpen] = useState(false);
|
26 |
const [isLoading, setIsLoading] = useState(false);
|
27 |
-
const { globalAiLoading } = useAi();
|
28 |
-
const { globalEditorLoading } = useEditor();
|
29 |
-
const { user } = useUser();
|
30 |
-
const { openLoginModal } = useLoginModal();
|
31 |
|
32 |
const checkIfUrlIsValid = (url: string) => {
|
33 |
const urlPattern = new RegExp(
|
@@ -62,31 +54,16 @@ export function ReImagine({
|
|
62 |
setIsLoading(false);
|
63 |
};
|
64 |
|
65 |
-
if (!user)
|
66 |
-
return (
|
67 |
-
<Button
|
68 |
-
size="xs"
|
69 |
-
variant="outline"
|
70 |
-
className="!rounded-md"
|
71 |
-
onClick={() => openLoginModal()}
|
72 |
-
>
|
73 |
-
<Paintbrush className="size-3.5" />
|
74 |
-
Redesign
|
75 |
-
</Button>
|
76 |
-
);
|
77 |
-
|
78 |
return (
|
79 |
<Popover open={open} onOpenChange={setOpen}>
|
80 |
<form>
|
81 |
<PopoverTrigger asChild>
|
82 |
<Button
|
83 |
-
size="
|
84 |
-
variant=
|
85 |
-
className="!
|
86 |
-
disabled={globalAiLoading || globalEditorLoading}
|
87 |
>
|
88 |
-
<Paintbrush className="size-
|
89 |
-
Redesign
|
90 |
</Button>
|
91 |
</PopoverTrigger>
|
92 |
<PopoverContent
|
|
|
11 |
import { Input } from "@/components/ui/input";
|
12 |
import Loading from "@/components/loading";
|
13 |
import { api } from "@/lib/api";
|
|
|
|
|
|
|
|
|
14 |
|
15 |
export function ReImagine({
|
16 |
onRedesign,
|
|
|
20 |
const [url, setUrl] = useState<string>("");
|
21 |
const [open, setOpen] = useState(false);
|
22 |
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
|
|
|
23 |
|
24 |
const checkIfUrlIsValid = (url: string) => {
|
25 |
const urlPattern = new RegExp(
|
|
|
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
|
components/editor/ask-ai/selected-files.tsx
DELETED
@@ -1,47 +0,0 @@
|
|
1 |
-
import Image from "next/image";
|
2 |
-
|
3 |
-
import { Button } from "@/components/ui/button";
|
4 |
-
import { Minus } from "lucide-react";
|
5 |
-
|
6 |
-
export const SelectedFiles = ({
|
7 |
-
files,
|
8 |
-
isAiWorking,
|
9 |
-
onDelete,
|
10 |
-
}: {
|
11 |
-
files: string[];
|
12 |
-
isAiWorking: boolean;
|
13 |
-
onDelete: (file: string) => void;
|
14 |
-
}) => {
|
15 |
-
if (files.length === 0) return null;
|
16 |
-
return (
|
17 |
-
<div className="px-4 pt-3">
|
18 |
-
<div className="flex items-center justify-start gap-2">
|
19 |
-
{files.map((file) => (
|
20 |
-
<div
|
21 |
-
key={file}
|
22 |
-
className="flex items-center relative justify-start gap-2 p-1 bg-neutral-700 rounded-md"
|
23 |
-
>
|
24 |
-
<Image
|
25 |
-
src={file}
|
26 |
-
alt="uploaded image"
|
27 |
-
className="size-12 rounded-md object-cover"
|
28 |
-
width={40}
|
29 |
-
height={40}
|
30 |
-
/>
|
31 |
-
<Button
|
32 |
-
size="iconXsss"
|
33 |
-
variant="secondary"
|
34 |
-
className={`absolute top-0.5 right-0.5 ${
|
35 |
-
isAiWorking ? "opacity-50 !cursor-not-allowed" : ""
|
36 |
-
}`}
|
37 |
-
disabled={isAiWorking}
|
38 |
-
onClick={() => onDelete(file)}
|
39 |
-
>
|
40 |
-
<Minus className="size-4" />
|
41 |
-
</Button>
|
42 |
-
</div>
|
43 |
-
))}
|
44 |
-
</div>
|
45 |
-
</div>
|
46 |
-
);
|
47 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/selector.tsx
DELETED
@@ -1,41 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { Crosshair } from "lucide-react";
|
3 |
-
|
4 |
-
import { Button } from "@/components/ui/button";
|
5 |
-
import {
|
6 |
-
Tooltip,
|
7 |
-
TooltipContent,
|
8 |
-
TooltipTrigger,
|
9 |
-
} from "@/components/ui/tooltip";
|
10 |
-
import { useAi } from "@/hooks/useAi";
|
11 |
-
import { useEditor } from "@/hooks/useEditor";
|
12 |
-
|
13 |
-
export const Selector = () => {
|
14 |
-
const { globalEditorLoading } = useEditor();
|
15 |
-
const { isEditableModeEnabled, setIsEditableModeEnabled, globalAiLoading } =
|
16 |
-
useAi();
|
17 |
-
return (
|
18 |
-
<Tooltip>
|
19 |
-
<TooltipTrigger asChild>
|
20 |
-
<Button
|
21 |
-
size="xs"
|
22 |
-
variant={isEditableModeEnabled ? "default" : "outline"}
|
23 |
-
onClick={() => {
|
24 |
-
setIsEditableModeEnabled?.(!isEditableModeEnabled);
|
25 |
-
}}
|
26 |
-
disabled={globalAiLoading || globalEditorLoading}
|
27 |
-
className="!rounded-md"
|
28 |
-
>
|
29 |
-
<Crosshair className="size-3.5" />
|
30 |
-
Edit
|
31 |
-
</Button>
|
32 |
-
</TooltipTrigger>
|
33 |
-
<TooltipContent
|
34 |
-
align="start"
|
35 |
-
className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
|
36 |
-
>
|
37 |
-
Select an element on the page to ask DeepSite edit it directly.
|
38 |
-
</TooltipContent>
|
39 |
-
</Tooltip>
|
40 |
-
);
|
41 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/settings.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
-
"use client";
|
2 |
import classNames from "classnames";
|
|
|
|
|
3 |
|
4 |
import {
|
5 |
Popover,
|
@@ -17,37 +18,29 @@ import {
|
|
17 |
SelectTrigger,
|
18 |
SelectValue,
|
19 |
} from "@/components/ui/select";
|
20 |
-
import { useMemo
|
21 |
import { useUpdateEffect } from "react-use";
|
22 |
import Image from "next/image";
|
23 |
-
import { Brain, CheckCheck, ChevronDown } from "lucide-react";
|
24 |
-
import { useAi } from "@/hooks/useAi";
|
25 |
|
26 |
export function Settings({
|
27 |
open,
|
28 |
onClose,
|
|
|
|
|
29 |
error,
|
30 |
isFollowUp = false,
|
|
|
|
|
31 |
}: {
|
32 |
open: boolean;
|
|
|
|
|
33 |
error?: string;
|
34 |
isFollowUp?: boolean;
|
35 |
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
|
|
|
|
36 |
}) {
|
37 |
-
const {
|
38 |
-
model,
|
39 |
-
provider,
|
40 |
-
setProvider,
|
41 |
-
setModel,
|
42 |
-
selectedModel,
|
43 |
-
globalAiLoading,
|
44 |
-
} = useAi();
|
45 |
-
const [isMounted, setIsMounted] = useState(false);
|
46 |
-
|
47 |
-
useEffect(() => {
|
48 |
-
setIsMounted(true);
|
49 |
-
}, []);
|
50 |
-
|
51 |
const modelAvailableProviders = useMemo(() => {
|
52 |
const availableProviders = MODELS.find(
|
53 |
(m: { value: string }) => m.value === model
|
@@ -59,162 +52,153 @@ export function Settings({
|
|
59 |
}, [model]);
|
60 |
|
61 |
useUpdateEffect(() => {
|
62 |
-
if (
|
63 |
-
|
64 |
-
!modelAvailableProviders.includes(provider as string)
|
65 |
-
) {
|
66 |
-
setProvider("auto");
|
67 |
}
|
68 |
}, [model, provider]);
|
69 |
|
70 |
return (
|
71 |
-
<
|
72 |
-
<
|
73 |
-
<
|
74 |
-
variant=
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
78 |
>
|
79 |
-
<
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
</SelectItem>
|
135 |
-
)
|
136 |
-
)}
|
137 |
-
</SelectGroup>
|
138 |
-
</SelectContent>
|
139 |
-
</Select>
|
140 |
-
</label>
|
141 |
-
{isFollowUp && (
|
142 |
-
<div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
|
143 |
-
Note: You can't use a Thinker model for follow-up requests.
|
144 |
-
We automatically switch to the default model for you.
|
145 |
-
</div>
|
146 |
-
)}
|
147 |
-
<div className="flex flex-col gap-3">
|
148 |
-
<div className="flex items-center justify-between">
|
149 |
-
<div>
|
150 |
-
<p className="text-neutral-300 text-sm mb-1.5">
|
151 |
-
Use auto-provider
|
152 |
-
</p>
|
153 |
-
<p className="text-xs text-neutral-400/70">
|
154 |
-
We'll automatically select the best provider for you
|
155 |
-
based on your prompt.
|
156 |
-
</p>
|
157 |
</div>
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
setProvider(foundModel.autoProvider);
|
171 |
-
} else {
|
172 |
-
setProvider("auto");
|
173 |
-
}
|
174 |
-
}}
|
175 |
-
>
|
176 |
<div
|
177 |
className={classNames(
|
178 |
-
"w-
|
179 |
{
|
180 |
-
"
|
181 |
}
|
182 |
)}
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
<Image
|
201 |
-
src={`/providers/${id}.svg`}
|
202 |
-
alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
|
203 |
-
className="size-5 mr-2"
|
204 |
-
width={20}
|
205 |
-
height={20}
|
206 |
-
/>
|
207 |
-
{PROVIDERS[id as keyof typeof PROVIDERS].name}
|
208 |
-
{id === provider && (
|
209 |
-
<CheckCheck className="ml-2 size-4 text-blue-500" />
|
210 |
)}
|
211 |
-
|
212 |
-
|
213 |
</div>
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
219 |
);
|
220 |
}
|
|
|
|
|
1 |
import classNames from "classnames";
|
2 |
+
import { PiGearSixFill } from "react-icons/pi";
|
3 |
+
import { RiCheckboxCircleFill } from "react-icons/ri";
|
4 |
|
5 |
import {
|
6 |
Popover,
|
|
|
18 |
SelectTrigger,
|
19 |
SelectValue,
|
20 |
} from "@/components/ui/select";
|
21 |
+
import { useMemo } from "react";
|
22 |
import { useUpdateEffect } from "react-use";
|
23 |
import Image from "next/image";
|
|
|
|
|
24 |
|
25 |
export function Settings({
|
26 |
open,
|
27 |
onClose,
|
28 |
+
provider,
|
29 |
+
model,
|
30 |
error,
|
31 |
isFollowUp = false,
|
32 |
+
onChange,
|
33 |
+
onModelChange,
|
34 |
}: {
|
35 |
open: boolean;
|
36 |
+
provider: string;
|
37 |
+
model: string;
|
38 |
error?: string;
|
39 |
isFollowUp?: boolean;
|
40 |
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
41 |
+
onChange: (provider: string) => void;
|
42 |
+
onModelChange: (model: string) => void;
|
43 |
}) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
const modelAvailableProviders = useMemo(() => {
|
45 |
const availableProviders = MODELS.find(
|
46 |
(m: { value: string }) => m.value === model
|
|
|
52 |
}, [model]);
|
53 |
|
54 |
useUpdateEffect(() => {
|
55 |
+
if (provider !== "auto" && !modelAvailableProviders.includes(provider)) {
|
56 |
+
onChange("auto");
|
|
|
|
|
|
|
57 |
}
|
58 |
}, [model, provider]);
|
59 |
|
60 |
return (
|
61 |
+
<div className="">
|
62 |
+
<Popover open={open} onOpenChange={onClose}>
|
63 |
+
<PopoverTrigger asChild>
|
64 |
+
<Button variant="black" size="sm">
|
65 |
+
<PiGearSixFill className="size-4" />
|
66 |
+
Settings
|
67 |
+
</Button>
|
68 |
+
</PopoverTrigger>
|
69 |
+
<PopoverContent
|
70 |
+
className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
|
71 |
+
align="center"
|
72 |
>
|
73 |
+
<header className="flex items-center justify-center text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
|
74 |
+
Customize Settings
|
75 |
+
</header>
|
76 |
+
<main className="px-4 pt-5 pb-6 space-y-5">
|
77 |
+
{error !== "" && (
|
78 |
+
<p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
|
79 |
+
{error}
|
80 |
+
</p>
|
81 |
+
)}
|
82 |
+
<label className="block">
|
83 |
+
<p className="text-neutral-300 text-sm mb-2.5">
|
84 |
+
Choose a DeepSeek model
|
85 |
+
</p>
|
86 |
+
<Select defaultValue={model} onValueChange={onModelChange}>
|
87 |
+
<SelectTrigger className="w-full">
|
88 |
+
<SelectValue placeholder="Select a DeepSeek model" />
|
89 |
+
</SelectTrigger>
|
90 |
+
<SelectContent>
|
91 |
+
<SelectGroup>
|
92 |
+
<SelectLabel>DeepSeek models</SelectLabel>
|
93 |
+
{MODELS.map(
|
94 |
+
({
|
95 |
+
value,
|
96 |
+
label,
|
97 |
+
isNew = false,
|
98 |
+
isThinker = false,
|
99 |
+
}: {
|
100 |
+
value: string;
|
101 |
+
label: string;
|
102 |
+
isNew?: boolean;
|
103 |
+
isThinker?: boolean;
|
104 |
+
}) => (
|
105 |
+
<SelectItem
|
106 |
+
key={value}
|
107 |
+
value={value}
|
108 |
+
className=""
|
109 |
+
disabled={isThinker && isFollowUp}
|
110 |
+
>
|
111 |
+
{label}
|
112 |
+
{isNew && (
|
113 |
+
<span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
|
114 |
+
New
|
115 |
+
</span>
|
116 |
+
)}
|
117 |
+
</SelectItem>
|
118 |
+
)
|
119 |
+
)}
|
120 |
+
</SelectGroup>
|
121 |
+
</SelectContent>
|
122 |
+
</Select>
|
123 |
+
</label>
|
124 |
+
{isFollowUp && (
|
125 |
+
<div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
|
126 |
+
Note: You can't use a Thinker model for follow-up requests.
|
127 |
+
We automatically switch to the default model for you.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
</div>
|
129 |
+
)}
|
130 |
+
<div className="flex flex-col gap-3">
|
131 |
+
<div className="flex items-center justify-between">
|
132 |
+
<div>
|
133 |
+
<p className="text-neutral-300 text-sm mb-1.5">
|
134 |
+
Use auto-provider
|
135 |
+
</p>
|
136 |
+
<p className="text-xs text-neutral-400/70">
|
137 |
+
We'll automatically select the best provider for you
|
138 |
+
based on your prompt.
|
139 |
+
</p>
|
140 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
<div
|
142 |
className={classNames(
|
143 |
+
"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",
|
144 |
{
|
145 |
+
"!bg-sky-500": provider === "auto",
|
146 |
}
|
147 |
)}
|
148 |
+
onClick={() => {
|
149 |
+
const foundModel = MODELS.find(
|
150 |
+
(m: { value: string }) => m.value === model
|
151 |
+
);
|
152 |
+
if (provider === "auto" && foundModel?.autoProvider) {
|
153 |
+
onChange(foundModel.autoProvider);
|
154 |
+
} else {
|
155 |
+
onChange("auto");
|
156 |
+
}
|
157 |
+
}}
|
158 |
+
>
|
159 |
+
<div
|
160 |
+
className={classNames(
|
161 |
+
"w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
|
162 |
+
{
|
163 |
+
"translate-x-4": provider === "auto",
|
164 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
)}
|
166 |
+
/>
|
167 |
+
</div>
|
168 |
</div>
|
169 |
+
<label className="block">
|
170 |
+
<p className="text-neutral-300 text-sm mb-2">
|
171 |
+
Inference Provider
|
172 |
+
</p>
|
173 |
+
<div className="grid grid-cols-2 gap-1.5">
|
174 |
+
{modelAvailableProviders.map((id: string) => (
|
175 |
+
<Button
|
176 |
+
key={id}
|
177 |
+
variant={id === provider ? "default" : "secondary"}
|
178 |
+
size="sm"
|
179 |
+
onClick={() => {
|
180 |
+
onChange(id);
|
181 |
+
}}
|
182 |
+
>
|
183 |
+
<Image
|
184 |
+
src={`/providers/${id}.svg`}
|
185 |
+
alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
|
186 |
+
className="size-5 mr-2"
|
187 |
+
width={20}
|
188 |
+
height={20}
|
189 |
+
/>
|
190 |
+
{PROVIDERS[id as keyof typeof PROVIDERS].name}
|
191 |
+
{id === provider && (
|
192 |
+
<RiCheckboxCircleFill className="ml-2 size-4 text-blue-500" />
|
193 |
+
)}
|
194 |
+
</Button>
|
195 |
+
))}
|
196 |
+
</div>
|
197 |
+
</label>
|
198 |
+
</div>
|
199 |
+
</main>
|
200 |
+
</PopoverContent>
|
201 |
+
</Popover>
|
202 |
+
</div>
|
203 |
);
|
204 |
}
|
components/editor/ask-ai/uploader.tsx
DELETED
@@ -1,165 +0,0 @@
|
|
1 |
-
import { useRef, useState } from "react";
|
2 |
-
import {
|
3 |
-
CheckCircle,
|
4 |
-
ImageIcon,
|
5 |
-
Images,
|
6 |
-
Link,
|
7 |
-
Paperclip,
|
8 |
-
Upload,
|
9 |
-
} from "lucide-react";
|
10 |
-
import Image from "next/image";
|
11 |
-
|
12 |
-
import {
|
13 |
-
Popover,
|
14 |
-
PopoverContent,
|
15 |
-
PopoverTrigger,
|
16 |
-
} from "@/components/ui/popover";
|
17 |
-
import { Button } from "@/components/ui/button";
|
18 |
-
import { Project } from "@/types";
|
19 |
-
import Loading from "@/components/loading";
|
20 |
-
import { useUser } from "@/hooks/useUser";
|
21 |
-
import { useEditor } from "@/hooks/useEditor";
|
22 |
-
import { useAi } from "@/hooks/useAi";
|
23 |
-
import { useLoginModal } from "@/components/contexts/login-context";
|
24 |
-
|
25 |
-
export const Uploader = ({ project }: { project: Project | undefined }) => {
|
26 |
-
const { user } = useUser();
|
27 |
-
const { openLoginModal } = useLoginModal();
|
28 |
-
const { uploadFiles, isUploading, files, globalEditorLoading } = useEditor();
|
29 |
-
const { selectedFiles, setSelectedFiles, globalAiLoading } = useAi();
|
30 |
-
|
31 |
-
const [open, setOpen] = useState(false);
|
32 |
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
33 |
-
|
34 |
-
if (!user)
|
35 |
-
return (
|
36 |
-
<Button
|
37 |
-
size="xs"
|
38 |
-
variant="outline"
|
39 |
-
className="!rounded-md"
|
40 |
-
disabled={globalAiLoading || globalEditorLoading}
|
41 |
-
onClick={() => openLoginModal()}
|
42 |
-
>
|
43 |
-
<Paperclip className="size-3.5" />
|
44 |
-
Attach
|
45 |
-
</Button>
|
46 |
-
);
|
47 |
-
|
48 |
-
return (
|
49 |
-
<Popover open={open} onOpenChange={setOpen}>
|
50 |
-
<form className="h-[24px]">
|
51 |
-
<PopoverTrigger asChild>
|
52 |
-
<Button
|
53 |
-
size="xs"
|
54 |
-
variant={open ? "default" : "outline"}
|
55 |
-
className="!rounded-md"
|
56 |
-
disabled={globalAiLoading || globalEditorLoading}
|
57 |
-
>
|
58 |
-
<Paperclip className="size-3.5" />
|
59 |
-
Attach
|
60 |
-
</Button>
|
61 |
-
</PopoverTrigger>
|
62 |
-
<PopoverContent
|
63 |
-
align="start"
|
64 |
-
className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
|
65 |
-
>
|
66 |
-
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
67 |
-
<div className="flex items-center justify-center -space-x-4 mb-3">
|
68 |
-
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
69 |
-
🎨
|
70 |
-
</div>
|
71 |
-
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
72 |
-
🖼️
|
73 |
-
</div>
|
74 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
75 |
-
💻
|
76 |
-
</div>
|
77 |
-
</div>
|
78 |
-
<p className="text-xl font-semibold text-neutral-950">
|
79 |
-
Add Custom Images
|
80 |
-
</p>
|
81 |
-
<p className="text-sm text-neutral-500 mt-1.5">
|
82 |
-
Upload images to your project and use them with DeepSite!
|
83 |
-
</p>
|
84 |
-
</header>
|
85 |
-
<main className="space-y-4 p-5">
|
86 |
-
<div>
|
87 |
-
<p className="text-xs text-left text-neutral-700 mb-2">
|
88 |
-
Uploaded Images
|
89 |
-
</p>
|
90 |
-
{files?.length > 0 ? (
|
91 |
-
<div className="grid grid-cols-4 gap-1 flex-wrap max-h-40 overflow-y-auto">
|
92 |
-
{files.map((file: string) => (
|
93 |
-
<div
|
94 |
-
key={file}
|
95 |
-
className="select-none relative cursor-pointer bg-white rounded-md border-[2px] border-white hover:shadow-2xl transition-all duration-300"
|
96 |
-
onClick={() =>
|
97 |
-
setSelectedFiles(
|
98 |
-
selectedFiles.includes(file)
|
99 |
-
? selectedFiles.filter((f) => f !== file)
|
100 |
-
: [...selectedFiles, file]
|
101 |
-
)
|
102 |
-
}
|
103 |
-
>
|
104 |
-
<Image
|
105 |
-
src={file}
|
106 |
-
alt="uploaded image"
|
107 |
-
width={56}
|
108 |
-
height={56}
|
109 |
-
className="object-cover w-full rounded-sm aspect-square"
|
110 |
-
/>
|
111 |
-
{selectedFiles.includes(file) && (
|
112 |
-
<div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
|
113 |
-
<CheckCircle className="size-6 text-neutral-100" />
|
114 |
-
</div>
|
115 |
-
)}
|
116 |
-
</div>
|
117 |
-
))}
|
118 |
-
</div>
|
119 |
-
) : (
|
120 |
-
<p className="text-sm text-muted-foreground font-mono flex flex-col items-center gap-1 pt-2">
|
121 |
-
<ImageIcon className="size-4" />
|
122 |
-
No images uploaded yet
|
123 |
-
</p>
|
124 |
-
)}
|
125 |
-
</div>
|
126 |
-
<div>
|
127 |
-
<p className="text-xs text-left text-neutral-700 mb-2">
|
128 |
-
Or import images from your computer
|
129 |
-
</p>
|
130 |
-
<Button
|
131 |
-
variant="black"
|
132 |
-
onClick={() => fileInputRef.current?.click()}
|
133 |
-
className="relative w-full"
|
134 |
-
disabled={isUploading}
|
135 |
-
>
|
136 |
-
{isUploading ? (
|
137 |
-
<>
|
138 |
-
<Loading
|
139 |
-
overlay={false}
|
140 |
-
className="ml-2 size-4 animate-spin"
|
141 |
-
/>
|
142 |
-
Uploading image(s)...
|
143 |
-
</>
|
144 |
-
) : (
|
145 |
-
<>
|
146 |
-
<Upload className="size-4" />
|
147 |
-
Upload Images
|
148 |
-
</>
|
149 |
-
)}
|
150 |
-
</Button>
|
151 |
-
<input
|
152 |
-
ref={fileInputRef}
|
153 |
-
type="file"
|
154 |
-
className="hidden"
|
155 |
-
multiple
|
156 |
-
accept="image/*"
|
157 |
-
onChange={(e) => uploadFiles(e.target.files, project!)}
|
158 |
-
/>
|
159 |
-
</div>
|
160 |
-
</main>
|
161 |
-
</PopoverContent>
|
162 |
-
</form>
|
163 |
-
</Popover>
|
164 |
-
);
|
165 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/deploy-button/index.tsx
ADDED
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
// TODO add a way to do not allow people to deploy if the html is broken.
|
64 |
+
|
65 |
+
return (
|
66 |
+
<div className="flex items-center justify-end gap-5">
|
67 |
+
<div className="relative flex items-center justify-end">
|
68 |
+
{user?.id ? (
|
69 |
+
<Popover>
|
70 |
+
<PopoverTrigger asChild>
|
71 |
+
<div>
|
72 |
+
<Button variant="default" className="max-lg:hidden !px-4">
|
73 |
+
<MdSave className="size-4" />
|
74 |
+
Deploy your Project
|
75 |
+
</Button>
|
76 |
+
<Button variant="default" size="sm" className="lg:hidden">
|
77 |
+
Deploy
|
78 |
+
</Button>
|
79 |
+
</div>
|
80 |
+
</PopoverTrigger>
|
81 |
+
<PopoverContent
|
82 |
+
className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
|
83 |
+
align="end"
|
84 |
+
>
|
85 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
86 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
87 |
+
<div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
88 |
+
🚀
|
89 |
+
</div>
|
90 |
+
<div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
|
91 |
+
<Image
|
92 |
+
src={SpaceIcon}
|
93 |
+
alt="Space Icon"
|
94 |
+
className="size-7"
|
95 |
+
/>
|
96 |
+
</div>
|
97 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
98 |
+
👻
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
<p className="text-xl font-semibold text-neutral-950">
|
102 |
+
Deploy as Space!
|
103 |
+
</p>
|
104 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
105 |
+
Save and Deploy your project to a Space on the Hub. Spaces are
|
106 |
+
a way to share your project with the world.
|
107 |
+
</p>
|
108 |
+
</header>
|
109 |
+
<main className="space-y-4 p-6">
|
110 |
+
<div>
|
111 |
+
<p className="text-sm text-neutral-700 mb-2">
|
112 |
+
Choose a title for your space:
|
113 |
+
</p>
|
114 |
+
<Input
|
115 |
+
type="text"
|
116 |
+
placeholder="My Awesome Website"
|
117 |
+
value={config.title}
|
118 |
+
onChange={(e) =>
|
119 |
+
setConfig({ ...config, title: e.target.value })
|
120 |
+
}
|
121 |
+
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
122 |
+
/>
|
123 |
+
</div>
|
124 |
+
<div>
|
125 |
+
<p className="text-sm text-neutral-700 mb-2">
|
126 |
+
Then, let's deploy it!
|
127 |
+
</p>
|
128 |
+
<Button
|
129 |
+
variant="black"
|
130 |
+
onClick={createSpace}
|
131 |
+
className="relative w-full"
|
132 |
+
disabled={loading}
|
133 |
+
>
|
134 |
+
Deploy Space <Rocket className="size-4" />
|
135 |
+
{loading && (
|
136 |
+
<Loading className="ml-2 size-4 animate-spin" />
|
137 |
+
)}
|
138 |
+
</Button>
|
139 |
+
</div>
|
140 |
+
</main>
|
141 |
+
</PopoverContent>
|
142 |
+
</Popover>
|
143 |
+
) : (
|
144 |
+
<>
|
145 |
+
<Button
|
146 |
+
variant="default"
|
147 |
+
className="max-lg:hidden !px-4"
|
148 |
+
onClick={() => setOpen(true)}
|
149 |
+
>
|
150 |
+
<MdSave className="size-4" />
|
151 |
+
Save your Project
|
152 |
+
</Button>
|
153 |
+
<Button
|
154 |
+
variant="default"
|
155 |
+
size="sm"
|
156 |
+
className="lg:hidden"
|
157 |
+
onClick={() => setOpen(true)}
|
158 |
+
>
|
159 |
+
Save
|
160 |
+
</Button>
|
161 |
+
</>
|
162 |
+
)}
|
163 |
+
<LoginModal
|
164 |
+
open={open}
|
165 |
+
onClose={() => setOpen(false)}
|
166 |
+
html={html}
|
167 |
+
title="Log In to save your Project"
|
168 |
+
description="Log In through your Hugging Face account to save your project and increase your monthly free limit."
|
169 |
+
/>
|
170 |
+
</div>
|
171 |
+
</div>
|
172 |
+
);
|
173 |
+
}
|
components/editor/footer/index.tsx
ADDED
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import classNames from "classnames";
|
2 |
+
import { FaMobileAlt } from "react-icons/fa";
|
3 |
+
import { HelpCircle, RefreshCcw, SparkleIcon } from "lucide-react";
|
4 |
+
import { FaLaptopCode } from "react-icons/fa6";
|
5 |
+
import { HtmlHistory } 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 |
+
<a
|
87 |
+
target="_blank"
|
88 |
+
href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/157"
|
89 |
+
>
|
90 |
+
<Button size="sm" variant="outline">
|
91 |
+
<HelpCircle className="size-3.5" />
|
92 |
+
<span className="max-lg:hidden">Help</span>
|
93 |
+
</Button>
|
94 |
+
</a>
|
95 |
+
<Button size="sm" variant="outline" onClick={handleRefreshIframe}>
|
96 |
+
<RefreshCcw className="size-3.5" />
|
97 |
+
<span className="max-lg:hidden">Refresh Preview</span>
|
98 |
+
</Button>
|
99 |
+
<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">
|
100 |
+
<div
|
101 |
+
className={classNames(
|
102 |
+
"absolute left-0.5 top-0.5 rounded-full bg-white size-7 -z-[1] transition-all duration-200",
|
103 |
+
{
|
104 |
+
"translate-x-[calc(100%+2px)]": device === "mobile",
|
105 |
+
}
|
106 |
+
)}
|
107 |
+
/>
|
108 |
+
{DEVICES.map((deviceItem) => (
|
109 |
+
<button
|
110 |
+
key={deviceItem.name}
|
111 |
+
className={classNames(
|
112 |
+
"rounded-full text-neutral-300 size-7 flex items-center justify-center cursor-pointer",
|
113 |
+
{
|
114 |
+
"!text-black": device === deviceItem.name,
|
115 |
+
"hover:bg-neutral-800": device !== deviceItem.name,
|
116 |
+
}
|
117 |
+
)}
|
118 |
+
onClick={() => setDevice(deviceItem.name as "desktop" | "mobile")}
|
119 |
+
>
|
120 |
+
<deviceItem.icon className="text-sm" />
|
121 |
+
</button>
|
122 |
+
))}
|
123 |
+
</div>
|
124 |
+
</div>
|
125 |
+
</footer>
|
126 |
+
);
|
127 |
+
}
|
components/editor/header/index.tsx
CHANGED
@@ -1,112 +1,69 @@
|
|
1 |
-
import {
|
2 |
-
import
|
3 |
-
import Link from "next/link";
|
4 |
|
5 |
import Logo from "@/assets/logo.svg";
|
|
|
6 |
import { Button } from "@/components/ui/button";
|
7 |
-
import
|
8 |
-
import
|
9 |
-
import { UserMenu } from "@/components/user-menu";
|
10 |
-
import { SwitchDevice } from "@/components/editor/switch-devide";
|
11 |
-
import { SwitchTab } from "./switch-tab";
|
12 |
-
import { History } from "@/components/editor/history";
|
13 |
-
import { useEditor } from "@/hooks/useEditor";
|
14 |
-
import {
|
15 |
-
Tooltip,
|
16 |
-
TooltipContent,
|
17 |
-
TooltipTrigger,
|
18 |
-
} from "@/components/ui/tooltip";
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
return (
|
24 |
-
<header className="border-b bg-neutral-950 dark:border-neutral-800
|
25 |
-
<div className="flex items-center justify-
|
26 |
<h1 className="text-neutral-900 dark:text-white text-lg lg:text-xl font-bold flex items-center justify-start">
|
27 |
<Image
|
28 |
src={Logo}
|
29 |
alt="DeepSite Logo"
|
30 |
-
className="size-8 invert-100 dark:invert-0"
|
31 |
/>
|
32 |
-
<p className="
|
33 |
DeepSite
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
{" "}
|
39 |
-
v3
|
40 |
-
</span>
|
41 |
-
)}
|
42 |
</p>
|
43 |
</h1>
|
44 |
-
<div className="flex items-center justify-end gap-2">
|
45 |
-
<History />
|
46 |
-
<SwitchTab />
|
47 |
-
</div>
|
48 |
-
</div>
|
49 |
-
<div className="lg:hidden flex items-center justify-center whitespace-nowrap">
|
50 |
-
<SwitchTab isMobile />
|
51 |
</div>
|
52 |
-
<div className="lg:
|
53 |
-
|
54 |
-
<SwitchDevice />
|
55 |
<Button
|
56 |
-
|
57 |
-
variant="
|
58 |
-
className="
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
if (iframe) {
|
64 |
-
iframe.src = iframe.src;
|
65 |
-
}
|
66 |
-
}}
|
67 |
>
|
68 |
-
<
|
69 |
-
|
70 |
</Button>
|
71 |
-
|
72 |
-
href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/427"
|
73 |
-
target="_blank"
|
74 |
-
className="max-lg:hidden"
|
75 |
-
>
|
76 |
-
<Button size="xs" variant="bordered">
|
77 |
-
<HelpCircle className="size-3 mr-0.5" />
|
78 |
-
Help
|
79 |
-
</Button>
|
80 |
-
</Link>
|
81 |
-
</div>
|
82 |
-
<div className="flex items-center gap-2">
|
83 |
-
{project?.private && (
|
84 |
-
<Tooltip>
|
85 |
-
<TooltipTrigger>
|
86 |
-
<div className="max-lg:hidden flex items-center gap-1.5 bg-amber-500/10 backdrop-blur-sm px-3 py-1.5 rounded-full border border-amber-500/20 shadow-lg">
|
87 |
-
<Lock className="w-3 h-3 text-amber-500" />
|
88 |
-
<span className="text-amber-500 text-xs font-medium tracking-wide">
|
89 |
-
Private Project
|
90 |
-
</span>
|
91 |
-
</div>
|
92 |
-
</TooltipTrigger>
|
93 |
-
<TooltipContent>
|
94 |
-
<p className="text-xs">
|
95 |
-
This project is private. Only you can see it.
|
96 |
-
</p>
|
97 |
-
</TooltipContent>
|
98 |
-
</Tooltip>
|
99 |
-
)}
|
100 |
-
{user ? (
|
101 |
-
<UserMenu className="!pl-1 !pr-3 !py-1 !h-auto" />
|
102 |
-
) : (
|
103 |
-
<Button size="sm" onClick={openLoginWindow}>
|
104 |
-
Start Vibe Coding
|
105 |
-
<ArrowRight className="size-4" />
|
106 |
-
</Button>
|
107 |
-
)}
|
108 |
-
</div>
|
109 |
</div>
|
|
|
110 |
</header>
|
111 |
);
|
112 |
}
|
|
|
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/header/switch-tab.tsx
DELETED
@@ -1,58 +0,0 @@
|
|
1 |
-
import {
|
2 |
-
PanelLeftClose,
|
3 |
-
PanelLeftOpen,
|
4 |
-
Eye,
|
5 |
-
MessageCircleCode,
|
6 |
-
} from "lucide-react";
|
7 |
-
import classNames from "classnames";
|
8 |
-
|
9 |
-
import { Button } from "@/components/ui/button";
|
10 |
-
import { useEditor } from "@/hooks/useEditor";
|
11 |
-
|
12 |
-
const TABS = [
|
13 |
-
{
|
14 |
-
value: "chat",
|
15 |
-
label: "Chat",
|
16 |
-
icon: MessageCircleCode,
|
17 |
-
},
|
18 |
-
{
|
19 |
-
value: "preview",
|
20 |
-
label: "Preview",
|
21 |
-
icon: Eye,
|
22 |
-
},
|
23 |
-
];
|
24 |
-
|
25 |
-
export const SwitchTab = ({ isMobile = false }: { isMobile?: boolean }) => {
|
26 |
-
const { currentTab, setCurrentTab } = useEditor();
|
27 |
-
|
28 |
-
if (isMobile) {
|
29 |
-
return (
|
30 |
-
<div className="flex items-center justify-center gap-1 bg-neutral-900 rounded-full p-1">
|
31 |
-
{TABS.map((item) => (
|
32 |
-
<Button
|
33 |
-
key={item.value}
|
34 |
-
variant={currentTab === item.value ? "default" : "ghost"}
|
35 |
-
className={classNames("", {
|
36 |
-
"opacity-60": currentTab !== item.value,
|
37 |
-
})}
|
38 |
-
size="sm"
|
39 |
-
onClick={() => setCurrentTab(item.value)}
|
40 |
-
>
|
41 |
-
<item.icon className="size-4" />
|
42 |
-
<span className="inline">{item.label}</span>
|
43 |
-
</Button>
|
44 |
-
))}
|
45 |
-
</div>
|
46 |
-
);
|
47 |
-
}
|
48 |
-
return (
|
49 |
-
<Button
|
50 |
-
variant="ghost"
|
51 |
-
size="iconXs"
|
52 |
-
className="max-lg:hidden"
|
53 |
-
onClick={() => setCurrentTab(currentTab === "chat" ? "preview" : "chat")}
|
54 |
-
>
|
55 |
-
{currentTab === "chat" ? <PanelLeftClose /> : <PanelLeftOpen />}
|
56 |
-
</Button>
|
57 |
-
);
|
58 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/history-notification/index.tsx
DELETED
@@ -1,119 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import { useState } from "react";
|
4 |
-
import classNames from "classnames";
|
5 |
-
import { Button } from "@/components/ui/button";
|
6 |
-
import Loading from "@/components/loading";
|
7 |
-
import {
|
8 |
-
History,
|
9 |
-
ChevronUp,
|
10 |
-
ChevronDown,
|
11 |
-
MousePointerClick,
|
12 |
-
} from "lucide-react";
|
13 |
-
|
14 |
-
interface HistoryNotificationProps {
|
15 |
-
/** Whether the historical version notification should be visible */
|
16 |
-
isVisible: boolean;
|
17 |
-
/** Whether the version promotion is in progress */
|
18 |
-
isPromotingVersion: boolean;
|
19 |
-
/** Function to promote the current historical version */
|
20 |
-
onPromoteVersion: () => void;
|
21 |
-
/** Function to go back to the current version */
|
22 |
-
onGoBackToCurrent: () => void;
|
23 |
-
/** Additional CSS classes */
|
24 |
-
className?: string;
|
25 |
-
}
|
26 |
-
|
27 |
-
export const HistoryNotification = ({
|
28 |
-
isVisible,
|
29 |
-
isPromotingVersion,
|
30 |
-
onPromoteVersion,
|
31 |
-
onGoBackToCurrent,
|
32 |
-
className,
|
33 |
-
}: HistoryNotificationProps) => {
|
34 |
-
const [isCollapsed, setIsCollapsed] = useState(false);
|
35 |
-
|
36 |
-
if (!isVisible) {
|
37 |
-
return null;
|
38 |
-
}
|
39 |
-
|
40 |
-
return (
|
41 |
-
<div
|
42 |
-
className={classNames(
|
43 |
-
"absolute bottom-4 left-4 z-10 bg-white/95 backdrop-blur-sm border border-neutral-200 rounded-xl shadow-lg transition-all duration-300 ease-in-out",
|
44 |
-
className
|
45 |
-
)}
|
46 |
-
>
|
47 |
-
{isCollapsed ? (
|
48 |
-
// Collapsed state
|
49 |
-
<div className="flex items-center gap-2 p-3">
|
50 |
-
<History className="size-4 text-neutral-600" />
|
51 |
-
<span className="text-xs text-neutral-600 font-medium">
|
52 |
-
Historical Version
|
53 |
-
</span>
|
54 |
-
<Button
|
55 |
-
variant="outline"
|
56 |
-
size="iconXs"
|
57 |
-
className="!rounded-md !border-neutral-200"
|
58 |
-
onClick={() => setIsCollapsed(false)}
|
59 |
-
>
|
60 |
-
<ChevronUp className="text-neutral-400 size-3" />
|
61 |
-
</Button>
|
62 |
-
</div>
|
63 |
-
) : (
|
64 |
-
// Expanded state
|
65 |
-
<div className="p-4 max-w-sm w-full">
|
66 |
-
<div className="flex items-start gap-3">
|
67 |
-
<History className="size-4 text-neutral-600 translate-y-1.5" />
|
68 |
-
<div className="flex-1 min-w-0">
|
69 |
-
<div className="flex items-center justify-between mb-1">
|
70 |
-
<div className="flex items-center gap-2">
|
71 |
-
<p className="font-semibold text-sm text-neutral-800">
|
72 |
-
Historical Version
|
73 |
-
</p>
|
74 |
-
</div>
|
75 |
-
<Button
|
76 |
-
variant="outline"
|
77 |
-
size="iconXs"
|
78 |
-
className="!rounded-md !border-neutral-200"
|
79 |
-
onClick={() => setIsCollapsed(true)}
|
80 |
-
>
|
81 |
-
<ChevronDown className="text-neutral-400 size-3" />
|
82 |
-
</Button>
|
83 |
-
</div>
|
84 |
-
<p className="text-xs text-neutral-600 leading-relaxed mb-3">
|
85 |
-
You're viewing a previous version of this project. Promote this
|
86 |
-
version to make it current and deploy it live.
|
87 |
-
</p>
|
88 |
-
<div className="flex items-center gap-2">
|
89 |
-
<Button
|
90 |
-
size="xs"
|
91 |
-
variant="black"
|
92 |
-
className="!pr-3"
|
93 |
-
onClick={onPromoteVersion}
|
94 |
-
disabled={isPromotingVersion}
|
95 |
-
>
|
96 |
-
{isPromotingVersion ? (
|
97 |
-
<Loading overlay={false} />
|
98 |
-
) : (
|
99 |
-
<MousePointerClick className="size-3" />
|
100 |
-
)}
|
101 |
-
Promote Version
|
102 |
-
</Button>
|
103 |
-
<Button
|
104 |
-
size="xs"
|
105 |
-
variant="outline"
|
106 |
-
className=" !text-neutral-600 !border-neutral-200"
|
107 |
-
disabled={isPromotingVersion}
|
108 |
-
onClick={onGoBackToCurrent}
|
109 |
-
>
|
110 |
-
Go back to current
|
111 |
-
</Button>
|
112 |
-
</div>
|
113 |
-
</div>
|
114 |
-
</div>
|
115 |
-
</div>
|
116 |
-
)}
|
117 |
-
</div>
|
118 |
-
);
|
119 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/history/index.tsx
CHANGED
@@ -1,32 +1,25 @@
|
|
1 |
import { History as HistoryIcon } from "lucide-react";
|
2 |
-
import {
|
3 |
-
|
4 |
-
import { Commit } from "@/types";
|
5 |
import {
|
6 |
Popover,
|
7 |
PopoverContent,
|
8 |
PopoverTrigger,
|
9 |
} from "@/components/ui/popover";
|
10 |
import { Button } from "@/components/ui/button";
|
11 |
-
import { useEditor } from "@/hooks/useEditor";
|
12 |
-
import classNames from "classnames";
|
13 |
-
|
14 |
-
export function History() {
|
15 |
-
const { commits, currentCommit, setCurrentCommit, project } = useEditor();
|
16 |
-
const [open, setOpen] = useState(false);
|
17 |
-
|
18 |
-
if (commits.length === 0) return null;
|
19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
return (
|
21 |
-
<Popover
|
22 |
<PopoverTrigger asChild>
|
23 |
-
<Button
|
24 |
-
|
25 |
-
|
26 |
-
className="!rounded-md max-lg:hidden"
|
27 |
-
>
|
28 |
-
<HistoryIcon className="size-3.5" />
|
29 |
-
{commits?.length} edit{commits?.length !== 1 ? "s" : ""}
|
30 |
</Button>
|
31 |
</PopoverTrigger>
|
32 |
<PopoverContent
|
@@ -36,68 +29,39 @@ export function History() {
|
|
36 |
<header className="text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
|
37 |
History
|
38 |
</header>
|
39 |
-
<main className="space-y-3">
|
40 |
-
{project?.private && (
|
41 |
-
<div className="px-4 pt-3">
|
42 |
-
<p className="text-amber-500 text-xs px-2 py-1 bg-amber-500/10 border border-amber-500/20 rounded-md">
|
43 |
-
As this project is private, you can't see the history of
|
44 |
-
changes.
|
45 |
-
</p>
|
46 |
-
</div>
|
47 |
-
)}
|
48 |
<ul className="max-h-[250px] overflow-y-auto">
|
49 |
-
{
|
50 |
<li
|
51 |
key={index}
|
52 |
-
className=
|
53 |
-
"px-4 text-gray-200 py-2 border-b border-gray-800 last:border-0 space-y-1",
|
54 |
-
{
|
55 |
-
"bg-blue-500/10":
|
56 |
-
currentCommit === item.oid ||
|
57 |
-
(index === 0 && currentCommit === null),
|
58 |
-
}
|
59 |
-
)}
|
60 |
>
|
61 |
-
<
|
62 |
-
|
63 |
-
<
|
64 |
-
{new Date(item.
|
65 |
month: "2-digit",
|
66 |
day: "2-digit",
|
67 |
year: "2-digit",
|
68 |
}) +
|
69 |
" " +
|
70 |
-
new Date(item.
|
71 |
hour: "2-digit",
|
72 |
minute: "2-digit",
|
73 |
second: "2-digit",
|
74 |
hour12: false,
|
75 |
})}
|
76 |
-
</
|
77 |
-
{currentCommit === item.oid ||
|
78 |
-
(index === 0 && currentCommit === null) ? (
|
79 |
-
<span className="text-blue-500 bg-blue-500/10 border border-blue-500/20 rounded-full text-[10px] px-2 py-0.5">
|
80 |
-
Current version
|
81 |
-
</span>
|
82 |
-
) : (
|
83 |
-
!project?.private && (
|
84 |
-
<Button
|
85 |
-
variant="link"
|
86 |
-
size="xss"
|
87 |
-
className="text-gray-400 hover:text-gray-200"
|
88 |
-
onClick={() => {
|
89 |
-
if (index === 0) {
|
90 |
-
setCurrentCommit(null);
|
91 |
-
} else {
|
92 |
-
setCurrentCommit(item.oid);
|
93 |
-
}
|
94 |
-
}}
|
95 |
-
>
|
96 |
-
See version
|
97 |
-
</Button>
|
98 |
-
)
|
99 |
-
)}
|
100 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
</li>
|
102 |
))}
|
103 |
</ul>
|
|
|
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
|
|
|
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>
|
components/editor/index.tsx
CHANGED
@@ -1,154 +1,341 @@
|
|
1 |
"use client";
|
2 |
-
import {
|
3 |
-
import { useCopyToClipboard, useMount } from "react-use";
|
4 |
-
import { CopyIcon } from "lucide-react";
|
5 |
import { toast } from "sonner";
|
6 |
-
import classNames from "classnames";
|
7 |
import { editor } from "monaco-editor";
|
8 |
import Editor from "@monaco-editor/react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
import { useEditor } from "@/hooks/useEditor";
|
11 |
import { Header } from "@/components/editor/header";
|
12 |
-
import {
|
13 |
import { defaultHTML } from "@/lib/consts";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
-
|
16 |
-
|
17 |
-
import { Preview } from "./preview";
|
18 |
-
import { SaveChangesPopup } from "./save-changes-popup";
|
19 |
-
import Loading from "../loading";
|
20 |
-
import { LivePreviewRef } from "./live-preview";
|
21 |
-
|
22 |
-
export const AppEditor = ({
|
23 |
-
namespace,
|
24 |
-
repoId,
|
25 |
-
isNew = false,
|
26 |
-
}: {
|
27 |
-
namespace?: string;
|
28 |
-
repoId?: string;
|
29 |
-
isNew?: boolean;
|
30 |
-
}) => {
|
31 |
-
const {
|
32 |
-
project,
|
33 |
-
setPages,
|
34 |
-
files,
|
35 |
-
currentPageData,
|
36 |
-
currentTab,
|
37 |
-
currentCommit,
|
38 |
-
hasUnsavedChanges,
|
39 |
-
saveChanges,
|
40 |
-
pages,
|
41 |
-
} = useEditor(namespace, repoId);
|
42 |
-
const livePreviewRef = useRef<LivePreviewRef>(null);
|
43 |
-
const { isAiWorking } = useAi(undefined, livePreviewRef);
|
44 |
const [, copyToClipboard] = useCopyToClipboard();
|
45 |
-
const
|
|
|
|
|
|
|
|
|
|
|
46 |
|
47 |
-
const
|
|
|
48 |
const editor = useRef<HTMLDivElement>(null);
|
49 |
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
|
51 |
useMount(() => {
|
52 |
-
if (
|
53 |
-
|
54 |
-
{
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
57 |
},
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
}
|
60 |
});
|
61 |
|
62 |
-
|
63 |
-
if (
|
64 |
-
|
|
|
|
|
|
|
|
|
|
|
65 |
} else {
|
66 |
-
|
|
|
|
|
|
|
67 |
}
|
68 |
-
}, [
|
|
|
|
|
|
|
|
|
69 |
|
70 |
return (
|
71 |
-
<section className="h-
|
72 |
-
<Header
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
: "
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
)
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
/>
|
153 |
</section>
|
154 |
);
|
|
|
1 |
"use client";
|
2 |
+
import { useRef, useState } from "react";
|
|
|
|
|
3 |
import { toast } from "sonner";
|
|
|
4 |
import { editor } from "monaco-editor";
|
5 |
import Editor from "@monaco-editor/react";
|
6 |
+
import { CopyIcon } from "lucide-react";
|
7 |
+
import {
|
8 |
+
useCopyToClipboard,
|
9 |
+
useEvent,
|
10 |
+
useLocalStorage,
|
11 |
+
useMount,
|
12 |
+
useUnmount,
|
13 |
+
useUpdateEffect,
|
14 |
+
} from "react-use";
|
15 |
+
import classNames from "classnames";
|
16 |
+
import { useRouter, useSearchParams } from "next/navigation";
|
17 |
|
|
|
18 |
import { Header } from "@/components/editor/header";
|
19 |
+
import { Footer } from "@/components/editor/footer";
|
20 |
import { defaultHTML } from "@/lib/consts";
|
21 |
+
import { Preview } from "@/components/editor/preview";
|
22 |
+
import { useEditor } from "@/hooks/useEditor";
|
23 |
+
import { AskAI } from "@/components/editor/ask-ai";
|
24 |
+
import { DeployButton } from "./deploy-button";
|
25 |
+
import { Project } from "@/types";
|
26 |
+
import { SaveButton } from "./save-button";
|
27 |
+
import { LoadProject } from "../my-projects/load-project";
|
28 |
+
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
29 |
|
30 |
+
export const AppEditor = ({ project }: { project?: Project | null }) => {
|
31 |
+
const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
const [, copyToClipboard] = useCopyToClipboard();
|
33 |
+
const { html, setHtml, htmlHistory, setHtmlHistory, prompts, setPrompts } =
|
34 |
+
useEditor(project?.html ?? (htmlStorage as string) ?? defaultHTML);
|
35 |
+
// get query params from URL
|
36 |
+
const searchParams = useSearchParams();
|
37 |
+
const router = useRouter();
|
38 |
+
const deploy = searchParams.get("deploy") === "true";
|
39 |
|
40 |
+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
41 |
+
const preview = useRef<HTMLDivElement>(null);
|
42 |
const editor = useRef<HTMLDivElement>(null);
|
43 |
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
44 |
+
const resizer = useRef<HTMLDivElement>(null);
|
45 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
46 |
+
const monacoRef = useRef<any>(null);
|
47 |
+
|
48 |
+
const [currentTab, setCurrentTab] = useState("chat");
|
49 |
+
const [device, setDevice] = useState<"desktop" | "mobile">("desktop");
|
50 |
+
const [isResizing, setIsResizing] = useState(false);
|
51 |
+
const [isAiWorking, setIsAiWorking] = useState(false);
|
52 |
+
const [isEditableModeEnabled, setIsEditableModeEnabled] = useState(false);
|
53 |
+
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
|
54 |
+
null
|
55 |
+
);
|
56 |
+
|
57 |
+
/**
|
58 |
+
* Resets the layout based on screen size
|
59 |
+
* - For desktop: Sets editor to 1/3 width and preview to 2/3
|
60 |
+
* - For mobile: Removes inline styles to let CSS handle it
|
61 |
+
*/
|
62 |
+
const resetLayout = () => {
|
63 |
+
if (!editor.current || !preview.current) return;
|
64 |
+
|
65 |
+
// lg breakpoint is 1024px based on useBreakpoint definition and Tailwind defaults
|
66 |
+
if (window.innerWidth >= 1024) {
|
67 |
+
// Set initial 1/3 - 2/3 sizes for large screens, accounting for resizer width
|
68 |
+
const resizerWidth = resizer.current?.offsetWidth ?? 8; // w-2 = 0.5rem = 8px
|
69 |
+
const availableWidth = window.innerWidth - resizerWidth;
|
70 |
+
const initialEditorWidth = availableWidth / 3; // Editor takes 1/3 of space
|
71 |
+
const initialPreviewWidth = availableWidth - initialEditorWidth; // Preview takes 2/3
|
72 |
+
editor.current.style.width = `${initialEditorWidth}px`;
|
73 |
+
preview.current.style.width = `${initialPreviewWidth}px`;
|
74 |
+
} else {
|
75 |
+
// Remove inline styles for smaller screens, let CSS flex-col handle it
|
76 |
+
editor.current.style.width = "";
|
77 |
+
preview.current.style.width = "";
|
78 |
+
}
|
79 |
+
};
|
80 |
+
|
81 |
+
/**
|
82 |
+
* Handles resizing when the user drags the resizer
|
83 |
+
* Ensures minimum widths are maintained for both panels
|
84 |
+
*/
|
85 |
+
const handleResize = (e: MouseEvent) => {
|
86 |
+
if (!editor.current || !preview.current || !resizer.current) return;
|
87 |
+
|
88 |
+
const resizerWidth = resizer.current.offsetWidth;
|
89 |
+
const minWidth = 100; // Minimum width for editor/preview
|
90 |
+
const maxWidth = window.innerWidth - resizerWidth - minWidth;
|
91 |
+
|
92 |
+
const editorWidth = e.clientX;
|
93 |
+
const clampedEditorWidth = Math.max(
|
94 |
+
minWidth,
|
95 |
+
Math.min(editorWidth, maxWidth)
|
96 |
+
);
|
97 |
+
const calculatedPreviewWidth =
|
98 |
+
window.innerWidth - clampedEditorWidth - resizerWidth;
|
99 |
+
|
100 |
+
editor.current.style.width = `${clampedEditorWidth}px`;
|
101 |
+
preview.current.style.width = `${calculatedPreviewWidth}px`;
|
102 |
+
};
|
103 |
+
|
104 |
+
const handleMouseDown = () => {
|
105 |
+
setIsResizing(true);
|
106 |
+
document.addEventListener("mousemove", handleResize);
|
107 |
+
document.addEventListener("mouseup", handleMouseUp);
|
108 |
+
};
|
109 |
+
|
110 |
+
const handleMouseUp = () => {
|
111 |
+
setIsResizing(false);
|
112 |
+
document.removeEventListener("mousemove", handleResize);
|
113 |
+
document.removeEventListener("mouseup", handleMouseUp);
|
114 |
+
};
|
115 |
|
116 |
useMount(() => {
|
117 |
+
if (deploy && project?._id) {
|
118 |
+
toast.success("Your project is deployed! 🎉", {
|
119 |
+
action: {
|
120 |
+
label: "See Project",
|
121 |
+
onClick: () => {
|
122 |
+
window.open(
|
123 |
+
`https://huggingface.co/spaces/${project?.space_id}`,
|
124 |
+
"_blank"
|
125 |
+
);
|
126 |
+
},
|
127 |
},
|
128 |
+
});
|
129 |
+
router.replace(`/projects/${project?.space_id}`);
|
130 |
+
}
|
131 |
+
if (htmlStorage) {
|
132 |
+
removeHtmlStorage();
|
133 |
+
toast.warning("Previous HTML content restored from local storage.");
|
134 |
+
}
|
135 |
+
|
136 |
+
resetLayout();
|
137 |
+
if (!resizer.current) return;
|
138 |
+
resizer.current.addEventListener("mousedown", handleMouseDown);
|
139 |
+
window.addEventListener("resize", resetLayout);
|
140 |
+
});
|
141 |
+
useUnmount(() => {
|
142 |
+
document.removeEventListener("mousemove", handleResize);
|
143 |
+
document.removeEventListener("mouseup", handleMouseUp);
|
144 |
+
if (resizer.current) {
|
145 |
+
resizer.current.removeEventListener("mousedown", handleMouseDown);
|
146 |
+
}
|
147 |
+
window.removeEventListener("resize", resetLayout);
|
148 |
+
});
|
149 |
+
|
150 |
+
// Prevent accidental navigation away when AI is working or content has changed
|
151 |
+
useEvent("beforeunload", (e) => {
|
152 |
+
if (isAiWorking || !isTheSameHtml(html)) {
|
153 |
+
e.preventDefault();
|
154 |
+
return "";
|
155 |
}
|
156 |
});
|
157 |
|
158 |
+
useUpdateEffect(() => {
|
159 |
+
if (currentTab === "chat") {
|
160 |
+
// Reset editor width when switching to reasoning tab
|
161 |
+
resetLayout();
|
162 |
+
// re-add the event listener for resizing
|
163 |
+
if (resizer.current) {
|
164 |
+
resizer.current.addEventListener("mousedown", handleMouseDown);
|
165 |
+
}
|
166 |
} else {
|
167 |
+
if (preview.current) {
|
168 |
+
// Reset preview width when switching to preview tab
|
169 |
+
preview.current.style.width = "100%";
|
170 |
+
}
|
171 |
}
|
172 |
+
}, [currentTab]);
|
173 |
+
|
174 |
+
const handleEditorValidation = (markers: editor.IMarker[]) => {
|
175 |
+
console.log("Editor validation markers:", markers);
|
176 |
+
};
|
177 |
|
178 |
return (
|
179 |
+
<section className="h-[100dvh] bg-neutral-950 flex flex-col">
|
180 |
+
<Header tab={currentTab} onNewTab={setCurrentTab}>
|
181 |
+
<LoadProject
|
182 |
+
onSuccess={(project: Project) => {
|
183 |
+
router.push(`/projects/${project.space_id}`);
|
184 |
+
}}
|
185 |
+
/>
|
186 |
+
{project?._id ? (
|
187 |
+
<SaveButton html={html} prompts={prompts} />
|
188 |
+
) : (
|
189 |
+
<DeployButton html={html} prompts={prompts} />
|
190 |
+
)}
|
191 |
+
</Header>
|
192 |
+
<main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full max-lg:h-[calc(100%-82px)] relative">
|
193 |
+
{currentTab === "chat" && (
|
194 |
+
<>
|
195 |
+
<div
|
196 |
+
ref={editor}
|
197 |
+
className="bg-neutral-900 relative flex-1 overflow-hidden h-full flex flex-col gap-2 pb-3"
|
198 |
+
>
|
199 |
+
<CopyIcon
|
200 |
+
className="size-4 absolute top-2 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
|
201 |
+
onClick={() => {
|
202 |
+
copyToClipboard(html);
|
203 |
+
toast.success("HTML copied to clipboard!");
|
204 |
+
}}
|
205 |
+
/>
|
206 |
+
<Editor
|
207 |
+
defaultLanguage="html"
|
208 |
+
theme="vs-dark"
|
209 |
+
className={classNames(
|
210 |
+
"h-full bg-neutral-900 transition-all duration-200 absolute left-0 top-0",
|
211 |
+
{
|
212 |
+
"pointer-events-none": isAiWorking,
|
213 |
+
}
|
214 |
+
)}
|
215 |
+
options={{
|
216 |
+
colorDecorators: true,
|
217 |
+
fontLigatures: true,
|
218 |
+
theme: "vs-dark",
|
219 |
+
minimap: { enabled: false },
|
220 |
+
scrollbar: {
|
221 |
+
horizontal: "hidden",
|
222 |
+
},
|
223 |
+
wordWrap: "on",
|
224 |
+
}}
|
225 |
+
value={html}
|
226 |
+
onChange={(value) => {
|
227 |
+
const newValue = value ?? "";
|
228 |
+
setHtml(newValue);
|
229 |
+
}}
|
230 |
+
onMount={(editor, monaco) => {
|
231 |
+
editorRef.current = editor;
|
232 |
+
monacoRef.current = monaco;
|
233 |
+
}}
|
234 |
+
onValidate={handleEditorValidation}
|
235 |
+
/>
|
236 |
+
<AskAI
|
237 |
+
html={html}
|
238 |
+
setHtml={(newHtml: string) => {
|
239 |
+
setHtml(newHtml);
|
240 |
+
}}
|
241 |
+
htmlHistory={htmlHistory}
|
242 |
+
onSuccess={(
|
243 |
+
finalHtml: string,
|
244 |
+
p: string,
|
245 |
+
updatedLines?: number[][]
|
246 |
+
) => {
|
247 |
+
const currentHistory = [...htmlHistory];
|
248 |
+
currentHistory.unshift({
|
249 |
+
html: finalHtml,
|
250 |
+
createdAt: new Date(),
|
251 |
+
prompt: p,
|
252 |
+
});
|
253 |
+
setHtmlHistory(currentHistory);
|
254 |
+
setSelectedElement(null);
|
255 |
+
// if xs or sm
|
256 |
+
if (window.innerWidth <= 1024) {
|
257 |
+
setCurrentTab("preview");
|
258 |
+
}
|
259 |
+
if (updatedLines && updatedLines?.length > 0) {
|
260 |
+
const decorations = updatedLines.map((line) => ({
|
261 |
+
range: new monacoRef.current.Range(
|
262 |
+
line[0],
|
263 |
+
1,
|
264 |
+
line[1],
|
265 |
+
1
|
266 |
+
),
|
267 |
+
options: {
|
268 |
+
inlineClassName: "matched-line",
|
269 |
+
},
|
270 |
+
}));
|
271 |
+
setTimeout(() => {
|
272 |
+
editorRef?.current
|
273 |
+
?.getModel()
|
274 |
+
?.deltaDecorations([], decorations);
|
275 |
|
276 |
+
editorRef.current?.revealLine(updatedLines[0][0]);
|
277 |
+
}, 100);
|
278 |
+
}
|
279 |
+
}}
|
280 |
+
isAiWorking={isAiWorking}
|
281 |
+
setisAiWorking={setIsAiWorking}
|
282 |
+
onNewPrompt={(prompt: string) => {
|
283 |
+
setPrompts((prev) => [...prev, prompt]);
|
284 |
+
}}
|
285 |
+
onScrollToBottom={() => {
|
286 |
+
editorRef.current?.revealLine(
|
287 |
+
editorRef.current?.getModel()?.getLineCount() ?? 0
|
288 |
+
);
|
289 |
+
}}
|
290 |
+
isEditableModeEnabled={isEditableModeEnabled}
|
291 |
+
setIsEditableModeEnabled={setIsEditableModeEnabled}
|
292 |
+
selectedElement={selectedElement}
|
293 |
+
setSelectedElement={setSelectedElement}
|
294 |
+
/>
|
295 |
+
</div>
|
296 |
+
<div
|
297 |
+
ref={resizer}
|
298 |
+
className="bg-neutral-800 hover:bg-sky-500 active:bg-sky-500 w-1.5 cursor-col-resize h-full max-lg:hidden"
|
299 |
+
/>
|
300 |
+
</>
|
301 |
+
)}
|
302 |
+
<Preview
|
303 |
+
html={html}
|
304 |
+
isResizing={isResizing}
|
305 |
+
isAiWorking={isAiWorking}
|
306 |
+
ref={preview}
|
307 |
+
device={device}
|
308 |
+
currentTab={currentTab}
|
309 |
+
isEditableModeEnabled={isEditableModeEnabled}
|
310 |
+
iframeRef={iframeRef}
|
311 |
+
onClickElement={(element) => {
|
312 |
+
setIsEditableModeEnabled(false);
|
313 |
+
setSelectedElement(element);
|
314 |
+
setCurrentTab("chat");
|
315 |
+
}}
|
316 |
+
/>
|
317 |
+
</main>
|
318 |
+
<Footer
|
319 |
+
onReset={() => {
|
320 |
+
if (isAiWorking) {
|
321 |
+
toast.warning("Please wait for the AI to finish working.");
|
322 |
+
return;
|
323 |
+
}
|
324 |
+
if (
|
325 |
+
window.confirm("You're about to reset the editor. Are you sure?")
|
326 |
+
) {
|
327 |
+
setHtml(defaultHTML);
|
328 |
+
removeHtmlStorage();
|
329 |
+
editorRef.current?.revealLine(
|
330 |
+
editorRef.current?.getModel()?.getLineCount() ?? 0
|
331 |
+
);
|
332 |
+
}
|
333 |
+
}}
|
334 |
+
htmlHistory={htmlHistory}
|
335 |
+
setHtml={setHtml}
|
336 |
+
iframeRef={iframeRef}
|
337 |
+
device={device}
|
338 |
+
setDevice={setDevice}
|
339 |
/>
|
340 |
</section>
|
341 |
);
|
components/editor/live-preview/index.tsx
DELETED
@@ -1,165 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import {
|
4 |
-
useState,
|
5 |
-
useEffect,
|
6 |
-
useRef,
|
7 |
-
forwardRef,
|
8 |
-
useImperativeHandle,
|
9 |
-
} from "react";
|
10 |
-
import classNames from "classnames";
|
11 |
-
|
12 |
-
import { Button } from "@/components/ui/button";
|
13 |
-
import { Maximize, Minimize } from "lucide-react";
|
14 |
-
|
15 |
-
interface LivePreviewProps {
|
16 |
-
currentPageData: { path: string; html: string } | undefined;
|
17 |
-
isAiWorking: boolean;
|
18 |
-
defaultHTML: string;
|
19 |
-
className?: string;
|
20 |
-
}
|
21 |
-
|
22 |
-
export interface LivePreviewRef {
|
23 |
-
reset: () => void;
|
24 |
-
}
|
25 |
-
|
26 |
-
export const LivePreview = forwardRef<LivePreviewRef, LivePreviewProps>(
|
27 |
-
({ currentPageData, isAiWorking, defaultHTML, className }, ref) => {
|
28 |
-
const [isMaximized, setIsMaximized] = useState(false);
|
29 |
-
const [displayedHtml, setDisplayedHtml] = useState<string>("");
|
30 |
-
const latestHtmlRef = useRef<string>("");
|
31 |
-
const displayedHtmlRef = useRef<string>("");
|
32 |
-
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
33 |
-
|
34 |
-
const reset = () => {
|
35 |
-
setIsMaximized(false);
|
36 |
-
setDisplayedHtml("");
|
37 |
-
latestHtmlRef.current = "";
|
38 |
-
displayedHtmlRef.current = "";
|
39 |
-
if (intervalRef.current) {
|
40 |
-
clearInterval(intervalRef.current);
|
41 |
-
intervalRef.current = null;
|
42 |
-
}
|
43 |
-
};
|
44 |
-
|
45 |
-
useImperativeHandle(ref, () => ({
|
46 |
-
reset,
|
47 |
-
}));
|
48 |
-
|
49 |
-
useEffect(() => {
|
50 |
-
displayedHtmlRef.current = displayedHtml;
|
51 |
-
}, [displayedHtml]);
|
52 |
-
|
53 |
-
useEffect(() => {
|
54 |
-
if (currentPageData?.html && currentPageData.html !== defaultHTML) {
|
55 |
-
latestHtmlRef.current = currentPageData.html;
|
56 |
-
}
|
57 |
-
}, [currentPageData?.html, defaultHTML]);
|
58 |
-
|
59 |
-
useEffect(() => {
|
60 |
-
if (!currentPageData?.html || currentPageData.html === defaultHTML) {
|
61 |
-
return;
|
62 |
-
}
|
63 |
-
|
64 |
-
if (!displayedHtml || !isAiWorking) {
|
65 |
-
setDisplayedHtml(currentPageData.html);
|
66 |
-
if (intervalRef.current) {
|
67 |
-
clearInterval(intervalRef.current);
|
68 |
-
intervalRef.current = null;
|
69 |
-
}
|
70 |
-
return;
|
71 |
-
}
|
72 |
-
|
73 |
-
if (isAiWorking && !intervalRef.current) {
|
74 |
-
intervalRef.current = setInterval(() => {
|
75 |
-
if (
|
76 |
-
latestHtmlRef.current &&
|
77 |
-
latestHtmlRef.current !== displayedHtmlRef.current
|
78 |
-
) {
|
79 |
-
setDisplayedHtml(latestHtmlRef.current);
|
80 |
-
}
|
81 |
-
}, 3000);
|
82 |
-
}
|
83 |
-
}, [currentPageData?.html, defaultHTML, isAiWorking, displayedHtml]);
|
84 |
-
|
85 |
-
useEffect(() => {
|
86 |
-
if (!isAiWorking && intervalRef.current) {
|
87 |
-
clearInterval(intervalRef.current);
|
88 |
-
intervalRef.current = null;
|
89 |
-
if (latestHtmlRef.current) {
|
90 |
-
setDisplayedHtml(latestHtmlRef.current);
|
91 |
-
}
|
92 |
-
}
|
93 |
-
}, [isAiWorking]);
|
94 |
-
|
95 |
-
useEffect(() => {
|
96 |
-
return () => {
|
97 |
-
if (intervalRef.current) {
|
98 |
-
clearInterval(intervalRef.current);
|
99 |
-
intervalRef.current = null;
|
100 |
-
}
|
101 |
-
};
|
102 |
-
}, []);
|
103 |
-
|
104 |
-
if (!displayedHtml) {
|
105 |
-
return null;
|
106 |
-
}
|
107 |
-
|
108 |
-
return (
|
109 |
-
<div
|
110 |
-
className={classNames(
|
111 |
-
"absolute z-40 bg-white/95 backdrop-blur-sm border border-neutral-200 shadow-lg transition-all duration-500 ease-out transform scale-100 opacity-100 animate-in slide-in-from-bottom-4 zoom-in-95 rounded-xl",
|
112 |
-
{
|
113 |
-
"shadow-green-500/20 shadow-2xl border-green-200": isAiWorking,
|
114 |
-
},
|
115 |
-
className
|
116 |
-
)}
|
117 |
-
>
|
118 |
-
<div
|
119 |
-
className={classNames(
|
120 |
-
"flex flex-col animate-in fade-in duration-300",
|
121 |
-
isMaximized ? "w-[60dvw] h-[80dvh]" : "w-80 h-96"
|
122 |
-
)}
|
123 |
-
>
|
124 |
-
<div className="flex items-center justify-between p-3 border-b border-neutral-200">
|
125 |
-
<div className="flex items-center gap-2">
|
126 |
-
<div className="size-2 bg-green-500 rounded-full animate-pulse shadow-sm shadow-green-500/50"></div>
|
127 |
-
<span className="text-xs font-medium text-neutral-800">
|
128 |
-
Live Preview
|
129 |
-
</span>
|
130 |
-
{isAiWorking && (
|
131 |
-
<span className="text-xs text-green-600 font-medium animate-pulse">
|
132 |
-
• Updating
|
133 |
-
</span>
|
134 |
-
)}
|
135 |
-
</div>
|
136 |
-
<div className="flex items-center gap-1">
|
137 |
-
<Button
|
138 |
-
variant="outline"
|
139 |
-
size="iconXs"
|
140 |
-
className="!rounded-md !border-neutral-200 hover:bg-neutral-50"
|
141 |
-
onClick={() => setIsMaximized(!isMaximized)}
|
142 |
-
>
|
143 |
-
{isMaximized ? (
|
144 |
-
<Minimize className="text-neutral-400 size-3" />
|
145 |
-
) : (
|
146 |
-
<Maximize className="text-neutral-400 size-3" />
|
147 |
-
)}
|
148 |
-
</Button>
|
149 |
-
</div>
|
150 |
-
</div>
|
151 |
-
<div className="flex-1 bg-black overflow-hidden relative rounded-b-xl">
|
152 |
-
<iframe
|
153 |
-
className="w-full h-full border-0"
|
154 |
-
srcDoc={displayedHtml}
|
155 |
-
sandbox="allow-scripts allow-same-origin"
|
156 |
-
title="Live Preview"
|
157 |
-
/>
|
158 |
-
</div>
|
159 |
-
</div>
|
160 |
-
</div>
|
161 |
-
);
|
162 |
-
}
|
163 |
-
);
|
164 |
-
|
165 |
-
LivePreview.displayName = "LivePreview";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/pages/index.tsx
DELETED
@@ -1,24 +0,0 @@
|
|
1 |
-
import { Page } from "@/types";
|
2 |
-
import { ListPagesItem } from "./page";
|
3 |
-
import { useEditor } from "@/hooks/useEditor";
|
4 |
-
|
5 |
-
export function ListPages() {
|
6 |
-
const { pages, setPages, currentPage, setCurrentPage } = useEditor();
|
7 |
-
return (
|
8 |
-
<div className="w-full flex items-center justify-start bg-neutral-950 overflow-auto flex-nowrap min-h-[45px]">
|
9 |
-
{pages.map((page: Page, i: number) => (
|
10 |
-
<ListPagesItem
|
11 |
-
key={page.path ?? i}
|
12 |
-
page={page}
|
13 |
-
currentPage={currentPage}
|
14 |
-
onSelectPage={setCurrentPage}
|
15 |
-
onDeletePage={(path) => {
|
16 |
-
setPages(pages.filter((page) => page.path !== path));
|
17 |
-
setCurrentPage("index.html");
|
18 |
-
}}
|
19 |
-
index={i}
|
20 |
-
/>
|
21 |
-
))}
|
22 |
-
</div>
|
23 |
-
);
|
24 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/pages/page.tsx
DELETED
@@ -1,56 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { FileCode, XIcon } from "lucide-react";
|
3 |
-
|
4 |
-
import { Button } from "@/components/ui/button";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
export function ListPagesItem({
|
8 |
-
page,
|
9 |
-
currentPage,
|
10 |
-
onSelectPage,
|
11 |
-
onDeletePage,
|
12 |
-
index,
|
13 |
-
}: {
|
14 |
-
page: Page;
|
15 |
-
currentPage: string;
|
16 |
-
onSelectPage: (path: string, newPath?: string) => void;
|
17 |
-
onDeletePage: (path: string) => void;
|
18 |
-
index: number;
|
19 |
-
}) {
|
20 |
-
return (
|
21 |
-
<div
|
22 |
-
key={index}
|
23 |
-
className={classNames(
|
24 |
-
"pl-6 pr-1 py-3 text-neutral-400 cursor-pointer text-sm hover:bg-neutral-900 flex items-center justify-center gap-1 group text-nowrap border-r border-neutral-800",
|
25 |
-
{
|
26 |
-
"bg-neutral-900 !text-white": currentPage === page.path,
|
27 |
-
"!pr-6": index === 0, // Ensure the first item has padding on the right
|
28 |
-
}
|
29 |
-
)}
|
30 |
-
onClick={() => onSelectPage(page.path)}
|
31 |
-
title={page.path}
|
32 |
-
>
|
33 |
-
<FileCode className="size-4 mr-1" />
|
34 |
-
{page.path}
|
35 |
-
{index > 0 && (
|
36 |
-
<Button
|
37 |
-
size="iconXsss"
|
38 |
-
variant="ghost"
|
39 |
-
className="group-hover:opacity-100 opacity-0 !h-auto"
|
40 |
-
onClick={(e) => {
|
41 |
-
e.stopPropagation();
|
42 |
-
if (
|
43 |
-
window.confirm(
|
44 |
-
"Are you sure you want to delete this page? This action cannot be undone."
|
45 |
-
)
|
46 |
-
) {
|
47 |
-
onDeletePage(page.path);
|
48 |
-
}
|
49 |
-
}}
|
50 |
-
>
|
51 |
-
<XIcon className="h-3 text-neutral-400 cursor-pointer hover:text-neutral-300" />
|
52 |
-
</Button>
|
53 |
-
)}
|
54 |
-
</div>
|
55 |
-
);
|
56 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/preview/index.tsx
CHANGED
@@ -1,358 +1,176 @@
|
|
1 |
"use client";
|
2 |
-
|
3 |
-
import { useRef, useState, useEffect, forwardRef } from "react";
|
4 |
import { useUpdateEffect } from "react-use";
|
|
|
5 |
import classNames from "classnames";
|
|
|
6 |
|
7 |
import { cn } from "@/lib/utils";
|
8 |
import { GridPattern } from "@/components/magic-ui/grid-pattern";
|
9 |
-
import { useEditor } from "@/hooks/useEditor";
|
10 |
-
import { useAi } from "@/hooks/useAi";
|
11 |
import { htmlTagToText } from "@/lib/html-tag-to-text";
|
12 |
-
import { AnimatedBlobs } from "@/components/animated-blobs";
|
13 |
-
import { AiLoading } from "../ask-ai/loading";
|
14 |
-
import { defaultHTML } from "@/lib/consts";
|
15 |
-
import { Button } from "@/components/ui/button";
|
16 |
-
import { LivePreview, LivePreviewRef } from "../live-preview";
|
17 |
-
import { HistoryNotification } from "../history-notification";
|
18 |
-
import { AlertCircle } from "lucide-react";
|
19 |
-
import { api } from "@/lib/api";
|
20 |
-
import { toast } from "sonner";
|
21 |
-
import Loading from "@/components/loading";
|
22 |
-
|
23 |
-
export const Preview = forwardRef<LivePreviewRef, { isNew: boolean }>(
|
24 |
-
({ isNew }, ref) => {
|
25 |
-
const {
|
26 |
-
project,
|
27 |
-
device,
|
28 |
-
isLoadingProject,
|
29 |
-
currentTab,
|
30 |
-
currentCommit,
|
31 |
-
setCurrentCommit,
|
32 |
-
currentPageData,
|
33 |
-
pages,
|
34 |
-
setPages,
|
35 |
-
setCurrentPage,
|
36 |
-
isSameHtml,
|
37 |
-
} = useEditor();
|
38 |
-
const {
|
39 |
-
isEditableModeEnabled,
|
40 |
-
setSelectedElement,
|
41 |
-
isAiWorking,
|
42 |
-
globalAiLoading,
|
43 |
-
} = useAi();
|
44 |
-
|
45 |
-
const iframeRef = useRef<HTMLIFrameElement>(null);
|
46 |
-
|
47 |
-
const [hoveredElement, setHoveredElement] = useState<{
|
48 |
-
tagName: string;
|
49 |
-
rect: { top: number; left: number; width: number; height: number };
|
50 |
-
} | null>(null);
|
51 |
-
const [isPromotingVersion, setIsPromotingVersion] = useState(false);
|
52 |
-
const [stableHtml, setStableHtml] = useState<string>("");
|
53 |
-
|
54 |
-
useEffect(() => {
|
55 |
-
if (!isAiWorking && !globalAiLoading && currentPageData?.html) {
|
56 |
-
setStableHtml(currentPageData.html);
|
57 |
-
}
|
58 |
-
}, [isAiWorking, globalAiLoading, currentPageData?.html]);
|
59 |
-
|
60 |
-
useEffect(() => {
|
61 |
-
if (
|
62 |
-
currentPageData?.html &&
|
63 |
-
!stableHtml &&
|
64 |
-
!isAiWorking &&
|
65 |
-
!globalAiLoading
|
66 |
-
) {
|
67 |
-
setStableHtml(currentPageData.html);
|
68 |
-
}
|
69 |
-
}, [currentPageData?.html, stableHtml, isAiWorking, globalAiLoading]);
|
70 |
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
}
|
92 |
}
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
.
|
104 |
-
|
105 |
-
setCurrentCommit(null);
|
106 |
-
setPages(res.data.pages);
|
107 |
-
setCurrentPage(res.data.pages[0].path);
|
108 |
-
toast.success("Version promoted successfully");
|
109 |
-
}
|
110 |
-
})
|
111 |
-
.catch((err) => {
|
112 |
-
toast.error(err.response.data.error);
|
113 |
-
});
|
114 |
-
setIsPromotingVersion(false);
|
115 |
-
};
|
116 |
-
|
117 |
-
const handleMouseOver = (event: MouseEvent) => {
|
118 |
-
if (iframeRef?.current) {
|
119 |
-
const iframeDocument = iframeRef.current.contentDocument;
|
120 |
-
if (iframeDocument) {
|
121 |
-
const targetElement = event.target as HTMLElement;
|
122 |
-
if (
|
123 |
-
hoveredElement?.tagName !== targetElement.tagName ||
|
124 |
-
hoveredElement?.rect.top !==
|
125 |
-
targetElement.getBoundingClientRect().top ||
|
126 |
-
hoveredElement?.rect.left !==
|
127 |
-
targetElement.getBoundingClientRect().left ||
|
128 |
-
hoveredElement?.rect.width !==
|
129 |
-
targetElement.getBoundingClientRect().width ||
|
130 |
-
hoveredElement?.rect.height !==
|
131 |
-
targetElement.getBoundingClientRect().height
|
132 |
-
) {
|
133 |
-
if (targetElement !== iframeDocument.body) {
|
134 |
-
const rect = targetElement.getBoundingClientRect();
|
135 |
-
setHoveredElement({
|
136 |
-
tagName: targetElement.tagName,
|
137 |
-
rect: {
|
138 |
-
top: rect.top,
|
139 |
-
left: rect.left,
|
140 |
-
width: rect.width,
|
141 |
-
height: rect.height,
|
142 |
-
},
|
143 |
-
});
|
144 |
-
targetElement.classList.add("hovered-element");
|
145 |
-
} else {
|
146 |
-
return setHoveredElement(null);
|
147 |
-
}
|
148 |
-
}
|
149 |
}
|
150 |
}
|
151 |
-
}
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
const
|
156 |
-
if (iframeRef?.current) {
|
157 |
const iframeDocument = iframeRef.current.contentDocument;
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
setSelectedElement(targetElement);
|
162 |
-
}
|
163 |
-
}
|
164 |
}
|
165 |
};
|
166 |
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
element: HTMLElement
|
173 |
-
): HTMLAnchorElement | null => {
|
174 |
-
let current = element;
|
175 |
-
while (current && current !== iframeDocument.body) {
|
176 |
-
if (current.tagName === "A") {
|
177 |
-
return current as HTMLAnchorElement;
|
178 |
-
}
|
179 |
-
current = current.parentElement as HTMLElement;
|
180 |
-
}
|
181 |
-
return null;
|
182 |
-
};
|
183 |
-
|
184 |
-
const anchorElement = findClosestAnchor(event.target as HTMLElement);
|
185 |
-
if (anchorElement) {
|
186 |
-
let href = anchorElement.getAttribute("href");
|
187 |
-
if (href) {
|
188 |
-
event.stopPropagation();
|
189 |
-
event.preventDefault();
|
190 |
-
|
191 |
-
if (href.includes("#") && !href.includes(".html")) {
|
192 |
-
const targetElement = iframeDocument.querySelector(href);
|
193 |
-
if (targetElement) {
|
194 |
-
targetElement.scrollIntoView({ behavior: "smooth" });
|
195 |
-
}
|
196 |
-
return;
|
197 |
-
}
|
198 |
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
}
|
204 |
-
}
|
205 |
-
}
|
206 |
}
|
207 |
}
|
208 |
-
}
|
209 |
-
|
210 |
-
|
211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
className={classNames(
|
213 |
-
"
|
214 |
{
|
215 |
-
"
|
216 |
-
"max-lg:
|
|
|
|
|
|
|
217 |
}
|
218 |
)}
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
left: hoveredElement.rect.left,
|
234 |
-
width: hoveredElement.rect.width,
|
235 |
-
height: hoveredElement.rect.height,
|
236 |
-
}}
|
237 |
-
>
|
238 |
-
<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">
|
239 |
-
{htmlTagToText(hoveredElement.tagName.toLowerCase())}
|
240 |
-
</span>
|
241 |
-
</div>
|
242 |
-
)}
|
243 |
-
{isNew && !isLoadingProject && !globalAiLoading && isSameHtml ? (
|
244 |
-
<iframe
|
245 |
-
className={classNames(
|
246 |
-
"w-full select-none transition-all duration-200 bg-black h-full",
|
247 |
-
{
|
248 |
-
"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]":
|
249 |
-
device === "mobile",
|
250 |
-
}
|
251 |
-
)}
|
252 |
-
srcDoc={defaultHTML}
|
253 |
-
/>
|
254 |
-
) : (isNew && globalAiLoading) || isLoadingProject ? (
|
255 |
-
<div className="w-full h-full flex items-center justify-center relative">
|
256 |
-
<div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
|
257 |
-
<AiLoading
|
258 |
-
text={isLoadingProject ? "Fetching your project..." : undefined}
|
259 |
-
className="flex-col"
|
260 |
-
/>
|
261 |
-
<AnimatedBlobs />
|
262 |
-
<AnimatedBlobs />
|
263 |
-
</div>
|
264 |
-
{!isLoadingProject && (
|
265 |
-
<LivePreview
|
266 |
-
ref={ref}
|
267 |
-
currentPageData={currentPageData}
|
268 |
-
isAiWorking={isAiWorking}
|
269 |
-
defaultHTML={defaultHTML}
|
270 |
-
className="bottom-4 left-4"
|
271 |
-
/>
|
272 |
-
)}
|
273 |
-
</div>
|
274 |
-
) : (
|
275 |
-
<>
|
276 |
-
<iframe
|
277 |
-
id="preview-iframe"
|
278 |
-
ref={iframeRef}
|
279 |
-
className={classNames(
|
280 |
-
"w-full select-none transition-all duration-200 bg-black h-full",
|
281 |
-
{
|
282 |
-
"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]":
|
283 |
-
device === "mobile",
|
284 |
-
}
|
285 |
-
)}
|
286 |
-
src={
|
287 |
-
currentCommit
|
288 |
-
? `https://${project?.space_id?.replaceAll(
|
289 |
-
"/",
|
290 |
-
"-"
|
291 |
-
)}--rev-${currentCommit.slice(0, 7)}.static.hf.space`
|
292 |
-
: undefined
|
293 |
-
}
|
294 |
-
srcDoc={!currentCommit ? stableHtml : undefined}
|
295 |
-
onLoad={
|
296 |
-
!currentCommit
|
297 |
-
? () => {
|
298 |
-
if (iframeRef?.current?.contentWindow?.document?.body) {
|
299 |
-
iframeRef.current.contentWindow.document.body.scrollIntoView(
|
300 |
-
{
|
301 |
-
block: isAiWorking ? "end" : "start",
|
302 |
-
inline: "nearest",
|
303 |
-
behavior: isAiWorking ? "instant" : "smooth",
|
304 |
-
}
|
305 |
-
);
|
306 |
-
}
|
307 |
-
// add event listener to all links in the iframe to handle navigation
|
308 |
-
if (iframeRef?.current?.contentWindow?.document) {
|
309 |
-
const links =
|
310 |
-
iframeRef.current.contentWindow.document.querySelectorAll(
|
311 |
-
"a"
|
312 |
-
);
|
313 |
-
links.forEach((link) => {
|
314 |
-
link.addEventListener(
|
315 |
-
"click",
|
316 |
-
handleCustomNavigation
|
317 |
-
);
|
318 |
-
});
|
319 |
-
}
|
320 |
-
}
|
321 |
-
: undefined
|
322 |
-
}
|
323 |
-
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
324 |
-
allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"
|
325 |
-
/>
|
326 |
-
<div
|
327 |
-
className={classNames(
|
328 |
-
"w-full h-full flex items-center justify-center absolute left-0 top-0 bg-black/40 backdrop-blur-lg transition-all duration-200",
|
329 |
-
{
|
330 |
-
"opacity-0 pointer-events-none": !globalAiLoading,
|
331 |
-
}
|
332 |
-
)}
|
333 |
-
>
|
334 |
-
<div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">
|
335 |
-
<AiLoading
|
336 |
-
text={
|
337 |
-
isLoadingProject ? "Fetching your project..." : undefined
|
338 |
-
}
|
339 |
-
className="flex-col"
|
340 |
-
/>
|
341 |
-
<AnimatedBlobs />
|
342 |
-
<AnimatedBlobs />
|
343 |
-
</div>
|
344 |
-
</div>
|
345 |
-
<HistoryNotification
|
346 |
-
isVisible={!!currentCommit}
|
347 |
-
isPromotingVersion={isPromotingVersion}
|
348 |
-
onPromoteVersion={promoteVersion}
|
349 |
-
onGoBackToCurrent={() => setCurrentCommit(null)}
|
350 |
-
/>
|
351 |
-
</>
|
352 |
-
)}
|
353 |
-
</div>
|
354 |
-
);
|
355 |
-
}
|
356 |
-
);
|
357 |
-
|
358 |
-
Preview.displayName = "Preview";
|
|
|
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:
|
135 |
+
selectedElement.getBoundingClientRect().top +
|
136 |
+
(currentTab === "preview" ? 0 : 24),
|
137 |
+
left:
|
138 |
+
selectedElement.getBoundingClientRect().left +
|
139 |
+
(currentTab === "preview" ? 0 : 24),
|
140 |
+
width: selectedElement.getBoundingClientRect().width,
|
141 |
+
height: selectedElement.getBoundingClientRect().height,
|
142 |
+
}}
|
143 |
+
>
|
144 |
+
<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">
|
145 |
+
{htmlTagToText(selectedElement.tagName.toLowerCase())}
|
146 |
+
</span>
|
147 |
+
</div>
|
148 |
+
)}
|
149 |
+
<iframe
|
150 |
+
id="preview-iframe"
|
151 |
+
ref={iframeRef}
|
152 |
+
title="output"
|
153 |
className={classNames(
|
154 |
+
"w-full select-none transition-all duration-200 bg-black h-full",
|
155 |
{
|
156 |
+
"pointer-events-none": isResizing || isAiWorking,
|
157 |
+
"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]":
|
158 |
+
device === "mobile",
|
159 |
+
"lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
|
160 |
+
currentTab !== "preview" && device === "desktop",
|
161 |
}
|
162 |
)}
|
163 |
+
srcDoc={html}
|
164 |
+
onLoad={() => {
|
165 |
+
if (iframeRef?.current?.contentWindow?.document?.body) {
|
166 |
+
iframeRef.current.contentWindow.document.body.scrollIntoView({
|
167 |
+
block: isAiWorking ? "end" : "start",
|
168 |
+
inline: "nearest",
|
169 |
+
behavior: isAiWorking ? "instant" : "smooth",
|
170 |
+
});
|
171 |
+
}
|
172 |
+
}}
|
173 |
+
/>
|
174 |
+
</div>
|
175 |
+
);
|
176 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|