Spaces:
Running
Running
Update app/[namespace]/[repoId]/page.tsx
#556
by
Aileen-ai
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .env.example +0 -4
- .github/workflows/deploy-prod.yml +0 -77
- .gitignore +1 -7
- Dockerfile +6 -9
- MCP-SERVER.md +428 -0
- README.md +12 -7
- actions/mentions.ts +0 -31
- actions/projects.ts +0 -175
- app/(public)/layout.tsx +4 -3
- app/(public)/page.tsx +3 -23
- app/(public)/signin/page.tsx +0 -21
- app/[namespace]/[repoId]/page.tsx +43 -0
- app/[owner]/[repoId]/page.tsx +0 -35
- app/actions/auth.ts +18 -0
- app/actions/projects.ts +47 -0
- app/api/ask/route.ts +346 -135
- app/api/auth/[...nextauth]/route.ts +0 -6
- app/api/auth/login-url/route.ts +21 -0
- app/api/auth/logout/route.ts +25 -0
- app/api/auth/route.ts +86 -0
- app/api/healthcheck/route.ts +0 -5
- app/api/mcp/route.ts +435 -0
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +240 -0
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts +107 -0
- app/api/{projects → me/projects/[namespace]}/[repoId]/download/route.ts +55 -21
- app/api/me/projects/[namespace]/[repoId]/images/route.ts +123 -0
- app/api/me/projects/[namespace]/[repoId]/route.ts +207 -0
- app/api/me/projects/[namespace]/[repoId]/save/route.ts +72 -0
- app/api/me/projects/[namespace]/[repoId]/update/route.ts +141 -0
- app/api/me/projects/route.ts +121 -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 -92
- app/api/projects/[repoId]/route.ts +0 -104
- app/api/projects/route.ts +0 -145
- app/api/{redesign → re-design}/route.ts +14 -16
- app/auth/callback/page.tsx +97 -0
- app/auth/page.tsx +28 -0
- app/layout.tsx +69 -32
- app/new/page.tsx +10 -14
- app/not-found.tsx +0 -17
- app/sitemap.ts +28 -0
- {app → assets}/globals.css +244 -32
- assets/hf-logo.svg +0 -7
- assets/logo.svg +316 -0
- assets/pro.svg +0 -10
- chart/Chart.yaml +0 -5
- chart/env/prod.yaml +0 -59
- chart/templates/_helpers.tpl +0 -22
.env.example
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
AUTH_HUGGINGFACE_ID=
|
| 2 |
-
AUTH_HUGGINGFACE_SECRET=
|
| 3 |
-
NEXTAUTH_URL=http://localhost:3001
|
| 4 |
-
AUTH_SECRET=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.github/workflows/deploy-prod.yml
DELETED
|
@@ -1,77 +0,0 @@
|
|
| 1 |
-
name: Deploy to k8s
|
| 2 |
-
on:
|
| 3 |
-
# run this workflow manually from the Actions tab
|
| 4 |
-
workflow_dispatch:
|
| 5 |
-
|
| 6 |
-
jobs:
|
| 7 |
-
build-and-publish:
|
| 8 |
-
runs-on:
|
| 9 |
-
group: cpu-high
|
| 10 |
-
steps:
|
| 11 |
-
- name: Checkout
|
| 12 |
-
uses: actions/checkout@v4
|
| 13 |
-
|
| 14 |
-
- name: Login to Registry
|
| 15 |
-
uses: docker/login-action@v3
|
| 16 |
-
with:
|
| 17 |
-
registry: registry.internal.huggingface.tech
|
| 18 |
-
username: ${{ secrets.DOCKER_INTERNAL_USERNAME }}
|
| 19 |
-
password: ${{ secrets.DOCKER_INTERNAL_PASSWORD }}
|
| 20 |
-
|
| 21 |
-
- name: Docker metadata
|
| 22 |
-
id: meta
|
| 23 |
-
uses: docker/metadata-action@v5
|
| 24 |
-
with:
|
| 25 |
-
images: |
|
| 26 |
-
registry.internal.huggingface.tech/deepsite/deepsite
|
| 27 |
-
tags: |
|
| 28 |
-
type=raw,value=latest,enable={{is_default_branch}}
|
| 29 |
-
type=sha,enable=true,prefix=sha-,format=short,sha-len=8
|
| 30 |
-
|
| 31 |
-
- name: Set up Docker Buildx
|
| 32 |
-
uses: docker/setup-buildx-action@v3
|
| 33 |
-
|
| 34 |
-
- name: Inject slug/short variables
|
| 35 |
-
uses: rlespinasse/github-slug-action@v4
|
| 36 |
-
|
| 37 |
-
- name: Build and Publish image
|
| 38 |
-
uses: docker/build-push-action@v5
|
| 39 |
-
with:
|
| 40 |
-
context: .
|
| 41 |
-
file: Dockerfile
|
| 42 |
-
push: ${{ github.event_name != 'pull_request' }}
|
| 43 |
-
tags: ${{ steps.meta.outputs.tags }}
|
| 44 |
-
labels: ${{ steps.meta.outputs.labels }}
|
| 45 |
-
platforms: linux/amd64
|
| 46 |
-
cache-to: type=gha,mode=max,scope=amd64
|
| 47 |
-
cache-from: type=gha,scope=amd64
|
| 48 |
-
provenance: false
|
| 49 |
-
|
| 50 |
-
deploy:
|
| 51 |
-
name: Deploy on prod
|
| 52 |
-
runs-on: ubuntu-latest
|
| 53 |
-
needs: ["build-and-publish"]
|
| 54 |
-
steps:
|
| 55 |
-
- name: Inject slug/short variables
|
| 56 |
-
uses: rlespinasse/github-slug-action@v4
|
| 57 |
-
|
| 58 |
-
- name: Gen values
|
| 59 |
-
run: |
|
| 60 |
-
VALUES=$(cat <<-END
|
| 61 |
-
image:
|
| 62 |
-
tag: "sha-${{ env.GITHUB_SHA_SHORT }}"
|
| 63 |
-
END
|
| 64 |
-
)
|
| 65 |
-
echo "VALUES=$(echo "$VALUES" | yq -o=json | jq tostring)" >> $GITHUB_ENV
|
| 66 |
-
|
| 67 |
-
- name: Deploy on infra-deployments
|
| 68 |
-
uses: the-actions-org/workflow-dispatch@v2
|
| 69 |
-
with:
|
| 70 |
-
workflow: Update application single value
|
| 71 |
-
repo: huggingface/infra-deployments
|
| 72 |
-
wait-for-completion: true
|
| 73 |
-
wait-for-completion-interval: 10s
|
| 74 |
-
display-workflow-run-url-interval: 10s
|
| 75 |
-
ref: refs/heads/main
|
| 76 |
-
token: ${{ secrets.GIT_TOKEN_INFRA_DEPLOYMENT }}
|
| 77 |
-
inputs: '{"path": "hub/deepsite/deepsite.yaml", "value": ${{ env.VALUES }}, "url": "${{ github.event.head_commit.url }}"}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.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
|
|
@@ -39,9 +39,3 @@ yarn-error.log*
|
|
| 39 |
# typescript
|
| 40 |
*.tsbuildinfo
|
| 41 |
next-env.d.ts
|
| 42 |
-
|
| 43 |
-
.idea
|
| 44 |
-
|
| 45 |
-
# binary assets (hosted on CDN)
|
| 46 |
-
assets/assistant.jpg
|
| 47 |
-
.gitattributes
|
|
|
|
| 31 |
.pnpm-debug.log*
|
| 32 |
|
| 33 |
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
|
| 36 |
# vercel
|
| 37 |
.vercel
|
|
|
|
| 39 |
# typescript
|
| 40 |
*.tsbuildinfo
|
| 41 |
next-env.d.ts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,22 +1,19 @@
|
|
| 1 |
FROM node:20-alpine
|
| 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
|
| 10 |
-
COPY --chown=1000 package.json
|
| 11 |
|
| 12 |
# Copy the rest of the application files to the container
|
| 13 |
COPY --chown=1000 . .
|
| 14 |
|
| 15 |
-
RUN
|
| 16 |
-
RUN
|
| 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:20-alpine
|
| 2 |
USER root
|
| 3 |
|
|
|
|
|
|
|
|
|
|
| 4 |
USER 1000
|
| 5 |
WORKDIR /usr/src/app
|
| 6 |
+
# Copy package.json and package-lock.json to the container
|
| 7 |
+
COPY --chown=1000 package.json package-lock.json ./
|
| 8 |
|
| 9 |
# Copy the rest of the application files to the container
|
| 10 |
COPY --chown=1000 . .
|
| 11 |
|
| 12 |
+
RUN npm install
|
| 13 |
+
RUN npm run build
|
| 14 |
|
| 15 |
# Expose the application port (assuming your app runs on port 3000)
|
| 16 |
+
EXPOSE 3000
|
| 17 |
|
| 18 |
# Start the application
|
| 19 |
+
CMD ["npm", "start"]
|
MCP-SERVER.md
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DeepSite MCP Server
|
| 2 |
+
|
| 3 |
+
DeepSite is now available as an MCP (Model Context Protocol) server, enabling AI assistants like Claude to create websites directly using natural language.
|
| 4 |
+
|
| 5 |
+
## Two Ways to Use DeepSite MCP
|
| 6 |
+
|
| 7 |
+
**Quick Comparison:**
|
| 8 |
+
|
| 9 |
+
| Feature | Option 1: HTTP Server | Option 2: Local Server |
|
| 10 |
+
|---------|----------------------|------------------------|
|
| 11 |
+
| **Setup Difficulty** | ✅ Easy (just config) | ⚠️ Requires installation |
|
| 12 |
+
| **Authentication** | HF Token in config header | HF Token or session cookie in env |
|
| 13 |
+
| **Best For** | Most users | Developers, custom modifications |
|
| 14 |
+
| **Maintenance** | ✅ Always up-to-date | Need to rebuild for updates |
|
| 15 |
+
|
| 16 |
+
**Recommendation:** Use Option 1 (HTTP Server) unless you need to modify the MCP server code.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
### 🌐 Option 1: HTTP Server (Recommended)
|
| 21 |
+
|
| 22 |
+
**No installation required!** Use DeepSite's hosted MCP server.
|
| 23 |
+
|
| 24 |
+
#### Setup for Claude Desktop
|
| 25 |
+
|
| 26 |
+
Add to your Claude Desktop configuration file:
|
| 27 |
+
|
| 28 |
+
**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 29 |
+
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
| 30 |
+
|
| 31 |
+
```json
|
| 32 |
+
{
|
| 33 |
+
"mcpServers": {
|
| 34 |
+
"deepsite": {
|
| 35 |
+
"url": "https://huggingface.co/deepsite/api/mcp",
|
| 36 |
+
"transport": {
|
| 37 |
+
"type": "sse"
|
| 38 |
+
},
|
| 39 |
+
"headers": {
|
| 40 |
+
"Authorization": "Bearer hf_your_token_here"
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
**Getting Your Hugging Face Token:**
|
| 48 |
+
|
| 49 |
+
1. Go to https://huggingface.co/settings/tokens
|
| 50 |
+
2. Create a new token with `write` access
|
| 51 |
+
3. Copy the token
|
| 52 |
+
4. Add it to the `Authorization` header in your config (recommended for security)
|
| 53 |
+
5. Alternatively, you can pass it as the `hf_token` parameter when using the tool
|
| 54 |
+
|
| 55 |
+
**⚠️ Security Recommendation:** Use the `Authorization` header in your config instead of passing the token in chat. This keeps your token secure and out of conversation history.
|
| 56 |
+
|
| 57 |
+
#### Example Usage with Claude
|
| 58 |
+
|
| 59 |
+
> "Create a portfolio website using DeepSite. Include a hero section, about section, and contact form."
|
| 60 |
+
|
| 61 |
+
Claude will automatically:
|
| 62 |
+
1. Use the `create_project` tool
|
| 63 |
+
2. Authenticate using the token from your config
|
| 64 |
+
3. Create the website on Hugging Face Spaces
|
| 65 |
+
4. Return the URLs to access your new site
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
### 💻 Option 2: Local Server
|
| 70 |
+
|
| 71 |
+
Run the MCP server locally for more control or offline use.
|
| 72 |
+
|
| 73 |
+
> **Note:** Most users should use Option 1 (HTTP Server) instead. Option 2 is only needed if you want to run the MCP server locally or modify its behavior.
|
| 74 |
+
|
| 75 |
+
#### Installation
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
cd mcp-server
|
| 79 |
+
npm install
|
| 80 |
+
npm run build
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
#### Setup for Claude Desktop
|
| 84 |
+
|
| 85 |
+
**Method A: Using HF Token (Recommended)**
|
| 86 |
+
|
| 87 |
+
```json
|
| 88 |
+
{
|
| 89 |
+
"mcpServers": {
|
| 90 |
+
"deepsite-local": {
|
| 91 |
+
"command": "node",
|
| 92 |
+
"args": ["/absolute/path/to/deepsite-v3/mcp-server/dist/index.js"],
|
| 93 |
+
"env": {
|
| 94 |
+
"HF_TOKEN": "hf_your_token_here",
|
| 95 |
+
"DEEPSITE_API_URL": "https://huggingface.co/deepsite"
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
**Method B: Using Session Cookie (Alternative)**
|
| 103 |
+
|
| 104 |
+
```json
|
| 105 |
+
{
|
| 106 |
+
"mcpServers": {
|
| 107 |
+
"deepsite-local": {
|
| 108 |
+
"command": "node",
|
| 109 |
+
"args": ["/absolute/path/to/deepsite-v3/mcp-server/dist/index.js"],
|
| 110 |
+
"env": {
|
| 111 |
+
"DEEPSITE_AUTH_COOKIE": "your-session-cookie",
|
| 112 |
+
"DEEPSITE_API_URL": "https://huggingface.co/deepsite"
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
**Getting Your Session Cookie (Method B only):**
|
| 120 |
+
|
| 121 |
+
1. Log in to https://huggingface.co/deepsite
|
| 122 |
+
2. Open Developer Tools (F12)
|
| 123 |
+
3. Go to Application → Cookies
|
| 124 |
+
4. Copy the session cookie value
|
| 125 |
+
5. Set as `DEEPSITE_AUTH_COOKIE` in the config
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## Available Tools
|
| 130 |
+
|
| 131 |
+
### `create_project`
|
| 132 |
+
|
| 133 |
+
Creates a new DeepSite project with HTML/CSS/JS files.
|
| 134 |
+
|
| 135 |
+
**Parameters:**
|
| 136 |
+
|
| 137 |
+
| Parameter | Type | Required | Description |
|
| 138 |
+
|-----------|------|----------|-------------|
|
| 139 |
+
| `title` | string | No | Project title (defaults to "DeepSite Project") |
|
| 140 |
+
| `pages` | array | Yes | Array of file objects with `path` and `html` |
|
| 141 |
+
| `prompt` | string | No | Commit message/description |
|
| 142 |
+
| `hf_token` | string | No* | Hugging Face API token (*optional if provided via Authorization header in config) |
|
| 143 |
+
|
| 144 |
+
**Page Object:**
|
| 145 |
+
```typescript
|
| 146 |
+
{
|
| 147 |
+
path: string; // e.g., "index.html", "styles.css", "script.js"
|
| 148 |
+
html: string; // File content
|
| 149 |
+
}
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
**Returns:**
|
| 153 |
+
```json
|
| 154 |
+
{
|
| 155 |
+
"success": true,
|
| 156 |
+
"message": "Project created successfully!",
|
| 157 |
+
"projectUrl": "https://huggingface.co/deepsite/username/project-name",
|
| 158 |
+
"spaceUrl": "https://huggingface.co/spaces/username/project-name",
|
| 159 |
+
"liveUrl": "https://username-project-name.hf.space",
|
| 160 |
+
"spaceId": "username/project-name",
|
| 161 |
+
"projectId": "space-id",
|
| 162 |
+
"files": ["index.html", "styles.css"]
|
| 163 |
+
}
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
|
| 168 |
+
## Example Prompts for Claude
|
| 169 |
+
|
| 170 |
+
### Simple Landing Page
|
| 171 |
+
> "Create a modern landing page for my SaaS product using DeepSite. Include a hero section with CTA, features grid, and footer. Use gradient background."
|
| 172 |
+
|
| 173 |
+
### Portfolio Website
|
| 174 |
+
> "Build a portfolio website with DeepSite. I need:
|
| 175 |
+
> - Hero section with my name and photo
|
| 176 |
+
> - Projects gallery with 3 sample projects
|
| 177 |
+
> - Skills section with tech stack
|
| 178 |
+
> - Contact form
|
| 179 |
+
> Use dark mode with accent colors."
|
| 180 |
+
|
| 181 |
+
### Blog Homepage
|
| 182 |
+
> "Create a blog homepage using DeepSite. Include:
|
| 183 |
+
> - Header with navigation
|
| 184 |
+
> - Featured post section
|
| 185 |
+
> - Grid of recent posts (3 cards)
|
| 186 |
+
> - Sidebar with categories
|
| 187 |
+
> - Footer with social links
|
| 188 |
+
> Clean, minimal design."
|
| 189 |
+
|
| 190 |
+
### Interactive Dashboard
|
| 191 |
+
> "Make an analytics dashboard with DeepSite:
|
| 192 |
+
> - Sidebar navigation
|
| 193 |
+
> - 4 metric cards at top
|
| 194 |
+
> - 2 chart placeholders
|
| 195 |
+
> - Data table
|
| 196 |
+
> - Modern, professional UI with charts.css"
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
## Direct API Usage
|
| 201 |
+
|
| 202 |
+
You can also call the HTTP endpoint directly:
|
| 203 |
+
|
| 204 |
+
### Using Authorization Header (Recommended)
|
| 205 |
+
|
| 206 |
+
```bash
|
| 207 |
+
curl -X POST https://huggingface.co/deepsite/api/mcp \
|
| 208 |
+
-H "Content-Type: application/json" \
|
| 209 |
+
-H "Authorization: Bearer hf_your_token_here" \
|
| 210 |
+
-d '{
|
| 211 |
+
"jsonrpc": "2.0",
|
| 212 |
+
"id": 1,
|
| 213 |
+
"method": "tools/call",
|
| 214 |
+
"params": {
|
| 215 |
+
"name": "create_project",
|
| 216 |
+
"arguments": {
|
| 217 |
+
"title": "My Website",
|
| 218 |
+
"pages": [
|
| 219 |
+
{
|
| 220 |
+
"path": "index.html",
|
| 221 |
+
"html": "<!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello World!</h1></body></html>"
|
| 222 |
+
}
|
| 223 |
+
]
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
}'
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
### Using Token Parameter (Fallback)
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
curl -X POST https://huggingface.co/deepsite/api/mcp \
|
| 233 |
+
-H "Content-Type: application/json" \
|
| 234 |
+
-d '{
|
| 235 |
+
"jsonrpc": "2.0",
|
| 236 |
+
"id": 1,
|
| 237 |
+
"method": "tools/call",
|
| 238 |
+
"params": {
|
| 239 |
+
"name": "create_project",
|
| 240 |
+
"arguments": {
|
| 241 |
+
"title": "My Website",
|
| 242 |
+
"pages": [
|
| 243 |
+
{
|
| 244 |
+
"path": "index.html",
|
| 245 |
+
"html": "<!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello World!</h1></body></html>"
|
| 246 |
+
}
|
| 247 |
+
],
|
| 248 |
+
"hf_token": "hf_xxxxx"
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
}'
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
### List Available Tools
|
| 255 |
+
|
| 256 |
+
```bash
|
| 257 |
+
curl -X POST https://huggingface.co/deepsite/api/mcp \
|
| 258 |
+
-H "Content-Type: application/json" \
|
| 259 |
+
-d '{
|
| 260 |
+
"jsonrpc": "2.0",
|
| 261 |
+
"id": 1,
|
| 262 |
+
"method": "tools/list",
|
| 263 |
+
"params": {}
|
| 264 |
+
}'
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
## Testing
|
| 270 |
+
|
| 271 |
+
### Test Local Server
|
| 272 |
+
|
| 273 |
+
```bash
|
| 274 |
+
cd mcp-server
|
| 275 |
+
./test.sh
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
### Test HTTP Server
|
| 279 |
+
|
| 280 |
+
```bash
|
| 281 |
+
curl -X POST https://huggingface.co/deepsite/api/mcp \
|
| 282 |
+
-H "Content-Type: application/json" \
|
| 283 |
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
---
|
| 287 |
+
|
| 288 |
+
## Migration Guide: From Parameter to Header Auth
|
| 289 |
+
|
| 290 |
+
If you're currently passing the token as a parameter in your prompts, here's how to migrate to the more secure header-based authentication:
|
| 291 |
+
|
| 292 |
+
### Step 1: Update Your Config
|
| 293 |
+
|
| 294 |
+
Edit your Claude Desktop config file and add the `headers` section:
|
| 295 |
+
|
| 296 |
+
```json
|
| 297 |
+
{
|
| 298 |
+
"mcpServers": {
|
| 299 |
+
"deepsite": {
|
| 300 |
+
"url": "https://huggingface.co/deepsite/api/mcp",
|
| 301 |
+
"transport": {
|
| 302 |
+
"type": "sse"
|
| 303 |
+
},
|
| 304 |
+
"headers": {
|
| 305 |
+
"Authorization": "Bearer hf_your_actual_token_here"
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
```
|
| 311 |
+
|
| 312 |
+
### Step 2: Restart Claude Desktop
|
| 313 |
+
|
| 314 |
+
Completely quit and restart Claude Desktop for the changes to take effect.
|
| 315 |
+
|
| 316 |
+
### Step 3: Use Simpler Prompts
|
| 317 |
+
|
| 318 |
+
Now you can simply say:
|
| 319 |
+
> "Create a portfolio website with DeepSite"
|
| 320 |
+
|
| 321 |
+
Instead of:
|
| 322 |
+
> "Create a portfolio website with DeepSite using token `hf_xxxxx`"
|
| 323 |
+
|
| 324 |
+
Your token is automatically included in all requests via the header!
|
| 325 |
+
|
| 326 |
+
---
|
| 327 |
+
|
| 328 |
+
## Security Notes
|
| 329 |
+
|
| 330 |
+
### HTTP Server (Option 1)
|
| 331 |
+
- **✅ Recommended:** Store your HF token in the `Authorization` header in your Claude Desktop config
|
| 332 |
+
- The token is stored locally on your machine and never exposed in chat
|
| 333 |
+
- The token is sent with each request but only used to authenticate with Hugging Face API
|
| 334 |
+
- DeepSite does not store your token
|
| 335 |
+
- Use tokens with minimal required permissions (write access to spaces)
|
| 336 |
+
- You can revoke tokens anytime at https://huggingface.co/settings/tokens
|
| 337 |
+
- **⚠️ Fallback:** You can still pass the token as a parameter, but this is less secure as it appears in conversation history
|
| 338 |
+
|
| 339 |
+
### Local Server (Option 2)
|
| 340 |
+
- Use `HF_TOKEN` environment variable (same security as Option 1)
|
| 341 |
+
- Or use `DEEPSITE_AUTH_COOKIE` if you prefer session-based auth
|
| 342 |
+
- All authentication data stays on your local machine
|
| 343 |
+
- Better for development and testing
|
| 344 |
+
- No need for both HTTP Server and Local Server - choose one!
|
| 345 |
+
|
| 346 |
+
---
|
| 347 |
+
|
| 348 |
+
## Troubleshooting
|
| 349 |
+
|
| 350 |
+
### "Invalid Hugging Face token"
|
| 351 |
+
- Verify your token at https://huggingface.co/settings/tokens
|
| 352 |
+
- Ensure the token has write permissions
|
| 353 |
+
- Check that you copied the full token (starts with `hf_`)
|
| 354 |
+
|
| 355 |
+
### "At least one page is required"
|
| 356 |
+
- Make sure you're providing the `pages` array
|
| 357 |
+
- Each page must have both `path` and `html` properties
|
| 358 |
+
|
| 359 |
+
### "Failed to create project"
|
| 360 |
+
- Check your token permissions
|
| 361 |
+
- Ensure the project title doesn't conflict with existing spaces
|
| 362 |
+
- Verify your Hugging Face account is in good standing
|
| 363 |
+
|
| 364 |
+
### Claude doesn't see the tool
|
| 365 |
+
- Restart Claude Desktop after modifying the config
|
| 366 |
+
- Check that the JSON config is valid (no trailing commas)
|
| 367 |
+
- For HTTP: verify the URL is correct
|
| 368 |
+
- For local: check the absolute path to index.js
|
| 369 |
+
|
| 370 |
+
---
|
| 371 |
+
|
| 372 |
+
## Architecture
|
| 373 |
+
|
| 374 |
+
### HTTP Server Flow
|
| 375 |
+
```
|
| 376 |
+
Claude Desktop
|
| 377 |
+
↓
|
| 378 |
+
(HTTP Request)
|
| 379 |
+
↓
|
| 380 |
+
huggingface.co/deepsite/api/mcp
|
| 381 |
+
↓
|
| 382 |
+
Hugging Face API (with user's token)
|
| 383 |
+
↓
|
| 384 |
+
New Space Created
|
| 385 |
+
↓
|
| 386 |
+
URLs returned to Claude
|
| 387 |
+
```
|
| 388 |
+
|
| 389 |
+
### Local Server Flow
|
| 390 |
+
```
|
| 391 |
+
Claude Desktop
|
| 392 |
+
↓
|
| 393 |
+
(stdio transport)
|
| 394 |
+
↓
|
| 395 |
+
Local MCP Server
|
| 396 |
+
↓
|
| 397 |
+
(HTTP to DeepSite API)
|
| 398 |
+
↓
|
| 399 |
+
huggingface.co/deepsite/api/me/projects
|
| 400 |
+
↓
|
| 401 |
+
New Space Created
|
| 402 |
+
```
|
| 403 |
+
|
| 404 |
+
---
|
| 405 |
+
|
| 406 |
+
## Contributing
|
| 407 |
+
|
| 408 |
+
The MCP server implementation lives in:
|
| 409 |
+
- HTTP Server: `/app/api/mcp/route.ts`
|
| 410 |
+
- Local Server: `/mcp-server/index.ts`
|
| 411 |
+
|
| 412 |
+
Both use the same core DeepSite logic for creating projects - no duplication!
|
| 413 |
+
|
| 414 |
+
---
|
| 415 |
+
|
| 416 |
+
## License
|
| 417 |
+
|
| 418 |
+
MIT
|
| 419 |
+
|
| 420 |
+
---
|
| 421 |
+
|
| 422 |
+
## Resources
|
| 423 |
+
|
| 424 |
+
- [Model Context Protocol Spec](https://modelcontextprotocol.io/)
|
| 425 |
+
- [DeepSite Documentation](https://huggingface.co/deepsite)
|
| 426 |
+
- [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces)
|
| 427 |
+
- [Claude Desktop](https://claude.ai/desktop)
|
| 428 |
+
|
README.md
CHANGED
|
@@ -1,21 +1,26 @@
|
|
| 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 |
failure_strategy: rollback
|
| 11 |
-
short_description: Generate any application by Vibe Coding
|
| 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
|
|
|
|
| 19 |
---
|
| 20 |
|
| 21 |
# DeepSite 🐳
|
|
|
|
| 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 |
failure_strategy: rollback
|
| 11 |
+
short_description: Generate any application by Vibe Coding
|
| 12 |
models:
|
| 13 |
- deepseek-ai/DeepSeek-V3-0324
|
| 14 |
+
- deepseek-ai/DeepSeek-R1-0528
|
| 15 |
+
- deepseek-ai/DeepSeek-V3.1
|
| 16 |
+
- deepseek-ai/DeepSeek-V3.1-Terminus
|
| 17 |
+
- deepseek-ai/DeepSeek-V3.2-Exp
|
| 18 |
+
- Qwen/Qwen3-Coder-480B-A35B-Instruct
|
| 19 |
+
- moonshotai/Kimi-K2-Instruct
|
| 20 |
- moonshotai/Kimi-K2-Instruct-0905
|
| 21 |
+
- zai-org/GLM-4.6
|
| 22 |
+
- MiniMaxAI/MiniMax-M2
|
| 23 |
+
- moonshotai/Kimi-K2-Thinking
|
| 24 |
---
|
| 25 |
|
| 26 |
# DeepSite 🐳
|
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: session.user.username,
|
| 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,25 +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 |
-
import { Bento } from "@/components/public/bento";
|
| 6 |
|
| 7 |
-
export
|
| 8 |
-
|
| 9 |
-
export default async function Homepage() {
|
| 10 |
-
return (
|
| 11 |
-
<>
|
| 12 |
-
<section className="container mx-auto relative z-10">
|
| 13 |
-
<HeroHeader />
|
| 14 |
-
<div className="absolute inset-0 -z-10">
|
| 15 |
-
<AnimatedDotsBackground />
|
| 16 |
-
</div>
|
| 17 |
-
<div className="max-w-xl mx-auto px-6">
|
| 18 |
-
<AskAiLanding />
|
| 19 |
-
</div>
|
| 20 |
-
</section>
|
| 21 |
-
<UserProjects />
|
| 22 |
-
<Bento />
|
| 23 |
-
</>
|
| 24 |
-
);
|
| 25 |
}
|
|
|
|
| 1 |
+
import { MyProjects } from "@/components/my-projects";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
export default async function HomePage() {
|
| 4 |
+
return <MyProjects />;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
}
|
app/(public)/signin/page.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
import { LoginButtons } from "@/components/login/login-buttons";
|
| 2 |
-
|
| 3 |
-
export default async function SignInPage({
|
| 4 |
-
searchParams,
|
| 5 |
-
}: {
|
| 6 |
-
searchParams: Promise<{ callbackUrl: string }>;
|
| 7 |
-
}) {
|
| 8 |
-
const { callbackUrl } = await searchParams;
|
| 9 |
-
console.log(callbackUrl);
|
| 10 |
-
return (
|
| 11 |
-
<section className="min-h-screen font-sans">
|
| 12 |
-
<div className="px-6 py-16 max-w-5xl mx-auto text-center">
|
| 13 |
-
<h1 className="text-5xl font-bold mb-5">You shall not pass 🧙</h1>
|
| 14 |
-
<p className="text-lg text-muted-foreground mb-8">
|
| 15 |
-
You can't access this resource without being signed in.
|
| 16 |
-
</p>
|
| 17 |
-
<LoginButtons callbackUrl={callbackUrl ?? "/"} />
|
| 18 |
-
</div>
|
| 19 |
-
</section>
|
| 20 |
-
);
|
| 21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/[namespace]/[repoId]/page.tsx
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AppEditor } from "@/components/editor";
|
| 2 |
+
import { generateSEO } from "@/lib/seo";
|
| 3 |
+
import { Metadata } from "next";
|
| 4 |
+
|
| 5 |
+
// تعريف المستودع الافتراضي (سيتم استخدامه دائمًا إذا لم يُرسل اسم صحيح)
|
| 6 |
+
const DEFAULT_NAMESPACE = "Aileen-ai";
|
| 7 |
+
const DEFAULT_REPO = "Chekla";
|
| 8 |
+
|
| 9 |
+
export async function generateMetadata({
|
| 10 |
+
params,
|
| 11 |
+
}: {
|
| 12 |
+
params: Promise<{ namespace?: string; repoId?: string }>;
|
| 13 |
+
}): Promise<Metadata> {
|
| 14 |
+
const { namespace, repoId } = await params;
|
| 15 |
+
|
| 16 |
+
// استخدم القيم المرسلة أو القيم الافتراضية لتجنب الخطأ
|
| 17 |
+
const finalNamespace = namespace || DEFAULT_NAMESPACE;
|
| 18 |
+
const finalRepoId = repoId || DEFAULT_REPO;
|
| 19 |
+
const fullRepo = `${finalNamespace}/${finalRepoId}`;
|
| 20 |
+
|
| 21 |
+
return generateSEO({
|
| 22 |
+
title: `${fullRepo} - DeepSite Editor`,
|
| 23 |
+
description: `Edit and build ${fullRepo} with AI-powered tools on DeepSite. Create stunning websites with no code required.`,
|
| 24 |
+
path: `/${fullRepo}`,
|
| 25 |
+
noIndex: false,
|
| 26 |
+
});
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export default async function ProjectNamespacePage({
|
| 30 |
+
params,
|
| 31 |
+
}: {
|
| 32 |
+
params: Promise<{ namespace?: string; repoId?: string }>;
|
| 33 |
+
}) {
|
| 34 |
+
const { namespace, repoId } = await params;
|
| 35 |
+
|
| 36 |
+
// استخدام المستودع الافتراضي إذا لم تُرسل القيم الصحيحة
|
| 37 |
+
const finalNamespace = namespace || DEFAULT_NAMESPACE;
|
| 38 |
+
const finalRepoId = repoId || DEFAULT_REPO;
|
| 39 |
+
|
| 40 |
+
// الآن AppEditor يستخدم المستودع الكامل دائماً
|
| 41 |
+
// إذا GitHub غير متاح → يمكنه تجاوز الرفع داخلياً بدون أن يظهر الخطأ
|
| 42 |
+
return <AppEditor namespace={finalNamespace} repoId={finalRepoId} />;
|
| 43 |
+
}
|
app/[owner]/[repoId]/page.tsx
DELETED
|
@@ -1,35 +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 |
-
const { owner, repoId } = await params;
|
| 16 |
-
const { commit } = await searchParams;
|
| 17 |
-
if (!session) {
|
| 18 |
-
redirect(
|
| 19 |
-
`/api/auth/signin?callbackUrl=/${owner}/${repoId}${
|
| 20 |
-
commit ? `?commit=${commit}` : ""
|
| 21 |
-
}`
|
| 22 |
-
);
|
| 23 |
-
}
|
| 24 |
-
const datas = await getProject(`${owner}/${repoId}`, commit);
|
| 25 |
-
if (!datas?.project) {
|
| 26 |
-
return notFound();
|
| 27 |
-
}
|
| 28 |
-
return (
|
| 29 |
-
<AppEditor
|
| 30 |
-
project={datas.project}
|
| 31 |
-
files={datas.files ?? []}
|
| 32 |
-
isHistoryView={!!commit}
|
| 33 |
-
/>
|
| 34 |
-
);
|
| 35 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
"/deepsite/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,84 @@
|
|
|
|
|
|
|
|
| 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
|
| 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,139 +92,303 @@ 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 |
-
...
|
| 65 |
-
role:
|
| 66 |
-
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 |
-
|
| 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 |
-
|
| 100 |
-
|
| 101 |
-
|
| 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 |
-
errorMessage?.includes("exceeded your monthly included credits") ||
|
| 145 |
-
errorMessage?.includes("reached the free monthly usage limit")
|
| 146 |
-
) {
|
| 147 |
-
errorPayload = JSON.stringify({
|
| 148 |
-
messageError: errorMessage,
|
| 149 |
-
showProMessage: true,
|
| 150 |
-
isError: true,
|
| 151 |
-
});
|
| 152 |
-
} else {
|
| 153 |
-
errorPayload = JSON.stringify({
|
| 154 |
-
messageError: errorMessage,
|
| 155 |
-
isError: true,
|
| 156 |
-
});
|
| 157 |
-
}
|
| 158 |
-
await writer.write(encoder.encode(`\n\n__ERROR__:${errorPayload}`));
|
| 159 |
-
await writer.close();
|
| 160 |
-
} catch (closeError) {
|
| 161 |
-
console.error("Failed to send error message:", closeError);
|
| 162 |
-
try {
|
| 163 |
-
await writer.abort(error);
|
| 164 |
-
} catch (abortError) {
|
| 165 |
-
console.error("Failed to abort writer:", abortError);
|
| 166 |
-
}
|
| 167 |
}
|
| 168 |
}
|
| 169 |
-
};
|
| 170 |
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
})();
|
| 173 |
|
| 174 |
return response;
|
| 175 |
-
} catch (error) {
|
| 176 |
return NextResponse.json(
|
| 177 |
{
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
| 179 |
},
|
| 180 |
{ status: 500 }
|
| 181 |
);
|
| 182 |
}
|
| 183 |
}
|
|
|
|
|
|
| 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 |
+
FOLLOW_UP_SYSTEM_PROMPT,
|
| 10 |
+
FOLLOW_UP_SYSTEM_PROMPT_LIGHT,
|
| 11 |
+
INITIAL_SYSTEM_PROMPT,
|
| 12 |
+
INITIAL_SYSTEM_PROMPT_LIGHT,
|
| 13 |
+
MAX_REQUESTS_PER_IP,
|
| 14 |
+
PROMPT_FOR_PROJECT_NAME,
|
| 15 |
+
} from "@/lib/prompts";
|
| 16 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 17 |
+
import { Page } from "@/types";
|
| 18 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 19 |
+
import { getBestProvider } from "@/lib/best-provider";
|
| 20 |
|
| 21 |
+
const ipAddresses = new Map();
|
| 22 |
+
|
| 23 |
+
export async function POST(request: NextRequest) {
|
| 24 |
+
const authHeaders = await headers();
|
| 25 |
+
const tokenInHeaders = authHeaders.get("Authorization");
|
| 26 |
+
const userToken = tokenInHeaders ? tokenInHeaders.replace("Bearer ", "") : request.cookies.get(MY_TOKEN_KEY())?.value;
|
| 27 |
|
| 28 |
const body = await request.json();
|
| 29 |
+
const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
|
| 30 |
+
|
| 31 |
+
if (!model || (!prompt && !redesignMarkdown)) {
|
| 32 |
+
return NextResponse.json(
|
| 33 |
+
{ ok: false, error: "Missing required fields" },
|
| 34 |
+
{ status: 400 }
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const selectedModel = MODELS.find(
|
| 39 |
+
(m) => m.value === model || m.label === model
|
| 40 |
+
);
|
| 41 |
+
|
| 42 |
+
if (!selectedModel) {
|
| 43 |
+
return NextResponse.json(
|
| 44 |
+
{ ok: false, error: "Invalid model selected" },
|
| 45 |
+
{ status: 400 }
|
| 46 |
+
);
|
| 47 |
}
|
| 48 |
+
|
| 49 |
+
let token: string | null = null;
|
| 50 |
+
if (userToken) token = userToken;
|
| 51 |
+
let billTo: string | null = null;
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Handle local usage token, this bypass the need for a user token
|
| 55 |
+
* and allows local testing without authentication.
|
| 56 |
+
* This is useful for development and testing purposes.
|
| 57 |
+
*/
|
| 58 |
+
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
|
| 59 |
+
token = process.env.HF_TOKEN;
|
| 60 |
}
|
| 61 |
|
| 62 |
+
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
|
| 63 |
+
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 64 |
+
: authHeaders.get("x-forwarded-for");
|
| 65 |
+
|
| 66 |
+
if (!token || token === "null" || token === "") {
|
| 67 |
+
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 68 |
+
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 69 |
+
return NextResponse.json(
|
| 70 |
+
{
|
| 71 |
+
ok: false,
|
| 72 |
+
openLogin: true,
|
| 73 |
+
message: "Log In to continue using the service",
|
| 74 |
+
},
|
| 75 |
+
{ status: 429 }
|
| 76 |
+
);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
token = process.env.DEFAULT_HF_TOKEN as string;
|
| 80 |
+
billTo = "huggingface";
|
| 81 |
+
}
|
| 82 |
|
| 83 |
try {
|
| 84 |
const encoder = new TextEncoder();
|
|
|
|
| 92 |
Connection: "keep-alive",
|
| 93 |
},
|
| 94 |
});
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
(async () => {
|
| 97 |
+
try {
|
| 98 |
+
const client = new InferenceClient(token);
|
| 99 |
+
|
| 100 |
+
const systemPrompt = selectedModel.value.includes('MiniMax')
|
| 101 |
+
? INITIAL_SYSTEM_PROMPT_LIGHT
|
| 102 |
+
: INITIAL_SYSTEM_PROMPT;
|
| 103 |
+
|
| 104 |
+
const userPrompt = prompt;
|
| 105 |
+
|
| 106 |
+
const chatCompletion = client.chatCompletionStream(
|
| 107 |
+
{
|
| 108 |
+
model: selectedModel.value + (provider !== "auto" ? `:${provider}` : ""),
|
| 109 |
messages: [
|
| 110 |
{
|
| 111 |
role: "system",
|
| 112 |
+
content: systemPrompt,
|
|
|
|
|
|
|
|
|
|
| 113 |
},
|
| 114 |
+
...(redesignMarkdown ? [{
|
| 115 |
+
role: "assistant",
|
| 116 |
+
content: `User will ask you to redesign the site based on this markdown. Use the same images as the site, but you can improve the content and the design. Here is the markdown: ${redesignMarkdown}`
|
| 117 |
+
}] : []),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
{
|
| 119 |
role: "user",
|
| 120 |
+
content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
|
| 121 |
+
2. I want to use the following secondary color: ${enhancedSettings.secondaryColor} (eg: bg-${enhancedSettings.secondaryColor}-500).
|
| 122 |
+
3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
|
| 123 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
],
|
| 125 |
+
...(selectedModel.top_k ? { top_k: selectedModel.top_k } : {}),
|
| 126 |
+
...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
|
| 127 |
+
...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
|
| 128 |
+
max_tokens: 16_384,
|
| 129 |
+
},
|
| 130 |
+
billTo ? { billTo } : {}
|
| 131 |
+
);
|
| 132 |
+
|
| 133 |
+
while (true) {
|
| 134 |
+
const { done, value } = await chatCompletion.next()
|
| 135 |
+
if (done) {
|
| 136 |
+
break;
|
|
|
|
| 137 |
}
|
| 138 |
|
| 139 |
+
const chunk = value.choices[0]?.delta?.content;
|
| 140 |
+
if (chunk) {
|
| 141 |
+
await writer.write(encoder.encode(chunk));
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
await writer.close();
|
| 146 |
+
} catch (error: any) {
|
| 147 |
+
console.error(error);
|
| 148 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 149 |
+
await writer.write(
|
| 150 |
+
encoder.encode(
|
| 151 |
+
JSON.stringify({
|
| 152 |
+
ok: false,
|
| 153 |
+
openProModal: true,
|
| 154 |
+
message: error.message,
|
| 155 |
+
})
|
| 156 |
+
)
|
| 157 |
+
);
|
| 158 |
+
} else if (error?.message?.includes("inference provider information")) {
|
| 159 |
+
await writer.write(
|
| 160 |
+
encoder.encode(
|
| 161 |
+
JSON.stringify({
|
| 162 |
+
ok: false,
|
| 163 |
+
openSelectProvider: true,
|
| 164 |
+
message: error.message,
|
| 165 |
+
})
|
| 166 |
)
|
| 167 |
+
);
|
| 168 |
+
}
|
| 169 |
+
else {
|
| 170 |
+
await writer.write(
|
| 171 |
+
encoder.encode(
|
| 172 |
+
JSON.stringify({
|
| 173 |
+
ok: false,
|
| 174 |
+
message:
|
| 175 |
+
error.message ||
|
| 176 |
+
"An error occurred while processing your request.",
|
| 177 |
+
})
|
| 178 |
+
)
|
| 179 |
+
);
|
| 180 |
+
}
|
| 181 |
+
} finally {
|
| 182 |
+
try {
|
| 183 |
+
await writer?.close();
|
| 184 |
+
} catch {
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
})();
|
| 188 |
+
|
| 189 |
+
return response;
|
| 190 |
+
} catch (error: any) {
|
| 191 |
+
return NextResponse.json(
|
| 192 |
+
{
|
| 193 |
+
ok: false,
|
| 194 |
+
openSelectProvider: true,
|
| 195 |
+
message:
|
| 196 |
+
error?.message || "An error occurred while processing your request.",
|
| 197 |
+
},
|
| 198 |
+
{ status: 500 }
|
| 199 |
+
);
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
export async function PUT(request: NextRequest) {
|
| 204 |
+
const user = await isAuthenticated();
|
| 205 |
+
if (user instanceof NextResponse || !user) {
|
| 206 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
const authHeaders = await headers();
|
| 210 |
+
|
| 211 |
+
const body = await request.json();
|
| 212 |
+
const { prompt, provider, selectedElementHtml, model, pages, files, repoId, isNew } =
|
| 213 |
+
body;
|
| 214 |
+
|
| 215 |
+
if (!prompt || pages.length === 0) {
|
| 216 |
+
return NextResponse.json(
|
| 217 |
+
{ ok: false, error: "Missing required fields" },
|
| 218 |
+
{ status: 400 }
|
| 219 |
+
);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
const selectedModel = MODELS.find(
|
| 223 |
+
(m) => m.value === model || m.label === model
|
| 224 |
+
);
|
| 225 |
+
if (!selectedModel) {
|
| 226 |
+
return NextResponse.json(
|
| 227 |
+
{ ok: false, error: "Invalid model selected" },
|
| 228 |
+
{ status: 400 }
|
| 229 |
+
);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
let token = user.token as string;
|
| 233 |
+
let billTo: string | null = null;
|
| 234 |
+
|
| 235 |
+
/**
|
| 236 |
+
* Handle local usage token, this bypass the need for a user token
|
| 237 |
+
* and allows local testing without authentication.
|
| 238 |
+
* This is useful for development and testing purposes.
|
| 239 |
+
*/
|
| 240 |
+
if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
|
| 241 |
+
token = process.env.HF_TOKEN;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
const ip = authHeaders.get("x-forwarded-for")?.includes(",")
|
| 245 |
+
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 246 |
+
: authHeaders.get("x-forwarded-for");
|
| 247 |
+
|
| 248 |
+
if (!token || token === "null" || token === "") {
|
| 249 |
+
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 250 |
+
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 251 |
+
return NextResponse.json(
|
| 252 |
+
{
|
| 253 |
+
ok: false,
|
| 254 |
+
openLogin: true,
|
| 255 |
+
message: "Log In to continue using the service",
|
| 256 |
+
},
|
| 257 |
+
{ status: 429 }
|
| 258 |
+
);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
token = process.env.DEFAULT_HF_TOKEN as string;
|
| 262 |
+
billTo = "huggingface";
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
try {
|
| 266 |
+
const encoder = new TextEncoder();
|
| 267 |
+
const stream = new TransformStream();
|
| 268 |
+
const writer = stream.writable.getWriter();
|
| 269 |
+
|
| 270 |
+
const response = new NextResponse(stream.readable, {
|
| 271 |
+
headers: {
|
| 272 |
+
"Content-Type": "text/plain; charset=utf-8",
|
| 273 |
+
"Cache-Control": "no-cache",
|
| 274 |
+
Connection: "keep-alive",
|
| 275 |
+
},
|
| 276 |
+
});
|
| 277 |
+
|
| 278 |
+
(async () => {
|
| 279 |
+
try {
|
| 280 |
+
const client = new InferenceClient(token);
|
| 281 |
+
|
| 282 |
+
const basePrompt = selectedModel.value.includes('MiniMax')
|
| 283 |
+
? FOLLOW_UP_SYSTEM_PROMPT_LIGHT
|
| 284 |
+
: FOLLOW_UP_SYSTEM_PROMPT;
|
| 285 |
+
const systemPrompt = basePrompt + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
|
| 286 |
+
// const userContext = "You are modifying the HTML file based on the user's request.";
|
| 287 |
+
|
| 288 |
+
const allPages = pages || [];
|
| 289 |
+
const pagesContext = allPages
|
| 290 |
+
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
| 291 |
+
.join("\n\n");
|
| 292 |
+
|
| 293 |
+
const assistantContext = `${selectedElementHtml
|
| 294 |
+
? `\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.`
|
| 295 |
+
: ""
|
| 296 |
+
}. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
|
| 297 |
+
|
| 298 |
+
const chatCompletion = client.chatCompletionStream(
|
| 299 |
+
{
|
| 300 |
+
model: selectedModel.value + (provider !== "auto" ? `:${provider}` : ""),
|
| 301 |
+
messages: [
|
| 302 |
+
{
|
| 303 |
+
role: "system",
|
| 304 |
+
content: systemPrompt + assistantContext
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
role: "user",
|
| 308 |
+
content: prompt,
|
| 309 |
+
},
|
| 310 |
+
],
|
| 311 |
+
...(selectedModel.top_k ? { top_k: selectedModel.top_k } : {}),
|
| 312 |
+
...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
|
| 313 |
+
...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
|
| 314 |
+
max_tokens: 16_384,
|
| 315 |
+
},
|
| 316 |
+
billTo ? { billTo } : {}
|
| 317 |
+
);
|
| 318 |
+
|
| 319 |
+
// Stream the response chunks to the client
|
| 320 |
+
while (true) {
|
| 321 |
+
const { done, value } = await chatCompletion.next();
|
| 322 |
+
if (done) {
|
| 323 |
+
break;
|
| 324 |
}
|
| 325 |
|
| 326 |
+
const chunk = value.choices[0]?.delta?.content;
|
| 327 |
+
if (chunk) {
|
| 328 |
+
await writer.write(encoder.encode(chunk));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
}
|
| 330 |
}
|
|
|
|
| 331 |
|
| 332 |
+
await writer.write(encoder.encode(`\n___METADATA_START___\n${JSON.stringify({
|
| 333 |
+
repoId,
|
| 334 |
+
isNew,
|
| 335 |
+
userName: user.name,
|
| 336 |
+
})}\n___METADATA_END___\n`));
|
| 337 |
+
|
| 338 |
+
await writer.close();
|
| 339 |
+
} catch (error: any) {
|
| 340 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 341 |
+
await writer.write(
|
| 342 |
+
encoder.encode(
|
| 343 |
+
JSON.stringify({
|
| 344 |
+
ok: false,
|
| 345 |
+
openProModal: true,
|
| 346 |
+
message: error.message,
|
| 347 |
+
})
|
| 348 |
+
)
|
| 349 |
+
);
|
| 350 |
+
} else if (error?.message?.includes("inference provider information")) {
|
| 351 |
+
await writer.write(
|
| 352 |
+
encoder.encode(
|
| 353 |
+
JSON.stringify({
|
| 354 |
+
ok: false,
|
| 355 |
+
openSelectProvider: true,
|
| 356 |
+
message: error.message,
|
| 357 |
+
})
|
| 358 |
+
)
|
| 359 |
+
);
|
| 360 |
+
} else {
|
| 361 |
+
await writer.write(
|
| 362 |
+
encoder.encode(
|
| 363 |
+
JSON.stringify({
|
| 364 |
+
ok: false,
|
| 365 |
+
message:
|
| 366 |
+
error.message ||
|
| 367 |
+
"An error occurred while processing your request.",
|
| 368 |
+
})
|
| 369 |
+
)
|
| 370 |
+
);
|
| 371 |
+
}
|
| 372 |
+
} finally {
|
| 373 |
+
try {
|
| 374 |
+
await writer?.close();
|
| 375 |
+
} catch {
|
| 376 |
+
// ignore
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
})();
|
| 380 |
|
| 381 |
return response;
|
| 382 |
+
} catch (error: any) {
|
| 383 |
return NextResponse.json(
|
| 384 |
{
|
| 385 |
+
ok: false,
|
| 386 |
+
openSelectProvider: true,
|
| 387 |
+
message:
|
| 388 |
+
error.message || "An error occurred while processing your request.",
|
| 389 |
},
|
| 390 |
{ status: 500 }
|
| 391 |
);
|
| 392 |
}
|
| 393 |
}
|
| 394 |
+
|
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,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
| 10 |
+
url = "huggingface.co";
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const redirect_uri =
|
| 14 |
+
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 15 |
+
url +
|
| 16 |
+
"/deepsite/auth/callback";
|
| 17 |
+
|
| 18 |
+
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`;
|
| 19 |
+
|
| 20 |
+
return NextResponse.json({ loginUrl: loginRedirectUrl });
|
| 21 |
+
}
|
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,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
export async function POST(req: NextRequest) {
|
| 4 |
+
const body = await req.json();
|
| 5 |
+
const { code } = body;
|
| 6 |
+
|
| 7 |
+
if (!code) {
|
| 8 |
+
return NextResponse.json(
|
| 9 |
+
{ error: "Code is required" },
|
| 10 |
+
{
|
| 11 |
+
status: 400,
|
| 12 |
+
headers: {
|
| 13 |
+
"Content-Type": "application/json",
|
| 14 |
+
},
|
| 15 |
+
}
|
| 16 |
+
);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const Authorization = `Basic ${Buffer.from(
|
| 20 |
+
`${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
|
| 21 |
+
).toString("base64")}`;
|
| 22 |
+
|
| 23 |
+
const host =
|
| 24 |
+
req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
|
| 25 |
+
|
| 26 |
+
const url = host.includes("/spaces/enzostvs")
|
| 27 |
+
? "huggingface.co/deepsite"
|
| 28 |
+
: host;
|
| 29 |
+
const redirect_uri =
|
| 30 |
+
`${host.includes("localhost") ? "http://" : "https://"}` +
|
| 31 |
+
url +
|
| 32 |
+
"/deepsite/auth/callback";
|
| 33 |
+
const request_auth = await fetch("https://huggingface.co/oauth/token", {
|
| 34 |
+
method: "POST",
|
| 35 |
+
headers: {
|
| 36 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 37 |
+
Authorization,
|
| 38 |
+
},
|
| 39 |
+
body: new URLSearchParams({
|
| 40 |
+
grant_type: "authorization_code",
|
| 41 |
+
code,
|
| 42 |
+
redirect_uri,
|
| 43 |
+
}),
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
const response = await request_auth.json();
|
| 47 |
+
if (!response.access_token) {
|
| 48 |
+
return NextResponse.json(
|
| 49 |
+
{ error: "Failed to retrieve access token" },
|
| 50 |
+
{
|
| 51 |
+
status: 400,
|
| 52 |
+
headers: {
|
| 53 |
+
"Content-Type": "application/json",
|
| 54 |
+
},
|
| 55 |
+
}
|
| 56 |
+
);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
|
| 60 |
+
headers: {
|
| 61 |
+
Authorization: `Bearer ${response.access_token}`,
|
| 62 |
+
},
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
if (!userResponse.ok) {
|
| 66 |
+
return NextResponse.json(
|
| 67 |
+
{ user: null, errCode: userResponse.status },
|
| 68 |
+
{ status: userResponse.status }
|
| 69 |
+
);
|
| 70 |
+
}
|
| 71 |
+
const user = await userResponse.json();
|
| 72 |
+
|
| 73 |
+
return NextResponse.json(
|
| 74 |
+
{
|
| 75 |
+
access_token: response.access_token,
|
| 76 |
+
expires_in: response.expires_in,
|
| 77 |
+
user,
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
status: 200,
|
| 81 |
+
headers: {
|
| 82 |
+
"Content-Type": "application/json",
|
| 83 |
+
},
|
| 84 |
+
}
|
| 85 |
+
);
|
| 86 |
+
}
|
app/api/healthcheck/route.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
import { NextResponse } from "next/server";
|
| 2 |
-
|
| 3 |
-
export async function GET() {
|
| 4 |
-
return NextResponse.json({ status: "ok" }, { status: 200 });
|
| 5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/mcp/route.ts
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, createRepo, uploadFiles, spaceInfo, listCommits } from "@huggingface/hub";
|
| 3 |
+
import { COLORS } from "@/lib/utils";
|
| 4 |
+
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 5 |
+
import { Commit, Page } from "@/types";
|
| 6 |
+
|
| 7 |
+
// Timeout configuration (in milliseconds)
|
| 8 |
+
const OPERATION_TIMEOUT = 120000; // 2 minutes for HF operations
|
| 9 |
+
|
| 10 |
+
// Extend the maximum execution time for this route
|
| 11 |
+
export const maxDuration = 180; // 3 minutes
|
| 12 |
+
|
| 13 |
+
// Utility function to wrap promises with timeout
|
| 14 |
+
async function withTimeout<T>(
|
| 15 |
+
promise: Promise<T>,
|
| 16 |
+
timeoutMs: number,
|
| 17 |
+
errorMessage: string = "Operation timed out"
|
| 18 |
+
): Promise<T> {
|
| 19 |
+
let timeoutId: NodeJS.Timeout;
|
| 20 |
+
|
| 21 |
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
| 22 |
+
timeoutId = setTimeout(() => {
|
| 23 |
+
reject(new Error(errorMessage));
|
| 24 |
+
}, timeoutMs);
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
try {
|
| 28 |
+
const result = await Promise.race([promise, timeoutPromise]);
|
| 29 |
+
clearTimeout(timeoutId!);
|
| 30 |
+
return result;
|
| 31 |
+
} catch (error) {
|
| 32 |
+
clearTimeout(timeoutId!);
|
| 33 |
+
throw error;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
interface MCPRequest {
|
| 38 |
+
jsonrpc: "2.0";
|
| 39 |
+
id: number | string;
|
| 40 |
+
method: string;
|
| 41 |
+
params?: any;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
interface MCPResponse {
|
| 45 |
+
jsonrpc: "2.0";
|
| 46 |
+
id: number | string;
|
| 47 |
+
result?: any;
|
| 48 |
+
error?: {
|
| 49 |
+
code: number;
|
| 50 |
+
message: string;
|
| 51 |
+
data?: any;
|
| 52 |
+
};
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
interface CreateProjectParams {
|
| 56 |
+
title?: string;
|
| 57 |
+
pages: Page[];
|
| 58 |
+
prompt?: string;
|
| 59 |
+
hf_token?: string; // Optional - can come from header instead
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// MCP Server over HTTP
|
| 63 |
+
export async function POST(req: NextRequest) {
|
| 64 |
+
try {
|
| 65 |
+
const body: MCPRequest = await req.json();
|
| 66 |
+
const { jsonrpc, id, method, params } = body;
|
| 67 |
+
|
| 68 |
+
// Validate JSON-RPC 2.0 format
|
| 69 |
+
if (jsonrpc !== "2.0") {
|
| 70 |
+
return NextResponse.json({
|
| 71 |
+
jsonrpc: "2.0",
|
| 72 |
+
id: id || null,
|
| 73 |
+
error: {
|
| 74 |
+
code: -32600,
|
| 75 |
+
message: "Invalid Request: jsonrpc must be '2.0'",
|
| 76 |
+
},
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
let response: MCPResponse;
|
| 81 |
+
|
| 82 |
+
switch (method) {
|
| 83 |
+
case "initialize":
|
| 84 |
+
response = {
|
| 85 |
+
jsonrpc: "2.0",
|
| 86 |
+
id,
|
| 87 |
+
result: {
|
| 88 |
+
protocolVersion: "2024-11-05",
|
| 89 |
+
capabilities: {
|
| 90 |
+
tools: {},
|
| 91 |
+
},
|
| 92 |
+
serverInfo: {
|
| 93 |
+
name: "deepsite-mcp-server",
|
| 94 |
+
version: "1.0.0",
|
| 95 |
+
},
|
| 96 |
+
},
|
| 97 |
+
};
|
| 98 |
+
break;
|
| 99 |
+
|
| 100 |
+
case "tools/list":
|
| 101 |
+
response = {
|
| 102 |
+
jsonrpc: "2.0",
|
| 103 |
+
id,
|
| 104 |
+
result: {
|
| 105 |
+
tools: [
|
| 106 |
+
{
|
| 107 |
+
name: "create_project",
|
| 108 |
+
description: `Create a new DeepSite project. This will create a new Hugging Face Space with your HTML/CSS/JS files.
|
| 109 |
+
|
| 110 |
+
Example usage:
|
| 111 |
+
- Create a simple website with HTML, CSS, and JavaScript files
|
| 112 |
+
- Each page needs a 'path' (filename like "index.html", "styles.css", "script.js") and 'html' (the actual content)
|
| 113 |
+
- The title will be formatted to a valid repository name
|
| 114 |
+
- Returns the project URL and metadata`,
|
| 115 |
+
inputSchema: {
|
| 116 |
+
type: "object",
|
| 117 |
+
properties: {
|
| 118 |
+
title: {
|
| 119 |
+
type: "string",
|
| 120 |
+
description: "Project title (optional, defaults to 'DeepSite Project'). Will be formatted to a valid repo name.",
|
| 121 |
+
},
|
| 122 |
+
pages: {
|
| 123 |
+
type: "array",
|
| 124 |
+
description: "Array of files to include in the project",
|
| 125 |
+
items: {
|
| 126 |
+
type: "object",
|
| 127 |
+
properties: {
|
| 128 |
+
path: {
|
| 129 |
+
type: "string",
|
| 130 |
+
description: "File path (e.g., 'index.html', 'styles.css', 'script.js')",
|
| 131 |
+
},
|
| 132 |
+
html: {
|
| 133 |
+
type: "string",
|
| 134 |
+
description: "File content",
|
| 135 |
+
},
|
| 136 |
+
},
|
| 137 |
+
required: ["path", "html"],
|
| 138 |
+
},
|
| 139 |
+
},
|
| 140 |
+
prompt: {
|
| 141 |
+
type: "string",
|
| 142 |
+
description: "Optional prompt/description for the commit message",
|
| 143 |
+
},
|
| 144 |
+
hf_token: {
|
| 145 |
+
type: "string",
|
| 146 |
+
description: "Hugging Face API token (optional if provided via Authorization header)",
|
| 147 |
+
},
|
| 148 |
+
},
|
| 149 |
+
required: ["pages"],
|
| 150 |
+
},
|
| 151 |
+
},
|
| 152 |
+
],
|
| 153 |
+
},
|
| 154 |
+
};
|
| 155 |
+
break;
|
| 156 |
+
|
| 157 |
+
case "tools/call":
|
| 158 |
+
const { name, arguments: toolArgs } = params;
|
| 159 |
+
|
| 160 |
+
if (name === "create_project") {
|
| 161 |
+
try {
|
| 162 |
+
// Extract token from Authorization header if present
|
| 163 |
+
const authHeader = req.headers.get("authorization");
|
| 164 |
+
let hf_token = toolArgs.hf_token;
|
| 165 |
+
|
| 166 |
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
| 167 |
+
hf_token = authHeader.substring(7); // Remove "Bearer " prefix
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
const result = await handleCreateProject({
|
| 171 |
+
...toolArgs,
|
| 172 |
+
hf_token,
|
| 173 |
+
} as CreateProjectParams);
|
| 174 |
+
response = {
|
| 175 |
+
jsonrpc: "2.0",
|
| 176 |
+
id,
|
| 177 |
+
result,
|
| 178 |
+
};
|
| 179 |
+
} catch (error: any) {
|
| 180 |
+
response = {
|
| 181 |
+
jsonrpc: "2.0",
|
| 182 |
+
id,
|
| 183 |
+
error: {
|
| 184 |
+
code: -32000,
|
| 185 |
+
message: error.message || "Failed to create project",
|
| 186 |
+
data: error.data,
|
| 187 |
+
},
|
| 188 |
+
};
|
| 189 |
+
}
|
| 190 |
+
} else {
|
| 191 |
+
response = {
|
| 192 |
+
jsonrpc: "2.0",
|
| 193 |
+
id,
|
| 194 |
+
error: {
|
| 195 |
+
code: -32601,
|
| 196 |
+
message: `Unknown tool: ${name}`,
|
| 197 |
+
},
|
| 198 |
+
};
|
| 199 |
+
}
|
| 200 |
+
break;
|
| 201 |
+
|
| 202 |
+
default:
|
| 203 |
+
response = {
|
| 204 |
+
jsonrpc: "2.0",
|
| 205 |
+
id,
|
| 206 |
+
error: {
|
| 207 |
+
code: -32601,
|
| 208 |
+
message: `Method not found: ${method}`,
|
| 209 |
+
},
|
| 210 |
+
};
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
return NextResponse.json(response);
|
| 214 |
+
} catch (error: any) {
|
| 215 |
+
return NextResponse.json({
|
| 216 |
+
jsonrpc: "2.0",
|
| 217 |
+
id: null,
|
| 218 |
+
error: {
|
| 219 |
+
code: -32700,
|
| 220 |
+
message: "Parse error",
|
| 221 |
+
data: error.message,
|
| 222 |
+
},
|
| 223 |
+
});
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Handle OPTIONS for CORS
|
| 228 |
+
export async function OPTIONS() {
|
| 229 |
+
return new NextResponse(null, {
|
| 230 |
+
status: 200,
|
| 231 |
+
headers: {
|
| 232 |
+
"Access-Control-Allow-Origin": "*",
|
| 233 |
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
| 234 |
+
"Access-Control-Allow-Headers": "Content-Type",
|
| 235 |
+
},
|
| 236 |
+
});
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
async function handleCreateProject(params: CreateProjectParams) {
|
| 240 |
+
const { title: titleFromRequest, pages, prompt, hf_token } = params;
|
| 241 |
+
|
| 242 |
+
// Validate required parameters
|
| 243 |
+
if (!hf_token || typeof hf_token !== "string") {
|
| 244 |
+
throw new Error("hf_token is required and must be a string");
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
| 248 |
+
throw new Error("At least one page is required");
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// Validate that each page has required fields
|
| 252 |
+
for (const page of pages) {
|
| 253 |
+
if (!page.path || !page.html) {
|
| 254 |
+
throw new Error("Each page must have 'path' and 'html' properties");
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// Get user info from HF token
|
| 259 |
+
let username: string;
|
| 260 |
+
try {
|
| 261 |
+
const userResponse = await withTimeout(
|
| 262 |
+
fetch("https://huggingface.co/api/whoami-v2", {
|
| 263 |
+
headers: {
|
| 264 |
+
Authorization: `Bearer ${hf_token}`,
|
| 265 |
+
},
|
| 266 |
+
}),
|
| 267 |
+
30000, // 30 seconds for authentication
|
| 268 |
+
"Authentication timeout: Unable to verify Hugging Face token"
|
| 269 |
+
);
|
| 270 |
+
|
| 271 |
+
if (!userResponse.ok) {
|
| 272 |
+
throw new Error("Invalid Hugging Face token");
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
const userData = await userResponse.json();
|
| 276 |
+
username = userData.name;
|
| 277 |
+
} catch (error: any) {
|
| 278 |
+
if (error.message?.includes('timeout')) {
|
| 279 |
+
throw new Error(`Authentication timeout: ${error.message}`);
|
| 280 |
+
}
|
| 281 |
+
throw new Error(`Authentication failed: ${error.message}`);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
const title = titleFromRequest ?? "DeepSite Project";
|
| 285 |
+
|
| 286 |
+
const formattedTitle = title
|
| 287 |
+
.toLowerCase()
|
| 288 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 289 |
+
.split("-")
|
| 290 |
+
.filter(Boolean)
|
| 291 |
+
.join("-")
|
| 292 |
+
.slice(0, 96);
|
| 293 |
+
|
| 294 |
+
const repo: RepoDesignation = {
|
| 295 |
+
type: "space",
|
| 296 |
+
name: `${username}/${formattedTitle}`,
|
| 297 |
+
};
|
| 298 |
+
|
| 299 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 300 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 301 |
+
const README = `---
|
| 302 |
+
title: ${title}
|
| 303 |
+
colorFrom: ${colorFrom}
|
| 304 |
+
colorTo: ${colorTo}
|
| 305 |
+
emoji: 🐳
|
| 306 |
+
sdk: static
|
| 307 |
+
pinned: false
|
| 308 |
+
tags:
|
| 309 |
+
- deepsite-v3
|
| 310 |
+
---
|
| 311 |
+
|
| 312 |
+
# Welcome to your new DeepSite project!
|
| 313 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 314 |
+
`;
|
| 315 |
+
|
| 316 |
+
const files: File[] = [];
|
| 317 |
+
const readmeFile = new File([README], "README.md", { type: "text/markdown" });
|
| 318 |
+
files.push(readmeFile);
|
| 319 |
+
|
| 320 |
+
pages.forEach((page: Page) => {
|
| 321 |
+
// Determine MIME type based on file extension
|
| 322 |
+
let mimeType = "text/html";
|
| 323 |
+
if (page.path.endsWith(".css")) {
|
| 324 |
+
mimeType = "text/css";
|
| 325 |
+
} else if (page.path.endsWith(".js")) {
|
| 326 |
+
mimeType = "text/javascript";
|
| 327 |
+
} else if (page.path.endsWith(".json")) {
|
| 328 |
+
mimeType = "application/json";
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// Inject the DeepSite badge script into index pages only
|
| 332 |
+
const content = mimeType === "text/html" && isIndexPage(page.path)
|
| 333 |
+
? injectDeepSiteBadge(page.html)
|
| 334 |
+
: page.html;
|
| 335 |
+
const file = new File([content], page.path, { type: mimeType });
|
| 336 |
+
files.push(file);
|
| 337 |
+
});
|
| 338 |
+
|
| 339 |
+
try {
|
| 340 |
+
const { repoUrl } = await withTimeout(
|
| 341 |
+
createRepo({
|
| 342 |
+
repo,
|
| 343 |
+
accessToken: hf_token,
|
| 344 |
+
}),
|
| 345 |
+
60000, // 1 minute for repo creation
|
| 346 |
+
"Timeout creating repository. Please try again."
|
| 347 |
+
);
|
| 348 |
+
|
| 349 |
+
const commitTitle = !prompt || prompt.trim() === "" ? "Initial project creation via MCP" : prompt;
|
| 350 |
+
|
| 351 |
+
await withTimeout(
|
| 352 |
+
uploadFiles({
|
| 353 |
+
repo,
|
| 354 |
+
files,
|
| 355 |
+
accessToken: hf_token,
|
| 356 |
+
commitTitle,
|
| 357 |
+
}),
|
| 358 |
+
OPERATION_TIMEOUT,
|
| 359 |
+
"Timeout uploading files. The repository was created but files may not have been uploaded."
|
| 360 |
+
);
|
| 361 |
+
|
| 362 |
+
const path = repoUrl.split("/").slice(-2).join("/");
|
| 363 |
+
|
| 364 |
+
const commits: Commit[] = [];
|
| 365 |
+
const commitIterator = listCommits({ repo, accessToken: hf_token });
|
| 366 |
+
|
| 367 |
+
// Wrap the commit listing with a timeout
|
| 368 |
+
const commitTimeout = new Promise<void>((_, reject) => {
|
| 369 |
+
setTimeout(() => reject(new Error("Timeout listing commits")), 30000);
|
| 370 |
+
});
|
| 371 |
+
|
| 372 |
+
try {
|
| 373 |
+
await Promise.race([
|
| 374 |
+
(async () => {
|
| 375 |
+
for await (const commit of commitIterator) {
|
| 376 |
+
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
|
| 377 |
+
continue;
|
| 378 |
+
}
|
| 379 |
+
commits.push({
|
| 380 |
+
title: commit.title,
|
| 381 |
+
oid: commit.oid,
|
| 382 |
+
date: commit.date,
|
| 383 |
+
});
|
| 384 |
+
}
|
| 385 |
+
})(),
|
| 386 |
+
commitTimeout
|
| 387 |
+
]);
|
| 388 |
+
} catch (error: any) {
|
| 389 |
+
// If listing commits times out, continue with empty commits array
|
| 390 |
+
console.error("Failed to list commits:", error.message);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
const space = await withTimeout(
|
| 394 |
+
spaceInfo({
|
| 395 |
+
name: repo.name,
|
| 396 |
+
accessToken: hf_token,
|
| 397 |
+
}),
|
| 398 |
+
30000, // 30 seconds for space info
|
| 399 |
+
"Timeout fetching space information"
|
| 400 |
+
);
|
| 401 |
+
|
| 402 |
+
const projectUrl = `https://huggingface.co/deepsite/${path}`;
|
| 403 |
+
const spaceUrl = `https://huggingface.co/spaces/${path}`;
|
| 404 |
+
const liveUrl = `https://${username}-${formattedTitle}.hf.space`;
|
| 405 |
+
|
| 406 |
+
return {
|
| 407 |
+
content: [
|
| 408 |
+
{
|
| 409 |
+
type: "text",
|
| 410 |
+
text: JSON.stringify(
|
| 411 |
+
{
|
| 412 |
+
success: true,
|
| 413 |
+
message: "Project created successfully!",
|
| 414 |
+
projectUrl,
|
| 415 |
+
spaceUrl,
|
| 416 |
+
liveUrl,
|
| 417 |
+
spaceId: space.name,
|
| 418 |
+
projectId: space.id,
|
| 419 |
+
files: pages.map((p) => p.path),
|
| 420 |
+
updatedAt: space.updatedAt,
|
| 421 |
+
},
|
| 422 |
+
null,
|
| 423 |
+
2
|
| 424 |
+
),
|
| 425 |
+
},
|
| 426 |
+
],
|
| 427 |
+
};
|
| 428 |
+
} catch (err: any) {
|
| 429 |
+
if (err.message?.includes('timeout') || err.message?.includes('Timeout')) {
|
| 430 |
+
throw new Error(err.message || "Operation timed out. Please try again.");
|
| 431 |
+
}
|
| 432 |
+
throw new Error(err.message || "Failed to create project");
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles, downloadFile } 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 |
+
const files: File[] = [];
|
| 53 |
+
const pages: Page[] = [];
|
| 54 |
+
const mediaFiles: string[] = [];
|
| 55 |
+
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
| 56 |
+
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif", "mp4", "webm", "ogg", "avi", "mov", "mp3", "wav", "ogg", "aac", "m4a"];
|
| 57 |
+
const commitFilePaths: Set<string> = new Set();
|
| 58 |
+
|
| 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 (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
|
| 67 |
+
commitFilePaths.add(fileInfo.path);
|
| 68 |
+
|
| 69 |
+
const blob = await downloadFile({
|
| 70 |
+
repo,
|
| 71 |
+
accessToken: user.token as string,
|
| 72 |
+
path: fileInfo.path,
|
| 73 |
+
revision: commitId,
|
| 74 |
+
raw: true
|
| 75 |
+
}).catch((error) => {
|
| 76 |
+
return null;
|
| 77 |
+
});
|
| 78 |
+
if (!blob) {
|
| 79 |
+
continue;
|
| 80 |
+
}
|
| 81 |
+
const content = await blob?.text();
|
| 82 |
+
|
| 83 |
+
if (content) {
|
| 84 |
+
let mimeType = "text/plain";
|
| 85 |
+
|
| 86 |
+
switch (fileExtension) {
|
| 87 |
+
case "html":
|
| 88 |
+
mimeType = "text/html";
|
| 89 |
+
break;
|
| 90 |
+
case "css":
|
| 91 |
+
mimeType = "text/css";
|
| 92 |
+
break;
|
| 93 |
+
case "js":
|
| 94 |
+
mimeType = "application/javascript";
|
| 95 |
+
break;
|
| 96 |
+
case "json":
|
| 97 |
+
mimeType = "application/json";
|
| 98 |
+
break;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
if (fileInfo.path === "index.html") {
|
| 102 |
+
pages.unshift({
|
| 103 |
+
path: fileInfo.path,
|
| 104 |
+
html: content,
|
| 105 |
+
});
|
| 106 |
+
} else {
|
| 107 |
+
pages.push({
|
| 108 |
+
path: fileInfo.path,
|
| 109 |
+
html: content,
|
| 110 |
+
});
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
const file = new File([content], fileInfo.path, { type: mimeType });
|
| 114 |
+
files.push(file);
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
else if (fileInfo.type === "directory" && (["videos", "images", "audio"].includes(fileInfo.path) || fileInfo.path === "components")) {
|
| 118 |
+
for await (const subFileInfo of listFiles({
|
| 119 |
+
repo,
|
| 120 |
+
accessToken: user.token as string,
|
| 121 |
+
revision: commitId,
|
| 122 |
+
path: fileInfo.path,
|
| 123 |
+
})) {
|
| 124 |
+
if (subFileInfo.path.includes("components")) {
|
| 125 |
+
commitFilePaths.add(subFileInfo.path);
|
| 126 |
+
const blob = await downloadFile({
|
| 127 |
+
repo,
|
| 128 |
+
accessToken: user.token as string,
|
| 129 |
+
path: subFileInfo.path,
|
| 130 |
+
revision: commitId,
|
| 131 |
+
raw: true
|
| 132 |
+
}).catch((error) => {
|
| 133 |
+
return null;
|
| 134 |
+
});
|
| 135 |
+
if (!blob) {
|
| 136 |
+
continue;
|
| 137 |
+
}
|
| 138 |
+
const content = await blob?.text();
|
| 139 |
+
|
| 140 |
+
if (content) {
|
| 141 |
+
pages.push({
|
| 142 |
+
path: subFileInfo.path,
|
| 143 |
+
html: content,
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
const file = new File([content], subFileInfo.path, { type: "text/html" });
|
| 147 |
+
files.push(file);
|
| 148 |
+
}
|
| 149 |
+
} else if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
|
| 150 |
+
commitFilePaths.add(subFileInfo.path);
|
| 151 |
+
mediaFiles.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
else if (allowedExtensions.includes(fileExtension || "")) {
|
| 156 |
+
commitFilePaths.add(fileInfo.path);
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
const mainBranchFilePaths: Set<string> = new Set();
|
| 161 |
+
for await (const fileInfo of listFiles({
|
| 162 |
+
repo,
|
| 163 |
+
accessToken: user.token as string,
|
| 164 |
+
revision: "main",
|
| 165 |
+
})) {
|
| 166 |
+
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 167 |
+
|
| 168 |
+
if (allowedExtensions.includes(fileExtension || "")) {
|
| 169 |
+
mainBranchFilePaths.add(fileInfo.path);
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
const filesToDelete: string[] = [];
|
| 174 |
+
for (const mainFilePath of mainBranchFilePaths) {
|
| 175 |
+
if (!commitFilePaths.has(mainFilePath)) {
|
| 176 |
+
filesToDelete.push(mainFilePath);
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
if (files.length === 0 && filesToDelete.length === 0) {
|
| 181 |
+
return NextResponse.json(
|
| 182 |
+
{ ok: false, error: "No files found in the specified commit and no files to delete" },
|
| 183 |
+
{ status: 404 }
|
| 184 |
+
);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if (filesToDelete.length > 0) {
|
| 188 |
+
await deleteFiles({
|
| 189 |
+
repo,
|
| 190 |
+
paths: filesToDelete,
|
| 191 |
+
accessToken: user.token as string,
|
| 192 |
+
commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
|
| 193 |
+
commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
|
| 194 |
+
});
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
if (files.length > 0) {
|
| 198 |
+
await uploadFiles({
|
| 199 |
+
repo,
|
| 200 |
+
files,
|
| 201 |
+
accessToken: user.token as string,
|
| 202 |
+
commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
|
| 203 |
+
commitDescription: `Promoted commit ${commitId} to main branch`,
|
| 204 |
+
});
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
return NextResponse.json(
|
| 208 |
+
{
|
| 209 |
+
ok: true,
|
| 210 |
+
message: "Version promoted successfully",
|
| 211 |
+
promotedCommit: commitId,
|
| 212 |
+
pages: pages,
|
| 213 |
+
files: mediaFiles,
|
| 214 |
+
},
|
| 215 |
+
{ status: 200 }
|
| 216 |
+
);
|
| 217 |
+
|
| 218 |
+
} catch (error: any) {
|
| 219 |
+
|
| 220 |
+
// Handle specific HuggingFace API errors
|
| 221 |
+
if (error.statusCode === 404) {
|
| 222 |
+
return NextResponse.json(
|
| 223 |
+
{ ok: false, error: "Commit not found" },
|
| 224 |
+
{ status: 404 }
|
| 225 |
+
);
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
if (error.statusCode === 403) {
|
| 229 |
+
return NextResponse.json(
|
| 230 |
+
{ ok: false, error: "Access denied to repository" },
|
| 231 |
+
{ status: 403 }
|
| 232 |
+
);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
return NextResponse.json(
|
| 236 |
+
{ ok: false, error: error.message || "Failed to promote version" },
|
| 237 |
+
{ status: 500 }
|
| 238 |
+
);
|
| 239 |
+
}
|
| 240 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import { Page } from "@/types";
|
| 6 |
+
|
| 7 |
+
export async function GET(
|
| 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 |
+
const pages: Page[] = [];
|
| 53 |
+
|
| 54 |
+
for await (const fileInfo of listFiles({
|
| 55 |
+
repo,
|
| 56 |
+
accessToken: user.token as string,
|
| 57 |
+
revision: commitId,
|
| 58 |
+
})) {
|
| 59 |
+
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 60 |
+
|
| 61 |
+
if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
|
| 62 |
+
const blob = await downloadFile({
|
| 63 |
+
repo,
|
| 64 |
+
accessToken: user.token as string,
|
| 65 |
+
path: fileInfo.path,
|
| 66 |
+
revision: commitId,
|
| 67 |
+
raw: true
|
| 68 |
+
}).catch((error) => {
|
| 69 |
+
return null;
|
| 70 |
+
});
|
| 71 |
+
if (!blob) {
|
| 72 |
+
continue;
|
| 73 |
+
}
|
| 74 |
+
const content = await blob?.text();
|
| 75 |
+
|
| 76 |
+
if (content) {
|
| 77 |
+
if (fileInfo.path === "index.html") {
|
| 78 |
+
pages.unshift({
|
| 79 |
+
path: fileInfo.path,
|
| 80 |
+
html: content,
|
| 81 |
+
});
|
| 82 |
+
} else {
|
| 83 |
+
pages.push({
|
| 84 |
+
path: fileInfo.path,
|
| 85 |
+
html: content,
|
| 86 |
+
});
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
return NextResponse.json({
|
| 93 |
+
ok: true,
|
| 94 |
+
pages,
|
| 95 |
+
});
|
| 96 |
+
} catch (error: any) {
|
| 97 |
+
console.error("Error fetching commit pages:", error);
|
| 98 |
+
return NextResponse.json(
|
| 99 |
+
{
|
| 100 |
+
ok: false,
|
| 101 |
+
error: error.message || "Failed to fetch commit pages",
|
| 102 |
+
},
|
| 103 |
+
{ status: 500 }
|
| 104 |
+
);
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
app/api/{projects → me/projects/[namespace]}/[repoId]/download/route.ts
RENAMED
|
@@ -1,28 +1,59 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import {
|
| 3 |
-
import { NextResponse } from "next/server";
|
| 4 |
import JSZip from "jszip";
|
| 5 |
|
|
|
|
|
|
|
| 6 |
export async function GET(
|
| 7 |
-
|
| 8 |
-
{ params }: { params: Promise<{ repoId: string }> }
|
| 9 |
) {
|
| 10 |
-
const
|
| 11 |
-
|
| 12 |
-
if (!
|
| 13 |
-
return NextResponse.json({
|
| 14 |
}
|
| 15 |
-
|
| 16 |
-
const
|
| 17 |
-
|
| 18 |
-
name: session.user?.username + "/" + repoId,
|
| 19 |
-
};
|
| 20 |
|
| 21 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const zip = new JSZip();
|
|
|
|
| 23 |
for await (const fileInfo of listFiles({
|
| 24 |
repo,
|
| 25 |
-
accessToken: token as string,
|
| 26 |
recursive: true,
|
| 27 |
})) {
|
| 28 |
if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) {
|
|
@@ -32,7 +63,7 @@ export async function GET(
|
|
| 32 |
try {
|
| 33 |
const blob = await downloadFile({
|
| 34 |
repo,
|
| 35 |
-
accessToken: token as string,
|
| 36 |
path: fileInfo.path,
|
| 37 |
raw: true
|
| 38 |
}).catch((error) => {
|
|
@@ -59,7 +90,7 @@ export async function GET(
|
|
| 59 |
}
|
| 60 |
});
|
| 61 |
|
| 62 |
-
const projectName = `${
|
| 63 |
const filename = `${projectName}.zip`;
|
| 64 |
|
| 65 |
return new NextResponse(zipBlob, {
|
|
@@ -69,8 +100,11 @@ export async function GET(
|
|
| 69 |
"Content-Length": zipBlob.size.toString(),
|
| 70 |
},
|
| 71 |
});
|
| 72 |
-
} catch (error) {
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
-
}
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
|
|
|
|
| 3 |
import JSZip from "jszip";
|
| 4 |
|
| 5 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 6 |
+
|
| 7 |
export async function GET(
|
| 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 |
+
{
|
| 30 |
+
ok: false,
|
| 31 |
+
error: "Space is not a static space",
|
| 32 |
+
},
|
| 33 |
+
{ status: 404 }
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if (space.author !== user.name) {
|
| 38 |
+
return NextResponse.json(
|
| 39 |
+
{
|
| 40 |
+
ok: false,
|
| 41 |
+
error: "Space does not belong to the authenticated user",
|
| 42 |
+
},
|
| 43 |
+
{ status: 403 }
|
| 44 |
+
);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const repo: RepoDesignation = {
|
| 48 |
+
type: "space",
|
| 49 |
+
name: `${namespace}/${repoId}`,
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
const zip = new JSZip();
|
| 53 |
+
|
| 54 |
for await (const fileInfo of listFiles({
|
| 55 |
repo,
|
| 56 |
+
accessToken: user.token as string,
|
| 57 |
recursive: true,
|
| 58 |
})) {
|
| 59 |
if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) {
|
|
|
|
| 63 |
try {
|
| 64 |
const blob = await downloadFile({
|
| 65 |
repo,
|
| 66 |
+
accessToken: user.token as string,
|
| 67 |
path: fileInfo.path,
|
| 68 |
raw: true
|
| 69 |
}).catch((error) => {
|
|
|
|
| 90 |
}
|
| 91 |
});
|
| 92 |
|
| 93 |
+
const projectName = `${namespace}-${repoId}`.replace(/[^a-zA-Z0-9-_]/g, '_');
|
| 94 |
const filename = `${projectName}.zip`;
|
| 95 |
|
| 96 |
return new NextResponse(zipBlob, {
|
|
|
|
| 100 |
"Content-Length": zipBlob.size.toString(),
|
| 101 |
},
|
| 102 |
});
|
| 103 |
+
} catch (error: any) {
|
| 104 |
+
return NextResponse.json(
|
| 105 |
+
{ ok: false, error: error.message || "Failed to create ZIP file" },
|
| 106 |
+
{ status: 500 }
|
| 107 |
+
);
|
| 108 |
}
|
| 109 |
+
}
|
| 110 |
+
|
app/api/me/projects/[namespace]/[repoId]/images/route.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
|
| 6 |
+
export async function POST(
|
| 7 |
+
req: NextRequest,
|
| 8 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 9 |
+
) {
|
| 10 |
+
try {
|
| 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 |
+
const space = await spaceInfo({
|
| 21 |
+
name: `${namespace}/${repoId}`,
|
| 22 |
+
accessToken: user.token as string,
|
| 23 |
+
additionalFields: ["author"],
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
if (!space || space.sdk !== "static") {
|
| 27 |
+
return NextResponse.json(
|
| 28 |
+
{ ok: false, error: "Space is not a static space." },
|
| 29 |
+
{ status: 404 }
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
if (space.author !== user.name) {
|
| 34 |
+
return NextResponse.json(
|
| 35 |
+
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 36 |
+
{ status: 403 }
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Parse the FormData to get the media files
|
| 41 |
+
const formData = await req.formData();
|
| 42 |
+
const mediaFiles = formData.getAll("images") as File[];
|
| 43 |
+
|
| 44 |
+
if (!mediaFiles || mediaFiles.length === 0) {
|
| 45 |
+
return NextResponse.json(
|
| 46 |
+
{
|
| 47 |
+
ok: false,
|
| 48 |
+
error: "At least one media file is required under the 'images' key",
|
| 49 |
+
},
|
| 50 |
+
{ status: 400 }
|
| 51 |
+
);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const files: File[] = [];
|
| 55 |
+
for (const file of mediaFiles) {
|
| 56 |
+
if (!(file instanceof File)) {
|
| 57 |
+
return NextResponse.json(
|
| 58 |
+
{
|
| 59 |
+
ok: false,
|
| 60 |
+
error: "Invalid file format - all items under 'images' key must be files",
|
| 61 |
+
},
|
| 62 |
+
{ status: 400 }
|
| 63 |
+
);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Check if file is a supported media type
|
| 67 |
+
const isImage = file.type.startsWith('image/');
|
| 68 |
+
const isVideo = file.type.startsWith('video/');
|
| 69 |
+
const isAudio = file.type.startsWith('audio/');
|
| 70 |
+
|
| 71 |
+
if (!isImage && !isVideo && !isAudio) {
|
| 72 |
+
return NextResponse.json(
|
| 73 |
+
{
|
| 74 |
+
ok: false,
|
| 75 |
+
error: `File ${file.name} is not a supported media type (image, video, or audio)`,
|
| 76 |
+
},
|
| 77 |
+
{ status: 400 }
|
| 78 |
+
);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Create File object with appropriate folder prefix
|
| 82 |
+
let folderPrefix = 'images/';
|
| 83 |
+
if (isVideo) {
|
| 84 |
+
folderPrefix = 'videos/';
|
| 85 |
+
} else if (isAudio) {
|
| 86 |
+
folderPrefix = 'audio/';
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
const fileName = `${folderPrefix}${file.name}`;
|
| 90 |
+
const processedFile = new File([file], fileName, { type: file.type });
|
| 91 |
+
files.push(processedFile);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Upload files to HuggingFace space
|
| 95 |
+
const repo: RepoDesignation = {
|
| 96 |
+
type: "space",
|
| 97 |
+
name: `${namespace}/${repoId}`,
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
await uploadFiles({
|
| 101 |
+
repo,
|
| 102 |
+
files,
|
| 103 |
+
accessToken: user.token as string,
|
| 104 |
+
commitTitle: `Upload ${files.length} media file(s)`,
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
return NextResponse.json({
|
| 108 |
+
ok: true,
|
| 109 |
+
message: `Successfully uploaded ${files.length} media file(s) to ${namespace}/${repoId}/`,
|
| 110 |
+
uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
|
| 111 |
+
}, { status: 200 });
|
| 112 |
+
|
| 113 |
+
} catch (error) {
|
| 114 |
+
console.error('Error uploading media files:', error);
|
| 115 |
+
return NextResponse.json(
|
| 116 |
+
{
|
| 117 |
+
ok: false,
|
| 118 |
+
error: "Failed to upload media files",
|
| 119 |
+
},
|
| 120 |
+
{ status: 500 }
|
| 121 |
+
);
|
| 122 |
+
}
|
| 123 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/route.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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", "mp4", "webm", "ogg", "avi", "mov", "mp3", "wav", "ogg", "aac", "m4a"];
|
| 109 |
+
|
| 110 |
+
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
| 111 |
+
if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
|
| 112 |
+
const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true }).catch((error) => {
|
| 113 |
+
return null;
|
| 114 |
+
});
|
| 115 |
+
if (!blob) {
|
| 116 |
+
continue;
|
| 117 |
+
}
|
| 118 |
+
const html = await blob?.text();
|
| 119 |
+
if (!html) {
|
| 120 |
+
continue;
|
| 121 |
+
}
|
| 122 |
+
if (fileInfo.path === "index.html") {
|
| 123 |
+
htmlFiles.unshift({
|
| 124 |
+
path: fileInfo.path,
|
| 125 |
+
html,
|
| 126 |
+
});
|
| 127 |
+
} else {
|
| 128 |
+
htmlFiles.push({
|
| 129 |
+
path: fileInfo.path,
|
| 130 |
+
html,
|
| 131 |
+
});
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
if (fileInfo.type === "directory") {
|
| 135 |
+
for await (const subFileInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
| 136 |
+
if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
|
| 137 |
+
files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
|
| 138 |
+
} else {
|
| 139 |
+
const blob = await downloadFile({ repo, accessToken: user.token as string, path: subFileInfo.path, raw: true }).catch((error) => {
|
| 140 |
+
return null;
|
| 141 |
+
});
|
| 142 |
+
if (!blob) {
|
| 143 |
+
continue;
|
| 144 |
+
}
|
| 145 |
+
const html = await blob?.text();
|
| 146 |
+
if (!html) {
|
| 147 |
+
continue;
|
| 148 |
+
}
|
| 149 |
+
htmlFiles.push({
|
| 150 |
+
path: subFileInfo.path,
|
| 151 |
+
html,
|
| 152 |
+
});
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
const commits: Commit[] = [];
|
| 158 |
+
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
| 159 |
+
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
|
| 160 |
+
continue;
|
| 161 |
+
}
|
| 162 |
+
commits.push({
|
| 163 |
+
title: commit.title,
|
| 164 |
+
oid: commit.oid,
|
| 165 |
+
date: commit.date,
|
| 166 |
+
});
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
if (htmlFiles.length === 0) {
|
| 170 |
+
return NextResponse.json(
|
| 171 |
+
{
|
| 172 |
+
ok: false,
|
| 173 |
+
error: "No HTML files found",
|
| 174 |
+
},
|
| 175 |
+
{ status: 404 }
|
| 176 |
+
);
|
| 177 |
+
}
|
| 178 |
+
return NextResponse.json(
|
| 179 |
+
{
|
| 180 |
+
project: {
|
| 181 |
+
id: space.id,
|
| 182 |
+
space_id: space.name,
|
| 183 |
+
private: space.private,
|
| 184 |
+
_updatedAt: space.updatedAt,
|
| 185 |
+
},
|
| 186 |
+
pages: htmlFiles,
|
| 187 |
+
files,
|
| 188 |
+
commits,
|
| 189 |
+
ok: true,
|
| 190 |
+
},
|
| 191 |
+
{ status: 200 }
|
| 192 |
+
);
|
| 193 |
+
|
| 194 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 195 |
+
} catch (error: any) {
|
| 196 |
+
if (error.statusCode === 404) {
|
| 197 |
+
return NextResponse.json(
|
| 198 |
+
{ error: "Space not found", ok: false },
|
| 199 |
+
{ status: 404 }
|
| 200 |
+
);
|
| 201 |
+
}
|
| 202 |
+
return NextResponse.json(
|
| 203 |
+
{ error: error.message, ok: false },
|
| 204 |
+
{ status: 500 }
|
| 205 |
+
);
|
| 206 |
+
}
|
| 207 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/save/route.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
// Determine MIME type based on file extension
|
| 32 |
+
let mimeType = "text/html";
|
| 33 |
+
if (page.path.endsWith(".css")) {
|
| 34 |
+
mimeType = "text/css";
|
| 35 |
+
} else if (page.path.endsWith(".js")) {
|
| 36 |
+
mimeType = "text/javascript";
|
| 37 |
+
} else if (page.path.endsWith(".json")) {
|
| 38 |
+
mimeType = "application/json";
|
| 39 |
+
}
|
| 40 |
+
const file = new File([page.html], page.path, { type: mimeType });
|
| 41 |
+
files.push(file);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
const response = await uploadFiles({
|
| 45 |
+
repo: {
|
| 46 |
+
type: "space",
|
| 47 |
+
name: `${namespace}/${repoId}`,
|
| 48 |
+
},
|
| 49 |
+
files,
|
| 50 |
+
commitTitle,
|
| 51 |
+
accessToken: user.token as string,
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
return NextResponse.json({
|
| 55 |
+
ok: true,
|
| 56 |
+
pages,
|
| 57 |
+
commit: {
|
| 58 |
+
...response.commit,
|
| 59 |
+
title: commitTitle,
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
} catch (error: any) {
|
| 63 |
+
console.error("Error saving manual changes:", error);
|
| 64 |
+
return NextResponse.json(
|
| 65 |
+
{
|
| 66 |
+
ok: false,
|
| 67 |
+
error: error.message || "Failed to save changes",
|
| 68 |
+
},
|
| 69 |
+
{ status: 500 }
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/update/route.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import { Page } from "@/types";
|
| 6 |
+
import { COLORS } from "@/lib/utils";
|
| 7 |
+
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 8 |
+
import { pagesToFiles } from "@/lib/format-ai-response";
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* UPDATE route - for updating existing projects or creating new ones after AI streaming
|
| 12 |
+
* This route handles the HuggingFace upload after client-side AI response processing
|
| 13 |
+
*/
|
| 14 |
+
export async function PUT(
|
| 15 |
+
req: NextRequest,
|
| 16 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 17 |
+
) {
|
| 18 |
+
const user = await isAuthenticated();
|
| 19 |
+
if (user instanceof NextResponse || !user) {
|
| 20 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const param = await params;
|
| 24 |
+
let { namespace, repoId } = param;
|
| 25 |
+
const { pages, commitTitle = "AI-generated changes", isNew, projectName } = await req.json();
|
| 26 |
+
|
| 27 |
+
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
| 28 |
+
return NextResponse.json(
|
| 29 |
+
{ ok: false, error: "Pages are required" },
|
| 30 |
+
{ status: 400 }
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
let files: File[];
|
| 36 |
+
|
| 37 |
+
if (isNew) {
|
| 38 |
+
// Creating a new project
|
| 39 |
+
const title = projectName || "DeepSite Project";
|
| 40 |
+
const formattedTitle = title
|
| 41 |
+
.toLowerCase()
|
| 42 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 43 |
+
.split("-")
|
| 44 |
+
.filter(Boolean)
|
| 45 |
+
.join("-")
|
| 46 |
+
.slice(0, 96);
|
| 47 |
+
|
| 48 |
+
const repo: RepoDesignation = {
|
| 49 |
+
type: "space",
|
| 50 |
+
name: `${user.name}/${formattedTitle}`,
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
try {
|
| 54 |
+
const { repoUrl } = await createRepo({
|
| 55 |
+
repo,
|
| 56 |
+
accessToken: user.token as string,
|
| 57 |
+
});
|
| 58 |
+
namespace = user.name;
|
| 59 |
+
repoId = repoUrl.split("/").slice(-2).join("/").split("/")[1];
|
| 60 |
+
} catch (createRepoError: any) {
|
| 61 |
+
return NextResponse.json(
|
| 62 |
+
{
|
| 63 |
+
ok: false,
|
| 64 |
+
error: `Failed to create repository: ${createRepoError.message || 'Unknown error'}`,
|
| 65 |
+
},
|
| 66 |
+
{ status: 500 }
|
| 67 |
+
);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Prepare files with badge injection for new projects
|
| 71 |
+
files = [];
|
| 72 |
+
pages.forEach((page: Page) => {
|
| 73 |
+
let mimeType = "text/html";
|
| 74 |
+
if (page.path.endsWith(".css")) {
|
| 75 |
+
mimeType = "text/css";
|
| 76 |
+
} else if (page.path.endsWith(".js")) {
|
| 77 |
+
mimeType = "text/javascript";
|
| 78 |
+
} else if (page.path.endsWith(".json")) {
|
| 79 |
+
mimeType = "application/json";
|
| 80 |
+
}
|
| 81 |
+
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 82 |
+
? injectDeepSiteBadge(page.html)
|
| 83 |
+
: page.html;
|
| 84 |
+
const file = new File([content], page.path, { type: mimeType });
|
| 85 |
+
files.push(file);
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
// Add README.md for new projects
|
| 89 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 90 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 91 |
+
const README = `---
|
| 92 |
+
title: ${title}
|
| 93 |
+
colorFrom: ${colorFrom}
|
| 94 |
+
colorTo: ${colorTo}
|
| 95 |
+
emoji: 🐳
|
| 96 |
+
sdk: static
|
| 97 |
+
pinned: false
|
| 98 |
+
tags:
|
| 99 |
+
- deepsite-v3
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
# Welcome to your new DeepSite project!
|
| 103 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 104 |
+
`;
|
| 105 |
+
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
| 106 |
+
} else {
|
| 107 |
+
// Updating existing project - no badge injection
|
| 108 |
+
files = pagesToFiles(pages);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
const response = await uploadFiles({
|
| 112 |
+
repo: {
|
| 113 |
+
type: "space",
|
| 114 |
+
name: `${namespace}/${repoId}`,
|
| 115 |
+
},
|
| 116 |
+
files,
|
| 117 |
+
commitTitle,
|
| 118 |
+
accessToken: user.token as string,
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
return NextResponse.json({
|
| 122 |
+
ok: true,
|
| 123 |
+
pages,
|
| 124 |
+
repoId: `${namespace}/${repoId}`,
|
| 125 |
+
commit: {
|
| 126 |
+
...response.commit,
|
| 127 |
+
title: commitTitle,
|
| 128 |
+
}
|
| 129 |
+
});
|
| 130 |
+
} catch (error: any) {
|
| 131 |
+
console.error("Error updating project:", error);
|
| 132 |
+
return NextResponse.json(
|
| 133 |
+
{
|
| 134 |
+
ok: false,
|
| 135 |
+
error: error.message || "Failed to update project",
|
| 136 |
+
},
|
| 137 |
+
{ status: 500 }
|
| 138 |
+
);
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
app/api/me/projects/route.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 8 |
+
|
| 9 |
+
export async function POST(
|
| 10 |
+
req: NextRequest,
|
| 11 |
+
) {
|
| 12 |
+
const user = await isAuthenticated();
|
| 13 |
+
if (user instanceof NextResponse || !user) {
|
| 14 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const { title: titleFromRequest, pages, prompt } = await req.json();
|
| 18 |
+
|
| 19 |
+
const title = titleFromRequest ?? "DeepSite Project";
|
| 20 |
+
|
| 21 |
+
const formattedTitle = title
|
| 22 |
+
.toLowerCase()
|
| 23 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 24 |
+
.split("-")
|
| 25 |
+
.filter(Boolean)
|
| 26 |
+
.join("-")
|
| 27 |
+
.slice(0, 96);
|
| 28 |
+
|
| 29 |
+
const repo: RepoDesignation = {
|
| 30 |
+
type: "space",
|
| 31 |
+
name: `${user.name}/${formattedTitle}`,
|
| 32 |
+
};
|
| 33 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 34 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 35 |
+
const README = `---
|
| 36 |
+
title: ${title}
|
| 37 |
+
colorFrom: ${colorFrom}
|
| 38 |
+
colorTo: ${colorTo}
|
| 39 |
+
emoji: 🐳
|
| 40 |
+
sdk: static
|
| 41 |
+
pinned: false
|
| 42 |
+
tags:
|
| 43 |
+
- deepsite-v3
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
# Welcome to your new DeepSite project!
|
| 47 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 48 |
+
`;
|
| 49 |
+
|
| 50 |
+
const files: File[] = [];
|
| 51 |
+
const readmeFile = new File([README], "README.md", { type: "text/markdown" });
|
| 52 |
+
files.push(readmeFile);
|
| 53 |
+
pages.forEach((page: Page) => {
|
| 54 |
+
// Determine MIME type based on file extension
|
| 55 |
+
let mimeType = "text/html";
|
| 56 |
+
if (page.path.endsWith(".css")) {
|
| 57 |
+
mimeType = "text/css";
|
| 58 |
+
} else if (page.path.endsWith(".js")) {
|
| 59 |
+
mimeType = "text/javascript";
|
| 60 |
+
} else if (page.path.endsWith(".json")) {
|
| 61 |
+
mimeType = "application/json";
|
| 62 |
+
}
|
| 63 |
+
// Inject the DeepSite badge script into index pages only (not components or other HTML files)
|
| 64 |
+
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 65 |
+
? injectDeepSiteBadge(page.html)
|
| 66 |
+
: page.html;
|
| 67 |
+
const file = new File([content], page.path, { type: mimeType });
|
| 68 |
+
files.push(file);
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
try {
|
| 72 |
+
const { repoUrl} = await createRepo({
|
| 73 |
+
repo,
|
| 74 |
+
accessToken: user.token as string,
|
| 75 |
+
});
|
| 76 |
+
const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
|
| 77 |
+
await uploadFiles({
|
| 78 |
+
repo,
|
| 79 |
+
files,
|
| 80 |
+
accessToken: user.token as string,
|
| 81 |
+
commitTitle
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
const path = repoUrl.split("/").slice(-2).join("/");
|
| 85 |
+
|
| 86 |
+
const commits: Commit[] = [];
|
| 87 |
+
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
| 88 |
+
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
|
| 89 |
+
continue;
|
| 90 |
+
}
|
| 91 |
+
commits.push({
|
| 92 |
+
title: commit.title,
|
| 93 |
+
oid: commit.oid,
|
| 94 |
+
date: commit.date,
|
| 95 |
+
});
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
const space = await spaceInfo({
|
| 99 |
+
name: repo.name,
|
| 100 |
+
accessToken: user.token as string,
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
let newProject = {
|
| 104 |
+
files,
|
| 105 |
+
pages,
|
| 106 |
+
commits,
|
| 107 |
+
project: {
|
| 108 |
+
id: space.id,
|
| 109 |
+
space_id: space.name,
|
| 110 |
+
_updatedAt: space.updatedAt,
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
|
| 115 |
+
} catch (err: any) {
|
| 116 |
+
return NextResponse.json(
|
| 117 |
+
{ error: err.message, ok: false },
|
| 118 |
+
{ status: 500 }
|
| 119 |
+
);
|
| 120 |
+
}
|
| 121 |
+
}
|
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,92 +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 |
-
|
| 56 |
-
// Escape YAML values to prevent injection attacks
|
| 57 |
-
const escapeYamlValue = (value: string): string => {
|
| 58 |
-
if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) {
|
| 59 |
-
return `"${value.replace(/"/g, '\\"')}"`;
|
| 60 |
-
}
|
| 61 |
-
return value;
|
| 62 |
-
};
|
| 63 |
-
|
| 64 |
-
// Escape commit message to prevent injection
|
| 65 |
-
const escapeCommitMessage = (message: string): string => {
|
| 66 |
-
return message.replace(/[\r\n]/g, " ").slice(0, 200);
|
| 67 |
-
};
|
| 68 |
-
|
| 69 |
-
const updatedReadmeFile = readmeFile.replace(
|
| 70 |
-
/^title:\s*(.*)$/m,
|
| 71 |
-
`title: ${escapeYamlValue(newTitle)}`
|
| 72 |
-
);
|
| 73 |
-
|
| 74 |
-
await uploadFile({
|
| 75 |
-
repo,
|
| 76 |
-
accessToken: token,
|
| 77 |
-
file: new File([updatedReadmeFile], "README.md", { type: "text/markdown" }),
|
| 78 |
-
commitTitle: escapeCommitMessage(
|
| 79 |
-
`🐳 ${format(new Date(), "dd/MM")} - ${format(
|
| 80 |
-
new Date(),
|
| 81 |
-
"HH:mm"
|
| 82 |
-
)} - Rename project to "${newTitle}"`
|
| 83 |
-
),
|
| 84 |
-
});
|
| 85 |
-
|
| 86 |
-
return NextResponse.json(
|
| 87 |
-
{
|
| 88 |
-
success: true,
|
| 89 |
-
},
|
| 90 |
-
{ status: 200 }
|
| 91 |
-
);
|
| 92 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/projects/[repoId]/route.ts
DELETED
|
@@ -1,104 +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 |
-
// Escape commit title to prevent injection
|
| 46 |
-
const escapeCommitTitle = (title: string): string => {
|
| 47 |
-
return title.replace(/[\r\n]/g, " ").slice(0, 200);
|
| 48 |
-
};
|
| 49 |
-
|
| 50 |
-
const baseTitle = isManualChanges
|
| 51 |
-
? ""
|
| 52 |
-
: `🐳 ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `;
|
| 53 |
-
const commitTitle = escapeCommitTitle(
|
| 54 |
-
baseTitle + (prompt ?? "Follow-up DeepSite commit")
|
| 55 |
-
);
|
| 56 |
-
const response = await uploadFiles({
|
| 57 |
-
repo,
|
| 58 |
-
files: filesToUpload,
|
| 59 |
-
accessToken: token,
|
| 60 |
-
commitTitle,
|
| 61 |
-
});
|
| 62 |
-
|
| 63 |
-
return NextResponse.json(
|
| 64 |
-
{
|
| 65 |
-
success: true,
|
| 66 |
-
commit: {
|
| 67 |
-
oid: response.commit,
|
| 68 |
-
title: commitTitle,
|
| 69 |
-
date: new Date(),
|
| 70 |
-
},
|
| 71 |
-
},
|
| 72 |
-
{ status: 200 }
|
| 73 |
-
);
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
export async function DELETE(
|
| 77 |
-
request: Request,
|
| 78 |
-
{ params }: { params: Promise<{ repoId: string }> }
|
| 79 |
-
) {
|
| 80 |
-
const { repoId }: { repoId: string } = await params;
|
| 81 |
-
const session = await auth();
|
| 82 |
-
if (!session) {
|
| 83 |
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 84 |
-
}
|
| 85 |
-
const token = session.accessToken;
|
| 86 |
-
|
| 87 |
-
const repo: RepoDesignation = {
|
| 88 |
-
type: "space",
|
| 89 |
-
name: session.user?.username + "/" + repoId,
|
| 90 |
-
};
|
| 91 |
-
|
| 92 |
-
try {
|
| 93 |
-
await deleteRepo({
|
| 94 |
-
repo,
|
| 95 |
-
accessToken: token as string,
|
| 96 |
-
});
|
| 97 |
-
|
| 98 |
-
return NextResponse.json({ success: true }, { status: 200 });
|
| 99 |
-
} catch (error) {
|
| 100 |
-
const errMsg =
|
| 101 |
-
error instanceof Error ? error.message : "Failed to delete project";
|
| 102 |
-
return NextResponse.json({ error: errMsg }, { status: 500 });
|
| 103 |
-
}
|
| 104 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/projects/route.ts
DELETED
|
@@ -1,145 +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 |
-
// todo: catch error while publishing project, and return the error to the user
|
| 13 |
-
// if space has been created, but can't push, try again or catch well the error and return the error to the user
|
| 14 |
-
|
| 15 |
-
export async function POST(request: Request) {
|
| 16 |
-
const session = await auth();
|
| 17 |
-
if (!session) {
|
| 18 |
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 19 |
-
}
|
| 20 |
-
const token = session.accessToken;
|
| 21 |
-
|
| 22 |
-
const body = await request.json();
|
| 23 |
-
const { projectTitle, files, prompt } = body;
|
| 24 |
-
|
| 25 |
-
if (!files) {
|
| 26 |
-
return NextResponse.json(
|
| 27 |
-
{ error: "Project title and files are required" },
|
| 28 |
-
{ status: 400 }
|
| 29 |
-
);
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
const title =
|
| 33 |
-
projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project";
|
| 34 |
-
|
| 35 |
-
let formattedTitle = title
|
| 36 |
-
.toLowerCase()
|
| 37 |
-
.replace(/[^a-z0-9]+/g, "-")
|
| 38 |
-
.split("-")
|
| 39 |
-
.filter(Boolean)
|
| 40 |
-
.join("-")
|
| 41 |
-
.slice(0, 75);
|
| 42 |
-
|
| 43 |
-
formattedTitle =
|
| 44 |
-
formattedTitle + "-" + Math.random().toString(36).substring(2, 7);
|
| 45 |
-
|
| 46 |
-
const repo: RepoDesignation = {
|
| 47 |
-
type: "space",
|
| 48 |
-
name: session.user?.username + "/" + formattedTitle,
|
| 49 |
-
};
|
| 50 |
-
|
| 51 |
-
// Escape YAML values to prevent injection attacks
|
| 52 |
-
const escapeYamlValue = (value: string): string => {
|
| 53 |
-
if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) {
|
| 54 |
-
return `"${value.replace(/"/g, '\\"')}"`;
|
| 55 |
-
}
|
| 56 |
-
return value;
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
// Escape markdown headers to prevent injection
|
| 60 |
-
const escapeMarkdownHeader = (value: string): string => {
|
| 61 |
-
return value.replace(/^#+\s*/g, "").replace(/\n/g, " ");
|
| 62 |
-
};
|
| 63 |
-
|
| 64 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 65 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 66 |
-
const emoji =
|
| 67 |
-
EMOJIS_FOR_SPACE[Math.floor(Math.random() * EMOJIS_FOR_SPACE.length)];
|
| 68 |
-
const README = `---
|
| 69 |
-
title: ${escapeYamlValue(projectTitle)}
|
| 70 |
-
colorFrom: ${colorFrom}
|
| 71 |
-
colorTo: ${colorTo}
|
| 72 |
-
sdk: static
|
| 73 |
-
emoji: ${emoji}
|
| 74 |
-
tags:
|
| 75 |
-
- deepsite-v4
|
| 76 |
-
---
|
| 77 |
-
|
| 78 |
-
# ${escapeMarkdownHeader(title)}
|
| 79 |
-
|
| 80 |
-
This project has been created with [DeepSite](https://deepsite.hf.co) AI Vibe Coding.
|
| 81 |
-
`;
|
| 82 |
-
|
| 83 |
-
const filesToUpload: File[] = [
|
| 84 |
-
new File([README], "README.md", { type: "text/markdown" }),
|
| 85 |
-
];
|
| 86 |
-
for (const file of files) {
|
| 87 |
-
let mimeType = "text/html";
|
| 88 |
-
if (file.path.endsWith(".css")) {
|
| 89 |
-
mimeType = "text/css";
|
| 90 |
-
} else if (file.path.endsWith(".js")) {
|
| 91 |
-
mimeType = "text/javascript";
|
| 92 |
-
}
|
| 93 |
-
const content =
|
| 94 |
-
mimeType === "text/html" && isIndexPage(file.path)
|
| 95 |
-
? injectDeepSiteBadge(file.content)
|
| 96 |
-
: file.content;
|
| 97 |
-
|
| 98 |
-
filesToUpload.push(new File([content], file.path, { type: mimeType }));
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
let repoUrl: string | undefined;
|
| 102 |
-
|
| 103 |
-
try {
|
| 104 |
-
// Create the space first
|
| 105 |
-
const createResult = await createRepo({
|
| 106 |
-
accessToken: token as string,
|
| 107 |
-
repo: repo,
|
| 108 |
-
sdk: "static",
|
| 109 |
-
});
|
| 110 |
-
repoUrl = createResult.repoUrl;
|
| 111 |
-
|
| 112 |
-
// Escape commit message to prevent injection
|
| 113 |
-
const escapeCommitMessage = (message: string): string => {
|
| 114 |
-
return message.replace(/[\r\n]/g, " ").slice(0, 200);
|
| 115 |
-
};
|
| 116 |
-
const commitMessage = escapeCommitMessage(prompt ?? "Initial DeepSite commit");
|
| 117 |
-
|
| 118 |
-
// Upload files to the created space
|
| 119 |
-
await uploadFiles({
|
| 120 |
-
repo,
|
| 121 |
-
files: filesToUpload,
|
| 122 |
-
accessToken: token as string,
|
| 123 |
-
commitTitle: commitMessage,
|
| 124 |
-
});
|
| 125 |
-
|
| 126 |
-
const path = repoUrl.split("/").slice(-2).join("/");
|
| 127 |
-
|
| 128 |
-
return NextResponse.json({ repoUrl: path }, { status: 200 });
|
| 129 |
-
} catch (error) {
|
| 130 |
-
const errMsg =
|
| 131 |
-
error instanceof Error ? error.message : "Failed to create or upload to space";
|
| 132 |
-
|
| 133 |
-
// If space was created but upload failed, include the repo URL in the error
|
| 134 |
-
if (repoUrl) {
|
| 135 |
-
const path = repoUrl.split("/").slice(-2).join("/");
|
| 136 |
-
return NextResponse.json({
|
| 137 |
-
error: `${errMsg}. Space was created at ${path} but files could not be uploaded.`,
|
| 138 |
-
repoUrl: path,
|
| 139 |
-
partialSuccess: true
|
| 140 |
-
}, { status: 500 });
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
return NextResponse.json({ error: errMsg }, { status: 500 });
|
| 144 |
-
}
|
| 145 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/{redesign → re-design}/route.ts
RENAMED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
import { NextRequest, NextResponse } from "next/server";
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
export async function PUT(request: NextRequest) {
|
| 8 |
const body = await request.json();
|
|
@@ -13,6 +15,7 @@ export async function PUT(request: NextRequest) {
|
|
| 13 |
}
|
| 14 |
|
| 15 |
try {
|
|
|
|
| 16 |
const controller = new AbortController();
|
| 17 |
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
| 18 |
|
|
@@ -24,9 +27,9 @@ export async function PUT(request: NextRequest) {
|
|
| 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" },
|
|
@@ -43,25 +46,20 @@ export async function PUT(request: NextRequest) {
|
|
| 43 |
);
|
| 44 |
} catch (fetchError: any) {
|
| 45 |
clearTimeout(timeoutId);
|
| 46 |
-
|
| 47 |
-
if (fetchError.name ===
|
| 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 ===
|
| 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 |
}
|
|
|
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
|
| 3 |
+
// Timeout configuration (in milliseconds)
|
| 4 |
+
const FETCH_TIMEOUT = 30000; // 30 seconds for external fetch
|
| 5 |
+
|
| 6 |
+
// Extend the maximum execution time for this route
|
| 7 |
+
export const maxDuration = 60; // 1 minute
|
| 8 |
|
| 9 |
export async function PUT(request: NextRequest) {
|
| 10 |
const body = await request.json();
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
try {
|
| 18 |
+
// Create an AbortController for timeout
|
| 19 |
const controller = new AbortController();
|
| 20 |
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
| 21 |
|
|
|
|
| 27 |
signal: controller.signal,
|
| 28 |
}
|
| 29 |
);
|
| 30 |
+
|
| 31 |
clearTimeout(timeoutId);
|
| 32 |
+
|
| 33 |
if (!response.ok) {
|
| 34 |
return NextResponse.json(
|
| 35 |
{ error: "Failed to fetch redesign" },
|
|
|
|
| 46 |
);
|
| 47 |
} catch (fetchError: any) {
|
| 48 |
clearTimeout(timeoutId);
|
| 49 |
+
|
| 50 |
+
if (fetchError.name === 'AbortError') {
|
| 51 |
return NextResponse.json(
|
| 52 |
+
{ error: "Request timeout: The external service took too long to respond. Please try again." },
|
|
|
|
|
|
|
|
|
|
| 53 |
{ status: 504 }
|
| 54 |
);
|
| 55 |
}
|
| 56 |
throw fetchError;
|
| 57 |
}
|
| 58 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 59 |
} catch (error: any) {
|
| 60 |
+
if (error.name === 'AbortError' || error.message?.includes('timeout')) {
|
| 61 |
return NextResponse.json(
|
| 62 |
+
{ error: "Request timeout: The external service took too long to respond. Please try again." },
|
|
|
|
|
|
|
|
|
|
| 63 |
{ status: 504 }
|
| 64 |
);
|
| 65 |
}
|
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,28 @@
|
|
|
|
|
| 1 |
import type { Metadata, Viewport } from "next";
|
| 2 |
-
import {
|
| 3 |
-
import { NextStepProvider } from "nextstepjs";
|
| 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 = {
|
|
@@ -46,28 +50,68 @@ 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 |
<body
|
| 70 |
-
className={`${
|
| 71 |
>
|
| 72 |
<script
|
| 73 |
type="application/ld+json"
|
|
@@ -86,22 +130,15 @@ export default async function RootLayout({
|
|
| 86 |
data-domain="deepsite.hf.co"
|
| 87 |
src="https://plausible.io/js/script.js"
|
| 88 |
/>
|
| 89 |
-
<
|
| 90 |
-
<
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
<NextStepProvider>
|
| 99 |
-
{children}
|
| 100 |
-
<NotAuthorizedDomain />
|
| 101 |
-
</NextStepProvider>
|
| 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 Script from "next/script";
|
| 5 |
+
import { headers } from "next/headers";
|
| 6 |
+
import { redirect } from "next/navigation";
|
| 7 |
|
| 8 |
+
import "@/assets/globals.css";
|
|
|
|
|
|
|
| 9 |
import { Toaster } from "@/components/ui/sonner";
|
| 10 |
+
import IframeDetector from "@/components/iframe-detector";
|
| 11 |
+
import AppContext from "@/components/contexts/app-context";
|
| 12 |
+
import TanstackContext from "@/components/contexts/tanstack-query-context";
|
| 13 |
+
import { LoginProvider } from "@/components/contexts/login-context";
|
| 14 |
+
import { ProProvider } from "@/components/contexts/pro-context";
|
| 15 |
import { generateSEO, generateStructuredData } from "@/lib/seo";
|
|
|
|
| 16 |
|
| 17 |
+
const inter = Inter({
|
| 18 |
+
variable: "--font-inter-sans",
|
| 19 |
subsets: ["latin"],
|
| 20 |
});
|
| 21 |
|
| 22 |
+
const ptSans = PT_Sans({
|
| 23 |
+
variable: "--font-ptSans-mono",
|
| 24 |
subsets: ["latin"],
|
| 25 |
+
weight: ["400", "700"],
|
| 26 |
});
|
| 27 |
|
| 28 |
export const metadata: Metadata = {
|
|
|
|
| 50 |
export const viewport: Viewport = {
|
| 51 |
initialScale: 1,
|
| 52 |
maximumScale: 1,
|
| 53 |
+
themeColor: "#000000",
|
| 54 |
};
|
| 55 |
|
| 56 |
+
// async function getMe() {
|
| 57 |
+
// const cookieStore = await cookies();
|
| 58 |
+
// const cookieName = MY_TOKEN_KEY();
|
| 59 |
+
// const token = cookieStore.get(cookieName)?.value;
|
| 60 |
+
|
| 61 |
+
// if (!token) return { user: null, projects: [], errCode: null };
|
| 62 |
+
// try {
|
| 63 |
+
// const res = await apiServer.get("/me", {
|
| 64 |
+
// headers: {
|
| 65 |
+
// Authorization: `Bearer ${token}`,
|
| 66 |
+
// },
|
| 67 |
+
// });
|
| 68 |
+
// return { user: res.data.user, projects: res.data.projects, errCode: null };
|
| 69 |
+
// } catch (err: any) {
|
| 70 |
+
// return { user: null, projects: [], errCode: err.status };
|
| 71 |
+
// }
|
| 72 |
+
// }
|
| 73 |
+
|
| 74 |
export default async function RootLayout({
|
| 75 |
children,
|
| 76 |
}: Readonly<{
|
| 77 |
children: React.ReactNode;
|
| 78 |
}>) {
|
| 79 |
+
// Domain redirect check
|
| 80 |
+
const headersList = await headers();
|
| 81 |
+
const forwardedHost = headersList.get("x-forwarded-host");
|
| 82 |
+
const host = headersList.get("host");
|
| 83 |
+
const hostname = (forwardedHost || host || "").split(":")[0];
|
| 84 |
+
|
| 85 |
+
const isLocalDev =
|
| 86 |
+
hostname === "localhost" ||
|
| 87 |
+
hostname === "127.0.0.1" ||
|
| 88 |
+
hostname.startsWith("192.168.");
|
| 89 |
+
const isHuggingFace =
|
| 90 |
+
hostname === "huggingface.co" || hostname.endsWith(".huggingface.co");
|
| 91 |
+
|
| 92 |
+
if (!isHuggingFace && !isLocalDev) {
|
| 93 |
+
const pathname = headersList.get("x-invoke-path") || "/deepsite";
|
| 94 |
+
redirect(`https://huggingface.co${pathname}`);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// const data = await getMe();
|
| 98 |
+
|
| 99 |
+
// Generate structured data
|
| 100 |
const structuredData = generateStructuredData("WebApplication", {
|
| 101 |
name: "DeepSite",
|
| 102 |
description: "Build websites with AI, no code required",
|
| 103 |
+
url: "https://huggingface.co/deepsite",
|
| 104 |
});
|
| 105 |
+
|
| 106 |
const organizationData = generateStructuredData("Organization", {
|
| 107 |
name: "DeepSite",
|
| 108 |
+
url: "https://huggingface.co/deepsite",
|
| 109 |
});
|
| 110 |
|
| 111 |
return (
|
| 112 |
+
<html lang="en">
|
| 113 |
<body
|
| 114 |
+
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
| 115 |
>
|
| 116 |
<script
|
| 117 |
type="application/ld+json"
|
|
|
|
| 130 |
data-domain="deepsite.hf.co"
|
| 131 |
src="https://plausible.io/js/script.js"
|
| 132 |
/>
|
| 133 |
+
<IframeDetector />
|
| 134 |
+
<Toaster richColors position="bottom-center" />
|
| 135 |
+
<TanstackContext>
|
| 136 |
+
<AppContext>
|
| 137 |
+
<LoginProvider>
|
| 138 |
+
<ProProvider>{children}</ProProvider>
|
| 139 |
+
</LoginProvider>
|
| 140 |
+
</AppContext>
|
| 141 |
+
</TanstackContext>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
</body>
|
| 143 |
</html>
|
| 144 |
);
|
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://huggingface.co/deepsite';
|
| 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,258 @@
|
|
| 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 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
}
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
}
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
position: absolute;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
pointer-events: none;
|
| 148 |
-
|
| 149 |
-
|
| 150 |
}
|
| 151 |
|
| 152 |
-
.
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
.sp-explorer[data-active="true"] {
|
| 156 |
-
@apply text-indigo-500!;
|
| 157 |
}
|
| 158 |
|
| 159 |
-
.
|
| 160 |
-
|
| 161 |
-
.sp-tabs
|
| 162 |
-
.sp-tab-container[aria-selected="true"]
|
| 163 |
-
.sp-tab-button {
|
| 164 |
-
@apply text-indigo-500!;
|
| 165 |
-
}
|
| 166 |
-
.sp-layout .sp-stack .sp-tabs .sp-tab-container:has(button:focus) {
|
| 167 |
-
@apply outline-none! border-none!;
|
| 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("/deepsite/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 |
}
|
| 372 |
|
| 373 |
+
.transparent-scroll {
|
| 374 |
+
scrollbar-width: none; /* Firefox */
|
| 375 |
+
-ms-overflow-style: none; /* IE and Edge */
|
|
|
|
|
|
|
| 376 |
}
|
| 377 |
|
| 378 |
+
.transparent-scroll::-webkit-scrollbar {
|
| 379 |
+
display: none; /* Chrome, Safari, Opera */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
}
|
assets/hf-logo.svg
DELETED
assets/logo.svg
ADDED
|
|
assets/pro.svg
DELETED
chart/Chart.yaml
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
apiVersion: v2
|
| 2 |
-
name: deepsite
|
| 3 |
-
version: 0.0.0-latest
|
| 4 |
-
type: application
|
| 5 |
-
icon: https://huggingface.co/front/assets/huggingface_logo-noborder.svg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chart/env/prod.yaml
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
nodeSelector:
|
| 2 |
-
role-deepsite: "true"
|
| 3 |
-
|
| 4 |
-
tolerations:
|
| 5 |
-
- key: "huggingface.co/deepsite"
|
| 6 |
-
operator: "Equal"
|
| 7 |
-
value: "true"
|
| 8 |
-
effect: "NoSchedule"
|
| 9 |
-
|
| 10 |
-
serviceAccount:
|
| 11 |
-
enabled: true
|
| 12 |
-
create: true
|
| 13 |
-
name: deepsite-prod
|
| 14 |
-
|
| 15 |
-
ingress:
|
| 16 |
-
path: "/"
|
| 17 |
-
annotations:
|
| 18 |
-
alb.ingress.kubernetes.io/healthcheck-path: "/api/healthcheck"
|
| 19 |
-
alb.ingress.kubernetes.io/listen-ports: "[{\"HTTP\": 80}, {\"HTTPS\": 443}]"
|
| 20 |
-
alb.ingress.kubernetes.io/load-balancer-name: "hub-utils-prod-cloudfront"
|
| 21 |
-
alb.ingress.kubernetes.io/group.name: "hub-utils-prod-cloudfront"
|
| 22 |
-
alb.ingress.kubernetes.io/scheme: "internal"
|
| 23 |
-
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
| 24 |
-
alb.ingress.kubernetes.io/tags: "Env=prod,Project=hub,Terraform=true"
|
| 25 |
-
alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30
|
| 26 |
-
alb.ingress.kubernetes.io/target-type: "ip"
|
| 27 |
-
alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-east-1:707930574880:certificate/5b25b145-75db-4837-b9f3-7f238ba8a9c7,arn:aws:acm:us-east-1:707930574880:certificate/bfdf509c-f44b-400f-b9e1-6f7a861abe91"
|
| 28 |
-
kubernetes.io/ingress.class: "alb"
|
| 29 |
-
|
| 30 |
-
networkPolicy:
|
| 31 |
-
enabled: true
|
| 32 |
-
allowedBlocks:
|
| 33 |
-
- 10.0.0.0/16
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
ingressInternal:
|
| 37 |
-
enabled: false
|
| 38 |
-
|
| 39 |
-
envVars:
|
| 40 |
-
NEXTAUTH_URL: https://deepsite.hf.co/api/auth
|
| 41 |
-
|
| 42 |
-
infisical:
|
| 43 |
-
enabled: true
|
| 44 |
-
env: "prod-us-east-1"
|
| 45 |
-
|
| 46 |
-
autoscaling:
|
| 47 |
-
enabled: true
|
| 48 |
-
minReplicas: 1
|
| 49 |
-
maxReplicas: 10
|
| 50 |
-
targetMemoryUtilizationPercentage: "50"
|
| 51 |
-
targetCPUUtilizationPercentage: "50"
|
| 52 |
-
|
| 53 |
-
resources:
|
| 54 |
-
requests:
|
| 55 |
-
cpu: 2
|
| 56 |
-
memory: 4Gi
|
| 57 |
-
limits:
|
| 58 |
-
cpu: 4
|
| 59 |
-
memory: 8Gi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chart/templates/_helpers.tpl
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
{{- define "name" -}}
|
| 2 |
-
{{- default $.Release.Name | trunc 63 | trimSuffix "-" -}}
|
| 3 |
-
{{- end -}}
|
| 4 |
-
|
| 5 |
-
{{- define "app.name" -}}
|
| 6 |
-
chat-ui
|
| 7 |
-
{{- end -}}
|
| 8 |
-
|
| 9 |
-
{{- define "labels.standard" -}}
|
| 10 |
-
release: {{ $.Release.Name | quote }}
|
| 11 |
-
heritage: {{ $.Release.Service | quote }}
|
| 12 |
-
chart: "{{ include "name" . }}"
|
| 13 |
-
app: "{{ include "app.name" . }}"
|
| 14 |
-
{{- end -}}
|
| 15 |
-
|
| 16 |
-
{{- define "labels.resolver" -}}
|
| 17 |
-
release: {{ $.Release.Name | quote }}
|
| 18 |
-
heritage: {{ $.Release.Service | quote }}
|
| 19 |
-
chart: "{{ include "name" . }}"
|
| 20 |
-
app: "{{ include "app.name" . }}-resolver"
|
| 21 |
-
{{- end -}}
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|