diff --git a/Dockerfile.txt b/Dockerfile.txt new file mode 100644 index 0000000000000000000000000000000000000000..cbe0188aaee92186937765d2c85d76f7b212c537 --- /dev/null +++ b/Dockerfile.txt @@ -0,0 +1,19 @@ +FROM node:20-alpine +USER root + +USER 1000 +WORKDIR /usr/src/app +# Copy package.json and package-lock.json to the container +COPY --chown=1000 package.json package-lock.json ./ + +# Copy the rest of the application files to the container +COPY --chown=1000 . . + +RUN npm install +RUN npm run build + +# Expose the application port (assuming your app runs on port 3000) +EXPOSE 3000 + +# Start the application +CMD ["npm", "start"] \ No newline at end of file diff --git a/Project.ts b/Project.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f4d2943ef1137fa8d81b8055fdb6d0e105e73fd --- /dev/null +++ b/Project.ts @@ -0,0 +1,27 @@ +import mongoose from "mongoose"; + +const ProjectSchema = new mongoose.Schema({ + space_id: { + type: String, + required: true, + }, + user_id: { + type: String, + required: true, + }, + prompts: { + type: [String], + default: [], + }, + _createdAt: { + type: Date, + default: Date.now, + }, + _updatedAt: { + type: Date, + default: Date.now, + }, +}); + +export default mongoose.models.Project || + mongoose.model("Project", ProjectSchema); diff --git a/README (2).md b/README (2).md new file mode 100644 index 0000000000000000000000000000000000000000..5ab2231fc7dc96070548f1d03ab1d0f73a799600 --- /dev/null +++ b/README (2).md @@ -0,0 +1,22 @@ +--- +title: DeepSite v2 +emoji: 🐳 +colorFrom: blue +colorTo: blue +sdk: docker +pinned: true +app_port: 3000 +license: mit +short_description: Generate any application with DeepSeek +models: + - deepseek-ai/DeepSeek-V3-0324 + - deepseek-ai/DeepSeek-R1-0528 +--- + +# DeepSite 🐳 + +DeepSite is a coding platform powered by DeepSeek AI, designed to make coding smarter and more efficient. Tailored for developers, data scientists, and AI engineers, it integrates generative AI into your coding projects to enhance creativity and productivity. + +## How to use it locally + +Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74) diff --git a/api.ts b/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..7afcb38097bcebad958410a3788b50e229e76d0e --- /dev/null +++ b/api.ts @@ -0,0 +1,35 @@ +import axios from "axios"; +import MY_TOKEN_KEY from "./get-cookie-name"; + +export const api = axios.create({ + baseURL: `/api`, + headers: { + cache: "no-store", + }, +}); + +export const apiServer = axios.create({ + baseURL: process.env.NEXT_APP_API_URL as string, + headers: { + cache: "no-store", + }, +}); + +api.interceptors.request.use( + async (config) => { + // get the token from cookies + const cookie_name = MY_TOKEN_KEY(); + const token = document.cookie + .split("; ") + .find((row) => row.startsWith(`${cookie_name}=`)) + ?.split("=")[1]; + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + // Handle the error + return Promise.reject(error); + } +); diff --git a/app-context.tsx.txt b/app-context.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..a97820d9e26fa6197ef583ce88aba66a3bc10082 --- /dev/null +++ b/app-context.tsx.txt @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import { useUser } from "@/hooks/useUser"; +import { usePathname, useRouter } from "next/navigation"; +import { useMount } from "react-use"; +import { UserContext } from "@/components/contexts/user-context"; +import { User } from "@/types"; +import { toast } from "sonner"; +import { useBroadcastChannel } from "@/lib/useBroadcastChannel"; + +export default function AppContext({ + children, + me: initialData, +}: { + children: React.ReactNode; + me?: { + user: User | null; + errCode: number | null; + }; +}) { + const { loginFromCode, user, logout, loading, errCode } = + useUser(initialData); + const pathname = usePathname(); + const router = useRouter(); + + useMount(() => { + if (!initialData?.user && !user) { + if ([401, 403].includes(errCode as number)) { + logout(); + } else if (pathname.includes("/spaces")) { + if (errCode) { + toast.error("An error occured while trying to log in"); + } + // If we did not manage to log in (probs because api is down), we simply redirect to the home page + router.push("/"); + } + } + }); + + const events: any = {}; + + useBroadcastChannel("auth", (message) => { + if (pathname.includes("/auth/callback")) return; + + if (!message.code) return; + if (message.type === "user-oauth" && message?.code && !events.code) { + loginFromCode(message.code); + } + }); + + return ( + + {children} + + ); +} diff --git a/arrow.svg b/arrow.svg new file mode 100644 index 0000000000000000000000000000000000000000..fb915424ce2661999036154e50d12464e673e06a --- /dev/null +++ b/arrow.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/auth (1).ts b/auth (1).ts new file mode 100644 index 0000000000000000000000000000000000000000..a343e65e6726b35b32f022c117d3f3b5187d78e6 --- /dev/null +++ b/auth (1).ts @@ -0,0 +1,18 @@ +"use server"; + +import { headers } from "next/headers"; + +export async function getAuth() { + const authList = await headers(); + const host = authList.get("host") ?? "localhost:3000"; + const url = host.includes("/spaces/enzostvs") + ? "enzostvs-deepsite.hf.space" + : host; + const redirect_uri = + `${host.includes("localhost") ? "http://" : "https://"}` + + url + + "/auth/callback"; + + 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`; + return loginRedirectUrl; +} diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e3516693366004961acaa31b74ac3bc9e3cc555 --- /dev/null +++ b/auth.ts @@ -0,0 +1,72 @@ +import { User } from "@/types"; +import { NextResponse } from "next/server"; +import { cookies, headers } from "next/headers"; +import MY_TOKEN_KEY from "./get-cookie-name"; + +// UserResponse = type User & { token: string }; +type UserResponse = User & { token: string }; + +export const isAuthenticated = async (): // req: NextRequest +Promise | undefined> => { + const authHeaders = await headers(); + const cookieStore = await cookies(); + const token = cookieStore.get(MY_TOKEN_KEY())?.value + ? `Bearer ${cookieStore.get(MY_TOKEN_KEY())?.value}` + : authHeaders.get("Authorization"); + + if (!token) { + return NextResponse.json( + { + ok: false, + message: "Wrong castle fam :(", + }, + { + status: 401, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + const user = await fetch("https://huggingface.co/api/whoami-v2", { + headers: { + Authorization: token, + }, + method: "GET", + }) + .then((res) => res.json()) + .catch(() => { + return NextResponse.json( + { + ok: false, + message: "Invalid token", + }, + { + status: 401, + headers: { + "Content-Type": "application/json", + }, + } + ); + }); + if (!user || !user.id) { + return NextResponse.json( + { + ok: false, + message: "Invalid token", + }, + { + status: 401, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + return { + ...user, + token: token.replace("Bearer ", ""), + }; +}; diff --git a/avatar.tsx.txt b/avatar.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..71e428b4ca6154811e8f569d5fdd971ead095996 --- /dev/null +++ b/avatar.tsx.txt @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/background_noisy.webp b/background_noisy.webp new file mode 100644 index 0000000000000000000000000000000000000000..b3dd47eb3ea1192ec4b78f5c6b922d7f852bb492 Binary files /dev/null and b/background_noisy.webp differ diff --git a/banner.png b/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c4ab35880d4a77b6d17c7dd330c427460e16704c Binary files /dev/null and b/banner.png differ diff --git a/button.tsx.txt b/button.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..9d4fba01222a7008e4aa26d559b79fd7fb736451 --- /dev/null +++ b/button.tsx.txt @@ -0,0 +1,68 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-full text-sm font-sans font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-red-500 text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 [&_svg]:!text-white", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + lightGray: "bg-neutral-200/60 hover:bg-neutral-200", + link: "text-primary underline-offset-4 hover:underline", + ghostDarker: + "text-white shadow-xs focus-visible:ring-black/40 bg-black/40 hover:bg-black/70", + black: "bg-neutral-950 text-neutral-300 hover:brightness-110", + sky: "bg-sky-500 text-white hover:brightness-110", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-full text-[13px] gap-1.5 px-3", + lg: "h-10 rounded-full px-6 has-[>svg]:px-4", + icon: "size-9", + iconXs: "size-7", + iconXss: "size-6", + iconXsss: "size-5", + xs: "h-6 text-xs rounded-full pl-2 pr-2 gap-1", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/checkbox.tsx.txt b/checkbox.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..bd4167ec38b98869c4ad17d22cd6514ea4d85e3c --- /dev/null +++ b/checkbox.tsx.txt @@ -0,0 +1,32 @@ +"use client"; + +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { CheckIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/collapsible.tsx.txt b/collapsible.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae9fad04a3716b5d6f6c957b75841737eb8ed7a8 --- /dev/null +++ b/collapsible.tsx.txt @@ -0,0 +1,33 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +function Collapsible({ + ...props +}: React.ComponentProps) { + return +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/compare-html-diff.ts b/compare-html-diff.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f6d19cfe3ce1bb420b3020ee42cbce718f4721c --- /dev/null +++ b/compare-html-diff.ts @@ -0,0 +1,11 @@ +import { defaultHTML } from "./consts"; + +export const isTheSameHtml = (currentHtml: string): boolean => { + const normalize = (html: string): string => + html + .replace(//g, "") + .replace(/\s+/g, " ") + .trim(); + + return normalize(defaultHTML) === normalize(currentHtml); +}; diff --git a/consts.ts b/consts.ts new file mode 100644 index 0000000000000000000000000000000000000000..48db724c14489f1cc93ba9647e0e098e4016c80f --- /dev/null +++ b/consts.ts @@ -0,0 +1,21 @@ +export const defaultHTML = ` + + + My app + + + + + +
+ 🔥 New version dropped! +

+ I'm ready to work, + Ask me anything. +

