diff --git a/.env.example b/.env.example
deleted file mode 100644
index d7419a983b3e8d6606db990addea8d78dd660692..0000000000000000000000000000000000000000
--- a/.env.example
+++ /dev/null
@@ -1,4 +0,0 @@
-AUTH_HUGGINGFACE_ID=
-AUTH_HUGGINGFACE_SECRET=
-NEXTAUTH_URL=http://localhost:3001
-AUTH_SECRET=
\ No newline at end of file
diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml
deleted file mode 100644
index 88d7e85dd2f708097a42154a349c9cb217479120..0000000000000000000000000000000000000000
--- a/.github/workflows/deploy-prod.yml
+++ /dev/null
@@ -1,77 +0,0 @@
-name: Deploy to k8s
-on:
- # run this workflow manually from the Actions tab
- workflow_dispatch:
-
-jobs:
- build-and-publish:
- runs-on:
- group: cpu-high
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Login to Registry
- uses: docker/login-action@v3
- with:
- registry: registry.internal.huggingface.tech
- username: ${{ secrets.DOCKER_INTERNAL_USERNAME }}
- password: ${{ secrets.DOCKER_INTERNAL_PASSWORD }}
-
- - name: Docker metadata
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: |
- registry.internal.huggingface.tech/deepsite/deepsite
- tags: |
- type=raw,value=latest,enable={{is_default_branch}}
- type=sha,enable=true,prefix=sha-,format=short,sha-len=8
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Inject slug/short variables
- uses: rlespinasse/github-slug-action@v4
-
- - name: Build and Publish image
- uses: docker/build-push-action@v5
- with:
- context: .
- file: Dockerfile
- push: ${{ github.event_name != 'pull_request' }}
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- platforms: linux/amd64
- cache-to: type=gha,mode=max,scope=amd64
- cache-from: type=gha,scope=amd64
- provenance: false
-
- deploy:
- name: Deploy on prod
- runs-on: ubuntu-latest
- needs: ["build-and-publish"]
- steps:
- - name: Inject slug/short variables
- uses: rlespinasse/github-slug-action@v4
-
- - name: Gen values
- run: |
- VALUES=$(cat <<-END
- image:
- tag: "sha-${{ env.GITHUB_SHA_SHORT }}"
- END
- )
- echo "VALUES=$(echo "$VALUES" | yq -o=json | jq tostring)" >> $GITHUB_ENV
-
- - name: Deploy on infra-deployments
- uses: the-actions-org/workflow-dispatch@v2
- with:
- workflow: Update application single value
- repo: huggingface/infra-deployments
- wait-for-completion: true
- wait-for-completion-interval: 10s
- display-workflow-run-url-interval: 10s
- ref: refs/heads/main
- token: ${{ secrets.GIT_TOKEN_INFRA_DEPLOYMENT }}
- inputs: '{"path": "hub/deepsite/deepsite.yaml", "value": ${{ env.VALUES }}, "url": "${{ github.event.head_commit.url }}"}'
diff --git a/.gitignore b/.gitignore
index b8b89cbb382fac7dea7fdeb461bd43beb9937c49..5ef6a520780202a1d6addd833d800ccb1ecac0bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
-.env
+.env*
# vercel
.vercel
@@ -39,9 +39,3 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
-
-.idea
-
-# binary assets (hosted on CDN)
-assets/assistant.jpg
-.gitattributes
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index a2b0759c0a612c3be02d8c1eb3021252cdd4a7f8..cbe0188aaee92186937765d2c85d76f7b212c537 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,19 @@
FROM node:20-alpine
USER root
-# Install pnpm
-RUN corepack enable && corepack prepare pnpm@latest --activate
-
USER 1000
WORKDIR /usr/src/app
-# Copy package.json and pnpm-lock.yaml to the container
-COPY --chown=1000 package.json pnpm-lock.yaml ./
+# Copy package.json and package-lock.json to the container
+COPY --chown=1000 package.json package-lock.json ./
# Copy the rest of the application files to the container
COPY --chown=1000 . .
-RUN pnpm install
-RUN pnpm run build
+RUN npm install
+RUN npm run build
# Expose the application port (assuming your app runs on port 3000)
-EXPOSE 3001
+EXPOSE 3000
# Start the application
-CMD ["pnpm", "start"]
\ No newline at end of file
+CMD ["npm", "start"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 6b13fd8b95aeb699f979cbc2cbb8d7495aeacdf5..5ab2231fc7dc96070548f1d03ab1d0f73a799600 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,22 @@
---
-title: DeepSite v4
+title: DeepSite v2
emoji: 🐳
colorFrom: blue
colorTo: blue
sdk: docker
pinned: true
-app_port: 3001
+app_port: 3000
license: mit
-failure_strategy: rollback
-short_description: Generate any application by Vibe Coding it
+short_description: Generate any application with DeepSeek
models:
- deepseek-ai/DeepSeek-V3-0324
- - deepseek-ai/DeepSeek-V3.2
- - Qwen/Qwen3-Coder-30B-A3B-Instruct
- - moonshotai/Kimi-K2-Instruct-0905
- - zai-org/GLM-4.7
- - MiniMaxAI/MiniMax-M2.1
+ - deepseek-ai/DeepSeek-R1-0528
---
# DeepSite 🐳
-DeepSite is a Vibe Coding Platform designed to make coding smarter and more efficient. Tailored for developers, data scientists, and AI engineers, it integrates generative AI into your coding projects to enhance creativity and productivity.
+DeepSite is a coding platform powered by DeepSeek AI, designed to make coding smarter and more efficient. Tailored for developers, data scientists, and AI engineers, it integrates generative AI into your coding projects to enhance creativity and productivity.
+
+## How to use it locally
+
+Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
diff --git a/actions/mentions.ts b/actions/mentions.ts
deleted file mode 100644
index c6b7dabfbaa6f0efe0d492dfe5470353b0139bae..0000000000000000000000000000000000000000
--- a/actions/mentions.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import { File } from "@/lib/type";
-
-export const searchMentions = async (query: string) => {
- const promises = [searchModels(query), searchDatasets(query)];
- const results = await Promise.all(promises);
- return { models: results[0], datasets: results[1] };
-};
-
-const searchModels = async (query: string) => {
- const response = await fetch(
- `https://huggingface.co/api/quicksearch?q=${query}&type=model&limit=3`
- );
- const data = await response.json();
- return data?.models ?? [];
-};
-
-const searchDatasets = async (query: string) => {
- const response = await fetch(
- `https://huggingface.co/api/quicksearch?q=${query}&type=dataset&limit=3`
- );
- const data = await response.json();
- return data?.datasets ?? [];
-};
-
-export const searchFilesMentions = async (query: string, files: File[]) => {
- if (!query) return files;
- const lowerQuery = query.toLowerCase();
- return files.filter((file) => file.path.toLowerCase().includes(lowerQuery));
-};
diff --git a/actions/projects.ts b/actions/projects.ts
deleted file mode 100644
index d6f685b63290792fc597438a53eab0a14d6bbf8e..0000000000000000000000000000000000000000
--- a/actions/projects.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-"use server";
-import {
- downloadFile,
- listCommits,
- listFiles,
- listSpaces,
- RepoDesignation,
- SpaceEntry,
- spaceInfo,
-} from "@huggingface/hub";
-
-import { auth } from "@/lib/auth";
-import { Commit, File } from "@/lib/type";
-
-export interface ProjectWithCommits extends SpaceEntry {
- commits?: Commit[];
- medias?: string[];
-}
-
-const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"];
-const IGNORED_FORMATS = [
- ".png",
- ".jpg",
- ".jpeg",
- ".gif",
- ".svg",
- ".webp",
- ".mp4",
- ".mp3",
- ".wav",
-];
-
-export const getProjects = async () => {
- const projects: SpaceEntry[] = [];
- const session = await auth();
- if (!session?.user) {
- return projects;
- }
- const token = session.accessToken;
- for await (const space of listSpaces({
- accessToken: token,
- additionalFields: ["author", "cardData"],
- search: {
- owner: session.user.username,
- },
- })) {
- if (
- space.sdk === "static" &&
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
- (space.cardData as { tags?: string[] })?.tags?.some((tag) =>
- tag.includes("deepsite")
- )
- ) {
- projects.push(space);
- }
- }
- return projects;
-};
-export const getProject = async (id: string, commitId?: string) => {
- const session = await auth();
- if (!session?.user) {
- return null;
- }
- const token = session.accessToken;
- try {
- const project: ProjectWithCommits | null = await spaceInfo({
- name: id,
- accessToken: token,
- additionalFields: ["author", "cardData"],
- });
- const repo: RepoDesignation = {
- type: "space",
- name: id,
- };
- const files: File[] = [];
- const medias: string[] = [];
- const params = { repo, accessToken: token };
- if (commitId) {
- Object.assign(params, { revision: commitId });
- }
- for await (const fileInfo of listFiles(params)) {
- if (IGNORED_PATHS.includes(fileInfo.path)) continue;
- if (IGNORED_FORMATS.some((format) => fileInfo.path.endsWith(format))) {
- medias.push(
- `https://huggingface.co/spaces/${id}/resolve/main/${fileInfo.path}`
- );
- continue;
- }
-
- if (fileInfo.type === "directory") {
- for await (const subFile of listFiles({
- repo,
- accessToken: token,
- path: fileInfo.path,
- })) {
- if (IGNORED_FORMATS.some((format) => subFile.path.endsWith(format))) {
- medias.push(
- `https://huggingface.co/spaces/${id}/resolve/main/${subFile.path}`
- );
- }
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: subFile.path,
- raw: true,
- ...(commitId ? { revision: commitId } : {}),
- }).catch((_) => {
- return null;
- });
- if (!blob) {
- continue;
- }
- const html = await blob?.text();
- if (!html) {
- continue;
- }
- files[subFile.path === "index.html" ? "unshift" : "push"]({
- path: subFile.path,
- content: html,
- });
- }
- } else {
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: fileInfo.path,
- raw: true,
- ...(commitId ? { revision: commitId } : {}),
- }).catch((_) => {
- return null;
- });
- if (!blob) {
- continue;
- }
- const html = await blob?.text();
- if (!html) {
- continue;
- }
- files[fileInfo.path === "index.html" ? "unshift" : "push"]({
- path: fileInfo.path,
- content: html,
- });
- }
- }
- const commits: Commit[] = [];
- const commitIterator = listCommits({ repo, accessToken: token });
- for await (const commit of commitIterator) {
- if (
- commit.title?.toLowerCase() === "initial commit" ||
- commit.title
- ?.toLowerCase()
- ?.includes("upload media files through deepsite")
- )
- continue;
- commits.push({
- title: commit.title,
- oid: commit.oid,
- date: commit.date,
- });
- if (commits.length >= 20) {
- break;
- }
- }
-
- project.commits = commits;
- project.medias = medias;
-
- return { project, files };
- } catch (error) {
- return {
- project: null,
- files: [],
- };
- }
-};
diff --git a/app/(public)/layout.tsx b/app/(public)/layout.tsx
index 0eb2c8fbb85b745d5be01c9e79fa0ebc93a71d00..4a4ec57d2609c783602beb6c06c8dca6a1e6192d 100644
--- a/app/(public)/layout.tsx
+++ b/app/(public)/layout.tsx
@@ -1,12 +1,13 @@
-import { Navigation } from "@/components/public/navigation";
+import Navigation from "@/components/public/navigation";
-export default function PublicLayout({
+export default async function PublicLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
-
+
diff --git a/app/(public)/page.tsx b/app/(public)/page.tsx
index 86970b7a3dab7e60cce3bbb68c747b525baba45c..c0849e72cf29027524ec9ebc3818e80a8aee5ef3 100644
--- a/app/(public)/page.tsx
+++ b/app/(public)/page.tsx
@@ -1,25 +1,44 @@
-import { AnimatedDotsBackground } from "@/components/public/animated-dots-background";
-import { HeroHeader } from "@/components/public/hero-header";
-import { UserProjects } from "@/components/projects/user-projects";
-import { AskAiLanding } from "@/components/ask-ai/ask-ai-landing";
-import { Bento } from "@/components/public/bento";
-
-export const dynamic = "force-dynamic";
-
-export default async function Homepage() {
+import { AskAi } from "@/components/space/ask-ai";
+import { redirect } from "next/navigation";
+export default function Home() {
+ redirect("/projects/new");
return (
<>
-
-
-
-
+
+
+ ✨ DeepSite Public Beta
-
-
+
+ Code your website with AI in seconds
+
+
+ Vibe Coding has never been so easy.
+
+
-
-
-
+
+
+
+
+
+ Deploy your website in seconds
+
+
+
+
+ Features that make you smile
+
+
>
);
}
diff --git a/app/(public)/projects/page.tsx b/app/(public)/projects/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..374dc6b1194256c5b142a62168ce0f414f6098be
--- /dev/null
+++ b/app/(public)/projects/page.tsx
@@ -0,0 +1,13 @@
+import { redirect } from "next/navigation";
+
+import { MyProjects } from "@/components/my-projects";
+import { getProjects } from "@/app/actions/projects";
+
+export default async function ProjectsPage() {
+ const { ok, projects } = await getProjects();
+ if (!ok) {
+ redirect("/");
+ }
+
+ return
;
+}
diff --git a/app/(public)/signin/page.tsx b/app/(public)/signin/page.tsx
deleted file mode 100644
index 3200d0755372c79be57b9204331695cd8f0499ac..0000000000000000000000000000000000000000
--- a/app/(public)/signin/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { LoginButtons } from "@/components/login/login-buttons";
-
-export default async function SignInPage({
- searchParams,
-}: {
- searchParams: Promise<{ callbackUrl: string }>;
-}) {
- const { callbackUrl } = await searchParams;
- console.log(callbackUrl);
- return (
-
-
-
You shall not pass 🧙
-
- You can't access this resource without being signed in.
-
-
-
-
- );
-}
diff --git a/app/[owner]/[repoId]/page.tsx b/app/[owner]/[repoId]/page.tsx
deleted file mode 100644
index 5082396059ae126dbc2be8e0daf30d1ff014f922..0000000000000000000000000000000000000000
--- a/app/[owner]/[repoId]/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { getProject } from "@/actions/projects";
-import { AppEditor } from "@/components/editor";
-import { auth } from "@/lib/auth";
-import { notFound, redirect } from "next/navigation";
-
-export default async function ProjectPage({
- params,
- searchParams,
-}: {
- params: Promise<{ owner: string; repoId: string }>;
- searchParams: Promise<{ commit?: string }>;
-}) {
- const session = await auth();
-
- const { owner, repoId } = await params;
- const { commit } = await searchParams;
- if (!session) {
- redirect(
- `/api/auth/signin?callbackUrl=/${owner}/${repoId}${
- commit ? `?commit=${commit}` : ""
- }`
- );
- }
- const datas = await getProject(`${owner}/${repoId}`, commit);
- if (!datas?.project) {
- return notFound();
- }
- return (
-
- );
-}
diff --git a/app/actions/auth.ts b/app/actions/auth.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a343e65e6726b35b32f022c117d3f3b5187d78e6
--- /dev/null
+++ b/app/actions/auth.ts
@@ -0,0 +1,18 @@
+"use server";
+
+import { headers } from "next/headers";
+
+export async function getAuth() {
+ const authList = await headers();
+ const host = authList.get("host") ?? "localhost:3000";
+ const url = host.includes("/spaces/enzostvs")
+ ? "enzostvs-deepsite.hf.space"
+ : host;
+ const redirect_uri =
+ `${host.includes("localhost") ? "http://" : "https://"}` +
+ url +
+ "/auth/callback";
+
+ const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
+ return loginRedirectUrl;
+}
diff --git a/app/actions/projects.ts b/app/actions/projects.ts
new file mode 100644
index 0000000000000000000000000000000000000000..209b16d9d9960eeafb9e0b02d7b1b3eda638338d
--- /dev/null
+++ b/app/actions/projects.ts
@@ -0,0 +1,63 @@
+"use server";
+
+import { isAuthenticated } from "@/lib/auth";
+import { NextResponse } from "next/server";
+import dbConnect from "@/lib/mongodb";
+import Project from "@/models/Project";
+import { Project as ProjectType } from "@/types";
+
+export async function getProjects(): Promise<{
+ ok: boolean;
+ projects: ProjectType[];
+}> {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return {
+ ok: false,
+ projects: [],
+ };
+ }
+
+ await dbConnect();
+ const projects = await Project.find({
+ user_id: user?.id,
+ })
+ .sort({ _createdAt: -1 })
+ .limit(100)
+ .lean();
+ if (!projects) {
+ return {
+ ok: false,
+ projects: [],
+ };
+ }
+ return {
+ ok: true,
+ projects: JSON.parse(JSON.stringify(projects)) as ProjectType[],
+ };
+}
+
+export async function getProject(
+ namespace: string,
+ repoId: string
+): Promise
{
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return null;
+ }
+
+ await dbConnect();
+ const project = await Project.findOne({
+ user_id: user.id,
+ namespace,
+ repoId,
+ }).lean();
+
+ if (!project) {
+ return null;
+ }
+
+ return JSON.parse(JSON.stringify(project)) as ProjectType;
+}
diff --git a/app/actions/rewrite-prompt.ts b/app/actions/rewrite-prompt.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08bec54038b85c24c3f0d993ec388344d054fd09
--- /dev/null
+++ b/app/actions/rewrite-prompt.ts
@@ -0,0 +1,35 @@
+import { InferenceClient } from "@huggingface/inference";
+
+const START_REWRITE_PROMPT = ">>>>>>> START PROMPT >>>>>>";
+const END_REWRITE_PROMPT = ">>>>>>> END PROMPT >>>>>>";
+
+export const callAiRewritePrompt = async (prompt: string, { token, billTo }: { token: string, billTo?: string | null }) => {
+ const client = new InferenceClient(token);
+ const response = await client.chatCompletion(
+ {
+ model: "deepseek-ai/DeepSeek-V3.1",
+ provider: "novita",
+ messages: [{
+ role: "system",
+ content: `You are a helpful assistant that rewrites prompts to make them better. All the prompts will be about creating a website or app.
+Try to make the prompt more detailed and specific to create a good UI/UX Design and good code.
+Format the result by following this format:
+${START_REWRITE_PROMPT}
+new prompt here
+${END_REWRITE_PROMPT}
+If you don't rewrite the prompt, return the original prompt.
+Make sure to return the prompt in the same language as the prompt you are given. Also IMPORTANT: Make sure to keep the original intent of the prompt. Improve it it needed, but don't change the original intent.
+`
+ },{ role: "user", content: prompt }],
+ },
+ billTo ? { billTo } : {}
+ );
+
+ const responseContent = response.choices[0]?.message?.content;
+ if (!responseContent) {
+ return prompt;
+ }
+ const startIndex = responseContent.indexOf(START_REWRITE_PROMPT);
+ const endIndex = responseContent.indexOf(END_REWRITE_PROMPT);
+ return responseContent.substring(startIndex + START_REWRITE_PROMPT.length, endIndex);
+};
\ No newline at end of file
diff --git a/app/api/ask-ai/route.ts b/app/api/ask-ai/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..25a34108f99e8e3d0c6ccc00f345055445abcb47
--- /dev/null
+++ b/app/api/ask-ai/route.ts
@@ -0,0 +1,510 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { NextRequest } from "next/server";
+import { NextResponse } from "next/server";
+import { headers } from "next/headers";
+import { InferenceClient } from "@huggingface/inference";
+
+import { MODELS, PROVIDERS } from "@/lib/providers";
+import {
+ DIVIDER,
+ FOLLOW_UP_SYSTEM_PROMPT,
+ INITIAL_SYSTEM_PROMPT,
+ MAX_REQUESTS_PER_IP,
+ NEW_PAGE_END,
+ NEW_PAGE_START,
+ REPLACE_END,
+ SEARCH_START,
+ UPDATE_PAGE_START,
+ UPDATE_PAGE_END,
+} from "@/lib/prompts";
+import MY_TOKEN_KEY from "@/lib/get-cookie-name";
+import { Page } from "@/types";
+
+const ipAddresses = new Map();
+
+export async function POST(request: NextRequest) {
+ const authHeaders = await headers();
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
+
+ const body = await request.json();
+ const { prompt, provider, model, redesignMarkdown, previousPrompts, pages } = body;
+
+ if (!model || (!prompt && !redesignMarkdown)) {
+ return NextResponse.json(
+ { ok: false, error: "Missing required fields" },
+ { status: 400 }
+ );
+ }
+
+ const selectedModel = MODELS.find(
+ (m) => m.value === model || m.label === model
+ );
+
+ if (!selectedModel) {
+ return NextResponse.json(
+ { ok: false, error: "Invalid model selected" },
+ { status: 400 }
+ );
+ }
+
+ if (!selectedModel.providers.includes(provider) && provider !== "auto") {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: `The selected model does not support the ${provider} provider.`,
+ openSelectProvider: true,
+ },
+ { status: 400 }
+ );
+ }
+
+ let token = userToken;
+ let billTo: string | null = null;
+
+ /**
+ * Handle local usage token, this bypass the need for a user token
+ * and allows local testing without authentication.
+ * This is useful for development and testing purposes.
+ */
+ if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
+ token = process.env.HF_TOKEN;
+ }
+
+ const ip = authHeaders.get("x-forwarded-for")?.includes(",")
+ ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
+ : authHeaders.get("x-forwarded-for");
+
+ if (!token) {
+ ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
+ if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
+ return NextResponse.json(
+ {
+ ok: false,
+ openLogin: true,
+ message: "Log In to continue using the service",
+ },
+ { status: 429 }
+ );
+ }
+
+ token = process.env.DEFAULT_HF_TOKEN as string;
+ billTo = "huggingface";
+ }
+
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
+ const selectedProvider =
+ provider === "auto"
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
+
+ const rewrittenPrompt = prompt;
+
+ // if (prompt?.length < 240) {
+
+ //rewrittenPrompt = await callAiRewritePrompt(prompt, { token, billTo });
+ // }
+
+ try {
+ const encoder = new TextEncoder();
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+
+ const response = new NextResponse(stream.readable, {
+ headers: {
+ "Content-Type": "text/plain; charset=utf-8",
+ "Cache-Control": "no-cache",
+ Connection: "keep-alive",
+ },
+ });
+
+ (async () => {
+ // let completeResponse = "";
+ try {
+ const client = new InferenceClient(token);
+ const chatCompletion = client.chatCompletionStream(
+ {
+ model: selectedModel.value,
+ provider: selectedProvider.id as any,
+ messages: [
+ {
+ role: "system",
+ content: INITIAL_SYSTEM_PROMPT,
+ },
+ ...(pages?.length > 1 ? [{
+ role: "assistant",
+ content: `Here are the current pages:\n\n${pages.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}\n\nNow, please create a new page based on this code. Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
+ }] : []),
+ {
+ role: "user",
+ content: redesignMarkdown
+ ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
+ : rewrittenPrompt,
+ },
+ ],
+ max_tokens: selectedProvider.max_tokens,
+ },
+ billTo ? { billTo } : {}
+ );
+
+ while (true) {
+ const { done, value } = await chatCompletion.next();
+ if (done) {
+ break;
+ }
+
+ const chunk = value.choices[0]?.delta?.content;
+ if (chunk) {
+ await writer.write(encoder.encode(chunk));
+ }
+ }
+ } catch (error: any) {
+ if (error.message?.includes("exceeded your monthly included credits")) {
+ await writer.write(
+ encoder.encode(
+ JSON.stringify({
+ ok: false,
+ openProModal: true,
+ message: error.message,
+ })
+ )
+ );
+ } else {
+ await writer.write(
+ encoder.encode(
+ JSON.stringify({
+ ok: false,
+ message:
+ error.message ||
+ "An error occurred while processing your request.",
+ })
+ )
+ );
+ }
+ } finally {
+ await writer?.close();
+ }
+ })();
+
+ return response;
+ } catch (error: any) {
+ return NextResponse.json(
+ {
+ ok: false,
+ openSelectProvider: true,
+ message:
+ error?.message || "An error occurred while processing your request.",
+ },
+ { status: 500 }
+ );
+ }
+}
+
+export async function PUT(request: NextRequest) {
+ const authHeaders = await headers();
+ const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
+
+ const body = await request.json();
+ const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, } =
+ body;
+
+ if (!prompt || pages.length === 0) {
+ return NextResponse.json(
+ { ok: false, error: "Missing required fields" },
+ { status: 400 }
+ );
+ }
+
+ const selectedModel = MODELS.find(
+ (m) => m.value === model || m.label === model
+ );
+ if (!selectedModel) {
+ return NextResponse.json(
+ { ok: false, error: "Invalid model selected" },
+ { status: 400 }
+ );
+ }
+
+ let token = userToken;
+ let billTo: string | null = null;
+
+ /**
+ * Handle local usage token, this bypass the need for a user token
+ * and allows local testing without authentication.
+ * This is useful for development and testing purposes.
+ */
+ if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
+ token = process.env.HF_TOKEN;
+ }
+
+ const ip = authHeaders.get("x-forwarded-for")?.includes(",")
+ ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
+ : authHeaders.get("x-forwarded-for");
+
+ if (!token) {
+ ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
+ if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
+ return NextResponse.json(
+ {
+ ok: false,
+ openLogin: true,
+ message: "Log In to continue using the service",
+ },
+ { status: 429 }
+ );
+ }
+
+ token = process.env.DEFAULT_HF_TOKEN as string;
+ billTo = "huggingface";
+ }
+
+ const client = new InferenceClient(token);
+
+ const DEFAULT_PROVIDER = PROVIDERS.novita;
+ const selectedProvider =
+ provider === "auto"
+ ? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
+ : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
+
+ try {
+ const response = await client.chatCompletion(
+ {
+ model: selectedModel.value,
+ provider: selectedProvider.id as any,
+ messages: [
+ {
+ role: "system",
+ content: FOLLOW_UP_SYSTEM_PROMPT,
+ },
+ {
+ role: "user",
+ content: previousPrompts
+ ? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
+ : "You are modifying the HTML file based on the user's request.",
+ },
+ {
+ role: "assistant",
+
+ content: `${
+ selectedElementHtml
+ ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
+ : ""
+ }. Current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}. ${files?.length > 0 ? `Current images: ${files?.map((f: string) => `- ${f}`).join("\n")}.` : ""}`,
+ },
+ {
+ role: "user",
+ content: prompt,
+ },
+ ],
+ ...(selectedProvider.id !== "sambanova"
+ ? {
+ max_tokens: selectedProvider.max_tokens,
+ }
+ : {}),
+ },
+ billTo ? { billTo } : {}
+ );
+
+ const chunk = response.choices[0]?.message?.content;
+ if (!chunk) {
+ return NextResponse.json(
+ { ok: false, message: "No content returned from the model" },
+ { status: 400 }
+ );
+ }
+
+ if (chunk) {
+ const updatedLines: number[][] = [];
+ let newHtml = "";
+ const updatedPages = [...(pages || [])];
+
+ const updatePageRegex = new RegExp(`${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
+ let updatePageMatch;
+
+ while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
+ const [, pagePath, pageContent] = updatePageMatch;
+
+ const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
+ if (pageIndex !== -1) {
+ let pageHtml = updatedPages[pageIndex].html;
+
+ let processedContent = pageContent;
+ const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
+ if (htmlMatch) {
+ processedContent = htmlMatch[1];
+ }
+ let position = 0;
+ let moreBlocks = true;
+
+ while (moreBlocks) {
+ const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
+ if (searchStartIndex === -1) {
+ moreBlocks = false;
+ continue;
+ }
+
+ const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
+ if (dividerIndex === -1) {
+ moreBlocks = false;
+ continue;
+ }
+
+ const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
+ if (replaceEndIndex === -1) {
+ moreBlocks = false;
+ continue;
+ }
+
+ const searchBlock = processedContent.substring(
+ searchStartIndex + SEARCH_START.length,
+ dividerIndex
+ );
+ const replaceBlock = processedContent.substring(
+ dividerIndex + DIVIDER.length,
+ replaceEndIndex
+ );
+
+ if (searchBlock.trim() === "") {
+ pageHtml = `${replaceBlock}\n${pageHtml}`;
+ updatedLines.push([1, replaceBlock.split("\n").length]);
+ } else {
+ const blockPosition = pageHtml.indexOf(searchBlock);
+ if (blockPosition !== -1) {
+ const beforeText = pageHtml.substring(0, blockPosition);
+ const startLineNumber = beforeText.split("\n").length;
+ const replaceLines = replaceBlock.split("\n").length;
+ const endLineNumber = startLineNumber + replaceLines - 1;
+
+ updatedLines.push([startLineNumber, endLineNumber]);
+ pageHtml = pageHtml.replace(searchBlock, replaceBlock);
+ }
+ }
+
+ position = replaceEndIndex + REPLACE_END.length;
+ }
+
+ updatedPages[pageIndex].html = pageHtml;
+
+ if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
+ newHtml = pageHtml;
+ }
+ }
+ }
+
+ const newPageRegex = new RegExp(`${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_PAGE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_PAGE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
+ let newPageMatch;
+
+ while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
+ const [, pagePath, pageContent] = newPageMatch;
+
+ let pageHtml = pageContent;
+ const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
+ if (htmlMatch) {
+ pageHtml = htmlMatch[1];
+ }
+
+ const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
+
+ if (existingPageIndex !== -1) {
+ updatedPages[existingPageIndex] = {
+ path: pagePath,
+ html: pageHtml.trim()
+ };
+ } else {
+ updatedPages.push({
+ path: pagePath,
+ html: pageHtml.trim()
+ });
+ }
+ }
+
+ if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
+ let position = 0;
+ let moreBlocks = true;
+
+ while (moreBlocks) {
+ const searchStartIndex = chunk.indexOf(SEARCH_START, position);
+ if (searchStartIndex === -1) {
+ moreBlocks = false;
+ continue;
+ }
+
+ const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
+ if (dividerIndex === -1) {
+ moreBlocks = false;
+ continue;
+ }
+
+ const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
+ if (replaceEndIndex === -1) {
+ moreBlocks = false;
+ continue;
+ }
+
+ const searchBlock = chunk.substring(
+ searchStartIndex + SEARCH_START.length,
+ dividerIndex
+ );
+ const replaceBlock = chunk.substring(
+ dividerIndex + DIVIDER.length,
+ replaceEndIndex
+ );
+
+ if (searchBlock.trim() === "") {
+ newHtml = `${replaceBlock}\n${newHtml}`;
+ updatedLines.push([1, replaceBlock.split("\n").length]);
+ } else {
+ const blockPosition = newHtml.indexOf(searchBlock);
+ if (blockPosition !== -1) {
+ const beforeText = newHtml.substring(0, blockPosition);
+ const startLineNumber = beforeText.split("\n").length;
+ const replaceLines = replaceBlock.split("\n").length;
+ const endLineNumber = startLineNumber + replaceLines - 1;
+
+ updatedLines.push([startLineNumber, endLineNumber]);
+ newHtml = newHtml.replace(searchBlock, replaceBlock);
+ }
+ }
+
+ position = replaceEndIndex + REPLACE_END.length;
+ }
+
+ // Update the main HTML if it's the index page
+ const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
+ if (mainPageIndex !== -1) {
+ updatedPages[mainPageIndex].html = newHtml;
+ }
+ }
+
+ return NextResponse.json({
+ ok: true,
+ updatedLines,
+ pages: updatedPages,
+ });
+ } else {
+ return NextResponse.json(
+ { ok: false, message: "No content returned from the model" },
+ { status: 400 }
+ );
+ }
+ } catch (error: any) {
+ if (error.message?.includes("exceeded your monthly included credits")) {
+ return NextResponse.json(
+ {
+ ok: false,
+ openProModal: true,
+ message: error.message,
+ },
+ { status: 402 }
+ );
+ }
+ return NextResponse.json(
+ {
+ ok: false,
+ openSelectProvider: true,
+ message:
+ error.message || "An error occurred while processing your request.",
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/ask/route.ts b/app/api/ask/route.ts
deleted file mode 100644
index 293e8244308cc1191e32b3ef6b30e93ccb963fcf..0000000000000000000000000000000000000000
--- a/app/api/ask/route.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { NextResponse } from "next/server";
-import { InferenceClient } from "@huggingface/inference";
-
-import { FOLLOW_UP_SYSTEM_PROMPT, INITIAL_SYSTEM_PROMPT } from "@/lib/prompts";
-import { auth } from "@/lib/auth";
-import { File, Message } from "@/lib/type";
-import { DEFAULT_MODEL, MODELS } from "@/lib/providers";
-
-export async function POST(request: Request) {
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const {
- prompt,
- previousMessages = [],
- files = [],
- provider,
- model,
- redesignMd,
- medias,
- } = body;
-
- if (!prompt) {
- return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
- }
- if (!model || !MODELS.find((m: (typeof MODELS)[0]) => m.value === model)) {
- return NextResponse.json({ error: "Model is required" }, { status: 400 });
- }
-
- const client = new InferenceClient(token);
-
- try {
- const encoder = new TextEncoder();
- const stream = new TransformStream();
- const writer = stream.writable.getWriter();
-
- const response = new NextResponse(stream.readable, {
- headers: {
- "Content-Type": "text/plain; charset=utf-8",
- "Cache-Control": "no-cache",
- Connection: "keep-alive",
- },
- });
- (async () => {
- let hasRetried = false;
- let currentModel = model;
-
- const tryGeneration = async (): Promise => {
- try {
- const chatCompletion = client.chatCompletionStream({
- model: currentModel + (provider !== "auto" ? `:${provider}` : ""),
- messages: [
- {
- role: "system",
- content:
- files.length > 0
- ? FOLLOW_UP_SYSTEM_PROMPT
- : INITIAL_SYSTEM_PROMPT,
- },
- ...previousMessages.map((message: Message) => ({
- role: message.role,
- content: message.content,
- })),
- ...(files?.length > 0
- ? [
- {
- role: "user",
- content: `Here are the files that the user has provider:${files
- .map(
- (file: File) =>
- `File: ${file.path}\nContent: ${file.content}`
- )
- .join("\n")}\n\n${prompt}`,
- },
- ]
- : []),
- {
- role: "user",
- content: `${
- redesignMd?.url &&
- `Redesign the following website ${redesignMd.url}, try to use the same images and content, but you can still improve it if needed. Do the best version possibile. Here is the markdown:\n ${redesignMd.md} \n\n`
- }${prompt} ${
- medias && medias.length > 0
- ? `\nHere is the list of my media files: ${medias.join(
- ", "
- )}\n`
- : ""
- }`,
- }
- ],
- stream: true,
- max_tokens: 16_000,
- });
- while (true) {
- const { done, value } = await chatCompletion.next();
- if (done) {
- break;
- }
-
- const chunk = value.choices[0]?.delta?.content;
- if (chunk) {
- await writer.write(encoder.encode(chunk));
- }
- }
-
- await writer.close();
- } catch (error) {
- const errorMessage =
- error instanceof Error
- ? error.message
- : "An error occurred while processing your request";
-
- if (
- !hasRetried &&
- errorMessage?.includes(
- "Failed to perform inference: Model not found"
- )
- ) {
- hasRetried = true;
- if (model === DEFAULT_MODEL) {
- const availableFallbackModels = MODELS.filter(
- (m) => m.value !== model
- );
- const randomIndex = Math.floor(
- Math.random() * availableFallbackModels.length
- );
- currentModel = availableFallbackModels[randomIndex];
- } else {
- currentModel = DEFAULT_MODEL;
- }
- const switchMessage = `\n\n_Note: The selected model was not available. Switched to \`${currentModel}\`._\n\n`;
- await writer.write(encoder.encode(switchMessage));
-
- return tryGeneration();
- }
-
- try {
- let errorPayload = "";
- if (
- errorMessage?.includes("exceeded your monthly included credits") ||
- errorMessage?.includes("reached the free monthly usage limit")
- ) {
- errorPayload = JSON.stringify({
- messageError: errorMessage,
- showProMessage: true,
- isError: true,
- });
- } else {
- errorPayload = JSON.stringify({
- messageError: errorMessage,
- isError: true,
- });
- }
- await writer.write(encoder.encode(`\n\n__ERROR__:${errorPayload}`));
- await writer.close();
- } catch (closeError) {
- console.error("Failed to send error message:", closeError);
- try {
- await writer.abort(error);
- } catch (abortError) {
- console.error("Failed to abort writer:", abortError);
- }
- }
- }
- };
-
- await tryGeneration();
- })();
-
- return response;
- } catch (error) {
- return NextResponse.json(
- {
- error: error instanceof Error ? error.message : "Internal Server Error",
- },
- { status: 500 }
- );
- }
-}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
deleted file mode 100644
index 7b38c1bb45e38527a2c595c07df5e70d38ada7d9..0000000000000000000000000000000000000000
--- a/app/api/auth/[...nextauth]/route.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import NextAuth from "next-auth";
-import { authOptions } from "@/lib/auth";
-
-const handler = NextAuth(authOptions);
-
-export { handler as GET, handler as POST };
diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..221e0c266bd2e6673779b9e81caf333243147f60
--- /dev/null
+++ b/app/api/auth/route.ts
@@ -0,0 +1,86 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function POST(req: NextRequest) {
+ const body = await req.json();
+ const { code } = body;
+
+ if (!code) {
+ return NextResponse.json(
+ { error: "Code is required" },
+ {
+ status: 400,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ }
+
+ const Authorization = `Basic ${Buffer.from(
+ `${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
+ ).toString("base64")}`;
+
+ const host =
+ req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
+
+ const url = host.includes("/spaces/enzostvs")
+ ? "enzostvs-deepsite.hf.space"
+ : host;
+ const redirect_uri =
+ `${host.includes("localhost") ? "http://" : "https://"}` +
+ url +
+ "/auth/callback";
+ const request_auth = await fetch("https://huggingface.co/oauth/token", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ Authorization,
+ },
+ body: new URLSearchParams({
+ grant_type: "authorization_code",
+ code,
+ redirect_uri,
+ }),
+ });
+
+ const response = await request_auth.json();
+ if (!response.access_token) {
+ return NextResponse.json(
+ { error: "Failed to retrieve access token" },
+ {
+ status: 400,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ }
+
+ const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
+ headers: {
+ Authorization: `Bearer ${response.access_token}`,
+ },
+ });
+
+ if (!userResponse.ok) {
+ return NextResponse.json(
+ { user: null, errCode: userResponse.status },
+ { status: userResponse.status }
+ );
+ }
+ const user = await userResponse.json();
+
+ return NextResponse.json(
+ {
+ access_token: response.access_token,
+ expires_in: response.expires_in,
+ user,
+ },
+ {
+ status: 200,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+}
diff --git a/app/api/healthcheck/route.ts b/app/api/healthcheck/route.ts
deleted file mode 100644
index 35adb415d0f577a7a78dbc338bd6450e78cc9ec3..0000000000000000000000000000000000000000
--- a/app/api/healthcheck/route.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { NextResponse } from "next/server";
-
-export async function GET() {
- return NextResponse.json({ status: "ok" }, { status: 200 });
-}
diff --git a/app/api/me/projects/[namespace]/[repoId]/images/route.ts b/app/api/me/projects/[namespace]/[repoId]/images/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4ab46939bdde0aaeb7163af0e96e94651bfccfd7
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/images/route.ts
@@ -0,0 +1,111 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, uploadFiles } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import Project from "@/models/Project";
+import dbConnect from "@/lib/mongodb";
+
+// No longer need the ImageUpload interface since we're handling FormData with File objects
+
+export async function POST(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ try {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ await dbConnect();
+ const param = await params;
+ const { namespace, repoId } = param;
+
+ const project = await Project.findOne({
+ user_id: user.id,
+ space_id: `${namespace}/${repoId}`,
+ }).lean();
+
+ if (!project) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Project not found",
+ },
+ { status: 404 }
+ );
+ }
+
+ // Parse the FormData to get the images
+ const formData = await req.formData();
+ const imageFiles = formData.getAll("images") as File[];
+
+ if (!imageFiles || imageFiles.length === 0) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "At least one image file is required under the 'images' key",
+ },
+ { status: 400 }
+ );
+ }
+
+ const files: File[] = [];
+ for (const file of imageFiles) {
+ if (!(file instanceof File)) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Invalid file format - all items under 'images' key must be files",
+ },
+ { status: 400 }
+ );
+ }
+
+ if (!file.type.startsWith('image/')) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: `File ${file.name} is not an image`,
+ },
+ { status: 400 }
+ );
+ }
+
+ // Create File object with images/ folder prefix
+ const fileName = `images/${file.name}`;
+ const processedFile = new File([file], fileName, { type: file.type });
+ files.push(processedFile);
+ }
+
+ // Upload files to HuggingFace space
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ await uploadFiles({
+ repo,
+ files,
+ accessToken: user.token as string,
+ commitTitle: `Upload ${files.length} image(s)`,
+ });
+
+ return NextResponse.json({
+ ok: true,
+ message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
+ uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
+ }, { status: 200 });
+
+ } catch (error) {
+ console.error('Error uploading images:', error);
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Failed to upload images",
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/me/projects/[namespace]/[repoId]/route.ts b/app/api/me/projects/[namespace]/[repoId]/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..feecdd6215cc28e8bf952448636d781dff372d5a
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/route.ts
@@ -0,0 +1,276 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, spaceInfo, uploadFiles, listFiles } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import Project from "@/models/Project";
+import dbConnect from "@/lib/mongodb";
+import { Page } from "@/types";
+
+export async function GET(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ await dbConnect();
+ const param = await params;
+ const { namespace, repoId } = param;
+
+ const project = await Project.findOne({
+ user_id: user.id,
+ space_id: `${namespace}/${repoId}`,
+ }).lean();
+ if (!project) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Project not found",
+ },
+ { status: 404 }
+ );
+ }
+ try {
+ const space = await spaceInfo({
+ name: namespace + "/" + repoId,
+ accessToken: user.token as string,
+ additionalFields: ["author"],
+ });
+
+ if (!space || space.sdk !== "static") {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Space is not a static space",
+ },
+ { status: 404 }
+ );
+ }
+ if (space.author !== user.name) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Space does not belong to the authenticated user",
+ },
+ { status: 403 }
+ );
+ }
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ const htmlFiles: Page[] = [];
+ const images: string[] = [];
+
+ const allowedImagesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
+
+ for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
+ if (fileInfo.path.endsWith(".html")) {
+ const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`);
+ if (res.ok) {
+ const html = await res.text();
+ if (fileInfo.path === "index.html") {
+ htmlFiles.unshift({
+ path: fileInfo.path,
+ html,
+ });
+ } else {
+ htmlFiles.push({
+ path: fileInfo.path,
+ html,
+ });
+ }
+ }
+ }
+ if (fileInfo.type === "directory" && fileInfo.path === "images") {
+ for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
+ if (allowedImagesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
+ images.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
+ }
+ }
+ }
+ }
+
+ if (htmlFiles.length === 0) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "No HTML files found",
+ },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json(
+ {
+ project: {
+ ...project,
+ pages: htmlFiles,
+ images,
+ },
+ ok: true,
+ },
+ { status: 200 }
+ );
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ if (error.statusCode === 404) {
+ await Project.deleteOne({
+ user_id: user.id,
+ space_id: `${namespace}/${repoId}`,
+ });
+ return NextResponse.json(
+ { error: "Space not found", ok: false },
+ { status: 404 }
+ );
+ }
+ return NextResponse.json(
+ { error: error.message, ok: false },
+ { status: 500 }
+ );
+ }
+}
+
+export async function PUT(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ await dbConnect();
+ const param = await params;
+ const { namespace, repoId } = param;
+ const { pages, prompts } = await req.json();
+
+ const project = await Project.findOne({
+ user_id: user.id,
+ space_id: `${namespace}/${repoId}`,
+ }).lean();
+ if (!project) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Project not found",
+ },
+ { status: 404 }
+ );
+ }
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ const files: File[] = [];
+ const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
+ type: "text/plain",
+ });
+ files.push(promptsFile);
+ pages.forEach((page: Page) => {
+ const file = new File([page.html], page.path, { type: "text/html" });
+ files.push(file);
+ });
+ await uploadFiles({
+ repo,
+ files,
+ accessToken: user.token as string,
+ commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
+ });
+
+ await Project.updateOne(
+ { user_id: user.id, space_id: `${namespace}/${repoId}` },
+ {
+ $set: {
+ prompts: [
+ ...prompts,
+ ],
+ },
+ }
+ );
+ return NextResponse.json({ ok: true }, { status: 200 });
+}
+
+export async function POST(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ await dbConnect();
+ const param = await params;
+ const { namespace, repoId } = param;
+
+ const space = await spaceInfo({
+ name: namespace + "/" + repoId,
+ accessToken: user.token as string,
+ additionalFields: ["author"],
+ });
+
+ if (!space || space.sdk !== "static") {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Space is not a static space",
+ },
+ { status: 404 }
+ );
+ }
+ if (space.author !== user.name) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Space does not belong to the authenticated user",
+ },
+ { status: 403 }
+ );
+ }
+
+ const project = await Project.findOne({
+ user_id: user.id,
+ space_id: `${namespace}/${repoId}`,
+ }).lean();
+ if (project) {
+ // redirect to the project page if it already exists
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Project already exists",
+ redirect: `/projects/${namespace}/${repoId}`,
+ },
+ { status: 400 }
+ );
+ }
+
+ const newProject = new Project({
+ user_id: user.id,
+ space_id: `${namespace}/${repoId}`,
+ prompts: [],
+ });
+
+ await newProject.save();
+ return NextResponse.json(
+ {
+ ok: true,
+ project: {
+ id: newProject._id,
+ space_id: newProject.space_id,
+ prompts: newProject.prompts,
+ },
+ },
+ { status: 201 }
+ );
+}
diff --git a/app/api/me/projects/route.ts b/app/api/me/projects/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cc3a3907101a1c7c79a7e1968b508936fa0668e4
--- /dev/null
+++ b/app/api/me/projects/route.ts
@@ -0,0 +1,127 @@
+import { NextRequest, NextResponse } from "next/server";
+import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import Project from "@/models/Project";
+import dbConnect from "@/lib/mongodb";
+import { COLORS } from "@/lib/utils";
+import { Page } from "@/types";
+
+export async function GET() {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ await dbConnect();
+
+ const projects = await Project.find({
+ user_id: user?.id,
+ })
+ .sort({ _createdAt: -1 })
+ .limit(100)
+ .lean();
+ if (!projects) {
+ return NextResponse.json(
+ {
+ ok: false,
+ projects: [],
+ },
+ { status: 404 }
+ );
+ }
+ return NextResponse.json(
+ {
+ ok: true,
+ projects,
+ },
+ { status: 200 }
+ );
+}
+
+export async function POST(request: NextRequest) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const { title, pages, prompts } = await request.json();
+
+ if (!title || !pages || pages.length === 0) {
+ return NextResponse.json(
+ { message: "Title and HTML content are required.", ok: false },
+ { status: 400 }
+ );
+ }
+
+ await dbConnect();
+
+ try {
+ let readme = "";
+
+ const newTitle = title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .split("-")
+ .filter(Boolean)
+ .join("-")
+ .slice(0, 96);
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${user.name}/${newTitle}`,
+ };
+
+ const { repoUrl } = await createRepo({
+ repo,
+ accessToken: user.token as string,
+ });
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
+ readme = `---
+title: ${newTitle}
+emoji: 🐳
+colorFrom: ${colorFrom}
+colorTo: ${colorTo}
+sdk: static
+pinned: false
+tags:
+ - deepsite
+---
+
+Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
+
+ const readmeFile = new File([readme], "README.md", {
+ type: "text/markdown",
+ });
+ const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
+ type: "text/plain",
+ });
+ const files = [readmeFile, promptsFile];
+ pages.forEach((page: Page) => {
+ const file = new File([page.html], page.path, { type: "text/html" });
+ files.push(file);
+ });
+ await uploadFiles({
+ repo,
+ files,
+ accessToken: user.token as string,
+ commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
+ });
+ const path = repoUrl.split("/").slice(-2).join("/");
+ const project = await Project.create({
+ user_id: user.id,
+ space_id: path,
+ prompts,
+ });
+ return NextResponse.json({ project, path, ok: true }, { status: 201 });
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (err: any) {
+ return NextResponse.json(
+ { error: err.message, ok: false },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/me/route.ts b/app/api/me/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c4164daba5c58bb2fe7f4f7508de7165f32ca443
--- /dev/null
+++ b/app/api/me/route.ts
@@ -0,0 +1,25 @@
+import { headers } from "next/headers";
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ const authHeaders = await headers();
+ const token = authHeaders.get("Authorization");
+ if (!token) {
+ return NextResponse.json({ user: null, errCode: 401 }, { status: 401 });
+ }
+
+ const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
+ headers: {
+ Authorization: `${token}`,
+ },
+ });
+
+ if (!userResponse.ok) {
+ return NextResponse.json(
+ { user: null, errCode: userResponse.status },
+ { status: userResponse.status }
+ );
+ }
+ const user = await userResponse.json();
+ return NextResponse.json({ user, errCode: null }, { status: 200 });
+}
diff --git a/app/api/projects/[repoId]/[commitId]/route.ts b/app/api/projects/[repoId]/[commitId]/route.ts
deleted file mode 100644
index 0362d6c43ec226bf97837f2f88196ad43eb056f2..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/[commitId]/route.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { auth } from "@/lib/auth";
-import { createBranch, RepoDesignation } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function POST(
- request: Request,
- { params }: { params: Promise<{ repoId: string; commitId: string }> }
-) {
- const { repoId, commitId }: { repoId: string; commitId: string } =
- await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const commitTitle = `🔖 ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Set commit ${commitId} as default.`;
-
- await fetch(
- `https://huggingface.co/api/spaces/${session.user?.username}/${repoId}/branch/main`,
- {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- startingPoint: commitId,
- overwrite: true,
- }),
- }
- ).catch((error) => {
- return NextResponse.json(
- { error: error ?? "Failed to create branch" },
- { status: 500 }
- );
- });
-
- return NextResponse.json({ success: true }, { status: 200 });
-}
diff --git a/app/api/projects/[repoId]/download/route.ts b/app/api/projects/[repoId]/download/route.ts
deleted file mode 100644
index 43e8f6638d5eb2aca8ba3069d696b05056d96633..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/download/route.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { auth } from "@/lib/auth";
-import { downloadFile, listFiles, RepoDesignation } from "@huggingface/hub";
-import { NextResponse } from "next/server";
-import JSZip from "jszip";
-
-export async function GET(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- try {
- const zip = new JSZip();
- for await (const fileInfo of listFiles({
- repo,
- accessToken: token as string,
- recursive: true,
- })) {
- if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) {
- continue;
- }
-
- try {
- const blob = await downloadFile({
- repo,
- accessToken: token as string,
- path: fileInfo.path,
- raw: true
- }).catch((error) => {
- return null;
- });
- if (!blob) {
- continue;
- }
-
- if (blob) {
- const arrayBuffer = await blob.arrayBuffer();
- zip.file(fileInfo.path, arrayBuffer);
- }
- } catch (error) {
- console.error(`Error downloading file ${fileInfo.path}:`, error);
- }
- }
-
- const zipBlob = await zip.generateAsync({
- type: "blob",
- compression: "DEFLATE",
- compressionOptions: {
- level: 6
- }
- });
-
- const projectName = `${session.user?.username}-${repoId}`.replace(/[^a-zA-Z0-9-_]/g, '_');
- const filename = `${projectName}.zip`;
-
- return new NextResponse(zipBlob, {
- headers: {
- "Content-Type": "application/zip",
- "Content-Disposition": `attachment; filename="${filename}"`,
- "Content-Length": zipBlob.size.toString(),
- },
- });
- } catch (error) {
- console.error("Error downloading project:", error);
- return NextResponse.json({ error: "Failed to download project" }, { status: 500 });
- }
-}
\ No newline at end of file
diff --git a/app/api/projects/[repoId]/medias/route.ts b/app/api/projects/[repoId]/medias/route.ts
deleted file mode 100644
index 2040623536ab6775152177a317e0b11dc6cae3a2..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/medias/route.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { auth } from "@/lib/auth";
-import { RepoDesignation, uploadFiles } from "@huggingface/hub";
-import { NextResponse } from "next/server";
-
-export async function POST(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const formData = await request.formData();
- const newMedias = formData.getAll("images") as File[];
-
- const filesToUpload: File[] = [];
-
- if (!newMedias || newMedias.length === 0) {
- return NextResponse.json(
- {
- ok: false,
- error: "At least one media file is required under the 'images' key",
- },
- { status: 400 }
- );
- }
-
- try {
- for (const media of newMedias) {
- const isImage = media.type.startsWith("image/");
- const isVideo = media.type.startsWith("video/");
- const isAudio = media.type.startsWith("audio/");
-
- const folderPath = isImage
- ? "images/"
- : isVideo
- ? "videos/"
- : isAudio
- ? "audios/"
- : null;
-
- if (!folderPath) {
- return NextResponse.json(
- { ok: false, error: "Unsupported media type: " + media.type },
- { status: 400 }
- );
- }
-
- const mediaName = `${folderPath}${media.name}`;
- const processedFile = new File([media], mediaName, { type: media.type });
- filesToUpload.push(processedFile);
- }
-
- await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token,
- commitTitle: `📁 Upload media files through DeepSite`,
- });
-
- return NextResponse.json(
- {
- success: true,
- medias: filesToUpload.map(
- (file) =>
- `https://huggingface.co/spaces/${session.user?.username}/${repoId}/resolve/main/${file.name}`
- ),
- },
- { status: 200 }
- );
- } catch (error) {
- return NextResponse.json(
- { ok: false, error: error ?? "Failed to upload media files" },
- { status: 500 }
- );
- }
-
- return NextResponse.json({ success: true }, { status: 200 });
-}
diff --git a/app/api/projects/[repoId]/rename/route.ts b/app/api/projects/[repoId]/rename/route.ts
deleted file mode 100644
index 1d3a00b676effdc7d9106c02e6d7ec40384026c9..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/rename/route.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { auth } from "@/lib/auth";
-import { downloadFile, RepoDesignation, uploadFile } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function PUT(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { newTitle } = body;
-
- if (!newTitle) {
- return NextResponse.json(
- { error: "newTitle is required" },
- { status: 400 }
- );
- }
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: "README.md",
- raw: true,
- }).catch((_) => {
- return null;
- });
-
- if (!blob) {
- return NextResponse.json(
- { error: "Could not fetch README.md" },
- { status: 500 }
- );
- }
-
- const readmeFile = await blob?.text();
- if (!readmeFile) {
- return NextResponse.json(
- { error: "Could not read README.md content" },
- { status: 500 }
- );
- }
-
- // Escape YAML values to prevent injection attacks
- const escapeYamlValue = (value: string): string => {
- if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) {
- return `"${value.replace(/"/g, '\\"')}"`;
- }
- return value;
- };
-
- // Escape commit message to prevent injection
- const escapeCommitMessage = (message: string): string => {
- return message.replace(/[\r\n]/g, " ").slice(0, 200);
- };
-
- const updatedReadmeFile = readmeFile.replace(
- /^title:\s*(.*)$/m,
- `title: ${escapeYamlValue(newTitle)}`
- );
-
- await uploadFile({
- repo,
- accessToken: token,
- file: new File([updatedReadmeFile], "README.md", { type: "text/markdown" }),
- commitTitle: escapeCommitMessage(
- `🐳 ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Rename project to "${newTitle}"`
- ),
- });
-
- return NextResponse.json(
- {
- success: true,
- },
- { status: 200 }
- );
-}
diff --git a/app/api/projects/[repoId]/route.ts b/app/api/projects/[repoId]/route.ts
deleted file mode 100644
index e6daee03652b083d7f1d852f7367d7f55fd3f390..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/route.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { auth } from "@/lib/auth";
-import { RepoDesignation, deleteRepo, uploadFiles } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function PUT(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { files, prompt, isManualChanges } = body;
-
- if (!files) {
- return NextResponse.json({ error: "Files are required" }, { status: 400 });
- }
-
- if (!prompt) {
- return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
- }
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const filesToUpload: File[] = [];
- for (const file of files) {
- let mimeType = "text/x-python";
- if (file.path.endsWith(".txt")) {
- mimeType = "text/plain";
- } else if (file.path.endsWith(".md")) {
- mimeType = "text/markdown";
- } else if (file.path.endsWith(".json")) {
- mimeType = "application/json";
- }
- filesToUpload.push(new File([file.content], file.path, { type: mimeType }));
- }
- // Escape commit title to prevent injection
- const escapeCommitTitle = (title: string): string => {
- return title.replace(/[\r\n]/g, " ").slice(0, 200);
- };
-
- const baseTitle = isManualChanges
- ? ""
- : `🐳 ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `;
- const commitTitle = escapeCommitTitle(
- baseTitle + (prompt ?? "Follow-up DeepSite commit")
- );
- const response = await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token,
- commitTitle,
- });
-
- return NextResponse.json(
- {
- success: true,
- commit: {
- oid: response.commit,
- title: commitTitle,
- date: new Date(),
- },
- },
- { status: 200 }
- );
-}
-
-export async function DELETE(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- try {
- await deleteRepo({
- repo,
- accessToken: token as string,
- });
-
- return NextResponse.json({ success: true }, { status: 200 });
- } catch (error) {
- const errMsg =
- error instanceof Error ? error.message : "Failed to delete project";
- return NextResponse.json({ error: errMsg }, { status: 500 });
- }
-}
diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts
deleted file mode 100644
index b70c310b4167077d4d8bcd15423defb1d158e2dd..0000000000000000000000000000000000000000
--- a/app/api/projects/route.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { NextResponse } from "next/server";
-import { RepoDesignation, createRepo, uploadFiles } from "@huggingface/hub";
-
-import { auth } from "@/lib/auth";
-import {
- COLORS,
- EMOJIS_FOR_SPACE,
- injectDeepSiteBadge,
- isIndexPage,
-} from "@/lib/utils";
-
-// todo: catch error while publishing project, and return the error to the user
-// if space has been created, but can't push, try again or catch well the error and return the error to the user
-
-export async function POST(request: Request) {
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { projectTitle, files, prompt } = body;
-
- if (!files) {
- return NextResponse.json(
- { error: "Project title and files are required" },
- { status: 400 }
- );
- }
-
- const title =
- projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project";
-
- let formattedTitle = title
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, "-")
- .split("-")
- .filter(Boolean)
- .join("-")
- .slice(0, 75);
-
- formattedTitle =
- formattedTitle + "-" + Math.random().toString(36).substring(2, 7);
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + formattedTitle,
- };
-
- // Escape YAML values to prevent injection attacks
- const escapeYamlValue = (value: string): string => {
- if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) {
- return `"${value.replace(/"/g, '\\"')}"`;
- }
- return value;
- };
-
- // Escape markdown headers to prevent injection
- const escapeMarkdownHeader = (value: string): string => {
- return value.replace(/^#+\s*/g, "").replace(/\n/g, " ");
- };
-
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
- const emoji =
- EMOJIS_FOR_SPACE[Math.floor(Math.random() * EMOJIS_FOR_SPACE.length)];
- const README = `---
-title: ${escapeYamlValue(projectTitle)}
-colorFrom: ${colorFrom}
-colorTo: ${colorTo}
-sdk: static
-emoji: ${emoji}
-tags:
- - deepsite-v4
----
-
-# ${escapeMarkdownHeader(title)}
-
-This project has been created with [DeepSite](https://deepsite.hf.co) AI Vibe Coding.
-`;
-
- const filesToUpload: File[] = [
- new File([README], "README.md", { type: "text/markdown" }),
- ];
- for (const file of files) {
- let mimeType = "text/html";
- if (file.path.endsWith(".css")) {
- mimeType = "text/css";
- } else if (file.path.endsWith(".js")) {
- mimeType = "text/javascript";
- }
- const content =
- mimeType === "text/html" && isIndexPage(file.path)
- ? injectDeepSiteBadge(file.content)
- : file.content;
-
- filesToUpload.push(new File([content], file.path, { type: mimeType }));
- }
-
- let repoUrl: string | undefined;
-
- try {
- // Create the space first
- const createResult = await createRepo({
- accessToken: token as string,
- repo: repo,
- sdk: "static",
- });
- repoUrl = createResult.repoUrl;
-
- // Escape commit message to prevent injection
- const escapeCommitMessage = (message: string): string => {
- return message.replace(/[\r\n]/g, " ").slice(0, 200);
- };
- const commitMessage = escapeCommitMessage(prompt ?? "Initial DeepSite commit");
-
- // Upload files to the created space
- await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token as string,
- commitTitle: commitMessage,
- });
-
- const path = repoUrl.split("/").slice(-2).join("/");
-
- return NextResponse.json({ repoUrl: path }, { status: 200 });
- } catch (error) {
- const errMsg =
- error instanceof Error ? error.message : "Failed to create or upload to space";
-
- // If space was created but upload failed, include the repo URL in the error
- if (repoUrl) {
- const path = repoUrl.split("/").slice(-2).join("/");
- return NextResponse.json({
- error: `${errMsg}. Space was created at ${path} but files could not be uploaded.`,
- repoUrl: path,
- partialSuccess: true
- }, { status: 500 });
- }
-
- return NextResponse.json({ error: errMsg }, { status: 500 });
- }
-}
diff --git a/app/api/re-design/route.ts b/app/api/re-design/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..777c2cbd18f95592080c0fe1ad2cab23a5264397
--- /dev/null
+++ b/app/api/re-design/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function PUT(request: NextRequest) {
+ const body = await request.json();
+ const { url } = body;
+
+ if (!url) {
+ return NextResponse.json({ error: "URL is required" }, { status: 400 });
+ }
+
+ try {
+ const response = await fetch(
+ `https://r.jina.ai/${encodeURIComponent(url)}`,
+ {
+ method: "POST",
+ }
+ );
+ if (!response.ok) {
+ return NextResponse.json(
+ { error: "Failed to fetch redesign" },
+ { status: 500 }
+ );
+ }
+ const markdown = await response.text();
+ return NextResponse.json(
+ {
+ ok: true,
+ markdown,
+ },
+ { status: 200 }
+ );
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ return NextResponse.json(
+ { error: error.message || "An error occurred" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/redesign/route.ts b/app/api/redesign/route.ts
deleted file mode 100644
index 6b898d6fd364c5f3ef267706b62c37ee559e19cd..0000000000000000000000000000000000000000
--- a/app/api/redesign/route.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { NextRequest, NextResponse } from "next/server";
-
-const FETCH_TIMEOUT = 30_000;
-export const maxDuration = 60;
-
-export async function PUT(request: NextRequest) {
- const body = await request.json();
- const { url } = body;
-
- if (!url) {
- return NextResponse.json({ error: "URL is required" }, { status: 400 });
- }
-
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
-
- try {
- const response = await fetch(
- `https://r.jina.ai/${encodeURIComponent(url)}`,
- {
- method: "POST",
- signal: controller.signal,
- }
- );
-
- clearTimeout(timeoutId);
-
- if (!response.ok) {
- return NextResponse.json(
- { error: "Failed to fetch redesign" },
- { status: 500 }
- );
- }
- const markdown = await response.text();
- return NextResponse.json(
- {
- ok: true,
- markdown,
- },
- { status: 200 }
- );
- } catch (fetchError: any) {
- clearTimeout(timeoutId);
-
- if (fetchError.name === "AbortError") {
- return NextResponse.json(
- {
- error:
- "Request timeout: The external service took too long to respond. Please try again.",
- },
- { status: 504 }
- );
- }
- throw fetchError;
- }
- } catch (error: any) {
- if (error.name === "AbortError" || error.message?.includes("timeout")) {
- return NextResponse.json(
- {
- error:
- "Request timeout: The external service took too long to respond. Please try again.",
- },
- { status: 504 }
- );
- }
- return NextResponse.json(
- { error: error.message || "An error occurred" },
- { status: 500 }
- );
- }
-}
diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ac44f365b707908d6003a4266db1269d297a4336
--- /dev/null
+++ b/app/auth/callback/page.tsx
@@ -0,0 +1,72 @@
+"use client";
+import Link from "next/link";
+import { useUser } from "@/hooks/useUser";
+import { use, useState } from "react";
+import { useMount, useTimeoutFn } from "react-use";
+
+import { Button } from "@/components/ui/button";
+export default function AuthCallback({
+ searchParams,
+}: {
+ searchParams: Promise<{ code: string }>;
+}) {
+ const [showButton, setShowButton] = useState(false);
+ const { code } = use(searchParams);
+ const { loginFromCode } = useUser();
+
+ useMount(async () => {
+ if (code) {
+ await loginFromCode(code);
+ }
+ });
+
+ useTimeoutFn(
+ () => setShowButton(true),
+ 7000 // Show button after 5 seconds
+ );
+
+ return (
+
+
+
+
+
+
+ If you are not redirected automatically in the next 5 seconds,
+ please click the button below
+
+ {showButton ? (
+
+
+
+ ) : (
+
+ Please wait, we are logging you in...
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/auth/page.tsx b/app/auth/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a45a6bc6f58907b4ee5efbf0f70a51ee153625c7
--- /dev/null
+++ b/app/auth/page.tsx
@@ -0,0 +1,28 @@
+import { redirect } from "next/navigation";
+import { Metadata } from "next";
+
+import { getAuth } from "@/app/actions/auth";
+
+export const revalidate = 1;
+
+export const metadata: Metadata = {
+ robots: "noindex, nofollow",
+};
+
+export default async function Auth() {
+ const loginRedirectUrl = await getAuth();
+ if (loginRedirectUrl) {
+ redirect(loginRedirectUrl);
+ }
+
+ return (
+
+
+
Error
+
+ An error occurred while trying to log in. Please try again later.
+
+
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 469b6844c7b63d85ad5e9c7ba420add2f7f1125c..d960a121272f5b976ae70dbd1c7656e181ca6e6d 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,33 +1,54 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Metadata, Viewport } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import { NextStepProvider } from "nextstepjs";
-import Script from "next/script";
+import { Inter, PT_Sans } from "next/font/google";
+import { cookies } from "next/headers";
-import "@/app/globals.css";
-import { ThemeProvider } from "@/components/providers/theme";
-import { AuthProvider } from "@/components/providers/session";
+import TanstackProvider from "@/components/providers/tanstack-query-provider";
+import "@/assets/globals.css";
import { Toaster } from "@/components/ui/sonner";
-import { ReactQueryProvider } from "@/components/providers/react-query";
-import { generateSEO, generateStructuredData } from "@/lib/seo";
-import { NotAuthorizedDomain } from "@/components/not-authorized";
+import MY_TOKEN_KEY from "@/lib/get-cookie-name";
+import { apiServer } from "@/lib/api";
+import AppContext from "@/components/contexts/app-context";
+import Script from "next/script";
+import IframeDetector from "@/components/iframe-detector";
-const geistSans = Geist({
- variable: "--font-geist-sans",
+const inter = Inter({
+ variable: "--font-inter-sans",
subsets: ["latin"],
});
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const ptSans = PT_Sans({
+ variable: "--font-ptSans-mono",
subsets: ["latin"],
+ weight: ["400", "700"],
});
export const metadata: Metadata = {
- ...generateSEO({
+ title: "DeepSite | Build with AI ✨",
+ description:
+ "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
+ openGraph: {
+ title: "DeepSite | Build with AI ✨",
+ description:
+ "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
+ url: "https://deepsite.hf.co",
+ siteName: "DeepSite",
+ images: [
+ {
+ url: "https://deepsite.hf.co/banner.png",
+ width: 1200,
+ height: 630,
+ alt: "DeepSite Open Graph Image",
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
title: "DeepSite | Build with AI ✨",
description:
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
- path: "/",
- }),
+ images: ["https://deepsite.hf.co/banner.png"],
+ },
appleWebApp: {
capable: true,
title: "DeepSite",
@@ -38,70 +59,53 @@ export const metadata: Metadata = {
shortcut: "/logo.svg",
apple: "/logo.svg",
},
- verification: {
- google: process.env.GOOGLE_SITE_VERIFICATION,
- },
};
export const viewport: Viewport = {
initialScale: 1,
maximumScale: 1,
- themeColor: "#4f46e5",
+ themeColor: "#000000",
};
+async function getMe() {
+ const cookieStore = await cookies();
+ const token = cookieStore.get(MY_TOKEN_KEY())?.value;
+ if (!token) return { user: null, errCode: null };
+ try {
+ const res = await apiServer.get("/me", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return { user: res.data.user, errCode: null };
+ } catch (err: any) {
+ return { user: null, errCode: err.status };
+ }
+}
+
+// if domain isn't deepsite.hf.co or enzostvs-deepsite.hf.space redirect to deepsite.hf.co
+
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
- const structuredData = generateStructuredData("WebApplication", {
- name: "DeepSite",
- description: "Build websites with AI, no code required",
- url: "https://deepsite.hf.co",
- });
- const organizationData = generateStructuredData("Organization", {
- name: "DeepSite",
- url: "https://deepsite.hf.co",
- });
-
+ const data = await getMe();
return (
-
+
+
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
+
+
+
+ {children}
+
);
diff --git a/app/new/page.tsx b/app/new/page.tsx
deleted file mode 100644
index 920e799168597ccab9e644171bff837961cc45e6..0000000000000000000000000000000000000000
--- a/app/new/page.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { AppEditor } from "@/components/editor";
-import { auth } from "@/lib/auth";
-import { redirect } from "next/navigation";
-
-export default async function NewProjectPage({
- searchParams,
-}: {
- searchParams: Promise<{ prompt: string }>;
-}) {
- const session = await auth();
-
- if (!session) {
- redirect("/api/auth/signin?callbackUrl=/new");
- }
-
- const { prompt } = await searchParams;
- 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/projects/[namespace]/[repoId]/page.tsx b/app/projects/[namespace]/[repoId]/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e60c46e774de73ea30c6964ae49c5a32daf6c87d
--- /dev/null
+++ b/app/projects/[namespace]/[repoId]/page.tsx
@@ -0,0 +1,42 @@
+import { cookies } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { apiServer } from "@/lib/api";
+import MY_TOKEN_KEY from "@/lib/get-cookie-name";
+import { AppEditor } from "@/components/editor";
+
+async function getProject(namespace: string, repoId: string) {
+ // TODO replace with a server action
+ const cookieStore = await cookies();
+ const token = cookieStore.get(MY_TOKEN_KEY())?.value;
+ if (!token) return {};
+ try {
+ const { data } = await apiServer.get(
+ `/me/projects/${namespace}/${repoId}`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ return data.project;
+ } catch {
+ return {};
+ }
+}
+
+export default async function ProjectNamespacePage({
+ params,
+}: {
+ params: Promise<{ namespace: string; repoId: string }>;
+}) {
+ const { namespace, repoId } = await params;
+ const data = await getProject(namespace, repoId);
+ if (!data?.pages) {
+ redirect("/projects");
+ }
+ return (
+
+ );
+}
diff --git a/app/projects/new/page.tsx b/app/projects/new/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9dabea0dcf9de7c30b66ff35588740ed59de3f0e
--- /dev/null
+++ b/app/projects/new/page.tsx
@@ -0,0 +1,5 @@
+import { AppEditor } from "@/components/editor";
+
+export default function ProjectsNewPage() {
+ return ;
+}
diff --git a/assets/deepseek.svg b/assets/deepseek.svg
deleted file mode 100644
index dc224e43a4d68070ca6eed494476c8ddd900bf80..0000000000000000000000000000000000000000
--- a/assets/deepseek.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/globals.css b/assets/globals.css
similarity index 82%
rename from app/globals.css
rename to assets/globals.css
index 97005be73ce05b7bb1a207464af1945139fde62b..19dd59e7dcc34e453e9850a052ae8f039628e58a 100644
--- a/app/globals.css
+++ b/assets/globals.css
@@ -6,8 +6,8 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+ --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);
@@ -44,7 +44,7 @@
}
:root {
- --radius: 0.65rem;
+ --radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
@@ -68,7 +68,6 @@
--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);
@@ -120,49 +119,28 @@
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-background!;
+ @apply !bg-neutral-900;
}
.monaco-editor .monaco-editor-background {
- @apply bg-background!;
-}
-.monaco-editor .decorationsOverviewRuler {
- @apply opacity-0!;
-}
-.monaco-editor .view-line {
- /* @apply bg-primary/50!; */
+ @apply !bg-neutral-900;
}
-.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;
+.monaco-editor .line-numbers {
+ @apply !text-neutral-500;
}
-.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!;
+.matched-line {
+ @apply bg-sky-500/30;
}
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/kimi.svg b/assets/kimi.svg
deleted file mode 100644
index 4355c522a2dece99e187d9e5c898a66313f4a374..0000000000000000000000000000000000000000
--- a/assets/kimi.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
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 @@
-
\ 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/assets/qwen.svg b/assets/qwen.svg
deleted file mode 100644
index a4bb382a6359b82c581fd3e7fb7169fe8fba1657..0000000000000000000000000000000000000000
--- a/assets/qwen.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/zai.svg b/assets/zai.svg
deleted file mode 100644
index 2adcac387aaca06eb362a177e45c28ae3429b0d0..0000000000000000000000000000000000000000
--- a/assets/zai.svg
+++ /dev/null
@@ -1,13 +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..335484f9424bf72b98e3b892275740bc8f014754 100644
--- a/components.json
+++ b/components.json
@@ -6,11 +6,10 @@
"tailwind": {
"config": "",
"css": "app/globals.css",
- "baseColor": "zinc",
+ "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/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 (
-
- );
-}
diff --git a/components/ask-ai/ask-ai.tsx b/components/ask-ai/ask-ai.tsx
deleted file mode 100644
index 66ce78f0d25644ecc7c222911f9455fca60de75c..0000000000000000000000000000000000000000
--- a/components/ask-ai/ask-ai.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-"use client";
-import { ArrowUp, Paintbrush, X } from "lucide-react";
-import { FaHand } from "react-icons/fa6";
-import { useRef, useState } from "react";
-import { HiStop } from "react-icons/hi2";
-import { useLocalStorage, useMount, useUpdateEffect } from "react-use";
-import { useRouter } from "next/navigation";
-import { useNextStep } from "nextstepjs";
-
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import { useGeneration } from "./useGeneration";
-import { File, MobileTabType, ProviderType } from "@/lib/type";
-import { Models } from "./models";
-import { DEFAULT_MODEL, MODELS } from "@/lib/providers";
-import { Redesign } from "./redesign";
-import { Uploader } from "./uploader";
-import { InputMentions } from "./input-mentions";
-
-export function AskAI({
- initialPrompt,
- className,
- onToggleMobileTab,
- files,
- medias,
- tourHasBeenShown,
- isNew = false,
- isHistoryView,
- projectName = "new",
-}: {
- initialPrompt?: string;
- className?: string;
- files?: File[] | null;
- medias?: string[] | null;
- tourHasBeenShown?: boolean;
- onToggleMobileTab?: (tab: MobileTabType) => void;
- isNew?: boolean;
- isHistoryView?: boolean;
- projectName?: string;
-}) {
- const contentEditableRef = useRef(null);
- const [model = DEFAULT_MODEL, setModel] = useLocalStorage(
- "deepsite-model",
- DEFAULT_MODEL
- );
- const [provider, setProvider] = useLocalStorage(
- "deepsite-provider",
- "auto" as ProviderType
- );
-
- const [prompt, setPrompt] = useState(initialPrompt ?? "");
- const [redesignMd, setRedesignMd] = useState<{
- md: string;
- url: string;
- } | null>(null);
- const [selectedMedias, setSelectedMedias] = useState([]);
- const [imageLinks, setImageLinks] = useState([]);
- const [startTour, setStartTour] = useState(false);
-
- const router = useRouter();
- const { callAi, isLoading, stopGeneration, audio } =
- useGeneration(projectName);
- const { startNextStep } = useNextStep();
-
- const onComplete = () => {
- onToggleMobileTab?.("right-sidebar");
- };
-
- useMount(() => {
- if (initialPrompt && initialPrompt.trim() !== "" && isNew) {
- setTimeout(() => {
- if (isHistoryView) return;
- callAi(
- {
- prompt: initialPrompt,
- model,
- onComplete,
- provider,
- },
- setModel
- );
- router.replace("/new");
- }, 200);
- }
- });
-
- const onSubmit = () => {
- if (isHistoryView) return;
- if (contentEditableRef.current) {
- contentEditableRef.current.innerHTML = "";
- }
- callAi(
- {
- prompt,
- model,
- onComplete,
- provider,
- redesignMd,
- medias: [...(selectedMedias ?? []), ...(imageLinks ?? [])],
- },
- setModel
- );
- if (selectedMedias.length > 0) setSelectedMedias([]);
- if (imageLinks.length > 0) setImageLinks([]);
- if (redesignMd) setRedesignMd(null);
- };
-
- return (
-
-
-
-
-
- );
-}
diff --git a/components/ask-ai/context.tsx b/components/ask-ai/context.tsx
deleted file mode 100644
index c22f17fc81f95788b6d3e690cc70ac5d3b6f817a..0000000000000000000000000000000000000000
--- a/components/ask-ai/context.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { AtSign, Braces, FileCode, FileText, X } from "lucide-react";
-import { useMemo, useState } from "react";
-import { useQueryClient } from "@tanstack/react-query";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { File } from "@/lib/type";
-import { cn } from "@/lib/utils";
-
-export const Context = ({
- files,
- setFiles,
-}: {
- files: File[];
- setFiles: (files: File[]) => void;
-}) => {
- const queryClient = useQueryClient();
- const [open, setOpen] = useState(false);
-
- const getFileIcon = (filePath: string, size = "size-3.5") => {
- if (filePath.endsWith(".css")) {
- return ;
- } else if (filePath.endsWith(".js")) {
- return ;
- } else if (filePath.endsWith(".json")) {
- return ;
- } else {
- return ;
- }
- };
-
- const getFiles = () => queryClient.getQueryData(["files"]) ?? [];
-
- return (
-
-
-
-
-
-
-
-
- Select a file to send as context
-
-
- {getFiles().length === 0 ? (
-
- No files available
-
- ) : (
- <>
-
- {getFiles()?.map((page) => (
-
- ))}
- >
- )}
-
-
-
-
- {files?.map((file) => (
-
- ))}
-
- );
-};
diff --git a/components/ask-ai/input-mentions.tsx b/components/ask-ai/input-mentions.tsx
deleted file mode 100644
index 4ef11b0a0cd9aeaef85eddfd049a30272437b414..0000000000000000000000000000000000000000
--- a/components/ask-ai/input-mentions.tsx
+++ /dev/null
@@ -1,477 +0,0 @@
-import { useRef, useState, useEffect, RefObject } from "react";
-import { useClickAway } from "react-use";
-import { useQueryClient } from "@tanstack/react-query";
-
-import { searchFilesMentions } from "@/actions/mentions";
-import { File } from "@/lib/type";
-import { Braces, FileCode, FileText } from "lucide-react";
-
-export function InputMentions({
- ref,
- prompt,
- files,
- setPrompt,
- redesignMdUrl,
- onSubmit,
- imageLinks,
- setImageLinks,
-}: {
- ref: RefObject;
- prompt: string;
- files?: File[] | null;
- redesignMdUrl?: string;
- setPrompt: (prompt: string) => void;
- onSubmit: () => void;
- imageLinks?: string[];
- setImageLinks?: (links: string[]) => void;
-}) {
- const queryClient = useQueryClient();
- const [showMentionDropdown, setShowMentionDropdown] = useState(false);
- const [, setMentionSearch] = useState("");
- const dropdownRef = useRef(null);
- const [results, setResults] = useState([]);
-
- useClickAway(dropdownRef, () => {
- setShowMentionDropdown(false);
- });
-
- const isImageUrl = (url: string): boolean => {
- // Check if it's a valid HTTP/HTTPS URL with an image extension
- const imageUrlPattern = /^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i;
- return imageUrlPattern.test(url);
- };
-
- const getTextContent = (element: HTMLElement): string => {
- let text = "";
- const childNodes = element.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.TEXT_NODE) {
- text += node.textContent || "";
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("mention-chip")) {
- text += el.getAttribute("data-mention-id") || "";
- } else if (el.classList.contains("image-chip")) {
- // Include image URL in text content for display purposes
- const imageUrl = el.getAttribute("data-image-url") || "";
- text += imageUrl ? ` ${imageUrl} ` : "";
- } else {
- text += el.textContent || "";
- }
- }
- }
- return text + "\u0020";
- };
-
- const extractPromptWithIds = (): string => {
- if (!ref.current) return "";
-
- let text = "";
- const childNodes = ref.current.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.TEXT_NODE) {
- text += node.textContent || "";
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("mention-chip")) {
- text += el.getAttribute("data-mention-id") || "";
- } else if (el.classList.contains("image-chip")) {
- // Include image URL in prompt text
- const imageUrl = el.getAttribute("data-image-url") || "";
- text += imageUrl ? ` ${imageUrl} ` : "";
- } else {
- text += el.textContent || "";
- }
- }
- }
- return text;
- };
-
- const extractImageLinks = (): string[] => {
- if (!ref.current) return [];
-
- const links: string[] = [];
- const childNodes = ref.current.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("image-chip")) {
- const imageUrl = el.getAttribute("data-image-url");
- if (imageUrl) links.push(imageUrl);
- }
- }
- }
- return links;
- };
-
- const shouldDetectMention = (): {
- detect: boolean;
- textBeforeCursor: string;
- } => {
- const selection = window.getSelection();
- if (!selection || !ref.current) {
- return { detect: false, textBeforeCursor: "" };
- }
-
- const range = selection.getRangeAt(0);
- const node = range.startContainer;
-
- if (node.nodeType === Node.ELEMENT_NODE) {
- const element = node as HTMLElement;
- if (element.classList?.contains("mention-chip")) {
- return { detect: false, textBeforeCursor: "" };
- }
- }
-
- if (node.parentElement?.classList?.contains("mention-chip")) {
- return { detect: false, textBeforeCursor: "" };
- }
-
- if (node.nodeType === Node.TEXT_NODE) {
- const textContent = node.textContent || "";
- const cursorOffset = range.startOffset;
- const textBeforeCursor = textContent.substring(0, cursorOffset);
-
- const lastAtIndex = textBeforeCursor.lastIndexOf("@");
- if (lastAtIndex !== -1) {
- const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
- if (!textAfterAt.includes(" ")) {
- return { detect: true, textBeforeCursor: textAfterAt };
- }
- }
- }
-
- return { detect: false, textBeforeCursor: "" };
- };
-
- const handleInput = async () => {
- if (!ref.current) return;
- const text = getTextContent(ref.current);
-
- // Only clear if there's no content at all (including chips)
- const hasImageChips = ref.current.querySelectorAll(".image-chip").length > 0;
- const hasMentionChips = ref.current.querySelectorAll(".mention-chip").length > 0;
-
- if (text.trim() === "" && !hasImageChips && !hasMentionChips) {
- ref.current.innerHTML = "";
- }
-
- setPrompt(text);
-
- // Update image links whenever input changes
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
-
- const { detect, textBeforeCursor } = shouldDetectMention();
-
- if (detect && files && files?.length > 0) {
- setMentionSearch(textBeforeCursor);
- setShowMentionDropdown(true);
- const files = queryClient.getQueryData(["files"]) ?? [];
- const results = await searchFilesMentions(textBeforeCursor, files);
- setResults(results);
- } else {
- setShowMentionDropdown(false);
- }
- };
-
- const createMentionChipElement = (mentionId: string): HTMLSpanElement => {
- const mentionChip = document.createElement("span");
-
- mentionChip.className =
- "mention-chip inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400";
- mentionChip.contentEditable = "false";
- mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`);
- mentionChip.textContent = `@${mentionId}`;
-
- return mentionChip;
- };
-
- const createImageChipElement = (imageUrl: string): HTMLSpanElement => {
- const imageChip = document.createElement("span");
-
- imageChip.className =
- "image-chip inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400";
- imageChip.contentEditable = "false";
- imageChip.setAttribute("data-image-url", imageUrl);
-
- // Create icon (using emoji for simplicity)
- const icon = document.createElement("span");
- icon.textContent = "🖼️";
- icon.className = "text-[10px]";
-
- // Truncate URL for display
- const displayUrl =
- imageUrl.length > 30 ? imageUrl.substring(0, 30) + "..." : imageUrl;
- const text = document.createTextNode(displayUrl);
-
- imageChip.appendChild(icon);
- imageChip.appendChild(text);
-
- return imageChip;
- };
-
- const insertMention = (mentionId: string) => {
- if (!ref.current) return;
-
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0) return;
-
- const range = selection.getRangeAt(0);
- const textNode = range.startContainer;
-
- if (textNode.nodeType !== Node.TEXT_NODE) return;
-
- const textContent = textNode.textContent || "";
- const cursorOffset = range.startOffset;
-
- const textBeforeCursor = textContent.substring(0, cursorOffset);
- const lastAtIndex = textBeforeCursor.lastIndexOf("@");
-
- if (lastAtIndex !== -1) {
- const mentionChip = createMentionChipElement(mentionId);
-
- const beforeText = textContent.substring(0, lastAtIndex);
- const afterText = textContent.substring(cursorOffset);
- const parent = textNode.parentNode;
- if (!parent) return;
-
- const beforeNode = beforeText
- ? document.createTextNode(beforeText)
- : null;
- const spaceNode = document.createTextNode("\u0020");
- const afterNode = afterText ? document.createTextNode(afterText) : null;
-
- if (beforeNode) {
- parent.insertBefore(beforeNode, textNode);
- }
- parent.insertBefore(mentionChip, textNode);
- parent.insertBefore(spaceNode, textNode);
- if (afterNode) {
- parent.insertBefore(afterNode, textNode);
- }
-
- parent.removeChild(textNode);
-
- const newRange = document.createRange();
- if (afterNode) {
- newRange.setStart(afterNode, 0);
- } else {
- newRange.setStartAfter(spaceNode);
- }
- newRange.collapse(true);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- const newText = getTextContent(ref.current);
- setPrompt(newText);
- setShowMentionDropdown(false);
- setMentionSearch("");
- }
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (!prompt || prompt.trim() === "") return;
-
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- const promptWithIds = extractPromptWithIds();
- setPrompt(promptWithIds);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
-
- onSubmit();
-
- if (ref.current) {
- ref.current.innerHTML = "";
- }
- setPrompt("");
- setShowMentionDropdown(false);
- } else if (e.key === "Escape") {
- setShowMentionDropdown(false);
- }
- };
-
- useEffect(() => {
- if (ref.current && prompt === "" && ref.current.innerHTML !== "") {
- ref.current.innerHTML = "";
- }
- }, [prompt]);
-
- const handlePaste = (e: React.ClipboardEvent) => {
- e.preventDefault();
- const text = e.clipboardData.getData("text/plain");
-
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0 || !ref.current) {
- document.execCommand("insertText", false, text);
- return;
- }
-
- const trimmedText = text.trim();
-
- // Check if pasted text is ONLY an image URL
- if (isImageUrl(trimmedText)) {
- const range = selection.getRangeAt(0);
- const imageChip = createImageChipElement(trimmedText);
- const spaceNode = document.createTextNode("\u0020");
-
- range.deleteContents();
- range.insertNode(imageChip);
-
- // Insert space after the chip
- const afterChipRange = document.createRange();
- afterChipRange.setStartAfter(imageChip);
- afterChipRange.insertNode(spaceNode);
-
- // Move cursor after the space
- const newRange = document.createRange();
- newRange.setStartAfter(spaceNode);
- newRange.collapse(true);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- // Update state
- const newText = getTextContent(ref.current);
- setPrompt(newText);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
- } else {
- // Check if text contains image URLs
- const imageUrlPattern = /https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?[^\s]*)?/gi;
- const containsImageUrl = imageUrlPattern.test(text);
-
- if (containsImageUrl) {
- // Split text and replace image URLs with chips
- const parts = text.split(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?[^\s]*)?)/gi);
- const range = selection.getRangeAt(0);
- range.deleteContents();
-
- const fragment = document.createDocumentFragment();
-
- parts.forEach((part) => {
- if (isImageUrl(part)) {
- const imageChip = createImageChipElement(part);
- const spaceNode = document.createTextNode("\u0020");
- fragment.appendChild(imageChip);
- fragment.appendChild(spaceNode);
- } else if (part && !part.match(/^\?[^\s]*$/)) {
- // Skip query string matches, add other text
- const textNode = document.createTextNode(part);
- fragment.appendChild(textNode);
- }
- });
-
- range.insertNode(fragment);
-
- // Move cursor to end
- const newRange = document.createRange();
- newRange.selectNodeContents(ref.current);
- newRange.collapse(false);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- // Update state
- const newText = getTextContent(ref.current);
- setPrompt(newText);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
- } else {
- // Plain text, use default insertion
- document.execCommand("insertText", false, text);
- }
- }
- };
-
- return (
-
-
"']/g,
- ""
- )}, want to add something?`
- : files && files.length > 0
- ? "Ask me anything. Type @ to mention a file..."
- : "Ask me anything..."
- }
- onInput={handleInput}
- onKeyDown={handleKeyDown}
- onPaste={handlePaste}
- suppressContentEditableWarning
- >
- {showMentionDropdown && (
-
-
- {results?.length > 0 && (
-
- {results.map((file) => (
- insertMention(file.path)}
- />
- ))}
-
- )}
-
-
- )}
-
- );
-}
-
-export const getFileIcon = (filePath: string, size = "size-3.5") => {
- if (filePath.endsWith(".css")) {
- return ;
- } else if (filePath.endsWith(".js")) {
- return ;
- } else if (filePath.endsWith(".json")) {
- return ;
- } else {
- return ;
- }
-};
-
-function MentionResult({
- file,
- onSelect,
-}: {
- file: File;
- onSelect: () => void;
-}) {
- return (
-
- {getFileIcon(file.path, "size-3")}
- {file.path}
-
- );
-}
diff --git a/components/ask-ai/loading.tsx b/components/ask-ai/loading.tsx
deleted file mode 100644
index 060be494fb586788eab5c2139bb080fa51e209a2..0000000000000000000000000000000000000000
--- a/components/ask-ai/loading.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-"use client";
-import Loading from "@/components/loading";
-
-export const AiLoading = ({
- text = "AI Assistant is thinking...",
- showCircle = true,
- className,
-}: {
- text?: string;
- showCircle?: boolean;
- className?: string;
-}) => {
- return (
-
- {showCircle &&
}
-
-
- {text.split("").map((char, index) => (
-
- {char === " " ? "\u00A0" : char}
-
- ))}
-
-
-
- );
-};
diff --git a/components/ask-ai/models.tsx b/components/ask-ai/models.tsx
deleted file mode 100644
index 4e58acffcf87295047815246cf3b2dd14ef04d2f..0000000000000000000000000000000000000000
--- a/components/ask-ai/models.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import {
- BrainIcon,
- ChevronDown,
- DollarSign,
- StarsIcon,
- Zap,
-} from "lucide-react";
-import { useMemo, useState } from "react";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import { ProviderType } from "@/lib/type";
-import { MODELS } from "@/lib/providers";
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-export function Models({
- model,
- setModel,
- provider,
- setProvider,
-}: {
- model: string;
- setModel: (model: string) => void;
- provider: ProviderType;
- setProvider: (provider: ProviderType) => void;
-}) {
- const [open, setOpen] = useState(false);
-
- const formattedModels = useMemo(() => {
- const lists: ((typeof MODELS)[0] | { isCategory: true; name: string })[] =
- [];
- const keys = new Set();
- MODELS.forEach((model) => {
- if (!keys.has(model.companyName)) {
- lists.push({
- isCategory: true,
- name: model.companyName,
- logo: model.logo,
- });
- keys.add(model.companyName);
- }
- lists.push(model);
- });
- return lists;
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
Provider mode:
-
- {(
- [
- { value: "cheapest", icon: DollarSign, color: "emerald" },
- {
- value: "auto",
- icon: BrainIcon,
- color: "indigo",
- name: "Smartest",
- },
- { value: "fastest", icon: Zap, color: "amber" },
- ] as const
- ).map(
- ({
- value,
- icon: Icon,
- color,
- name,
- }: {
- value: string;
- icon: React.ElementType;
- color: string;
- name?: string;
- }) => (
-
setProvider(value)}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- setProvider(value);
- }
- }}
- className={cn(
- "inline-flex items-center gap-1.5 h-7 px-2.5 text-xs font-medium border border-border rounded-md cursor-pointer transition-colors select-none",
- "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
- provider === value && [
- "bg-transparent",
- color === "emerald" &&
- "[&_svg]:fill-emerald-500 [&_svg]:stroke-emerald-500 bg-emerald-500/10! border-emerald-500/10! text-emerald-500!",
- color === "indigo" &&
- "[&_svg]:fill-indigo-500 [&_svg]:stroke-indigo-500 bg-indigo-500/10! border-indigo-500/10! text-indigo-500!",
- color === "amber" &&
- "[&_svg]:fill-amber-500 [&_svg]:stroke-amber-500 bg-amber-500/10! border-amber-500/10! text-amber-500!",
- ]
- )}
- >
-
- {name ?? value.charAt(0).toUpperCase() + value.slice(1)}
-
- )
- )}
-
-
-
-
-
- );
-}
diff --git a/components/ask-ai/uploader.tsx b/components/ask-ai/uploader.tsx
deleted file mode 100644
index c2f5f7482790b2209222c01a827ace953ddde4ca..0000000000000000000000000000000000000000
--- a/components/ask-ai/uploader.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import {
- CheckCircle,
- FileVideo,
- ImageIcon,
- Music,
- Paperclip,
- Video,
-} from "lucide-react";
-import { useRef, useState } from "react";
-import Image from "next/image";
-import { useParams } from "next/navigation";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { getFileType, humanizeNumber } from "@/lib/utils";
-import { useQueryClient } from "@tanstack/react-query";
-import { ProjectWithCommits } from "@/actions/projects";
-import { toast } from "sonner";
-import Loading from "../loading";
-
-export const Uploader = ({
- medias,
- selected,
- setSelected,
-}: {
- medias?: string[] | null;
- selected: string[];
- setSelected: React.Dispatch>;
-}) => {
- const queryClient = useQueryClient();
- const { repoId } = useParams<{ repoId: string }>();
-
- const [open, setOpen] = useState(false);
- const [isUploading, setIsUploading] = useState(false);
- const [error, setError] = useState(null);
- const fileInputRef = useRef(null);
-
- const getFileIcon = (url: string) => {
- const fileType = getFileType(url);
- switch (fileType) {
- case "image":
- return ;
- case "video":
- return ;
- case "audio":
- return ;
- default:
- return ;
- }
- };
-
- const uploadFiles = async (files: FileList | null) => {
- setError(null);
- if (!files || files.length === 0) return;
-
- setIsUploading(true);
- const data = new FormData();
- Array.from(files).forEach((file) => {
- data.append("images", file);
- });
-
- const response = await fetch(`/api/projects/${repoId}/medias`, {
- method: "POST",
- body: data,
- })
- .then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to save changes");
- })
- .catch((err) => {
- return { success: false, err };
- });
-
- if (response.success) {
- queryClient.setQueryData(
- ["project"],
- (oldProject: ProjectWithCommits) => ({
- ...oldProject,
- medias: [...response.medias, ...(oldProject?.medias ?? [])],
- })
- );
- toast.success("Media files uploaded successfully!");
- } else {
- setError(
- response.err ?? "Failed to upload media files, try again later."
- );
- }
-
- setIsUploading(false);
- };
-
- return (
-
-
-
-
-
-
-
-
- {error && {error}
}
- {medias && medias.length > 0 && (
-
-
- Uploaded files:
-
-
-
- {medias.map((media: string) => {
- const fileType = getFileType(media);
- return (
-
- setSelected(
- selected.includes(media)
- ? selected.filter((f) => f !== media)
- : [...selected, media]
- )
- }
- >
- {fileType === "image" ? (
-
- ) : fileType === "video" ? (
-
- ) : fileType === "audio" ? (
-
-
-
- ) : (
-
- {getFileIcon(media)}
-
- )}
- {selected.includes(media) && (
-
-
-
- )}
-
- );
- })}
-
-
-
- )}
-
-
- uploadFiles(e.target.files)}
- />
-
-
-
-
-
- );
-};
diff --git a/components/ask-ai/useGeneration.ts b/components/ask-ai/useGeneration.ts
deleted file mode 100644
index e847b86b1ab2e61469fc568b0e35b47864242176..0000000000000000000000000000000000000000
--- a/components/ask-ai/useGeneration.ts
+++ /dev/null
@@ -1,437 +0,0 @@
-"use client";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useRef } from "react";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-import { v4 as uuidv4 } from "uuid";
-import { useLocalStorage } from "react-use";
-import { useSession } from "next-auth/react";
-
-import { formatResponse } from "@/lib/format";
-import { File, Message, MessageActionType, ProviderType } from "@/lib/type";
-import { getContextFilesFromPrompt } from "@/lib/utils";
-
-const MESSAGES_QUERY_KEY = (projectName: string) =>
- ["messages", projectName] as const;
-
-export const useGeneration = (projectName: string) => {
- const router = useRouter();
- const audio = useRef(null);
- const queryClient = useQueryClient();
- const abortController = useRef(null);
- const [, setStoredMessages] = useLocalStorage(
- `messages-${projectName}`,
- []
- );
- const { data: session } = useSession();
-
- const { data: isLoading } = useQuery({
- queryKey: ["ai.generation.isLoading"],
- queryFn: () => false,
- refetchOnWindowFocus: false,
- refetchOnReconnect: false,
- refetchOnMount: false,
- staleTime: Infinity,
- });
-
- const setIsLoading = (isLoading: boolean) => {
- queryClient.setQueryData(["ai.generation.isLoading"], isLoading);
- };
-
- const getFiles = () => queryClient.getQueryData(["files"]) ?? [];
- const setFiles = (newFiles: File[]) => {
- queryClient.setQueryData(["files"], (oldFiles: File[] = []) => {
- const currentFiles = oldFiles.filter(
- (file) => !newFiles.some((f) => f.path === file.path)
- );
- return [...currentFiles, ...newFiles];
- });
- };
-
- const getMessages = () =>
- queryClient.getQueryData(
- MESSAGES_QUERY_KEY(projectName ?? "new")
- ) ?? [];
- const addMessage = (message: Omit) => {
- const id = uuidv4();
- const key = MESSAGES_QUERY_KEY(projectName ?? "new");
- queryClient.setQueryData(key, (oldMessages = []) => {
- const newMessages = [...oldMessages, { ...message, id }];
- if (projectName !== "new") {
- localStorage.setItem(
- `messages-${projectName}`,
- JSON.stringify(newMessages)
- );
- }
- return newMessages;
- });
- return id;
- };
-
- const updateLastMessage = (content: string, files?: File[]) => {
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(projectName),
- (oldMessages = []) => {
- const newMessages = [
- ...oldMessages.slice(0, -1),
- {
- ...oldMessages[oldMessages.length - 1],
- content,
- isThinking: false,
- files: files?.map((file) => file.path),
- },
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- return newMessages;
- }
- );
- };
-
- const updateMessage = (messageId: string, message: Partial) => {
- const key = MESSAGES_QUERY_KEY(projectName ?? "new");
- const currentMessages = queryClient.getQueryData(key);
- if (!currentMessages) return;
- const index = currentMessages.findIndex((m) => m.id === messageId);
- if (index === -1) return;
- const newMessages = [
- ...currentMessages.slice(0, index),
- { ...currentMessages[index], ...message },
- ...currentMessages.slice(index + 1),
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- queryClient.setQueryData(key, newMessages);
- };
-
- const storeMessages = async (newProjectName: string) => {
- return new Promise((resolve) => {
- const currentMessages = queryClient.getQueryData(
- MESSAGES_QUERY_KEY("new")
- );
- localStorage.setItem(
- `messages-${newProjectName}`,
- JSON.stringify(currentMessages)
- );
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(newProjectName),
- currentMessages
- );
- setTimeout(() => resolve(true), 100);
- });
- };
-
- const createProject = async (
- files: File[],
- projectTitle: string,
- indexMessage: string,
- prompt: string
- ) => {
- updateMessage(indexMessage, {
- actions: [
- {
- label: "Publishing on Hugging Face...",
- variant: "default",
- loading: true,
- type: MessageActionType.PUBLISH_PROJECT,
- },
- ],
- });
- try {
- const response = await fetch("/api/projects", {
- method: "POST",
- body: JSON.stringify({
- projectTitle,
- files,
- prompt,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- }).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to publish project");
- });
-
- if (response.repoUrl) {
- toast.success("Project has been published, build in progress...");
- updateMessage(indexMessage, {
- actions: [
- {
- label: "See Live preview",
- variant: "default",
- type: MessageActionType.SEE_LIVE_PREVIEW,
- },
- ],
- });
- storeMessages(response.repoUrl).then(() => {
- router.push(`/${response.repoUrl}`);
- });
- }
- } catch (error) {
- toast.error("Failed to publish project");
- updateMessage(indexMessage, {
- actions: [
- {
- label: "Publish on Hugging Face",
- variant: "default",
- type: MessageActionType.PUBLISH_PROJECT,
- projectTitle,
- prompt,
- },
- ],
- });
- }
- };
-
- const callAi = async (
- {
- prompt,
- model,
- onComplete,
- provider = "auto",
- redesignMd,
- medias,
- }: {
- prompt: string;
- model: string;
- redesignMd?: {
- url: string;
- md: string;
- } | null;
- medias?: string[] | null;
- onComplete: () => void;
- provider?: ProviderType;
- },
- setModel: (model: string) => void
- ) => {
- setIsLoading(true);
- const messages = getMessages();
- const files = getFiles();
- const filesToUse = await getContextFilesFromPrompt(prompt, files);
- const previousMessages = [...messages]?.filter(
- (message) => !message.isAutomated || !message.isAborted
- );
- addMessage({
- role: "user",
- content: `${
- redesignMd?.url ? `Redesign: ${redesignMd.url}\n` : ""
- }${prompt}`,
- createdAt: new Date(),
- });
- addMessage({
- role: "assistant",
- isThinking: true,
- createdAt: new Date(),
- model,
- });
-
- const isFollowUp = files?.length > 0;
- abortController.current = new AbortController();
-
- const request = await fetch("/api/ask", {
- method: "POST",
- body: JSON.stringify({
- prompt,
- model,
- files: filesToUse,
- previousMessages,
- provider,
- redesignMd,
- medias,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- ...(abortController.current
- ? { signal: abortController.current.signal }
- : {}),
- });
-
- const currentMessages = getMessages();
-
- if (!request.ok) {
- const jsonResponse = await request.json()?.catch(() => null);
- const errorMessage =
- jsonResponse?.error || `Status code: ${request.status}`;
-
- const lastMessageId = currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isThinking: false,
- isAborted: true,
- content: `Error: ${errorMessage}`,
- });
- setIsLoading(false);
- return;
- }
-
- if (request && request.body) {
- const reader = request.body.getReader();
- const decoder = new TextDecoder();
- let completeResponse = "";
- const read = async () => {
- const { done, value } = await reader.read();
- if (done) {
- audio.current?.play();
- const files = getFiles();
- const {
- messageContent,
- files: newFiles,
- projectTitle,
- } = formatResponse(completeResponse, files ?? []);
- updateLastMessage(messageContent, newFiles);
- if (newFiles && newFiles.length > 0) {
- setFiles(newFiles);
- onComplete();
- if (projectName === "new") {
- addMessage({
- role: "assistant",
- content:
- "I've finished the generation. Now you can decide to publish the project on Hugging Face to share it!",
- createdAt: new Date(),
- isAutomated: true,
- actions: [
- {
- label: "Publish on Hugging Face",
- variant: "default",
- type: MessageActionType.PUBLISH_PROJECT,
- prompt,
- projectTitle,
- },
- ],
- });
- } else {
- const response = await fetch(
- `/api/projects/${projectName.split("/")[1]}`,
- {
- method: "PUT",
- body: JSON.stringify({
- files: newFiles,
- prompt,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- }
- ).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- });
- if (response.success) {
- toast.success("Project has been updated");
- } else {
- toast.error("Failed to update project");
- }
- }
- }
-
- setIsLoading(false);
- return;
- }
- const chunk = decoder.decode(value, { stream: true });
- completeResponse += chunk;
-
- if (completeResponse.includes("__ERROR__:")) {
- const errorMatch = completeResponse.match(/__ERROR__:(.+)/);
- if (errorMatch) {
- try {
- const errorData = JSON.parse(errorMatch[1]);
- if (errorData.isError) {
- const lastMessageId =
- currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isThinking: false,
- isAborted: true,
- content: errorData?.showProMessage
- ? session?.user?.isPro ? "You have already reached your monthly included credits with Hugging Face Pro plan. Please consider adding more credits to your account." : "You have exceeded your monthly included credits with Hugging Face inference provider. Please consider upgrading to a pro plan."
- : `Error: ${errorData.messageError}`,
- actions: errorData?.showProMessage
- ? session?.user?.isPro ? [{
- label: "Add more credits",
- variant: "default",
- type: MessageActionType.ADD_CREDITS,
- }] : [
- {
- label: "Upgrade to Pro",
- variant: "pro",
- type: MessageActionType.UPGRADE_TO_PRO,
- },
- ]
- : [],
- });
- setIsLoading(false);
- return;
- }
- } catch (e) {
- console.error("Failed to parse error message:", e);
- }
- }
- }
- if (
- completeResponse.includes(
- "_Note: The selected model was not available. Switched to"
- )
- ) {
- const newModel = completeResponse
- .match(
- /The selected model was not available. Switched to (.+)/
- )?.[1]
- .replace(/`/g, "")
- .replace(" ", "")
- .replace(/\.|_$/g, "");
- if (newModel) {
- setModel(newModel);
- updateMessage(currentMessages[currentMessages.length - 1].id, {
- model: newModel,
- });
- }
- }
-
- const files = getFiles();
- const { messageContent, files: newFiles } = formatResponse(
- completeResponse,
- files ?? []
- );
- if (messageContent) updateLastMessage(messageContent);
- if (newFiles && newFiles.length > 0) {
- if (!isFollowUp) {
- setFiles(newFiles);
- }
- updateLastMessage(messageContent, newFiles);
- }
- read();
- };
- return await read();
- }
- };
-
- const stopGeneration = () => {
- if (abortController.current) {
- abortController.current.abort();
- abortController.current = null;
- setIsLoading(false);
- const currentMessages = getMessages();
- const lastMessageId = currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isAborted: true,
- isThinking: false,
- });
- }
- };
-
- return {
- callAi,
- isLoading,
- stopGeneration,
- files: getFiles(),
- createProject,
- audio,
- };
-};
diff --git a/components/chat/index.tsx b/components/chat/index.tsx
deleted file mode 100644
index ececa27bb2dccd800b7796ebefe7cde50360e441..0000000000000000000000000000000000000000
--- a/components/chat/index.tsx
+++ /dev/null
@@ -1,377 +0,0 @@
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { useSession } from "next-auth/react";
-import { cn } from "@/lib/utils";
-import { ChevronRight, ExternalLink } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
-import Image from "next/image";
-import Markdown from "react-markdown";
-import { useQueryClient } from "@tanstack/react-query";
-import { formatDistanceToNow } from "date-fns";
-import { SpaceEntry } from "@huggingface/hub";
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism";
-
-import { useChat } from "./useChat";
-import { AiLoading } from "@/components/ask-ai/loading";
-import Loading from "@/components/loading";
-import { useGeneration } from "@/components/ask-ai/useGeneration";
-import { MessageAction, MessageActionType, File } from "@/lib/type";
-import { Button } from "@/components/ui/button";
-import { getFileIcon } from "@/components/ask-ai/input-mentions";
-import ProIcon from "@/assets/pro.svg";
-import { ProModal } from "../pro-modal";
-
-const ASSISTANT_AVATAR_URL = "https://i.imgur.com/Ho6v0or.jpeg";
-
-export function AppEditorChat({
- isNew,
- projectName,
- onSelectFile,
-}: {
- isNew?: boolean;
- projectName?: string;
- onSelectFile: (file: string) => void;
-}) {
- const chatContainerRef = useRef(null);
- const { data: session } = useSession();
- const queryClient = useQueryClient();
- const chatProjectName = isNew ? "new" : projectName ?? "new";
- const { messages } = useChat(chatProjectName);
- const { isLoading, createProject } = useGeneration(chatProjectName);
- const [openProModal, setOpenProModal] = useState(false);
-
- const project = queryClient.getQueryData(["project"]);
-
- const handleShowLastFile = (files?: string[]) => {
- if (files && files.length > 0) {
- const lastGeneratedFile = files[files.length - 1];
- if (lastGeneratedFile) {
- onSelectFile?.(lastGeneratedFile);
- }
- }
- };
- const handleActions = (action: MessageAction, messageId: string) => {
- if (!action) return;
- switch (action.type) {
- case MessageActionType.PUBLISH_PROJECT:
- const files = queryClient.getQueryData(["files"]) ?? [];
- return createProject(
- files ?? [],
- action.projectTitle ?? "",
- messageId,
- action.prompt ?? ""
- );
- case MessageActionType.SEE_LIVE_PREVIEW:
- return window.open(
- `https://huggingface.co/spaces/${project?.name}`,
- "_blank"
- );
- case MessageActionType.UPGRADE_TO_PRO:
- return setOpenProModal(true);
- case MessageActionType.ADD_CREDITS:
- return window.open(
- "https://huggingface.co/settings/billing?add-credits=true",
- "_blank"
- );
- }
- };
-
- useEffect(() => {
- if (chatContainerRef.current) {
- chatContainerRef.current.scrollTop =
- chatContainerRef.current.scrollHeight;
- }
- }, [messages]);
-
- return (
-
- {isNew ? (
-
- Start a new conversation with the AI
-
- ) : (
-
- Your last conversation has not been restored.
-
- )}
-
- {messages.map((message, id) => (
-
-
-
-
- {message.role === "user"
- ? session?.user?.name?.charAt(0) ?? ""
- : ""}
-
-
-
-
-
(
- {children}
- ),
- code: ({ className, children }) => {
- const isCodeBlock = className?.startsWith("language-");
- const language = className?.replace("language-", "");
- if (isCodeBlock) {
- return (
-
- {String(children).trim()}
-
- );
- }
-
- return (
-
- {children}
-
- );
- },
- ul: ({ children }) => (
-
- ),
- ol: ({ children }) => (
-
- {children}
-
- ),
- li: ({ children }) => (
- {children}
- ),
- a: ({ children, href }) => {
- const isValidUrl =
- href &&
- (href.startsWith("http://") ||
- href.startsWith("https://") ||
- href.startsWith("/") ||
- href.startsWith("#") ||
- href.startsWith("mailto:") ||
- href.startsWith("tel:"));
- const safeHref = isValidUrl ? href : "#";
-
- return (
-
-
- {children}
-
- );
- },
- pre: ({ children }) => <>{children}>,
- p: ({ children }) => {
- const content = String(children);
- if (
- typeof children === "string" &&
- (content.includes("file:/") ||
- content.match(/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?/i))
- ) {
- const parts = content.split(/(file:\/\S+|https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?)/gi);
- return (
-
- {parts.filter(Boolean).map((part, index) => {
- if (!part || part.trim() === "") return null;
-
- if (part.startsWith("file:/")) {
- return (
-
- {getFileIcon(part, "size-2.5")}
- {part.replace("file:/", "")}
-
- );
- } else if (
- part.match(
- /^https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?$/i
- )
- ) {
- const displayUrl =
- part.length > 35
- ? part.substring(0, 35) + "..."
- : part;
- return (
-
- 🖼️
- {displayUrl}
-
- );
- }
- return part;
- })}
-
- );
- }
- return {children}
;
- },
- }}
- >
- {message.content}
-
- {message.isThinking && (
-
- )}
- {message.isAborted && (
-
- The request has been aborted due to an error OR the user has
- stopped the generation.
-
- )}
-
- {message.model && message.createdAt && (
-
-
- via{" "}
-
- {message.model}
-
-
- {!message.isThinking && (
-
- {formatDistanceToNow(message.createdAt, {
- addSuffix: true,
- })}
-
- )}
-
- )}
- {message.files && message.files.length > 0 && (
-
-
- {isLoading &&
- messages[messages.length - 1].id === message.id ? (
- <>
-
-
- {isNew ? "Creating" : "Editing"}{" "}
-
-
- >
- ) : (
- <>
-
- {isNew ? "Created" : "Edited"}{" "}
-
- {message.files.length > 1 && (
-
- (+ {message.files.length - 1} files)
-
- )}
-
-
- >
- )}
-
-
- )}
- {message.actions &&
- message.actions.length > 0 &&
- messages.length - 1 === id && (
-
- {message.actions.map((action, id) => (
-
- ))}
-
- )}
-
-
- ))}
-
-
-
- );
-}
-
-const FileCode = ({ file }: { file: string }) => (
-
- {file}
-
-);
diff --git a/components/chat/useChat.ts b/components/chat/useChat.ts
deleted file mode 100644
index 2f32019379f2fed9411cae356d5ad7b50e4ef7d5..0000000000000000000000000000000000000000
--- a/components/chat/useChat.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useLocalStorage } from "react-use";
-import { useEffect } from "react";
-import { v4 as uuidv4 } from "uuid";
-
-import { Message } from "@/lib/type";
-
-const MESSAGES_QUERY_KEY = (projectName: string) =>
- ["messages", projectName] as const;
-
-export function useChat(projectName: string) {
- const queryClient = useQueryClient();
- const [, setStoredMessages, clearStoredMessages] = useLocalStorage(
- `messages-${projectName}`,
- []
- );
-
- useEffect(() => {
- if (projectName !== "new") {
- queryClient.invalidateQueries({
- queryKey: MESSAGES_QUERY_KEY(projectName),
- });
- }
- }, [projectName, queryClient]);
-
- const { data: messages = [] } = useQuery({
- queryKey: MESSAGES_QUERY_KEY(projectName),
- queryFn: () => {
- if (projectName === "new") {
- return [];
- }
- const storedData = localStorage.getItem(`messages-${projectName}`);
- if (storedData) {
- try {
- const parsedMessages = JSON.parse(storedData);
- if (parsedMessages && parsedMessages.length > 0) {
- return parsedMessages;
- }
- } catch (error) {
- console.error("Failed to parse stored messages:", error);
- }
- }
- return [];
- },
- refetchOnMount: false,
- refetchOnWindowFocus: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- refetchIntervalInBackground: false,
- });
-
- const addMessage = (message: Omit) => {
- const id = uuidv4();
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(projectName),
- (oldMessages = []) => {
- const newMessages = [
- ...oldMessages,
- {
- ...message,
- id,
- },
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- return newMessages;
- }
- );
- return id;
- };
- const clearMessages = () => {
- queryClient.setQueryData(MESSAGES_QUERY_KEY(projectName), []);
- clearStoredMessages();
- };
-
- const storeMessages = async (newProjectName: string) => {
- return new Promise((resolve) => {
- const currentMessages = queryClient.getQueryData(
- MESSAGES_QUERY_KEY("new")
- );
- localStorage.setItem(
- `messages-${newProjectName}`,
- JSON.stringify(currentMessages)
- );
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(newProjectName),
- currentMessages
- );
- setTimeout(() => resolve(true), 100);
- });
- };
-
- return {
- messages,
- addMessage,
- clearMessages,
- storeMessages,
- };
-}
diff --git a/components/code/index.tsx b/components/code/index.tsx
deleted file mode 100644
index 6b6b687f828b7bf8a5bd66afb958552132281f2f..0000000000000000000000000000000000000000
--- a/components/code/index.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import { useState } from "react";
-import {
- SandpackLayout,
- SandpackFileExplorer,
-} from "@codesandbox/sandpack-react";
-import { ChevronRight, ChevronLeft } from "lucide-react";
-import { useParams } from "next/navigation";
-import { useQueryClient } from "@tanstack/react-query";
-import { format } from "date-fns";
-import { useUpdateEffect } from "react-use";
-
-import { AppEditorMonacoEditor } from "./monaco-editor";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import Loading from "../loading";
-import { ProjectWithCommits } from "@/actions/projects";
-
-export function AppEditorCode() {
- const queryClient = useQueryClient();
- const { repoId } = useParams<{ repoId: string }>();
-
- const [isFileExplorerCollapsed, setIsFileExplorerCollapsed] = useState(true);
- const [isSavingChanges, setIsSavingChanges] = useState(false);
- const [isSavingChangesSuccess, setIsSavingChangesSuccess] = useState(false);
- const [isSavingChangesError, setIsSavingChangesError] = useState(false);
- const [isClosing, setIsClosing] = useState(false);
- const [showSaveChanges, setShowSaveChanges] = useState(false);
-
- const handleSaveChanges = async () => {
- if (repoId === "new") return;
- setIsSavingChanges(true);
- const manuallyUpdatedFiles =
- queryClient.getQueryData(["manuallyUpdatedFiles"]) ?? [];
- const response = await fetch(`/api/projects/${repoId}`, {
- method: "PUT",
- body: JSON.stringify({
- files: manuallyUpdatedFiles,
- prompt: `✍️ ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Manual changes.`,
- isManualChanges: true,
- }),
- }).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to save changes");
- });
- if (response.success) {
- setIsSavingChangesSuccess(true);
- queryClient.invalidateQueries({ queryKey: ["manuallyUpdatedFiles"] });
- queryClient.setQueryData(
- ["project"],
- (oldProject: ProjectWithCommits) => ({
- ...oldProject,
- commits: [response.commit, ...(oldProject?.commits ?? [])],
- })
- );
- } else {
- setIsSavingChangesError(true);
- }
- setIsSavingChanges(false);
- setShowSaveChanges(false);
- };
-
- const undoChanges = () => {
- queryClient.setQueryData(["manuallyUpdatedFiles"], []);
- setShowSaveChanges(false);
- };
-
- useUpdateEffect(() => {
- if (isSavingChangesSuccess) {
- setTimeout(() => setIsSavingChangesSuccess(false), 3000);
- }
- }, [isSavingChangesSuccess]);
-
- useUpdateEffect(() => {
- if (isClosing) {
- setTimeout(() => {
- setIsSavingChangesSuccess(false);
- setIsSavingChangesError(false);
- setIsClosing(false);
- }, 300);
- }
- }, [isClosing]);
-
- return (
-
-
-
-
-
-
- {repoId && (
-
-
-
-
- {isSavingChangesSuccess
- ? "Changes saved"
- : isSavingChangesError
- ? "Failed to save changes"
- : "Save Changes"}
-
-
- {isSavingChangesSuccess
- ? "Changes saved successfully"
- : isSavingChangesError
- ? "Something went wrong, please try again."
- : "You have unsaved manual changes. Click the button to save your changes."}
-
-
-
- {!isSavingChangesSuccess && !isSavingChanges && (
-
- )}
-
- {isSavingChangesSuccess || isSavingChangesError ? (
-
- ) : (
-
- )}
-
-
-
- )}
-
- );
-}
diff --git a/components/code/monaco-editor.tsx b/components/code/monaco-editor.tsx
deleted file mode 100644
index d222ce260193d76d9da71b784259a01653782381..0000000000000000000000000000000000000000
--- a/components/code/monaco-editor.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { useMemo, useRef, useCallback, useEffect } from "react";
-import { useQueryClient } from "@tanstack/react-query";
-import { Monaco } from "@monaco-editor/react";
-import {
- useActiveCode,
- SandpackStack,
- FileTabs,
- useSandpack,
-} from "@codesandbox/sandpack-react";
-import { useTheme } from "next-themes";
-
-import NightLight from "@/components/editor/night-light.json";
-import Night from "@/components/editor/night.json";
-import { File } from "@/lib/type";
-import dynamic from "next/dynamic";
-
-const Editor = dynamic(
- () => import("@monaco-editor/react").then((mod) => mod.Editor),
- { ssr: false }
-);
-
-const LANGUAGE_MAP = {
- js: "javascript",
- ts: "typescript",
- html: "html",
- css: "css",
- json: "json",
- txt: "text",
-};
-
-export function AppEditorMonacoEditor({
- setShowSaveChanges,
-}: {
- setShowSaveChanges: (show: boolean) => void;
-}) {
- const { theme } = useTheme();
- const { code, updateCode } = useActiveCode();
- const { sandpack } = useSandpack();
- const queryClient = useQueryClient();
- const updateTimeoutRef = useRef(null);
-
- const handleEditorDidMount = (monaco: Monaco) => {
- monaco.editor.defineTheme("NightLight", {
- base: "vs",
- inherit: true,
- ...NightLight,
- rules: [],
- });
- monaco.editor.defineTheme("Night", {
- base: "vs-dark",
- ...Night,
- rules: [],
- });
- };
-
- const language = useMemo(() => {
- const extension = sandpack.activeFile
- .split(".")
- .pop()
- ?.toLowerCase() as string;
- return LANGUAGE_MAP[extension as keyof typeof LANGUAGE_MAP] ?? "text";
- }, [sandpack.activeFile]);
-
- const updateFile = useCallback(
- (newValue: string, activeFile: string) => {
- if (updateTimeoutRef.current) {
- clearTimeout(updateTimeoutRef.current);
- }
-
- updateTimeoutRef.current = setTimeout(() => {
- const manuallyUpdatedFiles =
- queryClient.getQueryData(["manuallyUpdatedFiles"]) ?? [];
- const fileIndex = manuallyUpdatedFiles.findIndex(
- (file) => file.path === activeFile
- );
- if (fileIndex !== -1) {
- manuallyUpdatedFiles[fileIndex].content = newValue;
- } else {
- manuallyUpdatedFiles.push({
- path: activeFile,
- content: newValue,
- });
- }
- queryClient.setQueryData(
- ["manuallyUpdatedFiles"],
- manuallyUpdatedFiles
- );
- }, 100);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [queryClient]
- );
-
- const handleEditorChange = (value: string | undefined) => {
- setShowSaveChanges(true);
- const newValue = value || "";
- updateCode(newValue);
- const activeFile = sandpack.activeFile?.replace(/^\//, "");
- updateFile(newValue, activeFile);
- };
-
- useEffect(() => {
- return () => {
- if (updateTimeoutRef.current) {
- clearTimeout(updateTimeoutRef.current);
- }
- };
- }, []);
-
- const themeEditor = useMemo(() => {
- const isSystemDark =
- window.matchMedia &&
- window.matchMedia("(prefers-color-scheme: dark)").matches;
- const effectiveTheme =
- theme === "system" ? (isSystemDark ? "dark" : "light") : theme;
- return effectiveTheme === "dark" ? "Night" : "NightLight";
- }, [theme]);
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/components/code/useEditor.ts b/components/code/useEditor.ts
deleted file mode 100644
index 06ed500729a17413283a42c8f9bf395f255b1518..0000000000000000000000000000000000000000
--- a/components/code/useEditor.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useProject } from "@/components/projects/useProject";
-import { useMemo } from "react";
-
-const LANGUAGE_MAP = {
- "py": "python",
- "js": "javascript",
- "ts": "typescript",
- "html": "html",
- "css": "css",
- "json": "json",
- "txt": "text",
-}
-
-export const useEditor = () => {
- const queryClient = useQueryClient();
- const { files } = useProject();
-
- const { data: isFileListOpen } = useQuery({
- queryKey: ["isFileListOpen"],
- queryFn: () => {
- return false;
- },
- initialData: false,
- refetchInterval: false,
- refetchOnWindowFocus: false,
- });
-
- const setIsFileListOpen = (isOpen: boolean) => {
- queryClient.setQueryData(["isFileListOpen"], () => isOpen);
- }
-
- const { data: editorFilePath } = useQuery({
- queryKey: ["editorFile"],
- queryFn: () => {
- return "app.py";
- },
- initialData: "app.py",
- refetchInterval: false,
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchIntervalInBackground: false,
- });
-
- const setEditorFilePath = (path: string) => {
- queryClient.setQueryData(["editorFile"], () => path);
- }
-
- const editorFileData = useMemo(() => {
- const finalFile = { path: "", content: "", language: "" };
- if (editorFilePath) {
- const file = files?.find((file) => file.path === editorFilePath);
- if (file) {
- finalFile.path = file.path;
- finalFile.content = file.content ?? "";
- finalFile.language = LANGUAGE_MAP[file.path.split(".").pop()?.toLowerCase() as keyof typeof LANGUAGE_MAP] ?? "text";
- }
- }
-
- return finalFile
- }, [editorFilePath, files])
-
- return {
- editorFilePath,
- editorFileData,
- setEditorFilePath,
- isFileListOpen,
- setIsFileListOpen,
- }
-}
\ No newline at end of file
diff --git a/components/contexts/app-context.tsx b/components/contexts/app-context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a97820d9e26fa6197ef583ce88aba66a3bc10082
--- /dev/null
+++ b/components/contexts/app-context.tsx
@@ -0,0 +1,57 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import { useUser } from "@/hooks/useUser";
+import { usePathname, useRouter } from "next/navigation";
+import { useMount } from "react-use";
+import { UserContext } from "@/components/contexts/user-context";
+import { User } from "@/types";
+import { toast } from "sonner";
+import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
+
+export default function AppContext({
+ children,
+ me: initialData,
+}: {
+ children: React.ReactNode;
+ me?: {
+ user: User | null;
+ errCode: number | null;
+ };
+}) {
+ const { loginFromCode, user, logout, loading, errCode } =
+ useUser(initialData);
+ const pathname = usePathname();
+ const router = useRouter();
+
+ useMount(() => {
+ if (!initialData?.user && !user) {
+ if ([401, 403].includes(errCode as number)) {
+ logout();
+ } else if (pathname.includes("/spaces")) {
+ if (errCode) {
+ toast.error("An error occured while trying to log in");
+ }
+ // If we did not manage to log in (probs because api is down), we simply redirect to the home page
+ router.push("/");
+ }
+ }
+ });
+
+ const events: any = {};
+
+ useBroadcastChannel("auth", (message) => {
+ if (pathname.includes("/auth/callback")) return;
+
+ if (!message.code) return;
+ if (message.type === "user-oauth" && message?.code && !events.code) {
+ loginFromCode(message.code);
+ }
+ });
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/contexts/user-context.tsx b/components/contexts/user-context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8a3391744618bfcfc979401cdee76051c70fee8f
--- /dev/null
+++ b/components/contexts/user-context.tsx
@@ -0,0 +1,8 @@
+"use client";
+
+import { createContext } from "react";
+import { User } from "@/types";
+
+export const UserContext = createContext({
+ user: undefined as User | undefined,
+});
diff --git a/components/editor/ask-ai/follow-up-tooltip.tsx b/components/editor/ask-ai/follow-up-tooltip.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5ebb4a29de5de5cca175eb6795f1d069be6ba02b
--- /dev/null
+++ b/components/editor/ask-ai/follow-up-tooltip.tsx
@@ -0,0 +1,36 @@
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Info } from "lucide-react";
+
+export const FollowUpTooltip = () => {
+ return (
+
+
+
+
+
+
+
+
+ Using the Diff-Patch system, allow DeepSite to intelligently update
+ your project without rewritting the entire codebase.
+
+
+ This means faster updates, less data usage, and a more efficient
+ development process.
+
+
+
+
+ );
+};
diff --git a/components/editor/ask-ai/index.tsx b/components/editor/ask-ai/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2076173a327338cdc34a3d8c66c22cc3e72bc408
--- /dev/null
+++ b/components/editor/ask-ai/index.tsx
@@ -0,0 +1,500 @@
+"use client";
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { useState, useMemo, useRef } from "react";
+import classNames from "classnames";
+import { toast } from "sonner";
+import { useLocalStorage, useUpdateEffect } from "react-use";
+import { ArrowUp, ChevronDown, Crosshair } from "lucide-react";
+import { FaStopCircle } from "react-icons/fa";
+
+import ProModal from "@/components/pro-modal";
+import { Button } from "@/components/ui/button";
+import { MODELS } from "@/lib/providers";
+import { HtmlHistory, Page, Project } from "@/types";
+// import { InviteFriends } from "@/components/invite-friends";
+import { Settings } from "@/components/editor/ask-ai/settings";
+import { LoginModal } from "@/components/login-modal";
+import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
+import Loading from "@/components/loading";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
+import { TooltipContent } from "@radix-ui/react-tooltip";
+import { SelectedHtmlElement } from "./selected-html-element";
+import { FollowUpTooltip } from "./follow-up-tooltip";
+import { isTheSameHtml } from "@/lib/compare-html-diff";
+import { useCallAi } from "@/hooks/useCallAi";
+import { SelectedFiles } from "./selected-files";
+import { Uploader } from "./uploader";
+
+export function AskAI({
+ isNew,
+ project,
+ images,
+ currentPage,
+ previousPrompts,
+ onScrollToBottom,
+ isAiWorking,
+ setisAiWorking,
+ isEditableModeEnabled = false,
+ pages,
+ htmlHistory,
+ selectedElement,
+ setSelectedElement,
+ selectedFiles,
+ setSelectedFiles,
+ setIsEditableModeEnabled,
+ onNewPrompt,
+ onSuccess,
+ setPages,
+ setCurrentPage,
+}: {
+ project?: Project | null;
+ currentPage: Page;
+ images?: string[];
+ pages: Page[];
+ onScrollToBottom: () => void;
+ previousPrompts: string[];
+ isAiWorking: boolean;
+ onNewPrompt: (prompt: string) => void;
+ htmlHistory?: HtmlHistory[];
+ setisAiWorking: React.Dispatch>;
+ isNew?: boolean;
+ onSuccess: (page: Page[], p: string, n?: number[][]) => void;
+ isEditableModeEnabled: boolean;
+ setIsEditableModeEnabled: React.Dispatch>;
+ selectedElement?: HTMLElement | null;
+ setSelectedElement: React.Dispatch>;
+ selectedFiles: string[];
+ setSelectedFiles: React.Dispatch>;
+ setPages: React.Dispatch>;
+ setCurrentPage: React.Dispatch>;
+}) {
+ const refThink = useRef(null);
+
+ const [open, setOpen] = useState(false);
+ const [prompt, setPrompt] = useState("");
+ const [provider, setProvider] = useLocalStorage("provider", "auto");
+ const [model, setModel] = useLocalStorage("model", MODELS[0].value);
+ const [openProvider, setOpenProvider] = useState(false);
+ const [providerError, setProviderError] = useState("");
+ const [openProModal, setOpenProModal] = useState(false);
+ const [openThink, setOpenThink] = useState(false);
+ const [isThinking, setIsThinking] = useState(true);
+ const [think, setThink] = useState("");
+ const [isFollowUp, setIsFollowUp] = useState(true);
+ const [isUploading, setIsUploading] = useState(false);
+ const [files, setFiles] = useState(images ?? []);
+
+ const {
+ callAiNewProject,
+ callAiFollowUp,
+ callAiNewPage,
+ stopController,
+ audio: hookAudio,
+ } = useCallAi({
+ onNewPrompt,
+ onSuccess,
+ onScrollToBottom,
+ setPages,
+ setCurrentPage,
+ currentPage,
+ pages,
+ isAiWorking,
+ setisAiWorking,
+ });
+
+ const selectedModel = useMemo(() => {
+ return MODELS.find((m: { value: string }) => m.value === model);
+ }, [model]);
+
+ const callAi = async (redesignMarkdown?: string) => {
+ if (isAiWorking) return;
+ if (!redesignMarkdown && !prompt.trim()) return;
+
+ if (isFollowUp && !redesignMarkdown && !isSameHtml) {
+ // Use follow-up function for existing projects
+ const selectedElementHtml = selectedElement
+ ? selectedElement.outerHTML
+ : "";
+
+ const result = await callAiFollowUp(
+ prompt,
+ model,
+ provider,
+ previousPrompts,
+ selectedElementHtml,
+ selectedFiles
+ );
+
+ if (result?.error) {
+ handleError(result.error, result.message);
+ return;
+ }
+
+ if (result?.success) {
+ setPrompt("");
+ }
+ } else if (isFollowUp && pages.length > 1 && isSameHtml) {
+ const result = await callAiNewPage(
+ prompt,
+ model,
+ provider,
+ currentPage.path,
+ [
+ ...(previousPrompts ?? []),
+ ...(htmlHistory?.map((h) => h.prompt) ?? []),
+ ]
+ );
+ if (result?.error) {
+ handleError(result.error, result.message);
+ return;
+ }
+
+ if (result?.success) {
+ setPrompt("");
+ }
+ } else {
+ const result = await callAiNewProject(
+ prompt,
+ model,
+ provider,
+ redesignMarkdown,
+ handleThink,
+ () => {
+ setIsThinking(false);
+ }
+ );
+
+ if (result?.error) {
+ handleError(result.error, result.message);
+ return;
+ }
+
+ if (result?.success) {
+ setPrompt("");
+ if (selectedModel?.isThinker) {
+ setModel(MODELS[0].value);
+ }
+ }
+ }
+ };
+
+ const handleThink = (think: string) => {
+ setThink(think);
+ setIsThinking(true);
+ setOpenThink(true);
+ };
+
+ const handleError = (error: string, message?: string) => {
+ switch (error) {
+ case "login_required":
+ setOpen(true);
+ break;
+ case "provider_required":
+ setOpenProvider(true);
+ setProviderError(message || "");
+ break;
+ case "pro_required":
+ setOpenProModal(true);
+ break;
+ case "api_error":
+ toast.error(message || "An error occurred");
+ break;
+ case "network_error":
+ toast.error(message || "Network error occurred");
+ break;
+ default:
+ toast.error("An unexpected error occurred");
+ }
+ };
+
+ useUpdateEffect(() => {
+ if (refThink.current) {
+ refThink.current.scrollTop = refThink.current.scrollHeight;
+ }
+ }, [think]);
+
+ useUpdateEffect(() => {
+ if (!isThinking) {
+ setOpenThink(false);
+ }
+ }, [isThinking]);
+
+ const isSameHtml = useMemo(() => {
+ return isTheSameHtml(currentPage.html);
+ }, [currentPage.html]);
+
+ return (
+
+
+ {think && (
+
+ )}
+
+ setSelectedFiles((prev) => prev.filter((f) => f !== file))
+ }
+ />
+ {selectedElement && (
+
+ setSelectedElement(null)}
+ />
+
+ )}
+
+ {(isAiWorking || isUploading) && (
+
+
+
+
+ {isUploading ? (
+ "Uploading images..."
+ ) : isAiWorking && !isSameHtml ? (
+ "AI is working..."
+ ) : (
+
+ {[
+ "D",
+ "e",
+ "e",
+ "p",
+ "S",
+ "i",
+ "t",
+ "e",
+ " ",
+ "i",
+ "s",
+ " ",
+ "T",
+ "h",
+ "i",
+ "n",
+ "k",
+ "i",
+ "n",
+ "g",
+ ".",
+ ".",
+ ".",
+ " ",
+ "W",
+ "a",
+ "i",
+ "t",
+ " ",
+ "a",
+ " ",
+ "m",
+ "o",
+ "m",
+ "e",
+ "n",
+ "t",
+ ".",
+ ".",
+ ".",
+ ].map((char, index) => (
+
+ {char === " " ? "\u00A0" : char}
+
+ ))}
+
+ )}
+
+
+ {isAiWorking && (
+
+
+ Stop generation
+
+ )}
+
+ )}
+
+
+
+ {
+ if (selectedFiles.includes(file)) {
+ setSelectedFiles((prev) => prev.filter((f) => f !== file));
+ } else {
+ setSelectedFiles((prev) => [...prev, file]);
+ }
+ }}
+ files={files}
+ selectedFiles={selectedFiles}
+ project={project}
+ />
+ {isNew && callAi(md)} />}
+ {!isSameHtml && (
+
+
+
+
+
+ Select an element on the page to ask DeepSite edit it
+ directly.
+
+
+ )}
+ {/* */}
+
+
+
+ setOpen(false)} pages={pages} />
+ setOpenProModal(false)}
+ />
+ {pages.length === 1 && (
+
+
+ NEW
+
+
+ DeepSite can now create multiple pages at once. Try it!
+
+
+ )}
+ {!isSameHtml && (
+
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/components/ask-ai/redesign.tsx b/components/editor/ask-ai/re-imagine.tsx
similarity index 65%
rename from components/ask-ai/redesign.tsx
rename to components/editor/ask-ai/re-imagine.tsx
index 3cddd074da060741f6d413c5fdc31c9dba86c460..7fd5d170e0f417f4f2daf38f9b77629f64cf1e95 100644
--- a/components/ask-ai/redesign.tsx
+++ b/components/editor/ask-ai/re-imagine.tsx
@@ -10,16 +10,16 @@ import {
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import Loading from "@/components/loading";
+import { api } from "@/lib/api";
-export function Redesign({
+export function ReImagine({
onRedesign,
}: {
- onRedesign: (md: string, url: string) => void;
+ onRedesign: (md: string) => void;
}) {
const [url, setUrl] = useState("");
const [open, setOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
const checkIfUrlIsValid = (url: string) => {
const urlPattern = new RegExp(
@@ -30,8 +30,7 @@ export function Redesign({
};
const handleClick = async () => {
- setError(null);
- if (isLoading) return;
+ if (isLoading) return; // Prevent multiple clicks while loading
if (!url) {
toast.error("Please enter a URL.");
return;
@@ -41,24 +40,16 @@ export function Redesign({
return;
}
setIsLoading(true);
- const response = await fetch("/api/redesign", {
- method: "PUT",
- body: JSON.stringify({ url: url.trim() }),
- headers: {
- "Content-Type": "application/json",
- },
- })
- .then(async (response) => {
- if (response?.ok) {
- const data = await response.json();
- return data;
- }
- })
- .catch((error) => {
- setError(error.message);
- });
- if (response.ok) {
- onRedesign(response.markdown, url);
+ const response = await api.put("/re-design", {
+ url: url.trim(),
+ });
+ if (response?.data?.ok) {
+ setOpen(false);
+ setUrl("");
+ onRedesign(response.data.markdown);
+ toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
+ } else {
+ toast.error(response?.data?.error || "Failed to redesign the site.");
}
setIsLoading(false);
};
@@ -68,20 +59,18 @@ export function Redesign({