diff --git a/.env.example b/.env.example deleted file mode 100644 index d7419a983b3e8d6606db990addea8d78dd660692..0000000000000000000000000000000000000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -AUTH_HUGGINGFACE_ID= -AUTH_HUGGINGFACE_SECRET= -NEXTAUTH_URL=http://localhost:3001 -AUTH_SECRET= \ No newline at end of file diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml deleted file mode 100644 index 88d7e85dd2f708097a42154a349c9cb217479120..0000000000000000000000000000000000000000 --- a/.github/workflows/deploy-prod.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Deploy to k8s -on: - # run this workflow manually from the Actions tab - workflow_dispatch: - -jobs: - build-and-publish: - runs-on: - group: cpu-high - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to Registry - uses: docker/login-action@v3 - with: - registry: registry.internal.huggingface.tech - username: ${{ secrets.DOCKER_INTERNAL_USERNAME }} - password: ${{ secrets.DOCKER_INTERNAL_PASSWORD }} - - - name: Docker metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: | - registry.internal.huggingface.tech/deepsite/deepsite - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=sha,enable=true,prefix=sha-,format=short,sha-len=8 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@v4 - - - name: Build and Publish image - uses: docker/build-push-action@v5 - with: - context: . - file: Dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64 - cache-to: type=gha,mode=max,scope=amd64 - cache-from: type=gha,scope=amd64 - provenance: false - - deploy: - name: Deploy on prod - runs-on: ubuntu-latest - needs: ["build-and-publish"] - steps: - - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@v4 - - - name: Gen values - run: | - VALUES=$(cat <<-END - image: - tag: "sha-${{ env.GITHUB_SHA_SHORT }}" - END - ) - echo "VALUES=$(echo "$VALUES" | yq -o=json | jq tostring)" >> $GITHUB_ENV - - - name: Deploy on infra-deployments - uses: the-actions-org/workflow-dispatch@v2 - with: - workflow: Update application single value - repo: huggingface/infra-deployments - wait-for-completion: true - wait-for-completion-interval: 10s - display-workflow-run-url-interval: 10s - ref: refs/heads/main - token: ${{ secrets.GIT_TOKEN_INFRA_DEPLOYMENT }} - inputs: '{"path": "hub/deepsite/deepsite.yaml", "value": ${{ env.VALUES }}, "url": "${{ github.event.head_commit.url }}"}' diff --git a/.gitignore b/.gitignore index b8b89cbb382fac7dea7fdeb461bd43beb9937c49..5ef6a520780202a1d6addd833d800ccb1ecac0bb 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env +.env* # vercel .vercel @@ -39,9 +39,3 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts - -.idea - -# binary assets (hosted on CDN) -assets/assistant.jpg -.gitattributes \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a2b0759c0a612c3be02d8c1eb3021252cdd4a7f8..cbe0188aaee92186937765d2c85d76f7b212c537 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,19 @@ FROM node:20-alpine USER root -# Install pnpm -RUN corepack enable && corepack prepare pnpm@latest --activate - USER 1000 WORKDIR /usr/src/app -# Copy package.json and pnpm-lock.yaml to the container -COPY --chown=1000 package.json pnpm-lock.yaml ./ +# 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 pnpm install -RUN pnpm run build +RUN npm install +RUN npm run build # Expose the application port (assuming your app runs on port 3000) -EXPOSE 3001 +EXPOSE 3000 # Start the application -CMD ["pnpm", "start"] \ No newline at end of file +CMD ["npm", "start"] \ No newline at end of file diff --git a/README.md b/README.md index 6b13fd8b95aeb699f979cbc2cbb8d7495aeacdf5..5ab2231fc7dc96070548f1d03ab1d0f73a799600 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,22 @@ --- -title: DeepSite v4 +title: DeepSite v2 emoji: 🐳 colorFrom: blue colorTo: blue sdk: docker pinned: true -app_port: 3001 +app_port: 3000 license: mit -failure_strategy: rollback -short_description: Generate any application by Vibe Coding it +short_description: Generate any application with DeepSeek models: - deepseek-ai/DeepSeek-V3-0324 - - deepseek-ai/DeepSeek-V3.2 - - Qwen/Qwen3-Coder-30B-A3B-Instruct - - moonshotai/Kimi-K2-Instruct-0905 - - zai-org/GLM-4.7 - - MiniMaxAI/MiniMax-M2.1 + - deepseek-ai/DeepSeek-R1-0528 --- # DeepSite 🐳 -DeepSite is a Vibe Coding Platform 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. +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/actions/mentions.ts b/actions/mentions.ts deleted file mode 100644 index c6b7dabfbaa6f0efe0d492dfe5470353b0139bae..0000000000000000000000000000000000000000 --- a/actions/mentions.ts +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { File } from "@/lib/type"; - -export const searchMentions = async (query: string) => { - const promises = [searchModels(query), searchDatasets(query)]; - const results = await Promise.all(promises); - return { models: results[0], datasets: results[1] }; -}; - -const searchModels = async (query: string) => { - const response = await fetch( - `https://huggingface.co/api/quicksearch?q=${query}&type=model&limit=3` - ); - const data = await response.json(); - return data?.models ?? []; -}; - -const searchDatasets = async (query: string) => { - const response = await fetch( - `https://huggingface.co/api/quicksearch?q=${query}&type=dataset&limit=3` - ); - const data = await response.json(); - return data?.datasets ?? []; -}; - -export const searchFilesMentions = async (query: string, files: File[]) => { - if (!query) return files; - const lowerQuery = query.toLowerCase(); - return files.filter((file) => file.path.toLowerCase().includes(lowerQuery)); -}; diff --git a/actions/projects.ts b/actions/projects.ts deleted file mode 100644 index d6f685b63290792fc597438a53eab0a14d6bbf8e..0000000000000000000000000000000000000000 --- a/actions/projects.ts +++ /dev/null @@ -1,175 +0,0 @@ -"use server"; -import { - downloadFile, - listCommits, - listFiles, - listSpaces, - RepoDesignation, - SpaceEntry, - spaceInfo, -} from "@huggingface/hub"; - -import { auth } from "@/lib/auth"; -import { Commit, File } from "@/lib/type"; - -export interface ProjectWithCommits extends SpaceEntry { - commits?: Commit[]; - medias?: string[]; -} - -const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"]; -const IGNORED_FORMATS = [ - ".png", - ".jpg", - ".jpeg", - ".gif", - ".svg", - ".webp", - ".mp4", - ".mp3", - ".wav", -]; - -export const getProjects = async () => { - const projects: SpaceEntry[] = []; - const session = await auth(); - if (!session?.user) { - return projects; - } - const token = session.accessToken; - for await (const space of listSpaces({ - accessToken: token, - additionalFields: ["author", "cardData"], - search: { - owner: session.user.username, - }, - })) { - if ( - space.sdk === "static" && - Array.isArray((space.cardData as { tags?: string[] })?.tags) && - (space.cardData as { tags?: string[] })?.tags?.some((tag) => - tag.includes("deepsite") - ) - ) { - projects.push(space); - } - } - return projects; -}; -export const getProject = async (id: string, commitId?: string) => { - const session = await auth(); - if (!session?.user) { - return null; - } - const token = session.accessToken; - try { - const project: ProjectWithCommits | null = await spaceInfo({ - name: id, - accessToken: token, - additionalFields: ["author", "cardData"], - }); - const repo: RepoDesignation = { - type: "space", - name: id, - }; - const files: File[] = []; - const medias: string[] = []; - const params = { repo, accessToken: token }; - if (commitId) { - Object.assign(params, { revision: commitId }); - } - for await (const fileInfo of listFiles(params)) { - if (IGNORED_PATHS.includes(fileInfo.path)) continue; - if (IGNORED_FORMATS.some((format) => fileInfo.path.endsWith(format))) { - medias.push( - `https://huggingface.co/spaces/${id}/resolve/main/${fileInfo.path}` - ); - continue; - } - - if (fileInfo.type === "directory") { - for await (const subFile of listFiles({ - repo, - accessToken: token, - path: fileInfo.path, - })) { - if (IGNORED_FORMATS.some((format) => subFile.path.endsWith(format))) { - medias.push( - `https://huggingface.co/spaces/${id}/resolve/main/${subFile.path}` - ); - } - const blob = await downloadFile({ - repo, - accessToken: token, - path: subFile.path, - raw: true, - ...(commitId ? { revision: commitId } : {}), - }).catch((_) => { - return null; - }); - if (!blob) { - continue; - } - const html = await blob?.text(); - if (!html) { - continue; - } - files[subFile.path === "index.html" ? "unshift" : "push"]({ - path: subFile.path, - content: html, - }); - } - } else { - const blob = await downloadFile({ - repo, - accessToken: token, - path: fileInfo.path, - raw: true, - ...(commitId ? { revision: commitId } : {}), - }).catch((_) => { - return null; - }); - if (!blob) { - continue; - } - const html = await blob?.text(); - if (!html) { - continue; - } - files[fileInfo.path === "index.html" ? "unshift" : "push"]({ - path: fileInfo.path, - content: html, - }); - } - } - const commits: Commit[] = []; - const commitIterator = listCommits({ repo, accessToken: token }); - for await (const commit of commitIterator) { - if ( - commit.title?.toLowerCase() === "initial commit" || - commit.title - ?.toLowerCase() - ?.includes("upload media files through deepsite") - ) - continue; - commits.push({ - title: commit.title, - oid: commit.oid, - date: commit.date, - }); - if (commits.length >= 20) { - break; - } - } - - project.commits = commits; - project.medias = medias; - - return { project, files }; - } catch (error) { - return { - project: null, - files: [], - }; - } -}; diff --git a/app/(public)/layout.tsx b/app/(public)/layout.tsx index 0eb2c8fbb85b745d5be01c9e79fa0ebc93a71d00..4a4ec57d2609c783602beb6c06c8dca6a1e6192d 100644 --- a/app/(public)/layout.tsx +++ b/app/(public)/layout.tsx @@ -1,12 +1,13 @@ -import { Navigation } from "@/components/public/navigation"; +import Navigation from "@/components/public/navigation"; -export default function PublicLayout({ +export default async function PublicLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( -
+
+
{children}
diff --git a/app/(public)/page.tsx b/app/(public)/page.tsx index 86970b7a3dab7e60cce3bbb68c747b525baba45c..c0849e72cf29027524ec9ebc3818e80a8aee5ef3 100644 --- a/app/(public)/page.tsx +++ b/app/(public)/page.tsx @@ -1,25 +1,44 @@ -import { AnimatedDotsBackground } from "@/components/public/animated-dots-background"; -import { HeroHeader } from "@/components/public/hero-header"; -import { UserProjects } from "@/components/projects/user-projects"; -import { AskAiLanding } from "@/components/ask-ai/ask-ai-landing"; -import { Bento } from "@/components/public/bento"; - -export const dynamic = "force-dynamic"; - -export default async function Homepage() { +import { AskAi } from "@/components/space/ask-ai"; +import { redirect } from "next/navigation"; +export default function Home() { + redirect("/projects/new"); return ( <> -
- -
- +
+
+ ✨ DeepSite Public Beta
-
- +

+ Code your website with AI in seconds +

+

+ Vibe Coding has never been so easy. +

+
+
-
- - +
+
+
+
+
+
+ +
+

+ Community Driven +

+
+
+

+ Deploy your website in seconds +

+
+
+

+ Features that make you smile +

+
); } diff --git a/app/(public)/projects/page.tsx b/app/(public)/projects/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..374dc6b1194256c5b142a62168ce0f414f6098be --- /dev/null +++ b/app/(public)/projects/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from "next/navigation"; + +import { MyProjects } from "@/components/my-projects"; +import { getProjects } from "@/app/actions/projects"; + +export default async function ProjectsPage() { + const { ok, projects } = await getProjects(); + if (!ok) { + redirect("/"); + } + + return ; +} diff --git a/app/(public)/signin/page.tsx b/app/(public)/signin/page.tsx deleted file mode 100644 index 3200d0755372c79be57b9204331695cd8f0499ac..0000000000000000000000000000000000000000 --- a/app/(public)/signin/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { LoginButtons } from "@/components/login/login-buttons"; - -export default async function SignInPage({ - searchParams, -}: { - searchParams: Promise<{ callbackUrl: string }>; -}) { - const { callbackUrl } = await searchParams; - console.log(callbackUrl); - return ( -
-
-

You shall not pass 🧙

-

- You can't access this resource without being signed in. -

- -
-
- ); -} diff --git a/app/[owner]/[repoId]/page.tsx b/app/[owner]/[repoId]/page.tsx deleted file mode 100644 index 5082396059ae126dbc2be8e0daf30d1ff014f922..0000000000000000000000000000000000000000 --- a/app/[owner]/[repoId]/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { getProject } from "@/actions/projects"; -import { AppEditor } from "@/components/editor"; -import { auth } from "@/lib/auth"; -import { notFound, redirect } from "next/navigation"; - -export default async function ProjectPage({ - params, - searchParams, -}: { - params: Promise<{ owner: string; repoId: string }>; - searchParams: Promise<{ commit?: string }>; -}) { - const session = await auth(); - - const { owner, repoId } = await params; - const { commit } = await searchParams; - if (!session) { - redirect( - `/api/auth/signin?callbackUrl=/${owner}/${repoId}${ - commit ? `?commit=${commit}` : "" - }` - ); - } - const datas = await getProject(`${owner}/${repoId}`, commit); - if (!datas?.project) { - return notFound(); - } - return ( - - ); -} diff --git a/app/actions/auth.ts b/app/actions/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..a343e65e6726b35b32f022c117d3f3b5187d78e6 --- /dev/null +++ b/app/actions/auth.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/app/actions/projects.ts b/app/actions/projects.ts new file mode 100644 index 0000000000000000000000000000000000000000..209b16d9d9960eeafb9e0b02d7b1b3eda638338d --- /dev/null +++ b/app/actions/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/app/api/ask-ai/route.ts b/app/api/ask-ai/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbe6cbeed1dbbc625bdb7cc4bd033912e717d4b5 --- /dev/null +++ b/app/api/ask-ai/route.ts @@ -0,0 +1,440 @@ +/* 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, + REPLACE_END, + SEARCH_START, +} from "@/lib/prompts"; +import MY_TOKEN_KEY from "@/lib/get-cookie-name"; +import { createErrorFixPrompt } from "@/lib/error-formatter"; + +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, html } = 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; + + try { + // Create a stream response + const encoder = new TextEncoder(); + const stream = new TransformStream(); + const writer = stream.writable.getWriter(); + + // Start the response + 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, + }, + { + 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.` + : html + ? `Here is my current HTML code:\n\n\`\`\`html\n${html}\n\`\`\`\n\nNow, please create a new design based on this HTML.` + : prompt, + }, + ], + 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) { + let newChunk = chunk; + if (!selectedModel?.isThinker) { + if (provider !== "sambanova") { + await writer.write(encoder.encode(chunk)); + completeResponse += chunk; + + if (completeResponse.includes("")) { + break; + } + } else { + if (chunk.includes("")) { + newChunk = newChunk.replace(/<\/html>[\s\S]*/, ""); + } + completeResponse += newChunk; + await writer.write(encoder.encode(newChunk)); + if (newChunk.includes("")) { + break; + } + } + } else { + const lastThinkTagIndex = + completeResponse.lastIndexOf(""); + completeResponse += newChunk; + await writer.write(encoder.encode(newChunk)); + if (lastThinkTagIndex !== -1) { + const afterLastThinkTag = completeResponse.slice( + lastThinkTagIndex + "".length + ); + if (afterLastThinkTag.includes("")) { + break; + } + } + } + } + } + } 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, + html, + previousPrompt, + provider, + selectedElementHtml, + model, + errors, + } = body; + + if (!prompt || !html) { + 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 (process.env.NODE_ENV !== "production") { + // Sanitize error messages to prevent log injection + const errorCount = errors && Array.isArray(errors) ? errors.length : 0; + console.log( + `[PUT /api/ask-ai] Model: ${selectedModel.label}, Provider: ${provider}${ + errorCount > 0 ? `, Fixing ${errorCount} errors` : "" + }` + ); + } + + 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: previousPrompt + ? previousPrompt + : "You are modifying the HTML file based on the user's request.", + }, + { + role: "assistant", + + content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${ + selectedElementHtml + ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\`` + : "" + }`, + }, + { + role: "user", + content: + errors && errors.length > 0 + ? createErrorFixPrompt(errors, html) + : 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 = html; + 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; + } + + return NextResponse.json({ + ok: true, + html: newHtml, + updatedLines, + }); + } 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/app/api/ask/route.ts b/app/api/ask/route.ts deleted file mode 100644 index 293e8244308cc1191e32b3ef6b30e93ccb963fcf..0000000000000000000000000000000000000000 --- a/app/api/ask/route.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { NextResponse } from "next/server"; -import { InferenceClient } from "@huggingface/inference"; - -import { FOLLOW_UP_SYSTEM_PROMPT, INITIAL_SYSTEM_PROMPT } from "@/lib/prompts"; -import { auth } from "@/lib/auth"; -import { File, Message } from "@/lib/type"; -import { DEFAULT_MODEL, MODELS } from "@/lib/providers"; - -export async function POST(request: Request) { - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const body = await request.json(); - const { - prompt, - previousMessages = [], - files = [], - provider, - model, - redesignMd, - medias, - } = body; - - if (!prompt) { - return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); - } - if (!model || !MODELS.find((m: (typeof MODELS)[0]) => m.value === model)) { - return NextResponse.json({ error: "Model is required" }, { status: 400 }); - } - - const client = new InferenceClient(token); - - 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 hasRetried = false; - let currentModel = model; - - const tryGeneration = async (): Promise => { - try { - const chatCompletion = client.chatCompletionStream({ - model: currentModel + (provider !== "auto" ? `:${provider}` : ""), - messages: [ - { - role: "system", - content: - files.length > 0 - ? FOLLOW_UP_SYSTEM_PROMPT - : INITIAL_SYSTEM_PROMPT, - }, - ...previousMessages.map((message: Message) => ({ - role: message.role, - content: message.content, - })), - ...(files?.length > 0 - ? [ - { - role: "user", - content: `Here are the files that the user has provider:${files - .map( - (file: File) => - `File: ${file.path}\nContent: ${file.content}` - ) - .join("\n")}\n\n${prompt}`, - }, - ] - : []), - { - role: "user", - content: `${ - redesignMd?.url && - `Redesign the following website ${redesignMd.url}, try to use the same images and content, but you can still improve it if needed. Do the best version possibile. Here is the markdown:\n ${redesignMd.md} \n\n` - }${prompt} ${ - medias && medias.length > 0 - ? `\nHere is the list of my media files: ${medias.join( - ", " - )}\n` - : "" - }`, - } - ], - stream: true, - max_tokens: 16_000, - }); - 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)); - } - } - - await writer.close(); - } catch (error) { - const errorMessage = - error instanceof Error - ? error.message - : "An error occurred while processing your request"; - - if ( - !hasRetried && - errorMessage?.includes( - "Failed to perform inference: Model not found" - ) - ) { - hasRetried = true; - if (model === DEFAULT_MODEL) { - const availableFallbackModels = MODELS.filter( - (m) => m.value !== model - ); - const randomIndex = Math.floor( - Math.random() * availableFallbackModels.length - ); - currentModel = availableFallbackModels[randomIndex]; - } else { - currentModel = DEFAULT_MODEL; - } - const switchMessage = `\n\n_Note: The selected model was not available. Switched to \`${currentModel}\`._\n\n`; - await writer.write(encoder.encode(switchMessage)); - - return tryGeneration(); - } - - try { - let errorPayload = ""; - if ( - errorMessage?.includes("exceeded your monthly included credits") || - errorMessage?.includes("reached the free monthly usage limit") - ) { - errorPayload = JSON.stringify({ - messageError: errorMessage, - showProMessage: true, - isError: true, - }); - } else { - errorPayload = JSON.stringify({ - messageError: errorMessage, - isError: true, - }); - } - await writer.write(encoder.encode(`\n\n__ERROR__:${errorPayload}`)); - await writer.close(); - } catch (closeError) { - console.error("Failed to send error message:", closeError); - try { - await writer.abort(error); - } catch (abortError) { - console.error("Failed to abort writer:", abortError); - } - } - } - }; - - await tryGeneration(); - })(); - - return response; - } catch (error) { - return NextResponse.json( - { - error: error instanceof Error ? error.message : "Internal Server Error", - }, - { status: 500 } - ); - } -} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 7b38c1bb45e38527a2c595c07df5e70d38ada7d9..0000000000000000000000000000000000000000 --- a/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import NextAuth from "next-auth"; -import { authOptions } from "@/lib/auth"; - -const handler = NextAuth(authOptions); - -export { handler as GET, handler as POST }; diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..221e0c266bd2e6673779b9e81caf333243147f60 --- /dev/null +++ b/app/api/auth/route.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/app/api/healthcheck/route.ts b/app/api/healthcheck/route.ts deleted file mode 100644 index 35adb415d0f577a7a78dbc338bd6450e78cc9ec3..0000000000000000000000000000000000000000 --- a/app/api/healthcheck/route.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextResponse } from "next/server"; - -export async function GET() { - return NextResponse.json({ status: "ok" }, { status: 200 }); -} diff --git a/app/api/me/projects/[namespace]/[repoId]/route.ts b/app/api/me/projects/[namespace]/[repoId]/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b5bb19a9fe7a91d48b8b8cbcf340f1508032baf --- /dev/null +++ b/app/api/me/projects/[namespace]/[repoId]/route.ts @@ -0,0 +1,237 @@ +import { NextRequest, NextResponse } from "next/server"; +import { RepoDesignation, spaceInfo, uploadFile } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import Project from "@/models/Project"; +import dbConnect from "@/lib/mongodb"; +import { getPTag } from "@/lib/utils"; + +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 } + ); + } + const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`; + 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 response = await fetch(space_url); + if (!response.ok) { + return NextResponse.json( + { + ok: false, + error: "Failed to fetch space HTML", + }, + { status: 404 } + ); + } + let html = await response.text(); + // remove the last p tag including this url https://enzostvs-deepsite.hf.space + html = html.replace(getPTag(namespace + "/" + repoId), ""); + + return NextResponse.json( + { + project: { + ...project, + html, + }, + 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 { html, 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 newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}`); + const file = new File([newHtml], "index.html", { type: "text/html" }); + await uploadFile({ + repo, + file, + 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: [ + ...(project && "prompts" in project ? project.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/app/api/me/projects/route.ts b/app/api/me/projects/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..9899bc2533a40a28b9e35cfe123b9868c318e19d --- /dev/null +++ b/app/api/me/projects/route.ts @@ -0,0 +1,126 @@ +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, getPTag } from "@/lib/utils"; +// import type user +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 } + ); +} + +/** + * This API route creates a new project in Hugging Face Spaces. + * It requires an Authorization header with a valid token and a JSON body with the project details. + */ +export async function POST(request: NextRequest) { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + const { title, html, prompts } = await request.json(); + + if (!title || !html) { + return NextResponse.json( + { message: "Title and HTML content are required.", ok: false }, + { status: 400 } + ); + } + + await dbConnect(); + + try { + let readme = ""; + let newHtml = html; + + 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`; + + newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}`); + const file = new File([newHtml], "index.html", { type: "text/html" }); + const readmeFile = new File([readme], "README.md", { + type: "text/markdown", + }); + const files = [file, readmeFile]; + 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/app/api/me/route.ts b/app/api/me/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4164daba5c58bb2fe7f4f7508de7165f32ca443 --- /dev/null +++ b/app/api/me/route.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/app/api/projects/[repoId]/[commitId]/route.ts b/app/api/projects/[repoId]/[commitId]/route.ts deleted file mode 100644 index 0362d6c43ec226bf97837f2f88196ad43eb056f2..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/[commitId]/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { auth } from "@/lib/auth"; -import { createBranch, RepoDesignation } from "@huggingface/hub"; -import { format } from "date-fns"; -import { NextResponse } from "next/server"; - -export async function POST( - request: Request, - { params }: { params: Promise<{ repoId: string; commitId: string }> } -) { - const { repoId, commitId }: { repoId: string; commitId: string } = - await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - const commitTitle = `🔖 ${format(new Date(), "dd/MM")} - ${format( - new Date(), - "HH:mm" - )} - Set commit ${commitId} as default.`; - - await fetch( - `https://huggingface.co/api/spaces/${session.user?.username}/${repoId}/branch/main`, - { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - startingPoint: commitId, - overwrite: true, - }), - } - ).catch((error) => { - return NextResponse.json( - { error: error ?? "Failed to create branch" }, - { status: 500 } - ); - }); - - return NextResponse.json({ success: true }, { status: 200 }); -} diff --git a/app/api/projects/[repoId]/download/route.ts b/app/api/projects/[repoId]/download/route.ts deleted file mode 100644 index 43e8f6638d5eb2aca8ba3069d696b05056d96633..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/download/route.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { auth } from "@/lib/auth"; -import { downloadFile, listFiles, RepoDesignation } from "@huggingface/hub"; -import { NextResponse } from "next/server"; -import JSZip from "jszip"; - -export async function GET( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - try { - const zip = new JSZip(); - for await (const fileInfo of listFiles({ - repo, - accessToken: token as string, - recursive: true, - })) { - if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) { - continue; - } - - try { - const blob = await downloadFile({ - repo, - accessToken: token as string, - path: fileInfo.path, - raw: true - }).catch((error) => { - return null; - }); - if (!blob) { - continue; - } - - if (blob) { - const arrayBuffer = await blob.arrayBuffer(); - zip.file(fileInfo.path, arrayBuffer); - } - } catch (error) { - console.error(`Error downloading file ${fileInfo.path}:`, error); - } - } - - const zipBlob = await zip.generateAsync({ - type: "blob", - compression: "DEFLATE", - compressionOptions: { - level: 6 - } - }); - - const projectName = `${session.user?.username}-${repoId}`.replace(/[^a-zA-Z0-9-_]/g, '_'); - const filename = `${projectName}.zip`; - - return new NextResponse(zipBlob, { - headers: { - "Content-Type": "application/zip", - "Content-Disposition": `attachment; filename="${filename}"`, - "Content-Length": zipBlob.size.toString(), - }, - }); - } catch (error) { - console.error("Error downloading project:", error); - return NextResponse.json({ error: "Failed to download project" }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/projects/[repoId]/medias/route.ts b/app/api/projects/[repoId]/medias/route.ts deleted file mode 100644 index 2040623536ab6775152177a317e0b11dc6cae3a2..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/medias/route.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { auth } from "@/lib/auth"; -import { RepoDesignation, uploadFiles } from "@huggingface/hub"; -import { NextResponse } from "next/server"; - -export async function POST( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - const formData = await request.formData(); - const newMedias = formData.getAll("images") as File[]; - - const filesToUpload: File[] = []; - - if (!newMedias || newMedias.length === 0) { - return NextResponse.json( - { - ok: false, - error: "At least one media file is required under the 'images' key", - }, - { status: 400 } - ); - } - - try { - for (const media of newMedias) { - const isImage = media.type.startsWith("image/"); - const isVideo = media.type.startsWith("video/"); - const isAudio = media.type.startsWith("audio/"); - - const folderPath = isImage - ? "images/" - : isVideo - ? "videos/" - : isAudio - ? "audios/" - : null; - - if (!folderPath) { - return NextResponse.json( - { ok: false, error: "Unsupported media type: " + media.type }, - { status: 400 } - ); - } - - const mediaName = `${folderPath}${media.name}`; - const processedFile = new File([media], mediaName, { type: media.type }); - filesToUpload.push(processedFile); - } - - await uploadFiles({ - repo, - files: filesToUpload, - accessToken: token, - commitTitle: `📁 Upload media files through DeepSite`, - }); - - return NextResponse.json( - { - success: true, - medias: filesToUpload.map( - (file) => - `https://huggingface.co/spaces/${session.user?.username}/${repoId}/resolve/main/${file.name}` - ), - }, - { status: 200 } - ); - } catch (error) { - return NextResponse.json( - { ok: false, error: error ?? "Failed to upload media files" }, - { status: 500 } - ); - } - - return NextResponse.json({ success: true }, { status: 200 }); -} diff --git a/app/api/projects/[repoId]/rename/route.ts b/app/api/projects/[repoId]/rename/route.ts deleted file mode 100644 index 1d3a00b676effdc7d9106c02e6d7ec40384026c9..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/rename/route.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { auth } from "@/lib/auth"; -import { downloadFile, RepoDesignation, uploadFile } from "@huggingface/hub"; -import { format } from "date-fns"; -import { NextResponse } from "next/server"; - -export async function PUT( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const body = await request.json(); - const { newTitle } = body; - - if (!newTitle) { - return NextResponse.json( - { error: "newTitle is required" }, - { status: 400 } - ); - } - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - const blob = await downloadFile({ - repo, - accessToken: token, - path: "README.md", - raw: true, - }).catch((_) => { - return null; - }); - - if (!blob) { - return NextResponse.json( - { error: "Could not fetch README.md" }, - { status: 500 } - ); - } - - const readmeFile = await blob?.text(); - if (!readmeFile) { - return NextResponse.json( - { error: "Could not read README.md content" }, - { status: 500 } - ); - } - - // Escape YAML values to prevent injection attacks - const escapeYamlValue = (value: string): string => { - if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) { - return `"${value.replace(/"/g, '\\"')}"`; - } - return value; - }; - - // Escape commit message to prevent injection - const escapeCommitMessage = (message: string): string => { - return message.replace(/[\r\n]/g, " ").slice(0, 200); - }; - - const updatedReadmeFile = readmeFile.replace( - /^title:\s*(.*)$/m, - `title: ${escapeYamlValue(newTitle)}` - ); - - await uploadFile({ - repo, - accessToken: token, - file: new File([updatedReadmeFile], "README.md", { type: "text/markdown" }), - commitTitle: escapeCommitMessage( - `🐳 ${format(new Date(), "dd/MM")} - ${format( - new Date(), - "HH:mm" - )} - Rename project to "${newTitle}"` - ), - }); - - return NextResponse.json( - { - success: true, - }, - { status: 200 } - ); -} diff --git a/app/api/projects/[repoId]/route.ts b/app/api/projects/[repoId]/route.ts deleted file mode 100644 index e6daee03652b083d7f1d852f7367d7f55fd3f390..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/route.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { auth } from "@/lib/auth"; -import { RepoDesignation, deleteRepo, uploadFiles } from "@huggingface/hub"; -import { format } from "date-fns"; -import { NextResponse } from "next/server"; - -export async function PUT( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const body = await request.json(); - const { files, prompt, isManualChanges } = body; - - if (!files) { - return NextResponse.json({ error: "Files are required" }, { status: 400 }); - } - - if (!prompt) { - return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); - } - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - const filesToUpload: File[] = []; - for (const file of files) { - let mimeType = "text/x-python"; - if (file.path.endsWith(".txt")) { - mimeType = "text/plain"; - } else if (file.path.endsWith(".md")) { - mimeType = "text/markdown"; - } else if (file.path.endsWith(".json")) { - mimeType = "application/json"; - } - filesToUpload.push(new File([file.content], file.path, { type: mimeType })); - } - // Escape commit title to prevent injection - const escapeCommitTitle = (title: string): string => { - return title.replace(/[\r\n]/g, " ").slice(0, 200); - }; - - const baseTitle = isManualChanges - ? "" - : `🐳 ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `; - const commitTitle = escapeCommitTitle( - baseTitle + (prompt ?? "Follow-up DeepSite commit") - ); - const response = await uploadFiles({ - repo, - files: filesToUpload, - accessToken: token, - commitTitle, - }); - - return NextResponse.json( - { - success: true, - commit: { - oid: response.commit, - title: commitTitle, - date: new Date(), - }, - }, - { status: 200 } - ); -} - -export async function DELETE( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - try { - await deleteRepo({ - repo, - accessToken: token as string, - }); - - return NextResponse.json({ success: true }, { status: 200 }); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : "Failed to delete project"; - return NextResponse.json({ error: errMsg }, { status: 500 }); - } -} diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts deleted file mode 100644 index b70c310b4167077d4d8bcd15423defb1d158e2dd..0000000000000000000000000000000000000000 --- a/app/api/projects/route.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { NextResponse } from "next/server"; -import { RepoDesignation, createRepo, uploadFiles } from "@huggingface/hub"; - -import { auth } from "@/lib/auth"; -import { - COLORS, - EMOJIS_FOR_SPACE, - injectDeepSiteBadge, - isIndexPage, -} from "@/lib/utils"; - -// todo: catch error while publishing project, and return the error to the user -// if space has been created, but can't push, try again or catch well the error and return the error to the user - -export async function POST(request: Request) { - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const body = await request.json(); - const { projectTitle, files, prompt } = body; - - if (!files) { - return NextResponse.json( - { error: "Project title and files are required" }, - { status: 400 } - ); - } - - const title = - projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project"; - - let formattedTitle = title - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .split("-") - .filter(Boolean) - .join("-") - .slice(0, 75); - - formattedTitle = - formattedTitle + "-" + Math.random().toString(36).substring(2, 7); - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + formattedTitle, - }; - - // Escape YAML values to prevent injection attacks - const escapeYamlValue = (value: string): string => { - if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) { - return `"${value.replace(/"/g, '\\"')}"`; - } - return value; - }; - - // Escape markdown headers to prevent injection - const escapeMarkdownHeader = (value: string): string => { - return value.replace(/^#+\s*/g, "").replace(/\n/g, " "); - }; - - const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)]; - const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)]; - const emoji = - EMOJIS_FOR_SPACE[Math.floor(Math.random() * EMOJIS_FOR_SPACE.length)]; - const README = `--- -title: ${escapeYamlValue(projectTitle)} -colorFrom: ${colorFrom} -colorTo: ${colorTo} -sdk: static -emoji: ${emoji} -tags: - - deepsite-v4 ---- - -# ${escapeMarkdownHeader(title)} - -This project has been created with [DeepSite](https://deepsite.hf.co) AI Vibe Coding. -`; - - const filesToUpload: File[] = [ - new File([README], "README.md", { type: "text/markdown" }), - ]; - for (const file of files) { - let mimeType = "text/html"; - if (file.path.endsWith(".css")) { - mimeType = "text/css"; - } else if (file.path.endsWith(".js")) { - mimeType = "text/javascript"; - } - const content = - mimeType === "text/html" && isIndexPage(file.path) - ? injectDeepSiteBadge(file.content) - : file.content; - - filesToUpload.push(new File([content], file.path, { type: mimeType })); - } - - let repoUrl: string | undefined; - - try { - // Create the space first - const createResult = await createRepo({ - accessToken: token as string, - repo: repo, - sdk: "static", - }); - repoUrl = createResult.repoUrl; - - // Escape commit message to prevent injection - const escapeCommitMessage = (message: string): string => { - return message.replace(/[\r\n]/g, " ").slice(0, 200); - }; - const commitMessage = escapeCommitMessage(prompt ?? "Initial DeepSite commit"); - - // Upload files to the created space - await uploadFiles({ - repo, - files: filesToUpload, - accessToken: token as string, - commitTitle: commitMessage, - }); - - const path = repoUrl.split("/").slice(-2).join("/"); - - return NextResponse.json({ repoUrl: path }, { status: 200 }); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : "Failed to create or upload to space"; - - // If space was created but upload failed, include the repo URL in the error - if (repoUrl) { - const path = repoUrl.split("/").slice(-2).join("/"); - return NextResponse.json({ - error: `${errMsg}. Space was created at ${path} but files could not be uploaded.`, - repoUrl: path, - partialSuccess: true - }, { status: 500 }); - } - - return NextResponse.json({ error: errMsg }, { status: 500 }); - } -} diff --git a/app/api/re-design/route.ts b/app/api/re-design/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..777c2cbd18f95592080c0fe1ad2cab23a5264397 --- /dev/null +++ b/app/api/re-design/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/app/api/redesign/route.ts b/app/api/redesign/route.ts deleted file mode 100644 index 6b898d6fd364c5f3ef267706b62c37ee559e19cd..0000000000000000000000000000000000000000 --- a/app/api/redesign/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { NextRequest, NextResponse } from "next/server"; - -const FETCH_TIMEOUT = 30_000; -export const maxDuration = 60; - -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 controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT); - - try { - const response = await fetch( - `https://r.jina.ai/${encodeURIComponent(url)}`, - { - method: "POST", - signal: controller.signal, - } - ); - - clearTimeout(timeoutId); - - 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 } - ); - } catch (fetchError: any) { - clearTimeout(timeoutId); - - if (fetchError.name === "AbortError") { - return NextResponse.json( - { - error: - "Request timeout: The external service took too long to respond. Please try again.", - }, - { status: 504 } - ); - } - throw fetchError; - } - } catch (error: any) { - if (error.name === "AbortError" || error.message?.includes("timeout")) { - return NextResponse.json( - { - error: - "Request timeout: The external service took too long to respond. Please try again.", - }, - { status: 504 } - ); - } - return NextResponse.json( - { error: error.message || "An error occurred" }, - { status: 500 } - ); - } -} diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ac44f365b707908d6003a4266db1269d297a4336 --- /dev/null +++ b/app/auth/callback/page.tsx @@ -0,0 +1,72 @@ +"use client"; +import Link from "next/link"; +import { useUser } from "@/hooks/useUser"; +import { use, useState } from "react"; +import { useMount, useTimeoutFn } from "react-use"; + +import { Button } from "@/components/ui/button"; +export default function AuthCallback({ + searchParams, +}: { + searchParams: Promise<{ code: string }>; +}) { + const [showButton, setShowButton] = useState(false); + const { code } = use(searchParams); + const { loginFromCode } = useUser(); + + useMount(async () => { + if (code) { + await loginFromCode(code); + } + }); + + useTimeoutFn( + () => setShowButton(true), + 7000 // Show button after 5 seconds + ); + + return ( +
+
+
+
+
+ 🚀 +
+
+ 👋 +
+
+ 🙌 +
+
+

+ Login In Progress... +

+

+ Wait a moment while we log you in with your code. +

+
+
+
+

+ If you are not redirected automatically in the next 5 seconds, + please click the button below +

+ {showButton ? ( + + + + ) : ( +

+ Please wait, we are logging you in... +

+ )} +
+
+
+
+ ); +} diff --git a/app/auth/page.tsx b/app/auth/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a45a6bc6f58907b4ee5efbf0f70a51ee153625c7 --- /dev/null +++ b/app/auth/page.tsx @@ -0,0 +1,28 @@ +import { redirect } from "next/navigation"; +import { Metadata } from "next"; + +import { getAuth } from "@/app/actions/auth"; + +export const revalidate = 1; + +export const metadata: Metadata = { + robots: "noindex, nofollow", +}; + +export default async function Auth() { + const loginRedirectUrl = await getAuth(); + if (loginRedirectUrl) { + redirect(loginRedirectUrl); + } + + return ( +
+
+

Error

+

+ An error occurred while trying to log in. Please try again later. +

+
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 469b6844c7b63d85ad5e9c7ba420add2f7f1125c..09fe16906a16a6a8bff4a21d55004968eff2b163 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,33 +1,53 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import type { Metadata, Viewport } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import { NextStepProvider } from "nextstepjs"; -import Script from "next/script"; +import { Inter, PT_Sans } from "next/font/google"; +import { cookies } from "next/headers"; -import "@/app/globals.css"; -import { ThemeProvider } from "@/components/providers/theme"; -import { AuthProvider } from "@/components/providers/session"; +import TanstackProvider from "@/components/providers/tanstack-query-provider"; +import "@/assets/globals.css"; import { Toaster } from "@/components/ui/sonner"; -import { ReactQueryProvider } from "@/components/providers/react-query"; -import { generateSEO, generateStructuredData } from "@/lib/seo"; -import { NotAuthorizedDomain } from "@/components/not-authorized"; +import MY_TOKEN_KEY from "@/lib/get-cookie-name"; +import { apiServer } from "@/lib/api"; +import AppContext from "@/components/contexts/app-context"; +import Script from "next/script"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const inter = Inter({ + variable: "--font-inter-sans", subsets: ["latin"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const ptSans = PT_Sans({ + variable: "--font-ptSans-mono", subsets: ["latin"], + weight: ["400", "700"], }); export const metadata: Metadata = { - ...generateSEO({ + title: "DeepSite | Build with AI ✨", + description: + "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.", + openGraph: { + title: "DeepSite | Build with AI ✨", + description: + "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.", + url: "https://deepsite.hf.co", + siteName: "DeepSite", + images: [ + { + url: "https://deepsite.hf.co/banner.png", + width: 1200, + height: 630, + alt: "DeepSite Open Graph Image", + }, + ], + }, + twitter: { + card: "summary_large_image", title: "DeepSite | Build with AI ✨", description: "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.", - path: "/", - }), + images: ["https://deepsite.hf.co/banner.png"], + }, appleWebApp: { capable: true, title: "DeepSite", @@ -38,70 +58,50 @@ export const metadata: Metadata = { shortcut: "/logo.svg", apple: "/logo.svg", }, - verification: { - google: process.env.GOOGLE_SITE_VERIFICATION, - }, }; export const viewport: Viewport = { initialScale: 1, maximumScale: 1, - themeColor: "#4f46e5", + themeColor: "#000000", }; +async function getMe() { + const cookieStore = await cookies(); + const token = cookieStore.get(MY_TOKEN_KEY())?.value; + if (!token) return { user: null, errCode: null }; + try { + const res = await apiServer.get("/me", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return { user: res.data.user, errCode: null }; + } catch (err: any) { + return { user: null, errCode: err.status }; + } +} + export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - const structuredData = generateStructuredData("WebApplication", { - name: "DeepSite", - description: "Build websites with AI, no code required", - url: "https://deepsite.hf.co", - }); - const organizationData = generateStructuredData("Organization", { - name: "DeepSite", - url: "https://deepsite.hf.co", - }); - + const data = await getMe(); return ( - + + - `, + "gi", + ), + "", + ); + + // Create the script tag with proper ID + const scriptTag = ``; + + // If html already has a tag, inject script before it + if (cleanedHtml.includes("")) { + return cleanedHtml.replace("", `${scriptTag}`); + } + // If html already has a tag, inject script after it + else if (cleanedHtml.includes("]*)>/, `${scriptTag}`); + } + // Otherwise, wrap the content with proper HTML structure + else { + return `${scriptTag}${cleanedHtml}`; + } + }, [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/components/not-authorized.tsx b/components/not-authorized.tsx deleted file mode 100644 index 574e252b5e74f88bd99afaf131160e035b739097..0000000000000000000000000000000000000000 --- a/components/not-authorized.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; -import { useState } from "react"; -import Image from "next/image"; -import { Button } from "./ui/button"; -import Link from "next/link"; -import { useMount } from "react-use"; - -import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"; - -export const NotAuthorizedDomain = () => { - const [open, setOpen] = useState(false); - - useMount(() => { - const isAllowedDomain = (hostname: string) => { - const host = hostname.toLowerCase(); - return ( - host.endsWith(".huggingface.co") || - host.endsWith(".hf.co") || - host === "huggingface.co" || - host === "hf.co" || - host === "enzostvs-deepsite.hf.space" || - host === "deepsite.hf.co" - ); - }; - - const isInIframe = () => { - try { - return window.self !== window.top; - } catch { - return true; - } - }; - - const isEmbedded = () => { - try { - return window.location !== window.parent.location; - } catch { - return true; - } - }; - - const addCanonicalUrl = () => { - const existingCanonical = document.querySelector('link[rel="canonical"]'); - if (existingCanonical) { - existingCanonical.remove(); - } - - const canonical = document.createElement("link"); - canonical.rel = "canonical"; - canonical.href = window.location.href; - document.head.appendChild(canonical); - - if (isInIframe() || isEmbedded()) { - try { - const parentHostname = document.referrer - ? new URL(document.referrer).hostname - : null; - if (parentHostname && !isAllowedDomain(parentHostname)) { - const noIndexMeta = document.createElement("meta"); - noIndexMeta.name = "robots"; - noIndexMeta.content = "noindex, nofollow"; - document.head.appendChild(noIndexMeta); - } - } catch (error) { - console.debug( - "SEO: Could not determine parent domain for iframe indexing rules" - ); - } - } - }; - - const shouldShowWarning = () => { - if (!isInIframe() && !isEmbedded()) { - return false; // Not in an iframe - } - - try { - const parentHostname = window.parent.location.hostname; - return !isAllowedDomain(parentHostname); - } catch { - try { - if (document.referrer) { - const referrerUrl = new URL(document.referrer); - return !isAllowedDomain(referrerUrl.hostname); - } - } catch {} - return true; - } - }; - - addCanonicalUrl(); - - if (shouldShowWarning()) { - setOpen(true); - } - }); - - return ( - - - -
- DeepSite -

Access Denied

-

- Unfortunately, you don't have access to DeepSite from this - domain:{" "} - {open && ( - - {window?.location?.hostname ?? "unknown domain"} - - )} - . -

- - - - -
-
-
- ); -}; diff --git a/components/not-found/buttons.tsx b/components/not-found/buttons.tsx deleted file mode 100644 index a30f436d48121858411d727b457abff9c3bb56e2..0000000000000000000000000000000000000000 --- a/components/not-found/buttons.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; - -export function NotFoundButtons() { - return ( -
- - - - -
- ); -} diff --git a/components/pro-modal/index.tsx b/components/pro-modal/index.tsx index 777cd9a5ce7dc1110ffc810d525e0c4a31adc1f9..b9bdb4a73bdfa203f9055c9f4b65fe65f7d6258a 100644 --- a/components/pro-modal/index.tsx +++ b/components/pro-modal/index.tsx @@ -1,26 +1,31 @@ +import { useLocalStorage } from "react-use"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { CheckCheck } from "lucide-react"; +import { isTheSameHtml } from "@/lib/compare-html-diff"; export const ProModal = ({ open, + html, onClose, }: { open: boolean; + html: string; onClose: React.Dispatch>; }) => { + const [, setStorage] = useLocalStorage("html_content"); const handleProClick = () => { + if (!isTheSameHtml(html)) { + setStorage(html); + } window.open("https://huggingface.co/subscribe/pro?from=DeepSite", "_blank"); onClose(false); }; return ( - + -
+
🚀 @@ -32,21 +37,19 @@ export const ProModal = ({ 🥳
-

+

Only $9 to enhance your possibilities

-

+

It seems like you have reached the monthly free limit of DeepSite.

-
-
-
-

+


+

Upgrade to a Account, and unlock your DeepSite high quota access ⚡

-