diff --git a/.env.example b/.env.example deleted file mode 100644 index d7419a983b3e8d6606db990addea8d78dd660692..0000000000000000000000000000000000000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -AUTH_HUGGINGFACE_ID= -AUTH_HUGGINGFACE_SECRET= -NEXTAUTH_URL=http://localhost:3001 -AUTH_SECRET= \ No newline at end of file diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml deleted file mode 100644 index 88d7e85dd2f708097a42154a349c9cb217479120..0000000000000000000000000000000000000000 --- a/.github/workflows/deploy-prod.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Deploy to k8s -on: - # run this workflow manually from the Actions tab - workflow_dispatch: - -jobs: - build-and-publish: - runs-on: - group: cpu-high - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to Registry - uses: docker/login-action@v3 - with: - registry: registry.internal.huggingface.tech - username: ${{ secrets.DOCKER_INTERNAL_USERNAME }} - password: ${{ secrets.DOCKER_INTERNAL_PASSWORD }} - - - name: Docker metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: | - registry.internal.huggingface.tech/deepsite/deepsite - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=sha,enable=true,prefix=sha-,format=short,sha-len=8 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@v4 - - - name: Build and Publish image - uses: docker/build-push-action@v5 - with: - context: . - file: Dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64 - cache-to: type=gha,mode=max,scope=amd64 - cache-from: type=gha,scope=amd64 - provenance: false - - deploy: - name: Deploy on prod - runs-on: ubuntu-latest - needs: ["build-and-publish"] - steps: - - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@v4 - - - name: Gen values - run: | - VALUES=$(cat <<-END - image: - tag: "sha-${{ env.GITHUB_SHA_SHORT }}" - END - ) - echo "VALUES=$(echo "$VALUES" | yq -o=json | jq tostring)" >> $GITHUB_ENV - - - name: Deploy on infra-deployments - uses: the-actions-org/workflow-dispatch@v2 - with: - workflow: Update application single value - repo: huggingface/infra-deployments - wait-for-completion: true - wait-for-completion-interval: 10s - display-workflow-run-url-interval: 10s - ref: refs/heads/main - token: ${{ secrets.GIT_TOKEN_INFRA_DEPLOYMENT }} - inputs: '{"path": "hub/deepsite/deepsite.yaml", "value": ${{ env.VALUES }}, "url": "${{ github.event.head_commit.url }}"}' diff --git a/.gitignore b/.gitignore index b8b89cbb382fac7dea7fdeb461bd43beb9937c49..5ef6a520780202a1d6addd833d800ccb1ecac0bb 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env +.env* # vercel .vercel @@ -39,9 +39,3 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts - -.idea - -# binary assets (hosted on CDN) -assets/assistant.jpg -.gitattributes \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a2b0759c0a612c3be02d8c1eb3021252cdd4a7f8..cbe0188aaee92186937765d2c85d76f7b212c537 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,19 @@ FROM node:20-alpine USER root -# Install pnpm -RUN corepack enable && corepack prepare pnpm@latest --activate - USER 1000 WORKDIR /usr/src/app -# Copy package.json and pnpm-lock.yaml to the container -COPY --chown=1000 package.json pnpm-lock.yaml ./ +# Copy package.json and package-lock.json to the container +COPY --chown=1000 package.json package-lock.json ./ # Copy the rest of the application files to the container COPY --chown=1000 . . -RUN pnpm install -RUN pnpm run build +RUN npm install +RUN npm run build # Expose the application port (assuming your app runs on port 3000) -EXPOSE 3001 +EXPOSE 3000 # Start the application -CMD ["pnpm", "start"] \ No newline at end of file +CMD ["npm", "start"] \ No newline at end of file diff --git a/MCP-SERVER.md b/MCP-SERVER.md new file mode 100644 index 0000000000000000000000000000000000000000..675acdbb77beeb3bdc3e0b7b49a2e01d39df1b58 --- /dev/null +++ b/MCP-SERVER.md @@ -0,0 +1,428 @@ +# DeepSite MCP Server + +DeepSite is now available as an MCP (Model Context Protocol) server, enabling AI assistants like Claude to create websites directly using natural language. + +## Two Ways to Use DeepSite MCP + +**Quick Comparison:** + +| Feature | Option 1: HTTP Server | Option 2: Local Server | +|---------|----------------------|------------------------| +| **Setup Difficulty** | βœ… Easy (just config) | ⚠️ Requires installation | +| **Authentication** | HF Token in config header | HF Token or session cookie in env | +| **Best For** | Most users | Developers, custom modifications | +| **Maintenance** | βœ… Always up-to-date | Need to rebuild for updates | + +**Recommendation:** Use Option 1 (HTTP Server) unless you need to modify the MCP server code. + +--- + +### 🌐 Option 1: HTTP Server (Recommended) + +**No installation required!** Use DeepSite's hosted MCP server. + +#### Setup for Claude Desktop + +Add to your Claude Desktop configuration file: + +**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "deepsite": { + "url": "https://huggingface.co/deepsite/api/mcp", + "transport": { + "type": "sse" + }, + "headers": { + "Authorization": "Bearer hf_your_token_here" + } + } + } +} +``` + +**Getting Your Hugging Face Token:** + +1. Go to https://huggingface.co/settings/tokens +2. Create a new token with `write` access +3. Copy the token +4. Add it to the `Authorization` header in your config (recommended for security) +5. Alternatively, you can pass it as the `hf_token` parameter when using the tool + +**⚠️ 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. + +#### Example Usage with Claude + +> "Create a portfolio website using DeepSite. Include a hero section, about section, and contact form." + +Claude will automatically: +1. Use the `create_project` tool +2. Authenticate using the token from your config +3. Create the website on Hugging Face Spaces +4. Return the URLs to access your new site + +--- + +### πŸ’» Option 2: Local Server + +Run the MCP server locally for more control or offline use. + +> **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. + +#### Installation + +```bash +cd mcp-server +npm install +npm run build +``` + +#### Setup for Claude Desktop + +**Method A: Using HF Token (Recommended)** + +```json +{ + "mcpServers": { + "deepsite-local": { + "command": "node", + "args": ["/absolute/path/to/deepsite-v3/mcp-server/dist/index.js"], + "env": { + "HF_TOKEN": "hf_your_token_here", + "DEEPSITE_API_URL": "https://huggingface.co/deepsite" + } + } + } +} +``` + +**Method B: Using Session Cookie (Alternative)** + +```json +{ + "mcpServers": { + "deepsite-local": { + "command": "node", + "args": ["/absolute/path/to/deepsite-v3/mcp-server/dist/index.js"], + "env": { + "DEEPSITE_AUTH_COOKIE": "your-session-cookie", + "DEEPSITE_API_URL": "https://huggingface.co/deepsite" + } + } + } +} +``` + +**Getting Your Session Cookie (Method B only):** + +1. Log in to https://huggingface.co/deepsite +2. Open Developer Tools (F12) +3. Go to Application β†’ Cookies +4. Copy the session cookie value +5. Set as `DEEPSITE_AUTH_COOKIE` in the config + +--- + +## Available Tools + +### `create_project` + +Creates a new DeepSite project with HTML/CSS/JS files. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `title` | string | No | Project title (defaults to "DeepSite Project") | +| `pages` | array | Yes | Array of file objects with `path` and `html` | +| `prompt` | string | No | Commit message/description | +| `hf_token` | string | No* | Hugging Face API token (*optional if provided via Authorization header in config) | + +**Page Object:** +```typescript +{ + path: string; // e.g., "index.html", "styles.css", "script.js" + html: string; // File content +} +``` + +**Returns:** +```json +{ + "success": true, + "message": "Project created successfully!", + "projectUrl": "https://huggingface.co/deepsite/username/project-name", + "spaceUrl": "https://huggingface.co/spaces/username/project-name", + "liveUrl": "https://username-project-name.hf.space", + "spaceId": "username/project-name", + "projectId": "space-id", + "files": ["index.html", "styles.css"] +} +``` + +--- + +## Example Prompts for Claude + +### Simple Landing Page +> "Create a modern landing page for my SaaS product using DeepSite. Include a hero section with CTA, features grid, and footer. Use gradient background." + +### Portfolio Website +> "Build a portfolio website with DeepSite. I need: +> - Hero section with my name and photo +> - Projects gallery with 3 sample projects +> - Skills section with tech stack +> - Contact form +> Use dark mode with accent colors." + +### Blog Homepage +> "Create a blog homepage using DeepSite. Include: +> - Header with navigation +> - Featured post section +> - Grid of recent posts (3 cards) +> - Sidebar with categories +> - Footer with social links +> Clean, minimal design." + +### Interactive Dashboard +> "Make an analytics dashboard with DeepSite: +> - Sidebar navigation +> - 4 metric cards at top +> - 2 chart placeholders +> - Data table +> - Modern, professional UI with charts.css" + +--- + +## Direct API Usage + +You can also call the HTTP endpoint directly: + +### Using Authorization Header (Recommended) + +```bash +curl -X POST https://huggingface.co/deepsite/api/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer hf_your_token_here" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "create_project", + "arguments": { + "title": "My Website", + "pages": [ + { + "path": "index.html", + "html": "Hello

