Spaces:
Running
Running
initial
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- app/(public)/layout.tsx +15 -0
- app/(public)/page.tsx +193 -0
- app/(public)/projects/page.tsx +12 -0
- app/actions/auth.ts +18 -0
- app/actions/projects.ts +79 -0
- app/api/ask/route.ts +541 -0
- app/api/auth/route.ts +86 -0
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +141 -0
- app/api/me/projects/[namespace]/[repoId]/images/route.ts +109 -0
- app/api/me/projects/[namespace]/[repoId]/route.ts +235 -0
- app/api/me/projects/route.ts +104 -0
- app/api/me/route.ts +25 -0
- app/api/proxy/route.ts +246 -0
- app/api/re-design/route.ts +39 -0
- app/auth/callback/page.tsx +74 -0
- app/auth/page.tsx +28 -0
- app/globals.css +0 -26
- app/layout.tsx +94 -12
- app/page.tsx +0 -103
- app/projects/[namespace]/[repoId]/page.tsx +10 -0
- app/projects/new/page.tsx +5 -0
- assets/globals.css +371 -0
- assets/logo.svg +316 -0
- assets/space.svg +7 -0
- components.json +21 -0
- components/animated-blobs/index.tsx +34 -0
- components/animated-text/index.tsx +123 -0
- components/contexts/app-context.tsx +52 -0
- components/contexts/login-context.tsx +61 -0
- components/contexts/pro-context.tsx +48 -0
- components/contexts/tanstack-query-context.tsx +31 -0
- components/contexts/user-context.tsx +8 -0
- components/editor/ask-ai/index.tsx +259 -0
- components/editor/ask-ai/loading.tsx +32 -0
- components/editor/ask-ai/prompt-builder/content-modal.tsx +196 -0
- components/editor/ask-ai/prompt-builder/index.tsx +73 -0
- components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +58 -0
- components/editor/ask-ai/prompt-builder/themes.tsx +48 -0
- components/editor/ask-ai/re-imagine.tsx +169 -0
- components/editor/ask-ai/selected-files.tsx +47 -0
- components/editor/ask-ai/selected-html-element.tsx +57 -0
- components/editor/ask-ai/selector.tsx +41 -0
- components/editor/ask-ai/settings.tsx +220 -0
- components/editor/ask-ai/uploader.tsx +165 -0
- components/editor/header/index.tsx +86 -0
- components/editor/header/switch-tab.tsx +58 -0
- components/editor/history/index.tsx +91 -0
- components/editor/index.tsx +106 -0
- components/editor/pages/index.tsx +24 -0
- components/editor/pages/page.tsx +56 -0
app/(public)/layout.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Navigation from "@/components/public/navigation";
|
| 2 |
+
|
| 3 |
+
export default async function PublicLayout({
|
| 4 |
+
children,
|
| 5 |
+
}: Readonly<{
|
| 6 |
+
children: React.ReactNode;
|
| 7 |
+
}>) {
|
| 8 |
+
return (
|
| 9 |
+
<div className="h-screen bg-neutral-950 z-1 relative overflow-auto scroll-smooth">
|
| 10 |
+
<div className="background__noisy" />
|
| 11 |
+
<Navigation />
|
| 12 |
+
{children}
|
| 13 |
+
</div>
|
| 14 |
+
);
|
| 15 |
+
}
|
app/(public)/page.tsx
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// import { AskAi } from "@/components/space/ask-ai";
|
| 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 |
+
<div className="">
|
| 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-sky-100/10 bg-gradient-to-r from-sky-500/15 to-sky-sky-500/5 text-sm text-sky-300 px-3 py-1 max-w-max mx-auto mb-2">
|
| 12 |
+
✨ DeepSite v3 is out!
|
| 13 |
+
</div>
|
| 14 |
+
<h1 className="text-6xl lg:text-8xl font-semibold text-white font-mono max-w-4xl">
|
| 15 |
+
Code your website with AI in seconds
|
| 16 |
+
</h1>
|
| 17 |
+
<AnimatedText className="text-xl lg:text-2xl text-neutral-300/80 mt-4 text-center max-w-2xl" />
|
| 18 |
+
<div className="mt-14 max-w-2xl w-full mx-auto">{/* <AskAi /> */}</div>
|
| 19 |
+
<AnimatedBlobs />
|
| 20 |
+
</header>
|
| 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 |
+
{/* Bento Grid */}
|
| 37 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
|
| 38 |
+
{/* Multi Pages */}
|
| 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 |
+
{/* Background Effects */}
|
| 186 |
+
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
| 187 |
+
<div className="w-1/3 h-1/3 bg-gradient-to-r from-purple-500 to-pink-500 opacity-5 blur-3xl absolute top-20 left-10 rounded-full" />
|
| 188 |
+
<div className="w-1/4 h-1/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-10 blur-3xl absolute bottom-20 right-20 rounded-full" />
|
| 189 |
+
<div className="w-1/5 h-1/5 bg-gradient-to-r from-amber-500 to-rose-500 opacity-8 blur-3xl absolute top-1/2 left-1/3 rounded-full" />
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
);
|
| 193 |
+
}
|
app/(public)/projects/page.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getProjects } from "@/app/actions/projects";
|
| 2 |
+
import { MyProjects } from "@/components/my-projects";
|
| 3 |
+
import { NotLogged } from "@/components/not-logged/not-logged";
|
| 4 |
+
|
| 5 |
+
export default async function ProjectsPage() {
|
| 6 |
+
const { ok, projects } = await getProjects();
|
| 7 |
+
if (!ok) {
|
| 8 |
+
return <NotLogged />;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
return <MyProjects projects={projects} />;
|
| 12 |
+
}
|
app/actions/auth.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use server";
|
| 2 |
+
|
| 3 |
+
import { headers } from "next/headers";
|
| 4 |
+
|
| 5 |
+
export async function getAuth() {
|
| 6 |
+
const authList = await headers();
|
| 7 |
+
const host = authList.get("host") ?? "localhost:3000";
|
| 8 |
+
const url = host.includes("/spaces/enzostvs")
|
| 9 |
+
? "enzostvs-deepsite.hf.space"
|
| 10 |
+
: host;
|
| 11 |
+
const redirect_uri =
|
| 12 |
+
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 13 |
+
url +
|
| 14 |
+
"/auth/callback";
|
| 15 |
+
|
| 16 |
+
const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
|
| 17 |
+
return loginRedirectUrl;
|
| 18 |
+
}
|
app/actions/projects.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use server";
|
| 2 |
+
|
| 3 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 4 |
+
import { NextResponse } from "next/server";
|
| 5 |
+
import { listSpaces } from "@huggingface/hub";
|
| 6 |
+
import { ProjectType } from "@/types";
|
| 7 |
+
|
| 8 |
+
export async function getProjects(): Promise<{
|
| 9 |
+
ok: boolean;
|
| 10 |
+
projects: ProjectType[];
|
| 11 |
+
isEmpty?: boolean;
|
| 12 |
+
}> {
|
| 13 |
+
const user = await isAuthenticated();
|
| 14 |
+
|
| 15 |
+
if (user instanceof NextResponse || !user) {
|
| 16 |
+
return {
|
| 17 |
+
ok: false,
|
| 18 |
+
projects: [],
|
| 19 |
+
};
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// await dbConnect();
|
| 23 |
+
// const projects = await Project.find({
|
| 24 |
+
// user_id: user?.id,
|
| 25 |
+
// })
|
| 26 |
+
// .sort({ _createdAt: -1 })
|
| 27 |
+
// .limit(100)
|
| 28 |
+
// .lean();
|
| 29 |
+
// if (!projects) {
|
| 30 |
+
// return {
|
| 31 |
+
// ok: true,
|
| 32 |
+
// isEmpty: true,
|
| 33 |
+
// projects: [],
|
| 34 |
+
// };
|
| 35 |
+
// }
|
| 36 |
+
|
| 37 |
+
// const mappedProjects = []
|
| 38 |
+
|
| 39 |
+
// for (const project of projects) {
|
| 40 |
+
// const space = await spaceInfo({
|
| 41 |
+
// name: project.space_id,
|
| 42 |
+
// accessToken: user.token as string,
|
| 43 |
+
// additionalFields: ["author", "cardData"],
|
| 44 |
+
// });
|
| 45 |
+
// if (!space.private) {
|
| 46 |
+
// mappedProjects.push({
|
| 47 |
+
// ...project,
|
| 48 |
+
// name: space.name,
|
| 49 |
+
// cardData: space.cardData,
|
| 50 |
+
// });
|
| 51 |
+
// }
|
| 52 |
+
// }
|
| 53 |
+
const projects = [];
|
| 54 |
+
// get user spaces from Hugging Face
|
| 55 |
+
for await (const space of listSpaces({
|
| 56 |
+
accessToken: user.token as string,
|
| 57 |
+
additionalFields: ["author", "cardData"],
|
| 58 |
+
search: {
|
| 59 |
+
owner: user.name,
|
| 60 |
+
}
|
| 61 |
+
})) {
|
| 62 |
+
if (
|
| 63 |
+
!space.private &&
|
| 64 |
+
space.sdk === "static" &&
|
| 65 |
+
Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
|
| 66 |
+
(
|
| 67 |
+
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
| 68 |
+
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
| 69 |
+
)
|
| 70 |
+
) {
|
| 71 |
+
projects.push(space);
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
return {
|
| 76 |
+
ok: true,
|
| 77 |
+
projects,
|
| 78 |
+
};
|
| 79 |
+
}
|
app/api/ask/route.ts
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
+
import type { NextRequest } from "next/server";
|
| 3 |
+
import { NextResponse } from "next/server";
|
| 4 |
+
import { headers } from "next/headers";
|
| 5 |
+
import { InferenceClient } from "@huggingface/inference";
|
| 6 |
+
|
| 7 |
+
import { MODELS, PROVIDERS } from "@/lib/providers";
|
| 8 |
+
import {
|
| 9 |
+
DIVIDER,
|
| 10 |
+
FOLLOW_UP_SYSTEM_PROMPT,
|
| 11 |
+
INITIAL_SYSTEM_PROMPT,
|
| 12 |
+
MAX_REQUESTS_PER_IP,
|
| 13 |
+
NEW_PAGE_END,
|
| 14 |
+
NEW_PAGE_START,
|
| 15 |
+
REPLACE_END,
|
| 16 |
+
SEARCH_START,
|
| 17 |
+
UPDATE_PAGE_START,
|
| 18 |
+
UPDATE_PAGE_END,
|
| 19 |
+
} from "@/lib/prompts";
|
| 20 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 21 |
+
import { Page } from "@/types";
|
| 22 |
+
import { uploadFiles } from "@huggingface/hub";
|
| 23 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 24 |
+
import { getBestProvider } from "@/lib/best-provider";
|
| 25 |
+
import { rewritePrompt } from "@/lib/rewrite-prompt";
|
| 26 |
+
|
| 27 |
+
const ipAddresses = new Map();
|
| 28 |
+
|
| 29 |
+
export async function POST(request: NextRequest) {
|
| 30 |
+
const authHeaders = await headers();
|
| 31 |
+
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
| 32 |
+
|
| 33 |
+
const body = await request.json();
|
| 34 |
+
const { prompt, provider, model, redesignMarkdown, enhancedSettings } = body;
|
| 35 |
+
|
| 36 |
+
if (!model || (!prompt && !redesignMarkdown)) {
|
| 37 |
+
return NextResponse.json(
|
| 38 |
+
{ ok: false, error: "Missing required fields" },
|
| 39 |
+
{ status: 400 }
|
| 40 |
+
);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const selectedModel = MODELS.find(
|
| 44 |
+
(m) => m.value === model || m.label === model
|
| 45 |
+
);
|
| 46 |
+
|
| 47 |
+
if (!selectedModel) {
|
| 48 |
+
return NextResponse.json(
|
| 49 |
+
{ ok: false, error: "Invalid model selected" },
|
| 50 |
+
{ status: 400 }
|
| 51 |
+
);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
if (!selectedModel.providers.includes(provider) && provider !== "auto") {
|
| 55 |
+
return NextResponse.json(
|
| 56 |
+
{
|
| 57 |
+
ok: false,
|
| 58 |
+
error: `The selected model does not support the ${provider} provider.`,
|
| 59 |
+
openSelectProvider: true,
|
| 60 |
+
},
|
| 61 |
+
{ status: 400 }
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
let token = userToken;
|
| 66 |
+
let billTo: string | null = null;
|
| 67 |
+
|
| 68 |
+
/**
|
| 69 |
+
* Handle local usage token, this bypass the need for a user token
|
| 70 |
+
* and allows local testing without authentication.
|
| 71 |
+
* This is useful for development and testing purposes.
|
| 72 |
+
*/
|
| 73 |
+
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
|
| 74 |
+
token = process.env.HF_TOKEN;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
|
| 78 |
+
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 79 |
+
: authHeaders.get("x-forwarded-for");
|
| 80 |
+
|
| 81 |
+
if (!token) {
|
| 82 |
+
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 83 |
+
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 84 |
+
return NextResponse.json(
|
| 85 |
+
{
|
| 86 |
+
ok: false,
|
| 87 |
+
openLogin: true,
|
| 88 |
+
message: "Log In to continue using the service",
|
| 89 |
+
},
|
| 90 |
+
{ status: 429 }
|
| 91 |
+
);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
token = process.env.DEFAULT_HF_TOKEN as string;
|
| 95 |
+
billTo = "huggingface";
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
| 99 |
+
|
| 100 |
+
let rewrittenPrompt = prompt;
|
| 101 |
+
|
| 102 |
+
if (enhancedSettings.isActive) {
|
| 103 |
+
rewrittenPrompt = await rewritePrompt(prompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
console.log(rewrittenPrompt);
|
| 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",
|
| 116 |
+
"Cache-Control": "no-cache",
|
| 117 |
+
Connection: "keep-alive",
|
| 118 |
+
},
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
(async () => {
|
| 122 |
+
// let completeResponse = "";
|
| 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",
|
| 132 |
+
content: INITIAL_SYSTEM_PROMPT,
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
role: "user",
|
| 136 |
+
content: `${rewritePrompt}${redesignMarkdown ? `\n\nHere is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : ""}`
|
| 137 |
+
},
|
| 138 |
+
],
|
| 139 |
+
max_tokens: selectedProvider.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 |
+
await writer.write(encoder.encode(chunk));
|
| 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(
|
| 161 |
+
encoder.encode(
|
| 162 |
+
JSON.stringify({
|
| 163 |
+
ok: false,
|
| 164 |
+
openProModal: true,
|
| 165 |
+
message: error.message,
|
| 166 |
+
})
|
| 167 |
+
)
|
| 168 |
+
);
|
| 169 |
+
} else if (error?.message?.includes("inference provider information")) {
|
| 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({
|
| 184 |
+
ok: false,
|
| 185 |
+
message:
|
| 186 |
+
error.message ||
|
| 187 |
+
"An error occurred while processing your request.",
|
| 188 |
+
})
|
| 189 |
+
)
|
| 190 |
+
);
|
| 191 |
+
}
|
| 192 |
+
} finally {
|
| 193 |
+
// Ensure the writer is always closed, even if already closed
|
| 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 |
+
|
| 202 |
+
return response;
|
| 203 |
+
} catch (error: any) {
|
| 204 |
+
return NextResponse.json(
|
| 205 |
+
{
|
| 206 |
+
ok: false,
|
| 207 |
+
openSelectProvider: true,
|
| 208 |
+
message:
|
| 209 |
+
error?.message || "An error occurred while processing your request.",
|
| 210 |
+
},
|
| 211 |
+
{ status: 500 }
|
| 212 |
+
);
|
| 213 |
+
}
|
| 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, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId } =
|
| 226 |
+
body;
|
| 227 |
+
|
| 228 |
+
if (!prompt || pages.length === 0 || !repoId) {
|
| 229 |
+
return NextResponse.json(
|
| 230 |
+
{ ok: false, error: "Missing required fields" },
|
| 231 |
+
{ status: 400 }
|
| 232 |
+
);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
const selectedModel = MODELS.find(
|
| 236 |
+
(m) => m.value === model || m.label === model
|
| 237 |
+
);
|
| 238 |
+
if (!selectedModel) {
|
| 239 |
+
return NextResponse.json(
|
| 240 |
+
{ ok: false, error: "Invalid model selected" },
|
| 241 |
+
{ status: 400 }
|
| 242 |
+
);
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
let token = user.token as string;
|
| 246 |
+
let billTo: string | null = null;
|
| 247 |
+
|
| 248 |
+
/**
|
| 249 |
+
* Handle local usage token, this bypass the need for a user token
|
| 250 |
+
* and allows local testing without authentication.
|
| 251 |
+
* This is useful for development and testing purposes.
|
| 252 |
+
*/
|
| 253 |
+
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
|
| 254 |
+
token = process.env.HF_TOKEN;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
|
| 258 |
+
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 259 |
+
: authHeaders.get("x-forwarded-for");
|
| 260 |
+
|
| 261 |
+
if (!token) {
|
| 262 |
+
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 263 |
+
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 264 |
+
return NextResponse.json(
|
| 265 |
+
{
|
| 266 |
+
ok: false,
|
| 267 |
+
openLogin: true,
|
| 268 |
+
message: "Log In to continue using the service",
|
| 269 |
+
},
|
| 270 |
+
{ status: 429 }
|
| 271 |
+
);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
token = process.env.DEFAULT_HF_TOKEN as string;
|
| 275 |
+
billTo = "huggingface";
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
const client = new InferenceClient(token);
|
| 279 |
+
|
| 280 |
+
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
| 281 |
+
|
| 282 |
+
try {
|
| 283 |
+
const response = await client.chatCompletion(
|
| 284 |
+
{
|
| 285 |
+
model: selectedModel.value,
|
| 286 |
+
provider: selectedProvider,
|
| 287 |
+
messages: [
|
| 288 |
+
{
|
| 289 |
+
role: "system",
|
| 290 |
+
content: FOLLOW_UP_SYSTEM_PROMPT,
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
role: "user",
|
| 294 |
+
content: previousPrompts
|
| 295 |
+
? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
|
| 296 |
+
: "You are modifying the HTML file based on the user's request.",
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
role: "assistant",
|
| 300 |
+
|
| 301 |
+
content: `${
|
| 302 |
+
selectedElementHtml
|
| 303 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
| 304 |
+
: ""
|
| 305 |
+
}. Current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}. ${files?.length > 0 ? `Current images: ${files?.map((f: string) => `- ${f}`).join("\n")}.` : ""}`,
|
| 306 |
+
},
|
| 307 |
+
{
|
| 308 |
+
role: "user",
|
| 309 |
+
content: prompt,
|
| 310 |
+
},
|
| 311 |
+
],
|
| 312 |
+
...(selectedProvider.id !== "sambanova"
|
| 313 |
+
? {
|
| 314 |
+
max_tokens: selectedProvider.max_tokens,
|
| 315 |
+
}
|
| 316 |
+
: {}),
|
| 317 |
+
},
|
| 318 |
+
billTo ? { billTo } : {}
|
| 319 |
+
);
|
| 320 |
+
|
| 321 |
+
const chunk = response.choices[0]?.message?.content;
|
| 322 |
+
if (!chunk) {
|
| 323 |
+
return NextResponse.json(
|
| 324 |
+
{ ok: false, message: "No content returned from the model" },
|
| 325 |
+
{ status: 400 }
|
| 326 |
+
);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
if (chunk) {
|
| 330 |
+
const updatedLines: number[][] = [];
|
| 331 |
+
let newHtml = "";
|
| 332 |
+
const updatedPages = [...(pages || [])];
|
| 333 |
+
|
| 334 |
+
const updatePageRegex = new RegExp(`${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
|
| 335 |
+
let updatePageMatch;
|
| 336 |
+
|
| 337 |
+
while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
|
| 338 |
+
const [, pagePath, pageContent] = updatePageMatch;
|
| 339 |
+
|
| 340 |
+
const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
| 341 |
+
if (pageIndex !== -1) {
|
| 342 |
+
let pageHtml = updatedPages[pageIndex].html;
|
| 343 |
+
|
| 344 |
+
let processedContent = pageContent;
|
| 345 |
+
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 346 |
+
if (htmlMatch) {
|
| 347 |
+
processedContent = htmlMatch[1];
|
| 348 |
+
}
|
| 349 |
+
let position = 0;
|
| 350 |
+
let moreBlocks = true;
|
| 351 |
+
|
| 352 |
+
while (moreBlocks) {
|
| 353 |
+
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
| 354 |
+
if (searchStartIndex === -1) {
|
| 355 |
+
moreBlocks = false;
|
| 356 |
+
continue;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
| 360 |
+
if (dividerIndex === -1) {
|
| 361 |
+
moreBlocks = false;
|
| 362 |
+
continue;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
| 366 |
+
if (replaceEndIndex === -1) {
|
| 367 |
+
moreBlocks = false;
|
| 368 |
+
continue;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
const searchBlock = processedContent.substring(
|
| 372 |
+
searchStartIndex + SEARCH_START.length,
|
| 373 |
+
dividerIndex
|
| 374 |
+
);
|
| 375 |
+
const replaceBlock = processedContent.substring(
|
| 376 |
+
dividerIndex + DIVIDER.length,
|
| 377 |
+
replaceEndIndex
|
| 378 |
+
);
|
| 379 |
+
|
| 380 |
+
if (searchBlock.trim() === "") {
|
| 381 |
+
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
| 382 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 383 |
+
} else {
|
| 384 |
+
const blockPosition = pageHtml.indexOf(searchBlock);
|
| 385 |
+
if (blockPosition !== -1) {
|
| 386 |
+
const beforeText = pageHtml.substring(0, blockPosition);
|
| 387 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 388 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 389 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 390 |
+
|
| 391 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 392 |
+
pageHtml = pageHtml.replace(searchBlock, replaceBlock);
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
position = replaceEndIndex + REPLACE_END.length;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
updatedPages[pageIndex].html = pageHtml;
|
| 400 |
+
|
| 401 |
+
if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
|
| 402 |
+
newHtml = pageHtml;
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
const newPageRegex = new RegExp(`${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
|
| 408 |
+
let newPageMatch;
|
| 409 |
+
|
| 410 |
+
while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
|
| 411 |
+
const [, pagePath, pageContent] = newPageMatch;
|
| 412 |
+
|
| 413 |
+
let pageHtml = pageContent;
|
| 414 |
+
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 415 |
+
if (htmlMatch) {
|
| 416 |
+
pageHtml = htmlMatch[1];
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
| 420 |
+
|
| 421 |
+
if (existingPageIndex !== -1) {
|
| 422 |
+
updatedPages[existingPageIndex] = {
|
| 423 |
+
path: pagePath,
|
| 424 |
+
html: pageHtml.trim()
|
| 425 |
+
};
|
| 426 |
+
} else {
|
| 427 |
+
updatedPages.push({
|
| 428 |
+
path: pagePath,
|
| 429 |
+
html: pageHtml.trim()
|
| 430 |
+
});
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
|
| 435 |
+
let position = 0;
|
| 436 |
+
let moreBlocks = true;
|
| 437 |
+
|
| 438 |
+
while (moreBlocks) {
|
| 439 |
+
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
| 440 |
+
if (searchStartIndex === -1) {
|
| 441 |
+
moreBlocks = false;
|
| 442 |
+
continue;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
| 446 |
+
if (dividerIndex === -1) {
|
| 447 |
+
moreBlocks = false;
|
| 448 |
+
continue;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
| 452 |
+
if (replaceEndIndex === -1) {
|
| 453 |
+
moreBlocks = false;
|
| 454 |
+
continue;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
const searchBlock = chunk.substring(
|
| 458 |
+
searchStartIndex + SEARCH_START.length,
|
| 459 |
+
dividerIndex
|
| 460 |
+
);
|
| 461 |
+
const replaceBlock = chunk.substring(
|
| 462 |
+
dividerIndex + DIVIDER.length,
|
| 463 |
+
replaceEndIndex
|
| 464 |
+
);
|
| 465 |
+
|
| 466 |
+
if (searchBlock.trim() === "") {
|
| 467 |
+
newHtml = `${replaceBlock}\n${newHtml}`;
|
| 468 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 469 |
+
} else {
|
| 470 |
+
const blockPosition = newHtml.indexOf(searchBlock);
|
| 471 |
+
if (blockPosition !== -1) {
|
| 472 |
+
const beforeText = newHtml.substring(0, blockPosition);
|
| 473 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 474 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 475 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 476 |
+
|
| 477 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 478 |
+
newHtml = newHtml.replace(searchBlock, replaceBlock);
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
position = replaceEndIndex + REPLACE_END.length;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
// Update the main HTML if it's the index page
|
| 486 |
+
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 487 |
+
if (mainPageIndex !== -1) {
|
| 488 |
+
updatedPages[mainPageIndex].html = newHtml;
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
const files: File[] = [];
|
| 493 |
+
updatedPages.forEach((page: Page) => {
|
| 494 |
+
const file = new File([page.html], page.path, { type: "text/html" });
|
| 495 |
+
files.push(file);
|
| 496 |
+
});
|
| 497 |
+
|
| 498 |
+
uploadFiles({
|
| 499 |
+
repo: {
|
| 500 |
+
type: "space",
|
| 501 |
+
name: repoId,
|
| 502 |
+
},
|
| 503 |
+
files,
|
| 504 |
+
commitTitle: prompt,
|
| 505 |
+
accessToken: user.token as string,
|
| 506 |
+
});
|
| 507 |
+
|
| 508 |
+
return NextResponse.json({
|
| 509 |
+
ok: true,
|
| 510 |
+
updatedLines,
|
| 511 |
+
pages: updatedPages,
|
| 512 |
+
});
|
| 513 |
+
} else {
|
| 514 |
+
return NextResponse.json(
|
| 515 |
+
{ ok: false, message: "No content returned from the model" },
|
| 516 |
+
{ status: 400 }
|
| 517 |
+
);
|
| 518 |
+
}
|
| 519 |
+
} catch (error: any) {
|
| 520 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 521 |
+
return NextResponse.json(
|
| 522 |
+
{
|
| 523 |
+
ok: false,
|
| 524 |
+
openProModal: true,
|
| 525 |
+
message: error.message,
|
| 526 |
+
},
|
| 527 |
+
{ status: 402 }
|
| 528 |
+
);
|
| 529 |
+
}
|
| 530 |
+
return NextResponse.json(
|
| 531 |
+
{
|
| 532 |
+
ok: false,
|
| 533 |
+
openSelectProvider: true,
|
| 534 |
+
message:
|
| 535 |
+
error.message || "An error occurred while processing your request.",
|
| 536 |
+
},
|
| 537 |
+
{ status: 500 }
|
| 538 |
+
);
|
| 539 |
+
}
|
| 540 |
+
}
|
| 541 |
+
|
app/api/auth/route.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
export async function POST(req: NextRequest) {
|
| 4 |
+
const body = await req.json();
|
| 5 |
+
const { code } = body;
|
| 6 |
+
|
| 7 |
+
if (!code) {
|
| 8 |
+
return NextResponse.json(
|
| 9 |
+
{ error: "Code is required" },
|
| 10 |
+
{
|
| 11 |
+
status: 400,
|
| 12 |
+
headers: {
|
| 13 |
+
"Content-Type": "application/json",
|
| 14 |
+
},
|
| 15 |
+
}
|
| 16 |
+
);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const Authorization = `Basic ${Buffer.from(
|
| 20 |
+
`${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
|
| 21 |
+
).toString("base64")}`;
|
| 22 |
+
|
| 23 |
+
const host =
|
| 24 |
+
req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
|
| 25 |
+
|
| 26 |
+
const url = host.includes("/spaces/enzostvs")
|
| 27 |
+
? "enzostvs-deepsite.hf.space"
|
| 28 |
+
: host;
|
| 29 |
+
const redirect_uri =
|
| 30 |
+
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 31 |
+
url +
|
| 32 |
+
"/auth/callback";
|
| 33 |
+
const request_auth = await fetch("https://huggingface.co/oauth/token", {
|
| 34 |
+
method: "POST",
|
| 35 |
+
headers: {
|
| 36 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 37 |
+
Authorization,
|
| 38 |
+
},
|
| 39 |
+
body: new URLSearchParams({
|
| 40 |
+
grant_type: "authorization_code",
|
| 41 |
+
code,
|
| 42 |
+
redirect_uri,
|
| 43 |
+
}),
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
const response = await request_auth.json();
|
| 47 |
+
if (!response.access_token) {
|
| 48 |
+
return NextResponse.json(
|
| 49 |
+
{ error: "Failed to retrieve access token" },
|
| 50 |
+
{
|
| 51 |
+
status: 400,
|
| 52 |
+
headers: {
|
| 53 |
+
"Content-Type": "application/json",
|
| 54 |
+
},
|
| 55 |
+
}
|
| 56 |
+
);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
|
| 60 |
+
headers: {
|
| 61 |
+
Authorization: `Bearer ${response.access_token}`,
|
| 62 |
+
},
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
if (!userResponse.ok) {
|
| 66 |
+
return NextResponse.json(
|
| 67 |
+
{ user: null, errCode: userResponse.status },
|
| 68 |
+
{ status: userResponse.status }
|
| 69 |
+
);
|
| 70 |
+
}
|
| 71 |
+
const user = await userResponse.json();
|
| 72 |
+
|
| 73 |
+
return NextResponse.json(
|
| 74 |
+
{
|
| 75 |
+
access_token: response.access_token,
|
| 76 |
+
expires_in: response.expires_in,
|
| 77 |
+
user,
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
status: 200,
|
| 81 |
+
headers: {
|
| 82 |
+
"Content-Type": "application/json",
|
| 83 |
+
},
|
| 84 |
+
}
|
| 85 |
+
);
|
| 86 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, listFiles, 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 }: {
|
| 11 |
+
params: Promise<{
|
| 12 |
+
namespace: string;
|
| 13 |
+
repoId: string;
|
| 14 |
+
commitId: string;
|
| 15 |
+
}>
|
| 16 |
+
}
|
| 17 |
+
) {
|
| 18 |
+
const user = await isAuthenticated();
|
| 19 |
+
|
| 20 |
+
if (user instanceof NextResponse || !user) {
|
| 21 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
await dbConnect();
|
| 25 |
+
const param = await params;
|
| 26 |
+
const { namespace, repoId, commitId } = param;
|
| 27 |
+
|
| 28 |
+
const project = await Project.findOne({
|
| 29 |
+
user_id: user.id,
|
| 30 |
+
space_id: `${namespace}/${repoId}`,
|
| 31 |
+
}).lean();
|
| 32 |
+
|
| 33 |
+
if (!project) {
|
| 34 |
+
return NextResponse.json(
|
| 35 |
+
{ ok: false, error: "Project not found" },
|
| 36 |
+
{ status: 404 }
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
try {
|
| 41 |
+
const repo: RepoDesignation = {
|
| 42 |
+
type: "space",
|
| 43 |
+
name: `${namespace}/${repoId}`,
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
// Fetch files from the specific commit
|
| 47 |
+
const files: File[] = [];
|
| 48 |
+
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
| 49 |
+
|
| 50 |
+
// Get all files from the specific commit
|
| 51 |
+
for await (const fileInfo of listFiles({
|
| 52 |
+
repo,
|
| 53 |
+
accessToken: user.token as string,
|
| 54 |
+
revision: commitId,
|
| 55 |
+
})) {
|
| 56 |
+
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 57 |
+
|
| 58 |
+
if (allowedExtensions.includes(fileExtension || "")) {
|
| 59 |
+
// Fetch the file content from the specific commit
|
| 60 |
+
const response = await fetch(
|
| 61 |
+
`https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
|
| 62 |
+
);
|
| 63 |
+
|
| 64 |
+
if (response.ok) {
|
| 65 |
+
const content = await response.text();
|
| 66 |
+
let mimeType = "text/plain";
|
| 67 |
+
|
| 68 |
+
switch (fileExtension) {
|
| 69 |
+
case "html":
|
| 70 |
+
mimeType = "text/html";
|
| 71 |
+
break;
|
| 72 |
+
case "css":
|
| 73 |
+
mimeType = "text/css";
|
| 74 |
+
break;
|
| 75 |
+
case "js":
|
| 76 |
+
mimeType = "application/javascript";
|
| 77 |
+
break;
|
| 78 |
+
case "json":
|
| 79 |
+
mimeType = "application/json";
|
| 80 |
+
break;
|
| 81 |
+
case "md":
|
| 82 |
+
mimeType = "text/markdown";
|
| 83 |
+
break;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
const file = new File([content], fileInfo.path, { type: mimeType });
|
| 87 |
+
files.push(file);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
if (files.length === 0) {
|
| 93 |
+
return NextResponse.json(
|
| 94 |
+
{ ok: false, error: "No files found in the specified commit" },
|
| 95 |
+
{ status: 404 }
|
| 96 |
+
);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Upload the files to the main branch with a promotion commit message
|
| 100 |
+
await uploadFiles({
|
| 101 |
+
repo,
|
| 102 |
+
files,
|
| 103 |
+
accessToken: user.token as string,
|
| 104 |
+
commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
|
| 105 |
+
commitDescription: `Promoted commit ${commitId} to main branch`,
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
return NextResponse.json(
|
| 109 |
+
{
|
| 110 |
+
ok: true,
|
| 111 |
+
message: "Version promoted successfully",
|
| 112 |
+
promotedCommit: commitId,
|
| 113 |
+
filesPromoted: files.length
|
| 114 |
+
},
|
| 115 |
+
{ status: 200 }
|
| 116 |
+
);
|
| 117 |
+
|
| 118 |
+
} catch (error: any) {
|
| 119 |
+
console.error("Error promoting version:", error);
|
| 120 |
+
|
| 121 |
+
// Handle specific HuggingFace API errors
|
| 122 |
+
if (error.statusCode === 404) {
|
| 123 |
+
return NextResponse.json(
|
| 124 |
+
{ ok: false, error: "Commit not found" },
|
| 125 |
+
{ status: 404 }
|
| 126 |
+
);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
if (error.statusCode === 403) {
|
| 130 |
+
return NextResponse.json(
|
| 131 |
+
{ ok: false, error: "Access denied to repository" },
|
| 132 |
+
{ status: 403 }
|
| 133 |
+
);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
return NextResponse.json(
|
| 137 |
+
{ ok: false, error: error.message || "Failed to promote version" },
|
| 138 |
+
{ status: 500 }
|
| 139 |
+
);
|
| 140 |
+
}
|
| 141 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/images/route.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import Project from "@/models/Project";
|
| 6 |
+
import dbConnect from "@/lib/mongodb";
|
| 7 |
+
|
| 8 |
+
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 |
+
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 |
+
|
| 28 |
+
if (!project) {
|
| 29 |
+
return NextResponse.json(
|
| 30 |
+
{
|
| 31 |
+
ok: false,
|
| 32 |
+
error: "Project not found",
|
| 33 |
+
},
|
| 34 |
+
{ status: 404 }
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Parse the FormData to get the images
|
| 39 |
+
const formData = await req.formData();
|
| 40 |
+
const imageFiles = formData.getAll("images") as File[];
|
| 41 |
+
|
| 42 |
+
if (!imageFiles || imageFiles.length === 0) {
|
| 43 |
+
return NextResponse.json(
|
| 44 |
+
{
|
| 45 |
+
ok: false,
|
| 46 |
+
error: "At least one image file is required under the 'images' key",
|
| 47 |
+
},
|
| 48 |
+
{ status: 400 }
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
const files: File[] = [];
|
| 53 |
+
for (const file of imageFiles) {
|
| 54 |
+
if (!(file instanceof File)) {
|
| 55 |
+
return NextResponse.json(
|
| 56 |
+
{
|
| 57 |
+
ok: false,
|
| 58 |
+
error: "Invalid file format - all items under 'images' key must be files",
|
| 59 |
+
},
|
| 60 |
+
{ status: 400 }
|
| 61 |
+
);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
if (!file.type.startsWith('image/')) {
|
| 65 |
+
return NextResponse.json(
|
| 66 |
+
{
|
| 67 |
+
ok: false,
|
| 68 |
+
error: `File ${file.name} is not an image`,
|
| 69 |
+
},
|
| 70 |
+
{ status: 400 }
|
| 71 |
+
);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Create File object with images/ folder prefix
|
| 75 |
+
const fileName = `images/${file.name}`;
|
| 76 |
+
const processedFile = new File([file], fileName, { type: file.type });
|
| 77 |
+
files.push(processedFile);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Upload files to HuggingFace space
|
| 81 |
+
const repo: RepoDesignation = {
|
| 82 |
+
type: "space",
|
| 83 |
+
name: `${namespace}/${repoId}`,
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
await uploadFiles({
|
| 87 |
+
repo,
|
| 88 |
+
files,
|
| 89 |
+
accessToken: user.token as string,
|
| 90 |
+
commitTitle: `Upload ${files.length} image(s)`,
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
return NextResponse.json({
|
| 94 |
+
ok: true,
|
| 95 |
+
message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
|
| 96 |
+
uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
|
| 97 |
+
}, { status: 200 });
|
| 98 |
+
|
| 99 |
+
} catch (error) {
|
| 100 |
+
console.error('Error uploading images:', error);
|
| 101 |
+
return NextResponse.json(
|
| 102 |
+
{
|
| 103 |
+
ok: false,
|
| 104 |
+
error: "Failed to upload images",
|
| 105 |
+
},
|
| 106 |
+
{ status: 500 }
|
| 107 |
+
);
|
| 108 |
+
}
|
| 109 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/route.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, spaceInfo, uploadFiles, listFiles, deleteRepo, listCommits } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import Project from "@/models/Project";
|
| 6 |
+
import dbConnect from "@/lib/mongodb";
|
| 7 |
+
import { Commit, Page } from "@/types";
|
| 8 |
+
|
| 9 |
+
export async function DELETE(
|
| 10 |
+
req: NextRequest,
|
| 11 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 12 |
+
) {
|
| 13 |
+
const user = await isAuthenticated();
|
| 14 |
+
|
| 15 |
+
if (user instanceof NextResponse || !user) {
|
| 16 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
await dbConnect();
|
| 20 |
+
const param = await params;
|
| 21 |
+
const { namespace, repoId } = param;
|
| 22 |
+
|
| 23 |
+
const project = await Project.findOne({
|
| 24 |
+
user_id: user.id,
|
| 25 |
+
space_id: `${namespace}/${repoId}`,
|
| 26 |
+
}).lean();
|
| 27 |
+
|
| 28 |
+
if (!project) {
|
| 29 |
+
return NextResponse.json(
|
| 30 |
+
{ ok: false, error: "Project not found" },
|
| 31 |
+
{ status: 404 }
|
| 32 |
+
);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
try {
|
| 36 |
+
const space = await spaceInfo({
|
| 37 |
+
name: `${namespace}/${repoId}`,
|
| 38 |
+
accessToken: user.token as string,
|
| 39 |
+
additionalFields: ["author"],
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
if (!space || space.sdk !== "static") {
|
| 43 |
+
return NextResponse.json(
|
| 44 |
+
{ ok: false, error: "Space is not a static space." },
|
| 45 |
+
{ status: 404 }
|
| 46 |
+
);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
if (space.author !== user.name) {
|
| 50 |
+
return NextResponse.json(
|
| 51 |
+
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 52 |
+
{ status: 403 }
|
| 53 |
+
);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (space.private) {
|
| 57 |
+
return NextResponse.json(
|
| 58 |
+
{ ok: false, error: "Your space must be public to access it." },
|
| 59 |
+
{ status: 403 }
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const repo: RepoDesignation = {
|
| 64 |
+
type: "space",
|
| 65 |
+
name: `${namespace}/${repoId}`,
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
await deleteRepo({
|
| 69 |
+
repo,
|
| 70 |
+
accessToken: user.token as string,
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
await Project.deleteOne({
|
| 74 |
+
user_id: user.id,
|
| 75 |
+
space_id: `${namespace}/${repoId}`,
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
return NextResponse.json({ ok: true }, { status: 200 });
|
| 79 |
+
} catch (error: any) {
|
| 80 |
+
return NextResponse.json(
|
| 81 |
+
{ ok: false, error: error.message },
|
| 82 |
+
{ status: 500 }
|
| 83 |
+
);
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
export async function GET(
|
| 88 |
+
req: NextRequest,
|
| 89 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 90 |
+
) {
|
| 91 |
+
const user = await isAuthenticated();
|
| 92 |
+
|
| 93 |
+
if (user instanceof NextResponse || !user) {
|
| 94 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
await dbConnect();
|
| 98 |
+
const param = await params;
|
| 99 |
+
const { namespace, repoId } = param;
|
| 100 |
+
|
| 101 |
+
const project = await Project.findOne({
|
| 102 |
+
user_id: user.id,
|
| 103 |
+
space_id: `${namespace}/${repoId}`,
|
| 104 |
+
}).lean();
|
| 105 |
+
if (!project) {
|
| 106 |
+
return NextResponse.json(
|
| 107 |
+
{
|
| 108 |
+
ok: false,
|
| 109 |
+
error: "Project not found",
|
| 110 |
+
},
|
| 111 |
+
{ status: 404 }
|
| 112 |
+
);
|
| 113 |
+
}
|
| 114 |
+
try {
|
| 115 |
+
const space = await spaceInfo({
|
| 116 |
+
name: namespace + "/" + repoId,
|
| 117 |
+
accessToken: user.token as string,
|
| 118 |
+
additionalFields: ["author"],
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
if (!space || space.sdk !== "static") {
|
| 122 |
+
return NextResponse.json(
|
| 123 |
+
{
|
| 124 |
+
ok: false,
|
| 125 |
+
error: "Space is not a static space",
|
| 126 |
+
},
|
| 127 |
+
{ status: 404 }
|
| 128 |
+
);
|
| 129 |
+
}
|
| 130 |
+
if (space.author !== user.name) {
|
| 131 |
+
return NextResponse.json(
|
| 132 |
+
{
|
| 133 |
+
ok: false,
|
| 134 |
+
error: "Space does not belong to the authenticated user",
|
| 135 |
+
},
|
| 136 |
+
{ status: 403 }
|
| 137 |
+
);
|
| 138 |
+
}
|
| 139 |
+
if (space.private) {
|
| 140 |
+
return NextResponse.json(
|
| 141 |
+
{
|
| 142 |
+
ok: false,
|
| 143 |
+
error: "Space must be public to access it",
|
| 144 |
+
},
|
| 145 |
+
{ status: 403 }
|
| 146 |
+
);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
const repo: RepoDesignation = {
|
| 150 |
+
type: "space",
|
| 151 |
+
name: `${namespace}/${repoId}`,
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
const htmlFiles: Page[] = [];
|
| 155 |
+
const files: string[] = [];
|
| 156 |
+
|
| 157 |
+
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
|
| 158 |
+
|
| 159 |
+
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
| 160 |
+
if (fileInfo.path.endsWith(".html")) {
|
| 161 |
+
const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`);
|
| 162 |
+
if (res.ok) {
|
| 163 |
+
const html = await res.text();
|
| 164 |
+
if (fileInfo.path === "index.html") {
|
| 165 |
+
htmlFiles.unshift({
|
| 166 |
+
path: fileInfo.path,
|
| 167 |
+
html,
|
| 168 |
+
});
|
| 169 |
+
} else {
|
| 170 |
+
htmlFiles.push({
|
| 171 |
+
path: fileInfo.path,
|
| 172 |
+
html,
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
if (fileInfo.type === "directory" && fileInfo.path === "images") {
|
| 178 |
+
for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
| 179 |
+
if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
|
| 180 |
+
files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
const commits: Commit[] = [];
|
| 186 |
+
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
| 187 |
+
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
|
| 188 |
+
continue;
|
| 189 |
+
}
|
| 190 |
+
commits.push({
|
| 191 |
+
title: commit.title,
|
| 192 |
+
oid: commit.oid,
|
| 193 |
+
date: commit.date,
|
| 194 |
+
});
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
if (htmlFiles.length === 0) {
|
| 198 |
+
return NextResponse.json(
|
| 199 |
+
{
|
| 200 |
+
ok: false,
|
| 201 |
+
error: "No HTML files found",
|
| 202 |
+
},
|
| 203 |
+
{ status: 404 }
|
| 204 |
+
);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
return NextResponse.json(
|
| 208 |
+
{
|
| 209 |
+
project,
|
| 210 |
+
pages: htmlFiles,
|
| 211 |
+
files,
|
| 212 |
+
commits,
|
| 213 |
+
ok: true,
|
| 214 |
+
},
|
| 215 |
+
{ status: 200 }
|
| 216 |
+
);
|
| 217 |
+
|
| 218 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 219 |
+
} catch (error: any) {
|
| 220 |
+
if (error.statusCode === 404) {
|
| 221 |
+
await Project.deleteOne({
|
| 222 |
+
user_id: user.id,
|
| 223 |
+
space_id: `${namespace}/${repoId}`,
|
| 224 |
+
});
|
| 225 |
+
return NextResponse.json(
|
| 226 |
+
{ error: "Space not found", ok: false },
|
| 227 |
+
{ status: 404 }
|
| 228 |
+
);
|
| 229 |
+
}
|
| 230 |
+
return NextResponse.json(
|
| 231 |
+
{ error: error.message, ok: false },
|
| 232 |
+
{ status: 500 }
|
| 233 |
+
);
|
| 234 |
+
}
|
| 235 |
+
}
|
app/api/me/projects/route.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, createRepo, listCommits, 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 { Commit, Page } from "@/types";
|
| 8 |
+
import { COLORS } from "@/lib/utils";
|
| 9 |
+
|
| 10 |
+
export async function POST(
|
| 11 |
+
req: NextRequest,
|
| 12 |
+
) {
|
| 13 |
+
const user = await isAuthenticated();
|
| 14 |
+
if (user instanceof NextResponse || !user) {
|
| 15 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
await dbConnect();
|
| 19 |
+
const { title: titleFromRequest, pages, prompt } = await req.json();
|
| 20 |
+
|
| 21 |
+
const title = titleFromRequest ?? "DeepSite Project";
|
| 22 |
+
|
| 23 |
+
const formattedTitle = title
|
| 24 |
+
.toLowerCase()
|
| 25 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 26 |
+
.split("-")
|
| 27 |
+
.filter(Boolean)
|
| 28 |
+
.join("-")
|
| 29 |
+
.slice(0, 96);
|
| 30 |
+
|
| 31 |
+
const repo: RepoDesignation = {
|
| 32 |
+
type: "space",
|
| 33 |
+
name: `${user.name}/${formattedTitle}`,
|
| 34 |
+
};
|
| 35 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 36 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 37 |
+
const README = `---
|
| 38 |
+
title: ${title}
|
| 39 |
+
colorFrom: ${colorFrom}
|
| 40 |
+
colorTo: ${colorTo}
|
| 41 |
+
emoji: 🐳
|
| 42 |
+
sdk: static
|
| 43 |
+
pinned: false
|
| 44 |
+
tags:
|
| 45 |
+
- deepsite-v3
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
# Welcome to your new DeepSite project!
|
| 49 |
+
This project was created with [DeepSite](https://deepsite.hf.co).
|
| 50 |
+
`;
|
| 51 |
+
|
| 52 |
+
const files: File[] = [];
|
| 53 |
+
const readmeFile = new File([README], "README.md", { type: "text/markdown" });
|
| 54 |
+
files.push(readmeFile);
|
| 55 |
+
pages.forEach((page: Page) => {
|
| 56 |
+
const file = new File([page.html], page.path, { type: "text/html" });
|
| 57 |
+
files.push(file);
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
try {
|
| 61 |
+
const { repoUrl } = await createRepo({
|
| 62 |
+
repo,
|
| 63 |
+
accessToken: user.token as string,
|
| 64 |
+
});
|
| 65 |
+
await uploadFiles({
|
| 66 |
+
repo,
|
| 67 |
+
files,
|
| 68 |
+
accessToken: user.token as string,
|
| 69 |
+
commitTitle: prompt ?? "Redesign my website"
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
const path = repoUrl.split("/").slice(-2).join("/");
|
| 73 |
+
const project = await Project.create({
|
| 74 |
+
user_id: user.id,
|
| 75 |
+
space_id: path,
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
const commits: Commit[] = [];
|
| 79 |
+
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
| 80 |
+
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
|
| 81 |
+
continue;
|
| 82 |
+
}
|
| 83 |
+
commits.push({
|
| 84 |
+
title: commit.title,
|
| 85 |
+
oid: commit.oid,
|
| 86 |
+
date: commit.date,
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
let newProject = {
|
| 91 |
+
files,
|
| 92 |
+
pages,
|
| 93 |
+
commits,
|
| 94 |
+
project,
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
|
| 98 |
+
} catch (err: any) {
|
| 99 |
+
return NextResponse.json(
|
| 100 |
+
{ error: err.message, ok: false },
|
| 101 |
+
{ status: 500 }
|
| 102 |
+
);
|
| 103 |
+
}
|
| 104 |
+
}
|
app/api/me/route.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { headers } from "next/headers";
|
| 2 |
+
import { NextResponse } from "next/server";
|
| 3 |
+
|
| 4 |
+
export async function GET() {
|
| 5 |
+
const authHeaders = await headers();
|
| 6 |
+
const token = authHeaders.get("Authorization");
|
| 7 |
+
if (!token) {
|
| 8 |
+
return NextResponse.json({ user: null, errCode: 401 }, { status: 401 });
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
|
| 12 |
+
headers: {
|
| 13 |
+
Authorization: `${token}`,
|
| 14 |
+
},
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
if (!userResponse.ok) {
|
| 18 |
+
return NextResponse.json(
|
| 19 |
+
{ user: null, errCode: userResponse.status },
|
| 20 |
+
{ status: userResponse.status }
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
const user = await userResponse.json();
|
| 24 |
+
return NextResponse.json({ user, errCode: null }, { status: 200 });
|
| 25 |
+
}
|
app/api/proxy/route.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 3 |
+
|
| 4 |
+
export async function GET(req: NextRequest) {
|
| 5 |
+
const user: any = await isAuthenticated();
|
| 6 |
+
|
| 7 |
+
if (user instanceof NextResponse || !user) {
|
| 8 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const { searchParams } = new URL(req.url);
|
| 12 |
+
const spaceId = searchParams.get('spaceId');
|
| 13 |
+
const commitId = searchParams.get('commitId');
|
| 14 |
+
const path = searchParams.get('path') || '/';
|
| 15 |
+
|
| 16 |
+
if (!spaceId) {
|
| 17 |
+
return NextResponse.json({ error: "spaceId parameter required" }, { status: 400 });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
try {
|
| 21 |
+
const spaceDomain = `${spaceId.replace("/", "-")}${commitId !== null? `--rev-${commitId.slice(0, 7)}` : ""}.static.hf.space`;
|
| 22 |
+
const targetUrl = `https://${spaceDomain}${path}`;
|
| 23 |
+
|
| 24 |
+
const response = await fetch(targetUrl, {
|
| 25 |
+
headers: {
|
| 26 |
+
'User-Agent': req.headers.get('user-agent') || '',
|
| 27 |
+
},
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
if (!response.ok) {
|
| 31 |
+
console.error('Failed to fetch from HF space:', response.status, response.statusText);
|
| 32 |
+
return NextResponse.json({
|
| 33 |
+
error: "Failed to fetch content",
|
| 34 |
+
details: `${response.status} ${response.statusText}`,
|
| 35 |
+
targetUrl
|
| 36 |
+
}, { status: response.status });
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
let content = await response.text();
|
| 40 |
+
const contentType = response.headers.get('content-type') || 'text/html';
|
| 41 |
+
|
| 42 |
+
// Rewrite relative URLs to go through the proxy
|
| 43 |
+
if (contentType.includes('text/html')) {
|
| 44 |
+
const baseUrl = `https://${spaceDomain}`;
|
| 45 |
+
|
| 46 |
+
// Fix relative URLs in href attributes
|
| 47 |
+
content = content.replace(/href="([^"]+)"/g, (match, url) => {
|
| 48 |
+
if (url.startsWith('/') && !url.startsWith('//')) {
|
| 49 |
+
// Relative URL starting with /
|
| 50 |
+
return `href="${baseUrl}${url}"`;
|
| 51 |
+
} else if (!url.includes('://') && !url.startsWith('#') && !url.startsWith('mailto:') && !url.startsWith('tel:')) {
|
| 52 |
+
// Relative URL not starting with /
|
| 53 |
+
return `href="${baseUrl}/${url}"`;
|
| 54 |
+
}
|
| 55 |
+
return match;
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
// Fix relative URLs in src attributes
|
| 59 |
+
content = content.replace(/src="([^"]+)"/g, (match, url) => {
|
| 60 |
+
if (url.startsWith('/') && !url.startsWith('//')) {
|
| 61 |
+
return `src="${baseUrl}${url}"`;
|
| 62 |
+
} else if (!url.includes('://')) {
|
| 63 |
+
return `src="${baseUrl}/${url}"`;
|
| 64 |
+
}
|
| 65 |
+
return match;
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// Add base tag to ensure relative URLs work correctly
|
| 69 |
+
const baseTag = `<base href="${baseUrl}/">`;
|
| 70 |
+
if (content.includes('<head>')) {
|
| 71 |
+
content = content.replace('<head>', `<head>${baseTag}`);
|
| 72 |
+
} else if (content.includes('<html>')) {
|
| 73 |
+
content = content.replace('<html>', `<html><head>${baseTag}</head>`);
|
| 74 |
+
} else {
|
| 75 |
+
content = `<head>${baseTag}</head>` + content;
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
const injectedScript = `
|
| 80 |
+
<script>
|
| 81 |
+
// Add event listeners and communicate with parent
|
| 82 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 83 |
+
console.log('DOM loaded, setting up communication');
|
| 84 |
+
let hoveredElement = null;
|
| 85 |
+
let isEditModeEnabled = false;
|
| 86 |
+
|
| 87 |
+
document.addEventListener('mouseover', function(event) {
|
| 88 |
+
if (event.target !== document.body && event.target !== document.documentElement) {
|
| 89 |
+
hoveredElement = event.target;
|
| 90 |
+
console.log('Element hovered:', event.target.tagName, 'Edit mode:', isEditModeEnabled);
|
| 91 |
+
|
| 92 |
+
const rect = event.target.getBoundingClientRect();
|
| 93 |
+
const message = {
|
| 94 |
+
type: 'ELEMENT_HOVERED',
|
| 95 |
+
data: {
|
| 96 |
+
tagName: event.target.tagName,
|
| 97 |
+
rect: {
|
| 98 |
+
top: rect.top,
|
| 99 |
+
left: rect.left,
|
| 100 |
+
width: rect.width,
|
| 101 |
+
height: rect.height
|
| 102 |
+
},
|
| 103 |
+
element: event.target.outerHTML.substring(0, 200)
|
| 104 |
+
}
|
| 105 |
+
};
|
| 106 |
+
console.log('Sending message to parent:', message);
|
| 107 |
+
parent.postMessage(message, '*');
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
document.addEventListener('mouseout', function(event) {
|
| 112 |
+
hoveredElement = null;
|
| 113 |
+
|
| 114 |
+
parent.postMessage({
|
| 115 |
+
type: 'ELEMENT_MOUSE_OUT'
|
| 116 |
+
}, '*');
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
// Handle clicks - prevent default only in edit mode
|
| 120 |
+
document.addEventListener('click', function(event) {
|
| 121 |
+
// Only prevent default if edit mode is enabled
|
| 122 |
+
if (isEditModeEnabled) {
|
| 123 |
+
event.preventDefault();
|
| 124 |
+
event.stopPropagation();
|
| 125 |
+
|
| 126 |
+
const rect = event.target.getBoundingClientRect();
|
| 127 |
+
parent.postMessage({
|
| 128 |
+
type: 'ELEMENT_CLICKED',
|
| 129 |
+
data: {
|
| 130 |
+
tagName: event.target.tagName,
|
| 131 |
+
rect: {
|
| 132 |
+
top: rect.top,
|
| 133 |
+
left: rect.left,
|
| 134 |
+
width: rect.width,
|
| 135 |
+
height: rect.height
|
| 136 |
+
},
|
| 137 |
+
element: event.target.outerHTML.substring(0, 200)
|
| 138 |
+
}
|
| 139 |
+
}, '*');
|
| 140 |
+
} else {
|
| 141 |
+
// In non-edit mode, handle link clicks to maintain proxy context
|
| 142 |
+
const link = event.target.closest('a');
|
| 143 |
+
if (link && link.href) {
|
| 144 |
+
event.preventDefault();
|
| 145 |
+
|
| 146 |
+
const url = new URL(link.href);
|
| 147 |
+
|
| 148 |
+
// If it's an external link (different domain than the space), open in new tab
|
| 149 |
+
if (url.hostname !== '${spaceDomain}') {
|
| 150 |
+
window.open(link.href, '_blank');
|
| 151 |
+
} else {
|
| 152 |
+
// For internal links within the space, navigate through the proxy
|
| 153 |
+
// Extract the path and query parameters from the original link
|
| 154 |
+
const targetPath = url.pathname + url.search + url.hash;
|
| 155 |
+
|
| 156 |
+
// Get current proxy URL parameters
|
| 157 |
+
const currentUrl = new URL(window.location.href);
|
| 158 |
+
const spaceId = currentUrl.searchParams.get('spaceId') || '';
|
| 159 |
+
const commitId = currentUrl.searchParams.get('commitId') || '';
|
| 160 |
+
|
| 161 |
+
// Construct new proxy URL with the target path
|
| 162 |
+
const proxyUrl = '/api/proxy/?' +
|
| 163 |
+
'spaceId=' + encodeURIComponent(spaceId) +
|
| 164 |
+
(commitId ? '&commitId=' + encodeURIComponent(commitId) : '') +
|
| 165 |
+
'&path=' + encodeURIComponent(targetPath);
|
| 166 |
+
|
| 167 |
+
// Navigate to the new URL through the parent window
|
| 168 |
+
parent.postMessage({
|
| 169 |
+
type: 'NAVIGATE_TO_PROXY',
|
| 170 |
+
data: {
|
| 171 |
+
proxyUrl: proxyUrl,
|
| 172 |
+
targetPath: targetPath
|
| 173 |
+
}
|
| 174 |
+
}, '*');
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
});
|
| 179 |
+
|
| 180 |
+
// Prevent form submissions when in edit mode
|
| 181 |
+
document.addEventListener('submit', function(event) {
|
| 182 |
+
if (isEditModeEnabled) {
|
| 183 |
+
event.preventDefault();
|
| 184 |
+
event.stopPropagation();
|
| 185 |
+
}
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
+
// Prevent other navigation events when in edit mode
|
| 189 |
+
document.addEventListener('keydown', function(event) {
|
| 190 |
+
if (isEditModeEnabled && event.key === 'Enter' && (event.target.tagName === 'A' || event.target.tagName === 'BUTTON')) {
|
| 191 |
+
event.preventDefault();
|
| 192 |
+
event.stopPropagation();
|
| 193 |
+
}
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
// Listen for messages from parent
|
| 197 |
+
window.addEventListener('message', function(event) {
|
| 198 |
+
console.log('Iframe received message from parent:', event.data);
|
| 199 |
+
if (event.data.type === 'ENABLE_EDIT_MODE') {
|
| 200 |
+
console.log('Enabling edit mode');
|
| 201 |
+
isEditModeEnabled = true;
|
| 202 |
+
document.body.style.userSelect = 'none';
|
| 203 |
+
document.body.style.pointerEvents = 'auto';
|
| 204 |
+
} else if (event.data.type === 'DISABLE_EDIT_MODE') {
|
| 205 |
+
console.log('Disabling edit mode');
|
| 206 |
+
isEditModeEnabled = false;
|
| 207 |
+
document.body.style.userSelect = '';
|
| 208 |
+
document.body.style.pointerEvents = '';
|
| 209 |
+
}
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
// Notify parent that script is ready
|
| 213 |
+
parent.postMessage({
|
| 214 |
+
type: 'PROXY_SCRIPT_READY'
|
| 215 |
+
}, '*');
|
| 216 |
+
});
|
| 217 |
+
</script>
|
| 218 |
+
`;
|
| 219 |
+
|
| 220 |
+
let modifiedContent;
|
| 221 |
+
if (content.includes('</body>')) {
|
| 222 |
+
modifiedContent = content.replace(
|
| 223 |
+
/<\/body>/i,
|
| 224 |
+
`${injectedScript}</body>`
|
| 225 |
+
);
|
| 226 |
+
} else {
|
| 227 |
+
modifiedContent = content + injectedScript;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
return new NextResponse(modifiedContent, {
|
| 231 |
+
headers: {
|
| 232 |
+
'Content-Type': contentType,
|
| 233 |
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
| 234 |
+
'X-Frame-Options': 'SAMEORIGIN',
|
| 235 |
+
},
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
} catch (error) {
|
| 239 |
+
console.error('Proxy error:', error);
|
| 240 |
+
return NextResponse.json({
|
| 241 |
+
error: "Proxy request failed",
|
| 242 |
+
details: error instanceof Error ? error.message : String(error),
|
| 243 |
+
spaceId
|
| 244 |
+
}, { status: 500 });
|
| 245 |
+
}
|
| 246 |
+
}
|
app/api/re-design/route.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
export async function PUT(request: NextRequest) {
|
| 4 |
+
const body = await request.json();
|
| 5 |
+
const { url } = body;
|
| 6 |
+
|
| 7 |
+
if (!url) {
|
| 8 |
+
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
try {
|
| 12 |
+
const response = await fetch(
|
| 13 |
+
`https://r.jina.ai/${encodeURIComponent(url)}`,
|
| 14 |
+
{
|
| 15 |
+
method: "POST",
|
| 16 |
+
}
|
| 17 |
+
);
|
| 18 |
+
if (!response.ok) {
|
| 19 |
+
return NextResponse.json(
|
| 20 |
+
{ error: "Failed to fetch redesign" },
|
| 21 |
+
{ status: 500 }
|
| 22 |
+
);
|
| 23 |
+
}
|
| 24 |
+
const markdown = await response.text();
|
| 25 |
+
return NextResponse.json(
|
| 26 |
+
{
|
| 27 |
+
ok: true,
|
| 28 |
+
markdown,
|
| 29 |
+
},
|
| 30 |
+
{ status: 200 }
|
| 31 |
+
);
|
| 32 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 33 |
+
} catch (error: any) {
|
| 34 |
+
return NextResponse.json(
|
| 35 |
+
{ error: error.message || "An error occurred" },
|
| 36 |
+
{ status: 500 }
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
}
|
app/auth/callback/page.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
import { useUser } from "@/hooks/useUser";
|
| 4 |
+
import { use, useState } from "react";
|
| 5 |
+
import { useMount, useTimeoutFn } from "react-use";
|
| 6 |
+
|
| 7 |
+
import { Button } from "@/components/ui/button";
|
| 8 |
+
import { AnimatedBlobs } from "@/components/animated-blobs";
|
| 9 |
+
export default function AuthCallback({
|
| 10 |
+
searchParams,
|
| 11 |
+
}: {
|
| 12 |
+
searchParams: Promise<{ code: string }>;
|
| 13 |
+
}) {
|
| 14 |
+
const [showButton, setShowButton] = useState(false);
|
| 15 |
+
const { code } = use(searchParams);
|
| 16 |
+
const { loginFromCode } = useUser();
|
| 17 |
+
|
| 18 |
+
useMount(async () => {
|
| 19 |
+
if (code) {
|
| 20 |
+
await loginFromCode(code);
|
| 21 |
+
}
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
useTimeoutFn(() => setShowButton(true), 7000);
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<div className="h-screen flex flex-col justify-center items-center bg-neutral-950 z-1 relative">
|
| 28 |
+
<div className="background__noisy" />
|
| 29 |
+
<div className="relative max-w-4xl py-10 flex items-center justify-center w-full">
|
| 30 |
+
<div className="max-w-lg mx-auto !rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
|
| 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 |
+
<AnimatedBlobs />
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
);
|
| 74 |
+
}
|
app/auth/page.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { redirect } from "next/navigation";
|
| 2 |
+
import { Metadata } from "next";
|
| 3 |
+
|
| 4 |
+
import { getAuth } from "@/app/actions/auth";
|
| 5 |
+
|
| 6 |
+
export const revalidate = 1;
|
| 7 |
+
|
| 8 |
+
export const metadata: Metadata = {
|
| 9 |
+
robots: "noindex, nofollow",
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
export default async function Auth() {
|
| 13 |
+
const loginRedirectUrl = await getAuth();
|
| 14 |
+
if (loginRedirectUrl) {
|
| 15 |
+
redirect(loginRedirectUrl);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
return (
|
| 19 |
+
<div className="p-4">
|
| 20 |
+
<div className="border bg-red-500/10 border-red-500/20 text-red-500 px-5 py-3 rounded-lg">
|
| 21 |
+
<h1 className="text-xl font-bold">Error</h1>
|
| 22 |
+
<p className="text-sm">
|
| 23 |
+
An error occurred while trying to log in. Please try again later.
|
| 24 |
+
</p>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
);
|
| 28 |
+
}
|
app/globals.css
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
@import "tailwindcss";
|
| 2 |
-
|
| 3 |
-
:root {
|
| 4 |
-
--background: #ffffff;
|
| 5 |
-
--foreground: #171717;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
@theme inline {
|
| 9 |
-
--color-background: var(--background);
|
| 10 |
-
--color-foreground: var(--foreground);
|
| 11 |
-
--font-sans: var(--font-geist-sans);
|
| 12 |
-
--font-mono: var(--font-geist-mono);
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
@media (prefers-color-scheme: dark) {
|
| 16 |
-
:root {
|
| 17 |
-
--background: #0a0a0a;
|
| 18 |
-
--foreground: #ededed;
|
| 19 |
-
}
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
body {
|
| 23 |
-
background: var(--background);
|
| 24 |
-
color: var(--foreground);
|
| 25 |
-
font-family: Arial, Helvetica, sans-serif;
|
| 26 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/layout.tsx
CHANGED
|
@@ -1,33 +1,115 @@
|
|
| 1 |
-
|
| 2 |
-
import {
|
| 3 |
-
import "
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
subsets: ["latin"],
|
| 8 |
});
|
| 9 |
|
| 10 |
-
const
|
| 11 |
-
variable: "--font-
|
| 12 |
subsets: ["latin"],
|
|
|
|
| 13 |
});
|
| 14 |
|
| 15 |
export const metadata: Metadata = {
|
| 16 |
-
title: "
|
| 17 |
-
description:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
};
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
children,
|
| 22 |
}: Readonly<{
|
| 23 |
children: React.ReactNode;
|
| 24 |
}>) {
|
|
|
|
| 25 |
return (
|
| 26 |
<html lang="en">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
<body
|
| 28 |
-
className={`${
|
| 29 |
>
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</body>
|
| 32 |
</html>
|
| 33 |
);
|
|
|
|
| 1 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
+
import type { Metadata, Viewport } from "next";
|
| 3 |
+
import { Inter, PT_Sans } from "next/font/google";
|
| 4 |
+
import { cookies } from "next/headers";
|
| 5 |
+
import Script from "next/script";
|
| 6 |
|
| 7 |
+
import "@/assets/globals.css";
|
| 8 |
+
import { Toaster } from "@/components/ui/sonner";
|
| 9 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 10 |
+
import { apiServer } from "@/lib/api";
|
| 11 |
+
import IframeDetector from "@/components/iframe-detector";
|
| 12 |
+
import AppContext from "@/components/contexts/app-context";
|
| 13 |
+
import TanstackContext from "@/components/contexts/tanstack-query-context";
|
| 14 |
+
import { LoginProvider } from "@/components/contexts/login-context";
|
| 15 |
+
import { ProProvider } from "@/components/contexts/pro-context";
|
| 16 |
+
|
| 17 |
+
const inter = Inter({
|
| 18 |
+
variable: "--font-inter-sans",
|
| 19 |
subsets: ["latin"],
|
| 20 |
});
|
| 21 |
|
| 22 |
+
const ptSans = PT_Sans({
|
| 23 |
+
variable: "--font-ptSans-mono",
|
| 24 |
subsets: ["latin"],
|
| 25 |
+
weight: ["400", "700"],
|
| 26 |
});
|
| 27 |
|
| 28 |
export const metadata: Metadata = {
|
| 29 |
+
title: "DeepSite | Build with AI ✨",
|
| 30 |
+
description:
|
| 31 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
| 32 |
+
openGraph: {
|
| 33 |
+
title: "DeepSite | Build with AI ✨",
|
| 34 |
+
description:
|
| 35 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
| 36 |
+
url: "https://deepsite.hf.co",
|
| 37 |
+
siteName: "DeepSite",
|
| 38 |
+
images: [
|
| 39 |
+
{
|
| 40 |
+
url: "https://deepsite.hf.co/banner.png",
|
| 41 |
+
width: 1200,
|
| 42 |
+
height: 630,
|
| 43 |
+
alt: "DeepSite Open Graph Image",
|
| 44 |
+
},
|
| 45 |
+
],
|
| 46 |
+
},
|
| 47 |
+
twitter: {
|
| 48 |
+
card: "summary_large_image",
|
| 49 |
+
title: "DeepSite | Build with AI ✨",
|
| 50 |
+
description:
|
| 51 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
| 52 |
+
images: ["https://deepsite.hf.co/banner.png"],
|
| 53 |
+
},
|
| 54 |
+
appleWebApp: {
|
| 55 |
+
capable: true,
|
| 56 |
+
title: "DeepSite",
|
| 57 |
+
statusBarStyle: "black-translucent",
|
| 58 |
+
},
|
| 59 |
+
icons: {
|
| 60 |
+
icon: "/logo.svg",
|
| 61 |
+
shortcut: "/logo.svg",
|
| 62 |
+
apple: "/logo.svg",
|
| 63 |
+
},
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
export const viewport: Viewport = {
|
| 67 |
+
initialScale: 1,
|
| 68 |
+
maximumScale: 1,
|
| 69 |
+
themeColor: "#000000",
|
| 70 |
};
|
| 71 |
|
| 72 |
+
async function getMe() {
|
| 73 |
+
const cookieStore = await cookies();
|
| 74 |
+
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
| 75 |
+
if (!token) return { user: null, errCode: null };
|
| 76 |
+
try {
|
| 77 |
+
const res = await apiServer.get("/me", {
|
| 78 |
+
headers: {
|
| 79 |
+
Authorization: `Bearer ${token}`,
|
| 80 |
+
},
|
| 81 |
+
});
|
| 82 |
+
return { user: res.data.user, errCode: null };
|
| 83 |
+
} catch (err: any) {
|
| 84 |
+
return { user: null, errCode: err.status };
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
export default async function RootLayout({
|
| 89 |
children,
|
| 90 |
}: Readonly<{
|
| 91 |
children: React.ReactNode;
|
| 92 |
}>) {
|
| 93 |
+
const data = await getMe();
|
| 94 |
return (
|
| 95 |
<html lang="en">
|
| 96 |
+
<Script
|
| 97 |
+
defer
|
| 98 |
+
data-domain="deepsite.hf.co"
|
| 99 |
+
src="https://plausible.io/js/script.js"
|
| 100 |
+
></Script>
|
| 101 |
<body
|
| 102 |
+
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
| 103 |
>
|
| 104 |
+
<IframeDetector />
|
| 105 |
+
<Toaster richColors position="bottom-center" />
|
| 106 |
+
<TanstackContext>
|
| 107 |
+
<AppContext me={data}>
|
| 108 |
+
<LoginProvider>
|
| 109 |
+
<ProProvider>{children}</ProProvider>
|
| 110 |
+
</LoginProvider>
|
| 111 |
+
</AppContext>
|
| 112 |
+
</TanstackContext>
|
| 113 |
</body>
|
| 114 |
</html>
|
| 115 |
);
|
app/page.tsx
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
import Image from "next/image";
|
| 2 |
-
|
| 3 |
-
export default function Home() {
|
| 4 |
-
return (
|
| 5 |
-
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
| 6 |
-
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
| 7 |
-
<Image
|
| 8 |
-
className="dark:invert"
|
| 9 |
-
src="/next.svg"
|
| 10 |
-
alt="Next.js logo"
|
| 11 |
-
width={180}
|
| 12 |
-
height={38}
|
| 13 |
-
priority
|
| 14 |
-
/>
|
| 15 |
-
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
| 16 |
-
<li className="mb-2 tracking-[-.01em]">
|
| 17 |
-
Get started by editing{" "}
|
| 18 |
-
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
| 19 |
-
app/page.tsx
|
| 20 |
-
</code>
|
| 21 |
-
.
|
| 22 |
-
</li>
|
| 23 |
-
<li className="tracking-[-.01em]">
|
| 24 |
-
Save and see your changes instantly.
|
| 25 |
-
</li>
|
| 26 |
-
</ol>
|
| 27 |
-
|
| 28 |
-
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
| 29 |
-
<a
|
| 30 |
-
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
| 31 |
-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 32 |
-
target="_blank"
|
| 33 |
-
rel="noopener noreferrer"
|
| 34 |
-
>
|
| 35 |
-
<Image
|
| 36 |
-
className="dark:invert"
|
| 37 |
-
src="/vercel.svg"
|
| 38 |
-
alt="Vercel logomark"
|
| 39 |
-
width={20}
|
| 40 |
-
height={20}
|
| 41 |
-
/>
|
| 42 |
-
Deploy now
|
| 43 |
-
</a>
|
| 44 |
-
<a
|
| 45 |
-
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
| 46 |
-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 47 |
-
target="_blank"
|
| 48 |
-
rel="noopener noreferrer"
|
| 49 |
-
>
|
| 50 |
-
Read our docs
|
| 51 |
-
</a>
|
| 52 |
-
</div>
|
| 53 |
-
</main>
|
| 54 |
-
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
| 55 |
-
<a
|
| 56 |
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
| 57 |
-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 58 |
-
target="_blank"
|
| 59 |
-
rel="noopener noreferrer"
|
| 60 |
-
>
|
| 61 |
-
<Image
|
| 62 |
-
aria-hidden
|
| 63 |
-
src="/file.svg"
|
| 64 |
-
alt="File icon"
|
| 65 |
-
width={16}
|
| 66 |
-
height={16}
|
| 67 |
-
/>
|
| 68 |
-
Learn
|
| 69 |
-
</a>
|
| 70 |
-
<a
|
| 71 |
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
| 72 |
-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 73 |
-
target="_blank"
|
| 74 |
-
rel="noopener noreferrer"
|
| 75 |
-
>
|
| 76 |
-
<Image
|
| 77 |
-
aria-hidden
|
| 78 |
-
src="/window.svg"
|
| 79 |
-
alt="Window icon"
|
| 80 |
-
width={16}
|
| 81 |
-
height={16}
|
| 82 |
-
/>
|
| 83 |
-
Examples
|
| 84 |
-
</a>
|
| 85 |
-
<a
|
| 86 |
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
| 87 |
-
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 88 |
-
target="_blank"
|
| 89 |
-
rel="noopener noreferrer"
|
| 90 |
-
>
|
| 91 |
-
<Image
|
| 92 |
-
aria-hidden
|
| 93 |
-
src="/globe.svg"
|
| 94 |
-
alt="Globe icon"
|
| 95 |
-
width={16}
|
| 96 |
-
height={16}
|
| 97 |
-
/>
|
| 98 |
-
Go to nextjs.org →
|
| 99 |
-
</a>
|
| 100 |
-
</footer>
|
| 101 |
-
</div>
|
| 102 |
-
);
|
| 103 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/projects/[namespace]/[repoId]/page.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
return <AppEditor namespace={namespace} repoId={repoId} />;
|
| 10 |
+
}
|
app/projects/new/page.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AppEditor } from "@/components/editor";
|
| 2 |
+
|
| 3 |
+
export default function NewProjectPage() {
|
| 4 |
+
return <AppEditor isNew />;
|
| 5 |
+
}
|
assets/globals.css
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
@import "tw-animate-css";
|
| 3 |
+
|
| 4 |
+
@custom-variant dark (&:is(.dark *));
|
| 5 |
+
|
| 6 |
+
@theme inline {
|
| 7 |
+
--color-background: var(--background);
|
| 8 |
+
--color-foreground: var(--foreground);
|
| 9 |
+
--font-sans: var(--font-inter-sans);
|
| 10 |
+
--font-mono: var(--font-ptSans-mono);
|
| 11 |
+
--color-sidebar-ring: var(--sidebar-ring);
|
| 12 |
+
--color-sidebar-border: var(--sidebar-border);
|
| 13 |
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
| 14 |
+
--color-sidebar-accent: var(--sidebar-accent);
|
| 15 |
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
| 16 |
+
--color-sidebar-primary: var(--sidebar-primary);
|
| 17 |
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
| 18 |
+
--color-sidebar: var(--sidebar);
|
| 19 |
+
--color-chart-5: var(--chart-5);
|
| 20 |
+
--color-chart-4: var(--chart-4);
|
| 21 |
+
--color-chart-3: var(--chart-3);
|
| 22 |
+
--color-chart-2: var(--chart-2);
|
| 23 |
+
--color-chart-1: var(--chart-1);
|
| 24 |
+
--color-ring: var(--ring);
|
| 25 |
+
--color-input: var(--input);
|
| 26 |
+
--color-border: var(--border);
|
| 27 |
+
--color-destructive: var(--destructive);
|
| 28 |
+
--color-accent-foreground: var(--accent-foreground);
|
| 29 |
+
--color-accent: var(--accent);
|
| 30 |
+
--color-muted-foreground: var(--muted-foreground);
|
| 31 |
+
--color-muted: var(--muted);
|
| 32 |
+
--color-secondary-foreground: var(--secondary-foreground);
|
| 33 |
+
--color-secondary: var(--secondary);
|
| 34 |
+
--color-primary-foreground: var(--primary-foreground);
|
| 35 |
+
--color-primary: var(--primary);
|
| 36 |
+
--color-popover-foreground: var(--popover-foreground);
|
| 37 |
+
--color-popover: var(--popover);
|
| 38 |
+
--color-card-foreground: var(--card-foreground);
|
| 39 |
+
--color-card: var(--card);
|
| 40 |
+
--radius-sm: calc(var(--radius) - 4px);
|
| 41 |
+
--radius-md: calc(var(--radius) - 2px);
|
| 42 |
+
--radius-lg: var(--radius);
|
| 43 |
+
--radius-xl: calc(var(--radius) + 4px);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
:root {
|
| 47 |
+
--radius: 0.625rem;
|
| 48 |
+
--background: oklch(1 0 0);
|
| 49 |
+
--foreground: oklch(0.145 0 0);
|
| 50 |
+
--card: oklch(1 0 0);
|
| 51 |
+
--card-foreground: oklch(0.145 0 0);
|
| 52 |
+
--popover: oklch(1 0 0);
|
| 53 |
+
--popover-foreground: oklch(0.145 0 0);
|
| 54 |
+
--primary: oklch(0.205 0 0);
|
| 55 |
+
--primary-foreground: oklch(0.985 0 0);
|
| 56 |
+
--secondary: oklch(0.97 0 0);
|
| 57 |
+
--secondary-foreground: oklch(0.205 0 0);
|
| 58 |
+
--muted: oklch(0.97 0 0);
|
| 59 |
+
--muted-foreground: oklch(0.556 0 0);
|
| 60 |
+
--accent: oklch(0.97 0 0);
|
| 61 |
+
--accent-foreground: oklch(0.205 0 0);
|
| 62 |
+
--destructive: oklch(0.577 0.245 27.325);
|
| 63 |
+
--border: oklch(0.922 0 0);
|
| 64 |
+
--input: oklch(0.922 0 0);
|
| 65 |
+
--ring: oklch(0.708 0 0);
|
| 66 |
+
--chart-1: oklch(0.646 0.222 41.116);
|
| 67 |
+
--chart-2: oklch(0.6 0.118 184.704);
|
| 68 |
+
--chart-3: oklch(0.398 0.07 227.392);
|
| 69 |
+
--chart-4: oklch(0.828 0.189 84.429);
|
| 70 |
+
--chart-5: oklch(0.769 0.188 70.08);
|
| 71 |
+
--sidebar: oklch(0.985 0 0);
|
| 72 |
+
--sidebar-foreground: oklch(0.145 0 0);
|
| 73 |
+
--sidebar-primary: oklch(0.205 0 0);
|
| 74 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 75 |
+
--sidebar-accent: oklch(0.97 0 0);
|
| 76 |
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
| 77 |
+
--sidebar-border: oklch(0.922 0 0);
|
| 78 |
+
--sidebar-ring: oklch(0.708 0 0);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.dark {
|
| 82 |
+
--background: oklch(0.145 0 0);
|
| 83 |
+
--foreground: oklch(0.985 0 0);
|
| 84 |
+
--card: oklch(0.205 0 0);
|
| 85 |
+
--card-foreground: oklch(0.985 0 0);
|
| 86 |
+
--popover: oklch(0.205 0 0);
|
| 87 |
+
--popover-foreground: oklch(0.985 0 0);
|
| 88 |
+
--primary: oklch(0.922 0 0);
|
| 89 |
+
--primary-foreground: oklch(0.205 0 0);
|
| 90 |
+
--secondary: oklch(0.269 0 0);
|
| 91 |
+
--secondary-foreground: oklch(0.985 0 0);
|
| 92 |
+
--muted: oklch(0.269 0 0);
|
| 93 |
+
--muted-foreground: oklch(0.708 0 0);
|
| 94 |
+
--accent: oklch(0.269 0 0);
|
| 95 |
+
--accent-foreground: oklch(0.985 0 0);
|
| 96 |
+
--destructive: oklch(0.704 0.191 22.216);
|
| 97 |
+
--border: oklch(1 0 0 / 10%);
|
| 98 |
+
--input: oklch(1 0 0 / 15%);
|
| 99 |
+
--ring: oklch(0.556 0 0);
|
| 100 |
+
--chart-1: oklch(0.488 0.243 264.376);
|
| 101 |
+
--chart-2: oklch(0.696 0.17 162.48);
|
| 102 |
+
--chart-3: oklch(0.769 0.188 70.08);
|
| 103 |
+
--chart-4: oklch(0.627 0.265 303.9);
|
| 104 |
+
--chart-5: oklch(0.645 0.246 16.439);
|
| 105 |
+
--sidebar: oklch(0.205 0 0);
|
| 106 |
+
--sidebar-foreground: oklch(0.985 0 0);
|
| 107 |
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
| 108 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 109 |
+
--sidebar-accent: oklch(0.269 0 0);
|
| 110 |
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
| 111 |
+
--sidebar-border: oklch(1 0 0 / 10%);
|
| 112 |
+
--sidebar-ring: oklch(0.556 0 0);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
body {
|
| 116 |
+
@apply scroll-smooth
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
@layer base {
|
| 120 |
+
* {
|
| 121 |
+
@apply border-border outline-ring/50;
|
| 122 |
+
}
|
| 123 |
+
body {
|
| 124 |
+
@apply bg-background text-foreground;
|
| 125 |
+
}
|
| 126 |
+
html {
|
| 127 |
+
@apply scroll-smooth;
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.background__noisy {
|
| 132 |
+
@apply bg-blend-normal pointer-events-none opacity-90;
|
| 133 |
+
background-size: 25ww auto;
|
| 134 |
+
background-image: url("/background_noisy.webp");
|
| 135 |
+
@apply fixed w-screen h-screen -z-1 top-0 left-0;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.monaco-editor .margin {
|
| 139 |
+
@apply !bg-neutral-900;
|
| 140 |
+
}
|
| 141 |
+
.monaco-editor .monaco-editor-background {
|
| 142 |
+
@apply !bg-neutral-900;
|
| 143 |
+
}
|
| 144 |
+
.monaco-editor .line-numbers {
|
| 145 |
+
@apply !text-neutral-500;
|
| 146 |
+
}
|
| 147 |
+
|
| 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 |
+
}
|
assets/logo.svg
ADDED
|
|
assets/space.svg
ADDED
|
|
components.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
| 3 |
+
"style": "new-york",
|
| 4 |
+
"rsc": true,
|
| 5 |
+
"tsx": true,
|
| 6 |
+
"tailwind": {
|
| 7 |
+
"config": "",
|
| 8 |
+
"css": "assets/globals.css",
|
| 9 |
+
"baseColor": "neutral",
|
| 10 |
+
"cssVariables": true,
|
| 11 |
+
"prefix": ""
|
| 12 |
+
},
|
| 13 |
+
"aliases": {
|
| 14 |
+
"components": "@/components",
|
| 15 |
+
"utils": "@/lib/utils",
|
| 16 |
+
"ui": "@/components/ui",
|
| 17 |
+
"lib": "@/lib",
|
| 18 |
+
"hooks": "@/hooks"
|
| 19 |
+
},
|
| 20 |
+
"iconLibrary": "lucide"
|
| 21 |
+
}
|
components/animated-blobs/index.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 { User } from "@/types";
|
| 9 |
+
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
| 10 |
+
|
| 11 |
+
export default function AppContext({
|
| 12 |
+
children,
|
| 13 |
+
me: initialData,
|
| 14 |
+
}: {
|
| 15 |
+
children: React.ReactNode;
|
| 16 |
+
me?: {
|
| 17 |
+
user: User | null;
|
| 18 |
+
errCode: number | null;
|
| 19 |
+
};
|
| 20 |
+
}) {
|
| 21 |
+
const { loginFromCode, user, logout, loading, errCode } =
|
| 22 |
+
useUser(initialData);
|
| 23 |
+
const pathname = usePathname();
|
| 24 |
+
const router = useRouter();
|
| 25 |
+
|
| 26 |
+
useMount(() => {
|
| 27 |
+
if (!initialData?.user && !user) {
|
| 28 |
+
if ([401, 403].includes(errCode as number)) {
|
| 29 |
+
logout();
|
| 30 |
+
} else if (pathname.includes("/spaces")) {
|
| 31 |
+
if (errCode) {
|
| 32 |
+
toast.error("An error occured while trying to log in");
|
| 33 |
+
}
|
| 34 |
+
// If we did not manage to log in (probs because api is down), we simply redirect to the home page
|
| 35 |
+
router.push("/");
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
const events: any = {};
|
| 41 |
+
|
| 42 |
+
useBroadcastChannel("auth", (message) => {
|
| 43 |
+
if (pathname.includes("/auth/callback")) return;
|
| 44 |
+
|
| 45 |
+
if (!message.code) return;
|
| 46 |
+
if (message.type === "user-oauth" && message?.code && !events.code) {
|
| 47 |
+
loginFromCode(message.code);
|
| 48 |
+
}
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
return children;
|
| 52 |
+
}
|
components/contexts/login-context.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/contexts/tanstack-query-context.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
| 4 |
+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
| 5 |
+
import { useState } from "react";
|
| 6 |
+
|
| 7 |
+
export default function TanstackContext({
|
| 8 |
+
children,
|
| 9 |
+
}: {
|
| 10 |
+
children: React.ReactNode;
|
| 11 |
+
}) {
|
| 12 |
+
// Create QueryClient instance only once using useState with a function
|
| 13 |
+
const [queryClient] = useState(
|
| 14 |
+
() =>
|
| 15 |
+
new QueryClient({
|
| 16 |
+
defaultOptions: {
|
| 17 |
+
queries: {
|
| 18 |
+
staleTime: 60 * 1000, // 1 minute
|
| 19 |
+
refetchOnWindowFocus: false,
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
})
|
| 23 |
+
);
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<QueryClientProvider client={queryClient}>
|
| 27 |
+
{children}
|
| 28 |
+
<ReactQueryDevtools initialIsOpen={false} />
|
| 29 |
+
</QueryClientProvider>
|
| 30 |
+
);
|
| 31 |
+
}
|
components/contexts/user-context.tsx
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { createContext } from "react";
|
| 4 |
+
import { User } from "@/types";
|
| 5 |
+
|
| 6 |
+
export const UserContext = createContext({
|
| 7 |
+
user: undefined as User | undefined,
|
| 8 |
+
});
|
components/editor/ask-ai/index.tsx
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useMemo, useState } from "react";
|
| 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 { useAi } from "@/hooks/useAi";
|
| 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 |
+
|
| 32 |
+
export const AskAi = ({
|
| 33 |
+
project,
|
| 34 |
+
isNew,
|
| 35 |
+
onScrollToBottom,
|
| 36 |
+
}: {
|
| 37 |
+
project?: Project;
|
| 38 |
+
files?: string[];
|
| 39 |
+
isNew?: boolean;
|
| 40 |
+
onScrollToBottom?: () => void;
|
| 41 |
+
}) => {
|
| 42 |
+
const { user } = useUser();
|
| 43 |
+
const { currentPageData, isUploading, pages, isLoadingProject } = useEditor();
|
| 44 |
+
const {
|
| 45 |
+
isAiWorking,
|
| 46 |
+
isThinking,
|
| 47 |
+
selectedFiles,
|
| 48 |
+
setSelectedFiles,
|
| 49 |
+
selectedElement,
|
| 50 |
+
setSelectedElement,
|
| 51 |
+
setIsThinking,
|
| 52 |
+
callAiNewProject,
|
| 53 |
+
callAiFollowUp,
|
| 54 |
+
setModel,
|
| 55 |
+
selectedModel,
|
| 56 |
+
audio: hookAudio,
|
| 57 |
+
cancelRequest,
|
| 58 |
+
} = useAi(onScrollToBottom);
|
| 59 |
+
const { openLoginModal } = useLoginModal();
|
| 60 |
+
const { openProModal } = useProModal();
|
| 61 |
+
const [openProvider, setOpenProvider] = useState(false);
|
| 62 |
+
const [providerError, setProviderError] = useState("");
|
| 63 |
+
|
| 64 |
+
const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
|
| 65 |
+
useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
|
| 66 |
+
isActive: true,
|
| 67 |
+
primaryColor: undefined,
|
| 68 |
+
secondaryColor: undefined,
|
| 69 |
+
theme: undefined,
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
const [isFollowUp, setIsFollowUp] = useState(true);
|
| 73 |
+
const [prompt, setPrompt] = useState("");
|
| 74 |
+
const [think, setThink] = useState("");
|
| 75 |
+
const [openThink, setOpenThink] = useState(false);
|
| 76 |
+
|
| 77 |
+
const isSameHtml = useMemo(() => {
|
| 78 |
+
return isTheSameHtml(currentPageData.html);
|
| 79 |
+
}, [currentPageData.html]);
|
| 80 |
+
|
| 81 |
+
const handleThink = (think: string) => {
|
| 82 |
+
setThink(think);
|
| 83 |
+
setIsThinking(true);
|
| 84 |
+
setOpenThink(true);
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
const callAi = async (redesignMarkdown?: string) => {
|
| 88 |
+
if (!user) return openLoginModal();
|
| 89 |
+
if (isAiWorking) return;
|
| 90 |
+
if (!redesignMarkdown && !prompt.trim()) return;
|
| 91 |
+
|
| 92 |
+
if (isFollowUp && !redesignMarkdown && !isSameHtml) {
|
| 93 |
+
const result = await callAiFollowUp(prompt, enhancedSettings);
|
| 94 |
+
|
| 95 |
+
if (result?.error) {
|
| 96 |
+
handleError(result.error, result.message);
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if (result?.success) {
|
| 101 |
+
setPrompt("");
|
| 102 |
+
}
|
| 103 |
+
} else {
|
| 104 |
+
const result = await callAiNewProject(
|
| 105 |
+
prompt,
|
| 106 |
+
enhancedSettings,
|
| 107 |
+
redesignMarkdown,
|
| 108 |
+
handleThink,
|
| 109 |
+
() => {
|
| 110 |
+
setIsThinking(false);
|
| 111 |
+
}
|
| 112 |
+
);
|
| 113 |
+
|
| 114 |
+
if (result?.error) {
|
| 115 |
+
handleError(result.error, result.message);
|
| 116 |
+
return;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
if (result?.success) {
|
| 120 |
+
setPrompt("");
|
| 121 |
+
if (selectedModel?.isThinker) {
|
| 122 |
+
setModel(MODELS[0].value);
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
};
|
| 127 |
+
|
| 128 |
+
const handleError = (error: string, message?: string) => {
|
| 129 |
+
switch (error) {
|
| 130 |
+
case "login_required":
|
| 131 |
+
openLoginModal();
|
| 132 |
+
break;
|
| 133 |
+
case "provider_required":
|
| 134 |
+
setOpenProvider(true);
|
| 135 |
+
setProviderError(message || "");
|
| 136 |
+
break;
|
| 137 |
+
case "pro_required":
|
| 138 |
+
openProModal([]);
|
| 139 |
+
break;
|
| 140 |
+
case "api_error":
|
| 141 |
+
toast.error(message || "An error occurred");
|
| 142 |
+
break;
|
| 143 |
+
case "network_error":
|
| 144 |
+
toast.error(message || "Network error occurred");
|
| 145 |
+
break;
|
| 146 |
+
default:
|
| 147 |
+
toast.error("An unexpected error occurred");
|
| 148 |
+
}
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
return (
|
| 152 |
+
<div className="p-3 w-full">
|
| 153 |
+
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
|
| 154 |
+
<SelectedFiles
|
| 155 |
+
files={selectedFiles}
|
| 156 |
+
isAiWorking={isAiWorking}
|
| 157 |
+
onDelete={(file) =>
|
| 158 |
+
setSelectedFiles(selectedFiles.filter((f) => f !== file))
|
| 159 |
+
}
|
| 160 |
+
/>
|
| 161 |
+
{selectedElement && (
|
| 162 |
+
<div className="px-4 pt-3">
|
| 163 |
+
<SelectedHtmlElement
|
| 164 |
+
element={selectedElement}
|
| 165 |
+
isAiWorking={isAiWorking}
|
| 166 |
+
onDelete={() => setSelectedElement(null)}
|
| 167 |
+
/>
|
| 168 |
+
</div>
|
| 169 |
+
)}
|
| 170 |
+
<div className="w-full relative flex items-center justify-between">
|
| 171 |
+
{(isAiWorking || isUploading || isThinking) && (
|
| 172 |
+
<div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
|
| 173 |
+
<AiLoading
|
| 174 |
+
text={
|
| 175 |
+
isUploading
|
| 176 |
+
? "Uploading images..."
|
| 177 |
+
: isAiWorking && !isSameHtml
|
| 178 |
+
? "DeepSite is working..."
|
| 179 |
+
: "DeepSite is thinking..."
|
| 180 |
+
}
|
| 181 |
+
/>
|
| 182 |
+
{isAiWorking && (
|
| 183 |
+
<Button
|
| 184 |
+
size="iconXs"
|
| 185 |
+
variant="outline"
|
| 186 |
+
className="!rounded-md mr-0.5"
|
| 187 |
+
onClick={cancelRequest}
|
| 188 |
+
>
|
| 189 |
+
<CircleStop className="size-4" />
|
| 190 |
+
</Button>
|
| 191 |
+
)}
|
| 192 |
+
</div>
|
| 193 |
+
)}
|
| 194 |
+
<textarea
|
| 195 |
+
disabled={
|
| 196 |
+
isAiWorking || isUploading || isThinking || isLoadingProject
|
| 197 |
+
}
|
| 198 |
+
className={classNames(
|
| 199 |
+
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
|
| 200 |
+
{
|
| 201 |
+
"!pt-2.5":
|
| 202 |
+
selectedElement &&
|
| 203 |
+
!(isAiWorking || isUploading || isThinking),
|
| 204 |
+
}
|
| 205 |
+
)}
|
| 206 |
+
placeholder={
|
| 207 |
+
selectedElement
|
| 208 |
+
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
| 209 |
+
: isFollowUp && (!isSameHtml || pages?.length > 1)
|
| 210 |
+
? "Ask DeepSite for edits"
|
| 211 |
+
: "Ask DeepSite anything..."
|
| 212 |
+
}
|
| 213 |
+
value={prompt}
|
| 214 |
+
onChange={(e) => setPrompt(e.target.value)}
|
| 215 |
+
onKeyDown={(e) => {
|
| 216 |
+
if (e.key === "Enter" && !e.shiftKey) {
|
| 217 |
+
callAi();
|
| 218 |
+
}
|
| 219 |
+
}}
|
| 220 |
+
/>
|
| 221 |
+
</div>
|
| 222 |
+
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
| 223 |
+
<div className="flex-1 flex items-center justify-start gap-1.5">
|
| 224 |
+
<PromptBuilder
|
| 225 |
+
enhancedSettings={enhancedSettings!}
|
| 226 |
+
setEnhancedSettings={setEnhancedSettings}
|
| 227 |
+
/>
|
| 228 |
+
<Settings
|
| 229 |
+
open={openProvider}
|
| 230 |
+
error={providerError}
|
| 231 |
+
isFollowUp={!isSameHtml && isFollowUp}
|
| 232 |
+
onClose={setOpenProvider}
|
| 233 |
+
/>
|
| 234 |
+
{!isNew && <Uploader project={project} />}
|
| 235 |
+
{isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
|
| 236 |
+
{!isNew && <Selector />}
|
| 237 |
+
</div>
|
| 238 |
+
<div className="flex items-center justify-end gap-2">
|
| 239 |
+
<Button
|
| 240 |
+
size="iconXs"
|
| 241 |
+
variant="outline"
|
| 242 |
+
className="!rounded-md"
|
| 243 |
+
disabled={
|
| 244 |
+
isAiWorking || isUploading || isThinking || !prompt.trim()
|
| 245 |
+
}
|
| 246 |
+
onClick={() => callAi()}
|
| 247 |
+
>
|
| 248 |
+
<ArrowUp className="size-4" />
|
| 249 |
+
</Button>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
<audio ref={hookAudio} id="audio" className="hidden">
|
| 254 |
+
<source src="/success.mp3" type="audio/mpeg" />
|
| 255 |
+
Your browser does not support the audio element.
|
| 256 |
+
</audio>
|
| 257 |
+
</div>
|
| 258 |
+
);
|
| 259 |
+
};
|
components/editor/ask-ai/loading.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Loading from "@/components/loading";
|
| 2 |
+
|
| 3 |
+
export const AiLoading = ({
|
| 4 |
+
text = "Ai is working...",
|
| 5 |
+
className,
|
| 6 |
+
}: {
|
| 7 |
+
text?: string;
|
| 8 |
+
className?: string;
|
| 9 |
+
}) => {
|
| 10 |
+
return (
|
| 11 |
+
<div className={`flex items-center justify-start gap-2 ${className}`}>
|
| 12 |
+
<Loading overlay={false} className="!size-4 opacity-50" />
|
| 13 |
+
<p className="text-neutral-400 text-sm">
|
| 14 |
+
<span className="inline-flex">
|
| 15 |
+
{text.split("").map((char, index) => (
|
| 16 |
+
<span
|
| 17 |
+
key={index}
|
| 18 |
+
className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
|
| 19 |
+
style={{
|
| 20 |
+
animationDelay: `${index * 0.1}s`,
|
| 21 |
+
animationDuration: "1.3s",
|
| 22 |
+
animationIterationCount: "infinite",
|
| 23 |
+
}}
|
| 24 |
+
>
|
| 25 |
+
{char === " " ? "\u00A0" : char}
|
| 26 |
+
</span>
|
| 27 |
+
))}
|
| 28 |
+
</span>
|
| 29 |
+
</p>
|
| 30 |
+
</div>
|
| 31 |
+
);
|
| 32 |
+
};
|
components/editor/ask-ai/prompt-builder/content-modal.tsx
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 { EnhancedSettings } from ".";
|
| 7 |
+
import { Button } from "@/components/ui/button";
|
| 8 |
+
import { Themes } from "./themes";
|
| 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
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import { Paintbrush } from "lucide-react";
|
| 3 |
+
import { toast } from "sonner";
|
| 4 |
+
|
| 5 |
+
import { Button } from "@/components/ui/button";
|
| 6 |
+
import {
|
| 7 |
+
Popover,
|
| 8 |
+
PopoverContent,
|
| 9 |
+
PopoverTrigger,
|
| 10 |
+
} from "@/components/ui/popover";
|
| 11 |
+
import { Input } from "@/components/ui/input";
|
| 12 |
+
import Loading from "@/components/loading";
|
| 13 |
+
import { api } from "@/lib/api";
|
| 14 |
+
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,
|
| 21 |
+
}: {
|
| 22 |
+
onRedesign: (md: string) => void;
|
| 23 |
+
}) {
|
| 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(
|
| 34 |
+
/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,
|
| 35 |
+
"i"
|
| 36 |
+
);
|
| 37 |
+
return urlPattern.test(url);
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
const handleClick = async () => {
|
| 41 |
+
if (isLoading) return; // Prevent multiple clicks while loading
|
| 42 |
+
if (!url) {
|
| 43 |
+
toast.error("Please enter a URL.");
|
| 44 |
+
return;
|
| 45 |
+
}
|
| 46 |
+
if (!checkIfUrlIsValid(url)) {
|
| 47 |
+
toast.error("Please enter a valid URL.");
|
| 48 |
+
return;
|
| 49 |
+
}
|
| 50 |
+
setIsLoading(true);
|
| 51 |
+
const response = await api.put("/re-design", {
|
| 52 |
+
url: url.trim(),
|
| 53 |
+
});
|
| 54 |
+
if (response?.data?.ok) {
|
| 55 |
+
setOpen(false);
|
| 56 |
+
setUrl("");
|
| 57 |
+
onRedesign(response.data.markdown);
|
| 58 |
+
toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
|
| 59 |
+
} else {
|
| 60 |
+
toast.error(response?.data?.error || "Failed to redesign the site.");
|
| 61 |
+
}
|
| 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="xs"
|
| 84 |
+
variant={open ? "default" : "outline"}
|
| 85 |
+
className="!rounded-md"
|
| 86 |
+
disabled={globalAiLoading || globalEditorLoading}
|
| 87 |
+
>
|
| 88 |
+
<Paintbrush className="size-3.5" />
|
| 89 |
+
Redesign
|
| 90 |
+
</Button>
|
| 91 |
+
</PopoverTrigger>
|
| 92 |
+
<PopoverContent
|
| 93 |
+
align="start"
|
| 94 |
+
className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
|
| 95 |
+
>
|
| 96 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
| 97 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
| 98 |
+
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 99 |
+
🎨
|
| 100 |
+
</div>
|
| 101 |
+
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
| 102 |
+
🥳
|
| 103 |
+
</div>
|
| 104 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 105 |
+
💎
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
<p className="text-xl font-semibold text-neutral-950">
|
| 109 |
+
Redesign your Site!
|
| 110 |
+
</p>
|
| 111 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
| 112 |
+
Try our new Redesign feature to give your site a fresh look.
|
| 113 |
+
</p>
|
| 114 |
+
</header>
|
| 115 |
+
<main className="space-y-4 p-6">
|
| 116 |
+
<div>
|
| 117 |
+
<p className="text-sm text-neutral-700 mb-2">
|
| 118 |
+
Enter your website URL to get started:
|
| 119 |
+
</p>
|
| 120 |
+
<Input
|
| 121 |
+
type="text"
|
| 122 |
+
placeholder="https://example.com"
|
| 123 |
+
value={url}
|
| 124 |
+
onChange={(e) => setUrl(e.target.value)}
|
| 125 |
+
onBlur={(e) => {
|
| 126 |
+
const inputUrl = e.target.value.trim();
|
| 127 |
+
if (!inputUrl) {
|
| 128 |
+
setUrl("");
|
| 129 |
+
return;
|
| 130 |
+
}
|
| 131 |
+
if (!checkIfUrlIsValid(inputUrl)) {
|
| 132 |
+
toast.error("Please enter a valid URL.");
|
| 133 |
+
return;
|
| 134 |
+
}
|
| 135 |
+
setUrl(inputUrl);
|
| 136 |
+
}}
|
| 137 |
+
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
| 138 |
+
/>
|
| 139 |
+
</div>
|
| 140 |
+
<div>
|
| 141 |
+
<p className="text-sm text-neutral-700 mb-2">
|
| 142 |
+
Then, let's redesign it!
|
| 143 |
+
</p>
|
| 144 |
+
<Button
|
| 145 |
+
variant="black"
|
| 146 |
+
onClick={handleClick}
|
| 147 |
+
className="relative w-full"
|
| 148 |
+
>
|
| 149 |
+
{isLoading ? (
|
| 150 |
+
<>
|
| 151 |
+
<Loading
|
| 152 |
+
overlay={false}
|
| 153 |
+
className="ml-2 size-4 animate-spin"
|
| 154 |
+
/>
|
| 155 |
+
Fetching your site...
|
| 156 |
+
</>
|
| 157 |
+
) : (
|
| 158 |
+
<>
|
| 159 |
+
Redesign <Paintbrush className="size-4" />
|
| 160 |
+
</>
|
| 161 |
+
)}
|
| 162 |
+
</Button>
|
| 163 |
+
</div>
|
| 164 |
+
</main>
|
| 165 |
+
</PopoverContent>
|
| 166 |
+
</form>
|
| 167 |
+
</Popover>
|
| 168 |
+
);
|
| 169 |
+
}
|
components/editor/ask-ai/selected-files.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Image from "next/image";
|
| 2 |
+
|
| 3 |
+
import { Button } from "@/components/ui/button";
|
| 4 |
+
import { Minus } from "lucide-react";
|
| 5 |
+
|
| 6 |
+
export const SelectedFiles = ({
|
| 7 |
+
files,
|
| 8 |
+
isAiWorking,
|
| 9 |
+
onDelete,
|
| 10 |
+
}: {
|
| 11 |
+
files: string[];
|
| 12 |
+
isAiWorking: boolean;
|
| 13 |
+
onDelete: (file: string) => void;
|
| 14 |
+
}) => {
|
| 15 |
+
if (files.length === 0) return null;
|
| 16 |
+
return (
|
| 17 |
+
<div className="px-4 pt-3">
|
| 18 |
+
<div className="flex items-center justify-start gap-2">
|
| 19 |
+
{files.map((file) => (
|
| 20 |
+
<div
|
| 21 |
+
key={file}
|
| 22 |
+
className="flex items-center relative justify-start gap-2 p-1 bg-neutral-700 rounded-md"
|
| 23 |
+
>
|
| 24 |
+
<Image
|
| 25 |
+
src={file}
|
| 26 |
+
alt="uploaded image"
|
| 27 |
+
className="size-12 rounded-md object-cover"
|
| 28 |
+
width={40}
|
| 29 |
+
height={40}
|
| 30 |
+
/>
|
| 31 |
+
<Button
|
| 32 |
+
size="iconXsss"
|
| 33 |
+
variant="secondary"
|
| 34 |
+
className={`absolute top-0.5 right-0.5 ${
|
| 35 |
+
isAiWorking ? "opacity-50 !cursor-not-allowed" : ""
|
| 36 |
+
}`}
|
| 37 |
+
disabled={isAiWorking}
|
| 38 |
+
onClick={() => onDelete(file)}
|
| 39 |
+
>
|
| 40 |
+
<Minus className="size-4" />
|
| 41 |
+
</Button>
|
| 42 |
+
</div>
|
| 43 |
+
))}
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
);
|
| 47 |
+
};
|
components/editor/ask-ai/selected-html-element.tsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import classNames from "classnames";
|
| 2 |
+
import { Code, XCircle } from "lucide-react";
|
| 3 |
+
|
| 4 |
+
import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible";
|
| 5 |
+
import { htmlTagToText } from "@/lib/html-tag-to-text";
|
| 6 |
+
|
| 7 |
+
export const SelectedHtmlElement = ({
|
| 8 |
+
element,
|
| 9 |
+
isAiWorking = false,
|
| 10 |
+
onDelete,
|
| 11 |
+
}: {
|
| 12 |
+
element: HTMLElement | null;
|
| 13 |
+
isAiWorking: boolean;
|
| 14 |
+
onDelete?: () => void;
|
| 15 |
+
}) => {
|
| 16 |
+
if (!element) return null;
|
| 17 |
+
|
| 18 |
+
const tagName = element.tagName.toLowerCase();
|
| 19 |
+
return (
|
| 20 |
+
<Collapsible
|
| 21 |
+
className={classNames(
|
| 22 |
+
"border border-neutral-700 rounded-xl p-1.5 pr-3 max-w-max hover:brightness-110 transition-all duration-200 ease-in-out !cursor-pointer",
|
| 23 |
+
{
|
| 24 |
+
"!cursor-pointer": !isAiWorking,
|
| 25 |
+
"opacity-50 !cursor-not-allowed": isAiWorking,
|
| 26 |
+
}
|
| 27 |
+
)}
|
| 28 |
+
disabled={isAiWorking}
|
| 29 |
+
onClick={() => {
|
| 30 |
+
if (!isAiWorking && onDelete) {
|
| 31 |
+
onDelete();
|
| 32 |
+
}
|
| 33 |
+
}}
|
| 34 |
+
>
|
| 35 |
+
<CollapsibleTrigger className="flex items-center justify-start gap-2 cursor-pointer">
|
| 36 |
+
<div className="rounded-lg bg-neutral-700 size-6 flex items-center justify-center">
|
| 37 |
+
<Code className="text-neutral-300 size-3.5" />
|
| 38 |
+
</div>
|
| 39 |
+
<p className="text-sm font-semibold text-neutral-300">
|
| 40 |
+
{element.textContent?.trim().split(/\s+/)[0]} {htmlTagToText(tagName)}
|
| 41 |
+
</p>
|
| 42 |
+
<XCircle className="text-neutral-300 size-4" />
|
| 43 |
+
</CollapsibleTrigger>
|
| 44 |
+
{/* <CollapsibleContent className="border-t border-neutral-700 pt-2 mt-2">
|
| 45 |
+
<div className="text-xs text-neutral-400">
|
| 46 |
+
<p>
|
| 47 |
+
<span className="font-semibold">ID:</span> {element.id || "No ID"}
|
| 48 |
+
</p>
|
| 49 |
+
<p>
|
| 50 |
+
<span className="font-semibold">Classes:</span>{" "}
|
| 51 |
+
{element.className || "No classes"}
|
| 52 |
+
</p>
|
| 53 |
+
</div>
|
| 54 |
+
</CollapsibleContent> */}
|
| 55 |
+
</Collapsible>
|
| 56 |
+
);
|
| 57 |
+
};
|
components/editor/ask-ai/selector.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import classNames from "classnames";
|
| 3 |
+
|
| 4 |
+
import {
|
| 5 |
+
Popover,
|
| 6 |
+
PopoverContent,
|
| 7 |
+
PopoverTrigger,
|
| 8 |
+
} from "@/components/ui/popover";
|
| 9 |
+
import { PROVIDERS, MODELS } from "@/lib/providers";
|
| 10 |
+
import { Button } from "@/components/ui/button";
|
| 11 |
+
import {
|
| 12 |
+
Select,
|
| 13 |
+
SelectContent,
|
| 14 |
+
SelectGroup,
|
| 15 |
+
SelectItem,
|
| 16 |
+
SelectLabel,
|
| 17 |
+
SelectTrigger,
|
| 18 |
+
SelectValue,
|
| 19 |
+
} from "@/components/ui/select";
|
| 20 |
+
import { useMemo, useState, useEffect } from "react";
|
| 21 |
+
import { useUpdateEffect } from "react-use";
|
| 22 |
+
import Image from "next/image";
|
| 23 |
+
import { Brain, 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
|
| 54 |
+
)?.providers;
|
| 55 |
+
if (!availableProviders) return Object.keys(PROVIDERS);
|
| 56 |
+
return Object.keys(PROVIDERS).filter((id) =>
|
| 57 |
+
availableProviders.includes(id)
|
| 58 |
+
);
|
| 59 |
+
}, [model]);
|
| 60 |
+
|
| 61 |
+
useUpdateEffect(() => {
|
| 62 |
+
if (
|
| 63 |
+
provider !== "auto" &&
|
| 64 |
+
!modelAvailableProviders.includes(provider as string)
|
| 65 |
+
) {
|
| 66 |
+
setProvider("auto");
|
| 67 |
+
}
|
| 68 |
+
}, [model, provider]);
|
| 69 |
+
|
| 70 |
+
return (
|
| 71 |
+
<Popover open={open} onOpenChange={onClose}>
|
| 72 |
+
<PopoverTrigger asChild>
|
| 73 |
+
<Button
|
| 74 |
+
variant={open ? "default" : "outline"}
|
| 75 |
+
className="!rounded-md"
|
| 76 |
+
disabled={globalAiLoading}
|
| 77 |
+
size="xs"
|
| 78 |
+
>
|
| 79 |
+
<Brain className="size-3.5" />
|
| 80 |
+
<span className="truncate max-w-[120px]">
|
| 81 |
+
{isMounted
|
| 82 |
+
? selectedModel?.label?.split(" ").join("-").toLowerCase()
|
| 83 |
+
: "..."}
|
| 84 |
+
</span>
|
| 85 |
+
<ChevronDown className="size-3.5" />
|
| 86 |
+
</Button>
|
| 87 |
+
</PopoverTrigger>
|
| 88 |
+
<PopoverContent
|
| 89 |
+
className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
|
| 90 |
+
align="center"
|
| 91 |
+
>
|
| 92 |
+
<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">
|
| 93 |
+
Customize Settings
|
| 94 |
+
</header>
|
| 95 |
+
<main className="px-4 pt-5 pb-6 space-y-5">
|
| 96 |
+
{error !== "" && (
|
| 97 |
+
<p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
|
| 98 |
+
{error}
|
| 99 |
+
</p>
|
| 100 |
+
)}
|
| 101 |
+
<label className="block">
|
| 102 |
+
<p className="text-neutral-300 text-sm mb-2.5">Choose a model</p>
|
| 103 |
+
<Select defaultValue={model} onValueChange={setModel}>
|
| 104 |
+
<SelectTrigger className="w-full">
|
| 105 |
+
<SelectValue placeholder="Select a model" />
|
| 106 |
+
</SelectTrigger>
|
| 107 |
+
<SelectContent>
|
| 108 |
+
<SelectGroup>
|
| 109 |
+
<SelectLabel>Models</SelectLabel>
|
| 110 |
+
{MODELS.map(
|
| 111 |
+
({
|
| 112 |
+
value,
|
| 113 |
+
label,
|
| 114 |
+
isNew = false,
|
| 115 |
+
isThinker = false,
|
| 116 |
+
}: {
|
| 117 |
+
value: string;
|
| 118 |
+
label: string;
|
| 119 |
+
isNew?: boolean;
|
| 120 |
+
isThinker?: boolean;
|
| 121 |
+
}) => (
|
| 122 |
+
<SelectItem
|
| 123 |
+
key={value}
|
| 124 |
+
value={value}
|
| 125 |
+
className=""
|
| 126 |
+
disabled={isThinker && isFollowUp}
|
| 127 |
+
>
|
| 128 |
+
{label}
|
| 129 |
+
{isNew && (
|
| 130 |
+
<span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
|
| 131 |
+
New
|
| 132 |
+
</span>
|
| 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 |
+
<div
|
| 159 |
+
className={classNames(
|
| 160 |
+
"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",
|
| 161 |
+
{
|
| 162 |
+
"!bg-sky-500": provider === "auto",
|
| 163 |
+
}
|
| 164 |
+
)}
|
| 165 |
+
onClick={() => {
|
| 166 |
+
const foundModel = MODELS.find(
|
| 167 |
+
(m: { value: string }) => m.value === model
|
| 168 |
+
);
|
| 169 |
+
if (provider === "auto" && foundModel?.autoProvider) {
|
| 170 |
+
setProvider(foundModel.autoProvider);
|
| 171 |
+
} else {
|
| 172 |
+
setProvider("auto");
|
| 173 |
+
}
|
| 174 |
+
}}
|
| 175 |
+
>
|
| 176 |
+
<div
|
| 177 |
+
className={classNames(
|
| 178 |
+
"w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
|
| 179 |
+
{
|
| 180 |
+
"translate-x-4": provider === "auto",
|
| 181 |
+
}
|
| 182 |
+
)}
|
| 183 |
+
/>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
<label className="block">
|
| 187 |
+
<p className="text-neutral-300 text-sm mb-2">
|
| 188 |
+
Inference Provider
|
| 189 |
+
</p>
|
| 190 |
+
<div className="grid grid-cols-2 gap-1.5">
|
| 191 |
+
{modelAvailableProviders.map((id: string) => (
|
| 192 |
+
<Button
|
| 193 |
+
key={id}
|
| 194 |
+
variant={id === provider ? "default" : "secondary"}
|
| 195 |
+
size="sm"
|
| 196 |
+
onClick={() => {
|
| 197 |
+
setProvider(id);
|
| 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 |
+
</Button>
|
| 212 |
+
))}
|
| 213 |
+
</div>
|
| 214 |
+
</label>
|
| 215 |
+
</div>
|
| 216 |
+
</main>
|
| 217 |
+
</PopoverContent>
|
| 218 |
+
</Popover>
|
| 219 |
+
);
|
| 220 |
+
}
|
components/editor/ask-ai/uploader.tsx
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/header/index.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ArrowRight, HelpCircle, RefreshCcw } from "lucide-react";
|
| 2 |
+
import Image from "next/image";
|
| 3 |
+
import Link from "next/link";
|
| 4 |
+
|
| 5 |
+
import Logo from "@/assets/logo.svg";
|
| 6 |
+
import { Button } from "@/components/ui/button";
|
| 7 |
+
import { useUser } from "@/hooks/useUser";
|
| 8 |
+
import { ProTag } from "@/components/pro-modal";
|
| 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 |
+
|
| 14 |
+
export function Header() {
|
| 15 |
+
const { user, openLoginWindow } = useUser();
|
| 16 |
+
return (
|
| 17 |
+
<header className="border-b bg-neutral-950 dark:border-neutral-800 grid grid-cols-3 lg:flex items-center max-lg:gap-3 justify-between z-20">
|
| 18 |
+
<div className="flex items-center justify-between lg:max-w-[600px] lg:w-full py-2 px-2 lg:px-3 lg:pl-6 gap-3">
|
| 19 |
+
<h1 className="text-neutral-900 dark:text-white text-lg lg:text-xl font-bold flex items-center justify-start">
|
| 20 |
+
<Image
|
| 21 |
+
src={Logo}
|
| 22 |
+
alt="DeepSite Logo"
|
| 23 |
+
className="size-8 invert-100 dark:invert-0"
|
| 24 |
+
/>
|
| 25 |
+
<p className="ml-2 flex items-center justify-start max-lg:hidden">
|
| 26 |
+
DeepSite
|
| 27 |
+
{user?.isPro ? (
|
| 28 |
+
<ProTag className="ml-2 !text-[10px]" />
|
| 29 |
+
) : (
|
| 30 |
+
<span className="font-mono bg-gradient-to-r from-sky-500/20 to-sky-500/10 text-sky-500 rounded-full text-xs ml-2 px-1.5 py-0.5 border border-sky-500/20">
|
| 31 |
+
{" "}
|
| 32 |
+
v3
|
| 33 |
+
</span>
|
| 34 |
+
)}
|
| 35 |
+
</p>
|
| 36 |
+
</h1>
|
| 37 |
+
<div className="flex items-center justify-end gap-2">
|
| 38 |
+
<History />
|
| 39 |
+
<SwitchTab />
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
<div className="lg:hidden flex items-center justify-center whitespace-nowrap">
|
| 43 |
+
<SwitchTab isMobile />
|
| 44 |
+
</div>
|
| 45 |
+
<div className="lg:w-full px-2 lg:px-3 py-2 flex items-center justify-end lg:justify-between lg:border-l lg:border-neutral-800">
|
| 46 |
+
<div className="font-mono text-muted-foreground flex items-center gap-2">
|
| 47 |
+
<SwitchDevice />
|
| 48 |
+
<Button
|
| 49 |
+
size="xs"
|
| 50 |
+
variant="bordered"
|
| 51 |
+
className="max-lg:hidden"
|
| 52 |
+
onClick={() => {
|
| 53 |
+
const iframe = document.getElementById(
|
| 54 |
+
"preview-iframe"
|
| 55 |
+
) as HTMLIFrameElement;
|
| 56 |
+
if (iframe) {
|
| 57 |
+
iframe.src = iframe.src;
|
| 58 |
+
}
|
| 59 |
+
}}
|
| 60 |
+
>
|
| 61 |
+
<RefreshCcw className="size-3 mr-0.5" />
|
| 62 |
+
Refresh Preview
|
| 63 |
+
</Button>
|
| 64 |
+
<Link
|
| 65 |
+
href="https://huggingface.co/enzostvs/deepsite"
|
| 66 |
+
target="_blank"
|
| 67 |
+
className="max-lg:hidden"
|
| 68 |
+
>
|
| 69 |
+
<Button size="xs" variant="bordered">
|
| 70 |
+
<HelpCircle className="size-3 mr-0.5" />
|
| 71 |
+
Help
|
| 72 |
+
</Button>
|
| 73 |
+
</Link>
|
| 74 |
+
</div>
|
| 75 |
+
{user ? (
|
| 76 |
+
<UserMenu className="!pl-1 !pr-3 !py-1 !h-auto" />
|
| 77 |
+
) : (
|
| 78 |
+
<Button size="sm" onClick={openLoginWindow}>
|
| 79 |
+
Access to my Account
|
| 80 |
+
<ArrowRight className="size-4" />
|
| 81 |
+
</Button>
|
| 82 |
+
)}
|
| 83 |
+
</div>
|
| 84 |
+
</header>
|
| 85 |
+
);
|
| 86 |
+
}
|
components/editor/header/switch-tab.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/index.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { History as HistoryIcon } from "lucide-react";
|
| 2 |
+
import { useState } from "react";
|
| 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 } = useEditor();
|
| 16 |
+
const [open, setOpen] = useState(false);
|
| 17 |
+
|
| 18 |
+
if (commits.length === 0) return null;
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<Popover open={open} onOpenChange={setOpen}>
|
| 22 |
+
<PopoverTrigger asChild>
|
| 23 |
+
<Button
|
| 24 |
+
size="xs"
|
| 25 |
+
variant={open ? "default" : "outline"}
|
| 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
|
| 33 |
+
className="!rounded-2xl !p-0 overflow-hidden !bg-neutral-900"
|
| 34 |
+
align="start"
|
| 35 |
+
>
|
| 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 |
+
<ul className="max-h-[250px] overflow-y-auto">
|
| 41 |
+
{commits?.map((item: Commit, index: number) => (
|
| 42 |
+
<li
|
| 43 |
+
key={index}
|
| 44 |
+
className={classNames(
|
| 45 |
+
"px-4 text-gray-200 py-2 border-b border-gray-800 last:border-0 space-y-1",
|
| 46 |
+
{
|
| 47 |
+
"bg-blue-500/10": currentCommit === item.oid,
|
| 48 |
+
}
|
| 49 |
+
)}
|
| 50 |
+
>
|
| 51 |
+
<p className="line-clamp-1 text-sm">{item.title}</p>
|
| 52 |
+
<div className="w-full flex items-center justify-between gap-2">
|
| 53 |
+
<p className="text-gray-500 text-[10px]">
|
| 54 |
+
{new Date(item.date).toLocaleDateString("en-US", {
|
| 55 |
+
month: "2-digit",
|
| 56 |
+
day: "2-digit",
|
| 57 |
+
year: "2-digit",
|
| 58 |
+
}) +
|
| 59 |
+
" " +
|
| 60 |
+
new Date(item.date).toLocaleTimeString("en-US", {
|
| 61 |
+
hour: "2-digit",
|
| 62 |
+
minute: "2-digit",
|
| 63 |
+
second: "2-digit",
|
| 64 |
+
hour12: false,
|
| 65 |
+
})}
|
| 66 |
+
</p>
|
| 67 |
+
{currentCommit !== item.oid ? (
|
| 68 |
+
<Button
|
| 69 |
+
variant="link"
|
| 70 |
+
size="xss"
|
| 71 |
+
className="text-gray-400 hover:text-gray-200"
|
| 72 |
+
onClick={() => {
|
| 73 |
+
setCurrentCommit(item.oid);
|
| 74 |
+
}}
|
| 75 |
+
>
|
| 76 |
+
See version
|
| 77 |
+
</Button>
|
| 78 |
+
) : (
|
| 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 |
+
</div>
|
| 84 |
+
</li>
|
| 85 |
+
))}
|
| 86 |
+
</ul>
|
| 87 |
+
</main>
|
| 88 |
+
</PopoverContent>
|
| 89 |
+
</Popover>
|
| 90 |
+
);
|
| 91 |
+
}
|
components/editor/index.tsx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import { useMemo, useRef, useState } from "react";
|
| 3 |
+
import { useCopyToClipboard } 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 { useAi } from "@/hooks/useAi";
|
| 13 |
+
|
| 14 |
+
import { ListPages } from "./pages";
|
| 15 |
+
import { AskAi } from "./ask-ai";
|
| 16 |
+
import { Preview } from "./preview";
|
| 17 |
+
import Loading from "../loading";
|
| 18 |
+
|
| 19 |
+
export const AppEditor = ({
|
| 20 |
+
namespace,
|
| 21 |
+
repoId,
|
| 22 |
+
isNew = false,
|
| 23 |
+
}: {
|
| 24 |
+
namespace?: string;
|
| 25 |
+
repoId?: string;
|
| 26 |
+
isNew?: boolean;
|
| 27 |
+
}) => {
|
| 28 |
+
const { project, setPages, files, currentPageData, currentTab } = useEditor(
|
| 29 |
+
namespace,
|
| 30 |
+
repoId
|
| 31 |
+
);
|
| 32 |
+
const [, copyToClipboard] = useCopyToClipboard();
|
| 33 |
+
|
| 34 |
+
const monacoRef = useRef<any>(null);
|
| 35 |
+
const editor = useRef<HTMLDivElement>(null);
|
| 36 |
+
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<section className="h-screen w-full bg-neutral-950 flex flex-col">
|
| 40 |
+
<Header />
|
| 41 |
+
<main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full relative">
|
| 42 |
+
<div
|
| 43 |
+
ref={editor}
|
| 44 |
+
className={classNames(
|
| 45 |
+
"bg-neutral-900 relative flex h-full max-h-[calc(100dvh-47px)] w-full flex-col lg:max-w-[600px] transition-all duration-200",
|
| 46 |
+
{
|
| 47 |
+
"max-lg:hidden lg:!w-[0px] overflow-hidden":
|
| 48 |
+
currentTab !== "chat",
|
| 49 |
+
}
|
| 50 |
+
)}
|
| 51 |
+
>
|
| 52 |
+
<ListPages />
|
| 53 |
+
<CopyIcon
|
| 54 |
+
className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
|
| 55 |
+
onClick={() => {
|
| 56 |
+
copyToClipboard(currentPageData.html);
|
| 57 |
+
toast.success("HTML copied to clipboard!");
|
| 58 |
+
}}
|
| 59 |
+
/>
|
| 60 |
+
<Editor
|
| 61 |
+
defaultLanguage="html"
|
| 62 |
+
theme="vs-dark"
|
| 63 |
+
loading={<Loading overlay={false} />}
|
| 64 |
+
className="h-full absolute left-0 top-0 lg:min-w-[600px]"
|
| 65 |
+
options={{
|
| 66 |
+
colorDecorators: true,
|
| 67 |
+
fontLigatures: true,
|
| 68 |
+
theme: "vs-dark",
|
| 69 |
+
minimap: { enabled: false },
|
| 70 |
+
scrollbar: {
|
| 71 |
+
horizontal: "hidden",
|
| 72 |
+
},
|
| 73 |
+
wordWrap: "on",
|
| 74 |
+
}}
|
| 75 |
+
value={currentPageData.html}
|
| 76 |
+
onChange={(value) => {
|
| 77 |
+
const newValue = value ?? "";
|
| 78 |
+
setPages((prev) =>
|
| 79 |
+
prev.map((page) =>
|
| 80 |
+
page.path === currentPageData.path
|
| 81 |
+
? { ...page, html: newValue }
|
| 82 |
+
: page
|
| 83 |
+
)
|
| 84 |
+
);
|
| 85 |
+
}}
|
| 86 |
+
onMount={(editor, monaco) => {
|
| 87 |
+
editorRef.current = editor;
|
| 88 |
+
monacoRef.current = monaco;
|
| 89 |
+
}}
|
| 90 |
+
/>
|
| 91 |
+
<AskAi
|
| 92 |
+
project={project}
|
| 93 |
+
files={files}
|
| 94 |
+
isNew={isNew}
|
| 95 |
+
onScrollToBottom={() => {
|
| 96 |
+
editorRef.current?.revealLine(
|
| 97 |
+
editorRef.current?.getModel()?.getLineCount() ?? 0
|
| 98 |
+
);
|
| 99 |
+
}}
|
| 100 |
+
/>
|
| 101 |
+
</div>
|
| 102 |
+
<Preview isNew={isNew} />
|
| 103 |
+
</main>
|
| 104 |
+
</section>
|
| 105 |
+
);
|
| 106 |
+
};
|
components/editor/pages/index.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 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
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-4 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 border-b": 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 |
+
}
|