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/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..ea020910880b2a4e2c01e4f66f139ff23cec6f1a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+assets/PAGINA[[:space:]]DE[[:space:]]VENDAS/fome.png filter=lfs diff=lfs merge=lfs -text
+assets/PAGINA[[:space:]]DE[[:space:]]VENDAS/IMG/CAVALO.jpg filter=lfs diff=lfs merge=lfs -text
+assets/PAGINA[[:space:]]DE[[:space:]]VENDAS/ioga.png filter=lfs diff=lfs merge=lfs -text
+assets/PAGINA[[:space:]]DE[[:space:]]VENDAS/praia.png filter=lfs diff=lfs merge=lfs -text
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..847f856298f324b2ce481c2e8fefcbbf4f178caf 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,19 @@
---
-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
+ - Qwen/Qwen3-Coder-480B-A35B-Instruct
+ - moonshotai/Kimi-K2-Instruct
- moonshotai/Kimi-K2-Instruct-0905
- zai-org/GLM-4.7
- MiniMaxAI/MiniMax-M2.1
diff --git a/actions/mentions.ts b/actions/mentions.ts
deleted file mode 100644
index c6b7dabfbaa6f0efe0d492dfe5470353b0139bae..0000000000000000000000000000000000000000
--- a/actions/mentions.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import { File } from "@/lib/type";
-
-export const searchMentions = async (query: string) => {
- const promises = [searchModels(query), searchDatasets(query)];
- const results = await Promise.all(promises);
- return { models: results[0], datasets: results[1] };
-};
-
-const searchModels = async (query: string) => {
- const response = await fetch(
- `https://huggingface.co/api/quicksearch?q=${query}&type=model&limit=3`
- );
- const data = await response.json();
- return data?.models ?? [];
-};
-
-const searchDatasets = async (query: string) => {
- const response = await fetch(
- `https://huggingface.co/api/quicksearch?q=${query}&type=dataset&limit=3`
- );
- const data = await response.json();
- return data?.datasets ?? [];
-};
-
-export const searchFilesMentions = async (query: string, files: File[]) => {
- if (!query) return files;
- const lowerQuery = query.toLowerCase();
- return files.filter((file) => file.path.toLowerCase().includes(lowerQuery));
-};
diff --git a/actions/projects.ts b/actions/projects.ts
deleted file mode 100644
index d6f685b63290792fc597438a53eab0a14d6bbf8e..0000000000000000000000000000000000000000
--- a/actions/projects.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-"use server";
-import {
- downloadFile,
- listCommits,
- listFiles,
- listSpaces,
- RepoDesignation,
- SpaceEntry,
- spaceInfo,
-} from "@huggingface/hub";
-
-import { auth } from "@/lib/auth";
-import { Commit, File } from "@/lib/type";
-
-export interface ProjectWithCommits extends SpaceEntry {
- commits?: Commit[];
- medias?: string[];
-}
-
-const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"];
-const IGNORED_FORMATS = [
- ".png",
- ".jpg",
- ".jpeg",
- ".gif",
- ".svg",
- ".webp",
- ".mp4",
- ".mp3",
- ".wav",
-];
-
-export const getProjects = async () => {
- const projects: SpaceEntry[] = [];
- const session = await auth();
- if (!session?.user) {
- return projects;
- }
- const token = session.accessToken;
- for await (const space of listSpaces({
- accessToken: token,
- additionalFields: ["author", "cardData"],
- search: {
- owner: session.user.username,
- },
- })) {
- if (
- space.sdk === "static" &&
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
- (space.cardData as { tags?: string[] })?.tags?.some((tag) =>
- tag.includes("deepsite")
- )
- ) {
- projects.push(space);
- }
- }
- return projects;
-};
-export const getProject = async (id: string, commitId?: string) => {
- const session = await auth();
- if (!session?.user) {
- return null;
- }
- const token = session.accessToken;
- try {
- const project: ProjectWithCommits | null = await spaceInfo({
- name: id,
- accessToken: token,
- additionalFields: ["author", "cardData"],
- });
- const repo: RepoDesignation = {
- type: "space",
- name: id,
- };
- const files: File[] = [];
- const medias: string[] = [];
- const params = { repo, accessToken: token };
- if (commitId) {
- Object.assign(params, { revision: commitId });
- }
- for await (const fileInfo of listFiles(params)) {
- if (IGNORED_PATHS.includes(fileInfo.path)) continue;
- if (IGNORED_FORMATS.some((format) => fileInfo.path.endsWith(format))) {
- medias.push(
- `https://huggingface.co/spaces/${id}/resolve/main/${fileInfo.path}`
- );
- continue;
- }
-
- if (fileInfo.type === "directory") {
- for await (const subFile of listFiles({
- repo,
- accessToken: token,
- path: fileInfo.path,
- })) {
- if (IGNORED_FORMATS.some((format) => subFile.path.endsWith(format))) {
- medias.push(
- `https://huggingface.co/spaces/${id}/resolve/main/${subFile.path}`
- );
- }
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: subFile.path,
- raw: true,
- ...(commitId ? { revision: commitId } : {}),
- }).catch((_) => {
- return null;
- });
- if (!blob) {
- continue;
- }
- const html = await blob?.text();
- if (!html) {
- continue;
- }
- files[subFile.path === "index.html" ? "unshift" : "push"]({
- path: subFile.path,
- content: html,
- });
- }
- } else {
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: fileInfo.path,
- raw: true,
- ...(commitId ? { revision: commitId } : {}),
- }).catch((_) => {
- return null;
- });
- if (!blob) {
- continue;
- }
- const html = await blob?.text();
- if (!html) {
- continue;
- }
- files[fileInfo.path === "index.html" ? "unshift" : "push"]({
- path: fileInfo.path,
- content: html,
- });
- }
- }
- const commits: Commit[] = [];
- const commitIterator = listCommits({ repo, accessToken: token });
- for await (const commit of commitIterator) {
- if (
- commit.title?.toLowerCase() === "initial commit" ||
- commit.title
- ?.toLowerCase()
- ?.includes("upload media files through deepsite")
- )
- continue;
- commits.push({
- title: commit.title,
- oid: commit.oid,
- date: commit.date,
- });
- if (commits.length >= 20) {
- break;
- }
- }
-
- project.commits = commits;
- project.medias = medias;
-
- return { project, files };
- } catch (error) {
- return {
- project: null,
- files: [],
- };
- }
-};
diff --git a/app/(public)/layout.tsx b/app/(public)/layout.tsx
index 0eb2c8fbb85b745d5be01c9e79fa0ebc93a71d00..4c640fb83ccfced8842764d029c87a2d85718d65 100644
--- a/app/(public)/layout.tsx
+++ b/app/(public)/layout.tsx
@@ -1,12 +1,13 @@
-import { Navigation } from "@/components/public/navigation";
+import Navigation from "@/components/public/navigation";
-export default function PublicLayout({
+export default async function PublicLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
-
+
diff --git a/app/(public)/page.tsx b/app/(public)/page.tsx
index 86970b7a3dab7e60cce3bbb68c747b525baba45c..e2d5c88a4977801d6acabd904535b00ad622a267 100644
--- a/app/(public)/page.tsx
+++ b/app/(public)/page.tsx
@@ -1,25 +1,5 @@
-import { AnimatedDotsBackground } from "@/components/public/animated-dots-background";
-import { HeroHeader } from "@/components/public/hero-header";
-import { UserProjects } from "@/components/projects/user-projects";
-import { AskAiLanding } from "@/components/ask-ai/ask-ai-landing";
-import { Bento } from "@/components/public/bento";
+import { MyProjects } from "@/components/my-projects";
-export const dynamic = "force-dynamic";
-
-export default async function Homepage() {
- return (
- <>
-
-
-
- >
- );
+export default async function HomePage() {
+ return
;
}
diff --git a/app/(public)/signin/page.tsx b/app/(public)/signin/page.tsx
deleted file mode 100644
index 3200d0755372c79be57b9204331695cd8f0499ac..0000000000000000000000000000000000000000
--- a/app/(public)/signin/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { LoginButtons } from "@/components/login/login-buttons";
-
-export default async function SignInPage({
- searchParams,
-}: {
- searchParams: Promise<{ callbackUrl: string }>;
-}) {
- const { callbackUrl } = await searchParams;
- console.log(callbackUrl);
- return (
-
-
-
You shall not pass π§
-
- You can't access this resource without being signed in.
-
-
-
-
- );
-}
diff --git a/app/[namespace]/[repoId]/page.tsx b/app/[namespace]/[repoId]/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..19a09d72477dd437fa8a786601cbc584676609ee
--- /dev/null
+++ b/app/[namespace]/[repoId]/page.tsx
@@ -0,0 +1,28 @@
+import { AppEditor } from "@/components/editor";
+import { generateSEO } from "@/lib/seo";
+import { Metadata } from "next";
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ namespace: string; repoId: string }>;
+}): Promise
{
+ const { namespace, repoId } = await params;
+
+ return generateSEO({
+ title: `${namespace}/${repoId} - DeepSite Editor`,
+ description: `Edit and build ${namespace}/${repoId} with AI-powered tools on DeepSite. Create stunning websites with no code required.`,
+ path: `/${namespace}/${repoId}`,
+ // Prevent indexing of individual project editor pages if they contain sensitive content
+ noIndex: false, // Set to true if you want to keep project pages private
+ });
+}
+
+export default async function ProjectNamespacePage({
+ params,
+}: {
+ params: Promise<{ namespace: string; repoId: string }>;
+}) {
+ const { namespace, repoId } = await params;
+ return ;
+}
diff --git a/app/[owner]/[repoId]/page.tsx b/app/[owner]/[repoId]/page.tsx
deleted file mode 100644
index 5082396059ae126dbc2be8e0daf30d1ff014f922..0000000000000000000000000000000000000000
--- a/app/[owner]/[repoId]/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { getProject } from "@/actions/projects";
-import { AppEditor } from "@/components/editor";
-import { auth } from "@/lib/auth";
-import { notFound, redirect } from "next/navigation";
-
-export default async function ProjectPage({
- params,
- searchParams,
-}: {
- params: Promise<{ owner: string; repoId: string }>;
- searchParams: Promise<{ commit?: string }>;
-}) {
- const session = await auth();
-
- const { owner, repoId } = await params;
- const { commit } = await searchParams;
- if (!session) {
- redirect(
- `/api/auth/signin?callbackUrl=/${owner}/${repoId}${
- commit ? `?commit=${commit}` : ""
- }`
- );
- }
- const datas = await getProject(`${owner}/${repoId}`, commit);
- if (!datas?.project) {
- return notFound();
- }
- return (
-
- );
-}
diff --git a/app/actions/auth.ts b/app/actions/auth.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b0028105705dde5ace29f4484398edbd0e9c9dc0
--- /dev/null
+++ b/app/actions/auth.ts
@@ -0,0 +1,18 @@
+"use server";
+
+import { headers } from "next/headers";
+
+export async function getAuth() {
+ const authList = await headers();
+ const host = authList.get("host") ?? "localhost:3000";
+ const url = host.includes("/spaces/enzostvs")
+ ? "enzostvs-deepsite.hf.space"
+ : host;
+ const redirect_uri =
+ `${host.includes("localhost") ? "http://" : "https://"}` +
+ url +
+ "/deepsite/auth/callback";
+
+ const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
+ return loginRedirectUrl;
+}
diff --git a/app/actions/projects.ts b/app/actions/projects.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5f99d85273352e21e22efb85b03bced1cf210897
--- /dev/null
+++ b/app/actions/projects.ts
@@ -0,0 +1,47 @@
+"use server";
+
+import { isAuthenticated } from "@/lib/auth";
+import { NextResponse } from "next/server";
+import { listSpaces } from "@huggingface/hub";
+import { ProjectType } from "@/types";
+
+export async function getProjects(): Promise<{
+ ok: boolean;
+ projects: ProjectType[];
+ isEmpty?: boolean;
+}> {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return {
+ ok: false,
+ projects: [],
+ };
+ }
+
+ const projects = [];
+ for await (const space of listSpaces({
+ accessToken: user.token as string,
+ additionalFields: ["author", "cardData"],
+ search: {
+ owner: user.name,
+ }
+ })) {
+ if (
+ !space.private &&
+ space.sdk === "static" &&
+ Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
+ (
+ ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
+ ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
+ )
+ ) {
+ projects.push(space);
+ }
+ }
+
+ return {
+ ok: true,
+ projects,
+ };
+}
diff --git a/app/api/ask/route.ts b/app/api/ask/route.ts
index 293e8244308cc1191e32b3ef6b30e93ccb963fcf..d03e1a136f6a5b98ff976d1861d0e1f5aeae6000 100644
--- a/app/api/ask/route.ts
+++ b/app/api/ask/route.ts
@@ -1,37 +1,84 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
+import { headers } from "next/headers";
import { InferenceClient } from "@huggingface/inference";
-import { FOLLOW_UP_SYSTEM_PROMPT, INITIAL_SYSTEM_PROMPT } from "@/lib/prompts";
-import { auth } from "@/lib/auth";
-import { File, Message } from "@/lib/type";
-import { DEFAULT_MODEL, MODELS } from "@/lib/providers";
+import { MODELS } from "@/lib/providers";
+import {
+ FOLLOW_UP_SYSTEM_PROMPT,
+ FOLLOW_UP_SYSTEM_PROMPT_LIGHT,
+ INITIAL_SYSTEM_PROMPT,
+ INITIAL_SYSTEM_PROMPT_LIGHT,
+ MAX_REQUESTS_PER_IP,
+ PROMPT_FOR_PROJECT_NAME,
+} from "@/lib/prompts";
+import MY_TOKEN_KEY from "@/lib/get-cookie-name";
+import { Page } from "@/types";
+import { isAuthenticated } from "@/lib/auth";
+import { getBestProvider } from "@/lib/best-provider";
-export async function POST(request: Request) {
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
+const ipAddresses = new Map();
+
+export async function POST(request: NextRequest) {
+ const authHeaders = await headers();
+ const tokenInHeaders = authHeaders.get("Authorization");
+ const userToken = tokenInHeaders ? tokenInHeaders.replace("Bearer ", "") : request.cookies.get(MY_TOKEN_KEY())?.value;
const body = await request.json();
- const {
- prompt,
- previousMessages = [],
- files = [],
- provider,
- model,
- redesignMd,
- medias,
- } = body;
-
- if (!prompt) {
- return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
+ const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
+
+ if (!model || (!prompt && !redesignMarkdown)) {
+ return NextResponse.json(
+ { ok: false, error: "Missing required fields" },
+ { status: 400 }
+ );
+ }
+
+ const selectedModel = MODELS.find(
+ (m) => m.value === model || m.label === model
+ );
+
+ if (!selectedModel) {
+ return NextResponse.json(
+ { ok: false, error: "Invalid model selected" },
+ { status: 400 }
+ );
}
- if (!model || !MODELS.find((m: (typeof MODELS)[0]) => m.value === model)) {
- return NextResponse.json({ error: "Model is required" }, { status: 400 });
+
+ let token: string | null = null;
+ if (userToken) token = userToken;
+ let billTo: string | null = null;
+
+ /**
+ * Handle local usage token, this bypass the need for a user token
+ * and allows local testing without authentication.
+ * This is useful for development and testing purposes.
+ */
+ if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
+ token = process.env.HF_TOKEN;
}
- const client = new InferenceClient(token);
+ const ip = authHeaders.get("x-forwarded-for")?.includes(",")
+ ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
+ : authHeaders.get("x-forwarded-for");
+
+ if (!token ||Β token === "null" || token === "") {
+ ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
+ if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
+ return NextResponse.json(
+ {
+ ok: false,
+ openLogin: true,
+ message: "Log In to continue using the service",
+ },
+ { status: 429 }
+ );
+ }
+
+ token = process.env.DEFAULT_HF_TOKEN as string;
+ billTo = "huggingface";
+ }
try {
const encoder = new TextEncoder();
@@ -45,139 +92,303 @@ export async function POST(request: Request) {
Connection: "keep-alive",
},
});
- (async () => {
- let hasRetried = false;
- let currentModel = model;
- const tryGeneration = async (): Promise => {
- try {
- const chatCompletion = client.chatCompletionStream({
- model: currentModel + (provider !== "auto" ? `:${provider}` : ""),
+ (async () => {
+ try {
+ const client = new InferenceClient(token);
+
+ const systemPrompt = selectedModel.value.includes('MiniMax')
+ ? INITIAL_SYSTEM_PROMPT_LIGHT
+ : INITIAL_SYSTEM_PROMPT;
+
+ const userPrompt = prompt;
+
+ const chatCompletion = client.chatCompletionStream(
+ {
+ model: selectedModel.value + (provider !== "auto" ? `:${provider}` : ""),
messages: [
{
role: "system",
- content:
- files.length > 0
- ? FOLLOW_UP_SYSTEM_PROMPT
- : INITIAL_SYSTEM_PROMPT,
+ content: systemPrompt,
},
- ...previousMessages.map((message: Message) => ({
- role: message.role,
- content: message.content,
- })),
- ...(files?.length > 0
- ? [
- {
- role: "user",
- content: `Here are the files that the user has provider:${files
- .map(
- (file: File) =>
- `File: ${file.path}\nContent: ${file.content}`
- )
- .join("\n")}\n\n${prompt}`,
- },
- ]
- : []),
+ ...(redesignMarkdown ? [{
+ role: "assistant",
+ 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}`
+ }] : []),
{
role: "user",
- content: `${
- redesignMd?.url &&
- `Redesign the following website ${redesignMd.url}, try to use the same images and content, but you can still improve it if needed. Do the best version possibile. Here is the markdown:\n ${redesignMd.md} \n\n`
- }${prompt} ${
- medias && medias.length > 0
- ? `\nHere is the list of my media files: ${medias.join(
- ", "
- )}\n`
- : ""
- }`,
- }
+ content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
+2. I want to use the following secondary color: ${enhancedSettings.secondaryColor} (eg: bg-${enhancedSettings.secondaryColor}-500).
+3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
+ },
],
- stream: true,
- max_tokens: 16_000,
- });
- while (true) {
- const { done, value } = await chatCompletion.next();
- if (done) {
- break;
- }
-
- const chunk = value.choices[0]?.delta?.content;
- if (chunk) {
- await writer.write(encoder.encode(chunk));
- }
+ ...(selectedModel.top_k ? { top_k: selectedModel.top_k } : {}),
+ ...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
+ ...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
+ max_tokens: 16_384,
+ },
+ billTo ? { billTo } : {}
+ );
+
+ while (true) {
+ const { done, value } = await chatCompletion.next()
+ if (done) {
+ break;
}
- await writer.close();
- } catch (error) {
- const errorMessage =
- error instanceof Error
- ? error.message
- : "An error occurred while processing your request";
-
- if (
- !hasRetried &&
- errorMessage?.includes(
- "Failed to perform inference: Model not found"
+ const chunk = value.choices[0]?.delta?.content;
+ if (chunk) {
+ await writer.write(encoder.encode(chunk));
+ }
+ }
+
+ await writer.close();
+ } catch (error: any) {
+ console.error(error);
+ if (error.message?.includes("exceeded your monthly included credits")) {
+ await writer.write(
+ encoder.encode(
+ JSON.stringify({
+ ok: false,
+ openProModal: true,
+ message: error.message,
+ })
+ )
+ );
+ } else if (error?.message?.includes("inference provider information")) {
+ await writer.write(
+ encoder.encode(
+ JSON.stringify({
+ ok: false,
+ openSelectProvider: true,
+ message: error.message,
+ })
)
- ) {
- hasRetried = true;
- if (model === DEFAULT_MODEL) {
- const availableFallbackModels = MODELS.filter(
- (m) => m.value !== model
- );
- const randomIndex = Math.floor(
- Math.random() * availableFallbackModels.length
- );
- currentModel = availableFallbackModels[randomIndex];
- } else {
- currentModel = DEFAULT_MODEL;
- }
- const switchMessage = `\n\n_Note: The selected model was not available. Switched to \`${currentModel}\`._\n\n`;
- await writer.write(encoder.encode(switchMessage));
-
- return tryGeneration();
+ );
+ }
+ else {
+ await writer.write(
+ encoder.encode(
+ JSON.stringify({
+ ok: false,
+ message:
+ error.message ||
+ "An error occurred while processing your request.",
+ })
+ )
+ );
+ }
+ } finally {
+ try {
+ await writer?.close();
+ } catch {
+ }
+ }
+ })();
+
+ return response;
+ } catch (error: any) {
+ return NextResponse.json(
+ {
+ ok: false,
+ openSelectProvider: true,
+ message:
+ error?.message || "An error occurred while processing your request.",
+ },
+ { status: 500 }
+ );
+ }
+}
+
+export async function PUT(request: NextRequest) {
+ const user = await isAuthenticated();
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const authHeaders = await headers();
+
+ const body = await request.json();
+ const { prompt, provider, selectedElementHtml, model, pages, files, repoId, isNew } =
+ body;
+
+ if (!prompt || pages.length === 0) {
+ return NextResponse.json(
+ { ok: false, error: "Missing required fields" },
+ { status: 400 }
+ );
+ }
+
+ const selectedModel = MODELS.find(
+ (m) => m.value === model || m.label === model
+ );
+ if (!selectedModel) {
+ return NextResponse.json(
+ { ok: false, error: "Invalid model selected" },
+ { status: 400 }
+ );
+ }
+
+ let token = user.token as string;
+ let billTo: string | null = null;
+
+ /**
+ * Handle local usage token, this bypass the need for a user token
+ * and allows local testing without authentication.
+ * This is useful for development and testing purposes.
+ */
+ if (process.env.HF_TOKEN && process.env.HF_TOKEN.length > 0) {
+ token = process.env.HF_TOKEN;
+ }
+
+ const ip = authHeaders.get("x-forwarded-for")?.includes(",")
+ ? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
+ : authHeaders.get("x-forwarded-for");
+
+ if (!token ||Β token === "null" || token === "") {
+ ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
+ if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
+ return NextResponse.json(
+ {
+ ok: false,
+ openLogin: true,
+ message: "Log In to continue using the service",
+ },
+ { status: 429 }
+ );
+ }
+
+ token = process.env.DEFAULT_HF_TOKEN as string;
+ billTo = "huggingface";
+ }
+
+ 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 () => {
+ try {
+ const client = new InferenceClient(token);
+
+ const basePrompt = selectedModel.value.includes('MiniMax')
+ ? FOLLOW_UP_SYSTEM_PROMPT_LIGHT
+ : FOLLOW_UP_SYSTEM_PROMPT;
+ const systemPrompt = basePrompt + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
+ // const userContext = "You are modifying the HTML file based on the user's request.";
+
+ const allPages = pages || [];
+ const pagesContext = allPages
+ .map((p: Page) => `- ${p.path}\n${p.html}`)
+ .join("\n\n");
+
+ const assistantContext = `${selectedElementHtml
+ ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
+ : ""
+ }. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
+
+ const chatCompletion = client.chatCompletionStream(
+ {
+ model: selectedModel.value + (provider !== "auto" ? `:${provider}` : ""),
+ messages: [
+ {
+ role: "system",
+ content: systemPrompt + assistantContext
+ },
+ {
+ role: "user",
+ content: prompt,
+ },
+ ],
+ ...(selectedModel.top_k ? { top_k: selectedModel.top_k } : {}),
+ ...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
+ ...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
+ max_tokens: 16_384,
+ },
+ billTo ? { billTo } : {}
+ );
+
+ // Stream the response chunks to the client
+ while (true) {
+ const { done, value } = await chatCompletion.next();
+ if (done) {
+ break;
}
- 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);
- }
+ const chunk = value.choices[0]?.delta?.content;
+ if (chunk) {
+ await writer.write(encoder.encode(chunk));
}
}
- };
- await tryGeneration();
+ await writer.write(encoder.encode(`\n___METADATA_START___\n${JSON.stringify({
+ repoId,
+ isNew,
+ userName: user.name,
+ })}\n___METADATA_END___\n`));
+
+ await writer.close();
+ } catch (error: any) {
+ if (error.message?.includes("exceeded your monthly included credits")) {
+ await writer.write(
+ encoder.encode(
+ JSON.stringify({
+ ok: false,
+ openProModal: true,
+ message: error.message,
+ })
+ )
+ );
+ } else if (error?.message?.includes("inference provider information")) {
+ await writer.write(
+ encoder.encode(
+ JSON.stringify({
+ ok: false,
+ openSelectProvider: true,
+ message: error.message,
+ })
+ )
+ );
+ } else {
+ await writer.write(
+ encoder.encode(
+ JSON.stringify({
+ ok: false,
+ message:
+ error.message ||
+ "An error occurred while processing your request.",
+ })
+ )
+ );
+ }
+ } finally {
+ try {
+ await writer?.close();
+ } catch {
+ // ignore
+ }
+ }
})();
return response;
- } catch (error) {
+ } catch (error: any) {
return NextResponse.json(
{
- error: error instanceof Error ? error.message : "Internal Server Error",
+ ok: false,
+ openSelectProvider: true,
+ message:
+ error.message || "An error occurred while processing your request.",
},
{ status: 500 }
);
}
}
+
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
deleted file mode 100644
index 7b38c1bb45e38527a2c595c07df5e70d38ada7d9..0000000000000000000000000000000000000000
--- a/app/api/auth/[...nextauth]/route.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import NextAuth from "next-auth";
-import { authOptions } from "@/lib/auth";
-
-const handler = NextAuth(authOptions);
-
-export { handler as GET, handler as POST };
diff --git a/app/api/auth/login-url/route.ts b/app/api/auth/login-url/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e46df350e48e088ad278dec248f55651cdefd027
--- /dev/null
+++ b/app/api/auth/login-url/route.ts
@@ -0,0 +1,21 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function GET(req: NextRequest) {
+ const host = req.headers.get("host") ?? "localhost:3000";
+
+ let url: string;
+ if (host.includes("localhost")) {
+ url = host;
+ } else {
+ url = "huggingface.co";
+ }
+
+ const redirect_uri =
+ `${host.includes("localhost") ? "http://" : "https://"}` +
+ url +
+ "/deepsite/auth/callback";
+
+ const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
+
+ return NextResponse.json({ loginUrl: loginRedirectUrl });
+}
diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..01feb588119d8472e23d61fffbc6f43d8f5a508c
--- /dev/null
+++ b/app/api/auth/logout/route.ts
@@ -0,0 +1,25 @@
+import { NextResponse } from "next/server";
+import MY_TOKEN_KEY from "@/lib/get-cookie-name";
+
+export async function POST() {
+ const cookieName = MY_TOKEN_KEY();
+ const isProduction = process.env.NODE_ENV === "production";
+
+ const response = NextResponse.json(
+ { message: "Logged out successfully" },
+ { status: 200 }
+ );
+
+ // Clear the HTTP-only cookie
+ const cookieOptions = [
+ `${cookieName}=`,
+ "Max-Age=0",
+ "Path=/",
+ "HttpOnly",
+ ...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
+ ].join("; ");
+
+ response.headers.set("Set-Cookie", cookieOptions);
+
+ return response;
+}
diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f826af35fa35c5767a318d1e146e77fb6fc07592
--- /dev/null
+++ b/app/api/auth/route.ts
@@ -0,0 +1,86 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function POST(req: NextRequest) {
+ const body = await req.json();
+ const { code } = body;
+
+ if (!code) {
+ return NextResponse.json(
+ { error: "Code is required" },
+ {
+ status: 400,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ }
+
+ const Authorization = `Basic ${Buffer.from(
+ `${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
+ ).toString("base64")}`;
+
+ const host =
+ req.headers.get("host") ?? req.headers.get("origin") ?? "localhost:3000";
+
+ const url = host.includes("/spaces/enzostvs")
+ ? "huggingface.co/deepsite"
+ : host;
+ const redirect_uri =
+ `${host.includes("localhost") ? "http://" : "https://"}` +
+ url +
+ "/deepsite/auth/callback";
+ const request_auth = await fetch("https://huggingface.co/oauth/token", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ Authorization,
+ },
+ body: new URLSearchParams({
+ grant_type: "authorization_code",
+ code,
+ redirect_uri,
+ }),
+ });
+
+ const response = await request_auth.json();
+ if (!response.access_token) {
+ return NextResponse.json(
+ { error: "Failed to retrieve access token" },
+ {
+ status: 400,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ }
+
+ const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
+ headers: {
+ Authorization: `Bearer ${response.access_token}`,
+ },
+ });
+
+ if (!userResponse.ok) {
+ return NextResponse.json(
+ { user: null, errCode: userResponse.status },
+ { status: userResponse.status }
+ );
+ }
+ const user = await userResponse.json();
+
+ return NextResponse.json(
+ {
+ access_token: response.access_token,
+ expires_in: response.expires_in,
+ user,
+ },
+ {
+ status: 200,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+}
diff --git a/app/api/healthcheck/route.ts b/app/api/healthcheck/route.ts
deleted file mode 100644
index 35adb415d0f577a7a78dbc338bd6450e78cc9ec3..0000000000000000000000000000000000000000
--- a/app/api/healthcheck/route.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { NextResponse } from "next/server";
-
-export async function GET() {
- return NextResponse.json({ status: "ok" }, { status: 200 });
-}
diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be92c97380e2ad66379edede9fe07b96f697ff49
--- /dev/null
+++ b/app/api/mcp/route.ts
@@ -0,0 +1,435 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, createRepo, uploadFiles, spaceInfo, listCommits } from "@huggingface/hub";
+import { COLORS } from "@/lib/utils";
+import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
+import { Commit, Page } from "@/types";
+
+// Timeout configuration (in milliseconds)
+const OPERATION_TIMEOUT = 120000; // 2 minutes for HF operations
+
+// Extend the maximum execution time for this route
+export const maxDuration = 180; // 3 minutes
+
+// Utility function to wrap promises with timeout
+async function withTimeout(
+ promise: Promise,
+ timeoutMs: number,
+ errorMessage: string = "Operation timed out"
+): Promise {
+ let timeoutId: NodeJS.Timeout;
+
+ const timeoutPromise = new Promise((_, reject) => {
+ timeoutId = setTimeout(() => {
+ reject(new Error(errorMessage));
+ }, timeoutMs);
+ });
+
+ try {
+ const result = await Promise.race([promise, timeoutPromise]);
+ clearTimeout(timeoutId!);
+ return result;
+ } catch (error) {
+ clearTimeout(timeoutId!);
+ throw error;
+ }
+}
+
+interface MCPRequest {
+ jsonrpc: "2.0";
+ id: number | string;
+ method: string;
+ params?: any;
+}
+
+interface MCPResponse {
+ jsonrpc: "2.0";
+ id: number | string;
+ result?: any;
+ error?: {
+ code: number;
+ message: string;
+ data?: any;
+ };
+}
+
+interface CreateProjectParams {
+ title?: string;
+ pages: Page[];
+ prompt?: string;
+ hf_token?: string; // Optional - can come from header instead
+}
+
+// MCP Server over HTTP
+export async function POST(req: NextRequest) {
+ try {
+ const body: MCPRequest = await req.json();
+ const { jsonrpc, id, method, params } = body;
+
+ // Validate JSON-RPC 2.0 format
+ if (jsonrpc !== "2.0") {
+ return NextResponse.json({
+ jsonrpc: "2.0",
+ id: id || null,
+ error: {
+ code: -32600,
+ message: "Invalid Request: jsonrpc must be '2.0'",
+ },
+ });
+ }
+
+ let response: MCPResponse;
+
+ switch (method) {
+ case "initialize":
+ response = {
+ jsonrpc: "2.0",
+ id,
+ result: {
+ protocolVersion: "2024-11-05",
+ capabilities: {
+ tools: {},
+ },
+ serverInfo: {
+ name: "deepsite-mcp-server",
+ version: "1.0.0",
+ },
+ },
+ };
+ break;
+
+ case "tools/list":
+ response = {
+ jsonrpc: "2.0",
+ id,
+ result: {
+ tools: [
+ {
+ name: "create_project",
+ description: `Create a new DeepSite project. This will create a new Hugging Face Space with your HTML/CSS/JS files.
+
+Example usage:
+- Create a simple website with HTML, CSS, and JavaScript files
+- Each page needs a 'path' (filename like "index.html", "styles.css", "script.js") and 'html' (the actual content)
+- The title will be formatted to a valid repository name
+- Returns the project URL and metadata`,
+ inputSchema: {
+ type: "object",
+ properties: {
+ title: {
+ type: "string",
+ description: "Project title (optional, defaults to 'DeepSite Project'). Will be formatted to a valid repo name.",
+ },
+ pages: {
+ type: "array",
+ description: "Array of files to include in the project",
+ items: {
+ type: "object",
+ properties: {
+ path: {
+ type: "string",
+ description: "File path (e.g., 'index.html', 'styles.css', 'script.js')",
+ },
+ html: {
+ type: "string",
+ description: "File content",
+ },
+ },
+ required: ["path", "html"],
+ },
+ },
+ prompt: {
+ type: "string",
+ description: "Optional prompt/description for the commit message",
+ },
+ hf_token: {
+ type: "string",
+ description: "Hugging Face API token (optional if provided via Authorization header)",
+ },
+ },
+ required: ["pages"],
+ },
+ },
+ ],
+ },
+ };
+ break;
+
+ case "tools/call":
+ const { name, arguments: toolArgs } = params;
+
+ if (name === "create_project") {
+ try {
+ // Extract token from Authorization header if present
+ const authHeader = req.headers.get("authorization");
+ let hf_token = toolArgs.hf_token;
+
+ if (authHeader && authHeader.startsWith("Bearer ")) {
+ hf_token = authHeader.substring(7); // Remove "Bearer " prefix
+ }
+
+ const result = await handleCreateProject({
+ ...toolArgs,
+ hf_token,
+ } as CreateProjectParams);
+ response = {
+ jsonrpc: "2.0",
+ id,
+ result,
+ };
+ } catch (error: any) {
+ response = {
+ jsonrpc: "2.0",
+ id,
+ error: {
+ code: -32000,
+ message: error.message || "Failed to create project",
+ data: error.data,
+ },
+ };
+ }
+ } else {
+ response = {
+ jsonrpc: "2.0",
+ id,
+ error: {
+ code: -32601,
+ message: `Unknown tool: ${name}`,
+ },
+ };
+ }
+ break;
+
+ default:
+ response = {
+ jsonrpc: "2.0",
+ id,
+ error: {
+ code: -32601,
+ message: `Method not found: ${method}`,
+ },
+ };
+ }
+
+ return NextResponse.json(response);
+ } catch (error: any) {
+ return NextResponse.json({
+ jsonrpc: "2.0",
+ id: null,
+ error: {
+ code: -32700,
+ message: "Parse error",
+ data: error.message,
+ },
+ });
+ }
+}
+
+// Handle OPTIONS for CORS
+export async function OPTIONS() {
+ return new NextResponse(null, {
+ status: 200,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type",
+ },
+ });
+}
+
+async function handleCreateProject(params: CreateProjectParams) {
+ const { title: titleFromRequest, pages, prompt, hf_token } = params;
+
+ // Validate required parameters
+ if (!hf_token || typeof hf_token !== "string") {
+ throw new Error("hf_token is required and must be a string");
+ }
+
+ if (!pages || !Array.isArray(pages) || pages.length === 0) {
+ throw new Error("At least one page is required");
+ }
+
+ // Validate that each page has required fields
+ for (const page of pages) {
+ if (!page.path || !page.html) {
+ throw new Error("Each page must have 'path' and 'html' properties");
+ }
+ }
+
+ // Get user info from HF token
+ let username: string;
+ try {
+ const userResponse = await withTimeout(
+ fetch("https://huggingface.co/api/whoami-v2", {
+ headers: {
+ Authorization: `Bearer ${hf_token}`,
+ },
+ }),
+ 30000, // 30 seconds for authentication
+ "Authentication timeout: Unable to verify Hugging Face token"
+ );
+
+ if (!userResponse.ok) {
+ throw new Error("Invalid Hugging Face token");
+ }
+
+ const userData = await userResponse.json();
+ username = userData.name;
+ } catch (error: any) {
+ if (error.message?.includes('timeout')) {
+ throw new Error(`Authentication timeout: ${error.message}`);
+ }
+ throw new Error(`Authentication failed: ${error.message}`);
+ }
+
+ const title = titleFromRequest ?? "DeepSite Project";
+
+ const formattedTitle = title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .split("-")
+ .filter(Boolean)
+ .join("-")
+ .slice(0, 96);
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${username}/${formattedTitle}`,
+ };
+
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const README = `---
+title: ${title}
+colorFrom: ${colorFrom}
+colorTo: ${colorTo}
+emoji: π³
+sdk: static
+pinned: false
+tags:
+ - deepsite-v3
+---
+
+# Welcome to your new DeepSite project!
+This project was created with [DeepSite](https://huggingface.co/deepsite).
+`;
+
+ const files: File[] = [];
+ const readmeFile = new File([README], "README.md", { type: "text/markdown" });
+ files.push(readmeFile);
+
+ pages.forEach((page: Page) => {
+ // Determine MIME type based on file extension
+ let mimeType = "text/html";
+ if (page.path.endsWith(".css")) {
+ mimeType = "text/css";
+ } else if (page.path.endsWith(".js")) {
+ mimeType = "text/javascript";
+ } else if (page.path.endsWith(".json")) {
+ mimeType = "application/json";
+ }
+
+ // Inject the DeepSite badge script into index pages only
+ const content = mimeType === "text/html" && isIndexPage(page.path)
+ ? injectDeepSiteBadge(page.html)
+ : page.html;
+ const file = new File([content], page.path, { type: mimeType });
+ files.push(file);
+ });
+
+ try {
+ const { repoUrl } = await withTimeout(
+ createRepo({
+ repo,
+ accessToken: hf_token,
+ }),
+ 60000, // 1 minute for repo creation
+ "Timeout creating repository. Please try again."
+ );
+
+ const commitTitle = !prompt || prompt.trim() === "" ? "Initial project creation via MCP" : prompt;
+
+ await withTimeout(
+ uploadFiles({
+ repo,
+ files,
+ accessToken: hf_token,
+ commitTitle,
+ }),
+ OPERATION_TIMEOUT,
+ "Timeout uploading files. The repository was created but files may not have been uploaded."
+ );
+
+ const path = repoUrl.split("/").slice(-2).join("/");
+
+ const commits: Commit[] = [];
+ const commitIterator = listCommits({ repo, accessToken: hf_token });
+
+ // Wrap the commit listing with a timeout
+ const commitTimeout = new Promise((_, reject) => {
+ setTimeout(() => reject(new Error("Timeout listing commits")), 30000);
+ });
+
+ try {
+ await Promise.race([
+ (async () => {
+ for await (const commit of commitIterator) {
+ if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
+ continue;
+ }
+ commits.push({
+ title: commit.title,
+ oid: commit.oid,
+ date: commit.date,
+ });
+ }
+ })(),
+ commitTimeout
+ ]);
+ } catch (error: any) {
+ // If listing commits times out, continue with empty commits array
+ console.error("Failed to list commits:", error.message);
+ }
+
+ const space = await withTimeout(
+ spaceInfo({
+ name: repo.name,
+ accessToken: hf_token,
+ }),
+ 30000, // 30 seconds for space info
+ "Timeout fetching space information"
+ );
+
+ const projectUrl = `https://huggingface.co/deepsite/${path}`;
+ const spaceUrl = `https://huggingface.co/spaces/${path}`;
+ const liveUrl = `https://${username}-${formattedTitle}.hf.space`;
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(
+ {
+ success: true,
+ message: "Project created successfully!",
+ projectUrl,
+ spaceUrl,
+ liveUrl,
+ spaceId: space.name,
+ projectId: space.id,
+ files: pages.map((p) => p.path),
+ updatedAt: space.updatedAt,
+ },
+ null,
+ 2
+ ),
+ },
+ ],
+ };
+ } catch (err: any) {
+ if (err.message?.includes('timeout') || err.message?.includes('Timeout')) {
+ throw new Error(err.message || "Operation timed out. Please try again.");
+ }
+ throw new Error(err.message || "Failed to create project");
+ }
+}
+
diff --git a/app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts b/app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d62b38711819e9b9888fb186c825f334ff5c8f2c
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
@@ -0,0 +1,240 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles, downloadFile } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import { Page } from "@/types";
+
+export async function POST(
+ req: NextRequest,
+ { params }: {
+ params: Promise<{
+ namespace: string;
+ repoId: string;
+ commitId: string;
+ }>
+ }
+) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const param = await params;
+ const { namespace, repoId, commitId } = param;
+
+ try {
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ const space = await spaceInfo({
+ name: `${namespace}/${repoId}`,
+ accessToken: user.token as string,
+ additionalFields: ["author"],
+ });
+
+ if (!space || space.sdk !== "static") {
+ return NextResponse.json(
+ { ok: false, error: "Space is not a static space." },
+ { status: 404 }
+ );
+ }
+
+ if (space.author !== user.name) {
+ return NextResponse.json(
+ { ok: false, error: "Space does not belong to the authenticated user." },
+ { status: 403 }
+ );
+ }
+
+ const files: File[] = [];
+ const pages: Page[] = [];
+ const mediaFiles: string[] = [];
+ const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
+ 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"];
+ const commitFilePaths: Set = new Set();
+
+ for await (const fileInfo of listFiles({
+ repo,
+ accessToken: user.token as string,
+ revision: commitId,
+ })) {
+ const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
+
+ if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
+ commitFilePaths.add(fileInfo.path);
+
+ const blob = await downloadFile({
+ repo,
+ accessToken: user.token as string,
+ path: fileInfo.path,
+ revision: commitId,
+ raw: true
+ }).catch((error) => {
+ return null;
+ });
+ if (!blob) {
+ continue;
+ }
+ const content = await blob?.text();
+
+ if (content) {
+ let mimeType = "text/plain";
+
+ switch (fileExtension) {
+ case "html":
+ mimeType = "text/html";
+ break;
+ case "css":
+ mimeType = "text/css";
+ break;
+ case "js":
+ mimeType = "application/javascript";
+ break;
+ case "json":
+ mimeType = "application/json";
+ break;
+ }
+
+ if (fileInfo.path === "index.html") {
+ pages.unshift({
+ path: fileInfo.path,
+ html: content,
+ });
+ } else {
+ pages.push({
+ path: fileInfo.path,
+ html: content,
+ });
+ }
+
+ const file = new File([content], fileInfo.path, { type: mimeType });
+ files.push(file);
+ }
+ }
+ else if (fileInfo.type === "directory" && (["videos", "images", "audio"].includes(fileInfo.path) || fileInfo.path === "components")) {
+ for await (const subFileInfo of listFiles({
+ repo,
+ accessToken: user.token as string,
+ revision: commitId,
+ path: fileInfo.path,
+ })) {
+ if (subFileInfo.path.includes("components")) {
+ commitFilePaths.add(subFileInfo.path);
+ const blob = await downloadFile({
+ repo,
+ accessToken: user.token as string,
+ path: subFileInfo.path,
+ revision: commitId,
+ raw: true
+ }).catch((error) => {
+ return null;
+ });
+ if (!blob) {
+ continue;
+ }
+ const content = await blob?.text();
+
+ if (content) {
+ pages.push({
+ path: subFileInfo.path,
+ html: content,
+ });
+
+ const file = new File([content], subFileInfo.path, { type: "text/html" });
+ files.push(file);
+ }
+ } else if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
+ commitFilePaths.add(subFileInfo.path);
+ mediaFiles.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
+ }
+ }
+ }
+ else if (allowedExtensions.includes(fileExtension || "")) {
+ commitFilePaths.add(fileInfo.path);
+ }
+ }
+
+ const mainBranchFilePaths: Set = new Set();
+ for await (const fileInfo of listFiles({
+ repo,
+ accessToken: user.token as string,
+ revision: "main",
+ })) {
+ const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
+
+ if (allowedExtensions.includes(fileExtension || "")) {
+ mainBranchFilePaths.add(fileInfo.path);
+ }
+ }
+
+ const filesToDelete: string[] = [];
+ for (const mainFilePath of mainBranchFilePaths) {
+ if (!commitFilePaths.has(mainFilePath)) {
+ filesToDelete.push(mainFilePath);
+ }
+ }
+
+ if (files.length === 0 && filesToDelete.length === 0) {
+ return NextResponse.json(
+ { ok: false, error: "No files found in the specified commit and no files to delete" },
+ { status: 404 }
+ );
+ }
+
+ if (filesToDelete.length > 0) {
+ await deleteFiles({
+ repo,
+ paths: filesToDelete,
+ accessToken: user.token as string,
+ commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
+ commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
+ });
+ }
+
+ if (files.length > 0) {
+ await uploadFiles({
+ repo,
+ files,
+ accessToken: user.token as string,
+ commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
+ commitDescription: `Promoted commit ${commitId} to main branch`,
+ });
+ }
+
+ return NextResponse.json(
+ {
+ ok: true,
+ message: "Version promoted successfully",
+ promotedCommit: commitId,
+ pages: pages,
+ files: mediaFiles,
+ },
+ { status: 200 }
+ );
+
+ } catch (error: any) {
+
+ // Handle specific HuggingFace API errors
+ if (error.statusCode === 404) {
+ return NextResponse.json(
+ { ok: false, error: "Commit not found" },
+ { status: 404 }
+ );
+ }
+
+ if (error.statusCode === 403) {
+ return NextResponse.json(
+ { ok: false, error: "Access denied to repository" },
+ { status: 403 }
+ );
+ }
+
+ return NextResponse.json(
+ { ok: false, error: error.message || "Failed to promote version" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts b/app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..993215f7558d36bcb38f566650b0469f40876686
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts
@@ -0,0 +1,107 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import { Page } from "@/types";
+
+export async function GET(
+ req: NextRequest,
+ { params }: {
+ params: Promise<{
+ namespace: string;
+ repoId: string;
+ commitId: string;
+ }>
+ }
+) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const param = await params;
+ const { namespace, repoId, commitId } = param;
+
+ try {
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ const space = await spaceInfo({
+ name: `${namespace}/${repoId}`,
+ accessToken: user.token as string,
+ additionalFields: ["author"],
+ });
+
+ if (!space || space.sdk !== "static") {
+ return NextResponse.json(
+ { ok: false, error: "Space is not a static space." },
+ { status: 404 }
+ );
+ }
+
+ if (space.author !== user.name) {
+ return NextResponse.json(
+ { ok: false, error: "Space does not belong to the authenticated user." },
+ { status: 403 }
+ );
+ }
+
+ const pages: Page[] = [];
+
+ for await (const fileInfo of listFiles({
+ repo,
+ accessToken: user.token as string,
+ revision: commitId,
+ })) {
+ const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
+
+ if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
+ const blob = await downloadFile({
+ repo,
+ accessToken: user.token as string,
+ path: fileInfo.path,
+ revision: commitId,
+ raw: true
+ }).catch((error) => {
+ return null;
+ });
+ if (!blob) {
+ continue;
+ }
+ const content = await blob?.text();
+
+ if (content) {
+ if (fileInfo.path === "index.html") {
+ pages.unshift({
+ path: fileInfo.path,
+ html: content,
+ });
+ } else {
+ pages.push({
+ path: fileInfo.path,
+ html: content,
+ });
+ }
+ }
+ }
+ }
+
+ return NextResponse.json({
+ ok: true,
+ pages,
+ });
+ } catch (error: any) {
+ console.error("Error fetching commit pages:", error);
+ return NextResponse.json(
+ {
+ ok: false,
+ error: error.message || "Failed to fetch commit pages",
+ },
+ { status: 500 }
+ );
+ }
+}
+
diff --git a/app/api/me/projects/[namespace]/[repoId]/download/route.ts b/app/api/me/projects/[namespace]/[repoId]/download/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2c731029dfa09b56a6e4245a83c2518842e7f7a6
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/download/route.ts
@@ -0,0 +1,110 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
+import JSZip from "jszip";
+
+import { isAuthenticated } from "@/lib/auth";
+
+export async function GET(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const param = await params;
+ const { namespace, repoId } = param;
+
+ try {
+ const space = await spaceInfo({
+ name: `${namespace}/${repoId}`,
+ accessToken: user.token as string,
+ additionalFields: ["author"],
+ });
+
+ if (!space || space.sdk !== "static") {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Space is not a static space",
+ },
+ { status: 404 }
+ );
+ }
+
+ if (space.author !== user.name) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Space does not belong to the authenticated user",
+ },
+ { status: 403 }
+ );
+ }
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ const zip = new JSZip();
+
+ for await (const fileInfo of listFiles({
+ repo,
+ accessToken: user.token as string,
+ recursive: true,
+ })) {
+ if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) {
+ continue;
+ }
+
+ try {
+ const blob = await downloadFile({
+ repo,
+ accessToken: user.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 = `${namespace}-${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: any) {
+ return NextResponse.json(
+ { ok: false, error: error.message || "Failed to create ZIP file" },
+ { status: 500 }
+ );
+ }
+}
+
diff --git a/app/api/me/projects/[namespace]/[repoId]/images/route.ts b/app/api/me/projects/[namespace]/[repoId]/images/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d91aa3c8a33b9cc27c675289d902f168df79cdc4
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/images/route.ts
@@ -0,0 +1,123 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+
+export async function POST(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ try {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const param = await params;
+ const { namespace, repoId } = param;
+
+ const space = await spaceInfo({
+ name: `${namespace}/${repoId}`,
+ accessToken: user.token as string,
+ additionalFields: ["author"],
+ });
+
+ if (!space || space.sdk !== "static") {
+ return NextResponse.json(
+ { ok: false, error: "Space is not a static space." },
+ { status: 404 }
+ );
+ }
+
+ if (space.author !== user.name) {
+ return NextResponse.json(
+ { ok: false, error: "Space does not belong to the authenticated user." },
+ { status: 403 }
+ );
+ }
+
+ // Parse the FormData to get the media files
+ const formData = await req.formData();
+ const mediaFiles = formData.getAll("images") as File[];
+
+ if (!mediaFiles || mediaFiles.length === 0) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "At least one media file is required under the 'images' key",
+ },
+ { status: 400 }
+ );
+ }
+
+ const files: File[] = [];
+ for (const file of mediaFiles) {
+ if (!(file instanceof File)) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Invalid file format - all items under 'images' key must be files",
+ },
+ { status: 400 }
+ );
+ }
+
+ // Check if file is a supported media type
+ const isImage = file.type.startsWith('image/');
+ const isVideo = file.type.startsWith('video/');
+ const isAudio = file.type.startsWith('audio/');
+
+ if (!isImage && !isVideo && !isAudio) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: `File ${file.name} is not a supported media type (image, video, or audio)`,
+ },
+ { status: 400 }
+ );
+ }
+
+ // Create File object with appropriate folder prefix
+ let folderPrefix = 'images/';
+ if (isVideo) {
+ folderPrefix = 'videos/';
+ } else if (isAudio) {
+ folderPrefix = 'audio/';
+ }
+
+ const fileName = `${folderPrefix}${file.name}`;
+ const processedFile = new File([file], fileName, { type: file.type });
+ files.push(processedFile);
+ }
+
+ // Upload files to HuggingFace space
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ await uploadFiles({
+ repo,
+ files,
+ accessToken: user.token as string,
+ commitTitle: `Upload ${files.length} media file(s)`,
+ });
+
+ return NextResponse.json({
+ ok: true,
+ message: `Successfully uploaded ${files.length} media file(s) to ${namespace}/${repoId}/`,
+ uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
+ }, { status: 200 });
+
+ } catch (error) {
+ console.error('Error uploading media files:', error);
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Failed to upload media files",
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/me/projects/[namespace]/[repoId]/route.ts b/app/api/me/projects/[namespace]/[repoId]/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a6034cb3f27016817e14b55682c92a3ed9134223
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/route.ts
@@ -0,0 +1,207 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, spaceInfo, listFiles, deleteRepo, listCommits, downloadFile } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import { Commit, Page } from "@/types";
+
+export async function DELETE(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const param = await params;
+ const { namespace, repoId } = param;
+
+ try {
+ const space = await spaceInfo({
+ name: `${namespace}/${repoId}`,
+ accessToken: user.token as string,
+ additionalFields: ["author"],
+ });
+
+ if (!space || space.sdk !== "static") {
+ return NextResponse.json(
+ { ok: false, error: "Space is not a static space." },
+ { status: 404 }
+ );
+ }
+
+ if (space.author !== user.name) {
+ return NextResponse.json(
+ { ok: false, error: "Space does not belong to the authenticated user." },
+ { status: 403 }
+ );
+ }
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ await deleteRepo({
+ repo,
+ accessToken: user.token as string,
+ });
+
+
+ return NextResponse.json({ ok: true }, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json(
+ { ok: false, error: error.message },
+ { status: 500 }
+ );
+ }
+}
+
+export async function GET(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ const user = await isAuthenticated();
+
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const param = await params;
+ const { namespace, repoId } = param;
+
+ try {
+ const space = await spaceInfo({
+ name: namespace + "/" + repoId,
+ accessToken: user.token as string,
+ additionalFields: ["author"],
+ });
+
+ if (!space || space.sdk !== "static") {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Space is not a static space",
+ },
+ { status: 404 }
+ );
+ }
+ if (space.author !== user.name) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "Space does not belong to the authenticated user",
+ },
+ { status: 403 }
+ );
+ }
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ };
+
+ const htmlFiles: Page[] = [];
+ const files: string[] = [];
+
+ const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif", "mp4", "webm", "ogg", "avi", "mov", "mp3", "wav", "ogg", "aac", "m4a"];
+
+ for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
+ if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
+ const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true }).catch((error) => {
+ return null;
+ });
+ if (!blob) {
+ continue;
+ }
+ const html = await blob?.text();
+ if (!html) {
+ continue;
+ }
+ if (fileInfo.path === "index.html") {
+ htmlFiles.unshift({
+ path: fileInfo.path,
+ html,
+ });
+ } else {
+ htmlFiles.push({
+ path: fileInfo.path,
+ html,
+ });
+ }
+ }
+ if (fileInfo.type === "directory") {
+ for await (const subFileInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
+ if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
+ files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
+ } else {
+ const blob = await downloadFile({ repo, accessToken: user.token as string, path: subFileInfo.path, raw: true }).catch((error) => {
+ return null;
+ });
+ if (!blob) {
+ continue;
+ }
+ const html = await blob?.text();
+ if (!html) {
+ continue;
+ }
+ htmlFiles.push({
+ path: subFileInfo.path,
+ html,
+ });
+ }
+ }
+ }
+ }
+ const commits: Commit[] = [];
+ for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
+ if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
+ continue;
+ }
+ commits.push({
+ title: commit.title,
+ oid: commit.oid,
+ date: commit.date,
+ });
+ }
+
+ if (htmlFiles.length === 0) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: "No HTML files found",
+ },
+ { status: 404 }
+ );
+ }
+ return NextResponse.json(
+ {
+ project: {
+ id: space.id,
+ space_id: space.name,
+ private: space.private,
+ _updatedAt: space.updatedAt,
+ },
+ pages: htmlFiles,
+ files,
+ commits,
+ ok: true,
+ },
+ { status: 200 }
+ );
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ if (error.statusCode === 404) {
+ return NextResponse.json(
+ { error: "Space not found", ok: false },
+ { status: 404 }
+ );
+ }
+ return NextResponse.json(
+ { error: error.message, ok: false },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/me/projects/[namespace]/[repoId]/save/route.ts b/app/api/me/projects/[namespace]/[repoId]/save/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0a99d0c14b299872f4f9c8c874769bd198894ee
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/save/route.ts
@@ -0,0 +1,72 @@
+import { NextRequest, NextResponse } from "next/server";
+import { uploadFiles } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import { Page } from "@/types";
+
+export async function PUT(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ const user = await isAuthenticated();
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const param = await params;
+ const { namespace, repoId } = param;
+ const { pages, commitTitle = "Manual changes saved" } = await req.json();
+
+ if (!pages || !Array.isArray(pages) || pages.length === 0) {
+ return NextResponse.json(
+ { ok: false, error: "Pages are required" },
+ { status: 400 }
+ );
+ }
+
+ try {
+ // Prepare files for upload
+ const files: File[] = [];
+ pages.forEach((page: Page) => {
+ // Determine MIME type based on file extension
+ let mimeType = "text/html";
+ if (page.path.endsWith(".css")) {
+ mimeType = "text/css";
+ } else if (page.path.endsWith(".js")) {
+ mimeType = "text/javascript";
+ } else if (page.path.endsWith(".json")) {
+ mimeType = "application/json";
+ }
+ const file = new File([page.html], page.path, { type: mimeType });
+ files.push(file);
+ });
+
+ const response = await uploadFiles({
+ repo: {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ },
+ files,
+ commitTitle,
+ accessToken: user.token as string,
+ });
+
+ return NextResponse.json({
+ ok: true,
+ pages,
+ commit: {
+ ...response.commit,
+ title: commitTitle,
+ }
+ });
+ } catch (error: any) {
+ console.error("Error saving manual changes:", error);
+ return NextResponse.json(
+ {
+ ok: false,
+ error: error.message || "Failed to save changes",
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/me/projects/[namespace]/[repoId]/update/route.ts b/app/api/me/projects/[namespace]/[repoId]/update/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dad31b4e988a5ce815ba90ce76186a0ec4f00b1c
--- /dev/null
+++ b/app/api/me/projects/[namespace]/[repoId]/update/route.ts
@@ -0,0 +1,141 @@
+import { NextRequest, NextResponse } from "next/server";
+import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import { Page } from "@/types";
+import { COLORS } from "@/lib/utils";
+import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
+import { pagesToFiles } from "@/lib/format-ai-response";
+
+/**
+ * UPDATE route - for updating existing projects or creating new ones after AI streaming
+ * This route handles the HuggingFace upload after client-side AI response processing
+ */
+export async function PUT(
+ req: NextRequest,
+ { params }: { params: Promise<{ namespace: string; repoId: string }> }
+) {
+ const user = await isAuthenticated();
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const param = await params;
+ let { namespace, repoId } = param;
+ const { pages, commitTitle = "AI-generated changes", isNew, projectName } = await req.json();
+
+ if (!pages || !Array.isArray(pages) || pages.length === 0) {
+ return NextResponse.json(
+ { ok: false, error: "Pages are required" },
+ { status: 400 }
+ );
+ }
+
+ try {
+ let files: File[];
+
+ if (isNew) {
+ // Creating a new project
+ const title = projectName || "DeepSite Project";
+ const formattedTitle = title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .split("-")
+ .filter(Boolean)
+ .join("-")
+ .slice(0, 96);
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${user.name}/${formattedTitle}`,
+ };
+
+ try {
+ const { repoUrl } = await createRepo({
+ repo,
+ accessToken: user.token as string,
+ });
+ namespace = user.name;
+ repoId = repoUrl.split("/").slice(-2).join("/").split("/")[1];
+ } catch (createRepoError: any) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: `Failed to create repository: ${createRepoError.message || 'Unknown error'}`,
+ },
+ { status: 500 }
+ );
+ }
+
+ // Prepare files with badge injection for new projects
+ files = [];
+ pages.forEach((page: Page) => {
+ let mimeType = "text/html";
+ if (page.path.endsWith(".css")) {
+ mimeType = "text/css";
+ } else if (page.path.endsWith(".js")) {
+ mimeType = "text/javascript";
+ } else if (page.path.endsWith(".json")) {
+ mimeType = "application/json";
+ }
+ const content = (mimeType === "text/html" && isIndexPage(page.path))
+ ? injectDeepSiteBadge(page.html)
+ : page.html;
+ const file = new File([content], page.path, { type: mimeType });
+ files.push(file);
+ });
+
+ // Add README.md for new projects
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const README = `---
+title: ${title}
+colorFrom: ${colorFrom}
+colorTo: ${colorTo}
+emoji: π³
+sdk: static
+pinned: false
+tags:
+ - deepsite-v3
+---
+
+# Welcome to your new DeepSite project!
+This project was created with [DeepSite](https://huggingface.co/deepsite).
+`;
+ files.push(new File([README], "README.md", { type: "text/markdown" }));
+ } else {
+ // Updating existing project - no badge injection
+ files = pagesToFiles(pages);
+ }
+
+ const response = await uploadFiles({
+ repo: {
+ type: "space",
+ name: `${namespace}/${repoId}`,
+ },
+ files,
+ commitTitle,
+ accessToken: user.token as string,
+ });
+
+ return NextResponse.json({
+ ok: true,
+ pages,
+ repoId: `${namespace}/${repoId}`,
+ commit: {
+ ...response.commit,
+ title: commitTitle,
+ }
+ });
+ } catch (error: any) {
+ console.error("Error updating project:", error);
+ return NextResponse.json(
+ {
+ ok: false,
+ error: error.message || "Failed to update project",
+ },
+ { status: 500 }
+ );
+ }
+}
+
diff --git a/app/api/me/projects/route.ts b/app/api/me/projects/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dec5b655708390a0779bb2917defee6ab1bb02b2
--- /dev/null
+++ b/app/api/me/projects/route.ts
@@ -0,0 +1,121 @@
+import { NextRequest, NextResponse } from "next/server";
+import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from "@huggingface/hub";
+
+import { isAuthenticated } from "@/lib/auth";
+import { Commit, Page } from "@/types";
+import { COLORS } from "@/lib/utils";
+import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
+
+export async function POST(
+ req: NextRequest,
+) {
+ const user = await isAuthenticated();
+ if (user instanceof NextResponse || !user) {
+ return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
+ }
+
+ const { title: titleFromRequest, pages, prompt } = await req.json();
+
+ const title = titleFromRequest ?? "DeepSite Project";
+
+ const formattedTitle = title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .split("-")
+ .filter(Boolean)
+ .join("-")
+ .slice(0, 96);
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: `${user.name}/${formattedTitle}`,
+ };
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const README = `---
+title: ${title}
+colorFrom: ${colorFrom}
+colorTo: ${colorTo}
+emoji: π³
+sdk: static
+pinned: false
+tags:
+ - deepsite-v3
+---
+
+# Welcome to your new DeepSite project!
+This project was created with [DeepSite](https://huggingface.co/deepsite).
+`;
+
+ const files: File[] = [];
+ const readmeFile = new File([README], "README.md", { type: "text/markdown" });
+ files.push(readmeFile);
+ pages.forEach((page: Page) => {
+ // Determine MIME type based on file extension
+ let mimeType = "text/html";
+ if (page.path.endsWith(".css")) {
+ mimeType = "text/css";
+ } else if (page.path.endsWith(".js")) {
+ mimeType = "text/javascript";
+ } else if (page.path.endsWith(".json")) {
+ mimeType = "application/json";
+ }
+ // Inject the DeepSite badge script into index pages only (not components or other HTML files)
+ const content = (mimeType === "text/html" && isIndexPage(page.path))
+ ? injectDeepSiteBadge(page.html)
+ : page.html;
+ const file = new File([content], page.path, { type: mimeType });
+ files.push(file);
+ });
+
+ try {
+ const { repoUrl} = await createRepo({
+ repo,
+ accessToken: user.token as string,
+ });
+ const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
+ await uploadFiles({
+ repo,
+ files,
+ accessToken: user.token as string,
+ commitTitle
+ });
+
+ const path = repoUrl.split("/").slice(-2).join("/");
+
+ const commits: Commit[] = [];
+ for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
+ if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) {
+ continue;
+ }
+ commits.push({
+ title: commit.title,
+ oid: commit.oid,
+ date: commit.date,
+ });
+ }
+
+ const space = await spaceInfo({
+ name: repo.name,
+ accessToken: user.token as string,
+ });
+
+ let newProject = {
+ files,
+ pages,
+ commits,
+ project: {
+ id: space.id,
+ space_id: space.name,
+ _updatedAt: space.updatedAt,
+ }
+ }
+
+ return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
+ } catch (err: any) {
+ return NextResponse.json(
+ { error: err.message, ok: false },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/me/route.ts b/app/api/me/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..25bc6c81aa826d65cac703e43c5d4647ae5cd141
--- /dev/null
+++ b/app/api/me/route.ts
@@ -0,0 +1,46 @@
+import { listSpaces } from "@huggingface/hub";
+import { headers } from "next/headers";
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ const authHeaders = await headers();
+ const token = authHeaders.get("Authorization");
+ if (!token) {
+ return NextResponse.json({ user: null, errCode: 401 }, { status: 401 });
+ }
+
+ const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
+ headers: {
+ Authorization: `${token}`,
+ },
+ });
+
+ if (!userResponse.ok) {
+ return NextResponse.json(
+ { user: null, errCode: userResponse.status },
+ { status: userResponse.status }
+ );
+ }
+ const user = await userResponse.json();
+ const projects = [];
+ for await (const space of listSpaces({
+ accessToken: token.replace("Bearer ", "") as string,
+ additionalFields: ["author", "cardData"],
+ search: {
+ owner: user.name,
+ }
+ })) {
+ if (
+ space.sdk === "static" &&
+ Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
+ (
+ ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
+ ((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
+ )
+ ) {
+ projects.push(space);
+ }
+ }
+
+ return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
+}
diff --git a/app/api/projects/[repoId]/[commitId]/route.ts b/app/api/projects/[repoId]/[commitId]/route.ts
deleted file mode 100644
index 0362d6c43ec226bf97837f2f88196ad43eb056f2..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/[commitId]/route.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { auth } from "@/lib/auth";
-import { createBranch, RepoDesignation } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function POST(
- request: Request,
- { params }: { params: Promise<{ repoId: string; commitId: string }> }
-) {
- const { repoId, commitId }: { repoId: string; commitId: string } =
- await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const commitTitle = `π ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Set commit ${commitId} as default.`;
-
- await fetch(
- `https://huggingface.co/api/spaces/${session.user?.username}/${repoId}/branch/main`,
- {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- startingPoint: commitId,
- overwrite: true,
- }),
- }
- ).catch((error) => {
- return NextResponse.json(
- { error: error ?? "Failed to create branch" },
- { status: 500 }
- );
- });
-
- return NextResponse.json({ success: true }, { status: 200 });
-}
diff --git a/app/api/projects/[repoId]/download/route.ts b/app/api/projects/[repoId]/download/route.ts
deleted file mode 100644
index 43e8f6638d5eb2aca8ba3069d696b05056d96633..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/download/route.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { auth } from "@/lib/auth";
-import { downloadFile, listFiles, RepoDesignation } from "@huggingface/hub";
-import { NextResponse } from "next/server";
-import JSZip from "jszip";
-
-export async function GET(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- try {
- const zip = new JSZip();
- for await (const fileInfo of listFiles({
- repo,
- accessToken: token as string,
- recursive: true,
- })) {
- if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) {
- continue;
- }
-
- try {
- const blob = await downloadFile({
- repo,
- accessToken: token as string,
- path: fileInfo.path,
- raw: true
- }).catch((error) => {
- return null;
- });
- if (!blob) {
- continue;
- }
-
- if (blob) {
- const arrayBuffer = await blob.arrayBuffer();
- zip.file(fileInfo.path, arrayBuffer);
- }
- } catch (error) {
- console.error(`Error downloading file ${fileInfo.path}:`, error);
- }
- }
-
- const zipBlob = await zip.generateAsync({
- type: "blob",
- compression: "DEFLATE",
- compressionOptions: {
- level: 6
- }
- });
-
- const projectName = `${session.user?.username}-${repoId}`.replace(/[^a-zA-Z0-9-_]/g, '_');
- const filename = `${projectName}.zip`;
-
- return new NextResponse(zipBlob, {
- headers: {
- "Content-Type": "application/zip",
- "Content-Disposition": `attachment; filename="${filename}"`,
- "Content-Length": zipBlob.size.toString(),
- },
- });
- } catch (error) {
- console.error("Error downloading project:", error);
- return NextResponse.json({ error: "Failed to download project" }, { status: 500 });
- }
-}
\ No newline at end of file
diff --git a/app/api/projects/[repoId]/medias/route.ts b/app/api/projects/[repoId]/medias/route.ts
deleted file mode 100644
index 2040623536ab6775152177a317e0b11dc6cae3a2..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/medias/route.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { auth } from "@/lib/auth";
-import { RepoDesignation, uploadFiles } from "@huggingface/hub";
-import { NextResponse } from "next/server";
-
-export async function POST(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const formData = await request.formData();
- const newMedias = formData.getAll("images") as File[];
-
- const filesToUpload: File[] = [];
-
- if (!newMedias || newMedias.length === 0) {
- return NextResponse.json(
- {
- ok: false,
- error: "At least one media file is required under the 'images' key",
- },
- { status: 400 }
- );
- }
-
- try {
- for (const media of newMedias) {
- const isImage = media.type.startsWith("image/");
- const isVideo = media.type.startsWith("video/");
- const isAudio = media.type.startsWith("audio/");
-
- const folderPath = isImage
- ? "images/"
- : isVideo
- ? "videos/"
- : isAudio
- ? "audios/"
- : null;
-
- if (!folderPath) {
- return NextResponse.json(
- { ok: false, error: "Unsupported media type: " + media.type },
- { status: 400 }
- );
- }
-
- const mediaName = `${folderPath}${media.name}`;
- const processedFile = new File([media], mediaName, { type: media.type });
- filesToUpload.push(processedFile);
- }
-
- await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token,
- commitTitle: `π Upload media files through DeepSite`,
- });
-
- return NextResponse.json(
- {
- success: true,
- medias: filesToUpload.map(
- (file) =>
- `https://huggingface.co/spaces/${session.user?.username}/${repoId}/resolve/main/${file.name}`
- ),
- },
- { status: 200 }
- );
- } catch (error) {
- return NextResponse.json(
- { ok: false, error: error ?? "Failed to upload media files" },
- { status: 500 }
- );
- }
-
- return NextResponse.json({ success: true }, { status: 200 });
-}
diff --git a/app/api/projects/[repoId]/rename/route.ts b/app/api/projects/[repoId]/rename/route.ts
deleted file mode 100644
index 1d3a00b676effdc7d9106c02e6d7ec40384026c9..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/rename/route.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { auth } from "@/lib/auth";
-import { downloadFile, RepoDesignation, uploadFile } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function PUT(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { newTitle } = body;
-
- if (!newTitle) {
- return NextResponse.json(
- { error: "newTitle is required" },
- { status: 400 }
- );
- }
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: "README.md",
- raw: true,
- }).catch((_) => {
- return null;
- });
-
- if (!blob) {
- return NextResponse.json(
- { error: "Could not fetch README.md" },
- { status: 500 }
- );
- }
-
- const readmeFile = await blob?.text();
- if (!readmeFile) {
- return NextResponse.json(
- { error: "Could not read README.md content" },
- { status: 500 }
- );
- }
-
- // Escape YAML values to prevent injection attacks
- const escapeYamlValue = (value: string): string => {
- if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) {
- return `"${value.replace(/"/g, '\\"')}"`;
- }
- return value;
- };
-
- // Escape commit message to prevent injection
- const escapeCommitMessage = (message: string): string => {
- return message.replace(/[\r\n]/g, " ").slice(0, 200);
- };
-
- const updatedReadmeFile = readmeFile.replace(
- /^title:\s*(.*)$/m,
- `title: ${escapeYamlValue(newTitle)}`
- );
-
- await uploadFile({
- repo,
- accessToken: token,
- file: new File([updatedReadmeFile], "README.md", { type: "text/markdown" }),
- commitTitle: escapeCommitMessage(
- `π³ ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Rename project to "${newTitle}"`
- ),
- });
-
- return NextResponse.json(
- {
- success: true,
- },
- { status: 200 }
- );
-}
diff --git a/app/api/projects/[repoId]/route.ts b/app/api/projects/[repoId]/route.ts
deleted file mode 100644
index e6daee03652b083d7f1d852f7367d7f55fd3f390..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/route.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { auth } from "@/lib/auth";
-import { RepoDesignation, deleteRepo, uploadFiles } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function PUT(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { files, prompt, isManualChanges } = body;
-
- if (!files) {
- return NextResponse.json({ error: "Files are required" }, { status: 400 });
- }
-
- if (!prompt) {
- return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
- }
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const filesToUpload: File[] = [];
- for (const file of files) {
- let mimeType = "text/x-python";
- if (file.path.endsWith(".txt")) {
- mimeType = "text/plain";
- } else if (file.path.endsWith(".md")) {
- mimeType = "text/markdown";
- } else if (file.path.endsWith(".json")) {
- mimeType = "application/json";
- }
- filesToUpload.push(new File([file.content], file.path, { type: mimeType }));
- }
- // Escape commit title to prevent injection
- const escapeCommitTitle = (title: string): string => {
- return title.replace(/[\r\n]/g, " ").slice(0, 200);
- };
-
- const baseTitle = isManualChanges
- ? ""
- : `π³ ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `;
- const commitTitle = escapeCommitTitle(
- baseTitle + (prompt ?? "Follow-up DeepSite commit")
- );
- const response = await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token,
- commitTitle,
- });
-
- return NextResponse.json(
- {
- success: true,
- commit: {
- oid: response.commit,
- title: commitTitle,
- date: new Date(),
- },
- },
- { status: 200 }
- );
-}
-
-export async function DELETE(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- try {
- await deleteRepo({
- repo,
- accessToken: token as string,
- });
-
- return NextResponse.json({ success: true }, { status: 200 });
- } catch (error) {
- const errMsg =
- error instanceof Error ? error.message : "Failed to delete project";
- return NextResponse.json({ error: errMsg }, { status: 500 });
- }
-}
diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts
deleted file mode 100644
index b70c310b4167077d4d8bcd15423defb1d158e2dd..0000000000000000000000000000000000000000
--- a/app/api/projects/route.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { NextResponse } from "next/server";
-import { RepoDesignation, createRepo, uploadFiles } from "@huggingface/hub";
-
-import { auth } from "@/lib/auth";
-import {
- COLORS,
- EMOJIS_FOR_SPACE,
- injectDeepSiteBadge,
- isIndexPage,
-} from "@/lib/utils";
-
-// todo: catch error while publishing project, and return the error to the user
-// if space has been created, but can't push, try again or catch well the error and return the error to the user
-
-export async function POST(request: Request) {
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { projectTitle, files, prompt } = body;
-
- if (!files) {
- return NextResponse.json(
- { error: "Project title and files are required" },
- { status: 400 }
- );
- }
-
- const title =
- projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project";
-
- let formattedTitle = title
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, "-")
- .split("-")
- .filter(Boolean)
- .join("-")
- .slice(0, 75);
-
- formattedTitle =
- formattedTitle + "-" + Math.random().toString(36).substring(2, 7);
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + formattedTitle,
- };
-
- // Escape YAML values to prevent injection attacks
- const escapeYamlValue = (value: string): string => {
- if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) {
- return `"${value.replace(/"/g, '\\"')}"`;
- }
- return value;
- };
-
- // Escape markdown headers to prevent injection
- const escapeMarkdownHeader = (value: string): string => {
- return value.replace(/^#+\s*/g, "").replace(/\n/g, " ");
- };
-
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
- const emoji =
- EMOJIS_FOR_SPACE[Math.floor(Math.random() * EMOJIS_FOR_SPACE.length)];
- const README = `---
-title: ${escapeYamlValue(projectTitle)}
-colorFrom: ${colorFrom}
-colorTo: ${colorTo}
-sdk: static
-emoji: ${emoji}
-tags:
- - deepsite-v4
----
-
-# ${escapeMarkdownHeader(title)}
-
-This project has been created with [DeepSite](https://deepsite.hf.co) AI Vibe Coding.
-`;
-
- const filesToUpload: File[] = [
- new File([README], "README.md", { type: "text/markdown" }),
- ];
- for (const file of files) {
- let mimeType = "text/html";
- if (file.path.endsWith(".css")) {
- mimeType = "text/css";
- } else if (file.path.endsWith(".js")) {
- mimeType = "text/javascript";
- }
- const content =
- mimeType === "text/html" && isIndexPage(file.path)
- ? injectDeepSiteBadge(file.content)
- : file.content;
-
- filesToUpload.push(new File([content], file.path, { type: mimeType }));
- }
-
- let repoUrl: string | undefined;
-
- try {
- // Create the space first
- const createResult = await createRepo({
- accessToken: token as string,
- repo: repo,
- sdk: "static",
- });
- repoUrl = createResult.repoUrl;
-
- // Escape commit message to prevent injection
- const escapeCommitMessage = (message: string): string => {
- return message.replace(/[\r\n]/g, " ").slice(0, 200);
- };
- const commitMessage = escapeCommitMessage(prompt ?? "Initial DeepSite commit");
-
- // Upload files to the created space
- await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token as string,
- commitTitle: commitMessage,
- });
-
- const path = repoUrl.split("/").slice(-2).join("/");
-
- return NextResponse.json({ repoUrl: path }, { status: 200 });
- } catch (error) {
- const errMsg =
- error instanceof Error ? error.message : "Failed to create or upload to space";
-
- // If space was created but upload failed, include the repo URL in the error
- if (repoUrl) {
- const path = repoUrl.split("/").slice(-2).join("/");
- return NextResponse.json({
- error: `${errMsg}. Space was created at ${path} but files could not be uploaded.`,
- repoUrl: path,
- partialSuccess: true
- }, { status: 500 });
- }
-
- return NextResponse.json({ error: errMsg }, { status: 500 });
- }
-}
diff --git a/app/api/redesign/route.ts b/app/api/re-design/route.ts
similarity index 66%
rename from app/api/redesign/route.ts
rename to app/api/re-design/route.ts
index 6b898d6fd364c5f3ef267706b62c37ee559e19cd..aeff09fbc04f8b398a312b8db6f66f4a74113d2b 100644
--- a/app/api/redesign/route.ts
+++ b/app/api/re-design/route.ts
@@ -1,8 +1,10 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from "next/server";
-const FETCH_TIMEOUT = 30_000;
-export const maxDuration = 60;
+// Timeout configuration (in milliseconds)
+const FETCH_TIMEOUT = 30000; // 30 seconds for external fetch
+
+// Extend the maximum execution time for this route
+export const maxDuration = 60; // 1 minute
export async function PUT(request: NextRequest) {
const body = await request.json();
@@ -13,6 +15,7 @@ export async function PUT(request: NextRequest) {
}
try {
+ // Create an AbortController for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
@@ -24,9 +27,9 @@ export async function PUT(request: NextRequest) {
signal: controller.signal,
}
);
-
+
clearTimeout(timeoutId);
-
+
if (!response.ok) {
return NextResponse.json(
{ error: "Failed to fetch redesign" },
@@ -43,25 +46,20 @@ export async function PUT(request: NextRequest) {
);
} catch (fetchError: any) {
clearTimeout(timeoutId);
-
- if (fetchError.name === "AbortError") {
+
+ if (fetchError.name === 'AbortError') {
return NextResponse.json(
- {
- error:
- "Request timeout: The external service took too long to respond. Please try again.",
- },
+ { error: "Request timeout: The external service took too long to respond. Please try again." },
{ status: 504 }
);
}
throw fetchError;
}
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
- if (error.name === "AbortError" || error.message?.includes("timeout")) {
+ 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.",
- },
+ { error: "Request timeout: The external service took too long to respond. Please try again." },
{ status: 504 }
);
}
diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6cf420cea009bbe322d10d7fc9a9d7badee467f6
--- /dev/null
+++ b/app/auth/callback/page.tsx
@@ -0,0 +1,97 @@
+"use client";
+import Link from "next/link";
+import { useUser } from "@/hooks/useUser";
+import { use, useState } from "react";
+import { useMount, useTimeoutFn } from "react-use";
+
+import { Button } from "@/components/ui/button";
+import { AnimatedBlobs } from "@/components/animated-blobs";
+import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
+export default function AuthCallback({
+ searchParams,
+}: {
+ searchParams: Promise<{ code: string }>;
+}) {
+ const [showButton, setShowButton] = useState(false);
+ const [isPopupAuth, setIsPopupAuth] = useState(false);
+ const { code } = use(searchParams);
+ const { loginFromCode } = useUser();
+ const { postMessage } = useBroadcastChannel("auth", () => {});
+
+ useMount(async () => {
+ if (code) {
+ const isPopup = window.opener || window.parent !== window;
+ setIsPopupAuth(isPopup);
+
+ if (isPopup) {
+ postMessage({
+ type: "user-oauth",
+ code: code,
+ });
+
+ setTimeout(() => {
+ if (window.opener) {
+ window.close();
+ }
+ }, 1000);
+ } else {
+ await loginFromCode(code);
+ }
+ }
+ });
+
+ useTimeoutFn(() => setShowButton(true), 7000);
+
+ return (
+
+
+
+
+
+
+
+ π
+
+
+ π
+
+
+ π
+
+
+
+ {isPopupAuth
+ ? "Authentication Complete!"
+ : "Login In Progress..."}
+
+
+ {isPopupAuth
+ ? "You can now close this tab and return to the previous page."
+ : "Wait a moment while we log you in with your code."}
+
+
+
+
+
+ If you are not redirected automatically in the next 5 seconds,
+ please click the button below
+
+ {showButton ? (
+
+
+ Go to Home
+
+
+ ) : (
+
+ Please wait, we are logging you in...
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/app/auth/page.tsx b/app/auth/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a45a6bc6f58907b4ee5efbf0f70a51ee153625c7
--- /dev/null
+++ b/app/auth/page.tsx
@@ -0,0 +1,28 @@
+import { redirect } from "next/navigation";
+import { Metadata } from "next";
+
+import { getAuth } from "@/app/actions/auth";
+
+export const revalidate = 1;
+
+export const metadata: Metadata = {
+ robots: "noindex, nofollow",
+};
+
+export default async function Auth() {
+ const loginRedirectUrl = await getAuth();
+ if (loginRedirectUrl) {
+ redirect(loginRedirectUrl);
+ }
+
+ return (
+
+
+
Error
+
+ An error occurred while trying to log in. Please try again later.
+
+
+
+ );
+}
diff --git a/app/globals.css b/app/globals.css
deleted file mode 100644
index 97005be73ce05b7bb1a207464af1945139fde62b..0000000000000000000000000000000000000000
--- a/app/globals.css
+++ /dev/null
@@ -1,168 +0,0 @@
-@import "tailwindcss";
-@import "tw-animate-css";
-
-@custom-variant dark (&:is(.dark *));
-
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
- --color-sidebar-ring: var(--sidebar-ring);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar: var(--sidebar);
- --color-chart-5: var(--chart-5);
- --color-chart-4: var(--chart-4);
- --color-chart-3: var(--chart-3);
- --color-chart-2: var(--chart-2);
- --color-chart-1: var(--chart-1);
- --color-ring: var(--ring);
- --color-input: var(--input);
- --color-border: var(--border);
- --color-destructive: var(--destructive);
- --color-accent-foreground: var(--accent-foreground);
- --color-accent: var(--accent);
- --color-muted-foreground: var(--muted-foreground);
- --color-muted: var(--muted);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-secondary: var(--secondary);
- --color-primary-foreground: var(--primary-foreground);
- --color-primary: var(--primary);
- --color-popover-foreground: var(--popover-foreground);
- --color-popover: var(--popover);
- --color-card-foreground: var(--card-foreground);
- --color-card: var(--card);
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
-}
-
-:root {
- --radius: 0.65rem;
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --radius: 0.625rem;
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
-}
-
-.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
-}
-
-@layer base {
- * {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
-}
-
-.monaco-editor .margin {
- @apply bg-background!;
-}
-.monaco-editor .monaco-editor-background {
- @apply bg-background!;
-}
-.monaco-editor .decorationsOverviewRuler {
- @apply opacity-0!;
-}
-.monaco-editor .view-line {
- /* @apply bg-primary/50!; */
-}
-.monaco-editor .scroll-decoration {
- @apply opacity-0!;
-}
-.monaco-editor .cursors-layer .cursor {
- @apply bg-primary!;
-}
-
-.content-placeholder::before {
- content: attr(data-placeholder);
- position: absolute;
- pointer-events: none;
- opacity: 0.5;
- @apply top-5 left-6;
-}
-
-.sp-layout
- .sp-file-explorer
- .sp-file-explorer-list
- .sp-explorer[data-active="true"] {
- @apply text-indigo-500!;
-}
-
-.sp-layout
- .sp-stack
- .sp-tabs
- .sp-tab-container[aria-selected="true"]
- .sp-tab-button {
- @apply text-indigo-500!;
-}
-.sp-layout .sp-stack .sp-tabs .sp-tab-container:has(button:focus) {
- @apply outline-none! border-none!;
-}
diff --git a/app/layout.tsx b/app/layout.tsx
index 469b6844c7b63d85ad5e9c7ba420add2f7f1125c..a18c0c579f4f66e487a6e86c2bd4eb05e9fab87a 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,24 +1,28 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Metadata, Viewport } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import { NextStepProvider } from "nextstepjs";
+import { Inter, PT_Sans } from "next/font/google";
import Script from "next/script";
+import { headers } from "next/headers";
+import { redirect } from "next/navigation";
-import "@/app/globals.css";
-import { ThemeProvider } from "@/components/providers/theme";
-import { AuthProvider } from "@/components/providers/session";
+import "@/assets/globals.css";
import { Toaster } from "@/components/ui/sonner";
-import { ReactQueryProvider } from "@/components/providers/react-query";
+import IframeDetector from "@/components/iframe-detector";
+import AppContext from "@/components/contexts/app-context";
+import TanstackContext from "@/components/contexts/tanstack-query-context";
+import { LoginProvider } from "@/components/contexts/login-context";
+import { ProProvider } from "@/components/contexts/pro-context";
import { generateSEO, generateStructuredData } from "@/lib/seo";
-import { NotAuthorizedDomain } from "@/components/not-authorized";
-const geistSans = Geist({
- variable: "--font-geist-sans",
+const inter = Inter({
+ variable: "--font-inter-sans",
subsets: ["latin"],
});
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const ptSans = PT_Sans({
+ variable: "--font-ptSans-mono",
subsets: ["latin"],
+ weight: ["400", "700"],
});
export const metadata: Metadata = {
@@ -46,28 +50,68 @@ export const metadata: Metadata = {
export const viewport: Viewport = {
initialScale: 1,
maximumScale: 1,
- themeColor: "#4f46e5",
+ themeColor: "#000000",
};
+// async function getMe() {
+// const cookieStore = await cookies();
+// const cookieName = MY_TOKEN_KEY();
+// const token = cookieStore.get(cookieName)?.value;
+
+// if (!token) return { user: null, projects: [], errCode: null };
+// try {
+// const res = await apiServer.get("/me", {
+// headers: {
+// Authorization: `Bearer ${token}`,
+// },
+// });
+// return { user: res.data.user, projects: res.data.projects, errCode: null };
+// } catch (err: any) {
+// return { user: null, projects: [], errCode: err.status };
+// }
+// }
+
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
+ // Domain redirect check
+ const headersList = await headers();
+ const forwardedHost = headersList.get("x-forwarded-host");
+ const host = headersList.get("host");
+ const hostname = (forwardedHost || host || "").split(":")[0];
+
+ const isLocalDev =
+ hostname === "localhost" ||
+ hostname === "127.0.0.1" ||
+ hostname.startsWith("192.168.");
+ const isHuggingFace =
+ hostname === "huggingface.co" || hostname.endsWith(".huggingface.co");
+
+ if (!isHuggingFace && !isLocalDev) {
+ const pathname = headersList.get("x-invoke-path") || "/deepsite";
+ redirect(`https://huggingface.co${pathname}`);
+ }
+
+ // const data = await getMe();
+
+ // Generate structured data
const structuredData = generateStructuredData("WebApplication", {
name: "DeepSite",
description: "Build websites with AI, no code required",
- url: "https://deepsite.hf.co",
+ url: "https://huggingface.co/deepsite",
});
+
const organizationData = generateStructuredData("Organization", {
name: "DeepSite",
- url: "https://deepsite.hf.co",
+ url: "https://huggingface.co/deepsite",
});
return (
-
+
-
-
-
-
-
- {children}
-
-
-
-
-
+
+
+
+
+
+ {children}
+
+
+
);
diff --git a/app/new/page.tsx b/app/new/page.tsx
index 920e799168597ccab9e644171bff837961cc45e6..6f9a3e815dd4df433d10c4b0f850ade9d6fb38fb 100644
--- a/app/new/page.tsx
+++ b/app/new/page.tsx
@@ -1,18 +1,14 @@
import { AppEditor } from "@/components/editor";
-import { auth } from "@/lib/auth";
-import { redirect } from "next/navigation";
+import { Metadata } from "next";
+import { generateSEO } from "@/lib/seo";
-export default async function NewProjectPage({
- searchParams,
-}: {
- searchParams: Promise<{ prompt: string }>;
-}) {
- const session = await auth();
+export const metadata: Metadata = generateSEO({
+ title: "Create New Project - DeepSite",
+ description:
+ "Start building your next website with AI. Create a new project on DeepSite and experience the power of AI-driven web development.",
+ path: "/new",
+});
- if (!session) {
- redirect("/api/auth/signin?callbackUrl=/new");
- }
-
- const { prompt } = await searchParams;
- return ;
+export default function NewProjectPage() {
+ return ;
}
diff --git a/app/not-found.tsx b/app/not-found.tsx
deleted file mode 100644
index ae102854e6245bcc2f0d55780d9419b2679d6f38..0000000000000000000000000000000000000000
--- a/app/not-found.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { NotFoundButtons } from "@/components/not-found/buttons";
-import { Navigation } from "@/components/public/navigation";
-
-export default function NotFound() {
- return (
-
-
-
-
Oh no! Page not found.
-
- The page you are looking for does not exist.
-
-
-
-
- );
-}
diff --git a/app/sitemap.ts b/app/sitemap.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6c00dd749547a9187b999fc25953f78f31d1216d
--- /dev/null
+++ b/app/sitemap.ts
@@ -0,0 +1,28 @@
+import { MetadataRoute } from 'next';
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ const baseUrl = 'https://huggingface.co/deepsite';
+
+ return [
+ {
+ url: baseUrl,
+ lastModified: new Date(),
+ changeFrequency: 'daily',
+ priority: 1,
+ },
+ {
+ url: `${baseUrl}/new`,
+ lastModified: new Date(),
+ changeFrequency: 'weekly',
+ priority: 0.8,
+ },
+ {
+ url: `${baseUrl}/auth`,
+ lastModified: new Date(),
+ changeFrequency: 'monthly',
+ priority: 0.5,
+ },
+ // Note: Dynamic project routes will be handled by Next.js automatically
+ // but you can add specific high-priority project pages here if needed
+ ];
+}
diff --git a/assets/PAGINA DE VENDAS/IMG/CAVALO.jpg b/assets/PAGINA DE VENDAS/IMG/CAVALO.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f3040902b0230949873d24eac48d040998c7736d
--- /dev/null
+++ b/assets/PAGINA DE VENDAS/IMG/CAVALO.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84f0ac9c400e6f993ab5689b2d9eecbc5eb84b88f32519957bda412066af3b51
+size 709007
diff --git a/assets/PAGINA DE VENDAS/fome.png b/assets/PAGINA DE VENDAS/fome.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a2599b7dbb94fb6c1a43f16a9261e6f5d6c81bc
--- /dev/null
+++ b/assets/PAGINA DE VENDAS/fome.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c152041196787d3ccab0c12fccfc77b6b80c16f8ec2205ff1181bf3420d55c87
+size 3265338
diff --git a/assets/PAGINA DE VENDAS/index.htm b/assets/PAGINA DE VENDAS/index.htm
new file mode 100644
index 0000000000000000000000000000000000000000..155b2a810dfedb8fe53d3460839abe46f99f40c1
--- /dev/null
+++ b/assets/PAGINA DE VENDAS/index.htm
@@ -0,0 +1,531 @@
+
+
+
+
+
+ Mindful Slim - Emagrecimento com Leveza
+
+
+
+
+
+
+
+
+
+
+
+
+ EmagreΓ§a com leveza : mude sua mente e deixe seu corpo acompanhar.
+
+
+ Recupere sua autoestima, acabe com a culpa e viva leve de novo β sem sofrimento, sem dietas restritivas, com liberdade e equilΓbrio.
+
+
+ Quero emagrecer com leveza agora
+
+
+
+
+
+
+
+
+
+
+
+
+ O verdadeiro motivo pelo qual vocΓͺ nΓ£o consegue emagrecer (ainda)
+
+
+ VocΓͺ jΓ‘ percebeu que nΓ£o falta conhecimento sobre o que comer ou o que evitar. O que falta Γ© controle emocional, clareza mental e novos padrΓ΅es automΓ‘ticos.
+
+
+
+
+
+
Ansiedade que termina em compulsΓ£o
+
+
+
+
SensaΓ§Γ£o de "eu sei o que fazer, mas nΓ£o consigo"
+
+
+
+
CansaΓ§o de dietas restritivas que viram frustraΓ§Γ£o
+
+
+
+
CrenΓ§as internas que sabotam qualquer tentativa
+
+
+
+
+
+ Isso nΓ£o Γ© falta de forΓ§a de vontade. Γ o seu cΓ©rebro repetindo padrΓ΅es antigos.
+
+
+
+
+
+
+
+
+
+ MΓ©todo R.E.A.L.
+
+
+ O sistema que transforma de dentro para fora
+
+
+
+
+
R
+
ReprogramaΓ§Γ£o Interna
+
+ Aprenda a identificar e neutralizar gatilhos emocionais que levam Γ compulsΓ£o, culpa e desistΓͺncia.
+
+
+
+
E
+
EquilΓbrio Emocional
+
+ ReduΓ§Γ£o da ansiedade, da compulsΓ£o e da autossabotagem.
+
+
+
+
A
+
AΓ§Γ£o Consistente
+
+ Pequenas aΓ§Γ΅es diΓ‘rias que constroem disciplina sem sacrifΓcios.
+
+
+
+
L
+
Liberdade com Leveza
+
+ RelaΓ§Γ£o saudΓ‘vel com a comida e com o prΓ³prio corpo.
+
+
+
+
+
+
+
+ O comportamento muda primeiro. O corpo acompanha depois.
+
+
+
+
+
+
+
+
+
+
+ TransformaΓ§Γ£o Comportamental
+
+
+
+
+
+ Antes
+
+
+
+
+ Comer por ansiedade
+
+
+
+ Culpa constante
+
+
+
+ ComeΓ§os que duram poucos dias
+
+
+
+ SensaΓ§Γ£o de descontrole
+
+
+
+ Peso sanfona
+
+
+
+
+
+ Depois
+
+
+
+
+ Controle emocional
+
+
+
+ AlimentaΓ§Γ£o consciente
+
+
+
+ Rotina leve e sustentΓ‘vel
+
+
+
+ Ansiedade sob controle
+
+
+
+ Emagrecimento natural
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Como Funciona
+
+
+
+
+ 100% online
+
+
+
+ Acesso vitalΓcio
+
+
+
+ Celular, tablet ou computador
+
+
+
+ MΓ³dulos simples
+
+
+
+ VocΓͺ recebe:
+
+
+
+
+ Guia de micro-hΓ‘bitos diΓ‘rios
+
+
+
+ Materiais de apoio
+
+
+
+
+ NΓ£o Γ© mais uma dieta. Γ uma nova forma de pensar, sentir e agir.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BenefΓcios
+
+
+
+
+
+
Controle emocional
+
Aprenda a lidar com suas emoΓ§Γ΅es sem usar a comida como vΓ‘lvula de escape.
+
+
+
+
+
+
Menos ansiedade
+
TΓ©cnicas para reduzir a ansiedade e a compulsΓ£o alimentar.
+
+
+
+
+
+
RelaΓ§Γ£o leve com a comida
+
Sem proibiΓ§Γ΅es, sem culpa, apenas equilΓbrio e consciΓͺncia.
+
+
+
+
+
+
Rotina sustentΓ‘vel
+
HΓ‘bitos que se encaixam na sua vida, sem extremismos.
+
+
+
+
+
+
Autoestima elevada
+
Reconecte-se com seu corpo e valorize seu processo.
+
+
+
+
+
+
Emagrecimento consistente
+
Resultados que se mantΓͺm porque vΓͺm de dentro para fora.
+
+
+
+
+
+
+
+
+
+
+ BΓ΄nus Exclusivos
+
+
+
+
+
+
Planner de Micro-HΓ‘bitos
+
+
+ Um planner delicado e funcional para acompanhar seu progresso diΓ‘rio e celebrar cada pequena vitΓ³ria.
+
+
+
+
+
+
Guia de ManutenΓ§Γ£o PΓ³s-Emagrecimento
+
+
+ Como manter seus resultados e continuar evoluindo mesmo apΓ³s alcanΓ§ar seu peso ideal.
+
+
+
+
+
+
+
+
+
+
+ Escolha Seu Plano
+
+
+
+
VersΓ£o Essencial
+
R$ 9,90
+
+
+
+ Curso completo
+
+
+
+ Acesso vitalΓcio
+
+
+
+ Guia de micro-hΓ‘bitos
+
+
+
+ Material de apoio
+
+
+
+ Quero comeΓ§ar agora
+
+
+
+
+ Mais escolhida
+
+
VersΓ£o Premium
+
R$ 27,00
+
+
+
+ Tudo da versΓ£o essencial +
+
+
+
+ Planner de Micro-HΓ‘bitos
+
+
+
+ Guia de ManutenΓ§Γ£o
+
+
+
+ Quero a versΓ£o completa
+
+
+
+
+
+
+
+
+
+
+
+
+ Garantia Incondicional de 7 Dias
+
+
+ VocΓͺ tem 7 dias para experimentar o mΓ©todo. Se nΓ£o gostar, devolvemos 100% do seu investimento sem burocracia.
+
+
+ Sem risco. Sem complicaΓ§Γ΅es. Apenas resultados.
+
+
+
+
+
+
+
+
+
+ Perguntas Frequentes
+
+
+
+
+
+
Funciona para quem jΓ‘ tentou vΓ‘rias dietas e nΓ£o teve resultado?
+
+
+ Sim. O foco aqui nΓ£o Γ© dieta, e sim comportamento. Trabalhamos as causas emocionais e mentais que levam Γ compulsΓ£o e Γ dificuldade de manter hΓ‘bitos saudΓ‘veis.
+
+
+
+
+
+
Preciso ter muito tempo livre para aplicar o mΓ©todo?
+
+
+ As aulas sΓ£o curtas e vocΓͺ faz no seu ritmo. O mΓ©todo foi pensado para mulheres ocupadas, com micro-aΓ§Γ΅es que se encaixam na sua rotina.
+
+
+
+
+
+
Γ seguro? O mΓ©todo tem base confiΓ‘vel?
+
+
+ Sim. Todo o conteΓΊdo Γ© baseado em tΓ©cnicas reconhecidas de mudanΓ§a de hΓ‘bitos e regulaΓ§Γ£o emocional, com abordagem cientΓfica e humanizada.
+
+
+
+
+
+
Em quanto tempo comeΓ§o a ver resultados?
+
+
+ Depende da sua dedicaΓ§Γ£o, mas muitas alunas relatam mudanΓ§as de comportamento na primeira semana. O emagrecimento fΓsico vem como consequΓͺncia natural.
+
+
+
+
+
+
Preciso cortar alimentos ou seguir uma dieta restritiva?
+
+
+ NΓ£o. Aqui vocΓͺ aprende a se relacionar melhor com a comida, sem proibiΓ§Γ΅es extremas. O foco Γ© equilΓbrio, consciΓͺncia e liberdade β nΓ£o sofrimento.
+
+
+
+
+
+
Como recebo o acesso ao conteΓΊdo?
+
+
+ ApΓ³s a confirmaΓ§Γ£o do pagamento, o acesso Γ© liberado imediatamente no seu e-mail. VocΓͺ pode acessar pelo celular, tablet ou computador, com acesso vitalΓcio.
+
+
+
+
+
+
+
+
+
+
+ Pronta para transformar sua relaΓ§Γ£o com a comida e com seu corpo?
+
+
+ Comece hoje sua jornada de emagrecimento com leveza e autocompaixΓ£o.
+
+
+
+
+ Quero emagrecer com leveza agora
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/PAGINA DE VENDAS/ioga.png b/assets/PAGINA DE VENDAS/ioga.png
new file mode 100644
index 0000000000000000000000000000000000000000..016f5e15cbd331afa96a7803ab35fce404d73208
--- /dev/null
+++ b/assets/PAGINA DE VENDAS/ioga.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8ca44d56c0373c88dc7947ba20af3fefd9442db25276c1f4900cea2b0105bd31
+size 325559
diff --git a/assets/PAGINA DE VENDAS/praia.png b/assets/PAGINA DE VENDAS/praia.png
new file mode 100644
index 0000000000000000000000000000000000000000..482b9bccbe7b94e5d1313b77aadbdca06208add3
--- /dev/null
+++ b/assets/PAGINA DE VENDAS/praia.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:455b4aa1ec6e8230db795a60596dc4d941e363bf618e164cdf9607b55ca2b5ca
+size 191295
diff --git a/assets/globals.css b/assets/globals.css
new file mode 100644
index 0000000000000000000000000000000000000000..299014cc6db1db5b73dc03a97958b93fc2a9e811
--- /dev/null
+++ b/assets/globals.css
@@ -0,0 +1,380 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-inter-sans);
+ --font-mono: var(--font-ptSans-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+body {
+ @apply scroll-smooth
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ html {
+ @apply scroll-smooth;
+ }
+}
+
+.background__noisy {
+ @apply bg-blend-normal pointer-events-none opacity-90;
+ background-size: 25ww auto;
+ background-image: url("/deepsite/background_noisy.webp");
+ @apply fixed w-screen h-screen -z-1 top-0 left-0;
+}
+
+.monaco-editor .margin {
+ @apply !bg-neutral-900;
+}
+.monaco-editor .monaco-editor-background {
+ @apply !bg-neutral-900;
+}
+.monaco-editor .line-numbers {
+ @apply !text-neutral-500;
+}
+
+.matched-line {
+ @apply bg-sky-500/30;
+}
+
+/* Fast liquid deformation animations */
+@keyframes liquidBlob1 {
+ 0%, 100% {
+ border-radius: 40% 60% 50% 50%;
+ transform: scaleX(1) scaleY(1) rotate(0deg);
+ }
+ 12.5% {
+ border-radius: 20% 80% 70% 30%;
+ transform: scaleX(1.6) scaleY(0.4) rotate(25deg);
+ }
+ 25% {
+ border-radius: 80% 20% 30% 70%;
+ transform: scaleX(0.5) scaleY(2.1) rotate(-15deg);
+ }
+ 37.5% {
+ border-radius: 30% 70% 80% 20%;
+ transform: scaleX(1.8) scaleY(0.6) rotate(40deg);
+ }
+ 50% {
+ border-radius: 70% 30% 20% 80%;
+ transform: scaleX(0.4) scaleY(1.9) rotate(-30deg);
+ }
+ 62.5% {
+ border-radius: 25% 75% 60% 40%;
+ transform: scaleX(1.5) scaleY(0.7) rotate(55deg);
+ }
+ 75% {
+ border-radius: 75% 25% 40% 60%;
+ transform: scaleX(0.6) scaleY(1.7) rotate(-10deg);
+ }
+ 87.5% {
+ border-radius: 50% 50% 75% 25%;
+ transform: scaleX(1.3) scaleY(0.8) rotate(35deg);
+ }
+}
+
+@keyframes liquidBlob2 {
+ 0%, 100% {
+ border-radius: 60% 40% 50% 50%;
+ transform: scaleX(1) scaleY(1) rotate(12deg);
+ }
+ 16% {
+ border-radius: 15% 85% 60% 40%;
+ transform: scaleX(0.3) scaleY(2.3) rotate(50deg);
+ }
+ 32% {
+ border-radius: 85% 15% 25% 75%;
+ transform: scaleX(2.0) scaleY(0.5) rotate(-20deg);
+ }
+ 48% {
+ border-radius: 30% 70% 85% 15%;
+ transform: scaleX(0.4) scaleY(1.8) rotate(70deg);
+ }
+ 64% {
+ border-radius: 70% 30% 15% 85%;
+ transform: scaleX(1.9) scaleY(0.6) rotate(-35deg);
+ }
+ 80% {
+ border-radius: 40% 60% 70% 30%;
+ transform: scaleX(0.7) scaleY(1.6) rotate(45deg);
+ }
+}
+
+@keyframes liquidBlob3 {
+ 0%, 100% {
+ border-radius: 50% 50% 40% 60%;
+ transform: scaleX(1) scaleY(1) rotate(0deg);
+ }
+ 20% {
+ border-radius: 10% 90% 75% 25%;
+ transform: scaleX(2.2) scaleY(0.3) rotate(-45deg);
+ }
+ 40% {
+ border-radius: 90% 10% 20% 80%;
+ transform: scaleX(0.4) scaleY(2.5) rotate(60deg);
+ }
+ 60% {
+ border-radius: 25% 75% 90% 10%;
+ transform: scaleX(1.7) scaleY(0.5) rotate(-25deg);
+ }
+ 80% {
+ border-radius: 75% 25% 10% 90%;
+ transform: scaleX(0.6) scaleY(2.0) rotate(80deg);
+ }
+}
+
+@keyframes liquidBlob4 {
+ 0%, 100% {
+ border-radius: 45% 55% 50% 50%;
+ transform: scaleX(1) scaleY(1) rotate(-15deg);
+ }
+ 14% {
+ border-radius: 90% 10% 65% 35%;
+ transform: scaleX(0.2) scaleY(2.8) rotate(35deg);
+ }
+ 28% {
+ border-radius: 10% 90% 20% 80%;
+ transform: scaleX(2.4) scaleY(0.4) rotate(-50deg);
+ }
+ 42% {
+ border-radius: 35% 65% 90% 10%;
+ transform: scaleX(0.3) scaleY(2.1) rotate(70deg);
+ }
+ 56% {
+ border-radius: 80% 20% 10% 90%;
+ transform: scaleX(2.0) scaleY(0.5) rotate(-40deg);
+ }
+ 70% {
+ border-radius: 20% 80% 55% 45%;
+ transform: scaleX(0.5) scaleY(1.9) rotate(55deg);
+ }
+ 84% {
+ border-radius: 65% 35% 80% 20%;
+ transform: scaleX(1.6) scaleY(0.6) rotate(-25deg);
+ }
+}
+
+/* Fast flowing movement animations */
+@keyframes liquidFlow1 {
+ 0%, 100% { transform: translate(0, 0); }
+ 16% { transform: translate(60px, -40px); }
+ 32% { transform: translate(-45px, -70px); }
+ 48% { transform: translate(80px, 25px); }
+ 64% { transform: translate(-30px, 60px); }
+ 80% { transform: translate(50px, -20px); }
+}
+
+@keyframes liquidFlow2 {
+ 0%, 100% { transform: translate(0, 0); }
+ 20% { transform: translate(-70px, 50px); }
+ 40% { transform: translate(90px, -30px); }
+ 60% { transform: translate(-40px, -55px); }
+ 80% { transform: translate(65px, 35px); }
+}
+
+@keyframes liquidFlow3 {
+ 0%, 100% { transform: translate(0, 0); }
+ 12% { transform: translate(-50px, -60px); }
+ 24% { transform: translate(40px, -20px); }
+ 36% { transform: translate(-30px, 70px); }
+ 48% { transform: translate(70px, 20px); }
+ 60% { transform: translate(-60px, -35px); }
+ 72% { transform: translate(35px, 55px); }
+ 84% { transform: translate(-25px, -45px); }
+}
+
+@keyframes liquidFlow4 {
+ 0%, 100% { transform: translate(0, 0); }
+ 14% { transform: translate(50px, 60px); }
+ 28% { transform: translate(-80px, -40px); }
+ 42% { transform: translate(30px, -90px); }
+ 56% { transform: translate(-55px, 45px); }
+ 70% { transform: translate(75px, -25px); }
+ 84% { transform: translate(-35px, 65px); }
+}
+
+/* Light sweep animation for buttons */
+@keyframes lightSweep {
+ 0% {
+ transform: translateX(-150%);
+ opacity: 0;
+ }
+ 8% {
+ opacity: 0.3;
+ }
+ 25% {
+ opacity: 0.8;
+ }
+ 42% {
+ opacity: 0.3;
+ }
+ 50% {
+ transform: translateX(150%);
+ opacity: 0;
+ }
+ 58% {
+ opacity: 0.3;
+ }
+ 75% {
+ opacity: 0.8;
+ }
+ 92% {
+ opacity: 0.3;
+ }
+ 100% {
+ transform: translateX(-150%);
+ opacity: 0;
+ }
+}
+
+.light-sweep {
+ position: relative;
+ overflow: hidden;
+}
+
+.light-sweep::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 300%;
+ background: linear-gradient(
+ 90deg,
+ transparent 0%,
+ transparent 20%,
+ rgba(56, 189, 248, 0.1) 35%,
+ rgba(56, 189, 248, 0.2) 45%,
+ rgba(255, 255, 255, 0.2) 50%,
+ rgba(168, 85, 247, 0.2) 55%,
+ rgba(168, 85, 247, 0.1) 65%,
+ transparent 80%,
+ transparent 100%
+ );
+ animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+ pointer-events: none;
+ z-index: 1;
+ filter: blur(1px);
+}
+
+.transparent-scroll {
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE and Edge */
+}
+
+.transparent-scroll::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
diff --git a/assets/hf-logo.svg b/assets/hf-logo.svg
deleted file mode 100644
index b91ba37b236c18b7483716a33a99c05fc43bd00a..0000000000000000000000000000000000000000
--- a/assets/hf-logo.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/assets/logo.svg b/assets/logo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e69f057d4d4c256f02881888e781aa0943010c3e
--- /dev/null
+++ b/assets/logo.svg
@@ -0,0 +1,316 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/pro.svg b/assets/pro.svg
deleted file mode 100644
index 4d992b483109757a550a27a05caf48cd09daa892..0000000000000000000000000000000000000000
--- a/assets/pro.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/chart/Chart.yaml b/chart/Chart.yaml
deleted file mode 100644
index 83483de68757c3ed27fd68ca9bbb51aa48a64d25..0000000000000000000000000000000000000000
--- a/chart/Chart.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-apiVersion: v2
-name: deepsite
-version: 0.0.0-latest
-type: application
-icon: https://huggingface.co/front/assets/huggingface_logo-noborder.svg
diff --git a/chart/env/prod.yaml b/chart/env/prod.yaml
deleted file mode 100644
index 2ac82284c67dda993d0292685a8cc56cda2f25e8..0000000000000000000000000000000000000000
--- a/chart/env/prod.yaml
+++ /dev/null
@@ -1,59 +0,0 @@
-nodeSelector:
- role-deepsite: "true"
-
-tolerations:
- - key: "huggingface.co/deepsite"
- operator: "Equal"
- value: "true"
- effect: "NoSchedule"
-
-serviceAccount:
- enabled: true
- create: true
- name: deepsite-prod
-
-ingress:
- path: "/"
- annotations:
- alb.ingress.kubernetes.io/healthcheck-path: "/api/healthcheck"
- alb.ingress.kubernetes.io/listen-ports: "[{\"HTTP\": 80}, {\"HTTPS\": 443}]"
- alb.ingress.kubernetes.io/load-balancer-name: "hub-utils-prod-cloudfront"
- alb.ingress.kubernetes.io/group.name: "hub-utils-prod-cloudfront"
- alb.ingress.kubernetes.io/scheme: "internal"
- alb.ingress.kubernetes.io/ssl-redirect: "443"
- alb.ingress.kubernetes.io/tags: "Env=prod,Project=hub,Terraform=true"
- alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30
- alb.ingress.kubernetes.io/target-type: "ip"
- alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-east-1:707930574880:certificate/5b25b145-75db-4837-b9f3-7f238ba8a9c7,arn:aws:acm:us-east-1:707930574880:certificate/bfdf509c-f44b-400f-b9e1-6f7a861abe91"
- kubernetes.io/ingress.class: "alb"
-
-networkPolicy:
- enabled: true
- allowedBlocks:
- - 10.0.0.0/16
-
-
-ingressInternal:
- enabled: false
-
-envVars:
- NEXTAUTH_URL: https://deepsite.hf.co/api/auth
-
-infisical:
- enabled: true
- env: "prod-us-east-1"
-
-autoscaling:
- enabled: true
- minReplicas: 1
- maxReplicas: 10
- targetMemoryUtilizationPercentage: "50"
- targetCPUUtilizationPercentage: "50"
-
-resources:
- requests:
- cpu: 2
- memory: 4Gi
- limits:
- cpu: 4
- memory: 8Gi
diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl
deleted file mode 100644
index eee5a181d225c2aff53344c446288240d37d3d0b..0000000000000000000000000000000000000000
--- a/chart/templates/_helpers.tpl
+++ /dev/null
@@ -1,22 +0,0 @@
-{{- define "name" -}}
-{{- default $.Release.Name | trunc 63 | trimSuffix "-" -}}
-{{- end -}}
-
-{{- define "app.name" -}}
-chat-ui
-{{- end -}}
-
-{{- define "labels.standard" -}}
-release: {{ $.Release.Name | quote }}
-heritage: {{ $.Release.Service | quote }}
-chart: "{{ include "name" . }}"
-app: "{{ include "app.name" . }}"
-{{- end -}}
-
-{{- define "labels.resolver" -}}
-release: {{ $.Release.Name | quote }}
-heritage: {{ $.Release.Service | quote }}
-chart: "{{ include "name" . }}"
-app: "{{ include "app.name" . }}-resolver"
-{{- end -}}
-
diff --git a/chart/templates/config.yaml b/chart/templates/config.yaml
deleted file mode 100644
index c4c803e9e5f8b473ae216d5a2933cb67d46bc011..0000000000000000000000000000000000000000
--- a/chart/templates/config.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
- labels: {{ include "labels.standard" . | nindent 4 }}
- name: {{ include "name" . }}
- namespace: {{ .Release.Namespace }}
-data:
- {{- range $key, $value := $.Values.envVars }}
- {{ $key }}: {{ $value | quote }}
- {{- end }}
diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml
deleted file mode 100644
index b487ee6a3e5db9e315dcb024e6fdc6b753eea5a3..0000000000000000000000000000000000000000
--- a/chart/templates/deployment.yaml
+++ /dev/null
@@ -1,81 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- labels: {{ include "labels.standard" . | nindent 4 }}
- name: {{ include "name" . }}
- namespace: {{ .Release.Namespace }}
- {{- if .Values.infisical.enabled }}
- annotations:
- secrets.infisical.com/auto-reload: "true"
- {{- end }}
-spec:
- progressDeadlineSeconds: 600
- {{- if not $.Values.autoscaling.enabled }}
- replicas: {{ .Values.replicas }}
- {{- end }}
- revisionHistoryLimit: 10
- selector:
- matchLabels: {{ include "labels.standard" . | nindent 6 }}
- strategy:
- rollingUpdate:
- maxSurge: 25%
- maxUnavailable: 25%
- type: RollingUpdate
- template:
- metadata:
- labels: {{ include "labels.standard" . | nindent 8 }}
- annotations:
- checksum/config: {{ include (print $.Template.BasePath "/config.yaml") . | sha256sum }}
- {{- if $.Values.envVars.NODE_LOG_STRUCTURED_DATA }}
- co.elastic.logs/json.expand_keys: "true"
- {{- end }}
- spec:
- {{- if .Values.serviceAccount.enabled }}
- serviceAccountName: "{{ .Values.serviceAccount.name | default (include "name" .) }}"
- {{- end }}
- containers:
- - name: chat-ui
- image: "{{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }}"
- imagePullPolicy: {{ .Values.image.pullPolicy }}
- readinessProbe:
- failureThreshold: 30
- periodSeconds: 10
- httpGet:
- path: {{ $.Values.envVars.APP_BASE | default "" }}/api/healthcheck
- port: {{ $.Values.envVars.APP_PORT | default 3001 | int }}
- livenessProbe:
- failureThreshold: 30
- periodSeconds: 10
- httpGet:
- path: {{ $.Values.envVars.APP_BASE | default "" }}/api/healthcheck
- port: {{ $.Values.envVars.APP_PORT | default 3001 | int }}
- ports:
- - containerPort: {{ $.Values.envVars.APP_PORT | default 3001 | int }}
- name: http
- protocol: TCP
- {{- if eq "true" $.Values.envVars.METRICS_ENABLED }}
- - containerPort: {{ $.Values.envVars.METRICS_PORT | default 5565 | int }}
- name: metrics
- protocol: TCP
- {{- end }}
- resources: {{ toYaml .Values.resources | nindent 12 }}
- {{- with $.Values.extraEnv }}
- env:
- {{- toYaml . | nindent 14 }}
- {{- end }}
- envFrom:
- - configMapRef:
- name: {{ include "name" . }}
- {{- if $.Values.infisical.enabled }}
- - secretRef:
- name: {{ include "name" $ }}-secs
- {{- end }}
- {{- with $.Values.extraEnvFrom }}
- {{- toYaml . | nindent 14 }}
- {{- end }}
- nodeSelector: {{ toYaml .Values.nodeSelector | nindent 8 }}
- tolerations: {{ toYaml .Values.tolerations | nindent 8 }}
- volumes:
- - name: config
- configMap:
- name: {{ include "name" . }}
diff --git a/chart/templates/hpa.yaml b/chart/templates/hpa.yaml
deleted file mode 100644
index bf7bd3b256b79c54269ae39afb02c816878596dc..0000000000000000000000000000000000000000
--- a/chart/templates/hpa.yaml
+++ /dev/null
@@ -1,45 +0,0 @@
-{{- if $.Values.autoscaling.enabled }}
-apiVersion: autoscaling/v2
-kind: HorizontalPodAutoscaler
-metadata:
- labels: {{ include "labels.standard" . | nindent 4 }}
- name: {{ include "name" . }}
- namespace: {{ .Release.Namespace }}
-spec:
- scaleTargetRef:
- apiVersion: apps/v1
- kind: Deployment
- name: {{ include "name" . }}
- minReplicas: {{ $.Values.autoscaling.minReplicas }}
- maxReplicas: {{ $.Values.autoscaling.maxReplicas }}
- metrics:
- {{- if ne "" $.Values.autoscaling.targetMemoryUtilizationPercentage }}
- - type: Resource
- resource:
- name: memory
- target:
- type: Utilization
- averageUtilization: {{ $.Values.autoscaling.targetMemoryUtilizationPercentage | int }}
- {{- end }}
- {{- if ne "" $.Values.autoscaling.targetCPUUtilizationPercentage }}
- - type: Resource
- resource:
- name: cpu
- target:
- type: Utilization
- averageUtilization: {{ $.Values.autoscaling.targetCPUUtilizationPercentage | int }}
- {{- end }}
- behavior:
- scaleDown:
- stabilizationWindowSeconds: 600
- policies:
- - type: Percent
- value: 10
- periodSeconds: 60
- scaleUp:
- stabilizationWindowSeconds: 0
- policies:
- - type: Pods
- value: 1
- periodSeconds: 30
-{{- end }}
diff --git a/chart/templates/infisical.yaml b/chart/templates/infisical.yaml
deleted file mode 100644
index 6a11e084f6e0ab300ea4ec2b2694a79dadc1bdf8..0000000000000000000000000000000000000000
--- a/chart/templates/infisical.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-{{- if .Values.infisical.enabled }}
-apiVersion: secrets.infisical.com/v1alpha1
-kind: InfisicalSecret
-metadata:
- name: {{ include "name" $ }}-infisical-secret
- namespace: {{ $.Release.Namespace }}
-spec:
- authentication:
- universalAuth:
- credentialsRef:
- secretName: {{ .Values.infisical.operatorSecretName | quote }}
- secretNamespace: {{ .Values.infisical.operatorSecretNamespace | quote }}
- secretsScope:
- envSlug: {{ .Values.infisical.env | quote }}
- projectSlug: {{ .Values.infisical.project | quote }}
- secretsPath: /
- hostAPI: {{ .Values.infisical.url | quote }}
- managedSecretReference:
- creationPolicy: Owner
- secretName: {{ include "name" $ }}-secs
- secretNamespace: {{ .Release.Namespace | quote }}
- secretType: Opaque
- resyncInterval: {{ .Values.infisical.resyncInterval }}
-{{- end }}
diff --git a/chart/templates/ingress-internal.yaml b/chart/templates/ingress-internal.yaml
deleted file mode 100644
index bf87d0b6c960871327908e243e79e05475b825d5..0000000000000000000000000000000000000000
--- a/chart/templates/ingress-internal.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-{{- if $.Values.ingressInternal.enabled }}
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- annotations: {{ toYaml .Values.ingressInternal.annotations | nindent 4 }}
- labels: {{ include "labels.standard" . | nindent 4 }}
- name: {{ include "name" . }}-internal
- namespace: {{ .Release.Namespace }}
-spec:
- {{ if $.Values.ingressInternal.className }}
- ingressClassName: {{ .Values.ingressInternal.className }}
- {{ end }}
- {{- with .Values.ingressInternal.tls }}
- tls:
- - hosts:
- - {{ $.Values.domain | quote }}
- {{- with .secretName }}
- secretName: {{ . }}
- {{- end }}
- {{- end }}
- rules:
- - host: {{ .Values.domain }}
- http:
- paths:
- - backend:
- service:
- name: {{ include "name" . }}
- port:
- name: http
- path: {{ $.Values.ingressInternal.path | default "/" }}
- pathType: Prefix
-{{- end }}
diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml
deleted file mode 100644
index 563c911d72b906e18d716a4b2b4d3d26842d7fd7..0000000000000000000000000000000000000000
--- a/chart/templates/ingress.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-{{- if $.Values.ingress.enabled }}
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- annotations: {{ toYaml .Values.ingress.annotations | nindent 4 }}
- labels: {{ include "labels.standard" . | nindent 4 }}
- name: {{ include "name" . }}
- namespace: {{ .Release.Namespace }}
-spec:
- {{ if $.Values.ingress.className }}
- ingressClassName: {{ .Values.ingress.className }}
- {{ end }}
- {{- with .Values.ingress.tls }}
- tls:
- - hosts:
- - {{ $.Values.domain | quote }}
- {{- with .secretName }}
- secretName: {{ . }}
- {{- end }}
- {{- end }}
- rules:
- - host: {{ .Values.domain }}
- http:
- paths:
- - backend:
- service:
- name: {{ include "name" . }}
- port:
- name: http
- path: {{ $.Values.ingress.path | default "/" }}
- pathType: Prefix
-
- {{- end }}
diff --git a/chart/templates/network-policy.yaml b/chart/templates/network-policy.yaml
deleted file mode 100644
index 59f5df5893a97f4075237ac7cdb4979dce7298a9..0000000000000000000000000000000000000000
--- a/chart/templates/network-policy.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-{{- if $.Values.networkPolicy.enabled }}
-apiVersion: networking.k8s.io/v1
-kind: NetworkPolicy
-metadata:
- name: {{ include "name" . }}
- namespace: {{ .Release.Namespace }}
-spec:
- egress:
- - ports:
- - port: 53
- protocol: UDP
- to:
- - namespaceSelector:
- matchLabels:
- kubernetes.io/metadata.name: kube-system
- podSelector:
- matchLabels:
- k8s-app: kube-dns
- - to:
- {{- range $ip := .Values.networkPolicy.allowedBlocks }}
- - ipBlock:
- cidr: {{ $ip | quote }}
- {{- end }}
- - to:
- - ipBlock:
- cidr: 0.0.0.0/0
- except:
- - 10.0.0.0/8
- - 172.16.0.0/12
- - 192.168.0.0/16
- - 169.254.169.254/32
- podSelector:
- matchLabels: {{ include "labels.standard" . | nindent 6 }}
- policyTypes:
- - Egress
-{{- end }}
diff --git a/chart/templates/service-account.yaml b/chart/templates/service-account.yaml
deleted file mode 100644
index fc3a184c9def4cef61836f8eac152ab61fe4d047..0000000000000000000000000000000000000000
--- a/chart/templates/service-account.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-{{- if and .Values.serviceAccount.enabled .Values.serviceAccount.create }}
-apiVersion: v1
-kind: ServiceAccount
-automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
-metadata:
- name: "{{ .Values.serviceAccount.name | default (include "name" .) }}"
- namespace: {{ .Release.Namespace }}
- labels: {{ include "labels.standard" . | nindent 4 }}
- {{- with .Values.serviceAccount.annotations }}
- annotations:
- {{- toYaml . | nindent 4 }}
- {{- end }}
-{{- end }}
diff --git a/chart/templates/service-monitor.yaml b/chart/templates/service-monitor.yaml
deleted file mode 100644
index 0c8e4dab423946a318a3daee7bd4dfcab0cee151..0000000000000000000000000000000000000000
--- a/chart/templates/service-monitor.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-{{- if eq "true" $.Values.envVars.METRICS_ENABLED }}
-apiVersion: monitoring.coreos.com/v1
-kind: ServiceMonitor
-metadata:
- labels: {{ include "labels.standard" . | nindent 4 }}
- name: {{ include "name" . }}
- namespace: {{ .Release.Namespace }}
-spec:
- selector:
- matchLabels: {{ include "labels.standard" . | nindent 6 }}
- endpoints:
- - port: metrics
- path: /metrics
- interval: 10s
- scheme: http
- scrapeTimeout: 10s
-{{- end }}
diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml
deleted file mode 100644
index ef364f0926861b9e934e9830d7884cdd63ecd6ec..0000000000000000000000000000000000000000
--- a/chart/templates/service.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
- name: "{{ include "name" . }}"
- annotations: {{ toYaml .Values.service.annotations | nindent 4 }}
- namespace: {{ .Release.Namespace }}
- labels: {{ include "labels.standard" . | nindent 4 }}
-spec:
- ports:
- - name: http
- port: 80
- protocol: TCP
- targetPort: http
- {{- if eq "true" $.Values.envVars.METRICS_ENABLED }}
- - name: metrics
- port: {{ $.Values.envVars.METRICS_PORT | default 5565 | int }}
- protocol: TCP
- targetPort: metrics
- {{- end }}
- selector: {{ include "labels.standard" . | nindent 4 }}
- type: {{.Values.service.type}}
diff --git a/chart/values.yaml b/chart/values.yaml
deleted file mode 100644
index 5a2bd68b20d5efde44f87d7ffb8278717fa5aaf8..0000000000000000000000000000000000000000
--- a/chart/values.yaml
+++ /dev/null
@@ -1,81 +0,0 @@
-image:
- repository: registry.internal.huggingface.tech/deepsite
- name: deepsite
- tag: 0.0.0-latest
- pullPolicy: IfNotPresent
-
-replicas: 1
-
-domain: deepsite.hf.co
-
-networkPolicy:
- enabled: true
- allowedBlocks: []
- # allowedBlocks:
- # - 10.0.240.0/24
- # - 10.0.241.0/24
- # - 10.0.242.0/24
- # - 10.0.243.0/24
- # - 10.0.244.0/24
- # - 10.240.0.0/24
- # - 10.16.0.0/16
-
-service:
- type: NodePort
- annotations: { }
-
-serviceAccount:
- enabled: false
- create: false
- name: ""
- automountServiceAccountToken: true
- annotations: { }
-
-ingress:
- enabled: true
- path: "/"
- annotations: { }
- # className: "nginx"
- tls: { }
- # secretName: XXX
-
-ingressInternal:
- enabled: false
- path: "/"
- annotations: { }
- # className: "nginx"
- tls: { }
-
-resources:
- requests:
- cpu: 2
- memory: 4Gi
- limits:
- cpu: 2
- memory: 4Gi
-nodeSelector: {}
-tolerations: []
-
-envVars: { }
-
-infisical:
- enabled: false
- env: ""
- project: "deepsite-f-hvj"
- url: ""
- resyncInterval: 60
- operatorSecretName: "deepsite-operator-secrets"
- operatorSecretNamespace: "hub-utils"
-
-# Allow to environment injections on top or instead of infisical
-extraEnvFrom: []
-extraEnv: []
-
-autoscaling:
- enabled: false
- minReplicas: 1
- maxReplicas: 2
- targetMemoryUtilizationPercentage: ""
- targetCPUUtilizationPercentage: ""
-
-## Metrics removed; monitoring configuration no longer used
diff --git a/components.json b/components.json
index d5005f0974a11b0ea57843b9f52f82f995743963..8854f1e2cb22a6949612c72de37b6d9a57489b85 100644
--- a/components.json
+++ b/components.json
@@ -5,12 +5,11 @@
"tsx": true,
"tailwind": {
"config": "",
- "css": "app/globals.css",
- "baseColor": "zinc",
+ "css": "assets/globals.css",
+ "baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
- "iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
@@ -18,5 +17,5 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
- "registries": {}
-}
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/components/animated-blobs/index.tsx b/components/animated-blobs/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..516c36cf8ac5dd62a5f293b405bf3b2c480cb78f
--- /dev/null
+++ b/components/animated-blobs/index.tsx
@@ -0,0 +1,34 @@
+export function AnimatedBlobs() {
+ return (
+
+ );
+}
diff --git a/components/animated-text/index.tsx b/components/animated-text/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1bfef666235566c3f8fec1872b30ff5bb55b3168
--- /dev/null
+++ b/components/animated-text/index.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { useState, useEffect } from "react";
+
+interface AnimatedTextProps {
+ className?: string;
+}
+
+export function AnimatedText({ className = "" }: AnimatedTextProps) {
+ const [displayText, setDisplayText] = useState("");
+ const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
+ const [isTyping, setIsTyping] = useState(true);
+ const [showCursor, setShowCursor] = useState(true);
+ const [lastTypedIndex, setLastTypedIndex] = useState(-1);
+ const [animationComplete, setAnimationComplete] = useState(false);
+
+ // Randomize suggestions on each component mount
+ const [suggestions] = useState(() => {
+ const baseSuggestions = [
+ "create a stunning portfolio!",
+ "build a tic tac toe game!",
+ "design a website for my restaurant!",
+ "make a sleek landing page!",
+ "build an e-commerce store!",
+ "create a personal blog!",
+ "develop a modern dashboard!",
+ "design a company website!",
+ "build a todo app!",
+ "create an online gallery!",
+ "make a contact form!",
+ "build a weather app!",
+ ];
+
+ // Fisher-Yates shuffle algorithm
+ const shuffled = [...baseSuggestions];
+ for (let i = shuffled.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+ }
+
+ return shuffled;
+ });
+
+ useEffect(() => {
+ if (animationComplete) return;
+
+ let timeout: NodeJS.Timeout;
+
+ const typeText = () => {
+ const currentSuggestion = suggestions[currentSuggestionIndex];
+
+ if (isTyping) {
+ if (displayText.length < currentSuggestion.length) {
+ setDisplayText(currentSuggestion.slice(0, displayText.length + 1));
+ setLastTypedIndex(displayText.length);
+ timeout = setTimeout(typeText, 80);
+ } else {
+ // Finished typing, wait then start erasing
+ setLastTypedIndex(-1);
+ timeout = setTimeout(() => {
+ setIsTyping(false);
+ }, 2000);
+ }
+ }
+ };
+
+ timeout = setTimeout(typeText, 100);
+ return () => clearTimeout(timeout);
+ }, [
+ displayText,
+ currentSuggestionIndex,
+ isTyping,
+ suggestions,
+ animationComplete,
+ ]);
+
+ // Cursor blinking effect
+ useEffect(() => {
+ if (animationComplete) {
+ setShowCursor(false);
+ return;
+ }
+
+ const cursorInterval = setInterval(() => {
+ setShowCursor((prev) => !prev);
+ }, 600);
+
+ return () => clearInterval(cursorInterval);
+ }, [animationComplete]);
+
+ useEffect(() => {
+ if (lastTypedIndex >= 0) {
+ const timeout = setTimeout(() => {
+ setLastTypedIndex(-1);
+ }, 400);
+
+ return () => clearTimeout(timeout);
+ }
+ }, [lastTypedIndex]);
+
+ return (
+
+ Hey DeepSite,
+ {displayText.split("").map((char, index) => (
+
+ {char}
+
+ ))}
+
+ |
+
+
+ );
+}
diff --git a/components/ask-ai/ask-ai-landing.tsx b/components/ask-ai/ask-ai-landing.tsx
deleted file mode 100644
index db19f5be4bfc3fc05e4083a18a41cca6169bf65a..0000000000000000000000000000000000000000
--- a/components/ask-ai/ask-ai-landing.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-"use client";
-import { ArrowUp } from "lucide-react";
-import { useState } from "react";
-import { useLocalStorage, useMount } from "react-use";
-import { useRouter } from "next/navigation";
-
-import { Button } from "@/components/ui/button";
-import { ProviderType } from "@/lib/type";
-import { Models } from "./models";
-import { DEFAULT_MODEL } from "@/lib/providers";
-import { cn } from "@/lib/utils";
-
-export function AskAiLanding({ className }: { className?: string }) {
- const [model = DEFAULT_MODEL, setModel] = useLocalStorage(
- "deepsite-model",
- DEFAULT_MODEL
- );
- const [provider, setProvider] = useLocalStorage(
- "deepsite-provider",
- "auto" as ProviderType
- );
- const router = useRouter();
- const [prompt, setPrompt] = useState("");
- const [mounted, setMounted] = useState(false);
-
- useMount(() => {
- setMounted(true);
- });
-
- return (
-
- );
-}
diff --git a/components/ask-ai/ask-ai.tsx b/components/ask-ai/ask-ai.tsx
deleted file mode 100644
index 66ce78f0d25644ecc7c222911f9455fca60de75c..0000000000000000000000000000000000000000
--- a/components/ask-ai/ask-ai.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-"use client";
-import { ArrowUp, Paintbrush, X } from "lucide-react";
-import { FaHand } from "react-icons/fa6";
-import { useRef, useState } from "react";
-import { HiStop } from "react-icons/hi2";
-import { useLocalStorage, useMount, useUpdateEffect } from "react-use";
-import { useRouter } from "next/navigation";
-import { useNextStep } from "nextstepjs";
-
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import { useGeneration } from "./useGeneration";
-import { File, MobileTabType, ProviderType } from "@/lib/type";
-import { Models } from "./models";
-import { DEFAULT_MODEL, MODELS } from "@/lib/providers";
-import { Redesign } from "./redesign";
-import { Uploader } from "./uploader";
-import { InputMentions } from "./input-mentions";
-
-export function AskAI({
- initialPrompt,
- className,
- onToggleMobileTab,
- files,
- medias,
- tourHasBeenShown,
- isNew = false,
- isHistoryView,
- projectName = "new",
-}: {
- initialPrompt?: string;
- className?: string;
- files?: File[] | null;
- medias?: string[] | null;
- tourHasBeenShown?: boolean;
- onToggleMobileTab?: (tab: MobileTabType) => void;
- isNew?: boolean;
- isHistoryView?: boolean;
- projectName?: string;
-}) {
- const contentEditableRef = useRef(null);
- const [model = DEFAULT_MODEL, setModel] = useLocalStorage(
- "deepsite-model",
- DEFAULT_MODEL
- );
- const [provider, setProvider] = useLocalStorage(
- "deepsite-provider",
- "auto" as ProviderType
- );
-
- const [prompt, setPrompt] = useState(initialPrompt ?? "");
- const [redesignMd, setRedesignMd] = useState<{
- md: string;
- url: string;
- } | null>(null);
- const [selectedMedias, setSelectedMedias] = useState([]);
- const [imageLinks, setImageLinks] = useState([]);
- const [startTour, setStartTour] = useState(false);
-
- const router = useRouter();
- const { callAi, isLoading, stopGeneration, audio } =
- useGeneration(projectName);
- const { startNextStep } = useNextStep();
-
- const onComplete = () => {
- onToggleMobileTab?.("right-sidebar");
- };
-
- useMount(() => {
- if (initialPrompt && initialPrompt.trim() !== "" && isNew) {
- setTimeout(() => {
- if (isHistoryView) return;
- callAi(
- {
- prompt: initialPrompt,
- model,
- onComplete,
- provider,
- },
- setModel
- );
- router.replace("/new");
- }, 200);
- }
- });
-
- const onSubmit = () => {
- if (isHistoryView) return;
- if (contentEditableRef.current) {
- contentEditableRef.current.innerHTML = "";
- }
- callAi(
- {
- prompt,
- model,
- onComplete,
- provider,
- redesignMd,
- medias: [...(selectedMedias ?? []), ...(imageLinks ?? [])],
- },
- setModel
- );
- if (selectedMedias.length > 0) setSelectedMedias([]);
- if (imageLinks.length > 0) setImageLinks([]);
- if (redesignMd) setRedesignMd(null);
- };
-
- return (
-
-
-
-
-
- Your browser does not support the audio element.
-
-
- );
-}
diff --git a/components/ask-ai/context.tsx b/components/ask-ai/context.tsx
deleted file mode 100644
index c22f17fc81f95788b6d3e690cc70ac5d3b6f817a..0000000000000000000000000000000000000000
--- a/components/ask-ai/context.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { AtSign, Braces, FileCode, FileText, X } from "lucide-react";
-import { useMemo, useState } from "react";
-import { useQueryClient } from "@tanstack/react-query";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { File } from "@/lib/type";
-import { cn } from "@/lib/utils";
-
-export const Context = ({
- files,
- setFiles,
-}: {
- files: File[];
- setFiles: (files: File[]) => void;
-}) => {
- const queryClient = useQueryClient();
- const [open, setOpen] = useState(false);
-
- const getFileIcon = (filePath: string, size = "size-3.5") => {
- if (filePath.endsWith(".css")) {
- return ;
- } else if (filePath.endsWith(".js")) {
- return ;
- } else if (filePath.endsWith(".json")) {
- return ;
- } else {
- return ;
- }
- };
-
- const getFiles = () => queryClient.getQueryData(["files"]) ?? [];
-
- return (
-
-
-
-
-
- Add Context...
-
-
-
-
-
- Select a file to send as context
-
-
- {getFiles().length === 0 ? (
-
- No files available
-
- ) : (
- <>
-
{
- setFiles([]);
- setOpen(false);
- }}
- className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-accent hover:text-accent-foreground transition-colors ${
- files.length === 0
- ? "bg-linear-to-r from-indigo-500/20 to-indigo-500/5 text-primary font-medium"
- : "text-muted-foreground"
- }`}
- >
- All files (default)
-
- {getFiles()?.map((page) => (
-
{
- if (files.some((f) => f.path === page.path))
- setFiles(files.filter((f) => f.path !== page.path));
- else setFiles(files ? [...files, page] : [page]);
- setOpen(false);
- }}
- className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-accent hover:text-accent-foreground transition-colors flex items-center gap-1.5 ${
- files?.some((f) => f.path === page.path)
- ? "bg-linear-to-r from-indigo-500/20 to-indigo-500/5 text-primary font-medium"
- : "text-muted-foreground"
- }`}
- >
-
- {getFileIcon(page.path, "size-3")}
-
- {page.path}
-
- ))}
- >
- )}
-
-
-
-
- {files?.map((file) => (
-
- {getFileIcon(file.path, "size-3")}
- {file.path}
- {
- setFiles(files.filter((f) => f.path !== file.path));
- }}
- >
-
-
-
- ))}
-
- );
-};
diff --git a/components/ask-ai/input-mentions.tsx b/components/ask-ai/input-mentions.tsx
deleted file mode 100644
index 4ef11b0a0cd9aeaef85eddfd049a30272437b414..0000000000000000000000000000000000000000
--- a/components/ask-ai/input-mentions.tsx
+++ /dev/null
@@ -1,477 +0,0 @@
-import { useRef, useState, useEffect, RefObject } from "react";
-import { useClickAway } from "react-use";
-import { useQueryClient } from "@tanstack/react-query";
-
-import { searchFilesMentions } from "@/actions/mentions";
-import { File } from "@/lib/type";
-import { Braces, FileCode, FileText } from "lucide-react";
-
-export function InputMentions({
- ref,
- prompt,
- files,
- setPrompt,
- redesignMdUrl,
- onSubmit,
- imageLinks,
- setImageLinks,
-}: {
- ref: RefObject;
- prompt: string;
- files?: File[] | null;
- redesignMdUrl?: string;
- setPrompt: (prompt: string) => void;
- onSubmit: () => void;
- imageLinks?: string[];
- setImageLinks?: (links: string[]) => void;
-}) {
- const queryClient = useQueryClient();
- const [showMentionDropdown, setShowMentionDropdown] = useState(false);
- const [, setMentionSearch] = useState("");
- const dropdownRef = useRef(null);
- const [results, setResults] = useState([]);
-
- useClickAway(dropdownRef, () => {
- setShowMentionDropdown(false);
- });
-
- const isImageUrl = (url: string): boolean => {
- // Check if it's a valid HTTP/HTTPS URL with an image extension
- const imageUrlPattern = /^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i;
- return imageUrlPattern.test(url);
- };
-
- const getTextContent = (element: HTMLElement): string => {
- let text = "";
- const childNodes = element.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.TEXT_NODE) {
- text += node.textContent || "";
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("mention-chip")) {
- text += el.getAttribute("data-mention-id") || "";
- } else if (el.classList.contains("image-chip")) {
- // Include image URL in text content for display purposes
- const imageUrl = el.getAttribute("data-image-url") || "";
- text += imageUrl ? ` ${imageUrl} ` : "";
- } else {
- text += el.textContent || "";
- }
- }
- }
- return text + "\u0020";
- };
-
- const extractPromptWithIds = (): string => {
- if (!ref.current) return "";
-
- let text = "";
- const childNodes = ref.current.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.TEXT_NODE) {
- text += node.textContent || "";
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("mention-chip")) {
- text += el.getAttribute("data-mention-id") || "";
- } else if (el.classList.contains("image-chip")) {
- // Include image URL in prompt text
- const imageUrl = el.getAttribute("data-image-url") || "";
- text += imageUrl ? ` ${imageUrl} ` : "";
- } else {
- text += el.textContent || "";
- }
- }
- }
- return text;
- };
-
- const extractImageLinks = (): string[] => {
- if (!ref.current) return [];
-
- const links: string[] = [];
- const childNodes = ref.current.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("image-chip")) {
- const imageUrl = el.getAttribute("data-image-url");
- if (imageUrl) links.push(imageUrl);
- }
- }
- }
- return links;
- };
-
- const shouldDetectMention = (): {
- detect: boolean;
- textBeforeCursor: string;
- } => {
- const selection = window.getSelection();
- if (!selection || !ref.current) {
- return { detect: false, textBeforeCursor: "" };
- }
-
- const range = selection.getRangeAt(0);
- const node = range.startContainer;
-
- if (node.nodeType === Node.ELEMENT_NODE) {
- const element = node as HTMLElement;
- if (element.classList?.contains("mention-chip")) {
- return { detect: false, textBeforeCursor: "" };
- }
- }
-
- if (node.parentElement?.classList?.contains("mention-chip")) {
- return { detect: false, textBeforeCursor: "" };
- }
-
- if (node.nodeType === Node.TEXT_NODE) {
- const textContent = node.textContent || "";
- const cursorOffset = range.startOffset;
- const textBeforeCursor = textContent.substring(0, cursorOffset);
-
- const lastAtIndex = textBeforeCursor.lastIndexOf("@");
- if (lastAtIndex !== -1) {
- const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
- if (!textAfterAt.includes(" ")) {
- return { detect: true, textBeforeCursor: textAfterAt };
- }
- }
- }
-
- return { detect: false, textBeforeCursor: "" };
- };
-
- const handleInput = async () => {
- if (!ref.current) return;
- const text = getTextContent(ref.current);
-
- // Only clear if there's no content at all (including chips)
- const hasImageChips = ref.current.querySelectorAll(".image-chip").length > 0;
- const hasMentionChips = ref.current.querySelectorAll(".mention-chip").length > 0;
-
- if (text.trim() === "" && !hasImageChips && !hasMentionChips) {
- ref.current.innerHTML = "";
- }
-
- setPrompt(text);
-
- // Update image links whenever input changes
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
-
- const { detect, textBeforeCursor } = shouldDetectMention();
-
- if (detect && files && files?.length > 0) {
- setMentionSearch(textBeforeCursor);
- setShowMentionDropdown(true);
- const files = queryClient.getQueryData(["files"]) ?? [];
- const results = await searchFilesMentions(textBeforeCursor, files);
- setResults(results);
- } else {
- setShowMentionDropdown(false);
- }
- };
-
- const createMentionChipElement = (mentionId: string): HTMLSpanElement => {
- const mentionChip = document.createElement("span");
-
- mentionChip.className =
- "mention-chip inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400";
- mentionChip.contentEditable = "false";
- mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`);
- mentionChip.textContent = `@${mentionId}`;
-
- return mentionChip;
- };
-
- const createImageChipElement = (imageUrl: string): HTMLSpanElement => {
- const imageChip = document.createElement("span");
-
- imageChip.className =
- "image-chip inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400";
- imageChip.contentEditable = "false";
- imageChip.setAttribute("data-image-url", imageUrl);
-
- // Create icon (using emoji for simplicity)
- const icon = document.createElement("span");
- icon.textContent = "πΌοΈ";
- icon.className = "text-[10px]";
-
- // Truncate URL for display
- const displayUrl =
- imageUrl.length > 30 ? imageUrl.substring(0, 30) + "..." : imageUrl;
- const text = document.createTextNode(displayUrl);
-
- imageChip.appendChild(icon);
- imageChip.appendChild(text);
-
- return imageChip;
- };
-
- const insertMention = (mentionId: string) => {
- if (!ref.current) return;
-
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0) return;
-
- const range = selection.getRangeAt(0);
- const textNode = range.startContainer;
-
- if (textNode.nodeType !== Node.TEXT_NODE) return;
-
- const textContent = textNode.textContent || "";
- const cursorOffset = range.startOffset;
-
- const textBeforeCursor = textContent.substring(0, cursorOffset);
- const lastAtIndex = textBeforeCursor.lastIndexOf("@");
-
- if (lastAtIndex !== -1) {
- const mentionChip = createMentionChipElement(mentionId);
-
- const beforeText = textContent.substring(0, lastAtIndex);
- const afterText = textContent.substring(cursorOffset);
- const parent = textNode.parentNode;
- if (!parent) return;
-
- const beforeNode = beforeText
- ? document.createTextNode(beforeText)
- : null;
- const spaceNode = document.createTextNode("\u0020");
- const afterNode = afterText ? document.createTextNode(afterText) : null;
-
- if (beforeNode) {
- parent.insertBefore(beforeNode, textNode);
- }
- parent.insertBefore(mentionChip, textNode);
- parent.insertBefore(spaceNode, textNode);
- if (afterNode) {
- parent.insertBefore(afterNode, textNode);
- }
-
- parent.removeChild(textNode);
-
- const newRange = document.createRange();
- if (afterNode) {
- newRange.setStart(afterNode, 0);
- } else {
- newRange.setStartAfter(spaceNode);
- }
- newRange.collapse(true);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- const newText = getTextContent(ref.current);
- setPrompt(newText);
- setShowMentionDropdown(false);
- setMentionSearch("");
- }
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (!prompt || prompt.trim() === "") return;
-
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- const promptWithIds = extractPromptWithIds();
- setPrompt(promptWithIds);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
-
- onSubmit();
-
- if (ref.current) {
- ref.current.innerHTML = "";
- }
- setPrompt("");
- setShowMentionDropdown(false);
- } else if (e.key === "Escape") {
- setShowMentionDropdown(false);
- }
- };
-
- useEffect(() => {
- if (ref.current && prompt === "" && ref.current.innerHTML !== "") {
- ref.current.innerHTML = "";
- }
- }, [prompt]);
-
- const handlePaste = (e: React.ClipboardEvent) => {
- e.preventDefault();
- const text = e.clipboardData.getData("text/plain");
-
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0 || !ref.current) {
- document.execCommand("insertText", false, text);
- return;
- }
-
- const trimmedText = text.trim();
-
- // Check if pasted text is ONLY an image URL
- if (isImageUrl(trimmedText)) {
- const range = selection.getRangeAt(0);
- const imageChip = createImageChipElement(trimmedText);
- const spaceNode = document.createTextNode("\u0020");
-
- range.deleteContents();
- range.insertNode(imageChip);
-
- // Insert space after the chip
- const afterChipRange = document.createRange();
- afterChipRange.setStartAfter(imageChip);
- afterChipRange.insertNode(spaceNode);
-
- // Move cursor after the space
- const newRange = document.createRange();
- newRange.setStartAfter(spaceNode);
- newRange.collapse(true);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- // Update state
- const newText = getTextContent(ref.current);
- setPrompt(newText);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
- } else {
- // Check if text contains image URLs
- const imageUrlPattern = /https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?[^\s]*)?/gi;
- const containsImageUrl = imageUrlPattern.test(text);
-
- if (containsImageUrl) {
- // Split text and replace image URLs with chips
- const parts = text.split(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?[^\s]*)?)/gi);
- const range = selection.getRangeAt(0);
- range.deleteContents();
-
- const fragment = document.createDocumentFragment();
-
- parts.forEach((part) => {
- if (isImageUrl(part)) {
- const imageChip = createImageChipElement(part);
- const spaceNode = document.createTextNode("\u0020");
- fragment.appendChild(imageChip);
- fragment.appendChild(spaceNode);
- } else if (part && !part.match(/^\?[^\s]*$/)) {
- // Skip query string matches, add other text
- const textNode = document.createTextNode(part);
- fragment.appendChild(textNode);
- }
- });
-
- range.insertNode(fragment);
-
- // Move cursor to end
- const newRange = document.createRange();
- newRange.selectNodeContents(ref.current);
- newRange.collapse(false);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- // Update state
- const newText = getTextContent(ref.current);
- setPrompt(newText);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
- } else {
- // Plain text, use default insertion
- document.execCommand("insertText", false, text);
- }
- }
- };
-
- return (
-
-
"']/g,
- ""
- )}, want to add something?`
- : files && files.length > 0
- ? "Ask me anything. Type @ to mention a file..."
- : "Ask me anything..."
- }
- onInput={handleInput}
- onKeyDown={handleKeyDown}
- onPaste={handlePaste}
- suppressContentEditableWarning
- >
- {showMentionDropdown && (
-
-
- {results?.length > 0 && (
-
- {results.map((file) => (
- insertMention(file.path)}
- />
- ))}
-
- )}
-
-
- )}
-
- );
-}
-
-export const getFileIcon = (filePath: string, size = "size-3.5") => {
- if (filePath.endsWith(".css")) {
- return ;
- } else if (filePath.endsWith(".js")) {
- return ;
- } else if (filePath.endsWith(".json")) {
- return ;
- } else {
- return ;
- }
-};
-
-function MentionResult({
- file,
- onSelect,
-}: {
- file: File;
- onSelect: () => void;
-}) {
- return (
-
- {getFileIcon(file.path, "size-3")}
- {file.path}
-
- );
-}
diff --git a/components/ask-ai/loading.tsx b/components/ask-ai/loading.tsx
deleted file mode 100644
index 060be494fb586788eab5c2139bb080fa51e209a2..0000000000000000000000000000000000000000
--- a/components/ask-ai/loading.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-"use client";
-import Loading from "@/components/loading";
-
-export const AiLoading = ({
- text = "AI Assistant is thinking...",
- showCircle = true,
- className,
-}: {
- text?: string;
- showCircle?: boolean;
- className?: string;
-}) => {
- return (
-
- {showCircle &&
}
-
-
- {text.split("").map((char, index) => (
-
- {char === " " ? "\u00A0" : char}
-
- ))}
-
-
-
- );
-};
diff --git a/components/ask-ai/models.tsx b/components/ask-ai/models.tsx
deleted file mode 100644
index 4e58acffcf87295047815246cf3b2dd14ef04d2f..0000000000000000000000000000000000000000
--- a/components/ask-ai/models.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import {
- BrainIcon,
- ChevronDown,
- DollarSign,
- StarsIcon,
- Zap,
-} from "lucide-react";
-import { useMemo, useState } from "react";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import { ProviderType } from "@/lib/type";
-import { MODELS } from "@/lib/providers";
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-export function Models({
- model,
- setModel,
- provider,
- setProvider,
-}: {
- model: string;
- setModel: (model: string) => void;
- provider: ProviderType;
- setProvider: (provider: ProviderType) => void;
-}) {
- const [open, setOpen] = useState(false);
-
- const formattedModels = useMemo(() => {
- const lists: ((typeof MODELS)[0] | { isCategory: true; name: string })[] =
- [];
- const keys = new Set();
- MODELS.forEach((model) => {
- if (!keys.has(model.companyName)) {
- lists.push({
- isCategory: true,
- name: model.companyName,
- logo: model.logo,
- });
- keys.add(model.companyName);
- }
- lists.push(model);
- });
- return lists;
- }, []);
-
- return (
-
-
-
-
- {model.split("/").pop()?.toLowerCase()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {formattedModels.map(
- (
- item:
- | (typeof MODELS)[0]
- | { isCategory: true; name: string }
- ) => {
- if ("isCategory" in item) {
- return (
-
- {item.name}
-
- );
- }
- const {
- value,
- label,
- isNew = false,
- isBestSeller = false,
- } = item;
- return (
-
- {value.split("/").pop() || label}
- {isNew && (
-
- New
-
- )}
- {isBestSeller && (
-
- )}
-
- );
- }
- )}
-
-
-
-
-
-
Provider mode:
-
- {(
- [
- { value: "cheapest", icon: DollarSign, color: "emerald" },
- {
- value: "auto",
- icon: BrainIcon,
- color: "indigo",
- name: "Smartest",
- },
- { value: "fastest", icon: Zap, color: "amber" },
- ] as const
- ).map(
- ({
- value,
- icon: Icon,
- color,
- name,
- }: {
- value: string;
- icon: React.ElementType;
- color: string;
- name?: string;
- }) => (
-
setProvider(value)}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- setProvider(value);
- }
- }}
- className={cn(
- "inline-flex items-center gap-1.5 h-7 px-2.5 text-xs font-medium border border-border rounded-md cursor-pointer transition-colors select-none",
- "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
- provider === value && [
- "bg-transparent",
- color === "emerald" &&
- "[&_svg]:fill-emerald-500 [&_svg]:stroke-emerald-500 bg-emerald-500/10! border-emerald-500/10! text-emerald-500!",
- color === "indigo" &&
- "[&_svg]:fill-indigo-500 [&_svg]:stroke-indigo-500 bg-indigo-500/10! border-indigo-500/10! text-indigo-500!",
- color === "amber" &&
- "[&_svg]:fill-amber-500 [&_svg]:stroke-amber-500 bg-amber-500/10! border-amber-500/10! text-amber-500!",
- ]
- )}
- >
-
- {name ?? value.charAt(0).toUpperCase() + value.slice(1)}
-
- )
- )}
-
-
-
-
-
- );
-}
diff --git a/components/ask-ai/uploader.tsx b/components/ask-ai/uploader.tsx
deleted file mode 100644
index c2f5f7482790b2209222c01a827ace953ddde4ca..0000000000000000000000000000000000000000
--- a/components/ask-ai/uploader.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import {
- CheckCircle,
- FileVideo,
- ImageIcon,
- Music,
- Paperclip,
- Video,
-} from "lucide-react";
-import { useRef, useState } from "react";
-import Image from "next/image";
-import { useParams } from "next/navigation";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { getFileType, humanizeNumber } from "@/lib/utils";
-import { useQueryClient } from "@tanstack/react-query";
-import { ProjectWithCommits } from "@/actions/projects";
-import { toast } from "sonner";
-import Loading from "../loading";
-
-export const Uploader = ({
- medias,
- selected,
- setSelected,
-}: {
- medias?: string[] | null;
- selected: string[];
- setSelected: React.Dispatch>;
-}) => {
- const queryClient = useQueryClient();
- const { repoId } = useParams<{ repoId: string }>();
-
- const [open, setOpen] = useState(false);
- const [isUploading, setIsUploading] = useState(false);
- const [error, setError] = useState(null);
- const fileInputRef = useRef(null);
-
- const getFileIcon = (url: string) => {
- const fileType = getFileType(url);
- switch (fileType) {
- case "image":
- return ;
- case "video":
- return ;
- case "audio":
- return ;
- default:
- return ;
- }
- };
-
- const uploadFiles = async (files: FileList | null) => {
- setError(null);
- if (!files || files.length === 0) return;
-
- setIsUploading(true);
- const data = new FormData();
- Array.from(files).forEach((file) => {
- data.append("images", file);
- });
-
- const response = await fetch(`/api/projects/${repoId}/medias`, {
- method: "POST",
- body: data,
- })
- .then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to save changes");
- })
- .catch((err) => {
- return { success: false, err };
- });
-
- if (response.success) {
- queryClient.setQueryData(
- ["project"],
- (oldProject: ProjectWithCommits) => ({
- ...oldProject,
- medias: [...response.medias, ...(oldProject?.medias ?? [])],
- })
- );
- toast.success("Media files uploaded successfully!");
- } else {
- setError(
- response.err ?? "Failed to upload media files, try again later."
- );
- }
-
- setIsUploading(false);
- };
-
- return (
-
-
-
- 0 ? "xs" : "icon-xs"}
- variant={
- selected?.length > 0 ? "indigo" : open ? "default" : "bordered"
- }
- className="rounded-full! relative"
- >
-
- {selected?.length > 0 && (
-
- {humanizeNumber(selected.length)}
-
- )}
-
-
-
-
-
- {error && {error}
}
- {medias && medias.length > 0 && (
-
-
- Uploaded files:
-
-
-
- {medias.map((media: string) => {
- const fileType = getFileType(media);
- return (
-
- setSelected(
- selected.includes(media)
- ? selected.filter((f) => f !== media)
- : [...selected, media]
- )
- }
- >
- {fileType === "image" ? (
-
- ) : fileType === "video" ? (
-
- ) : fileType === "audio" ? (
-
-
-
- ) : (
-
- {getFileIcon(media)}
-
- )}
- {selected.includes(media) && (
-
-
-
- )}
-
- );
- })}
-
-
-
- )}
-
- fileInputRef.current?.click()}
- className="relative w-full"
- disabled={isUploading}
- >
- {isUploading ? (
- <>
-
- Uploading media file(s)...
- >
- ) : (
- "Upload Media Files"
- )}
-
- uploadFiles(e.target.files)}
- />
-
-
-
-
-
- );
-};
diff --git a/components/ask-ai/useGeneration.ts b/components/ask-ai/useGeneration.ts
deleted file mode 100644
index e847b86b1ab2e61469fc568b0e35b47864242176..0000000000000000000000000000000000000000
--- a/components/ask-ai/useGeneration.ts
+++ /dev/null
@@ -1,437 +0,0 @@
-"use client";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useRef } from "react";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-import { v4 as uuidv4 } from "uuid";
-import { useLocalStorage } from "react-use";
-import { useSession } from "next-auth/react";
-
-import { formatResponse } from "@/lib/format";
-import { File, Message, MessageActionType, ProviderType } from "@/lib/type";
-import { getContextFilesFromPrompt } from "@/lib/utils";
-
-const MESSAGES_QUERY_KEY = (projectName: string) =>
- ["messages", projectName] as const;
-
-export const useGeneration = (projectName: string) => {
- const router = useRouter();
- const audio = useRef(null);
- const queryClient = useQueryClient();
- const abortController = useRef(null);
- const [, setStoredMessages] = useLocalStorage(
- `messages-${projectName}`,
- []
- );
- const { data: session } = useSession();
-
- const { data: isLoading } = useQuery({
- queryKey: ["ai.generation.isLoading"],
- queryFn: () => false,
- refetchOnWindowFocus: false,
- refetchOnReconnect: false,
- refetchOnMount: false,
- staleTime: Infinity,
- });
-
- const setIsLoading = (isLoading: boolean) => {
- queryClient.setQueryData(["ai.generation.isLoading"], isLoading);
- };
-
- const getFiles = () => queryClient.getQueryData(["files"]) ?? [];
- const setFiles = (newFiles: File[]) => {
- queryClient.setQueryData(["files"], (oldFiles: File[] = []) => {
- const currentFiles = oldFiles.filter(
- (file) => !newFiles.some((f) => f.path === file.path)
- );
- return [...currentFiles, ...newFiles];
- });
- };
-
- const getMessages = () =>
- queryClient.getQueryData(
- MESSAGES_QUERY_KEY(projectName ?? "new")
- ) ?? [];
- const addMessage = (message: Omit) => {
- const id = uuidv4();
- const key = MESSAGES_QUERY_KEY(projectName ?? "new");
- queryClient.setQueryData(key, (oldMessages = []) => {
- const newMessages = [...oldMessages, { ...message, id }];
- if (projectName !== "new") {
- localStorage.setItem(
- `messages-${projectName}`,
- JSON.stringify(newMessages)
- );
- }
- return newMessages;
- });
- return id;
- };
-
- const updateLastMessage = (content: string, files?: File[]) => {
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(projectName),
- (oldMessages = []) => {
- const newMessages = [
- ...oldMessages.slice(0, -1),
- {
- ...oldMessages[oldMessages.length - 1],
- content,
- isThinking: false,
- files: files?.map((file) => file.path),
- },
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- return newMessages;
- }
- );
- };
-
- const updateMessage = (messageId: string, message: Partial) => {
- const key = MESSAGES_QUERY_KEY(projectName ?? "new");
- const currentMessages = queryClient.getQueryData(key);
- if (!currentMessages) return;
- const index = currentMessages.findIndex((m) => m.id === messageId);
- if (index === -1) return;
- const newMessages = [
- ...currentMessages.slice(0, index),
- { ...currentMessages[index], ...message },
- ...currentMessages.slice(index + 1),
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- queryClient.setQueryData(key, newMessages);
- };
-
- const storeMessages = async (newProjectName: string) => {
- return new Promise((resolve) => {
- const currentMessages = queryClient.getQueryData(
- MESSAGES_QUERY_KEY("new")
- );
- localStorage.setItem(
- `messages-${newProjectName}`,
- JSON.stringify(currentMessages)
- );
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(newProjectName),
- currentMessages
- );
- setTimeout(() => resolve(true), 100);
- });
- };
-
- const createProject = async (
- files: File[],
- projectTitle: string,
- indexMessage: string,
- prompt: string
- ) => {
- updateMessage(indexMessage, {
- actions: [
- {
- label: "Publishing on Hugging Face...",
- variant: "default",
- loading: true,
- type: MessageActionType.PUBLISH_PROJECT,
- },
- ],
- });
- try {
- const response = await fetch("/api/projects", {
- method: "POST",
- body: JSON.stringify({
- projectTitle,
- files,
- prompt,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- }).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to publish project");
- });
-
- if (response.repoUrl) {
- toast.success("Project has been published, build in progress...");
- updateMessage(indexMessage, {
- actions: [
- {
- label: "See Live preview",
- variant: "default",
- type: MessageActionType.SEE_LIVE_PREVIEW,
- },
- ],
- });
- storeMessages(response.repoUrl).then(() => {
- router.push(`/${response.repoUrl}`);
- });
- }
- } catch (error) {
- toast.error("Failed to publish project");
- updateMessage(indexMessage, {
- actions: [
- {
- label: "Publish on Hugging Face",
- variant: "default",
- type: MessageActionType.PUBLISH_PROJECT,
- projectTitle,
- prompt,
- },
- ],
- });
- }
- };
-
- const callAi = async (
- {
- prompt,
- model,
- onComplete,
- provider = "auto",
- redesignMd,
- medias,
- }: {
- prompt: string;
- model: string;
- redesignMd?: {
- url: string;
- md: string;
- } | null;
- medias?: string[] | null;
- onComplete: () => void;
- provider?: ProviderType;
- },
- setModel: (model: string) => void
- ) => {
- setIsLoading(true);
- const messages = getMessages();
- const files = getFiles();
- const filesToUse = await getContextFilesFromPrompt(prompt, files);
- const previousMessages = [...messages]?.filter(
- (message) => !message.isAutomated || !message.isAborted
- );
- addMessage({
- role: "user",
- content: `${
- redesignMd?.url ? `Redesign: ${redesignMd.url}\n` : ""
- }${prompt}`,
- createdAt: new Date(),
- });
- addMessage({
- role: "assistant",
- isThinking: true,
- createdAt: new Date(),
- model,
- });
-
- const isFollowUp = files?.length > 0;
- abortController.current = new AbortController();
-
- const request = await fetch("/api/ask", {
- method: "POST",
- body: JSON.stringify({
- prompt,
- model,
- files: filesToUse,
- previousMessages,
- provider,
- redesignMd,
- medias,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- ...(abortController.current
- ? { signal: abortController.current.signal }
- : {}),
- });
-
- const currentMessages = getMessages();
-
- if (!request.ok) {
- const jsonResponse = await request.json()?.catch(() => null);
- const errorMessage =
- jsonResponse?.error || `Status code: ${request.status}`;
-
- const lastMessageId = currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isThinking: false,
- isAborted: true,
- content: `Error: ${errorMessage}`,
- });
- setIsLoading(false);
- return;
- }
-
- if (request && request.body) {
- const reader = request.body.getReader();
- const decoder = new TextDecoder();
- let completeResponse = "";
- const read = async () => {
- const { done, value } = await reader.read();
- if (done) {
- audio.current?.play();
- const files = getFiles();
- const {
- messageContent,
- files: newFiles,
- projectTitle,
- } = formatResponse(completeResponse, files ?? []);
- updateLastMessage(messageContent, newFiles);
- if (newFiles && newFiles.length > 0) {
- setFiles(newFiles);
- onComplete();
- if (projectName === "new") {
- addMessage({
- role: "assistant",
- content:
- "I've finished the generation. Now you can decide to publish the project on Hugging Face to share it!",
- createdAt: new Date(),
- isAutomated: true,
- actions: [
- {
- label: "Publish on Hugging Face",
- variant: "default",
- type: MessageActionType.PUBLISH_PROJECT,
- prompt,
- projectTitle,
- },
- ],
- });
- } else {
- const response = await fetch(
- `/api/projects/${projectName.split("/")[1]}`,
- {
- method: "PUT",
- body: JSON.stringify({
- files: newFiles,
- prompt,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- }
- ).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- });
- if (response.success) {
- toast.success("Project has been updated");
- } else {
- toast.error("Failed to update project");
- }
- }
- }
-
- setIsLoading(false);
- return;
- }
- const chunk = decoder.decode(value, { stream: true });
- completeResponse += chunk;
-
- if (completeResponse.includes("__ERROR__:")) {
- const errorMatch = completeResponse.match(/__ERROR__:(.+)/);
- if (errorMatch) {
- try {
- const errorData = JSON.parse(errorMatch[1]);
- if (errorData.isError) {
- const lastMessageId =
- currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isThinking: false,
- isAborted: true,
- content: errorData?.showProMessage
- ? session?.user?.isPro ? "You have already reached your monthly included credits with Hugging Face Pro plan. Please consider adding more credits to your account." : "You have exceeded your monthly included credits with Hugging Face inference provider. Please consider upgrading to a pro plan."
- : `Error: ${errorData.messageError}`,
- actions: errorData?.showProMessage
- ? session?.user?.isPro ? [{
- label: "Add more credits",
- variant: "default",
- type: MessageActionType.ADD_CREDITS,
- }] : [
- {
- label: "Upgrade to Pro",
- variant: "pro",
- type: MessageActionType.UPGRADE_TO_PRO,
- },
- ]
- : [],
- });
- setIsLoading(false);
- return;
- }
- } catch (e) {
- console.error("Failed to parse error message:", e);
- }
- }
- }
- if (
- completeResponse.includes(
- "_Note: The selected model was not available. Switched to"
- )
- ) {
- const newModel = completeResponse
- .match(
- /The selected model was not available. Switched to (.+)/
- )?.[1]
- .replace(/`/g, "")
- .replace(" ", "")
- .replace(/\.|_$/g, "");
- if (newModel) {
- setModel(newModel);
- updateMessage(currentMessages[currentMessages.length - 1].id, {
- model: newModel,
- });
- }
- }
-
- const files = getFiles();
- const { messageContent, files: newFiles } = formatResponse(
- completeResponse,
- files ?? []
- );
- if (messageContent) updateLastMessage(messageContent);
- if (newFiles && newFiles.length > 0) {
- if (!isFollowUp) {
- setFiles(newFiles);
- }
- updateLastMessage(messageContent, newFiles);
- }
- read();
- };
- return await read();
- }
- };
-
- const stopGeneration = () => {
- if (abortController.current) {
- abortController.current.abort();
- abortController.current = null;
- setIsLoading(false);
- const currentMessages = getMessages();
- const lastMessageId = currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isAborted: true,
- isThinking: false,
- });
- }
- };
-
- return {
- callAi,
- isLoading,
- stopGeneration,
- files: getFiles(),
- createProject,
- audio,
- };
-};
diff --git a/components/chat/index.tsx b/components/chat/index.tsx
deleted file mode 100644
index ececa27bb2dccd800b7796ebefe7cde50360e441..0000000000000000000000000000000000000000
--- a/components/chat/index.tsx
+++ /dev/null
@@ -1,377 +0,0 @@
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { useSession } from "next-auth/react";
-import { cn } from "@/lib/utils";
-import { ChevronRight, ExternalLink } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
-import Image from "next/image";
-import Markdown from "react-markdown";
-import { useQueryClient } from "@tanstack/react-query";
-import { formatDistanceToNow } from "date-fns";
-import { SpaceEntry } from "@huggingface/hub";
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism";
-
-import { useChat } from "./useChat";
-import { AiLoading } from "@/components/ask-ai/loading";
-import Loading from "@/components/loading";
-import { useGeneration } from "@/components/ask-ai/useGeneration";
-import { MessageAction, MessageActionType, File } from "@/lib/type";
-import { Button } from "@/components/ui/button";
-import { getFileIcon } from "@/components/ask-ai/input-mentions";
-import ProIcon from "@/assets/pro.svg";
-import { ProModal } from "../pro-modal";
-
-const ASSISTANT_AVATAR_URL = "https://i.imgur.com/Ho6v0or.jpeg";
-
-export function AppEditorChat({
- isNew,
- projectName,
- onSelectFile,
-}: {
- isNew?: boolean;
- projectName?: string;
- onSelectFile: (file: string) => void;
-}) {
- const chatContainerRef = useRef(null);
- const { data: session } = useSession();
- const queryClient = useQueryClient();
- const chatProjectName = isNew ? "new" : projectName ?? "new";
- const { messages } = useChat(chatProjectName);
- const { isLoading, createProject } = useGeneration(chatProjectName);
- const [openProModal, setOpenProModal] = useState(false);
-
- const project = queryClient.getQueryData(["project"]);
-
- const handleShowLastFile = (files?: string[]) => {
- if (files && files.length > 0) {
- const lastGeneratedFile = files[files.length - 1];
- if (lastGeneratedFile) {
- onSelectFile?.(lastGeneratedFile);
- }
- }
- };
- const handleActions = (action: MessageAction, messageId: string) => {
- if (!action) return;
- switch (action.type) {
- case MessageActionType.PUBLISH_PROJECT:
- const files = queryClient.getQueryData(["files"]) ?? [];
- return createProject(
- files ?? [],
- action.projectTitle ?? "",
- messageId,
- action.prompt ?? ""
- );
- case MessageActionType.SEE_LIVE_PREVIEW:
- return window.open(
- `https://huggingface.co/spaces/${project?.name}`,
- "_blank"
- );
- case MessageActionType.UPGRADE_TO_PRO:
- return setOpenProModal(true);
- case MessageActionType.ADD_CREDITS:
- return window.open(
- "https://huggingface.co/settings/billing?add-credits=true",
- "_blank"
- );
- }
- };
-
- useEffect(() => {
- if (chatContainerRef.current) {
- chatContainerRef.current.scrollTop =
- chatContainerRef.current.scrollHeight;
- }
- }, [messages]);
-
- return (
-
- {isNew ? (
-
- Start a new conversation with the AI
-
- ) : (
-
- Your last conversation has not been restored.
-
- )}
-
- {messages.map((message, id) => (
-
-
-
-
- {message.role === "user"
- ? session?.user?.name?.charAt(0) ?? ""
- : ""}
-
-
-
-
-
(
- {children}
- ),
- code: ({ className, children }) => {
- const isCodeBlock = className?.startsWith("language-");
- const language = className?.replace("language-", "");
- if (isCodeBlock) {
- return (
-
- {String(children).trim()}
-
- );
- }
-
- return (
-
- {children}
-
- );
- },
- ul: ({ children }) => (
-
- ),
- ol: ({ children }) => (
-
- {children}
-
- ),
- li: ({ children }) => (
- {children}
- ),
- a: ({ children, href }) => {
- const isValidUrl =
- href &&
- (href.startsWith("http://") ||
- href.startsWith("https://") ||
- href.startsWith("/") ||
- href.startsWith("#") ||
- href.startsWith("mailto:") ||
- href.startsWith("tel:"));
- const safeHref = isValidUrl ? href : "#";
-
- return (
-
-
- {children}
-
- );
- },
- pre: ({ children }) => <>{children}>,
- p: ({ children }) => {
- const content = String(children);
- if (
- typeof children === "string" &&
- (content.includes("file:/") ||
- content.match(/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?/i))
- ) {
- const parts = content.split(/(file:\/\S+|https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?)/gi);
- return (
-
- {parts.filter(Boolean).map((part, index) => {
- if (!part || part.trim() === "") return null;
-
- if (part.startsWith("file:/")) {
- return (
-
- {getFileIcon(part, "size-2.5")}
- {part.replace("file:/", "")}
-
- );
- } else if (
- part.match(
- /^https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?$/i
- )
- ) {
- const displayUrl =
- part.length > 35
- ? part.substring(0, 35) + "..."
- : part;
- return (
-
- πΌοΈ
- {displayUrl}
-
- );
- }
- return part;
- })}
-
- );
- }
- return {children}
;
- },
- }}
- >
- {message.content}
-
- {message.isThinking && (
-
- )}
- {message.isAborted && (
-
- The request has been aborted due to an error OR the user has
- stopped the generation.
-
- )}
-
- {message.model && message.createdAt && (
-
-
- via{" "}
-
- {message.model}
-
-
- {!message.isThinking && (
-
- {formatDistanceToNow(message.createdAt, {
- addSuffix: true,
- })}
-
- )}
-
- )}
- {message.files && message.files.length > 0 && (
-
-
- {isLoading &&
- messages[messages.length - 1].id === message.id ? (
- <>
-
-
- {isNew ? "Creating" : "Editing"}{" "}
-
-
- >
- ) : (
- <>
-
- {isNew ? "Created" : "Edited"}{" "}
-
- {message.files.length > 1 && (
-
- (+ {message.files.length - 1} files)
-
- )}
-
-
handleShowLastFile(message.files)}
- >
-
-
- >
- )}
-
-
- )}
- {message.actions &&
- message.actions.length > 0 &&
- messages.length - 1 === id && (
-
- {message.actions.map((action, id) => (
- handleActions(action, message.id)}
- >
- {action.loading && (
-
- )}
- {action.type === MessageActionType.UPGRADE_TO_PRO && (
-
- )}
- {action.label}
-
- ))}
-
- )}
-
-
- ))}
-
-
-
- );
-}
-
-const FileCode = ({ file }: { file: string }) => (
-
- {file}
-
-);
diff --git a/components/chat/useChat.ts b/components/chat/useChat.ts
deleted file mode 100644
index 2f32019379f2fed9411cae356d5ad7b50e4ef7d5..0000000000000000000000000000000000000000
--- a/components/chat/useChat.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useLocalStorage } from "react-use";
-import { useEffect } from "react";
-import { v4 as uuidv4 } from "uuid";
-
-import { Message } from "@/lib/type";
-
-const MESSAGES_QUERY_KEY = (projectName: string) =>
- ["messages", projectName] as const;
-
-export function useChat(projectName: string) {
- const queryClient = useQueryClient();
- const [, setStoredMessages, clearStoredMessages] = useLocalStorage(
- `messages-${projectName}`,
- []
- );
-
- useEffect(() => {
- if (projectName !== "new") {
- queryClient.invalidateQueries({
- queryKey: MESSAGES_QUERY_KEY(projectName),
- });
- }
- }, [projectName, queryClient]);
-
- const { data: messages = [] } = useQuery({
- queryKey: MESSAGES_QUERY_KEY(projectName),
- queryFn: () => {
- if (projectName === "new") {
- return [];
- }
- const storedData = localStorage.getItem(`messages-${projectName}`);
- if (storedData) {
- try {
- const parsedMessages = JSON.parse(storedData);
- if (parsedMessages && parsedMessages.length > 0) {
- return parsedMessages;
- }
- } catch (error) {
- console.error("Failed to parse stored messages:", error);
- }
- }
- return [];
- },
- refetchOnMount: false,
- refetchOnWindowFocus: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- refetchIntervalInBackground: false,
- });
-
- const addMessage = (message: Omit) => {
- const id = uuidv4();
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(projectName),
- (oldMessages = []) => {
- const newMessages = [
- ...oldMessages,
- {
- ...message,
- id,
- },
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- return newMessages;
- }
- );
- return id;
- };
- const clearMessages = () => {
- queryClient.setQueryData(MESSAGES_QUERY_KEY(projectName), []);
- clearStoredMessages();
- };
-
- const storeMessages = async (newProjectName: string) => {
- return new Promise((resolve) => {
- const currentMessages = queryClient.getQueryData(
- MESSAGES_QUERY_KEY("new")
- );
- localStorage.setItem(
- `messages-${newProjectName}`,
- JSON.stringify(currentMessages)
- );
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(newProjectName),
- currentMessages
- );
- setTimeout(() => resolve(true), 100);
- });
- };
-
- return {
- messages,
- addMessage,
- clearMessages,
- storeMessages,
- };
-}
diff --git a/components/code/index.tsx b/components/code/index.tsx
deleted file mode 100644
index 6b6b687f828b7bf8a5bd66afb958552132281f2f..0000000000000000000000000000000000000000
--- a/components/code/index.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import { useState } from "react";
-import {
- SandpackLayout,
- SandpackFileExplorer,
-} from "@codesandbox/sandpack-react";
-import { ChevronRight, ChevronLeft } from "lucide-react";
-import { useParams } from "next/navigation";
-import { useQueryClient } from "@tanstack/react-query";
-import { format } from "date-fns";
-import { useUpdateEffect } from "react-use";
-
-import { AppEditorMonacoEditor } from "./monaco-editor";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import Loading from "../loading";
-import { ProjectWithCommits } from "@/actions/projects";
-
-export function AppEditorCode() {
- const queryClient = useQueryClient();
- const { repoId } = useParams<{ repoId: string }>();
-
- const [isFileExplorerCollapsed, setIsFileExplorerCollapsed] = useState(true);
- const [isSavingChanges, setIsSavingChanges] = useState(false);
- const [isSavingChangesSuccess, setIsSavingChangesSuccess] = useState(false);
- const [isSavingChangesError, setIsSavingChangesError] = useState(false);
- const [isClosing, setIsClosing] = useState(false);
- const [showSaveChanges, setShowSaveChanges] = useState(false);
-
- const handleSaveChanges = async () => {
- if (repoId === "new") return;
- setIsSavingChanges(true);
- const manuallyUpdatedFiles =
- queryClient.getQueryData(["manuallyUpdatedFiles"]) ?? [];
- const response = await fetch(`/api/projects/${repoId}`, {
- method: "PUT",
- body: JSON.stringify({
- files: manuallyUpdatedFiles,
- prompt: `βοΈ ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Manual changes.`,
- isManualChanges: true,
- }),
- }).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to save changes");
- });
- if (response.success) {
- setIsSavingChangesSuccess(true);
- queryClient.invalidateQueries({ queryKey: ["manuallyUpdatedFiles"] });
- queryClient.setQueryData(
- ["project"],
- (oldProject: ProjectWithCommits) => ({
- ...oldProject,
- commits: [response.commit, ...(oldProject?.commits ?? [])],
- })
- );
- } else {
- setIsSavingChangesError(true);
- }
- setIsSavingChanges(false);
- setShowSaveChanges(false);
- };
-
- const undoChanges = () => {
- queryClient.setQueryData(["manuallyUpdatedFiles"], []);
- setShowSaveChanges(false);
- };
-
- useUpdateEffect(() => {
- if (isSavingChangesSuccess) {
- setTimeout(() => setIsSavingChangesSuccess(false), 3000);
- }
- }, [isSavingChangesSuccess]);
-
- useUpdateEffect(() => {
- if (isClosing) {
- setTimeout(() => {
- setIsSavingChangesSuccess(false);
- setIsSavingChangesError(false);
- setIsClosing(false);
- }, 300);
- }
- }, [isClosing]);
-
- return (
-
-
- setIsFileExplorerCollapsed(!isFileExplorerCollapsed)}
- title={
- isFileExplorerCollapsed
- ? "Expand file explorer"
- : "Collapse file explorer"
- }
- >
- {isFileExplorerCollapsed ? (
-
- ) : (
-
- )}
-
-
-
-
- {repoId && (
-
-
-
-
- {isSavingChangesSuccess
- ? "Changes saved"
- : isSavingChangesError
- ? "Failed to save changes"
- : "Save Changes"}
-
-
- {isSavingChangesSuccess
- ? "Changes saved successfully"
- : isSavingChangesError
- ? "Something went wrong, please try again."
- : "You have unsaved manual changes. Click the button to save your changes."}
-
-
-
- {!isSavingChangesSuccess && !isSavingChanges && (
-
- Undo
-
- )}
-
- {isSavingChangesSuccess || isSavingChangesError ? (
- setIsClosing(true)}
- >
- Close
-
- ) : (
-
- Save Changes
- {isSavingChanges && (
-
- )}
-
- )}
-
-
-
- )}
-
- );
-}
diff --git a/components/code/monaco-editor.tsx b/components/code/monaco-editor.tsx
deleted file mode 100644
index d222ce260193d76d9da71b784259a01653782381..0000000000000000000000000000000000000000
--- a/components/code/monaco-editor.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { useMemo, useRef, useCallback, useEffect } from "react";
-import { useQueryClient } from "@tanstack/react-query";
-import { Monaco } from "@monaco-editor/react";
-import {
- useActiveCode,
- SandpackStack,
- FileTabs,
- useSandpack,
-} from "@codesandbox/sandpack-react";
-import { useTheme } from "next-themes";
-
-import NightLight from "@/components/editor/night-light.json";
-import Night from "@/components/editor/night.json";
-import { File } from "@/lib/type";
-import dynamic from "next/dynamic";
-
-const Editor = dynamic(
- () => import("@monaco-editor/react").then((mod) => mod.Editor),
- { ssr: false }
-);
-
-const LANGUAGE_MAP = {
- js: "javascript",
- ts: "typescript",
- html: "html",
- css: "css",
- json: "json",
- txt: "text",
-};
-
-export function AppEditorMonacoEditor({
- setShowSaveChanges,
-}: {
- setShowSaveChanges: (show: boolean) => void;
-}) {
- const { theme } = useTheme();
- const { code, updateCode } = useActiveCode();
- const { sandpack } = useSandpack();
- const queryClient = useQueryClient();
- const updateTimeoutRef = useRef(null);
-
- const handleEditorDidMount = (monaco: Monaco) => {
- monaco.editor.defineTheme("NightLight", {
- base: "vs",
- inherit: true,
- ...NightLight,
- rules: [],
- });
- monaco.editor.defineTheme("Night", {
- base: "vs-dark",
- ...Night,
- rules: [],
- });
- };
-
- const language = useMemo(() => {
- const extension = sandpack.activeFile
- .split(".")
- .pop()
- ?.toLowerCase() as string;
- return LANGUAGE_MAP[extension as keyof typeof LANGUAGE_MAP] ?? "text";
- }, [sandpack.activeFile]);
-
- const updateFile = useCallback(
- (newValue: string, activeFile: string) => {
- if (updateTimeoutRef.current) {
- clearTimeout(updateTimeoutRef.current);
- }
-
- updateTimeoutRef.current = setTimeout(() => {
- const manuallyUpdatedFiles =
- queryClient.getQueryData(["manuallyUpdatedFiles"]) ?? [];
- const fileIndex = manuallyUpdatedFiles.findIndex(
- (file) => file.path === activeFile
- );
- if (fileIndex !== -1) {
- manuallyUpdatedFiles[fileIndex].content = newValue;
- } else {
- manuallyUpdatedFiles.push({
- path: activeFile,
- content: newValue,
- });
- }
- queryClient.setQueryData(
- ["manuallyUpdatedFiles"],
- manuallyUpdatedFiles
- );
- }, 100);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [queryClient]
- );
-
- const handleEditorChange = (value: string | undefined) => {
- setShowSaveChanges(true);
- const newValue = value || "";
- updateCode(newValue);
- const activeFile = sandpack.activeFile?.replace(/^\//, "");
- updateFile(newValue, activeFile);
- };
-
- useEffect(() => {
- return () => {
- if (updateTimeoutRef.current) {
- clearTimeout(updateTimeoutRef.current);
- }
- };
- }, []);
-
- const themeEditor = useMemo(() => {
- const isSystemDark =
- window.matchMedia &&
- window.matchMedia("(prefers-color-scheme: dark)").matches;
- const effectiveTheme =
- theme === "system" ? (isSystemDark ? "dark" : "light") : theme;
- return effectiveTheme === "dark" ? "Night" : "NightLight";
- }, [theme]);
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/components/code/useEditor.ts b/components/code/useEditor.ts
deleted file mode 100644
index 06ed500729a17413283a42c8f9bf395f255b1518..0000000000000000000000000000000000000000
--- a/components/code/useEditor.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useProject } from "@/components/projects/useProject";
-import { useMemo } from "react";
-
-const LANGUAGE_MAP = {
- "py": "python",
- "js": "javascript",
- "ts": "typescript",
- "html": "html",
- "css": "css",
- "json": "json",
- "txt": "text",
-}
-
-export const useEditor = () => {
- const queryClient = useQueryClient();
- const { files } = useProject();
-
- const { data: isFileListOpen } = useQuery({
- queryKey: ["isFileListOpen"],
- queryFn: () => {
- return false;
- },
- initialData: false,
- refetchInterval: false,
- refetchOnWindowFocus: false,
- });
-
- const setIsFileListOpen = (isOpen: boolean) => {
- queryClient.setQueryData(["isFileListOpen"], () => isOpen);
- }
-
- const { data: editorFilePath } = useQuery({
- queryKey: ["editorFile"],
- queryFn: () => {
- return "app.py";
- },
- initialData: "app.py",
- refetchInterval: false,
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchIntervalInBackground: false,
- });
-
- const setEditorFilePath = (path: string) => {
- queryClient.setQueryData(["editorFile"], () => path);
- }
-
- const editorFileData = useMemo(() => {
- const finalFile = { path: "", content: "", language: "" };
- if (editorFilePath) {
- const file = files?.find((file) => file.path === editorFilePath);
- if (file) {
- finalFile.path = file.path;
- finalFile.content = file.content ?? "";
- finalFile.language = LANGUAGE_MAP[file.path.split(".").pop()?.toLowerCase() as keyof typeof LANGUAGE_MAP] ?? "text";
- }
- }
-
- return finalFile
- }, [editorFilePath, files])
-
- return {
- editorFilePath,
- editorFileData,
- setEditorFilePath,
- isFileListOpen,
- setIsFileListOpen,
- }
-}
\ No newline at end of file
diff --git a/components/contexts/app-context.tsx b/components/contexts/app-context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b5fdbca49aaf1b2a2ac866efa13014eb667f3fb7
--- /dev/null
+++ b/components/contexts/app-context.tsx
@@ -0,0 +1,52 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+import { useMount } from "react-use";
+import { toast } from "sonner";
+import { usePathname, useRouter } from "next/navigation";
+
+import { useUser } from "@/hooks/useUser";
+import { ProjectType, User } from "@/types";
+import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
+
+export default function AppContext({
+ children,
+}: // me: initialData,
+{
+ children: React.ReactNode;
+ // me?: {
+ // user: User | null;
+ // projects: ProjectType[];
+ // errCode: number | null;
+ // };
+}) {
+ const { loginFromCode, user, logout, loading, errCode } = useUser();
+ const pathname = usePathname();
+ const router = useRouter();
+
+ // useMount(() => {
+ // if (!initialData?.user && !user) {
+ // if ([401, 403].includes(errCode as number)) {
+ // logout();
+ // } else if (pathname.includes("/spaces")) {
+ // if (errCode) {
+ // toast.error("An error occured while trying to log in");
+ // }
+ // // If we did not manage to log in (probs because api is down), we simply redirect to the home page
+ // router.push("/");
+ // }
+ // }
+ // });
+
+ const events: any = {};
+
+ useBroadcastChannel("auth", (message) => {
+ if (pathname.includes("/auth/callback")) return;
+
+ if (!message.code) return;
+ if (message.type === "user-oauth" && message?.code && !events.code) {
+ loginFromCode(message.code);
+ }
+ });
+
+ return children;
+}
diff --git a/components/contexts/login-context.tsx b/components/contexts/login-context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2aa4842b55a7f878bc75aa39a71d7b0d68e83945
--- /dev/null
+++ b/components/contexts/login-context.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import React, { createContext, useContext, useState, ReactNode } from "react";
+import { LoginModal } from "@/components/login-modal";
+import { Page } from "@/types";
+
+interface LoginContextType {
+ isOpen: boolean;
+ openLoginModal: (options?: LoginModalOptions) => void;
+ closeLoginModal: () => void;
+}
+
+interface LoginModalOptions {
+ pages?: Page[];
+ title?: string;
+ prompt?: string;
+ description?: string;
+}
+
+const LoginContext = createContext(undefined);
+
+export function LoginProvider({ children }: { children: ReactNode }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [modalOptions, setModalOptions] = useState({});
+
+ const openLoginModal = (options: LoginModalOptions = {}) => {
+ setModalOptions(options);
+ setIsOpen(true);
+ };
+
+ const closeLoginModal = () => {
+ setIsOpen(false);
+ setModalOptions({});
+ };
+
+ const value = {
+ isOpen,
+ openLoginModal,
+ closeLoginModal,
+ };
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+export function useLoginModal() {
+ const context = useContext(LoginContext);
+ if (context === undefined) {
+ throw new Error("useLoginModal must be used within a LoginProvider");
+ }
+ return context;
+}
diff --git a/components/contexts/pro-context.tsx b/components/contexts/pro-context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ec7c0fc0443594232efe097d1c4e7dec455eeb0d
--- /dev/null
+++ b/components/contexts/pro-context.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import React, { createContext, useContext, useState, ReactNode } from "react";
+import { ProModal } from "@/components/pro-modal";
+import { Page } from "@/types";
+import { useEditor } from "@/hooks/useEditor";
+
+interface ProContextType {
+ isOpen: boolean;
+ openProModal: (pages: Page[]) => void;
+ closeProModal: () => void;
+}
+
+const ProContext = createContext(undefined);
+
+export function ProProvider({ children }: { children: ReactNode }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const { pages } = useEditor();
+
+ const openProModal = () => {
+ setIsOpen(true);
+ };
+
+ const closeProModal = () => {
+ setIsOpen(false);
+ };
+
+ const value = {
+ isOpen,
+ openProModal,
+ closeProModal,
+ };
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+export function useProModal() {
+ const context = useContext(ProContext);
+ if (context === undefined) {
+ throw new Error("useProModal must be used within a ProProvider");
+ }
+ return context;
+}
diff --git a/components/providers/react-query.tsx b/components/contexts/tanstack-query-context.tsx
similarity index 50%
rename from components/providers/react-query.tsx
rename to components/contexts/tanstack-query-context.tsx
index 70f1710fbfb56a37bfe8f6fd888a8df1690acefc..d0c214de91a6ce2a9711eb70e96e79152ddee4bb 100644
--- a/components/providers/react-query.tsx
+++ b/components/contexts/tanstack-query-context.tsx
@@ -1,19 +1,30 @@
"use client";
-import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import { useState } from "react";
-export function ReactQueryProvider({
+export default function TanstackContext({
children,
}: {
children: React.ReactNode;
}) {
- const [queryClient] = useState(() => new QueryClient());
+ // Create QueryClient instance only once using useState with a function
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60 * 1000, // 1 minute
+ refetchOnWindowFocus: false,
+ },
+ },
+ })
+ );
+
return (
- {children}
+ {children}
);
diff --git a/components/contexts/user-context.tsx b/components/contexts/user-context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8a3391744618bfcfc979401cdee76051c70fee8f
--- /dev/null
+++ b/components/contexts/user-context.tsx
@@ -0,0 +1,8 @@
+"use client";
+
+import { createContext } from "react";
+import { User } from "@/types";
+
+export const UserContext = createContext({
+ user: undefined as User | undefined,
+});
diff --git a/components/discord-promo-modal/index.tsx b/components/discord-promo-modal/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..83f15c11859473dfc50ceb34e76a58da7384c22b
--- /dev/null
+++ b/components/discord-promo-modal/index.tsx
@@ -0,0 +1,225 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useLocalStorage } from "react-use";
+import Image from "next/image";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
+import { DiscordIcon } from "@/components/icons/discord";
+import Logo from "@/assets/logo.svg";
+
+const DISCORD_PROMO_KEY = "discord-promo-dismissed";
+const DISCORD_URL = "https://discord.gg/KpanwM3vXa";
+
+const Sparkle = ({
+ size = "w-3 h-3",
+ delay = "0s",
+ top = "20%",
+ left = "20%",
+}: {
+ size?: string;
+ delay?: string;
+ top?: string;
+ left?: string;
+}) => (
+
+);
+
+export const DiscordPromoModal = () => {
+ const [open, setOpen] = useState(false);
+ const [dismissed, setDismissed] = useLocalStorage(
+ DISCORD_PROMO_KEY,
+ false
+ );
+
+ useEffect(() => {
+ const cookieDismissed = document.cookie
+ .split("; ")
+ .find((row) => row.startsWith(`${DISCORD_PROMO_KEY}=`))
+ ?.split("=")[1];
+
+ if (dismissed || cookieDismissed === "true") {
+ return;
+ }
+
+ const timer = setTimeout(() => {
+ setOpen(true);
+ }, 60000);
+
+ return () => clearTimeout(timer);
+ }, [dismissed]);
+
+ const handleClose = () => {
+ setOpen(false);
+ setDismissed(true);
+
+ const expiryDate = new Date();
+ expiryDate.setDate(expiryDate.getDate() + 5);
+ document.cookie = `${DISCORD_PROMO_KEY}=true; expires=${expiryDate.toUTCString()}; path=/; SameSite=Lax`;
+ };
+
+ const handleJoinDiscord = () => {
+ window.open(DISCORD_URL, "_blank");
+ handleClose();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready to level up your DeepSite experience?
+
+
+ Get help, share your projects and ask for suggestions!
+
+
+
+
+ {[
+ "Get exclusive preview to new features",
+ "Share your projects and get feedback",
+ "Priority support from the team",
+ "Enjoy real-time updates",
+ ].map((benefit, index) => (
+
+ ))}
+
+
+ {/* CTA Button */}
+
+
+
+ Join Discord Community
+
+
+
+ Free to join. Connect instantly.
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/editor/ask-ai/context.tsx b/components/editor/ask-ai/context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3f852d6240cc4cbe0eedc9c647ed8b1ada118de3
--- /dev/null
+++ b/components/editor/ask-ai/context.tsx
@@ -0,0 +1,128 @@
+import { useState, useMemo } from "react";
+import { FileCode, FileText, Braces, AtSign } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { useEditor } from "@/hooks/useEditor";
+import { useAi } from "@/hooks/useAi";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import classNames from "classnames";
+
+export const Context = () => {
+ const { pages, currentPage, globalEditorLoading } = useEditor();
+ const { contextFile, setContextFile, globalAiLoading } = useAi();
+ const [open, setOpen] = useState(false);
+
+ const selectedFile = contextFile || null;
+
+ const getFileIcon = (filePath: string, size = "size-3.5") => {
+ if (filePath.endsWith(".css")) {
+ return ;
+ } else if (filePath.endsWith(".js")) {
+ return ;
+ } else if (filePath.endsWith(".json")) {
+ return ;
+ } else {
+ return ;
+ }
+ };
+
+ const buttonContent = useMemo(() => {
+ if (selectedFile) {
+ return (
+ <>
+ {selectedFile}
+ >
+ );
+ }
+ return <>Add Context>;
+ }, [selectedFile]);
+
+ return (
+
+
+
+
+
+ {buttonContent}
+
+
+
+
+ Select a file to send as context
+
+
+
+ {pages.length === 0 ? (
+
+ No files available
+
+ ) : (
+ <>
+
{
+ setContextFile(null);
+ setOpen(false);
+ }}
+ className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors ${
+ !selectedFile
+ ? "bg-neutral-800 text-neutral-200 font-medium"
+ : "text-neutral-400 hover:text-neutral-200"
+ }`}
+ >
+ All files (default)
+
+ {pages.map((page) => (
+
{
+ setContextFile(page.path);
+ setOpen(false);
+ }}
+ className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors flex items-center gap-1.5 ${
+ selectedFile === page.path
+ ? "bg-neutral-800 text-neutral-200 font-medium"
+ : "text-neutral-400 hover:text-neutral-200"
+ }`}
+ >
+
+ {getFileIcon(page.path, "size-3")}
+
+ {page.path}
+ {page.path === currentPage && (
+
+ (current)
+
+ )}
+
+ ))}
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/components/editor/ask-ai/fake-ask.tsx b/components/editor/ask-ai/fake-ask.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..41962ca7644fbd674870ed9adb16553bffdfa5a3
--- /dev/null
+++ b/components/editor/ask-ai/fake-ask.tsx
@@ -0,0 +1,97 @@
+import { useState } from "react";
+import { useLocalStorage } from "react-use";
+import { ArrowUp, Dice6 } from "lucide-react";
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/components/ui/button";
+import { PromptBuilder } from "./prompt-builder";
+import { EnhancedSettings } from "@/types";
+import { Settings } from "./settings";
+import classNames from "classnames";
+import { PROMPTS_FOR_AI } from "@/lib/prompts";
+
+export const FakeAskAi = () => {
+ const router = useRouter();
+ const [prompt, setPrompt] = useState("");
+ const [openProvider, setOpenProvider] = useState(false);
+ const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
+ useLocalStorage("deepsite-enhancedSettings", {
+ isActive: true,
+ primaryColor: undefined,
+ secondaryColor: undefined,
+ theme: undefined,
+ });
+ const [, setPromptStorage] = useLocalStorage("prompt", "");
+ const [randomPromptLoading, setRandomPromptLoading] = useState(false);
+
+ const callAi = async () => {
+ setPromptStorage(prompt);
+ router.push("/new");
+ };
+
+ const randomPrompt = () => {
+ setRandomPromptLoading(true);
+ setTimeout(() => {
+ setPrompt(
+ PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
+ );
+ setRandomPromptLoading(false);
+ }, 400);
+ };
+
+ return (
+
+ );
+};
diff --git a/components/editor/ask-ai/index.tsx b/components/editor/ask-ai/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e5fcc9095419569a545be3d9577d7dba1802f03c
--- /dev/null
+++ b/components/editor/ask-ai/index.tsx
@@ -0,0 +1,353 @@
+import { useRef, useState } from "react";
+import classNames from "classnames";
+import { ArrowUp, ChevronDown, CircleStop, Dice6 } from "lucide-react";
+import { useLocalStorage, useUpdateEffect, useMount } from "react-use";
+import { toast } from "sonner";
+
+import { useAi } from "@/hooks/useAi";
+import { useEditor } from "@/hooks/useEditor";
+import { EnhancedSettings, Project } from "@/types";
+import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
+import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
+import { AiLoading } from "@/components/editor/ask-ai/loading";
+import { Button } from "@/components/ui/button";
+import { Uploader } from "@/components/editor/ask-ai/uploader";
+import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
+import { Selector } from "@/components/editor/ask-ai/selector";
+import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
+import { Context } from "@/components/editor/ask-ai/context";
+import { useUser } from "@/hooks/useUser";
+import { useLoginModal } from "@/components/contexts/login-context";
+import { Settings } from "./settings";
+import { useProModal } from "@/components/contexts/pro-context";
+import { MAX_FREE_PROJECTS } from "@/lib/utils";
+import { PROMPTS_FOR_AI } from "@/lib/prompts";
+import { SelectedRedesignUrl } from "./selected-redesign-url";
+
+export const AskAi = ({
+ project,
+ isNew,
+ onScrollToBottom,
+}: {
+ project?: Project;
+ files?: string[];
+ isNew?: boolean;
+ onScrollToBottom?: () => void;
+}) => {
+ const { user, projects } = useUser();
+ const { isSameHtml, isUploading, pages, isLoadingProject } = useEditor();
+ const {
+ isAiWorking,
+ isThinking,
+ thinkingContent,
+ selectedFiles,
+ setSelectedFiles,
+ selectedElement,
+ setSelectedElement,
+ callAiNewProject,
+ callAiFollowUp,
+ audio: hookAudio,
+ cancelRequest,
+ } = useAi(onScrollToBottom);
+ const { openLoginModal } = useLoginModal();
+ const { openProModal } = useProModal();
+ const [openProvider, setOpenProvider] = useState(false);
+ const [providerError, setProviderError] = useState("");
+ const [redesignData, setRedesignData] = useState<
+ undefined | { markdown: string; url: string }
+ >(undefined);
+ const refThink = useRef(null);
+
+ const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
+ useLocalStorage("deepsite-enhancedSettings", {
+ isActive: false,
+ primaryColor: undefined,
+ secondaryColor: undefined,
+ theme: undefined,
+ });
+ const [promptStorage, , removePromptStorage] = useLocalStorage("prompt", "");
+
+ const [isFollowUp, setIsFollowUp] = useState(true);
+ const [prompt, setPrompt] = useState(
+ promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
+ );
+ const [openThink, setOpenThink] = useState(false);
+ const [randomPromptLoading, setRandomPromptLoading] = useState(false);
+
+ useMount(() => {
+ if (promptStorage && promptStorage.trim() !== "") {
+ callAi();
+ }
+ });
+
+ const callAi = async (redesignMarkdown?: string | undefined) => {
+ removePromptStorage();
+ if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
+ return openProModal([]);
+ if (isAiWorking) return;
+ if (!redesignMarkdown && !prompt.trim()) return;
+
+ if (isFollowUp && !redesignMarkdown && !isSameHtml) {
+ if (!user) return openLoginModal({ prompt });
+ const result = await callAiFollowUp(prompt, enhancedSettings, isNew);
+
+ if (result?.error) {
+ handleError(result.error, result.message);
+ return;
+ }
+
+ if (result?.success) {
+ setPrompt("");
+ }
+ } else {
+ const result = await callAiNewProject(
+ prompt,
+ enhancedSettings,
+ redesignMarkdown,
+ !!user,
+ user?.name
+ );
+
+ if (result?.error) {
+ handleError(result.error, result.message);
+ return;
+ }
+
+ if (result?.success) {
+ setPrompt("");
+ }
+ }
+ };
+
+ const handleError = (error: string, message?: string) => {
+ switch (error) {
+ case "login_required":
+ openLoginModal();
+ break;
+ case "provider_required":
+ setOpenProvider(true);
+ setProviderError(message || "");
+ break;
+ case "pro_required":
+ openProModal([]);
+ break;
+ case "api_error":
+ toast.error(message || "An error occurred");
+ break;
+ case "network_error":
+ toast.error(message || "Network error occurred");
+ break;
+ default:
+ toast.error("An unexpected error occurred");
+ }
+ };
+
+ useUpdateEffect(() => {
+ if (refThink.current) {
+ refThink.current.scrollTop = refThink.current.scrollHeight;
+ }
+ // Auto-open dropdown when thinking content appears
+ if (thinkingContent && isThinking && !openThink) {
+ setOpenThink(true);
+ }
+ // Auto-collapse when thinking is complete
+ if (thinkingContent && !isThinking && openThink) {
+ setOpenThink(false);
+ }
+ }, [thinkingContent, isThinking]);
+
+ const randomPrompt = () => {
+ setRandomPromptLoading(true);
+ setTimeout(() => {
+ setPrompt(
+ PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
+ );
+ setRandomPromptLoading(false);
+ }, 400);
+ };
+
+ return (
+
+
+ {thinkingContent && (
+
+
+
+
+ {thinkingContent}
+
+
+
+ )}
+
+ setSelectedFiles(selectedFiles.filter((f) => f !== file))
+ }
+ />
+ {selectedElement && (
+
+ setSelectedElement(null)}
+ />
+
+ )}
+ {redesignData && (
+
+ setRedesignData(undefined)}
+ />
+
+ )}
+
+ {(isAiWorking || isUploading || isThinking || isLoadingProject) && (
+
+
+ {isAiWorking && (
+
+
+
+ )}
+
+ )}
+
+
+
+ {isNew ? (
+
+ ) : (
+
+ )}
+
+ {!isNew &&
}
+ {isNew && (
+
+ setRedesignData({ markdown: md, url: url })
+ }
+ />
+ )}
+ {!isNew && !isSameHtml && }
+
+
+
callAi(redesignData?.markdown)}
+ >
+
+
+
+
+
+
+
+ Your browser does not support the audio element.
+
+
+ );
+};
diff --git a/components/editor/ask-ai/loading.tsx b/components/editor/ask-ai/loading.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c22740e34cadceb16604dce377d823bf69ecc6ac
--- /dev/null
+++ b/components/editor/ask-ai/loading.tsx
@@ -0,0 +1,68 @@
+"use client";
+import Loading from "@/components/loading";
+import { useState, useEffect } from "react";
+import { useInterval } from "react-use";
+
+const TEXTS = [
+ "Teaching pixels to dance with style...",
+ "AI is having a creative breakthrough...",
+ "Channeling digital vibes into pure code...",
+ "Summoning the website spirits...",
+ "Brewing some algorithmic magic...",
+ "Composing a symphony of divs and spans...",
+ "Riding the wave of computational creativity...",
+ "Aligning the stars for perfect design...",
+ "Training circus animals to write CSS...",
+ "Launching ideas into the digital stratosphere...",
+];
+
+export const AiLoading = ({
+ text,
+ className,
+}: {
+ text?: string;
+ className?: string;
+}) => {
+ const [selectedText, setSelectedText] = useState(
+ text ?? TEXTS[0] // Start with first text to avoid hydration issues
+ );
+
+ // Set random text on client-side only to avoid hydration mismatch
+ useEffect(() => {
+ if (!text) {
+ setSelectedText(TEXTS[Math.floor(Math.random() * TEXTS.length)]);
+ }
+ }, [text]);
+
+ useInterval(() => {
+ if (!text) {
+ if (selectedText === TEXTS[TEXTS.length - 1]) {
+ setSelectedText(TEXTS[0]);
+ } else {
+ setSelectedText(TEXTS[TEXTS.indexOf(selectedText) + 1]);
+ }
+ }
+ }, 12000);
+ return (
+
+
+
+
+ {selectedText.split("").map((char, index) => (
+
+ {char === " " ? "\u00A0" : char}
+
+ ))}
+
+
+
+ );
+};
diff --git a/components/editor/ask-ai/prompt-builder/content-modal.tsx b/components/editor/ask-ai/prompt-builder/content-modal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..19f4340bc7f743a09d4b1ee96a5e9514d068ba87
--- /dev/null
+++ b/components/editor/ask-ai/prompt-builder/content-modal.tsx
@@ -0,0 +1,196 @@
+import classNames from "classnames";
+import { ChevronRight, RefreshCcw } from "lucide-react";
+import { useState } from "react";
+import { TailwindColors } from "./tailwind-colors";
+import { Switch } from "@/components/ui/switch";
+import { Button } from "@/components/ui/button";
+import { Themes } from "./themes";
+import { EnhancedSettings } from "@/types";
+
+export const ContentModal = ({
+ enhancedSettings,
+ setEnhancedSettings,
+}: {
+ enhancedSettings: EnhancedSettings;
+ setEnhancedSettings: (settings: EnhancedSettings) => void;
+}) => {
+ const [collapsed, setCollapsed] = useState(["colors", "theme"]);
+ return (
+
+
+
+
+ Allow DeepSite to enhance your prompt
+
+
+ setEnhancedSettings({
+ ...enhancedSettings,
+ isActive: !enhancedSettings.isActive,
+ })
+ }
+ />
+
+
+ While using DeepSite enhanced prompt, you'll get better results. We'll
+ add more details and features to your request.
+
+
+
+ You can also use the custom properties below to set specific
+ information.
+
+
+
+
+
+ setCollapsed((prev) => {
+ if (prev.includes("colors")) {
+ return prev.filter((item) => item !== "colors");
+ }
+ return [...prev, "colors"];
+ })
+ }
+ >
+
+
Colors
+
+ {collapsed.includes("colors") && (
+
+
+
+
+ Primary Color
+
+
+ setEnhancedSettings({
+ ...enhancedSettings,
+ primaryColor: undefined,
+ })
+ }
+ >
+
+ Reset
+
+
+
+
+ setEnhancedSettings({
+ ...enhancedSettings,
+ primaryColor: value,
+ })
+ }
+ />
+
+
+
+
+
+ Secondary Color
+
+
+ setEnhancedSettings({
+ ...enhancedSettings,
+ secondaryColor: undefined,
+ })
+ }
+ >
+
+ Reset
+
+
+
+
+ setEnhancedSettings({
+ ...enhancedSettings,
+ secondaryColor: value,
+ })
+ }
+ />
+
+
+
+ )}
+
+
+
+ setCollapsed((prev) => {
+ if (prev.includes("theme")) {
+ return prev.filter((item) => item !== "theme");
+ }
+ return [...prev, "theme"];
+ })
+ }
+ >
+
+
Theme
+
+ {collapsed.includes("theme") && (
+
+
+
+ Theme
+
+
+ setEnhancedSettings({
+ ...enhancedSettings,
+ theme: undefined,
+ })
+ }
+ >
+
+ Reset
+
+
+
+
+ setEnhancedSettings({
+ ...enhancedSettings,
+ theme: value,
+ })
+ }
+ />
+
+
+ )}
+
+
+ );
+};
diff --git a/components/editor/ask-ai/prompt-builder/index.tsx b/components/editor/ask-ai/prompt-builder/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f59e8f8a95f47bba52520923f7f86e6e425a39f9
--- /dev/null
+++ b/components/editor/ask-ai/prompt-builder/index.tsx
@@ -0,0 +1,68 @@
+import { useState } from "react";
+import { WandSparkles } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { useEditor } from "@/hooks/useEditor";
+import { useAi } from "@/hooks/useAi";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { ContentModal } from "./content-modal";
+import { EnhancedSettings } from "@/types";
+
+export const PromptBuilder = ({
+ enhancedSettings,
+ setEnhancedSettings,
+}: {
+ enhancedSettings: EnhancedSettings;
+ setEnhancedSettings: (settings: EnhancedSettings) => void;
+}) => {
+ const { globalAiLoading } = useAi();
+ const { globalEditorLoading } = useEditor();
+
+ const [open, setOpen] = useState(false);
+ return (
+ <>
+ {
+ setOpen(true);
+ }}
+ >
+
+
+ Enhance
+
+
+ setOpen(false)}>
+
+
+
+
+
+
+ setOpen(false)}
+ >
+ Close
+
+
+
+
+ >
+ );
+};
diff --git a/components/editor/ask-ai/prompt-builder/tailwind-colors.tsx b/components/editor/ask-ai/prompt-builder/tailwind-colors.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..70a2ab847cdeabb50887a29a03b831eb3cc65114
--- /dev/null
+++ b/components/editor/ask-ai/prompt-builder/tailwind-colors.tsx
@@ -0,0 +1,58 @@
+import classNames from "classnames";
+import { useRef } from "react";
+
+import { TAILWIND_COLORS } from "@/lib/prompt-builder";
+import { useMount } from "react-use";
+
+export const TailwindColors = ({
+ value,
+ onChange,
+}: {
+ value: string | undefined;
+ onChange: (value: string) => void;
+}) => {
+ const ref = useRef(null);
+
+ useMount(() => {
+ if (ref.current) {
+ if (value) {
+ const color = ref.current.querySelector(`[data-color="${value}"]`);
+ if (color) {
+ color.scrollIntoView({ inline: "center" });
+ }
+ }
+ }
+ });
+ return (
+
+ {TAILWIND_COLORS.map((color) => (
+
onChange(color)}
+ >
+
+
+ {color}
+
+
+ ))}
+
+ );
+};
diff --git a/components/editor/ask-ai/prompt-builder/themes.tsx b/components/editor/ask-ai/prompt-builder/themes.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b419e8618153c8fd467dd3018fd521ac8930d487
--- /dev/null
+++ b/components/editor/ask-ai/prompt-builder/themes.tsx
@@ -0,0 +1,48 @@
+import { Theme } from "@/types";
+import classNames from "classnames";
+import { Moon, Sun } from "lucide-react";
+import { useRef } from "react";
+
+export const Themes = ({
+ value,
+ onChange,
+}: {
+ value: Theme;
+ onChange: (value: Theme) => void;
+}) => {
+ const ref = useRef(null);
+
+ return (
+
+
onChange("light")}
+ >
+
+
Light
+
+
onChange("dark")}
+ >
+
+
Dark
+
+
+ );
+};
diff --git a/components/ask-ai/redesign.tsx b/components/editor/ask-ai/re-imagine.tsx
similarity index 68%
rename from components/ask-ai/redesign.tsx
rename to components/editor/ask-ai/re-imagine.tsx
index 3cddd074da060741f6d413c5fdc31c9dba86c460..9531db25a30e4eabb3e17f1b399eab7b32f64d67 100644
--- a/components/ask-ai/redesign.tsx
+++ b/components/editor/ask-ai/re-imagine.tsx
@@ -10,8 +10,12 @@ import {
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import Loading from "@/components/loading";
+import { api } from "@/lib/api";
+import { useAi } from "@/hooks/useAi";
+import { useEditor } from "@/hooks/useEditor";
+import classNames from "classnames";
-export function Redesign({
+export function ReImagine({
onRedesign,
}: {
onRedesign: (md: string, url: string) => void;
@@ -19,7 +23,8 @@ export function Redesign({
const [url, setUrl] = useState("");
const [open, setOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
+ const { globalAiLoading } = useAi();
+ const { globalEditorLoading } = useEditor();
const checkIfUrlIsValid = (url: string) => {
const urlPattern = new RegExp(
@@ -30,8 +35,7 @@ export function Redesign({
};
const handleClick = async () => {
- setError(null);
- if (isLoading) return;
+ if (isLoading) return; // Prevent multiple clicks while loading
if (!url) {
toast.error("Please enter a URL.");
return;
@@ -41,24 +45,16 @@ export function Redesign({
return;
}
setIsLoading(true);
- const response = await fetch("/api/redesign", {
- method: "PUT",
- body: JSON.stringify({ url: url.trim() }),
- headers: {
- "Content-Type": "application/json",
- },
- })
- .then(async (response) => {
- if (response?.ok) {
- const data = await response.json();
- return data;
- }
- })
- .catch((error) => {
- setError(error.message);
- });
- if (response.ok) {
- onRedesign(response.markdown, url);
+ const response = await api.put("/re-design", {
+ url: url.trim(),
+ });
+ if (response?.data?.ok) {
+ setOpen(false);
+ onRedesign(response.data.markdown, url.trim());
+ setUrl("");
+ toast.success("DeepSite is redesigning your site! Let him cook... π₯");
+ } else {
+ toast.error(response?.data?.error || "Failed to redesign the site.");
}
setIsLoading(false);
};
@@ -69,9 +65,9 @@ export function Redesign({
Redesign
@@ -79,9 +75,9 @@ export function Redesign({
- ")) {
+ modifiedHtml = modifiedHtml + allJsonContent;
+ } else {
+ modifiedHtml = modifiedHtml + "\n" + allJsonContent;
+ }
+
+ jsonFiles.forEach((file) => {
+ const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ modifiedHtml = modifiedHtml.replace(
+ new RegExp(
+ ``
+ );
+ } 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.
+
+
+
+
+ Refresh
+
+
+ )}
+
+ );
+};
diff --git a/components/editor/project-settings.tsx b/components/editor/project-settings.tsx
deleted file mode 100644
index 67c09ccea881f078a6db95a51d0671c471d9c08a..0000000000000000000000000000000000000000
--- a/components/editor/project-settings.tsx
+++ /dev/null
@@ -1,276 +0,0 @@
-import { useState } from "react";
-import Image from "next/image";
-import Link from "next/link";
-import { useTheme } from "next-themes";
-import { Settings, Check, Plus, Download } from "lucide-react";
-import { RiContrastFill } from "react-icons/ri";
-import { useSession } from "next-auth/react";
-import { useParams } from "next/navigation";
-import { ChevronDown, ChevronLeft, Edit } from "lucide-react";
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuPortal,
- DropdownMenuSeparator,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { ProjectWithCommits } from "@/actions/projects";
-import ProIcon from "@/assets/pro.svg";
-import { Input } from "@/components/ui/input";
-import { useQueryClient } from "@tanstack/react-query";
-import Loading from "@/components/loading";
-import { toast } from "sonner";
-
-export const ProjectSettings = ({
- project,
-}: {
- project?: ProjectWithCommits | null;
-}) => {
- const queryClient = useQueryClient();
- const { repoId, owner } = useParams();
- const { theme, setTheme } = useTheme();
- const { data: session } = useSession();
-
- const [open, setOpen] = useState(false);
- const [projectName, setProjectName] = useState(
- project?.cardData?.title || "New DeepSite website",
- );
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const handleRenameProject = async () => {
- setIsLoading(true);
- setError(null);
- try {
- const response = await fetch(`/api/projects/${repoId}/rename`, {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ newTitle: projectName }),
- }).then(async (response) => {
- if (response.ok) {
- return response.json();
- }
- });
- if (response?.success) {
- setOpen(false);
- queryClient.setQueryData(
- ["project"],
- (oldProject: ProjectWithCommits) => ({
- ...oldProject,
- cardData: {
- ...oldProject.cardData,
- title: projectName,
- },
- }),
- );
- } else {
- setError("Could not rename the project. Please try again.");
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- } catch (err: any) {
- setError(err.message || "An error occurred");
- }
- setIsLoading(false);
- };
-
- const handleDownload = async () => {
- try {
- toast.info("Preparing download...");
- const response = await fetch(`/api/projects/${repoId}/download`, {
- headers: {
- Accept: "application/zip",
- },
- });
- if (!response.ok) {
- throw new Error("Failed to download project");
- }
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = `${session?.user?.username}-${repoId}.zip`;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
- toast.success("Download started!");
- } catch (error) {
- toast.error("Failed to download project");
- }
- };
-
- return (
- <>
-
-
-
- {project?.cardData?.emoji ? (
- {project?.cardData?.emoji}
- ) : (
-
- )}
-
-
- {project?.cardData?.title ?? "New DeepSite website"}{" "}
-
-
- Live preview of your app
-
-
-
-
-
-
-
-
-
- Go to Projects
-
-
-
- {!session?.user?.isPro && (
- <>
-
-
-
- Subscribe to Pro
-
-
-
- >
- )}
-
-
-
- New Project
-
-
- {project && (
- <>
- setOpen(true)}>
-
- Rename the project
-
-
-
-
- Project settings
-
-
- handleDownload()}>
-
- Download project
-
- >
- )}
-
-
-
- Appearance
-
-
-
- setTheme("light")}>
- Light
- {theme === "light" && }
-
- setTheme("dark")}>
- Dark
- {theme === "dark" && }
-
- setTheme("system")}>
- System
- {theme === "system" && }
-
-
-
-
-
-
-
-
-
- Rename your Project
-
- Update the name of your project below.
-
-
-
-
- {error &&
{error}
}
-
- The URL of your project will remain the same after changing the
- name, only the displayed name will be updated.
-
-
setProjectName(e.target.value)}
- />
-
-
-
-
- Cancel
-
- handleRenameProject()}
- >
- {isLoading ? (
- <>
-
- Saving project...
- >
- ) : (
- <>
-
- Update Project name
- >
- )}
-
-
-
-
- >
- );
-};
diff --git a/components/editor/save-changes-popup/index.tsx b/components/editor/save-changes-popup/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..76c9a9153835ce859ccfd64320e4e725ad9e1b1b
--- /dev/null
+++ b/components/editor/save-changes-popup/index.tsx
@@ -0,0 +1,133 @@
+"use client";
+import { useState } from "react";
+import { toast } from "sonner";
+import { Save, X, ChevronUp, ChevronDown } from "lucide-react";
+import { motion, AnimatePresence } from "framer-motion";
+import classNames from "classnames";
+
+import { Page } from "@/types";
+import { api } from "@/lib/api";
+import { Button } from "@/components/ui/button";
+import Loading from "@/components/loading";
+
+interface SaveChangesPopupProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSave: () => Promise;
+ hasUnsavedChanges: boolean;
+ pages: Page[];
+ project?: any;
+}
+
+export const SaveChangesPopup = ({
+ isOpen,
+ onClose,
+ onSave,
+ hasUnsavedChanges,
+ pages,
+ project,
+}: SaveChangesPopupProps) => {
+ const [isSaving, setIsSaving] = useState(false);
+ const [isCollapsed, setIsCollapsed] = useState(false);
+
+ const handleSave = async () => {
+ if (!project || !hasUnsavedChanges) return;
+
+ setIsSaving(true);
+ try {
+ await onSave();
+ toast.success("Changes saved successfully!");
+ onClose();
+ } catch (error: any) {
+ toast.error(error.message || "Failed to save changes");
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ if (!hasUnsavedChanges || !isOpen) return null;
+
+ return (
+
+
+ {isCollapsed ? (
+ // Collapsed state
+
+
+
+ Unsaved Changes
+
+ setIsCollapsed(false)}
+ >
+
+
+
+ ) : (
+ // Expanded state
+
+
+
+
+
+
+
setIsCollapsed(true)}
+ >
+
+
+
+
+ You have unsaved changes in your project. Save them to
+ preserve your work.
+
+
+
+ {isSaving ? (
+
+ ) : (
+
+ )}
+ Save Changes
+
+
+ Later
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/components/editor/switch-devide/index.tsx b/components/editor/switch-devide/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..934e75e0a84674a485c06386ae984818b1401bba
--- /dev/null
+++ b/components/editor/switch-devide/index.tsx
@@ -0,0 +1,39 @@
+import classNames from "classnames";
+import { Laptop, Smartphone } from "lucide-react";
+
+import { useEditor } from "@/hooks/useEditor";
+
+const DEVICES = [
+ {
+ name: "desktop",
+ icon: Laptop,
+ },
+ {
+ name: "mobile",
+ icon: Smartphone,
+ },
+];
+
+export const SwitchDevice = () => {
+ const { device, setDevice } = useEditor();
+ return (
+
+ {DEVICES.map((dvc) => (
+ setDevice(dvc.name)}
+ >
+
+ {dvc.name}
+
+ ))}
+
+ );
+};
diff --git a/components/icons/discord.tsx b/components/icons/discord.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ac42b39459807e623639cf3f74e12a78b0e63778
--- /dev/null
+++ b/components/icons/discord.tsx
@@ -0,0 +1,27 @@
+export const DiscordIcon = ({ className }: { className?: string }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/not-authorized.tsx b/components/iframe-detector/index.tsx
similarity index 57%
rename from components/not-authorized.tsx
rename to components/iframe-detector/index.tsx
index 574e252b5e74f88bd99afaf131160e035b739097..f3c7e7ed0adb3874aec45df5e52a0572daa85326 100644
--- a/components/not-authorized.tsx
+++ b/components/iframe-detector/index.tsx
@@ -1,16 +1,13 @@
"use client";
-import { useState } from "react";
-import Image from "next/image";
-import { Button } from "./ui/button";
-import Link from "next/link";
-import { useMount } from "react-use";
-import { Dialog, DialogContent, DialogTitle } from "./ui/dialog";
+import { useEffect, useState } from "react";
+import IframeWarningModal from "./modal";
-export const NotAuthorizedDomain = () => {
- const [open, setOpen] = useState(false);
+export default function IframeDetector() {
+ const [showWarning, setShowWarning] = useState(false);
- useMount(() => {
+ useEffect(() => {
+ // Helper function to check if a hostname is from allowed domains
const isAllowedDomain = (hostname: string) => {
const host = hostname.toLowerCase();
return (
@@ -19,49 +16,60 @@ export const NotAuthorizedDomain = () => {
host === "huggingface.co" ||
host === "hf.co" ||
host === "enzostvs-deepsite.hf.space" ||
- host === "deepsite.hf.co"
+ host === "huggingface.co/deepsite"
);
};
+ // Check if the current window is in an iframe
const isInIframe = () => {
try {
return window.self !== window.top;
} catch {
+ // If we can't access window.top due to cross-origin restrictions,
+ // we're likely in an iframe
return true;
}
};
+ // Additional check: compare window location with parent location
const isEmbedded = () => {
try {
return window.location !== window.parent.location;
} catch {
+ // Cross-origin iframe
return true;
}
};
+ // SEO: Add canonical URL meta tag when in iframe
const addCanonicalUrl = () => {
+ // Remove existing canonical link if present
const existingCanonical = document.querySelector('link[rel="canonical"]');
if (existingCanonical) {
existingCanonical.remove();
}
+ // Add canonical URL pointing to the standalone version
const canonical = document.createElement("link");
canonical.rel = "canonical";
canonical.href = window.location.href;
document.head.appendChild(canonical);
+ // Add meta tag to indicate this page should not be indexed when in iframe from unauthorized domains
if (isInIframe() || isEmbedded()) {
try {
const parentHostname = document.referrer
? new URL(document.referrer).hostname
: null;
if (parentHostname && !isAllowedDomain(parentHostname)) {
+ // Add noindex meta tag when embedded in unauthorized domains
const noIndexMeta = document.createElement("meta");
noIndexMeta.name = "robots";
noIndexMeta.content = "noindex, nofollow";
document.head.appendChild(noIndexMeta);
}
} catch (error) {
+ // Silently handle cross-origin errors
console.debug(
"SEO: Could not determine parent domain for iframe indexing rules"
);
@@ -69,66 +77,40 @@ export const NotAuthorizedDomain = () => {
}
};
+ // Check if we're in an iframe from a non-allowed domain
const shouldShowWarning = () => {
if (!isInIframe() && !isEmbedded()) {
return false; // Not in an iframe
}
try {
+ // Try to get the parent's hostname
const parentHostname = window.parent.location.hostname;
return !isAllowedDomain(parentHostname);
} catch {
+ // Cross-origin iframe - try to get referrer instead
try {
if (document.referrer) {
const referrerUrl = new URL(document.referrer);
return !isAllowedDomain(referrerUrl.hostname);
}
- } catch {}
+ } catch {
+ // If we can't determine the parent domain, assume it's not allowed
+ }
return true;
}
};
+ // Always add canonical URL for SEO, regardless of iframe status
addCanonicalUrl();
if (shouldShowWarning()) {
- setOpen(true);
+ // Show warning modal instead of redirecting immediately
+ setShowWarning(true);
}
- });
+ }, []);
return (
-
-
-
-
-
-
Access Denied
-
- Unfortunately, you don't have access to DeepSite from this
- domain:{" "}
- {open && (
-
- {window?.location?.hostname ?? "unknown domain"}
-
- )}
- .
-
-
-
-
- Go to DeepSite
-
-
-
-
-
+
);
-};
+}
diff --git a/components/iframe-detector/modal.tsx b/components/iframe-detector/modal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8c5e3f34bc11bd86ae491707f0000db839d25f2d
--- /dev/null
+++ b/components/iframe-detector/modal.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { ExternalLink, AlertTriangle } from "lucide-react";
+
+interface IframeWarningModalProps {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export default function IframeWarningModal({
+ isOpen,
+}: // onOpenChange,
+IframeWarningModalProps) {
+ const handleVisitSite = () => {
+ window.open("https://huggingface.co/deepsite", "_blank");
+ };
+
+ return (
+ {}}>
+
+
+
+
+
Unauthorized Embedding
+
+
+ You're viewing DeepSite through an unauthorized iframe. For the
+ best experience and security, please visit the official website
+ directly.
+
+
+
+
+
Why visit the official site?
+
+ β’ Better performance and security
+ β’ Full functionality access
+ β’ Latest features and updates
+ β’ Proper authentication support
+
+
+
+
+
+
+ Visit huggingface.co/deepsite
+
+
+
+
+ );
+}
diff --git a/components/loading/full-loading.tsx b/components/loading/full-loading.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2ee9c53f9845b3c8b4e9ea073fbacbe26addc7c6
--- /dev/null
+++ b/components/loading/full-loading.tsx
@@ -0,0 +1,12 @@
+import Loading from ".";
+import { AnimatedBlobs } from "../animated-blobs";
+
+export const FullLoading = () => {
+ return (
+
+
+ {/*
Fetching user data...
*/}
+
+
+ );
+};
diff --git a/components/loading/index.tsx b/components/loading/index.tsx
index f812ff62fae2b935ae8016152bda3b7d9403dca9..b55a26b0d0c47033eb70c73960c8e4d028f7f2a0 100644
--- a/components/loading/index.tsx
+++ b/components/loading/index.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils";
+import classNames from "classnames";
function Loading({
overlay = true,
@@ -9,13 +9,13 @@ function Loading({
}) {
return (
>;
+ title?: string;
+ description?: string;
+ prompt?: string;
+}) => {
+ const { openLoginWindow } = useUser();
+ const { pages } = useEditor();
+ const [, setStorage] = useLocalStorage("pages");
+ const [, setPromptStorage] = useLocalStorage("prompt", "");
+ const handleClick = async () => {
+ if (prompt) {
+ setPromptStorage(prompt);
+ }
+ if (pages && !isTheSameHtml(pages[0].html)) {
+ setStorage(pages);
+ }
+ openLoginWindow();
+ onClose(false);
+ };
+ return (
+
+
+
+
+
+
+ πͺ
+
+
+ π
+
+
+ π
+
+
+ {title}
+
+ {description}
+
+
+ Log In to Continue
+
+
+
+
+ );
+};
diff --git a/components/login/login-buttons.tsx b/components/login/login-buttons.tsx
deleted file mode 100644
index 8354ba6530afc6cf87bdec6d25707a936d0fb5a6..0000000000000000000000000000000000000000
--- a/components/login/login-buttons.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-"use client";
-import { signIn } from "next-auth/react";
-import Image from "next/image";
-
-import { Button } from "@/components/ui/button";
-import HFLogo from "@/assets/hf-logo.svg";
-
-export const LoginButtons = ({ callbackUrl }: { callbackUrl: string }) => {
- return (
- signIn("huggingface", { callbackUrl })}>
-
- Sign in with Hugging Face
-
- );
-};
diff --git a/components/grid-pattern/index.tsx b/components/magic-ui/grid-pattern.tsx
similarity index 94%
rename from components/grid-pattern/index.tsx
rename to components/magic-ui/grid-pattern.tsx
index 1006f5253f4f5d749d15c07eb75c81b924c54aae..0ead903d1de84aba97f328e5686f3c631a33cef6 100644
--- a/components/grid-pattern/index.tsx
+++ b/components/magic-ui/grid-pattern.tsx
@@ -1,4 +1,3 @@
-"use client";
import { useId } from "react";
import { cn } from "@/lib/utils";
@@ -29,7 +28,7 @@ export function GridPattern({
;
+ }
+
+ const onDelete = async (project: ProjectType) => {
+ const response = await api.delete(`/me/projects/${project.name}`);
+ if (response.data.ok) {
+ toast.success("Project deleted successfully!");
+ const newProjects = projects.filter((p) => p.id !== project.id);
+ setProjects(newProjects);
+ } else {
+ toast.error(response.data.error);
+ }
+ };
+ return (
+ <>
+
+
+
+ {projects.length < MAX_FREE_PROJECTS || user?.isPro ? (
+
+
+ Create Project
+
+ ) : (
+
openProModal([])}
+ >
+
+ Create Project
+
+ )}
+ {projects.map((project: ProjectType, index: number) => (
+
onDelete(project)}
+ />
+ ))}
+
+
+ >
+ );
+}
diff --git a/components/my-projects/load-project.tsx b/components/my-projects/load-project.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a86a6f2d32095b046d614b2730c2b6d50811d48d
--- /dev/null
+++ b/components/my-projects/load-project.tsx
@@ -0,0 +1,200 @@
+"use client";
+import { useState } from "react";
+import { Import } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import Loading from "@/components/loading";
+import { Input } from "../ui/input";
+import { toast } from "sonner";
+import { api } from "@/lib/api";
+import { useUser } from "@/hooks/useUser";
+import { LoginModal } from "@/components/login-modal";
+import { useRouter } from "next/navigation";
+import { SpaceEntry } from "@huggingface/hub";
+
+export const LoadProject = ({
+ fullXsBtn = false,
+ onSuccess,
+}: {
+ fullXsBtn?: boolean;
+ onSuccess: (project: SpaceEntry) => void;
+}) => {
+ const { user } = useUser();
+ const router = useRouter();
+
+ const [openLoginModal, setOpenLoginModal] = useState(false);
+ const [open, setOpen] = useState(false);
+ const [url, setUrl] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+
+ const checkIfUrlIsValid = (url: string) => {
+ // should match a hugging face spaces URL like: https://huggingface.co/spaces/username/project or https://hf.co/spaces/username/project
+ const urlPattern = new RegExp(
+ /^(https?:\/\/)?(huggingface\.co|hf\.co)\/spaces\/([\w-]+)\/([\w-]+)$/,
+ "i"
+ );
+ return urlPattern.test(url);
+ };
+
+ const handleClick = async () => {
+ if (isLoading) return; // Prevent multiple clicks while loading
+ if (!url) {
+ toast.error("Please enter a URL.");
+ return;
+ }
+ if (!checkIfUrlIsValid(url)) {
+ toast.error("Please enter a valid Hugging Face Spaces URL.");
+ return;
+ }
+
+ const [username, namespace] = url
+ .replace("https://huggingface.co/spaces/", "")
+ .replace("https://hf.co/spaces/", "")
+ .split("/");
+
+ setIsLoading(true);
+ try {
+ const response = await api.post(`/me/projects/${username}/${namespace}`);
+ toast.success("Project imported successfully!");
+ setOpen(false);
+ setUrl("");
+ onSuccess(response.data.project);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ if (error?.response?.data?.redirect) {
+ return router.push(error.response.data.redirect);
+ }
+ toast.error(
+ error?.response?.data?.error ?? "Failed to import the project."
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <>
+ {!user ? (
+ <>
+ setOpenLoginModal(true)}
+ >
+
+ Load existing Project
+
+ setOpenLoginModal(true)}
+ >
+ {fullXsBtn && }
+ Load
+ {fullXsBtn && " existing Project"}
+
+
+ >
+ ) : (
+
+
+
+
+
+ Load existing Project
+
+
+ {fullXsBtn && }
+ Load
+ {fullXsBtn && " existing Project"}
+
+
+
+
+
+
+
+
+
+ Enter your Hugging Face Space
+
+
setUrl(e.target.value)}
+ onBlur={(e) => {
+ const inputUrl = e.target.value.trim();
+ if (!inputUrl) {
+ setUrl("");
+ return;
+ }
+ if (!checkIfUrlIsValid(inputUrl)) {
+ toast.error("Please enter a valid URL.");
+ return;
+ }
+ setUrl(inputUrl);
+ }}
+ className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
+ />
+
+
+
+ Then, let's import it!
+
+
+ {isLoading ? (
+ <>
+
+ Fetching your Space...
+ >
+ ) : (
+ <>Import your Space>
+ )}
+
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/components/my-projects/project-card.tsx b/components/my-projects/project-card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..40d70d3d2412c5813e35dfba96e755d29a36a2ae
--- /dev/null
+++ b/components/my-projects/project-card.tsx
@@ -0,0 +1,186 @@
+import Link from "next/link";
+import { formatDistance } from "date-fns";
+import {
+ Download,
+ EllipsisVertical,
+ Lock,
+ Settings,
+ Trash,
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { ProjectType } from "@/types";
+import { toast } from "sonner";
+import { useUser } from "@/hooks/useUser";
+
+// from-red-500 to-red-500
+// from-yellow-500 to-yellow-500
+// from-green-500 to-green-500
+// from-purple-500 to-purple-500
+// from-blue-500 to-blue-500
+// from-pink-500 to-pink-500
+// from-gray-500 to-gray-500
+// from-indigo-500 to-indigo-500
+
+export function ProjectCard({
+ project,
+ onDelete,
+}: {
+ project: ProjectType;
+ onDelete: () => void;
+}) {
+ const { token } = useUser();
+ const handleDelete = () => {
+ if (
+ confirm(
+ "Are you sure you want to delete this project? This action cannot be undone."
+ )
+ ) {
+ onDelete();
+ }
+ };
+
+ const handleDownload = async () => {
+ try {
+ toast.info("Preparing download...");
+ const response = await fetch(
+ `/deepsite/api/me/projects/${project.name}/download`,
+ {
+ credentials: "include",
+ headers: {
+ Accept: "application/zip",
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response
+ .json()
+ .catch(() => ({ error: "Download failed" }));
+ toast.error(error.error || "Failed to download project");
+ return;
+ }
+
+ const blob = await response.blob();
+
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `${project.name.replace(/\//g, "-")}.zip`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+
+ toast.success("Download started!");
+ } catch (error) {
+ console.error("Download error:", error);
+ toast.error("Failed to download project");
+ }
+ };
+ // from-gray-600 to-gray-600
+ // from-blue-600 to-blue-600
+ // from-green-600 to-green-600
+ // from-yellow-600 to-yellow-600
+ // from-purple-600 to-purple-600
+ // from-pink-600 to-pink-600
+ // from-red-600 to-red-600
+ // from-orange-600 to-orange-600
+
+ return (
+
+
+ {project.private ? (
+
+
+
+
+ Private
+
+
+
{project.cardData?.emoji}
+
+ ) : (
+
+
+
+ )}
+
+
+ Open project
+
+
+
+
+
+ {project?.cardData?.title ?? project.name}
+
+
+ Updated{" "}
+ {formatDistance(
+ new Date(project.updatedAt || Date.now()),
+ new Date(),
+ {
+ addSuffix: true,
+ }
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Project Settings
+
+
+
+
+ Download as ZIP
+
+
+
+ Delete Project
+
+
+
+
+
+
+ );
+}
diff --git a/components/not-found/buttons.tsx b/components/not-found/buttons.tsx
deleted file mode 100644
index a30f436d48121858411d727b457abff9c3bb56e2..0000000000000000000000000000000000000000
--- a/components/not-found/buttons.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-"use client";
-import Link from "next/link";
-import { Button } from "@/components/ui/button";
-
-export function NotFoundButtons() {
- return (
-
-
- Return Home
-
- window.history.back()}>
- Back to previous page
-
-
- );
-}
diff --git a/components/not-logged/not-logged.tsx b/components/not-logged/not-logged.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..330b45cea1064d1e47840eee3f52956441fdf980
--- /dev/null
+++ b/components/not-logged/not-logged.tsx
@@ -0,0 +1,188 @@
+"use client";
+
+import { AnimatedBlobs } from "../animated-blobs";
+import { FakeAskAi } from "../editor/ask-ai/fake-ask";
+
+export const NotLogged = () => {
+ return (
+
+
+
+ β¨ DeepSite v3 is out!
+
+
+ Welcome to DeepSite
+
+
+ Code your website with AI in seconds.
+ Access the most simple and powerful AI Vibe Code Editor to create your
+ next project.
+
+
+
+
+
+
+
+
+ π Powerful Features
+
+
+ Everything you need
+
+
+ Build, deploy, and scale your websites with cutting-edge features
+
+
+
+ {/* Bento Grid */}
+
+ {/* Multi Pages */}
+
+
+
π
+
+ Multi Pages
+
+
+ Create complex websites with multiple interconnected pages.
+ Build everything from simple landing pages to full-featured web
+ applications with dynamic routing and navigation.
+
+
+
+ Dynamic Routing
+
+
+ Navigation
+
+
+ SEO Ready
+
+
+
+
+
+
+ {/* Auto Deploy */}
+
+
+
β‘
+
+ Auto Deploy
+
+
+ Push your changes and watch them go live instantly. No complex
+ CI/CD setup required.
+
+
+
+
+
+ {/* Free Hosting */}
+
+
+
π
+
+ Free Hosting
+
+
+ Host your websites for free with global CDN and lightning-fast
+ performance.
+
+
+
+
+
+ {/* Open Source Models */}
+
+
+
π
+
+ Open Source Models
+
+
+ Powered by cutting-edge open source AI models. Transparent,
+ customizable, and community-driven development.
+
+
+
+ Llama
+
+
+ Mistral
+
+
+ CodeLlama
+
+
+
+
+
+
+ {/* UX Focus */}
+
+
+
β¨
+
+ Perfect UX
+
+
+ Intuitive interface designed for developers and non-developers
+ alike.
+
+
+
+
+
+ {/* Hugging Face Integration */}
+
+
+
π€
+
+ Hugging Face
+
+
+ Seamless integration with Hugging Face models and datasets for
+ cutting-edge AI capabilities.
+
+
+
+
+
+ {/* Performance */}
+
+
+
π
+
+ Blazing Fast
+
+
+ Optimized performance with edge computing and smart caching.
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/pro-modal/index.tsx b/components/pro-modal/index.tsx
index 777cd9a5ce7dc1110ffc810d525e0c4a31adc1f9..8b40e0372131f7c1a148f3f91d33da43321b361e 100644
--- a/components/pro-modal/index.tsx
+++ b/components/pro-modal/index.tsx
@@ -1,15 +1,24 @@
+import { useLocalStorage } from "react-use";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { CheckCheck } from "lucide-react";
+import { isTheSameHtml } from "@/lib/compare-html-diff";
+import { Page } from "@/types";
export const ProModal = ({
open,
+ pages,
onClose,
}: {
open: boolean;
+ pages?: Page[];
onClose: React.Dispatch>;
}) => {
+ const [, setStorage] = useLocalStorage("pages");
const handleProClick = () => {
+ if (pages && !isTheSameHtml(pages?.[0].html)) {
+ setStorage(pages);
+ }
window.open("https://huggingface.co/subscribe/pro?from=DeepSite", "_blank");
onClose(false);
};
@@ -17,10 +26,10 @@ export const ProModal = ({
-
-
-
-
+
+
Upgrade to a Account, and unlock your
DeepSite high quota access β‘
-
-
+
+
You'll also unlock some Hugging Face PRO features, like:
@@ -61,15 +68,15 @@ export const ProModal = ({
Get free credits across all Inference Providers
-
+
... and lots more!
Subscribe to PRO ($9/month)
@@ -96,3 +103,4 @@ export const ProTag = ({
PRO
);
+export default ProModal;
diff --git a/components/projects/big-project-card.tsx b/components/projects/big-project-card.tsx
deleted file mode 100644
index 609a49e187e06bdbe968c9b00b3b96962c9fde23..0000000000000000000000000000000000000000
--- a/components/projects/big-project-card.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import Link from "next/link";
-import { SpaceEntry } from "@huggingface/hub";
-import { format } from "date-fns";
-import {
- CogIcon,
- Download,
- EllipsisVertical,
- ExternalLink,
- TrashIcon,
-} from "lucide-react";
-
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { toast } from "sonner";
-
-// from-red-500 to-red-500
-// from-yellow-500 to-yellow-500
-// from-green-500 to-green-500
-// from-purple-500 to-purple-500
-// from-blue-500 to-blue-500
-// from-pink-500 to-pink-500
-// from-gray-500 to-gray-500
-// from-indigo-500 to-indigo-500
-
-export function BigProjectCard({
- project,
- onOpenDeleteDialog,
-}: {
- project: SpaceEntry;
- onOpenDeleteDialog: (id: string) => void;
-}) {
- const handleDownload = async () => {
- try {
- toast.info("Preparing download...");
- const response = await fetch(
- `/api/projects/${project.name.split("/")[1]}/download`,
- {
- headers: {
- Accept: "application/zip",
- },
- },
- );
- if (!response.ok) {
- throw new Error("Failed to download project");
- }
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = `${project.name.replaceAll("/", "-")}.zip`;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
- toast.success("Download started!");
- } catch (error) {
- toast.error("Failed to download project");
- }
- };
-
- return (
-
-
- {project.private ? (
-
-
{project.cardData?.emoji}
-
- {project.cardData?.title}
-
-
- ) : (
-
-
-
- )}
-
-
-
- Open project
-
-
-
- {project.cardData?.title}
-
-
-
-
- Last update: {format(project.updatedAt, "MMM d, yyyy")}
-
-
-
-
-
-
-
-
- {
- e.preventDefault();
- e.stopPropagation();
- window.open(
- `https://huggingface.co/spaces/${project.name}/settings`,
- "_blank",
- );
- }}
- >
-
- Settings
-
- {
- handleDownload();
- e.preventDefault();
- e.stopPropagation();
- }}
- >
-
- Download
-
- {
- e.preventDefault();
- e.stopPropagation();
- onOpenDeleteDialog(project.name);
- }}
- >
-
- Delete this project
-
-
-
-
-
- );
-}
diff --git a/components/projects/project-card.tsx b/components/projects/project-card.tsx
deleted file mode 100644
index 2112aa66c0c857b3180fd9f7baa65f3586456d76..0000000000000000000000000000000000000000
--- a/components/projects/project-card.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { SpaceEntry } from "@huggingface/hub";
-
-// from-red-500 to-red-500
-// from-yellow-500 to-yellow-500
-// from-green-500 to-green-500
-// from-purple-500 to-purple-500
-// from-blue-500 to-blue-500
-// from-pink-500 to-pink-500
-// from-gray-500 to-gray-500
-// from-indigo-500 to-indigo-500
-
-export function ProjectCard({ project }: { project: SpaceEntry }) {
- return (
-
-
-
- {project?.cardData?.emoji || "π"}
-
-
-
- {project.cardData?.title}
-
-
- {project.name}
-
-
-
-
- );
-}
diff --git a/components/projects/useManualUpdates.ts b/components/projects/useManualUpdates.ts
deleted file mode 100644
index 6d3cdeee36528c027635a85d1183f470c887aa7f..0000000000000000000000000000000000000000
--- a/components/projects/useManualUpdates.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { File } from "@/lib/type";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useMemo } from "react";
-
-export const useManualUpdates = () => {
- const queryClient = useQueryClient();
-
- const { data: manuallyUpdatedFiles } = useQuery({
- queryKey: ["manuallyUpdatedFiles"],
- queryFn: () => {
- return [];
- },
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- refetchIntervalInBackground: false,
- });
- const files = useMemo(() => {
- return queryClient.getQueryData(["files"]) ?? [];
- }, [queryClient]);
-
- const isManuallyUpdatedSameAsFiles = useMemo(() => {
- return manuallyUpdatedFiles?.every((file) =>
- files?.some((f) => f.path === file.path && f.content === file.content)
- );
- }, [manuallyUpdatedFiles, files]);
-
- return { manuallyUpdatedFiles, isManuallyUpdatedSameAsFiles };
-};
diff --git a/components/projects/useProject.ts b/components/projects/useProject.ts
deleted file mode 100644
index 5a8b4f4bcd21d45bfc1eb1bc8ccf184404bbb066..0000000000000000000000000000000000000000
--- a/components/projects/useProject.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useEffect } from "react";
-
-import { getProject, ProjectWithCommits } from "@/actions/projects";
-import { File } from "@/lib/type";
-
-export const useProject = (
- initialProject?: ProjectWithCommits | null,
- initialFiles?: File[] | null,
- isNew?: boolean
-) => {
- const queryClient = useQueryClient();
-
- useEffect(() => {
- if (isNew) {
- queryClient.setQueryData(["project"], null);
- queryClient.setQueryData(["files"], []);
- } else if (initialProject) {
- queryClient.setQueryData(["project"], initialProject);
- if (initialFiles) {
- queryClient.setQueryData(["files"], initialFiles);
- }
- }
- }, [initialProject, initialFiles, isNew, queryClient]);
-
- const {
- data: project,
- isLoading,
- error,
- } = useQuery({
- queryKey: ["project"],
- initialData: initialProject,
- queryFn: async () => {
- if (isNew) return null;
- const datas = await getProject(initialProject?.name as string);
- if (datas?.files) {
- setFiles(datas.files);
- }
- return datas?.project ?? null;
- },
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- refetchIntervalInBackground: false,
- });
-
- const { data: files } = useQuery({
- queryKey: ["files"],
- initialData: initialFiles,
- queryFn: () => {
- return [];
- },
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- refetchIntervalInBackground: false,
- });
-
- const setFiles = (newFiles: File[]) => {
- queryClient.setQueryData(["files"], (oldFiles: File[] = []) => {
- const currentFiles = oldFiles.filter(
- (file) => !newFiles.some((f) => f.path === file.path)
- );
- return [...currentFiles, ...newFiles];
- });
- };
-
- return {
- project,
- isLoading,
- error,
- files,
- };
-};
diff --git a/components/projects/useProjects.ts b/components/projects/useProjects.ts
deleted file mode 100644
index 4b67775c489e76b3b8ffef0fa5929f06f42df896..0000000000000000000000000000000000000000
--- a/components/projects/useProjects.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { getProjects } from "@/actions/projects";
-import { useQuery } from "@tanstack/react-query";
-
-export const useProjects = () => {
- const { data: projects, isLoading, error, refetch } = useQuery({
- queryKey: ["projects"],
- queryFn: () => getProjects(),
- });
- return { projects, isLoading, error, refetch };
-}
\ No newline at end of file
diff --git a/components/projects/user-projects.tsx b/components/projects/user-projects.tsx
deleted file mode 100644
index 1b447dd91a4cbccd6bc00aaa048919b7bf3b57e2..0000000000000000000000000000000000000000
--- a/components/projects/user-projects.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { useState } from "react";
-import { Plus, TrashIcon } from "lucide-react";
-import { useSession } from "next-auth/react";
-
-import { useProjects } from "./useProjects";
-import { Button } from "@/components/ui/button";
-import { BigProjectCard } from "./big-project-card";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { toast } from "sonner";
-
-export function UserProjects() {
- const { data: session } = useSession();
- const { projects, isLoading, error, refetch } = useProjects();
- const [open, setOpen] = useState(false);
- const [deleteProjectId, setDeleteProjectId] = useState(null);
- const [input, setInput] = useState("");
- if (isLoading || error || !session?.user?.name) return null;
-
- const handleDeleteProject = async () => {
- const response = await fetch(
- `/api/projects/${deleteProjectId?.split("/")[1]}`,
- {
- method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- },
- }
- ).then(async (response) => {
- if (response.ok) {
- return response.json();
- }
- });
- if (response?.success) {
- toast.success("Project has been deleted");
- refetch();
- setOpen(false);
- setDeleteProjectId(null);
- setInput("");
- } else {
- toast.error("Failed to delete project");
- }
- };
-
- return (
-
-
-
-
- {projects?.map((project) => (
-
{
- setOpen(true);
- setDeleteProjectId(id);
- }}
- />
- ))}
- {projects?.length === 0 && (
-
-
-
- )}
-
-
-
-
-
- Delete project
-
- This action is irreversible. Are you sure you want to delete this
- project?
-
-
-
-
-
- Type{" "}
-
- {deleteProjectId}
- {" "}
- to confirm deletion
-
-
setInput(e.target.value)}
- />
-
-
-
-
- Cancel
-
- handleDeleteProject()}
- >
-
- Delete project
-
-
-
-
-
- );
-}
diff --git a/components/providers/session.tsx b/components/providers/session.tsx
deleted file mode 100644
index dcc0b7efdac893d0c929fb85b0dd0941ca79fde4..0000000000000000000000000000000000000000
--- a/components/providers/session.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-"use client";
-
-import { SessionProvider } from "next-auth/react";
-
-export function AuthProvider({ children }: { children: React.ReactNode }) {
- return (
- {children}
- );
-}
diff --git a/components/providers/theme.tsx b/components/providers/theme.tsx
deleted file mode 100644
index 189a2b1a12231255b53038166eb92d2d03bd0e9d..0000000000000000000000000000000000000000
--- a/components/providers/theme.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { ThemeProvider as NextThemesProvider } from "next-themes";
-
-export function ThemeProvider({
- children,
- ...props
-}: React.ComponentProps) {
- return {children} ;
-}
diff --git a/components/public/animated-dots-background.tsx b/components/public/animated-dots-background.tsx
deleted file mode 100644
index ed91f8c570942139278117fe7bb82a087b26b1e2..0000000000000000000000000000000000000000
--- a/components/public/animated-dots-background.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-"use client";
-
-import { useEffect, useRef } from "react";
-
-export function AnimatedDotsBackground() {
- const canvasRef = useRef(null);
- const dotsRef = useRef<
- Array<{ x: number; y: number; targetColor: number; currentColor: number }>
- >([]);
- const mouseRef = useRef({ x: -1000, y: -1000 });
- const animationFrameRef = useRef(null);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) return undefined;
-
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
-
- // Helper function to convert OKLCH to RGB
- const oklchToRgb = (
- oklchString: string
- ): { r: number; g: number; b: number } => {
- const temp = document.createElement("div");
- temp.style.color = oklchString;
- document.body.appendChild(temp);
- const computed = window.getComputedStyle(temp).color;
- document.body.removeChild(temp);
-
- // Parse RGB values from the computed color string
- const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
- if (match) {
- return {
- r: parseInt(match[1]),
- g: parseInt(match[2]),
- b: parseInt(match[3]),
- };
- }
- return { r: 200, g: 200, b: 200 }; // Fallback
- };
-
- // Get theme colors from CSS variables
- const getThemeColors = () => {
- const style = getComputedStyle(document.documentElement);
- const mutedFg = style.getPropertyValue("--muted-foreground").trim();
- const primary = style.getPropertyValue("--primary").trim();
-
- return {
- base: oklchToRgb(`oklch(${mutedFg})`),
- accent: oklchToRgb(`oklch(${primary})`),
- };
- };
-
- const colors = getThemeColors();
-
- // Set canvas size
- const updateCanvasSize = () => {
- const rect = canvas.getBoundingClientRect();
- canvas.width = rect.width * window.devicePixelRatio;
- canvas.height = rect.height * window.devicePixelRatio;
- ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
-
- // Reinitialize dots when canvas size changes
- initializeDots();
- };
-
- const initializeDots = () => {
- const rect = canvas.getBoundingClientRect();
- const spacing = 30; // Space between dots
- const cols = Math.ceil(rect.width / spacing);
- const rows = Math.ceil(rect.height / spacing);
-
- dotsRef.current = [];
- for (let i = 0; i < cols; i++) {
- for (let j = 0; j < rows; j++) {
- dotsRef.current.push({
- x: i * spacing + spacing / 2,
- y: j * spacing + spacing / 2,
- targetColor: 0, // 0 = gray, 1 = orange
- currentColor: 0,
- });
- }
- }
- };
-
- updateCanvasSize();
-
- // Handle mouse move
- const handleMouseMove = (e: MouseEvent) => {
- const rect = canvas.getBoundingClientRect();
- mouseRef.current = {
- x: e.clientX - rect.left,
- y: e.clientY - rect.top,
- };
- };
-
- // Handle mouse leave
- const handleMouseLeave = () => {
- mouseRef.current = { x: -1000, y: -1000 };
- };
-
- canvas.addEventListener("mousemove", handleMouseMove);
- canvas.addEventListener("mouseleave", handleMouseLeave);
- window.addEventListener("resize", updateCanvasSize);
-
- // Animation loop
- const animate = () => {
- const rect = canvas.getBoundingClientRect();
- ctx.clearRect(0, 0, rect.width, rect.height);
-
- const hoverRadius = 80; // Distance for hover effect
- const { x: mouseX, y: mouseY } = mouseRef.current;
-
- dotsRef.current.forEach((dot) => {
- // Calculate distance to mouse
- const dx = dot.x - mouseX;
- const dy = dot.y - mouseY;
- const distance = Math.sqrt(dx * dx + dy * dy);
-
- // Update target color based on proximity to mouse
- if (distance < hoverRadius) {
- const intensity = 1 - distance / hoverRadius;
- dot.targetColor = intensity;
- } else {
- dot.targetColor = 0;
- }
-
- // Smooth transition with ease (lerp with easing factor)
- const easeSpeed = 0.1;
- dot.currentColor += (dot.targetColor - dot.currentColor) * easeSpeed;
-
- // Interpolate between base and accent colors from theme
- const r = Math.round(
- colors.base.r + (colors.accent.r - colors.base.r) * dot.currentColor
- );
- const g = Math.round(
- colors.base.g + (colors.accent.g - colors.base.g) * dot.currentColor
- );
- const b = Math.round(
- colors.base.b + (colors.accent.b - colors.base.b) * dot.currentColor
- );
-
- ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
-
- // Draw dot with size based on hover intensity
- const baseSize = 2;
- const hoverSize = 4;
- const size = baseSize + (hoverSize - baseSize) * dot.currentColor;
-
- ctx.beginPath();
- ctx.arc(dot.x, dot.y, size, 0, Math.PI * 2);
- ctx.fill();
- });
-
- animationFrameRef.current = requestAnimationFrame(animate);
- };
-
- animate();
-
- return () => {
- canvas.removeEventListener("mousemove", handleMouseMove);
- canvas.removeEventListener("mouseleave", handleMouseLeave);
- window.removeEventListener("resize", updateCanvasSize);
- if (animationFrameRef.current) {
- cancelAnimationFrame(animationFrameRef.current);
- }
- };
- }, []);
-
- return (
-
- );
-}
diff --git a/components/public/bento.tsx b/components/public/bento.tsx
deleted file mode 100644
index 21aea3c59623c0d0a26dc96b6ebf4870a61795b3..0000000000000000000000000000000000000000
--- a/components/public/bento.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-"use client";
-
-export const Bento = () => {
- return (
-
- {/* Header with fade-in animation */}
-
-
- Powerful Features π
-
-
- Everything you need
-
-
- Build, deploy, and scale your websites with cutting-edge features
-
-
-
- {/* Grid with staggered card animations */}
-
- {/* Multi Pages - Large Card */}
-
-
-
- π
-
-
- Multi Pages
-
-
- Create complex websites with multiple interconnected pages. Build
- everything from simple landing pages to full-featured web
- applications with dynamic routing and navigation.
-
-
-
- Dynamic Routing
-
-
- Navigation
-
-
- SEO Ready
-
-
-
-
-
-
- {/* Auto Deploy */}
-
-
-
- β‘
-
-
- Auto Deploy
-
-
- Push your changes and watch them go live instantly. No complex
- CI/CD setup required.
-
-
-
-
-
- {/* Free Hosting */}
-
-
-
- π
-
-
- Free Hosting
-
-
- Host your websites for free with global CDN and lightning-fast
- performance.
-
-
-
-
-
- {/* Open Source Models */}
-
-
-
- π
-
-
- Open Source Models
-
-
- Powered by cutting-edge open source AI models. Transparent,
- customizable, and community-driven development.
-
-
-
- DeepSeek
-
-
- MiniMax
-
-
- Kimi
-
-
-
-
-
-
- {/* Perfect UX */}
-
-
-
- β¨
-
-
- Perfect UX
-
-
- Intuitive interface designed for developers and non-developers
- alike.
-
-
-
-
-
- {/* Hugging Face */}
-
-
-
- π€
-
-
- Hugging Face
-
-
- Seamless integration with Hugging Face models and datasets for
- cutting-edge AI capabilities.
-
-
-
-
-
- {/* Blazing Fast */}
-
-
-
- π
-
-
- Blazing Fast
-
-
- Optimized performance with edge computing and smart caching.
-
-
-
-
-
-
- );
-};
diff --git a/components/public/hero-header.tsx b/components/public/hero-header.tsx
deleted file mode 100644
index f50710eefe0f5a9c2ab3c1a2d0dee2364f545105..0000000000000000000000000000000000000000
--- a/components/public/hero-header.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-"use client";
-import { motion } from "framer-motion";
-
-const TITLE = [
- "Create",
- "stunning",
- "websites",
- "with",
- "next-gen",
- "AI",
- "tools",
-];
-
-export function HeroHeader() {
- return (
-
-
- DeepSite: v4 is here π
-
-
- {TITLE.map((word, index) => (
-
- {word}
-
- ))}
-
-
- Build, design, and deploy stunning websites in minutes with AI-powered
- development.
-
-
- );
-}
diff --git a/components/public/navigation.tsx b/components/public/navigation.tsx
deleted file mode 100644
index c803023a33a841d2057f0130c40204c04c5c5847..0000000000000000000000000000000000000000
--- a/components/public/navigation.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import Image from "next/image";
-import { UserMenu } from "@/components/user-menu";
-
-export function Navigation() {
- return (
-
-
-
-
-
-
- );
-}
diff --git a/components/public/navigation/index.tsx b/components/public/navigation/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d4f0598ccb8684330b7faa056eea61d4b05a98c6
--- /dev/null
+++ b/components/public/navigation/index.tsx
@@ -0,0 +1,181 @@
+"use client";
+
+import { useRef, useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { useMount, useUnmount } from "react-use";
+import classNames from "classnames";
+import { ArrowRight } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import Logo from "@/assets/logo.svg";
+import { useUser } from "@/hooks/useUser";
+import { UserMenu } from "@/components/user-menu";
+import { ProTag } from "@/components/pro-modal";
+import { DiscordIcon } from "@/components/icons/discord";
+
+const navigationLinks = [
+ {
+ name: "Create Website",
+ href: "/new",
+ },
+ {
+ name: "Features",
+ href: "#features",
+ },
+ {
+ name: "Community",
+ href: "#community",
+ },
+ {
+ name: "Deploy",
+ href: "#deploy",
+ },
+];
+
+export default function Navigation() {
+ const { openLoginWindow, user, loading } = useUser();
+ const [hash, setHash] = useState("");
+
+ const selectorRef = useRef(null);
+ const linksRef = useRef(
+ new Array(navigationLinks.length).fill(null)
+ );
+ const [isScrolled, setIsScrolled] = useState(false);
+
+ useMount(() => {
+ const handleScroll = () => {
+ const scrollTop = window.scrollY;
+ setIsScrolled(scrollTop > 100);
+ };
+
+ const initialHash = window.location.hash;
+ if (initialHash) {
+ setHash(initialHash);
+ calculateSelectorPosition(initialHash);
+ }
+
+ window.addEventListener("scroll", handleScroll);
+ });
+
+ useUnmount(() => {
+ window.removeEventListener("scroll", () => {});
+ });
+
+ const handleClick = (href: string) => {
+ setHash(href);
+ calculateSelectorPosition(href);
+ };
+
+ const calculateSelectorPosition = (href: string) => {
+ if (selectorRef.current && linksRef.current) {
+ const index = navigationLinks.findIndex((l) => l.href === href);
+ const targetLink = linksRef.current[index];
+ if (targetLink) {
+ const targetRect = targetLink.getBoundingClientRect();
+ selectorRef.current.style.left = targetRect.left + "px";
+ selectorRef.current.style.width = targetRect.width + "px";
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+ DeepSite
+
+ {user?.isPro && }
+
+
+ {navigationLinks.map((link) => (
+ {
+ const index = navigationLinks.findIndex(
+ (l) => l.href === link.href
+ );
+ if (el && linksRef.current[index] !== el) {
+ linksRef.current[index] = el;
+ }
+ }}
+ className="inline-block font-sans text-sm"
+ >
+ {
+ handleClick(link.href);
+ }}
+ >
+ {link.name}
+
+
+ ))}
+
+
+
+
+
+
+ Discord Community
+ Discord
+
+
+ {loading ? (
+
+
+
+
+ ) : user ? (
+
+ ) : (
+ <>
+
+ Start Vibe Coding
+
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/components/tour/card.tsx b/components/tour/card.tsx
deleted file mode 100644
index 23f96a04bc6962b3d07b497ea7aa42d8392138be..0000000000000000000000000000000000000000
--- a/components/tour/card.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-"use client";
-
-import React from "react";
-import { Step } from "nextstepjs";
-import { Button } from "@/components/ui/button";
-import { X } from "lucide-react";
-
-interface CustomCardProps {
- step: Step;
- currentStep: number;
- totalSteps: number;
- nextStep: () => void;
- prevStep: () => void;
- skipTour?: () => void;
- arrow: React.ReactNode;
-}
-
-export const TourCustomCard = ({
- step,
- currentStep,
- totalSteps,
- nextStep,
- prevStep,
- skipTour,
- arrow,
-}: CustomCardProps) => {
- return (
-
-
-
- {currentStep > 0 && (
-
- Previous
-
- )}
-
- {currentStep === totalSteps - 1 ? "Finish" : "Next"}
-
-
-
- {arrow}
-
-
- );
-};
diff --git a/components/ui/button-group.tsx b/components/ui/button-group.tsx
deleted file mode 100644
index 8600af03eabb4dbef01dbf618fda5dfe96464b82..0000000000000000000000000000000000000000
--- a/components/ui/button-group.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-import { Separator } from "@/components/ui/separator"
-
-const buttonGroupVariants = cva(
- "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
- {
- variants: {
- orientation: {
- horizontal:
- "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
- vertical:
- "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
- },
- },
- defaultVariants: {
- orientation: "horizontal",
- },
- }
-)
-
-function ButtonGroup({
- className,
- orientation,
- ...props
-}: React.ComponentProps<"div"> & VariantProps) {
- return (
-
- )
-}
-
-function ButtonGroupText({
- className,
- asChild = false,
- ...props
-}: React.ComponentProps<"div"> & {
- asChild?: boolean
-}) {
- const Comp = asChild ? Slot : "div"
-
- return (
-
- )
-}
-
-function ButtonGroupSeparator({
- className,
- orientation = "vertical",
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export {
- ButtonGroup,
- ButtonGroupSeparator,
- ButtonGroupText,
- buttonGroupVariants,
-}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index 863ba4b543815af610f1a7f8d43b1107edf5942d..a13ae96063c5e4b0eadcc5db34fb96ae37f3001a 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -5,42 +5,41 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap cursor-pointer rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-full text-sm font-sans font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
- "bg-primary text-primary-foreground hover:bg-primary/90 border border-primary hover:border-primary/90",
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 border border-primary",
destructive:
- "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ "bg-red-500 text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 [&_svg]:!text-white",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ bordered:
+ "border border-neutral-700/70 text-neutral-200 hover:brightness-120 !rounded-md bg-neutral-900",
secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
- transparent: "bg-accent-foreground/5 hover:bg-accent-foreground/10",
+ lightGray: "bg-neutral-200/60 hover:bg-neutral-200",
+ gray: "bg-neutral-800 !rounded-md text-neutral-300 border border-neutral-700/40 hover:brightness-120",
link: "text-primary underline-offset-4 hover:underline",
- bordered:
- "border bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
- indigo:
- "border border-indigo-500 bg-indigo-500 text-white hover:bg-indigo-600 dark:border-indigo-500/30 dark:bg-indigo-500/20 dark:text-indigo-400 dark:hover:bg-indigo-500/30",
- gradient:
- "bg-linear-to-r from-rose-500/80 via-fushia-500/80 to-yellow-500/80 text-white hover:brightness-110 font-semibold!",
- "ghost-bordered":
- "border bg-primary-foreground hover:bg-background hover:text-accent-foreground dark:hover:bg-accent/50",
- pro: "bg-linear-to-br from-pink-500 dark:from-pink-500/50 via-green-500 dark:via-green-500/50 to-amber-500 dark:to-amber-500/50 text-white hover:brightness-120 font-semibold! [&_img]:grayscale [&_img]:brightness-1 [&_img]:invert dark:[&_img]:invert-0 dark:[&_img]:grayscale-0 dark:[&_img]:brightness-100",
+ ghostDarker:
+ "text-white shadow-xs focus-visible:ring-black/40 bg-black/40 hover:bg-black/70",
+ black: "bg-neutral-950 text-neutral-300 hover:brightness-110",
+ sky: "bg-sky-500 text-white hover:brightness-110",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
- xs: "h-7 rounded-md gap-1.5 px-2 has-[>svg]:px-1.5 text-xs",
- xxs: "h-6 rounded-md gap-1.5 px-2 has-[>svg]:px-1.5 text-[10px]",
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ sm: "h-8 rounded-full text-[13px] gap-1.5 px-3",
+ lg: "h-10 rounded-full px-6 has-[>svg]:px-4",
+ xl: "h-12 rounded-full px-8 has-[>svg]:px-5",
icon: "size-9",
- "icon-sm": "size-8",
- "icon-lg": "size-10",
- "icon-xs": "size-7 rounded-md!",
+ iconXs: "size-7",
+ iconXss: "size-6",
+ iconXsss: "size-5",
+ xs: "h-6 text-xs rounded-full pl-2 pr-2 gap-1",
+ xss: "h-5 text-[10px] rounded-full pl-1.5 pr-1.5 gap-1",
},
},
defaultVariants: {
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
deleted file mode 100644
index 681ad980f27a34dd3d4ba08c7614ce1f7c006143..0000000000000000000000000000000000000000
--- a/components/ui/card.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
-
-function Card({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardAction({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardContent({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardAction,
- CardDescription,
- CardContent,
-}
diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bd4167ec38b98869c4ad17d22cd6514ea4d85e3c
--- /dev/null
+++ b/components/ui/checkbox.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import * as React from "react";
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { CheckIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Checkbox({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+ );
+}
+
+export { Checkbox };
diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ae9fad04a3716b5d6f6c957b75841737eb8ed7a8
--- /dev/null
+++ b/components/ui/collapsible.tsx
@@ -0,0 +1,33 @@
+"use client"
+
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+function Collapsible({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
index d9ccec91d22fab844bd04340c2b07e8677955350..a6f410cbff8a4524949b2cec5f3b05bcb459b6b7 100644
--- a/components/ui/dialog.tsx
+++ b/components/ui/dialog.tsx
@@ -1,33 +1,33 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { XIcon } from "lucide-react"
+import * as React from "react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Dialog({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DialogTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DialogPortal({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DialogClose({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function DialogOverlay({
@@ -43,7 +43,7 @@ function DialogOverlay({
)}
{...props}
/>
- )
+ );
}
function DialogContent({
@@ -52,7 +52,7 @@ function DialogContent({
showCloseButton = true,
...props
}: React.ComponentProps & {
- showCloseButton?: boolean
+ showCloseButton?: boolean;
}) {
return (
@@ -69,7 +69,7 @@ function DialogContent({
{showCloseButton && (
Close
@@ -77,7 +77,7 @@ function DialogContent({
)}
- )
+ );
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -87,7 +87,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
- )
+ );
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -100,7 +100,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
- )
+ );
}
function DialogTitle({
@@ -113,7 +113,7 @@ function DialogTitle({
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
- )
+ );
}
function DialogDescription({
@@ -126,7 +126,7 @@ function DialogDescription({
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
- )
+ );
}
export {
@@ -140,4 +140,4 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
-}
+};
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
index 171d5da6c40e6cf5b85625df5905f09a42d64bdb..6bbd969389aa30c911b6388de5f22191eca62a32 100644
--- a/components/ui/dropdown-menu.tsx
+++ b/components/ui/dropdown-menu.tsx
@@ -74,7 +74,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
- "focus:bg-accent cursor-pointer! focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
- "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
index 868dec6cb3f78d22f4b2cb108c3c5c250de413a8..03295ca6ac617de95b78b09e5e3a6de897a204f0 100644
--- a/components/ui/input.tsx
+++ b/components/ui/input.tsx
@@ -1,6 +1,6 @@
-import * as React from "react";
+import * as React from "react"
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
@@ -8,14 +8,14 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
- );
+ )
}
-export { Input };
+export { Input }
diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx
index 01e468b6783992d04775aa52376221b8fa4b0f95..27576b28e331f9c4f42f91569c963bd99d97c598 100644
--- a/components/ui/popover.tsx
+++ b/components/ui/popover.tsx
@@ -1,20 +1,20 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as PopoverPrimitive from "@radix-ui/react-popover"
+import * as React from "react";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Popover({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function PopoverTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function PopoverContent({
@@ -36,13 +36,13 @@ function PopoverContent({
{...props}
/>
- )
+ );
}
function PopoverAnchor({
...props
}: React.ComponentProps) {
- return
+ return ;
}
-export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
index 88302a8d50922c94626dfe7c7113d3c58af87ae3..dcbbc0ca0c781dfa6d2fe4ee6f1c9c2cad905a9b 100644
--- a/components/ui/select.tsx
+++ b/components/ui/select.tsx
@@ -53,8 +53,7 @@ function SelectTrigger({
function SelectContent({
className,
children,
- position = "item-aligned",
- align = "center",
+ position = "popper",
...props
}: React.ComponentProps) {
return (
@@ -68,7 +67,6 @@ function SelectContent({
className
)}
position={position}
- align={align}
{...props}
>
@@ -114,10 +112,7 @@ function SelectItem({
)}
{...props}
>
-
+
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
deleted file mode 100644
index 275381cab0adbc778ea634e7c46fbf80db821bf3..0000000000000000000000000000000000000000
--- a/components/ui/separator.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as SeparatorPrimitive from "@radix-ui/react-separator"
-
-import { cn } from "@/lib/utils"
-
-function Separator({
- className,
- orientation = "horizontal",
- decorative = true,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export { Separator }
diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e8f898a0f1935fcfad88d0c62129781e9931eaab
--- /dev/null
+++ b/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client";
+
+import * as React from "react";
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { cva, type VariantProps } from "class-variance-authority";
+import { X } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const Sheet = SheetPrimitive.Root;
+
+const SheetTrigger = SheetPrimitive.Trigger;
+
+const SheetClose = SheetPrimitive.Close;
+
+const SheetPortal = SheetPrimitive.Portal;
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-neutral-900 p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+);
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+SheetContent.displayName = SheetPrimitive.Content.displayName;
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetHeader.displayName = "SheetHeader";
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetFooter.displayName = "SheetFooter";
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetTitle.displayName = SheetPrimitive.Title.displayName;
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetDescription.displayName = SheetPrimitive.Description.displayName;
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+};
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
index 9b20afe2e10fd9b94d1abc1ae732054b2fb9821c..2922154241f1415b767c55ca77bbfa8a1569cefd 100644
--- a/components/ui/sonner.tsx
+++ b/components/ui/sonner.tsx
@@ -1,40 +1,22 @@
-"use client"
+"use client";
-import {
- CircleCheckIcon,
- InfoIcon,
- Loader2Icon,
- OctagonXIcon,
- TriangleAlertIcon,
-} from "lucide-react"
-import { useTheme } from "next-themes"
-import { Toaster as Sonner, type ToasterProps } from "sonner"
+import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme()
-
return (
,
- info: ,
- warning: ,
- error: ,
- loading: ,
- }}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
- "--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
- )
-}
+ );
+};
-export { Toaster }
+export { Toaster };
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4864d4e8cfa54dc9ae2e29f9dfb636d8f541f5ce
--- /dev/null
+++ b/components/ui/switch.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import * as React from "react";
+import * as SwitchPrimitive from "@radix-ui/react-switch";
+
+import { cn } from "@/lib/utils";
+
+function Switch({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+export { Switch };
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..497ba5ea34247f6843e0c58ccd7da61b7c8edb46
--- /dev/null
+++ b/components/ui/tabs.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/components/ui/toggle-group.tsx b/components/ui/toggle-group.tsx
index 24a4850d73e82be5e81108fc6f7e8052aabe9819..5eed401b6c9c19f7b6f88e90d3cbe38783ef198b 100644
--- a/components/ui/toggle-group.tsx
+++ b/components/ui/toggle-group.tsx
@@ -8,40 +8,32 @@ import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
- VariantProps & {
- spacing?: number
- }
+ VariantProps
>({
size: "default",
variant: "default",
- spacing: 0,
})
function ToggleGroup({
className,
variant,
size,
- spacing = 0,
children,
...props
}: React.ComponentProps &
- VariantProps & {
- spacing?: number
- }) {
+ VariantProps) {
return (
-
+
{children}
@@ -63,14 +55,12 @@ function ToggleGroupItem({
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
- data-spacing={context.spacing}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
- "w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
- "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
+ "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{...props}
diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx
index e3fea1f7fcdc8e4ce5dcd45f56111b57743826fd..94ec8f589b345a8c33b165463dfda393c7255967 100644
--- a/components/ui/toggle.tsx
+++ b/components/ui/toggle.tsx
@@ -1,13 +1,13 @@
-"use client";
+"use client"
-import * as React from "react";
-import * as TogglePrimitive from "@radix-ui/react-toggle";
-import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react"
+import * as TogglePrimitive from "@radix-ui/react-toggle"
+import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/utils"
const toggleVariants = cva(
- "inline-flex items-center cursor-pointer justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
@@ -18,7 +18,6 @@ const toggleVariants = cva(
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
- xs: "h-7 px-1 min-w-7 text-xs",
lg: "h-10 px-2.5 min-w-10",
},
},
@@ -27,7 +26,7 @@ const toggleVariants = cva(
size: "default",
},
}
-);
+)
function Toggle({
className,
@@ -42,7 +41,7 @@ function Toggle({
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
- );
+ )
}
-export { Toggle, toggleVariants };
+export { Toggle, toggleVariants }
diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9a8f2af7839f0d4c3910431419695a70855ace37
--- /dev/null
+++ b/components/ui/tooltip.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "@/lib/utils";
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/components/user-menu/index.tsx b/components/user-menu/index.tsx
index 2efba21475d7a1c94af81dc3c7e7c4cb537800d4..5d729788d4115763fbcb8d917283b1682f71463b 100644
--- a/components/user-menu/index.tsx
+++ b/components/user-menu/index.tsx
@@ -1,273 +1,83 @@
+import {
+ ChartSpline,
+ CirclePlus,
+ FolderCode,
+ Import,
+ LogOut,
+} from "lucide-react";
import Link from "next/link";
-import { useSession, signIn, signOut } from "next-auth/react";
-import { ArrowRight, Check, Folder, LogOut, Plus } from "lucide-react";
-import { RiContrastFill } from "react-icons/ri";
-
-import { useTheme } from "next-themes";
-import { useEffect } from "react";
-import { FaDiscord } from "react-icons/fa6";
-import Image from "next/image";
import {
DropdownMenu,
DropdownMenuContent,
+ DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
- DropdownMenuPortal,
DropdownMenuSeparator,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
-import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { useProjects } from "@/components/projects/useProjects";
-import { ProjectCard } from "@/components/projects/project-card";
-import { cn, DISCORD_URL } from "@/lib/utils";
-import HFLogo from "@/assets/hf-logo.svg";
-import ProIcon from "@/assets/pro.svg";
-
-export function UserMenu() {
- const { data: session, status } = useSession();
- const { projects } = useProjects();
- const isLoading = status === "loading";
- const { theme, setTheme } = useTheme();
-
- useEffect(() => {
- if (typeof window !== "undefined" && !session && status !== "loading") {
- const urlParams = new URLSearchParams(window.location.search);
- if (urlParams.get("signin") === "true") {
- const newUrl = new URL(window.location.href);
- newUrl.searchParams.delete("signin");
- window.history.replaceState({}, "", newUrl.toString());
-
- signIn("huggingface", { callbackUrl: "/" });
- }
- }
- }, [session, status]);
-
- const handleSignIn = () => {
- if (window.location.hostname === "localhost") {
- signIn("huggingface", {
- callbackUrl: "/",
- });
- return;
- }
- const targetUrl = "https://deepsite.hf.co";
-
- let isOnTargetPage = false;
- if (typeof window !== "undefined") {
- try {
- const isInIframe = window !== window.parent;
-
- if (isInIframe) {
- try {
- isOnTargetPage = window.parent.location.href.startsWith(targetUrl);
- } catch {
- isOnTargetPage = false;
- }
- } else {
- isOnTargetPage = window.location.href.startsWith(targetUrl);
- }
- } catch {
- isOnTargetPage = false;
- }
- }
-
- if (!isOnTargetPage) {
- window.open(`${targetUrl}?signin=true`, "_blank");
- } else {
- signIn("huggingface", { callbackUrl: "/" });
- }
- };
-
- const handleSignOut = () => {
- signOut({ callbackUrl: "/" });
- };
+import { Button } from "@/components/ui/button";
+import { useUser } from "@/hooks/useUser";
- if (isLoading) {
- return (
-
- Loading...
-
- );
- }
- return session?.user ? (
+export const UserMenu = ({ className }: { className?: string }) => {
+ const { logout, user } = useUser();
+ return (
-
-
-
-
-
- {session.user.name?.charAt(0).toUpperCase() || "U"}
-
-
- {session.user.isPro && (
-
- )}
-
- {session.user.name}
+
+
+
+
+ {user?.fullname?.charAt(0).toUpperCase() ?? "E"}
+
+
+ {user?.fullname}
+
+ {user?.fullname?.slice(0, 10)}
+ {(user?.fullname?.length ?? 0) > 10 ? "..." : ""}
+
-
- My Account
+
+
+ My Account
+
- {!session?.user?.isPro && (
- <>
+
+ (window.location.href = "/deepsite/new")}
+ >
+
+ New Project
+
+
-
-
- Subscribe to Pro
-
+
+ View Projects
-
- >
- )}
-
-
- Profile
-
-
-
-
- Settings
-
-
-
-
- Appearance
-
-
-
- setTheme("light")}>
- Light
- {theme === "light" && }
-
- setTheme("dark")}>
- Dark
- {theme === "dark" && }
-
- setTheme("system")}>
- System
- {theme === "system" && }
-
-
-
-
- <>
-
- Projects
- {projects && projects?.length > 0 ? (
-
- {projects?.slice(0, 2).map((project) => (
-
- ))}
-
- {projects?.length > 2 && (
-
- View all projects
-
-
- )}
-
- New project
-
-
-
-
- ) : (
-
-
-
-
-
-
- No projects found.
- Create a new project to get started.
-
-
-
- New project
-
-
-
-
-
- )}
- >
+
+
+
+ Usage Quota
+
+
+
-
-
-
- Discord
-
-
-
-
-
- Hugging Face
-
-
-
-
- Sign out
+ {
+ if (confirm("Are you sure you want to log out?")) {
+ logout();
+ }
+ }}
+ >
+
+
+ Log out
+
- ) : (
- <>
-
- Sign in
-
- Get Started
- >
);
-}
+};
diff --git a/components/viewer/blank-page.tsx b/components/viewer/blank-page.tsx
deleted file mode 100644
index 956bb79be125cfb6072e5b63ca18c845d1786061..0000000000000000000000000000000000000000
--- a/components/viewer/blank-page.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client";
-import Image from "next/image";
-import { useMount } from "react-use";
-import { useState } from "react";
-import { EXAMPLES_OF_PROJECT_SUGGESTIONS } from "@/lib/prompts";
-
-interface ExampleOfProjectSuggestion {
- short_value: string;
- long_value: string;
-}
-
-export function BlankPage() {
- const [suggestions, setSuggestions] = useState(
- []
- );
-
- useMount(() => {
- const randomSuggestions = [...EXAMPLES_OF_PROJECT_SUGGESTIONS]
- .sort(() => Math.random() - 0.5)
- .slice(0, 3);
- setSuggestions(randomSuggestions);
- });
- return (
-
-
-
-
- Here are some suggestions for you:
-
-
- {suggestions.map((suggestion) => (
-
{
- const promptInput = document.getElementById("prompt-input");
- if (promptInput) {
- (promptInput as HTMLDivElement).textContent =
- suggestion.long_value;
- }
- }}
- >
- {suggestion.short_value}
-
- ))}
-
-
-
- );
-}
diff --git a/components/viewer/index.tsx b/components/viewer/index.tsx
deleted file mode 100644
index 9a2aafc06d2375aa03ddef235b39a930d33d6ac9..0000000000000000000000000000000000000000
--- a/components/viewer/index.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-"use client";
-import { ArrowLeft } from "lucide-react";
-import { SandpackLayout, SandpackPreview } from "@codesandbox/sandpack-react";
-
-import { DeviceType, MobileTabType } from "@/lib/type";
-import { cn } from "@/lib/utils";
-import { GridPattern } from "@/components/grid-pattern";
-import { useProject } from "@/components/projects/useProject";
-import { BlankPage } from "./blank-page";
-import { Button } from "@/components/ui/button";
-
-export function AppViewer({
- mobileTab,
- onToggleMobileTab,
- device,
-}: {
- mobileTab: MobileTabType;
- device: DeviceType;
- onToggleMobileTab: (tab: MobileTabType) => void;
-}) {
- const { project, files } = useProject();
-
- return (
- 0)
- ? "lg:border lg:border-border-muted lg:dark:border-secondary"
- : "bg-accent border border-border-muted dark:border-secondary"
- )}
- >
-
- {project?.name || (files && files.length > 0) ? (
-
-
-
- ) : (
-
- )}
-
0)
- ? "justify-start"
- : "p-2 justify-center"
- )}
- >
-
onToggleMobileTab("left-sidebar")}
- >
- Go to Chat
-
-
-
- );
-}
diff --git a/eslint.config.mjs b/eslint.config.mjs
deleted file mode 100644
index 05e726d1b4201bc8c7716d2b058279676582e8c0..0000000000000000000000000000000000000000
--- a/eslint.config.mjs
+++ /dev/null
@@ -1,18 +0,0 @@
-import { defineConfig, globalIgnores } from "eslint/config";
-import nextVitals from "eslint-config-next/core-web-vitals";
-import nextTs from "eslint-config-next/typescript";
-
-const eslintConfig = defineConfig([
- ...nextVitals,
- ...nextTs,
- // Override default ignores of eslint-config-next.
- globalIgnores([
- // Default ignores of eslint-config-next:
- ".next/**",
- "out/**",
- "build/**",
- "next-env.d.ts",
- ]),
-]);
-
-export default eslintConfig;
diff --git a/hooks/useAi.ts b/hooks/useAi.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cb02b23a7f897bbd3b0bb580f307e2168892a027
--- /dev/null
+++ b/hooks/useAi.ts
@@ -0,0 +1,720 @@
+import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
+import { useMemo, useRef, useState } from "react";
+import { toast } from "sonner";
+import { useLocalStorage } from "react-use";
+
+import { MODELS } from "@/lib/providers";
+import { useEditor } from "./useEditor";
+import { Page, EnhancedSettings } from "@/types";
+import { api } from "@/lib/api";
+import { usePathname, useRouter } from "next/navigation";
+import { useUser } from "./useUser";
+import { isTheSameHtml } from "@/lib/compare-html-diff";
+
+export const useAi = (onScrollToBottom?: () => void) => {
+ const client = useQueryClient();
+ const audio = useRef(null);
+ const { setPages, setCurrentPage, setPreviewPage, setPrompts, prompts, pages, project, setProject, commits, setCommits, setLastSavedPages, isSameHtml } = useEditor();
+ const [controller, setController] = useState(null);
+ const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
+ const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
+ const router = useRouter();
+ const { token } = useUser();
+ const pathname = usePathname();
+ const namespace = pathname.split("/")[1];
+ const repoId = pathname.split("/")[2];
+ const streamingPagesRef = useRef>(new Set());
+
+ const { data: isAiWorking = false } = useQuery({
+ queryKey: ["ai.isAiWorking"],
+ queryFn: async () => false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ });
+ const setIsAiWorking = (newIsAiWorking: boolean) => {
+ client.setQueryData(["ai.isAiWorking"], newIsAiWorking);
+ };
+
+ const { data: isThinking = false } = useQuery({
+ queryKey: ["ai.isThinking"],
+ queryFn: async () => false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ });
+ const setIsThinking = (newIsThinking: boolean) => {
+ client.setQueryData(["ai.isThinking"], newIsThinking);
+ };
+
+ const { data: thinkingContent } = useQuery({
+ queryKey: ["ai.thinkingContent"],
+ queryFn: async () => "",
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ initialData: ""
+ });
+ const setThinkingContent = (newThinkingContent: string) => {
+ client.setQueryData(["ai.thinkingContent"], newThinkingContent);
+ };
+
+ const { data: selectedElement } = useQuery({
+ queryKey: ["ai.selectedElement"],
+ queryFn: async () => null,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ initialData: null
+ });
+ const setSelectedElement = (newSelectedElement: HTMLElement | null) => {
+ client.setQueryData(["ai.selectedElement"], newSelectedElement);
+ setIsEditableModeEnabled(false);
+ };
+
+ const { data: isEditableModeEnabled = false } = useQuery({
+ queryKey: ["ai.isEditableModeEnabled"],
+ queryFn: async () => false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ });
+ const setIsEditableModeEnabled = (newIsEditableModeEnabled: boolean) => {
+ client.setQueryData(["ai.isEditableModeEnabled"], newIsEditableModeEnabled);
+ };
+
+ const { data: selectedFiles } = useQuery({
+ queryKey: ["ai.selectedFiles"],
+ queryFn: async () => [],
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ initialData: []
+ });
+ const setSelectedFiles = (newFiles: string[]) => {
+ client.setQueryData(["ai.selectedFiles"], newFiles)
+ };
+
+ const { data: contextFile } = useQuery({
+ queryKey: ["ai.contextFile"],
+ queryFn: async () => null,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ initialData: null
+ });
+ const setContextFile = (newContextFile: string | null) => {
+ client.setQueryData(["ai.contextFile"], newContextFile)
+ };
+
+ const { data: provider } = useQuery({
+ queryKey: ["ai.provider"],
+ queryFn: async () => storageProvider ?? "auto",
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ initialData: storageProvider ?? "auto"
+ });
+ const setProvider = (newProvider: string) => {
+ setStorageProvider(newProvider);
+ client.setQueryData(["ai.provider"], newProvider);
+ };
+
+ const { data: model } = useQuery({
+ queryKey: ["ai.model"],
+ queryFn: async () => {
+ // check if the model exist in the MODELS array
+ const selectedModel = MODELS.find(m => m.value === storageModel || m.label === storageModel);
+ if (selectedModel) {
+ return selectedModel.value;
+ }
+ return MODELS[0].value;
+ },
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ initialData: undefined,
+ });
+ const setModel = (newModel: string) => {
+ setStorageModel(newModel);
+ client.setQueryData(["ai.model"], newModel);
+ };
+
+ const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean, userName?: string) => {
+ if (isLoggedIn && userName) {
+ try {
+ const uploadRequest = await fetch(`/deepsite/api/me/projects/${userName}/new/update`, {
+ method: "PUT",
+ body: JSON.stringify({
+ pages: htmlPages,
+ commitTitle: prompt,
+ isNew: true,
+ projectName,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${token}`,
+ },
+ });
+
+ const uploadRes = await uploadRequest.json();
+
+ if (!uploadRequest.ok || !uploadRes.ok) {
+ throw new Error(uploadRes.error || "Failed to create project");
+ }
+
+ setIsAiWorking(false);
+ router.replace(`/${uploadRes.repoId}`);
+ toast.success("AI responded successfully");
+ if (audio.current) audio.current.play();
+ } catch (error: any) {
+ setIsAiWorking(false);
+ toast.error(error?.message || "Failed to create project");
+ }
+ } else {
+ setIsAiWorking(false);
+ toast.success("AI responded successfully");
+ if (audio.current) audio.current.play();
+ }
+ }
+
+ const callAiNewProject = async (prompt: string, enhancedSettings?: EnhancedSettings, redesignMarkdown?: string, isLoggedIn?: boolean, userName?: string) => {
+ if (isAiWorking) return;
+ if (!redesignMarkdown && !prompt.trim()) return;
+
+ setIsAiWorking(true);
+ setThinkingContent(""); // Reset thinking content
+ streamingPagesRef.current.clear(); // Reset tracking for new generation
+
+ const abortController = new AbortController();
+ setController(abortController);
+
+ try {
+ const request = await fetch("/deepsite/api/ask", {
+ method: "POST",
+ body: JSON.stringify({
+ prompt,
+ provider,
+ model,
+ redesignMarkdown,
+ enhancedSettings,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ "x-forwarded-for": window.location.hostname,
+ "Authorization": `Bearer ${token}`,
+ },
+ signal: abortController.signal,
+ });
+
+ if (request && request.body) {
+ const reader = request.body.getReader();
+ const decoder = new TextDecoder("utf-8");
+ let contentResponse = "";
+
+ const read = async (): Promise => {
+ const { done, value } = await reader.read();
+
+ if (done) {
+ // Final processing - extract and remove thinking content
+ const thinkMatch = contentResponse.match(/([\s\S]*?)<\/think>/);
+ if (thinkMatch) {
+ setThinkingContent(thinkMatch[1].trim());
+ setIsThinking(false);
+ contentResponse = contentResponse.replace(/[\s\S]*?<\/think>/, '').trim();
+ }
+
+ const trimmedResponse = contentResponse.trim();
+ if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
+ try {
+ const jsonResponse = JSON.parse(trimmedResponse);
+ if (jsonResponse && !jsonResponse.ok) {
+ setIsAiWorking(false);
+ if (jsonResponse.openLogin) {
+ return { error: "login_required" };
+ } else if (jsonResponse.openSelectProvider) {
+ return { error: "provider_required", message: jsonResponse.message };
+ } else if (jsonResponse.openProModal) {
+ return { error: "pro_required" };
+ } else {
+ toast.error(jsonResponse.message);
+ return { error: "api_error", message: jsonResponse.message };
+ }
+ }
+ } catch (e) {
+ }
+ }
+
+ const newPages = formatPages(contentResponse, false);
+ let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
+ if (!projectName) {
+ projectName = prompt.substring(0, 20).replace(/[^a-zA-Z0-9]/g, "-") + "-" + Math.random().toString(36).substring(2, 9);
+ }
+ setPages(newPages);
+ setLastSavedPages([...newPages]);
+ if (newPages.length > 0 && !isTheSameHtml(newPages[0].html)) {
+ createNewProject(prompt, newPages, projectName, isLoggedIn, userName);
+ }
+ setPrompts([...prompts, prompt]);
+
+ return { success: true, pages: newPages };
+ }
+
+ const chunk = decoder.decode(value, { stream: true });
+ contentResponse += chunk;
+
+ // Extract thinking content while streaming
+ if (contentResponse.includes(' ')) {
+ // Thinking is complete, extract final content and stop thinking
+ const thinkMatch = contentResponse.match(/([\s\S]*?)<\/think>/);
+ if (thinkMatch) {
+ setThinkingContent(thinkMatch[1].trim());
+ setIsThinking(false);
+ }
+ } else if (contentResponse.includes('')) {
+ // Still thinking, update content
+ const thinkMatch = contentResponse.match(/([\s\S]*)$/);
+ if (thinkMatch) {
+ const thinkingText = thinkMatch[1].trim();
+ if (thinkingText) {
+ setIsThinking(true);
+ setThinkingContent(thinkingText);
+ }
+ }
+ }
+
+ const trimmedResponse = contentResponse.trim();
+ if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
+ try {
+ const jsonResponse = JSON.parse(trimmedResponse);
+ if (jsonResponse && !jsonResponse.ok) {
+ setIsAiWorking(false);
+ if (jsonResponse.openLogin) {
+ return { error: "login_required" };
+ } else if (jsonResponse.openSelectProvider) {
+ return { error: "provider_required", message: jsonResponse.message };
+ } else if (jsonResponse.openProModal) {
+ return { error: "pro_required" };
+ } else {
+ toast.error(jsonResponse.message);
+ return { error: "api_error", message: jsonResponse.message };
+ }
+ }
+ } catch (e) {
+ }
+ }
+
+ formatPages(contentResponse, true);
+
+ return read();
+ };
+
+ return await read();
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ setIsAiWorking(false);
+ setIsThinking(false);
+ setThinkingContent("");
+ setController(null);
+
+ if (!abortController.signal.aborted) {
+ toast.error(error.message || "Network error occurred");
+ }
+
+ if (error.openLogin) {
+ return { error: "login_required" };
+ }
+ return { error: "network_error", message: error.message };
+ }
+ };
+
+ const callAiFollowUp = async (prompt: string, enhancedSettings?: EnhancedSettings, isNew?: boolean) => {
+ if (isAiWorking) return;
+ if (!prompt.trim()) return;
+
+
+ setIsAiWorking(true);
+ setThinkingContent(""); // Reset thinking content
+
+ const abortController = new AbortController();
+ setController(abortController);
+
+ try {
+ const pagesToSend = contextFile
+ ? pages.filter(page => page.path === contextFile)
+ : pages;
+
+ const request = await fetch("/deepsite/api/ask", {
+ method: "PUT",
+ body: JSON.stringify({
+ prompt,
+ provider,
+ previousPrompts: prompts,
+ model,
+ pages: pagesToSend,
+ selectedElementHtml: selectedElement?.outerHTML,
+ files: selectedFiles,
+ repoId: project?.space_id,
+ isNew,
+ enhancedSettings,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ "x-forwarded-for": window.location.hostname,
+ "Authorization": `Bearer ${token}`,
+ },
+ signal: abortController.signal,
+ });
+
+ if (request && request.body) {
+ const reader = request.body.getReader();
+ const decoder = new TextDecoder("utf-8");
+ let contentResponse = "";
+ let metadata: any = null;
+
+ const read = async (): Promise => {
+ const { done, value } = await reader.read();
+
+ if (done) {
+ // Extract and remove thinking content
+ const thinkMatch = contentResponse.match(/([\s\S]*?)<\/think>/);
+ if (thinkMatch) {
+ setThinkingContent(thinkMatch[1].trim());
+ setIsThinking(false);
+ contentResponse = contentResponse.replace(/[\s\S]*?<\/think>/, '').trim();
+ }
+
+ // const metadataMatch = contentResponse.match(/___METADATA_START___([\s\S]*?)___METADATA_END___/);
+ // if (metadataMatch) {
+ // try {
+ // metadata = JSON.parse(metadataMatch[1]);
+ // contentResponse = contentResponse.replace(/___METADATA_START___[\s\S]*?___METADATA_END___/, '').trim();
+ // } catch (e) {
+ // console.error("Failed to parse metadata", e);
+ // }
+ // }
+
+ const trimmedResponse = contentResponse.trim();
+ if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
+ try {
+ const jsonResponse = JSON.parse(trimmedResponse);
+ if (jsonResponse && !jsonResponse.ok) {
+ setIsAiWorking(false);
+ if (jsonResponse.openLogin) {
+ return { error: "login_required" };
+ } else if (jsonResponse.openSelectProvider) {
+ return { error: "provider_required", message: jsonResponse.message };
+ } else if (jsonResponse.openProModal) {
+ return { error: "pro_required" };
+ } else {
+ toast.error(jsonResponse.message);
+ return { error: "api_error", message: jsonResponse.message };
+ }
+ }
+ } catch (e) {
+ // Not JSON, continue with normal processing
+ }
+ }
+
+ const { processAiResponse, extractProjectName } = await import("@/lib/format-ai-response");
+ const { updatedPages, updatedLines } = processAiResponse(contentResponse, pagesToSend);
+
+ const updatedPagesMap = new Map(updatedPages.map((p: Page) => [p.path, p]));
+ const mergedPages: Page[] = pages.map(page =>
+ updatedPagesMap.has(page.path) ? updatedPagesMap.get(page.path)! : page
+ );
+ updatedPages.forEach((page: Page) => {
+ if (!pages.find(p => p.path === page.path)) {
+ mergedPages.push(page);
+ }
+ });
+
+ let projectName = null;
+ if (isNew) {
+ projectName = extractProjectName(contentResponse);
+ if (!projectName) {
+ projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40) + "-" + Math.random().toString(36).substring(2, 15);
+ }
+ }
+
+ try {
+ const uploadRequest = await fetch(`/deepsite/api/me/projects/${namespace ?? 'unknown'}/${repoId ?? 'unknown'}/update`, {
+ method: "PUT",
+ body: JSON.stringify({
+ pages: mergedPages,
+ commitTitle: prompt,
+ isNew,
+ projectName,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${token}`,
+ },
+ });
+
+ const uploadRes = await uploadRequest.json();
+
+ if (!uploadRequest.ok || !uploadRes.ok) {
+ throw new Error(uploadRes.error || "Failed to upload to HuggingFace");
+ }
+
+ toast.success("AI responded successfully");
+ const iframe = document.getElementById("preview-iframe") as HTMLIFrameElement;
+
+ if (isNew && uploadRes.repoId) {
+ router.push(`/${uploadRes.repoId}`);
+ setIsAiWorking(false);
+ } else {
+ setPages(mergedPages);
+ setLastSavedPages([...mergedPages]);
+ setCommits([uploadRes.commit, ...commits]);
+ setPrompts([...prompts, prompt]);
+ setSelectedElement(null);
+ setSelectedFiles([]);
+ setIsEditableModeEnabled(false);
+ setIsAiWorking(false);
+ }
+
+ if (audio.current) audio.current.play();
+ if (iframe) {
+ setTimeout(() => {
+ iframe.src = iframe.src;
+ }, 500);
+ }
+
+ return { success: true, updatedLines };
+ } catch (uploadError: any) {
+ setIsAiWorking(false);
+ toast.error(uploadError.message || "Failed to save changes");
+ return { error: "upload_error", message: uploadError.message };
+ }
+ }
+
+ const chunk = decoder.decode(value, { stream: true });
+ contentResponse += chunk;
+
+ // Extract thinking content while streaming
+ if (contentResponse.includes(' ')) {
+ // Thinking is complete, extract final content and stop thinking
+ const thinkMatch = contentResponse.match(/([\s\S]*?)<\/think>/);
+ if (thinkMatch) {
+ setThinkingContent(thinkMatch[1].trim());
+ setIsThinking(false);
+ }
+ } else if (contentResponse.includes('')) {
+ // Still thinking, update content
+ const thinkMatch = contentResponse.match(/([\s\S]*)$/);
+ if (thinkMatch) {
+ const thinkingText = thinkMatch[1].trim();
+ if (thinkingText) {
+ setIsThinking(true);
+ setThinkingContent(thinkingText);
+ }
+ }
+ }
+
+ // Check for error responses during streaming
+ const trimmedResponse = contentResponse.trim();
+ if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
+ try {
+ const jsonResponse = JSON.parse(trimmedResponse);
+ if (jsonResponse && !jsonResponse.ok) {
+ setIsAiWorking(false);
+ if (jsonResponse.openLogin) {
+ return { error: "login_required" };
+ } else if (jsonResponse.openSelectProvider) {
+ return { error: "provider_required", message: jsonResponse.message };
+ } else if (jsonResponse.openProModal) {
+ return { error: "pro_required" };
+ } else {
+ toast.error(jsonResponse.message);
+ return { error: "api_error", message: jsonResponse.message };
+ }
+ }
+ } catch (e) {
+ // Not complete JSON yet, continue
+ }
+ }
+
+ return read();
+ };
+
+ return await read();
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ setIsAiWorking(false);
+ setIsThinking(false);
+ setThinkingContent("");
+ setController(null);
+
+ if (!abortController.signal.aborted) {
+ toast.error(error.message || "Network error occurred");
+ }
+
+ if (error.openLogin) {
+ return { error: "login_required" };
+ }
+ return { error: "network_error", message: error.message };
+ }
+ };
+
+ const formatPages = (content: string, isStreaming: boolean = true) => {
+ const pages: Page[] = [];
+ if (!content.match(/<<<<<<< NEW_FILE_START[\s\S]*?>>>>>>> NEW_FILE_END/)) {
+ return pages;
+ }
+
+ const cleanedContent = content.replace(
+ /[\s\S]*?<<<<<<< NEW_FILE_START\s+([\s\S]*?)\s+>>>>>>> NEW_FILE_END/,
+ "<<<<<<< NEW_FILE_START $1 >>>>>>> NEW_FILE_END"
+ );
+ const fileChunks = cleanedContent.split(
+ /<<<<<<< NEW_FILE_START\s+([\s\S]*?)\s+>>>>>>> NEW_FILE_END/
+ );
+ const processedChunks = new Set();
+
+ fileChunks.forEach((chunk, index) => {
+ if (processedChunks.has(index) || !chunk?.trim()) {
+ return;
+ }
+ const filePath = chunk.trim();
+ const fileContent = extractFileContent(fileChunks[index + 1], filePath);
+
+ if (fileContent) {
+ const page: Page = {
+ path: filePath,
+ html: fileContent,
+ };
+ pages.push(page);
+
+ if (fileContent.length > 200) {
+ onScrollToBottom?.();
+ }
+
+ processedChunks.add(index);
+ processedChunks.add(index + 1);
+ }
+ });
+ if (pages.length > 0) {
+ setPages(pages);
+ if (isStreaming) {
+ const newPages = pages.filter(p =>
+ !streamingPagesRef.current.has(p.path)
+ );
+
+ if (newPages.length > 0) {
+ const newPage = newPages[0];
+ setCurrentPage(newPage.path);
+ streamingPagesRef.current.add(newPage.path);
+
+ if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
+ setPreviewPage(newPage.path);
+ }
+ }
+ } else {
+ streamingPagesRef.current.clear();
+ const indexPage = pages.find(p => p.path === 'index.html' || p.path === 'index' || p.path === '/');
+ if (indexPage) {
+ setCurrentPage(indexPage.path);
+ }
+ }
+ }
+
+ return pages;
+ };
+
+ const extractFileContent = (chunk: string, filePath: string): string => {
+ if (!chunk) return "";
+
+ let content = chunk.trim();
+
+ if (filePath.endsWith('.css')) {
+ const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
+ if (cssMatch) {
+ content = cssMatch[1];
+ } else {
+ content = content.replace(/^```css\s*/i, "");
+ }
+ return content.replace(/```/g, "").trim();
+ } else if (filePath.endsWith('.js')) {
+ const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
+ if (jsMatch) {
+ content = jsMatch[1];
+ } else {
+ content = content.replace(/^```(?:javascript|js)\s*/i, "");
+ }
+ return content.replace(/```/g, "").trim();
+ } else {
+ const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
+ if (htmlMatch) {
+ content = htmlMatch[1];
+ } else {
+ content = content.replace(/^```html\s*/i, "");
+ const doctypeMatch = content.match(/[\s\S]*/);
+ if (doctypeMatch) {
+ content = doctypeMatch[0];
+ }
+ }
+
+ let htmlContent = content.replace(/```/g, "");
+ htmlContent = ensureCompleteHtml(htmlContent);
+ return htmlContent;
+ }
+ };
+
+ const ensureCompleteHtml = (html: string): string => {
+ let completeHtml = html;
+ if (completeHtml.includes("") && !completeHtml.includes("")) {
+ completeHtml += "\n";
+ }
+ if (completeHtml.includes("")) {
+ completeHtml += "\n";
+ }
+ if (!completeHtml.includes("