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 | 
            +
            }
         | 