Hello World!

" + } + ] + } + } + }' +``` + +### Using Token Parameter (Fallback) + +```bash +curl -X POST https://huggingface.co/deepsite/api/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "create_project", + "arguments": { + "title": "My Website", + "pages": [ + { + "path": "index.html", + "html": "Hello

Hello World!

" + } + ], + "hf_token": "hf_xxxxx" + } + } + }' +``` + +### List Available Tools + +```bash +curl -X POST https://huggingface.co/deepsite/api/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/list", + "params": {} + }' +``` + +--- + +## Testing + +### Test Local Server + +```bash +cd mcp-server +./test.sh +``` + +### Test HTTP Server + +```bash +curl -X POST https://huggingface.co/deepsite/api/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' +``` + +--- + +## Migration Guide: From Parameter to Header Auth + +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: + +### Step 1: Update Your Config + +Edit your Claude Desktop config file and add the `headers` section: + +```json +{ + "mcpServers": { + "deepsite": { + "url": "https://huggingface.co/deepsite/api/mcp", + "transport": { + "type": "sse" + }, + "headers": { + "Authorization": "Bearer hf_your_actual_token_here" + } + } + } +} +``` + +### Step 2: Restart Claude Desktop + +Completely quit and restart Claude Desktop for the changes to take effect. + +### Step 3: Use Simpler Prompts + +Now you can simply say: +> "Create a portfolio website with DeepSite" + +Instead of: +> "Create a portfolio website with DeepSite using token `hf_xxxxx`" + +Your token is automatically included in all requests via the header! + +--- + +## Security Notes + +### HTTP Server (Option 1) +- **βœ… Recommended:** Store your HF token in the `Authorization` header in your Claude Desktop config +- The token is stored locally on your machine and never exposed in chat +- The token is sent with each request but only used to authenticate with Hugging Face API +- DeepSite does not store your token +- Use tokens with minimal required permissions (write access to spaces) +- You can revoke tokens anytime at https://huggingface.co/settings/tokens +- **⚠️ Fallback:** You can still pass the token as a parameter, but this is less secure as it appears in conversation history + +### Local Server (Option 2) +- Use `HF_TOKEN` environment variable (same security as Option 1) +- Or use `DEEPSITE_AUTH_COOKIE` if you prefer session-based auth +- All authentication data stays on your local machine +- Better for development and testing +- No need for both HTTP Server and Local Server - choose one! + +--- + +## Troubleshooting + +### "Invalid Hugging Face token" +- Verify your token at https://huggingface.co/settings/tokens +- Ensure the token has write permissions +- Check that you copied the full token (starts with `hf_`) + +### "At least one page is required" +- Make sure you're providing the `pages` array +- Each page must have both `path` and `html` properties + +### "Failed to create project" +- Check your token permissions +- Ensure the project title doesn't conflict with existing spaces +- Verify your Hugging Face account is in good standing + +### Claude doesn't see the tool +- Restart Claude Desktop after modifying the config +- Check that the JSON config is valid (no trailing commas) +- For HTTP: verify the URL is correct +- For local: check the absolute path to index.js + +--- + +## Architecture + +### HTTP Server Flow +``` +Claude Desktop + ↓ + (HTTP Request) + ↓ +huggingface.co/deepsite/api/mcp + ↓ +Hugging Face API (with user's token) + ↓ +New Space Created + ↓ +URLs returned to Claude +``` + +### Local Server Flow +``` +Claude Desktop + ↓ + (stdio transport) + ↓ +Local MCP Server + ↓ + (HTTP to DeepSite API) + ↓ +huggingface.co/deepsite/api/me/projects + ↓ +New Space Created +``` + +--- + +## Contributing + +The MCP server implementation lives in: +- HTTP Server: `/app/api/mcp/route.ts` +- Local Server: `/mcp-server/index.ts` + +Both use the same core DeepSite logic for creating projects - no duplication! + +--- + +## License + +MIT + +--- + +## Resources + +- [Model Context Protocol Spec](https://modelcontextprotocol.io/) +- [DeepSite Documentation](https://huggingface.co/deepsite) +- [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces) +- [Claude Desktop](https://claude.ai/desktop) + diff --git a/README.md b/README.md index 6b13fd8b95aeb699f979cbc2cbb8d7495aeacdf5..9154f9026a5ccedb6d9aa26c2b34462372d7741b 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,26 @@ --- -title: DeepSite v4 +title: DeepSite v3 emoji: 🐳 colorFrom: blue colorTo: blue sdk: docker pinned: true -app_port: 3001 +app_port: 3000 license: mit failure_strategy: rollback -short_description: Generate any application by Vibe Coding it +short_description: Generate any application by Vibe Coding models: - deepseek-ai/DeepSeek-V3-0324 - - deepseek-ai/DeepSeek-V3.2 - - Qwen/Qwen3-Coder-30B-A3B-Instruct + - deepseek-ai/DeepSeek-R1-0528 + - deepseek-ai/DeepSeek-V3.1 + - deepseek-ai/DeepSeek-V3.1-Terminus + - deepseek-ai/DeepSeek-V3.2-Exp + - Qwen/Qwen3-Coder-480B-A35B-Instruct + - moonshotai/Kimi-K2-Instruct - moonshotai/Kimi-K2-Instruct-0905 - - zai-org/GLM-4.7 - - MiniMaxAI/MiniMax-M2.1 + - zai-org/GLM-4.6 + - MiniMaxAI/MiniMax-M2 + - moonshotai/Kimi-K2-Thinking --- # DeepSite 🐳 diff --git a/actions/mentions.ts b/actions/mentions.ts deleted file mode 100644 index c6b7dabfbaa6f0efe0d492dfe5470353b0139bae..0000000000000000000000000000000000000000 --- a/actions/mentions.ts +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { File } from "@/lib/type"; - -export const searchMentions = async (query: string) => { - const promises = [searchModels(query), searchDatasets(query)]; - const results = await Promise.all(promises); - return { models: results[0], datasets: results[1] }; -}; - -const searchModels = async (query: string) => { - const response = await fetch( - `https://huggingface.co/api/quicksearch?q=${query}&type=model&limit=3` - ); - const data = await response.json(); - return data?.models ?? []; -}; - -const searchDatasets = async (query: string) => { - const response = await fetch( - `https://huggingface.co/api/quicksearch?q=${query}&type=dataset&limit=3` - ); - const data = await response.json(); - return data?.datasets ?? []; -}; - -export const searchFilesMentions = async (query: string, files: File[]) => { - if (!query) return files; - const lowerQuery = query.toLowerCase(); - return files.filter((file) => file.path.toLowerCase().includes(lowerQuery)); -}; diff --git a/actions/projects.ts b/actions/projects.ts deleted file mode 100644 index d6f685b63290792fc597438a53eab0a14d6bbf8e..0000000000000000000000000000000000000000 --- a/actions/projects.ts +++ /dev/null @@ -1,175 +0,0 @@ -"use server"; -import { - downloadFile, - listCommits, - listFiles, - listSpaces, - RepoDesignation, - SpaceEntry, - spaceInfo, -} from "@huggingface/hub"; - -import { auth } from "@/lib/auth"; -import { Commit, File } from "@/lib/type"; - -export interface ProjectWithCommits extends SpaceEntry { - commits?: Commit[]; - medias?: string[]; -} - -const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"]; -const IGNORED_FORMATS = [ - ".png", - ".jpg", - ".jpeg", - ".gif", - ".svg", - ".webp", - ".mp4", - ".mp3", - ".wav", -]; - -export const getProjects = async () => { - const projects: SpaceEntry[] = []; - const session = await auth(); - if (!session?.user) { - return projects; - } - const token = session.accessToken; - for await (const space of listSpaces({ - accessToken: token, - additionalFields: ["author", "cardData"], - search: { - owner: session.user.username, - }, - })) { - if ( - space.sdk === "static" && - Array.isArray((space.cardData as { tags?: string[] })?.tags) && - (space.cardData as { tags?: string[] })?.tags?.some((tag) => - tag.includes("deepsite") - ) - ) { - projects.push(space); - } - } - return projects; -}; -export const getProject = async (id: string, commitId?: string) => { - const session = await auth(); - if (!session?.user) { - return null; - } - const token = session.accessToken; - try { - const project: ProjectWithCommits | null = await spaceInfo({ - name: id, - accessToken: token, - additionalFields: ["author", "cardData"], - }); - const repo: RepoDesignation = { - type: "space", - name: id, - }; - const files: File[] = []; - const medias: string[] = []; - const params = { repo, accessToken: token }; - if (commitId) { - Object.assign(params, { revision: commitId }); - } - for await (const fileInfo of listFiles(params)) { - if (IGNORED_PATHS.includes(fileInfo.path)) continue; - if (IGNORED_FORMATS.some((format) => fileInfo.path.endsWith(format))) { - medias.push( - `https://huggingface.co/spaces/${id}/resolve/main/${fileInfo.path}` - ); - continue; - } - - if (fileInfo.type === "directory") { - for await (const subFile of listFiles({ - repo, - accessToken: token, - path: fileInfo.path, - })) { - if (IGNORED_FORMATS.some((format) => subFile.path.endsWith(format))) { - medias.push( - `https://huggingface.co/spaces/${id}/resolve/main/${subFile.path}` - ); - } - const blob = await downloadFile({ - repo, - accessToken: token, - path: subFile.path, - raw: true, - ...(commitId ? { revision: commitId } : {}), - }).catch((_) => { - return null; - }); - if (!blob) { - continue; - } - const html = await blob?.text(); - if (!html) { - continue; - } - files[subFile.path === "index.html" ? "unshift" : "push"]({ - path: subFile.path, - content: html, - }); - } - } else { - const blob = await downloadFile({ - repo, - accessToken: token, - path: fileInfo.path, - raw: true, - ...(commitId ? { revision: commitId } : {}), - }).catch((_) => { - return null; - }); - if (!blob) { - continue; - } - const html = await blob?.text(); - if (!html) { - continue; - } - files[fileInfo.path === "index.html" ? "unshift" : "push"]({ - path: fileInfo.path, - content: html, - }); - } - } - const commits: Commit[] = []; - const commitIterator = listCommits({ repo, accessToken: token }); - for await (const commit of commitIterator) { - if ( - commit.title?.toLowerCase() === "initial commit" || - commit.title - ?.toLowerCase() - ?.includes("upload media files through deepsite") - ) - continue; - commits.push({ - title: commit.title, - oid: commit.oid, - date: commit.date, - }); - if (commits.length >= 20) { - break; - } - } - - project.commits = commits; - project.medias = medias; - - return { project, files }; - } catch (error) { - return { - project: null, - files: [], - }; - } -}; diff --git a/app/(public)/layout.tsx b/app/(public)/layout.tsx deleted file mode 100644 index 0eb2c8fbb85b745d5be01c9e79fa0ebc93a71d00..0000000000000000000000000000000000000000 --- a/app/(public)/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Navigation } from "@/components/public/navigation"; - -export default function PublicLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( -
- - {children} -
- ); -} diff --git a/app/(public)/page.tsx b/app/(public)/page.tsx deleted file mode 100644 index 86970b7a3dab7e60cce3bbb68c747b525baba45c..0000000000000000000000000000000000000000 --- a/app/(public)/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { AnimatedDotsBackground } from "@/components/public/animated-dots-background"; -import { HeroHeader } from "@/components/public/hero-header"; -import { UserProjects } from "@/components/projects/user-projects"; -import { AskAiLanding } from "@/components/ask-ai/ask-ai-landing"; -import { Bento } from "@/components/public/bento"; - -export const dynamic = "force-dynamic"; - -export default async function Homepage() { - return ( - <> -
- -
- -
-
- -
-
- - - - ); -} diff --git a/app/(public)/signin/page.tsx b/app/(public)/signin/page.tsx deleted file mode 100644 index 3200d0755372c79be57b9204331695cd8f0499ac..0000000000000000000000000000000000000000 --- a/app/(public)/signin/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { LoginButtons } from "@/components/login/login-buttons"; - -export default async function SignInPage({ - searchParams, -}: { - searchParams: Promise<{ callbackUrl: string }>; -}) { - const { callbackUrl } = await searchParams; - console.log(callbackUrl); - return ( -
-
-

You shall not pass πŸ§™

-

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

- -
-
- ); -} diff --git a/app/[owner]/[repoId]/page.tsx b/app/[owner]/[repoId]/page.tsx deleted file mode 100644 index 5082396059ae126dbc2be8e0daf30d1ff014f922..0000000000000000000000000000000000000000 --- a/app/[owner]/[repoId]/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { getProject } from "@/actions/projects"; -import { AppEditor } from "@/components/editor"; -import { auth } from "@/lib/auth"; -import { notFound, redirect } from "next/navigation"; - -export default async function ProjectPage({ - params, - searchParams, -}: { - params: Promise<{ owner: string; repoId: string }>; - searchParams: Promise<{ commit?: string }>; -}) { - const session = await auth(); - - const { owner, repoId } = await params; - const { commit } = await searchParams; - if (!session) { - redirect( - `/api/auth/signin?callbackUrl=/${owner}/${repoId}${ - commit ? `?commit=${commit}` : "" - }` - ); - } - const datas = await getProject(`${owner}/${repoId}`, commit); - if (!datas?.project) { - return notFound(); - } - return ( - - ); -} diff --git a/app/api/ask/route.ts b/app/api/ask/route.ts deleted file mode 100644 index 293e8244308cc1191e32b3ef6b30e93ccb963fcf..0000000000000000000000000000000000000000 --- a/app/api/ask/route.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { NextResponse } from "next/server"; -import { InferenceClient } from "@huggingface/inference"; - -import { FOLLOW_UP_SYSTEM_PROMPT, INITIAL_SYSTEM_PROMPT } from "@/lib/prompts"; -import { auth } from "@/lib/auth"; -import { File, Message } from "@/lib/type"; -import { DEFAULT_MODEL, MODELS } from "@/lib/providers"; - -export async function POST(request: Request) { - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const body = await request.json(); - const { - prompt, - previousMessages = [], - files = [], - provider, - model, - redesignMd, - medias, - } = body; - - if (!prompt) { - return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); - } - if (!model || !MODELS.find((m: (typeof MODELS)[0]) => m.value === model)) { - return NextResponse.json({ error: "Model is required" }, { status: 400 }); - } - - const client = new InferenceClient(token); - - try { - const encoder = new TextEncoder(); - const stream = new TransformStream(); - const writer = stream.writable.getWriter(); - - const response = new NextResponse(stream.readable, { - headers: { - "Content-Type": "text/plain; charset=utf-8", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }); - (async () => { - let hasRetried = false; - let currentModel = model; - - const tryGeneration = async (): Promise => { - try { - const chatCompletion = client.chatCompletionStream({ - model: currentModel + (provider !== "auto" ? `:${provider}` : ""), - messages: [ - { - role: "system", - content: - files.length > 0 - ? FOLLOW_UP_SYSTEM_PROMPT - : INITIAL_SYSTEM_PROMPT, - }, - ...previousMessages.map((message: Message) => ({ - role: message.role, - content: message.content, - })), - ...(files?.length > 0 - ? [ - { - role: "user", - content: `Here are the files that the user has provider:${files - .map( - (file: File) => - `File: ${file.path}\nContent: ${file.content}` - ) - .join("\n")}\n\n${prompt}`, - }, - ] - : []), - { - role: "user", - content: `${ - redesignMd?.url && - `Redesign the following website ${redesignMd.url}, try to use the same images and content, but you can still improve it if needed. Do the best version possibile. Here is the markdown:\n ${redesignMd.md} \n\n` - }${prompt} ${ - medias && medias.length > 0 - ? `\nHere is the list of my media files: ${medias.join( - ", " - )}\n` - : "" - }`, - } - ], - stream: true, - max_tokens: 16_000, - }); - while (true) { - const { done, value } = await chatCompletion.next(); - if (done) { - break; - } - - const chunk = value.choices[0]?.delta?.content; - if (chunk) { - await writer.write(encoder.encode(chunk)); - } - } - - await writer.close(); - } catch (error) { - const errorMessage = - error instanceof Error - ? error.message - : "An error occurred while processing your request"; - - if ( - !hasRetried && - errorMessage?.includes( - "Failed to perform inference: Model not found" - ) - ) { - hasRetried = true; - if (model === DEFAULT_MODEL) { - const availableFallbackModels = MODELS.filter( - (m) => m.value !== model - ); - const randomIndex = Math.floor( - Math.random() * availableFallbackModels.length - ); - currentModel = availableFallbackModels[randomIndex]; - } else { - currentModel = DEFAULT_MODEL; - } - const switchMessage = `\n\n_Note: The selected model was not available. Switched to \`${currentModel}\`._\n\n`; - await writer.write(encoder.encode(switchMessage)); - - return tryGeneration(); - } - - try { - let errorPayload = ""; - if ( - errorMessage?.includes("exceeded your monthly included credits") || - errorMessage?.includes("reached the free monthly usage limit") - ) { - errorPayload = JSON.stringify({ - messageError: errorMessage, - showProMessage: true, - isError: true, - }); - } else { - errorPayload = JSON.stringify({ - messageError: errorMessage, - isError: true, - }); - } - await writer.write(encoder.encode(`\n\n__ERROR__:${errorPayload}`)); - await writer.close(); - } catch (closeError) { - console.error("Failed to send error message:", closeError); - try { - await writer.abort(error); - } catch (abortError) { - console.error("Failed to abort writer:", abortError); - } - } - } - }; - - await tryGeneration(); - })(); - - return response; - } catch (error) { - return NextResponse.json( - { - error: error instanceof Error ? error.message : "Internal Server Error", - }, - { status: 500 } - ); - } -} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 7b38c1bb45e38527a2c595c07df5e70d38ada7d9..0000000000000000000000000000000000000000 --- a/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import NextAuth from "next-auth"; -import { authOptions } from "@/lib/auth"; - -const handler = NextAuth(authOptions); - -export { handler as GET, handler as POST }; diff --git a/app/api/healthcheck/route.ts b/app/api/healthcheck/route.ts deleted file mode 100644 index 35adb415d0f577a7a78dbc338bd6450e78cc9ec3..0000000000000000000000000000000000000000 --- a/app/api/healthcheck/route.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextResponse } from "next/server"; - -export async function GET() { - return NextResponse.json({ status: "ok" }, { status: 200 }); -} diff --git a/app/api/projects/[repoId]/[commitId]/route.ts b/app/api/projects/[repoId]/[commitId]/route.ts deleted file mode 100644 index 0362d6c43ec226bf97837f2f88196ad43eb056f2..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/[commitId]/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { auth } from "@/lib/auth"; -import { createBranch, RepoDesignation } from "@huggingface/hub"; -import { format } from "date-fns"; -import { NextResponse } from "next/server"; - -export async function POST( - request: Request, - { params }: { params: Promise<{ repoId: string; commitId: string }> } -) { - const { repoId, commitId }: { repoId: string; commitId: string } = - await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - const commitTitle = `πŸ”– ${format(new Date(), "dd/MM")} - ${format( - new Date(), - "HH:mm" - )} - Set commit ${commitId} as default.`; - - await fetch( - `https://huggingface.co/api/spaces/${session.user?.username}/${repoId}/branch/main`, - { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - startingPoint: commitId, - overwrite: true, - }), - } - ).catch((error) => { - return NextResponse.json( - { error: error ?? "Failed to create branch" }, - { status: 500 } - ); - }); - - return NextResponse.json({ success: true }, { status: 200 }); -} diff --git a/app/api/projects/[repoId]/download/route.ts b/app/api/projects/[repoId]/download/route.ts deleted file mode 100644 index 43e8f6638d5eb2aca8ba3069d696b05056d96633..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/download/route.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { auth } from "@/lib/auth"; -import { downloadFile, listFiles, RepoDesignation } from "@huggingface/hub"; -import { NextResponse } from "next/server"; -import JSZip from "jszip"; - -export async function GET( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - try { - const zip = new JSZip(); - for await (const fileInfo of listFiles({ - repo, - accessToken: token as string, - recursive: true, - })) { - if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) { - continue; - } - - try { - const blob = await downloadFile({ - repo, - accessToken: token as string, - path: fileInfo.path, - raw: true - }).catch((error) => { - return null; - }); - if (!blob) { - continue; - } - - if (blob) { - const arrayBuffer = await blob.arrayBuffer(); - zip.file(fileInfo.path, arrayBuffer); - } - } catch (error) { - console.error(`Error downloading file ${fileInfo.path}:`, error); - } - } - - const zipBlob = await zip.generateAsync({ - type: "blob", - compression: "DEFLATE", - compressionOptions: { - level: 6 - } - }); - - const projectName = `${session.user?.username}-${repoId}`.replace(/[^a-zA-Z0-9-_]/g, '_'); - const filename = `${projectName}.zip`; - - return new NextResponse(zipBlob, { - headers: { - "Content-Type": "application/zip", - "Content-Disposition": `attachment; filename="${filename}"`, - "Content-Length": zipBlob.size.toString(), - }, - }); - } catch (error) { - console.error("Error downloading project:", error); - return NextResponse.json({ error: "Failed to download project" }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/projects/[repoId]/medias/route.ts b/app/api/projects/[repoId]/medias/route.ts deleted file mode 100644 index 2040623536ab6775152177a317e0b11dc6cae3a2..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/medias/route.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { auth } from "@/lib/auth"; -import { RepoDesignation, uploadFiles } from "@huggingface/hub"; -import { NextResponse } from "next/server"; - -export async function POST( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - const formData = await request.formData(); - const newMedias = formData.getAll("images") as File[]; - - const filesToUpload: File[] = []; - - if (!newMedias || newMedias.length === 0) { - return NextResponse.json( - { - ok: false, - error: "At least one media file is required under the 'images' key", - }, - { status: 400 } - ); - } - - try { - for (const media of newMedias) { - const isImage = media.type.startsWith("image/"); - const isVideo = media.type.startsWith("video/"); - const isAudio = media.type.startsWith("audio/"); - - const folderPath = isImage - ? "images/" - : isVideo - ? "videos/" - : isAudio - ? "audios/" - : null; - - if (!folderPath) { - return NextResponse.json( - { ok: false, error: "Unsupported media type: " + media.type }, - { status: 400 } - ); - } - - const mediaName = `${folderPath}${media.name}`; - const processedFile = new File([media], mediaName, { type: media.type }); - filesToUpload.push(processedFile); - } - - await uploadFiles({ - repo, - files: filesToUpload, - accessToken: token, - commitTitle: `πŸ“ Upload media files through DeepSite`, - }); - - return NextResponse.json( - { - success: true, - medias: filesToUpload.map( - (file) => - `https://huggingface.co/spaces/${session.user?.username}/${repoId}/resolve/main/${file.name}` - ), - }, - { status: 200 } - ); - } catch (error) { - return NextResponse.json( - { ok: false, error: error ?? "Failed to upload media files" }, - { status: 500 } - ); - } - - return NextResponse.json({ success: true }, { status: 200 }); -} diff --git a/app/api/projects/[repoId]/rename/route.ts b/app/api/projects/[repoId]/rename/route.ts deleted file mode 100644 index 1d3a00b676effdc7d9106c02e6d7ec40384026c9..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/rename/route.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { auth } from "@/lib/auth"; -import { downloadFile, RepoDesignation, uploadFile } from "@huggingface/hub"; -import { format } from "date-fns"; -import { NextResponse } from "next/server"; - -export async function PUT( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const body = await request.json(); - const { newTitle } = body; - - if (!newTitle) { - return NextResponse.json( - { error: "newTitle is required" }, - { status: 400 } - ); - } - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - const blob = await downloadFile({ - repo, - accessToken: token, - path: "README.md", - raw: true, - }).catch((_) => { - return null; - }); - - if (!blob) { - return NextResponse.json( - { error: "Could not fetch README.md" }, - { status: 500 } - ); - } - - const readmeFile = await blob?.text(); - if (!readmeFile) { - return NextResponse.json( - { error: "Could not read README.md content" }, - { status: 500 } - ); - } - - // Escape YAML values to prevent injection attacks - const escapeYamlValue = (value: string): string => { - if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) { - return `"${value.replace(/"/g, '\\"')}"`; - } - return value; - }; - - // Escape commit message to prevent injection - const escapeCommitMessage = (message: string): string => { - return message.replace(/[\r\n]/g, " ").slice(0, 200); - }; - - const updatedReadmeFile = readmeFile.replace( - /^title:\s*(.*)$/m, - `title: ${escapeYamlValue(newTitle)}` - ); - - await uploadFile({ - repo, - accessToken: token, - file: new File([updatedReadmeFile], "README.md", { type: "text/markdown" }), - commitTitle: escapeCommitMessage( - `🐳 ${format(new Date(), "dd/MM")} - ${format( - new Date(), - "HH:mm" - )} - Rename project to "${newTitle}"` - ), - }); - - return NextResponse.json( - { - success: true, - }, - { status: 200 } - ); -} diff --git a/app/api/projects/[repoId]/route.ts b/app/api/projects/[repoId]/route.ts deleted file mode 100644 index e6daee03652b083d7f1d852f7367d7f55fd3f390..0000000000000000000000000000000000000000 --- a/app/api/projects/[repoId]/route.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { auth } from "@/lib/auth"; -import { RepoDesignation, deleteRepo, uploadFiles } from "@huggingface/hub"; -import { format } from "date-fns"; -import { NextResponse } from "next/server"; - -export async function PUT( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const body = await request.json(); - const { files, prompt, isManualChanges } = body; - - if (!files) { - return NextResponse.json({ error: "Files are required" }, { status: 400 }); - } - - if (!prompt) { - return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); - } - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - const filesToUpload: File[] = []; - for (const file of files) { - let mimeType = "text/x-python"; - if (file.path.endsWith(".txt")) { - mimeType = "text/plain"; - } else if (file.path.endsWith(".md")) { - mimeType = "text/markdown"; - } else if (file.path.endsWith(".json")) { - mimeType = "application/json"; - } - filesToUpload.push(new File([file.content], file.path, { type: mimeType })); - } - // Escape commit title to prevent injection - const escapeCommitTitle = (title: string): string => { - return title.replace(/[\r\n]/g, " ").slice(0, 200); - }; - - const baseTitle = isManualChanges - ? "" - : `🐳 ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `; - const commitTitle = escapeCommitTitle( - baseTitle + (prompt ?? "Follow-up DeepSite commit") - ); - const response = await uploadFiles({ - repo, - files: filesToUpload, - accessToken: token, - commitTitle, - }); - - return NextResponse.json( - { - success: true, - commit: { - oid: response.commit, - title: commitTitle, - date: new Date(), - }, - }, - { status: 200 } - ); -} - -export async function DELETE( - request: Request, - { params }: { params: Promise<{ repoId: string }> } -) { - const { repoId }: { repoId: string } = await params; - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + repoId, - }; - - try { - await deleteRepo({ - repo, - accessToken: token as string, - }); - - return NextResponse.json({ success: true }, { status: 200 }); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : "Failed to delete project"; - return NextResponse.json({ error: errMsg }, { status: 500 }); - } -} diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts deleted file mode 100644 index b70c310b4167077d4d8bcd15423defb1d158e2dd..0000000000000000000000000000000000000000 --- a/app/api/projects/route.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { NextResponse } from "next/server"; -import { RepoDesignation, createRepo, uploadFiles } from "@huggingface/hub"; - -import { auth } from "@/lib/auth"; -import { - COLORS, - EMOJIS_FOR_SPACE, - injectDeepSiteBadge, - isIndexPage, -} from "@/lib/utils"; - -// todo: catch error while publishing project, and return the error to the user -// if space has been created, but can't push, try again or catch well the error and return the error to the user - -export async function POST(request: Request) { - const session = await auth(); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - const token = session.accessToken; - - const body = await request.json(); - const { projectTitle, files, prompt } = body; - - if (!files) { - return NextResponse.json( - { error: "Project title and files are required" }, - { status: 400 } - ); - } - - const title = - projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project"; - - let formattedTitle = title - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .split("-") - .filter(Boolean) - .join("-") - .slice(0, 75); - - formattedTitle = - formattedTitle + "-" + Math.random().toString(36).substring(2, 7); - - const repo: RepoDesignation = { - type: "space", - name: session.user?.username + "/" + formattedTitle, - }; - - // Escape YAML values to prevent injection attacks - const escapeYamlValue = (value: string): string => { - if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) { - return `"${value.replace(/"/g, '\\"')}"`; - } - return value; - }; - - // Escape markdown headers to prevent injection - const escapeMarkdownHeader = (value: string): string => { - return value.replace(/^#+\s*/g, "").replace(/\n/g, " "); - }; - - const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)]; - const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)]; - const emoji = - EMOJIS_FOR_SPACE[Math.floor(Math.random() * EMOJIS_FOR_SPACE.length)]; - const README = `--- -title: ${escapeYamlValue(projectTitle)} -colorFrom: ${colorFrom} -colorTo: ${colorTo} -sdk: static -emoji: ${emoji} -tags: - - deepsite-v4 ---- - -# ${escapeMarkdownHeader(title)} - -This project has been created with [DeepSite](https://deepsite.hf.co) AI Vibe Coding. -`; - - const filesToUpload: File[] = [ - new File([README], "README.md", { type: "text/markdown" }), - ]; - for (const file of files) { - let mimeType = "text/html"; - if (file.path.endsWith(".css")) { - mimeType = "text/css"; - } else if (file.path.endsWith(".js")) { - mimeType = "text/javascript"; - } - const content = - mimeType === "text/html" && isIndexPage(file.path) - ? injectDeepSiteBadge(file.content) - : file.content; - - filesToUpload.push(new File([content], file.path, { type: mimeType })); - } - - let repoUrl: string | undefined; - - try { - // Create the space first - const createResult = await createRepo({ - accessToken: token as string, - repo: repo, - sdk: "static", - }); - repoUrl = createResult.repoUrl; - - // Escape commit message to prevent injection - const escapeCommitMessage = (message: string): string => { - return message.replace(/[\r\n]/g, " ").slice(0, 200); - }; - const commitMessage = escapeCommitMessage(prompt ?? "Initial DeepSite commit"); - - // Upload files to the created space - await uploadFiles({ - repo, - files: filesToUpload, - accessToken: token as string, - commitTitle: commitMessage, - }); - - const path = repoUrl.split("/").slice(-2).join("/"); - - return NextResponse.json({ repoUrl: path }, { status: 200 }); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : "Failed to create or upload to space"; - - // If space was created but upload failed, include the repo URL in the error - if (repoUrl) { - const path = repoUrl.split("/").slice(-2).join("/"); - return NextResponse.json({ - error: `${errMsg}. Space was created at ${path} but files could not be uploaded.`, - repoUrl: path, - partialSuccess: true - }, { status: 500 }); - } - - return NextResponse.json({ error: errMsg }, { status: 500 }); - } -} diff --git a/app/api/redesign/route.ts b/app/api/redesign/route.ts deleted file mode 100644 index 6b898d6fd364c5f3ef267706b62c37ee559e19cd..0000000000000000000000000000000000000000 --- a/app/api/redesign/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { NextRequest, NextResponse } from "next/server"; - -const FETCH_TIMEOUT = 30_000; -export const maxDuration = 60; - -export async function PUT(request: NextRequest) { - const body = await request.json(); - const { url } = body; - - if (!url) { - return NextResponse.json({ error: "URL is required" }, { status: 400 }); - } - - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT); - - try { - const response = await fetch( - `https://r.jina.ai/${encodeURIComponent(url)}`, - { - method: "POST", - signal: controller.signal, - } - ); - - clearTimeout(timeoutId); - - if (!response.ok) { - return NextResponse.json( - { error: "Failed to fetch redesign" }, - { status: 500 } - ); - } - const markdown = await response.text(); - return NextResponse.json( - { - ok: true, - markdown, - }, - { status: 200 } - ); - } catch (fetchError: any) { - clearTimeout(timeoutId); - - if (fetchError.name === "AbortError") { - return NextResponse.json( - { - error: - "Request timeout: The external service took too long to respond. Please try again.", - }, - { status: 504 } - ); - } - throw fetchError; - } - } catch (error: any) { - if (error.name === "AbortError" || error.message?.includes("timeout")) { - return NextResponse.json( - { - error: - "Request timeout: The external service took too long to respond. Please try again.", - }, - { status: 504 } - ); - } - return NextResponse.json( - { error: error.message || "An error occurred" }, - { status: 500 } - ); - } -} diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 Binary files a/app/favicon.ico and /dev/null differ diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index 97005be73ce05b7bb1a207464af1945139fde62b..0000000000000000000000000000000000000000 --- a/app/globals.css +++ /dev/null @@ -1,168 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); -} - -:root { - --radius: 0.65rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} - -.monaco-editor .margin { - @apply bg-background!; -} -.monaco-editor .monaco-editor-background { - @apply bg-background!; -} -.monaco-editor .decorationsOverviewRuler { - @apply opacity-0!; -} -.monaco-editor .view-line { - /* @apply bg-primary/50!; */ -} -.monaco-editor .scroll-decoration { - @apply opacity-0!; -} -.monaco-editor .cursors-layer .cursor { - @apply bg-primary!; -} - -.content-placeholder::before { - content: attr(data-placeholder); - position: absolute; - pointer-events: none; - opacity: 0.5; - @apply top-5 left-6; -} - -.sp-layout - .sp-file-explorer - .sp-file-explorer-list - .sp-explorer[data-active="true"] { - @apply text-indigo-500!; -} - -.sp-layout - .sp-stack - .sp-tabs - .sp-tab-container[aria-selected="true"] - .sp-tab-button { - @apply text-indigo-500!; -} -.sp-layout .sp-stack .sp-tabs .sp-tab-container:has(button:focus) { - @apply outline-none! border-none!; -} diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index 469b6844c7b63d85ad5e9c7ba420add2f7f1125c..0000000000000000000000000000000000000000 --- a/app/layout.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { Metadata, Viewport } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import { NextStepProvider } from "nextstepjs"; -import Script from "next/script"; - -import "@/app/globals.css"; -import { ThemeProvider } from "@/components/providers/theme"; -import { AuthProvider } from "@/components/providers/session"; -import { Toaster } from "@/components/ui/sonner"; -import { ReactQueryProvider } from "@/components/providers/react-query"; -import { generateSEO, generateStructuredData } from "@/lib/seo"; -import { NotAuthorizedDomain } from "@/components/not-authorized"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - ...generateSEO({ - title: "DeepSite | Build with AI ✨", - description: - "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.", - path: "/", - }), - appleWebApp: { - capable: true, - title: "DeepSite", - statusBarStyle: "black-translucent", - }, - icons: { - icon: "/logo.svg", - shortcut: "/logo.svg", - apple: "/logo.svg", - }, - verification: { - google: process.env.GOOGLE_SITE_VERIFICATION, - }, -}; - -export const viewport: Viewport = { - initialScale: 1, - maximumScale: 1, - themeColor: "#4f46e5", -}; - -export default async function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const structuredData = generateStructuredData("WebApplication", { - name: "DeepSite", - description: "Build websites with AI, no code required", - url: "https://deepsite.hf.co", - }); - const organizationData = generateStructuredData("Organization", { - name: "DeepSite", - url: "https://deepsite.hf.co", - }); - - return ( - - - ` + ) + .join("\n"); + + if (modifiedHtml.includes("")) { + modifiedHtml = modifiedHtml.replace( + "", + `${allJsContent}\n` + ); + } else if (modifiedHtml.includes("")) { + modifiedHtml = modifiedHtml + allJsContent; + } else { + modifiedHtml = modifiedHtml + "\n" + allJsContent; + } + + jsFiles.forEach((file) => { + const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + modifiedHtml = modifiedHtml.replace( + new RegExp( + `]*src=["'][\\.\/]*${escapedPath}["'][^>]*><\\/script>`, + "gi" + ), + "" + ); + }); + } + + // Inject all JSON files as script tags with type="application/json" + if (jsonFiles.length > 0) { + const allJsonContent = jsonFiles + .map( + (file) => + `` + ) + .join("\n"); + + if (modifiedHtml.includes("")) { + modifiedHtml = modifiedHtml.replace( + "", + `${allJsonContent}\n` + ); + } else if (modifiedHtml.includes("")) { + modifiedHtml = modifiedHtml + allJsonContent; + } else { + modifiedHtml = modifiedHtml + "\n" + allJsonContent; + } + + jsonFiles.forEach((file) => { + const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + modifiedHtml = modifiedHtml.replace( + new RegExp( + `]*src=["'][\\.\/]*${escapedPath}["'][^>]*><\\/script>`, + "gi" + ), + "" + ); + }); + } + + // Inject navigation script early in the document + if (navigationScript) { + // Try to inject right after or opening tag + if (modifiedHtml.includes("")) { + modifiedHtml = modifiedHtml.replace( + "", + `\n` + ); + } else if (modifiedHtml.includes("")) { + modifiedHtml = modifiedHtml.replace( + "", + `\n` + ); + } else if (modifiedHtml.includes("")) { + modifiedHtml = modifiedHtml.replace( + "", + `\n` + ); + } else { + modifiedHtml = + `\n` + modifiedHtml; + } + } + + return modifiedHtml; + }, + [pages, previewPageData?.path, createNavigationScript] + ); + + useEffect(() => { + if (isNew && previewPageData?.html) { + const now = Date.now(); + const timeSinceLastUpdate = now - lastUpdateTimeRef.current; + + if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) { + const processedHtml = injectAssetsIntoHtml( + previewPageData.html, + pagesToUse + ); + setThrottledHtml(processedHtml); + lastUpdateTimeRef.current = now; + } else { + const timeUntilNextUpdate = 3000 - timeSinceLastUpdate; + const timer = setTimeout(() => { + const processedHtml = injectAssetsIntoHtml( + previewPageData.html, + pagesToUse + ); + setThrottledHtml(processedHtml); + lastUpdateTimeRef.current = Date.now(); + }, timeUntilNextUpdate); + return () => clearTimeout(timer); + } + } + }, [isNew, previewPageData?.html, injectAssetsIntoHtml, pagesToUse]); + + useEffect(() => { + if (!isAiWorking && !globalAiLoading && previewPageData?.html) { + const processedHtml = injectAssetsIntoHtml( + previewPageData.html, + pagesToUse + ); + setStableHtml(processedHtml); + } + }, [ + isAiWorking, + globalAiLoading, + previewPageData?.html, + injectAssetsIntoHtml, + previewPage, + pagesToUse, + ]); + + useEffect(() => { + if ( + previewPageData?.html && + !stableHtml && + !isAiWorking && + !globalAiLoading + ) { + const processedHtml = injectAssetsIntoHtml( + previewPageData.html, + pagesToUse + ); + setStableHtml(processedHtml); + } + }, [ + previewPageData?.html, + stableHtml, + isAiWorking, + globalAiLoading, + injectAssetsIntoHtml, + pagesToUse, + ]); + + const setupIframeListeners = () => { + if (iframeRef?.current?.contentDocument) { + const iframeDocument = iframeRef.current.contentDocument; + + iframeDocument.addEventListener( + "click", + handleCustomNavigation as any, + true + ); + + if (isEditableModeEnabled) { + iframeDocument.addEventListener("mouseover", handleMouseOver); + iframeDocument.addEventListener("mouseout", handleMouseOut); + iframeDocument.addEventListener("click", handleClick); + } + } + }; + + // Listen for navigation messages from iframe + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === "navigate" && event.data?.path) { + setPreviewPage(event.data.path); + } + }; + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, [setPreviewPage]); + + useEffect(() => { + const cleanupListeners = () => { + if (iframeRef?.current?.contentDocument) { + const iframeDocument = iframeRef.current.contentDocument; + iframeDocument.removeEventListener( + "click", + handleCustomNavigation as any, + true + ); + iframeDocument.removeEventListener("mouseover", handleMouseOver); + iframeDocument.removeEventListener("mouseout", handleMouseOut); + iframeDocument.removeEventListener("click", handleClick); + } + }; + + const timer = setTimeout(() => { + if (iframeRef?.current?.contentDocument) { + cleanupListeners(); + setupIframeListeners(); + } + }, 50); + + return () => { + clearTimeout(timer); + cleanupListeners(); + }; + }, [isEditableModeEnabled, stableHtml, throttledHtml, previewPage]); + + const refreshIframe = () => { + setIframeKey((prev) => prev + 1); + }; + + const promoteVersion = async () => { + setIsPromotingVersion(true); + await api + .post( + `/me/projects/${project?.space_id}/commits/${currentCommit}/promote` + ) + .then((res) => { + if (res.data.ok) { + setCurrentCommit(null); + setPages(res.data.pages); + setCurrentPage(res.data.pages[0].path); + setLastSavedPages(res.data.pages); + setPreviewPage(res.data.pages[0].path); + toast.success("Version promoted successfully"); + } + }) + .catch((err) => { + toast.error(err.response.data.error); + }); + setIsPromotingVersion(false); + }; + + const handleMouseOver = (event: MouseEvent) => { + if (iframeRef?.current) { + const iframeDocument = iframeRef.current.contentDocument; + if (iframeDocument) { + const targetElement = event.target as HTMLElement; + if ( + hoveredElement?.tagName !== targetElement.tagName || + hoveredElement?.rect.top !== + targetElement.getBoundingClientRect().top || + hoveredElement?.rect.left !== + targetElement.getBoundingClientRect().left || + hoveredElement?.rect.width !== + targetElement.getBoundingClientRect().width || + hoveredElement?.rect.height !== + targetElement.getBoundingClientRect().height + ) { + if (targetElement !== iframeDocument.body) { + const rect = targetElement.getBoundingClientRect(); + setHoveredElement({ + tagName: targetElement.tagName, + rect: { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + }, + }); + targetElement.classList.add("hovered-element"); + } else { + return setHoveredElement(null); + } + } + } + } + }; + const handleMouseOut = () => { + setHoveredElement(null); + }; + const handleClick = (event: MouseEvent) => { + if (iframeRef?.current) { + const iframeDocument = iframeRef.current.contentDocument; + if (iframeDocument) { + const path = event.composedPath(); + const targetElement = path[0] as HTMLElement; + + const findClosestAnchor = ( + element: HTMLElement + ): HTMLAnchorElement | null => { + let current: HTMLElement | null = element; + while (current) { + if (current.tagName?.toUpperCase() === "A") { + return current as HTMLAnchorElement; + } + if (current === iframeDocument.body) { + break; + } + const parent: Node | null = current.parentNode; + if (parent && parent.nodeType === 11) { + current = (parent as ShadowRoot).host as HTMLElement; + } else if (parent && parent.nodeType === 1) { + current = parent as HTMLElement; + } else { + break; + } + } + return null; + }; + + const anchorElement = findClosestAnchor(targetElement); + + if (anchorElement) { + return; + } + + if (targetElement !== iframeDocument.body) { + setSelectedElement(targetElement); + } + } + } + }; + + const handleCustomNavigation = (event: MouseEvent) => { + if (iframeRef?.current) { + const iframeDocument = iframeRef.current.contentDocument; + if (iframeDocument) { + const path = event.composedPath(); + const actualTarget = path[0] as HTMLElement; + + const findClosestAnchor = ( + element: HTMLElement + ): HTMLAnchorElement | null => { + let current: HTMLElement | null = element; + while (current) { + if (current.tagName?.toUpperCase() === "A") { + return current as HTMLAnchorElement; + } + if (current === iframeDocument.body) { + break; + } + const parent: Node | null = current.parentNode; + if (parent && parent.nodeType === 11) { + current = (parent as ShadowRoot).host as HTMLElement; + } else if (parent && parent.nodeType === 1) { + current = parent as HTMLElement; + } else { + break; + } + } + return null; + }; + + const anchorElement = findClosestAnchor(actualTarget); + if (anchorElement) { + let href = anchorElement.getAttribute("href"); + if (href) { + event.stopPropagation(); + event.preventDefault(); + + if (href.startsWith("#")) { + let targetElement = iframeDocument.querySelector(href); + + if (!targetElement) { + const searchInShadows = ( + root: Document | ShadowRoot + ): Element | null => { + const elements = root.querySelectorAll("*"); + for (const el of elements) { + if (el.shadowRoot) { + const found = el.shadowRoot.querySelector(href); + if (found) return found; + const nested = searchInShadows(el.shadowRoot); + if (nested) return nested; + } + } + return null; + }; + targetElement = searchInShadows(iframeDocument); + } + + if (targetElement) { + targetElement.scrollIntoView({ behavior: "smooth" }); + } + return; + } + + let normalizedHref = href.replace(/^\.?\//, ""); + + if (normalizedHref === "" || normalizedHref === "/") { + normalizedHref = "index.html"; + } + + const hashIndex = normalizedHref.indexOf("#"); + if (hashIndex !== -1) { + normalizedHref = normalizedHref.substring(0, hashIndex); + } + + if (!normalizedHref.includes(".")) { + normalizedHref = normalizedHref + ".html"; + } + + const isPageExist = pagesToUse.some((page) => { + const pagePath = page.path.replace(/^\.?\//, ""); + return pagePath === normalizedHref; + }); + + if (isPageExist) { + setPreviewPage(normalizedHref); + } + } + } + } + } + }; + + return ( +
+ + {/* Preview page indicator */} + {!isAiWorking && hoveredElement && isEditableModeEnabled && ( +
+ + {htmlTagToText(hoveredElement.tagName.toLowerCase())} + +
+ )} + {isLoadingProject ? ( +
+
+ + + +
+
+ ) : ( + <> + {isLoadingCommitPages && ( +
+
+ +
+
+ )} + {!isNew && !currentCommit && ( +
+
+ + + Preview version of the project. Try refreshing the preview if + you experience any issues. + +
+ +
+ )} +