+
+ + + + +`; diff --git a/content.tsx.txt b/content.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca54b7e09348f31e4f83ba302fb73d8c261e08ba --- /dev/null +++ b/content.tsx.txt @@ -0,0 +1,111 @@ +import { Rocket } from "lucide-react"; +import Image from "next/image"; + +import Loading from "@/components/loading"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import SpaceIcon from "@/assets/space.svg"; +import { Page } from "@/types"; +import { api } from "@/lib/api"; +import { toast } from "sonner"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export const DeployButtonContent = ({ + pages, + options, + prompts, +}: { + pages: Page[]; + options?: { + title?: string; + description?: string; + }; + prompts: string[]; +}) => { + const router = useRouter(); + const [loading, setLoading] = useState(false); + + const [config, setConfig] = useState({ + title: "", + }); + + const createSpace = async () => { + if (!config.title) { + toast.error("Please enter a title for your space."); + return; + } + setLoading(true); + + try { + const res = await api.post("/me/projects", { + title: config.title, + pages, + prompts, + }); + if (res.data.ok) { + router.push(`/projects/${res.data.path}?deploy=true`); + } else { + toast.error(res?.data?.error || "Failed to create space"); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + toast.error(err.response?.data?.error || err.message); + } finally { + setLoading(false); + } + }; + + return ( + <> +
+
+
+ 🚀 +
+
+ Space Icon +
+
+ 👻 +
+
+

+ Publish as Space! +

+

+ {options?.description ?? + "Save and Publish your project to a Space on the Hub. Spaces are a way to share your project with the world."} +

+
+
+
+

+ Choose a title for your space: +

+ setConfig({ ...config, title: e.target.value })} + className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100" + /> +
+
+

+ Then, let's publish it! +

+ +
+
+ + ); +}; diff --git a/declare.d.ts b/declare.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..b49b3ac74b890e2f76f577861869518dfd832312 --- /dev/null +++ b/declare.d.ts @@ -0,0 +1,4 @@ +declare module "*.mp3" { + const src: string; + export default src; +} diff --git a/dialog.tsx.txt b/dialog.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..d9ccec91d22fab844bd04340c2b07e8677955350 --- /dev/null +++ b/dialog.tsx.txt @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/dropdown-menu.tsx.txt b/dropdown-menu.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..6bbd969389aa30c911b6388de5f22191eca62a32 --- /dev/null +++ b/dropdown-menu.tsx.txt @@ -0,0 +1,257 @@ +"use client"; + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c Binary files /dev/null and b/favicon.ico differ diff --git a/fireworks-ai.svg b/fireworks-ai.svg new file mode 100644 index 0000000000000000000000000000000000000000..934e9a739b720ec176ee55877777cf24191a75bb --- /dev/null +++ b/fireworks-ai.svg @@ -0,0 +1,4 @@ + + + + diff --git a/follow-up-tooltip.tsx.txt b/follow-up-tooltip.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..5ebb4a29de5de5cca175eb6795f1d069be6ba02b --- /dev/null +++ b/follow-up-tooltip.tsx.txt @@ -0,0 +1,36 @@ +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Info } from "lucide-react"; + +export const FollowUpTooltip = () => { + return ( + + + + + +
+

+ ⚡ Faster, Smarter Updates +

+
+
+

+ Using the Diff-Patch system, allow DeepSite to intelligently update + your project without rewritting the entire codebase. +

+

+ This means faster updates, less data usage, and a more efficient + development process. +

+
+
+
+ ); +}; diff --git a/get-cookie-name.ts b/get-cookie-name.ts new file mode 100644 index 0000000000000000000000000000000000000000..4aba7c78947957e2a996b271f1d100b279bc7d9c --- /dev/null +++ b/get-cookie-name.ts @@ -0,0 +1,3 @@ +export default function MY_TOKEN_KEY() { + return "deepsite-auth-token"; +} diff --git a/gitignore.txt b/gitignore.txt new file mode 100644 index 0000000000000000000000000000000000000000..5ef6a520780202a1d6addd833d800ccb1ecac0bb --- /dev/null +++ b/gitignore.txt @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/globals.css b/globals.css new file mode 100644 index 0000000000000000000000000000000000000000..19dd59e7dcc34e453e9850a052ae8f039628e58a --- /dev/null +++ b/globals.css @@ -0,0 +1,146 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-inter-sans); + --font-mono: var(--font-ptSans-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply scroll-smooth; + } +} + +.background__noisy { + @apply bg-blend-normal pointer-events-none opacity-90; + background-size: 25ww auto; + background-image: url("/background_noisy.webp"); + @apply fixed w-screen h-screen -z-1 top-0 left-0; +} + +.monaco-editor .margin { + @apply !bg-neutral-900; +} +.monaco-editor .monaco-editor-background { + @apply !bg-neutral-900; +} +.monaco-editor .line-numbers { + @apply !text-neutral-500; +} + +.matched-line { + @apply bg-sky-500/30; +} diff --git a/grid-pattern.tsx.txt b/grid-pattern.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..0ead903d1de84aba97f328e5686f3c631a33cef6 --- /dev/null +++ b/grid-pattern.tsx.txt @@ -0,0 +1,69 @@ +import { useId } from "react"; +import { cn } from "@/lib/utils"; + +interface GridPatternProps extends React.SVGProps { + width?: number; + height?: number; + x?: number; + y?: number; + squares?: Array<[x: number, y: number]>; + strokeDasharray?: string; + className?: string; + [key: string]: unknown; +} + +export function GridPattern({ + width = 40, + height = 40, + x = -1, + y = -1, + strokeDasharray = "0", + squares, + className, + ...props +}: GridPatternProps) { + const id = useId(); + + return ( + + ); +} diff --git a/groq.svg b/groq.svg new file mode 100644 index 0000000000000000000000000000000000000000..2c1f3edb8af82414f88d31090a316a9d54a23668 --- /dev/null +++ b/groq.svg @@ -0,0 +1,4 @@ + + + + diff --git a/html-tag-to-text.ts b/html-tag-to-text.ts new file mode 100644 index 0000000000000000000000000000000000000000..62296f4cbe06f57eabe299126cf85bcf7694b115 --- /dev/null +++ b/html-tag-to-text.ts @@ -0,0 +1,96 @@ +export const htmlTagToText = (tagName: string): string => { + switch (tagName.toLowerCase()) { + case "h1": + return "Heading 1"; + case "h2": + return "Heading 2"; + case "h3": + return "Heading 3"; + case "h4": + return "Heading 4"; + case "h5": + return "Heading 5"; + case "h6": + return "Heading 6"; + case "p": + return "Text Paragraph"; + case "span": + return "Inline Text"; + case "button": + return "Button"; + case "input": + return "Input Field"; + case "select": + return "Select Dropdown"; + case "textarea": + return "Text Area"; + case "form": + return "Form"; + case "table": + return "Table"; + case "thead": + return "Table Header"; + case "tbody": + return "Table Body"; + case "tr": + return "Table Row"; + case "th": + return "Table Header Cell"; + case "td": + return "Table Data Cell"; + case "nav": + return "Navigation"; + case "header": + return "Header"; + case "footer": + return "Footer"; + case "section": + return "Section"; + case "article": + return "Article"; + case "aside": + return "Aside"; + case "div": + return "Block"; + case "main": + return "Main Content"; + case "details": + return "Details"; + case "summary": + return "Summary"; + case "code": + return "Code Snippet"; + case "pre": + return "Preformatted Text"; + case "kbd": + return "Keyboard Input"; + case "label": + return "Label"; + case "canvas": + return "Canvas"; + case "svg": + return "SVG Graphic"; + case "video": + return "Video Player"; + case "audio": + return "Audio Player"; + case "iframe": + return "Embedded Frame"; + case "link": + return "Link"; + case "a": + return "Link"; + case "img": + return "Image"; + case "ul": + return "Unordered List"; + case "ol": + return "Ordered List"; + case "li": + return "List Item"; + case "blockquote": + return "Blockquote"; + default: + return tagName.charAt(0).toUpperCase() + tagName.slice(1); + } +}; diff --git a/hyperbolic.svg b/hyperbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..7af378deca446054bdfd15074f9ed4b7e3bc4a51 --- /dev/null +++ b/hyperbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/iframe-detector.tsx.txt b/iframe-detector.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..d686b664e443782c288e5b056fa907559b23ec8c --- /dev/null +++ b/iframe-detector.tsx.txt @@ -0,0 +1,75 @@ +"use client"; + +import { useEffect, useState } from "react"; +import IframeWarningModal from "./iframe-warning-modal"; + +export default function IframeDetector() { + const [showWarning, setShowWarning] = useState(false); + + useEffect(() => { + // Helper function to check if a hostname is from allowed domains + const isAllowedDomain = (hostname: string) => { + const host = hostname.toLowerCase(); + return ( + host.endsWith(".huggingface.co") || + host.endsWith(".hf.co") || + host === "huggingface.co" || + host === "hf.co" + ); + }; + + // Check if the current window is in an iframe + const isInIframe = () => { + try { + return window.self !== window.top; + } catch { + // If we can't access window.top due to cross-origin restrictions, + // we're likely in an iframe + return true; + } + }; + + // Additional check: compare window location with parent location + const isEmbedded = () => { + try { + return window.location !== window.parent.location; + } catch { + // Cross-origin iframe + return true; + } + }; + + // Check if we're in an iframe from a non-allowed domain + const shouldShowWarning = () => { + if (!isInIframe() && !isEmbedded()) { + return false; // Not in an iframe + } + + try { + // Try to get the parent's hostname + const parentHostname = window.parent.location.hostname; + return !isAllowedDomain(parentHostname); + } catch { + // Cross-origin iframe - try to get referrer instead + try { + if (document.referrer) { + const referrerUrl = new URL(document.referrer); + return !isAllowedDomain(referrerUrl.hostname); + } + } catch { + // If we can't determine the parent domain, assume it's not allowed + } + return true; + } + }; + + if (shouldShowWarning()) { + // Show warning modal instead of redirecting immediately + setShowWarning(true); + } + }, []); + + return ( + + ); +} diff --git a/iframe-warning-modal.tsx.txt b/iframe-warning-modal.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..acb58b87ee0709e716954cfcd5b8a78c3bcebc67 --- /dev/null +++ b/iframe-warning-modal.tsx.txt @@ -0,0 +1,61 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { ExternalLink, AlertTriangle } from "lucide-react"; + +interface IframeWarningModalProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function IframeWarningModal({ + isOpen, +}: // onOpenChange, +IframeWarningModalProps) { + const handleVisitSite = () => { + window.open("https://deepsite.hf.co", "_blank"); + }; + + return ( + {}}> + + +
+ + Unauthorized Embedding +
+ + You're viewing DeepSite through an unauthorized iframe. For the + best experience and security, please visit the official website + directly. + +
+ +
+

Why visit the official site?

+
    +
  • • Better performance and security
  • +
  • • Full functionality access
  • +
  • • Latest features and updates
  • +
  • • Proper authentication support
  • +
+
+ + + + +
+
+ ); +} diff --git a/index (1).html b/index (1).html new file mode 100644 index 0000000000000000000000000000000000000000..2394f43fa4736034312baacc9e8cc56b18ac88f4 --- /dev/null +++ b/index (1).html @@ -0,0 +1,562 @@ + + + + + Enhanced ID Generator Tool + + + + + + + + + + + + + +
+
+ + Fake ID Data Tools+ +
+ +
+
+ +
+
+ +

Card Generation

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + + + + + + +
+
+
+ +
+

Generated Cards History

+
+ + + + + + + + + + + + + + + + + +
#Card HolderNumberMM/YYCVVBankBINEmailIPStatus
+
No generated cards yet.
+
+
+ +
+

Card Validator (Simulated)

+
+ + + +
+
+
+ +
+

Simulated Analyzers

+
+ +
+
Fraud Score Simulator
+
+ + + +
+
+
+ +
+
IP Address Simulator
+
+ + + +
+
+
+ +
+
Email Analyzer Simulator
+
+ + + +
+
+
+
+
+ +
+

SSN Tools (Fake Data)

+
+ +
+
Fake SSN Generator
+
+ + + +
+
+ +
+
+ +
+
SSN Validator (Basic)
+
+ + + +
+
+
+
+
Generated SSNs are fake, follow known allocation patterns, but are not real. Validation is basic format/rule check only.
+
+
+ + + +
+ © 2025 Fake ID Data Tools+ — Data is for testing and educational purposes only.
+ Design optimized for browser PDF export - use browser’s print-to-PDF for full content. +
+ + + \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..07d088c3ebf9b2e0f0ab00814c92d07399889480 --- /dev/null +++ b/index.ts @@ -0,0 +1,31 @@ +export interface User { + fullname: string; + avatarUrl: string; + name: string; + isLocalUse?: boolean; + isPro: boolean; + id: string; + token?: string; +} + +export interface HtmlHistory { + pages: Page[]; + createdAt: Date; + prompt: string; +} + +export interface Project { + title: string; + html: string; + prompts: string[]; + user_id: string; + space_id: string; + _id?: string; + _updatedAt?: Date; + _createdAt?: Date; +} + +export interface Page { + path: string; + html: string; +} diff --git a/index.tsx (1).txt b/index.tsx (1).txt new file mode 100644 index 0000000000000000000000000000000000000000..95c6975c5f3799b2a1db77ae56520297a9e0760e --- /dev/null +++ b/index.tsx (1).txt @@ -0,0 +1,156 @@ +"use client"; + +import { useRef, useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { useMount, useUnmount } from "react-use"; +import classNames from "classnames"; + +import { Button } from "@/components/ui/button"; +import Logo from "@/assets/logo.svg"; +import { useUser } from "@/hooks/useUser"; +import { UserMenu } from "@/components/user-menu"; + +const navigationLinks = [ + { + name: "Create Website", + href: "/projects/new", + }, + { + name: "Features", + href: "#features", + }, + { + name: "Community", + href: "#community", + }, + { + name: "Deploy", + href: "#deploy", + }, +]; + +export default function Navigation() { + const { openLoginWindow, user } = useUser(); + const [hash, setHash] = useState(""); + + const selectorRef = useRef(null); + const linksRef = useRef( + new Array(navigationLinks.length).fill(null) + ); + const [isScrolled, setIsScrolled] = useState(false); + + useMount(() => { + const handleScroll = () => { + const scrollTop = window.scrollY; + setIsScrolled(scrollTop > 100); + }; + + const initialHash = window.location.hash; + if (initialHash) { + setHash(initialHash); + calculateSelectorPosition(initialHash); + } + + window.addEventListener("scroll", handleScroll); + }); + + useUnmount(() => { + window.removeEventListener("scroll", () => {}); + }); + + const handleClick = (href: string) => { + setHash(href); + calculateSelectorPosition(href); + }; + + const calculateSelectorPosition = (href: string) => { + if (selectorRef.current && linksRef.current) { + const index = navigationLinks.findIndex((l) => l.href === href); + const targetLink = linksRef.current[index]; + if (targetLink) { + const targetRect = targetLink.getBoundingClientRect(); + selectorRef.current.style.left = targetRect.left + "px"; + selectorRef.current.style.width = targetRect.width + "px"; + } + } + }; + + return ( +
+ +
+ ); +} diff --git a/index.tsx (10).txt b/index.tsx (10).txt new file mode 100644 index 0000000000000000000000000000000000000000..4698327e9c85f19779fda5f379045c202bd75c61 --- /dev/null +++ b/index.tsx (10).txt @@ -0,0 +1,231 @@ +"use client"; +import { useUpdateEffect } from "react-use"; +import { useMemo, useState } from "react"; +import classNames from "classnames"; +import { toast } from "sonner"; +import { useThrottleFn } from "react-use"; + +import { cn } from "@/lib/utils"; +import { GridPattern } from "@/components/magic-ui/grid-pattern"; +import { htmlTagToText } from "@/lib/html-tag-to-text"; +import { Page } from "@/types"; + +export const Preview = ({ + html, + isResizing, + isAiWorking, + ref, + device, + currentTab, + iframeRef, + pages, + setCurrentPage, + isEditableModeEnabled, + onClickElement, +}: { + html: string; + isResizing: boolean; + isAiWorking: boolean; + pages: Page[]; + setCurrentPage: React.Dispatch>; + ref: React.RefObject; + iframeRef?: React.RefObject; + device: "desktop" | "mobile"; + currentTab: string; + isEditableModeEnabled?: boolean; + onClickElement?: (element: HTMLElement) => void; +}) => { + const [hoveredElement, setHoveredElement] = useState( + null + ); + + const handleMouseOver = (event: MouseEvent) => { + if (iframeRef?.current) { + const iframeDocument = iframeRef.current.contentDocument; + if (iframeDocument) { + const targetElement = event.target as HTMLElement; + if ( + hoveredElement !== targetElement && + targetElement !== iframeDocument.body + ) { + setHoveredElement(targetElement); + targetElement.classList.add("hovered-element"); + } else { + return setHoveredElement(null); + } + } + } + }; + const handleMouseOut = () => { + setHoveredElement(null); + }; + const handleClick = (event: MouseEvent) => { + if (iframeRef?.current) { + const iframeDocument = iframeRef.current.contentDocument; + if (iframeDocument) { + const targetElement = event.target as HTMLElement; + if (targetElement !== iframeDocument.body) { + onClickElement?.(targetElement); + } + } + } + }; + const handleCustomNavigation = (event: MouseEvent) => { + if (iframeRef?.current) { + const iframeDocument = iframeRef.current.contentDocument; + if (iframeDocument) { + const findClosestAnchor = ( + element: HTMLElement + ): HTMLAnchorElement | null => { + let current = element; + while (current && current !== iframeDocument.body) { + if (current.tagName === "A") { + return current as HTMLAnchorElement; + } + current = current.parentElement as HTMLElement; + } + return null; + }; + + const anchorElement = findClosestAnchor(event.target as HTMLElement); + if (anchorElement) { + let href = anchorElement.getAttribute("href"); + if (href) { + event.stopPropagation(); + event.preventDefault(); + + if (href.includes("#") && !href.includes(".html")) { + const targetElement = iframeDocument.querySelector(href); + if (targetElement) { + targetElement.scrollIntoView({ behavior: "smooth" }); + } + return; + } + + href = href.split(".html")[0] + ".html"; + const isPageExist = pages.some((page) => page.path === href); + if (isPageExist) { + setCurrentPage(href); + } + } + } + } + } + }; + + useUpdateEffect(() => { + const cleanupListeners = () => { + if (iframeRef?.current?.contentDocument) { + const iframeDocument = iframeRef.current.contentDocument; + iframeDocument.removeEventListener("mouseover", handleMouseOver); + iframeDocument.removeEventListener("mouseout", handleMouseOut); + iframeDocument.removeEventListener("click", handleClick); + } + }; + + if (iframeRef?.current) { + const iframeDocument = iframeRef.current.contentDocument; + if (iframeDocument) { + cleanupListeners(); + + if (isEditableModeEnabled) { + iframeDocument.addEventListener("mouseover", handleMouseOver); + iframeDocument.addEventListener("mouseout", handleMouseOut); + iframeDocument.addEventListener("click", handleClick); + } + } + } + + return cleanupListeners; + }, [iframeRef, isEditableModeEnabled]); + + const selectedElement = useMemo(() => { + if (!isEditableModeEnabled) return null; + if (!hoveredElement) return null; + return hoveredElement; + }, [hoveredElement, isEditableModeEnabled]); + + const throttledHtml = useThrottleFn((html) => html, 1000, [html]); + + return ( +
{ + if (isAiWorking) { + e.preventDefault(); + e.stopPropagation(); + toast.warning("Please wait for the AI to finish working."); + } + }} + > + + {!isAiWorking && hoveredElement && selectedElement && ( +
+ + {htmlTagToText(selectedElement.tagName.toLowerCase())} + +
+ )} + + + + +
+
+

+ {project.space_id} +

+

+ Updated{" "} + {formatDistance( + new Date(project._updatedAt || Date.now()), + new Date(), + { + addSuffix: true, + } + )} +

+
+ + + + + + + + + + Project Settings + + + + + +
+
+ ); +} diff --git a/projects.ts b/projects.ts new file mode 100644 index 0000000000000000000000000000000000000000..209b16d9d9960eeafb9e0b02d7b1b3eda638338d --- /dev/null +++ b/projects.ts @@ -0,0 +1,63 @@ +"use server"; + +import { isAuthenticated } from "@/lib/auth"; +import { NextResponse } from "next/server"; +import dbConnect from "@/lib/mongodb"; +import Project from "@/models/Project"; +import { Project as ProjectType } from "@/types"; + +export async function getProjects(): Promise<{ + ok: boolean; + projects: ProjectType[]; +}> { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return { + ok: false, + projects: [], + }; + } + + await dbConnect(); + const projects = await Project.find({ + user_id: user?.id, + }) + .sort({ _createdAt: -1 }) + .limit(100) + .lean(); + if (!projects) { + return { + ok: false, + projects: [], + }; + } + return { + ok: true, + projects: JSON.parse(JSON.stringify(projects)) as ProjectType[], + }; +} + +export async function getProject( + namespace: string, + repoId: string +): Promise { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return null; + } + + await dbConnect(); + const project = await Project.findOne({ + user_id: user.id, + namespace, + repoId, + }).lean(); + + if (!project) { + return null; + } + + return JSON.parse(JSON.stringify(project)) as ProjectType; +} diff --git a/prompts.ts b/prompts.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e8c13accbbf58dd2cfce11187076a33a8ef0866 --- /dev/null +++ b/prompts.ts @@ -0,0 +1,141 @@ +export const SEARCH_START = "<<<<<<< SEARCH"; +export const DIVIDER = "======="; +export const REPLACE_END = ">>>>>>> REPLACE"; +export const MAX_REQUESTS_PER_IP = 2; +export const TITLE_PAGE_START = "<<<<<<< START_TITLE "; +export const TITLE_PAGE_END = " >>>>>>> END_TITLE"; +export const NEW_PAGE_START = "<<<<<<< NEW_PAGE_START "; +export const NEW_PAGE_END = " >>>>>>> NEW_PAGE_END"; +export const UPDATE_PAGE_START = "<<<<<<< UPDATE_PAGE_START "; +export const UPDATE_PAGE_END = " >>>>>>> UPDATE_PAGE_END"; + +// TODO REVIEW LINK. MAYBE GO BACK TO SANDPACK. +// FIX PREVIEW LINK NOT WORKING ONCE THE SITE IS DEPLOYED. + +export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder, http://Static.photos Usage:Format: http://static.photos/[category]/[dimensions]/[seed] where dimensions must be one of: 200x200, 320x240, 640x360, 1024x576, or 1200x630; seed can be any number (1-999+) for consistent images or omit for random; categories include: nature, office, people, technology, minimal, abstract, aerial, blurred, bokeh, gradient, monochrome, vintage, white, black, blue, red, green, yellow, cityscape, workspace, food, travel, textures, industry, indoor, outdoor, studio, finance, medical, season, holiday, event, sport, science, legal, estate, restaurant, retail, wellness, agriculture, construction, craft, cosmetic, automotive, gaming, or education. +Examples: http://static.photos/red/320x240/133 (red-themed with seed 133), http://static.photos/640x360 (random category and image), http://static.photos/nature/1200x630/42 (nature-themed with seed 42).` + +export const INITIAL_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer. +You create website in a way a designer would, using ONLY HTML, CSS and Javascript. +Try to create the best UI possible. Important: Make the website responsive by using TailwindCSS. Use it as much as you can, if you can't use it, use custom css (make sure to import tailwind with in the head). +Also try to elaborate as much as you can, to create something unique, with a great design. +If you want to use ICONS import Feather Icons (Make sure to add and in the head., and in the body. Ex : ). +For scroll animations you can use: AOS.com (Make sure to add and and ). +For interactive animations you can use: Vanta.js (Make sure to add and in the body.). +You can create multiple pages website at once (following the format rules below) or a Single Page Application. If the user doesn't ask for a specific version, you have to determine the best version for the user, depending on the request. (Try to avoid the Single Page Application if the user asks for multiple pages.) +${PROMPT_FOR_IMAGE_GENERATION} +No need to explain what you did. Just return the expected result. AVOID Chinese characters in the code if not asked by the user. +Return the results in a \`\`\`html\`\`\` markdown. Format the results like: +1. Start with ${TITLE_PAGE_START}. +2. Add the name of the page without special character, such as spaces or punctuation, using the .html format only, right after the start tag. +3. Close the start tag with the ${TITLE_PAGE_END}. +4. Start the HTML response with the triple backticks, like \`\`\`html. +5. Insert the following html there. +6. Close with the triple backticks, like \`\`\`. +7. Retry if another pages. +Example Code: +${TITLE_PAGE_START}index.html${TITLE_PAGE_END} +\`\`\`html + + + + + + Index + + + + + + + + + +

Hello World

+ + + + + +\`\`\` +IMPORTANT: The first file should be always named index.html.` + +export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer modifying an existing HTML files. +The user wants to apply changes and probably add new features/pages to the website, based on their request. +You MUST output ONLY the changes required using the following UPDATE_PAGE_START and SEARCH/REPLACE format. Do NOT output the entire file. +If it's a new page, you MUST applied the following NEW_PAGE_START and UPDATE_PAGE_END format. +${PROMPT_FOR_IMAGE_GENERATION} +Do NOT explain the changes or what you did, just return the expected results. +Update Format Rules: +1. Start with ${UPDATE_PAGE_START} +2. Provide the name of the page you are modifying. +3. Close the start tag with the ${UPDATE_PAGE_END}. +4. Start with ${SEARCH_START} +5. Provide the exact lines from the current code that need to be replaced. +6. Use ${DIVIDER} to separate the search block from the replacement. +7. Provide the new lines that should replace the original lines. +8. End with ${REPLACE_END} +9. You can use multiple SEARCH/REPLACE blocks if changes are needed in different parts of the file. +10. To insert code, use an empty SEARCH block (only ${SEARCH_START} and ${DIVIDER} on their lines) if inserting at the very beginning, otherwise provide the line *before* the insertion point in the SEARCH block and include that line plus the new lines in the REPLACE block. +11. To delete code, provide the lines to delete in the SEARCH block and leave the REPLACE block empty (only ${DIVIDER} and ${REPLACE_END} on their lines). +12. IMPORTANT: The SEARCH block must *exactly* match the current code, including indentation and whitespace. +Example Modifying Code: +\`\`\` +Some explanation... +${UPDATE_PAGE_START}index.html${UPDATE_PAGE_END} +${SEARCH_START} +

