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..faa5ac0a3e9b0d6be03db8ed4b25adf1e47d878d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,16 @@ -FROM node:20-alpine -USER root - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@latest --activate +# FROM registry.hf.space/enzostvs-deepsite:cpu-bbc7882 +FROM node:22 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 the rest of the application files to the container COPY --chown=1000 . . -RUN pnpm install -RUN pnpm run build +RUN npm install && 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..7ccf31d982730ec847528e80004330ab71f11539 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,29 @@ --- -title: DeepSite v4 +title: DeepSite v3 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 by Vibe Coding models: - deepseek-ai/DeepSeek-V3-0324 - - deepseek-ai/DeepSeek-V3.2 - - Qwen/Qwen3-Coder-30B-A3B-Instruct + - deepseek-ai/DeepSeek-R1-0528 + - deepseek-ai/DeepSeek-V3.1 + - deepseek-ai/DeepSeek-V3.1-Terminus + - deepseek-ai/DeepSeek-V3.2-Exp + - Qwen/Qwen3-Coder-480B-A35B-Instruct + - moonshotai/Kimi-K2-Instruct - moonshotai/Kimi-K2-Instruct-0905 - - zai-org/GLM-4.7 - - MiniMaxAI/MiniMax-M2.1 + - zai-org/GLM-4.6 --- # 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. + +## How to use it locally + +Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74) \ No newline at end of file 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..4c640fb83ccfced8842764d029c87a2d85718d65 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..e2d5c88a4977801d6acabd904535b00ad622a267 100644 --- a/app/(public)/page.tsx +++ b/app/(public)/page.tsx @@ -1,25 +1,5 @@ -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"; +import { MyProjects } from "@/components/my-projects"; -export const dynamic = "force-dynamic"; - -export default async function Homepage() { - return ( - <> -
- -
- -
-
- -
-
- - - - ); +export default async function HomePage() { + 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/[namespace]/[repoId]/page.tsx b/app/[namespace]/[repoId]/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..19a09d72477dd437fa8a786601cbc584676609ee --- /dev/null +++ b/app/[namespace]/[repoId]/page.tsx @@ -0,0 +1,28 @@ +import { AppEditor } from "@/components/editor"; +import { generateSEO } from "@/lib/seo"; +import { Metadata } from "next"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ namespace: string; repoId: string }>; +}): Promise { + const { namespace, repoId } = await params; + + return generateSEO({ + title: `${namespace}/${repoId} - DeepSite Editor`, + description: `Edit and build ${namespace}/${repoId} with AI-powered tools on DeepSite. Create stunning websites with no code required.`, + path: `/${namespace}/${repoId}`, + // Prevent indexing of individual project editor pages if they contain sensitive content + noIndex: false, // Set to true if you want to keep project pages private + }); +} + +export default async function ProjectNamespacePage({ + params, +}: { + params: Promise<{ namespace: string; repoId: string }>; +}) { + const { namespace, repoId } = await params; + return ; +} 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..5f99d85273352e21e22efb85b03bced1cf210897 --- /dev/null +++ b/app/actions/projects.ts @@ -0,0 +1,47 @@ +"use server"; + +import { isAuthenticated } from "@/lib/auth"; +import { NextResponse } from "next/server"; +import { listSpaces } from "@huggingface/hub"; +import { ProjectType } from "@/types"; + +export async function getProjects(): Promise<{ + ok: boolean; + projects: ProjectType[]; + isEmpty?: boolean; +}> { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return { + ok: false, + projects: [], + }; + } + + const projects = []; + for await (const space of listSpaces({ + accessToken: user.token as string, + additionalFields: ["author", "cardData"], + search: { + owner: user.name, + } + })) { + if ( + !space.private && + space.sdk === "static" && + Array.isArray((space.cardData as { tags?: string[] })?.tags) && + ( + ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) || + ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite")) + ) + ) { + projects.push(space); + } + } + + return { + ok: true, + projects, + }; +} diff --git a/app/api/ask/route.ts b/app/api/ask/route.ts index 293e8244308cc1191e32b3ef6b30e93ccb963fcf..78e6d004cccdd543e1889ea447ae2d8d5b720715 100644 --- a/app/api/ask/route.ts +++ b/app/api/ask/route.ts @@ -1,37 +1,101 @@ +/* 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 { 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"; +import { MODELS } 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, + PROMPT_FOR_PROJECT_NAME, +} from "@/lib/prompts"; +import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens"; +import MY_TOKEN_KEY from "@/lib/get-cookie-name"; +import { Page } from "@/types"; +import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub"; +import { isAuthenticated } from "@/lib/auth"; +import { getBestProvider } from "@/lib/best-provider"; +// import { rewritePrompt } from "@/lib/rewrite-prompt"; +import { COLORS } from "@/lib/utils"; +import { templates } from "@/lib/templates"; -export async function POST(request: Request) { - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; +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, - previousMessages = [], - files = [], - provider, - model, - redesignMd, - medias, - } = body; - - if (!prompt) { - return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); + const { prompt, provider, model, redesignMarkdown, enhancedSettings, 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 (!model || !MODELS.find((m: (typeof MODELS)[0]) => m.value === model)) { - return NextResponse.json({ error: "Model is required" }, { status: 400 }); + + let token: string | null = null; + if (userToken) 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 client = new InferenceClient(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 selectedProvider = await getBestProvider(selectedModel.value, provider) + + let rewrittenPrompt = redesignMarkdown ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : prompt; + + if (enhancedSettings.isActive) { + // rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider); + } try { const encoder = new TextEncoder(); @@ -45,139 +109,607 @@ export async function POST(request: Request) { Connection: "keep-alive", }, }); - (async () => { - let hasRetried = false; - let currentModel = model; - const tryGeneration = async (): Promise => { - try { - const chatCompletion = client.chatCompletionStream({ - model: currentModel + (provider !== "auto" ? `:${provider}` : ""), + (async () => { + // let completeResponse = ""; + try { + const client = new InferenceClient(token); + + const systemPrompt = INITIAL_SYSTEM_PROMPT; + + const userPrompt = rewrittenPrompt; + const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt); + const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true); + const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens); + + const chatCompletion = client.chatCompletionStream( + { + model: selectedModel.value, + provider: selectedProvider.provider, messages: [ { role: "system", - content: - files.length > 0 - ? FOLLOW_UP_SYSTEM_PROMPT - : INITIAL_SYSTEM_PROMPT, + content: systemPrompt, }, - ...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` - : "" - }`, - } + content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500). +2. I want to use the following secondary color: ${enhancedSettings.secondaryColor} (eg: bg-${enhancedSettings.secondaryColor}-500). +3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "") + }, ], - stream: true, - max_tokens: 16_000, - }); - while (true) { - const { done, value } = await chatCompletion.next(); - if (done) { - break; - } + ...providerConfig, + }, + billTo ? { billTo } : {} + ); - const chunk = value.choices[0]?.delta?.content; - if (chunk) { - await writer.write(encoder.encode(chunk)); - } + while (true) { + const { done, value } = await chatCompletion.next() + if (done) { + break; } - 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" + const chunk = value.choices[0]?.delta?.content; + if (chunk) { + await writer.write(encoder.encode(chunk)); + } + } + + // Explicitly close the writer after successful completion + await writer.close(); + } 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, + }) ) - ) { - 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)); + ); + } else if (error?.message?.includes("inference provider information")) { + await writer.write( + encoder.encode( + JSON.stringify({ + ok: false, + openSelectProvider: 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 { + // Ensure the writer is always closed, even if already closed + try { + await writer?.close(); + } catch { + // Ignore errors when closing the writer as it might already be closed + } + } + })(); + + 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 user = await isAuthenticated(); + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + const authHeaders = await headers(); + + const body = await request.json(); + const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew, enhancedSettings } = + body; + + let repoId = repoIdFromBody; + + 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 } + ); + } - return tryGeneration(); + let token = user.token as string; + 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 escapeRegExp = (string: string) => { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }; + + const normalizeHtml = (html: string): string => { + return html + // Normalize whitespace within tags + .replace(/\s+/g, ' ') + // Remove spaces before closing > + .replace(/\s+>/g, '>') + // Remove spaces before /> + .replace(/\s+\/>/g, '/>') + // Normalize spaces around = in attributes + .replace(/\s*=\s*/g, '=') + // Normalize quotes (convert single to double) + .replace(/='([^']*)'/g, '="$1"') + // Remove trailing spaces in opening/closing tags + .replace(/<([^>]*?)\s+>/g, '<$1>') + // Normalize self-closing tags + .replace(/\/\s*>/g, '/>') + .trim(); + }; + + const createFlexibleHtmlRegex = (searchBlock: string) => { + // Normalize both the search block for comparison + const normalizedSearch = normalizeHtml(searchBlock); + + // Escape regex special characters + let searchRegex = escapeRegExp(normalizedSearch) + // Make whitespace flexible (but only between elements, not within tags) + .replace(/>\s*\\s*<') + // Make line breaks and spaces around content flexible + .replace(/>\s*([^<]+)\s*\\s*$1\\s*<'); + + return new RegExp(searchRegex, 'g'); + }; + + const selectedProvider = await getBestProvider(selectedModel.value, provider) + + try { + const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : ""); + const userContext = "You are modifying the HTML file based on the user's request."; + + // Send all pages without filtering + const allPages = pages || []; + const pagesContext = allPages + .map((p: Page) => `- ${p.path}\n${p.html}`) + .join("\n\n"); + + const assistantContext = `${ + selectedElementHtml + ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.` + : "" + }. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`; + + const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext); + const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false); + const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens); + + const chatCompletion = client.chatCompletionStream( + { + model: selectedModel.value, + provider: selectedProvider.provider, + messages: [ + { + role: "system", + content: systemPrompt, + }, + { + role: "user", + content: userContext, + }, + { + role: "assistant", + content: assistantContext, + }, + { + role: "user", + content: prompt, + }, + ], + ...providerConfig, + }, + billTo ? { billTo } : {} + ); + + let chunk = ""; + while (true) { + const { done, value } = await chatCompletion.next(); + if (done) { + break; + } + + const deltaContent = value.choices[0]?.delta?.content; + if (deltaContent) { + chunk += deltaContent; + } + } + 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; + } - 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, - }); + 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 { - errorPayload = JSON.stringify({ - messageError: errorMessage, - isError: true, - }); + const regex = createFlexibleHtmlRegex(searchBlock); + + // Normalize the pageHtml for matching + const normalizedPageHtml = normalizeHtml(pageHtml); + const match = regex.exec(normalizedPageHtml); + + if (match) { + // Find the original match in the non-normalized HTML + const normalizedSearch = normalizeHtml(searchBlock); + const originalMatchIndex = pageHtml.indexOf(searchBlock); + + if (originalMatchIndex !== -1) { + const beforeText = pageHtml.substring(0, originalMatchIndex); + 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); + } else { + // Fallback: try to find similar pattern in the original HTML + const flexibleRegex = new RegExp( + escapeRegExp(searchBlock) + .replace(/\s+/g, '\\s+') + .replace(/\s*=\s*/g, '\\s*=\\s*') + .replace(/'\s*([^']*)\s*'/g, "'\\s*$1\\s*'") + .replace(/"\s*([^"]*)\s*"/g, '"\\s*$1\\s*"') + .replace(/\s*>/g, '\\s*>') + .replace(/\s*\/>/g, '\\s*/>'), + 'g' + ); + + const flexibleMatch = flexibleRegex.exec(pageHtml); + if (flexibleMatch) { + const matchedText = flexibleMatch[0]; + const beforeText = pageHtml.substring(0, flexibleMatch.index); + const startLineNumber = beforeText.split("\n").length; + const replaceLines = replaceBlock.split("\n").length; + const endLineNumber = startLineNumber + replaceLines - 1; + + updatedLines.push([startLineNumber, endLineNumber]); + pageHtml = pageHtml.replace(matchedText, replaceBlock); + } + } + } } - 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); + + 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 regex = createFlexibleHtmlRegex(searchBlock); + + // Get the main page HTML (first page or index page) + const mainPage = updatedPages.find(p => p.path === '/' || p.path === '/index' || p.path === 'index') || updatedPages[0]; + if (!mainPage) continue; + + newHtml = mainPage.html; + + // Normalize the newHtml for matching + const normalizedNewHtml = normalizeHtml(newHtml); + const match = regex.exec(normalizedNewHtml); + + if (match) { + // Find the original match in the non-normalized HTML + const originalMatchIndex = newHtml.indexOf(searchBlock); + + if (originalMatchIndex !== -1) { + const beforeText = newHtml.substring(0, originalMatchIndex); + 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); + } else { + // Fallback: try to find similar pattern in the original HTML + const flexibleRegex = new RegExp( + escapeRegExp(searchBlock) + .replace(/\s+/g, '\\s+') + .replace(/\s*=\s*/g, '\\s*=\\s*') + .replace(/'\s*([^']*)\s*'/g, "'\\s*$1\\s*'") + .replace(/"\s*([^"]*)\s*"/g, '"\\s*$1\\s*"') + .replace(/\s*>/g, '\\s*>') + .replace(/\s*\/>/g, '\\s*/>'), + 'g' + ); + + const flexibleMatch = flexibleRegex.exec(newHtml); + if (flexibleMatch) { + const matchedText = flexibleMatch[0]; + const beforeText = newHtml.substring(0, flexibleMatch.index); + const startLineNumber = beforeText.split("\n").length; + const replaceLines = replaceBlock.split("\n").length; + const endLineNumber = startLineNumber + replaceLines - 1; + + updatedLines.push([startLineNumber, endLineNumber]); + newHtml = newHtml.replace(matchedText, replaceBlock); + } + } } } + + position = replaceEndIndex + REPLACE_END.length; } - }; - await tryGeneration(); - })(); + // 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 response; - } catch (error) { + const files: File[] = []; + updatedPages.forEach((page: Page) => { + const file = new File([page.html], page.path, { type: "text/html" }); + files.push(file); + }); + + if (isNew) { + const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim(); + const formattedTitle = projectName?.toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .split("-") + .filter(Boolean) + .join("-") + .slice(0, 96); + const repo: RepoDesignation = { + type: "space", + name: `${user.name}/${formattedTitle}`, + }; + const { repoUrl} = await createRepo({ + repo, + accessToken: user.token as string, + }); + repoId = repoUrl.split("/").slice(-2).join("/"); + const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)]; + const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)]; + const README = `--- +title: ${projectName} +colorFrom: ${colorFrom} +colorTo: ${colorTo} +emoji: 🐳 +sdk: static +pinned: false +tags: + - deepsite-v3 +--- +# Welcome to your new DeepSite project! +This project was created with [DeepSite](https://deepsite.hf.co). + `; + files.push(new File([README], "README.md", { type: "text/markdown" })); + } + + const response = await uploadFiles({ + repo: { + type: "space", + name: repoId, + }, + files, + commitTitle: prompt, + accessToken: user.token as string, + }); + + return NextResponse.json({ + ok: true, + updatedLines, + pages: updatedPages, + repoId, + commit: { + ...response.commit, + title: prompt, + } + }); + } 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( { - error: error instanceof Error ? error.message : "Internal Server Error", + ok: false, + openSelectProvider: true, + message: + error.message || "An error occurred while processing your request.", }, { status: 500 } ); } -} +} \ No newline at end of file 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/login-url/route.ts b/app/api/auth/login-url/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec05f2f8a66a5d85e9f7f615300871bebe9f8171 --- /dev/null +++ b/app/api/auth/login-url/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + const host = req.headers.get("host") ?? "localhost:3000"; + + let url: string; + if (host.includes("localhost")) { + url = host; + } else if (host.includes("hf.space") || host.includes("/spaces/enzostvs")) { + url = "enzostvs-deepsite.hf.space"; + } else { + url = "deepsite.hf.co"; + } + + 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 NextResponse.json({ loginUrl: loginRedirectUrl }); +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..01feb588119d8472e23d61fffbc6f43d8f5a508c --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import MY_TOKEN_KEY from "@/lib/get-cookie-name"; + +export async function POST() { + const cookieName = MY_TOKEN_KEY(); + const isProduction = process.env.NODE_ENV === "production"; + + const response = NextResponse.json( + { message: "Logged out successfully" }, + { status: 200 } + ); + + // Clear the HTTP-only cookie + const cookieOptions = [ + `${cookieName}=`, + "Max-Age=0", + "Path=/", + "HttpOnly", + ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"]) + ].join("; "); + + response.headers.set("Set-Cookie", cookieOptions); + + return response; +} diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..a57a0447aeea3d9f9f526762aaf7d4ddab16b663 --- /dev/null +++ b/app/api/auth/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from "next/server"; +import MY_TOKEN_KEY from "@/lib/get-cookie-name"; + +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(); + + const cookieName = MY_TOKEN_KEY(); + const isProduction = process.env.NODE_ENV === "production"; + + // Create response with user data + const nextResponse = NextResponse.json( + { + access_token: response.access_token, + expires_in: response.expires_in, + user, + // Include fallback flag for iframe contexts + useLocalStorageFallback: true, + }, + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ); + + // Set HTTP-only cookie with proper attributes for iframe support + const cookieOptions = [ + `${cookieName}=${response.access_token}`, + `Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided + "Path=/", + "HttpOnly", + ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"]) + ].join("; "); + + nextResponse.headers.set("Set-Cookie", cookieOptions); + + return nextResponse; +} 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]/commits/[commitId]/promote/route.ts b/app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e070e50ad1654ddd200786d4feac6d6919c8502 --- /dev/null +++ b/app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts @@ -0,0 +1,190 @@ +import { NextRequest, NextResponse } from "next/server"; +import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import { Page } from "@/types"; + +export async function POST( + req: NextRequest, + { params }: { + params: Promise<{ + namespace: string; + repoId: string; + commitId: string; + }> + } +) { + const user = await isAuthenticated(); + + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + const param = await params; + const { namespace, repoId, commitId } = param; + + try { + const repo: RepoDesignation = { + type: "space", + name: `${namespace}/${repoId}`, + }; + + 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 } + ); + } + + // Fetch files from the specific commit + const files: File[] = []; + const pages: Page[] = []; + const allowedExtensions = ["html", "md", "css", "js", "json", "txt"]; + const commitFilePaths: Set = new Set(); + + // Get all files from the specific commit + for await (const fileInfo of listFiles({ + repo, + accessToken: user.token as string, + revision: commitId, + })) { + const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase(); + + if (allowedExtensions.includes(fileExtension || "")) { + commitFilePaths.add(fileInfo.path); + + // Fetch the file content from the specific commit + const response = await fetch( + `https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}` + ); + + if (response.ok) { + const content = await response.text(); + let mimeType = "text/plain"; + + switch (fileExtension) { + case "html": + mimeType = "text/html"; + // Add HTML files to pages array for client-side setPages + pages.push({ + path: fileInfo.path, + html: content, + }); + break; + case "css": + mimeType = "text/css"; + break; + case "js": + mimeType = "application/javascript"; + break; + case "json": + mimeType = "application/json"; + break; + case "md": + mimeType = "text/markdown"; + break; + } + + const file = new File([content], fileInfo.path, { type: mimeType }); + files.push(file); + } + } + } + + // Get files currently in main branch to identify files to delete + const mainBranchFilePaths: Set = new Set(); + for await (const fileInfo of listFiles({ + repo, + accessToken: user.token as string, + revision: "main", + })) { + const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase(); + + if (allowedExtensions.includes(fileExtension || "")) { + mainBranchFilePaths.add(fileInfo.path); + } + } + + // Identify files to delete (exist in main but not in commit) + const filesToDelete: string[] = []; + for (const mainFilePath of mainBranchFilePaths) { + if (!commitFilePaths.has(mainFilePath)) { + filesToDelete.push(mainFilePath); + } + } + + if (files.length === 0 && filesToDelete.length === 0) { + return NextResponse.json( + { ok: false, error: "No files found in the specified commit and no files to delete" }, + { status: 404 } + ); + } + + // Delete files that exist in main but not in the commit being promoted + if (filesToDelete.length > 0) { + await deleteFiles({ + repo, + paths: filesToDelete, + accessToken: user.token as string, + commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`, + commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`, + }); + } + + // Upload the files to the main branch with a promotion commit message + if (files.length > 0) { + await uploadFiles({ + repo, + files, + accessToken: user.token as string, + commitTitle: `Promote version ${commitId.slice(0, 7)} to main`, + commitDescription: `Promoted commit ${commitId} to main branch`, + }); + } + + return NextResponse.json( + { + ok: true, + message: "Version promoted successfully", + promotedCommit: commitId, + pages: pages, + }, + { status: 200 } + ); + + } catch (error: any) { + + // Handle specific HuggingFace API errors + if (error.statusCode === 404) { + return NextResponse.json( + { ok: false, error: "Commit not found" }, + { status: 404 } + ); + } + + if (error.statusCode === 403) { + return NextResponse.json( + { ok: false, error: "Access denied to repository" }, + { status: 403 } + ); + } + + return NextResponse.json( + { ok: false, error: error.message || "Failed to promote version" }, + { status: 500 } + ); + } +} diff --git a/app/api/me/projects/[namespace]/[repoId]/images/route.ts b/app/api/me/projects/[namespace]/[repoId]/images/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6ec4e96591667e0e33bb47743037c1a2aa11d17 --- /dev/null +++ b/app/api/me/projects/[namespace]/[repoId]/images/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from "next/server"; +import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import Project from "@/models/Project"; +import dbConnect from "@/lib/mongodb"; + +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 }); + } + + 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 } + ); + } + + // 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/app/api/me/projects/[namespace]/[repoId]/route.ts b/app/api/me/projects/[namespace]/[repoId]/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..f19049c9925fe4a1f0fd44fe8fc5bac299f4b12e --- /dev/null +++ b/app/api/me/projects/[namespace]/[repoId]/route.ts @@ -0,0 +1,187 @@ +import { NextRequest, NextResponse } from "next/server"; +import { RepoDesignation, spaceInfo, listFiles, deleteRepo, listCommits, downloadFile } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import { Commit, Page } from "@/types"; + +export async function DELETE( + 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 }); + } + + const param = await params; + const { namespace, repoId } = param; + + 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}`, + }; + + await deleteRepo({ + repo, + accessToken: user.token as string, + }); + + + return NextResponse.json({ ok: true }, { status: 200 }); + } catch (error: any) { + return NextResponse.json( + { ok: false, error: error.message }, + { status: 500 } + ); + } +} + +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 }); + } + + const param = await params; + const { namespace, repoId } = param; + + 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 files: string[] = []; + + const allowedFilesExtensions = ["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 blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true }); + const html = await blob?.text(); + if (!html) { + continue; + } + 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 (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) { + files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`); + } + } + } + } + const commits: Commit[] = []; + for await (const commit of listCommits({ repo, accessToken: user.token as string })) { + if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) { + continue; + } + commits.push({ + title: commit.title, + oid: commit.oid, + date: commit.date, + }); + } + + if (htmlFiles.length === 0) { + return NextResponse.json( + { + ok: false, + error: "No HTML files found", + }, + { status: 404 } + ); + } + return NextResponse.json( + { + project: { + id: space.id, + space_id: space.name, + private: space.private, + _updatedAt: space.updatedAt, + }, + pages: htmlFiles, + files, + commits, + ok: true, + }, + { status: 200 } + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (error.statusCode === 404) { + return NextResponse.json( + { error: "Space not found", ok: false }, + { status: 404 } + ); + } + return NextResponse.json( + { error: error.message, ok: false }, + { status: 500 } + ); + } +} diff --git a/app/api/me/projects/[namespace]/[repoId]/save/route.ts b/app/api/me/projects/[namespace]/[repoId]/save/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..c129dae5508f9a5e50650f7872ae4c9cd8c062a5 --- /dev/null +++ b/app/api/me/projects/[namespace]/[repoId]/save/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from "next/server"; +import { uploadFiles } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import { Page } from "@/types"; + +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 }); + } + + const param = await params; + const { namespace, repoId } = param; + const { pages, commitTitle = "Manual changes saved" } = await req.json(); + + if (!pages || !Array.isArray(pages) || pages.length === 0) { + return NextResponse.json( + { ok: false, error: "Pages are required" }, + { status: 400 } + ); + } + + try { + // Prepare files for upload + const files: File[] = []; + pages.forEach((page: Page) => { + const file = new File([page.html], page.path, { type: "text/html" }); + files.push(file); + }); + + // Upload files to HuggingFace Hub + const response = await uploadFiles({ + repo: { + type: "space", + name: `${namespace}/${repoId}`, + }, + files, + commitTitle, + accessToken: user.token as string, + }); + + return NextResponse.json({ + ok: true, + pages, + commit: { + ...response.commit, + title: commitTitle, + } + }); + } catch (error: any) { + console.error("Error saving manual changes:", error); + return NextResponse.json( + { + ok: false, + error: error.message || "Failed to save changes", + }, + { status: 500 } + ); + } +} diff --git a/app/api/me/projects/route.ts b/app/api/me/projects/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..a37afbc9dd93320d2bd808ca9f3981eca3936555 --- /dev/null +++ b/app/api/me/projects/route.ts @@ -0,0 +1,107 @@ +import { NextRequest, NextResponse } from "next/server"; +import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from "@huggingface/hub"; + +import { isAuthenticated } from "@/lib/auth"; +import { Commit, Page } from "@/types"; +import { COLORS } from "@/lib/utils"; + +export async function POST( + req: NextRequest, +) { + const user = await isAuthenticated(); + if (user instanceof NextResponse || !user) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + const { title: titleFromRequest, pages, prompt } = await req.json(); + + const title = titleFromRequest ?? "DeepSite Project"; + + const formattedTitle = title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .split("-") + .filter(Boolean) + .join("-") + .slice(0, 96); + + const repo: RepoDesignation = { + type: "space", + name: `${user.name}/${formattedTitle}`, + }; + const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)]; + const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)]; + const README = `--- +title: ${title} +colorFrom: ${colorFrom} +colorTo: ${colorTo} +emoji: 🐳 +sdk: static +pinned: false +tags: + - deepsite-v3 +--- + +# Welcome to your new DeepSite project! +This project was created with [DeepSite](https://deepsite.hf.co). +`; + + const files: File[] = []; + const readmeFile = new File([README], "README.md", { type: "text/markdown" }); + files.push(readmeFile); + pages.forEach((page: Page) => { + const file = new File([page.html], page.path, { type: "text/html" }); + files.push(file); + }); + + try { + const { repoUrl} = await createRepo({ + repo, + accessToken: user.token as string, + }); + const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt; + await uploadFiles({ + repo, + files, + accessToken: user.token as string, + commitTitle + }); + + const path = repoUrl.split("/").slice(-2).join("/"); + + const commits: Commit[] = []; + for await (const commit of listCommits({ repo, accessToken: user.token as string })) { + if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) { + continue; + } + commits.push({ + title: commit.title, + oid: commit.oid, + date: commit.date, + }); + } + + const space = await spaceInfo({ + name: repo.name, + accessToken: user.token as string, + }); + + let newProject = { + files, + pages, + commits, + project: { + id: space.id, + space_id: space.name, + _updatedAt: space.updatedAt, + } + } + + return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 }); + } catch (err: any) { + return NextResponse.json( + { error: err.message, ok: false }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/me/route.ts b/app/api/me/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..25bc6c81aa826d65cac703e43c5d4647ae5cd141 --- /dev/null +++ b/app/api/me/route.ts @@ -0,0 +1,46 @@ +import { listSpaces } from "@huggingface/hub"; +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(); + const projects = []; + for await (const space of listSpaces({ + accessToken: token.replace("Bearer ", "") as string, + additionalFields: ["author", "cardData"], + search: { + owner: user.name, + } + })) { + if ( + space.sdk === "static" && + Array.isArray((space.cardData as { tags?: string[] })?.tags) && + ( + ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) || + ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite")) + ) + ) { + projects.push(space); + } + } + + return NextResponse.json({ user, projects, 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..6cf420cea009bbe322d10d7fc9a9d7badee467f6 --- /dev/null +++ b/app/auth/callback/page.tsx @@ -0,0 +1,97 @@ +"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"; +import { AnimatedBlobs } from "@/components/animated-blobs"; +import { useBroadcastChannel } from "@/lib/useBroadcastChannel"; +export default function AuthCallback({ + searchParams, +}: { + searchParams: Promise<{ code: string }>; +}) { + const [showButton, setShowButton] = useState(false); + const [isPopupAuth, setIsPopupAuth] = useState(false); + const { code } = use(searchParams); + const { loginFromCode } = useUser(); + const { postMessage } = useBroadcastChannel("auth", () => {}); + + useMount(async () => { + if (code) { + const isPopup = window.opener || window.parent !== window; + setIsPopupAuth(isPopup); + + if (isPopup) { + postMessage({ + type: "user-oauth", + code: code, + }); + + setTimeout(() => { + if (window.opener) { + window.close(); + } + }, 1000); + } else { + await loginFromCode(code); + } + } + }); + + useTimeoutFn(() => setShowButton(true), 7000); + + return ( +
+
+
+
+
+
+
+ πŸš€ +
+
+ πŸ‘‹ +
+
+ πŸ™Œ +
+
+

+ {isPopupAuth + ? "Authentication Complete!" + : "Login In Progress..."} +

+

+ {isPopupAuth + ? "You can now close this tab and return to the previous page." + : "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/globals.css b/app/globals.css deleted file mode 100644 index 97005be73ce05b7bb1a207464af1945139fde62b..0000000000000000000000000000000000000000 --- a/app/globals.css +++ /dev/null @@ -1,168 +0,0 @@ -@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-geist-sans); - --font-mono: var(--font-geist-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.65rem; - --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); - --radius: 0.625rem; - --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; - } -} - -.monaco-editor .margin { - @apply bg-background!; -} -.monaco-editor .monaco-editor-background { - @apply bg-background!; -} -.monaco-editor .decorationsOverviewRuler { - @apply opacity-0!; -} -.monaco-editor .view-line { - /* @apply bg-primary/50!; */ -} -.monaco-editor .scroll-decoration { - @apply opacity-0!; -} -.monaco-editor .cursors-layer .cursor { - @apply bg-primary!; -} - -.content-placeholder::before { - content: attr(data-placeholder); - position: absolute; - pointer-events: none; - opacity: 0.5; - @apply top-5 left-6; -} - -.sp-layout - .sp-file-explorer - .sp-file-explorer-list - .sp-explorer[data-active="true"] { - @apply text-indigo-500!; -} - -.sp-layout - .sp-stack - .sp-tabs - .sp-tab-container[aria-selected="true"] - .sp-tab-button { - @apply text-indigo-500!; -} -.sp-layout .sp-stack .sp-tabs .sp-tab-container:has(button:focus) { - @apply outline-none! border-none!; -} diff --git a/app/layout.tsx b/app/layout.tsx index 469b6844c7b63d85ad5e9c7ba420add2f7f1125c..6b47502e83638b72e7c4bcec73865ed846b62036 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,24 +1,29 @@ +/* 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 { Inter, PT_Sans } from "next/font/google"; +import { cookies } from "next/headers"; import Script from "next/script"; -import "@/app/globals.css"; -import { ThemeProvider } from "@/components/providers/theme"; -import { AuthProvider } from "@/components/providers/session"; +import "@/assets/globals.css"; import { Toaster } from "@/components/ui/sonner"; -import { ReactQueryProvider } from "@/components/providers/react-query"; +import MY_TOKEN_KEY from "@/lib/get-cookie-name"; +import { apiServer } from "@/lib/api"; +import IframeDetector from "@/components/iframe-detector"; +import AppContext from "@/components/contexts/app-context"; +import TanstackContext from "@/components/contexts/tanstack-query-context"; +import { LoginProvider } from "@/components/contexts/login-context"; +import { ProProvider } from "@/components/contexts/pro-context"; import { generateSEO, generateStructuredData } from "@/lib/seo"; -import { NotAuthorizedDomain } from "@/components/not-authorized"; -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 = { @@ -46,29 +51,47 @@ export const metadata: Metadata = { 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, projects: [], errCode: null }; + try { + const res = await apiServer.get("/me", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return { user: res.data.user, projects: res.data.projects, errCode: null }; + } catch (err: any) { + return { user: null, projects: [], errCode: err.status }; + } +} + export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const data = await getMe(); + + // Generate structured data 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", }); return ( - - + + + + + + + + + {children} + + + ); diff --git a/app/new/page.tsx b/app/new/page.tsx index 920e799168597ccab9e644171bff837961cc45e6..6f9a3e815dd4df433d10c4b0f850ade9d6fb38fb 100644 --- a/app/new/page.tsx +++ b/app/new/page.tsx @@ -1,18 +1,14 @@ import { AppEditor } from "@/components/editor"; -import { auth } from "@/lib/auth"; -import { redirect } from "next/navigation"; +import { Metadata } from "next"; +import { generateSEO } from "@/lib/seo"; -export default async function NewProjectPage({ - searchParams, -}: { - searchParams: Promise<{ prompt: string }>; -}) { - const session = await auth(); +export const metadata: Metadata = generateSEO({ + title: "Create New Project - DeepSite", + description: + "Start building your next website with AI. Create a new project on DeepSite and experience the power of AI-driven web development.", + path: "/new", +}); - if (!session) { - redirect("/api/auth/signin?callbackUrl=/new"); - } - - const { prompt } = await searchParams; - return ; +export default function NewProjectPage() { + return ; } diff --git a/app/not-found.tsx b/app/not-found.tsx deleted file mode 100644 index ae102854e6245bcc2f0d55780d9419b2679d6f38..0000000000000000000000000000000000000000 --- a/app/not-found.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { NotFoundButtons } from "@/components/not-found/buttons"; -import { Navigation } from "@/components/public/navigation"; - -export default function NotFound() { - return ( -
- -
-

Oh no! Page not found.

-

- The page you are looking for does not exist. -

- -
-
- ); -} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000000000000000000000000000000000000..42b4b3583695e12a62159778c441ff8e0e7cf637 --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,28 @@ +import { MetadataRoute } from 'next'; + +export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = 'https://deepsite.hf.co'; + + return [ + { + url: baseUrl, + lastModified: new Date(), + changeFrequency: 'daily', + priority: 1, + }, + { + url: `${baseUrl}/new`, + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 0.8, + }, + { + url: `${baseUrl}/auth`, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 0.5, + }, + // Note: Dynamic project routes will be handled by Next.js automatically + // but you can add specific high-priority project pages here if needed + ]; +} diff --git a/assets/globals.css b/assets/globals.css new file mode 100644 index 0000000000000000000000000000000000000000..9b6618f9d91f8066c44b1ce2608d9af665f7d42f --- /dev/null +++ b/assets/globals.css @@ -0,0 +1,371 @@ +@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); +} + +body { + @apply scroll-smooth +} + +@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; +} + +/* Fast liquid deformation animations */ +@keyframes liquidBlob1 { + 0%, 100% { + border-radius: 40% 60% 50% 50%; + transform: scaleX(1) scaleY(1) rotate(0deg); + } + 12.5% { + border-radius: 20% 80% 70% 30%; + transform: scaleX(1.6) scaleY(0.4) rotate(25deg); + } + 25% { + border-radius: 80% 20% 30% 70%; + transform: scaleX(0.5) scaleY(2.1) rotate(-15deg); + } + 37.5% { + border-radius: 30% 70% 80% 20%; + transform: scaleX(1.8) scaleY(0.6) rotate(40deg); + } + 50% { + border-radius: 70% 30% 20% 80%; + transform: scaleX(0.4) scaleY(1.9) rotate(-30deg); + } + 62.5% { + border-radius: 25% 75% 60% 40%; + transform: scaleX(1.5) scaleY(0.7) rotate(55deg); + } + 75% { + border-radius: 75% 25% 40% 60%; + transform: scaleX(0.6) scaleY(1.7) rotate(-10deg); + } + 87.5% { + border-radius: 50% 50% 75% 25%; + transform: scaleX(1.3) scaleY(0.8) rotate(35deg); + } +} + +@keyframes liquidBlob2 { + 0%, 100% { + border-radius: 60% 40% 50% 50%; + transform: scaleX(1) scaleY(1) rotate(12deg); + } + 16% { + border-radius: 15% 85% 60% 40%; + transform: scaleX(0.3) scaleY(2.3) rotate(50deg); + } + 32% { + border-radius: 85% 15% 25% 75%; + transform: scaleX(2.0) scaleY(0.5) rotate(-20deg); + } + 48% { + border-radius: 30% 70% 85% 15%; + transform: scaleX(0.4) scaleY(1.8) rotate(70deg); + } + 64% { + border-radius: 70% 30% 15% 85%; + transform: scaleX(1.9) scaleY(0.6) rotate(-35deg); + } + 80% { + border-radius: 40% 60% 70% 30%; + transform: scaleX(0.7) scaleY(1.6) rotate(45deg); + } +} + +@keyframes liquidBlob3 { + 0%, 100% { + border-radius: 50% 50% 40% 60%; + transform: scaleX(1) scaleY(1) rotate(0deg); + } + 20% { + border-radius: 10% 90% 75% 25%; + transform: scaleX(2.2) scaleY(0.3) rotate(-45deg); + } + 40% { + border-radius: 90% 10% 20% 80%; + transform: scaleX(0.4) scaleY(2.5) rotate(60deg); + } + 60% { + border-radius: 25% 75% 90% 10%; + transform: scaleX(1.7) scaleY(0.5) rotate(-25deg); + } + 80% { + border-radius: 75% 25% 10% 90%; + transform: scaleX(0.6) scaleY(2.0) rotate(80deg); + } +} + +@keyframes liquidBlob4 { + 0%, 100% { + border-radius: 45% 55% 50% 50%; + transform: scaleX(1) scaleY(1) rotate(-15deg); + } + 14% { + border-radius: 90% 10% 65% 35%; + transform: scaleX(0.2) scaleY(2.8) rotate(35deg); + } + 28% { + border-radius: 10% 90% 20% 80%; + transform: scaleX(2.4) scaleY(0.4) rotate(-50deg); + } + 42% { + border-radius: 35% 65% 90% 10%; + transform: scaleX(0.3) scaleY(2.1) rotate(70deg); + } + 56% { + border-radius: 80% 20% 10% 90%; + transform: scaleX(2.0) scaleY(0.5) rotate(-40deg); + } + 70% { + border-radius: 20% 80% 55% 45%; + transform: scaleX(0.5) scaleY(1.9) rotate(55deg); + } + 84% { + border-radius: 65% 35% 80% 20%; + transform: scaleX(1.6) scaleY(0.6) rotate(-25deg); + } +} + +/* Fast flowing movement animations */ +@keyframes liquidFlow1 { + 0%, 100% { transform: translate(0, 0); } + 16% { transform: translate(60px, -40px); } + 32% { transform: translate(-45px, -70px); } + 48% { transform: translate(80px, 25px); } + 64% { transform: translate(-30px, 60px); } + 80% { transform: translate(50px, -20px); } +} + +@keyframes liquidFlow2 { + 0%, 100% { transform: translate(0, 0); } + 20% { transform: translate(-70px, 50px); } + 40% { transform: translate(90px, -30px); } + 60% { transform: translate(-40px, -55px); } + 80% { transform: translate(65px, 35px); } +} + +@keyframes liquidFlow3 { + 0%, 100% { transform: translate(0, 0); } + 12% { transform: translate(-50px, -60px); } + 24% { transform: translate(40px, -20px); } + 36% { transform: translate(-30px, 70px); } + 48% { transform: translate(70px, 20px); } + 60% { transform: translate(-60px, -35px); } + 72% { transform: translate(35px, 55px); } + 84% { transform: translate(-25px, -45px); } +} + +@keyframes liquidFlow4 { + 0%, 100% { transform: translate(0, 0); } + 14% { transform: translate(50px, 60px); } + 28% { transform: translate(-80px, -40px); } + 42% { transform: translate(30px, -90px); } + 56% { transform: translate(-55px, 45px); } + 70% { transform: translate(75px, -25px); } + 84% { transform: translate(-35px, 65px); } +} + +/* Light sweep animation for buttons */ +@keyframes lightSweep { + 0% { + transform: translateX(-150%); + opacity: 0; + } + 8% { + opacity: 0.3; + } + 25% { + opacity: 0.8; + } + 42% { + opacity: 0.3; + } + 50% { + transform: translateX(150%); + opacity: 0; + } + 58% { + opacity: 0.3; + } + 75% { + opacity: 0.8; + } + 92% { + opacity: 0.3; + } + 100% { + transform: translateX(-150%); + opacity: 0; + } +} + +.light-sweep { + position: relative; + overflow: hidden; +} + +.light-sweep::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 300%; + background: linear-gradient( + 90deg, + transparent 0%, + transparent 20%, + rgba(56, 189, 248, 0.1) 35%, + rgba(56, 189, 248, 0.2) 45%, + rgba(255, 255, 255, 0.2) 50%, + rgba(168, 85, 247, 0.2) 55%, + rgba(168, 85, 247, 0.1) 65%, + transparent 80%, + transparent 100% + ); + animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite; + pointer-events: none; + z-index: 1; + filter: blur(1px); +} diff --git a/assets/hf-logo.svg b/assets/hf-logo.svg deleted file mode 100644 index b91ba37b236c18b7483716a33a99c05fc43bd00a..0000000000000000000000000000000000000000 --- a/assets/hf-logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..e69f057d4d4c256f02881888e781aa0943010c3e --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/minimax.svg b/assets/minimax.svg deleted file mode 100644 index 1d32449ab8fb0fe9a6c50006a41e67ef49c8dd1c..0000000000000000000000000000000000000000 --- a/assets/minimax.svg +++ /dev/null @@ -1 +0,0 @@ -Minimax \ No newline at end of file diff --git a/assets/pro.svg b/assets/pro.svg deleted file mode 100644 index 4d992b483109757a550a27a05caf48cd09daa892..0000000000000000000000000000000000000000 --- a/assets/pro.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/chart/Chart.yaml b/chart/Chart.yaml deleted file mode 100644 index 83483de68757c3ed27fd68ca9bbb51aa48a64d25..0000000000000000000000000000000000000000 --- a/chart/Chart.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v2 -name: deepsite -version: 0.0.0-latest -type: application -icon: https://huggingface.co/front/assets/huggingface_logo-noborder.svg diff --git a/chart/env/prod.yaml b/chart/env/prod.yaml deleted file mode 100644 index 2ac82284c67dda993d0292685a8cc56cda2f25e8..0000000000000000000000000000000000000000 --- a/chart/env/prod.yaml +++ /dev/null @@ -1,59 +0,0 @@ -nodeSelector: - role-deepsite: "true" - -tolerations: - - key: "huggingface.co/deepsite" - operator: "Equal" - value: "true" - effect: "NoSchedule" - -serviceAccount: - enabled: true - create: true - name: deepsite-prod - -ingress: - path: "/" - annotations: - alb.ingress.kubernetes.io/healthcheck-path: "/api/healthcheck" - alb.ingress.kubernetes.io/listen-ports: "[{\"HTTP\": 80}, {\"HTTPS\": 443}]" - alb.ingress.kubernetes.io/load-balancer-name: "hub-utils-prod-cloudfront" - alb.ingress.kubernetes.io/group.name: "hub-utils-prod-cloudfront" - alb.ingress.kubernetes.io/scheme: "internal" - alb.ingress.kubernetes.io/ssl-redirect: "443" - alb.ingress.kubernetes.io/tags: "Env=prod,Project=hub,Terraform=true" - alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30 - alb.ingress.kubernetes.io/target-type: "ip" - alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-east-1:707930574880:certificate/5b25b145-75db-4837-b9f3-7f238ba8a9c7,arn:aws:acm:us-east-1:707930574880:certificate/bfdf509c-f44b-400f-b9e1-6f7a861abe91" - kubernetes.io/ingress.class: "alb" - -networkPolicy: - enabled: true - allowedBlocks: - - 10.0.0.0/16 - - -ingressInternal: - enabled: false - -envVars: - NEXTAUTH_URL: https://deepsite.hf.co/api/auth - -infisical: - enabled: true - env: "prod-us-east-1" - -autoscaling: - enabled: true - minReplicas: 1 - maxReplicas: 10 - targetMemoryUtilizationPercentage: "50" - targetCPUUtilizationPercentage: "50" - -resources: - requests: - cpu: 2 - memory: 4Gi - limits: - cpu: 4 - memory: 8Gi diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl deleted file mode 100644 index eee5a181d225c2aff53344c446288240d37d3d0b..0000000000000000000000000000000000000000 --- a/chart/templates/_helpers.tpl +++ /dev/null @@ -1,22 +0,0 @@ -{{- define "name" -}} -{{- default $.Release.Name | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "app.name" -}} -chat-ui -{{- end -}} - -{{- define "labels.standard" -}} -release: {{ $.Release.Name | quote }} -heritage: {{ $.Release.Service | quote }} -chart: "{{ include "name" . }}" -app: "{{ include "app.name" . }}" -{{- end -}} - -{{- define "labels.resolver" -}} -release: {{ $.Release.Name | quote }} -heritage: {{ $.Release.Service | quote }} -chart: "{{ include "name" . }}" -app: "{{ include "app.name" . }}-resolver" -{{- end -}} - diff --git a/chart/templates/config.yaml b/chart/templates/config.yaml deleted file mode 100644 index c4c803e9e5f8b473ae216d5a2933cb67d46bc011..0000000000000000000000000000000000000000 --- a/chart/templates/config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - labels: {{ include "labels.standard" . | nindent 4 }} - name: {{ include "name" . }} - namespace: {{ .Release.Namespace }} -data: - {{- range $key, $value := $.Values.envVars }} - {{ $key }}: {{ $value | quote }} - {{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml deleted file mode 100644 index b487ee6a3e5db9e315dcb024e6fdc6b753eea5a3..0000000000000000000000000000000000000000 --- a/chart/templates/deployment.yaml +++ /dev/null @@ -1,81 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: {{ include "labels.standard" . | nindent 4 }} - name: {{ include "name" . }} - namespace: {{ .Release.Namespace }} - {{- if .Values.infisical.enabled }} - annotations: - secrets.infisical.com/auto-reload: "true" - {{- end }} -spec: - progressDeadlineSeconds: 600 - {{- if not $.Values.autoscaling.enabled }} - replicas: {{ .Values.replicas }} - {{- end }} - revisionHistoryLimit: 10 - selector: - matchLabels: {{ include "labels.standard" . | nindent 6 }} - strategy: - rollingUpdate: - maxSurge: 25% - maxUnavailable: 25% - type: RollingUpdate - template: - metadata: - labels: {{ include "labels.standard" . | nindent 8 }} - annotations: - checksum/config: {{ include (print $.Template.BasePath "/config.yaml") . | sha256sum }} - {{- if $.Values.envVars.NODE_LOG_STRUCTURED_DATA }} - co.elastic.logs/json.expand_keys: "true" - {{- end }} - spec: - {{- if .Values.serviceAccount.enabled }} - serviceAccountName: "{{ .Values.serviceAccount.name | default (include "name" .) }}" - {{- end }} - containers: - - name: chat-ui - image: "{{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - readinessProbe: - failureThreshold: 30 - periodSeconds: 10 - httpGet: - path: {{ $.Values.envVars.APP_BASE | default "" }}/api/healthcheck - port: {{ $.Values.envVars.APP_PORT | default 3001 | int }} - livenessProbe: - failureThreshold: 30 - periodSeconds: 10 - httpGet: - path: {{ $.Values.envVars.APP_BASE | default "" }}/api/healthcheck - port: {{ $.Values.envVars.APP_PORT | default 3001 | int }} - ports: - - containerPort: {{ $.Values.envVars.APP_PORT | default 3001 | int }} - name: http - protocol: TCP - {{- if eq "true" $.Values.envVars.METRICS_ENABLED }} - - containerPort: {{ $.Values.envVars.METRICS_PORT | default 5565 | int }} - name: metrics - protocol: TCP - {{- end }} - resources: {{ toYaml .Values.resources | nindent 12 }} - {{- with $.Values.extraEnv }} - env: - {{- toYaml . | nindent 14 }} - {{- end }} - envFrom: - - configMapRef: - name: {{ include "name" . }} - {{- if $.Values.infisical.enabled }} - - secretRef: - name: {{ include "name" $ }}-secs - {{- end }} - {{- with $.Values.extraEnvFrom }} - {{- toYaml . | nindent 14 }} - {{- end }} - nodeSelector: {{ toYaml .Values.nodeSelector | nindent 8 }} - tolerations: {{ toYaml .Values.tolerations | nindent 8 }} - volumes: - - name: config - configMap: - name: {{ include "name" . }} diff --git a/chart/templates/hpa.yaml b/chart/templates/hpa.yaml deleted file mode 100644 index bf7bd3b256b79c54269ae39afb02c816878596dc..0000000000000000000000000000000000000000 --- a/chart/templates/hpa.yaml +++ /dev/null @@ -1,45 +0,0 @@ -{{- if $.Values.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: {{ include "labels.standard" . | nindent 4 }} - name: {{ include "name" . }} - namespace: {{ .Release.Namespace }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "name" . }} - minReplicas: {{ $.Values.autoscaling.minReplicas }} - maxReplicas: {{ $.Values.autoscaling.maxReplicas }} - metrics: - {{- if ne "" $.Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ $.Values.autoscaling.targetMemoryUtilizationPercentage | int }} - {{- end }} - {{- if ne "" $.Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ $.Values.autoscaling.targetCPUUtilizationPercentage | int }} - {{- end }} - behavior: - scaleDown: - stabilizationWindowSeconds: 600 - policies: - - type: Percent - value: 10 - periodSeconds: 60 - scaleUp: - stabilizationWindowSeconds: 0 - policies: - - type: Pods - value: 1 - periodSeconds: 30 -{{- end }} diff --git a/chart/templates/infisical.yaml b/chart/templates/infisical.yaml deleted file mode 100644 index 6a11e084f6e0ab300ea4ec2b2694a79dadc1bdf8..0000000000000000000000000000000000000000 --- a/chart/templates/infisical.yaml +++ /dev/null @@ -1,24 +0,0 @@ -{{- if .Values.infisical.enabled }} -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: {{ include "name" $ }}-infisical-secret - namespace: {{ $.Release.Namespace }} -spec: - authentication: - universalAuth: - credentialsRef: - secretName: {{ .Values.infisical.operatorSecretName | quote }} - secretNamespace: {{ .Values.infisical.operatorSecretNamespace | quote }} - secretsScope: - envSlug: {{ .Values.infisical.env | quote }} - projectSlug: {{ .Values.infisical.project | quote }} - secretsPath: / - hostAPI: {{ .Values.infisical.url | quote }} - managedSecretReference: - creationPolicy: Owner - secretName: {{ include "name" $ }}-secs - secretNamespace: {{ .Release.Namespace | quote }} - secretType: Opaque - resyncInterval: {{ .Values.infisical.resyncInterval }} -{{- end }} diff --git a/chart/templates/ingress-internal.yaml b/chart/templates/ingress-internal.yaml deleted file mode 100644 index bf87d0b6c960871327908e243e79e05475b825d5..0000000000000000000000000000000000000000 --- a/chart/templates/ingress-internal.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if $.Values.ingressInternal.enabled }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: {{ toYaml .Values.ingressInternal.annotations | nindent 4 }} - labels: {{ include "labels.standard" . | nindent 4 }} - name: {{ include "name" . }}-internal - namespace: {{ .Release.Namespace }} -spec: - {{ if $.Values.ingressInternal.className }} - ingressClassName: {{ .Values.ingressInternal.className }} - {{ end }} - {{- with .Values.ingressInternal.tls }} - tls: - - hosts: - - {{ $.Values.domain | quote }} - {{- with .secretName }} - secretName: {{ . }} - {{- end }} - {{- end }} - rules: - - host: {{ .Values.domain }} - http: - paths: - - backend: - service: - name: {{ include "name" . }} - port: - name: http - path: {{ $.Values.ingressInternal.path | default "/" }} - pathType: Prefix -{{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml deleted file mode 100644 index 563c911d72b906e18d716a4b2b4d3d26842d7fd7..0000000000000000000000000000000000000000 --- a/chart/templates/ingress.yaml +++ /dev/null @@ -1,33 +0,0 @@ -{{- if $.Values.ingress.enabled }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: {{ toYaml .Values.ingress.annotations | nindent 4 }} - labels: {{ include "labels.standard" . | nindent 4 }} - name: {{ include "name" . }} - namespace: {{ .Release.Namespace }} -spec: - {{ if $.Values.ingress.className }} - ingressClassName: {{ .Values.ingress.className }} - {{ end }} - {{- with .Values.ingress.tls }} - tls: - - hosts: - - {{ $.Values.domain | quote }} - {{- with .secretName }} - secretName: {{ . }} - {{- end }} - {{- end }} - rules: - - host: {{ .Values.domain }} - http: - paths: - - backend: - service: - name: {{ include "name" . }} - port: - name: http - path: {{ $.Values.ingress.path | default "/" }} - pathType: Prefix - - {{- end }} diff --git a/chart/templates/network-policy.yaml b/chart/templates/network-policy.yaml deleted file mode 100644 index 59f5df5893a97f4075237ac7cdb4979dce7298a9..0000000000000000000000000000000000000000 --- a/chart/templates/network-policy.yaml +++ /dev/null @@ -1,36 +0,0 @@ -{{- if $.Values.networkPolicy.enabled }} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "name" . }} - namespace: {{ .Release.Namespace }} -spec: - egress: - - ports: - - port: 53 - protocol: UDP - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: kube-system - podSelector: - matchLabels: - k8s-app: kube-dns - - to: - {{- range $ip := .Values.networkPolicy.allowedBlocks }} - - ipBlock: - cidr: {{ $ip | quote }} - {{- end }} - - to: - - ipBlock: - cidr: 0.0.0.0/0 - except: - - 10.0.0.0/8 - - 172.16.0.0/12 - - 192.168.0.0/16 - - 169.254.169.254/32 - podSelector: - matchLabels: {{ include "labels.standard" . | nindent 6 }} - policyTypes: - - Egress -{{- end }} diff --git a/chart/templates/service-account.yaml b/chart/templates/service-account.yaml deleted file mode 100644 index fc3a184c9def4cef61836f8eac152ab61fe4d047..0000000000000000000000000000000000000000 --- a/chart/templates/service-account.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if and .Values.serviceAccount.enabled .Values.serviceAccount.create }} -apiVersion: v1 -kind: ServiceAccount -automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} -metadata: - name: "{{ .Values.serviceAccount.name | default (include "name" .) }}" - namespace: {{ .Release.Namespace }} - labels: {{ include "labels.standard" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/chart/templates/service-monitor.yaml b/chart/templates/service-monitor.yaml deleted file mode 100644 index 0c8e4dab423946a318a3daee7bd4dfcab0cee151..0000000000000000000000000000000000000000 --- a/chart/templates/service-monitor.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if eq "true" $.Values.envVars.METRICS_ENABLED }} -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - labels: {{ include "labels.standard" . | nindent 4 }} - name: {{ include "name" . }} - namespace: {{ .Release.Namespace }} -spec: - selector: - matchLabels: {{ include "labels.standard" . | nindent 6 }} - endpoints: - - port: metrics - path: /metrics - interval: 10s - scheme: http - scrapeTimeout: 10s -{{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml deleted file mode 100644 index ef364f0926861b9e934e9830d7884cdd63ecd6ec..0000000000000000000000000000000000000000 --- a/chart/templates/service.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: "{{ include "name" . }}" - annotations: {{ toYaml .Values.service.annotations | nindent 4 }} - namespace: {{ .Release.Namespace }} - labels: {{ include "labels.standard" . | nindent 4 }} -spec: - ports: - - name: http - port: 80 - protocol: TCP - targetPort: http - {{- if eq "true" $.Values.envVars.METRICS_ENABLED }} - - name: metrics - port: {{ $.Values.envVars.METRICS_PORT | default 5565 | int }} - protocol: TCP - targetPort: metrics - {{- end }} - selector: {{ include "labels.standard" . | nindent 4 }} - type: {{.Values.service.type}} diff --git a/chart/values.yaml b/chart/values.yaml deleted file mode 100644 index 5a2bd68b20d5efde44f87d7ffb8278717fa5aaf8..0000000000000000000000000000000000000000 --- a/chart/values.yaml +++ /dev/null @@ -1,81 +0,0 @@ -image: - repository: registry.internal.huggingface.tech/deepsite - name: deepsite - tag: 0.0.0-latest - pullPolicy: IfNotPresent - -replicas: 1 - -domain: deepsite.hf.co - -networkPolicy: - enabled: true - allowedBlocks: [] - # allowedBlocks: - # - 10.0.240.0/24 - # - 10.0.241.0/24 - # - 10.0.242.0/24 - # - 10.0.243.0/24 - # - 10.0.244.0/24 - # - 10.240.0.0/24 - # - 10.16.0.0/16 - -service: - type: NodePort - annotations: { } - -serviceAccount: - enabled: false - create: false - name: "" - automountServiceAccountToken: true - annotations: { } - -ingress: - enabled: true - path: "/" - annotations: { } - # className: "nginx" - tls: { } - # secretName: XXX - -ingressInternal: - enabled: false - path: "/" - annotations: { } - # className: "nginx" - tls: { } - -resources: - requests: - cpu: 2 - memory: 4Gi - limits: - cpu: 2 - memory: 4Gi -nodeSelector: {} -tolerations: [] - -envVars: { } - -infisical: - enabled: false - env: "" - project: "deepsite-f-hvj" - url: "" - resyncInterval: 60 - operatorSecretName: "deepsite-operator-secrets" - operatorSecretNamespace: "hub-utils" - -# Allow to environment injections on top or instead of infisical -extraEnvFrom: [] -extraEnv: [] - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 2 - targetMemoryUtilizationPercentage: "" - targetCPUUtilizationPercentage: "" - -## Metrics removed; monitoring configuration no longer used diff --git a/components.json b/components.json index d5005f0974a11b0ea57843b9f52f82f995743963..8854f1e2cb22a6949612c72de37b6d9a57489b85 100644 --- a/components.json +++ b/components.json @@ -5,12 +5,11 @@ "tsx": true, "tailwind": { "config": "", - "css": "app/globals.css", - "baseColor": "zinc", + "css": "assets/globals.css", + "baseColor": "neutral", "cssVariables": true, "prefix": "" }, - "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -18,5 +17,5 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "registries": {} -} + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/animated-blobs/index.tsx b/components/animated-blobs/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..516c36cf8ac5dd62a5f293b405bf3b2c480cb78f --- /dev/null +++ b/components/animated-blobs/index.tsx @@ -0,0 +1,34 @@ +export function AnimatedBlobs() { + return ( +
+
+
+
+
+
+ ); +} diff --git a/components/animated-text/index.tsx b/components/animated-text/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1bfef666235566c3f8fec1872b30ff5bb55b3168 --- /dev/null +++ b/components/animated-text/index.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useState, useEffect } from "react"; + +interface AnimatedTextProps { + className?: string; +} + +export function AnimatedText({ className = "" }: AnimatedTextProps) { + const [displayText, setDisplayText] = useState(""); + const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0); + const [isTyping, setIsTyping] = useState(true); + const [showCursor, setShowCursor] = useState(true); + const [lastTypedIndex, setLastTypedIndex] = useState(-1); + const [animationComplete, setAnimationComplete] = useState(false); + + // Randomize suggestions on each component mount + const [suggestions] = useState(() => { + const baseSuggestions = [ + "create a stunning portfolio!", + "build a tic tac toe game!", + "design a website for my restaurant!", + "make a sleek landing page!", + "build an e-commerce store!", + "create a personal blog!", + "develop a modern dashboard!", + "design a company website!", + "build a todo app!", + "create an online gallery!", + "make a contact form!", + "build a weather app!", + ]; + + // Fisher-Yates shuffle algorithm + const shuffled = [...baseSuggestions]; + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + + return shuffled; + }); + + useEffect(() => { + if (animationComplete) return; + + let timeout: NodeJS.Timeout; + + const typeText = () => { + const currentSuggestion = suggestions[currentSuggestionIndex]; + + if (isTyping) { + if (displayText.length < currentSuggestion.length) { + setDisplayText(currentSuggestion.slice(0, displayText.length + 1)); + setLastTypedIndex(displayText.length); + timeout = setTimeout(typeText, 80); + } else { + // Finished typing, wait then start erasing + setLastTypedIndex(-1); + timeout = setTimeout(() => { + setIsTyping(false); + }, 2000); + } + } + }; + + timeout = setTimeout(typeText, 100); + return () => clearTimeout(timeout); + }, [ + displayText, + currentSuggestionIndex, + isTyping, + suggestions, + animationComplete, + ]); + + // Cursor blinking effect + useEffect(() => { + if (animationComplete) { + setShowCursor(false); + return; + } + + const cursorInterval = setInterval(() => { + setShowCursor((prev) => !prev); + }, 600); + + return () => clearInterval(cursorInterval); + }, [animationComplete]); + + useEffect(() => { + if (lastTypedIndex >= 0) { + const timeout = setTimeout(() => { + setLastTypedIndex(-1); + }, 400); + + return () => clearTimeout(timeout); + } + }, [lastTypedIndex]); + + return ( +

+ Hey DeepSite,  + {displayText.split("").map((char, index) => ( + + {char} + + ))} + + | + +

+ ); +} diff --git a/components/ask-ai/ask-ai-landing.tsx b/components/ask-ai/ask-ai-landing.tsx deleted file mode 100644 index db19f5be4bfc3fc05e4083a18a41cca6169bf65a..0000000000000000000000000000000000000000 --- a/components/ask-ai/ask-ai-landing.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; -import { ArrowUp } from "lucide-react"; -import { useState } from "react"; -import { useLocalStorage, useMount } from "react-use"; -import { useRouter } from "next/navigation"; - -import { Button } from "@/components/ui/button"; -import { ProviderType } from "@/lib/type"; -import { Models } from "./models"; -import { DEFAULT_MODEL } from "@/lib/providers"; -import { cn } from "@/lib/utils"; - -export function AskAiLanding({ className }: { className?: string }) { - const [model = DEFAULT_MODEL, setModel] = useLocalStorage( - "deepsite-model", - DEFAULT_MODEL - ); - const [provider, setProvider] = useLocalStorage( - "deepsite-provider", - "auto" as ProviderType - ); - const router = useRouter(); - const [prompt, setPrompt] = useState(""); - const [mounted, setMounted] = useState(false); - - useMount(() => { - setMounted(true); - }); - - return ( -
-