diff --git a/.env.example b/.env.example
deleted file mode 100644
index d7419a983b3e8d6606db990addea8d78dd660692..0000000000000000000000000000000000000000
--- a/.env.example
+++ /dev/null
@@ -1,4 +0,0 @@
-AUTH_HUGGINGFACE_ID=
-AUTH_HUGGINGFACE_SECRET=
-NEXTAUTH_URL=http://localhost:3001
-AUTH_SECRET=
\ No newline at end of file
diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml
deleted file mode 100644
index 88d7e85dd2f708097a42154a349c9cb217479120..0000000000000000000000000000000000000000
--- a/.github/workflows/deploy-prod.yml
+++ /dev/null
@@ -1,77 +0,0 @@
-name: Deploy to k8s
-on:
- # run this workflow manually from the Actions tab
- workflow_dispatch:
-
-jobs:
- build-and-publish:
- runs-on:
- group: cpu-high
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Login to Registry
- uses: docker/login-action@v3
- with:
- registry: registry.internal.huggingface.tech
- username: ${{ secrets.DOCKER_INTERNAL_USERNAME }}
- password: ${{ secrets.DOCKER_INTERNAL_PASSWORD }}
-
- - name: Docker metadata
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: |
- registry.internal.huggingface.tech/deepsite/deepsite
- tags: |
- type=raw,value=latest,enable={{is_default_branch}}
- type=sha,enable=true,prefix=sha-,format=short,sha-len=8
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Inject slug/short variables
- uses: rlespinasse/github-slug-action@v4
-
- - name: Build and Publish image
- uses: docker/build-push-action@v5
- with:
- context: .
- file: Dockerfile
- push: ${{ github.event_name != 'pull_request' }}
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- platforms: linux/amd64
- cache-to: type=gha,mode=max,scope=amd64
- cache-from: type=gha,scope=amd64
- provenance: false
-
- deploy:
- name: Deploy on prod
- runs-on: ubuntu-latest
- needs: ["build-and-publish"]
- steps:
- - name: Inject slug/short variables
- uses: rlespinasse/github-slug-action@v4
-
- - name: Gen values
- run: |
- VALUES=$(cat <<-END
- image:
- tag: "sha-${{ env.GITHUB_SHA_SHORT }}"
- END
- )
- echo "VALUES=$(echo "$VALUES" | yq -o=json | jq tostring)" >> $GITHUB_ENV
-
- - name: Deploy on infra-deployments
- uses: the-actions-org/workflow-dispatch@v2
- with:
- workflow: Update application single value
- repo: huggingface/infra-deployments
- wait-for-completion: true
- wait-for-completion-interval: 10s
- display-workflow-run-url-interval: 10s
- ref: refs/heads/main
- token: ${{ secrets.GIT_TOKEN_INFRA_DEPLOYMENT }}
- inputs: '{"path": "hub/deepsite/deepsite.yaml", "value": ${{ env.VALUES }}, "url": "${{ github.event.head_commit.url }}"}'
diff --git a/.gitignore b/.gitignore
index b8b89cbb382fac7dea7fdeb461bd43beb9937c49..5ef6a520780202a1d6addd833d800ccb1ecac0bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
-.env
+.env*
# vercel
.vercel
@@ -39,9 +39,3 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
-
-.idea
-
-# binary assets (hosted on CDN)
-assets/assistant.jpg
-.gitattributes
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index a2b0759c0a612c3be02d8c1eb3021252cdd4a7f8..cbe0188aaee92186937765d2c85d76f7b212c537 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,19 @@
FROM node:20-alpine
USER root
-# Install pnpm
-RUN corepack enable && corepack prepare pnpm@latest --activate
-
USER 1000
WORKDIR /usr/src/app
-# Copy package.json and pnpm-lock.yaml to the container
-COPY --chown=1000 package.json pnpm-lock.yaml ./
+# Copy package.json and package-lock.json to the container
+COPY --chown=1000 package.json package-lock.json ./
# Copy the rest of the application files to the container
COPY --chown=1000 . .
-RUN pnpm install
-RUN pnpm run build
+RUN npm install
+RUN npm run build
# Expose the application port (assuming your app runs on port 3000)
-EXPOSE 3001
+EXPOSE 3000
# Start the application
-CMD ["pnpm", "start"]
\ No newline at end of file
+CMD ["npm", "start"]
\ No newline at end of file
diff --git a/MCP-SERVER.md b/MCP-SERVER.md
new file mode 100644
index 0000000000000000000000000000000000000000..675acdbb77beeb3bdc3e0b7b49a2e01d39df1b58
--- /dev/null
+++ b/MCP-SERVER.md
@@ -0,0 +1,428 @@
+# DeepSite MCP Server
+
+DeepSite is now available as an MCP (Model Context Protocol) server, enabling AI assistants like Claude to create websites directly using natural language.
+
+## Two Ways to Use DeepSite MCP
+
+**Quick Comparison:**
+
+| Feature | Option 1: HTTP Server | Option 2: Local Server |
+|---------|----------------------|------------------------|
+| **Setup Difficulty** | β
Easy (just config) | β οΈ Requires installation |
+| **Authentication** | HF Token in config header | HF Token or session cookie in env |
+| **Best For** | Most users | Developers, custom modifications |
+| **Maintenance** | β
Always up-to-date | Need to rebuild for updates |
+
+**Recommendation:** Use Option 1 (HTTP Server) unless you need to modify the MCP server code.
+
+---
+
+### π Option 1: HTTP Server (Recommended)
+
+**No installation required!** Use DeepSite's hosted MCP server.
+
+#### Setup for Claude Desktop
+
+Add to your Claude Desktop configuration file:
+
+**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
+**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
+
+```json
+{
+ "mcpServers": {
+ "deepsite": {
+ "url": "https://huggingface.co/deepsite/api/mcp",
+ "transport": {
+ "type": "sse"
+ },
+ "headers": {
+ "Authorization": "Bearer hf_your_token_here"
+ }
+ }
+ }
+}
+```
+
+**Getting Your Hugging Face Token:**
+
+1. Go to https://huggingface.co/settings/tokens
+2. Create a new token with `write` access
+3. Copy the token
+4. Add it to the `Authorization` header in your config (recommended for security)
+5. Alternatively, you can pass it as the `hf_token` parameter when using the tool
+
+**β οΈ Security Recommendation:** Use the `Authorization` header in your config instead of passing the token in chat. This keeps your token secure and out of conversation history.
+
+#### Example Usage with Claude
+
+> "Create a portfolio website using DeepSite. Include a hero section, about section, and contact form."
+
+Claude will automatically:
+1. Use the `create_project` tool
+2. Authenticate using the token from your config
+3. Create the website on Hugging Face Spaces
+4. Return the URLs to access your new site
+
+---
+
+### π» Option 2: Local Server
+
+Run the MCP server locally for more control or offline use.
+
+> **Note:** Most users should use Option 1 (HTTP Server) instead. Option 2 is only needed if you want to run the MCP server locally or modify its behavior.
+
+#### Installation
+
+```bash
+cd mcp-server
+npm install
+npm run build
+```
+
+#### Setup for Claude Desktop
+
+**Method A: Using HF Token (Recommended)**
+
+```json
+{
+ "mcpServers": {
+ "deepsite-local": {
+ "command": "node",
+ "args": ["/absolute/path/to/deepsite-v3/mcp-server/dist/index.js"],
+ "env": {
+ "HF_TOKEN": "hf_your_token_here",
+ "DEEPSITE_API_URL": "https://huggingface.co/deepsite"
+ }
+ }
+ }
+}
+```
+
+**Method B: Using Session Cookie (Alternative)**
+
+```json
+{
+ "mcpServers": {
+ "deepsite-local": {
+ "command": "node",
+ "args": ["/absolute/path/to/deepsite-v3/mcp-server/dist/index.js"],
+ "env": {
+ "DEEPSITE_AUTH_COOKIE": "your-session-cookie",
+ "DEEPSITE_API_URL": "https://huggingface.co/deepsite"
+ }
+ }
+ }
+}
+```
+
+**Getting Your Session Cookie (Method B only):**
+
+1. Log in to https://huggingface.co/deepsite
+2. Open Developer Tools (F12)
+3. Go to Application β Cookies
+4. Copy the session cookie value
+5. Set as `DEEPSITE_AUTH_COOKIE` in the config
+
+---
+
+## Available Tools
+
+### `create_project`
+
+Creates a new DeepSite project with HTML/CSS/JS files.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `title` | string | No | Project title (defaults to "DeepSite Project") |
+| `pages` | array | Yes | Array of file objects with `path` and `html` |
+| `prompt` | string | No | Commit message/description |
+| `hf_token` | string | No* | Hugging Face API token (*optional if provided via Authorization header in config) |
+
+**Page Object:**
+```typescript
+{
+ path: string; // e.g., "index.html", "styles.css", "script.js"
+ html: string; // File content
+}
+```
+
+**Returns:**
+```json
+{
+ "success": true,
+ "message": "Project created successfully!",
+ "projectUrl": "https://huggingface.co/deepsite/username/project-name",
+ "spaceUrl": "https://huggingface.co/spaces/username/project-name",
+ "liveUrl": "https://username-project-name.hf.space",
+ "spaceId": "username/project-name",
+ "projectId": "space-id",
+ "files": ["index.html", "styles.css"]
+}
+```
+
+---
+
+## Example Prompts for Claude
+
+### Simple Landing Page
+> "Create a modern landing page for my SaaS product using DeepSite. Include a hero section with CTA, features grid, and footer. Use gradient background."
+
+### Portfolio Website
+> "Build a portfolio website with DeepSite. I need:
+> - Hero section with my name and photo
+> - Projects gallery with 3 sample projects
+> - Skills section with tech stack
+> - Contact form
+> Use dark mode with accent colors."
+
+### Blog Homepage
+> "Create a blog homepage using DeepSite. Include:
+> - Header with navigation
+> - Featured post section
+> - Grid of recent posts (3 cards)
+> - Sidebar with categories
+> - Footer with social links
+> Clean, minimal design."
+
+### Interactive Dashboard
+> "Make an analytics dashboard with DeepSite:
+> - Sidebar navigation
+> - 4 metric cards at top
+> - 2 chart placeholders
+> - Data table
+> - Modern, professional UI with charts.css"
+
+---
+
+## Direct API Usage
+
+You can also call the HTTP endpoint directly:
+
+### Using Authorization Header (Recommended)
+
+```bash
+curl -X POST https://huggingface.co/deepsite/api/mcp \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer hf_your_token_here" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "tools/call",
+ "params": {
+ "name": "create_project",
+ "arguments": {
+ "title": "My Website",
+ "pages": [
+ {
+ "path": "index.html",
+ "html": "
HelloHello 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": "HelloHello World!
"
+ }
+ ],
+ "hf_token": "hf_xxxxx"
+ }
+ }
+ }'
+```
+
+### List Available Tools
+
+```bash
+curl -X POST https://huggingface.co/deepsite/api/mcp \
+ -H "Content-Type: application/json" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "tools/list",
+ "params": {}
+ }'
+```
+
+---
+
+## Testing
+
+### Test Local Server
+
+```bash
+cd mcp-server
+./test.sh
+```
+
+### Test HTTP Server
+
+```bash
+curl -X POST https://huggingface.co/deepsite/api/mcp \
+ -H "Content-Type: application/json" \
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
+```
+
+---
+
+## Migration Guide: From Parameter to Header Auth
+
+If you're currently passing the token as a parameter in your prompts, here's how to migrate to the more secure header-based authentication:
+
+### Step 1: Update Your Config
+
+Edit your Claude Desktop config file and add the `headers` section:
+
+```json
+{
+ "mcpServers": {
+ "deepsite": {
+ "url": "https://huggingface.co/deepsite/api/mcp",
+ "transport": {
+ "type": "sse"
+ },
+ "headers": {
+ "Authorization": "Bearer hf_your_actual_token_here"
+ }
+ }
+ }
+}
+```
+
+### Step 2: Restart Claude Desktop
+
+Completely quit and restart Claude Desktop for the changes to take effect.
+
+### Step 3: Use Simpler Prompts
+
+Now you can simply say:
+> "Create a portfolio website with DeepSite"
+
+Instead of:
+> "Create a portfolio website with DeepSite using token `hf_xxxxx`"
+
+Your token is automatically included in all requests via the header!
+
+---
+
+## Security Notes
+
+### HTTP Server (Option 1)
+- **β
Recommended:** Store your HF token in the `Authorization` header in your Claude Desktop config
+- The token is stored locally on your machine and never exposed in chat
+- The token is sent with each request but only used to authenticate with Hugging Face API
+- DeepSite does not store your token
+- Use tokens with minimal required permissions (write access to spaces)
+- You can revoke tokens anytime at https://huggingface.co/settings/tokens
+- **β οΈ Fallback:** You can still pass the token as a parameter, but this is less secure as it appears in conversation history
+
+### Local Server (Option 2)
+- Use `HF_TOKEN` environment variable (same security as Option 1)
+- Or use `DEEPSITE_AUTH_COOKIE` if you prefer session-based auth
+- All authentication data stays on your local machine
+- Better for development and testing
+- No need for both HTTP Server and Local Server - choose one!
+
+---
+
+## Troubleshooting
+
+### "Invalid Hugging Face token"
+- Verify your token at https://huggingface.co/settings/tokens
+- Ensure the token has write permissions
+- Check that you copied the full token (starts with `hf_`)
+
+### "At least one page is required"
+- Make sure you're providing the `pages` array
+- Each page must have both `path` and `html` properties
+
+### "Failed to create project"
+- Check your token permissions
+- Ensure the project title doesn't conflict with existing spaces
+- Verify your Hugging Face account is in good standing
+
+### Claude doesn't see the tool
+- Restart Claude Desktop after modifying the config
+- Check that the JSON config is valid (no trailing commas)
+- For HTTP: verify the URL is correct
+- For local: check the absolute path to index.js
+
+---
+
+## Architecture
+
+### HTTP Server Flow
+```
+Claude Desktop
+ β
+ (HTTP Request)
+ β
+huggingface.co/deepsite/api/mcp
+ β
+Hugging Face API (with user's token)
+ β
+New Space Created
+ β
+URLs returned to Claude
+```
+
+### Local Server Flow
+```
+Claude Desktop
+ β
+ (stdio transport)
+ β
+Local MCP Server
+ β
+ (HTTP to DeepSite API)
+ β
+huggingface.co/deepsite/api/me/projects
+ β
+New Space Created
+```
+
+---
+
+## Contributing
+
+The MCP server implementation lives in:
+- HTTP Server: `/app/api/mcp/route.ts`
+- Local Server: `/mcp-server/index.ts`
+
+Both use the same core DeepSite logic for creating projects - no duplication!
+
+---
+
+## License
+
+MIT
+
+---
+
+## Resources
+
+- [Model Context Protocol Spec](https://modelcontextprotocol.io/)
+- [DeepSite Documentation](https://huggingface.co/deepsite)
+- [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces)
+- [Claude Desktop](https://claude.ai/desktop)
+
diff --git a/README.md b/README.md
index 6b13fd8b95aeb699f979cbc2cbb8d7495aeacdf5..9154f9026a5ccedb6d9aa26c2b34462372d7741b 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,26 @@
---
-title: DeepSite v4
+title: DeepSite v3
emoji: π³
colorFrom: blue
colorTo: blue
sdk: docker
pinned: true
-app_port: 3001
+app_port: 3000
license: mit
failure_strategy: rollback
-short_description: Generate any application by Vibe Coding it
+short_description: Generate any application by Vibe Coding
models:
- deepseek-ai/DeepSeek-V3-0324
- - deepseek-ai/DeepSeek-V3.2
- - Qwen/Qwen3-Coder-30B-A3B-Instruct
+ - deepseek-ai/DeepSeek-R1-0528
+ - deepseek-ai/DeepSeek-V3.1
+ - deepseek-ai/DeepSeek-V3.1-Terminus
+ - deepseek-ai/DeepSeek-V3.2-Exp
+ - Qwen/Qwen3-Coder-480B-A35B-Instruct
+ - moonshotai/Kimi-K2-Instruct
- moonshotai/Kimi-K2-Instruct-0905
- - zai-org/GLM-4.7
- - MiniMaxAI/MiniMax-M2.1
+ - zai-org/GLM-4.6
+ - MiniMaxAI/MiniMax-M2
+ - moonshotai/Kimi-K2-Thinking
---
# DeepSite π³
diff --git a/actions/mentions.ts b/actions/mentions.ts
deleted file mode 100644
index c6b7dabfbaa6f0efe0d492dfe5470353b0139bae..0000000000000000000000000000000000000000
--- a/actions/mentions.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import { File } from "@/lib/type";
-
-export const searchMentions = async (query: string) => {
- const promises = [searchModels(query), searchDatasets(query)];
- const results = await Promise.all(promises);
- return { models: results[0], datasets: results[1] };
-};
-
-const searchModels = async (query: string) => {
- const response = await fetch(
- `https://huggingface.co/api/quicksearch?q=${query}&type=model&limit=3`
- );
- const data = await response.json();
- return data?.models ?? [];
-};
-
-const searchDatasets = async (query: string) => {
- const response = await fetch(
- `https://huggingface.co/api/quicksearch?q=${query}&type=dataset&limit=3`
- );
- const data = await response.json();
- return data?.datasets ?? [];
-};
-
-export const searchFilesMentions = async (query: string, files: File[]) => {
- if (!query) return files;
- const lowerQuery = query.toLowerCase();
- return files.filter((file) => file.path.toLowerCase().includes(lowerQuery));
-};
diff --git a/actions/projects.ts b/actions/projects.ts
deleted file mode 100644
index d6f685b63290792fc597438a53eab0a14d6bbf8e..0000000000000000000000000000000000000000
--- a/actions/projects.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-"use server";
-import {
- downloadFile,
- listCommits,
- listFiles,
- listSpaces,
- RepoDesignation,
- SpaceEntry,
- spaceInfo,
-} from "@huggingface/hub";
-
-import { auth } from "@/lib/auth";
-import { Commit, File } from "@/lib/type";
-
-export interface ProjectWithCommits extends SpaceEntry {
- commits?: Commit[];
- medias?: string[];
-}
-
-const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"];
-const IGNORED_FORMATS = [
- ".png",
- ".jpg",
- ".jpeg",
- ".gif",
- ".svg",
- ".webp",
- ".mp4",
- ".mp3",
- ".wav",
-];
-
-export const getProjects = async () => {
- const projects: SpaceEntry[] = [];
- const session = await auth();
- if (!session?.user) {
- return projects;
- }
- const token = session.accessToken;
- for await (const space of listSpaces({
- accessToken: token,
- additionalFields: ["author", "cardData"],
- search: {
- owner: session.user.username,
- },
- })) {
- if (
- space.sdk === "static" &&
- Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
- (space.cardData as { tags?: string[] })?.tags?.some((tag) =>
- tag.includes("deepsite")
- )
- ) {
- projects.push(space);
- }
- }
- return projects;
-};
-export const getProject = async (id: string, commitId?: string) => {
- const session = await auth();
- if (!session?.user) {
- return null;
- }
- const token = session.accessToken;
- try {
- const project: ProjectWithCommits | null = await spaceInfo({
- name: id,
- accessToken: token,
- additionalFields: ["author", "cardData"],
- });
- const repo: RepoDesignation = {
- type: "space",
- name: id,
- };
- const files: File[] = [];
- const medias: string[] = [];
- const params = { repo, accessToken: token };
- if (commitId) {
- Object.assign(params, { revision: commitId });
- }
- for await (const fileInfo of listFiles(params)) {
- if (IGNORED_PATHS.includes(fileInfo.path)) continue;
- if (IGNORED_FORMATS.some((format) => fileInfo.path.endsWith(format))) {
- medias.push(
- `https://huggingface.co/spaces/${id}/resolve/main/${fileInfo.path}`
- );
- continue;
- }
-
- if (fileInfo.type === "directory") {
- for await (const subFile of listFiles({
- repo,
- accessToken: token,
- path: fileInfo.path,
- })) {
- if (IGNORED_FORMATS.some((format) => subFile.path.endsWith(format))) {
- medias.push(
- `https://huggingface.co/spaces/${id}/resolve/main/${subFile.path}`
- );
- }
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: subFile.path,
- raw: true,
- ...(commitId ? { revision: commitId } : {}),
- }).catch((_) => {
- return null;
- });
- if (!blob) {
- continue;
- }
- const html = await blob?.text();
- if (!html) {
- continue;
- }
- files[subFile.path === "index.html" ? "unshift" : "push"]({
- path: subFile.path,
- content: html,
- });
- }
- } else {
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: fileInfo.path,
- raw: true,
- ...(commitId ? { revision: commitId } : {}),
- }).catch((_) => {
- return null;
- });
- if (!blob) {
- continue;
- }
- const html = await blob?.text();
- if (!html) {
- continue;
- }
- files[fileInfo.path === "index.html" ? "unshift" : "push"]({
- path: fileInfo.path,
- content: html,
- });
- }
- }
- const commits: Commit[] = [];
- const commitIterator = listCommits({ repo, accessToken: token });
- for await (const commit of commitIterator) {
- if (
- commit.title?.toLowerCase() === "initial commit" ||
- commit.title
- ?.toLowerCase()
- ?.includes("upload media files through deepsite")
- )
- continue;
- commits.push({
- title: commit.title,
- oid: commit.oid,
- date: commit.date,
- });
- if (commits.length >= 20) {
- break;
- }
- }
-
- project.commits = commits;
- project.medias = medias;
-
- return { project, files };
- } catch (error) {
- return {
- project: null,
- files: [],
- };
- }
-};
diff --git a/app/(public)/layout.tsx b/app/(public)/layout.tsx
deleted file mode 100644
index 0eb2c8fbb85b745d5be01c9e79fa0ebc93a71d00..0000000000000000000000000000000000000000
--- a/app/(public)/layout.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Navigation } from "@/components/public/navigation";
-
-export default function PublicLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return (
-
-
- {children}
-
- );
-}
diff --git a/app/(public)/page.tsx b/app/(public)/page.tsx
deleted file mode 100644
index 86970b7a3dab7e60cce3bbb68c747b525baba45c..0000000000000000000000000000000000000000
--- a/app/(public)/page.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { AnimatedDotsBackground } from "@/components/public/animated-dots-background";
-import { HeroHeader } from "@/components/public/hero-header";
-import { UserProjects } from "@/components/projects/user-projects";
-import { AskAiLanding } from "@/components/ask-ai/ask-ai-landing";
-import { Bento } from "@/components/public/bento";
-
-export const dynamic = "force-dynamic";
-
-export default async function Homepage() {
- return (
- <>
-
-
-
- >
- );
-}
diff --git a/app/(public)/signin/page.tsx b/app/(public)/signin/page.tsx
deleted file mode 100644
index 3200d0755372c79be57b9204331695cd8f0499ac..0000000000000000000000000000000000000000
--- a/app/(public)/signin/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { LoginButtons } from "@/components/login/login-buttons";
-
-export default async function SignInPage({
- searchParams,
-}: {
- searchParams: Promise<{ callbackUrl: string }>;
-}) {
- const { callbackUrl } = await searchParams;
- console.log(callbackUrl);
- return (
-
-
-
You shall not pass π§
-
- You can't access this resource without being signed in.
-
-
-
-
- );
-}
diff --git a/app/[owner]/[repoId]/page.tsx b/app/[owner]/[repoId]/page.tsx
deleted file mode 100644
index 5082396059ae126dbc2be8e0daf30d1ff014f922..0000000000000000000000000000000000000000
--- a/app/[owner]/[repoId]/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { getProject } from "@/actions/projects";
-import { AppEditor } from "@/components/editor";
-import { auth } from "@/lib/auth";
-import { notFound, redirect } from "next/navigation";
-
-export default async function ProjectPage({
- params,
- searchParams,
-}: {
- params: Promise<{ owner: string; repoId: string }>;
- searchParams: Promise<{ commit?: string }>;
-}) {
- const session = await auth();
-
- const { owner, repoId } = await params;
- const { commit } = await searchParams;
- if (!session) {
- redirect(
- `/api/auth/signin?callbackUrl=/${owner}/${repoId}${
- commit ? `?commit=${commit}` : ""
- }`
- );
- }
- const datas = await getProject(`${owner}/${repoId}`, commit);
- if (!datas?.project) {
- return notFound();
- }
- return (
-
- );
-}
diff --git a/app/api/ask/route.ts b/app/api/ask/route.ts
deleted file mode 100644
index 293e8244308cc1191e32b3ef6b30e93ccb963fcf..0000000000000000000000000000000000000000
--- a/app/api/ask/route.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { NextResponse } from "next/server";
-import { InferenceClient } from "@huggingface/inference";
-
-import { FOLLOW_UP_SYSTEM_PROMPT, INITIAL_SYSTEM_PROMPT } from "@/lib/prompts";
-import { auth } from "@/lib/auth";
-import { File, Message } from "@/lib/type";
-import { DEFAULT_MODEL, MODELS } from "@/lib/providers";
-
-export async function POST(request: Request) {
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const {
- prompt,
- previousMessages = [],
- files = [],
- provider,
- model,
- redesignMd,
- medias,
- } = body;
-
- if (!prompt) {
- return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
- }
- if (!model || !MODELS.find((m: (typeof MODELS)[0]) => m.value === model)) {
- return NextResponse.json({ error: "Model is required" }, { status: 400 });
- }
-
- const client = new InferenceClient(token);
-
- try {
- const encoder = new TextEncoder();
- const stream = new TransformStream();
- const writer = stream.writable.getWriter();
-
- const response = new NextResponse(stream.readable, {
- headers: {
- "Content-Type": "text/plain; charset=utf-8",
- "Cache-Control": "no-cache",
- Connection: "keep-alive",
- },
- });
- (async () => {
- let hasRetried = false;
- let currentModel = model;
-
- const tryGeneration = async (): Promise => {
- try {
- const chatCompletion = client.chatCompletionStream({
- model: currentModel + (provider !== "auto" ? `:${provider}` : ""),
- messages: [
- {
- role: "system",
- content:
- files.length > 0
- ? FOLLOW_UP_SYSTEM_PROMPT
- : INITIAL_SYSTEM_PROMPT,
- },
- ...previousMessages.map((message: Message) => ({
- role: message.role,
- content: message.content,
- })),
- ...(files?.length > 0
- ? [
- {
- role: "user",
- content: `Here are the files that the user has provider:${files
- .map(
- (file: File) =>
- `File: ${file.path}\nContent: ${file.content}`
- )
- .join("\n")}\n\n${prompt}`,
- },
- ]
- : []),
- {
- role: "user",
- content: `${
- redesignMd?.url &&
- `Redesign the following website ${redesignMd.url}, try to use the same images and content, but you can still improve it if needed. Do the best version possibile. Here is the markdown:\n ${redesignMd.md} \n\n`
- }${prompt} ${
- medias && medias.length > 0
- ? `\nHere is the list of my media files: ${medias.join(
- ", "
- )}\n`
- : ""
- }`,
- }
- ],
- stream: true,
- max_tokens: 16_000,
- });
- while (true) {
- const { done, value } = await chatCompletion.next();
- if (done) {
- break;
- }
-
- const chunk = value.choices[0]?.delta?.content;
- if (chunk) {
- await writer.write(encoder.encode(chunk));
- }
- }
-
- await writer.close();
- } catch (error) {
- const errorMessage =
- error instanceof Error
- ? error.message
- : "An error occurred while processing your request";
-
- if (
- !hasRetried &&
- errorMessage?.includes(
- "Failed to perform inference: Model not found"
- )
- ) {
- hasRetried = true;
- if (model === DEFAULT_MODEL) {
- const availableFallbackModels = MODELS.filter(
- (m) => m.value !== model
- );
- const randomIndex = Math.floor(
- Math.random() * availableFallbackModels.length
- );
- currentModel = availableFallbackModels[randomIndex];
- } else {
- currentModel = DEFAULT_MODEL;
- }
- const switchMessage = `\n\n_Note: The selected model was not available. Switched to \`${currentModel}\`._\n\n`;
- await writer.write(encoder.encode(switchMessage));
-
- return tryGeneration();
- }
-
- try {
- let errorPayload = "";
- if (
- errorMessage?.includes("exceeded your monthly included credits") ||
- errorMessage?.includes("reached the free monthly usage limit")
- ) {
- errorPayload = JSON.stringify({
- messageError: errorMessage,
- showProMessage: true,
- isError: true,
- });
- } else {
- errorPayload = JSON.stringify({
- messageError: errorMessage,
- isError: true,
- });
- }
- await writer.write(encoder.encode(`\n\n__ERROR__:${errorPayload}`));
- await writer.close();
- } catch (closeError) {
- console.error("Failed to send error message:", closeError);
- try {
- await writer.abort(error);
- } catch (abortError) {
- console.error("Failed to abort writer:", abortError);
- }
- }
- }
- };
-
- await tryGeneration();
- })();
-
- return response;
- } catch (error) {
- return NextResponse.json(
- {
- error: error instanceof Error ? error.message : "Internal Server Error",
- },
- { status: 500 }
- );
- }
-}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
deleted file mode 100644
index 7b38c1bb45e38527a2c595c07df5e70d38ada7d9..0000000000000000000000000000000000000000
--- a/app/api/auth/[...nextauth]/route.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import NextAuth from "next-auth";
-import { authOptions } from "@/lib/auth";
-
-const handler = NextAuth(authOptions);
-
-export { handler as GET, handler as POST };
diff --git a/app/api/healthcheck/route.ts b/app/api/healthcheck/route.ts
deleted file mode 100644
index 35adb415d0f577a7a78dbc338bd6450e78cc9ec3..0000000000000000000000000000000000000000
--- a/app/api/healthcheck/route.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { NextResponse } from "next/server";
-
-export async function GET() {
- return NextResponse.json({ status: "ok" }, { status: 200 });
-}
diff --git a/app/api/projects/[repoId]/[commitId]/route.ts b/app/api/projects/[repoId]/[commitId]/route.ts
deleted file mode 100644
index 0362d6c43ec226bf97837f2f88196ad43eb056f2..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/[commitId]/route.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { auth } from "@/lib/auth";
-import { createBranch, RepoDesignation } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function POST(
- request: Request,
- { params }: { params: Promise<{ repoId: string; commitId: string }> }
-) {
- const { repoId, commitId }: { repoId: string; commitId: string } =
- await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const commitTitle = `π ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Set commit ${commitId} as default.`;
-
- await fetch(
- `https://huggingface.co/api/spaces/${session.user?.username}/${repoId}/branch/main`,
- {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- startingPoint: commitId,
- overwrite: true,
- }),
- }
- ).catch((error) => {
- return NextResponse.json(
- { error: error ?? "Failed to create branch" },
- { status: 500 }
- );
- });
-
- return NextResponse.json({ success: true }, { status: 200 });
-}
diff --git a/app/api/projects/[repoId]/download/route.ts b/app/api/projects/[repoId]/download/route.ts
deleted file mode 100644
index 43e8f6638d5eb2aca8ba3069d696b05056d96633..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/download/route.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { auth } from "@/lib/auth";
-import { downloadFile, listFiles, RepoDesignation } from "@huggingface/hub";
-import { NextResponse } from "next/server";
-import JSZip from "jszip";
-
-export async function GET(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- try {
- const zip = new JSZip();
- for await (const fileInfo of listFiles({
- repo,
- accessToken: token as string,
- recursive: true,
- })) {
- if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) {
- continue;
- }
-
- try {
- const blob = await downloadFile({
- repo,
- accessToken: token as string,
- path: fileInfo.path,
- raw: true
- }).catch((error) => {
- return null;
- });
- if (!blob) {
- continue;
- }
-
- if (blob) {
- const arrayBuffer = await blob.arrayBuffer();
- zip.file(fileInfo.path, arrayBuffer);
- }
- } catch (error) {
- console.error(`Error downloading file ${fileInfo.path}:`, error);
- }
- }
-
- const zipBlob = await zip.generateAsync({
- type: "blob",
- compression: "DEFLATE",
- compressionOptions: {
- level: 6
- }
- });
-
- const projectName = `${session.user?.username}-${repoId}`.replace(/[^a-zA-Z0-9-_]/g, '_');
- const filename = `${projectName}.zip`;
-
- return new NextResponse(zipBlob, {
- headers: {
- "Content-Type": "application/zip",
- "Content-Disposition": `attachment; filename="${filename}"`,
- "Content-Length": zipBlob.size.toString(),
- },
- });
- } catch (error) {
- console.error("Error downloading project:", error);
- return NextResponse.json({ error: "Failed to download project" }, { status: 500 });
- }
-}
\ No newline at end of file
diff --git a/app/api/projects/[repoId]/medias/route.ts b/app/api/projects/[repoId]/medias/route.ts
deleted file mode 100644
index 2040623536ab6775152177a317e0b11dc6cae3a2..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/medias/route.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { auth } from "@/lib/auth";
-import { RepoDesignation, uploadFiles } from "@huggingface/hub";
-import { NextResponse } from "next/server";
-
-export async function POST(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const formData = await request.formData();
- const newMedias = formData.getAll("images") as File[];
-
- const filesToUpload: File[] = [];
-
- if (!newMedias || newMedias.length === 0) {
- return NextResponse.json(
- {
- ok: false,
- error: "At least one media file is required under the 'images' key",
- },
- { status: 400 }
- );
- }
-
- try {
- for (const media of newMedias) {
- const isImage = media.type.startsWith("image/");
- const isVideo = media.type.startsWith("video/");
- const isAudio = media.type.startsWith("audio/");
-
- const folderPath = isImage
- ? "images/"
- : isVideo
- ? "videos/"
- : isAudio
- ? "audios/"
- : null;
-
- if (!folderPath) {
- return NextResponse.json(
- { ok: false, error: "Unsupported media type: " + media.type },
- { status: 400 }
- );
- }
-
- const mediaName = `${folderPath}${media.name}`;
- const processedFile = new File([media], mediaName, { type: media.type });
- filesToUpload.push(processedFile);
- }
-
- await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token,
- commitTitle: `π Upload media files through DeepSite`,
- });
-
- return NextResponse.json(
- {
- success: true,
- medias: filesToUpload.map(
- (file) =>
- `https://huggingface.co/spaces/${session.user?.username}/${repoId}/resolve/main/${file.name}`
- ),
- },
- { status: 200 }
- );
- } catch (error) {
- return NextResponse.json(
- { ok: false, error: error ?? "Failed to upload media files" },
- { status: 500 }
- );
- }
-
- return NextResponse.json({ success: true }, { status: 200 });
-}
diff --git a/app/api/projects/[repoId]/rename/route.ts b/app/api/projects/[repoId]/rename/route.ts
deleted file mode 100644
index 1d3a00b676effdc7d9106c02e6d7ec40384026c9..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/rename/route.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { auth } from "@/lib/auth";
-import { downloadFile, RepoDesignation, uploadFile } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function PUT(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { newTitle } = body;
-
- if (!newTitle) {
- return NextResponse.json(
- { error: "newTitle is required" },
- { status: 400 }
- );
- }
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const blob = await downloadFile({
- repo,
- accessToken: token,
- path: "README.md",
- raw: true,
- }).catch((_) => {
- return null;
- });
-
- if (!blob) {
- return NextResponse.json(
- { error: "Could not fetch README.md" },
- { status: 500 }
- );
- }
-
- const readmeFile = await blob?.text();
- if (!readmeFile) {
- return NextResponse.json(
- { error: "Could not read README.md content" },
- { status: 500 }
- );
- }
-
- // Escape YAML values to prevent injection attacks
- const escapeYamlValue = (value: string): string => {
- if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) {
- return `"${value.replace(/"/g, '\\"')}"`;
- }
- return value;
- };
-
- // Escape commit message to prevent injection
- const escapeCommitMessage = (message: string): string => {
- return message.replace(/[\r\n]/g, " ").slice(0, 200);
- };
-
- const updatedReadmeFile = readmeFile.replace(
- /^title:\s*(.*)$/m,
- `title: ${escapeYamlValue(newTitle)}`
- );
-
- await uploadFile({
- repo,
- accessToken: token,
- file: new File([updatedReadmeFile], "README.md", { type: "text/markdown" }),
- commitTitle: escapeCommitMessage(
- `π³ ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Rename project to "${newTitle}"`
- ),
- });
-
- return NextResponse.json(
- {
- success: true,
- },
- { status: 200 }
- );
-}
diff --git a/app/api/projects/[repoId]/route.ts b/app/api/projects/[repoId]/route.ts
deleted file mode 100644
index e6daee03652b083d7f1d852f7367d7f55fd3f390..0000000000000000000000000000000000000000
--- a/app/api/projects/[repoId]/route.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { auth } from "@/lib/auth";
-import { RepoDesignation, deleteRepo, uploadFiles } from "@huggingface/hub";
-import { format } from "date-fns";
-import { NextResponse } from "next/server";
-
-export async function PUT(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { files, prompt, isManualChanges } = body;
-
- if (!files) {
- return NextResponse.json({ error: "Files are required" }, { status: 400 });
- }
-
- if (!prompt) {
- return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
- }
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- const filesToUpload: File[] = [];
- for (const file of files) {
- let mimeType = "text/x-python";
- if (file.path.endsWith(".txt")) {
- mimeType = "text/plain";
- } else if (file.path.endsWith(".md")) {
- mimeType = "text/markdown";
- } else if (file.path.endsWith(".json")) {
- mimeType = "application/json";
- }
- filesToUpload.push(new File([file.content], file.path, { type: mimeType }));
- }
- // Escape commit title to prevent injection
- const escapeCommitTitle = (title: string): string => {
- return title.replace(/[\r\n]/g, " ").slice(0, 200);
- };
-
- const baseTitle = isManualChanges
- ? ""
- : `π³ ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `;
- const commitTitle = escapeCommitTitle(
- baseTitle + (prompt ?? "Follow-up DeepSite commit")
- );
- const response = await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token,
- commitTitle,
- });
-
- return NextResponse.json(
- {
- success: true,
- commit: {
- oid: response.commit,
- title: commitTitle,
- date: new Date(),
- },
- },
- { status: 200 }
- );
-}
-
-export async function DELETE(
- request: Request,
- { params }: { params: Promise<{ repoId: string }> }
-) {
- const { repoId }: { repoId: string } = await params;
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + repoId,
- };
-
- try {
- await deleteRepo({
- repo,
- accessToken: token as string,
- });
-
- return NextResponse.json({ success: true }, { status: 200 });
- } catch (error) {
- const errMsg =
- error instanceof Error ? error.message : "Failed to delete project";
- return NextResponse.json({ error: errMsg }, { status: 500 });
- }
-}
diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts
deleted file mode 100644
index b70c310b4167077d4d8bcd15423defb1d158e2dd..0000000000000000000000000000000000000000
--- a/app/api/projects/route.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { NextResponse } from "next/server";
-import { RepoDesignation, createRepo, uploadFiles } from "@huggingface/hub";
-
-import { auth } from "@/lib/auth";
-import {
- COLORS,
- EMOJIS_FOR_SPACE,
- injectDeepSiteBadge,
- isIndexPage,
-} from "@/lib/utils";
-
-// todo: catch error while publishing project, and return the error to the user
-// if space has been created, but can't push, try again or catch well the error and return the error to the user
-
-export async function POST(request: Request) {
- const session = await auth();
- if (!session) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- const token = session.accessToken;
-
- const body = await request.json();
- const { projectTitle, files, prompt } = body;
-
- if (!files) {
- return NextResponse.json(
- { error: "Project title and files are required" },
- { status: 400 }
- );
- }
-
- const title =
- projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project";
-
- let formattedTitle = title
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, "-")
- .split("-")
- .filter(Boolean)
- .join("-")
- .slice(0, 75);
-
- formattedTitle =
- formattedTitle + "-" + Math.random().toString(36).substring(2, 7);
-
- const repo: RepoDesignation = {
- type: "space",
- name: session.user?.username + "/" + formattedTitle,
- };
-
- // Escape YAML values to prevent injection attacks
- const escapeYamlValue = (value: string): string => {
- if (/[:|>]|^[-*#]|^\s|['"]/.test(value) || value.includes("\n")) {
- return `"${value.replace(/"/g, '\\"')}"`;
- }
- return value;
- };
-
- // Escape markdown headers to prevent injection
- const escapeMarkdownHeader = (value: string): string => {
- return value.replace(/^#+\s*/g, "").replace(/\n/g, " ");
- };
-
- const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
- const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
- const emoji =
- EMOJIS_FOR_SPACE[Math.floor(Math.random() * EMOJIS_FOR_SPACE.length)];
- const README = `---
-title: ${escapeYamlValue(projectTitle)}
-colorFrom: ${colorFrom}
-colorTo: ${colorTo}
-sdk: static
-emoji: ${emoji}
-tags:
- - deepsite-v4
----
-
-# ${escapeMarkdownHeader(title)}
-
-This project has been created with [DeepSite](https://deepsite.hf.co) AI Vibe Coding.
-`;
-
- const filesToUpload: File[] = [
- new File([README], "README.md", { type: "text/markdown" }),
- ];
- for (const file of files) {
- let mimeType = "text/html";
- if (file.path.endsWith(".css")) {
- mimeType = "text/css";
- } else if (file.path.endsWith(".js")) {
- mimeType = "text/javascript";
- }
- const content =
- mimeType === "text/html" && isIndexPage(file.path)
- ? injectDeepSiteBadge(file.content)
- : file.content;
-
- filesToUpload.push(new File([content], file.path, { type: mimeType }));
- }
-
- let repoUrl: string | undefined;
-
- try {
- // Create the space first
- const createResult = await createRepo({
- accessToken: token as string,
- repo: repo,
- sdk: "static",
- });
- repoUrl = createResult.repoUrl;
-
- // Escape commit message to prevent injection
- const escapeCommitMessage = (message: string): string => {
- return message.replace(/[\r\n]/g, " ").slice(0, 200);
- };
- const commitMessage = escapeCommitMessage(prompt ?? "Initial DeepSite commit");
-
- // Upload files to the created space
- await uploadFiles({
- repo,
- files: filesToUpload,
- accessToken: token as string,
- commitTitle: commitMessage,
- });
-
- const path = repoUrl.split("/").slice(-2).join("/");
-
- return NextResponse.json({ repoUrl: path }, { status: 200 });
- } catch (error) {
- const errMsg =
- error instanceof Error ? error.message : "Failed to create or upload to space";
-
- // If space was created but upload failed, include the repo URL in the error
- if (repoUrl) {
- const path = repoUrl.split("/").slice(-2).join("/");
- return NextResponse.json({
- error: `${errMsg}. Space was created at ${path} but files could not be uploaded.`,
- repoUrl: path,
- partialSuccess: true
- }, { status: 500 });
- }
-
- return NextResponse.json({ error: errMsg }, { status: 500 });
- }
-}
diff --git a/app/api/redesign/route.ts b/app/api/redesign/route.ts
deleted file mode 100644
index 6b898d6fd364c5f3ef267706b62c37ee559e19cd..0000000000000000000000000000000000000000
--- a/app/api/redesign/route.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { NextRequest, NextResponse } from "next/server";
-
-const FETCH_TIMEOUT = 30_000;
-export const maxDuration = 60;
-
-export async function PUT(request: NextRequest) {
- const body = await request.json();
- const { url } = body;
-
- if (!url) {
- return NextResponse.json({ error: "URL is required" }, { status: 400 });
- }
-
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
-
- try {
- const response = await fetch(
- `https://r.jina.ai/${encodeURIComponent(url)}`,
- {
- method: "POST",
- signal: controller.signal,
- }
- );
-
- clearTimeout(timeoutId);
-
- if (!response.ok) {
- return NextResponse.json(
- { error: "Failed to fetch redesign" },
- { status: 500 }
- );
- }
- const markdown = await response.text();
- return NextResponse.json(
- {
- ok: true,
- markdown,
- },
- { status: 200 }
- );
- } catch (fetchError: any) {
- clearTimeout(timeoutId);
-
- if (fetchError.name === "AbortError") {
- return NextResponse.json(
- {
- error:
- "Request timeout: The external service took too long to respond. Please try again.",
- },
- { status: 504 }
- );
- }
- throw fetchError;
- }
- } catch (error: any) {
- if (error.name === "AbortError" || error.message?.includes("timeout")) {
- return NextResponse.json(
- {
- error:
- "Request timeout: The external service took too long to respond. Please try again.",
- },
- { status: 504 }
- );
- }
- return NextResponse.json(
- { error: error.message || "An error occurred" },
- { status: 500 }
- );
- }
-}
diff --git a/app/favicon.ico b/app/favicon.ico
deleted file mode 100644
index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000
Binary files a/app/favicon.ico and /dev/null differ
diff --git a/app/globals.css b/app/globals.css
deleted file mode 100644
index 97005be73ce05b7bb1a207464af1945139fde62b..0000000000000000000000000000000000000000
--- a/app/globals.css
+++ /dev/null
@@ -1,168 +0,0 @@
-@import "tailwindcss";
-@import "tw-animate-css";
-
-@custom-variant dark (&:is(.dark *));
-
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
- --color-sidebar-ring: var(--sidebar-ring);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar: var(--sidebar);
- --color-chart-5: var(--chart-5);
- --color-chart-4: var(--chart-4);
- --color-chart-3: var(--chart-3);
- --color-chart-2: var(--chart-2);
- --color-chart-1: var(--chart-1);
- --color-ring: var(--ring);
- --color-input: var(--input);
- --color-border: var(--border);
- --color-destructive: var(--destructive);
- --color-accent-foreground: var(--accent-foreground);
- --color-accent: var(--accent);
- --color-muted-foreground: var(--muted-foreground);
- --color-muted: var(--muted);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-secondary: var(--secondary);
- --color-primary-foreground: var(--primary-foreground);
- --color-primary: var(--primary);
- --color-popover-foreground: var(--popover-foreground);
- --color-popover: var(--popover);
- --color-card-foreground: var(--card-foreground);
- --color-card: var(--card);
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
-}
-
-:root {
- --radius: 0.65rem;
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --radius: 0.625rem;
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
-}
-
-.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
-}
-
-@layer base {
- * {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
-}
-
-.monaco-editor .margin {
- @apply bg-background!;
-}
-.monaco-editor .monaco-editor-background {
- @apply bg-background!;
-}
-.monaco-editor .decorationsOverviewRuler {
- @apply opacity-0!;
-}
-.monaco-editor .view-line {
- /* @apply bg-primary/50!; */
-}
-.monaco-editor .scroll-decoration {
- @apply opacity-0!;
-}
-.monaco-editor .cursors-layer .cursor {
- @apply bg-primary!;
-}
-
-.content-placeholder::before {
- content: attr(data-placeholder);
- position: absolute;
- pointer-events: none;
- opacity: 0.5;
- @apply top-5 left-6;
-}
-
-.sp-layout
- .sp-file-explorer
- .sp-file-explorer-list
- .sp-explorer[data-active="true"] {
- @apply text-indigo-500!;
-}
-
-.sp-layout
- .sp-stack
- .sp-tabs
- .sp-tab-container[aria-selected="true"]
- .sp-tab-button {
- @apply text-indigo-500!;
-}
-.sp-layout .sp-stack .sp-tabs .sp-tab-container:has(button:focus) {
- @apply outline-none! border-none!;
-}
diff --git a/app/layout.tsx b/app/layout.tsx
deleted file mode 100644
index 469b6844c7b63d85ad5e9c7ba420add2f7f1125c..0000000000000000000000000000000000000000
--- a/app/layout.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import type { Metadata, Viewport } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import { NextStepProvider } from "nextstepjs";
-import Script from "next/script";
-
-import "@/app/globals.css";
-import { ThemeProvider } from "@/components/providers/theme";
-import { AuthProvider } from "@/components/providers/session";
-import { Toaster } from "@/components/ui/sonner";
-import { ReactQueryProvider } from "@/components/providers/react-query";
-import { generateSEO, generateStructuredData } from "@/lib/seo";
-import { NotAuthorizedDomain } from "@/components/not-authorized";
-
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
-
-export const metadata: Metadata = {
- ...generateSEO({
- title: "DeepSite | Build with AI β¨",
- description:
- "DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
- path: "/",
- }),
- appleWebApp: {
- capable: true,
- title: "DeepSite",
- statusBarStyle: "black-translucent",
- },
- icons: {
- icon: "/logo.svg",
- shortcut: "/logo.svg",
- apple: "/logo.svg",
- },
- verification: {
- google: process.env.GOOGLE_SITE_VERIFICATION,
- },
-};
-
-export const viewport: Viewport = {
- initialScale: 1,
- maximumScale: 1,
- themeColor: "#4f46e5",
-};
-
-export default async function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- const structuredData = generateStructuredData("WebApplication", {
- name: "DeepSite",
- description: "Build websites with AI, no code required",
- url: "https://deepsite.hf.co",
- });
- const organizationData = generateStructuredData("Organization", {
- name: "DeepSite",
- url: "https://deepsite.hf.co",
- });
-
- return (
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
- );
-}
diff --git a/app/new/page.tsx b/app/new/page.tsx
deleted file mode 100644
index 920e799168597ccab9e644171bff837961cc45e6..0000000000000000000000000000000000000000
--- a/app/new/page.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { AppEditor } from "@/components/editor";
-import { auth } from "@/lib/auth";
-import { redirect } from "next/navigation";
-
-export default async function NewProjectPage({
- searchParams,
-}: {
- searchParams: Promise<{ prompt: string }>;
-}) {
- const session = await auth();
-
- if (!session) {
- redirect("/api/auth/signin?callbackUrl=/new");
- }
-
- const { prompt } = await searchParams;
- return ;
-}
diff --git a/app/not-found.tsx b/app/not-found.tsx
deleted file mode 100644
index ae102854e6245bcc2f0d55780d9419b2679d6f38..0000000000000000000000000000000000000000
--- a/app/not-found.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { NotFoundButtons } from "@/components/not-found/buttons";
-import { Navigation } from "@/components/public/navigation";
-
-export default function NotFound() {
- return (
-
-
-
-
Oh no! Page not found.
-
- The page you are looking for does not exist.
-
-
-
-
- );
-}
diff --git a/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 (
-
-
-
-
-
- );
-}
diff --git a/components/ask-ai/context.tsx b/components/ask-ai/context.tsx
deleted file mode 100644
index c22f17fc81f95788b6d3e690cc70ac5d3b6f817a..0000000000000000000000000000000000000000
--- a/components/ask-ai/context.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { AtSign, Braces, FileCode, FileText, X } from "lucide-react";
-import { useMemo, useState } from "react";
-import { useQueryClient } from "@tanstack/react-query";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { File } from "@/lib/type";
-import { cn } from "@/lib/utils";
-
-export const Context = ({
- files,
- setFiles,
-}: {
- files: File[];
- setFiles: (files: File[]) => void;
-}) => {
- const queryClient = useQueryClient();
- const [open, setOpen] = useState(false);
-
- const getFileIcon = (filePath: string, size = "size-3.5") => {
- if (filePath.endsWith(".css")) {
- return ;
- } else if (filePath.endsWith(".js")) {
- return ;
- } else if (filePath.endsWith(".json")) {
- return ;
- } else {
- return ;
- }
- };
-
- const getFiles = () => queryClient.getQueryData(["files"]) ?? [];
-
- return (
-
-
-
-
-
-
-
-
- Select a file to send as context
-
-
- {getFiles().length === 0 ? (
-
- No files available
-
- ) : (
- <>
-
- {getFiles()?.map((page) => (
-
- ))}
- >
- )}
-
-
-
-
- {files?.map((file) => (
-
- ))}
-
- );
-};
diff --git a/components/ask-ai/input-mentions.tsx b/components/ask-ai/input-mentions.tsx
deleted file mode 100644
index 4ef11b0a0cd9aeaef85eddfd049a30272437b414..0000000000000000000000000000000000000000
--- a/components/ask-ai/input-mentions.tsx
+++ /dev/null
@@ -1,477 +0,0 @@
-import { useRef, useState, useEffect, RefObject } from "react";
-import { useClickAway } from "react-use";
-import { useQueryClient } from "@tanstack/react-query";
-
-import { searchFilesMentions } from "@/actions/mentions";
-import { File } from "@/lib/type";
-import { Braces, FileCode, FileText } from "lucide-react";
-
-export function InputMentions({
- ref,
- prompt,
- files,
- setPrompt,
- redesignMdUrl,
- onSubmit,
- imageLinks,
- setImageLinks,
-}: {
- ref: RefObject;
- prompt: string;
- files?: File[] | null;
- redesignMdUrl?: string;
- setPrompt: (prompt: string) => void;
- onSubmit: () => void;
- imageLinks?: string[];
- setImageLinks?: (links: string[]) => void;
-}) {
- const queryClient = useQueryClient();
- const [showMentionDropdown, setShowMentionDropdown] = useState(false);
- const [, setMentionSearch] = useState("");
- const dropdownRef = useRef(null);
- const [results, setResults] = useState([]);
-
- useClickAway(dropdownRef, () => {
- setShowMentionDropdown(false);
- });
-
- const isImageUrl = (url: string): boolean => {
- // Check if it's a valid HTTP/HTTPS URL with an image extension
- const imageUrlPattern = /^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i;
- return imageUrlPattern.test(url);
- };
-
- const getTextContent = (element: HTMLElement): string => {
- let text = "";
- const childNodes = element.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.TEXT_NODE) {
- text += node.textContent || "";
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("mention-chip")) {
- text += el.getAttribute("data-mention-id") || "";
- } else if (el.classList.contains("image-chip")) {
- // Include image URL in text content for display purposes
- const imageUrl = el.getAttribute("data-image-url") || "";
- text += imageUrl ? ` ${imageUrl} ` : "";
- } else {
- text += el.textContent || "";
- }
- }
- }
- return text + "\u0020";
- };
-
- const extractPromptWithIds = (): string => {
- if (!ref.current) return "";
-
- let text = "";
- const childNodes = ref.current.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.TEXT_NODE) {
- text += node.textContent || "";
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("mention-chip")) {
- text += el.getAttribute("data-mention-id") || "";
- } else if (el.classList.contains("image-chip")) {
- // Include image URL in prompt text
- const imageUrl = el.getAttribute("data-image-url") || "";
- text += imageUrl ? ` ${imageUrl} ` : "";
- } else {
- text += el.textContent || "";
- }
- }
- }
- return text;
- };
-
- const extractImageLinks = (): string[] => {
- if (!ref.current) return [];
-
- const links: string[] = [];
- const childNodes = ref.current.childNodes;
-
- for (let i = 0; i < childNodes.length; i++) {
- const node = childNodes[i];
- if (node.nodeType === Node.ELEMENT_NODE) {
- const el = node as HTMLElement;
- if (el.classList.contains("image-chip")) {
- const imageUrl = el.getAttribute("data-image-url");
- if (imageUrl) links.push(imageUrl);
- }
- }
- }
- return links;
- };
-
- const shouldDetectMention = (): {
- detect: boolean;
- textBeforeCursor: string;
- } => {
- const selection = window.getSelection();
- if (!selection || !ref.current) {
- return { detect: false, textBeforeCursor: "" };
- }
-
- const range = selection.getRangeAt(0);
- const node = range.startContainer;
-
- if (node.nodeType === Node.ELEMENT_NODE) {
- const element = node as HTMLElement;
- if (element.classList?.contains("mention-chip")) {
- return { detect: false, textBeforeCursor: "" };
- }
- }
-
- if (node.parentElement?.classList?.contains("mention-chip")) {
- return { detect: false, textBeforeCursor: "" };
- }
-
- if (node.nodeType === Node.TEXT_NODE) {
- const textContent = node.textContent || "";
- const cursorOffset = range.startOffset;
- const textBeforeCursor = textContent.substring(0, cursorOffset);
-
- const lastAtIndex = textBeforeCursor.lastIndexOf("@");
- if (lastAtIndex !== -1) {
- const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
- if (!textAfterAt.includes(" ")) {
- return { detect: true, textBeforeCursor: textAfterAt };
- }
- }
- }
-
- return { detect: false, textBeforeCursor: "" };
- };
-
- const handleInput = async () => {
- if (!ref.current) return;
- const text = getTextContent(ref.current);
-
- // Only clear if there's no content at all (including chips)
- const hasImageChips = ref.current.querySelectorAll(".image-chip").length > 0;
- const hasMentionChips = ref.current.querySelectorAll(".mention-chip").length > 0;
-
- if (text.trim() === "" && !hasImageChips && !hasMentionChips) {
- ref.current.innerHTML = "";
- }
-
- setPrompt(text);
-
- // Update image links whenever input changes
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
-
- const { detect, textBeforeCursor } = shouldDetectMention();
-
- if (detect && files && files?.length > 0) {
- setMentionSearch(textBeforeCursor);
- setShowMentionDropdown(true);
- const files = queryClient.getQueryData(["files"]) ?? [];
- const results = await searchFilesMentions(textBeforeCursor, files);
- setResults(results);
- } else {
- setShowMentionDropdown(false);
- }
- };
-
- const createMentionChipElement = (mentionId: string): HTMLSpanElement => {
- const mentionChip = document.createElement("span");
-
- mentionChip.className =
- "mention-chip inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400";
- mentionChip.contentEditable = "false";
- mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`);
- mentionChip.textContent = `@${mentionId}`;
-
- return mentionChip;
- };
-
- const createImageChipElement = (imageUrl: string): HTMLSpanElement => {
- const imageChip = document.createElement("span");
-
- imageChip.className =
- "image-chip inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400";
- imageChip.contentEditable = "false";
- imageChip.setAttribute("data-image-url", imageUrl);
-
- // Create icon (using emoji for simplicity)
- const icon = document.createElement("span");
- icon.textContent = "πΌοΈ";
- icon.className = "text-[10px]";
-
- // Truncate URL for display
- const displayUrl =
- imageUrl.length > 30 ? imageUrl.substring(0, 30) + "..." : imageUrl;
- const text = document.createTextNode(displayUrl);
-
- imageChip.appendChild(icon);
- imageChip.appendChild(text);
-
- return imageChip;
- };
-
- const insertMention = (mentionId: string) => {
- if (!ref.current) return;
-
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0) return;
-
- const range = selection.getRangeAt(0);
- const textNode = range.startContainer;
-
- if (textNode.nodeType !== Node.TEXT_NODE) return;
-
- const textContent = textNode.textContent || "";
- const cursorOffset = range.startOffset;
-
- const textBeforeCursor = textContent.substring(0, cursorOffset);
- const lastAtIndex = textBeforeCursor.lastIndexOf("@");
-
- if (lastAtIndex !== -1) {
- const mentionChip = createMentionChipElement(mentionId);
-
- const beforeText = textContent.substring(0, lastAtIndex);
- const afterText = textContent.substring(cursorOffset);
- const parent = textNode.parentNode;
- if (!parent) return;
-
- const beforeNode = beforeText
- ? document.createTextNode(beforeText)
- : null;
- const spaceNode = document.createTextNode("\u0020");
- const afterNode = afterText ? document.createTextNode(afterText) : null;
-
- if (beforeNode) {
- parent.insertBefore(beforeNode, textNode);
- }
- parent.insertBefore(mentionChip, textNode);
- parent.insertBefore(spaceNode, textNode);
- if (afterNode) {
- parent.insertBefore(afterNode, textNode);
- }
-
- parent.removeChild(textNode);
-
- const newRange = document.createRange();
- if (afterNode) {
- newRange.setStart(afterNode, 0);
- } else {
- newRange.setStartAfter(spaceNode);
- }
- newRange.collapse(true);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- const newText = getTextContent(ref.current);
- setPrompt(newText);
- setShowMentionDropdown(false);
- setMentionSearch("");
- }
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (!prompt || prompt.trim() === "") return;
-
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- const promptWithIds = extractPromptWithIds();
- setPrompt(promptWithIds);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
-
- onSubmit();
-
- if (ref.current) {
- ref.current.innerHTML = "";
- }
- setPrompt("");
- setShowMentionDropdown(false);
- } else if (e.key === "Escape") {
- setShowMentionDropdown(false);
- }
- };
-
- useEffect(() => {
- if (ref.current && prompt === "" && ref.current.innerHTML !== "") {
- ref.current.innerHTML = "";
- }
- }, [prompt]);
-
- const handlePaste = (e: React.ClipboardEvent) => {
- e.preventDefault();
- const text = e.clipboardData.getData("text/plain");
-
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0 || !ref.current) {
- document.execCommand("insertText", false, text);
- return;
- }
-
- const trimmedText = text.trim();
-
- // Check if pasted text is ONLY an image URL
- if (isImageUrl(trimmedText)) {
- const range = selection.getRangeAt(0);
- const imageChip = createImageChipElement(trimmedText);
- const spaceNode = document.createTextNode("\u0020");
-
- range.deleteContents();
- range.insertNode(imageChip);
-
- // Insert space after the chip
- const afterChipRange = document.createRange();
- afterChipRange.setStartAfter(imageChip);
- afterChipRange.insertNode(spaceNode);
-
- // Move cursor after the space
- const newRange = document.createRange();
- newRange.setStartAfter(spaceNode);
- newRange.collapse(true);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- // Update state
- const newText = getTextContent(ref.current);
- setPrompt(newText);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
- } else {
- // Check if text contains image URLs
- const imageUrlPattern = /https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?[^\s]*)?/gi;
- const containsImageUrl = imageUrlPattern.test(text);
-
- if (containsImageUrl) {
- // Split text and replace image URLs with chips
- const parts = text.split(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?[^\s]*)?)/gi);
- const range = selection.getRangeAt(0);
- range.deleteContents();
-
- const fragment = document.createDocumentFragment();
-
- parts.forEach((part) => {
- if (isImageUrl(part)) {
- const imageChip = createImageChipElement(part);
- const spaceNode = document.createTextNode("\u0020");
- fragment.appendChild(imageChip);
- fragment.appendChild(spaceNode);
- } else if (part && !part.match(/^\?[^\s]*$/)) {
- // Skip query string matches, add other text
- const textNode = document.createTextNode(part);
- fragment.appendChild(textNode);
- }
- });
-
- range.insertNode(fragment);
-
- // Move cursor to end
- const newRange = document.createRange();
- newRange.selectNodeContents(ref.current);
- newRange.collapse(false);
- selection.removeAllRanges();
- selection.addRange(newRange);
-
- // Update state
- const newText = getTextContent(ref.current);
- setPrompt(newText);
-
- if (setImageLinks) {
- const links = extractImageLinks();
- setImageLinks(links);
- }
- } else {
- // Plain text, use default insertion
- document.execCommand("insertText", false, text);
- }
- }
- };
-
- return (
-
-
"']/g,
- ""
- )}, want to add something?`
- : files && files.length > 0
- ? "Ask me anything. Type @ to mention a file..."
- : "Ask me anything..."
- }
- onInput={handleInput}
- onKeyDown={handleKeyDown}
- onPaste={handlePaste}
- suppressContentEditableWarning
- >
- {showMentionDropdown && (
-
-
- {results?.length > 0 && (
-
- {results.map((file) => (
- insertMention(file.path)}
- />
- ))}
-
- )}
-
-
- )}
-
- );
-}
-
-export const getFileIcon = (filePath: string, size = "size-3.5") => {
- if (filePath.endsWith(".css")) {
- return ;
- } else if (filePath.endsWith(".js")) {
- return ;
- } else if (filePath.endsWith(".json")) {
- return ;
- } else {
- return ;
- }
-};
-
-function MentionResult({
- file,
- onSelect,
-}: {
- file: File;
- onSelect: () => void;
-}) {
- return (
-
- {getFileIcon(file.path, "size-3")}
- {file.path}
-
- );
-}
diff --git a/components/ask-ai/loading.tsx b/components/ask-ai/loading.tsx
deleted file mode 100644
index 060be494fb586788eab5c2139bb080fa51e209a2..0000000000000000000000000000000000000000
--- a/components/ask-ai/loading.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-"use client";
-import Loading from "@/components/loading";
-
-export const AiLoading = ({
- text = "AI Assistant is thinking...",
- showCircle = true,
- className,
-}: {
- text?: string;
- showCircle?: boolean;
- className?: string;
-}) => {
- return (
-
- {showCircle &&
}
-
-
- {text.split("").map((char, index) => (
-
- {char === " " ? "\u00A0" : char}
-
- ))}
-
-
-
- );
-};
diff --git a/components/ask-ai/models.tsx b/components/ask-ai/models.tsx
deleted file mode 100644
index 4e58acffcf87295047815246cf3b2dd14ef04d2f..0000000000000000000000000000000000000000
--- a/components/ask-ai/models.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import {
- BrainIcon,
- ChevronDown,
- DollarSign,
- StarsIcon,
- Zap,
-} from "lucide-react";
-import { useMemo, useState } from "react";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import { ProviderType } from "@/lib/type";
-import { MODELS } from "@/lib/providers";
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-export function Models({
- model,
- setModel,
- provider,
- setProvider,
-}: {
- model: string;
- setModel: (model: string) => void;
- provider: ProviderType;
- setProvider: (provider: ProviderType) => void;
-}) {
- const [open, setOpen] = useState(false);
-
- const formattedModels = useMemo(() => {
- const lists: ((typeof MODELS)[0] | { isCategory: true; name: string })[] =
- [];
- const keys = new Set();
- MODELS.forEach((model) => {
- if (!keys.has(model.companyName)) {
- lists.push({
- isCategory: true,
- name: model.companyName,
- logo: model.logo,
- });
- keys.add(model.companyName);
- }
- lists.push(model);
- });
- return lists;
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
Provider mode:
-
- {(
- [
- { value: "cheapest", icon: DollarSign, color: "emerald" },
- {
- value: "auto",
- icon: BrainIcon,
- color: "indigo",
- name: "Smartest",
- },
- { value: "fastest", icon: Zap, color: "amber" },
- ] as const
- ).map(
- ({
- value,
- icon: Icon,
- color,
- name,
- }: {
- value: string;
- icon: React.ElementType;
- color: string;
- name?: string;
- }) => (
-
setProvider(value)}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- setProvider(value);
- }
- }}
- className={cn(
- "inline-flex items-center gap-1.5 h-7 px-2.5 text-xs font-medium border border-border rounded-md cursor-pointer transition-colors select-none",
- "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
- provider === value && [
- "bg-transparent",
- color === "emerald" &&
- "[&_svg]:fill-emerald-500 [&_svg]:stroke-emerald-500 bg-emerald-500/10! border-emerald-500/10! text-emerald-500!",
- color === "indigo" &&
- "[&_svg]:fill-indigo-500 [&_svg]:stroke-indigo-500 bg-indigo-500/10! border-indigo-500/10! text-indigo-500!",
- color === "amber" &&
- "[&_svg]:fill-amber-500 [&_svg]:stroke-amber-500 bg-amber-500/10! border-amber-500/10! text-amber-500!",
- ]
- )}
- >
-
- {name ?? value.charAt(0).toUpperCase() + value.slice(1)}
-
- )
- )}
-
-
-
-
-
- );
-}
diff --git a/components/ask-ai/uploader.tsx b/components/ask-ai/uploader.tsx
deleted file mode 100644
index c2f5f7482790b2209222c01a827ace953ddde4ca..0000000000000000000000000000000000000000
--- a/components/ask-ai/uploader.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import {
- CheckCircle,
- FileVideo,
- ImageIcon,
- Music,
- Paperclip,
- Video,
-} from "lucide-react";
-import { useRef, useState } from "react";
-import Image from "next/image";
-import { useParams } from "next/navigation";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { getFileType, humanizeNumber } from "@/lib/utils";
-import { useQueryClient } from "@tanstack/react-query";
-import { ProjectWithCommits } from "@/actions/projects";
-import { toast } from "sonner";
-import Loading from "../loading";
-
-export const Uploader = ({
- medias,
- selected,
- setSelected,
-}: {
- medias?: string[] | null;
- selected: string[];
- setSelected: React.Dispatch>;
-}) => {
- const queryClient = useQueryClient();
- const { repoId } = useParams<{ repoId: string }>();
-
- const [open, setOpen] = useState(false);
- const [isUploading, setIsUploading] = useState(false);
- const [error, setError] = useState(null);
- const fileInputRef = useRef(null);
-
- const getFileIcon = (url: string) => {
- const fileType = getFileType(url);
- switch (fileType) {
- case "image":
- return ;
- case "video":
- return ;
- case "audio":
- return ;
- default:
- return ;
- }
- };
-
- const uploadFiles = async (files: FileList | null) => {
- setError(null);
- if (!files || files.length === 0) return;
-
- setIsUploading(true);
- const data = new FormData();
- Array.from(files).forEach((file) => {
- data.append("images", file);
- });
-
- const response = await fetch(`/api/projects/${repoId}/medias`, {
- method: "POST",
- body: data,
- })
- .then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to save changes");
- })
- .catch((err) => {
- return { success: false, err };
- });
-
- if (response.success) {
- queryClient.setQueryData(
- ["project"],
- (oldProject: ProjectWithCommits) => ({
- ...oldProject,
- medias: [...response.medias, ...(oldProject?.medias ?? [])],
- })
- );
- toast.success("Media files uploaded successfully!");
- } else {
- setError(
- response.err ?? "Failed to upload media files, try again later."
- );
- }
-
- setIsUploading(false);
- };
-
- return (
-
-
-
-
-
-
-
-
- {error && {error}
}
- {medias && medias.length > 0 && (
-
-
- Uploaded files:
-
-
-
- {medias.map((media: string) => {
- const fileType = getFileType(media);
- return (
-
- setSelected(
- selected.includes(media)
- ? selected.filter((f) => f !== media)
- : [...selected, media]
- )
- }
- >
- {fileType === "image" ? (
-
- ) : fileType === "video" ? (
-
- ) : fileType === "audio" ? (
-
-
-
- ) : (
-
- {getFileIcon(media)}
-
- )}
- {selected.includes(media) && (
-
-
-
- )}
-
- );
- })}
-
-
-
- )}
-
-
- uploadFiles(e.target.files)}
- />
-
-
-
-
-
- );
-};
diff --git a/components/ask-ai/useGeneration.ts b/components/ask-ai/useGeneration.ts
deleted file mode 100644
index e847b86b1ab2e61469fc568b0e35b47864242176..0000000000000000000000000000000000000000
--- a/components/ask-ai/useGeneration.ts
+++ /dev/null
@@ -1,437 +0,0 @@
-"use client";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useRef } from "react";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-import { v4 as uuidv4 } from "uuid";
-import { useLocalStorage } from "react-use";
-import { useSession } from "next-auth/react";
-
-import { formatResponse } from "@/lib/format";
-import { File, Message, MessageActionType, ProviderType } from "@/lib/type";
-import { getContextFilesFromPrompt } from "@/lib/utils";
-
-const MESSAGES_QUERY_KEY = (projectName: string) =>
- ["messages", projectName] as const;
-
-export const useGeneration = (projectName: string) => {
- const router = useRouter();
- const audio = useRef(null);
- const queryClient = useQueryClient();
- const abortController = useRef(null);
- const [, setStoredMessages] = useLocalStorage(
- `messages-${projectName}`,
- []
- );
- const { data: session } = useSession();
-
- const { data: isLoading } = useQuery({
- queryKey: ["ai.generation.isLoading"],
- queryFn: () => false,
- refetchOnWindowFocus: false,
- refetchOnReconnect: false,
- refetchOnMount: false,
- staleTime: Infinity,
- });
-
- const setIsLoading = (isLoading: boolean) => {
- queryClient.setQueryData(["ai.generation.isLoading"], isLoading);
- };
-
- const getFiles = () => queryClient.getQueryData(["files"]) ?? [];
- const setFiles = (newFiles: File[]) => {
- queryClient.setQueryData(["files"], (oldFiles: File[] = []) => {
- const currentFiles = oldFiles.filter(
- (file) => !newFiles.some((f) => f.path === file.path)
- );
- return [...currentFiles, ...newFiles];
- });
- };
-
- const getMessages = () =>
- queryClient.getQueryData(
- MESSAGES_QUERY_KEY(projectName ?? "new")
- ) ?? [];
- const addMessage = (message: Omit) => {
- const id = uuidv4();
- const key = MESSAGES_QUERY_KEY(projectName ?? "new");
- queryClient.setQueryData(key, (oldMessages = []) => {
- const newMessages = [...oldMessages, { ...message, id }];
- if (projectName !== "new") {
- localStorage.setItem(
- `messages-${projectName}`,
- JSON.stringify(newMessages)
- );
- }
- return newMessages;
- });
- return id;
- };
-
- const updateLastMessage = (content: string, files?: File[]) => {
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(projectName),
- (oldMessages = []) => {
- const newMessages = [
- ...oldMessages.slice(0, -1),
- {
- ...oldMessages[oldMessages.length - 1],
- content,
- isThinking: false,
- files: files?.map((file) => file.path),
- },
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- return newMessages;
- }
- );
- };
-
- const updateMessage = (messageId: string, message: Partial) => {
- const key = MESSAGES_QUERY_KEY(projectName ?? "new");
- const currentMessages = queryClient.getQueryData(key);
- if (!currentMessages) return;
- const index = currentMessages.findIndex((m) => m.id === messageId);
- if (index === -1) return;
- const newMessages = [
- ...currentMessages.slice(0, index),
- { ...currentMessages[index], ...message },
- ...currentMessages.slice(index + 1),
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- queryClient.setQueryData(key, newMessages);
- };
-
- const storeMessages = async (newProjectName: string) => {
- return new Promise((resolve) => {
- const currentMessages = queryClient.getQueryData(
- MESSAGES_QUERY_KEY("new")
- );
- localStorage.setItem(
- `messages-${newProjectName}`,
- JSON.stringify(currentMessages)
- );
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(newProjectName),
- currentMessages
- );
- setTimeout(() => resolve(true), 100);
- });
- };
-
- const createProject = async (
- files: File[],
- projectTitle: string,
- indexMessage: string,
- prompt: string
- ) => {
- updateMessage(indexMessage, {
- actions: [
- {
- label: "Publishing on Hugging Face...",
- variant: "default",
- loading: true,
- type: MessageActionType.PUBLISH_PROJECT,
- },
- ],
- });
- try {
- const response = await fetch("/api/projects", {
- method: "POST",
- body: JSON.stringify({
- projectTitle,
- files,
- prompt,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- }).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to publish project");
- });
-
- if (response.repoUrl) {
- toast.success("Project has been published, build in progress...");
- updateMessage(indexMessage, {
- actions: [
- {
- label: "See Live preview",
- variant: "default",
- type: MessageActionType.SEE_LIVE_PREVIEW,
- },
- ],
- });
- storeMessages(response.repoUrl).then(() => {
- router.push(`/${response.repoUrl}`);
- });
- }
- } catch (error) {
- toast.error("Failed to publish project");
- updateMessage(indexMessage, {
- actions: [
- {
- label: "Publish on Hugging Face",
- variant: "default",
- type: MessageActionType.PUBLISH_PROJECT,
- projectTitle,
- prompt,
- },
- ],
- });
- }
- };
-
- const callAi = async (
- {
- prompt,
- model,
- onComplete,
- provider = "auto",
- redesignMd,
- medias,
- }: {
- prompt: string;
- model: string;
- redesignMd?: {
- url: string;
- md: string;
- } | null;
- medias?: string[] | null;
- onComplete: () => void;
- provider?: ProviderType;
- },
- setModel: (model: string) => void
- ) => {
- setIsLoading(true);
- const messages = getMessages();
- const files = getFiles();
- const filesToUse = await getContextFilesFromPrompt(prompt, files);
- const previousMessages = [...messages]?.filter(
- (message) => !message.isAutomated || !message.isAborted
- );
- addMessage({
- role: "user",
- content: `${
- redesignMd?.url ? `Redesign: ${redesignMd.url}\n` : ""
- }${prompt}`,
- createdAt: new Date(),
- });
- addMessage({
- role: "assistant",
- isThinking: true,
- createdAt: new Date(),
- model,
- });
-
- const isFollowUp = files?.length > 0;
- abortController.current = new AbortController();
-
- const request = await fetch("/api/ask", {
- method: "POST",
- body: JSON.stringify({
- prompt,
- model,
- files: filesToUse,
- previousMessages,
- provider,
- redesignMd,
- medias,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- ...(abortController.current
- ? { signal: abortController.current.signal }
- : {}),
- });
-
- const currentMessages = getMessages();
-
- if (!request.ok) {
- const jsonResponse = await request.json()?.catch(() => null);
- const errorMessage =
- jsonResponse?.error || `Status code: ${request.status}`;
-
- const lastMessageId = currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isThinking: false,
- isAborted: true,
- content: `Error: ${errorMessage}`,
- });
- setIsLoading(false);
- return;
- }
-
- if (request && request.body) {
- const reader = request.body.getReader();
- const decoder = new TextDecoder();
- let completeResponse = "";
- const read = async () => {
- const { done, value } = await reader.read();
- if (done) {
- audio.current?.play();
- const files = getFiles();
- const {
- messageContent,
- files: newFiles,
- projectTitle,
- } = formatResponse(completeResponse, files ?? []);
- updateLastMessage(messageContent, newFiles);
- if (newFiles && newFiles.length > 0) {
- setFiles(newFiles);
- onComplete();
- if (projectName === "new") {
- addMessage({
- role: "assistant",
- content:
- "I've finished the generation. Now you can decide to publish the project on Hugging Face to share it!",
- createdAt: new Date(),
- isAutomated: true,
- actions: [
- {
- label: "Publish on Hugging Face",
- variant: "default",
- type: MessageActionType.PUBLISH_PROJECT,
- prompt,
- projectTitle,
- },
- ],
- });
- } else {
- const response = await fetch(
- `/api/projects/${projectName.split("/")[1]}`,
- {
- method: "PUT",
- body: JSON.stringify({
- files: newFiles,
- prompt,
- }),
- headers: {
- "Content-Type": "application/json",
- },
- }
- ).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- });
- if (response.success) {
- toast.success("Project has been updated");
- } else {
- toast.error("Failed to update project");
- }
- }
- }
-
- setIsLoading(false);
- return;
- }
- const chunk = decoder.decode(value, { stream: true });
- completeResponse += chunk;
-
- if (completeResponse.includes("__ERROR__:")) {
- const errorMatch = completeResponse.match(/__ERROR__:(.+)/);
- if (errorMatch) {
- try {
- const errorData = JSON.parse(errorMatch[1]);
- if (errorData.isError) {
- const lastMessageId =
- currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isThinking: false,
- isAborted: true,
- content: errorData?.showProMessage
- ? session?.user?.isPro ? "You have already reached your monthly included credits with Hugging Face Pro plan. Please consider adding more credits to your account." : "You have exceeded your monthly included credits with Hugging Face inference provider. Please consider upgrading to a pro plan."
- : `Error: ${errorData.messageError}`,
- actions: errorData?.showProMessage
- ? session?.user?.isPro ? [{
- label: "Add more credits",
- variant: "default",
- type: MessageActionType.ADD_CREDITS,
- }] : [
- {
- label: "Upgrade to Pro",
- variant: "pro",
- type: MessageActionType.UPGRADE_TO_PRO,
- },
- ]
- : [],
- });
- setIsLoading(false);
- return;
- }
- } catch (e) {
- console.error("Failed to parse error message:", e);
- }
- }
- }
- if (
- completeResponse.includes(
- "_Note: The selected model was not available. Switched to"
- )
- ) {
- const newModel = completeResponse
- .match(
- /The selected model was not available. Switched to (.+)/
- )?.[1]
- .replace(/`/g, "")
- .replace(" ", "")
- .replace(/\.|_$/g, "");
- if (newModel) {
- setModel(newModel);
- updateMessage(currentMessages[currentMessages.length - 1].id, {
- model: newModel,
- });
- }
- }
-
- const files = getFiles();
- const { messageContent, files: newFiles } = formatResponse(
- completeResponse,
- files ?? []
- );
- if (messageContent) updateLastMessage(messageContent);
- if (newFiles && newFiles.length > 0) {
- if (!isFollowUp) {
- setFiles(newFiles);
- }
- updateLastMessage(messageContent, newFiles);
- }
- read();
- };
- return await read();
- }
- };
-
- const stopGeneration = () => {
- if (abortController.current) {
- abortController.current.abort();
- abortController.current = null;
- setIsLoading(false);
- const currentMessages = getMessages();
- const lastMessageId = currentMessages[currentMessages.length - 1].id;
- updateMessage(lastMessageId, {
- isAborted: true,
- isThinking: false,
- });
- }
- };
-
- return {
- callAi,
- isLoading,
- stopGeneration,
- files: getFiles(),
- createProject,
- audio,
- };
-};
diff --git a/components/chat/index.tsx b/components/chat/index.tsx
deleted file mode 100644
index ececa27bb2dccd800b7796ebefe7cde50360e441..0000000000000000000000000000000000000000
--- a/components/chat/index.tsx
+++ /dev/null
@@ -1,377 +0,0 @@
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { useSession } from "next-auth/react";
-import { cn } from "@/lib/utils";
-import { ChevronRight, ExternalLink } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
-import Image from "next/image";
-import Markdown from "react-markdown";
-import { useQueryClient } from "@tanstack/react-query";
-import { formatDistanceToNow } from "date-fns";
-import { SpaceEntry } from "@huggingface/hub";
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism";
-
-import { useChat } from "./useChat";
-import { AiLoading } from "@/components/ask-ai/loading";
-import Loading from "@/components/loading";
-import { useGeneration } from "@/components/ask-ai/useGeneration";
-import { MessageAction, MessageActionType, File } from "@/lib/type";
-import { Button } from "@/components/ui/button";
-import { getFileIcon } from "@/components/ask-ai/input-mentions";
-import ProIcon from "@/assets/pro.svg";
-import { ProModal } from "../pro-modal";
-
-const ASSISTANT_AVATAR_URL = "https://i.imgur.com/Ho6v0or.jpeg";
-
-export function AppEditorChat({
- isNew,
- projectName,
- onSelectFile,
-}: {
- isNew?: boolean;
- projectName?: string;
- onSelectFile: (file: string) => void;
-}) {
- const chatContainerRef = useRef(null);
- const { data: session } = useSession();
- const queryClient = useQueryClient();
- const chatProjectName = isNew ? "new" : projectName ?? "new";
- const { messages } = useChat(chatProjectName);
- const { isLoading, createProject } = useGeneration(chatProjectName);
- const [openProModal, setOpenProModal] = useState(false);
-
- const project = queryClient.getQueryData(["project"]);
-
- const handleShowLastFile = (files?: string[]) => {
- if (files && files.length > 0) {
- const lastGeneratedFile = files[files.length - 1];
- if (lastGeneratedFile) {
- onSelectFile?.(lastGeneratedFile);
- }
- }
- };
- const handleActions = (action: MessageAction, messageId: string) => {
- if (!action) return;
- switch (action.type) {
- case MessageActionType.PUBLISH_PROJECT:
- const files = queryClient.getQueryData(["files"]) ?? [];
- return createProject(
- files ?? [],
- action.projectTitle ?? "",
- messageId,
- action.prompt ?? ""
- );
- case MessageActionType.SEE_LIVE_PREVIEW:
- return window.open(
- `https://huggingface.co/spaces/${project?.name}`,
- "_blank"
- );
- case MessageActionType.UPGRADE_TO_PRO:
- return setOpenProModal(true);
- case MessageActionType.ADD_CREDITS:
- return window.open(
- "https://huggingface.co/settings/billing?add-credits=true",
- "_blank"
- );
- }
- };
-
- useEffect(() => {
- if (chatContainerRef.current) {
- chatContainerRef.current.scrollTop =
- chatContainerRef.current.scrollHeight;
- }
- }, [messages]);
-
- return (
-
- {isNew ? (
-
- Start a new conversation with the AI
-
- ) : (
-
- Your last conversation has not been restored.
-
- )}
-
- {messages.map((message, id) => (
-
-
-
-
- {message.role === "user"
- ? session?.user?.name?.charAt(0) ?? ""
- : ""}
-
-
-
-
-
(
- {children}
- ),
- code: ({ className, children }) => {
- const isCodeBlock = className?.startsWith("language-");
- const language = className?.replace("language-", "");
- if (isCodeBlock) {
- return (
-
- {String(children).trim()}
-
- );
- }
-
- return (
-
- {children}
-
- );
- },
- ul: ({ children }) => (
-
- ),
- ol: ({ children }) => (
-
- {children}
-
- ),
- li: ({ children }) => (
- {children}
- ),
- a: ({ children, href }) => {
- const isValidUrl =
- href &&
- (href.startsWith("http://") ||
- href.startsWith("https://") ||
- href.startsWith("/") ||
- href.startsWith("#") ||
- href.startsWith("mailto:") ||
- href.startsWith("tel:"));
- const safeHref = isValidUrl ? href : "#";
-
- return (
-
-
- {children}
-
- );
- },
- pre: ({ children }) => <>{children}>,
- p: ({ children }) => {
- const content = String(children);
- if (
- typeof children === "string" &&
- (content.includes("file:/") ||
- content.match(/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?/i))
- ) {
- const parts = content.split(/(file:\/\S+|https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?)/gi);
- return (
-
- {parts.filter(Boolean).map((part, index) => {
- if (!part || part.trim() === "") return null;
-
- if (part.startsWith("file:/")) {
- return (
-
- {getFileIcon(part, "size-2.5")}
- {part.replace("file:/", "")}
-
- );
- } else if (
- part.match(
- /^https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?$/i
- )
- ) {
- const displayUrl =
- part.length > 35
- ? part.substring(0, 35) + "..."
- : part;
- return (
-
- πΌοΈ
- {displayUrl}
-
- );
- }
- return part;
- })}
-
- );
- }
- return {children}
;
- },
- }}
- >
- {message.content}
-
- {message.isThinking && (
-
- )}
- {message.isAborted && (
-
- The request has been aborted due to an error OR the user has
- stopped the generation.
-
- )}
-
- {message.model && message.createdAt && (
-
-
- via{" "}
-
- {message.model}
-
-
- {!message.isThinking && (
-
- {formatDistanceToNow(message.createdAt, {
- addSuffix: true,
- })}
-
- )}
-
- )}
- {message.files && message.files.length > 0 && (
-
-
- {isLoading &&
- messages[messages.length - 1].id === message.id ? (
- <>
-
-
- {isNew ? "Creating" : "Editing"}{" "}
-
-
- >
- ) : (
- <>
-
- {isNew ? "Created" : "Edited"}{" "}
-
- {message.files.length > 1 && (
-
- (+ {message.files.length - 1} files)
-
- )}
-
-
- >
- )}
-
-
- )}
- {message.actions &&
- message.actions.length > 0 &&
- messages.length - 1 === id && (
-
- {message.actions.map((action, id) => (
-
- ))}
-
- )}
-
-
- ))}
-
-
-
- );
-}
-
-const FileCode = ({ file }: { file: string }) => (
-
- {file}
-
-);
diff --git a/components/chat/useChat.ts b/components/chat/useChat.ts
deleted file mode 100644
index 2f32019379f2fed9411cae356d5ad7b50e4ef7d5..0000000000000000000000000000000000000000
--- a/components/chat/useChat.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useLocalStorage } from "react-use";
-import { useEffect } from "react";
-import { v4 as uuidv4 } from "uuid";
-
-import { Message } from "@/lib/type";
-
-const MESSAGES_QUERY_KEY = (projectName: string) =>
- ["messages", projectName] as const;
-
-export function useChat(projectName: string) {
- const queryClient = useQueryClient();
- const [, setStoredMessages, clearStoredMessages] = useLocalStorage(
- `messages-${projectName}`,
- []
- );
-
- useEffect(() => {
- if (projectName !== "new") {
- queryClient.invalidateQueries({
- queryKey: MESSAGES_QUERY_KEY(projectName),
- });
- }
- }, [projectName, queryClient]);
-
- const { data: messages = [] } = useQuery({
- queryKey: MESSAGES_QUERY_KEY(projectName),
- queryFn: () => {
- if (projectName === "new") {
- return [];
- }
- const storedData = localStorage.getItem(`messages-${projectName}`);
- if (storedData) {
- try {
- const parsedMessages = JSON.parse(storedData);
- if (parsedMessages && parsedMessages.length > 0) {
- return parsedMessages;
- }
- } catch (error) {
- console.error("Failed to parse stored messages:", error);
- }
- }
- return [];
- },
- refetchOnMount: false,
- refetchOnWindowFocus: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- refetchIntervalInBackground: false,
- });
-
- const addMessage = (message: Omit) => {
- const id = uuidv4();
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(projectName),
- (oldMessages = []) => {
- const newMessages = [
- ...oldMessages,
- {
- ...message,
- id,
- },
- ];
- if (projectName !== "new") {
- setStoredMessages(newMessages);
- }
- return newMessages;
- }
- );
- return id;
- };
- const clearMessages = () => {
- queryClient.setQueryData(MESSAGES_QUERY_KEY(projectName), []);
- clearStoredMessages();
- };
-
- const storeMessages = async (newProjectName: string) => {
- return new Promise((resolve) => {
- const currentMessages = queryClient.getQueryData(
- MESSAGES_QUERY_KEY("new")
- );
- localStorage.setItem(
- `messages-${newProjectName}`,
- JSON.stringify(currentMessages)
- );
- queryClient.setQueryData(
- MESSAGES_QUERY_KEY(newProjectName),
- currentMessages
- );
- setTimeout(() => resolve(true), 100);
- });
- };
-
- return {
- messages,
- addMessage,
- clearMessages,
- storeMessages,
- };
-}
diff --git a/components/code/index.tsx b/components/code/index.tsx
deleted file mode 100644
index 6b6b687f828b7bf8a5bd66afb958552132281f2f..0000000000000000000000000000000000000000
--- a/components/code/index.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import { useState } from "react";
-import {
- SandpackLayout,
- SandpackFileExplorer,
-} from "@codesandbox/sandpack-react";
-import { ChevronRight, ChevronLeft } from "lucide-react";
-import { useParams } from "next/navigation";
-import { useQueryClient } from "@tanstack/react-query";
-import { format } from "date-fns";
-import { useUpdateEffect } from "react-use";
-
-import { AppEditorMonacoEditor } from "./monaco-editor";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import Loading from "../loading";
-import { ProjectWithCommits } from "@/actions/projects";
-
-export function AppEditorCode() {
- const queryClient = useQueryClient();
- const { repoId } = useParams<{ repoId: string }>();
-
- const [isFileExplorerCollapsed, setIsFileExplorerCollapsed] = useState(true);
- const [isSavingChanges, setIsSavingChanges] = useState(false);
- const [isSavingChangesSuccess, setIsSavingChangesSuccess] = useState(false);
- const [isSavingChangesError, setIsSavingChangesError] = useState(false);
- const [isClosing, setIsClosing] = useState(false);
- const [showSaveChanges, setShowSaveChanges] = useState(false);
-
- const handleSaveChanges = async () => {
- if (repoId === "new") return;
- setIsSavingChanges(true);
- const manuallyUpdatedFiles =
- queryClient.getQueryData(["manuallyUpdatedFiles"]) ?? [];
- const response = await fetch(`/api/projects/${repoId}`, {
- method: "PUT",
- body: JSON.stringify({
- files: manuallyUpdatedFiles,
- prompt: `βοΈ ${format(new Date(), "dd/MM")} - ${format(
- new Date(),
- "HH:mm"
- )} - Manual changes.`,
- isManualChanges: true,
- }),
- }).then(async (response) => {
- if (response.ok) {
- const data = await response.json();
- return data;
- }
- throw new Error("Failed to save changes");
- });
- if (response.success) {
- setIsSavingChangesSuccess(true);
- queryClient.invalidateQueries({ queryKey: ["manuallyUpdatedFiles"] });
- queryClient.setQueryData(
- ["project"],
- (oldProject: ProjectWithCommits) => ({
- ...oldProject,
- commits: [response.commit, ...(oldProject?.commits ?? [])],
- })
- );
- } else {
- setIsSavingChangesError(true);
- }
- setIsSavingChanges(false);
- setShowSaveChanges(false);
- };
-
- const undoChanges = () => {
- queryClient.setQueryData(["manuallyUpdatedFiles"], []);
- setShowSaveChanges(false);
- };
-
- useUpdateEffect(() => {
- if (isSavingChangesSuccess) {
- setTimeout(() => setIsSavingChangesSuccess(false), 3000);
- }
- }, [isSavingChangesSuccess]);
-
- useUpdateEffect(() => {
- if (isClosing) {
- setTimeout(() => {
- setIsSavingChangesSuccess(false);
- setIsSavingChangesError(false);
- setIsClosing(false);
- }, 300);
- }
- }, [isClosing]);
-
- return (
-
-
-
-
-
-
- {repoId && (
-
-
-
-
- {isSavingChangesSuccess
- ? "Changes saved"
- : isSavingChangesError
- ? "Failed to save changes"
- : "Save Changes"}
-
-
- {isSavingChangesSuccess
- ? "Changes saved successfully"
- : isSavingChangesError
- ? "Something went wrong, please try again."
- : "You have unsaved manual changes. Click the button to save your changes."}
-
-
-
- {!isSavingChangesSuccess && !isSavingChanges && (
-
- )}
-
- {isSavingChangesSuccess || isSavingChangesError ? (
-
- ) : (
-
- )}
-
-
-
- )}
-
- );
-}
diff --git a/components/code/monaco-editor.tsx b/components/code/monaco-editor.tsx
deleted file mode 100644
index d222ce260193d76d9da71b784259a01653782381..0000000000000000000000000000000000000000
--- a/components/code/monaco-editor.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { useMemo, useRef, useCallback, useEffect } from "react";
-import { useQueryClient } from "@tanstack/react-query";
-import { Monaco } from "@monaco-editor/react";
-import {
- useActiveCode,
- SandpackStack,
- FileTabs,
- useSandpack,
-} from "@codesandbox/sandpack-react";
-import { useTheme } from "next-themes";
-
-import NightLight from "@/components/editor/night-light.json";
-import Night from "@/components/editor/night.json";
-import { File } from "@/lib/type";
-import dynamic from "next/dynamic";
-
-const Editor = dynamic(
- () => import("@monaco-editor/react").then((mod) => mod.Editor),
- { ssr: false }
-);
-
-const LANGUAGE_MAP = {
- js: "javascript",
- ts: "typescript",
- html: "html",
- css: "css",
- json: "json",
- txt: "text",
-};
-
-export function AppEditorMonacoEditor({
- setShowSaveChanges,
-}: {
- setShowSaveChanges: (show: boolean) => void;
-}) {
- const { theme } = useTheme();
- const { code, updateCode } = useActiveCode();
- const { sandpack } = useSandpack();
- const queryClient = useQueryClient();
- const updateTimeoutRef = useRef(null);
-
- const handleEditorDidMount = (monaco: Monaco) => {
- monaco.editor.defineTheme("NightLight", {
- base: "vs",
- inherit: true,
- ...NightLight,
- rules: [],
- });
- monaco.editor.defineTheme("Night", {
- base: "vs-dark",
- ...Night,
- rules: [],
- });
- };
-
- const language = useMemo(() => {
- const extension = sandpack.activeFile
- .split(".")
- .pop()
- ?.toLowerCase() as string;
- return LANGUAGE_MAP[extension as keyof typeof LANGUAGE_MAP] ?? "text";
- }, [sandpack.activeFile]);
-
- const updateFile = useCallback(
- (newValue: string, activeFile: string) => {
- if (updateTimeoutRef.current) {
- clearTimeout(updateTimeoutRef.current);
- }
-
- updateTimeoutRef.current = setTimeout(() => {
- const manuallyUpdatedFiles =
- queryClient.getQueryData(["manuallyUpdatedFiles"]) ?? [];
- const fileIndex = manuallyUpdatedFiles.findIndex(
- (file) => file.path === activeFile
- );
- if (fileIndex !== -1) {
- manuallyUpdatedFiles[fileIndex].content = newValue;
- } else {
- manuallyUpdatedFiles.push({
- path: activeFile,
- content: newValue,
- });
- }
- queryClient.setQueryData(
- ["manuallyUpdatedFiles"],
- manuallyUpdatedFiles
- );
- }, 100);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [queryClient]
- );
-
- const handleEditorChange = (value: string | undefined) => {
- setShowSaveChanges(true);
- const newValue = value || "";
- updateCode(newValue);
- const activeFile = sandpack.activeFile?.replace(/^\//, "");
- updateFile(newValue, activeFile);
- };
-
- useEffect(() => {
- return () => {
- if (updateTimeoutRef.current) {
- clearTimeout(updateTimeoutRef.current);
- }
- };
- }, []);
-
- const themeEditor = useMemo(() => {
- const isSystemDark =
- window.matchMedia &&
- window.matchMedia("(prefers-color-scheme: dark)").matches;
- const effectiveTheme =
- theme === "system" ? (isSystemDark ? "dark" : "light") : theme;
- return effectiveTheme === "dark" ? "Night" : "NightLight";
- }, [theme]);
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/components/code/useEditor.ts b/components/code/useEditor.ts
deleted file mode 100644
index 06ed500729a17413283a42c8f9bf395f255b1518..0000000000000000000000000000000000000000
--- a/components/code/useEditor.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useProject } from "@/components/projects/useProject";
-import { useMemo } from "react";
-
-const LANGUAGE_MAP = {
- "py": "python",
- "js": "javascript",
- "ts": "typescript",
- "html": "html",
- "css": "css",
- "json": "json",
- "txt": "text",
-}
-
-export const useEditor = () => {
- const queryClient = useQueryClient();
- const { files } = useProject();
-
- const { data: isFileListOpen } = useQuery({
- queryKey: ["isFileListOpen"],
- queryFn: () => {
- return false;
- },
- initialData: false,
- refetchInterval: false,
- refetchOnWindowFocus: false,
- });
-
- const setIsFileListOpen = (isOpen: boolean) => {
- queryClient.setQueryData(["isFileListOpen"], () => isOpen);
- }
-
- const { data: editorFilePath } = useQuery({
- queryKey: ["editorFile"],
- queryFn: () => {
- return "app.py";
- },
- initialData: "app.py",
- refetchInterval: false,
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchIntervalInBackground: false,
- });
-
- const setEditorFilePath = (path: string) => {
- queryClient.setQueryData(["editorFile"], () => path);
- }
-
- const editorFileData = useMemo(() => {
- const finalFile = { path: "", content: "", language: "" };
- if (editorFilePath) {
- const file = files?.find((file) => file.path === editorFilePath);
- if (file) {
- finalFile.path = file.path;
- finalFile.content = file.content ?? "";
- finalFile.language = LANGUAGE_MAP[file.path.split(".").pop()?.toLowerCase() as keyof typeof LANGUAGE_MAP] ?? "text";
- }
- }
-
- return finalFile
- }, [editorFilePath, files])
-
- return {
- editorFilePath,
- editorFileData,
- setEditorFilePath,
- isFileListOpen,
- setIsFileListOpen,
- }
-}
\ No newline at end of file
diff --git a/components/contexts/app-context.tsx b/components/contexts/app-context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..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 (
+
+ );
+};
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 (
+
+
+
+
+
+
+ Select a file to send as context
+
+
+
+ {pages.length === 0 ? (
+
+ No files available
+
+ ) : (
+ <>
+
+ {pages.map((page) => (
+
+ ))}
+ >
+ )}
+
+
+
+
+ );
+};
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 && }
+
+
+
+
+
+
+
+
+ );
+};
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: value,
+ })
+ }
+ />
+
+
+
+
+
+ Secondary Color
+
+
+
+
+
+ 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: 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 (
+ <>
+
+
+ >
+ );
+};
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({
- ")) {
+ 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.
+
+
+
+
+ )}
+
+ );
+};
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 (
- <>
-
-
-
-
-
-
-
-
- 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" && }
-
-
-
-
-
-
-
- >
- );
-};
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
+
+
+
+ ) : (
+ // Expanded state
+
+
+
+
+
+
+
+
+
+ You have unsaved changes in your project. Save them to
+ preserve your work.
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
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) => (
+
+ ))}
+
+ );
+};
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 (
-
+
);
-};
+}
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 (
+
+ );
+}
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 (