Spaces:
Building
Building
Update Dockerfile
#446
by
XciD
HF Staff
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .env.example +0 -4
- .gitattributes +0 -1
- .gitignore +1 -1
- Dockerfile +4 -11
- README.md +14 -8
- actions/mentions.ts +0 -31
- actions/projects.ts +0 -175
- app/(public)/layout.tsx +4 -3
- app/(public)/page.tsx +3 -19
- app/[namespace]/[repoId]/page.tsx +28 -0
- app/[owner]/[repoId]/page.tsx +0 -32
- app/actions/auth.ts +18 -0
- app/actions/projects.ts +47 -0
- app/api/ask/route.ts +697 -130
- app/api/auth/[...nextauth]/route.ts +0 -6
- app/api/auth/login-url/route.ts +23 -0
- app/api/auth/logout/route.ts +25 -0
- app/api/auth/route.ts +106 -0
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +190 -0
- app/api/me/projects/[namespace]/[repoId]/images/route.ts +113 -0
- app/api/me/projects/[namespace]/[repoId]/route.ts +187 -0
- app/api/me/projects/[namespace]/[repoId]/save/route.ts +64 -0
- app/api/me/projects/route.ts +107 -0
- app/api/me/route.ts +46 -0
- app/api/projects/[repoId]/[commitId]/route.ts +0 -49
- app/api/projects/[repoId]/medias/route.ts +0 -87
- app/api/projects/[repoId]/rename/route.ts +0 -76
- app/api/projects/[repoId]/route.ts +0 -97
- app/api/projects/route.ts +0 -108
- app/api/re-design/route.ts +39 -0
- app/api/redesign/route.ts +0 -73
- app/auth/callback/page.tsx +97 -0
- app/auth/page.tsx +28 -0
- app/layout.tsx +62 -42
- app/new/page.tsx +10 -14
- app/not-found.tsx +0 -17
- app/sitemap.ts +28 -0
- {app → assets}/globals.css +236 -33
- assets/hf-logo.svg +0 -7
- assets/logo.svg +316 -0
- assets/minimax.svg +0 -1
- assets/pro.svg +0 -10
- components.json +4 -5
- components/animated-blobs/index.tsx +34 -0
- components/animated-text/index.tsx +123 -0
- components/ask-ai/ask-ai-landing.tsx +0 -75
- components/ask-ai/ask-ai.tsx +0 -211
- components/ask-ai/context.tsx +0 -123
- components/ask-ai/input-mentions.tsx +0 -314
- components/ask-ai/models.tsx +0 -211
.env.example
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
AUTH_HUGGINGFACE_ID=
|
| 2 |
-
AUTH_HUGGINGFACE_SECRET=
|
| 3 |
-
NEXTAUTH_URL=http://localhost:3001
|
| 4 |
-
AUTH_SECRET=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitattributes
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
public/banner.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -31,7 +31,7 @@ yarn-error.log*
|
|
| 31 |
.pnpm-debug.log*
|
| 32 |
|
| 33 |
# env files (can opt-in for committing if needed)
|
| 34 |
-
.env
|
| 35 |
|
| 36 |
# vercel
|
| 37 |
.vercel
|
|
|
|
| 31 |
.pnpm-debug.log*
|
| 32 |
|
| 33 |
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
|
| 36 |
# vercel
|
| 37 |
.vercel
|
Dockerfile
CHANGED
|
@@ -1,22 +1,15 @@
|
|
| 1 |
-
FROM node:
|
| 2 |
-
USER root
|
| 3 |
-
|
| 4 |
-
# Install pnpm
|
| 5 |
-
RUN corepack enable && corepack prepare pnpm@latest --activate
|
| 6 |
|
| 7 |
USER 1000
|
| 8 |
WORKDIR /usr/src/app
|
| 9 |
-
# Copy package.json and pnpm-lock.yaml to the container
|
| 10 |
-
COPY --chown=1000 package.json pnpm-lock.yaml ./
|
| 11 |
|
| 12 |
# Copy the rest of the application files to the container
|
| 13 |
COPY --chown=1000 . .
|
| 14 |
|
| 15 |
-
RUN
|
| 16 |
-
RUN pnpm run build
|
| 17 |
|
| 18 |
# Expose the application port (assuming your app runs on port 3000)
|
| 19 |
-
EXPOSE
|
| 20 |
|
| 21 |
# Start the application
|
| 22 |
-
CMD ["
|
|
|
|
| 1 |
+
FROM node:22
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
USER 1000
|
| 4 |
WORKDIR /usr/src/app
|
|
|
|
|
|
|
| 5 |
|
| 6 |
# Copy the rest of the application files to the container
|
| 7 |
COPY --chown=1000 . .
|
| 8 |
|
| 9 |
+
RUN npm install && npm run build
|
|
|
|
| 10 |
|
| 11 |
# Expose the application port (assuming your app runs on port 3000)
|
| 12 |
+
EXPOSE 3000
|
| 13 |
|
| 14 |
# Start the application
|
| 15 |
+
CMD ["npm", "start"]
|
README.md
CHANGED
|
@@ -1,23 +1,29 @@
|
|
| 1 |
---
|
| 2 |
-
title: DeepSite
|
| 3 |
emoji: 🐳
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: true
|
| 8 |
-
app_port:
|
| 9 |
license: mit
|
| 10 |
-
|
| 11 |
-
short_description: Generate any application by Vibe Coding it
|
| 12 |
models:
|
| 13 |
- deepseek-ai/DeepSeek-V3-0324
|
| 14 |
-
- deepseek-ai/DeepSeek-
|
| 15 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
- moonshotai/Kimi-K2-Instruct-0905
|
| 17 |
-
- zai-org/GLM-4.
|
| 18 |
-
- MiniMaxAI/MiniMax-M2.1
|
| 19 |
---
|
| 20 |
|
| 21 |
# DeepSite 🐳
|
| 22 |
|
| 23 |
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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: DeepSite v3
|
| 3 |
emoji: 🐳
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: true
|
| 8 |
+
app_port: 3000
|
| 9 |
license: mit
|
| 10 |
+
short_description: Generate any application by Vibe Coding
|
|
|
|
| 11 |
models:
|
| 12 |
- deepseek-ai/DeepSeek-V3-0324
|
| 13 |
+
- deepseek-ai/DeepSeek-R1-0528
|
| 14 |
+
- deepseek-ai/DeepSeek-V3.1
|
| 15 |
+
- deepseek-ai/DeepSeek-V3.1-Terminus
|
| 16 |
+
- deepseek-ai/DeepSeek-V3.2-Exp
|
| 17 |
+
- Qwen/Qwen3-Coder-480B-A35B-Instruct
|
| 18 |
+
- moonshotai/Kimi-K2-Instruct
|
| 19 |
- moonshotai/Kimi-K2-Instruct-0905
|
| 20 |
+
- zai-org/GLM-4.6
|
|
|
|
| 21 |
---
|
| 22 |
|
| 23 |
# DeepSite 🐳
|
| 24 |
|
| 25 |
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.
|
| 26 |
+
|
| 27 |
+
## How to use it locally
|
| 28 |
+
|
| 29 |
+
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
actions/mentions.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { File } from "@/lib/type";
|
| 4 |
-
|
| 5 |
-
export const searchMentions = async (query: string) => {
|
| 6 |
-
const promises = [searchModels(query), searchDatasets(query)];
|
| 7 |
-
const results = await Promise.all(promises);
|
| 8 |
-
return { models: results[0], datasets: results[1] };
|
| 9 |
-
};
|
| 10 |
-
|
| 11 |
-
const searchModels = async (query: string) => {
|
| 12 |
-
const response = await fetch(
|
| 13 |
-
`https://huggingface.co/api/quicksearch?q=${query}&type=model&limit=3`
|
| 14 |
-
);
|
| 15 |
-
const data = await response.json();
|
| 16 |
-
return data?.models ?? [];
|
| 17 |
-
};
|
| 18 |
-
|
| 19 |
-
const searchDatasets = async (query: string) => {
|
| 20 |
-
const response = await fetch(
|
| 21 |
-
`https://huggingface.co/api/quicksearch?q=${query}&type=dataset&limit=3`
|
| 22 |
-
);
|
| 23 |
-
const data = await response.json();
|
| 24 |
-
return data?.datasets ?? [];
|
| 25 |
-
};
|
| 26 |
-
|
| 27 |
-
export const searchFilesMentions = async (query: string, files: File[]) => {
|
| 28 |
-
if (!query) return files;
|
| 29 |
-
const lowerQuery = query.toLowerCase();
|
| 30 |
-
return files.filter((file) => file.path.toLowerCase().includes(lowerQuery));
|
| 31 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actions/projects.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
| 1 |
-
"use server";
|
| 2 |
-
import {
|
| 3 |
-
downloadFile,
|
| 4 |
-
listCommits,
|
| 5 |
-
listFiles,
|
| 6 |
-
listSpaces,
|
| 7 |
-
RepoDesignation,
|
| 8 |
-
SpaceEntry,
|
| 9 |
-
spaceInfo,
|
| 10 |
-
} from "@huggingface/hub";
|
| 11 |
-
|
| 12 |
-
import { auth } from "@/lib/auth";
|
| 13 |
-
import { Commit, File } from "@/lib/type";
|
| 14 |
-
|
| 15 |
-
export interface ProjectWithCommits extends SpaceEntry {
|
| 16 |
-
commits?: Commit[];
|
| 17 |
-
medias?: string[];
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"];
|
| 21 |
-
const IGNORED_FORMATS = [
|
| 22 |
-
".png",
|
| 23 |
-
".jpg",
|
| 24 |
-
".jpeg",
|
| 25 |
-
".gif",
|
| 26 |
-
".svg",
|
| 27 |
-
".webp",
|
| 28 |
-
".mp4",
|
| 29 |
-
".mp3",
|
| 30 |
-
".wav",
|
| 31 |
-
];
|
| 32 |
-
|
| 33 |
-
export const getProjects = async () => {
|
| 34 |
-
const projects: SpaceEntry[] = [];
|
| 35 |
-
const session = await auth();
|
| 36 |
-
if (!session?.user) {
|
| 37 |
-
return projects;
|
| 38 |
-
}
|
| 39 |
-
const token = session.accessToken;
|
| 40 |
-
for await (const space of listSpaces({
|
| 41 |
-
accessToken: token,
|
| 42 |
-
additionalFields: ["author", "cardData"],
|
| 43 |
-
search: {
|
| 44 |
-
owner: "enzostvs",
|
| 45 |
-
},
|
| 46 |
-
})) {
|
| 47 |
-
if (
|
| 48 |
-
space.sdk === "static" &&
|
| 49 |
-
Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
|
| 50 |
-
(space.cardData as { tags?: string[] })?.tags?.some((tag) =>
|
| 51 |
-
tag.includes("deepsite")
|
| 52 |
-
)
|
| 53 |
-
) {
|
| 54 |
-
projects.push(space);
|
| 55 |
-
}
|
| 56 |
-
}
|
| 57 |
-
return projects;
|
| 58 |
-
};
|
| 59 |
-
export const getProject = async (id: string, commitId?: string) => {
|
| 60 |
-
const session = await auth();
|
| 61 |
-
if (!session?.user) {
|
| 62 |
-
return null;
|
| 63 |
-
}
|
| 64 |
-
const token = session.accessToken;
|
| 65 |
-
try {
|
| 66 |
-
const project: ProjectWithCommits | null = await spaceInfo({
|
| 67 |
-
name: id,
|
| 68 |
-
accessToken: token,
|
| 69 |
-
additionalFields: ["author", "cardData"],
|
| 70 |
-
});
|
| 71 |
-
const repo: RepoDesignation = {
|
| 72 |
-
type: "space",
|
| 73 |
-
name: id,
|
| 74 |
-
};
|
| 75 |
-
const files: File[] = [];
|
| 76 |
-
const medias: string[] = [];
|
| 77 |
-
const params = { repo, accessToken: token };
|
| 78 |
-
if (commitId) {
|
| 79 |
-
Object.assign(params, { revision: commitId });
|
| 80 |
-
}
|
| 81 |
-
for await (const fileInfo of listFiles(params)) {
|
| 82 |
-
if (IGNORED_PATHS.includes(fileInfo.path)) continue;
|
| 83 |
-
if (IGNORED_FORMATS.some((format) => fileInfo.path.endsWith(format))) {
|
| 84 |
-
medias.push(
|
| 85 |
-
`https://huggingface.co/spaces/${id}/resolve/main/${fileInfo.path}`
|
| 86 |
-
);
|
| 87 |
-
continue;
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
if (fileInfo.type === "directory") {
|
| 91 |
-
for await (const subFile of listFiles({
|
| 92 |
-
repo,
|
| 93 |
-
accessToken: token,
|
| 94 |
-
path: fileInfo.path,
|
| 95 |
-
})) {
|
| 96 |
-
if (IGNORED_FORMATS.some((format) => subFile.path.endsWith(format))) {
|
| 97 |
-
medias.push(
|
| 98 |
-
`https://huggingface.co/spaces/${id}/resolve/main/${subFile.path}`
|
| 99 |
-
);
|
| 100 |
-
}
|
| 101 |
-
const blob = await downloadFile({
|
| 102 |
-
repo,
|
| 103 |
-
accessToken: token,
|
| 104 |
-
path: subFile.path,
|
| 105 |
-
raw: true,
|
| 106 |
-
...(commitId ? { revision: commitId } : {}),
|
| 107 |
-
}).catch((_) => {
|
| 108 |
-
return null;
|
| 109 |
-
});
|
| 110 |
-
if (!blob) {
|
| 111 |
-
continue;
|
| 112 |
-
}
|
| 113 |
-
const html = await blob?.text();
|
| 114 |
-
if (!html) {
|
| 115 |
-
continue;
|
| 116 |
-
}
|
| 117 |
-
files[subFile.path === "index.html" ? "unshift" : "push"]({
|
| 118 |
-
path: subFile.path,
|
| 119 |
-
content: html,
|
| 120 |
-
});
|
| 121 |
-
}
|
| 122 |
-
} else {
|
| 123 |
-
const blob = await downloadFile({
|
| 124 |
-
repo,
|
| 125 |
-
accessToken: token,
|
| 126 |
-
path: fileInfo.path,
|
| 127 |
-
raw: true,
|
| 128 |
-
...(commitId ? { revision: commitId } : {}),
|
| 129 |
-
}).catch((_) => {
|
| 130 |
-
return null;
|
| 131 |
-
});
|
| 132 |
-
if (!blob) {
|
| 133 |
-
continue;
|
| 134 |
-
}
|
| 135 |
-
const html = await blob?.text();
|
| 136 |
-
if (!html) {
|
| 137 |
-
continue;
|
| 138 |
-
}
|
| 139 |
-
files[fileInfo.path === "index.html" ? "unshift" : "push"]({
|
| 140 |
-
path: fileInfo.path,
|
| 141 |
-
content: html,
|
| 142 |
-
});
|
| 143 |
-
}
|
| 144 |
-
}
|
| 145 |
-
const commits: Commit[] = [];
|
| 146 |
-
const commitIterator = listCommits({ repo, accessToken: token });
|
| 147 |
-
for await (const commit of commitIterator) {
|
| 148 |
-
if (
|
| 149 |
-
commit.title?.toLowerCase() === "initial commit" ||
|
| 150 |
-
commit.title
|
| 151 |
-
?.toLowerCase()
|
| 152 |
-
?.includes("upload media files through deepsite")
|
| 153 |
-
)
|
| 154 |
-
continue;
|
| 155 |
-
commits.push({
|
| 156 |
-
title: commit.title,
|
| 157 |
-
oid: commit.oid,
|
| 158 |
-
date: commit.date,
|
| 159 |
-
});
|
| 160 |
-
if (commits.length >= 20) {
|
| 161 |
-
break;
|
| 162 |
-
}
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
project.commits = commits;
|
| 166 |
-
project.medias = medias;
|
| 167 |
-
|
| 168 |
-
return { project, files };
|
| 169 |
-
} catch (error) {
|
| 170 |
-
return {
|
| 171 |
-
project: null,
|
| 172 |
-
files: [],
|
| 173 |
-
};
|
| 174 |
-
}
|
| 175 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/(public)/layout.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
-
import
|
| 2 |
|
| 3 |
-
export default function PublicLayout({
|
| 4 |
children,
|
| 5 |
}: Readonly<{
|
| 6 |
children: React.ReactNode;
|
| 7 |
}>) {
|
| 8 |
return (
|
| 9 |
-
<div className="
|
|
|
|
| 10 |
<Navigation />
|
| 11 |
{children}
|
| 12 |
</div>
|
|
|
|
| 1 |
+
import Navigation from "@/components/public/navigation";
|
| 2 |
|
| 3 |
+
export default async function PublicLayout({
|
| 4 |
children,
|
| 5 |
}: Readonly<{
|
| 6 |
children: React.ReactNode;
|
| 7 |
}>) {
|
| 8 |
return (
|
| 9 |
+
<div className="h-screen bg-neutral-950 z-1 relative overflow-auto scroll-smooth">
|
| 10 |
+
<div className="background__noisy" />
|
| 11 |
<Navigation />
|
| 12 |
{children}
|
| 13 |
</div>
|
app/(public)/page.tsx
CHANGED
|
@@ -1,21 +1,5 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import { HeroHeader } from "@/components/public/hero-header";
|
| 3 |
-
import { UserProjects } from "@/components/projects/user-projects";
|
| 4 |
-
import { AskAiLanding } from "@/components/ask-ai/ask-ai-landing";
|
| 5 |
|
| 6 |
-
export default async function
|
| 7 |
-
return
|
| 8 |
-
<>
|
| 9 |
-
<section className="container mx-auto relative z-10">
|
| 10 |
-
<HeroHeader />
|
| 11 |
-
<div className="absolute inset-0 -z-10">
|
| 12 |
-
<AnimatedDotsBackground />
|
| 13 |
-
</div>
|
| 14 |
-
<div className="max-w-xl mx-auto px-6">
|
| 15 |
-
<AskAiLanding />
|
| 16 |
-
</div>
|
| 17 |
-
</section>
|
| 18 |
-
<UserProjects />
|
| 19 |
-
</>
|
| 20 |
-
);
|
| 21 |
}
|
|
|
|
| 1 |
+
import { MyProjects } from "@/components/my-projects";
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
export default async function HomePage() {
|
| 4 |
+
return <MyProjects />;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
}
|
app/[namespace]/[repoId]/page.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AppEditor } from "@/components/editor";
|
| 2 |
+
import { generateSEO } from "@/lib/seo";
|
| 3 |
+
import { Metadata } from "next";
|
| 4 |
+
|
| 5 |
+
export async function generateMetadata({
|
| 6 |
+
params,
|
| 7 |
+
}: {
|
| 8 |
+
params: Promise<{ namespace: string; repoId: string }>;
|
| 9 |
+
}): Promise<Metadata> {
|
| 10 |
+
const { namespace, repoId } = await params;
|
| 11 |
+
|
| 12 |
+
return generateSEO({
|
| 13 |
+
title: `${namespace}/${repoId} - DeepSite Editor`,
|
| 14 |
+
description: `Edit and build ${namespace}/${repoId} with AI-powered tools on DeepSite. Create stunning websites with no code required.`,
|
| 15 |
+
path: `/${namespace}/${repoId}`,
|
| 16 |
+
// Prevent indexing of individual project editor pages if they contain sensitive content
|
| 17 |
+
noIndex: false, // Set to true if you want to keep project pages private
|
| 18 |
+
});
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export default async function ProjectNamespacePage({
|
| 22 |
+
params,
|
| 23 |
+
}: {
|
| 24 |
+
params: Promise<{ namespace: string; repoId: string }>;
|
| 25 |
+
}) {
|
| 26 |
+
const { namespace, repoId } = await params;
|
| 27 |
+
return <AppEditor namespace={namespace} repoId={repoId} />;
|
| 28 |
+
}
|
app/[owner]/[repoId]/page.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
import { getProject } from "@/actions/projects";
|
| 2 |
-
import { AppEditor } from "@/components/editor";
|
| 3 |
-
import { auth } from "@/lib/auth";
|
| 4 |
-
import { notFound, redirect } from "next/navigation";
|
| 5 |
-
|
| 6 |
-
export default async function ProjectPage({
|
| 7 |
-
params,
|
| 8 |
-
searchParams,
|
| 9 |
-
}: {
|
| 10 |
-
params: Promise<{ owner: string; repoId: string }>;
|
| 11 |
-
searchParams: Promise<{ commit?: string }>;
|
| 12 |
-
}) {
|
| 13 |
-
const session = await auth();
|
| 14 |
-
|
| 15 |
-
if (!session) {
|
| 16 |
-
redirect("/deepsite/api/auth/signin");
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
const { owner, repoId } = await params;
|
| 20 |
-
const { commit } = await searchParams;
|
| 21 |
-
const datas = await getProject(`${owner}/${repoId}`, commit);
|
| 22 |
-
if (!datas?.project) {
|
| 23 |
-
return notFound();
|
| 24 |
-
}
|
| 25 |
-
return (
|
| 26 |
-
<AppEditor
|
| 27 |
-
project={datas.project}
|
| 28 |
-
files={datas.files ?? []}
|
| 29 |
-
isHistoryView={!!commit}
|
| 30 |
-
/>
|
| 31 |
-
);
|
| 32 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/actions/auth.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use server";
|
| 2 |
+
|
| 3 |
+
import { headers } from "next/headers";
|
| 4 |
+
|
| 5 |
+
export async function getAuth() {
|
| 6 |
+
const authList = await headers();
|
| 7 |
+
const host = authList.get("host") ?? "localhost:3000";
|
| 8 |
+
const url = host.includes("/spaces/enzostvs")
|
| 9 |
+
? "enzostvs-deepsite.hf.space"
|
| 10 |
+
: host;
|
| 11 |
+
const redirect_uri =
|
| 12 |
+
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 13 |
+
url +
|
| 14 |
+
"/auth/callback";
|
| 15 |
+
|
| 16 |
+
const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
|
| 17 |
+
return loginRedirectUrl;
|
| 18 |
+
}
|
app/actions/projects.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use server";
|
| 2 |
+
|
| 3 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 4 |
+
import { NextResponse } from "next/server";
|
| 5 |
+
import { listSpaces } from "@huggingface/hub";
|
| 6 |
+
import { ProjectType } from "@/types";
|
| 7 |
+
|
| 8 |
+
export async function getProjects(): Promise<{
|
| 9 |
+
ok: boolean;
|
| 10 |
+
projects: ProjectType[];
|
| 11 |
+
isEmpty?: boolean;
|
| 12 |
+
}> {
|
| 13 |
+
const user = await isAuthenticated();
|
| 14 |
+
|
| 15 |
+
if (user instanceof NextResponse || !user) {
|
| 16 |
+
return {
|
| 17 |
+
ok: false,
|
| 18 |
+
projects: [],
|
| 19 |
+
};
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const projects = [];
|
| 23 |
+
for await (const space of listSpaces({
|
| 24 |
+
accessToken: user.token as string,
|
| 25 |
+
additionalFields: ["author", "cardData"],
|
| 26 |
+
search: {
|
| 27 |
+
owner: user.name,
|
| 28 |
+
}
|
| 29 |
+
})) {
|
| 30 |
+
if (
|
| 31 |
+
!space.private &&
|
| 32 |
+
space.sdk === "static" &&
|
| 33 |
+
Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
|
| 34 |
+
(
|
| 35 |
+
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
| 36 |
+
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
| 37 |
+
)
|
| 38 |
+
) {
|
| 39 |
+
projects.push(space);
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return {
|
| 44 |
+
ok: true,
|
| 45 |
+
projects,
|
| 46 |
+
};
|
| 47 |
+
}
|
app/api/ask/route.ts
CHANGED
|
@@ -1,37 +1,101 @@
|
|
|
|
|
|
|
|
| 1 |
import { NextResponse } from "next/server";
|
|
|
|
| 2 |
import { InferenceClient } from "@huggingface/inference";
|
| 3 |
|
| 4 |
-
import {
|
| 5 |
-
import {
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
const token = session.accessToken;
|
| 15 |
|
| 16 |
const body = await request.json();
|
| 17 |
-
const {
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
try {
|
| 37 |
const encoder = new TextEncoder();
|
|
@@ -45,138 +109,641 @@ export async function POST(request: Request) {
|
|
| 45 |
Connection: "keep-alive",
|
| 46 |
},
|
| 47 |
});
|
| 48 |
-
(async () => {
|
| 49 |
-
let hasRetried = false;
|
| 50 |
-
let currentModel = model;
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
messages: [
|
| 57 |
{
|
| 58 |
role: "system",
|
| 59 |
-
content:
|
| 60 |
-
files.length > 0
|
| 61 |
-
? FOLLOW_UP_SYSTEM_PROMPT
|
| 62 |
-
: INITIAL_SYSTEM_PROMPT,
|
| 63 |
},
|
| 64 |
-
...previousMessages.map((message: Message) => ({
|
| 65 |
-
role: message.role,
|
| 66 |
-
content: message.content,
|
| 67 |
-
})),
|
| 68 |
-
...(files?.length > 0
|
| 69 |
-
? [
|
| 70 |
-
{
|
| 71 |
-
role: "user",
|
| 72 |
-
content: `Here are the files that the user has provider:${files
|
| 73 |
-
.map(
|
| 74 |
-
(file: File) =>
|
| 75 |
-
`File: ${file.path}\nContent: ${file.content}`
|
| 76 |
-
)
|
| 77 |
-
.join("\n")}\n\n${prompt}`,
|
| 78 |
-
},
|
| 79 |
-
]
|
| 80 |
-
: []),
|
| 81 |
{
|
| 82 |
role: "user",
|
| 83 |
-
content:
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
}${prompt} ${
|
| 87 |
-
medias && medias.length > 0
|
| 88 |
-
? `\nHere is the list of my media files: ${medias.join(
|
| 89 |
-
", "
|
| 90 |
-
)}\n`
|
| 91 |
-
: ""
|
| 92 |
-
}`,
|
| 93 |
},
|
| 94 |
],
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
const { done, value } = await chatCompletion.next();
|
| 100 |
-
if (done) {
|
| 101 |
-
break;
|
| 102 |
-
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
}
|
| 109 |
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
)
|
| 122 |
-
)
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
} else {
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
}
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
}
|
|
|
|
|
|
|
| 167 |
}
|
| 168 |
-
};
|
| 169 |
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
return NextResponse.json(
|
| 176 |
{
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
| 178 |
},
|
| 179 |
{ status: 500 }
|
| 180 |
);
|
| 181 |
}
|
| 182 |
}
|
|
|
|
|
|
| 1 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
+
import type { NextRequest } from "next/server";
|
| 3 |
import { NextResponse } from "next/server";
|
| 4 |
+
import { headers } from "next/headers";
|
| 5 |
import { InferenceClient } from "@huggingface/inference";
|
| 6 |
|
| 7 |
+
import { MODELS } from "@/lib/providers";
|
| 8 |
+
import {
|
| 9 |
+
DIVIDER,
|
| 10 |
+
FOLLOW_UP_SYSTEM_PROMPT,
|
| 11 |
+
INITIAL_SYSTEM_PROMPT,
|
| 12 |
+
MAX_REQUESTS_PER_IP,
|
| 13 |
+
NEW_PAGE_END,
|
| 14 |
+
NEW_PAGE_START,
|
| 15 |
+
REPLACE_END,
|
| 16 |
+
SEARCH_START,
|
| 17 |
+
UPDATE_PAGE_START,
|
| 18 |
+
UPDATE_PAGE_END,
|
| 19 |
+
PROMPT_FOR_PROJECT_NAME,
|
| 20 |
+
} from "@/lib/prompts";
|
| 21 |
+
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
|
| 22 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 23 |
+
import { Page } from "@/types";
|
| 24 |
+
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 25 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 26 |
+
import { getBestProvider } from "@/lib/best-provider";
|
| 27 |
+
// import { rewritePrompt } from "@/lib/rewrite-prompt";
|
| 28 |
+
import { COLORS } from "@/lib/utils";
|
| 29 |
+
import { templates } from "@/lib/templates";
|
| 30 |
|
| 31 |
+
const ipAddresses = new Map();
|
| 32 |
+
|
| 33 |
+
export async function POST(request: NextRequest) {
|
| 34 |
+
const authHeaders = await headers();
|
| 35 |
+
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
|
|
|
| 36 |
|
| 37 |
const body = await request.json();
|
| 38 |
+
const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
|
| 39 |
+
|
| 40 |
+
if (!model || (!prompt && !redesignMarkdown)) {
|
| 41 |
+
return NextResponse.json(
|
| 42 |
+
{ ok: false, error: "Missing required fields" },
|
| 43 |
+
{ status: 400 }
|
| 44 |
+
);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const selectedModel = MODELS.find(
|
| 48 |
+
(m) => m.value === model || m.label === model
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
if (!selectedModel) {
|
| 52 |
+
return NextResponse.json(
|
| 53 |
+
{ ok: false, error: "Invalid model selected" },
|
| 54 |
+
{ status: 400 }
|
| 55 |
+
);
|
| 56 |
}
|
| 57 |
+
|
| 58 |
+
let token: string | null = null;
|
| 59 |
+
if (userToken) token = userToken;
|
| 60 |
+
let billTo: string | null = null;
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Handle local usage token, this bypass the need for a user token
|
| 64 |
+
* and allows local testing without authentication.
|
| 65 |
+
* This is useful for development and testing purposes.
|
| 66 |
+
*/
|
| 67 |
+
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
|
| 68 |
+
token = process.env.HF_TOKEN;
|
| 69 |
}
|
| 70 |
|
| 71 |
+
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
|
| 72 |
+
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 73 |
+
: authHeaders.get("x-forwarded-for");
|
| 74 |
+
|
| 75 |
+
if (!token) {
|
| 76 |
+
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 77 |
+
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 78 |
+
return NextResponse.json(
|
| 79 |
+
{
|
| 80 |
+
ok: false,
|
| 81 |
+
openLogin: true,
|
| 82 |
+
message: "Log In to continue using the service",
|
| 83 |
+
},
|
| 84 |
+
{ status: 429 }
|
| 85 |
+
);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
token = process.env.DEFAULT_HF_TOKEN as string;
|
| 89 |
+
billTo = "huggingface";
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
| 93 |
+
|
| 94 |
+
let rewrittenPrompt = redesignMarkdown ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : prompt;
|
| 95 |
+
|
| 96 |
+
if (enhancedSettings.isActive) {
|
| 97 |
+
// rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
|
| 98 |
+
}
|
| 99 |
|
| 100 |
try {
|
| 101 |
const encoder = new TextEncoder();
|
|
|
|
| 109 |
Connection: "keep-alive",
|
| 110 |
},
|
| 111 |
});
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
+
(async () => {
|
| 114 |
+
// let completeResponse = "";
|
| 115 |
+
try {
|
| 116 |
+
const client = new InferenceClient(token);
|
| 117 |
+
|
| 118 |
+
const systemPrompt = INITIAL_SYSTEM_PROMPT;
|
| 119 |
+
|
| 120 |
+
const userPrompt = rewrittenPrompt;
|
| 121 |
+
const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
|
| 122 |
+
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
|
| 123 |
+
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
| 124 |
+
|
| 125 |
+
const chatCompletion = client.chatCompletionStream(
|
| 126 |
+
{
|
| 127 |
+
model: selectedModel.value,
|
| 128 |
+
provider: selectedProvider.provider,
|
| 129 |
messages: [
|
| 130 |
{
|
| 131 |
role: "system",
|
| 132 |
+
content: systemPrompt,
|
|
|
|
|
|
|
|
|
|
| 133 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
{
|
| 135 |
role: "user",
|
| 136 |
+
content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
|
| 137 |
+
2. I want to use the following secondary color: ${enhancedSettings.secondaryColor} (eg: bg-${enhancedSettings.secondaryColor}-500).
|
| 138 |
+
3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
},
|
| 140 |
],
|
| 141 |
+
...providerConfig,
|
| 142 |
+
},
|
| 143 |
+
billTo ? { billTo } : {}
|
| 144 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
+
while (true) {
|
| 147 |
+
const { done, value } = await chatCompletion.next()
|
| 148 |
+
if (done) {
|
| 149 |
+
break;
|
| 150 |
}
|
| 151 |
|
| 152 |
+
const chunk = value.choices[0]?.delta?.content;
|
| 153 |
+
if (chunk) {
|
| 154 |
+
await writer.write(encoder.encode(chunk));
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Explicitly close the writer after successful completion
|
| 159 |
+
await writer.close();
|
| 160 |
+
} catch (error: any) {
|
| 161 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 162 |
+
await writer.write(
|
| 163 |
+
encoder.encode(
|
| 164 |
+
JSON.stringify({
|
| 165 |
+
ok: false,
|
| 166 |
+
openProModal: true,
|
| 167 |
+
message: error.message,
|
| 168 |
+
})
|
| 169 |
)
|
| 170 |
+
);
|
| 171 |
+
} else if (error?.message?.includes("inference provider information")) {
|
| 172 |
+
await writer.write(
|
| 173 |
+
encoder.encode(
|
| 174 |
+
JSON.stringify({
|
| 175 |
+
ok: false,
|
| 176 |
+
openSelectProvider: true,
|
| 177 |
+
message: error.message,
|
| 178 |
+
})
|
| 179 |
+
)
|
| 180 |
+
);
|
| 181 |
+
}
|
| 182 |
+
else {
|
| 183 |
+
await writer.write(
|
| 184 |
+
encoder.encode(
|
| 185 |
+
JSON.stringify({
|
| 186 |
+
ok: false,
|
| 187 |
+
message:
|
| 188 |
+
error.message ||
|
| 189 |
+
"An error occurred while processing your request.",
|
| 190 |
+
})
|
| 191 |
+
)
|
| 192 |
+
);
|
| 193 |
+
}
|
| 194 |
+
} finally {
|
| 195 |
+
// Ensure the writer is always closed, even if already closed
|
| 196 |
+
try {
|
| 197 |
+
await writer?.close();
|
| 198 |
+
} catch {
|
| 199 |
+
// Ignore errors when closing the writer as it might already be closed
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
})();
|
| 203 |
+
|
| 204 |
+
return response;
|
| 205 |
+
} catch (error: any) {
|
| 206 |
+
return NextResponse.json(
|
| 207 |
+
{
|
| 208 |
+
ok: false,
|
| 209 |
+
openSelectProvider: true,
|
| 210 |
+
message:
|
| 211 |
+
error?.message || "An error occurred while processing your request.",
|
| 212 |
+
},
|
| 213 |
+
{ status: 500 }
|
| 214 |
+
);
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
export async function PUT(request: NextRequest) {
|
| 219 |
+
const user = await isAuthenticated();
|
| 220 |
+
if (user instanceof NextResponse || !user) {
|
| 221 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
const authHeaders = await headers();
|
| 225 |
+
|
| 226 |
+
const body = await request.json();
|
| 227 |
+
const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew, enhancedSettings } =
|
| 228 |
+
body;
|
| 229 |
+
|
| 230 |
+
let repoId = repoIdFromBody;
|
| 231 |
+
|
| 232 |
+
if (!prompt || pages.length === 0) {
|
| 233 |
+
return NextResponse.json(
|
| 234 |
+
{ ok: false, error: "Missing required fields" },
|
| 235 |
+
{ status: 400 }
|
| 236 |
+
);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
const selectedModel = MODELS.find(
|
| 240 |
+
(m) => m.value === model || m.label === model
|
| 241 |
+
);
|
| 242 |
+
if (!selectedModel) {
|
| 243 |
+
return NextResponse.json(
|
| 244 |
+
{ ok: false, error: "Invalid model selected" },
|
| 245 |
+
{ status: 400 }
|
| 246 |
+
);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
let token = user.token as string;
|
| 250 |
+
let billTo: string | null = null;
|
| 251 |
|
| 252 |
+
/**
|
| 253 |
+
* Handle local usage token, this bypass the need for a user token
|
| 254 |
+
* and allows local testing without authentication.
|
| 255 |
+
* This is useful for development and testing purposes.
|
| 256 |
+
*/
|
| 257 |
+
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
|
| 258 |
+
token = process.env.HF_TOKEN;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
|
| 262 |
+
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 263 |
+
: authHeaders.get("x-forwarded-for");
|
| 264 |
+
|
| 265 |
+
if (!token) {
|
| 266 |
+
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 267 |
+
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 268 |
+
return NextResponse.json(
|
| 269 |
+
{
|
| 270 |
+
ok: false,
|
| 271 |
+
openLogin: true,
|
| 272 |
+
message: "Log In to continue using the service",
|
| 273 |
+
},
|
| 274 |
+
{ status: 429 }
|
| 275 |
+
);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
token = process.env.DEFAULT_HF_TOKEN as string;
|
| 279 |
+
billTo = "huggingface";
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
const client = new InferenceClient(token);
|
| 283 |
+
|
| 284 |
+
const escapeRegExp = (string: string) => {
|
| 285 |
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
| 286 |
+
};
|
| 287 |
+
|
| 288 |
+
const normalizeHtml = (html: string): string => {
|
| 289 |
+
return html
|
| 290 |
+
// Normalize whitespace within tags
|
| 291 |
+
.replace(/\s+/g, ' ')
|
| 292 |
+
// Remove spaces before closing >
|
| 293 |
+
.replace(/\s+>/g, '>')
|
| 294 |
+
// Remove spaces before />
|
| 295 |
+
.replace(/\s+\/>/g, '/>')
|
| 296 |
+
// Normalize spaces around = in attributes
|
| 297 |
+
.replace(/\s*=\s*/g, '=')
|
| 298 |
+
// Normalize quotes (convert single to double)
|
| 299 |
+
.replace(/='([^']*)'/g, '="$1"')
|
| 300 |
+
// Remove trailing spaces in opening/closing tags
|
| 301 |
+
.replace(/<([^>]*?)\s+>/g, '<$1>')
|
| 302 |
+
// Normalize self-closing tags
|
| 303 |
+
.replace(/\/\s*>/g, '/>')
|
| 304 |
+
.trim();
|
| 305 |
+
};
|
| 306 |
+
|
| 307 |
+
const createFlexibleHtmlRegex = (searchBlock: string) => {
|
| 308 |
+
// Normalize both the search block for comparison
|
| 309 |
+
const normalizedSearch = normalizeHtml(searchBlock);
|
| 310 |
+
|
| 311 |
+
// Escape regex special characters
|
| 312 |
+
let searchRegex = escapeRegExp(normalizedSearch)
|
| 313 |
+
// Make whitespace flexible (but only between elements, not within tags)
|
| 314 |
+
.replace(/>\s*</g, '>\\s*<')
|
| 315 |
+
// Make line breaks and spaces around content flexible
|
| 316 |
+
.replace(/>\s*([^<]+)\s*</g, '>\\s*$1\\s*<');
|
| 317 |
+
|
| 318 |
+
return new RegExp(searchRegex, 'g');
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
| 322 |
+
|
| 323 |
+
try {
|
| 324 |
+
const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
|
| 325 |
+
const userContext = "You are modifying the HTML file based on the user's request.";
|
| 326 |
+
|
| 327 |
+
const getRelevantPages = (pages: Page[], prompt: string, maxPages: number = 2): Page[] => {
|
| 328 |
+
if (pages.length <= maxPages) return pages;
|
| 329 |
+
|
| 330 |
+
const indexPage = pages.find(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 331 |
+
const otherPages = pages.filter(p => p !== indexPage);
|
| 332 |
+
|
| 333 |
+
if (selectedElementHtml) {
|
| 334 |
+
const elementKeywords = selectedElementHtml.toLowerCase().match(/class=["']([^"']*)["']|id=["']([^"']*)["']/g) || [];
|
| 335 |
+
const relevantPages = otherPages.filter(page => {
|
| 336 |
+
const pageContent = page.html.toLowerCase();
|
| 337 |
+
return elementKeywords.some((keyword: string) => pageContent.includes(keyword.toLowerCase()));
|
| 338 |
+
});
|
| 339 |
+
|
| 340 |
+
return indexPage ? [indexPage, ...relevantPages.slice(0, maxPages - 1)] : relevantPages.slice(0, maxPages);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
const keywords = prompt.toLowerCase().split(/\s+/).filter(word => word.length > 3);
|
| 344 |
+
const scoredPages = otherPages.map(page => {
|
| 345 |
+
const pageContent = (page.path + ' ' + page.html).toLowerCase();
|
| 346 |
+
const score = keywords.reduce((acc, keyword) => {
|
| 347 |
+
return acc + (pageContent.includes(keyword) ? 1 : 0);
|
| 348 |
+
}, 0);
|
| 349 |
+
return { page, score };
|
| 350 |
+
});
|
| 351 |
+
|
| 352 |
+
const topPages = scoredPages
|
| 353 |
+
.sort((a, b) => b.score - a.score)
|
| 354 |
+
.slice(0, maxPages - (indexPage ? 1 : 0))
|
| 355 |
+
.map(item => item.page);
|
| 356 |
+
|
| 357 |
+
return indexPage ? [indexPage, ...topPages] : topPages;
|
| 358 |
+
};
|
| 359 |
+
|
| 360 |
+
const relevantPages = getRelevantPages(pages || [], prompt);
|
| 361 |
+
const pagesContext = relevantPages
|
| 362 |
+
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
| 363 |
+
.join("\n\n");
|
| 364 |
+
|
| 365 |
+
const assistantContext = `${
|
| 366 |
+
selectedElementHtml
|
| 367 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
|
| 368 |
+
: ""
|
| 369 |
+
}. Current pages (${relevantPages.length}/${pages?.length || 0} shown): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
|
| 370 |
+
|
| 371 |
+
const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
|
| 372 |
+
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
|
| 373 |
+
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
| 374 |
+
|
| 375 |
+
const chatCompletion = client.chatCompletionStream(
|
| 376 |
+
{
|
| 377 |
+
model: selectedModel.value,
|
| 378 |
+
provider: selectedProvider.provider,
|
| 379 |
+
messages: [
|
| 380 |
+
{
|
| 381 |
+
role: "system",
|
| 382 |
+
content: systemPrompt,
|
| 383 |
+
},
|
| 384 |
+
{
|
| 385 |
+
role: "user",
|
| 386 |
+
content: userContext,
|
| 387 |
+
},
|
| 388 |
+
{
|
| 389 |
+
role: "assistant",
|
| 390 |
+
content: assistantContext,
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
role: "user",
|
| 394 |
+
content: prompt,
|
| 395 |
+
},
|
| 396 |
+
],
|
| 397 |
+
...providerConfig,
|
| 398 |
+
},
|
| 399 |
+
billTo ? { billTo } : {}
|
| 400 |
+
);
|
| 401 |
+
|
| 402 |
+
let chunk = "";
|
| 403 |
+
while (true) {
|
| 404 |
+
const { done, value } = await chatCompletion.next();
|
| 405 |
+
if (done) {
|
| 406 |
+
break;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
const deltaContent = value.choices[0]?.delta?.content;
|
| 410 |
+
if (deltaContent) {
|
| 411 |
+
chunk += deltaContent;
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
if (!chunk) {
|
| 415 |
+
return NextResponse.json(
|
| 416 |
+
{ ok: false, message: "No content returned from the model" },
|
| 417 |
+
{ status: 400 }
|
| 418 |
+
);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
if (chunk) {
|
| 422 |
+
const updatedLines: number[][] = [];
|
| 423 |
+
let newHtml = "";
|
| 424 |
+
const updatedPages = [...(pages || [])];
|
| 425 |
+
|
| 426 |
+
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');
|
| 427 |
+
let updatePageMatch;
|
| 428 |
+
|
| 429 |
+
while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
|
| 430 |
+
const [, pagePath, pageContent] = updatePageMatch;
|
| 431 |
+
|
| 432 |
+
const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
| 433 |
+
if (pageIndex !== -1) {
|
| 434 |
+
let pageHtml = updatedPages[pageIndex].html;
|
| 435 |
+
|
| 436 |
+
let processedContent = pageContent;
|
| 437 |
+
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 438 |
+
if (htmlMatch) {
|
| 439 |
+
processedContent = htmlMatch[1];
|
| 440 |
}
|
| 441 |
+
let position = 0;
|
| 442 |
+
let moreBlocks = true;
|
| 443 |
+
|
| 444 |
+
while (moreBlocks) {
|
| 445 |
+
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
| 446 |
+
if (searchStartIndex === -1) {
|
| 447 |
+
moreBlocks = false;
|
| 448 |
+
continue;
|
| 449 |
+
}
|
| 450 |
|
| 451 |
+
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
| 452 |
+
if (dividerIndex === -1) {
|
| 453 |
+
moreBlocks = false;
|
| 454 |
+
continue;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
| 458 |
+
if (replaceEndIndex === -1) {
|
| 459 |
+
moreBlocks = false;
|
| 460 |
+
continue;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
const searchBlock = processedContent.substring(
|
| 464 |
+
searchStartIndex + SEARCH_START.length,
|
| 465 |
+
dividerIndex
|
| 466 |
+
);
|
| 467 |
+
const replaceBlock = processedContent.substring(
|
| 468 |
+
dividerIndex + DIVIDER.length,
|
| 469 |
+
replaceEndIndex
|
| 470 |
+
);
|
| 471 |
+
|
| 472 |
+
if (searchBlock.trim() === "") {
|
| 473 |
+
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
| 474 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 475 |
} else {
|
| 476 |
+
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 477 |
+
|
| 478 |
+
// Normalize the pageHtml for matching
|
| 479 |
+
const normalizedPageHtml = normalizeHtml(pageHtml);
|
| 480 |
+
const match = regex.exec(normalizedPageHtml);
|
| 481 |
+
|
| 482 |
+
if (match) {
|
| 483 |
+
// Find the original match in the non-normalized HTML
|
| 484 |
+
const normalizedSearch = normalizeHtml(searchBlock);
|
| 485 |
+
const originalMatchIndex = pageHtml.indexOf(searchBlock);
|
| 486 |
+
|
| 487 |
+
if (originalMatchIndex !== -1) {
|
| 488 |
+
const beforeText = pageHtml.substring(0, originalMatchIndex);
|
| 489 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 490 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 491 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 492 |
+
|
| 493 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 494 |
+
pageHtml = pageHtml.replace(searchBlock, replaceBlock);
|
| 495 |
+
} else {
|
| 496 |
+
// Fallback: try to find similar pattern in the original HTML
|
| 497 |
+
const flexibleRegex = new RegExp(
|
| 498 |
+
escapeRegExp(searchBlock)
|
| 499 |
+
.replace(/\s+/g, '\\s+')
|
| 500 |
+
.replace(/\s*=\s*/g, '\\s*=\\s*')
|
| 501 |
+
.replace(/'\s*([^']*)\s*'/g, "'\\s*$1\\s*'")
|
| 502 |
+
.replace(/"\s*([^"]*)\s*"/g, '"\\s*$1\\s*"')
|
| 503 |
+
.replace(/\s*>/g, '\\s*>')
|
| 504 |
+
.replace(/\s*\/>/g, '\\s*/>'),
|
| 505 |
+
'g'
|
| 506 |
+
);
|
| 507 |
+
|
| 508 |
+
const flexibleMatch = flexibleRegex.exec(pageHtml);
|
| 509 |
+
if (flexibleMatch) {
|
| 510 |
+
const matchedText = flexibleMatch[0];
|
| 511 |
+
const beforeText = pageHtml.substring(0, flexibleMatch.index);
|
| 512 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 513 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 514 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 515 |
+
|
| 516 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 517 |
+
pageHtml = pageHtml.replace(matchedText, replaceBlock);
|
| 518 |
+
}
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
}
|
| 522 |
+
|
| 523 |
+
position = replaceEndIndex + REPLACE_END.length;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
updatedPages[pageIndex].html = pageHtml;
|
| 527 |
+
|
| 528 |
+
if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
|
| 529 |
+
newHtml = pageHtml;
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
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');
|
| 535 |
+
let newPageMatch;
|
| 536 |
+
|
| 537 |
+
while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
|
| 538 |
+
const [, pagePath, pageContent] = newPageMatch;
|
| 539 |
+
|
| 540 |
+
let pageHtml = pageContent;
|
| 541 |
+
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 542 |
+
if (htmlMatch) {
|
| 543 |
+
pageHtml = htmlMatch[1];
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
| 547 |
+
|
| 548 |
+
if (existingPageIndex !== -1) {
|
| 549 |
+
updatedPages[existingPageIndex] = {
|
| 550 |
+
path: pagePath,
|
| 551 |
+
html: pageHtml.trim()
|
| 552 |
+
};
|
| 553 |
+
} else {
|
| 554 |
+
updatedPages.push({
|
| 555 |
+
path: pagePath,
|
| 556 |
+
html: pageHtml.trim()
|
| 557 |
+
});
|
| 558 |
+
}
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
|
| 562 |
+
let position = 0;
|
| 563 |
+
let moreBlocks = true;
|
| 564 |
+
|
| 565 |
+
while (moreBlocks) {
|
| 566 |
+
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
| 567 |
+
if (searchStartIndex === -1) {
|
| 568 |
+
moreBlocks = false;
|
| 569 |
+
continue;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
| 573 |
+
if (dividerIndex === -1) {
|
| 574 |
+
moreBlocks = false;
|
| 575 |
+
continue;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
| 579 |
+
if (replaceEndIndex === -1) {
|
| 580 |
+
moreBlocks = false;
|
| 581 |
+
continue;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
const searchBlock = chunk.substring(
|
| 585 |
+
searchStartIndex + SEARCH_START.length,
|
| 586 |
+
dividerIndex
|
| 587 |
+
);
|
| 588 |
+
const replaceBlock = chunk.substring(
|
| 589 |
+
dividerIndex + DIVIDER.length,
|
| 590 |
+
replaceEndIndex
|
| 591 |
+
);
|
| 592 |
+
|
| 593 |
+
if (searchBlock.trim() === "") {
|
| 594 |
+
newHtml = `${replaceBlock}\n${newHtml}`;
|
| 595 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 596 |
+
} else {
|
| 597 |
+
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 598 |
+
|
| 599 |
+
// Get the main page HTML (first page or index page)
|
| 600 |
+
const mainPage = updatedPages.find(p => p.path === '/' || p.path === '/index' || p.path === 'index') || updatedPages[0];
|
| 601 |
+
if (!mainPage) continue;
|
| 602 |
+
|
| 603 |
+
newHtml = mainPage.html;
|
| 604 |
+
|
| 605 |
+
// Normalize the newHtml for matching
|
| 606 |
+
const normalizedNewHtml = normalizeHtml(newHtml);
|
| 607 |
+
const match = regex.exec(normalizedNewHtml);
|
| 608 |
+
|
| 609 |
+
if (match) {
|
| 610 |
+
// Find the original match in the non-normalized HTML
|
| 611 |
+
const originalMatchIndex = newHtml.indexOf(searchBlock);
|
| 612 |
+
|
| 613 |
+
if (originalMatchIndex !== -1) {
|
| 614 |
+
const beforeText = newHtml.substring(0, originalMatchIndex);
|
| 615 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 616 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 617 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 618 |
+
|
| 619 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 620 |
+
newHtml = newHtml.replace(searchBlock, replaceBlock);
|
| 621 |
+
} else {
|
| 622 |
+
// Fallback: try to find similar pattern in the original HTML
|
| 623 |
+
const flexibleRegex = new RegExp(
|
| 624 |
+
escapeRegExp(searchBlock)
|
| 625 |
+
.replace(/\s+/g, '\\s+')
|
| 626 |
+
.replace(/\s*=\s*/g, '\\s*=\\s*')
|
| 627 |
+
.replace(/'\s*([^']*)\s*'/g, "'\\s*$1\\s*'")
|
| 628 |
+
.replace(/"\s*([^"]*)\s*"/g, '"\\s*$1\\s*"')
|
| 629 |
+
.replace(/\s*>/g, '\\s*>')
|
| 630 |
+
.replace(/\s*\/>/g, '\\s*/>'),
|
| 631 |
+
'g'
|
| 632 |
+
);
|
| 633 |
+
|
| 634 |
+
const flexibleMatch = flexibleRegex.exec(newHtml);
|
| 635 |
+
if (flexibleMatch) {
|
| 636 |
+
const matchedText = flexibleMatch[0];
|
| 637 |
+
const beforeText = newHtml.substring(0, flexibleMatch.index);
|
| 638 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 639 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 640 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 641 |
+
|
| 642 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 643 |
+
newHtml = newHtml.replace(matchedText, replaceBlock);
|
| 644 |
+
}
|
| 645 |
+
}
|
| 646 |
}
|
| 647 |
}
|
| 648 |
+
|
| 649 |
+
position = replaceEndIndex + REPLACE_END.length;
|
| 650 |
}
|
|
|
|
| 651 |
|
| 652 |
+
// Update the main HTML if it's the index page
|
| 653 |
+
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 654 |
+
if (mainPageIndex !== -1) {
|
| 655 |
+
updatedPages[mainPageIndex].html = newHtml;
|
| 656 |
+
}
|
| 657 |
+
}
|
| 658 |
|
| 659 |
+
const files: File[] = [];
|
| 660 |
+
updatedPages.forEach((page: Page) => {
|
| 661 |
+
const file = new File([page.html], page.path, { type: "text/html" });
|
| 662 |
+
files.push(file);
|
| 663 |
+
});
|
| 664 |
+
|
| 665 |
+
if (isNew) {
|
| 666 |
+
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 667 |
+
const formattedTitle = projectName?.toLowerCase()
|
| 668 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 669 |
+
.split("-")
|
| 670 |
+
.filter(Boolean)
|
| 671 |
+
.join("-")
|
| 672 |
+
.slice(0, 96);
|
| 673 |
+
const repo: RepoDesignation = {
|
| 674 |
+
type: "space",
|
| 675 |
+
name: `${user.name}/${formattedTitle}`,
|
| 676 |
+
};
|
| 677 |
+
const { repoUrl} = await createRepo({
|
| 678 |
+
repo,
|
| 679 |
+
accessToken: user.token as string,
|
| 680 |
+
});
|
| 681 |
+
repoId = repoUrl.split("/").slice(-2).join("/");
|
| 682 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 683 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 684 |
+
const README = `---
|
| 685 |
+
title: ${projectName}
|
| 686 |
+
colorFrom: ${colorFrom}
|
| 687 |
+
colorTo: ${colorTo}
|
| 688 |
+
emoji: 🐳
|
| 689 |
+
sdk: static
|
| 690 |
+
pinned: false
|
| 691 |
+
tags:
|
| 692 |
+
- deepsite-v3
|
| 693 |
+
---
|
| 694 |
+
|
| 695 |
+
# Welcome to your new DeepSite project!
|
| 696 |
+
This project was created with [DeepSite](https://deepsite.hf.co).
|
| 697 |
+
`;
|
| 698 |
+
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
const response = await uploadFiles({
|
| 702 |
+
repo: {
|
| 703 |
+
type: "space",
|
| 704 |
+
name: repoId,
|
| 705 |
+
},
|
| 706 |
+
files,
|
| 707 |
+
commitTitle: prompt,
|
| 708 |
+
accessToken: user.token as string,
|
| 709 |
+
});
|
| 710 |
+
|
| 711 |
+
return NextResponse.json({
|
| 712 |
+
ok: true,
|
| 713 |
+
updatedLines,
|
| 714 |
+
pages: updatedPages,
|
| 715 |
+
repoId,
|
| 716 |
+
commit: {
|
| 717 |
+
...response.commit,
|
| 718 |
+
title: prompt,
|
| 719 |
+
}
|
| 720 |
+
});
|
| 721 |
+
} else {
|
| 722 |
+
return NextResponse.json(
|
| 723 |
+
{ ok: false, message: "No content returned from the model" },
|
| 724 |
+
{ status: 400 }
|
| 725 |
+
);
|
| 726 |
+
}
|
| 727 |
+
} catch (error: any) {
|
| 728 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 729 |
+
return NextResponse.json(
|
| 730 |
+
{
|
| 731 |
+
ok: false,
|
| 732 |
+
openProModal: true,
|
| 733 |
+
message: error.message,
|
| 734 |
+
},
|
| 735 |
+
{ status: 402 }
|
| 736 |
+
);
|
| 737 |
+
}
|
| 738 |
return NextResponse.json(
|
| 739 |
{
|
| 740 |
+
ok: false,
|
| 741 |
+
openSelectProvider: true,
|
| 742 |
+
message:
|
| 743 |
+
error.message || "An error occurred while processing your request.",
|
| 744 |
},
|
| 745 |
{ status: 500 }
|
| 746 |
);
|
| 747 |
}
|
| 748 |
}
|
| 749 |
+
|
app/api/auth/[...nextauth]/route.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
import NextAuth from "next-auth";
|
| 2 |
-
import { authOptions } from "@/lib/auth";
|
| 3 |
-
|
| 4 |
-
const handler = NextAuth(authOptions);
|
| 5 |
-
|
| 6 |
-
export { handler as GET, handler as POST };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/login-url/route.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
export async function GET(req: NextRequest) {
|
| 4 |
+
const host = req.headers.get("host") ?? "localhost:3000";
|
| 5 |
+
|
| 6 |
+
let url: string;
|
| 7 |
+
if (host.includes("localhost")) {
|
| 8 |
+
url = host;
|
| 9 |
+
} else if (host.includes("hf.space") || host.includes("/spaces/enzostvs")) {
|
| 10 |
+
url = "enzostvs-deepsite.hf.space";
|
| 11 |
+
} else {
|
| 12 |
+
url = "deepsite.hf.co";
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const redirect_uri =
|
| 16 |
+
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 17 |
+
url +
|
| 18 |
+
"/auth/callback";
|
| 19 |
+
|
| 20 |
+
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`;
|
| 21 |
+
|
| 22 |
+
return NextResponse.json({ loginUrl: loginRedirectUrl });
|
| 23 |
+
}
|
app/api/auth/logout/route.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from "next/server";
|
| 2 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 3 |
+
|
| 4 |
+
export async function POST() {
|
| 5 |
+
const cookieName = MY_TOKEN_KEY();
|
| 6 |
+
const isProduction = process.env.NODE_ENV === "production";
|
| 7 |
+
|
| 8 |
+
const response = NextResponse.json(
|
| 9 |
+
{ message: "Logged out successfully" },
|
| 10 |
+
{ status: 200 }
|
| 11 |
+
);
|
| 12 |
+
|
| 13 |
+
// Clear the HTTP-only cookie
|
| 14 |
+
const cookieOptions = [
|
| 15 |
+
`${cookieName}=`,
|
| 16 |
+
"Max-Age=0",
|
| 17 |
+
"Path=/",
|
| 18 |
+
"HttpOnly",
|
| 19 |
+
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
| 20 |
+
].join("; ");
|
| 21 |
+
|
| 22 |
+
response.headers.set("Set-Cookie", cookieOptions);
|
| 23 |
+
|
| 24 |
+
return response;
|
| 25 |
+
}
|
app/api/auth/route.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 3 |
+
|
| 4 |
+
export async function POST(req: NextRequest) {
|
| 5 |
+
const body = await req.json();
|
| 6 |
+
const { code } = body;
|
| 7 |
+
|
| 8 |
+
if (!code) {
|
| 9 |
+
return NextResponse.json(
|
| 10 |
+
{ error: "Code is required" },
|
| 11 |
+
{
|
| 12 |
+
status: 400,
|
| 13 |
+
headers: {
|
| 14 |
+
"Content-Type": "application/json",
|
| 15 |
+
},
|
| 16 |
+
}
|
| 17 |
+
);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const Authorization = `Basic ${Buffer.from(
|
| 21 |
+
`${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
|
| 22 |
+
).toString("base64")}`;
|
| 23 |
+
|
| 24 |
+
const host =
|
| 25 |
+
req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
|
| 26 |
+
|
| 27 |
+
const url = host.includes("/spaces/enzostvs")
|
| 28 |
+
? "enzostvs-deepsite.hf.space"
|
| 29 |
+
: host;
|
| 30 |
+
const redirect_uri =
|
| 31 |
+
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 32 |
+
url +
|
| 33 |
+
"/auth/callback";
|
| 34 |
+
const request_auth = await fetch("https://huggingface.co/oauth/token", {
|
| 35 |
+
method: "POST",
|
| 36 |
+
headers: {
|
| 37 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 38 |
+
Authorization,
|
| 39 |
+
},
|
| 40 |
+
body: new URLSearchParams({
|
| 41 |
+
grant_type: "authorization_code",
|
| 42 |
+
code,
|
| 43 |
+
redirect_uri,
|
| 44 |
+
}),
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
const response = await request_auth.json();
|
| 48 |
+
if (!response.access_token) {
|
| 49 |
+
return NextResponse.json(
|
| 50 |
+
{ error: "Failed to retrieve access token" },
|
| 51 |
+
{
|
| 52 |
+
status: 400,
|
| 53 |
+
headers: {
|
| 54 |
+
"Content-Type": "application/json",
|
| 55 |
+
},
|
| 56 |
+
}
|
| 57 |
+
);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
|
| 61 |
+
headers: {
|
| 62 |
+
Authorization: `Bearer ${response.access_token}`,
|
| 63 |
+
},
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
if (!userResponse.ok) {
|
| 67 |
+
return NextResponse.json(
|
| 68 |
+
{ user: null, errCode: userResponse.status },
|
| 69 |
+
{ status: userResponse.status }
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
const user = await userResponse.json();
|
| 73 |
+
|
| 74 |
+
const cookieName = MY_TOKEN_KEY();
|
| 75 |
+
const isProduction = process.env.NODE_ENV === "production";
|
| 76 |
+
|
| 77 |
+
// Create response with user data
|
| 78 |
+
const nextResponse = NextResponse.json(
|
| 79 |
+
{
|
| 80 |
+
access_token: response.access_token,
|
| 81 |
+
expires_in: response.expires_in,
|
| 82 |
+
user,
|
| 83 |
+
// Include fallback flag for iframe contexts
|
| 84 |
+
useLocalStorageFallback: true,
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
status: 200,
|
| 88 |
+
headers: {
|
| 89 |
+
"Content-Type": "application/json",
|
| 90 |
+
},
|
| 91 |
+
}
|
| 92 |
+
);
|
| 93 |
+
|
| 94 |
+
// Set HTTP-only cookie with proper attributes for iframe support
|
| 95 |
+
const cookieOptions = [
|
| 96 |
+
`${cookieName}=${response.access_token}`,
|
| 97 |
+
`Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided
|
| 98 |
+
"Path=/",
|
| 99 |
+
"HttpOnly",
|
| 100 |
+
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
| 101 |
+
].join("; ");
|
| 102 |
+
|
| 103 |
+
nextResponse.headers.set("Set-Cookie", cookieOptions);
|
| 104 |
+
|
| 105 |
+
return nextResponse;
|
| 106 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import { Page } from "@/types";
|
| 6 |
+
|
| 7 |
+
export async function POST(
|
| 8 |
+
req: NextRequest,
|
| 9 |
+
{ params }: {
|
| 10 |
+
params: Promise<{
|
| 11 |
+
namespace: string;
|
| 12 |
+
repoId: string;
|
| 13 |
+
commitId: string;
|
| 14 |
+
}>
|
| 15 |
+
}
|
| 16 |
+
) {
|
| 17 |
+
const user = await isAuthenticated();
|
| 18 |
+
|
| 19 |
+
if (user instanceof NextResponse || !user) {
|
| 20 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const param = await params;
|
| 24 |
+
const { namespace, repoId, commitId } = param;
|
| 25 |
+
|
| 26 |
+
try {
|
| 27 |
+
const repo: RepoDesignation = {
|
| 28 |
+
type: "space",
|
| 29 |
+
name: `${namespace}/${repoId}`,
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
const space = await spaceInfo({
|
| 33 |
+
name: `${namespace}/${repoId}`,
|
| 34 |
+
accessToken: user.token as string,
|
| 35 |
+
additionalFields: ["author"],
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
if (!space || space.sdk !== "static") {
|
| 39 |
+
return NextResponse.json(
|
| 40 |
+
{ ok: false, error: "Space is not a static space." },
|
| 41 |
+
{ status: 404 }
|
| 42 |
+
);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
if (space.author !== user.name) {
|
| 46 |
+
return NextResponse.json(
|
| 47 |
+
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 48 |
+
{ status: 403 }
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Fetch files from the specific commit
|
| 53 |
+
const files: File[] = [];
|
| 54 |
+
const pages: Page[] = [];
|
| 55 |
+
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
| 56 |
+
const commitFilePaths: Set<string> = new Set();
|
| 57 |
+
|
| 58 |
+
// Get all files from the specific commit
|
| 59 |
+
for await (const fileInfo of listFiles({
|
| 60 |
+
repo,
|
| 61 |
+
accessToken: user.token as string,
|
| 62 |
+
revision: commitId,
|
| 63 |
+
})) {
|
| 64 |
+
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 65 |
+
|
| 66 |
+
if (allowedExtensions.includes(fileExtension || "")) {
|
| 67 |
+
commitFilePaths.add(fileInfo.path);
|
| 68 |
+
|
| 69 |
+
// Fetch the file content from the specific commit
|
| 70 |
+
const response = await fetch(
|
| 71 |
+
`https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
|
| 72 |
+
);
|
| 73 |
+
|
| 74 |
+
if (response.ok) {
|
| 75 |
+
const content = await response.text();
|
| 76 |
+
let mimeType = "text/plain";
|
| 77 |
+
|
| 78 |
+
switch (fileExtension) {
|
| 79 |
+
case "html":
|
| 80 |
+
mimeType = "text/html";
|
| 81 |
+
// Add HTML files to pages array for client-side setPages
|
| 82 |
+
pages.push({
|
| 83 |
+
path: fileInfo.path,
|
| 84 |
+
html: content,
|
| 85 |
+
});
|
| 86 |
+
break;
|
| 87 |
+
case "css":
|
| 88 |
+
mimeType = "text/css";
|
| 89 |
+
break;
|
| 90 |
+
case "js":
|
| 91 |
+
mimeType = "application/javascript";
|
| 92 |
+
break;
|
| 93 |
+
case "json":
|
| 94 |
+
mimeType = "application/json";
|
| 95 |
+
break;
|
| 96 |
+
case "md":
|
| 97 |
+
mimeType = "text/markdown";
|
| 98 |
+
break;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
const file = new File([content], fileInfo.path, { type: mimeType });
|
| 102 |
+
files.push(file);
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Get files currently in main branch to identify files to delete
|
| 108 |
+
const mainBranchFilePaths: Set<string> = new Set();
|
| 109 |
+
for await (const fileInfo of listFiles({
|
| 110 |
+
repo,
|
| 111 |
+
accessToken: user.token as string,
|
| 112 |
+
revision: "main",
|
| 113 |
+
})) {
|
| 114 |
+
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 115 |
+
|
| 116 |
+
if (allowedExtensions.includes(fileExtension || "")) {
|
| 117 |
+
mainBranchFilePaths.add(fileInfo.path);
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Identify files to delete (exist in main but not in commit)
|
| 122 |
+
const filesToDelete: string[] = [];
|
| 123 |
+
for (const mainFilePath of mainBranchFilePaths) {
|
| 124 |
+
if (!commitFilePaths.has(mainFilePath)) {
|
| 125 |
+
filesToDelete.push(mainFilePath);
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
if (files.length === 0 && filesToDelete.length === 0) {
|
| 130 |
+
return NextResponse.json(
|
| 131 |
+
{ ok: false, error: "No files found in the specified commit and no files to delete" },
|
| 132 |
+
{ status: 404 }
|
| 133 |
+
);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Delete files that exist in main but not in the commit being promoted
|
| 137 |
+
if (filesToDelete.length > 0) {
|
| 138 |
+
await deleteFiles({
|
| 139 |
+
repo,
|
| 140 |
+
paths: filesToDelete,
|
| 141 |
+
accessToken: user.token as string,
|
| 142 |
+
commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
|
| 143 |
+
commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
|
| 144 |
+
});
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Upload the files to the main branch with a promotion commit message
|
| 148 |
+
if (files.length > 0) {
|
| 149 |
+
await uploadFiles({
|
| 150 |
+
repo,
|
| 151 |
+
files,
|
| 152 |
+
accessToken: user.token as string,
|
| 153 |
+
commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
|
| 154 |
+
commitDescription: `Promoted commit ${commitId} to main branch`,
|
| 155 |
+
});
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
return NextResponse.json(
|
| 159 |
+
{
|
| 160 |
+
ok: true,
|
| 161 |
+
message: "Version promoted successfully",
|
| 162 |
+
promotedCommit: commitId,
|
| 163 |
+
pages: pages,
|
| 164 |
+
},
|
| 165 |
+
{ status: 200 }
|
| 166 |
+
);
|
| 167 |
+
|
| 168 |
+
} catch (error: any) {
|
| 169 |
+
|
| 170 |
+
// Handle specific HuggingFace API errors
|
| 171 |
+
if (error.statusCode === 404) {
|
| 172 |
+
return NextResponse.json(
|
| 173 |
+
{ ok: false, error: "Commit not found" },
|
| 174 |
+
{ status: 404 }
|
| 175 |
+
);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
if (error.statusCode === 403) {
|
| 179 |
+
return NextResponse.json(
|
| 180 |
+
{ ok: false, error: "Access denied to repository" },
|
| 181 |
+
{ status: 403 }
|
| 182 |
+
);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
return NextResponse.json(
|
| 186 |
+
{ ok: false, error: error.message || "Failed to promote version" },
|
| 187 |
+
{ status: 500 }
|
| 188 |
+
);
|
| 189 |
+
}
|
| 190 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/images/route.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import Project from "@/models/Project";
|
| 6 |
+
import dbConnect from "@/lib/mongodb";
|
| 7 |
+
|
| 8 |
+
export async function POST(
|
| 9 |
+
req: NextRequest,
|
| 10 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 11 |
+
) {
|
| 12 |
+
try {
|
| 13 |
+
const user = await isAuthenticated();
|
| 14 |
+
|
| 15 |
+
if (user instanceof NextResponse || !user) {
|
| 16 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const param = await params;
|
| 20 |
+
const { namespace, repoId } = param;
|
| 21 |
+
|
| 22 |
+
const space = await spaceInfo({
|
| 23 |
+
name: `${namespace}/${repoId}`,
|
| 24 |
+
accessToken: user.token as string,
|
| 25 |
+
additionalFields: ["author"],
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
if (!space || space.sdk !== "static") {
|
| 29 |
+
return NextResponse.json(
|
| 30 |
+
{ ok: false, error: "Space is not a static space." },
|
| 31 |
+
{ status: 404 }
|
| 32 |
+
);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
if (space.author !== user.name) {
|
| 36 |
+
return NextResponse.json(
|
| 37 |
+
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 38 |
+
{ status: 403 }
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Parse the FormData to get the images
|
| 43 |
+
const formData = await req.formData();
|
| 44 |
+
const imageFiles = formData.getAll("images") as File[];
|
| 45 |
+
|
| 46 |
+
if (!imageFiles || imageFiles.length === 0) {
|
| 47 |
+
return NextResponse.json(
|
| 48 |
+
{
|
| 49 |
+
ok: false,
|
| 50 |
+
error: "At least one image file is required under the 'images' key",
|
| 51 |
+
},
|
| 52 |
+
{ status: 400 }
|
| 53 |
+
);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const files: File[] = [];
|
| 57 |
+
for (const file of imageFiles) {
|
| 58 |
+
if (!(file instanceof File)) {
|
| 59 |
+
return NextResponse.json(
|
| 60 |
+
{
|
| 61 |
+
ok: false,
|
| 62 |
+
error: "Invalid file format - all items under 'images' key must be files",
|
| 63 |
+
},
|
| 64 |
+
{ status: 400 }
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
if (!file.type.startsWith('image/')) {
|
| 69 |
+
return NextResponse.json(
|
| 70 |
+
{
|
| 71 |
+
ok: false,
|
| 72 |
+
error: `File ${file.name} is not an image`,
|
| 73 |
+
},
|
| 74 |
+
{ status: 400 }
|
| 75 |
+
);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Create File object with images/ folder prefix
|
| 79 |
+
const fileName = `images/${file.name}`;
|
| 80 |
+
const processedFile = new File([file], fileName, { type: file.type });
|
| 81 |
+
files.push(processedFile);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Upload files to HuggingFace space
|
| 85 |
+
const repo: RepoDesignation = {
|
| 86 |
+
type: "space",
|
| 87 |
+
name: `${namespace}/${repoId}`,
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
await uploadFiles({
|
| 91 |
+
repo,
|
| 92 |
+
files,
|
| 93 |
+
accessToken: user.token as string,
|
| 94 |
+
commitTitle: `Upload ${files.length} image(s)`,
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
return NextResponse.json({
|
| 98 |
+
ok: true,
|
| 99 |
+
message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
|
| 100 |
+
uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
|
| 101 |
+
}, { status: 200 });
|
| 102 |
+
|
| 103 |
+
} catch (error) {
|
| 104 |
+
console.error('Error uploading images:', error);
|
| 105 |
+
return NextResponse.json(
|
| 106 |
+
{
|
| 107 |
+
ok: false,
|
| 108 |
+
error: "Failed to upload images",
|
| 109 |
+
},
|
| 110 |
+
{ status: 500 }
|
| 111 |
+
);
|
| 112 |
+
}
|
| 113 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/route.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, spaceInfo, listFiles, deleteRepo, listCommits, downloadFile } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import { Commit, Page } from "@/types";
|
| 6 |
+
|
| 7 |
+
export async function DELETE(
|
| 8 |
+
req: NextRequest,
|
| 9 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 10 |
+
) {
|
| 11 |
+
const user = await isAuthenticated();
|
| 12 |
+
|
| 13 |
+
if (user instanceof NextResponse || !user) {
|
| 14 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const param = await params;
|
| 18 |
+
const { namespace, repoId } = param;
|
| 19 |
+
|
| 20 |
+
try {
|
| 21 |
+
const space = await spaceInfo({
|
| 22 |
+
name: `${namespace}/${repoId}`,
|
| 23 |
+
accessToken: user.token as string,
|
| 24 |
+
additionalFields: ["author"],
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
if (!space || space.sdk !== "static") {
|
| 28 |
+
return NextResponse.json(
|
| 29 |
+
{ ok: false, error: "Space is not a static space." },
|
| 30 |
+
{ status: 404 }
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
if (space.author !== user.name) {
|
| 35 |
+
return NextResponse.json(
|
| 36 |
+
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 37 |
+
{ status: 403 }
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const repo: RepoDesignation = {
|
| 42 |
+
type: "space",
|
| 43 |
+
name: `${namespace}/${repoId}`,
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
await deleteRepo({
|
| 47 |
+
repo,
|
| 48 |
+
accessToken: user.token as string,
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
return NextResponse.json({ ok: true }, { status: 200 });
|
| 53 |
+
} catch (error: any) {
|
| 54 |
+
return NextResponse.json(
|
| 55 |
+
{ ok: false, error: error.message },
|
| 56 |
+
{ status: 500 }
|
| 57 |
+
);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export async function GET(
|
| 62 |
+
req: NextRequest,
|
| 63 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 64 |
+
) {
|
| 65 |
+
const user = await isAuthenticated();
|
| 66 |
+
|
| 67 |
+
if (user instanceof NextResponse || !user) {
|
| 68 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
const param = await params;
|
| 72 |
+
const { namespace, repoId } = param;
|
| 73 |
+
|
| 74 |
+
try {
|
| 75 |
+
const space = await spaceInfo({
|
| 76 |
+
name: namespace + "/" + repoId,
|
| 77 |
+
accessToken: user.token as string,
|
| 78 |
+
additionalFields: ["author"],
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
if (!space || space.sdk !== "static") {
|
| 82 |
+
return NextResponse.json(
|
| 83 |
+
{
|
| 84 |
+
ok: false,
|
| 85 |
+
error: "Space is not a static space",
|
| 86 |
+
},
|
| 87 |
+
{ status: 404 }
|
| 88 |
+
);
|
| 89 |
+
}
|
| 90 |
+
if (space.author !== user.name) {
|
| 91 |
+
return NextResponse.json(
|
| 92 |
+
{
|
| 93 |
+
ok: false,
|
| 94 |
+
error: "Space does not belong to the authenticated user",
|
| 95 |
+
},
|
| 96 |
+
{ status: 403 }
|
| 97 |
+
);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
const repo: RepoDesignation = {
|
| 101 |
+
type: "space",
|
| 102 |
+
name: `${namespace}/${repoId}`,
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
const htmlFiles: Page[] = [];
|
| 106 |
+
const files: string[] = [];
|
| 107 |
+
|
| 108 |
+
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
|
| 109 |
+
|
| 110 |
+
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
| 111 |
+
if (fileInfo.path.endsWith(".html")) {
|
| 112 |
+
const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
|
| 113 |
+
const html = await blob?.text();
|
| 114 |
+
if (!html) {
|
| 115 |
+
continue;
|
| 116 |
+
}
|
| 117 |
+
if (fileInfo.path === "index.html") {
|
| 118 |
+
htmlFiles.unshift({
|
| 119 |
+
path: fileInfo.path,
|
| 120 |
+
html,
|
| 121 |
+
});
|
| 122 |
+
} else {
|
| 123 |
+
htmlFiles.push({
|
| 124 |
+
path: fileInfo.path,
|
| 125 |
+
html,
|
| 126 |
+
});
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
if (fileInfo.type === "directory" && fileInfo.path === "images") {
|
| 130 |
+
for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
| 131 |
+
if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
|
| 132 |
+
files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
const commits: Commit[] = [];
|
| 138 |
+
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
| 139 |
+
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
|
| 140 |
+
continue;
|
| 141 |
+
}
|
| 142 |
+
commits.push({
|
| 143 |
+
title: commit.title,
|
| 144 |
+
oid: commit.oid,
|
| 145 |
+
date: commit.date,
|
| 146 |
+
});
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
if (htmlFiles.length === 0) {
|
| 150 |
+
return NextResponse.json(
|
| 151 |
+
{
|
| 152 |
+
ok: false,
|
| 153 |
+
error: "No HTML files found",
|
| 154 |
+
},
|
| 155 |
+
{ status: 404 }
|
| 156 |
+
);
|
| 157 |
+
}
|
| 158 |
+
return NextResponse.json(
|
| 159 |
+
{
|
| 160 |
+
project: {
|
| 161 |
+
id: space.id,
|
| 162 |
+
space_id: space.name,
|
| 163 |
+
private: space.private,
|
| 164 |
+
_updatedAt: space.updatedAt,
|
| 165 |
+
},
|
| 166 |
+
pages: htmlFiles,
|
| 167 |
+
files,
|
| 168 |
+
commits,
|
| 169 |
+
ok: true,
|
| 170 |
+
},
|
| 171 |
+
{ status: 200 }
|
| 172 |
+
);
|
| 173 |
+
|
| 174 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 175 |
+
} catch (error: any) {
|
| 176 |
+
if (error.statusCode === 404) {
|
| 177 |
+
return NextResponse.json(
|
| 178 |
+
{ error: "Space not found", ok: false },
|
| 179 |
+
{ status: 404 }
|
| 180 |
+
);
|
| 181 |
+
}
|
| 182 |
+
return NextResponse.json(
|
| 183 |
+
{ error: error.message, ok: false },
|
| 184 |
+
{ status: 500 }
|
| 185 |
+
);
|
| 186 |
+
}
|
| 187 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/save/route.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { uploadFiles } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import { Page } from "@/types";
|
| 6 |
+
|
| 7 |
+
export async function PUT(
|
| 8 |
+
req: NextRequest,
|
| 9 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 10 |
+
) {
|
| 11 |
+
const user = await isAuthenticated();
|
| 12 |
+
if (user instanceof NextResponse || !user) {
|
| 13 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const param = await params;
|
| 17 |
+
const { namespace, repoId } = param;
|
| 18 |
+
const { pages, commitTitle = "Manual changes saved" } = await req.json();
|
| 19 |
+
|
| 20 |
+
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
| 21 |
+
return NextResponse.json(
|
| 22 |
+
{ ok: false, error: "Pages are required" },
|
| 23 |
+
{ status: 400 }
|
| 24 |
+
);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
try {
|
| 28 |
+
// Prepare files for upload
|
| 29 |
+
const files: File[] = [];
|
| 30 |
+
pages.forEach((page: Page) => {
|
| 31 |
+
const file = new File([page.html], page.path, { type: "text/html" });
|
| 32 |
+
files.push(file);
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// Upload files to HuggingFace Hub
|
| 36 |
+
const response = await uploadFiles({
|
| 37 |
+
repo: {
|
| 38 |
+
type: "space",
|
| 39 |
+
name: `${namespace}/${repoId}`,
|
| 40 |
+
},
|
| 41 |
+
files,
|
| 42 |
+
commitTitle,
|
| 43 |
+
accessToken: user.token as string,
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
return NextResponse.json({
|
| 47 |
+
ok: true,
|
| 48 |
+
pages,
|
| 49 |
+
commit: {
|
| 50 |
+
...response.commit,
|
| 51 |
+
title: commitTitle,
|
| 52 |
+
}
|
| 53 |
+
});
|
| 54 |
+
} catch (error: any) {
|
| 55 |
+
console.error("Error saving manual changes:", error);
|
| 56 |
+
return NextResponse.json(
|
| 57 |
+
{
|
| 58 |
+
ok: false,
|
| 59 |
+
error: error.message || "Failed to save changes",
|
| 60 |
+
},
|
| 61 |
+
{ status: 500 }
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
}
|
app/api/me/projects/route.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import { Commit, Page } from "@/types";
|
| 6 |
+
import { COLORS } from "@/lib/utils";
|
| 7 |
+
|
| 8 |
+
export async function POST(
|
| 9 |
+
req: NextRequest,
|
| 10 |
+
) {
|
| 11 |
+
const user = await isAuthenticated();
|
| 12 |
+
if (user instanceof NextResponse || !user) {
|
| 13 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const { title: titleFromRequest, pages, prompt } = await req.json();
|
| 17 |
+
|
| 18 |
+
const title = titleFromRequest ?? "DeepSite Project";
|
| 19 |
+
|
| 20 |
+
const formattedTitle = title
|
| 21 |
+
.toLowerCase()
|
| 22 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 23 |
+
.split("-")
|
| 24 |
+
.filter(Boolean)
|
| 25 |
+
.join("-")
|
| 26 |
+
.slice(0, 96);
|
| 27 |
+
|
| 28 |
+
const repo: RepoDesignation = {
|
| 29 |
+
type: "space",
|
| 30 |
+
name: `${user.name}/${formattedTitle}`,
|
| 31 |
+
};
|
| 32 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 33 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 34 |
+
const README = `---
|
| 35 |
+
title: ${title}
|
| 36 |
+
colorFrom: ${colorFrom}
|
| 37 |
+
colorTo: ${colorTo}
|
| 38 |
+
emoji: 🐳
|
| 39 |
+
sdk: static
|
| 40 |
+
pinned: false
|
| 41 |
+
tags:
|
| 42 |
+
- deepsite-v3
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
# Welcome to your new DeepSite project!
|
| 46 |
+
This project was created with [DeepSite](https://deepsite.hf.co).
|
| 47 |
+
`;
|
| 48 |
+
|
| 49 |
+
const files: File[] = [];
|
| 50 |
+
const readmeFile = new File([README], "README.md", { type: "text/markdown" });
|
| 51 |
+
files.push(readmeFile);
|
| 52 |
+
pages.forEach((page: Page) => {
|
| 53 |
+
const file = new File([page.html], page.path, { type: "text/html" });
|
| 54 |
+
files.push(file);
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
try {
|
| 58 |
+
const { repoUrl} = await createRepo({
|
| 59 |
+
repo,
|
| 60 |
+
accessToken: user.token as string,
|
| 61 |
+
});
|
| 62 |
+
const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
|
| 63 |
+
await uploadFiles({
|
| 64 |
+
repo,
|
| 65 |
+
files,
|
| 66 |
+
accessToken: user.token as string,
|
| 67 |
+
commitTitle
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
const path = repoUrl.split("/").slice(-2).join("/");
|
| 71 |
+
|
| 72 |
+
const commits: Commit[] = [];
|
| 73 |
+
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
| 74 |
+
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
|
| 75 |
+
continue;
|
| 76 |
+
}
|
| 77 |
+
commits.push({
|
| 78 |
+
title: commit.title,
|
| 79 |
+
oid: commit.oid,
|
| 80 |
+
date: commit.date,
|
| 81 |
+
});
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
const space = await spaceInfo({
|
| 85 |
+
name: repo.name,
|
| 86 |
+
accessToken: user.token as string,
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
let newProject = {
|
| 90 |
+
files,
|
| 91 |
+
pages,
|
| 92 |
+
commits,
|
| 93 |
+
project: {
|
| 94 |
+
id: space.id,
|
| 95 |
+
space_id: space.name,
|
| 96 |
+
_updatedAt: space.updatedAt,
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
|
| 101 |
+
} catch (err: any) {
|
| 102 |
+
return NextResponse.json(
|
| 103 |
+
{ error: err.message, ok: false },
|
| 104 |
+
{ status: 500 }
|
| 105 |
+
);
|
| 106 |
+
}
|
| 107 |
+
}
|
app/api/me/route.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { listSpaces } from "@huggingface/hub";
|
| 2 |
+
import { headers } from "next/headers";
|
| 3 |
+
import { NextResponse } from "next/server";
|
| 4 |
+
|
| 5 |
+
export async function GET() {
|
| 6 |
+
const authHeaders = await headers();
|
| 7 |
+
const token = authHeaders.get("Authorization");
|
| 8 |
+
if (!token) {
|
| 9 |
+
return NextResponse.json({ user: null, errCode: 401 }, { status: 401 });
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
|
| 13 |
+
headers: {
|
| 14 |
+
Authorization: `${token}`,
|
| 15 |
+
},
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
if (!userResponse.ok) {
|
| 19 |
+
return NextResponse.json(
|
| 20 |
+
{ user: null, errCode: userResponse.status },
|
| 21 |
+
{ status: userResponse.status }
|
| 22 |
+
);
|
| 23 |
+
}
|
| 24 |
+
const user = await userResponse.json();
|
| 25 |
+
const projects = [];
|
| 26 |
+
for await (const space of listSpaces({
|
| 27 |
+
accessToken: token.replace("Bearer ", "") as string,
|
| 28 |
+
additionalFields: ["author", "cardData"],
|
| 29 |
+
search: {
|
| 30 |
+
owner: user.name,
|
| 31 |
+
}
|
| 32 |
+
})) {
|
| 33 |
+
if (
|
| 34 |
+
space.sdk === "static" &&
|
| 35 |
+
Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
|
| 36 |
+
(
|
| 37 |
+
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
| 38 |
+
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
| 39 |
+
)
|
| 40 |
+
) {
|
| 41 |
+
projects.push(space);
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
|
| 46 |
+
}
|
app/api/projects/[repoId]/[commitId]/route.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
| 1 |
-
import { auth } from "@/lib/auth";
|
| 2 |
-
import { createBranch, RepoDesignation } from "@huggingface/hub";
|
| 3 |
-
import { format } from "date-fns";
|
| 4 |
-
import { NextResponse } from "next/server";
|
| 5 |
-
|
| 6 |
-
export async function POST(
|
| 7 |
-
request: Request,
|
| 8 |
-
{ params }: { params: Promise<{ repoId: string; commitId: string }> }
|
| 9 |
-
) {
|
| 10 |
-
const { repoId, commitId }: { repoId: string; commitId: string } =
|
| 11 |
-
await params;
|
| 12 |
-
const session = await auth();
|
| 13 |
-
if (!session) {
|
| 14 |
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 15 |
-
}
|
| 16 |
-
const token = session.accessToken;
|
| 17 |
-
|
| 18 |
-
const repo: RepoDesignation = {
|
| 19 |
-
type: "space",
|
| 20 |
-
name: session.user?.username + "/" + repoId,
|
| 21 |
-
};
|
| 22 |
-
|
| 23 |
-
const commitTitle = `🔖 ${format(new Date(), "dd/MM")} - ${format(
|
| 24 |
-
new Date(),
|
| 25 |
-
"HH:mm"
|
| 26 |
-
)} - Set commit ${commitId} as default.`;
|
| 27 |
-
|
| 28 |
-
await fetch(
|
| 29 |
-
`https://huggingface.co/api/spaces/${session.user?.username}/${repoId}/branch/main`,
|
| 30 |
-
{
|
| 31 |
-
method: "POST",
|
| 32 |
-
headers: {
|
| 33 |
-
Authorization: `Bearer ${token}`,
|
| 34 |
-
"Content-Type": "application/json",
|
| 35 |
-
},
|
| 36 |
-
body: JSON.stringify({
|
| 37 |
-
startingPoint: commitId,
|
| 38 |
-
overwrite: true,
|
| 39 |
-
}),
|
| 40 |
-
}
|
| 41 |
-
).catch((error) => {
|
| 42 |
-
return NextResponse.json(
|
| 43 |
-
{ error: error ?? "Failed to create branch" },
|
| 44 |
-
{ status: 500 }
|
| 45 |
-
);
|
| 46 |
-
});
|
| 47 |
-
|
| 48 |
-
return NextResponse.json({ success: true }, { status: 200 });
|
| 49 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/projects/[repoId]/medias/route.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
import { auth } from "@/lib/auth";
|
| 2 |
-
import { RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 3 |
-
import { NextResponse } from "next/server";
|
| 4 |
-
|
| 5 |
-
export async function POST(
|
| 6 |
-
request: Request,
|
| 7 |
-
{ params }: { params: Promise<{ repoId: string }> }
|
| 8 |
-
) {
|
| 9 |
-
const { repoId }: { repoId: string } = await params;
|
| 10 |
-
const session = await auth();
|
| 11 |
-
if (!session) {
|
| 12 |
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 13 |
-
}
|
| 14 |
-
const token = session.accessToken;
|
| 15 |
-
|
| 16 |
-
const repo: RepoDesignation = {
|
| 17 |
-
type: "space",
|
| 18 |
-
name: session.user?.username + "/" + repoId,
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
const formData = await request.formData();
|
| 22 |
-
const newMedias = formData.getAll("images") as File[];
|
| 23 |
-
|
| 24 |
-
const filesToUpload: File[] = [];
|
| 25 |
-
|
| 26 |
-
if (!newMedias || newMedias.length === 0) {
|
| 27 |
-
return NextResponse.json(
|
| 28 |
-
{
|
| 29 |
-
ok: false,
|
| 30 |
-
error: "At least one media file is required under the 'images' key",
|
| 31 |
-
},
|
| 32 |
-
{ status: 400 }
|
| 33 |
-
);
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
try {
|
| 37 |
-
for (const media of newMedias) {
|
| 38 |
-
const isImage = media.type.startsWith("image/");
|
| 39 |
-
const isVideo = media.type.startsWith("video/");
|
| 40 |
-
const isAudio = media.type.startsWith("audio/");
|
| 41 |
-
|
| 42 |
-
const folderPath = isImage
|
| 43 |
-
? "images/"
|
| 44 |
-
: isVideo
|
| 45 |
-
? "videos/"
|
| 46 |
-
: isAudio
|
| 47 |
-
? "audios/"
|
| 48 |
-
: null;
|
| 49 |
-
|
| 50 |
-
if (!folderPath) {
|
| 51 |
-
return NextResponse.json(
|
| 52 |
-
{ ok: false, error: "Unsupported media type: " + media.type },
|
| 53 |
-
{ status: 400 }
|
| 54 |
-
);
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
const mediaName = `${folderPath}${media.name}`;
|
| 58 |
-
const processedFile = new File([media], mediaName, { type: media.type });
|
| 59 |
-
filesToUpload.push(processedFile);
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
await uploadFiles({
|
| 63 |
-
repo,
|
| 64 |
-
files: filesToUpload,
|
| 65 |
-
accessToken: token,
|
| 66 |
-
commitTitle: `📁 Upload media files through DeepSite`,
|
| 67 |
-
});
|
| 68 |
-
|
| 69 |
-
return NextResponse.json(
|
| 70 |
-
{
|
| 71 |
-
success: true,
|
| 72 |
-
medias: filesToUpload.map(
|
| 73 |
-
(file) =>
|
| 74 |
-
`https://huggingface.co/spaces/${session.user?.username}/${repoId}/resolve/main/${file.name}`
|
| 75 |
-
),
|
| 76 |
-
},
|
| 77 |
-
{ status: 200 }
|
| 78 |
-
);
|
| 79 |
-
} catch (error) {
|
| 80 |
-
return NextResponse.json(
|
| 81 |
-
{ ok: false, error: error ?? "Failed to upload media files" },
|
| 82 |
-
{ status: 500 }
|
| 83 |
-
);
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
return NextResponse.json({ success: true }, { status: 200 });
|
| 87 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/projects/[repoId]/rename/route.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
| 1 |
-
import { auth } from "@/lib/auth";
|
| 2 |
-
import { downloadFile, RepoDesignation, uploadFile } from "@huggingface/hub";
|
| 3 |
-
import { format } from "date-fns";
|
| 4 |
-
import { NextResponse } from "next/server";
|
| 5 |
-
|
| 6 |
-
export async function PUT(
|
| 7 |
-
request: Request,
|
| 8 |
-
{ params }: { params: Promise<{ repoId: string }> }
|
| 9 |
-
) {
|
| 10 |
-
const { repoId }: { repoId: string } = await params;
|
| 11 |
-
const session = await auth();
|
| 12 |
-
if (!session) {
|
| 13 |
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 14 |
-
}
|
| 15 |
-
const token = session.accessToken;
|
| 16 |
-
|
| 17 |
-
const body = await request.json();
|
| 18 |
-
const { newTitle } = body;
|
| 19 |
-
|
| 20 |
-
if (!newTitle) {
|
| 21 |
-
return NextResponse.json(
|
| 22 |
-
{ error: "newTitle is required" },
|
| 23 |
-
{ status: 400 }
|
| 24 |
-
);
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
const repo: RepoDesignation = {
|
| 28 |
-
type: "space",
|
| 29 |
-
name: session.user?.username + "/" + repoId,
|
| 30 |
-
};
|
| 31 |
-
|
| 32 |
-
const blob = await downloadFile({
|
| 33 |
-
repo,
|
| 34 |
-
accessToken: token,
|
| 35 |
-
path: "README.md",
|
| 36 |
-
raw: true,
|
| 37 |
-
}).catch((_) => {
|
| 38 |
-
return null;
|
| 39 |
-
});
|
| 40 |
-
|
| 41 |
-
if (!blob) {
|
| 42 |
-
return NextResponse.json(
|
| 43 |
-
{ error: "Could not fetch README.md" },
|
| 44 |
-
{ status: 500 }
|
| 45 |
-
);
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
const readmeFile = await blob?.text();
|
| 49 |
-
if (!readmeFile) {
|
| 50 |
-
return NextResponse.json(
|
| 51 |
-
{ error: "Could not read README.md content" },
|
| 52 |
-
{ status: 500 }
|
| 53 |
-
);
|
| 54 |
-
}
|
| 55 |
-
const updatedReadmeFile = readmeFile.replace(
|
| 56 |
-
/^title:\s*(.*)$/m,
|
| 57 |
-
`title: ${newTitle}`
|
| 58 |
-
);
|
| 59 |
-
|
| 60 |
-
await uploadFile({
|
| 61 |
-
repo,
|
| 62 |
-
accessToken: token,
|
| 63 |
-
file: new File([updatedReadmeFile], "README.md", { type: "text/markdown" }),
|
| 64 |
-
commitTitle: `🐳 ${format(new Date(), "dd/MM")} - ${format(
|
| 65 |
-
new Date(),
|
| 66 |
-
"HH:mm"
|
| 67 |
-
)} - Rename project to "${newTitle}"`,
|
| 68 |
-
});
|
| 69 |
-
|
| 70 |
-
return NextResponse.json(
|
| 71 |
-
{
|
| 72 |
-
success: true,
|
| 73 |
-
},
|
| 74 |
-
{ status: 200 }
|
| 75 |
-
);
|
| 76 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/projects/[repoId]/route.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
| 1 |
-
import { auth } from "@/lib/auth";
|
| 2 |
-
import { RepoDesignation, deleteRepo, uploadFiles } from "@huggingface/hub";
|
| 3 |
-
import { format } from "date-fns";
|
| 4 |
-
import { NextResponse } from "next/server";
|
| 5 |
-
|
| 6 |
-
export async function PUT(
|
| 7 |
-
request: Request,
|
| 8 |
-
{ params }: { params: Promise<{ repoId: string }> }
|
| 9 |
-
) {
|
| 10 |
-
const { repoId }: { repoId: string } = await params;
|
| 11 |
-
const session = await auth();
|
| 12 |
-
if (!session) {
|
| 13 |
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 14 |
-
}
|
| 15 |
-
const token = session.accessToken;
|
| 16 |
-
|
| 17 |
-
const body = await request.json();
|
| 18 |
-
const { files, prompt, isManualChanges } = body;
|
| 19 |
-
|
| 20 |
-
if (!files) {
|
| 21 |
-
return NextResponse.json({ error: "Files are required" }, { status: 400 });
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
if (!prompt) {
|
| 25 |
-
return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
const repo: RepoDesignation = {
|
| 29 |
-
type: "space",
|
| 30 |
-
name: session.user?.username + "/" + repoId,
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
const filesToUpload: File[] = [];
|
| 34 |
-
for (const file of files) {
|
| 35 |
-
let mimeType = "text/x-python";
|
| 36 |
-
if (file.path.endsWith(".txt")) {
|
| 37 |
-
mimeType = "text/plain";
|
| 38 |
-
} else if (file.path.endsWith(".md")) {
|
| 39 |
-
mimeType = "text/markdown";
|
| 40 |
-
} else if (file.path.endsWith(".json")) {
|
| 41 |
-
mimeType = "application/json";
|
| 42 |
-
}
|
| 43 |
-
filesToUpload.push(new File([file.content], file.path, { type: mimeType }));
|
| 44 |
-
}
|
| 45 |
-
const baseTitle = isManualChanges
|
| 46 |
-
? ""
|
| 47 |
-
: `🐳 ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `;
|
| 48 |
-
const commitTitle = baseTitle + (prompt ?? "Follow-up DeepSite commit");
|
| 49 |
-
const response = await uploadFiles({
|
| 50 |
-
repo,
|
| 51 |
-
files: filesToUpload,
|
| 52 |
-
accessToken: token,
|
| 53 |
-
commitTitle,
|
| 54 |
-
});
|
| 55 |
-
|
| 56 |
-
return NextResponse.json(
|
| 57 |
-
{
|
| 58 |
-
success: true,
|
| 59 |
-
commit: {
|
| 60 |
-
oid: response.commit,
|
| 61 |
-
title: commitTitle,
|
| 62 |
-
date: new Date(),
|
| 63 |
-
},
|
| 64 |
-
},
|
| 65 |
-
{ status: 200 }
|
| 66 |
-
);
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
export async function DELETE(
|
| 70 |
-
request: Request,
|
| 71 |
-
{ params }: { params: Promise<{ repoId: string }> }
|
| 72 |
-
) {
|
| 73 |
-
const { repoId }: { repoId: string } = await params;
|
| 74 |
-
const session = await auth();
|
| 75 |
-
if (!session) {
|
| 76 |
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 77 |
-
}
|
| 78 |
-
const token = session.accessToken;
|
| 79 |
-
|
| 80 |
-
const repo: RepoDesignation = {
|
| 81 |
-
type: "space",
|
| 82 |
-
name: session.user?.username + "/" + repoId,
|
| 83 |
-
};
|
| 84 |
-
|
| 85 |
-
try {
|
| 86 |
-
await deleteRepo({
|
| 87 |
-
repo,
|
| 88 |
-
accessToken: token as string,
|
| 89 |
-
});
|
| 90 |
-
|
| 91 |
-
return NextResponse.json({ success: true }, { status: 200 });
|
| 92 |
-
} catch (error) {
|
| 93 |
-
const errMsg =
|
| 94 |
-
error instanceof Error ? error.message : "Failed to delete project";
|
| 95 |
-
return NextResponse.json({ error: errMsg }, { status: 500 });
|
| 96 |
-
}
|
| 97 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/projects/route.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
| 1 |
-
import { NextResponse } from "next/server";
|
| 2 |
-
import { RepoDesignation, createRepo, uploadFiles } from "@huggingface/hub";
|
| 3 |
-
|
| 4 |
-
import { auth } from "@/lib/auth";
|
| 5 |
-
import {
|
| 6 |
-
COLORS,
|
| 7 |
-
EMOJIS_FOR_SPACE,
|
| 8 |
-
injectDeepSiteBadge,
|
| 9 |
-
isIndexPage,
|
| 10 |
-
} from "@/lib/utils";
|
| 11 |
-
|
| 12 |
-
export async function POST(request: Request) {
|
| 13 |
-
const session = await auth();
|
| 14 |
-
if (!session) {
|
| 15 |
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 16 |
-
}
|
| 17 |
-
const token = session.accessToken;
|
| 18 |
-
|
| 19 |
-
const body = await request.json();
|
| 20 |
-
const { projectTitle, files, prompt } = body;
|
| 21 |
-
|
| 22 |
-
if (!files) {
|
| 23 |
-
return NextResponse.json(
|
| 24 |
-
{ error: "Project title and files are required" },
|
| 25 |
-
{ status: 400 }
|
| 26 |
-
);
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
const title =
|
| 30 |
-
projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project";
|
| 31 |
-
|
| 32 |
-
let formattedTitle = title
|
| 33 |
-
.toLowerCase()
|
| 34 |
-
.replace(/[^a-z0-9]+/g, "-")
|
| 35 |
-
.split("-")
|
| 36 |
-
.filter(Boolean)
|
| 37 |
-
.join("-")
|
| 38 |
-
.slice(0, 75);
|
| 39 |
-
|
| 40 |
-
formattedTitle =
|
| 41 |
-
formattedTitle + "-" + Math.random().toString(36).substring(2, 7);
|
| 42 |
-
|
| 43 |
-
const repo: RepoDesignation = {
|
| 44 |
-
type: "space",
|
| 45 |
-
name: session.user?.username + "/" + formattedTitle,
|
| 46 |
-
};
|
| 47 |
-
|
| 48 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 49 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 50 |
-
const emoji =
|
| 51 |
-
EMOJIS_FOR_SPACE[Math.floor(Math.random() * EMOJIS_FOR_SPACE.length)];
|
| 52 |
-
const README = `---
|
| 53 |
-
title: ${projectTitle}
|
| 54 |
-
colorFrom: ${colorFrom}
|
| 55 |
-
colorTo: ${colorTo}
|
| 56 |
-
sdk: static
|
| 57 |
-
emoji: ${emoji}
|
| 58 |
-
tags:
|
| 59 |
-
- deepsite-v4
|
| 60 |
-
---
|
| 61 |
-
|
| 62 |
-
# ${title}
|
| 63 |
-
|
| 64 |
-
This project has been created with [DeepSite](https://huggingface.co/deepsite) AI Vibe Coding.
|
| 65 |
-
`;
|
| 66 |
-
|
| 67 |
-
const filesToUpload: File[] = [
|
| 68 |
-
new File([README], "README.md", { type: "text/markdown" }),
|
| 69 |
-
];
|
| 70 |
-
for (const file of files) {
|
| 71 |
-
let mimeType = "text/html";
|
| 72 |
-
if (file.path.endsWith(".css")) {
|
| 73 |
-
mimeType = "text/css";
|
| 74 |
-
} else if (file.path.endsWith(".js")) {
|
| 75 |
-
mimeType = "text/javascript";
|
| 76 |
-
}
|
| 77 |
-
const content =
|
| 78 |
-
mimeType === "text/html" && isIndexPage(file.path)
|
| 79 |
-
? injectDeepSiteBadge(file.content)
|
| 80 |
-
: file.content;
|
| 81 |
-
|
| 82 |
-
filesToUpload.push(new File([content], file.path, { type: mimeType }));
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
try {
|
| 86 |
-
const { repoUrl } = await createRepo({
|
| 87 |
-
accessToken: token as string,
|
| 88 |
-
repo: repo,
|
| 89 |
-
sdk: "static",
|
| 90 |
-
});
|
| 91 |
-
|
| 92 |
-
const commitTitle = prompt ?? "Initial DeepSite commit";
|
| 93 |
-
await uploadFiles({
|
| 94 |
-
repo,
|
| 95 |
-
files: filesToUpload,
|
| 96 |
-
accessToken: token as string,
|
| 97 |
-
commitTitle,
|
| 98 |
-
});
|
| 99 |
-
|
| 100 |
-
const path = repoUrl.split("/").slice(-2).join("/");
|
| 101 |
-
|
| 102 |
-
return NextResponse.json({ repoUrl: path }, { status: 200 });
|
| 103 |
-
} catch (error) {
|
| 104 |
-
const errMsg =
|
| 105 |
-
error instanceof Error ? error.message : "Failed to upload files";
|
| 106 |
-
return NextResponse.json({ error: errMsg }, { status: 500 });
|
| 107 |
-
}
|
| 108 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/re-design/route.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
export async function PUT(request: NextRequest) {
|
| 4 |
+
const body = await request.json();
|
| 5 |
+
const { url } = body;
|
| 6 |
+
|
| 7 |
+
if (!url) {
|
| 8 |
+
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
try {
|
| 12 |
+
const response = await fetch(
|
| 13 |
+
`https://r.jina.ai/${encodeURIComponent(url)}`,
|
| 14 |
+
{
|
| 15 |
+
method: "POST",
|
| 16 |
+
}
|
| 17 |
+
);
|
| 18 |
+
if (!response.ok) {
|
| 19 |
+
return NextResponse.json(
|
| 20 |
+
{ error: "Failed to fetch redesign" },
|
| 21 |
+
{ status: 500 }
|
| 22 |
+
);
|
| 23 |
+
}
|
| 24 |
+
const markdown = await response.text();
|
| 25 |
+
return NextResponse.json(
|
| 26 |
+
{
|
| 27 |
+
ok: true,
|
| 28 |
+
markdown,
|
| 29 |
+
},
|
| 30 |
+
{ status: 200 }
|
| 31 |
+
);
|
| 32 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 33 |
+
} catch (error: any) {
|
| 34 |
+
return NextResponse.json(
|
| 35 |
+
{ error: error.message || "An error occurred" },
|
| 36 |
+
{ status: 500 }
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
}
|
app/api/redesign/route.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
| 1 |
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 3 |
-
|
| 4 |
-
const FETCH_TIMEOUT = 30_000;
|
| 5 |
-
export const maxDuration = 60;
|
| 6 |
-
|
| 7 |
-
export async function PUT(request: NextRequest) {
|
| 8 |
-
const body = await request.json();
|
| 9 |
-
const { url } = body;
|
| 10 |
-
|
| 11 |
-
if (!url) {
|
| 12 |
-
return NextResponse.json({ error: "URL is required" }, { status: 400 });
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
try {
|
| 16 |
-
const controller = new AbortController();
|
| 17 |
-
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
| 18 |
-
|
| 19 |
-
try {
|
| 20 |
-
const response = await fetch(
|
| 21 |
-
`https://r.jina.ai/${encodeURIComponent(url)}`,
|
| 22 |
-
{
|
| 23 |
-
method: "POST",
|
| 24 |
-
signal: controller.signal,
|
| 25 |
-
}
|
| 26 |
-
);
|
| 27 |
-
|
| 28 |
-
clearTimeout(timeoutId);
|
| 29 |
-
|
| 30 |
-
if (!response.ok) {
|
| 31 |
-
return NextResponse.json(
|
| 32 |
-
{ error: "Failed to fetch redesign" },
|
| 33 |
-
{ status: 500 }
|
| 34 |
-
);
|
| 35 |
-
}
|
| 36 |
-
const markdown = await response.text();
|
| 37 |
-
return NextResponse.json(
|
| 38 |
-
{
|
| 39 |
-
ok: true,
|
| 40 |
-
markdown,
|
| 41 |
-
},
|
| 42 |
-
{ status: 200 }
|
| 43 |
-
);
|
| 44 |
-
} catch (fetchError: any) {
|
| 45 |
-
clearTimeout(timeoutId);
|
| 46 |
-
|
| 47 |
-
if (fetchError.name === "AbortError") {
|
| 48 |
-
return NextResponse.json(
|
| 49 |
-
{
|
| 50 |
-
error:
|
| 51 |
-
"Request timeout: The external service took too long to respond. Please try again.",
|
| 52 |
-
},
|
| 53 |
-
{ status: 504 }
|
| 54 |
-
);
|
| 55 |
-
}
|
| 56 |
-
throw fetchError;
|
| 57 |
-
}
|
| 58 |
-
} catch (error: any) {
|
| 59 |
-
if (error.name === "AbortError" || error.message?.includes("timeout")) {
|
| 60 |
-
return NextResponse.json(
|
| 61 |
-
{
|
| 62 |
-
error:
|
| 63 |
-
"Request timeout: The external service took too long to respond. Please try again.",
|
| 64 |
-
},
|
| 65 |
-
{ status: 504 }
|
| 66 |
-
);
|
| 67 |
-
}
|
| 68 |
-
return NextResponse.json(
|
| 69 |
-
{ error: error.message || "An error occurred" },
|
| 70 |
-
{ status: 500 }
|
| 71 |
-
);
|
| 72 |
-
}
|
| 73 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/auth/callback/page.tsx
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
import { useUser } from "@/hooks/useUser";
|
| 4 |
+
import { use, useState } from "react";
|
| 5 |
+
import { useMount, useTimeoutFn } from "react-use";
|
| 6 |
+
|
| 7 |
+
import { Button } from "@/components/ui/button";
|
| 8 |
+
import { AnimatedBlobs } from "@/components/animated-blobs";
|
| 9 |
+
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
| 10 |
+
export default function AuthCallback({
|
| 11 |
+
searchParams,
|
| 12 |
+
}: {
|
| 13 |
+
searchParams: Promise<{ code: string }>;
|
| 14 |
+
}) {
|
| 15 |
+
const [showButton, setShowButton] = useState(false);
|
| 16 |
+
const [isPopupAuth, setIsPopupAuth] = useState(false);
|
| 17 |
+
const { code } = use(searchParams);
|
| 18 |
+
const { loginFromCode } = useUser();
|
| 19 |
+
const { postMessage } = useBroadcastChannel("auth", () => {});
|
| 20 |
+
|
| 21 |
+
useMount(async () => {
|
| 22 |
+
if (code) {
|
| 23 |
+
const isPopup = window.opener || window.parent !== window;
|
| 24 |
+
setIsPopupAuth(isPopup);
|
| 25 |
+
|
| 26 |
+
if (isPopup) {
|
| 27 |
+
postMessage({
|
| 28 |
+
type: "user-oauth",
|
| 29 |
+
code: code,
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
setTimeout(() => {
|
| 33 |
+
if (window.opener) {
|
| 34 |
+
window.close();
|
| 35 |
+
}
|
| 36 |
+
}, 1000);
|
| 37 |
+
} else {
|
| 38 |
+
await loginFromCode(code);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
useTimeoutFn(() => setShowButton(true), 7000);
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<div className="h-screen flex flex-col justify-center items-center bg-neutral-950 z-1 relative">
|
| 47 |
+
<div className="background__noisy" />
|
| 48 |
+
<div className="relative max-w-4xl py-10 flex items-center justify-center w-full">
|
| 49 |
+
<div className="max-w-lg mx-auto !rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
|
| 50 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
| 51 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
| 52 |
+
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 53 |
+
🚀
|
| 54 |
+
</div>
|
| 55 |
+
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
| 56 |
+
👋
|
| 57 |
+
</div>
|
| 58 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 59 |
+
🙌
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
<p className="text-xl font-semibold text-neutral-950">
|
| 63 |
+
{isPopupAuth
|
| 64 |
+
? "Authentication Complete!"
|
| 65 |
+
: "Login In Progress..."}
|
| 66 |
+
</p>
|
| 67 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
| 68 |
+
{isPopupAuth
|
| 69 |
+
? "You can now close this tab and return to the previous page."
|
| 70 |
+
: "Wait a moment while we log you in with your code."}
|
| 71 |
+
</p>
|
| 72 |
+
</header>
|
| 73 |
+
<main className="space-y-4 p-6">
|
| 74 |
+
<div>
|
| 75 |
+
<p className="text-sm text-neutral-700 mb-4 max-w-xs">
|
| 76 |
+
If you are not redirected automatically in the next 5 seconds,
|
| 77 |
+
please click the button below
|
| 78 |
+
</p>
|
| 79 |
+
{showButton ? (
|
| 80 |
+
<Link href="/">
|
| 81 |
+
<Button variant="black" className="relative">
|
| 82 |
+
Go to Home
|
| 83 |
+
</Button>
|
| 84 |
+
</Link>
|
| 85 |
+
) : (
|
| 86 |
+
<p className="text-xs text-neutral-500">
|
| 87 |
+
Please wait, we are logging you in...
|
| 88 |
+
</p>
|
| 89 |
+
)}
|
| 90 |
+
</div>
|
| 91 |
+
</main>
|
| 92 |
+
</div>
|
| 93 |
+
<AnimatedBlobs />
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
);
|
| 97 |
+
}
|
app/auth/page.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { redirect } from "next/navigation";
|
| 2 |
+
import { Metadata } from "next";
|
| 3 |
+
|
| 4 |
+
import { getAuth } from "@/app/actions/auth";
|
| 5 |
+
|
| 6 |
+
export const revalidate = 1;
|
| 7 |
+
|
| 8 |
+
export const metadata: Metadata = {
|
| 9 |
+
robots: "noindex, nofollow",
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
export default async function Auth() {
|
| 13 |
+
const loginRedirectUrl = await getAuth();
|
| 14 |
+
if (loginRedirectUrl) {
|
| 15 |
+
redirect(loginRedirectUrl);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
return (
|
| 19 |
+
<div className="p-4">
|
| 20 |
+
<div className="border bg-red-500/10 border-red-500/20 text-red-500 px-5 py-3 rounded-lg">
|
| 21 |
+
<h1 className="text-xl font-bold">Error</h1>
|
| 22 |
+
<p className="text-sm">
|
| 23 |
+
An error occurred while trying to log in. Please try again later.
|
| 24 |
+
</p>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
);
|
| 28 |
+
}
|
app/layout.tsx
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
|
|
| 1 |
import type { Metadata, Viewport } from "next";
|
| 2 |
-
import {
|
| 3 |
-
import {
|
| 4 |
import Script from "next/script";
|
| 5 |
|
| 6 |
-
import "@/
|
| 7 |
-
import { ThemeProvider } from "@/components/providers/theme";
|
| 8 |
-
import { AuthProvider } from "@/components/providers/session";
|
| 9 |
import { Toaster } from "@/components/ui/sonner";
|
| 10 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
import { generateSEO, generateStructuredData } from "@/lib/seo";
|
| 12 |
-
import { NotAuthorizedDomain } from "@/components/not-authorized";
|
| 13 |
|
| 14 |
-
const
|
| 15 |
-
variable: "--font-
|
| 16 |
subsets: ["latin"],
|
| 17 |
});
|
| 18 |
|
| 19 |
-
const
|
| 20 |
-
variable: "--font-
|
| 21 |
subsets: ["latin"],
|
|
|
|
| 22 |
});
|
| 23 |
|
| 24 |
export const metadata: Metadata = {
|
|
@@ -34,9 +39,9 @@ export const metadata: Metadata = {
|
|
| 34 |
statusBarStyle: "black-translucent",
|
| 35 |
},
|
| 36 |
icons: {
|
| 37 |
-
icon: "/
|
| 38 |
-
shortcut: "/
|
| 39 |
-
apple: "/
|
| 40 |
},
|
| 41 |
verification: {
|
| 42 |
google: process.env.GOOGLE_SITE_VERIFICATION,
|
|
@@ -46,29 +51,47 @@ export const metadata: Metadata = {
|
|
| 46 |
export const viewport: Viewport = {
|
| 47 |
initialScale: 1,
|
| 48 |
maximumScale: 1,
|
| 49 |
-
themeColor: "#
|
| 50 |
};
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
export default async function RootLayout({
|
| 53 |
children,
|
| 54 |
}: Readonly<{
|
| 55 |
children: React.ReactNode;
|
| 56 |
}>) {
|
|
|
|
|
|
|
|
|
|
| 57 |
const structuredData = generateStructuredData("WebApplication", {
|
| 58 |
name: "DeepSite",
|
| 59 |
description: "Build websites with AI, no code required",
|
| 60 |
-
url: "https://
|
| 61 |
});
|
|
|
|
| 62 |
const organizationData = generateStructuredData("Organization", {
|
| 63 |
name: "DeepSite",
|
| 64 |
-
url: "https://
|
| 65 |
});
|
| 66 |
|
| 67 |
return (
|
| 68 |
-
<html lang="en"
|
| 69 |
-
<
|
| 70 |
-
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
| 71 |
-
>
|
| 72 |
<script
|
| 73 |
type="application/ld+json"
|
| 74 |
dangerouslySetInnerHTML={{
|
|
@@ -81,27 +104,24 @@ export default async function RootLayout({
|
|
| 81 |
__html: JSON.stringify(organizationData),
|
| 82 |
}}
|
| 83 |
/>
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
>
|
| 98 |
-
<
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
</ThemeProvider>
|
| 103 |
-
</ReactQueryProvider>
|
| 104 |
-
</AuthProvider>
|
| 105 |
</body>
|
| 106 |
</html>
|
| 107 |
);
|
|
|
|
| 1 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
import type { Metadata, Viewport } from "next";
|
| 3 |
+
import { Inter, PT_Sans } from "next/font/google";
|
| 4 |
+
import { cookies } from "next/headers";
|
| 5 |
import Script from "next/script";
|
| 6 |
|
| 7 |
+
import "@/assets/globals.css";
|
|
|
|
|
|
|
| 8 |
import { Toaster } from "@/components/ui/sonner";
|
| 9 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 10 |
+
import { apiServer } from "@/lib/api";
|
| 11 |
+
import IframeDetector from "@/components/iframe-detector";
|
| 12 |
+
import AppContext from "@/components/contexts/app-context";
|
| 13 |
+
import TanstackContext from "@/components/contexts/tanstack-query-context";
|
| 14 |
+
import { LoginProvider } from "@/components/contexts/login-context";
|
| 15 |
+
import { ProProvider } from "@/components/contexts/pro-context";
|
| 16 |
import { generateSEO, generateStructuredData } from "@/lib/seo";
|
|
|
|
| 17 |
|
| 18 |
+
const inter = Inter({
|
| 19 |
+
variable: "--font-inter-sans",
|
| 20 |
subsets: ["latin"],
|
| 21 |
});
|
| 22 |
|
| 23 |
+
const ptSans = PT_Sans({
|
| 24 |
+
variable: "--font-ptSans-mono",
|
| 25 |
subsets: ["latin"],
|
| 26 |
+
weight: ["400", "700"],
|
| 27 |
});
|
| 28 |
|
| 29 |
export const metadata: Metadata = {
|
|
|
|
| 39 |
statusBarStyle: "black-translucent",
|
| 40 |
},
|
| 41 |
icons: {
|
| 42 |
+
icon: "/logo.svg",
|
| 43 |
+
shortcut: "/logo.svg",
|
| 44 |
+
apple: "/logo.svg",
|
| 45 |
},
|
| 46 |
verification: {
|
| 47 |
google: process.env.GOOGLE_SITE_VERIFICATION,
|
|
|
|
| 51 |
export const viewport: Viewport = {
|
| 52 |
initialScale: 1,
|
| 53 |
maximumScale: 1,
|
| 54 |
+
themeColor: "#000000",
|
| 55 |
};
|
| 56 |
|
| 57 |
+
async function getMe() {
|
| 58 |
+
const cookieStore = await cookies();
|
| 59 |
+
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
| 60 |
+
if (!token) return { user: null, projects: [], errCode: null };
|
| 61 |
+
try {
|
| 62 |
+
const res = await apiServer.get("/me", {
|
| 63 |
+
headers: {
|
| 64 |
+
Authorization: `Bearer ${token}`,
|
| 65 |
+
},
|
| 66 |
+
});
|
| 67 |
+
return { user: res.data.user, projects: res.data.projects, errCode: null };
|
| 68 |
+
} catch (err: any) {
|
| 69 |
+
return { user: null, projects: [], errCode: err.status };
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
export default async function RootLayout({
|
| 74 |
children,
|
| 75 |
}: Readonly<{
|
| 76 |
children: React.ReactNode;
|
| 77 |
}>) {
|
| 78 |
+
const data = await getMe();
|
| 79 |
+
|
| 80 |
+
// Generate structured data
|
| 81 |
const structuredData = generateStructuredData("WebApplication", {
|
| 82 |
name: "DeepSite",
|
| 83 |
description: "Build websites with AI, no code required",
|
| 84 |
+
url: "https://deepsite.hf.co",
|
| 85 |
});
|
| 86 |
+
|
| 87 |
const organizationData = generateStructuredData("Organization", {
|
| 88 |
name: "DeepSite",
|
| 89 |
+
url: "https://deepsite.hf.co",
|
| 90 |
});
|
| 91 |
|
| 92 |
return (
|
| 93 |
+
<html lang="en">
|
| 94 |
+
<head>
|
|
|
|
|
|
|
| 95 |
<script
|
| 96 |
type="application/ld+json"
|
| 97 |
dangerouslySetInnerHTML={{
|
|
|
|
| 104 |
__html: JSON.stringify(organizationData),
|
| 105 |
}}
|
| 106 |
/>
|
| 107 |
+
</head>
|
| 108 |
+
<Script
|
| 109 |
+
defer
|
| 110 |
+
data-domain="deepsite.hf.co"
|
| 111 |
+
src="https://plausible.io/js/script.js"
|
| 112 |
+
></Script>
|
| 113 |
+
<body
|
| 114 |
+
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
| 115 |
+
>
|
| 116 |
+
<IframeDetector />
|
| 117 |
+
<Toaster richColors position="bottom-center" />
|
| 118 |
+
<TanstackContext>
|
| 119 |
+
<AppContext me={data}>
|
| 120 |
+
<LoginProvider>
|
| 121 |
+
<ProProvider>{children}</ProProvider>
|
| 122 |
+
</LoginProvider>
|
| 123 |
+
</AppContext>
|
| 124 |
+
</TanstackContext>
|
|
|
|
|
|
|
|
|
|
| 125 |
</body>
|
| 126 |
</html>
|
| 127 |
);
|
app/new/page.tsx
CHANGED
|
@@ -1,18 +1,14 @@
|
|
| 1 |
import { AppEditor } from "@/components/editor";
|
| 2 |
-
import {
|
| 3 |
-
import {
|
| 4 |
|
| 5 |
-
export
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
const { prompt } = await searchParams;
|
| 17 |
-
return <AppEditor isNew={true} initialPrompt={prompt} />;
|
| 18 |
}
|
|
|
|
| 1 |
import { AppEditor } from "@/components/editor";
|
| 2 |
+
import { Metadata } from "next";
|
| 3 |
+
import { generateSEO } from "@/lib/seo";
|
| 4 |
|
| 5 |
+
export const metadata: Metadata = generateSEO({
|
| 6 |
+
title: "Create New Project - DeepSite",
|
| 7 |
+
description:
|
| 8 |
+
"Start building your next website with AI. Create a new project on DeepSite and experience the power of AI-driven web development.",
|
| 9 |
+
path: "/new",
|
| 10 |
+
});
|
| 11 |
|
| 12 |
+
export default function NewProjectPage() {
|
| 13 |
+
return <AppEditor isNew />;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
}
|
app/not-found.tsx
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
import { NotFoundButtons } from "@/components/not-found/buttons";
|
| 2 |
-
import { Navigation } from "@/components/public/navigation";
|
| 3 |
-
|
| 4 |
-
export default function NotFound() {
|
| 5 |
-
return (
|
| 6 |
-
<div className="min-h-screen font-sans">
|
| 7 |
-
<Navigation />
|
| 8 |
-
<div className="px-6 py-16 max-w-5xl mx-auto text-center">
|
| 9 |
-
<h1 className="text-5xl font-bold mb-5">Oh no! Page not found.</h1>
|
| 10 |
-
<p className="text-lg text-muted-foreground mb-8">
|
| 11 |
-
The page you are looking for does not exist.
|
| 12 |
-
</p>
|
| 13 |
-
<NotFoundButtons />
|
| 14 |
-
</div>
|
| 15 |
-
</div>
|
| 16 |
-
);
|
| 17 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/sitemap.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { MetadataRoute } from 'next';
|
| 2 |
+
|
| 3 |
+
export default function sitemap(): MetadataRoute.Sitemap {
|
| 4 |
+
const baseUrl = 'https://deepsite.hf.co';
|
| 5 |
+
|
| 6 |
+
return [
|
| 7 |
+
{
|
| 8 |
+
url: baseUrl,
|
| 9 |
+
lastModified: new Date(),
|
| 10 |
+
changeFrequency: 'daily',
|
| 11 |
+
priority: 1,
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
url: `${baseUrl}/new`,
|
| 15 |
+
lastModified: new Date(),
|
| 16 |
+
changeFrequency: 'weekly',
|
| 17 |
+
priority: 0.8,
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
url: `${baseUrl}/auth`,
|
| 21 |
+
lastModified: new Date(),
|
| 22 |
+
changeFrequency: 'monthly',
|
| 23 |
+
priority: 0.5,
|
| 24 |
+
},
|
| 25 |
+
// Note: Dynamic project routes will be handled by Next.js automatically
|
| 26 |
+
// but you can add specific high-priority project pages here if needed
|
| 27 |
+
];
|
| 28 |
+
}
|
{app → assets}/globals.css
RENAMED
|
@@ -6,8 +6,8 @@
|
|
| 6 |
@theme inline {
|
| 7 |
--color-background: var(--background);
|
| 8 |
--color-foreground: var(--foreground);
|
| 9 |
-
--font-sans: var(--font-
|
| 10 |
-
--font-mono: var(--font-
|
| 11 |
--color-sidebar-ring: var(--sidebar-ring);
|
| 12 |
--color-sidebar-border: var(--sidebar-border);
|
| 13 |
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
@@ -44,7 +44,7 @@
|
|
| 44 |
}
|
| 45 |
|
| 46 |
:root {
|
| 47 |
-
--radius: 0.
|
| 48 |
--background: oklch(1 0 0);
|
| 49 |
--foreground: oklch(0.145 0 0);
|
| 50 |
--card: oklch(1 0 0);
|
|
@@ -68,7 +68,6 @@
|
|
| 68 |
--chart-3: oklch(0.398 0.07 227.392);
|
| 69 |
--chart-4: oklch(0.828 0.189 84.429);
|
| 70 |
--chart-5: oklch(0.769 0.188 70.08);
|
| 71 |
-
--radius: 0.625rem;
|
| 72 |
--sidebar: oklch(0.985 0 0);
|
| 73 |
--sidebar-foreground: oklch(0.145 0 0);
|
| 74 |
--sidebar-primary: oklch(0.205 0 0);
|
|
@@ -113,6 +112,10 @@
|
|
| 113 |
--sidebar-ring: oklch(0.556 0 0);
|
| 114 |
}
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
@layer base {
|
| 117 |
* {
|
| 118 |
@apply border-border outline-ring/50;
|
|
@@ -120,49 +123,249 @@
|
|
| 120 |
body {
|
| 121 |
@apply bg-background text-foreground;
|
| 122 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
.monaco-editor .margin {
|
| 126 |
-
@apply bg-
|
| 127 |
}
|
| 128 |
.monaco-editor .monaco-editor-background {
|
| 129 |
-
@apply bg-
|
| 130 |
}
|
| 131 |
-
.monaco-editor .
|
| 132 |
-
@apply
|
| 133 |
}
|
| 134 |
-
|
| 135 |
-
|
|
|
|
| 136 |
}
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
}
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
|
|
|
| 6 |
@theme inline {
|
| 7 |
--color-background: var(--background);
|
| 8 |
--color-foreground: var(--foreground);
|
| 9 |
+
--font-sans: var(--font-inter-sans);
|
| 10 |
+
--font-mono: var(--font-ptSans-mono);
|
| 11 |
--color-sidebar-ring: var(--sidebar-ring);
|
| 12 |
--color-sidebar-border: var(--sidebar-border);
|
| 13 |
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
:root {
|
| 47 |
+
--radius: 0.625rem;
|
| 48 |
--background: oklch(1 0 0);
|
| 49 |
--foreground: oklch(0.145 0 0);
|
| 50 |
--card: oklch(1 0 0);
|
|
|
|
| 68 |
--chart-3: oklch(0.398 0.07 227.392);
|
| 69 |
--chart-4: oklch(0.828 0.189 84.429);
|
| 70 |
--chart-5: oklch(0.769 0.188 70.08);
|
|
|
|
| 71 |
--sidebar: oklch(0.985 0 0);
|
| 72 |
--sidebar-foreground: oklch(0.145 0 0);
|
| 73 |
--sidebar-primary: oklch(0.205 0 0);
|
|
|
|
| 112 |
--sidebar-ring: oklch(0.556 0 0);
|
| 113 |
}
|
| 114 |
|
| 115 |
+
body {
|
| 116 |
+
@apply scroll-smooth
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
@layer base {
|
| 120 |
* {
|
| 121 |
@apply border-border outline-ring/50;
|
|
|
|
| 123 |
body {
|
| 124 |
@apply bg-background text-foreground;
|
| 125 |
}
|
| 126 |
+
html {
|
| 127 |
+
@apply scroll-smooth;
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.background__noisy {
|
| 132 |
+
@apply bg-blend-normal pointer-events-none opacity-90;
|
| 133 |
+
background-size: 25ww auto;
|
| 134 |
+
background-image: url("/background_noisy.webp");
|
| 135 |
+
@apply fixed w-screen h-screen -z-1 top-0 left-0;
|
| 136 |
}
|
| 137 |
|
| 138 |
.monaco-editor .margin {
|
| 139 |
+
@apply !bg-neutral-900;
|
| 140 |
}
|
| 141 |
.monaco-editor .monaco-editor-background {
|
| 142 |
+
@apply !bg-neutral-900;
|
| 143 |
}
|
| 144 |
+
.monaco-editor .line-numbers {
|
| 145 |
+
@apply !text-neutral-500;
|
| 146 |
}
|
| 147 |
+
|
| 148 |
+
.matched-line {
|
| 149 |
+
@apply bg-sky-500/30;
|
| 150 |
}
|
| 151 |
+
|
| 152 |
+
/* Fast liquid deformation animations */
|
| 153 |
+
@keyframes liquidBlob1 {
|
| 154 |
+
0%, 100% {
|
| 155 |
+
border-radius: 40% 60% 50% 50%;
|
| 156 |
+
transform: scaleX(1) scaleY(1) rotate(0deg);
|
| 157 |
+
}
|
| 158 |
+
12.5% {
|
| 159 |
+
border-radius: 20% 80% 70% 30%;
|
| 160 |
+
transform: scaleX(1.6) scaleY(0.4) rotate(25deg);
|
| 161 |
+
}
|
| 162 |
+
25% {
|
| 163 |
+
border-radius: 80% 20% 30% 70%;
|
| 164 |
+
transform: scaleX(0.5) scaleY(2.1) rotate(-15deg);
|
| 165 |
+
}
|
| 166 |
+
37.5% {
|
| 167 |
+
border-radius: 30% 70% 80% 20%;
|
| 168 |
+
transform: scaleX(1.8) scaleY(0.6) rotate(40deg);
|
| 169 |
+
}
|
| 170 |
+
50% {
|
| 171 |
+
border-radius: 70% 30% 20% 80%;
|
| 172 |
+
transform: scaleX(0.4) scaleY(1.9) rotate(-30deg);
|
| 173 |
+
}
|
| 174 |
+
62.5% {
|
| 175 |
+
border-radius: 25% 75% 60% 40%;
|
| 176 |
+
transform: scaleX(1.5) scaleY(0.7) rotate(55deg);
|
| 177 |
+
}
|
| 178 |
+
75% {
|
| 179 |
+
border-radius: 75% 25% 40% 60%;
|
| 180 |
+
transform: scaleX(0.6) scaleY(1.7) rotate(-10deg);
|
| 181 |
+
}
|
| 182 |
+
87.5% {
|
| 183 |
+
border-radius: 50% 50% 75% 25%;
|
| 184 |
+
transform: scaleX(1.3) scaleY(0.8) rotate(35deg);
|
| 185 |
+
}
|
| 186 |
}
|
| 187 |
+
|
| 188 |
+
@keyframes liquidBlob2 {
|
| 189 |
+
0%, 100% {
|
| 190 |
+
border-radius: 60% 40% 50% 50%;
|
| 191 |
+
transform: scaleX(1) scaleY(1) rotate(12deg);
|
| 192 |
+
}
|
| 193 |
+
16% {
|
| 194 |
+
border-radius: 15% 85% 60% 40%;
|
| 195 |
+
transform: scaleX(0.3) scaleY(2.3) rotate(50deg);
|
| 196 |
+
}
|
| 197 |
+
32% {
|
| 198 |
+
border-radius: 85% 15% 25% 75%;
|
| 199 |
+
transform: scaleX(2.0) scaleY(0.5) rotate(-20deg);
|
| 200 |
+
}
|
| 201 |
+
48% {
|
| 202 |
+
border-radius: 30% 70% 85% 15%;
|
| 203 |
+
transform: scaleX(0.4) scaleY(1.8) rotate(70deg);
|
| 204 |
+
}
|
| 205 |
+
64% {
|
| 206 |
+
border-radius: 70% 30% 15% 85%;
|
| 207 |
+
transform: scaleX(1.9) scaleY(0.6) rotate(-35deg);
|
| 208 |
+
}
|
| 209 |
+
80% {
|
| 210 |
+
border-radius: 40% 60% 70% 30%;
|
| 211 |
+
transform: scaleX(0.7) scaleY(1.6) rotate(45deg);
|
| 212 |
+
}
|
| 213 |
}
|
| 214 |
|
| 215 |
+
@keyframes liquidBlob3 {
|
| 216 |
+
0%, 100% {
|
| 217 |
+
border-radius: 50% 50% 40% 60%;
|
| 218 |
+
transform: scaleX(1) scaleY(1) rotate(0deg);
|
| 219 |
+
}
|
| 220 |
+
20% {
|
| 221 |
+
border-radius: 10% 90% 75% 25%;
|
| 222 |
+
transform: scaleX(2.2) scaleY(0.3) rotate(-45deg);
|
| 223 |
+
}
|
| 224 |
+
40% {
|
| 225 |
+
border-radius: 90% 10% 20% 80%;
|
| 226 |
+
transform: scaleX(0.4) scaleY(2.5) rotate(60deg);
|
| 227 |
+
}
|
| 228 |
+
60% {
|
| 229 |
+
border-radius: 25% 75% 90% 10%;
|
| 230 |
+
transform: scaleX(1.7) scaleY(0.5) rotate(-25deg);
|
| 231 |
+
}
|
| 232 |
+
80% {
|
| 233 |
+
border-radius: 75% 25% 10% 90%;
|
| 234 |
+
transform: scaleX(0.6) scaleY(2.0) rotate(80deg);
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
@keyframes liquidBlob4 {
|
| 239 |
+
0%, 100% {
|
| 240 |
+
border-radius: 45% 55% 50% 50%;
|
| 241 |
+
transform: scaleX(1) scaleY(1) rotate(-15deg);
|
| 242 |
+
}
|
| 243 |
+
14% {
|
| 244 |
+
border-radius: 90% 10% 65% 35%;
|
| 245 |
+
transform: scaleX(0.2) scaleY(2.8) rotate(35deg);
|
| 246 |
+
}
|
| 247 |
+
28% {
|
| 248 |
+
border-radius: 10% 90% 20% 80%;
|
| 249 |
+
transform: scaleX(2.4) scaleY(0.4) rotate(-50deg);
|
| 250 |
+
}
|
| 251 |
+
42% {
|
| 252 |
+
border-radius: 35% 65% 90% 10%;
|
| 253 |
+
transform: scaleX(0.3) scaleY(2.1) rotate(70deg);
|
| 254 |
+
}
|
| 255 |
+
56% {
|
| 256 |
+
border-radius: 80% 20% 10% 90%;
|
| 257 |
+
transform: scaleX(2.0) scaleY(0.5) rotate(-40deg);
|
| 258 |
+
}
|
| 259 |
+
70% {
|
| 260 |
+
border-radius: 20% 80% 55% 45%;
|
| 261 |
+
transform: scaleX(0.5) scaleY(1.9) rotate(55deg);
|
| 262 |
+
}
|
| 263 |
+
84% {
|
| 264 |
+
border-radius: 65% 35% 80% 20%;
|
| 265 |
+
transform: scaleX(1.6) scaleY(0.6) rotate(-25deg);
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
/* Fast flowing movement animations */
|
| 270 |
+
@keyframes liquidFlow1 {
|
| 271 |
+
0%, 100% { transform: translate(0, 0); }
|
| 272 |
+
16% { transform: translate(60px, -40px); }
|
| 273 |
+
32% { transform: translate(-45px, -70px); }
|
| 274 |
+
48% { transform: translate(80px, 25px); }
|
| 275 |
+
64% { transform: translate(-30px, 60px); }
|
| 276 |
+
80% { transform: translate(50px, -20px); }
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
@keyframes liquidFlow2 {
|
| 280 |
+
0%, 100% { transform: translate(0, 0); }
|
| 281 |
+
20% { transform: translate(-70px, 50px); }
|
| 282 |
+
40% { transform: translate(90px, -30px); }
|
| 283 |
+
60% { transform: translate(-40px, -55px); }
|
| 284 |
+
80% { transform: translate(65px, 35px); }
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
@keyframes liquidFlow3 {
|
| 288 |
+
0%, 100% { transform: translate(0, 0); }
|
| 289 |
+
12% { transform: translate(-50px, -60px); }
|
| 290 |
+
24% { transform: translate(40px, -20px); }
|
| 291 |
+
36% { transform: translate(-30px, 70px); }
|
| 292 |
+
48% { transform: translate(70px, 20px); }
|
| 293 |
+
60% { transform: translate(-60px, -35px); }
|
| 294 |
+
72% { transform: translate(35px, 55px); }
|
| 295 |
+
84% { transform: translate(-25px, -45px); }
|
| 296 |
}
|
| 297 |
|
| 298 |
+
@keyframes liquidFlow4 {
|
| 299 |
+
0%, 100% { transform: translate(0, 0); }
|
| 300 |
+
14% { transform: translate(50px, 60px); }
|
| 301 |
+
28% { transform: translate(-80px, -40px); }
|
| 302 |
+
42% { transform: translate(30px, -90px); }
|
| 303 |
+
56% { transform: translate(-55px, 45px); }
|
| 304 |
+
70% { transform: translate(75px, -25px); }
|
| 305 |
+
84% { transform: translate(-35px, 65px); }
|
| 306 |
}
|
| 307 |
|
| 308 |
+
/* Light sweep animation for buttons */
|
| 309 |
+
@keyframes lightSweep {
|
| 310 |
+
0% {
|
| 311 |
+
transform: translateX(-150%);
|
| 312 |
+
opacity: 0;
|
| 313 |
+
}
|
| 314 |
+
8% {
|
| 315 |
+
opacity: 0.3;
|
| 316 |
+
}
|
| 317 |
+
25% {
|
| 318 |
+
opacity: 0.8;
|
| 319 |
+
}
|
| 320 |
+
42% {
|
| 321 |
+
opacity: 0.3;
|
| 322 |
+
}
|
| 323 |
+
50% {
|
| 324 |
+
transform: translateX(150%);
|
| 325 |
+
opacity: 0;
|
| 326 |
+
}
|
| 327 |
+
58% {
|
| 328 |
+
opacity: 0.3;
|
| 329 |
+
}
|
| 330 |
+
75% {
|
| 331 |
+
opacity: 0.8;
|
| 332 |
+
}
|
| 333 |
+
92% {
|
| 334 |
+
opacity: 0.3;
|
| 335 |
+
}
|
| 336 |
+
100% {
|
| 337 |
+
transform: translateX(-150%);
|
| 338 |
+
opacity: 0;
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.light-sweep {
|
| 343 |
+
position: relative;
|
| 344 |
+
overflow: hidden;
|
| 345 |
}
|
| 346 |
+
|
| 347 |
+
.light-sweep::before {
|
| 348 |
+
content: '';
|
| 349 |
+
position: absolute;
|
| 350 |
+
top: 0;
|
| 351 |
+
left: 0;
|
| 352 |
+
right: 0;
|
| 353 |
+
bottom: 0;
|
| 354 |
+
width: 300%;
|
| 355 |
+
background: linear-gradient(
|
| 356 |
+
90deg,
|
| 357 |
+
transparent 0%,
|
| 358 |
+
transparent 20%,
|
| 359 |
+
rgba(56, 189, 248, 0.1) 35%,
|
| 360 |
+
rgba(56, 189, 248, 0.2) 45%,
|
| 361 |
+
rgba(255, 255, 255, 0.2) 50%,
|
| 362 |
+
rgba(168, 85, 247, 0.2) 55%,
|
| 363 |
+
rgba(168, 85, 247, 0.1) 65%,
|
| 364 |
+
transparent 80%,
|
| 365 |
+
transparent 100%
|
| 366 |
+
);
|
| 367 |
+
animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
| 368 |
+
pointer-events: none;
|
| 369 |
+
z-index: 1;
|
| 370 |
+
filter: blur(1px);
|
| 371 |
}
|
assets/hf-logo.svg
DELETED
assets/logo.svg
ADDED
|
|
assets/minimax.svg
DELETED
assets/pro.svg
DELETED
components.json
CHANGED
|
@@ -5,12 +5,11 @@
|
|
| 5 |
"tsx": true,
|
| 6 |
"tailwind": {
|
| 7 |
"config": "",
|
| 8 |
-
"css": "
|
| 9 |
-
"baseColor": "
|
| 10 |
"cssVariables": true,
|
| 11 |
"prefix": ""
|
| 12 |
},
|
| 13 |
-
"iconLibrary": "lucide",
|
| 14 |
"aliases": {
|
| 15 |
"components": "@/components",
|
| 16 |
"utils": "@/lib/utils",
|
|
@@ -18,5 +17,5 @@
|
|
| 18 |
"lib": "@/lib",
|
| 19 |
"hooks": "@/hooks"
|
| 20 |
},
|
| 21 |
-
"
|
| 22 |
-
}
|
|
|
|
| 5 |
"tsx": true,
|
| 6 |
"tailwind": {
|
| 7 |
"config": "",
|
| 8 |
+
"css": "assets/globals.css",
|
| 9 |
+
"baseColor": "neutral",
|
| 10 |
"cssVariables": true,
|
| 11 |
"prefix": ""
|
| 12 |
},
|
|
|
|
| 13 |
"aliases": {
|
| 14 |
"components": "@/components",
|
| 15 |
"utils": "@/lib/utils",
|
|
|
|
| 17 |
"lib": "@/lib",
|
| 18 |
"hooks": "@/hooks"
|
| 19 |
},
|
| 20 |
+
"iconLibrary": "lucide"
|
| 21 |
+
}
|
components/animated-blobs/index.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function AnimatedBlobs() {
|
| 2 |
+
return (
|
| 3 |
+
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
| 4 |
+
<div
|
| 5 |
+
className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl"
|
| 6 |
+
style={{
|
| 7 |
+
animation:
|
| 8 |
+
"liquidBlob1 4s ease-in-out infinite, liquidFlow1 6s ease-in-out infinite",
|
| 9 |
+
}}
|
| 10 |
+
/>
|
| 11 |
+
<div
|
| 12 |
+
className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10"
|
| 13 |
+
style={{
|
| 14 |
+
animation:
|
| 15 |
+
"liquidBlob2 5s ease-in-out infinite, liquidFlow2 7s ease-in-out infinite",
|
| 16 |
+
}}
|
| 17 |
+
/>
|
| 18 |
+
<div
|
| 19 |
+
className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10"
|
| 20 |
+
style={{
|
| 21 |
+
animation:
|
| 22 |
+
"liquidBlob3 3.5s ease-in-out infinite, liquidFlow3 8s ease-in-out infinite",
|
| 23 |
+
}}
|
| 24 |
+
/>
|
| 25 |
+
<div
|
| 26 |
+
className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3"
|
| 27 |
+
style={{
|
| 28 |
+
animation:
|
| 29 |
+
"liquidBlob4 4.5s ease-in-out infinite, liquidFlow4 6.5s ease-in-out infinite",
|
| 30 |
+
}}
|
| 31 |
+
/>
|
| 32 |
+
</div>
|
| 33 |
+
);
|
| 34 |
+
}
|
components/animated-text/index.tsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState, useEffect } from "react";
|
| 4 |
+
|
| 5 |
+
interface AnimatedTextProps {
|
| 6 |
+
className?: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export function AnimatedText({ className = "" }: AnimatedTextProps) {
|
| 10 |
+
const [displayText, setDisplayText] = useState("");
|
| 11 |
+
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
|
| 12 |
+
const [isTyping, setIsTyping] = useState(true);
|
| 13 |
+
const [showCursor, setShowCursor] = useState(true);
|
| 14 |
+
const [lastTypedIndex, setLastTypedIndex] = useState(-1);
|
| 15 |
+
const [animationComplete, setAnimationComplete] = useState(false);
|
| 16 |
+
|
| 17 |
+
// Randomize suggestions on each component mount
|
| 18 |
+
const [suggestions] = useState(() => {
|
| 19 |
+
const baseSuggestions = [
|
| 20 |
+
"create a stunning portfolio!",
|
| 21 |
+
"build a tic tac toe game!",
|
| 22 |
+
"design a website for my restaurant!",
|
| 23 |
+
"make a sleek landing page!",
|
| 24 |
+
"build an e-commerce store!",
|
| 25 |
+
"create a personal blog!",
|
| 26 |
+
"develop a modern dashboard!",
|
| 27 |
+
"design a company website!",
|
| 28 |
+
"build a todo app!",
|
| 29 |
+
"create an online gallery!",
|
| 30 |
+
"make a contact form!",
|
| 31 |
+
"build a weather app!",
|
| 32 |
+
];
|
| 33 |
+
|
| 34 |
+
// Fisher-Yates shuffle algorithm
|
| 35 |
+
const shuffled = [...baseSuggestions];
|
| 36 |
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
| 37 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 38 |
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
return shuffled;
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
useEffect(() => {
|
| 45 |
+
if (animationComplete) return;
|
| 46 |
+
|
| 47 |
+
let timeout: NodeJS.Timeout;
|
| 48 |
+
|
| 49 |
+
const typeText = () => {
|
| 50 |
+
const currentSuggestion = suggestions[currentSuggestionIndex];
|
| 51 |
+
|
| 52 |
+
if (isTyping) {
|
| 53 |
+
if (displayText.length < currentSuggestion.length) {
|
| 54 |
+
setDisplayText(currentSuggestion.slice(0, displayText.length + 1));
|
| 55 |
+
setLastTypedIndex(displayText.length);
|
| 56 |
+
timeout = setTimeout(typeText, 80);
|
| 57 |
+
} else {
|
| 58 |
+
// Finished typing, wait then start erasing
|
| 59 |
+
setLastTypedIndex(-1);
|
| 60 |
+
timeout = setTimeout(() => {
|
| 61 |
+
setIsTyping(false);
|
| 62 |
+
}, 2000);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
timeout = setTimeout(typeText, 100);
|
| 68 |
+
return () => clearTimeout(timeout);
|
| 69 |
+
}, [
|
| 70 |
+
displayText,
|
| 71 |
+
currentSuggestionIndex,
|
| 72 |
+
isTyping,
|
| 73 |
+
suggestions,
|
| 74 |
+
animationComplete,
|
| 75 |
+
]);
|
| 76 |
+
|
| 77 |
+
// Cursor blinking effect
|
| 78 |
+
useEffect(() => {
|
| 79 |
+
if (animationComplete) {
|
| 80 |
+
setShowCursor(false);
|
| 81 |
+
return;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
const cursorInterval = setInterval(() => {
|
| 85 |
+
setShowCursor((prev) => !prev);
|
| 86 |
+
}, 600);
|
| 87 |
+
|
| 88 |
+
return () => clearInterval(cursorInterval);
|
| 89 |
+
}, [animationComplete]);
|
| 90 |
+
|
| 91 |
+
useEffect(() => {
|
| 92 |
+
if (lastTypedIndex >= 0) {
|
| 93 |
+
const timeout = setTimeout(() => {
|
| 94 |
+
setLastTypedIndex(-1);
|
| 95 |
+
}, 400);
|
| 96 |
+
|
| 97 |
+
return () => clearTimeout(timeout);
|
| 98 |
+
}
|
| 99 |
+
}, [lastTypedIndex]);
|
| 100 |
+
|
| 101 |
+
return (
|
| 102 |
+
<p className={`font-mono ${className}`}>
|
| 103 |
+
Hey DeepSite,
|
| 104 |
+
{displayText.split("").map((char, index) => (
|
| 105 |
+
<span
|
| 106 |
+
key={`${currentSuggestionIndex}-${index}`}
|
| 107 |
+
className={`transition-colors duration-300 ${
|
| 108 |
+
index === lastTypedIndex ? "text-neutral-100" : ""
|
| 109 |
+
}`}
|
| 110 |
+
>
|
| 111 |
+
{char}
|
| 112 |
+
</span>
|
| 113 |
+
))}
|
| 114 |
+
<span
|
| 115 |
+
className={`${
|
| 116 |
+
showCursor ? "opacity-100" : "opacity-0"
|
| 117 |
+
} transition-opacity`}
|
| 118 |
+
>
|
| 119 |
+
|
|
| 120 |
+
</span>
|
| 121 |
+
</p>
|
| 122 |
+
);
|
| 123 |
+
}
|
components/ask-ai/ask-ai-landing.tsx
DELETED
|
@@ -1,75 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
import { ArrowUp } from "lucide-react";
|
| 3 |
-
import { useState } from "react";
|
| 4 |
-
import { useLocalStorage, useMount } from "react-use";
|
| 5 |
-
import { useRouter } from "next/navigation";
|
| 6 |
-
|
| 7 |
-
import { Button } from "@/components/ui/button";
|
| 8 |
-
import { ProviderType } from "@/lib/type";
|
| 9 |
-
import { Models } from "./models";
|
| 10 |
-
import { DEFAULT_MODEL } from "@/lib/providers";
|
| 11 |
-
import { cn } from "@/lib/utils";
|
| 12 |
-
|
| 13 |
-
export function AskAiLanding({ className }: { className?: string }) {
|
| 14 |
-
const [model = DEFAULT_MODEL, setModel] = useLocalStorage<string>(
|
| 15 |
-
"deepsite-model",
|
| 16 |
-
DEFAULT_MODEL
|
| 17 |
-
);
|
| 18 |
-
const [provider, setProvider] = useLocalStorage<ProviderType>(
|
| 19 |
-
"deepsite-provider",
|
| 20 |
-
"auto" as ProviderType
|
| 21 |
-
);
|
| 22 |
-
const router = useRouter();
|
| 23 |
-
const [prompt, setPrompt] = useState<string>("");
|
| 24 |
-
const [mounted, setMounted] = useState<boolean>(false);
|
| 25 |
-
|
| 26 |
-
useMount(() => {
|
| 27 |
-
setMounted(true);
|
| 28 |
-
});
|
| 29 |
-
|
| 30 |
-
return (
|
| 31 |
-
<div
|
| 32 |
-
className={cn(
|
| 33 |
-
"dark:bg-[#222222] bg-accent border border-border-muted rounded-xl p-3 block relative",
|
| 34 |
-
className
|
| 35 |
-
)}
|
| 36 |
-
>
|
| 37 |
-
<textarea
|
| 38 |
-
id="prompt-input"
|
| 39 |
-
className="w-full h-full resize-none outline-none text-primary text-sm"
|
| 40 |
-
placeholder="Ask me anything..."
|
| 41 |
-
value={prompt}
|
| 42 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 43 |
-
onKeyDown={(e) => {
|
| 44 |
-
if (e.key === "Enter" && !e.shiftKey) {
|
| 45 |
-
e.preventDefault();
|
| 46 |
-
router.push(`/new?prompt=${prompt}`);
|
| 47 |
-
}
|
| 48 |
-
}}
|
| 49 |
-
/>
|
| 50 |
-
<footer className="flex items-center justify-between">
|
| 51 |
-
<div className="flex items-center gap-2">
|
| 52 |
-
{mounted && (
|
| 53 |
-
<Models
|
| 54 |
-
model={model}
|
| 55 |
-
setModel={setModel}
|
| 56 |
-
provider={provider as ProviderType}
|
| 57 |
-
setProvider={setProvider}
|
| 58 |
-
/>
|
| 59 |
-
)}
|
| 60 |
-
</div>
|
| 61 |
-
<div>
|
| 62 |
-
<Button
|
| 63 |
-
size="icon-sm"
|
| 64 |
-
className="rounded-full!"
|
| 65 |
-
onClick={() => {
|
| 66 |
-
router.push(`/new?prompt=${prompt}`);
|
| 67 |
-
}}
|
| 68 |
-
>
|
| 69 |
-
<ArrowUp />
|
| 70 |
-
</Button>
|
| 71 |
-
</div>
|
| 72 |
-
</footer>
|
| 73 |
-
</div>
|
| 74 |
-
);
|
| 75 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ask-ai/ask-ai.tsx
DELETED
|
@@ -1,211 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
import { ArrowUp, Paintbrush, X } from "lucide-react";
|
| 3 |
-
import { FaHand } from "react-icons/fa6";
|
| 4 |
-
import { useRef, useState } from "react";
|
| 5 |
-
import { HiStop } from "react-icons/hi2";
|
| 6 |
-
import { useLocalStorage, useMount, useUpdateEffect } from "react-use";
|
| 7 |
-
import { useRouter } from "next/navigation";
|
| 8 |
-
import { useNextStep } from "nextstepjs";
|
| 9 |
-
|
| 10 |
-
import { Button } from "@/components/ui/button";
|
| 11 |
-
import { cn } from "@/lib/utils";
|
| 12 |
-
import { useGeneration } from "./useGeneration";
|
| 13 |
-
import { File, MobileTabType, ProviderType } from "@/lib/type";
|
| 14 |
-
import { Models } from "./models";
|
| 15 |
-
import { DEFAULT_MODEL, MODELS } from "@/lib/providers";
|
| 16 |
-
import { Redesign } from "./redesign";
|
| 17 |
-
import { Uploader } from "./uploader";
|
| 18 |
-
import { InputMentions } from "./input-mentions";
|
| 19 |
-
|
| 20 |
-
export function AskAI({
|
| 21 |
-
initialPrompt,
|
| 22 |
-
className,
|
| 23 |
-
onToggleMobileTab,
|
| 24 |
-
files,
|
| 25 |
-
medias,
|
| 26 |
-
tourHasBeenShown,
|
| 27 |
-
isNew = false,
|
| 28 |
-
isHistoryView,
|
| 29 |
-
projectName = "new",
|
| 30 |
-
}: {
|
| 31 |
-
initialPrompt?: string;
|
| 32 |
-
className?: string;
|
| 33 |
-
files?: File[] | null;
|
| 34 |
-
medias?: string[] | null;
|
| 35 |
-
tourHasBeenShown?: boolean;
|
| 36 |
-
onToggleMobileTab?: (tab: MobileTabType) => void;
|
| 37 |
-
isNew?: boolean;
|
| 38 |
-
isHistoryView?: boolean;
|
| 39 |
-
projectName?: string;
|
| 40 |
-
}) {
|
| 41 |
-
const contentEditableRef = useRef<HTMLDivElement | null>(null);
|
| 42 |
-
const [model = DEFAULT_MODEL, setModel] = useLocalStorage<string>(
|
| 43 |
-
"deepsite-model",
|
| 44 |
-
DEFAULT_MODEL
|
| 45 |
-
);
|
| 46 |
-
const [provider, setProvider] = useLocalStorage<ProviderType>(
|
| 47 |
-
"deepsite-provider",
|
| 48 |
-
"auto" as ProviderType
|
| 49 |
-
);
|
| 50 |
-
|
| 51 |
-
const [prompt, setPrompt] = useState(initialPrompt ?? "");
|
| 52 |
-
const [redesignMd, setRedesignMd] = useState<{
|
| 53 |
-
md: string;
|
| 54 |
-
url: string;
|
| 55 |
-
} | null>(null);
|
| 56 |
-
const [selectedMedias, setSelectedMedias] = useState<string[]>([]);
|
| 57 |
-
const [startTour, setStartTour] = useState<boolean>(false);
|
| 58 |
-
|
| 59 |
-
const router = useRouter();
|
| 60 |
-
const { callAi, isLoading, stopGeneration, audio } =
|
| 61 |
-
useGeneration(projectName);
|
| 62 |
-
const { startNextStep } = useNextStep();
|
| 63 |
-
|
| 64 |
-
const onComplete = () => {
|
| 65 |
-
onToggleMobileTab?.("right-sidebar");
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
useMount(() => {
|
| 69 |
-
if (initialPrompt && initialPrompt.trim() !== "" && isNew) {
|
| 70 |
-
setTimeout(() => {
|
| 71 |
-
if (isHistoryView) return;
|
| 72 |
-
callAi(
|
| 73 |
-
{
|
| 74 |
-
prompt: initialPrompt,
|
| 75 |
-
model,
|
| 76 |
-
onComplete,
|
| 77 |
-
provider,
|
| 78 |
-
},
|
| 79 |
-
setModel
|
| 80 |
-
);
|
| 81 |
-
router.replace("/new");
|
| 82 |
-
}, 200);
|
| 83 |
-
}
|
| 84 |
-
});
|
| 85 |
-
|
| 86 |
-
const onSubmit = () => {
|
| 87 |
-
if (isHistoryView) return;
|
| 88 |
-
if (contentEditableRef.current) {
|
| 89 |
-
contentEditableRef.current.innerHTML = "";
|
| 90 |
-
}
|
| 91 |
-
callAi(
|
| 92 |
-
{
|
| 93 |
-
prompt,
|
| 94 |
-
model,
|
| 95 |
-
onComplete,
|
| 96 |
-
provider,
|
| 97 |
-
redesignMd,
|
| 98 |
-
medias: selectedMedias ?? [],
|
| 99 |
-
},
|
| 100 |
-
setModel
|
| 101 |
-
);
|
| 102 |
-
if (selectedMedias.length > 0) setSelectedMedias([]);
|
| 103 |
-
if (redesignMd) setRedesignMd(null);
|
| 104 |
-
};
|
| 105 |
-
|
| 106 |
-
return (
|
| 107 |
-
<div
|
| 108 |
-
id="tour-ask-ai-section"
|
| 109 |
-
className={cn(
|
| 110 |
-
"dark:bg-[#222222] bg-accent border border-border-muted rounded-xl p-2.5 block relative",
|
| 111 |
-
className
|
| 112 |
-
)}
|
| 113 |
-
>
|
| 114 |
-
<InputMentions
|
| 115 |
-
ref={contentEditableRef}
|
| 116 |
-
files={files}
|
| 117 |
-
prompt={prompt}
|
| 118 |
-
setPrompt={setPrompt}
|
| 119 |
-
redesignMdUrl={redesignMd?.url?.replace(/(^\w+:|^)\/\//, "")}
|
| 120 |
-
onSubmit={onSubmit}
|
| 121 |
-
/>
|
| 122 |
-
<footer className="flex items-center justify-between mt-0">
|
| 123 |
-
<div className="flex items-center gap-1.5">
|
| 124 |
-
{!tourHasBeenShown && (
|
| 125 |
-
<div className="relative z-1">
|
| 126 |
-
<Button
|
| 127 |
-
variant="indigo"
|
| 128 |
-
size="icon-xs"
|
| 129 |
-
className="rounded-full!"
|
| 130 |
-
onClick={() => {
|
| 131 |
-
setStartTour(true);
|
| 132 |
-
startNextStep("onboarding");
|
| 133 |
-
}}
|
| 134 |
-
>
|
| 135 |
-
<FaHand className="size-3" />
|
| 136 |
-
</Button>
|
| 137 |
-
{!startTour && (
|
| 138 |
-
<div className="animate-ping h-full rounded-full bg-indigo-500 w-full top-0 left-0 absolute -z-1" />
|
| 139 |
-
)}
|
| 140 |
-
</div>
|
| 141 |
-
)}
|
| 142 |
-
{!isNew && (
|
| 143 |
-
<Uploader
|
| 144 |
-
medias={medias}
|
| 145 |
-
selected={selectedMedias}
|
| 146 |
-
setSelected={setSelectedMedias}
|
| 147 |
-
/>
|
| 148 |
-
)}
|
| 149 |
-
<Models
|
| 150 |
-
model={model}
|
| 151 |
-
setModel={setModel}
|
| 152 |
-
provider={provider as ProviderType}
|
| 153 |
-
setProvider={setProvider}
|
| 154 |
-
/>
|
| 155 |
-
{!files ||
|
| 156 |
-
(files?.length === 0 &&
|
| 157 |
-
(redesignMd ? (
|
| 158 |
-
<Button
|
| 159 |
-
size="xs"
|
| 160 |
-
variant="indigo"
|
| 161 |
-
className="rounded-full! px-2.5!"
|
| 162 |
-
onClick={() => setRedesignMd(null)}
|
| 163 |
-
>
|
| 164 |
-
<Paintbrush className="size-3" />
|
| 165 |
-
{redesignMd.url?.replace(/(^\w+:|^)\/\//, "")}
|
| 166 |
-
<X className="size-3.5" onClick={() => setRedesignMd(null)} />
|
| 167 |
-
</Button>
|
| 168 |
-
) : (
|
| 169 |
-
<Redesign
|
| 170 |
-
onRedesign={(md, url) => {
|
| 171 |
-
setRedesignMd({
|
| 172 |
-
md,
|
| 173 |
-
url,
|
| 174 |
-
});
|
| 175 |
-
}}
|
| 176 |
-
/>
|
| 177 |
-
)))}
|
| 178 |
-
</div>
|
| 179 |
-
<div>
|
| 180 |
-
{isLoading ? (
|
| 181 |
-
<Button
|
| 182 |
-
size="icon-sm"
|
| 183 |
-
className="rounded-full!"
|
| 184 |
-
variant="bordered"
|
| 185 |
-
onClick={stopGeneration}
|
| 186 |
-
>
|
| 187 |
-
<HiStop />
|
| 188 |
-
</Button>
|
| 189 |
-
) : (
|
| 190 |
-
<Button
|
| 191 |
-
size="icon-sm"
|
| 192 |
-
className="rounded-full!"
|
| 193 |
-
disabled={
|
| 194 |
-
isHistoryView ||
|
| 195 |
-
isLoading ||
|
| 196 |
-
(prompt.trim() === "" && !redesignMd)
|
| 197 |
-
}
|
| 198 |
-
onClick={onSubmit}
|
| 199 |
-
>
|
| 200 |
-
<ArrowUp />
|
| 201 |
-
</Button>
|
| 202 |
-
)}
|
| 203 |
-
</div>
|
| 204 |
-
</footer>
|
| 205 |
-
<audio ref={audio} id="audio" className="hidden">
|
| 206 |
-
<source src="/ding.mp3" type="audio/mpeg" />
|
| 207 |
-
Your browser does not support the audio element.
|
| 208 |
-
</audio>
|
| 209 |
-
</div>
|
| 210 |
-
);
|
| 211 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ask-ai/context.tsx
DELETED
|
@@ -1,123 +0,0 @@
|
|
| 1 |
-
import { AtSign, Braces, FileCode, FileText, X } from "lucide-react";
|
| 2 |
-
import { useMemo, useState } from "react";
|
| 3 |
-
import { useQueryClient } from "@tanstack/react-query";
|
| 4 |
-
|
| 5 |
-
import {
|
| 6 |
-
Popover,
|
| 7 |
-
PopoverContent,
|
| 8 |
-
PopoverTrigger,
|
| 9 |
-
} from "@/components/ui/popover";
|
| 10 |
-
import { Button } from "@/components/ui/button";
|
| 11 |
-
import { File } from "@/lib/type";
|
| 12 |
-
import { cn } from "@/lib/utils";
|
| 13 |
-
|
| 14 |
-
export const Context = ({
|
| 15 |
-
files,
|
| 16 |
-
setFiles,
|
| 17 |
-
}: {
|
| 18 |
-
files: File[];
|
| 19 |
-
setFiles: (files: File[]) => void;
|
| 20 |
-
}) => {
|
| 21 |
-
const queryClient = useQueryClient();
|
| 22 |
-
const [open, setOpen] = useState(false);
|
| 23 |
-
|
| 24 |
-
const getFileIcon = (filePath: string, size = "size-3.5") => {
|
| 25 |
-
if (filePath.endsWith(".css")) {
|
| 26 |
-
return <Braces className={size} />;
|
| 27 |
-
} else if (filePath.endsWith(".js")) {
|
| 28 |
-
return <FileCode className={size} />;
|
| 29 |
-
} else if (filePath.endsWith(".json")) {
|
| 30 |
-
return <Braces className={size} />;
|
| 31 |
-
} else {
|
| 32 |
-
return <FileText className={size} />;
|
| 33 |
-
}
|
| 34 |
-
};
|
| 35 |
-
|
| 36 |
-
const getFiles = () => queryClient.getQueryData<File[]>(["files"]) ?? [];
|
| 37 |
-
|
| 38 |
-
return (
|
| 39 |
-
<div className="flex items-center justify-start gap-1 flex-wrap">
|
| 40 |
-
<Popover open={open} onOpenChange={setOpen}>
|
| 41 |
-
<PopoverTrigger asChild>
|
| 42 |
-
<Button size="xxs" variant={open ? "default" : "bordered"}>
|
| 43 |
-
<AtSign className="size-3" />
|
| 44 |
-
Add Context...
|
| 45 |
-
</Button>
|
| 46 |
-
</PopoverTrigger>
|
| 47 |
-
<PopoverContent
|
| 48 |
-
align="start"
|
| 49 |
-
className="translate-x-6 space-y-4 min-w-fit rounded-2xl! p-0!"
|
| 50 |
-
>
|
| 51 |
-
<main className="p-4">
|
| 52 |
-
<p className="text-xs text-muted-foreground mb-2.5">
|
| 53 |
-
Select a file to send as context
|
| 54 |
-
</p>
|
| 55 |
-
<div className="max-h-[200px] overflow-y-auto space-y-0.5">
|
| 56 |
-
{getFiles().length === 0 ? (
|
| 57 |
-
<div className="text-xs text-muted-foreground">
|
| 58 |
-
No files available
|
| 59 |
-
</div>
|
| 60 |
-
) : (
|
| 61 |
-
<>
|
| 62 |
-
<button
|
| 63 |
-
onClick={() => {
|
| 64 |
-
setFiles([]);
|
| 65 |
-
setOpen(false);
|
| 66 |
-
}}
|
| 67 |
-
className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-accent hover:text-accent-foreground transition-colors ${
|
| 68 |
-
files.length === 0
|
| 69 |
-
? "bg-linear-to-r from-indigo-500/20 to-indigo-500/5 text-primary font-medium"
|
| 70 |
-
: "text-muted-foreground"
|
| 71 |
-
}`}
|
| 72 |
-
>
|
| 73 |
-
All files (default)
|
| 74 |
-
</button>
|
| 75 |
-
{getFiles()?.map((page) => (
|
| 76 |
-
<button
|
| 77 |
-
key={page.path}
|
| 78 |
-
onClick={() => {
|
| 79 |
-
if (files.some((f) => f.path === page.path))
|
| 80 |
-
setFiles(files.filter((f) => f.path !== page.path));
|
| 81 |
-
else setFiles(files ? [...files, page] : [page]);
|
| 82 |
-
setOpen(false);
|
| 83 |
-
}}
|
| 84 |
-
className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-accent hover:text-accent-foreground transition-colors flex items-center gap-1.5 ${
|
| 85 |
-
files?.some((f) => f.path === page.path)
|
| 86 |
-
? "bg-linear-to-r from-indigo-500/20 to-indigo-500/5 text-primary font-medium"
|
| 87 |
-
: "text-muted-foreground"
|
| 88 |
-
}`}
|
| 89 |
-
>
|
| 90 |
-
<span className="shrink-0">
|
| 91 |
-
{getFileIcon(page.path, "size-3")}
|
| 92 |
-
</span>
|
| 93 |
-
<span className="truncate flex-1">{page.path}</span>
|
| 94 |
-
</button>
|
| 95 |
-
))}
|
| 96 |
-
</>
|
| 97 |
-
)}
|
| 98 |
-
</div>
|
| 99 |
-
</main>
|
| 100 |
-
</PopoverContent>
|
| 101 |
-
</Popover>
|
| 102 |
-
{files?.map((file) => (
|
| 103 |
-
<Button
|
| 104 |
-
key={file.path}
|
| 105 |
-
size="xxs"
|
| 106 |
-
variant="bordered"
|
| 107 |
-
className="cursor-default!"
|
| 108 |
-
>
|
| 109 |
-
{getFileIcon(file.path, "size-3")}
|
| 110 |
-
{file.path}
|
| 111 |
-
<span
|
| 112 |
-
className="opacity-50 hover:opacity-80 cursor-pointer"
|
| 113 |
-
onClick={() => {
|
| 114 |
-
setFiles(files.filter((f) => f.path !== file.path));
|
| 115 |
-
}}
|
| 116 |
-
>
|
| 117 |
-
<X className="size-3.5 opacity-50 hover:opacity-80 shrink-0" />
|
| 118 |
-
</span>
|
| 119 |
-
</Button>
|
| 120 |
-
))}
|
| 121 |
-
</div>
|
| 122 |
-
);
|
| 123 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ask-ai/input-mentions.tsx
DELETED
|
@@ -1,314 +0,0 @@
|
|
| 1 |
-
import { useRef, useState, useEffect, RefObject } from "react";
|
| 2 |
-
import { useClickAway } from "react-use";
|
| 3 |
-
import { useQueryClient } from "@tanstack/react-query";
|
| 4 |
-
|
| 5 |
-
import { searchFilesMentions } from "@/actions/mentions";
|
| 6 |
-
import { File } from "@/lib/type";
|
| 7 |
-
import { Braces, FileCode, FileText } from "lucide-react";
|
| 8 |
-
|
| 9 |
-
export function InputMentions({
|
| 10 |
-
ref,
|
| 11 |
-
prompt,
|
| 12 |
-
files,
|
| 13 |
-
setPrompt,
|
| 14 |
-
redesignMdUrl,
|
| 15 |
-
onSubmit,
|
| 16 |
-
}: {
|
| 17 |
-
ref: RefObject<HTMLDivElement | null>;
|
| 18 |
-
prompt: string;
|
| 19 |
-
files?: File[] | null;
|
| 20 |
-
redesignMdUrl?: string;
|
| 21 |
-
setPrompt: (prompt: string) => void;
|
| 22 |
-
onSubmit: () => void;
|
| 23 |
-
}) {
|
| 24 |
-
const queryClient = useQueryClient();
|
| 25 |
-
const [showMentionDropdown, setShowMentionDropdown] = useState(false);
|
| 26 |
-
const [, setMentionSearch] = useState("");
|
| 27 |
-
const dropdownRef = useRef<HTMLDivElement>(null);
|
| 28 |
-
const [results, setResults] = useState<File[]>([]);
|
| 29 |
-
|
| 30 |
-
useClickAway(dropdownRef, () => {
|
| 31 |
-
setShowMentionDropdown(false);
|
| 32 |
-
});
|
| 33 |
-
|
| 34 |
-
const getTextContent = (element: HTMLElement): string => {
|
| 35 |
-
let text = "";
|
| 36 |
-
const childNodes = element.childNodes;
|
| 37 |
-
|
| 38 |
-
for (let i = 0; i < childNodes.length; i++) {
|
| 39 |
-
const node = childNodes[i];
|
| 40 |
-
if (node.nodeType === Node.TEXT_NODE) {
|
| 41 |
-
text += node.textContent || "";
|
| 42 |
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
| 43 |
-
const el = node as HTMLElement;
|
| 44 |
-
if (el.classList.contains("mention-chip")) {
|
| 45 |
-
text += el.getAttribute("data-mention-id") || "";
|
| 46 |
-
} else {
|
| 47 |
-
text += el.textContent || "";
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
-
}
|
| 51 |
-
return text + "\u0020";
|
| 52 |
-
};
|
| 53 |
-
|
| 54 |
-
const extractPromptWithIds = (): string => {
|
| 55 |
-
if (!ref.current) return "";
|
| 56 |
-
|
| 57 |
-
let text = "";
|
| 58 |
-
const childNodes = ref.current.childNodes;
|
| 59 |
-
|
| 60 |
-
for (let i = 0; i < childNodes.length; i++) {
|
| 61 |
-
const node = childNodes[i];
|
| 62 |
-
if (node.nodeType === Node.TEXT_NODE) {
|
| 63 |
-
text += node.textContent || "";
|
| 64 |
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
| 65 |
-
const el = node as HTMLElement;
|
| 66 |
-
if (el.classList.contains("mention-chip")) {
|
| 67 |
-
text += el.getAttribute("data-mention-id") || "";
|
| 68 |
-
} else {
|
| 69 |
-
text += el.textContent || "";
|
| 70 |
-
}
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
return text;
|
| 74 |
-
};
|
| 75 |
-
|
| 76 |
-
const shouldDetectMention = (): {
|
| 77 |
-
detect: boolean;
|
| 78 |
-
textBeforeCursor: string;
|
| 79 |
-
} => {
|
| 80 |
-
const selection = window.getSelection();
|
| 81 |
-
if (!selection || !ref.current) {
|
| 82 |
-
return { detect: false, textBeforeCursor: "" };
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
const range = selection.getRangeAt(0);
|
| 86 |
-
const node = range.startContainer;
|
| 87 |
-
|
| 88 |
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
| 89 |
-
const element = node as HTMLElement;
|
| 90 |
-
if (element.classList?.contains("mention-chip")) {
|
| 91 |
-
return { detect: false, textBeforeCursor: "" };
|
| 92 |
-
}
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
if (node.parentElement?.classList?.contains("mention-chip")) {
|
| 96 |
-
return { detect: false, textBeforeCursor: "" };
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
if (node.nodeType === Node.TEXT_NODE) {
|
| 100 |
-
const textContent = node.textContent || "";
|
| 101 |
-
const cursorOffset = range.startOffset;
|
| 102 |
-
const textBeforeCursor = textContent.substring(0, cursorOffset);
|
| 103 |
-
|
| 104 |
-
const lastAtIndex = textBeforeCursor.lastIndexOf("@");
|
| 105 |
-
if (lastAtIndex !== -1) {
|
| 106 |
-
const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
|
| 107 |
-
if (!textAfterAt.includes(" ")) {
|
| 108 |
-
return { detect: true, textBeforeCursor: textAfterAt };
|
| 109 |
-
}
|
| 110 |
-
}
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
return { detect: false, textBeforeCursor: "" };
|
| 114 |
-
};
|
| 115 |
-
|
| 116 |
-
const handleInput = async () => {
|
| 117 |
-
if (!ref.current) return;
|
| 118 |
-
const text = getTextContent(ref.current);
|
| 119 |
-
if (text.trim() === "") {
|
| 120 |
-
ref.current.innerHTML = "";
|
| 121 |
-
}
|
| 122 |
-
setPrompt(text);
|
| 123 |
-
|
| 124 |
-
const { detect, textBeforeCursor } = shouldDetectMention();
|
| 125 |
-
|
| 126 |
-
if (detect && files && files?.length > 0) {
|
| 127 |
-
setMentionSearch(textBeforeCursor);
|
| 128 |
-
setShowMentionDropdown(true);
|
| 129 |
-
const files = queryClient.getQueryData<File[]>(["files"]) ?? [];
|
| 130 |
-
const results = await searchFilesMentions(textBeforeCursor, files);
|
| 131 |
-
setResults(results);
|
| 132 |
-
} else {
|
| 133 |
-
setShowMentionDropdown(false);
|
| 134 |
-
}
|
| 135 |
-
};
|
| 136 |
-
|
| 137 |
-
const createMentionChipElement = (mentionId: string): HTMLSpanElement => {
|
| 138 |
-
const mentionChip = document.createElement("span");
|
| 139 |
-
|
| 140 |
-
const baseClasses =
|
| 141 |
-
"mention-chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium";
|
| 142 |
-
const typeClasses =
|
| 143 |
-
"bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400";
|
| 144 |
-
|
| 145 |
-
mentionChip.className = `${baseClasses} ${typeClasses}`;
|
| 146 |
-
mentionChip.contentEditable = "false";
|
| 147 |
-
mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`);
|
| 148 |
-
mentionChip.textContent = `@${mentionId}`;
|
| 149 |
-
|
| 150 |
-
return mentionChip;
|
| 151 |
-
};
|
| 152 |
-
|
| 153 |
-
const insertMention = (mentionId: string) => {
|
| 154 |
-
if (!ref.current) return;
|
| 155 |
-
|
| 156 |
-
const selection = window.getSelection();
|
| 157 |
-
if (!selection || selection.rangeCount === 0) return;
|
| 158 |
-
|
| 159 |
-
const range = selection.getRangeAt(0);
|
| 160 |
-
const textNode = range.startContainer;
|
| 161 |
-
|
| 162 |
-
if (textNode.nodeType !== Node.TEXT_NODE) return;
|
| 163 |
-
|
| 164 |
-
const textContent = textNode.textContent || "";
|
| 165 |
-
const cursorOffset = range.startOffset;
|
| 166 |
-
|
| 167 |
-
const textBeforeCursor = textContent.substring(0, cursorOffset);
|
| 168 |
-
const lastAtIndex = textBeforeCursor.lastIndexOf("@");
|
| 169 |
-
|
| 170 |
-
if (lastAtIndex !== -1) {
|
| 171 |
-
const mentionChip = createMentionChipElement(mentionId);
|
| 172 |
-
|
| 173 |
-
const beforeText = textContent.substring(0, lastAtIndex);
|
| 174 |
-
const afterText = textContent.substring(cursorOffset);
|
| 175 |
-
const parent = textNode.parentNode;
|
| 176 |
-
if (!parent) return;
|
| 177 |
-
|
| 178 |
-
const beforeNode = beforeText
|
| 179 |
-
? document.createTextNode(beforeText)
|
| 180 |
-
: null;
|
| 181 |
-
const spaceNode = document.createTextNode("\u0020");
|
| 182 |
-
const afterNode = afterText ? document.createTextNode(afterText) : null;
|
| 183 |
-
|
| 184 |
-
if (beforeNode) {
|
| 185 |
-
parent.insertBefore(beforeNode, textNode);
|
| 186 |
-
}
|
| 187 |
-
parent.insertBefore(mentionChip, textNode);
|
| 188 |
-
parent.insertBefore(spaceNode, textNode);
|
| 189 |
-
if (afterNode) {
|
| 190 |
-
parent.insertBefore(afterNode, textNode);
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
parent.removeChild(textNode);
|
| 194 |
-
|
| 195 |
-
const newRange = document.createRange();
|
| 196 |
-
if (afterNode) {
|
| 197 |
-
newRange.setStart(afterNode, 0);
|
| 198 |
-
} else {
|
| 199 |
-
newRange.setStartAfter(spaceNode);
|
| 200 |
-
}
|
| 201 |
-
newRange.collapse(true);
|
| 202 |
-
selection.removeAllRanges();
|
| 203 |
-
selection.addRange(newRange);
|
| 204 |
-
|
| 205 |
-
const newText = getTextContent(ref.current);
|
| 206 |
-
setPrompt(newText);
|
| 207 |
-
setShowMentionDropdown(false);
|
| 208 |
-
setMentionSearch("");
|
| 209 |
-
}
|
| 210 |
-
};
|
| 211 |
-
|
| 212 |
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
| 213 |
-
if (!prompt || prompt.trim() === "") return;
|
| 214 |
-
|
| 215 |
-
if (e.key === "Enter" && !e.shiftKey) {
|
| 216 |
-
e.preventDefault();
|
| 217 |
-
const promptWithIds = extractPromptWithIds();
|
| 218 |
-
setPrompt(promptWithIds);
|
| 219 |
-
onSubmit();
|
| 220 |
-
|
| 221 |
-
if (ref.current) {
|
| 222 |
-
ref.current.innerHTML = "";
|
| 223 |
-
}
|
| 224 |
-
setPrompt("");
|
| 225 |
-
setShowMentionDropdown(false);
|
| 226 |
-
} else if (e.key === "Escape") {
|
| 227 |
-
setShowMentionDropdown(false);
|
| 228 |
-
}
|
| 229 |
-
};
|
| 230 |
-
|
| 231 |
-
useEffect(() => {
|
| 232 |
-
if (ref.current && prompt === "" && ref.current.innerHTML !== "") {
|
| 233 |
-
ref.current.innerHTML = "";
|
| 234 |
-
}
|
| 235 |
-
}, [prompt]);
|
| 236 |
-
|
| 237 |
-
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
|
| 238 |
-
e.preventDefault();
|
| 239 |
-
const text = e.clipboardData.getData("text/plain");
|
| 240 |
-
document.execCommand("insertText", false, text);
|
| 241 |
-
};
|
| 242 |
-
|
| 243 |
-
return (
|
| 244 |
-
<div className="relative">
|
| 245 |
-
<div
|
| 246 |
-
id="prompt-input"
|
| 247 |
-
ref={ref}
|
| 248 |
-
contentEditable
|
| 249 |
-
className="pb-2 min-h-10 max-h-[130px] overflow-y-auto w-full h-full resize-none outline-none text-primary text-sm bg-transparent empty:before:block empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground empty:before:pointer-events-none"
|
| 250 |
-
data-placeholder={
|
| 251 |
-
redesignMdUrl
|
| 252 |
-
? `I'll redesign ${redesignMdUrl}, want to add something?`
|
| 253 |
-
: files && files.length > 0
|
| 254 |
-
? "Ask me anything. Type @ to mention a file..."
|
| 255 |
-
: "Ask me anything..."
|
| 256 |
-
}
|
| 257 |
-
onInput={handleInput}
|
| 258 |
-
onKeyDown={handleKeyDown}
|
| 259 |
-
onPaste={handlePaste}
|
| 260 |
-
suppressContentEditableWarning
|
| 261 |
-
></div>
|
| 262 |
-
{showMentionDropdown && (
|
| 263 |
-
<div
|
| 264 |
-
ref={dropdownRef}
|
| 265 |
-
className="absolute bottom-full mb-2 left-0 z-50 bg-background border border-border rounded-lg shadow-lg min-w-[250px] animate-in fade-in slide-in-from-bottom-2 duration-200"
|
| 266 |
-
>
|
| 267 |
-
<div className="text-xs text-muted-foreground/60 px-2 py-2">
|
| 268 |
-
{results?.length > 0 && (
|
| 269 |
-
<ul>
|
| 270 |
-
{results.map((file) => (
|
| 271 |
-
<MentionResult
|
| 272 |
-
key={file.path}
|
| 273 |
-
file={file}
|
| 274 |
-
onSelect={() => insertMention(file.path)}
|
| 275 |
-
/>
|
| 276 |
-
))}
|
| 277 |
-
</ul>
|
| 278 |
-
)}
|
| 279 |
-
</div>
|
| 280 |
-
</div>
|
| 281 |
-
)}
|
| 282 |
-
</div>
|
| 283 |
-
);
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
export const getFileIcon = (filePath: string, size = "size-3.5") => {
|
| 287 |
-
if (filePath.endsWith(".css")) {
|
| 288 |
-
return <Braces className={size} />;
|
| 289 |
-
} else if (filePath.endsWith(".js")) {
|
| 290 |
-
return <FileCode className={size} />;
|
| 291 |
-
} else if (filePath.endsWith(".json")) {
|
| 292 |
-
return <Braces className={size} />;
|
| 293 |
-
} else {
|
| 294 |
-
return <FileText className={size} />;
|
| 295 |
-
}
|
| 296 |
-
};
|
| 297 |
-
|
| 298 |
-
function MentionResult({
|
| 299 |
-
file,
|
| 300 |
-
onSelect,
|
| 301 |
-
}: {
|
| 302 |
-
file: File;
|
| 303 |
-
onSelect: () => void;
|
| 304 |
-
}) {
|
| 305 |
-
return (
|
| 306 |
-
<li
|
| 307 |
-
className="flex items-center justify-start gap-2 transition-all duration-200 hover:bg-linear-to-r from-indigo-500/40 to-indigo-500/5 text-primary font-medium rounded-lg px-2 py-2 cursor-pointer select-none"
|
| 308 |
-
onClick={onSelect}
|
| 309 |
-
>
|
| 310 |
-
{getFileIcon(file.path, "size-3")}
|
| 311 |
-
{file.path}
|
| 312 |
-
</li>
|
| 313 |
-
);
|
| 314 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ask-ai/models.tsx
DELETED
|
@@ -1,211 +0,0 @@
|
|
| 1 |
-
import {
|
| 2 |
-
BrainIcon,
|
| 3 |
-
ChevronDown,
|
| 4 |
-
DollarSign,
|
| 5 |
-
StarsIcon,
|
| 6 |
-
Zap,
|
| 7 |
-
} from "lucide-react";
|
| 8 |
-
import { useMemo, useState } from "react";
|
| 9 |
-
|
| 10 |
-
import {
|
| 11 |
-
Popover,
|
| 12 |
-
PopoverContent,
|
| 13 |
-
PopoverTrigger,
|
| 14 |
-
} from "@/components/ui/popover";
|
| 15 |
-
import { Button } from "@/components/ui/button";
|
| 16 |
-
import { cn } from "@/lib/utils";
|
| 17 |
-
import { ProviderType } from "@/lib/type";
|
| 18 |
-
import { MODELS } from "@/lib/providers";
|
| 19 |
-
import {
|
| 20 |
-
Select,
|
| 21 |
-
SelectContent,
|
| 22 |
-
SelectGroup,
|
| 23 |
-
SelectItem,
|
| 24 |
-
SelectLabel,
|
| 25 |
-
SelectTrigger,
|
| 26 |
-
SelectValue,
|
| 27 |
-
} from "@/components/ui/select";
|
| 28 |
-
|
| 29 |
-
export function Models({
|
| 30 |
-
model,
|
| 31 |
-
setModel,
|
| 32 |
-
provider,
|
| 33 |
-
setProvider,
|
| 34 |
-
}: {
|
| 35 |
-
model: string;
|
| 36 |
-
setModel: (model: string) => void;
|
| 37 |
-
provider: ProviderType;
|
| 38 |
-
setProvider: (provider: ProviderType) => void;
|
| 39 |
-
}) {
|
| 40 |
-
const [open, setOpen] = useState(false);
|
| 41 |
-
|
| 42 |
-
const formattedModels = useMemo(() => {
|
| 43 |
-
const lists: ((typeof MODELS)[0] | { isCategory: true; name: string })[] =
|
| 44 |
-
[];
|
| 45 |
-
const keys = new Set<string>();
|
| 46 |
-
MODELS.forEach((model) => {
|
| 47 |
-
if (!keys.has(model.companyName)) {
|
| 48 |
-
lists.push({
|
| 49 |
-
isCategory: true,
|
| 50 |
-
name: model.companyName,
|
| 51 |
-
logo: model.logo,
|
| 52 |
-
});
|
| 53 |
-
keys.add(model.companyName);
|
| 54 |
-
}
|
| 55 |
-
lists.push(model);
|
| 56 |
-
});
|
| 57 |
-
return lists;
|
| 58 |
-
}, []);
|
| 59 |
-
|
| 60 |
-
return (
|
| 61 |
-
<Popover open={open} onOpenChange={setOpen}>
|
| 62 |
-
<PopoverTrigger asChild>
|
| 63 |
-
<Button
|
| 64 |
-
id="tour-model-section"
|
| 65 |
-
variant={open ? "default" : "bordered"}
|
| 66 |
-
size="xs"
|
| 67 |
-
className="flex items-center gap-1 rounded-full! px-2.5!"
|
| 68 |
-
>
|
| 69 |
-
<span className="max-w-48 truncate">
|
| 70 |
-
{model.split("/").pop()?.toLowerCase()}
|
| 71 |
-
</span>
|
| 72 |
-
<ChevronDown className="size-3" />
|
| 73 |
-
</Button>
|
| 74 |
-
</PopoverTrigger>
|
| 75 |
-
<PopoverContent className="translate-x-6 rounded-2xl! p-0! bg-accent! border-border-muted! min-w-fit text-center overflow-hidden">
|
| 76 |
-
<header className="bg-linear-to-b from-indigo-500/25 dark:from-indigo-500/40 to-accent p-6">
|
| 77 |
-
<div className="flex items-center justify-center -space-x-4 mb-3">
|
| 78 |
-
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 79 |
-
💬
|
| 80 |
-
</div>
|
| 81 |
-
<div className="size-11 rounded-full bg-yellow-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
| 82 |
-
🧠
|
| 83 |
-
</div>
|
| 84 |
-
<div className="size-9 rounded-full bg-green-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
| 85 |
-
🤖
|
| 86 |
-
</div>
|
| 87 |
-
</div>
|
| 88 |
-
<p className="text-xl font-semibold text-primary">
|
| 89 |
-
Choose your AI model
|
| 90 |
-
</p>
|
| 91 |
-
<p className="text-sm text-muted-foreground mt-1.5">
|
| 92 |
-
Select the AI model that best fits your needs.
|
| 93 |
-
</p>
|
| 94 |
-
</header>
|
| 95 |
-
<main className="space-y-4 px-6 pb-6">
|
| 96 |
-
<div>
|
| 97 |
-
<Select defaultValue={model} onValueChange={setModel}>
|
| 98 |
-
<SelectTrigger className="w-full">
|
| 99 |
-
<SelectValue placeholder="Select a model" />
|
| 100 |
-
</SelectTrigger>
|
| 101 |
-
<SelectContent>
|
| 102 |
-
<SelectGroup>
|
| 103 |
-
{formattedModels.map(
|
| 104 |
-
(
|
| 105 |
-
item:
|
| 106 |
-
| (typeof MODELS)[0]
|
| 107 |
-
| { isCategory: true; name: string }
|
| 108 |
-
) => {
|
| 109 |
-
if ("isCategory" in item) {
|
| 110 |
-
return (
|
| 111 |
-
<SelectLabel
|
| 112 |
-
key={item.name}
|
| 113 |
-
className="flex items-center gap-1"
|
| 114 |
-
>
|
| 115 |
-
{item.name}
|
| 116 |
-
</SelectLabel>
|
| 117 |
-
);
|
| 118 |
-
}
|
| 119 |
-
const {
|
| 120 |
-
value,
|
| 121 |
-
label,
|
| 122 |
-
isNew = false,
|
| 123 |
-
isBestSeller = false,
|
| 124 |
-
} = item;
|
| 125 |
-
return (
|
| 126 |
-
<SelectItem key={value} value={value} className="">
|
| 127 |
-
{value.split("/").pop() || label}
|
| 128 |
-
{isNew && (
|
| 129 |
-
<span className="text-xs bg-indigo-500 dark:bg-indigo-500/20 text-primary-foreground dark:text-indigo-500 rounded-full px-1.5 py-0.5">
|
| 130 |
-
New
|
| 131 |
-
</span>
|
| 132 |
-
)}
|
| 133 |
-
{isBestSeller && (
|
| 134 |
-
<StarsIcon className="size-3.5 text-yellow-500 fill-yellow-500" />
|
| 135 |
-
)}
|
| 136 |
-
</SelectItem>
|
| 137 |
-
);
|
| 138 |
-
}
|
| 139 |
-
)}
|
| 140 |
-
</SelectGroup>
|
| 141 |
-
</SelectContent>
|
| 142 |
-
</Select>
|
| 143 |
-
</div>
|
| 144 |
-
<div>
|
| 145 |
-
<p className="text-sm text-muted-foreground mb-2">Provider mode:</p>
|
| 146 |
-
<div
|
| 147 |
-
role="radiogroup"
|
| 148 |
-
aria-label="Provider mode"
|
| 149 |
-
className="flex w-fit items-center gap-1.5"
|
| 150 |
-
>
|
| 151 |
-
{(
|
| 152 |
-
[
|
| 153 |
-
{ value: "cheapest", icon: DollarSign, color: "emerald" },
|
| 154 |
-
{
|
| 155 |
-
value: "auto",
|
| 156 |
-
icon: BrainIcon,
|
| 157 |
-
color: "indigo",
|
| 158 |
-
name: "Smartest",
|
| 159 |
-
},
|
| 160 |
-
{ value: "fastest", icon: Zap, color: "amber" },
|
| 161 |
-
] as const
|
| 162 |
-
).map(
|
| 163 |
-
({
|
| 164 |
-
value,
|
| 165 |
-
icon: Icon,
|
| 166 |
-
color,
|
| 167 |
-
name,
|
| 168 |
-
}: {
|
| 169 |
-
value: string;
|
| 170 |
-
icon: React.ElementType;
|
| 171 |
-
color: string;
|
| 172 |
-
name?: string;
|
| 173 |
-
}) => (
|
| 174 |
-
<div
|
| 175 |
-
key={value}
|
| 176 |
-
role="radio"
|
| 177 |
-
aria-checked={provider === value}
|
| 178 |
-
tabIndex={0}
|
| 179 |
-
onClick={() => setProvider(value)}
|
| 180 |
-
onKeyDown={(e) => {
|
| 181 |
-
if (e.key === "Enter" || e.key === " ") {
|
| 182 |
-
e.preventDefault();
|
| 183 |
-
setProvider(value);
|
| 184 |
-
}
|
| 185 |
-
}}
|
| 186 |
-
className={cn(
|
| 187 |
-
"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",
|
| 188 |
-
"hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
| 189 |
-
provider === value && [
|
| 190 |
-
"bg-transparent",
|
| 191 |
-
color === "emerald" &&
|
| 192 |
-
"[&_svg]:fill-emerald-500 [&_svg]:stroke-emerald-500 bg-emerald-500/10! border-emerald-500/10! text-emerald-500!",
|
| 193 |
-
color === "indigo" &&
|
| 194 |
-
"[&_svg]:fill-indigo-500 [&_svg]:stroke-indigo-500 bg-indigo-500/10! border-indigo-500/10! text-indigo-500!",
|
| 195 |
-
color === "amber" &&
|
| 196 |
-
"[&_svg]:fill-amber-500 [&_svg]:stroke-amber-500 bg-amber-500/10! border-amber-500/10! text-amber-500!",
|
| 197 |
-
]
|
| 198 |
-
)}
|
| 199 |
-
>
|
| 200 |
-
<Icon className="size-3.5" />
|
| 201 |
-
{name ?? value.charAt(0).toUpperCase() + value.slice(1)}
|
| 202 |
-
</div>
|
| 203 |
-
)
|
| 204 |
-
)}
|
| 205 |
-
</div>
|
| 206 |
-
</div>
|
| 207 |
-
</main>
|
| 208 |
-
</PopoverContent>
|
| 209 |
-
</Popover>
|
| 210 |
-
);
|
| 211 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|