Old Title

+${DIVIDER} +

New Title

+${REPLACE_END} +${SEARCH_START} + +${DIVIDER} + + +${REPLACE_END} +\`\`\` +Example Deleting Code: +\`\`\` +Removing the paragraph... +${TITLE_PAGE_START}index.html${TITLE_PAGE_END} +${SEARCH_START} +

This paragraph will be deleted.

+${DIVIDER} +${REPLACE_END} +\`\`\` +The user can also ask to add a new page, in this case you should return the new page in the following format: +1. Start with ${NEW_PAGE_START}. +2. Add the name of the page without special character, such as spaces or punctuation, using the .html format only, right after the start tag. +3. Close the start tag with the ${NEW_PAGE_END}. +4. Start the HTML response with the triple backticks, like \`\`\`html. +5. Insert the following html there. +6. Close with the triple backticks, like \`\`\`. +7. Retry if another pages. +Example Code: +${NEW_PAGE_START}index.html${NEW_PAGE_END} +\`\`\`html + + + + + + Index + + + + + + + + + +

Hello World

+ + + + + +\`\`\` +IMPORTANT: While creating a new page, UPDATE ALL THE OTHERS (using the UPDATE_PAGE_START and SEARCH/REPLACE format) pages to add or replace the link to the new page, otherwise the user will not be able to navigate to the new page. (Dont use onclick to navigate, only href) +No need to explain what you did. Just return the expected result.` \ No newline at end of file diff --git a/re-imagine.tsx.txt b/re-imagine.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..7fd5d170e0f417f4f2daf38f9b77629f64cf1e95 --- /dev/null +++ b/re-imagine.tsx.txt @@ -0,0 +1,146 @@ +import { useState } from "react"; +import { Paintbrush } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; +import Loading from "@/components/loading"; +import { api } from "@/lib/api"; + +export function ReImagine({ + onRedesign, +}: { + onRedesign: (md: string) => void; +}) { + const [url, setUrl] = useState(""); + const [open, setOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const checkIfUrlIsValid = (url: string) => { + const urlPattern = new RegExp( + /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/, + "i" + ); + return urlPattern.test(url); + }; + + const handleClick = async () => { + if (isLoading) return; // Prevent multiple clicks while loading + if (!url) { + toast.error("Please enter a URL."); + return; + } + if (!checkIfUrlIsValid(url)) { + toast.error("Please enter a valid URL."); + return; + } + setIsLoading(true); + const response = await api.put("/re-design", { + url: url.trim(), + }); + if (response?.data?.ok) { + setOpen(false); + setUrl(""); + onRedesign(response.data.markdown); + toast.success("DeepSite is redesigning your site! Let him cook... 🔥"); + } else { + toast.error(response?.data?.error || "Failed to redesign the site."); + } + setIsLoading(false); + }; + + return ( + +
+ + + + +
+
+
+ 🎨 +
+
+ 🥳 +
+
+ 💎 +
+
+

+ Redesign your Site! +

+

+ Try our new Redesign feature to give your site a fresh look. +

+
+
+
+

+ Enter your website URL to get started: +

+ setUrl(e.target.value)} + onBlur={(e) => { + const inputUrl = e.target.value.trim(); + if (!inputUrl) { + setUrl(""); + return; + } + if (!checkIfUrlIsValid(inputUrl)) { + toast.error("Please enter a valid URL."); + return; + } + setUrl(inputUrl); + }} + className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100" + /> +
+
+

+ Then, let's redesign it! +

+ +
+
+
+
+
+ ); +} diff --git a/rewrite-prompt.ts b/rewrite-prompt.ts new file mode 100644 index 0000000000000000000000000000000000000000..08bec54038b85c24c3f0d993ec388344d054fd09 --- /dev/null +++ b/rewrite-prompt.ts @@ -0,0 +1,35 @@ +import { InferenceClient } from "@huggingface/inference"; + +const START_REWRITE_PROMPT = ">>>>>>> START PROMPT >>>>>>"; +const END_REWRITE_PROMPT = ">>>>>>> END PROMPT >>>>>>"; + +export const callAiRewritePrompt = async (prompt: string, { token, billTo }: { token: string, billTo?: string | null }) => { + const client = new InferenceClient(token); + const response = await client.chatCompletion( + { + model: "deepseek-ai/DeepSeek-V3.1", + provider: "novita", + messages: [{ + role: "system", + content: `You are a helpful assistant that rewrites prompts to make them better. All the prompts will be about creating a website or app. +Try to make the prompt more detailed and specific to create a good UI/UX Design and good code. +Format the result by following this format: +${START_REWRITE_PROMPT} +new prompt here +${END_REWRITE_PROMPT} +If you don't rewrite the prompt, return the original prompt. +Make sure to return the prompt in the same language as the prompt you are given. Also IMPORTANT: Make sure to keep the original intent of the prompt. Improve it it needed, but don't change the original intent. +` + },{ role: "user", content: prompt }], + }, + billTo ? { billTo } : {} + ); + + const responseContent = response.choices[0]?.message?.content; + if (!responseContent) { + return prompt; + } + const startIndex = responseContent.indexOf(START_REWRITE_PROMPT); + const endIndex = responseContent.indexOf(END_REWRITE_PROMPT); + return responseContent.substring(startIndex + START_REWRITE_PROMPT.length, endIndex); +}; \ No newline at end of file diff --git a/route (1).ts b/route (1).ts new file mode 100644 index 0000000000000000000000000000000000000000..c4164daba5c58bb2fe7f4f7508de7165f32ca443 --- /dev/null +++ b/route (1).ts @@ -0,0 +1,25 @@ +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; + +export async function GET() { + const authHeaders = await headers(); + const token = authHeaders.get("Authorization"); + if (!token) { + return NextResponse.json({ user: null, errCode: 401 }, { status: 401 }); + } + + const userResponse = await fetch("https://huggingface.co/api/whoami-v2", { + headers: { + Authorization: `${token}`, + }, + }); + + if (!userResponse.ok) { + return NextResponse.json( + { user: null, errCode: userResponse.status }, + { status: userResponse.status } + ); + } + const user = await userResponse.json(); + return NextResponse.json({ user, errCode: null }, { status: 200 }); +} diff --git a/route (2).ts b/route (2).ts new file mode 100644 index 0000000000000000000000000000000000000000..cc3a3907101a1c7c79a7e1968b508936fa0668e4 --- /dev/null +++ b/route (2).ts @@ -0,0 +1,127 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import Project from "@/models/Project"; +import dbConnect from "@/lib/mongodb"; +import { COLORS } from "@/lib/utils"; +import { Page } from "@/types"; + +export async function GET() { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + await dbConnect(); + + const projects = await Project.find({ + user_id: user?.id, + }) + .sort({ _createdAt: -1 }) + .limit(100) + .lean(); + if (!projects) { + return NextResponse.json( + { + ok: false, + projects: [], + }, + { status: 404 } + ); + } + return NextResponse.json( + { + ok: true, + projects, + }, + { status: 200 } + ); +} + +export async function POST(request: NextRequest) { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + const { title, pages, prompts } = await request.json(); + + if (!title || !pages || pages.length === 0) { + return NextResponse.json( + { message: "Title and HTML content are required.", ok: false }, + { status: 400 } + ); + } + + await dbConnect(); + + try { + let readme = ""; + + const newTitle = title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .split("-") + .filter(Boolean) + .join("-") + .slice(0, 96); + + const repo: RepoDesignation = { + type: "space", + name: `${user.name}/${newTitle}`, + }; + + const { repoUrl } = await createRepo({ + repo, + accessToken: user.token as string, + }); + const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)]; + const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)]; + readme = `--- +title: ${newTitle} +emoji: 🐳 +colorFrom: ${colorFrom} +colorTo: ${colorTo} +sdk: static +pinned: false +tags: + - deepsite +--- + +Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`; + + const readmeFile = new File([readme], "README.md", { + type: "text/markdown", + }); + const promptsFile = new File([prompts.join("\n")], "prompts.txt", { + type: "text/plain", + }); + const files = [readmeFile, promptsFile]; + pages.forEach((page: Page) => { + const file = new File([page.html], page.path, { type: "text/html" }); + files.push(file); + }); + await uploadFiles({ + repo, + files, + accessToken: user.token as string, + commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`, + }); + const path = repoUrl.split("/").slice(-2).join("/"); + const project = await Project.create({ + user_id: user.id, + space_id: path, + prompts, + }); + return NextResponse.json({ project, path, ok: true }, { status: 201 }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + return NextResponse.json( + { error: err.message, ok: false }, + { status: 500 } + ); + } +} diff --git a/route (3).ts b/route (3).ts new file mode 100644 index 0000000000000000000000000000000000000000..feecdd6215cc28e8bf952448636d781dff372d5a --- /dev/null +++ b/route (3).ts @@ -0,0 +1,276 @@ +import { NextRequest, NextResponse } from "next/server"; +import { RepoDesignation, spaceInfo, uploadFiles, listFiles } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import Project from "@/models/Project"; +import dbConnect from "@/lib/mongodb"; +import { Page } from "@/types"; + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ namespace: string; repoId: string }> } +) { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + await dbConnect(); + const param = await params; + const { namespace, repoId } = param; + + const project = await Project.findOne({ + user_id: user.id, + space_id: `${namespace}/${repoId}`, + }).lean(); + if (!project) { + return NextResponse.json( + { + ok: false, + error: "Project not found", + }, + { status: 404 } + ); + } + try { + const space = await spaceInfo({ + name: namespace + "/" + repoId, + accessToken: user.token as string, + additionalFields: ["author"], + }); + + if (!space || space.sdk !== "static") { + return NextResponse.json( + { + ok: false, + error: "Space is not a static space", + }, + { status: 404 } + ); + } + if (space.author !== user.name) { + return NextResponse.json( + { + ok: false, + error: "Space does not belong to the authenticated user", + }, + { status: 403 } + ); + } + + const repo: RepoDesignation = { + type: "space", + name: `${namespace}/${repoId}`, + }; + + const htmlFiles: Page[] = []; + const images: string[] = []; + + const allowedImagesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"]; + + for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) { + if (fileInfo.path.endsWith(".html")) { + const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`); + if (res.ok) { + const html = await res.text(); + if (fileInfo.path === "index.html") { + htmlFiles.unshift({ + path: fileInfo.path, + html, + }); + } else { + htmlFiles.push({ + path: fileInfo.path, + html, + }); + } + } + } + if (fileInfo.type === "directory" && fileInfo.path === "images") { + for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) { + if (allowedImagesExtensions.includes(imageInfo.path.split(".").pop() || "")) { + images.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`); + } + } + } + } + + if (htmlFiles.length === 0) { + return NextResponse.json( + { + ok: false, + error: "No HTML files found", + }, + { status: 404 } + ); + } + + return NextResponse.json( + { + project: { + ...project, + pages: htmlFiles, + images, + }, + ok: true, + }, + { status: 200 } + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (error.statusCode === 404) { + await Project.deleteOne({ + user_id: user.id, + space_id: `${namespace}/${repoId}`, + }); + return NextResponse.json( + { error: "Space not found", ok: false }, + { status: 404 } + ); + } + return NextResponse.json( + { error: error.message, ok: false }, + { status: 500 } + ); + } +} + +export async function PUT( + req: NextRequest, + { params }: { params: Promise<{ namespace: string; repoId: string }> } +) { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + await dbConnect(); + const param = await params; + const { namespace, repoId } = param; + const { pages, prompts } = await req.json(); + + const project = await Project.findOne({ + user_id: user.id, + space_id: `${namespace}/${repoId}`, + }).lean(); + if (!project) { + return NextResponse.json( + { + ok: false, + error: "Project not found", + }, + { status: 404 } + ); + } + + const repo: RepoDesignation = { + type: "space", + name: `${namespace}/${repoId}`, + }; + + const files: File[] = []; + const promptsFile = new File([prompts.join("\n")], "prompts.txt", { + type: "text/plain", + }); + files.push(promptsFile); + pages.forEach((page: Page) => { + const file = new File([page.html], page.path, { type: "text/html" }); + files.push(file); + }); + await uploadFiles({ + repo, + files, + accessToken: user.token as string, + commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`, + }); + + await Project.updateOne( + { user_id: user.id, space_id: `${namespace}/${repoId}` }, + { + $set: { + prompts: [ + ...prompts, + ], + }, + } + ); + return NextResponse.json({ ok: true }, { status: 200 }); +} + +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ namespace: string; repoId: string }> } +) { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + await dbConnect(); + const param = await params; + const { namespace, repoId } = param; + + const space = await spaceInfo({ + name: namespace + "/" + repoId, + accessToken: user.token as string, + additionalFields: ["author"], + }); + + if (!space || space.sdk !== "static") { + return NextResponse.json( + { + ok: false, + error: "Space is not a static space", + }, + { status: 404 } + ); + } + if (space.author !== user.name) { + return NextResponse.json( + { + ok: false, + error: "Space does not belong to the authenticated user", + }, + { status: 403 } + ); + } + + const project = await Project.findOne({ + user_id: user.id, + space_id: `${namespace}/${repoId}`, + }).lean(); + if (project) { + // redirect to the project page if it already exists + return NextResponse.json( + { + ok: false, + error: "Project already exists", + redirect: `/projects/${namespace}/${repoId}`, + }, + { status: 400 } + ); + } + + const newProject = new Project({ + user_id: user.id, + space_id: `${namespace}/${repoId}`, + prompts: [], + }); + + await newProject.save(); + return NextResponse.json( + { + ok: true, + project: { + id: newProject._id, + space_id: newProject.space_id, + prompts: newProject.prompts, + }, + }, + { status: 201 } + ); +} diff --git a/route (4).ts b/route (4).ts new file mode 100644 index 0000000000000000000000000000000000000000..4ab46939bdde0aaeb7163af0e96e94651bfccfd7 --- /dev/null +++ b/route (4).ts @@ -0,0 +1,111 @@ +import { NextRequest, NextResponse } from "next/server"; +import { RepoDesignation, uploadFiles } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import Project from "@/models/Project"; +import dbConnect from "@/lib/mongodb"; + +// No longer need the ImageUpload interface since we're handling FormData with File objects + +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ namespace: string; repoId: string }> } +) { + try { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + await dbConnect(); + const param = await params; + const { namespace, repoId } = param; + + const project = await Project.findOne({ + user_id: user.id, + space_id: `${namespace}/${repoId}`, + }).lean(); + + if (!project) { + return NextResponse.json( + { + ok: false, + error: "Project not found", + }, + { status: 404 } + ); + } + + // Parse the FormData to get the images + const formData = await req.formData(); + const imageFiles = formData.getAll("images") as File[]; + + if (!imageFiles || imageFiles.length === 0) { + return NextResponse.json( + { + ok: false, + error: "At least one image file is required under the 'images' key", + }, + { status: 400 } + ); + } + + const files: File[] = []; + for (const file of imageFiles) { + if (!(file instanceof File)) { + return NextResponse.json( + { + ok: false, + error: "Invalid file format - all items under 'images' key must be files", + }, + { status: 400 } + ); + } + + if (!file.type.startsWith('image/')) { + return NextResponse.json( + { + ok: false, + error: `File ${file.name} is not an image`, + }, + { status: 400 } + ); + } + + // Create File object with images/ folder prefix + const fileName = `images/${file.name}`; + const processedFile = new File([file], fileName, { type: file.type }); + files.push(processedFile); + } + + // Upload files to HuggingFace space + const repo: RepoDesignation = { + type: "space", + name: `${namespace}/${repoId}`, + }; + + await uploadFiles({ + repo, + files, + accessToken: user.token as string, + commitTitle: `Upload ${files.length} image(s)`, + }); + + return NextResponse.json({ + ok: true, + message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`, + uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`), + }, { status: 200 }); + + } catch (error) { + console.error('Error uploading images:', error); + return NextResponse.json( + { + ok: false, + error: "Failed to upload images", + }, + { status: 500 } + ); + } +} diff --git a/route (5).ts b/route (5).ts new file mode 100644 index 0000000000000000000000000000000000000000..c4164daba5c58bb2fe7f4f7508de7165f32ca443 --- /dev/null +++ b/route (5).ts @@ -0,0 +1,25 @@ +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; + +export async function GET() { + const authHeaders = await headers(); + const token = authHeaders.get("Authorization"); + if (!token) { + return NextResponse.json({ user: null, errCode: 401 }, { status: 401 }); + } + + const userResponse = await fetch("https://huggingface.co/api/whoami-v2", { + headers: { + Authorization: `${token}`, + }, + }); + + if (!userResponse.ok) { + return NextResponse.json( + { user: null, errCode: userResponse.status }, + { status: userResponse.status } + ); + } + const user = await userResponse.json(); + return NextResponse.json({ user, errCode: null }, { status: 200 }); +} diff --git a/route (6).ts b/route (6).ts new file mode 100644 index 0000000000000000000000000000000000000000..221e0c266bd2e6673779b9e81caf333243147f60 --- /dev/null +++ b/route (6).ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const { code } = body; + + if (!code) { + return NextResponse.json( + { error: "Code is required" }, + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + const Authorization = `Basic ${Buffer.from( + `${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}` + ).toString("base64")}`; + + const host = + req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000"; + + const url = host.includes("/spaces/enzostvs") + ? "enzostvs-deepsite.hf.space" + : host; + const redirect_uri = + `${host.includes("localhost") ? "http://" : "https://"}` + + url + + "/auth/callback"; + const request_auth = await fetch("https://huggingface.co/oauth/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization, + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri, + }), + }); + + const response = await request_auth.json(); + if (!response.access_token) { + return NextResponse.json( + { error: "Failed to retrieve access token" }, + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + const userResponse = await fetch("https://huggingface.co/api/whoami-v2", { + headers: { + Authorization: `Bearer ${response.access_token}`, + }, + }); + + if (!userResponse.ok) { + return NextResponse.json( + { user: null, errCode: userResponse.status }, + { status: userResponse.status } + ); + } + const user = await userResponse.json(); + + return NextResponse.json( + { + access_token: response.access_token, + expires_in: response.expires_in, + user, + }, + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ); +} diff --git a/route (7).ts b/route (7).ts new file mode 100644 index 0000000000000000000000000000000000000000..25a34108f99e8e3d0c6ccc00f345055445abcb47 --- /dev/null +++ b/route (7).ts @@ -0,0 +1,510 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { headers } from "next/headers"; +import { InferenceClient } from "@huggingface/inference"; + +import { MODELS, PROVIDERS } from "@/lib/providers"; +import { + DIVIDER, + FOLLOW_UP_SYSTEM_PROMPT, + INITIAL_SYSTEM_PROMPT, + MAX_REQUESTS_PER_IP, + NEW_PAGE_END, + NEW_PAGE_START, + REPLACE_END, + SEARCH_START, + UPDATE_PAGE_START, + UPDATE_PAGE_END, +} from "@/lib/prompts"; +import MY_TOKEN_KEY from "@/lib/get-cookie-name"; +import { Page } from "@/types"; + +const ipAddresses = new Map(); + +export async function POST(request: NextRequest) { + const authHeaders = await headers(); + const userToken = request.cookies.get(MY_TOKEN_KEY())?.value; + + const body = await request.json(); + const { prompt, provider, model, redesignMarkdown, previousPrompts, pages } = body; + + if (!model || (!prompt && !redesignMarkdown)) { + return NextResponse.json( + { ok: false, error: "Missing required fields" }, + { status: 400 } + ); + } + + const selectedModel = MODELS.find( + (m) => m.value === model || m.label === model + ); + + if (!selectedModel) { + return NextResponse.json( + { ok: false, error: "Invalid model selected" }, + { status: 400 } + ); + } + + if (!selectedModel.providers.includes(provider) && provider !== "auto") { + return NextResponse.json( + { + ok: false, + error: `The selected model does not support the ${provider} provider.`, + openSelectProvider: true, + }, + { status: 400 } + ); + } + + let token = userToken; + let billTo: string | null = null; + + /** + * Handle local usage token, this bypass the need for a user token + * and allows local testing without authentication. + * This is useful for development and testing purposes. + */ + if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) { + token = process.env.HF_TOKEN; + } + + const ip = authHeaders.get("x-forwarded-for")?.includes(",") + ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim() + : authHeaders.get("x-forwarded-for"); + + if (!token) { + ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1); + if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) { + return NextResponse.json( + { + ok: false, + openLogin: true, + message: "Log In to continue using the service", + }, + { status: 429 } + ); + } + + token = process.env.DEFAULT_HF_TOKEN as string; + billTo = "huggingface"; + } + + const DEFAULT_PROVIDER = PROVIDERS.novita; + const selectedProvider = + provider === "auto" + ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS] + : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER; + + const rewrittenPrompt = prompt; + + // if (prompt?.length < 240) { + + //rewrittenPrompt = await callAiRewritePrompt(prompt, { token, billTo }); + // } + + try { + const encoder = new TextEncoder(); + const stream = new TransformStream(); + const writer = stream.writable.getWriter(); + + const response = new NextResponse(stream.readable, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); + + (async () => { + // let completeResponse = ""; + try { + const client = new InferenceClient(token); + const chatCompletion = client.chatCompletionStream( + { + model: selectedModel.value, + provider: selectedProvider.id as any, + messages: [ + { + role: "system", + content: INITIAL_SYSTEM_PROMPT, + }, + ...(pages?.length > 1 ? [{ + role: "assistant", + content: `Here are the current pages:\n\n${pages.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}\n\nNow, please create a new page based on this code. Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}` + }] : []), + { + role: "user", + content: redesignMarkdown + ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.` + : rewrittenPrompt, + }, + ], + max_tokens: selectedProvider.max_tokens, + }, + billTo ? { billTo } : {} + ); + + while (true) { + const { done, value } = await chatCompletion.next(); + if (done) { + break; + } + + const chunk = value.choices[0]?.delta?.content; + if (chunk) { + await writer.write(encoder.encode(chunk)); + } + } + } catch (error: any) { + if (error.message?.includes("exceeded your monthly included credits")) { + await writer.write( + encoder.encode( + JSON.stringify({ + ok: false, + openProModal: true, + message: error.message, + }) + ) + ); + } else { + await writer.write( + encoder.encode( + JSON.stringify({ + ok: false, + message: + error.message || + "An error occurred while processing your request.", + }) + ) + ); + } + } finally { + await writer?.close(); + } + })(); + + return response; + } catch (error: any) { + return NextResponse.json( + { + ok: false, + openSelectProvider: true, + message: + error?.message || "An error occurred while processing your request.", + }, + { status: 500 } + ); + } +} + +export async function PUT(request: NextRequest) { + const authHeaders = await headers(); + const userToken = request.cookies.get(MY_TOKEN_KEY())?.value; + + const body = await request.json(); + const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, } = + body; + + if (!prompt || pages.length === 0) { + return NextResponse.json( + { ok: false, error: "Missing required fields" }, + { status: 400 } + ); + } + + const selectedModel = MODELS.find( + (m) => m.value === model || m.label === model + ); + if (!selectedModel) { + return NextResponse.json( + { ok: false, error: "Invalid model selected" }, + { status: 400 } + ); + } + + let token = userToken; + let billTo: string | null = null; + + /** + * Handle local usage token, this bypass the need for a user token + * and allows local testing without authentication. + * This is useful for development and testing purposes. + */ + if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) { + token = process.env.HF_TOKEN; + } + + const ip = authHeaders.get("x-forwarded-for")?.includes(",") + ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim() + : authHeaders.get("x-forwarded-for"); + + if (!token) { + ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1); + if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) { + return NextResponse.json( + { + ok: false, + openLogin: true, + message: "Log In to continue using the service", + }, + { status: 429 } + ); + } + + token = process.env.DEFAULT_HF_TOKEN as string; + billTo = "huggingface"; + } + + const client = new InferenceClient(token); + + const DEFAULT_PROVIDER = PROVIDERS.novita; + const selectedProvider = + provider === "auto" + ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS] + : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER; + + try { + const response = await client.chatCompletion( + { + model: selectedModel.value, + provider: selectedProvider.id as any, + messages: [ + { + role: "system", + content: FOLLOW_UP_SYSTEM_PROMPT, + }, + { + role: "user", + content: previousPrompts + ? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}` + : "You are modifying the HTML file based on the user's request.", + }, + { + role: "assistant", + + content: `${ + selectedElementHtml + ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\`` + : "" + }. 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")}.` : ""}`, + }, + { + role: "user", + content: prompt, + }, + ], + ...(selectedProvider.id !== "sambanova" + ? { + max_tokens: selectedProvider.max_tokens, + } + : {}), + }, + billTo ? { billTo } : {} + ); + + const chunk = response.choices[0]?.message?.content; + if (!chunk) { + return NextResponse.json( + { ok: false, message: "No content returned from the model" }, + { status: 400 } + ); + } + + if (chunk) { + const updatedLines: number[][] = []; + let newHtml = ""; + const updatedPages = [...(pages || [])]; + + 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'); + let updatePageMatch; + + while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) { + const [, pagePath, pageContent] = updatePageMatch; + + const pageIndex = updatedPages.findIndex(p => p.path === pagePath); + if (pageIndex !== -1) { + let pageHtml = updatedPages[pageIndex].html; + + let processedContent = pageContent; + const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/); + if (htmlMatch) { + processedContent = htmlMatch[1]; + } + let position = 0; + let moreBlocks = true; + + while (moreBlocks) { + const searchStartIndex = processedContent.indexOf(SEARCH_START, position); + if (searchStartIndex === -1) { + moreBlocks = false; + continue; + } + + const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex); + if (dividerIndex === -1) { + moreBlocks = false; + continue; + } + + const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex); + if (replaceEndIndex === -1) { + moreBlocks = false; + continue; + } + + const searchBlock = processedContent.substring( + searchStartIndex + SEARCH_START.length, + dividerIndex + ); + const replaceBlock = processedContent.substring( + dividerIndex + DIVIDER.length, + replaceEndIndex + ); + + if (searchBlock.trim() === "") { + pageHtml = `${replaceBlock}\n${pageHtml}`; + updatedLines.push([1, replaceBlock.split("\n").length]); + } else { + const blockPosition = pageHtml.indexOf(searchBlock); + if (blockPosition !== -1) { + const beforeText = pageHtml.substring(0, blockPosition); + const startLineNumber = beforeText.split("\n").length; + const replaceLines = replaceBlock.split("\n").length; + const endLineNumber = startLineNumber + replaceLines - 1; + + updatedLines.push([startLineNumber, endLineNumber]); + pageHtml = pageHtml.replace(searchBlock, replaceBlock); + } + } + + position = replaceEndIndex + REPLACE_END.length; + } + + updatedPages[pageIndex].html = pageHtml; + + if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') { + newHtml = pageHtml; + } + } + } + + 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'); + let newPageMatch; + + while ((newPageMatch = newPageRegex.exec(chunk)) !== null) { + const [, pagePath, pageContent] = newPageMatch; + + let pageHtml = pageContent; + const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/); + if (htmlMatch) { + pageHtml = htmlMatch[1]; + } + + const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath); + + if (existingPageIndex !== -1) { + updatedPages[existingPageIndex] = { + path: pagePath, + html: pageHtml.trim() + }; + } else { + updatedPages.push({ + path: pagePath, + html: pageHtml.trim() + }); + } + } + + if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) { + let position = 0; + let moreBlocks = true; + + while (moreBlocks) { + const searchStartIndex = chunk.indexOf(SEARCH_START, position); + if (searchStartIndex === -1) { + moreBlocks = false; + continue; + } + + const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex); + if (dividerIndex === -1) { + moreBlocks = false; + continue; + } + + const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex); + if (replaceEndIndex === -1) { + moreBlocks = false; + continue; + } + + const searchBlock = chunk.substring( + searchStartIndex + SEARCH_START.length, + dividerIndex + ); + const replaceBlock = chunk.substring( + dividerIndex + DIVIDER.length, + replaceEndIndex + ); + + if (searchBlock.trim() === "") { + newHtml = `${replaceBlock}\n${newHtml}`; + updatedLines.push([1, replaceBlock.split("\n").length]); + } else { + const blockPosition = newHtml.indexOf(searchBlock); + if (blockPosition !== -1) { + const beforeText = newHtml.substring(0, blockPosition); + const startLineNumber = beforeText.split("\n").length; + const replaceLines = replaceBlock.split("\n").length; + const endLineNumber = startLineNumber + replaceLines - 1; + + updatedLines.push([startLineNumber, endLineNumber]); + newHtml = newHtml.replace(searchBlock, replaceBlock); + } + } + + position = replaceEndIndex + REPLACE_END.length; + } + + // Update the main HTML if it's the index page + const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index'); + if (mainPageIndex !== -1) { + updatedPages[mainPageIndex].html = newHtml; + } + } + + return NextResponse.json({ + ok: true, + updatedLines, + pages: updatedPages, + }); + } else { + return NextResponse.json( + { ok: false, message: "No content returned from the model" }, + { status: 400 } + ); + } + } catch (error: any) { + if (error.message?.includes("exceeded your monthly included credits")) { + return NextResponse.json( + { + ok: false, + openProModal: true, + message: error.message, + }, + { status: 402 } + ); + } + return NextResponse.json( + { + ok: false, + openSelectProvider: true, + message: + error.message || "An error occurred while processing your request.", + }, + { status: 500 } + ); + } +} diff --git a/route.ts b/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..777c2cbd18f95592080c0fe1ad2cab23a5264397 --- /dev/null +++ b/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function PUT(request: NextRequest) { + const body = await request.json(); + const { url } = body; + + if (!url) { + return NextResponse.json({ error: "URL is required" }, { status: 400 }); + } + + try { + const response = await fetch( + `https://r.jina.ai/${encodeURIComponent(url)}`, + { + method: "POST", + } + ); + if (!response.ok) { + return NextResponse.json( + { error: "Failed to fetch redesign" }, + { status: 500 } + ); + } + const markdown = await response.text(); + return NextResponse.json( + { + ok: true, + markdown, + }, + { status: 200 } + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + return NextResponse.json( + { error: error.message || "An error occurred" }, + { status: 500 } + ); + } +} diff --git a/sambanova.svg b/sambanova.svg new file mode 100644 index 0000000000000000000000000000000000000000..1d2593c9bc0c0f5741b5c1e051c08f204419bbcf --- /dev/null +++ b/sambanova.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/select.tsx (1).txt b/select.tsx (1).txt new file mode 100644 index 0000000000000000000000000000000000000000..dcbbc0ca0c781dfa6d2fe4ee6f1c9c2cad905a9b --- /dev/null +++ b/select.tsx (1).txt @@ -0,0 +1,185 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/select.tsx.txt b/select.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..dcbbc0ca0c781dfa6d2fe4ee6f1c9c2cad905a9b --- /dev/null +++ b/select.tsx.txt @@ -0,0 +1,185 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/selected-files.tsx.txt b/selected-files.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..fa4901e3254b42fd4f9f7d93f2b4b13eaf31bd70 --- /dev/null +++ b/selected-files.tsx.txt @@ -0,0 +1,47 @@ +import Image from "next/image"; + +import { Button } from "@/components/ui/button"; +import { Minus } from "lucide-react"; + +export const SelectedFiles = ({ + files, + isAiWorking, + onDelete, +}: { + files: string[]; + isAiWorking: boolean; + onDelete: (file: string) => void; +}) => { + if (files.length === 0) return null; + return ( +
+
+ {files.map((file) => ( +
+ uploaded image + +
+ ))} +
+
+ ); +}; diff --git a/selected-html-element.tsx.txt b/selected-html-element.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..a0a8930db4d067ef7adc46aa4917983bfcfd55f5 --- /dev/null +++ b/selected-html-element.tsx.txt @@ -0,0 +1,57 @@ +import classNames from "classnames"; +import { Code, XCircle } from "lucide-react"; + +import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { htmlTagToText } from "@/lib/html-tag-to-text"; + +export const SelectedHtmlElement = ({ + element, + isAiWorking = false, + onDelete, +}: { + element: HTMLElement | null; + isAiWorking: boolean; + onDelete?: () => void; +}) => { + if (!element) return null; + + const tagName = element.tagName.toLowerCase(); + return ( + { + if (!isAiWorking && onDelete) { + onDelete(); + } + }} + > + +
+ +
+

+ {element.textContent?.trim().split(/\s+/)[0]} {htmlTagToText(tagName)} +

+ +
+ {/* +
+

+ ID: {element.id || "No ID"} +

+

+ Classes:{" "} + {element.className || "No classes"} +

+
+
*/} +
+ ); +}; diff --git a/settings.tsx.txt b/settings.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..87a322d94f883a72d2a8d12a4f250f504c3fcf7e --- /dev/null +++ b/settings.tsx.txt @@ -0,0 +1,202 @@ +import classNames from "classnames"; +import { PiGearSixFill } from "react-icons/pi"; +import { RiCheckboxCircleFill } from "react-icons/ri"; + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { PROVIDERS, MODELS } from "@/lib/providers"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useMemo } from "react"; +import { useUpdateEffect } from "react-use"; +import Image from "next/image"; + +export function Settings({ + open, + onClose, + provider, + model, + error, + isFollowUp = false, + onChange, + onModelChange, +}: { + open: boolean; + provider: string; + model: string; + error?: string; + isFollowUp?: boolean; + onClose: React.Dispatch>; + onChange: (provider: string) => void; + onModelChange: (model: string) => void; +}) { + const modelAvailableProviders = useMemo(() => { + const availableProviders = MODELS.find( + (m: { value: string }) => m.value === model + )?.providers; + if (!availableProviders) return Object.keys(PROVIDERS); + return Object.keys(PROVIDERS).filter((id) => + availableProviders.includes(id) + ); + }, [model]); + + useUpdateEffect(() => { + if (provider !== "auto" && !modelAvailableProviders.includes(provider)) { + onChange("auto"); + } + }, [model, provider]); + + return ( +
+ + + + + +
+ Customize Settings +
+
+ {error !== "" && ( +

+ {error} +

+ )} + + {isFollowUp && ( +
+ Note: You can't use a Thinker model for follow-up requests. + We automatically switch to the default model for you. +
+ )} +
+
+
+

+ Use auto-provider +

+

+ We'll automatically select the best provider for you + based on your prompt. +

+
+
{ + const foundModel = MODELS.find( + (m: { value: string }) => m.value === model + ); + if (provider === "auto" && foundModel?.autoProvider) { + onChange(foundModel.autoProvider); + } else { + onChange("auto"); + } + }} + > +
+
+
+ +
+
+
+
+
+ ); +} diff --git a/sonner.tsx.txt b/sonner.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..2922154241f1415b767c55ca77bbfa8a1569cefd --- /dev/null +++ b/sonner.tsx.txt @@ -0,0 +1,22 @@ +"use client"; + +import { Toaster as Sonner, ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + + ); +}; + +export { Toaster }; diff --git a/space.svg b/space.svg new file mode 100644 index 0000000000000000000000000000000000000000..f133cf120bb1f4fe43c949d099965ae9a84db240 --- /dev/null +++ b/space.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000000000000000000000000000000000000..114adf441e9032febb46bc056b2a8bb651075f0d --- /dev/null +++ b/style.css @@ -0,0 +1,28 @@ +body { + padding: 2rem; + font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif; +} + +h1 { + font-size: 16px; + margin-top: 0; +} + +p { + color: rgb(107, 114, 128); + font-size: 15px; + margin-bottom: 10px; + margin-top: 5px; +} + +.card { + max-width: 620px; + margin: 0 auto; + padding: 16px; + border: 1px solid lightgray; + border-radius: 16px; +} + +.card p:last-child { + margin-bottom: 0; +} diff --git a/switch.tsx.txt b/switch.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..6a2b5241d8c0b3df98dec769e9999df4978975bf --- /dev/null +++ b/switch.tsx.txt @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitive from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Switch } diff --git a/tabs.tsx.txt b/tabs.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..497ba5ea34247f6843e0c58ccd7da61b7c8edb46 --- /dev/null +++ b/tabs.tsx.txt @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/tanstack-query-provider.tsx.txt b/tanstack-query-provider.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..618056fcc0e27cb4433d0debe722ac8bb457f840 --- /dev/null +++ b/tanstack-query-provider.tsx.txt @@ -0,0 +1,18 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +export default function TanstackProvider({ + children, +}: { + children: React.ReactNode; +}) { + const queryClient = new QueryClient(); + + return ( + + {children} + {/* */} + + ); +} diff --git a/together.svg b/together.svg new file mode 100644 index 0000000000000000000000000000000000000000..2faae86b2c8a5694e7317fec8e5df0c858bcba6e --- /dev/null +++ b/together.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/toggle-group.tsx.txt b/toggle-group.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..5eed401b6c9c19f7b6f88e90d3cbe38783ef198b --- /dev/null +++ b/toggle-group.tsx.txt @@ -0,0 +1,73 @@ +"use client" + +import * as React from "react" +import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" +import { type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { toggleVariants } from "@/components/ui/toggle" + +const ToggleGroupContext = React.createContext< + VariantProps +>({ + size: "default", + variant: "default", +}) + +function ToggleGroup({ + className, + variant, + size, + children, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + + {children} + + + ) +} + +function ToggleGroupItem({ + className, + children, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + const context = React.useContext(ToggleGroupContext) + + return ( + + {children} + + ) +} + +export { ToggleGroup, ToggleGroupItem } diff --git a/toggle.tsx.txt b/toggle.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..94ec8f589b345a8c33b165463dfda393c7255967 --- /dev/null +++ b/toggle.tsx.txt @@ -0,0 +1,47 @@ +"use client" + +import * as React from "react" +import * as TogglePrimitive from "@radix-ui/react-toggle" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const toggleVariants = cva( + "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-9 px-2 min-w-9", + sm: "h-8 px-1.5 min-w-8", + lg: "h-10 px-2.5 min-w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Toggle({ + className, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +export { Toggle, toggleVariants } diff --git a/tooltip.tsx.txt b/tooltip.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..4ee26b38a595862a5a082013d564d1ccf8452a7c --- /dev/null +++ b/tooltip.tsx.txt @@ -0,0 +1,61 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/uploader.tsx.txt b/uploader.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..8f63598a6c0d7028ca4e448c9ff6f2faba0e4397 --- /dev/null +++ b/uploader.tsx.txt @@ -0,0 +1,203 @@ +import { useRef, useState } from "react"; +import { Images, Upload } from "lucide-react"; +import Image from "next/image"; + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Page, Project } from "@/types"; +import Loading from "@/components/loading"; +import { RiCheckboxCircleFill } from "react-icons/ri"; +import { useUser } from "@/hooks/useUser"; +import { LoginModal } from "@/components/login-modal"; +import { DeployButtonContent } from "../deploy-button/content"; + +export const Uploader = ({ + pages, + onLoading, + isLoading, + onFiles, + onSelectFile, + selectedFiles, + files, + project, +}: { + pages: Page[]; + onLoading: (isLoading: boolean) => void; + isLoading: boolean; + files: string[]; + onFiles: React.Dispatch>; + onSelectFile: (file: string) => void; + selectedFiles: string[]; + project?: Project | null; +}) => { + const { user } = useUser(); + + const [open, setOpen] = useState(false); + const fileInputRef = useRef(null); + + const uploadFiles = async (files: FileList | null) => { + if (!files) return; + if (!project) return; + + onLoading(true); + + const images = Array.from(files).filter((file) => { + return file.type.startsWith("image/"); + }); + + const data = new FormData(); + images.forEach((image) => { + data.append("images", image); + }); + + const response = await fetch( + `/api/me/projects/${project.space_id}/images`, + { + method: "POST", + body: data, + } + ); + if (response.ok) { + const data = await response.json(); + onFiles((prev) => [...prev, ...data.uploadedFiles]); + } + onLoading(false); + }; + + // TODO FIRST PUBLISH YOUR PROJECT TO UPLOAD IMAGES. + return user?.id ? ( + +
+ + + + + {project?.space_id ? ( + <> +
+
+
+ 🎨 +
+
+ 🖼️ +
+
+ 💻 +
+
+

+ Add Custom Images +

+

+ Upload images to your project and use them with DeepSite! +

+
+
+
+

+ Uploaded Images +

+
+ {files.map((file) => ( +
onSelectFile(file)} + > + uploaded image + {selectedFiles.includes(file) && ( +
+ +
+ )} +
+ ))} +
+
+
+

+ Or import images from your computer +

+ + uploadFiles(e.target.files)} + /> +
+
+ + ) : ( + + )} +
+
+
+ ) : ( + <> + + setOpen(false)} + pages={pages} + title="Log In to add Custom Images" + description="Log In through your Hugging Face account to publish your project and increase your monthly free limit." + /> + + ); +}; diff --git a/useCallAi.ts b/useCallAi.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a045739f9faddd9e26d5e3661f29409fd10146e --- /dev/null +++ b/useCallAi.ts @@ -0,0 +1,461 @@ +import { useState, useRef } from "react"; +import { toast } from "sonner"; +import { MODELS } from "@/lib/providers"; +import { Page } from "@/types"; + +interface UseCallAiProps { + onNewPrompt: (prompt: string) => void; + onSuccess: (page: Page[], p: string, n?: number[][]) => void; + onScrollToBottom: () => void; + setPages: React.Dispatch>; + setCurrentPage: React.Dispatch>; + currentPage: Page; + pages: Page[]; + isAiWorking: boolean; + setisAiWorking: React.Dispatch>; +} + +export const useCallAi = ({ + onNewPrompt, + onSuccess, + onScrollToBottom, + setPages, + setCurrentPage, + pages, + isAiWorking, + setisAiWorking, +}: UseCallAiProps) => { + const audio = useRef(null); + const [controller, setController] = useState(null); + + const callAiNewProject = async (prompt: string, model: string | undefined, provider: string | undefined, redesignMarkdown?: string, handleThink?: (think: string) => void, onFinishThink?: () => void) => { + if (isAiWorking) return; + if (!redesignMarkdown && !prompt.trim()) return; + + setisAiWorking(true); + + const abortController = new AbortController(); + setController(abortController); + + try { + onNewPrompt(prompt); + + const request = await fetch("/api/ask-ai", { + method: "POST", + body: JSON.stringify({ + prompt, + provider, + model, + redesignMarkdown, + }), + headers: { + "Content-Type": "application/json", + "x-forwarded-for": window.location.hostname, + }, + signal: abortController.signal, + }); + + if (request && request.body) { + const reader = request.body.getReader(); + const decoder = new TextDecoder("utf-8"); + const selectedModel = MODELS.find( + (m: { value: string }) => m.value === model + ); + let contentResponse = ""; + + const read = async () => { + const { done, value } = await reader.read(); + if (done) { + const isJson = + contentResponse.trim().startsWith("{") && + contentResponse.trim().endsWith("}"); + const jsonResponse = isJson ? JSON.parse(contentResponse) : null; + + if (jsonResponse && !jsonResponse.ok) { + if (jsonResponse.openLogin) { + // Handle login required + return { error: "login_required" }; + } else if (jsonResponse.openSelectProvider) { + // Handle provider selection required + return { error: "provider_required", message: jsonResponse.message }; + } else if (jsonResponse.openProModal) { + // Handle pro modal required + return { error: "pro_required" }; + } else { + toast.error(jsonResponse.message); + setisAiWorking(false); + return { error: "api_error", message: jsonResponse.message }; + } + } + + toast.success("AI responded successfully"); + setisAiWorking(false); + + if (audio.current) audio.current.play(); + + const newPages = formatPages(contentResponse); + onSuccess(newPages, prompt); + + return { success: true, pages: newPages }; + } + + const chunk = decoder.decode(value, { stream: true }); + contentResponse += chunk; + + if (selectedModel?.isThinker) { + const thinkMatch = contentResponse.match(/[\s\S]*/)?.[0]; + if (thinkMatch && !contentResponse?.includes("")) { + handleThink?.(thinkMatch.replace("", "").trim()); + return read(); + } + } + + if (contentResponse.includes("")) { + onFinishThink?.(); + } + + formatPages(contentResponse); + return read(); + }; + + return await read(); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + setisAiWorking(false); + toast.error(error.message); + if (error.openLogin) { + return { error: "login_required" }; + } + return { error: "network_error", message: error.message }; + } + }; + + const callAiNewPage = async (prompt: string, model: string | undefined, provider: string | undefined, currentPagePath: string, previousPrompts?: string[]) => { + if (isAiWorking) return; + if (!prompt.trim()) return; + + setisAiWorking(true); + + const abortController = new AbortController(); + setController(abortController); + + try { + onNewPrompt(prompt); + + const request = await fetch("/api/ask-ai", { + method: "POST", + body: JSON.stringify({ + prompt, + provider, + model, + pages, + previousPrompts, + }), + headers: { + "Content-Type": "application/json", + "x-forwarded-for": window.location.hostname, + }, + signal: abortController.signal, + }); + + if (request && request.body) { + const reader = request.body.getReader(); + const decoder = new TextDecoder("utf-8"); + const selectedModel = MODELS.find( + (m: { value: string }) => m.value === model + ); + let contentResponse = ""; + + const read = async () => { + const { done, value } = await reader.read(); + if (done) { + const isJson = + contentResponse.trim().startsWith("{") && + contentResponse.trim().endsWith("}"); + const jsonResponse = isJson ? JSON.parse(contentResponse) : null; + + if (jsonResponse && !jsonResponse.ok) { + if (jsonResponse.openLogin) { + // Handle login required + return { error: "login_required" }; + } else if (jsonResponse.openSelectProvider) { + // Handle provider selection required + return { error: "provider_required", message: jsonResponse.message }; + } else if (jsonResponse.openProModal) { + // Handle pro modal required + return { error: "pro_required" }; + } else { + toast.error(jsonResponse.message); + setisAiWorking(false); + return { error: "api_error", message: jsonResponse.message }; + } + } + + toast.success("AI responded successfully"); + setisAiWorking(false); + + if (selectedModel?.isThinker) { + // Reset to default model if using thinker model + // Note: You might want to add a callback for this + } + + if (audio.current) audio.current.play(); + + const newPage = formatPage(contentResponse, currentPagePath); + if (!newPage) { return { error: "api_error", message: "Failed to format page" } } + onSuccess([...pages, newPage], prompt); + + return { success: true, pages: [...pages, newPage] }; + } + + const chunk = decoder.decode(value, { stream: true }); + contentResponse += chunk; + + if (selectedModel?.isThinker) { + const thinkMatch = contentResponse.match(/[\s\S]*/)?.[0]; + if (thinkMatch && !contentResponse?.includes("")) { + // contentThink += chunk; + return read(); + } + } + + formatPage(contentResponse, currentPagePath); + return read(); + }; + + return await read(); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + setisAiWorking(false); + toast.error(error.message); + if (error.openLogin) { + return { error: "login_required" }; + } + return { error: "network_error", message: error.message }; + } + }; + + const callAiFollowUp = async (prompt: string, model: string | undefined, provider: string | undefined, previousPrompts: string[], selectedElementHtml?: string, files?: string[]) => { + if (isAiWorking) return; + if (!prompt.trim()) return; + + setisAiWorking(true); + + const abortController = new AbortController(); + setController(abortController); + + try { + onNewPrompt(prompt); + + const request = await fetch("/api/ask-ai", { + method: "PUT", + body: JSON.stringify({ + prompt, + provider, + previousPrompts, + model, + pages, + selectedElementHtml, + files, + }), + headers: { + "Content-Type": "application/json", + "x-forwarded-for": window.location.hostname, + }, + signal: abortController.signal, + }); + + if (request && request.body) { + const res = await request.json(); + + if (!request.ok) { + if (res.openLogin) { + setisAiWorking(false); + return { error: "login_required" }; + } else if (res.openSelectProvider) { + setisAiWorking(false); + return { error: "provider_required", message: res.message }; + } else if (res.openProModal) { + setisAiWorking(false); + return { error: "pro_required" }; + } else { + toast.error(res.message); + setisAiWorking(false); + return { error: "api_error", message: res.message }; + } + } + + toast.success("AI responded successfully"); + setisAiWorking(false); + + setPages(res.pages); + onSuccess(res.pages, prompt, res.updatedLines); + + if (audio.current) audio.current.play(); + + return { success: true, html: res.html, updatedLines: res.updatedLines }; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + setisAiWorking(false); + toast.error(error.message); + if (error.openLogin) { + return { error: "login_required" }; + } + return { error: "network_error", message: error.message }; + } + }; + + // Stop the current AI generation + const stopController = () => { + if (controller) { + controller.abort(); + setController(null); + setisAiWorking(false); + } + }; + + const formatPages = (content: string) => { + const pages: Page[] = []; + if (!content.match(/<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/)) { + return pages; + } + + const cleanedContent = content.replace( + /[\s\S]*?<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/, + "<<<<<<< START_TITLE $1 >>>>>>> END_TITLE" + ); + const htmlChunks = cleanedContent.split( + /<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/ + ); + const processedChunks = new Set(); + + htmlChunks.forEach((chunk, index) => { + if (processedChunks.has(index) || !chunk?.trim()) { + return; + } + const htmlContent = extractHtmlContent(htmlChunks[index + 1]); + + if (htmlContent) { + const page: Page = { + path: chunk.trim(), + html: htmlContent, + }; + pages.push(page); + + if (htmlContent.length > 200) { + onScrollToBottom(); + } + + processedChunks.add(index); + processedChunks.add(index + 1); + } + }); + if (pages.length > 0) { + setPages(pages); + const lastPagePath = pages[pages.length - 1]?.path; + setCurrentPage(lastPagePath || "index.html"); + } + + return pages; + }; + + const formatPage = (content: string, currentPagePath: string) => { + if (!content.match(/<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/)) { + return null; + } + + const cleanedContent = content.replace( + /[\s\S]*?<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/, + "<<<<<<< START_TITLE $1 >>>>>>> END_TITLE" + ); + + const htmlChunks = cleanedContent.split( + /<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/ + )?.filter(Boolean); + + const pagePath = htmlChunks[0]?.trim() || ""; + const htmlContent = extractHtmlContent(htmlChunks[1]); + + if (!pagePath || !htmlContent) { + return null; + } + + const page: Page = { + path: pagePath, + html: htmlContent, + }; + + setPages(prevPages => { + const existingPageIndex = prevPages.findIndex(p => p.path === currentPagePath || p.path === pagePath); + + if (existingPageIndex !== -1) { + const updatedPages = [...prevPages]; + updatedPages[existingPageIndex] = page; + return updatedPages; + } else { + return [...prevPages, page]; + } + }); + + setCurrentPage(pagePath); + + if (htmlContent.length > 200) { + onScrollToBottom(); + } + + return page; + }; + + // Helper function to extract and clean HTML content + const extractHtmlContent = (chunk: string): string => { + if (!chunk) return ""; + + // Extract HTML content + const htmlMatch = chunk.trim().match(/[\s\S]*/); + if (!htmlMatch) return ""; + + let htmlContent = htmlMatch[0]; + + // Ensure proper HTML structure + htmlContent = ensureCompleteHtml(htmlContent); + + // Remove markdown code blocks if present + htmlContent = htmlContent.replace(/```/g, ""); + + return htmlContent; + }; + + // Helper function to ensure HTML has complete structure + const ensureCompleteHtml = (html: string): string => { + let completeHtml = html; + + // Add missing head closing tag + if (completeHtml.includes("") && !completeHtml.includes("")) { + completeHtml += "\n"; + } + + // Add missing body closing tag + if (completeHtml.includes("")) { + completeHtml += "\n"; + } + + // Add missing html closing tag + if (!completeHtml.includes("")) { + completeHtml += "\n"; + } + + return completeHtml; + }; + + return { + callAiNewProject, + callAiFollowUp, + callAiNewPage, + stopController, + controller, + audio, + }; +}; diff --git a/useEditor.ts b/useEditor.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2c1e3d0235f62f8db71c097c2ffea5be56e0a3c --- /dev/null +++ b/useEditor.ts @@ -0,0 +1,39 @@ +import { defaultHTML } from "@/lib/consts"; +import { HtmlHistory, Page } from "@/types"; +import { useState } from "react"; + +export const useEditor = (initialPages?: Page[], initialPrompts?: string[], initialHtmlStorage?: string) => { + /** + * State to manage the HTML content of the editor. + * This will be the main content that users edit. + */ + const [pages, setPages] = useState>(initialPages ??[ + { + path: "index.html", + html: initialHtmlStorage ?? defaultHTML, + }, + ]); + /** + * State to manage the history of HTML edits. + * This will store previous versions of the HTML content along with metadata. (not saved to DB) + */ + const [htmlHistory, setHtmlHistory] = useState([]); + + /** + * State to manage the prompts used for generating HTML content. + * This can be used to track what prompts were used in the editor. + */ + const [prompts, setPrompts] = useState( + initialPrompts ?? [] + ); + + + return { + htmlHistory, + setHtmlHistory, + prompts, + pages, + setPages, + setPrompts, + }; +}; diff --git a/useUser.ts b/useUser.ts new file mode 100644 index 0000000000000000000000000000000000000000..14d407d70e69baf900afd866f39f84d9f257e738 --- /dev/null +++ b/useUser.ts @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCookie } from "react-use"; +import { useRouter } from "next/navigation"; + +import { User } from "@/types"; +import MY_TOKEN_KEY from "@/lib/get-cookie-name"; +import { api } from "@/lib/api"; +import { toast } from "sonner"; + +export const useUser = (initialData?: { + user: User | null; + errCode: number | null; +}) => { + const cookie_name = MY_TOKEN_KEY(); + const client = useQueryClient(); + const router = useRouter(); + const [, setCookie, removeCookie] = useCookie(cookie_name); + const [currentRoute, setCurrentRoute] = useCookie("deepsite-currentRoute"); + + const { data: { user, errCode } = { user: null, errCode: null }, isLoading } = + useQuery({ + queryKey: ["user.me"], + queryFn: async () => { + return { user: initialData?.user, errCode: initialData?.errCode }; + }, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + retry: false, + initialData: initialData + ? { user: initialData?.user, errCode: initialData?.errCode } + : undefined, + enabled: false, + }); + + const { data: loadingAuth } = useQuery({ + queryKey: ["loadingAuth"], + queryFn: async () => false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + }); + const setLoadingAuth = (value: boolean) => { + client.setQueryData(["setLoadingAuth"], value); + }; + + const openLoginWindow = async () => { + setCurrentRoute(window.location.pathname); + return router.push("/auth"); + }; + + const loginFromCode = async (code: string) => { + setLoadingAuth(true); + if (loadingAuth) return; + await api + .post("/auth", { code }) + .then(async (res: any) => { + if (res.data) { + setCookie(res.data.access_token, { + expires: res.data.expires_in + ? new Date(Date.now() + res.data.expires_in * 1000) + : undefined, + sameSite: "none", + secure: true, + }); + client.setQueryData(["user.me"], { + user: res.data.user, + errCode: null, + }); + if (currentRoute) { + router.push(currentRoute); + setCurrentRoute(""); + } else { + router.push("/projects"); + } + toast.success("Login successful"); + } + }) + .catch((err: any) => { + toast.error(err?.data?.message ?? err.message ?? "An error occurred"); + }) + .finally(() => { + setLoadingAuth(false); + }); + }; + + const logout = async () => { + removeCookie(); + router.push("/"); + toast.success("Logout successful"); + client.setQueryData(["user.me"], { + user: null, + errCode: null, + }); + window.location.reload(); + }; + + return { + user, + errCode, + loading: isLoading || loadingAuth, + openLoginWindow, + loginFromCode, + logout, + }; +}; diff --git a/user-context.tsx.txt b/user-context.tsx.txt new file mode 100644 index 0000000000000000000000000000000000000000..8a3391744618bfcfc979401cdee76051c70fee8f --- /dev/null +++ b/user-context.tsx.txt @@ -0,0 +1,8 @@ +"use client"; + +import { createContext } from "react"; +import { User } from "@/types"; + +export const UserContext = createContext({ + user: undefined as User | undefined, +});